diff options
author | Chocobozzz <me@florianbigard.com> | 2022-02-28 16:27:25 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-02-28 16:27:25 +0100 |
commit | 5a51ecc2172282786dab47bd874026621554ba6d (patch) | |
tree | 129a6bc40d2f048fd73362e427b75974f040ac5f /client/src/app/+admin/overview | |
parent | f1c70a8666e53414f4e604290d35d26ae725b691 (diff) | |
download | PeerTube-5a51ecc2172282786dab47bd874026621554ba6d.tar.gz PeerTube-5a51ecc2172282786dab47bd874026621554ba6d.tar.zst PeerTube-5a51ecc2172282786dab47bd874026621554ba6d.zip |
Move admin comments list in overviews menu
Diffstat (limited to 'client/src/app/+admin/overview')
9 files changed, 390 insertions, 6 deletions
diff --git a/client/src/app/+admin/overview/comments/index.ts b/client/src/app/+admin/overview/comments/index.ts new file mode 100644 index 000000000..c487f7a81 --- /dev/null +++ b/client/src/app/+admin/overview/comments/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './video-comment-list.component' | ||
2 | export * from './video-comment.routes' | ||
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.html b/client/src/app/+admin/overview/comments/video-comment-list.component.html new file mode 100644 index 000000000..0dbbbe1cc --- /dev/null +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.html | |||
@@ -0,0 +1,111 @@ | |||
1 | <h1> | ||
2 | <my-global-icon iconName="message-circle" aria-hidden="true"></my-global-icon> | ||
3 | <ng-container i18n>Video comments</ng-container> | ||
4 | |||
5 | <my-feed [syndicationItems]="syndicationItems"></my-feed> | ||
6 | </h1> | ||
7 | |||
8 | <em i18n>This view also shows comments from muted accounts.</em> | ||
9 | |||
10 | <p-table | ||
11 | [value]="comments" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions" | ||
12 | [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" | ||
13 | [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" | ||
14 | [showCurrentPageReport]="true" i18n-currentPageReportTemplate | ||
15 | currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} comments" | ||
16 | [expandedRowKeys]="expandedRows" [(selection)]="selectedComments" | ||
17 | > | ||
18 | <ng-template pTemplate="caption"> | ||
19 | <div class="caption"> | ||
20 | <div> | ||
21 | <my-action-dropdown | ||
22 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | ||
23 | [actions]="bulkCommentActions" [entry]="selectedComments" | ||
24 | > | ||
25 | </my-action-dropdown> | ||
26 | </div> | ||
27 | |||
28 | <div class="ml-auto"> | ||
29 | <my-advanced-input-filter [filters]="inputFilters" (search)="onSearch($event)"></my-advanced-input-filter> | ||
30 | </div> | ||
31 | </div> | ||
32 | </ng-template> | ||
33 | |||
34 | <ng-template pTemplate="header"> | ||
35 | <tr> | ||
36 | <th style="width: 40px;"> | ||
37 | <p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox> | ||
38 | </th> | ||
39 | <th style="width: 40px;"></th> | ||
40 | <th style="width: 150px;"></th> | ||
41 | <th style="width: 300px;" i18n>Account</th> | ||
42 | <th style="width: 300px;" i18n>Video</th> | ||
43 | <th i18n>Comment</th> | ||
44 | <th style="width: 150px;" i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
45 | </tr> | ||
46 | </ng-template> | ||
47 | |||
48 | <ng-template pTemplate="body" let-videoComment let-expanded="expanded"> | ||
49 | <tr [pSelectableRow]="videoComment"> | ||
50 | |||
51 | <td class="checkbox-cell"> | ||
52 | <p-tableCheckbox [value]="videoComment" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox> | ||
53 | </td> | ||
54 | |||
55 | <td class="expand-cell" [pRowToggler]="videoComment"> | ||
56 | <my-table-expander-icon i18n-ngbTooltip ngbTooltip="See full comment" [expanded]="expanded"></my-table-expander-icon> | ||
57 | </td> | ||
58 | |||
59 | <td class="action-cell"> | ||
60 | <my-action-dropdown | ||
61 | [ngClass]="{ 'show': expanded }" placement="bottom-right" container="body" | ||
62 | i18n-label label="Actions" [actions]="videoCommentActions" [entry]="videoComment" | ||
63 | ></my-action-dropdown> | ||
64 | </td> | ||
65 | |||
66 | <td> | ||
67 | <a [href]="videoComment.account.localUrl" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer"> | ||
68 | <div class="chip two-lines"> | ||
69 | <my-actor-avatar [account]="videoComment.account" size="32"></my-actor-avatar> | ||
70 | <div> | ||
71 | {{ videoComment.account.displayName }} | ||
72 | <span>{{ videoComment.by }}</span> | ||
73 | </div> | ||
74 | </div> | ||
75 | </a> | ||
76 | </td> | ||
77 | |||
78 | <td class="video"> | ||
79 | <em i18n>Commented video</em> | ||
80 | |||
81 | <a [href]="videoComment.localUrl" target="_blank" rel="noopener noreferrer">{{ videoComment.video.name }}</a> | ||
82 | </td> | ||
83 | |||
84 | <td class="comment-html c-hand" [pRowToggler]="videoComment"> | ||
85 | <div [innerHTML]="videoComment.textHtml"></div> | ||
86 | </td> | ||
87 | |||
88 | <td class="c-hand" [pRowToggler]="videoComment">{{ videoComment.createdAt | date: 'short' }}</td> | ||
89 | </tr> | ||
90 | </ng-template> | ||
91 | |||
92 | <ng-template pTemplate="rowexpansion" let-videoComment> | ||
93 | <tr> | ||
94 | <td class="expand-cell" colspan="5"> | ||
95 | <div [innerHTML]="videoComment.textHtml"></div> | ||
96 | </td> | ||
97 | </tr> | ||
98 | </ng-template> | ||
99 | |||
100 | <ng-template pTemplate="emptymessage"> | ||
101 | <tr> | ||
102 | <td colspan="7"> | ||
103 | <div class="no-results"> | ||
104 | <ng-container *ngIf="search" i18n>No comments found matching current filters.</ng-container> | ||
105 | <ng-container *ngIf="!search" i18n>No comments found.</ng-container> | ||
106 | </div> | ||
107 | </td> | ||
108 | </tr> | ||
109 | </ng-template> | ||
110 | </p-table> | ||
111 | |||
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.scss b/client/src/app/+admin/overview/comments/video-comment-list.component.scss new file mode 100644 index 000000000..3cf7b8db6 --- /dev/null +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.scss | |||
@@ -0,0 +1,52 @@ | |||
1 | @use '_mixins' as *; | ||
2 | @use '_variables' as *; | ||
3 | |||
4 | my-feed { | ||
5 | @include margin-left(5px); | ||
6 | |||
7 | display: inline-block; | ||
8 | width: 15px; | ||
9 | } | ||
10 | |||
11 | my-global-icon { | ||
12 | width: 24px; | ||
13 | height: 24px; | ||
14 | } | ||
15 | |||
16 | .video { | ||
17 | display: flex; | ||
18 | flex-direction: column; | ||
19 | |||
20 | em { | ||
21 | font-size: 11px; | ||
22 | } | ||
23 | |||
24 | a { | ||
25 | @include ellipsis; | ||
26 | |||
27 | color: pvar(--mainForegroundColor); | ||
28 | } | ||
29 | } | ||
30 | |||
31 | .comment-html { | ||
32 | ::ng-deep { | ||
33 | > div { | ||
34 | max-height: 22px; | ||
35 | } | ||
36 | |||
37 | div, | ||
38 | p { | ||
39 | @include ellipsis; | ||
40 | } | ||
41 | |||
42 | p { | ||
43 | margin: 0; | ||
44 | } | ||
45 | } | ||
46 | } | ||
47 | |||
48 | @media screen and (max-width: $primeng-breakpoint) { | ||
49 | .video { | ||
50 | align-items: flex-start !important; | ||
51 | } | ||
52 | } | ||
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.ts b/client/src/app/+admin/overview/comments/video-comment-list.component.ts new file mode 100644 index 000000000..25fe65133 --- /dev/null +++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts | |||
@@ -0,0 +1,186 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { Component, OnInit } from '@angular/core' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { AuthService, ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' | ||
5 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | ||
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' | ||
9 | import { FeedFormat, UserRight } from '@shared/models' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-video-comment-list', | ||
13 | templateUrl: './video-comment-list.component.html', | ||
14 | styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ] | ||
15 | }) | ||
16 | export class VideoCommentListComponent extends RestTable implements OnInit { | ||
17 | comments: VideoCommentAdmin[] | ||
18 | totalRecords = 0 | ||
19 | sort: SortMeta = { field: 'createdAt', order: -1 } | ||
20 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
21 | |||
22 | videoCommentActions: DropdownAction<VideoCommentAdmin>[][] = [] | ||
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 | selectedComments: VideoCommentAdmin[] = [] | ||
43 | bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = [] | ||
44 | |||
45 | inputFilters: AdvancedInputFilter[] = [ | ||
46 | { | ||
47 | title: $localize`Advanced filters`, | ||
48 | children: [ | ||
49 | { | ||
50 | value: 'local:true', | ||
51 | label: $localize`Local comments` | ||
52 | }, | ||
53 | { | ||
54 | value: 'local:false', | ||
55 | label: $localize`Remote comments` | ||
56 | } | ||
57 | ] | ||
58 | } | ||
59 | ] | ||
60 | |||
61 | get authUser () { | ||
62 | return this.auth.getUser() | ||
63 | } | ||
64 | |||
65 | constructor ( | ||
66 | protected router: Router, | ||
67 | protected route: ActivatedRoute, | ||
68 | private auth: AuthService, | ||
69 | private notifier: Notifier, | ||
70 | private confirmService: ConfirmService, | ||
71 | private videoCommentService: VideoCommentService, | ||
72 | private markdownRenderer: MarkdownService, | ||
73 | private bulkService: BulkService | ||
74 | ) { | ||
75 | super() | ||
76 | |||
77 | this.videoCommentActions = [ | ||
78 | [ | ||
79 | { | ||
80 | label: $localize`Delete this comment`, | ||
81 | handler: comment => this.deleteComment(comment), | ||
82 | isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) | ||
83 | }, | ||
84 | |||
85 | { | ||
86 | label: $localize`Delete all comments of this account`, | ||
87 | description: $localize`Comments are deleted after a few minutes`, | ||
88 | handler: comment => this.deleteUserComments(comment), | ||
89 | isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) | ||
90 | } | ||
91 | ] | ||
92 | ] | ||
93 | } | ||
94 | |||
95 | ngOnInit () { | ||
96 | this.initialize() | ||
97 | |||
98 | this.bulkCommentActions = [ | ||
99 | { | ||
100 | label: $localize`Delete`, | ||
101 | handler: comments => this.removeComments(comments), | ||
102 | isDisplayed: () => this.authUser.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT), | ||
103 | iconName: 'delete' | ||
104 | } | ||
105 | ] | ||
106 | } | ||
107 | |||
108 | getIdentifier () { | ||
109 | return 'VideoCommentListComponent' | ||
110 | } | ||
111 | |||
112 | toHtml (text: string) { | ||
113 | return this.markdownRenderer.textMarkdownToHTML(text, true, true) | ||
114 | } | ||
115 | |||
116 | isInSelectionMode () { | ||
117 | return this.selectedComments.length !== 0 | ||
118 | } | ||
119 | |||
120 | protected reloadData () { | ||
121 | this.videoCommentService.getAdminVideoComments({ | ||
122 | pagination: this.pagination, | ||
123 | sort: this.sort, | ||
124 | search: this.search | ||
125 | }).subscribe({ | ||
126 | next: async resultList => { | ||
127 | this.totalRecords = resultList.total | ||
128 | |||
129 | this.comments = [] | ||
130 | |||
131 | for (const c of resultList.data) { | ||
132 | this.comments.push( | ||
133 | new VideoCommentAdmin(c, await this.toHtml(c.text)) | ||
134 | ) | ||
135 | } | ||
136 | }, | ||
137 | |||
138 | error: err => this.notifier.error(err.message) | ||
139 | }) | ||
140 | } | ||
141 | |||
142 | private removeComments (comments: VideoCommentAdmin[]) { | ||
143 | const commentArgs = comments.map(c => ({ videoId: c.video.id, commentId: c.id })) | ||
144 | |||
145 | this.videoCommentService.deleteVideoComments(commentArgs) | ||
146 | .subscribe({ | ||
147 | next: () => { | ||
148 | this.notifier.success($localize`${commentArgs.length} comments deleted.`) | ||
149 | this.reloadData() | ||
150 | }, | ||
151 | |||
152 | error: err => this.notifier.error(err.message), | ||
153 | |||
154 | complete: () => this.selectedComments = [] | ||
155 | }) | ||
156 | } | ||
157 | |||
158 | private deleteComment (comment: VideoCommentAdmin) { | ||
159 | this.videoCommentService.deleteVideoComment(comment.video.id, comment.id) | ||
160 | .subscribe({ | ||
161 | next: () => this.reloadData(), | ||
162 | |||
163 | error: err => this.notifier.error(err.message) | ||
164 | }) | ||
165 | } | ||
166 | |||
167 | private async deleteUserComments (comment: VideoCommentAdmin) { | ||
168 | const message = $localize`Do you really want to delete all comments of ${comment.by}?` | ||
169 | const res = await this.confirmService.confirm(message, $localize`Delete`) | ||
170 | if (res === false) return | ||
171 | |||
172 | const options = { | ||
173 | accountName: comment.by, | ||
174 | scope: 'instance' as 'instance' | ||
175 | } | ||
176 | |||
177 | this.bulkService.removeCommentsOf(options) | ||
178 | .subscribe({ | ||
179 | next: () => { | ||
180 | this.notifier.success($localize`Comments of ${options.accountName} will be deleted in a few minutes`) | ||
181 | }, | ||
182 | |||
183 | error: err => this.notifier.error(err.message) | ||
184 | }) | ||
185 | } | ||
186 | } | ||
diff --git a/client/src/app/+admin/overview/comments/video-comment.routes.ts b/client/src/app/+admin/overview/comments/video-comment.routes.ts new file mode 100644 index 000000000..f0bd440ad --- /dev/null +++ b/client/src/app/+admin/overview/comments/video-comment.routes.ts | |||
@@ -0,0 +1,30 @@ | |||
1 | import { Routes } from '@angular/router' | ||
2 | import { UserRightGuard } from '@app/core' | ||
3 | import { UserRight } from '@shared/models' | ||
4 | import { VideoCommentListComponent } from './video-comment-list.component' | ||
5 | |||
6 | export const commentRoutes: Routes = [ | ||
7 | { | ||
8 | path: 'comments', | ||
9 | canActivate: [ UserRightGuard ], | ||
10 | data: { | ||
11 | userRight: UserRight.SEE_ALL_COMMENTS | ||
12 | }, | ||
13 | children: [ | ||
14 | { | ||
15 | path: '', | ||
16 | redirectTo: 'list', | ||
17 | pathMatch: 'full' | ||
18 | }, | ||
19 | { | ||
20 | path: 'list', | ||
21 | component: VideoCommentListComponent, | ||
22 | data: { | ||
23 | meta: { | ||
24 | title: $localize`Comments list` | ||
25 | } | ||
26 | } | ||
27 | } | ||
28 | ] | ||
29 | } | ||
30 | ] | ||
diff --git a/client/src/app/+admin/overview/index.ts b/client/src/app/+admin/overview/index.ts index a9c46893f..111360734 100644 --- a/client/src/app/+admin/overview/index.ts +++ b/client/src/app/+admin/overview/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './comments' | ||
1 | export * from './users' | 2 | export * from './users' |
2 | export * from './videos' | 3 | export * from './videos' |
3 | export * from './overview.routes' | 4 | export * from './overview.routes' |
diff --git a/client/src/app/+admin/overview/overview.routes.ts b/client/src/app/+admin/overview/overview.routes.ts index 1e6686d16..72d6835d7 100644 --- a/client/src/app/+admin/overview/overview.routes.ts +++ b/client/src/app/+admin/overview/overview.routes.ts | |||
@@ -1,8 +1,10 @@ | |||
1 | import { Routes } from '@angular/router' | 1 | import { Routes } from '@angular/router' |
2 | import { UsersRoutes } from './users' | 2 | import { commentRoutes } from './comments' |
3 | import { VideosRoutes } from './videos' | 3 | import { usersRoutes } from './users' |
4 | import { videosRoutes } from './videos' | ||
4 | 5 | ||
5 | export const OverviewRoutes: Routes = [ | 6 | export const OverviewRoutes: Routes = [ |
6 | ...UsersRoutes, | 7 | ...commentRoutes, |
7 | ...VideosRoutes | 8 | ...usersRoutes, |
9 | ...videosRoutes | ||
8 | ] | 10 | ] |
diff --git a/client/src/app/+admin/overview/users/users.routes.ts b/client/src/app/+admin/overview/users/users.routes.ts index 8b63f5bc7..c9724e5fb 100644 --- a/client/src/app/+admin/overview/users/users.routes.ts +++ b/client/src/app/+admin/overview/users/users.routes.ts | |||
@@ -4,7 +4,7 @@ import { UserRight } from '@shared/models' | |||
4 | import { UserCreateComponent, UserUpdateComponent } from './user-edit' | 4 | import { UserCreateComponent, UserUpdateComponent } from './user-edit' |
5 | import { UserListComponent } from './user-list' | 5 | import { UserListComponent } from './user-list' |
6 | 6 | ||
7 | export const UsersRoutes: Routes = [ | 7 | export const usersRoutes: Routes = [ |
8 | { | 8 | { |
9 | path: 'users', | 9 | path: 'users', |
10 | canActivate: [ UserRightGuard ], | 10 | canActivate: [ UserRightGuard ], |
diff --git a/client/src/app/+admin/overview/videos/video.routes.ts b/client/src/app/+admin/overview/videos/video.routes.ts index 984df7b82..01cb5b497 100644 --- a/client/src/app/+admin/overview/videos/video.routes.ts +++ b/client/src/app/+admin/overview/videos/video.routes.ts | |||
@@ -3,7 +3,7 @@ import { UserRightGuard } from '@app/core' | |||
3 | import { UserRight } from '@shared/models' | 3 | import { UserRight } from '@shared/models' |
4 | import { VideoListComponent } from './video-list.component' | 4 | import { VideoListComponent } from './video-list.component' |
5 | 5 | ||
6 | export const VideosRoutes: Routes = [ | 6 | export const videosRoutes: Routes = [ |
7 | { | 7 | { |
8 | path: 'videos', | 8 | path: 'videos', |
9 | canActivate: [ UserRightGuard ], | 9 | canActivate: [ UserRightGuard ], |