Add nth abuse count for a given video, add reporter/reportee reports stats
authorRigel Kent <sendmemail@rigelk.eu>
Sat, 18 Apr 2020 20:57:20 +0000 (22:57 +0200)
committerRigel Kent <par@rigelk.eu>
Fri, 1 May 2020 14:41:02 +0000 (16:41 +0200)
client/src/app/+admin/moderation/moderation.component.scss
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
server/models/video/video-abuse.ts
shared/models/videos/abuse/video-abuse.model.ts

index 9ceff11610631c5e7449e13463dda539be0a20a4..ef6a39b5d5670403776030489961f85529baa67c 100644 (file)
@@ -7,19 +7,23 @@
   margin-right: 30px;
 }
 
-.moderation-expanded-label {
-  font-weight: $font-semibold;
-  display: inline-block;
-  vertical-align: top;
-  text-align: right;
-}
-
-.moderation-expanded-text {
-  display: inline-block;
-  word-wrap: break-word;
+.moderation-expanded {
+  font-size: 90%;
 
-  ::ng-deep p:last-child {
-    margin-bottom: 0px !important;
+  .moderation-expanded-label {
+    font-weight: $font-semibold;
+    display: inline-block;
+    vertical-align: top;
+    text-align: right;
+  }
+  
+  .moderation-expanded-text {
+    display: inline-flex;
+    word-wrap: break-word;
+  
+    ::ng-deep p:last-child {
+      margin-bottom: 0px !important;
+    }
   }
 }
 
@@ -58,3 +62,9 @@
 .chip {
   @include chip;
 }
+
+my-action-dropdown.show {
+  ::ng-deep .dropdown-root {
+    display: block !important;
+  }
+}
index c1ce093d7d3aeaaba8d35eb60ec552041d9b1cb5..67ef284083aeca3abc5ca853ab1129a408f1cac6 100644 (file)
       <td *ngIf="!videoAbuse.video.deleted">
         <a [href]="getVideoUrl(videoAbuse)" class="video-abuse-video-link" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer">
           <div class="video-abuse-video">
-            <div class="video-abuse-video-image"><img [src]="videoAbuse.video.thumbnailPath"></div>
+            <div class="video-abuse-video-image">
+              <img [src]="videoAbuse.video.thumbnailPath">
+              <span
+                class="video-abuse-video-image-label" *ngIf="videoAbuse.count > 1"
+                i18n-title title="This video has been reported multiple times."
+              >{{ videoAbuse.nth }}/{{ videoAbuse.count }}</span>
+            </div>
             <div class="video-abuse-video-text">
               <div>
                 {{ videoAbuse.video.name }}
       <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse">
         <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
         <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
-        <span *ngIf="videoAbuse.moderationComment" [title]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
+        <span *ngIf="videoAbuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
       </td>
 
       <td class="action-cell">
-        <my-action-dropdown placement="bottom-right auto" container="body" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
+        <my-action-dropdown
+          [ngClass]="{ 'show': expanded }" placement="bottom-right auto" container="body"
+          i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"
+        ></my-action-dropdown>
       </td>
     </tr>
   </ng-template>
   <ng-template pTemplate="rowexpansion" let-videoAbuse>
       <tr>
         <td class="expand-cell" colspan="6">
-          <div class="d-flex">
+          <div class="d-flex moderation-expanded">
+            <!-- report metadata -->
             <div class="col-8">
               <div class="d-flex">
-                <span class="col-3 moderation-expanded-label" i18n>Reason:</span>
+                <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
+                <span class="col-9 moderation-expanded-text">
+                  <div class="chip">
+                    <img
+                      class="avatar"
+                      [src]="videoAbuse.reporterAccount.avatar.path"
+                      (error)="switchToDefaultAvatar($event)"
+                      alt="Avatar"
+                    >
+                    <div>
+                      <span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span>
+                    </div>
+                  </div>
+                  <a routerLink="/admin/moderation/video-abuses/list" class="ml-auto text-muted video-abuse-links" i18n>
+                    {videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
+                  </a>
+                </span>
+              </div>
+              <div class="d-flex">
+                <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
+                <span class="col-9 moderation-expanded-text">
+                  <div class="chip">
+                    <img
+                      class="avatar"
+                      [src]="videoAbuse.video.channel.ownerAccount?.avatar.path"
+                      (error)="switchToDefaultAvatar($event)"
+                      alt="Avatar"
+                    >
+                    <div>
+                      <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span>
+                    </div>
+                  </div>
+                  <a routerLink="/admin/moderation/video-abuses/list" class="ml-auto text-muted video-abuse-links" *ngIf="!videoAbuse.video.deleted" i18n>
+                    {videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
+                  </a>
+                </span>
+              </div>
+              <div class="d-flex">
+                <span class="col-3 moderation-expanded-label" i18n>Updated</span>
+                <time class="col-9 moderation-expanded-text video-abuse-date-updated">{{ videoAbuse.updatedAt | date: 'medium' }}</time>
+              </div>
+
+              <!-- report text -->
+              <div class="mt-3 d-flex">
+                <span class="col-3 moderation-expanded-label" i18n>Report</span>
                 <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span>
               </div>
               <div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment">
-                <span class="col-3 moderation-expanded-label" i18n>Note:</span>
+                <span class="col-3 moderation-expanded-label" i18n>Note</span>
                 <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
               </div>
             </div>
index 9b60c39dc08c87556514f266c663909b91870ea5..b5dc53b3a4d7e4bc6e2f7174ca5d14a015c00b3d 100644 (file)
@@ -9,6 +9,15 @@
   }
 }
 
+.video-abuse-date-updated {
+  font-size: 90%;
+  margin-top: .1rem;
+}
+
+.video-abuse-links {
+  @include disable-default-a-behaviour;
+}
+
 .video-abuse-video-link {
   @include disable-outline;
   position: relative;
@@ -32,6 +41,7 @@
     display: inline-flex;
     justify-content: center;
     align-items: center;
+    position: relative;
 
     img {
       height: 100%;
     span {
       color: var(--inputPlaceholderColor);
     }
+
+    .video-abuse-video-image-label {
+      @include static-thumbnail-overlay;
+      position: absolute;
+      border-radius: 3px;
+      font-size: 10px;
+      padding: 0 3px;
+      line-height: 1.3;
+      bottom: 2px;
+      right: 2px;
+    }
   }
 
   .video-abuse-video-text {
index 6dcf96ccfe02f5ae588ae185ac4cfaa8fec9d6ba..e4e78cdf70ab00a82820c7e918ee75963c16827a 100644 (file)
@@ -46,7 +46,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
     private i18n: I18n,
     private markdownRenderer: MarkdownService,
     private sanitizer: DomSanitizer,
-    private route: ActivatedRoute,
+    private route: ActivatedRoute
   ) {
     super()
 
@@ -223,7 +223,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
   }
 
   getVideoEmbed (videoAbuse: VideoAbuse) {
-    const absoluteAPIUrl = 'http://localhost:9000' || getAbsoluteAPIUrl()
+    const absoluteAPIUrl = 'http://localhost:9000' || getAbsoluteAPIUrl() // TODO
     const embedUrl = buildVideoLink({
       baseUrl: absoluteAPIUrl + '/videos/embed/' + videoAbuse.video.uuid,
       warningTitle: false
index 2f9fc8ba410a11f2d6221261d3804929e1aaaaf3..c5c0fdbbf6c5799afea5cd4b0b28d30a3b3991d4 100644 (file)
         </a>
       </td>
 
-      <td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
-      <td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
-      <td>{{ videoBlacklist.createdAt }}</td>
+      <ng-container *ngIf="videoBlacklist.reason">
+        <td class="c-hand" [pRowToggler]="videoBlacklist">{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
+        <td class="c-hand" [pRowToggler]="videoBlacklist">{{ booleanToText(videoBlacklist.unfederated) }}</td>
+        <td class="c-hand" [pRowToggler]="videoBlacklist">{{ videoBlacklist.createdAt }}</td>
+      </ng-container>
+      <ng-container *ngIf="!videoBlacklist.reason">
+        <td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
+        <td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
+        <td>{{ videoBlacklist.createdAt }}</td>
+      </ng-container>
 
       <td class="action-cell">
         <my-action-dropdown i18n-label  placement="bottom-right" label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
index 5ead02ecae5d46b446b1a18d2be99e2848ddac7e..d68608ca668cc33a757c1d49b0a1b42b43db8803 100644 (file)
@@ -11,14 +11,13 @@ import {
 import { AccountModel } from '../account/account'
 import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
 import { VideoModel } from './video'
-import { VideoAbuseState, Video } from '../../../shared'
+import { VideoAbuseState, VideoDetails } from '../../../shared'
 import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
 import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
 import * as Bluebird from 'bluebird'
 import { literal, Op } from 'sequelize'
 import { ThumbnailModel } from './thumbnail'
 import { VideoChannelModel } from './video-channel'
-import { ActorModel } from '../activitypub/actor'
 import { VideoBlacklistModel } from './video-blacklist'
 
 export enum ScopeNames {
@@ -78,9 +77,73 @@ export enum ScopeNames {
       })
     }
 
-    console.log(where)
-
     return {
+      attributes: {
+        include: [
+          [
+            literal(
+              '(' +
+                'SELECT t.count ' +
+                'FROM ( ' +
+                  'SELECT id, ' +
+                         'count(id) OVER (PARTITION BY "videoId") ' +
+                  'FROM "videoAbuse" ' +
+                ') t ' +
+                'WHERE t.id = "VideoAbuseModel".id ' +
+              ')'
+            ),
+            'countReportsForVideo'
+          ],
+          [
+            literal(
+              '(' +
+                'SELECT t.nth ' +
+                'FROM ( ' +
+                  'SELECT id, ' +
+                         'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
+                  'FROM "videoAbuse" ' +
+                ') t ' +
+                'WHERE t.id = "VideoAbuseModel".id ' +
+              ')'
+            ),
+            'nthReportForVideo'
+          ],
+          [
+            literal(
+              '(' +
+                'SELECT count("videoAbuse"."id") ' +
+                'FROM "videoAbuse" ' +
+                'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
+                'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
+                'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
+                'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
+              ')'
+            ),
+            'countReportsForReporter'
+          ],
+          [
+            literal(
+              '(' +
+                'WITH ' +
+                  'ids AS ( ' +
+                    'SELECT "account"."id" ' +
+                    'FROM "account" ' +
+                    'INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ' +
+                    'INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" ' +
+                    'WHERE "video"."id" = "VideoAbuseModel"."videoId" ' +
+                  ') ' +
+                'SELECT count("videoAbuse"."id") ' +
+                'FROM "videoAbuse" ' +
+                'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
+                'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
+                'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
+                'INNER JOIN ids ON "account"."id" = ids.id ' +
+              ')'
+            ),
+            'countReportsForReportee'
+          ]
+        ]
+      },
       include: [
         {
           model: AccountModel,
@@ -96,13 +159,8 @@ export enum ScopeNames {
               model: ThumbnailModel
             },
             {
-              model: VideoChannelModel.unscoped(),
-              where: { ...search(options.searchVideoChannel, 'name') },
-              include: [
-                {
-                  model: ActorModel
-                }
-              ]
+              model: VideoChannelModel.scope([ 'WITH_ACTOR', 'WITH_ACCOUNT' ]),
+              where: { ...search(options.searchVideoChannel, 'name') }
             },
             {
               attributes: [ 'id', 'reason', 'unfederated' ],
@@ -149,7 +207,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
   @AllowNull(true)
   @Default(null)
   @Column(DataType.JSONB)
-  deletedVideo: Video
+  deletedVideo: VideoDetails
 
   @CreatedAt
   createdAt: Date
@@ -229,6 +287,11 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
   }
 
   toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
+    const countReportsForVideo = this.get('countReportsForVideo') as number
+    const nthReportForVideo = this.get('nthReportForVideo') as number
+    const countReportsForReporter = this.get('countReportsForReporter') as number
+    const countReportsForReportee = this.get('countReportsForReportee') as number
+
     const video = this.Video
       ? this.Video
       : this.deletedVideo
@@ -250,9 +313,14 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
         deleted: !this.Video,
         blacklisted: this.Video && this.Video.isBlacklisted(),
         thumbnailPath: this.Video?.getMiniatureStaticPath(),
-        channel: this.Video?.VideoChannel.toFormattedSummaryJSON() || this.deletedVideo?.channel
+        channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
       },
-      createdAt: this.createdAt
+      createdAt: this.createdAt,
+      updatedAt: this.updatedAt,
+      count: countReportsForVideo || 0,
+      nth: nthReportForVideo || 0,
+      countReportsForReporter: countReportsForReporter || 0,
+      countReportsForReportee: countReportsForReportee || 0
     }
   }
 
index 953193e5ea8954f4222c04e967f6a25e7371aee5..f2c2cdc415d99b07d2b9079f0d86fab7ee721474 100644 (file)
@@ -1,7 +1,7 @@
 import { Account } from '../../actors/index'
 import { VideoConstant } from '../video-constant.model'
 import { VideoAbuseState } from './video-abuse-state.model'
-import { VideoChannelSummary } from '../channel/video-channel.model'
+import { VideoChannel } from '../channel/video-channel.model'
 
 export interface VideoAbuse {
   id: number
@@ -19,8 +19,15 @@ export interface VideoAbuse {
     deleted: boolean
     blacklisted: boolean
     thumbnailPath?: string
-    channel?: VideoChannelSummary
+    channel?: VideoChannel
   }
 
   createdAt: Date
+  updatedAt: Date
+
+  count?: number
+  nth?: number
+
+  countReportsForReporter?: number
+  countReportsForReportee?: number
 }