diff options
author | Chocobozzz <chocobozzz@framasoft.org> | 2020-06-11 10:36:04 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@framasoft.org> | 2020-06-11 10:36:04 +0200 |
commit | fa58a19819cb821a5c5a26802efd492461be9c04 (patch) | |
tree | 72bd8307da8ac447bb9934b1301ab0d93ea97e6b /client/src/app/+admin/moderation/video-block-list | |
parent | 50a04125dac1a126cb79a9fb575fcf673c5ac2d6 (diff) | |
parent | e95bede8fe660a38c99e51560caa5dd58a8eb23e (diff) | |
download | PeerTube-fa58a19819cb821a5c5a26802efd492461be9c04.tar.gz PeerTube-fa58a19819cb821a5c5a26802efd492461be9c04.tar.zst PeerTube-fa58a19819cb821a5c5a26802efd492461be9c04.zip |
Merge branch 'blacklist' into 'develop'
rename blacklist to block/blocklist, merge block and auto-block views
See merge request framasoft/peertube/PeerTube!30
Diffstat (limited to 'client/src/app/+admin/moderation/video-block-list')
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 | |||
3 | my-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..7b3691332 --- /dev/null +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts | |||
@@ -0,0 +1,196 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { SortMeta } from 'primeng/api' | ||
3 | import { Notifier, ServerService } from '@app/core' | ||
4 | import { ConfirmService } from '../../../core' | ||
5 | import { RestPagination, RestTable, VideoBlockService } from '../../../shared' | ||
6 | import { VideoBlacklist, VideoBlacklistType } from '../../../../../../shared' | ||
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
8 | import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' | ||
9 | import { Video } from '../../../shared/video/video.model' | ||
10 | import { MarkdownService } from '@app/shared/renderer' | ||
11 | import { Params, ActivatedRoute, Router } from '@angular/router' | ||
12 | import { filter, switchMap } from 'rxjs/operators' | ||
13 | import { 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 | }) | ||
20 | export class VideoBlockListComponent extends RestTable implements OnInit { | ||
21 | blocklist: (VideoBlacklist & { reasonHtml?: string })[] = [] | ||
22 | totalRecords = 0 | ||
23 | sort: SortMeta = { field: 'createdAt', order: -1 } | ||
24 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
25 | blocklistTypeFilter: VideoBlacklistType = undefined | ||
26 | |||
27 | videoBlocklistActions: DropdownAction<VideoBlacklist>[][] = [] | ||
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.blocklistTypeFilter = VideoBlacklistType.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: VideoBlacklist) { | ||
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: VideoBlacklist) { | ||
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 | } | ||