Add video channel and video thumbnail, rework video appearance in row
authorRigel Kent <sendmemail@rigelk.eu>
Fri, 17 Apr 2020 08:47:22 +0000 (10:47 +0200)
committerRigel Kent <par@rigelk.eu>
Fri, 1 May 2020 14:41:02 +0000 (16:41 +0200)
12 files changed:
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html
client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss
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 [new file with mode: 0644]
client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
client/src/app/+admin/users/user-edit/user-update.component.ts
server/lib/emailer.ts
server/models/video/video-abuse.ts
server/models/video/video.ts
server/typings/models/video/video-abuse.ts
shared/models/videos/abuse/video-abuse.model.ts

index 075be8498aac1ad4bfb62e32119afaa54f8c7105..19b33a0f50b0580d1ea7aa28b4ad27974282b42e 100644 (file)
 
   <ng-template pTemplate="body" let-serverBlock>
     <tr>
-      <td>{{ serverBlock.blockedServer.host }}</td>
+      <td>
+        <a [href]="'https://' + serverBlock.blockedServer.host" i18n-title title="Open instance in a new tab" target="_blank" rel="noopener noreferrer">
+          {{ serverBlock.blockedServer.host }}
+          <span class="glyphicon glyphicon-new-window"></span>
+        </a>
+      </td>
       <td>{{ serverBlock.createdAt }}</td>
       <td class="action-cell">
         <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
index 9d3bedd80621138ba58adf09d91cc5971527e158..c6c71587f2d69121c6372f18265074e25a95107d 100644 (file)
@@ -1,6 +1,20 @@
 @import '_variables';
 @import '_mixins';
 
+a {
+  @include disable-default-a-behaviour;
+  display: inline-block;
+
+  &, &:hover {
+    color: var(--mainForegroundColor);
+  }
+
+  span {
+    font-size: 80%;
+    color: var(--inputPlaceholderColor);
+  }
+}
+
 .unblock-button {
   @include peertube-button;
   @include grey-button;
index a015b6d85d75b544bcf3ca6b01166c4dd3f8f1f3..9ceff11610631c5e7449e13463dda539be0a20a4 100644 (file)
   }
 }
 
-.glyphicon-trash {
-  font-size: 80%;
-}
-
 .screenratio {
   position: relative;
   width: 100%;
@@ -47,6 +43,7 @@
     display: inline-flex;
     justify-content: center;
     align-items: center;
+    color: var(--inputPlaceholderColor);
   }
 
   ::ng-deep iframe {
index 204cb209eb4a245826aa7a01b5c9ffe0b99f3cea..2204bb371c5ffb0ded2bc7c4fb51d1d514404fb3 100644 (file)
       </td>
 
       <td>
-        <span *ngIf="videoAbuse.video.deleted" i18n-title title="Video was deleted" class="glyphicon glyphicon-trash"></span>
-        <a [href]="getVideoUrl(videoAbuse)" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer">
-          {{ videoAbuse.video.name }}
+        <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 *ngIf="!videoAbuse.video.deleted" [src]="videoAbuse.video.thumbnailPath">
+              <span *ngIf="videoAbuse.video.deleted" i18n>Deleted</span>
+            </div>
+            <div class="video-abuse-video-text">
+              <div>
+                {{ videoAbuse.video.name }}
+                <span *ngIf="!videoAbuse.video.deleted && !videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
+                <span *ngIf="videoAbuse.video.deleted" i18n-title title="Video was deleted" class="glyphicon glyphicon-trash"></span>
+                <span *ngIf="videoAbuse.video.blacklisted" i18n-title title="Video was blacklisted" class="glyphicon glyphicon-ban-circle"></span>
+              </div>
+              <div class="text-muted">by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div>
+            </div>
+          </div>
         </a>
       </td>
 
 
             <div class="col-4">
               <div class="screenratio">
-                <div *ngIf="videoAbuse.video.deleted">
+                <div *ngIf="videoAbuse.video.deleted || videoAbuse.video.blacklisted">
                   <span i18n>The video was {{ videoAbuse.video.deleted ? 'deleted' : 'blacklisted' }}</span>
                 </div>
-                <div *ngIf="!videoAbuse.video.deleted" [innerHTML]="videoAbuse.embedHtml"></div>
+                <div *ngIf="!videoAbuse.video.deleted && !videoAbuse.video.blacklisted" [innerHTML]="videoAbuse.embedHtml"></div>
               </div>
             </div>
           </div>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss
new file mode 100644 (file)
index 0000000..09402fd
--- /dev/null
@@ -0,0 +1,57 @@
+@import 'mixins';
+@import 'miniature';
+
+.video-abuse-video-link {
+  @include disable-outline;
+  position: relative;
+  top: 3px;
+}
+
+.video-abuse-video {
+  display: inline-flex;
+
+  .video-abuse-video-image {
+    @include miniature-thumbnail;
+
+    $image-height: 45px;
+
+    height: $image-height;
+    width: #{(16/9) * $image-height};
+    margin-right: 0.5rem;
+    border-radius: 2px;
+    border: none;
+    background: transparent;
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+
+    img {
+      height: 100%;
+      width: 100%;
+      border-radius: 2px;
+    }
+
+    span {
+      color: var(--inputPlaceholderColor);
+    }
+  }
+
+  .video-abuse-video-text {
+    display: inline-flex;
+    flex-direction: column;
+    justify-content: center;
+    font-size: 90%;
+    color: var(--mainForegroundColor);
+    line-height: 1rem;
+
+    div .glyphicon {
+      font-size: 80%;
+      color: gray;
+      margin-left: 0.1rem;
+    }
+
+    div + div {
+      font-size: 80%;
+    }
+  }
+}
index 9858cbce2958cdb14432c2dbdaac7343d1656842..cc5014ae8596bbd8b211320608d5cf16ecb864a6 100644 (file)
@@ -20,14 +20,14 @@ import { VideoService } from '@app/shared/video/video.service'
 @Component({
   selector: 'my-video-abuse-list',
   templateUrl: './video-abuse-list.component.html',
-  styleUrls: [ '../moderation.component.scss']
+  styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ]
 })
 export class VideoAbuseListComponent extends RestTable implements OnInit {
   @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
 
   videoAbuses: (VideoAbuse & { moderationCommentHtml?: string, reasonHtml?: string })[] = []
   totalRecords = 0
-  rowsPerPageOptions = [ 20, 50, 100 ]
+  rowsPerPageOptions = [ 20, 50, 100 ]
   rowsPerPage = this.rowsPerPageOptions[0]
   sort: SortMeta = { field: 'createdAt', order: 1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
@@ -86,7 +86,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
         },
         {
           label: this.i18n('Blacklist video'),
-          isDisplayed: videoAbuse => !videoAbuse.video.deleted,
+          isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted,
           handler: videoAbuse => {
             this.videoBlacklistService.blacklistVideo(videoAbuse.video.id, undefined, true)
               .subscribe(
@@ -100,11 +100,30 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
               )
           }
         },
+        {
+          label: this.i18n('Unblacklist video'),
+          isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted,
+          handler: videoAbuse => {
+            this.videoBlacklistService.removeVideoFromBlacklist(videoAbuse.video.id)
+              .subscribe(
+                () => {
+                  this.notifier.success(this.i18n('Video unblacklisted.'))
+
+                  this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
+                },
+
+                err => this.notifier.error(err.message)
+              )
+          }
+        },
         {
           label: this.i18n('Delete video'),
           isDisplayed: videoAbuse => !videoAbuse.video.deleted,
           handler: async videoAbuse => {
-            const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete'))
+            const res = await this.confirmService.confirm(
+              this.i18n('Do you really want to delete this video?'),
+              this.i18n('Delete')
+            )
             if (res === false) return
 
             this.videoService.removeVideo(videoAbuse.video.id)
@@ -126,18 +145,36 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
           isHeader: true
         },
         {
-          label: this.i18n('Mute reporter'),
+          label: this.i18n('Mute reporter'),
           handler: async videoAbuse => {
             const account = videoAbuse.reporterAccount as Account
 
             this.blocklistService.blockAccountByInstance(account)
               .subscribe(
                 () => {
-                  this.notifier.success(this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }))
+                  this.notifier.success(
+                    this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
+                  )
 
                   account.mutedByInstance = true
                 },
 
+                err => this.notifier.error(err.message)
+              )
+          }
+        },
+        {
+          label: this.i18n('Mute server'),
+          isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId,
+          handler: async videoAbuse => {
+            this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host)
+              .subscribe(
+                () => {
+                  this.notifier.success(
+                    this.i18n('Server {{host}} muted by the instance.', { host: videoAbuse.reporterAccount.host })
+                  )
+                },
+
                 err => this.notifier.error(err.message)
               )
           }
index fbe3d695035fd84a4615a4029227973493723e6d..e0e1fbddf43c42088a215e360384327cdc72c051 100644 (file)
@@ -85,7 +85,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
 
     this.userService.updateUser(this.user.id, userUpdate).subscribe(
       () => {
-        this.notifier.success(this.i18n('User {{user.username}} updated.', { username: this.user.username }))
+        this.notifier.success(this.i18n('User {{username}} updated.', { username: this.user.username }))
         this.router.navigate([ '/admin/users/list' ])
       },
 
index 2c0641f3af3df7d377b8a93b856820039e610b0a..5a99edc7f68bd196ed8e32de6254c0800b7db30a 100644 (file)
@@ -292,7 +292,7 @@ class Emailer {
     const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath()
 
     const text = 'Hi,\n\n' +
-      `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` +
+      `${WEBSERVER.HOST} received an abuse for the following video: ${videoUrl}\n\n` +
       'Cheers,\n' +
       `${CONFIG.EMAIL.BODY.SIGNATURE}`
 
index ea985621317a5b9d142df807c4a1c653193b06a4..ea943ffdfe4202a0a4ccb103d9be706f1de69a1d 100644 (file)
@@ -1,4 +1,6 @@
-import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import {
+  AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt, DefaultScope
+} from 'sequelize-typescript'
 import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
 import { VideoAbuse } from '../../../shared/models/videos'
 import {
@@ -14,7 +16,40 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/const
 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'
 
+@DefaultScope(() => ({
+  include: [
+    {
+      model: AccountModel,
+      required: true
+    },
+    {
+      model: VideoModel,
+      required: false,
+      include: [
+        {
+          model: ThumbnailModel
+        },
+        {
+          model: VideoChannelModel.unscoped(),
+          include: [
+            {
+              model: ActorModel
+            }
+          ]
+        },
+        {
+          attributes: [ 'id', 'reason', 'unfederated' ],
+          model: VideoBlacklistModel
+        }
+      ]
+    }
+  ]
+}))
 @Table({
   tableName: 'videoAbuse',
   indexes: [
@@ -114,16 +149,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
           [Op.notIn]: literal('(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')')
         }
       },
-      include: [
-        {
-          model: AccountModel,
-          required: true
-        },
-        {
-          model: VideoModel,
-          required: false
-        }
-      ]
+      col: 'VideoAbuseModel.id',
+      distinct: true
     }
 
     return VideoAbuseModel.findAndCountAll(query)
@@ -151,7 +178,10 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
         uuid: video.uuid,
         name: video.name,
         nsfw: video.nsfw,
-        deleted: !this.Video
+        deleted: !this.Video,
+        blacklisted: this.Video && this.Video.isBlacklisted(),
+        thumbnailPath: this.Video?.getMiniatureStaticPath(),
+        channel: this.Video?.VideoChannel.toFormattedSummaryJSON() || this.deletedVideo?.channel
       },
       createdAt: this.createdAt
     }
index 2636ebd8ebd6a2aac7f2fb3312bb9df582a6b652..f32216e908c4604d83fd7bc9d2f376bc1fb5bedb 100644 (file)
@@ -810,7 +810,7 @@ export class VideoModel extends Model<VideoModel> {
       if (instance.VideoAbuses.length === 0) return undefined
     }
 
-    const details = instance.toFormattedJSON()
+    const details = instance.toFormattedDetailsJSON()
 
     for (const abuse of instance.VideoAbuses) {
       tasks.push((_ => {
index 49bd1ff2e7e9e20d585935ce188ab14199d516d2..54acccdf5a7290581bc270ab4ce06ba4a1c60a4b 100644 (file)
@@ -1,6 +1,6 @@
 import { VideoAbuseModel } from '../../../models/video/video-abuse'
 import { PickWith } from '../../utils'
-import { MVideo } from './video'
+import { MVideoAccountLightBlacklistAllFiles } from './video'
 import { MAccountDefault, MAccountFormattable } from '../account'
 
 type Use<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
@@ -16,12 +16,12 @@ export type MVideoAbuseId = Pick<VideoAbuseModel, 'id'>
 export type MVideoAbuseVideo =
   MVideoAbuse &
   Pick<VideoAbuseModel, 'toActivityPubObject'> &
-  Use<'Video', MVideo>
+  Use<'Video', MVideoAccountLightBlacklistAllFiles>
 
 export type MVideoAbuseAccountVideo =
   MVideoAbuse &
   Pick<VideoAbuseModel, 'toActivityPubObject'> &
-  Use<'Video', MVideo> &
+  Use<'Video', MVideoAccountLightBlacklistAllFiles> &
   Use<'Account', MAccountDefault>
 
 // ############################################################################
@@ -31,4 +31,5 @@ export type MVideoAbuseAccountVideo =
 export type MVideoAbuseFormattable =
   MVideoAbuse &
   Use<'Account', MAccountFormattable> &
-  Use<'Video', Pick<MVideo, 'id' | 'uuid' | 'name' | 'nsfw'>>
+  Use<'Video', Pick<MVideoAccountLightBlacklistAllFiles,
+  'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel'>>
index b47ee05a0a119abddd5367b61aeec92660a50104..953193e5ea8954f4222c04e967f6a25e7371aee5 100644 (file)
@@ -1,6 +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'
 
 export interface VideoAbuse {
   id: number
@@ -16,6 +17,9 @@ export interface VideoAbuse {
     uuid: string
     nsfw: boolean
     deleted: boolean
+    blacklisted: boolean
+    thumbnailPath?: string
+    channel?: VideoChannelSummary
   }
 
   createdAt: Date