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.ts16
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html25
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts1
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html2
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html2
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html4
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts19
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html4
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts19
-rw-r--r--client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts2
-rw-r--r--client/src/app/+admin/moderation/index.ts1
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts15
-rw-r--r--client/src/app/+admin/moderation/registration-list/admin-registration.service.ts81
-rw-r--r--client/src/app/+admin/moderation/registration-list/index.ts4
-rw-r--r--client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html74
-rw-r--r--client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss3
-rw-r--r--client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts122
-rw-r--r--client/src/app/+admin/moderation/registration-list/process-registration-validators.ts11
-rw-r--r--client/src/app/+admin/moderation/registration-list/registration-list.component.html135
-rw-r--r--client/src/app/+admin/moderation/registration-list/registration-list.component.scss7
-rw-r--r--client/src/app/+admin/moderation/registration-list/registration-list.component.ts151
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts27
-rw-r--r--client/src/app/+admin/overview/comments/video-comment-list.component.html4
-rw-r--r--client/src/app/+admin/overview/comments/video-comment-list.component.ts15
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.html23
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.scss10
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.ts17
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.html4
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.ts46
-rw-r--r--client/src/app/+admin/shared/shared-admin.module.ts7
-rw-r--r--client/src/app/+admin/shared/user-email-info.component.html13
-rw-r--r--client/src/app/+admin/shared/user-email-info.component.scss10
-rw-r--r--client/src/app/+admin/shared/user-email-info.component.ts20
-rw-r--r--client/src/app/+admin/system/jobs/job.service.ts2
-rw-r--r--client/src/app/+admin/system/jobs/jobs.component.ts4
36 files changed, 773 insertions, 139 deletions
diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts
index 746549555..630bfe253 100644
--- a/client/src/app/+admin/admin.component.ts
+++ b/client/src/app/+admin/admin.component.ts
@@ -96,6 +96,14 @@ export class AdminComponent implements OnInit {
96 children: [] 96 children: []
97 } 97 }
98 98
99 if (this.hasRegistrationsRight()) {
100 moderationItems.children.push({
101 label: $localize`Registrations`,
102 routerLink: '/admin/moderation/registrations/list',
103 iconName: 'user'
104 })
105 }
106
99 if (this.hasAbusesRight()) { 107 if (this.hasAbusesRight()) {
100 moderationItems.children.push({ 108 moderationItems.children.push({
101 label: $localize`Reports`, 109 label: $localize`Reports`,
@@ -229,4 +237,8 @@ export class AdminComponent implements OnInit {
229 private hasVideosRight () { 237 private hasVideosRight () {
230 return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS) 238 return this.auth.getUser().hasRight(UserRight.SEE_ALL_VIDEOS)
231 } 239 }
240
241 private hasRegistrationsRight () {
242 return this.auth.getUser().hasRight(UserRight.MANAGE_REGISTRATIONS)
243 }
232} 244}
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index f01967ea6..891ff4ed1 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -30,7 +30,13 @@ import { FollowersListComponent, FollowModalComponent, VideoRedundanciesListComp
30import { FollowingListComponent } from './follows/following-list/following-list.component' 30import { FollowingListComponent } from './follows/following-list/following-list.component'
31import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' 31import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
32import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' 32import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
33import { AbuseListComponent, VideoBlockListComponent } from './moderation' 33import {
34 AbuseListComponent,
35 AdminRegistrationService,
36 ProcessRegistrationModalComponent,
37 RegistrationListComponent,
38 VideoBlockListComponent
39} from './moderation'
34import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' 40import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
35import { 41import {
36 UserCreateComponent, 42 UserCreateComponent,
@@ -116,7 +122,10 @@ import { JobsComponent } from './system/jobs/jobs.component'
116 EditLiveConfigurationComponent, 122 EditLiveConfigurationComponent,
117 EditAdvancedConfigurationComponent, 123 EditAdvancedConfigurationComponent,
118 EditInstanceInformationComponent, 124 EditInstanceInformationComponent,
119 EditHomepageComponent 125 EditHomepageComponent,
126
127 RegistrationListComponent,
128 ProcessRegistrationModalComponent
120 ], 129 ],
121 130
122 exports: [ 131 exports: [
@@ -130,7 +139,8 @@ import { JobsComponent } from './system/jobs/jobs.component'
130 ConfigService, 139 ConfigService,
131 PluginApiService, 140 PluginApiService,
132 EditConfigurationService, 141 EditConfigurationService,
133 VideoAdminService 142 VideoAdminService,
143 AdminRegistrationService
134 ] 144 ]
135}) 145})
136export class AdminModule { } 146export class AdminModule { }
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
index 43f1438e0..0f3803f97 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
@@ -44,9 +44,13 @@
44 44
45 <div class="peertube-select-container"> 45 <div class="peertube-select-container">
46 <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control"> 46 <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
47 <option i18n value="publishedAt">Recently added videos</option>
48 <option i18n value="originallyPublishedAt">Original publication date</option>
49 <option i18n value="name">Name</option>
47 <option i18n value="hot">Hot videos</option> 50 <option i18n value="hot">Hot videos</option>
48 <option i18n value="most-viewed">Most viewed videos</option> 51 <option i18n value="most-viewed">Recent views</option>
49 <option i18n value="most-liked">Most liked videos</option> 52 <option i18n value="most-liked">Most liked videos</option>
53 <option i18n value="views">Global views</option>
50 </select> 54 </select>
51 </div> 55 </div>
52 56
@@ -167,12 +171,21 @@
167 </ng-container> 171 </ng-container>
168 172
169 <ng-container ngProjectAs="extra"> 173 <ng-container ngProjectAs="extra">
170 <my-peertube-checkbox [ngClass]="getDisabledSignupClass()" 174 <div class="form-group">
171 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification" 175 <my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
172 i18n-labelText labelText="Signup requires email verification" 176 inputName="signupRequiresApproval" formControlName="requiresApproval"
173 ></my-peertube-checkbox> 177 i18n-labelText labelText="Signup requires approval by moderators"
178 ></my-peertube-checkbox>
179 </div>
174 180
175 <div [ngClass]="getDisabledSignupClass()" class="mt-3"> 181 <div class="form-group">
182 <my-peertube-checkbox [ngClass]="getDisabledSignupClass()"
183 inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
184 i18n-labelText labelText="Signup requires email verification"
185 ></my-peertube-checkbox>
186 </div>
187
188 <div [ngClass]="getDisabledSignupClass()">
176 <label i18n for="signupLimit">Signup limit</label> 189 <label i18n for="signupLimit">Signup limit</label>
177 190
178 <div class="number-with-unit"> 191 <div class="number-with-unit">
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 168f4702c..2afe80a03 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -132,6 +132,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
132 signup: { 132 signup: {
133 enabled: null, 133 enabled: null,
134 limit: SIGNUP_LIMIT_VALIDATOR, 134 limit: SIGNUP_LIMIT_VALIDATOR,
135 requiresApproval: null,
135 requiresEmailVerification: null, 136 requiresEmailVerification: null,
136 minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR 137 minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR
137 }, 138 },
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
index 5339240bb..3d8414f5c 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
@@ -17,7 +17,7 @@
17 17
18 <my-markdown-textarea 18 <my-markdown-textarea
19 name="instanceCustomHomepageContent" formControlName="content" 19 name="instanceCustomHomepageContent" formControlName="content"
20 [customMarkdownRenderer]="getCustomMarkdownRenderer()" 20 [customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
21 [formError]="formErrors['instanceCustomHomepage.content']" 21 [formError]="formErrors['instanceCustomHomepage.content']"
22 ></my-markdown-textarea> 22 ></my-markdown-textarea>
23 23
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
index b54733327..504afa189 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-instance-information.component.html
@@ -38,7 +38,7 @@
38 38
39 <my-markdown-textarea 39 <my-markdown-textarea
40 name="instanceDescription" formControlName="description" 40 name="instanceDescription" formControlName="description"
41 [customMarkdownRenderer]="getCustomMarkdownRenderer()" 41 [customMarkdownRenderer]="getCustomMarkdownRenderer()" [debounceTime]="500"
42 [formError]="formErrors['instance.description']" 42 [formError]="formErrors['instance.description']"
43 ></my-markdown-textarea> 43 ></my-markdown-textarea>
44 </div> 44 </div>
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html
index 8fe0d2348..14c62f1af 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.html
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html
@@ -9,14 +9,14 @@
9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" 9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers" 11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} followers"
12 [(selection)]="selectedFollows" 12 [(selection)]="selectedRows"
13> 13>
14 <ng-template pTemplate="caption"> 14 <ng-template pTemplate="caption">
15 <div class="caption"> 15 <div class="caption">
16 <div class="left-buttons"> 16 <div class="left-buttons">
17 <my-action-dropdown 17 <my-action-dropdown
18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" 18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
19 [actions]="bulkFollowsActions" [entry]="selectedFollows" 19 [actions]="bulkActions" [entry]="selectedRows"
20 > 20 >
21 </my-action-dropdown> 21 </my-action-dropdown>
22 </div> 22 </div>
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
index b2d333e83..cebb2e1a2 100644
--- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts
+++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts
@@ -12,7 +12,7 @@ import { ActorFollow } from '@shared/models'
12 templateUrl: './followers-list.component.html', 12 templateUrl: './followers-list.component.html',
13 styleUrls: [ './followers-list.component.scss' ] 13 styleUrls: [ './followers-list.component.scss' ]
14}) 14})
15export class FollowersListComponent extends RestTable implements OnInit { 15export class FollowersListComponent extends RestTable <ActorFollow> implements OnInit {
16 followers: ActorFollow[] = [] 16 followers: ActorFollow[] = []
17 totalRecords = 0 17 totalRecords = 0
18 sort: SortMeta = { field: 'createdAt', order: -1 } 18 sort: SortMeta = { field: 'createdAt', order: -1 }
@@ -20,8 +20,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
20 20
21 searchFilters: AdvancedInputFilter[] = [] 21 searchFilters: AdvancedInputFilter[] = []
22 22
23 selectedFollows: ActorFollow[] = [] 23 bulkActions: DropdownAction<ActorFollow[]>[] = []
24 bulkFollowsActions: DropdownAction<ActorFollow[]>[] = []
25 24
26 constructor ( 25 constructor (
27 private confirmService: ConfirmService, 26 private confirmService: ConfirmService,
@@ -36,7 +35,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
36 35
37 this.searchFilters = this.followService.buildFollowsListFilters() 36 this.searchFilters = this.followService.buildFollowsListFilters()
38 37
39 this.bulkFollowsActions = [ 38 this.bulkActions = [
40 { 39 {
41 label: $localize`Reject`, 40 label: $localize`Reject`,
42 handler: follows => this.rejectFollower(follows), 41 handler: follows => this.rejectFollower(follows),
@@ -105,12 +104,14 @@ export class FollowersListComponent extends RestTable implements OnInit {
105 } 104 }
106 105
107 async deleteFollowers (follows: ActorFollow[]) { 106 async deleteFollowers (follows: ActorFollow[]) {
107 const icuParams = { count: follows.length, followerName: this.buildFollowerName(follows[0]) }
108
108 let message = $localize`Deleted followers will be able to send again a follow request.` 109 let message = $localize`Deleted followers will be able to send again a follow request.`
109 message += '<br /><br />' 110 message += '<br /><br />'
110 111
111 // eslint-disable-next-line max-len 112 // eslint-disable-next-line max-len
112 message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)( 113 message += prepareIcu($localize`Do you really want to delete {count, plural, =1 {{followerName} follow request?} other {{count} follow requests?}}`)(
113 { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, 114 icuParams,
114 $localize`Do you really want to delete these follow requests?` 115 $localize`Do you really want to delete these follow requests?`
115 ) 116 )
116 117
@@ -122,7 +123,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
122 next: () => { 123 next: () => {
123 // eslint-disable-next-line max-len 124 // eslint-disable-next-line max-len
124 const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)( 125 const message = prepareIcu($localize`Removed {count, plural, =1 {{followerName} follow request} other {{count} follow requests}}`)(
125 { count: follows.length, followerName: this.buildFollowerName(follows[0]) }, 126 icuParams,
126 $localize`Follow requests removed` 127 $localize`Follow requests removed`
127 ) 128 )
128 129
@@ -139,11 +140,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
139 return follow.follower.name + '@' + follow.follower.host 140 return follow.follower.name + '@' + follow.follower.host
140 } 141 }
141 142
142 isInSelectionMode () { 143 protected reloadDataInternal () {
143 return this.selectedFollows.length !== 0
144 }
145
146 protected reloadData () {
147 this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search }) 144 this.followService.getFollowers({ pagination: this.pagination, sort: this.sort, search: this.search })
148 .subscribe({ 145 .subscribe({
149 next: resultList => { 146 next: resultList => {
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html
index f7abb7ede..eca79be71 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.html
+++ b/client/src/app/+admin/follows/following-list/following-list.component.html
@@ -9,14 +9,14 @@
9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" 9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts" 11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} hosts"
12 [(selection)]="selectedFollows" 12 [(selection)]="selectedRows"
13> 13>
14 <ng-template pTemplate="caption"> 14 <ng-template pTemplate="caption">
15 <div class="caption"> 15 <div class="caption">
16 <div class="left-buttons"> 16 <div class="left-buttons">
17 <my-action-dropdown 17 <my-action-dropdown
18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" 18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
19 [actions]="bulkFollowsActions" [entry]="selectedFollows" 19 [actions]="bulkActions" [entry]="selectedRows"
20 > 20 >
21 </my-action-dropdown> 21 </my-action-dropdown>
22 22
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts
index e3a56651a..71f2fbe66 100644
--- a/client/src/app/+admin/follows/following-list/following-list.component.ts
+++ b/client/src/app/+admin/follows/following-list/following-list.component.ts
@@ -12,7 +12,7 @@ import { prepareIcu } from '@app/helpers'
12 templateUrl: './following-list.component.html', 12 templateUrl: './following-list.component.html',
13 styleUrls: [ './following-list.component.scss' ] 13 styleUrls: [ './following-list.component.scss' ]
14}) 14})
15export class FollowingListComponent extends RestTable implements OnInit { 15export class FollowingListComponent extends RestTable <ActorFollow> implements OnInit {
16 @ViewChild('followModal') followModal: FollowModalComponent 16 @ViewChild('followModal') followModal: FollowModalComponent
17 17
18 following: ActorFollow[] = [] 18 following: ActorFollow[] = []
@@ -22,8 +22,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
22 22
23 searchFilters: AdvancedInputFilter[] = [] 23 searchFilters: AdvancedInputFilter[] = []
24 24
25 selectedFollows: ActorFollow[] = [] 25 bulkActions: DropdownAction<ActorFollow[]>[] = []
26 bulkFollowsActions: DropdownAction<ActorFollow[]>[] = []
27 26
28 constructor ( 27 constructor (
29 private notifier: Notifier, 28 private notifier: Notifier,
@@ -38,7 +37,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
38 37
39 this.searchFilters = this.followService.buildFollowsListFilters() 38 this.searchFilters = this.followService.buildFollowsListFilters()
40 39
41 this.bulkFollowsActions = [ 40 this.bulkActions = [
42 { 41 {
43 label: $localize`Delete`, 42 label: $localize`Delete`,
44 handler: follows => this.removeFollowing(follows) 43 handler: follows => this.removeFollowing(follows)
@@ -58,17 +57,15 @@ export class FollowingListComponent extends RestTable implements OnInit {
58 return follow.following.name === 'peertube' 57 return follow.following.name === 'peertube'
59 } 58 }
60 59
61 isInSelectionMode () {
62 return this.selectedFollows.length !== 0
63 }
64
65 buildFollowingName (follow: ActorFollow) { 60 buildFollowingName (follow: ActorFollow) {
66 return follow.following.name + '@' + follow.following.host 61 return follow.following.name + '@' + follow.following.host
67 } 62 }
68 63
69 async removeFollowing (follows: ActorFollow[]) { 64 async removeFollowing (follows: ActorFollow[]) {
65 const icuParams = { count: follows.length, entryName: this.buildFollowingName(follows[0]) }
66
70 const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)( 67 const message = prepareIcu($localize`Do you really want to unfollow {count, plural, =1 {{entryName}?} other {{count} entries?}}`)(
71 { count: follows.length, entryName: this.buildFollowingName(follows[0]) }, 68 icuParams,
72 $localize`Do you really want to unfollow these entries?` 69 $localize`Do you really want to unfollow these entries?`
73 ) 70 )
74 71
@@ -80,7 +77,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
80 next: () => { 77 next: () => {
81 // eslint-disable-next-line max-len 78 // eslint-disable-next-line max-len
82 const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)( 79 const message = prepareIcu($localize`You are not following {count, plural, =1 {{entryName} anymore.} other {these {count} entries anymore.}}`)(
83 { count: follows.length, entryName: this.buildFollowingName(follows[0]) }, 80 icuParams,
84 $localize`You are not following them anymore.` 81 $localize`You are not following them anymore.`
85 ) 82 )
86 83
@@ -92,7 +89,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
92 }) 89 })
93 } 90 }
94 91
95 protected reloadData () { 92 protected reloadDataInternal () {
96 this.followService.getFollowing({ pagination: this.pagination, sort: this.sort, search: this.search }) 93 this.followService.getFollowing({ pagination: this.pagination, sort: this.sort, search: this.search })
97 .subscribe({ 94 .subscribe({
98 next: resultList => { 95 next: resultList => {
diff --git a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts
index a89603048..b31c5b35e 100644
--- a/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts
+++ b/client/src/app/+admin/follows/video-redundancies-list/video-redundancies-list.component.ts
@@ -162,7 +162,7 @@ export class VideoRedundanciesListComponent extends RestTable implements OnInit
162 162
163 } 163 }
164 164
165 protected reloadData () { 165 protected reloadDataInternal () {
166 const options = { 166 const options = {
167 pagination: this.pagination, 167 pagination: this.pagination,
168 sort: this.sort, 168 sort: this.sort,
diff --git a/client/src/app/+admin/moderation/index.ts b/client/src/app/+admin/moderation/index.ts
index 9dab270cc..135b4b408 100644
--- a/client/src/app/+admin/moderation/index.ts
+++ b/client/src/app/+admin/moderation/index.ts
@@ -1,4 +1,5 @@
1export * from './abuse-list' 1export * from './abuse-list'
2export * from './instance-blocklist' 2export * from './instance-blocklist'
3export * from './video-block-list' 3export * from './video-block-list'
4export * from './registration-list'
4export * from './moderation.routes' 5export * from './moderation.routes'
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts
index 1ad301039..378d2bed7 100644
--- a/client/src/app/+admin/moderation/moderation.routes.ts
+++ b/client/src/app/+admin/moderation/moderation.routes.ts
@@ -4,6 +4,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
4import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' 4import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
5import { UserRightGuard } from '@app/core' 5import { UserRightGuard } from '@app/core'
6import { UserRight } from '@shared/models' 6import { UserRight } from '@shared/models'
7import { RegistrationListComponent } from './registration-list'
7 8
8export const ModerationRoutes: Routes = [ 9export const ModerationRoutes: Routes = [
9 { 10 {
@@ -68,7 +69,19 @@ export const ModerationRoutes: Routes = [
68 } 69 }
69 }, 70 },
70 71
71 // We move this component in admin overview pages 72 {
73 path: 'registrations/list',
74 component: RegistrationListComponent,
75 canActivate: [ UserRightGuard ],
76 data: {
77 userRight: UserRight.MANAGE_REGISTRATIONS,
78 meta: {
79 title: $localize`User registrations`
80 }
81 }
82 },
83
84 // We moved this component in admin overview pages
72 { 85 {
73 path: 'video-comments', 86 path: 'video-comments',
74 redirectTo: 'video-comments/list', 87 redirectTo: 'video-comments/list',
diff --git a/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts b/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts
new file mode 100644
index 000000000..a9f13cf2f
--- /dev/null
+++ b/client/src/app/+admin/moderation/registration-list/admin-registration.service.ts
@@ -0,0 +1,81 @@
1import { SortMeta } from 'primeng/api'
2import { from } from 'rxjs'
3import { catchError, concatMap, toArray } from 'rxjs/operators'
4import { HttpClient, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core'
6import { RestExtractor, RestPagination, RestService } from '@app/core'
7import { arrayify } from '@shared/core-utils'
8import { ResultList, UserRegistration, UserRegistrationUpdateState } from '@shared/models'
9import { environment } from '../../../../environments/environment'
10
11@Injectable()
12export class AdminRegistrationService {
13 private static BASE_REGISTRATION_URL = environment.apiUrl + '/api/v1/users/registrations'
14
15 constructor (
16 private authHttp: HttpClient,
17 private restExtractor: RestExtractor,
18 private restService: RestService
19 ) { }
20
21 listRegistrations (options: {
22 pagination: RestPagination
23 sort: SortMeta
24 search?: string
25 }) {
26 const { pagination, sort, search } = options
27
28 const url = AdminRegistrationService.BASE_REGISTRATION_URL
29
30 let params = new HttpParams()
31 params = this.restService.addRestGetParams(params, pagination, sort)
32
33 if (search) {
34 params = params.append('search', search)
35 }
36
37 return this.authHttp.get<ResultList<UserRegistration>>(url, { params })
38 .pipe(
39 catchError(res => this.restExtractor.handleError(res))
40 )
41 }
42
43 acceptRegistration (options: {
44 registration: UserRegistration
45 moderationResponse: string
46 preventEmailDelivery: boolean
47 }) {
48 const { registration, moderationResponse, preventEmailDelivery } = options
49
50 const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/accept'
51 const body: UserRegistrationUpdateState = { moderationResponse, preventEmailDelivery }
52
53 return this.authHttp.post(url, body)
54 .pipe(catchError(res => this.restExtractor.handleError(res)))
55 }
56
57 rejectRegistration (options: {
58 registration: UserRegistration
59 moderationResponse: string
60 preventEmailDelivery: boolean
61 }) {
62 const { registration, moderationResponse, preventEmailDelivery } = options
63
64 const url = AdminRegistrationService.BASE_REGISTRATION_URL + '/' + registration.id + '/reject'
65 const body: UserRegistrationUpdateState = { moderationResponse, preventEmailDelivery }
66
67 return this.authHttp.post(url, body)
68 .pipe(catchError(res => this.restExtractor.handleError(res)))
69 }
70
71 removeRegistrations (registrationsArg: UserRegistration | UserRegistration[]) {
72 const registrations = arrayify(registrationsArg)
73
74 return from(registrations)
75 .pipe(
76 concatMap(r => this.authHttp.delete(AdminRegistrationService.BASE_REGISTRATION_URL + '/' + r.id)),
77 toArray(),
78 catchError(err => this.restExtractor.handleError(err))
79 )
80 }
81}
diff --git a/client/src/app/+admin/moderation/registration-list/index.ts b/client/src/app/+admin/moderation/registration-list/index.ts
new file mode 100644
index 000000000..060b676a4
--- /dev/null
+++ b/client/src/app/+admin/moderation/registration-list/index.ts
@@ -0,0 +1,4 @@
1export * from './admin-registration.service'
2export * from './process-registration-modal.component'
3export * from './process-registration-validators'
4export * from './registration-list.component'
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html
new file mode 100644
index 000000000..8e46b0cf9
--- /dev/null
+++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.html
@@ -0,0 +1,74 @@
1<ng-template #modal>
2 <div class="modal-header">
3 <h4 i18n class="modal-title">
4 <ng-container *ngIf="isAccept()">Accept {{ registration.username }} registration</ng-container>
5 <ng-container *ngIf="isReject()">Reject {{ registration.username }} registration</ng-container>
6 </h4>
7
8 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
9 </div>
10
11 <form novalidate [formGroup]="form" (ngSubmit)="processRegistration()">
12 <div class="modal-body mb-3">
13
14 <div i18n *ngIf="!registration.emailVerified" class="alert alert-warning">
15 Registration email has not been verified. Email delivery has been disabled by default.
16 </div>
17
18 <div class="description">
19 <ng-container *ngIf="isAccept()">
20 <p i18n>
21 <strong>Accepting</strong>&nbsp;<em>{{ registration.username }}</em> registration will create the account and channel.
22 </p>
23
24 <p *ngIf="isEmailEnabled()" i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }">
25 An email will be sent to <em>{{ registration.email }}</em> explaining its account has been created with the moderation response you'll write below.
26 </p>
27
28 <div *ngIf="!isEmailEnabled()" class="alert alert-warning" i18n>
29 Emails are not enabled on this instance so PeerTube won't be able to send an email to <em>{{ registration.email }}</em> explaining its account has been created.
30 </div>
31 </ng-container>
32
33 <ng-container *ngIf="isReject()">
34 <p i18n [ngClass]="{ 'text-decoration-line-through': isPreventEmailDeliveryChecked() }">
35 An email will be sent to <em>{{ registration.email }}</em> explaining its registration request has been <strong>rejected</strong> with the moderation response you'll write below.
36 </p>
37
38 <div *ngIf="!isEmailEnabled()" class="alert alert-warning" i18n>
39 Emails are not enabled on this instance so PeerTube won't be able to send an email to <em>{{ registration.email }}</em> explaining its registration request has been rejected.
40 </div>
41 </ng-container>
42 </div>
43
44 <div class="form-group">
45 <label for="moderationResponse" i18n>Send a message to the user</label>
46
47 <textarea
48 formControlName="moderationResponse" ngbAutofocus name="moderationResponse" id="moderationResponse"
49 [ngClass]="{ 'input-error': formErrors['moderationResponse'] }" class="form-control"
50 ></textarea>
51
52 <div *ngIf="formErrors.moderationResponse" class="form-error">
53 {{ formErrors.moderationResponse }}
54 </div>
55 </div>
56
57 <div class="form-group">
58 <my-peertube-checkbox
59 inputName="preventEmailDelivery" formControlName="preventEmailDelivery" [disabled]="!isEmailEnabled()"
60 i18n-labelText labelText="Prevent email from being sent to the user"
61 ></my-peertube-checkbox>
62 </div>
63 </div>
64
65 <div class="modal-footer inputs">
66 <input
67 type="button" role="button" i18n-value value="Cancel" class="peertube-button grey-button"
68 (click)="hide()" (key.enter)="hide()"
69 >
70
71 <input type="submit" [value]="getSubmitValue()" class="peertube-button orange-button" [disabled]="!form.valid">
72 </div>
73 </form>
74</ng-template>
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss
new file mode 100644
index 000000000..3e03bed89
--- /dev/null
+++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.scss
@@ -0,0 +1,3 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts
new file mode 100644
index 000000000..3a7e5dea1
--- /dev/null
+++ b/client/src/app/+admin/moderation/registration-list/process-registration-modal.component.ts
@@ -0,0 +1,122 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier, ServerService } from '@app/core'
3import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { UserRegistration } from '@shared/models'
7import { AdminRegistrationService } from './admin-registration.service'
8import { REGISTRATION_MODERATION_RESPONSE_VALIDATOR } from './process-registration-validators'
9
10@Component({
11 selector: 'my-process-registration-modal',
12 templateUrl: './process-registration-modal.component.html',
13 styleUrls: [ './process-registration-modal.component.scss' ]
14})
15export class ProcessRegistrationModalComponent extends FormReactive implements OnInit {
16 @ViewChild('modal', { static: true }) modal: NgbModal
17
18 @Output() registrationProcessed = new EventEmitter()
19
20 registration: UserRegistration
21
22 private openedModal: NgbModalRef
23 private processMode: 'accept' | 'reject'
24
25 constructor (
26 protected formReactiveService: FormReactiveService,
27 private server: ServerService,
28 private modalService: NgbModal,
29 private notifier: Notifier,
30 private registrationService: AdminRegistrationService
31 ) {
32 super()
33 }
34
35 ngOnInit () {
36 this.buildForm({
37 moderationResponse: REGISTRATION_MODERATION_RESPONSE_VALIDATOR,
38 preventEmailDelivery: null
39 })
40 }
41
42 isAccept () {
43 return this.processMode === 'accept'
44 }
45
46 isReject () {
47 return this.processMode === 'reject'
48 }
49
50 openModal (registration: UserRegistration, mode: 'accept' | 'reject') {
51 this.processMode = mode
52 this.registration = registration
53
54 this.form.patchValue({
55 preventEmailDelivery: !this.isEmailEnabled() || registration.emailVerified !== true
56 })
57
58 this.openedModal = this.modalService.open(this.modal, { centered: true })
59 }
60
61 hide () {
62 this.form.reset()
63
64 this.openedModal.close()
65 }
66
67 getSubmitValue () {
68 if (this.isAccept()) {
69 return $localize`Accept registration`
70 }
71
72 return $localize`Reject registration`
73 }
74
75 processRegistration () {
76 if (this.isAccept()) return this.acceptRegistration()
77
78 return this.rejectRegistration()
79 }
80
81 isEmailEnabled () {
82 return this.server.getHTMLConfig().email.enabled
83 }
84
85 isPreventEmailDeliveryChecked () {
86 return this.form.value.preventEmailDelivery
87 }
88
89 private acceptRegistration () {
90 this.registrationService.acceptRegistration({
91 registration: this.registration,
92 moderationResponse: this.form.value.moderationResponse,
93 preventEmailDelivery: this.form.value.preventEmailDelivery
94 }).subscribe({
95 next: () => {
96 this.notifier.success($localize`${this.registration.username} account created`)
97
98 this.registrationProcessed.emit()
99 this.hide()
100 },
101
102 error: err => this.notifier.error(err.message)
103 })
104 }
105
106 private rejectRegistration () {
107 this.registrationService.rejectRegistration({
108 registration: this.registration,
109 moderationResponse: this.form.value.moderationResponse,
110 preventEmailDelivery: this.form.value.preventEmailDelivery
111 }).subscribe({
112 next: () => {
113 this.notifier.success($localize`${this.registration.username} registration rejected`)
114
115 this.registrationProcessed.emit()
116 this.hide()
117 },
118
119 error: err => this.notifier.error(err.message)
120 })
121 }
122}
diff --git a/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts b/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts
new file mode 100644
index 000000000..e01a07d9d
--- /dev/null
+++ b/client/src/app/+admin/moderation/registration-list/process-registration-validators.ts
@@ -0,0 +1,11 @@
1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from '@app/shared/form-validators'
3
4export const REGISTRATION_MODERATION_RESPONSE_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
6 MESSAGES: {
7 required: $localize`Moderation response is required.`,
8 minlength: $localize`Moderation response must be at least 2 characters long.`,
9 maxlength: $localize`Moderation response cannot be more than 3000 characters long.`
10 }
11}
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.html b/client/src/app/+admin/moderation/registration-list/registration-list.component.html
new file mode 100644
index 000000000..a2b888101
--- /dev/null
+++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.html
@@ -0,0 +1,135 @@
1<h1>
2 <my-global-icon iconName="user" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>Registration requests</ng-container>
4</h1>
5
6<p-table
7 [value]="registrations" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
8 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id"
9 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false"
10 [(selection)]="selectedRows" [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} registrations"
12 [expandedRowKeys]="expandedRows"
13>
14 <ng-template pTemplate="caption">
15 <div class="caption">
16 <div class="left-buttons">
17 <my-action-dropdown
18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
19 [actions]="bulkActions" [entry]="selectedRows"
20 >
21 </my-action-dropdown>
22 </div>
23
24 <div class="ms-auto">
25 <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
26 </div>
27 </div>
28 </ng-template>
29
30 <ng-template pTemplate="header">
31 <tr> <!-- header -->
32 <th style="width: 40px">
33 <p-tableHeaderCheckbox ariaLabel="Select all rows" i18n-ariaLabel></p-tableHeaderCheckbox>
34 </th>
35 <th style="width: 40px;"></th>
36 <th style="width: 150px;"></th>
37 <th i18n>Account</th>
38 <th i18n>Email</th>
39 <th i18n>Channel</th>
40 <th i18n>Registration reason</th>
41 <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
42 <th i18n>Moderation response</th>
43 <th style="width: 150px;" i18n pSortableColumn="createdAt">Requested on <p-sortIcon field="createdAt"></p-sortIcon></th>
44 </tr>
45 </ng-template>
46
47 <ng-template pTemplate="body" let-expanded="expanded" let-registration>
48 <tr [pSelectableRow]="registration">
49 <td class="checkbox-cell">
50 <p-tableCheckbox [value]="registration" ariaLabel="Select this row" i18n-ariaLabel></p-tableCheckbox>
51 </td>
52
53 <td class="expand-cell" [pRowToggler]="registration">
54 <my-table-expander-icon [expanded]="expanded"></my-table-expander-icon>
55 </td>
56
57 <td class="action-cell">
58 <my-action-dropdown
59 [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
60 i18n-label label="Actions" [actions]="registrationActions" [entry]="registration"
61 ></my-action-dropdown>
62 </td>
63
64 <td>
65 <div class="chip two-lines">
66 <div>
67 <span>{{ registration.username }}</span>
68 <span class="muted">{{ registration.accountDisplayName }}</span>
69 </div>
70 </div>
71 </td>
72
73 <td>
74 <my-user-email-info [entry]="registration" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info>
75 </td>
76
77 <td>
78 <div class="chip two-lines">
79 <div>
80 <span>{{ registration.channelHandle }}</span>
81 <span class="muted">{{ registration.channelDisplayName }}</span>
82 </div>
83 </div>
84 </td>
85
86 <td container="body" placement="left auto" [ngbTooltip]="registration.registrationReason">
87 {{ registration.registrationReason }}
88 </td>
89
90 <td class="c-hand abuse-states" [pRowToggler]="registration">
91 <my-global-icon *ngIf="isRegistrationAccepted(registration)" [title]="registration.state.label" iconName="tick"></my-global-icon>
92 <my-global-icon *ngIf="isRegistrationRejected(registration)" [title]="registration.state.label" iconName="cross"></my-global-icon>
93 </td>
94
95 <td container="body" placement="left auto" [ngbTooltip]="registration.moderationResponse">
96 {{ registration.moderationResponse }}
97 </td>
98
99 <td class="c-hand" [pRowToggler]="registration">{{ registration.createdAt | date: 'short' }}</td>
100 </tr>
101 </ng-template>
102
103 <ng-template pTemplate="rowexpansion" let-registration>
104 <tr>
105 <td colspan="9">
106 <div class="moderation-expanded">
107 <div class="left">
108 <div class="d-flex">
109 <span class="moderation-expanded-label" i18n>Registration reason:</span>
110 <span class="moderation-expanded-text" [innerHTML]="registration.registrationReasonHTML"></span>
111 </div>
112
113 <div *ngIf="registration.moderationResponse">
114 <span class="moderation-expanded-label" i18n>Moderation response:</span>
115 <span class="moderation-expanded-text" [innerHTML]="registration.moderationResponseHTML"></span>
116 </div>
117 </div>
118 </div>
119 </td>
120 </tr>
121 </ng-template>
122
123 <ng-template pTemplate="emptymessage">
124 <tr>
125 <td colspan="9">
126 <div class="no-results">
127 <ng-container *ngIf="search" i18n>No registrations found matching current filters.</ng-container>
128 <ng-container *ngIf="!search" i18n>No registrations found.</ng-container>
129 </div>
130 </td>
131 </tr>
132 </ng-template>
133</p-table>
134
135<my-process-registration-modal #processRegistrationModal (registrationProcessed)="onRegistrationProcessed()"></my-process-registration-modal>
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.scss b/client/src/app/+admin/moderation/registration-list/registration-list.component.scss
new file mode 100644
index 000000000..9cae08e85
--- /dev/null
+++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.scss
@@ -0,0 +1,7 @@
1@use '_mixins' as *;
2@use '_variables' as *;
3
4my-global-icon {
5 width: 24px;
6 height: 24px;
7}
diff --git a/client/src/app/+admin/moderation/registration-list/registration-list.component.ts b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts
new file mode 100644
index 000000000..ed8fbec51
--- /dev/null
+++ b/client/src/app/+admin/moderation/registration-list/registration-list.component.ts
@@ -0,0 +1,151 @@
1import { SortMeta } from 'primeng/api'
2import { Component, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router'
4import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
5import { prepareIcu } from '@app/helpers'
6import { AdvancedInputFilter } from '@app/shared/shared-forms'
7import { DropdownAction } from '@app/shared/shared-main'
8import { UserRegistration, UserRegistrationState } from '@shared/models'
9import { AdminRegistrationService } from './admin-registration.service'
10import { ProcessRegistrationModalComponent } from './process-registration-modal.component'
11
12@Component({
13 selector: 'my-registration-list',
14 templateUrl: './registration-list.component.html',
15 styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './registration-list.component.scss' ]
16})
17export class RegistrationListComponent extends RestTable <UserRegistration> implements OnInit {
18 @ViewChild('processRegistrationModal', { static: true }) processRegistrationModal: ProcessRegistrationModalComponent
19
20 registrations: (UserRegistration & { registrationReasonHTML?: string, moderationResponseHTML?: string })[] = []
21 totalRecords = 0
22 sort: SortMeta = { field: 'createdAt', order: -1 }
23 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
24
25 registrationActions: DropdownAction<UserRegistration>[][] = []
26 bulkActions: DropdownAction<UserRegistration[]>[] = []
27
28 inputFilters: AdvancedInputFilter[] = []
29
30 requiresEmailVerification: boolean
31
32 constructor (
33 protected route: ActivatedRoute,
34 protected router: Router,
35 private server: ServerService,
36 private notifier: Notifier,
37 private markdownRenderer: MarkdownService,
38 private confirmService: ConfirmService,
39 private adminRegistrationService: AdminRegistrationService
40 ) {
41 super()
42
43 this.registrationActions = [
44 [
45 {
46 label: $localize`Accept this request`,
47 handler: registration => this.openRegistrationRequestProcessModal(registration, 'accept'),
48 isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING
49 },
50 {
51 label: $localize`Reject this request`,
52 handler: registration => this.openRegistrationRequestProcessModal(registration, 'reject'),
53 isDisplayed: registration => registration.state.id === UserRegistrationState.PENDING
54 },
55 {
56 label: $localize`Remove this request`,
57 handler: registration => this.removeRegistrations([ registration ])
58 }
59 ]
60 ]
61
62 this.bulkActions = [
63 {
64 label: $localize`Delete`,
65 handler: registrations => this.removeRegistrations(registrations)
66 }
67 ]
68 }
69
70 ngOnInit () {
71 this.initialize()
72
73 this.server.getConfig()
74 .subscribe(config => {
75 this.requiresEmailVerification = config.signup.requiresEmailVerification
76 })
77 }
78
79 getIdentifier () {
80 return 'RegistrationListComponent'
81 }
82
83 isRegistrationAccepted (registration: UserRegistration) {
84 return registration.state.id === UserRegistrationState.ACCEPTED
85 }
86
87 isRegistrationRejected (registration: UserRegistration) {
88 return registration.state.id === UserRegistrationState.REJECTED
89 }
90
91 onRegistrationProcessed () {
92 this.reloadData()
93 }
94
95 protected reloadDataInternal () {
96 this.adminRegistrationService.listRegistrations({
97 pagination: this.pagination,
98 sort: this.sort,
99 search: this.search
100 }).subscribe({
101 next: async resultList => {
102 this.totalRecords = resultList.total
103 this.registrations = resultList.data
104
105 for (const registration of this.registrations) {
106 registration.registrationReasonHTML = await this.toHtml(registration.registrationReason)
107 registration.moderationResponseHTML = await this.toHtml(registration.moderationResponse)
108 }
109 },
110
111 error: err => this.notifier.error(err.message)
112 })
113 }
114
115 private openRegistrationRequestProcessModal (registration: UserRegistration, mode: 'accept' | 'reject') {
116 this.processRegistrationModal.openModal(registration, mode)
117 }
118
119 private async removeRegistrations (registrations: UserRegistration[]) {
120 const icuParams = { count: registrations.length, username: registrations[0].username }
121
122 // eslint-disable-next-line max-len
123 const message = prepareIcu($localize`Do you really want to delete {count, plural, =1 {{username} registration request?} other {{count} registration requests?}}`)(
124 icuParams,
125 $localize`Do you really want to delete these registration requests?`
126 )
127
128 const res = await this.confirmService.confirm(message, $localize`Delete`)
129 if (res === false) return
130
131 this.adminRegistrationService.removeRegistrations(registrations)
132 .subscribe({
133 next: () => {
134 // eslint-disable-next-line max-len
135 const message = prepareIcu($localize`Removed {count, plural, =1 {{username} registration request} other {{count} registration requests}}`)(
136 icuParams,
137 $localize`Registration requests removed`
138 )
139
140 this.notifier.success(message)
141 this.reloadData()
142 },
143
144 error: err => this.notifier.error(err.message)
145 })
146 }
147
148 private toHtml (text: string) {
149 return this.markdownRenderer.textMarkdownToHTML({ markdown: text })
150 }
151}
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
index efd99e52b..f365a2500 100644
--- 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
@@ -159,26 +159,25 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
159 }) 159 })
160 } 160 }
161 161
162 protected reloadData () { 162 protected reloadDataInternal () {
163 this.videoBlocklistService.listBlocks({ 163 this.videoBlocklistService.listBlocks({
164 pagination: this.pagination, 164 pagination: this.pagination,
165 sort: this.sort, 165 sort: this.sort,
166 search: this.search 166 search: this.search
167 }) 167 }).subscribe({
168 .subscribe({ 168 next: async resultList => {
169 next: async resultList => { 169 this.totalRecords = resultList.total
170 this.totalRecords = resultList.total
171 170
172 this.blocklist = resultList.data 171 this.blocklist = resultList.data
173 172
174 for (const element of this.blocklist) { 173 for (const element of this.blocklist) {
175 Object.assign(element, { 174 Object.assign(element, {
176 reasonHtml: await this.toHtml(element.reason) 175 reasonHtml: await this.toHtml(element.reason)
177 }) 176 })
178 } 177 }
179 }, 178 },
180 179
181 error: err => this.notifier.error(err.message) 180 error: err => this.notifier.error(err.message)
182 }) 181 })
183 } 182 }
184} 183}
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
index d2ca5f700..b0d8131bf 100644
--- a/client/src/app/+admin/overview/comments/video-comment-list.component.html
+++ b/client/src/app/+admin/overview/comments/video-comment-list.component.html
@@ -13,14 +13,14 @@
13 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" 13 [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true"
14 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 14 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
15 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} comments" 15 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} comments"
16 [expandedRowKeys]="expandedRows" [(selection)]="selectedComments" 16 [expandedRowKeys]="expandedRows" [(selection)]="selectedRows"
17> 17>
18 <ng-template pTemplate="caption"> 18 <ng-template pTemplate="caption">
19 <div class="caption"> 19 <div class="caption">
20 <div> 20 <div>
21 <my-action-dropdown 21 <my-action-dropdown
22 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" 22 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
23 [actions]="bulkCommentActions" [entry]="selectedComments" 23 [actions]="bulkActions" [entry]="selectedRows"
24 > 24 >
25 </my-action-dropdown> 25 </my-action-dropdown>
26 </div> 26 </div>
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
index c95d2ffeb..28efdc076 100644
--- a/client/src/app/+admin/overview/comments/video-comment-list.component.ts
+++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts
@@ -14,7 +14,7 @@ import { prepareIcu } from '@app/helpers'
14 templateUrl: './video-comment-list.component.html', 14 templateUrl: './video-comment-list.component.html',
15 styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ] 15 styleUrls: [ '../../../shared/shared-moderation/moderation.scss', './video-comment-list.component.scss' ]
16}) 16})
17export class VideoCommentListComponent extends RestTable implements OnInit { 17export class VideoCommentListComponent extends RestTable <VideoCommentAdmin> implements OnInit {
18 comments: VideoCommentAdmin[] 18 comments: VideoCommentAdmin[]
19 totalRecords = 0 19 totalRecords = 0
20 sort: SortMeta = { field: 'createdAt', order: -1 } 20 sort: SortMeta = { field: 'createdAt', order: -1 }
@@ -40,8 +40,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
40 } 40 }
41 ] 41 ]
42 42
43 selectedComments: VideoCommentAdmin[] = [] 43 bulkActions: DropdownAction<VideoCommentAdmin[]>[] = []
44 bulkCommentActions: DropdownAction<VideoCommentAdmin[]>[] = []
45 44
46 inputFilters: AdvancedInputFilter[] = [ 45 inputFilters: AdvancedInputFilter[] = [
47 { 46 {
@@ -100,7 +99,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
100 ngOnInit () { 99 ngOnInit () {
101 this.initialize() 100 this.initialize()
102 101
103 this.bulkCommentActions = [ 102 this.bulkActions = [
104 { 103 {
105 label: $localize`Delete`, 104 label: $localize`Delete`,
106 handler: comments => this.removeComments(comments), 105 handler: comments => this.removeComments(comments),
@@ -118,11 +117,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
118 return this.markdownRenderer.textMarkdownToHTML({ markdown: text, withHtml: true, withEmoji: true }) 117 return this.markdownRenderer.textMarkdownToHTML({ markdown: text, withHtml: true, withEmoji: true })
119 } 118 }
120 119
121 isInSelectionMode () { 120 protected reloadDataInternal () {
122 return this.selectedComments.length !== 0
123 }
124
125 reloadData () {
126 this.videoCommentService.getAdminVideoComments({ 121 this.videoCommentService.getAdminVideoComments({
127 pagination: this.pagination, 122 pagination: this.pagination,
128 sort: this.sort, 123 sort: this.sort,
@@ -162,7 +157,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
162 157
163 error: err => this.notifier.error(err.message), 158 error: err => this.notifier.error(err.message),
164 159
165 complete: () => this.selectedComments = [] 160 complete: () => this.selectedRows = []
166 }) 161 })
167 } 162 }
168 163
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.html b/client/src/app/+admin/overview/users/user-list/user-list.component.html
index a96ce561c..7eb5e0fc7 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.html
@@ -6,7 +6,7 @@
6<p-table 6<p-table
7 [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" 7 [value]="users" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
8 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" 8 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true"
9 [(selection)]="selectedUsers" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" 9 [(selection)]="selectedRows" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users" 11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} users"
12 [expandedRowKeys]="expandedRows" 12 [expandedRowKeys]="expandedRows"
@@ -16,7 +16,7 @@
16 <div class="left-buttons"> 16 <div class="left-buttons">
17 <my-action-dropdown 17 <my-action-dropdown
18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" 18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
19 [actions]="bulkUserActions" [entry]="selectedUsers" 19 [actions]="bulkActions" [entry]="selectedRows"
20 > 20 >
21 </my-action-dropdown> 21 </my-action-dropdown>
22 22
@@ -95,7 +95,7 @@
95 <div class="chip two-lines"> 95 <div class="chip two-lines">
96 <my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar> 96 <my-actor-avatar [actor]="user?.account" actorType="account" size="32"></my-actor-avatar>
97 <div> 97 <div>
98 <span class="user-table-primary-text">{{ user.account.displayName }}</span> 98 <span>{{ user.account.displayName }}</span>
99 <span class="muted">{{ user.username }}</span> 99 <span class="muted">{{ user.username }}</span>
100 </div> 100 </div>
101 </div> 101 </div>
@@ -110,23 +110,10 @@
110 <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span> 110 <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span>
111 </td> 111 </td>
112 112
113 <td *ngIf="isSelected('email')" [title]="user.email"> 113 <td *ngIf="isSelected('email')">
114 <ng-container *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus"> 114 <my-user-email-info [entry]="user" [requiresEmailVerification]="requiresEmailVerification"></my-user-email-info>
115 <a class="table-email" [href]="'mailto:' + user.email">{{ user.email }}</a>
116 </ng-container>
117 </td> 115 </td>
118 116
119 <ng-template #emailWithVerificationStatus>
120 <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
121 <em>? {{ user.email }}</em>
122 </td>
123 <ng-template #emailVerifiedNotFalse>
124 <td i18n-title title="User's email is verified / User can login without email verification">
125 &#x2713; {{ user.email }}
126 </td>
127 </ng-template>
128 </ng-template>
129
130 <td *ngIf="isSelected('quota')"> 117 <td *ngIf="isSelected('quota')">
131 <div class="progress" i18n-title title="Total video quota"> 118 <div class="progress" i18n-title title="Total video quota">
132 <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }" 119 <div class="progress-bar" role="progressbar" [style]="{ width: getUserVideoQuotaPercentage(user) + '%' }"
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.scss b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
index 23e0d29ee..2a3b955d2 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
@@ -10,12 +10,6 @@ tr.banned > td {
10 background-color: lighten($color: $red, $amount: 40) !important; 10 background-color: lighten($color: $red, $amount: 40) !important;
11} 11}
12 12
13.table-email {
14 @include disable-default-a-behaviour;
15
16 color: pvar(--mainForegroundColor);
17}
18
19.banned-info { 13.banned-info {
20 font-style: italic; 14 font-style: italic;
21} 15}
@@ -37,10 +31,6 @@ my-global-icon {
37 width: 18px; 31 width: 18px;
38} 32}
39 33
40.chip {
41 @include chip;
42}
43
44.progress { 34.progress {
45 @include progressbar($small: true); 35 @include progressbar($small: true);
46 36
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts
index 99987fdff..19420b748 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.ts
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts
@@ -22,7 +22,7 @@ type UserForList = User & {
22 templateUrl: './user-list.component.html', 22 templateUrl: './user-list.component.html',
23 styleUrls: [ './user-list.component.scss' ] 23 styleUrls: [ './user-list.component.scss' ]
24}) 24})
25export class UserListComponent extends RestTable implements OnInit { 25export class UserListComponent extends RestTable <User> implements OnInit {
26 private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns' 26 private static readonly LOCAL_STORAGE_SELECTED_COLUMNS_KEY = 'admin-user-list-selected-columns'
27 27
28 @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent 28 @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent
@@ -35,8 +35,7 @@ export class UserListComponent extends RestTable implements OnInit {
35 35
36 highlightBannedUsers = false 36 highlightBannedUsers = false
37 37
38 selectedUsers: User[] = [] 38 bulkActions: DropdownAction<User[]>[][] = []
39 bulkUserActions: DropdownAction<User[]>[][] = []
40 columns: { id: string, label: string }[] 39 columns: { id: string, label: string }[]
41 40
42 inputFilters: AdvancedInputFilter[] = [ 41 inputFilters: AdvancedInputFilter[] = [
@@ -95,7 +94,7 @@ export class UserListComponent extends RestTable implements OnInit {
95 94
96 this.initialize() 95 this.initialize()
97 96
98 this.bulkUserActions = [ 97 this.bulkActions = [
99 [ 98 [
100 { 99 {
101 label: $localize`Delete`, 100 label: $localize`Delete`,
@@ -249,7 +248,7 @@ export class UserListComponent extends RestTable implements OnInit {
249 const res = await this.confirmService.confirm(message, $localize`Delete`) 248 const res = await this.confirmService.confirm(message, $localize`Delete`)
250 if (res === false) return 249 if (res === false) return
251 250
252 this.userAdminService.removeUser(users) 251 this.userAdminService.removeUsers(users)
253 .subscribe({ 252 .subscribe({
254 next: () => { 253 next: () => {
255 this.notifier.success( 254 this.notifier.success(
@@ -284,13 +283,7 @@ export class UserListComponent extends RestTable implements OnInit {
284 }) 283 })
285 } 284 }
286 285
287 isInSelectionMode () { 286 protected reloadDataInternal () {
288 return this.selectedUsers.length !== 0
289 }
290
291 protected reloadData () {
292 this.selectedUsers = []
293
294 this.userAdminService.getUsers({ 287 this.userAdminService.getUsers({
295 pagination: this.pagination, 288 pagination: this.pagination,
296 sort: this.sort, 289 sort: this.sort,
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html
index a6cd2e257..5b8405ad9 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.html
+++ b/client/src/app/+admin/overview/videos/video-list.component.html
@@ -6,7 +6,7 @@
6<p-table 6<p-table
7 [value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start" 7 [value]="videos" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [first]="pagination.start"
8 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true" 8 [rowsPerPageOptions]="rowsPerPageOptions" [sortField]="sort.field" [sortOrder]="sort.order" dataKey="id" [resizableColumns]="true"
9 [(selection)]="selectedVideos" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true" 9 [(selection)]="selectedRows" [lazy]="true" (onLazyLoad)="loadLazy($event)" [lazyLoadOnInit]="false" [selectionPageOnly]="true"
10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate 10 [showCurrentPageReport]="true" i18n-currentPageReportTemplate
11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} videos" 11 currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} videos"
12 [expandedRowKeys]="expandedRows" [ngClass]="{ loading: loading }" 12 [expandedRowKeys]="expandedRows" [ngClass]="{ loading: loading }"
@@ -16,7 +16,7 @@
16 <div class="left-buttons"> 16 <div class="left-buttons">
17 <my-action-dropdown 17 <my-action-dropdown
18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" 18 *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
19 [actions]="bulkVideoActions" [entry]="selectedVideos" 19 [actions]="bulkActions" [entry]="selectedRows"
20 > 20 >
21 </my-action-dropdown> 21 </my-action-dropdown>
22 </div> 22 </div>
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts
index 4d3e9873c..1ea295499 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.ts
+++ b/client/src/app/+admin/overview/videos/video-list.component.ts
@@ -17,7 +17,7 @@ import { VideoAdminService } from './video-admin.service'
17 templateUrl: './video-list.component.html', 17 templateUrl: './video-list.component.html',
18 styleUrls: [ './video-list.component.scss' ] 18 styleUrls: [ './video-list.component.scss' ]
19}) 19})
20export class VideoListComponent extends RestTable implements OnInit { 20export class VideoListComponent extends RestTable <Video> implements OnInit {
21 @ViewChild('videoBlockModal') videoBlockModal: VideoBlockComponent 21 @ViewChild('videoBlockModal') videoBlockModal: VideoBlockComponent
22 22
23 videos: Video[] = [] 23 videos: Video[] = []
@@ -26,9 +26,7 @@ export class VideoListComponent extends RestTable implements OnInit {
26 sort: SortMeta = { field: 'publishedAt', order: -1 } 26 sort: SortMeta = { field: 'publishedAt', order: -1 }
27 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 27 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
28 28
29 bulkVideoActions: DropdownAction<Video[]>[][] = [] 29 bulkActions: DropdownAction<Video[]>[][] = []
30
31 selectedVideos: Video[] = []
32 30
33 inputFilters: AdvancedInputFilter[] 31 inputFilters: AdvancedInputFilter[]
34 32
@@ -72,7 +70,7 @@ export class VideoListComponent extends RestTable implements OnInit {
72 70
73 this.inputFilters = this.videoAdminService.buildAdminInputFilter() 71 this.inputFilters = this.videoAdminService.buildAdminInputFilter()
74 72
75 this.bulkVideoActions = [ 73 this.bulkActions = [
76 [ 74 [
77 { 75 {
78 label: $localize`Delete`, 76 label: $localize`Delete`,
@@ -126,10 +124,6 @@ export class VideoListComponent extends RestTable implements OnInit {
126 return 'VideoListComponent' 124 return 'VideoListComponent'
127 } 125 }
128 126
129 isInSelectionMode () {
130 return this.selectedVideos.length !== 0
131 }
132
133 getPrivacyBadgeClass (video: Video) { 127 getPrivacyBadgeClass (video: Video) {
134 if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green' 128 if (video.privacy.id === VideoPrivacy.PUBLIC) return 'badge-green'
135 129
@@ -189,9 +183,23 @@ export class VideoListComponent extends RestTable implements OnInit {
189 return files.reduce((p, f) => p += f.size, 0) 183 return files.reduce((p, f) => p += f.size, 0)
190 } 184 }
191 185
192 reloadData () { 186 async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'webtorrent') {
193 this.selectedVideos = [] 187 const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?`
188 const res = await this.confirmService.confirm(message, $localize`Delete file`)
189 if (res === false) return
190
191 this.videoService.removeFile(video.uuid, file.id, type)
192 .subscribe({
193 next: () => {
194 this.notifier.success($localize`File removed.`)
195 this.reloadData()
196 },
197
198 error: err => this.notifier.error(err.message)
199 })
200 }
194 201
202 protected reloadDataInternal () {
195 this.loading = true 203 this.loading = true
196 204
197 this.videoAdminService.getAdminVideos({ 205 this.videoAdminService.getAdminVideos({
@@ -209,22 +217,6 @@ export class VideoListComponent extends RestTable implements OnInit {
209 }) 217 })
210 } 218 }
211 219
212 async removeVideoFile (video: Video, file: VideoFile, type: 'hls' | 'webtorrent') {
213 const message = $localize`Are you sure you want to delete this ${file.resolution.label} file?`
214 const res = await this.confirmService.confirm(message, $localize`Delete file`)
215 if (res === false) return
216
217 this.videoService.removeFile(video.uuid, file.id, type)
218 .subscribe({
219 next: () => {
220 this.notifier.success($localize`File removed.`)
221 this.reloadData()
222 },
223
224 error: err => this.notifier.error(err.message)
225 })
226 }
227
228 private async removeVideos (videos: Video[]) { 220 private async removeVideos (videos: Video[]) {
229 const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)( 221 const message = prepareIcu($localize`Are you sure you want to delete {count, plural, =1 {this video} other {these {count} videos}}?`)(
230 { count: videos.length }, 222 { count: videos.length },
diff --git a/client/src/app/+admin/shared/shared-admin.module.ts b/client/src/app/+admin/shared/shared-admin.module.ts
index bef7d54ef..a5c300d12 100644
--- a/client/src/app/+admin/shared/shared-admin.module.ts
+++ b/client/src/app/+admin/shared/shared-admin.module.ts
@@ -1,5 +1,6 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { SharedMainModule } from '../../shared/shared-main/shared-main.module' 2import { SharedMainModule } from '../../shared/shared-main/shared-main.module'
3import { UserEmailInfoComponent } from './user-email-info.component'
3import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' 4import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
4 5
5@NgModule({ 6@NgModule({
@@ -8,11 +9,13 @@ import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
8 ], 9 ],
9 10
10 declarations: [ 11 declarations: [
11 UserRealQuotaInfoComponent 12 UserRealQuotaInfoComponent,
13 UserEmailInfoComponent
12 ], 14 ],
13 15
14 exports: [ 16 exports: [
15 UserRealQuotaInfoComponent 17 UserRealQuotaInfoComponent,
18 UserEmailInfoComponent
16 ], 19 ],
17 20
18 providers: [] 21 providers: []
diff --git a/client/src/app/+admin/shared/user-email-info.component.html b/client/src/app/+admin/shared/user-email-info.component.html
new file mode 100644
index 000000000..244240619
--- /dev/null
+++ b/client/src/app/+admin/shared/user-email-info.component.html
@@ -0,0 +1,13 @@
1<ng-container>
2 <a [href]="'mailto:' + entry.email" [title]="getTitle()">
3 <ng-container *ngIf="!requiresEmailVerification">
4 {{ entry.email }}
5 </ng-container>
6
7 <ng-container *ngIf="requiresEmailVerification">
8 <em *ngIf="!entry.emailVerified">? {{ entry.email }}</em>
9
10 <ng-container *ngIf="entry.emailVerified === true">&#x2713; {{ entry.email }}</ng-container>
11 </ng-container>
12 </a>
13</ng-container>
diff --git a/client/src/app/+admin/shared/user-email-info.component.scss b/client/src/app/+admin/shared/user-email-info.component.scss
new file mode 100644
index 000000000..d34947edd
--- /dev/null
+++ b/client/src/app/+admin/shared/user-email-info.component.scss
@@ -0,0 +1,10 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4a {
5 color: pvar(--mainForegroundColor);
6
7 &:hover {
8 text-decoration: underline;
9 }
10}
diff --git a/client/src/app/+admin/shared/user-email-info.component.ts b/client/src/app/+admin/shared/user-email-info.component.ts
new file mode 100644
index 000000000..e33948b60
--- /dev/null
+++ b/client/src/app/+admin/shared/user-email-info.component.ts
@@ -0,0 +1,20 @@
1import { Component, Input } from '@angular/core'
2import { User, UserRegistration } from '@shared/models/users'
3
4@Component({
5 selector: 'my-user-email-info',
6 templateUrl: './user-email-info.component.html',
7 styleUrls: [ './user-email-info.component.scss' ]
8})
9export class UserEmailInfoComponent {
10 @Input() entry: User | UserRegistration
11 @Input() requiresEmailVerification: boolean
12
13 getTitle () {
14 if (this.entry.emailVerified) {
15 return $localize`User email has been verified`
16 }
17
18 return $localize`User email hasn't been verified`
19 }
20}
diff --git a/client/src/app/+admin/system/jobs/job.service.ts b/client/src/app/+admin/system/jobs/job.service.ts
index ef8ddd3b4..031e2bad8 100644
--- a/client/src/app/+admin/system/jobs/job.service.ts
+++ b/client/src/app/+admin/system/jobs/job.service.ts
@@ -19,7 +19,7 @@ export class JobService {
19 private restExtractor: RestExtractor 19 private restExtractor: RestExtractor
20 ) {} 20 ) {}
21 21
22 getJobs (options: { 22 listJobs (options: {
23 jobState?: JobStateClient 23 jobState?: JobStateClient
24 jobType: JobTypeClient 24 jobType: JobTypeClient
25 pagination: RestPagination 25 pagination: RestPagination
diff --git a/client/src/app/+admin/system/jobs/jobs.component.ts b/client/src/app/+admin/system/jobs/jobs.component.ts
index b8f3c3a68..6e10c81ff 100644
--- a/client/src/app/+admin/system/jobs/jobs.component.ts
+++ b/client/src/app/+admin/system/jobs/jobs.component.ts
@@ -120,12 +120,12 @@ export class JobsComponent extends RestTable implements OnInit {
120 this.reloadData() 120 this.reloadData()
121 } 121 }
122 122
123 protected reloadData () { 123 protected reloadDataInternal () {
124 let jobState = this.jobState as JobState 124 let jobState = this.jobState as JobState
125 if (this.jobState === 'all') jobState = null 125 if (this.jobState === 'all') jobState = null
126 126
127 this.jobsService 127 this.jobsService
128 .getJobs({ 128 .listJobs({
129 jobState, 129 jobState,
130 jobType: this.jobType, 130 jobType: this.jobType,
131 pagination: this.pagination, 131 pagination: this.pagination,