Add video channel and video thumbnail, rework video appearance in row
[oweals/peertube.git] / client / src / app / +admin / moderation / video-abuse-list / video-abuse-list.component.ts
1 import { Component, OnInit, ViewChild } from '@angular/core'
2 import { Account } from '@app/shared/account/account.model'
3 import { Notifier } from '@app/core'
4 import { SortMeta } from 'primeng/api'
5 import { VideoAbuse, VideoAbuseState } from '../../../../../../shared'
6 import { RestPagination, RestTable, VideoAbuseService, VideoBlacklistService } from '../../../shared'
7 import { I18n } from '@ngx-translate/i18n-polyfill'
8 import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
9 import { ConfirmService } from '../../../core/index'
10 import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
11 import { Video } from '../../../shared/video/video.model'
12 import { MarkdownService } from '@app/shared/renderer'
13 import { Actor } from '@app/shared/actor/actor.model'
14 import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils'
15 import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
16 import { DomSanitizer } from '@angular/platform-browser'
17 import { BlocklistService } from '@app/shared/blocklist'
18 import { VideoService } from '@app/shared/video/video.service'
19
20 @Component({
21   selector: 'my-video-abuse-list',
22   templateUrl: './video-abuse-list.component.html',
23   styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ]
24 })
25 export class VideoAbuseListComponent extends RestTable implements OnInit {
26   @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
27
28   videoAbuses: (VideoAbuse & { moderationCommentHtml?: string, reasonHtml?: string })[] = []
29   totalRecords = 0
30   rowsPerPageOptions = [ 20, 50, 100 ]
31   rowsPerPage = this.rowsPerPageOptions[0]
32   sort: SortMeta = { field: 'createdAt', order: 1 }
33   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
34
35   videoAbuseActions: DropdownAction<VideoAbuse>[][] = []
36
37   constructor (
38     private notifier: Notifier,
39     private videoAbuseService: VideoAbuseService,
40     private blocklistService: BlocklistService,
41     private videoService: VideoService,
42     private videoBlacklistService: VideoBlacklistService,
43     private confirmService: ConfirmService,
44     private i18n: I18n,
45     private markdownRenderer: MarkdownService,
46     private sanitizer: DomSanitizer
47   ) {
48     super()
49
50     this.videoAbuseActions = [
51       [
52         {
53           label: this.i18n('Internal actions'),
54           isHeader: true
55         },
56         {
57           label: this.i18n('Delete report'),
58           handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
59         },
60         {
61           label: this.i18n('Add note'),
62           handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
63           isDisplayed: videoAbuse => !videoAbuse.moderationComment
64         },
65         {
66           label: this.i18n('Update note'),
67           handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
68           isDisplayed: videoAbuse => !!videoAbuse.moderationComment
69         },
70         {
71           label: this.i18n('Mark as accepted'),
72           handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED),
73           isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse)
74         },
75         {
76           label: this.i18n('Mark as rejected'),
77           handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED),
78           isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse)
79         }
80       ],
81       [
82         {
83           label: this.i18n('Actions for the video'),
84           isHeader: true,
85           isDisplayed: videoAbuse => !videoAbuse.video.deleted
86         },
87         {
88           label: this.i18n('Blacklist video'),
89           isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted,
90           handler: videoAbuse => {
91             this.videoBlacklistService.blacklistVideo(videoAbuse.video.id, undefined, true)
92               .subscribe(
93                 () => {
94                   this.notifier.success(this.i18n('Video blacklisted.'))
95
96                   this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
97                 },
98
99                 err => this.notifier.error(err.message)
100               )
101           }
102         },
103         {
104           label: this.i18n('Unblacklist video'),
105           isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted,
106           handler: videoAbuse => {
107             this.videoBlacklistService.removeVideoFromBlacklist(videoAbuse.video.id)
108               .subscribe(
109                 () => {
110                   this.notifier.success(this.i18n('Video unblacklisted.'))
111
112                   this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
113                 },
114
115                 err => this.notifier.error(err.message)
116               )
117           }
118         },
119         {
120           label: this.i18n('Delete video'),
121           isDisplayed: videoAbuse => !videoAbuse.video.deleted,
122           handler: async videoAbuse => {
123             const res = await this.confirmService.confirm(
124               this.i18n('Do you really want to delete this video?'),
125               this.i18n('Delete')
126             )
127             if (res === false) return
128
129             this.videoService.removeVideo(videoAbuse.video.id)
130               .subscribe(
131                 () => {
132                   this.notifier.success(this.i18n('Video deleted.'))
133
134                   this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
135                 },
136
137                 err => this.notifier.error(err.message)
138               )
139           }
140         }
141       ],
142       [
143         {
144           label: this.i18n('Actions for the reporter'),
145           isHeader: true
146         },
147         {
148           label: this.i18n('Mute reporter'),
149           handler: async videoAbuse => {
150             const account = videoAbuse.reporterAccount as Account
151
152             this.blocklistService.blockAccountByInstance(account)
153               .subscribe(
154                 () => {
155                   this.notifier.success(
156                     this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
157                   )
158
159                   account.mutedByInstance = true
160                 },
161
162                 err => this.notifier.error(err.message)
163               )
164           }
165         },
166         {
167           label: this.i18n('Mute server'),
168           isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId,
169           handler: async videoAbuse => {
170             this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host)
171               .subscribe(
172                 () => {
173                   this.notifier.success(
174                     this.i18n('Server {{host}} muted by the instance.', { host: videoAbuse.reporterAccount.host })
175                   )
176                 },
177
178                 err => this.notifier.error(err.message)
179               )
180           }
181         }
182       ]
183     ]
184   }
185
186   ngOnInit () {
187     this.initialize()
188   }
189
190   getIdentifier () {
191     return 'VideoAbuseListComponent'
192   }
193
194   openModerationCommentModal (videoAbuse: VideoAbuse) {
195     this.moderationCommentModal.openModal(videoAbuse)
196   }
197
198   onModerationCommentUpdated () {
199     this.loadData()
200   }
201
202   createByString (account: Account) {
203     return Account.CREATE_BY_STRING(account.name, account.host)
204   }
205
206   isVideoAbuseAccepted (videoAbuse: VideoAbuse) {
207     return videoAbuse.state.id === VideoAbuseState.ACCEPTED
208   }
209
210   isVideoAbuseRejected (videoAbuse: VideoAbuse) {
211     return videoAbuse.state.id === VideoAbuseState.REJECTED
212   }
213
214   getVideoUrl (videoAbuse: VideoAbuse) {
215     return Video.buildClientUrl(videoAbuse.video.uuid)
216   }
217
218   getVideoEmbed (videoAbuse: VideoAbuse) {
219     const absoluteAPIUrl = 'http://localhost:9000' || getAbsoluteAPIUrl()
220     const embedUrl = buildVideoLink({
221       baseUrl: absoluteAPIUrl + '/videos/embed/' + videoAbuse.video.uuid,
222       warningTitle: false
223     })
224     return buildVideoEmbed(embedUrl)
225   }
226
227   switchToDefaultAvatar ($event: Event) {
228     ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
229   }
230
231   async removeVideoAbuse (videoAbuse: VideoAbuse) {
232     const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
233     if (res === false) return
234
235     this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe(
236       () => {
237         this.notifier.success(this.i18n('Abuse deleted.'))
238         this.loadData()
239       },
240
241       err => this.notifier.error(err.message)
242     )
243   }
244
245   updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) {
246     this.videoAbuseService.updateVideoAbuse(videoAbuse, { state })
247       .subscribe(
248         () => this.loadData(),
249
250         err => this.notifier.error(err.message)
251       )
252
253   }
254
255   protected loadData () {
256     return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
257                .subscribe(
258                  async resultList => {
259                    this.totalRecords = resultList.total
260
261                    this.videoAbuses = resultList.data
262
263                    for (const abuse of this.videoAbuses) {
264                      Object.assign(abuse, {
265                        reasonHtml: await this.toHtml(abuse.reason),
266                        moderationCommentHtml: await this.toHtml(abuse.moderationComment),
267                        embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)),
268                        reporterAccount: new Account(abuse.reporterAccount)
269                      })
270                    }
271
272                  },
273
274                  err => this.notifier.error(err.message)
275                )
276   }
277
278   private toHtml (text: string) {
279     return this.markdownRenderer.textMarkdownToHTML(text)
280   }
281 }