aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+admin')
-rw-r--r--client/src/app/+admin/admin.component.ts12
-rw-r--r--client/src/app/+admin/admin.module.ts10
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-details.component.html115
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts (renamed from client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts)23
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.html184
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss (renamed from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss)2
-rw-r--r--client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts454
-rw-r--r--client/src/app/+admin/moderation/abuse-list/index.ts3
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html (renamed from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html)0
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss (renamed from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss)0
-rw-r--r--client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts (renamed from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts)18
-rw-r--r--client/src/app/+admin/moderation/index.ts2
-rw-r--r--client/src/app/+admin/moderation/moderation.component.scss48
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts17
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/index.ts2
-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-list.component.html149
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts328
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html8
19 files changed, 845 insertions, 623 deletions
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts
index 6f340884f..4345d1945 100644
--- a/client/src/app/+admin/admin.component.ts
+++ b/client/src/app/+admin/admin.component.ts
@@ -45,10 +45,10 @@ export class AdminComponent implements OnInit {
45 children: [] 45 children: []
46 } 46 }
47 47
48 if (this.hasVideoAbusesRight()) { 48 if (this.hasAbusesRight()) {
49 moderationItems.children.push({ 49 moderationItems.children.push({
50 label: this.i18n('Video reports'), 50 label: this.i18n('Reports'),
51 routerLink: '/admin/moderation/video-abuses/list', 51 routerLink: '/admin/moderation/abuses/list',
52 iconName: 'flag' 52 iconName: 'flag'
53 }) 53 })
54 } 54 }
@@ -76,7 +76,7 @@ export class AdminComponent implements OnInit {
76 76
77 if (this.hasUsersRight()) this.menuEntries.push({ label: this.i18n('Users'), routerLink: '/admin/users' }) 77 if (this.hasUsersRight()) this.menuEntries.push({ label: this.i18n('Users'), routerLink: '/admin/users' })
78 if (this.hasServerFollowRight()) this.menuEntries.push(federationItems) 78 if (this.hasServerFollowRight()) this.menuEntries.push(federationItems)
79 if (this.hasVideoAbusesRight() || this.hasVideoBlocklistRight()) this.menuEntries.push(moderationItems) 79 if (this.hasAbusesRight() || this.hasVideoBlocklistRight()) this.menuEntries.push(moderationItems)
80 if (this.hasConfigRight()) this.menuEntries.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' }) 80 if (this.hasConfigRight()) this.menuEntries.push({ label: this.i18n('Configuration'), routerLink: '/admin/config' })
81 if (this.hasPluginsRight()) this.menuEntries.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' }) 81 if (this.hasPluginsRight()) this.menuEntries.push({ label: this.i18n('Plugins/Themes'), routerLink: '/admin/plugins' })
82 if (this.hasJobsRight() || this.hasLogsRight() || this.hasDebugRight()) this.menuEntries.push({ label: this.i18n('System'), routerLink: '/admin/system' }) 82 if (this.hasJobsRight() || this.hasLogsRight() || this.hasDebugRight()) this.menuEntries.push({ label: this.i18n('System'), routerLink: '/admin/system' })
@@ -90,8 +90,8 @@ export class AdminComponent implements OnInit {
90 return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW) 90 return this.auth.getUser().hasRight(UserRight.MANAGE_SERVER_FOLLOW)
91 } 91 }
92 92
93 hasVideoAbusesRight () { 93 hasAbusesRight () {
94 return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) 94 return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES)
95 } 95 }
96 96
97 hasVideoBlocklistRight () { 97 hasVideoBlocklistRight () {
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 728227a84..c59bd2927 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -14,10 +14,10 @@ import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponen
14import { FollowingListComponent } from './follows/following-list/following-list.component' 14import { FollowingListComponent } from './follows/following-list/following-list.component'
15import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' 15import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
16import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' 16import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
17import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlockListComponent } from './moderation' 17import { ModerationCommentModalComponent, AbuseListComponent, VideoBlockListComponent } from './moderation'
18import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' 18import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
19import { ModerationComponent } from './moderation/moderation.component' 19import { ModerationComponent } from './moderation/moderation.component'
20import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-abuse-details.component' 20import { AbuseDetailsComponent } from './moderation/abuse-list/abuse-details.component'
21import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' 21import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component'
22import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' 22import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
23import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' 23import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component'
@@ -60,8 +60,10 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
60 60
61 ModerationComponent, 61 ModerationComponent,
62 VideoBlockListComponent, 62 VideoBlockListComponent,
63 VideoAbuseListComponent, 63
64 VideoAbuseDetailsComponent, 64 AbuseListComponent,
65 AbuseDetailsComponent,
66
65 ModerationCommentModalComponent, 67 ModerationCommentModalComponent,
66 InstanceServerBlocklistComponent, 68 InstanceServerBlocklistComponent,
67 InstanceAccountBlocklistComponent, 69 InstanceAccountBlocklistComponent,
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html
new file mode 100644
index 000000000..cba9cfb73
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html
@@ -0,0 +1,115 @@
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" *ngIf="abuse.reporterAccount">
7 <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
8
9 <span class="col-9 moderation-expanded-text">
10 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
11 class="chip"
12 >
13 <img
14 class="avatar"
15 [src]="abuse.reporterAccount.avatar?.path"
16 (error)="switchToDefaultAvatar($event)"
17 alt="Avatar"
18 >
19 <div>
20 <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
21 </div>
22 </a>
23
24 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }"
25 class="ml-auto text-muted abuse-details-links" i18n
26 >
27 {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
28 </a>
29 </span>
30 </div>
31
32 <div class="d-flex" *ngIf="abuse.flaggedAccount">
33 <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
34 <span class="col-9 moderation-expanded-text">
35 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
36 class="chip"
37 >
38 <img
39 class="avatar"
40 [src]="abuse.flaggedAccount?.avatar?.path"
41 (error)="switchToDefaultAvatar($event)"
42 alt="Avatar"
43 >
44 <div>
45 <span class="text-muted">{{ abuse.flaggedAccount ? abuse.flaggedAccount.nameWithHost : '' }}</span>
46 </div>
47 </a>
48
49 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.flaggedAccount.displayName + '&quot;' }"
50 class="ml-auto text-muted abuse-details-links" i18n
51 >
52 {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
53 </a>
54 </span>
55 </div>
56
57 <div class="d-flex" *ngIf="abuse.updatedAt">
58 <span class="col-3 moderation-expanded-label" i18n>Updated</span>
59 <time class="col-9 moderation-expanded-text abuse-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time>
60 </div>
61
62 <!-- report text -->
63 <div class="mt-3 d-flex">
64 <span class="col-3 moderation-expanded-label">
65 <ng-container i18n>Report</ng-container>
66 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
67 </span>
68 <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
69 </div>
70
71 <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
72 <span class="col-3"></span>
73 <span class="col-9">
74 <a *ngFor="let reason of getPredefinedReasons()" [routerLink]="[ '/admin/moderation/abuses/list' ]"
75 [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light"
76 >
77 <div>{{ reason.label }}</div>
78 </a>
79 </span>
80 </div>
81
82 <div *ngIf="abuse.video?.startAt" class="mt-2 d-flex">
83 <span class="col-3 moderation-expanded-label" i18n>Reported part</span>
84 <span class="col-9">
85 {{ startAt }}<ng-container *ngIf="abuse.video.endAt"> - {{ endAt }}</ng-container>
86 </span>
87 </div>
88
89 <div class="mt-3 d-flex" *ngIf="abuse.moderationComment">
90 <span class="col-3 moderation-expanded-label" i18n>Note</span>
91 <span class="col-9 moderation-expanded-text d-block" [innerHTML]="abuse.moderationCommentHtml"></span>
92 </div>
93
94 </div>
95
96 <!-- report right part (video/comment details) -->
97 <div class="col-4">
98 <div *ngIf="abuse.video" class="screenratio">
99 <div>
100 <span i18n *ngIf="abuse.video.deleted">The video was deleted</span>
101 <span i18n *ngIf="!abuse.video.deleted">The video was blocked</span>
102 </div>
103
104 <div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div>
105 </div>
106
107 <div *ngIf="abuse.comment" class="comment-html">
108 <div>
109 <strong i18n>Comment:</strong>
110 </div>
111
112 <div [innerHTML]="abuse.commentHtml"></div>
113 </div>
114 </div>
115</div>
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts
index 5db2887fa..fb0f65764 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts
@@ -1,19 +1,19 @@
1import { Component, Input } from '@angular/core' 1import { Component, Input } from '@angular/core'
2import { Actor } from '@app/shared/shared-main' 2import { Actor } from '@app/shared/shared-main'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model' 4import { AbusePredefinedReasonsString } from '@shared/models'
5import { ProcessedVideoAbuse } from './video-abuse-list.component' 5import { ProcessedAbuse } from './abuse-list.component'
6import { durationToString } from '@app/helpers' 6import { durationToString } from '@app/helpers'
7 7
8@Component({ 8@Component({
9 selector: 'my-video-abuse-details', 9 selector: 'my-abuse-details',
10 templateUrl: './video-abuse-details.component.html', 10 templateUrl: './abuse-details.component.html',
11 styleUrls: [ '../moderation.component.scss' ] 11 styleUrls: [ '../moderation.component.scss' ]
12}) 12})
13export class VideoAbuseDetailsComponent { 13export class AbuseDetailsComponent {
14 @Input() videoAbuse: ProcessedVideoAbuse 14 @Input() abuse: ProcessedAbuse
15 15
16 private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string } 16 private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
17 17
18 constructor ( 18 constructor (
19 private i18n: I18n 19 private i18n: I18n
@@ -31,16 +31,17 @@ export class VideoAbuseDetailsComponent {
31 } 31 }
32 32
33 get startAt () { 33 get startAt () {
34 return durationToString(this.videoAbuse.startAt) 34 return durationToString(this.abuse.video.startAt)
35 } 35 }
36 36
37 get endAt () { 37 get endAt () {
38 return durationToString(this.videoAbuse.endAt) 38 return durationToString(this.abuse.video.endAt)
39 } 39 }
40 40
41 getPredefinedReasons () { 41 getPredefinedReasons () {
42 if (!this.videoAbuse.predefinedReasons) return [] 42 if (!this.abuse.predefinedReasons) return []
43 return this.videoAbuse.predefinedReasons.map(r => ({ 43
44 return this.abuse.predefinedReasons.map(r => ({
44 id: r, 45 id: r,
45 label: this.predefinedReasonsTranslations[r] 46 label: this.predefinedReasonsTranslations[r]
46 })) 47 }))
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
new file mode 100644
index 000000000..99502304d
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
@@ -0,0 +1,184 @@
1<p-table
2 [value]="abuses" [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" [lazyLoadOnInit]="false"
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/abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
20 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
21 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
22 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
23 <a [routerLink]="[ '/admin/moderation/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/Comment/Account</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-abuse>
49 <tr>
50 <td class="c-hand" [pRowToggler]="abuse" 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 *ngIf="abuse.reporterAccount" [href]="abuse.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]="abuse.reporterAccount.avatar?.path"
62 (error)="switchToDefaultAvatar($event)"
63 alt="Avatar"
64 >
65 <div>
66 {{ abuse.reporterAccount.displayName }}
67 <span>{{ abuse.reporterAccount.nameWithHost }}</span>
68 </div>
69 </div>
70 </a>
71
72 <span i18n *ngIf="!abuse.reporterAccount">
73 Deleted account
74 </span>
75 </td>
76
77 <ng-container *ngIf="abuse.video">
78
79 <td *ngIf="!abuse.video.deleted">
80 <a [href]="getVideoUrl(abuse)" class="table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
81 <div class="table-video">
82 <div class="table-video-image">
83 <img [src]="abuse.video.thumbnailPath">
84 <span
85 class="table-video-image-label" *ngIf="abuse.count > 1"
86 i18n-title title="This video has been reported multiple times."
87 >
88 {{ abuse.nth }}/{{ abuse.count }}
89 </span>
90 </div>
91
92 <div class="table-video-text">
93 <div>
94 <span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
95 <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
96 {{ abuse.video.name }}
97 </div>
98 <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
99 </div>
100 </div>
101 </a>
102 </td>
103
104 <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">
105 <div class="table-video" i18n-title title="Video was deleted">
106 <div class="table-video-image">
107 <span i18n>Deleted</span>
108 </div>
109
110 <div class="table-video-text">
111 <div>
112 {{ abuse.video.name }}
113 <span class="glyphicon glyphicon-trash"></span>
114 </div>
115 <div i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
116 </div>
117 </div>
118 </td>
119 </ng-container>
120
121 <ng-container *ngIf="abuse.comment">
122 <td>
123 <a [href]="getCommentUrl(abuse)" [innerHTML]="abuse.truncatedCommentHtml" class="table-comment-link"
124 [title]="abuse.comment.video.name" target="_blank" rel="noopener noreferrer"
125 ></a>
126
127 <div class="comment-flagged-account" *ngIf="abuse.flaggedAccount">by {{ abuse.flaggedAccount.displayName }}</div>
128 </td>
129 </ng-container>
130
131 <ng-container *ngIf="!abuse.comment && !abuse.video">
132 <td *ngIf="abuse.flaggedAccount">
133 <a [href]="getAccountUrl(abuse)" class="table-account-link" target="_blank" rel="noopener noreferrer">
134 <span>{{ abuse.flaggedAccount.displayName }}</span>
135
136 <span class="account-flagged-handle">{{ abuse.flaggedAccount.nameWithHostForced }}</span>
137 </a>
138 </td>
139
140 <td i18n *ngIf="!abuse.flaggedAccount">
141 Account deleted
142 </td>
143
144 </ng-container>
145
146
147 <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
148
149 <td class="c-hand abuse-states" [pRowToggler]="abuse">
150 <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
151 <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
152 <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
153 </td>
154
155 <td class="action-cell">
156 <my-action-dropdown
157 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
158 i18n-label label="Actions" [actions]="abuseActions" [entry]="abuse"
159 ></my-action-dropdown>
160 </td>
161 </tr>
162 </ng-template>
163
164 <ng-template pTemplate="rowexpansion" let-abuse>
165 <tr>
166 <td class="expand-cell" colspan="6">
167 <my-abuse-details [abuse]="abuse"></my-abuse-details>
168 </td>
169 </tr>
170 </ng-template>
171
172 <ng-template pTemplate="emptymessage">
173 <tr>
174 <td colspan="6">
175 <div class="no-results">
176 <ng-container *ngIf="search" i18n>No abuses found matching current filters.</ng-container>
177 <ng-container *ngIf="!search" i18n>No abuses found.</ng-container>
178 </div>
179 </td>
180 </tr>
181 </ng-template>
182</p-table>
183
184<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/abuse-list/abuse-list.component.scss
index 8eee15b64..c22f98c47 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss
@@ -10,7 +10,7 @@
10 @include disable-default-a-behaviour; 10 @include disable-default-a-behaviour;
11} 11}
12 12
13.video-abuse-states .glyphicon-comment { 13.abuse-states .glyphicon-comment {
14 margin-left: 0.5rem; 14 margin-left: 0.5rem;
15} 15}
16 16
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
new file mode 100644
index 000000000..74c5fe2b3
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
@@ -0,0 +1,454 @@
1import * as debug from 'debug'
2import truncate from 'lodash-es/truncate'
3import { SortMeta } from 'primeng/api'
4import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
5import { environment } from 'src/environments/environment'
6import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
7import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
8import { ActivatedRoute, Params, Router } from '@angular/router'
9import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
10import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
11import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
12import { VideoCommentService } from '@app/shared/shared-video-comment'
13import { I18n } from '@ngx-translate/i18n-polyfill'
14import { Abuse, AbuseState } from '@shared/models'
15import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
16
17const logger = debug('peertube:moderation:AbuseListComponent')
18
19// Don't use an abuse model because we need external services to compute some properties
20// And this model is only used in this component
21export type ProcessedAbuse = Abuse & {
22 moderationCommentHtml?: string,
23 reasonHtml?: string
24 embedHtml?: SafeHtml
25 updatedAt?: Date
26
27 // override bare server-side definitions with rich client-side definitions
28 reporterAccount?: Account
29 flaggedAccount?: Account
30
31 truncatedCommentHtml?: string
32 commentHtml?: string
33
34 video: Abuse['video'] & {
35 channel: Abuse['video']['channel'] & {
36 ownerAccount: Account
37 }
38 }
39}
40
41@Component({
42 selector: 'my-abuse-list',
43 templateUrl: './abuse-list.component.html',
44 styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ]
45})
46export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit {
47 @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
48
49 abuses: ProcessedAbuse[] = []
50 totalRecords = 0
51 sort: SortMeta = { field: 'createdAt', order: 1 }
52 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
53
54 abuseActions: DropdownAction<ProcessedAbuse>[][] = []
55
56 constructor (
57 private notifier: Notifier,
58 private abuseService: AbuseService,
59 private blocklistService: BlocklistService,
60 private commentService: VideoCommentService,
61 private videoService: VideoService,
62 private videoBlocklistService: VideoBlockService,
63 private confirmService: ConfirmService,
64 private i18n: I18n,
65 private markdownRenderer: MarkdownService,
66 private sanitizer: DomSanitizer,
67 private route: ActivatedRoute,
68 private router: Router
69 ) {
70 super()
71
72 this.abuseActions = [
73 this.buildInternalActions(),
74
75 this.buildFlaggedAccountActions(),
76
77 this.buildCommentActions(),
78
79 this.buildVideoActions(),
80
81 this.buildAccountActions()
82 ]
83 }
84
85 ngOnInit () {
86 this.initialize()
87
88 this.route.queryParams
89 .subscribe(params => {
90 this.search = params.search || ''
91
92 logger('On URL change (search: %s).', this.search)
93
94 this.setTableFilter(this.search)
95 this.loadData()
96 })
97 }
98
99 ngAfterViewInit () {
100 if (this.search) this.setTableFilter(this.search)
101 }
102
103 getIdentifier () {
104 return 'AbuseListComponent'
105 }
106
107 openModerationCommentModal (abuse: Abuse) {
108 this.moderationCommentModal.openModal(abuse)
109 }
110
111 onModerationCommentUpdated () {
112 this.loadData()
113 }
114
115 /* Table filter functions */
116 onAbuseSearch (event: Event) {
117 this.onSearch(event)
118 this.setQueryParams((event.target as HTMLInputElement).value)
119 }
120
121 setQueryParams (search: string) {
122 const queryParams: Params = {}
123 if (search) Object.assign(queryParams, { search })
124
125 this.router.navigate([ '/admin/moderation/abuses/list' ], { queryParams })
126 }
127
128 resetTableFilter () {
129 this.setTableFilter('')
130 this.setQueryParams('')
131 this.resetSearch()
132 }
133 /* END Table filter functions */
134
135 isAbuseAccepted (abuse: Abuse) {
136 return abuse.state.id === AbuseState.ACCEPTED
137 }
138
139 isAbuseRejected (abuse: Abuse) {
140 return abuse.state.id === AbuseState.REJECTED
141 }
142
143 getVideoUrl (abuse: Abuse) {
144 return Video.buildClientUrl(abuse.video.uuid)
145 }
146
147 getCommentUrl (abuse: Abuse) {
148 return Video.buildClientUrl(abuse.comment.video.uuid) + ';threadId=' + abuse.comment.threadId
149 }
150
151 getAccountUrl (abuse: ProcessedAbuse) {
152 return '/accounts/' + abuse.flaggedAccount.nameWithHost
153 }
154
155 getVideoEmbed (abuse: Abuse) {
156 return buildVideoEmbed(
157 buildVideoLink({
158 baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
159 title: false,
160 warningTitle: false,
161 startTime: abuse.startAt,
162 stopTime: abuse.endAt
163 })
164 )
165 }
166
167 switchToDefaultAvatar ($event: Event) {
168 ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
169 }
170
171 async removeAbuse (abuse: Abuse) {
172 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
173 if (res === false) return
174
175 this.abuseService.removeAbuse(abuse).subscribe(
176 () => {
177 this.notifier.success(this.i18n('Abuse deleted.'))
178 this.loadData()
179 },
180
181 err => this.notifier.error(err.message)
182 )
183 }
184
185 updateAbuseState (abuse: Abuse, state: AbuseState) {
186 this.abuseService.updateAbuse(abuse, { state })
187 .subscribe(
188 () => this.loadData(),
189
190 err => this.notifier.error(err.message)
191 )
192 }
193
194 protected loadData () {
195 logger('Load data.')
196
197 return this.abuseService.getAbuses({
198 pagination: this.pagination,
199 sort: this.sort,
200 search: this.search
201 }).subscribe(
202 async resultList => {
203 this.totalRecords = resultList.total
204
205 this.abuses = []
206
207 for (const a of resultList.data) {
208 const abuse = a as ProcessedAbuse
209
210 abuse.reasonHtml = await this.toHtml(abuse.reason)
211 abuse.moderationCommentHtml = await this.toHtml(abuse.moderationComment)
212
213 if (abuse.video) {
214 abuse.embedHtml = this.sanitizer.bypassSecurityTrustHtml(this.getVideoEmbed(abuse))
215
216 if (abuse.video.channel?.ownerAccount) {
217 abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
218 }
219 }
220
221 if (abuse.comment) {
222 if (abuse.comment.deleted) {
223 abuse.truncatedCommentHtml = abuse.commentHtml = this.i18n('Deleted comment')
224 } else {
225 const truncated = truncate(abuse.comment.text, { length: 100 })
226 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true)
227 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true)
228 }
229 }
230
231 if (abuse.reporterAccount) {
232 abuse.reporterAccount = new Account(abuse.reporterAccount)
233 }
234
235 if (abuse.flaggedAccount) {
236 abuse.flaggedAccount = new Account(abuse.flaggedAccount)
237 }
238
239 if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
240
241 this.abuses.push(abuse)
242 }
243 },
244
245 err => this.notifier.error(err.message)
246 )
247 }
248
249 private buildInternalActions (): DropdownAction<ProcessedAbuse>[] {
250 return [
251 {
252 label: this.i18n('Internal actions'),
253 isHeader: true
254 },
255 {
256 label: this.i18n('Delete report'),
257 handler: abuse => this.removeAbuse(abuse)
258 },
259 {
260 label: this.i18n('Add note'),
261 handler: abuse => this.openModerationCommentModal(abuse),
262 isDisplayed: abuse => !abuse.moderationComment
263 },
264 {
265 label: this.i18n('Update note'),
266 handler: abuse => this.openModerationCommentModal(abuse),
267 isDisplayed: abuse => !!abuse.moderationComment
268 },
269 {
270 label: this.i18n('Mark as accepted'),
271 handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
272 isDisplayed: abuse => !this.isAbuseAccepted(abuse)
273 },
274 {
275 label: this.i18n('Mark as rejected'),
276 handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
277 isDisplayed: abuse => !this.isAbuseRejected(abuse)
278 }
279 ]
280 }
281
282 private buildFlaggedAccountActions (): DropdownAction<ProcessedAbuse>[] {
283 return [
284 {
285 label: this.i18n('Actions for the flagged account'),
286 isHeader: true,
287 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video
288 },
289
290 {
291 label: this.i18n('Mute account'),
292 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
293 handler: abuse => this.muteAccountHelper(abuse.flaggedAccount)
294 },
295
296 {
297 label: this.i18n('Mute server account'),
298 isDisplayed: abuse => abuse.flaggedAccount && !abuse.comment && !abuse.video,
299 handler: abuse => this.muteServerHelper(abuse.flaggedAccount.host)
300 }
301 ]
302 }
303
304 private buildAccountActions (): DropdownAction<ProcessedAbuse>[] {
305 return [
306 {
307 label: this.i18n('Actions for the reporter'),
308 isHeader: true,
309 isDisplayed: abuse => !!abuse.reporterAccount
310 },
311
312 {
313 label: this.i18n('Mute reporter'),
314 isDisplayed: abuse => !!abuse.reporterAccount,
315 handler: abuse => this.muteAccountHelper(abuse.reporterAccount)
316 },
317
318 {
319 label: this.i18n('Mute server'),
320 isDisplayed: abuse => abuse.reporterAccount && !abuse.reporterAccount.userId,
321 handler: abuse => this.muteServerHelper(abuse.reporterAccount.host)
322 }
323 ]
324 }
325
326 private buildVideoActions (): DropdownAction<ProcessedAbuse>[] {
327 return [
328 {
329 label: this.i18n('Actions for the video'),
330 isHeader: true,
331 isDisplayed: abuse => abuse.video && !abuse.video.deleted
332 },
333 {
334 label: this.i18n('Block video'),
335 isDisplayed: abuse => abuse.video && !abuse.video.deleted && !abuse.video.blacklisted,
336 handler: abuse => {
337 this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
338 .subscribe(
339 () => {
340 this.notifier.success(this.i18n('Video blocked.'))
341
342 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
343 },
344
345 err => this.notifier.error(err.message)
346 )
347 }
348 },
349 {
350 label: this.i18n('Unblock video'),
351 isDisplayed: abuse => abuse.video && !abuse.video.deleted && abuse.video.blacklisted,
352 handler: abuse => {
353 this.videoBlocklistService.unblockVideo(abuse.video.id)
354 .subscribe(
355 () => {
356 this.notifier.success(this.i18n('Video unblocked.'))
357
358 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
359 },
360
361 err => this.notifier.error(err.message)
362 )
363 }
364 },
365 {
366 label: this.i18n('Delete video'),
367 isDisplayed: abuse => abuse.video && !abuse.video.deleted,
368 handler: async abuse => {
369 const res = await this.confirmService.confirm(
370 this.i18n('Do you really want to delete this video?'),
371 this.i18n('Delete')
372 )
373 if (res === false) return
374
375 this.videoService.removeVideo(abuse.video.id)
376 .subscribe(
377 () => {
378 this.notifier.success(this.i18n('Video deleted.'))
379
380 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
381 },
382
383 err => this.notifier.error(err.message)
384 )
385 }
386 }
387 ]
388 }
389
390 private buildCommentActions (): DropdownAction<ProcessedAbuse>[] {
391 return [
392 {
393 label: this.i18n('Actions for the comment'),
394 isHeader: true,
395 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted
396 },
397
398 {
399 label: this.i18n('Delete comment'),
400 isDisplayed: abuse => abuse.comment && !abuse.comment.deleted,
401 handler: async abuse => {
402 const res = await this.confirmService.confirm(
403 this.i18n('Do you really want to delete this comment?'),
404 this.i18n('Delete')
405 )
406 if (res === false) return
407
408 this.commentService.deleteVideoComment(abuse.comment.video.id, abuse.comment.id)
409 .subscribe(
410 () => {
411 this.notifier.success(this.i18n('Comment deleted.'))
412
413 this.updateAbuseState(abuse, AbuseState.ACCEPTED)
414 },
415
416 err => this.notifier.error(err.message)
417 )
418 }
419 }
420 ]
421 }
422
423 private muteAccountHelper (account: Account) {
424 this.blocklistService.blockAccountByInstance(account)
425 .subscribe(
426 () => {
427 this.notifier.success(
428 this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })
429 )
430
431 account.mutedByInstance = true
432 },
433
434 err => this.notifier.error(err.message)
435 )
436 }
437
438 private muteServerHelper (host: string) {
439 this.blocklistService.blockServerByInstance(host)
440 .subscribe(
441 () => {
442 this.notifier.success(
443 this.i18n('Server {{host}} muted by the instance.', { host: host })
444 )
445 },
446
447 err => this.notifier.error(err.message)
448 )
449 }
450
451 private toHtml (text: string) {
452 return this.markdownRenderer.textMarkdownToHTML(text)
453 }
454}
diff --git a/client/src/app/+admin/moderation/abuse-list/index.ts b/client/src/app/+admin/moderation/abuse-list/index.ts
new file mode 100644
index 000000000..c6037dab4
--- /dev/null
+++ b/client/src/app/+admin/moderation/abuse-list/index.ts
@@ -0,0 +1,3 @@
1export * from './abuse-details.component'
2export * from './abuse-list.component'
3export * 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/abuse-list/moderation-comment-modal.component.html
index 8082e93f4..8082e93f4 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html
+++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss
index afcdb9a16..afcdb9a16 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss
+++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts
index 3cd763ca4..23738f9cd 100644
--- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts
+++ b/client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts
@@ -1,11 +1,11 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms' 3import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms'
4import { VideoAbuseService } from '@app/shared/shared-moderation' 4import { AbuseService } from '@app/shared/shared-moderation'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { VideoAbuse } from '@shared/models' 8import { Abuse } from '@shared/models'
9 9
10@Component({ 10@Component({
11 selector: 'my-moderation-comment-modal', 11 selector: 'my-moderation-comment-modal',
@@ -16,15 +16,15 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
16 @ViewChild('modal', { static: true }) modal: NgbModal 16 @ViewChild('modal', { static: true }) modal: NgbModal
17 @Output() commentUpdated = new EventEmitter<string>() 17 @Output() commentUpdated = new EventEmitter<string>()
18 18
19 private abuseToComment: VideoAbuse 19 private abuseToComment: Abuse
20 private openedModal: NgbModalRef 20 private openedModal: NgbModalRef
21 21
22 constructor ( 22 constructor (
23 protected formValidatorService: FormValidatorService, 23 protected formValidatorService: FormValidatorService,
24 private modalService: NgbModal, 24 private modalService: NgbModal,
25 private notifier: Notifier, 25 private notifier: Notifier,
26 private videoAbuseService: VideoAbuseService, 26 private abuseService: AbuseService,
27 private videoAbuseValidatorsService: VideoAbuseValidatorsService, 27 private abuseValidatorsService: AbuseValidatorsService,
28 private i18n: I18n 28 private i18n: I18n
29 ) { 29 ) {
30 super() 30 super()
@@ -32,11 +32,11 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
32 32
33 ngOnInit () { 33 ngOnInit () {
34 this.buildForm({ 34 this.buildForm({
35 moderationComment: this.videoAbuseValidatorsService.VIDEO_ABUSE_MODERATION_COMMENT 35 moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT
36 }) 36 })
37 } 37 }
38 38
39 openModal (abuseToComment: VideoAbuse) { 39 openModal (abuseToComment: Abuse) {
40 this.abuseToComment = abuseToComment 40 this.abuseToComment = abuseToComment
41 this.openedModal = this.modalService.open(this.modal, { centered: true }) 41 this.openedModal = this.modalService.open(this.modal, { centered: true })
42 42
@@ -54,7 +54,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
54 async banUser () { 54 async banUser () {
55 const moderationComment: string = this.form.value[ 'moderationComment' ] 55 const moderationComment: string = this.form.value[ 'moderationComment' ]
56 56
57 this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment }) 57 this.abuseService.updateAbuse(this.abuseToComment, { moderationComment })
58 .subscribe( 58 .subscribe(
59 () => { 59 () => {
60 this.notifier.success(this.i18n('Comment updated.')) 60 this.notifier.success(this.i18n('Comment updated.'))
diff --git a/client/src/app/+admin/moderation/index.ts b/client/src/app/+admin/moderation/index.ts
index 16249236c..53e4bc991 100644
--- a/client/src/app/+admin/moderation/index.ts
+++ b/client/src/app/+admin/moderation/index.ts
@@ -1,5 +1,5 @@
1export * from './abuse-list'
1export * from './instance-blocklist' 2export * from './instance-blocklist'
2export * from './video-abuse-list'
3export * from './video-block-list' 3export * from './video-block-list'
4export * from './moderation.component' 4export * from './moderation.component'
5export * from './moderation.routes' 5export * from './moderation.routes'
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss
index 0ec420af9..65fe94d39 100644
--- a/client/src/app/+admin/moderation/moderation.component.scss
+++ b/client/src/app/+admin/moderation/moderation.component.scss
@@ -25,18 +25,18 @@
25 vertical-align: top; 25 vertical-align: top;
26 text-align: right; 26 text-align: right;
27 } 27 }
28 28
29 .moderation-expanded-text { 29 .moderation-expanded-text {
30 display: inline-flex; 30 display: inline-flex;
31 word-wrap: break-word; 31 word-wrap: break-word;
32 32
33 ::ng-deep p:last-child { 33 ::ng-deep p:last-child {
34 margin-bottom: 0px !important; 34 margin-bottom: 0px !important;
35 } 35 }
36 } 36 }
37} 37}
38 38
39.video-table-states { 39.table-states {
40 & > :not(:first-child) { 40 & > :not(:first-child) {
41 margin-left: .4rem; 41 margin-left: .4rem;
42 } 42 }
@@ -59,6 +59,7 @@ p-calendar {
59.screenratio { 59.screenratio {
60 div { 60 div {
61 @include miniature-thumbnail; 61 @include miniature-thumbnail;
62
62 display: inline-flex; 63 display: inline-flex;
63 justify-content: center; 64 justify-content: center;
64 align-items: center; 65 align-items: center;
@@ -72,6 +73,11 @@ p-calendar {
72 }; 73 };
73} 74}
74 75
76.comment-html {
77 background-color: #ececec;
78 padding: 10px;
79}
80
75.chip { 81.chip {
76 @include chip; 82 @include chip;
77} 83}
@@ -83,16 +89,39 @@ my-action-dropdown.show {
83} 89}
84 90
85 91
86.video-table-video-link { 92.table-video-link {
87 @include disable-outline; 93 @include disable-outline;
94
88 position: relative; 95 position: relative;
89 top: 3px; 96 top: 3px;
90} 97}
91 98
92.video-table-video { 99.table-comment-link,
100.table-account-link {
101 @include disable-outline;
102
103 color: var(--mainForegroundColor);
104
105 ::ng-deep p:last-child {
106 margin: 0;
107 }
108}
109
110.table-account-link {
111 display: flex;
112 flex-direction: column;
113}
114
115.comment-flagged-account,
116.account-flagged-handle {
117 font-size: 11px;
118 color: var(--greyForegroundColor);
119}
120
121.table-video {
93 display: inline-flex; 122 display: inline-flex;
94 123
95 .video-table-video-image { 124 .table-video-image {
96 @include miniature-thumbnail; 125 @include miniature-thumbnail;
97 126
98 $image-height: 45px; 127 $image-height: 45px;
@@ -118,7 +147,7 @@ my-action-dropdown.show {
118 color: pvar(--inputPlaceholderColor); 147 color: pvar(--inputPlaceholderColor);
119 } 148 }
120 149
121 .video-table-video-image-label { 150 .table-video-image-label {
122 @include static-thumbnail-overlay; 151 @include static-thumbnail-overlay;
123 position: absolute; 152 position: absolute;
124 border-radius: 3px; 153 border-radius: 3px;
@@ -130,7 +159,7 @@ my-action-dropdown.show {
130 } 159 }
131 } 160 }
132 161
133 .video-table-video-text { 162 .table-video-text {
134 display: inline-flex; 163 display: inline-flex;
135 flex-direction: column; 164 flex-direction: column;
136 justify-content: center; 165 justify-content: center;
@@ -145,7 +174,8 @@ my-action-dropdown.show {
145 } 174 }
146 175
147 div + div { 176 div + div {
148 font-size: 80%; 177 color: var(--greyForegroundColor);
178 font-size: 11px;
149 } 179 }
150 } 180 }
151} 181}
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts
index cd837bcb9..8a31a54dc 100644
--- a/client/src/app/+admin/moderation/moderation.routes.ts
+++ b/client/src/app/+admin/moderation/moderation.routes.ts
@@ -1,7 +1,7 @@
1import { Routes } from '@angular/router' 1import { Routes } from '@angular/router'
2import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' 2import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
3import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 3import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' 4import { AbuseListComponent } from '@app/+admin/moderation/abuse-list'
5import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' 5import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
6import { UserRightGuard } from '@app/core' 6import { UserRightGuard } from '@app/core'
7import { UserRight } from '@shared/models' 7import { UserRight } from '@shared/models'
@@ -13,22 +13,27 @@ export const ModerationRoutes: Routes = [
13 children: [ 13 children: [
14 { 14 {
15 path: '', 15 path: '',
16 redirectTo: 'video-abuses/list', 16 redirectTo: 'abuses/list',
17 pathMatch: 'full' 17 pathMatch: 'full'
18 }, 18 },
19 { 19 {
20 path: 'video-abuses', 20 path: 'video-abuses',
21 redirectTo: 'video-abuses/list', 21 redirectTo: 'abuses/list',
22 pathMatch: 'full' 22 pathMatch: 'full'
23 }, 23 },
24 { 24 {
25 path: 'video-abuses/list', 25 path: 'video-abuses/list',
26 component: VideoAbuseListComponent, 26 redirectTo: 'abuses/list',
27 pathMatch: 'full'
28 },
29 {
30 path: 'abuses/list',
31 component: AbuseListComponent,
27 canActivate: [ UserRightGuard ], 32 canActivate: [ UserRightGuard ],
28 data: { 33 data: {
29 userRight: UserRight.MANAGE_VIDEO_ABUSES, 34 userRight: UserRight.MANAGE_ABUSES,
30 meta: { 35 meta: {
31 title: 'Video reports' 36 title: 'Reports'
32 } 37 }
33 } 38 }
34 }, 39 },
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/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-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.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}
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 037040902..d103f8e2f 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -37,14 +37,14 @@
37 </a> 37 </a>
38 </div> 38 </div>
39 <div> 39 <div>
40 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' + user?.account.displayName + '&quot;' }"> 40 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' + user?.account.displayName + '&quot;' }">
41 <div class="dashboard-num">{{ user.videoAbusesCount }}</div> 41 <div class="dashboard-num">{{ user.abusesCount }}</div>
42 <div class="dashboard-label" i18n>Incriminated in reports</div> 42 <div class="dashboard-label" i18n>Incriminated in reports</div>
43 </a> 43 </a>
44 </div> 44 </div>
45 <div> 45 <div>
46 <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + user?.account.displayName + '&quot; state:accepted' }"> 46 <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + user?.account.displayName + '&quot; state:accepted' }">
47 <div class="dashboard-num">{{ user.videoAbusesAcceptedCount }} / {{ user.videoAbusesCreatedCount }}</div> 47 <div class="dashboard-num">{{ user.abusesAcceptedCount }} / {{ user.abusesCreatedCount }}</div>
48 <div class="dashboard-label" i18n>Authored reports accepted</div> 48 <div class="dashboard-label" i18n>Authored reports accepted</div>
49 </a> 49 </a>
50 </div> 50 </div>