diff options
author | Chocobozzz <me@florianbigard.com> | 2020-11-16 11:55:17 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2020-11-16 13:48:58 +0100 |
commit | f1273314593a4a7dc7ec9594ce0c6c3ae8f62b34 (patch) | |
tree | cf1f3949e64a24a820833950d7b2bbf9ccd40013 /client/src/app | |
parent | 0f8d00e3144060270d7fe603865fccaf18649c47 (diff) | |
download | PeerTube-f1273314593a4a7dc7ec9594ce0c6c3ae8f62b34.tar.gz PeerTube-f1273314593a4a7dc7ec9594ce0c6c3ae8f62b34.tar.zst PeerTube-f1273314593a4a7dc7ec9594ce0c6c3ae8f62b34.zip |
Add admin view to manage comments
Diffstat (limited to 'client/src/app')
8 files changed, 160 insertions, 43 deletions
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 index c92d1c39c..0e34150c1 100644 --- 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 | |||
@@ -1,12 +1,8 @@ | |||
1 | @import 'mixins'; | 1 | @import 'mixins'; |
2 | 2 | ||
3 | my-global-icon { | 3 | my-global-icon { |
4 | @include apply-svg-color(#7d7d7d); | 4 | width: 24px; |
5 | 5 | height: 24px; | |
6 | width: 12px; | ||
7 | height: 12px; | ||
8 | position: relative; | ||
9 | top: -1px; | ||
10 | } | 6 | } |
11 | 7 | ||
12 | .input-group { | 8 | .input-group { |
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html index b4f66a75f..45c5fe28f 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html +++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.html | |||
@@ -1,9 +1,11 @@ | |||
1 | <h1> | 1 | <h1> |
2 | <my-global-icon iconName="cross" aria-hidden="true"></my-global-icon> | 2 | <my-global-icon iconName="message-circle" aria-hidden="true"></my-global-icon> |
3 | <ng-container i18n>Video comments</ng-container> | 3 | <ng-container i18n>Video comments</ng-container> |
4 | |||
5 | <my-feed [syndicationItems]="syndicationItems"></my-feed> | ||
4 | </h1> | 6 | </h1> |
5 | 7 | ||
6 | this view does show comments from muted accounts so you can delete them | 8 | <em>This view also shows comments from muted accounts.</em> |
7 | 9 | ||
8 | <p-table | 10 | <p-table |
9 | [value]="comments" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" | 11 | [value]="comments" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" |
@@ -29,7 +31,7 @@ this view does show comments from muted accounts so you can delete them | |||
29 | </div> | 31 | </div> |
30 | <input | 32 | <input |
31 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | 33 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." |
32 | (keyup)="onSearch($event)" | 34 | (keyup)="onInputSearch($event)" |
33 | > | 35 | > |
34 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> | 36 | <a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a> |
35 | <span class="sr-only" i18n>Clear filters</span> | 37 | <span class="sr-only" i18n>Clear filters</span> |
@@ -41,9 +43,9 @@ this view does show comments from muted accounts so you can delete them | |||
41 | <ng-template pTemplate="header"> | 43 | <ng-template pTemplate="header"> |
42 | <tr> | 44 | <tr> |
43 | <th style="width: 40px"></th> | 45 | <th style="width: 40px"></th> |
44 | <th style="width: 100px;" i18n>Account</th> | 46 | <th style="width: 300px" i18n>Account</th> |
45 | <th style="width: 100px;" i18n>Video</th> | 47 | <th style="width: 300px" i18n>Video</th> |
46 | <th style="width: 100px;" i18n>Comment</th> | 48 | <th i18n>Comment</th> |
47 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> | 49 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> |
48 | <th style="width: 150px;"></th> | 50 | <th style="width: 150px;"></th> |
49 | </tr> | 51 | </tr> |
@@ -58,14 +60,28 @@ this view does show comments from muted accounts so you can delete them | |||
58 | </td> | 60 | </td> |
59 | 61 | ||
60 | <td> | 62 | <td> |
61 | {{ videoComment.by }} | 63 | <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> |
64 | <div class="chip two-lines"> | ||
65 | <img | ||
66 | class="avatar" | ||
67 | [src]="videoComment.accountAvatarUrl" | ||
68 | alt="" | ||
69 | > | ||
70 | <div> | ||
71 | {{ videoComment.account.displayName }} | ||
72 | <span>{{ videoComment.by }}</span> | ||
73 | </div> | ||
74 | </div> | ||
75 | </a> | ||
62 | </td> | 76 | </td> |
63 | 77 | ||
64 | <td> | 78 | <td class="video"> |
65 | {{ videoComment.video.name }} | 79 | <em i18n>Commented video</em> |
80 | |||
81 | <a [href]="videoComment.localUrl" target="_blank" rel="noopener noreferrer">{{ videoComment.video.name }}</a> | ||
66 | </td> | 82 | </td> |
67 | 83 | ||
68 | <td> | 84 | <td class="comment-html"> |
69 | <div [innerHTML]="videoComment.textHtml"></div> | 85 | <div [innerHTML]="videoComment.textHtml"></div> |
70 | </td> | 86 | </td> |
71 | 87 | ||
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss index c92d1c39c..b3746b0c5 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss +++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.scss | |||
@@ -1,12 +1,22 @@ | |||
1 | @import 'mixins'; | 1 | @import 'mixins'; |
2 | 2 | ||
3 | my-global-icon { | 3 | h1 { |
4 | @include apply-svg-color(#7d7d7d); | 4 | my-feed { |
5 | margin-left: 5px; | ||
6 | display: inline-block; | ||
7 | |||
8 | ::ng-deep { | ||
9 | my-global-icon { | ||
10 | width: 15px !important; | ||
11 | top: 0 !important; | ||
12 | } | ||
13 | } | ||
14 | } | ||
15 | } | ||
5 | 16 | ||
6 | width: 12px; | 17 | my-global-icon { |
7 | height: 12px; | 18 | width: 24px; |
8 | position: relative; | 19 | height: 24px; |
9 | top: -1px; | ||
10 | } | 20 | } |
11 | 21 | ||
12 | .input-group { | 22 | .input-group { |
@@ -25,3 +35,32 @@ my-global-icon { | |||
25 | flex-grow: 1; | 35 | flex-grow: 1; |
26 | } | 36 | } |
27 | } | 37 | } |
38 | |||
39 | .video { | ||
40 | display: flex; | ||
41 | flex-direction: column; | ||
42 | |||
43 | em { | ||
44 | font-size: 11px; | ||
45 | } | ||
46 | |||
47 | a { | ||
48 | @include ellipsis | ||
49 | } | ||
50 | } | ||
51 | |||
52 | .comment-html { | ||
53 | ::ng-deep { | ||
54 | > div { | ||
55 | max-height: 22px; | ||
56 | } | ||
57 | |||
58 | div, p { | ||
59 | @include ellipsis; | ||
60 | } | ||
61 | |||
62 | p { | ||
63 | margin: 0; | ||
64 | } | ||
65 | } | ||
66 | } | ||
diff --git a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts index fdd5ec76e..d26047125 100644 --- a/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts +++ b/client/src/app/+admin/moderation/video-comment-list/video-comment-list.component.ts | |||
@@ -1,16 +1,17 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { filter } from 'rxjs/operators' | 2 | import { filter } from 'rxjs/operators' |
3 | import { AfterViewInit, Component, OnInit } from '@angular/core' | 3 | import { AfterViewInit, Component, OnInit } from '@angular/core' |
4 | import { DomSanitizer } from '@angular/platform-browser' | ||
5 | import { ActivatedRoute, Params, Router } from '@angular/router' | 4 | import { ActivatedRoute, Params, Router } from '@angular/router' |
6 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 5 | import { AuthService, ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' |
7 | import { DropdownAction, VideoService } from '@app/shared/shared-main' | 6 | import { DropdownAction } from '@app/shared/shared-main' |
7 | import { BulkService } from '@app/shared/shared-moderation' | ||
8 | import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' | 8 | import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment' |
9 | import { FeedFormat, UserRight } from '@shared/models' | ||
9 | 10 | ||
10 | @Component({ | 11 | @Component({ |
11 | selector: 'my-video-comment-list', | 12 | selector: 'my-video-comment-list', |
12 | templateUrl: './video-comment-list.component.html', | 13 | templateUrl: './video-comment-list.component.html', |
13 | styleUrls: [ './video-comment-list.component.scss' ] | 14 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ] |
14 | }) | 15 | }) |
15 | export class VideoCommentListComponent extends RestTable implements OnInit, AfterViewInit { | 16 | export class VideoCommentListComponent extends RestTable implements OnInit, AfterViewInit { |
16 | comments: VideoCommentAdmin[] | 17 | comments: VideoCommentAdmin[] |
@@ -20,26 +21,54 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte | |||
20 | 21 | ||
21 | videoCommentActions: DropdownAction<VideoCommentAdmin>[][] = [] | 22 | videoCommentActions: DropdownAction<VideoCommentAdmin>[][] = [] |
22 | 23 | ||
24 | syndicationItems = [ | ||
25 | { | ||
26 | format: FeedFormat.RSS, | ||
27 | label: 'media rss 2.0', | ||
28 | url: VideoCommentService.BASE_FEEDS_URL + FeedFormat.RSS.toLowerCase() | ||
29 | }, | ||
30 | { | ||
31 | format: FeedFormat.ATOM, | ||
32 | label: 'atom 1.0', | ||
33 | url: VideoCommentService.BASE_FEEDS_URL + FeedFormat.ATOM.toLowerCase() | ||
34 | }, | ||
35 | { | ||
36 | format: FeedFormat.JSON, | ||
37 | label: 'json 1.0', | ||
38 | url: VideoCommentService.BASE_FEEDS_URL + FeedFormat.JSON.toLowerCase() | ||
39 | } | ||
40 | ] | ||
41 | |||
42 | get authUser () { | ||
43 | return this.auth.getUser() | ||
44 | } | ||
45 | |||
23 | constructor ( | 46 | constructor ( |
47 | private auth: AuthService, | ||
24 | private notifier: Notifier, | 48 | private notifier: Notifier, |
25 | private serverService: ServerService, | ||
26 | private confirmService: ConfirmService, | 49 | private confirmService: ConfirmService, |
27 | private videoCommentService: VideoCommentService, | 50 | private videoCommentService: VideoCommentService, |
28 | private markdownRenderer: MarkdownService, | 51 | private markdownRenderer: MarkdownService, |
29 | private sanitizer: DomSanitizer, | ||
30 | private videoService: VideoService, | ||
31 | private route: ActivatedRoute, | 52 | private route: ActivatedRoute, |
32 | private router: Router | 53 | private router: Router, |
54 | private bulkService: BulkService | ||
33 | ) { | 55 | ) { |
34 | super() | 56 | super() |
35 | 57 | ||
36 | this.videoCommentActions = [ | 58 | this.videoCommentActions = [ |
37 | [ | 59 | [ |
60 | { | ||
61 | label: $localize`Delete this comment`, | ||
62 | handler: comment => this.deleteComment(comment), | ||
63 | isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) | ||
64 | }, | ||
38 | 65 | ||
39 | // remove this comment, | 66 | { |
40 | 67 | label: $localize`Delete all comments of this account`, | |
41 | // remove all comments of this account | 68 | description: $localize`Comments are deleted after a few minutes`, |
42 | 69 | handler: comment => this.deleteUserComments(comment), | |
70 | isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) | ||
71 | } | ||
43 | ] | 72 | ] |
44 | ] | 73 | ] |
45 | } | 74 | } |
@@ -60,7 +89,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte | |||
60 | if (this.search) this.setTableFilter(this.search) | 89 | if (this.search) this.setTableFilter(this.search) |
61 | } | 90 | } |
62 | 91 | ||
63 | onSearch (event: Event) { | 92 | onInputSearch (event: Event) { |
64 | this.onSearch(event) | 93 | this.onSearch(event) |
65 | this.setQueryParams((event.target as HTMLInputElement).value) | 94 | this.setQueryParams((event.target as HTMLInputElement).value) |
66 | } | 95 | } |
@@ -84,7 +113,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte | |||
84 | } | 113 | } |
85 | 114 | ||
86 | toHtml (text: string) { | 115 | toHtml (text: string) { |
87 | return this.markdownRenderer.textMarkdownToHTML(text) | 116 | return this.markdownRenderer.textMarkdownToHTML(text, true, true) |
88 | } | 117 | } |
89 | 118 | ||
90 | protected loadData () { | 119 | protected loadData () { |
@@ -108,4 +137,33 @@ export class VideoCommentListComponent extends RestTable implements OnInit, Afte | |||
108 | err => this.notifier.error(err.message) | 137 | err => this.notifier.error(err.message) |
109 | ) | 138 | ) |
110 | } | 139 | } |
140 | |||
141 | private deleteComment (comment: VideoCommentAdmin) { | ||
142 | this.videoCommentService.deleteVideoComment(comment.video.id, comment.id) | ||
143 | .subscribe( | ||
144 | () => this.loadData(), | ||
145 | |||
146 | err => this.notifier.error(err.message) | ||
147 | ) | ||
148 | } | ||
149 | |||
150 | private async deleteUserComments (comment: VideoCommentAdmin) { | ||
151 | const message = $localize`Do you really want to delete all comments of ${comment.by}?` | ||
152 | const res = await this.confirmService.confirm(message, $localize`Delete`) | ||
153 | if (res === false) return | ||
154 | |||
155 | const options = { | ||
156 | accountName: comment.by, | ||
157 | scope: 'instance' as 'instance' | ||
158 | } | ||
159 | |||
160 | this.bulkService.removeCommentsOf(options) | ||
161 | .subscribe( | ||
162 | () => { | ||
163 | this.notifier.success($localize`Comments of ${options.accountName} will be deleted in a few minutes`) | ||
164 | }, | ||
165 | |||
166 | err => this.notifier.error(err.message) | ||
167 | ) | ||
168 | } | ||
111 | } | 169 | } |
diff --git a/client/src/app/shared/shared-main/feeds/feed.component.html b/client/src/app/shared/shared-main/feeds/feed.component.html index 13883fd9b..a00011785 100644 --- a/client/src/app/shared/shared-main/feeds/feed.component.html +++ b/client/src/app/shared/shared-main/feeds/feed.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <div class="video-feed"> | 1 | <div class="feed"> |
2 | <my-global-icon | 2 | <my-global-icon |
3 | *ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom left auto" | 3 | *ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom left auto" |
4 | class="icon-syndication" role="button" iconName="syndication" | 4 | class="icon-syndication" role="button" iconName="syndication" |
diff --git a/client/src/app/shared/shared-main/feeds/feed.component.scss b/client/src/app/shared/shared-main/feeds/feed.component.scss index 34dd0e937..333d59440 100644 --- a/client/src/app/shared/shared-main/feeds/feed.component.scss +++ b/client/src/app/shared/shared-main/feeds/feed.component.scss | |||
@@ -1,7 +1,7 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .video-feed { | 4 | .feed { |
5 | width: min-content; | 5 | width: min-content; |
6 | 6 | ||
7 | a { | 7 | a { |
diff --git a/client/src/app/shared/shared-video-comment/video-comment.model.ts b/client/src/app/shared/shared-video-comment/video-comment.model.ts index 1589091e5..eeee397af 100644 --- a/client/src/app/shared/shared-video-comment/video-comment.model.ts +++ b/client/src/app/shared/shared-video-comment/video-comment.model.ts | |||
@@ -59,12 +59,14 @@ export class VideoCommentAdmin implements VideoCommentAdminServerModel { | |||
59 | createdAt: Date | string | 59 | createdAt: Date | string |
60 | updatedAt: Date | string | 60 | updatedAt: Date | string |
61 | 61 | ||
62 | account: AccountInterface | 62 | account: AccountInterface & { localUrl?: string } |
63 | localUrl: string | ||
63 | 64 | ||
64 | video: { | 65 | video: { |
65 | id: number | 66 | id: number |
66 | uuid: string | 67 | uuid: string |
67 | name: string | 68 | name: string |
69 | localUrl: string | ||
68 | } | 70 | } |
69 | 71 | ||
70 | by: string | 72 | by: string |
@@ -85,14 +87,19 @@ export class VideoCommentAdmin implements VideoCommentAdminServerModel { | |||
85 | this.video = { | 87 | this.video = { |
86 | id: hash.video.id, | 88 | id: hash.video.id, |
87 | uuid: hash.video.uuid, | 89 | uuid: hash.video.uuid, |
88 | name: hash.video.name | 90 | name: hash.video.name, |
91 | localUrl: '/videos/watch/' + hash.video.uuid | ||
89 | } | 92 | } |
90 | 93 | ||
94 | this.localUrl = this.video.localUrl + ';threadId=' + this.threadId | ||
95 | |||
91 | this.account = hash.account | 96 | this.account = hash.account |
92 | 97 | ||
93 | if (this.account) { | 98 | if (this.account) { |
94 | this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host) | 99 | this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host) |
95 | this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account) | 100 | this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account) |
101 | |||
102 | this.account.localUrl = '/accounts/' + this.by | ||
96 | } | 103 | } |
97 | } | 104 | } |
98 | } | 105 | } |
diff --git a/client/src/app/shared/shared-video-comment/video-comment.service.ts b/client/src/app/shared/shared-video-comment/video-comment.service.ts index e318e069d..1ab996a76 100644 --- a/client/src/app/shared/shared-video-comment/video-comment.service.ts +++ b/client/src/app/shared/shared-video-comment/video-comment.service.ts | |||
@@ -19,8 +19,9 @@ import { SortMeta } from 'primeng/api' | |||
19 | 19 | ||
20 | @Injectable() | 20 | @Injectable() |
21 | export class VideoCommentService { | 21 | export class VideoCommentService { |
22 | static BASE_FEEDS_URL = environment.apiUrl + '/feeds/video-comments.' | ||
23 | |||
22 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' | 24 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' |
23 | private static BASE_FEEDS_URL = environment.apiUrl + '/feeds/video-comments.' | ||
24 | 25 | ||
25 | constructor ( | 26 | constructor ( |
26 | private authHttp: HttpClient, | 27 | private authHttp: HttpClient, |
@@ -56,7 +57,7 @@ export class VideoCommentService { | |||
56 | search?: string | 57 | search?: string |
57 | }): Observable<ResultList<VideoCommentAdmin>> { | 58 | }): Observable<ResultList<VideoCommentAdmin>> { |
58 | const { pagination, sort, search } = options | 59 | const { pagination, sort, search } = options |
59 | const url = VideoCommentService.BASE_VIDEO_URL + '/comments' | 60 | const url = VideoCommentService.BASE_VIDEO_URL + 'comments' |
60 | 61 | ||
61 | let params = new HttpParams() | 62 | let params = new HttpParams() |
62 | params = this.restService.addRestGetParams(params, pagination, sort) | 63 | params = this.restService.addRestGetParams(params, pagination, sort) |
@@ -172,7 +173,7 @@ export class VideoCommentService { | |||
172 | 173 | ||
173 | private buildParamsFromSearch (search: string, params: HttpParams) { | 174 | private buildParamsFromSearch (search: string, params: HttpParams) { |
174 | const filters = this.restService.parseQueryStringFilter(search, { | 175 | const filters = this.restService.parseQueryStringFilter(search, { |
175 | state: { | 176 | isLocal: { |
176 | prefix: 'local:', | 177 | prefix: 'local:', |
177 | isBoolean: true, | 178 | isBoolean: true, |
178 | handler: v => { | 179 | handler: v => { |