aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin/moderation/video-abuse-list
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+admin/moderation/video-abuse-list')
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/index.ts2
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html38
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss6
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts70
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html93
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts52
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html149
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss23
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts328
9 files changed, 0 insertions, 761 deletions
diff --git a/client/src/app/+admin/moderation/video-abuse-list/index.ts b/client/src/app/+admin/moderation/video-abuse-list/index.ts
deleted file mode 100644
index da7176e52..000000000
--- a/client/src/app/+admin/moderation/video-abuse-list/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
1export * from './video-abuse-list.component'
2export * from './moderation-comment-modal.component'
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html
deleted file mode 100644
index 8082e93f4..000000000
--- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html
+++ /dev/null
@@ -1,38 +0,0 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Moderation comment</h4>
4
5 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
6 </div>
7
8 <div class="modal-body">
9 <form novalidate [formGroup]="form" (ngSubmit)="banUser()">
10 <div class="form-group">
11 <textarea
12 formControlName="moderationComment" ngbAutofocus i18-placeholder placeholder="Comment this report…"
13 [ngClass]="{ 'input-error': formErrors['moderationComment'] }" class="form-control">
14 </textarea>
15 <div *ngIf="formErrors.moderationComment" class="form-error">
16 {{ formErrors.moderationComment }}
17 </div>
18 </div>
19
20 <div class="form-group" i18n>
21 This comment can only be seen by you or the other moderators.
22 </div>
23
24 <div class="form-group inputs">
25 <input
26 type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
27 (click)="hide()" (key.enter)="hide()"
28 >
29
30 <input
31 type="submit" i18n-value value="Update this comment" class="action-button-submit"
32 [disabled]="!form.valid"
33 >
34 </div>
35 </form>
36 </div>
37
38</ng-template>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss
deleted file mode 100644
index afcdb9a16..000000000
--- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss
+++ /dev/null
@@ -1,6 +0,0 @@
1@import 'variables';
2@import 'mixins';
3
4textarea {
5 @include peertube-textarea(100%, 100px);
6}
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts
deleted file mode 100644
index 3cd763ca4..000000000
--- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts
+++ /dev/null
@@ -1,70 +0,0 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms'
4import { VideoAbuseService } from '@app/shared/shared-moderation'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { VideoAbuse } from '@shared/models'
9
10@Component({
11 selector: 'my-moderation-comment-modal',
12 templateUrl: './moderation-comment-modal.component.html',
13 styleUrls: [ './moderation-comment-modal.component.scss' ]
14})
15export class ModerationCommentModalComponent extends FormReactive implements OnInit {
16 @ViewChild('modal', { static: true }) modal: NgbModal
17 @Output() commentUpdated = new EventEmitter<string>()
18
19 private abuseToComment: VideoAbuse
20 private openedModal: NgbModalRef
21
22 constructor (
23 protected formValidatorService: FormValidatorService,
24 private modalService: NgbModal,
25 private notifier: Notifier,
26 private videoAbuseService: VideoAbuseService,
27 private videoAbuseValidatorsService: VideoAbuseValidatorsService,
28 private i18n: I18n
29 ) {
30 super()
31 }
32
33 ngOnInit () {
34 this.buildForm({
35 moderationComment: this.videoAbuseValidatorsService.VIDEO_ABUSE_MODERATION_COMMENT
36 })
37 }
38
39 openModal (abuseToComment: VideoAbuse) {
40 this.abuseToComment = abuseToComment
41 this.openedModal = this.modalService.open(this.modal, { centered: true })
42
43 this.form.patchValue({
44 moderationComment: this.abuseToComment.moderationComment
45 })
46 }
47
48 hide () {
49 this.abuseToComment = undefined
50 this.openedModal.close()
51 this.form.reset()
52 }
53
54 async banUser () {
55 const moderationComment: string = this.form.value[ 'moderationComment' ]
56
57 this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment })
58 .subscribe(
59 () => {
60 this.notifier.success(this.i18n('Comment updated.'))
61
62 this.commentUpdated.emit(moderationComment)
63 this.hide()
64 },
65
66 err => this.notifier.error(err.message)
67 )
68 }
69
70}
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html
deleted file mode 100644
index ec808cdb8..000000000
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html
+++ /dev/null
@@ -1,93 +0,0 @@
1<div class="d-flex moderation-expanded">
2 <!-- report left part (report details) -->
3 <div class="col-8">
4
5 <!-- report metadata -->
6 <div class="d-flex">
7 <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
8 <span class="col-9 moderation-expanded-text">
9 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" class="chip">
10 <img
11 class="avatar"
12 [src]="videoAbuse.reporterAccount.avatar?.path"
13 (error)="switchToDefaultAvatar($event)"
14 alt="Avatar"
15 >
16 <div>
17 <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
18 </div>
19 </a>
20 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
21 {videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
22 </a>
23 </span>
24 </div>
25
26 <div class="d-flex">
27 <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
28 <span class="col-9 moderation-expanded-text">
29 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" class="chip">
30 <img
31 class="avatar"
32 [src]="videoAbuse.video.channel.ownerAccount?.avatar?.path"
33 (error)="switchToDefaultAvatar($event)"
34 alt="Avatar"
35 >
36 <div>
37 <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? videoAbuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
38 </div>
39 </a>
40 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
41 {videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
42 </a>
43 </span>
44 </div>
45
46 <div class="d-flex" *ngIf="videoAbuse.updatedAt">
47 <span class="col-3 moderation-expanded-label" i18n>Updated</span>
48 <time class="col-9 moderation-expanded-text video-details-date-updated">{{ videoAbuse.updatedAt | date: 'medium' }}</time>
49 </div>
50
51 <!-- report text -->
52 <div class="mt-3 d-flex">
53 <span class="col-3 moderation-expanded-label">
54 <ng-container i18n>Report</ng-container>
55 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': '#' + videoAbuse.id }" class="ml-1 text-muted">#{{ videoAbuse.id }}</a>
56 </span>
57 <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span>
58 </div>
59
60 <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
61 <span class="col-3"></span>
62 <span class="col-9">
63 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()">
64 <div>{{ reason.label }}</div>
65 </a>
66 </span>
67 </div>
68
69 <div *ngIf="videoAbuse.startAt" class="mt-2 d-flex">
70 <span class="col-3 moderation-expanded-label" i18n>Reported part</span>
71 <span class="col-9">
72 {{ startAt }}<ng-container *ngIf="videoAbuse.endAt"> - {{ endAt }}</ng-container>
73 </span>
74 </div>
75
76 <div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment">
77 <span class="col-3 moderation-expanded-label" i18n>Note</span>
78 <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
79 </div>
80
81 </div>
82
83 <!-- report right part (video details) -->
84 <div class="col-4">
85 <div class="screenratio">
86 <div *ngIf="videoAbuse.video.deleted || videoAbuse.video.blacklisted">
87 <span i18n *ngIf="videoAbuse.video.deleted">The video was deleted</span>
88 <span i18n *ngIf="!videoAbuse.video.deleted">The video was blocked</span>
89 </div>
90 <div *ngIf="!videoAbuse.video.deleted && !videoAbuse.video.blacklisted" [innerHTML]="videoAbuse.embedHtml"></div>
91 </div>
92 </div>
93</div>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts
deleted file mode 100644
index 5db2887fa..000000000
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts
+++ /dev/null
@@ -1,52 +0,0 @@
1import { Component, Input } from '@angular/core'
2import { Actor } from '@app/shared/shared-main'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model'
5import { ProcessedVideoAbuse } from './video-abuse-list.component'
6import { durationToString } from '@app/helpers'
7
8@Component({
9 selector: 'my-video-abuse-details',
10 templateUrl: './video-abuse-details.component.html',
11 styleUrls: [ '../moderation.component.scss' ]
12})
13export class VideoAbuseDetailsComponent {
14 @Input() videoAbuse: ProcessedVideoAbuse
15
16 private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string }
17
18 constructor (
19 private i18n: I18n
20 ) {
21 this.predefinedReasonsTranslations = {
22 violentOrRepulsive: this.i18n('Violent or Repulsive'),
23 hatefulOrAbusive: this.i18n('Hateful or Abusive'),
24 spamOrMisleading: this.i18n('Spam or Misleading'),
25 privacy: this.i18n('Privacy'),
26 rights: this.i18n('Rights'),
27 serverRules: this.i18n('Server rules'),
28 thumbnails: this.i18n('Thumbnails'),
29 captions: this.i18n('Captions')
30 }
31 }
32
33 get startAt () {
34 return durationToString(this.videoAbuse.startAt)
35 }
36
37 get endAt () {
38 return durationToString(this.videoAbuse.endAt)
39 }
40
41 getPredefinedReasons () {
42 if (!this.videoAbuse.predefinedReasons) return []
43 return this.videoAbuse.predefinedReasons.map(r => ({
44 id: r,
45 label: this.predefinedReasonsTranslations[r]
46 }))
47 }
48
49 switchToDefaultAvatar ($event: Event) {
50 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
51 }
52}
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
deleted file mode 100644
index 64641b28a..000000000
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
+++ /dev/null
@@ -1,149 +0,0 @@
1<p-table
2 [value]="videoAbuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
6 (onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
7>
8 <ng-template pTemplate="caption">
9 <div class="caption">
10 <div class="ml-auto">
11 <div class="input-group has-feedback has-clear">
12 <div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
13 <div class="input-group-text" ngbDropdownToggle>
14 <span class="caret" aria-haspopup="menu" role="button"></span>
15 </div>
16
17 <div role="menu" ngbDropdownMenu>
18 <h6 class="dropdown-header" i18n>Advanced report filters</h6>
19 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
20 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
21 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
22 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
23 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
24 </div>
25 </div>
26 <input
27 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
28 (keyup)="onAbuseSearch($event)"
29 >
30 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
31 <span class="sr-only" i18n>Clear filters</span>
32 </div>
33 </div>
34 </div>
35 </ng-template>
36
37 <ng-template pTemplate="header">
38 <tr> <!-- header -->
39 <th style="width: 40px;"></th>
40 <th style="width: 20%;" pResizableColumn i18n>Reporter</th>
41 <th i18n>Video</th>
42 <th style="width: 150px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
43 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
44 <th style="width: 150px;"></th>
45 </tr>
46 </ng-template>
47
48 <ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse>
49 <tr>
50 <td class="c-hand" [pRowToggler]="videoAbuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
51 <span class="expander">
52 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
53 </span>
54 </td>
55
56 <td>
57 <a [href]="videoAbuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
58 <div class="chip two-lines">
59 <img
60 class="avatar"
61 [src]="videoAbuse.reporterAccount.avatar?.path"
62 (error)="switchToDefaultAvatar($event)"
63 alt="Avatar"
64 >
65 <div>
66 {{ videoAbuse.reporterAccount.displayName }}
67 <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
68 </div>
69 </div>
70 </a>
71 </td>
72
73 <td *ngIf="!videoAbuse.video.deleted">
74 <a [href]="getVideoUrl(videoAbuse)" class="video-table-video-link" [title]="videoAbuse.video.name" target="_blank" rel="noopener noreferrer">
75 <div class="video-table-video">
76 <div class="video-table-video-image">
77 <img [src]="videoAbuse.video.thumbnailPath">
78 <span
79 class="video-table-video-image-label" *ngIf="videoAbuse.count > 1"
80 i18n-title title="This video has been reported multiple times."
81 >
82 {{ videoAbuse.nth }}/{{ videoAbuse.count }}
83 </span>
84 </div>
85 <div class="video-table-video-text">
86 <div>
87 <span *ngIf="!videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
88 <span *ngIf="videoAbuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
89 {{ videoAbuse.video.name }}
90 </div>
91 <div class="text-muted" i18n>by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div>
92 </div>
93 </div>
94 </a>
95 </td>
96
97 <td *ngIf="videoAbuse.video.deleted" class="c-hand" [pRowToggler]="videoAbuse">
98 <div class="video-table-video" i18n-title title="Video was deleted">
99 <div class="video-table-video-image">
100 <span i18n>Deleted</span>
101 </div>
102 <div class="video-table-video-text">
103 <div>
104 {{ videoAbuse.video.name }}
105 <span class="glyphicon glyphicon-trash"></span>
106 </div>
107 <div class="text-muted" i18n>by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div>
108 </div>
109 </div>
110 </td>
111
112 <td class="c-hand" [pRowToggler]="videoAbuse">{{ videoAbuse.createdAt | date: 'short' }}</td>
113
114 <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse">
115 <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
116 <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
117 <span *ngIf="videoAbuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
118 </td>
119
120 <td class="action-cell">
121 <my-action-dropdown
122 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
123 i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"
124 ></my-action-dropdown>
125 </td>
126 </tr>
127 </ng-template>
128
129 <ng-template pTemplate="rowexpansion" let-videoAbuse>
130 <tr>
131 <td class="expand-cell" colspan="6">
132 <my-video-abuse-details [videoAbuse]="videoAbuse"></my-video-abuse-details>
133 </td>
134 </tr>
135 </ng-template>
136
137 <ng-template pTemplate="emptymessage">
138 <tr>
139 <td colspan="6">
140 <div class="no-results">
141 <ng-container *ngIf="search" i18n>No video abuses found matching current filters.</ng-container>
142 <ng-container *ngIf="!search" i18n>No video abuses found.</ng-container>
143 </div>
144 </td>
145 </tr>
146 </ng-template>
147</p-table>
148
149<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
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
deleted file mode 100644
index 8eee15b64..000000000
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss
+++ /dev/null
@@ -1,23 +0,0 @@
1@import 'mixins';
2@import 'miniature';
3
4.video-details-date-updated {
5 font-size: 90%;
6 margin-top: .1rem;
7}
8
9.video-details-links {
10 @include disable-default-a-behaviour;
11}
12
13.video-abuse-states .glyphicon-comment {
14 margin-left: 0.5rem;
15}
16
17.input-group {
18 @include peertube-input-group(300px);
19
20 .dropdown-toggle::after {
21 margin-left: 0;
22 }
23}
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
deleted file mode 100644
index 409dd42c7..000000000
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
+++ /dev/null
@@ -1,328 +0,0 @@
1import { SortMeta } from 'primeng/api'
2import { filter } from 'rxjs/operators'
3import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
4import { environment } from 'src/environments/environment'
5import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
6import { DomSanitizer } from '@angular/platform-browser'
7import { ActivatedRoute, Params, Router } from '@angular/router'
8import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
9import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
10import { BlocklistService, VideoAbuseService, VideoBlockService } from '@app/shared/shared-moderation'
11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { VideoAbuse, VideoAbuseState } from '@shared/models'
13import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
14
15export type ProcessedVideoAbuse = VideoAbuse & {
16 moderationCommentHtml?: string,
17 reasonHtml?: string
18 embedHtml?: string
19 updatedAt?: Date
20 // override bare server-side definitions with rich client-side definitions
21 reporterAccount: Account
22 video: VideoAbuse['video'] & {
23 channel: VideoAbuse['video']['channel'] & {
24 ownerAccount: Account
25 }
26 }
27}
28
29@Component({
30 selector: 'my-video-abuse-list',
31 templateUrl: './video-abuse-list.component.html',
32 styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ]
33})
34export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit {
35 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
36
37 videoAbuses: ProcessedVideoAbuse[] = []
38 totalRecords = 0
39 sort: SortMeta = { field: 'createdAt', order: 1 }
40 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
41
42 videoAbuseActions: DropdownAction<VideoAbuse>[][] = []
43
44 constructor (
45 private notifier: Notifier,
46 private videoAbuseService: VideoAbuseService,
47 private blocklistService: BlocklistService,
48 private videoService: VideoService,
49 private videoBlocklistService: VideoBlockService,
50 private confirmService: ConfirmService,
51 private i18n: I18n,
52 private markdownRenderer: MarkdownService,
53 private sanitizer: DomSanitizer,
54 private route: ActivatedRoute,
55 private router: Router
56 ) {
57 super()
58
59 this.videoAbuseActions = [
60 [
61 {
62 label: this.i18n('Internal actions'),
63 isHeader: true
64 },
65 {
66 label: this.i18n('Delete report'),
67 handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
68 },
69 {
70 label: this.i18n('Add note'),
71 handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
72 isDisplayed: videoAbuse => !videoAbuse.moderationComment
73 },
74 {
75 label: this.i18n('Update note'),
76 handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
77 isDisplayed: videoAbuse => !!videoAbuse.moderationComment
78 },
79 {
80 label: this.i18n('Mark as accepted'),
81 handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED),
82 isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse)
83 },
84 {
85 label: this.i18n('Mark as rejected'),
86 handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED),
87 isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse)
88 }
89 ],
90 [
91 {
92 label: this.i18n('Actions for the video'),
93 isHeader: true,
94 isDisplayed: videoAbuse => !videoAbuse.video.deleted
95 },
96 {
97 label: this.i18n('Block video'),
98 isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted,
99 handler: videoAbuse => {
100 this.videoBlocklistService.blockVideo(videoAbuse.video.id, undefined, true)
101 .subscribe(
102 () => {
103 this.notifier.success(this.i18n('Video blocked.'))
104
105 this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
106 },
107
108 err => this.notifier.error(err.message)
109 )
110 }
111 },
112 {
113 label: this.i18n('Unblock video'),
114 isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted,
115 handler: videoAbuse => {
116 this.videoBlocklistService.unblockVideo(videoAbuse.video.id)
117 .subscribe(
118 () => {
119 this.notifier.success(this.i18n('Video unblocked.'))
120
121 this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
122 },
123
124 err => this.notifier.error(err.message)
125 )
126 }
127 },
128 {
129 label: this.i18n('Delete video'),
130 isDisplayed: videoAbuse => !videoAbuse.video.deleted,
131 handler: async videoAbuse => {
132 const res = await this.confirmService.confirm(
133 this.i18n('Do you really want to delete this video?'),
134 this.i18n('Delete')
135 )
136 if (res === false) return
137
138 this.videoService.removeVideo(videoAbuse.video.id)
139 .subscribe(
140 () => {
141 this.notifier.success(this.i18n('Video deleted.'))
142
143 this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
144 },
145
146 err => this.notifier.error(err.message)
147 )
148 }
149 }
150 ],
151 [
152 {
153 label: this.i18n('Actions for the reporter'),
154 isHeader: true
155 },
156 {
157 label: this.i18n('Mute reporter'),
158 handler: async videoAbuse => {
159 const account = videoAbuse.reporterAccount as Account
160
161 this.blocklistService.blockAccountByInstance(account)
162 .subscribe(
163 () => {
164 this.notifier.success(
165 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
166 )
167
168 account.mutedByInstance = true
169 },
170
171 err => this.notifier.error(err.message)
172 )
173 }
174 },
175 {
176 label: this.i18n('Mute server'),
177 isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId,
178 handler: async videoAbuse => {
179 this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host)
180 .subscribe(
181 () => {
182 this.notifier.success(
183 this.i18n('Server {{host}} muted by the instance.', { host: videoAbuse.reporterAccount.host })
184 )
185 },
186
187 err => this.notifier.error(err.message)
188 )
189 }
190 }
191 ]
192 ]
193 }
194
195 ngOnInit () {
196 this.initialize()
197
198 this.route.queryParams
199 .subscribe(params => {
200 this.search = params.search || ''
201
202 this.setTableFilter(this.search)
203 this.loadData()
204 })
205 }
206
207 ngAfterViewInit () {
208 if (this.search) this.setTableFilter(this.search)
209 }
210
211 getIdentifier () {
212 return 'VideoAbuseListComponent'
213 }
214
215 openModerationCommentModal (videoAbuse: VideoAbuse) {
216 this.moderationCommentModal.openModal(videoAbuse)
217 }
218
219 onModerationCommentUpdated () {
220 this.loadData()
221 }
222
223 /* Table filter functions */
224 onAbuseSearch (event: Event) {
225 this.onSearch(event)
226 this.setQueryParams((event.target as HTMLInputElement).value)
227 }
228
229 setQueryParams (search: string) {
230 const queryParams: Params = {}
231 if (search) Object.assign(queryParams, { search })
232
233 this.router.navigate([ '/admin/moderation/video-abuses/list' ], { queryParams })
234 }
235
236 resetTableFilter () {
237 this.setTableFilter('')
238 this.setQueryParams('')
239 this.resetSearch()
240 }
241 /* END Table filter functions */
242
243 isVideoAbuseAccepted (videoAbuse: VideoAbuse) {
244 return videoAbuse.state.id === VideoAbuseState.ACCEPTED
245 }
246
247 isVideoAbuseRejected (videoAbuse: VideoAbuse) {
248 return videoAbuse.state.id === VideoAbuseState.REJECTED
249 }
250
251 getVideoUrl (videoAbuse: VideoAbuse) {
252 return Video.buildClientUrl(videoAbuse.video.uuid)
253 }
254
255 getVideoEmbed (videoAbuse: VideoAbuse) {
256 return buildVideoEmbed(
257 buildVideoLink({
258 baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`,
259 title: false,
260 warningTitle: false,
261 startTime: videoAbuse.startAt,
262 stopTime: videoAbuse.endAt
263 })
264 )
265 }
266
267 switchToDefaultAvatar ($event: Event) {
268 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
269 }
270
271 async removeVideoAbuse (videoAbuse: VideoAbuse) {
272 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
273 if (res === false) return
274
275 this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe(
276 () => {
277 this.notifier.success(this.i18n('Abuse deleted.'))
278 this.loadData()
279 },
280
281 err => this.notifier.error(err.message)
282 )
283 }
284
285 updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) {
286 this.videoAbuseService.updateVideoAbuse(videoAbuse, { state })
287 .subscribe(
288 () => this.loadData(),
289
290 err => this.notifier.error(err.message)
291 )
292 }
293
294 protected loadData () {
295 return this.videoAbuseService.getVideoAbuses({
296 pagination: this.pagination,
297 sort: this.sort,
298 search: this.search
299 }).subscribe(
300 async resultList => {
301 this.totalRecords = resultList.total
302 const videoAbuses = []
303
304 for (const abuse of resultList.data) {
305 Object.assign(abuse, {
306 reasonHtml: await this.toHtml(abuse.reason),
307 moderationCommentHtml: await this.toHtml(abuse.moderationComment),
308 embedHtml: this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse)),
309 reporterAccount: new Account(abuse.reporterAccount)
310 })
311
312 if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
313 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
314
315 videoAbuses.push(abuse as ProcessedVideoAbuse)
316 }
317
318 this.videoAbuses = videoAbuses
319 },
320
321 err => this.notifier.error(err.message)
322 )
323 }
324
325 private toHtml (text: string) {
326 return this.markdownRenderer.textMarkdownToHTML(text)
327 }
328}