aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+admin/moderation/video-block-list
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+admin/moderation/video-block-list')
-rw-r--r--client/src/app/+admin/moderation/video-block-list/index.ts1
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.html113
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.scss18
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts196
4 files changed, 328 insertions, 0 deletions
diff --git a/client/src/app/+admin/moderation/video-block-list/index.ts b/client/src/app/+admin/moderation/video-block-list/index.ts
new file mode 100644
index 000000000..ec4de8f62
--- /dev/null
+++ b/client/src/app/+admin/moderation/video-block-list/index.ts
@@ -0,0 +1 @@
export * from './video-block-list.component'
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
new file mode 100644
index 000000000..f3ec37314
--- /dev/null
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.html
@@ -0,0 +1,113 @@
1<p-table
2 [value]="blocklist" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
3 [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
4 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
5 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} blocked videos"
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 block filters</h6>
19 <a [routerLink]="[ '/admin/moderation/video-blocks/list' ]" [queryParams]="{ 'search': 'type:auto' }" class="dropdown-item" i18n>Automatic blocks</a>
20 <a [routerLink]="[ '/admin/moderation/video-blocks/list' ]" [queryParams]="{ 'search': 'type:manual' }" class="dropdown-item" i18n>Manual blocks</a>
21 </div>
22 </div>
23 <input
24 type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
25 (keyup)="onBlockSearch($event)"
26 >
27 <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
28 <span class="sr-only" i18n>Clear filters</span>
29 </div>
30 </div>
31 </div>
32 </ng-template>
33
34 <ng-template pTemplate="header">
35 <tr>
36 <th style="width: 40px"></th>
37 <th i18n pSortableColumn="name">Video <p-sortIcon field="name"></p-sortIcon></th>
38 <th style="width: 100px;" i18n>Sensitive</th>
39 <th style="width: 120px;" i18n>Unfederated</th>
40 <th style="width: 150px;" i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
41 <th style="width: 150px;"></th>
42 </tr>
43 </ng-template>
44
45 <ng-template pTemplate="body" let-videoBlock let-expanded="expanded">
46 <tr>
47 <td *ngIf="!videoBlock.reason"></td>
48 <td *ngIf="videoBlock.reason" class="expand-cell c-hand" [pRowToggler]="videoBlock" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
49 <span class="expander">
50 <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
51 </span>
52 </td>
53
54 <td>
55 <a [href]="getVideoUrl(videoBlock)" class="video-table-video-link" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer">
56 <div class="video-table-video">
57 <div class="video-table-video-image">
58 <img [src]="videoBlock.video.thumbnailPath">
59 </div>
60 <div class="video-table-video-text">
61 <div>
62 <my-global-icon i18n-title title="The video was blocked due to automatic blocking of new videos" *ngIf="videoBlock.type == 2" iconName="robot"></my-global-icon>
63 {{ videoBlock.video.name }}
64 </div>
65 <div class="text-muted">by {{ videoBlock.video.channel?.displayName }} on {{ videoBlock.video.channel?.host }} </div>
66 </div>
67 </div>
68 </a>
69 </td>
70
71 <ng-container *ngIf="videoBlock.reason">
72 <td class="c-hand" [pRowToggler]="videoBlock">{{ booleanToText(videoBlock.video.nsfw) }}</td>
73 <td class="c-hand" [pRowToggler]="videoBlock">{{ booleanToText(videoBlock.unfederated) }}</td>
74 <td class="c-hand" [pRowToggler]="videoBlock">{{ videoBlock.createdAt | date: 'short' }}</td>
75 </ng-container>
76 <ng-container *ngIf="!videoBlock.reason">
77 <td>{{ booleanToText(videoBlock.video.nsfw) }}</td>
78 <td>{{ booleanToText(videoBlock.unfederated) }}</td>
79 <td>{{ videoBlock.createdAt | date: 'short' }}</td>
80 </ng-container>
81
82 <td class="action-cell">
83 <my-action-dropdown
84 [ngClass]="{ 'show': expanded }" placement="bottom-right" container="body"
85 i18n-label label="Actions" [actions]="videoBlocklistActions" [entry]="videoBlock"
86 ></my-action-dropdown>
87 </td>
88 </tr>
89 </ng-template>
90
91 <ng-template pTemplate="rowexpansion" let-videoBlock>
92 <tr>
93 <td class="expand-cell" colspan="6">
94 <div class="d-flex moderation-expanded">
95 <span class="col-2 moderation-expanded-label" i18n>Block reason:</span>
96 <span class="col-9 moderation-expanded-text" [innerHTML]="videoBlock.reasonHtml"></span>
97 </div>
98 </td>
99 </tr>
100 </ng-template>
101
102 <ng-template pTemplate="emptymessage">
103 <tr>
104 <td colspan="6">
105 <div class="empty-table-message">
106 <ng-container *ngIf="search" i18n>No blocked video found matching current filters.</ng-container>
107 <ng-container *ngIf="!search" i18n>No blocked video found.</ng-container>
108 </div>
109 </td>
110 </tr>
111 </ng-template>
112</p-table>
113
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.scss b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.scss
new file mode 100644
index 000000000..43a365608
--- /dev/null
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.scss
@@ -0,0 +1,18 @@
1@import 'mixins';
2
3my-global-icon {
4 @include apply-svg-color(#7d7d7d);
5
6 width: 12px;
7 height: 12px;
8 position: relative;
9 top: -1px;
10}
11
12.input-group {
13 @include peertube-input-group(300px);
14
15 .dropdown-toggle::after {
16 margin-left: 0;
17 }
18}
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
new file mode 100644
index 000000000..e72ab5348
--- /dev/null
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
@@ -0,0 +1,196 @@
1import { Component, OnInit } from '@angular/core'
2import { SortMeta } from 'primeng/api'
3import { Notifier, ServerService } from '@app/core'
4import { ConfirmService } from '../../../core'
5import { RestPagination, RestTable, VideoBlockService } from '../../../shared'
6import { VideoBlocklist, VideoBlockType } from '../../../../../../shared'
7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
9import { Video } from '../../../shared/video/video.model'
10import { MarkdownService } from '@app/shared/renderer'
11import { Params, ActivatedRoute, Router } from '@angular/router'
12import { filter, switchMap } from 'rxjs/operators'
13import { VideoService } from '@app/shared/video/video.service'
14
15@Component({
16 selector: 'my-video-block-list',
17 templateUrl: './video-block-list.component.html',
18 styleUrls: [ '../moderation.component.scss', './video-block-list.component.scss' ]
19})
20export class VideoBlockListComponent extends RestTable implements OnInit {
21 blocklist: (VideoBlocklist & { reasonHtml?: string })[] = []
22 totalRecords = 0
23 sort: SortMeta = { field: 'createdAt', order: -1 }
24 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
25 listBlockTypeFilter: VideoBlockType = undefined
26
27 videoBlocklistActions: DropdownAction<VideoBlocklist>[][] = []
28
29 constructor (
30 private notifier: Notifier,
31 private serverService: ServerService,
32 private confirmService: ConfirmService,
33 private videoBlocklistService: VideoBlockService,
34 private markdownRenderer: MarkdownService,
35 private videoService: VideoService,
36 private route: ActivatedRoute,
37 private router: Router,
38 private i18n: I18n
39 ) {
40 super()
41
42 this.videoBlocklistActions = [
43 [
44 {
45 label: this.i18n('Internal actions'),
46 isHeader: true
47 },
48 {
49 label: this.i18n('Switch video block to manual'),
50 handler: videoBlock => {
51 this.videoBlocklistService.unblockVideo(videoBlock.video.id).pipe(
52 switchMap(_ => this.videoBlocklistService.blockVideo(videoBlock.video.id, undefined, true))
53 ).subscribe(
54 () => {
55 this.notifier.success(this.i18n('Video {{name}} switched to manual block.', { name: videoBlock.video.name }))
56 this.loadData()
57 },
58
59 err => this.notifier.error(err.message)
60 )
61 }
62 }
63 ],
64 [
65 {
66 label: this.i18n('Actions for the video'),
67 isHeader: true,
68 },
69 {
70 label: this.i18n('Unblock video'),
71 handler: videoBlock => this.unblockVideo(videoBlock)
72 },
73
74 {
75 label: this.i18n('Delete video'),
76 handler: async videoBlock => {
77 const res = await this.confirmService.confirm(
78 this.i18n('Do you really want to delete this video?'),
79 this.i18n('Delete')
80 )
81 if (res === false) return
82
83 this.videoService.removeVideo(videoBlock.video.id)
84 .subscribe(
85 () => {
86 this.notifier.success(this.i18n('Video deleted.'))
87 },
88
89 err => this.notifier.error(err.message)
90 )
91 }
92 }
93 ]
94 ]
95 }
96
97 ngOnInit () {
98 this.serverService.getConfig()
99 .subscribe(config => {
100 // don't filter if auto-blacklist is not enabled as this will be the only list
101 if (config.autoBlacklist.videos.ofUsers.enabled) {
102 this.listBlockTypeFilter = VideoBlockType.MANUAL
103 }
104 })
105
106 this.initialize()
107
108 this.route.queryParams
109 .pipe(filter(params => params.search !== undefined && params.search !== null))
110 .subscribe(params => {
111 this.search = params.search
112 this.setTableFilter(params.search)
113 this.loadData()
114 })
115 }
116
117 ngAfterViewInit () {
118 if (this.search) this.setTableFilter(this.search)
119 }
120
121 /* Table filter functions */
122 onBlockSearch (event: Event) {
123 this.onSearch(event)
124 this.setQueryParams((event.target as HTMLInputElement).value)
125 }
126
127 setQueryParams (search: string) {
128 const queryParams: Params = {}
129 if (search) Object.assign(queryParams, { search })
130 this.router.navigate([ '/admin/moderation/video-blocks/list' ], { queryParams })
131 }
132
133 resetTableFilter () {
134 this.setTableFilter('')
135 this.setQueryParams('')
136 this.resetSearch()
137 }
138 /* END Table filter functions */
139
140 getIdentifier () {
141 return 'VideoBlockListComponent'
142 }
143
144 getVideoUrl (videoBlock: VideoBlocklist) {
145 return Video.buildClientUrl(videoBlock.video.uuid)
146 }
147
148 booleanToText (value: boolean) {
149 if (value === true) return this.i18n('yes')
150
151 return this.i18n('no')
152 }
153
154 toHtml (text: string) {
155 return this.markdownRenderer.textMarkdownToHTML(text)
156 }
157
158 async unblockVideo (entry: VideoBlocklist) {
159 const confirmMessage = this.i18n(
160 'Do you really want to unblock this video? It will be available again in the videos list.'
161 )
162
163 const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblock'))
164 if (res === false) return
165
166 this.videoBlocklistService.unblockVideo(entry.video.id).subscribe(
167 () => {
168 this.notifier.success(this.i18n('Video {{name}} unblocked.', { name: entry.video.name }))
169 this.loadData()
170 },
171
172 err => this.notifier.error(err.message)
173 )
174 }
175
176 protected loadData () {
177 this.videoBlocklistService.listBlocks({
178 pagination: this.pagination,
179 sort: this.sort,
180 search: this.search,
181 })
182 .subscribe(
183 async resultList => {
184 this.totalRecords = resultList.total
185
186 this.blocklist = resultList.data
187
188 for (const element of this.blocklist) {
189 Object.assign(element, { reasonHtml: await this.toHtml(element.reason) })
190 }
191 },
192
193 err => this.notifier.error(err.message)
194 )
195 }
196}