1 import { SortMeta } from 'primeng/api'
2 import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
3 import { environment } from 'src/environments/environment'
4 import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
5 import { DomSanitizer } from '@angular/platform-browser'
6 import { ActivatedRoute, Params, Router } from '@angular/router'
7 import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
8 import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9 import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
10 import { I18n } from '@ngx-translate/i18n-polyfill'
11 import { Abuse, AbuseState } from '@shared/models'
12 import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
14 export type ProcessedAbuse = Abuse & {
15 moderationCommentHtml?: string,
20 // override bare server-side definitions with rich client-side definitions
21 reporterAccount: Account
23 video: Abuse['video'] & {
24 channel: Abuse['video']['channel'] & {
31 selector: 'my-abuse-list',
32 templateUrl: './abuse-list.component.html',
33 styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ]
35 export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit {
36 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
38 abuses: ProcessedAbuse[] = []
40 sort: SortMeta = { field: 'createdAt', order: 1 }
41 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
43 abuseActions: DropdownAction<Abuse>[][] = []
46 private notifier: Notifier,
47 private abuseService: AbuseService,
48 private blocklistService: BlocklistService,
49 private videoService: VideoService,
50 private videoBlocklistService: VideoBlockService,
51 private confirmService: ConfirmService,
53 private markdownRenderer: MarkdownService,
54 private sanitizer: DomSanitizer,
55 private route: ActivatedRoute,
56 private router: Router
63 label: this.i18n('Internal actions'),
67 label: this.i18n('Delete report'),
68 handler: abuse => this.removeAbuse(abuse)
71 label: this.i18n('Add note'),
72 handler: abuse => this.openModerationCommentModal(abuse),
73 isDisplayed: abuse => !abuse.moderationComment
76 label: this.i18n('Update note'),
77 handler: abuse => this.openModerationCommentModal(abuse),
78 isDisplayed: abuse => !!abuse.moderationComment
81 label: this.i18n('Mark as accepted'),
82 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
83 isDisplayed: abuse => !this.isAbuseAccepted(abuse)
86 label: this.i18n('Mark as rejected'),
87 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
88 isDisplayed: abuse => !this.isAbuseRejected(abuse)
93 label: this.i18n('Actions for the video'),
95 isDisplayed: abuse => !abuse.video.deleted
98 label: this.i18n('Block video'),
99 isDisplayed: abuse => !abuse.video.deleted && !abuse.video.blacklisted,
101 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
104 this.notifier.success(this.i18n('Video blocked.'))
106 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
109 err => this.notifier.error(err.message)
114 label: this.i18n('Unblock video'),
115 isDisplayed: abuse => !abuse.video.deleted && abuse.video.blacklisted,
117 this.videoBlocklistService.unblockVideo(abuse.video.id)
120 this.notifier.success(this.i18n('Video unblocked.'))
122 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
125 err => this.notifier.error(err.message)
130 label: this.i18n('Delete video'),
131 isDisplayed: abuse => !abuse.video.deleted,
132 handler: async abuse => {
133 const res = await this.confirmService.confirm(
134 this.i18n('Do you really want to delete this video?'),
137 if (res === false) return
139 this.videoService.removeVideo(abuse.video.id)
142 this.notifier.success(this.i18n('Video deleted.'))
144 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
147 err => this.notifier.error(err.message)
154 label: this.i18n('Actions for the reporter'),
158 label: this.i18n('Mute reporter'),
159 handler: async abuse => {
160 const account = abuse.reporterAccount as Account
162 this.blocklistService.blockAccountByInstance(account)
165 this.notifier.success(
166 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
169 account.mutedByInstance = true
172 err => this.notifier.error(err.message)
177 label: this.i18n('Mute server'),
178 isDisplayed: abuse => !abuse.reporterAccount.userId,
179 handler: async abuse => {
180 this.blocklistService.blockServerByInstance(abuse.reporterAccount.host)
183 this.notifier.success(
184 this.i18n('Server {{host}} muted by the instance.', { host: abuse.reporterAccount.host })
188 err => this.notifier.error(err.message)
199 this.route.queryParams
200 .subscribe(params => {
201 this.search = params.search || ''
203 this.setTableFilter(this.search)
209 if (this.search) this.setTableFilter(this.search)
213 return 'AbuseListComponent'
216 openModerationCommentModal (abuse: Abuse) {
217 this.moderationCommentModal.openModal(abuse)
220 onModerationCommentUpdated () {
224 /* Table filter functions */
225 onAbuseSearch (event: Event) {
227 this.setQueryParams((event.target as HTMLInputElement).value)
230 setQueryParams (search: string) {
231 const queryParams: Params = {}
232 if (search) Object.assign(queryParams, { search })
234 this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams })
237 resetTableFilter () {
238 this.setTableFilter('')
239 this.setQueryParams('')
242 /* END Table filter functions */
244 isAbuseAccepted (abuse: Abuse) {
245 return abuse.state.id === AbuseState.ACCEPTED
248 isAbuseRejected (abuse: Abuse) {
249 return abuse.state.id === AbuseState.REJECTED
252 getVideoUrl (abuse: Abuse) {
253 return Video.buildClientUrl(abuse.video.uuid)
256 getVideoEmbed (abuse: Abuse) {
257 return buildVideoEmbed(
259 baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
262 startTime: abuse.startAt,
263 stopTime: abuse.endAt
268 switchToDefaultAvatar ($event: Event) {
269 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
272 async removeAbuse (abuse: Abuse) {
273 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
274 if (res === false) return
276 this.abuseService.removeAbuse(abuse).subscribe(
278 this.notifier.success(this.i18n('Abuse deleted.'))
282 err => this.notifier.error(err.message)
286 updateAbuseState (abuse: Abuse, state: AbuseState) {
287 this.abuseService.updateAbuse(abuse, { state })
289 () => this.loadData(),
291 err => this.notifier.error(err.message)
295 protected loadData () {
296 return this.abuseService.getAbuses({
297 pagination: this.pagination,
301 async resultList => {
302 this.totalRecords = resultList.total
305 for (const abuse of resultList.data) {
306 Object.assign(abuse, {
307 reasonHtml: await this.toHtml(abuse.reason),
308 moderationCommentHtml: await this.toHtml(abuse.moderationComment),
309 embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)),
310 reporterAccount: new Account(abuse.reporterAccount)
313 if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
314 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
316 abuses.push(abuse as ProcessedAbuse)
322 err => this.notifier.error(err.message)
326 private toHtml (text: string) {
327 return this.markdownRenderer.textMarkdownToHTML(text)