-<div *ngIf="isNotDeletedOrDeletedWithReplies()" class="root-comment">
+<div *ngIf="isCommentDisplayed()" class="root-comment">
<div class="left">
<a *ngIf="!comment.isDeleted" [href]="comment.account.url" target="_blank" rel="noopener noreferrer">
<img
if (!this.commentTree) {
this.commentTree = {
comment: this.comment,
+ hasDisplayedChildren: false,
children: []
}
this.commentTree.children.unshift({
comment: createdComment,
+ hasDisplayedChildren: false,
children: []
})
($event.target as HTMLImageElement).src = Account.GET_DEFAULT_AVATAR_URL()
}
- isNotDeletedOrDeletedWithReplies () {
- return !this.comment.isDeleted || this.comment.isDeleted && this.comment.totalReplies !== 0
+ isCommentDisplayed () {
+ // Not deleted
+ return !this.comment.isDeleted ||
+ this.comment.totalReplies !== 0 || // Or root comment thread has replies
+ (this.commentTree?.hasDisplayedChildren) // Or this is a reply that have other replies
}
private getUserIfNeeded (account: Account) {
<div>
<div class="title-block">
<h2 class="title-page title-page-single">
- <ng-container *ngIf="componentPagination.totalItems > 0; then hasComments; else noComments"></ng-container>
+ <ng-container *ngIf="totalNotDeletedComments > 0; then hasComments; else noComments"></ng-container>
<ng-template #hasComments>
- <ng-container i18n *ngIf="componentPagination.totalItems === 1; else manyComments">1 Comment</ng-container>
- <ng-template i18n #manyComments>{{ componentPagination.totalItems }} Comments</ng-template>
+ <ng-container i18n *ngIf="totalNotDeletedComments === 1; else manyComments">1 Comment</ng-container>
+ <ng-template i18n #manyComments>{{ totalNotDeletedComments }} Comments</ng-template>
</ng-template>
<ng-template i18n #noComments>Comments</ng-template>
</h2>
[textValue]="commentThreadRedraftValue"
></my-video-comment-add>
- <div *ngIf="componentPagination.totalItems === 0 && comments.length === 0" i18n>No comments.</div>
+ <div *ngIf="totalNotDeletedComments === 0 && comments.length === 0" i18n>No comments.</div>
<div
class="comment-threads"
comments: VideoComment[] = []
highlightedThread: VideoComment
+
sort = '-createdAt'
+
componentPagination: ComponentPagination = {
currentPage: 1,
itemsPerPage: 10,
totalItems: null
}
+ totalNotDeletedComments: number
+
inReplyToCommentId: number
commentReplyRedraftValue: string
commentThreadRedraftValue: string
+
threadComments: { [ id: number ]: VideoCommentThreadTree } = {}
threadLoading: { [ id: number ]: boolean } = {}
obs.subscribe(
res => {
this.comments = this.comments.concat(res.data)
- // Client does not display removed comments
- this.componentPagination.totalItems = res.total - this.comments.filter(c => c.isDeleted).length
+ this.componentPagination.totalItems = res.total
+ this.totalNotDeletedComments = res.totalNotDeletedComments
this.onDataSubject.next(res.data)
this.hooks.runAction('action:video-watch.video-threads.loaded', 'video-watch', { data: this.componentPagination })
this.inReplyToCommentId = undefined
this.componentPagination.currentPage = 1
this.componentPagination.totalItems = null
+ this.totalNotDeletedComments = null
this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid)
this.loadMoreThreads()
export class VideoCommentThreadTree implements VideoCommentThreadTreeServerModel {
comment: VideoComment
+ hasDisplayedChildren: boolean
children: VideoCommentThreadTree[]
}
import {
FeedFormat,
ResultList,
+ ThreadsResultList,
VideoComment as VideoCommentServerModel,
VideoCommentAdmin,
VideoCommentCreate,
videoId: number | string,
componentPagination: ComponentPaginationLight,
sort: string
- }): Observable<ResultList<VideoComment>> {
+ }): Observable<ThreadsResultList<VideoComment>> {
const { videoId, componentPagination, sort } = parameters
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
params = this.restService.addRestGetParams(params, pagination, sort)
const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads'
- return this.authHttp.get<ResultList<VideoComment>>(url, { params })
+ return this.authHttp.get<ThreadsResultList<VideoComment>>(url, { params })
.pipe(
map(result => this.extractVideoComments(result)),
catchError(err => this.restExtractor.handleError(err))
return new VideoComment(videoComment)
}
- private extractVideoComments (result: ResultList<VideoCommentServerModel>) {
+ private extractVideoComments (result: ThreadsResultList<VideoCommentServerModel>) {
const videoCommentsJson = result.data
const totalComments = result.total
const comments: VideoComment[] = []
comments.push(new VideoComment(videoCommentJson))
}
- return { data: comments, total: totalComments }
+ return { data: comments, total: totalComments, totalNotDeletedComments: result.totalNotDeletedComments }
}
- private extractVideoCommentTree (tree: VideoCommentThreadTreeServerModel) {
- if (!tree) return tree as VideoCommentThreadTree
+ private extractVideoCommentTree (serverTree: VideoCommentThreadTreeServerModel): VideoCommentThreadTree {
+ if (!serverTree) return null
- tree.comment = new VideoComment(tree.comment)
- tree.children.forEach(c => this.extractVideoCommentTree(c))
+ const tree = {
+ comment: new VideoComment(serverTree.comment),
+ children: serverTree.children.map(c => this.extractVideoCommentTree(c))
+ }
+
+ const hasDisplayedChildren = tree.children.length === 0
+ ? !tree.comment.isDeleted
+ : tree.children.some(c => c.hasDisplayedChildren)
- return tree as VideoCommentThreadTree
+ return Object.assign(tree, { hasDisplayedChildren })
}
private buildParamsFromSearch (search: string, params: HttpParams) {
import * as express from 'express'
-import { ResultList, UserRight } from '../../../../shared/models'
+import { ResultList, ThreadsResultList, UserRight } from '../../../../shared/models'
import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
import { getFormattedObjects } from '../../../helpers/utils'
import { AccountModel } from '../../../models/account/account'
import { VideoCommentModel } from '../../../models/video/video-comment'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { logger } from '@server/helpers/logger'
const auditLogger = auditLoggerFactory('comments')
const videoCommentRouter = express.Router()
const video = res.locals.onlyVideo
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
- let resultList: ResultList<VideoCommentModel>
+ let resultList: ThreadsResultList<VideoCommentModel>
if (video.commentsEnabled === true) {
const apiOptions = await Hooks.wrapObject({
} else {
resultList = {
total: 0,
+ totalNotDeletedComments: 0,
data: []
}
}
- return res.json(getFormattedObjects(resultList.data, resultList.total))
+ return res.json({
+ ...getFormattedObjects(resultList.data, resultList.total),
+ totalNotDeletedComments: resultList.totalNotDeletedComments
+ })
}
async function listVideoThreadComments (req: express.Request, res: express.Response) {
}
}
+ logger.info('coucou', { resultList })
+
if (resultList.data.length === 0) {
return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
}
const blockerIdsString = blockerIds.join(', ')
return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' +
- ' UNION ALL ' +
+ ' UNION ' +
'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' +
'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' +
'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')'
const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned })
- const query = {
+ const accountBlockedWhere = {
+ accountId: {
+ [Op.notIn]: Sequelize.literal(
+ '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
+ )
+ }
+ }
+
+ const queryList = {
offset: start,
limit: count,
order: getCommentSort(sort),
},
{
[Op.or]: [
- {
- accountId: {
- [Op.notIn]: Sequelize.literal(
- '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
- )
- }
- },
+ accountBlockedWhere,
{
accountId: null
}
}
}
- const scopes: (string | ScopeOptions)[] = [
+ const scopesList: (string | ScopeOptions)[] = [
ScopeNames.WITH_ACCOUNT_FOR_API,
{
method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ]
}
]
- return VideoCommentModel
- .scope(scopes)
- .findAndCountAll(query)
- .then(({ rows, count }) => {
- return { total: count, data: rows }
- })
+ const queryCount = {
+ where: {
+ videoId,
+ deletedAt: null,
+ ...accountBlockedWhere
+ }
+ }
+
+ return Promise.all([
+ VideoCommentModel.scope(scopesList).findAndCountAll(queryList),
+ VideoCommentModel.count(queryCount)
+ ]).then(([ { rows, count }, totalNotDeletedComments ]) => {
+ return { total: count, data: rows, totalNotDeletedComments }
+ })
}
static async listThreadCommentsForApi (parameters: {
{ id: threadId },
{ originCommentId: threadId }
],
- accountId: {
- [Op.notIn]: Sequelize.literal(
- '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
- )
- }
+ [Op.or]: [
+ {
+ accountId: {
+ [Op.notIn]: Sequelize.literal(
+ '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
+ )
+ }
+ },
+ {
+ accountId: null
+ }
+ ]
}
}
}
]
- return VideoCommentModel
- .scope(scopes)
+ return VideoCommentModel.scope(scopes)
.findAndCountAll(query)
.then(({ rows, count }) => {
return { total: count, data: rows }
const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
expect(res.body.total).to.equal(0)
+ expect(res.body.totalNotDeletedComments).to.equal(0)
expect(res.body.data).to.be.an('array')
expect(res.body.data).to.have.lengthOf(0)
})
const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5)
expect(res.body.total).to.equal(1)
+ expect(res.body.totalNotDeletedComments).to.equal(1)
expect(res.body.data).to.be.an('array')
expect(res.body.data).to.have.lengthOf(1)
const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt')
expect(res.body.total).to.equal(3)
+ expect(res.body.totalNotDeletedComments).to.equal(6)
expect(res.body.data).to.be.an('array')
expect(res.body.data).to.have.lengthOf(3)
it('Should delete a reply', async function () {
await deleteVideoComment(server.url, server.accessToken, videoId, replyToDeleteId)
- const res = await getVideoThreadComments(server.url, videoUUID, threadId)
-
- const tree: VideoCommentThreadTree = res.body
- expect(tree.comment.text).equal('my super first comment')
- expect(tree.children).to.have.lengthOf(2)
-
- const firstChild = tree.children[0]
- expect(firstChild.comment.text).to.equal('my super answer to thread 1')
- expect(firstChild.children).to.have.lengthOf(1)
+ {
+ const res = await getVideoCommentThreads(server.url, videoUUID, 0, 5, 'createdAt')
- const childOfFirstChild = firstChild.children[0]
- expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
- expect(childOfFirstChild.children).to.have.lengthOf(0)
+ expect(res.body.total).to.equal(3)
+ expect(res.body.totalNotDeletedComments).to.equal(5)
+ }
- const deletedChildOfFirstChild = tree.children[1]
- expect(deletedChildOfFirstChild.comment.text).to.equal('')
- expect(deletedChildOfFirstChild.comment.isDeleted).to.be.true
- expect(deletedChildOfFirstChild.comment.deletedAt).to.not.be.null
- expect(deletedChildOfFirstChild.comment.account).to.be.null
- expect(deletedChildOfFirstChild.children).to.have.lengthOf(0)
+ {
+ const res = await getVideoThreadComments(server.url, videoUUID, threadId)
+
+ const tree: VideoCommentThreadTree = res.body
+ expect(tree.comment.text).equal('my super first comment')
+ expect(tree.children).to.have.lengthOf(2)
+
+ const firstChild = tree.children[0]
+ expect(firstChild.comment.text).to.equal('my super answer to thread 1')
+ expect(firstChild.children).to.have.lengthOf(1)
+
+ const childOfFirstChild = firstChild.children[0]
+ expect(childOfFirstChild.comment.text).to.equal('my super answer to answer of thread 1')
+ expect(childOfFirstChild.children).to.have.lengthOf(0)
+
+ const deletedChildOfFirstChild = tree.children[1]
+ expect(deletedChildOfFirstChild.comment.text).to.equal('')
+ expect(deletedChildOfFirstChild.comment.isDeleted).to.be.true
+ expect(deletedChildOfFirstChild.comment.deletedAt).to.not.be.null
+ expect(deletedChildOfFirstChild.comment.account).to.be.null
+ expect(deletedChildOfFirstChild.children).to.have.lengthOf(0)
+ }
})
it('Should delete a complete thread', async function () {
total: number
data: T[]
}
+
+export interface ThreadsResultList <T> extends ResultList <T> {
+ totalNotDeletedComments: number
+}