]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Fix video comments display with deleted comments
authorChocobozzz <me@florianbigard.com>
Fri, 19 Feb 2021 08:50:13 +0000 (09:50 +0100)
committerChocobozzz <me@florianbigard.com>
Fri, 19 Feb 2021 09:06:52 +0000 (10:06 +0100)
client/src/app/+videos/+video-watch/comment/video-comment.component.html
client/src/app/+videos/+video-watch/comment/video-comment.component.ts
client/src/app/+videos/+video-watch/comment/video-comments.component.html
client/src/app/+videos/+video-watch/comment/video-comments.component.ts
client/src/app/shared/shared-video-comment/video-comment-thread-tree.model.ts
client/src/app/shared/shared-video-comment/video-comment.service.ts
server/controllers/api/videos/comment.ts
server/models/utils.ts
server/models/video/video-comment.ts
server/tests/api/videos/video-comments.ts
shared/models/result-list.model.ts

index 8847753a57ce917eddabe9d952f37f93116035f2..ba41b6f480b6df2f44a6d30b525eaee9a0230c8f 100644 (file)
@@ -1,4 +1,4 @@
-<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
index 0958b25c035c92581d14e45277482d6c30fd6498..5c5d72b2291f03854660475a92767f5e8da52284 100644 (file)
@@ -62,6 +62,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
     if (!this.commentTree) {
       this.commentTree = {
         comment: this.comment,
+        hasDisplayedChildren: false,
         children: []
       }
 
@@ -70,6 +71,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
 
     this.commentTree.children.unshift({
       comment: createdComment,
+      hasDisplayedChildren: false,
       children: []
     })
 
@@ -133,8 +135,11 @@ export class VideoCommentComponent implements OnInit, OnChanges {
     ($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) {
index f9ebfcc1f89a607c290a37b102826e4e6ae697b8..4a6426d3041ea52ea3c26d624d30eeb83a13f911 100644 (file)
@@ -1,10 +1,10 @@
 <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>
@@ -30,7 +30,7 @@
       [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"
index f83a73ccd5f0792aa0180133defdb1baca765c34..d36dd9e34c6ad1481f762338c874622734649155 100644 (file)
@@ -21,15 +21,20 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
 
   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 } = {}
 
@@ -122,8 +127,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
     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 })
@@ -241,6 +246,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy {
       this.inReplyToCommentId = undefined
       this.componentPagination.currentPage = 1
       this.componentPagination.totalItems = null
+      this.totalNotDeletedComments = null
 
       this.syndicationItems = this.videoCommentService.getVideoCommentsFeeds(this.video.uuid)
       this.loadMoreThreads()
index 7c2aaeadde86ce62174f7b9f56ef6cd803864455..9956c88a6fe8ba41f09639e9dcf124a6a68444aa 100644 (file)
@@ -3,5 +3,6 @@ import { VideoComment } from './video-comment.model'
 
 export class VideoCommentThreadTree implements VideoCommentThreadTreeServerModel {
   comment: VideoComment
+  hasDisplayedChildren: boolean
   children: VideoCommentThreadTree[]
 }
index c107a33abb5937bb9ee500b108175a2e503cd4f4..0f09778df5be694cde3e688186a49b05016f90fa 100644 (file)
@@ -8,6 +8,7 @@ import { objectLineFeedToHtml } from '@app/helpers'
 import {
   FeedFormat,
   ResultList,
+  ThreadsResultList,
   VideoComment as VideoCommentServerModel,
   VideoCommentAdmin,
   VideoCommentCreate,
@@ -76,7 +77,7 @@ export class VideoCommentService {
     videoId: number | string,
     componentPagination: ComponentPaginationLight,
     sort: string
-  }): Observable<ResultList<VideoComment>> {
+  }): Observable<ThreadsResultList<VideoComment>> {
     const { videoId, componentPagination, sort } = parameters
 
     const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
@@ -85,7 +86,7 @@ export class VideoCommentService {
     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))
@@ -158,7 +159,7 @@ export class VideoCommentService {
     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[] = []
@@ -167,16 +168,22 @@ export class VideoCommentService {
       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) {
index 752a335968a43aec232f985998d336d7a57a0781..b21698525576e37bd7ab6fb1c18181f0cc793213 100644 (file)
@@ -1,5 +1,5 @@
 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'
@@ -30,6 +30,7 @@ import {
 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()
@@ -108,7 +109,7 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
   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({
@@ -128,11 +129,15 @@ async function listVideoThreads (req: express.Request, res: express.Response) {
   } 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) {
@@ -161,6 +166,8 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo
     }
   }
 
+  logger.info('coucou', { resultList })
+
   if (resultList.data.length === 0) {
     return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
   }
index 143c1a23c13508627e1b1f1a9ee3e0ac935a8501..5337ae75dc38f924b3a89974e9b28c930a735eb4 100644 (file)
@@ -134,7 +134,7 @@ function buildBlockedAccountSQL (blockerIds: number[]) {
   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 + ')'
index 8d1c388266aeeb823121a7ed36c6e8bb9cdfc16e..cfd1d5b7a6188be05cbefc02317e53ccfe8e200f 100644 (file)
@@ -414,7 +414,15 @@ export class VideoCommentModel extends Model {
 
     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),
@@ -428,13 +436,7 @@ export class VideoCommentModel extends Model {
           },
           {
             [Op.or]: [
-              {
-                accountId: {
-                  [Op.notIn]: Sequelize.literal(
-                    '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
-                  )
-                }
-              },
+              accountBlockedWhere,
               {
                 accountId: null
               }
@@ -444,19 +446,27 @@ export class VideoCommentModel extends Model {
       }
     }
 
-    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: {
@@ -477,11 +487,18 @@ export class VideoCommentModel extends Model {
           { id: threadId },
           { originCommentId: threadId }
         ],
-        accountId: {
-          [Op.notIn]: Sequelize.literal(
-            '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
-          )
-        }
+        [Op.or]: [
+          {
+            accountId: {
+              [Op.notIn]: Sequelize.literal(
+                '(' + buildBlockedAccountSQL(blockerAccountIds) + ')'
+              )
+            }
+          },
+          {
+            accountId: null
+          }
+        ]
       }
     }
 
@@ -492,8 +509,7 @@ export class VideoCommentModel extends Model {
       }
     ]
 
-    return VideoCommentModel
-      .scope(scopes)
+    return VideoCommentModel.scope(scopes)
       .findAndCountAll(query)
       .then(({ rows, count }) => {
         return { total: count, data: rows }
index 141a80690a184cd7d9a376b235f1f8f24b104a37..615e0ea45a5fe1658d62ef9627233d3d0fe754a6 100644 (file)
@@ -67,6 +67,7 @@ describe('Test video comments', function () {
       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)
     })
@@ -94,6 +95,7 @@ describe('Test video comments', function () {
       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)
 
@@ -172,6 +174,7 @@ describe('Test video comments', function () {
       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)
 
@@ -186,26 +189,35 @@ describe('Test video comments', function () {
     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 () {
index 2d5147a865f4c632ba9cca5c548386a13713bfaf..fcafcfb2f23f80cd215dee3631cf1467053dc462 100644 (file)
@@ -2,3 +2,7 @@ export interface ResultList<T> {
   total: number
   data: T[]
 }
+
+export interface ThreadsResultList <T> extends ResultList <T> {
+  totalNotDeletedComments: number
+}