diff --git a/client/src/app/shared/video/sort-field.type.ts b/client/src/app/shared/video/sort-field.type.ts index d1088d24495e35f3cbd8bac0f88ce8dc34ef1867..65b24d9463ee57e4fb608280ea6f912fa4543fcf 100644 --- a/client/src/app/shared/video/sort-field.type.ts +++ b/client/src/app/shared/video/sort-field.type.ts @@ -5,3 +5,6 @@ export type VideoSortField = 'name' | '-name' | 'views' | '-views' | 'likes' | '-likes' | 'trending' | '-trending' + +export type CommentSortField = 'createdAt' | '-createdAt' + | 'totalReplies' | '-totalReplies' diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts index 550d42fa86552c65a2b100ff8691a6b4a26c2f14..72fbf5d2596042315ccbb13c687801af1b691740 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts @@ -12,7 +12,7 @@ import { import { environment } from '../../../../environments/environment' import { RestExtractor, RestService } from '../../../shared/rest' import { ComponentPagination } from '../../../shared/rest/component-pagination.model' -import { VideoSortField } from '../../../shared/video/sort-field.type' +import { CommentSortField } from '../../../shared/video/sort-field.type' import { VideoComment } from './video-comment.model' @Injectable() @@ -51,7 +51,7 @@ export class VideoCommentService { getVideoCommentThreads (parameters: { videoId: number | string, componentPagination: ComponentPagination, - sort: VideoSortField + sort: CommentSortField }): Observable<ResultList<VideoComment>> { const { videoId, componentPagination, sort } = parameters diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html index 5fabb7dfe70a62df015e88980eeb70b239532bd7..e284eab0a1e3203bb30c78d9356217de1cc4d9e3 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.html +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html @@ -10,6 +10,16 @@ </div> <my-feed [syndicationItems]="syndicationItems"></my-feed> + + <div ngbDropdown class="d-inline-block ml-4"> + <button class="btn btn-sm btn-outline-secondary" id="dropdownSortComments" ngbDropdownToggle i18n> + Sort by + </button> + <div ngbDropdownMenu aria-labelledby="dropdownSortComments"> + <button (click)="handleSortChange('-createdAt')" ngbDropdownItem i18n>Most recent first (default)</button> + <button (click)="handleSortChange('-totalReplies')" ngbDropdownItem i18n>Most replies first</button> + </div> + </div> </div> <ng-template [ngIf]="video.commentsEnabled === true"> diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.scss b/client/src/app/videos/+video-watch/comment/video-comments.component.scss index dde10b068c110f319191c5eb7503b16f395a0a6b..9e368229528fc88993ae5df4e7b0404a1b093dfb 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.scss +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.scss @@ -23,6 +23,12 @@ margin-right: 0; } +#dropdownSortComments { + font-weight: 600; + text-transform: uppercase; + border: none; +} + my-feed { display: inline-block; margin-left: 5px; diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts index e814015534614c4e6bd9f8c805a9e7914bd3d8ca..974c61d6cc3f0a598b93ae9b8f79ca08b9e3bb30 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts @@ -6,7 +6,7 @@ import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/v import { AuthService } from '../../../core/auth' import { ComponentPagination, hasMoreItems } from '../../../shared/rest/component-pagination.model' import { User } from '../../../shared/users' -import { VideoSortField } from '../../../shared/video/sort-field.type' +import { CommentSortField } from '../../../shared/video/sort-field.type' import { VideoDetails } from '../../../shared/video/video-details.model' import { VideoComment } from './video-comment.model' import { VideoCommentService } from './video-comment.service' @@ -28,7 +28,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { comments: VideoComment[] = [] highlightedThread: VideoComment - sort: VideoSortField = '-createdAt' + sort: CommentSortField = '-createdAt' componentPagination: ComponentPagination = { currentPage: 1, itemsPerPage: 10, @@ -152,6 +152,13 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { this.viewReplies(commentTree.comment.id) } + handleSortChange (sort: CommentSortField) { + if (this.sort === sort) return + + this.sort = sort + this.resetVideo() + } + handleTimestampClicked (timestamp: number) { this.timestampClicked.emit(timestamp) } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 79fcd0edf5aeab242766359d8249922c646cb875..1b7b94d744a320b3aa2f2fa51834115fbc7c200a 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -46,7 +46,7 @@ const SORTABLE_COLUMNS = { VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ], VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], VIDEO_IMPORTS: [ 'createdAt' ], - VIDEO_COMMENT_THREADS: [ 'createdAt' ], + VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ], VIDEO_RATES: [ 'createdAt' ], BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ], FOLLOWERS: [ 'createdAt', 'state', 'score' ], diff --git a/server/models/utils.ts b/server/models/utils.ts index b53a52a053f9e86276a2632a3185ef2665044832..4199cc4430c2a371dce1dce33eb3f7e31f2a447b 100644 --- a/server/models/utils.ts +++ b/server/models/utils.ts @@ -22,6 +22,19 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt return [ [ finalField, direction ], lastSort ] } +function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { + const { direction, field } = buildDirectionAndField(value) + + if (field === 'totalReplies') { + return [ + [ Sequelize.literal('"totalReplies"'), direction ], + lastSort + ] + } + + return getSort(value, lastSort) +} + function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { const { direction, field } = buildDirectionAndField(value) @@ -167,6 +180,7 @@ export { SortType, buildLocalAccountIdsIn, getSort, + getCommentSort, getVideoSort, getBlacklistSort, createSimilarityAttribute, diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 869a42afef0f14197fa8bf4643c7e87058651017..28f011b03b3a1b13a6fc7b577d886f09b488098e 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -6,7 +6,7 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' import { AccountModel } from '../account/account' import { ActorModel } from '../activitypub/actor' -import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils' +import { buildBlockedAccountSQL, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils' import { VideoModel } from './video' import { VideoChannelModel } from './video-channel' import { getServerActor } from '../../helpers/utils' @@ -259,7 +259,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> { const query = { offset: start, limit: count, - order: getSort(sort), + order: getCommentSort(sort), where: { videoId, inReplyToCommentId: null,