diff options
Diffstat (limited to 'client/src')
127 files changed, 1668 insertions, 519 deletions
diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html index b113df82f..fdd6157e5 100644 --- a/client/src/app/+about/about-instance/about-instance.component.html +++ b/client/src/app/+about/about-instance/about-instance.component.html | |||
@@ -21,7 +21,7 @@ | |||
21 | 21 | ||
22 | <div class="anchor" id="administrators-and-sustainability"></div> | 22 | <div class="anchor" id="administrators-and-sustainability"></div> |
23 | <a | 23 | <a |
24 | *ngIf="html.administrator || html.maintenanceLifetime || html.businessModel" | 24 | *ngIf="aboutHTML.administrator || aboutHTML.maintenanceLifetime || aboutHTML.businessModel" |
25 | class="anchor-link" | 25 | class="anchor-link" |
26 | routerLink="/about/instance" | 26 | routerLink="/about/instance" |
27 | fragment="administrators-and-sustainability" | 27 | fragment="administrators-and-sustainability" |
@@ -33,7 +33,7 @@ | |||
33 | </h2> | 33 | </h2> |
34 | </a> | 34 | </a> |
35 | 35 | ||
36 | <div class="block administrator" *ngIf="html.administrator"> | 36 | <div class="block administrator" *ngIf="aboutHTML.administrator"> |
37 | <div class="anchor" id="administrators"></div> | 37 | <div class="anchor" id="administrators"></div> |
38 | <a | 38 | <a |
39 | class="anchor-link" | 39 | class="anchor-link" |
@@ -44,10 +44,10 @@ | |||
44 | <h3 i18n class="section-title">Who we are</h3> | 44 | <h3 i18n class="section-title">Who we are</h3> |
45 | </a> | 45 | </a> |
46 | 46 | ||
47 | <div [innerHTML]="html.administrator"></div> | 47 | <div [innerHTML]="aboutHTML.administrator"></div> |
48 | </div> | 48 | </div> |
49 | 49 | ||
50 | <div class="block creation-reason" *ngIf="html.creationReason"> | 50 | <div class="block creation-reason" *ngIf="aboutHTML.creationReason"> |
51 | <div class="anchor" id="creation-reason"></div> | 51 | <div class="anchor" id="creation-reason"></div> |
52 | <a | 52 | <a |
53 | class="anchor-link" | 53 | class="anchor-link" |
@@ -58,10 +58,10 @@ | |||
58 | <h3 i18n class="section-title">Why we created this instance</h3> | 58 | <h3 i18n class="section-title">Why we created this instance</h3> |
59 | </a> | 59 | </a> |
60 | 60 | ||
61 | <div [innerHTML]="html.creationReason"></div> | 61 | <div [innerHTML]="aboutHTML.creationReason"></div> |
62 | </div> | 62 | </div> |
63 | 63 | ||
64 | <div class="block maintenance-lifetime" *ngIf="html.maintenanceLifetime"> | 64 | <div class="block maintenance-lifetime" *ngIf="aboutHTML.maintenanceLifetime"> |
65 | <div class="anchor" id="maintenance-lifetime"></div> | 65 | <div class="anchor" id="maintenance-lifetime"></div> |
66 | <a | 66 | <a |
67 | class="anchor-link" | 67 | class="anchor-link" |
@@ -72,10 +72,10 @@ | |||
72 | <h3 i18n class="section-title">How long we plan to maintain this instance</h3> | 72 | <h3 i18n class="section-title">How long we plan to maintain this instance</h3> |
73 | </a> | 73 | </a> |
74 | 74 | ||
75 | <div [innerHTML]="html.maintenanceLifetime"></div> | 75 | <div [innerHTML]="aboutHTML.maintenanceLifetime"></div> |
76 | </div> | 76 | </div> |
77 | 77 | ||
78 | <div class="block business-model" *ngIf="html.businessModel"> | 78 | <div class="block business-model" *ngIf="aboutHTML.businessModel"> |
79 | <div class="anchor" id="business-model"></div> | 79 | <div class="anchor" id="business-model"></div> |
80 | <a | 80 | <a |
81 | class="anchor-link" | 81 | class="anchor-link" |
@@ -86,12 +86,12 @@ | |||
86 | <h3 i18n class="section-title">How we will pay for keeping our instance running</h3> | 86 | <h3 i18n class="section-title">How we will pay for keeping our instance running</h3> |
87 | </a> | 87 | </a> |
88 | 88 | ||
89 | <div [innerHTML]="html.businessModel"></div> | 89 | <div [innerHTML]="aboutHTML.businessModel"></div> |
90 | </div> | 90 | </div> |
91 | 91 | ||
92 | <div class="anchor" id="information"></div> | 92 | <div class="anchor" id="information"></div> |
93 | <a | 93 | <a |
94 | *ngIf="descriptionContent" | 94 | *ngIf="descriptionElement" |
95 | class="anchor-link" | 95 | class="anchor-link" |
96 | routerLink="/about/instance" | 96 | routerLink="/about/instance" |
97 | fragment="information" | 97 | fragment="information" |
@@ -113,13 +113,13 @@ | |||
113 | <h3 i18n class="section-title">Description</h3> | 113 | <h3 i18n class="section-title">Description</h3> |
114 | </a> | 114 | </a> |
115 | 115 | ||
116 | <my-custom-markup-container [content]="descriptionContent"></my-custom-markup-container> | 116 | <my-custom-markup-container [content]="descriptionElement"></my-custom-markup-container> |
117 | </div> | 117 | </div> |
118 | 118 | ||
119 | <div myPluginSelector pluginSelectorId="about-instance-moderation"> | 119 | <div myPluginSelector pluginSelectorId="about-instance-moderation"> |
120 | <div class="anchor" id="moderation"></div> | 120 | <div class="anchor" id="moderation"></div> |
121 | <a | 121 | <a |
122 | *ngIf="html.moderationInformation || html.codeOfConduct || html.terms" | 122 | *ngIf="aboutHTML.moderationInformation || aboutHTML.codeOfConduct || aboutHTML.terms" |
123 | class="anchor-link" | 123 | class="anchor-link" |
124 | routerLink="/about/instance" | 124 | routerLink="/about/instance" |
125 | fragment="moderation" | 125 | fragment="moderation" |
@@ -130,7 +130,7 @@ | |||
130 | </h2> | 130 | </h2> |
131 | </a> | 131 | </a> |
132 | 132 | ||
133 | <div class="block moderation-information" *ngIf="html.moderationInformation"> | 133 | <div class="block moderation-information" *ngIf="aboutHTML.moderationInformation"> |
134 | <div class="anchor" id="moderation-information"></div> | 134 | <div class="anchor" id="moderation-information"></div> |
135 | <a | 135 | <a |
136 | class="anchor-link" | 136 | class="anchor-link" |
@@ -141,10 +141,10 @@ | |||
141 | <h3 i18n class="section-title">Moderation information</h3> | 141 | <h3 i18n class="section-title">Moderation information</h3> |
142 | </a> | 142 | </a> |
143 | 143 | ||
144 | <div [innerHTML]="html.moderationInformation"></div> | 144 | <div [innerHTML]="aboutHTML.moderationInformation"></div> |
145 | </div> | 145 | </div> |
146 | 146 | ||
147 | <div class="block code-of-conduct" *ngIf="html.codeOfConduct"> | 147 | <div class="block code-of-conduct" *ngIf="aboutHTML.codeOfConduct"> |
148 | <div class="anchor" id="code-of-conduct"></div> | 148 | <div class="anchor" id="code-of-conduct"></div> |
149 | <a | 149 | <a |
150 | class="anchor-link" | 150 | class="anchor-link" |
@@ -155,7 +155,7 @@ | |||
155 | <h3 i18n class="section-title">Code of conduct</h3> | 155 | <h3 i18n class="section-title">Code of conduct</h3> |
156 | </a> | 156 | </a> |
157 | 157 | ||
158 | <div [innerHTML]="html.codeOfConduct"></div> | 158 | <div [innerHTML]="aboutHTML.codeOfConduct"></div> |
159 | </div> | 159 | </div> |
160 | 160 | ||
161 | <div class="block terms"> | 161 | <div class="block terms"> |
@@ -169,14 +169,14 @@ | |||
169 | <h3 i18n class="section-title">Terms</h3> | 169 | <h3 i18n class="section-title">Terms</h3> |
170 | </a> | 170 | </a> |
171 | 171 | ||
172 | <div [innerHTML]="html.terms"></div> | 172 | <div [innerHTML]="aboutHTML.terms"></div> |
173 | </div> | 173 | </div> |
174 | </div> | 174 | </div> |
175 | 175 | ||
176 | <div myPluginSelector pluginSelectorId="about-instance-other-information"> | 176 | <div myPluginSelector pluginSelectorId="about-instance-other-information"> |
177 | <div class="anchor" id="other-information"></div> | 177 | <div class="anchor" id="other-information"></div> |
178 | <a | 178 | <a |
179 | *ngIf="html.hardwareInformation" | 179 | *ngIf="aboutHTML.hardwareInformation" |
180 | class="anchor-link" | 180 | class="anchor-link" |
181 | routerLink="/about/instance" | 181 | routerLink="/about/instance" |
182 | fragment="other-information" | 182 | fragment="other-information" |
@@ -187,7 +187,7 @@ | |||
187 | </h2> | 187 | </h2> |
188 | </a> | 188 | </a> |
189 | 189 | ||
190 | <div class="block hardware-information" *ngIf="html.hardwareInformation"> | 190 | <div class="block hardware-information" *ngIf="aboutHTML.hardwareInformation"> |
191 | <div class="anchor" id="hardware-information"></div> | 191 | <div class="anchor" id="hardware-information"></div> |
192 | <a | 192 | <a |
193 | class="anchor-link" | 193 | class="anchor-link" |
@@ -198,7 +198,7 @@ | |||
198 | <h3 i18n class="section-title">Hardware information</h3> | 198 | <h3 i18n class="section-title">Hardware information</h3> |
199 | </a> | 199 | </a> |
200 | 200 | ||
201 | <div [innerHTML]="html.hardwareInformation"></div> | 201 | <div [innerHTML]="aboutHTML.hardwareInformation"></div> |
202 | </div> | 202 | </div> |
203 | </div> | 203 | </div> |
204 | </div> | 204 | </div> |
diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts index 0826bbc5a..e1501d7de 100644 --- a/client/src/app/+about/about-instance/about-instance.component.ts +++ b/client/src/app/+about/about-instance/about-instance.component.ts | |||
@@ -2,7 +2,7 @@ import { ViewportScroller } from '@angular/common' | |||
2 | import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 2 | import { AfterViewChecked, Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute } from '@angular/router' | 3 | import { ActivatedRoute } from '@angular/router' |
4 | import { Notifier, ServerService } from '@app/core' | 4 | import { Notifier, ServerService } from '@app/core' |
5 | import { InstanceService } from '@app/shared/shared-instance' | 5 | import { AboutHTML } from '@app/shared/shared-instance' |
6 | import { copyToClipboard } from '@root-helpers/utils' | 6 | import { copyToClipboard } from '@root-helpers/utils' |
7 | import { HTMLServerConfig } from '@shared/models/server' | 7 | import { HTMLServerConfig } from '@shared/models/server' |
8 | import { ResolverData } from './about-instance.resolver' | 8 | import { ResolverData } from './about-instance.resolver' |
@@ -17,22 +17,12 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
17 | @ViewChild('descriptionWrapper') descriptionWrapper: ElementRef<HTMLInputElement> | 17 | @ViewChild('descriptionWrapper') descriptionWrapper: ElementRef<HTMLInputElement> |
18 | @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent | 18 | @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent |
19 | 19 | ||
20 | shortDescription = '' | 20 | aboutHTML: AboutHTML |
21 | descriptionContent: string | 21 | descriptionElement: HTMLDivElement |
22 | |||
23 | html = { | ||
24 | terms: '', | ||
25 | codeOfConduct: '', | ||
26 | moderationInformation: '', | ||
27 | administrator: '', | ||
28 | creationReason: '', | ||
29 | maintenanceLifetime: '', | ||
30 | businessModel: '', | ||
31 | hardwareInformation: '' | ||
32 | } | ||
33 | 22 | ||
34 | languages: string[] = [] | 23 | languages: string[] = [] |
35 | categories: string[] = [] | 24 | categories: string[] = [] |
25 | shortDescription = '' | ||
36 | 26 | ||
37 | initialized = false | 27 | initialized = false |
38 | 28 | ||
@@ -44,8 +34,7 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
44 | private viewportScroller: ViewportScroller, | 34 | private viewportScroller: ViewportScroller, |
45 | private route: ActivatedRoute, | 35 | private route: ActivatedRoute, |
46 | private notifier: Notifier, | 36 | private notifier: Notifier, |
47 | private serverService: ServerService, | 37 | private serverService: ServerService |
48 | private instanceService: InstanceService | ||
49 | ) {} | 38 | ) {} |
50 | 39 | ||
51 | get instanceName () { | 40 | get instanceName () { |
@@ -60,8 +49,16 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
60 | return this.serverConfig.instance.isNSFW | 49 | return this.serverConfig.instance.isNSFW |
61 | } | 50 | } |
62 | 51 | ||
63 | async ngOnInit () { | 52 | ngOnInit () { |
64 | const { about, languages, categories }: ResolverData = this.route.snapshot.data.instanceData | 53 | const { about, languages, categories, aboutHTML, descriptionElement }: ResolverData = this.route.snapshot.data.instanceData |
54 | |||
55 | this.aboutHTML = aboutHTML | ||
56 | this.descriptionElement = descriptionElement | ||
57 | |||
58 | this.languages = languages | ||
59 | this.categories = categories | ||
60 | |||
61 | this.shortDescription = about.instance.shortDescription | ||
65 | 62 | ||
66 | this.serverConfig = this.serverService.getHTMLConfig() | 63 | this.serverConfig = this.serverService.getHTMLConfig() |
67 | 64 | ||
@@ -73,14 +70,6 @@ export class AboutInstanceComponent implements OnInit, AfterViewChecked { | |||
73 | this.contactAdminModal.show(prefill) | 70 | this.contactAdminModal.show(prefill) |
74 | }) | 71 | }) |
75 | 72 | ||
76 | this.languages = languages | ||
77 | this.categories = categories | ||
78 | |||
79 | this.shortDescription = about.instance.shortDescription | ||
80 | this.descriptionContent = about.instance.description | ||
81 | |||
82 | this.html = await this.instanceService.buildHtml(about) | ||
83 | |||
84 | this.initialized = true | 73 | this.initialized = true |
85 | } | 74 | } |
86 | 75 | ||
diff --git a/client/src/app/+about/about-instance/about-instance.resolver.ts b/client/src/app/+about/about-instance/about-instance.resolver.ts index ee0219df0..8818fc582 100644 --- a/client/src/app/+about/about-instance/about-instance.resolver.ts +++ b/client/src/app/+about/about-instance/about-instance.resolver.ts | |||
@@ -2,16 +2,25 @@ import { forkJoin } from 'rxjs' | |||
2 | import { map, switchMap } from 'rxjs/operators' | 2 | import { map, switchMap } from 'rxjs/operators' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { Resolve } from '@angular/router' | 4 | import { Resolve } from '@angular/router' |
5 | import { InstanceService } from '@app/shared/shared-instance' | 5 | import { CustomMarkupService } from '@app/shared/shared-custom-markup' |
6 | import { AboutHTML, InstanceService } from '@app/shared/shared-instance' | ||
6 | import { About } from '@shared/models/server' | 7 | import { About } from '@shared/models/server' |
7 | 8 | ||
8 | export type ResolverData = { about: About, languages: string[], categories: string[] } | 9 | export type ResolverData = { |
10 | about: About | ||
11 | languages: string[] | ||
12 | categories: string[] | ||
13 | aboutHTML: AboutHTML | ||
14 | descriptionElement: HTMLDivElement | ||
15 | } | ||
9 | 16 | ||
10 | @Injectable() | 17 | @Injectable() |
11 | export class AboutInstanceResolver implements Resolve<any> { | 18 | export class AboutInstanceResolver implements Resolve<any> { |
12 | 19 | ||
13 | constructor ( | 20 | constructor ( |
14 | private instanceService: InstanceService | 21 | private instanceService: InstanceService, |
22 | private customMarkupService: CustomMarkupService | ||
23 | |||
15 | ) {} | 24 | ) {} |
16 | 25 | ||
17 | resolve () { | 26 | resolve () { |
@@ -19,9 +28,15 @@ export class AboutInstanceResolver implements Resolve<any> { | |||
19 | .pipe( | 28 | .pipe( |
20 | switchMap(about => { | 29 | switchMap(about => { |
21 | return forkJoin([ | 30 | return forkJoin([ |
31 | Promise.resolve(about), | ||
22 | this.instanceService.buildTranslatedLanguages(about), | 32 | this.instanceService.buildTranslatedLanguages(about), |
23 | this.instanceService.buildTranslatedCategories(about) | 33 | this.instanceService.buildTranslatedCategories(about), |
24 | ]).pipe(map(([ languages, categories ]) => ({ about, languages, categories }) as ResolverData)) | 34 | this.instanceService.buildHtml(about), |
35 | this.customMarkupService.buildElement(about.instance.description) | ||
36 | ]) | ||
37 | }), | ||
38 | map(([ about, languages, categories, aboutHTML, { rootElement } ]) => { | ||
39 | return { about, languages, categories, aboutHTML, descriptionElement: rootElement } as ResolverData | ||
25 | }) | 40 | }) |
26 | ) | 41 | ) |
27 | } | 42 | } |
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 | |||
30 | import { FollowingListComponent } from './follows/following-list/following-list.component' | 30 | import { FollowingListComponent } from './follows/following-list/following-list.component' |
31 | import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' | 31 | import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component' |
32 | import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' | 32 | import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component' |
33 | import { AbuseListComponent, VideoBlockListComponent } from './moderation' | 33 | import { |
34 | AbuseListComponent, | ||
35 | AdminRegistrationService, | ||
36 | ProcessRegistrationModalComponent, | ||
37 | RegistrationListComponent, | ||
38 | VideoBlockListComponent | ||
39 | } from './moderation' | ||
34 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' | 40 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' |
35 | import { | 41 | import { |
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 | }) |
136 | export class AdminModule { } | 146 | export 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 | }) |
15 | export class FollowersListComponent extends RestTable implements OnInit { | 15 | export 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 | }) |
15 | export class FollowingListComponent extends RestTable implements OnInit { | 15 | export 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 @@ | |||
1 | export * from './abuse-list' | 1 | export * from './abuse-list' |
2 | export * from './instance-blocklist' | 2 | export * from './instance-blocklist' |
3 | export * from './video-block-list' | 3 | export * from './video-block-list' |
4 | export * from './registration-list' | ||
4 | export * from './moderation.routes' | 5 | export * 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 | |||
4 | import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' | 4 | import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list' |
5 | import { UserRightGuard } from '@app/core' | 5 | import { UserRightGuard } from '@app/core' |
6 | import { UserRight } from '@shared/models' | 6 | import { UserRight } from '@shared/models' |
7 | import { RegistrationListComponent } from './registration-list' | ||
7 | 8 | ||
8 | export const ModerationRoutes: Routes = [ | 9 | export 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 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { from } from 'rxjs' | ||
3 | import { catchError, concatMap, toArray } from 'rxjs/operators' | ||
4 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
5 | import { Injectable } from '@angular/core' | ||
6 | import { RestExtractor, RestPagination, RestService } from '@app/core' | ||
7 | import { arrayify } from '@shared/core-utils' | ||
8 | import { ResultList, UserRegistration, UserRegistrationUpdateState } from '@shared/models' | ||
9 | import { environment } from '../../../../environments/environment' | ||
10 | |||
11 | @Injectable() | ||
12 | export 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 @@ | |||
1 | export * from './admin-registration.service' | ||
2 | export * from './process-registration-modal.component' | ||
3 | export * from './process-registration-validators' | ||
4 | export * 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> <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 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | ||
2 | import { Notifier, ServerService } from '@app/core' | ||
3 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | ||
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
6 | import { UserRegistration } from '@shared/models' | ||
7 | import { AdminRegistrationService } from './admin-registration.service' | ||
8 | import { 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 | }) | ||
15 | export 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 @@ | |||
1 | import { Validators } from '@angular/forms' | ||
2 | import { BuildFormValidator } from '@app/shared/form-validators' | ||
3 | |||
4 | export 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 | |||
4 | my-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 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | ||
5 | import { prepareIcu } from '@app/helpers' | ||
6 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | ||
7 | import { DropdownAction } from '@app/shared/shared-main' | ||
8 | import { UserRegistration, UserRegistrationState } from '@shared/models' | ||
9 | import { AdminRegistrationService } from './admin-registration.service' | ||
10 | import { 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 | }) | ||
17 | export 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 | }) |
17 | export class VideoCommentListComponent extends RestTable implements OnInit { | 17 | export 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 | ✓ {{ 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 | }) |
25 | export class UserListComponent extends RestTable implements OnInit { | 25 | export 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 | }) |
20 | export class VideoListComponent extends RestTable implements OnInit { | 20 | export 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 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { SharedMainModule } from '../../shared/shared-main/shared-main.module' | 2 | import { SharedMainModule } from '../../shared/shared-main/shared-main.module' |
3 | import { UserEmailInfoComponent } from './user-email-info.component' | ||
3 | import { UserRealQuotaInfoComponent } from './user-real-quota-info.component' | 4 | import { 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">✓ {{ 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 | |||
4 | a { | ||
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 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { 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 | }) | ||
9 | export 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, |
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts index c1705807f..c03af38f2 100644 --- a/client/src/app/+login/login.component.ts +++ b/client/src/app/+login/login.component.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { environment } from 'src/environments/environment' | ||
1 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 2 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' | 4 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' |
@@ -7,8 +8,8 @@ import { USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-valid | |||
7 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' | 8 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' |
8 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | 9 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' |
9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 10 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
10 | import { PluginsManager } from '@root-helpers/plugins-manager' | 11 | import { getExternalAuthHref } from '@shared/core-utils' |
11 | import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' | 12 | import { RegisteredExternalAuthConfig, ServerConfig, ServerErrorCode } from '@shared/models' |
12 | 13 | ||
13 | @Component({ | 14 | @Component({ |
14 | selector: 'my-login', | 15 | selector: 'my-login', |
@@ -119,7 +120,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni | |||
119 | } | 120 | } |
120 | 121 | ||
121 | getAuthHref (auth: RegisteredExternalAuthConfig) { | 122 | getAuthHref (auth: RegisteredExternalAuthConfig) { |
122 | return PluginsManager.getExternalAuthHref(auth) | 123 | return getExternalAuthHref(environment.apiUrl, auth) |
123 | } | 124 | } |
124 | 125 | ||
125 | login () { | 126 | login () { |
@@ -196,6 +197,8 @@ The link will expire within 1 hour.` | |||
196 | } | 197 | } |
197 | 198 | ||
198 | private handleError (err: any) { | 199 | private handleError (err: any) { |
200 | console.log(err) | ||
201 | |||
199 | if (this.authService.isOTPMissingError(err)) { | 202 | if (this.authService.isOTPMissingError(err)) { |
200 | this.otpStep = true | 203 | this.otpStep = true |
201 | 204 | ||
@@ -207,8 +210,26 @@ The link will expire within 1 hour.` | |||
207 | return | 210 | return |
208 | } | 211 | } |
209 | 212 | ||
210 | if (err.message.indexOf('credentials are invalid') !== -1) this.error = $localize`Incorrect username or password.` | 213 | if (err.message.includes('credentials are invalid')) { |
211 | else if (err.message.indexOf('blocked') !== -1) this.error = $localize`Your account is blocked.` | 214 | this.error = $localize`Incorrect username or password.` |
212 | else this.error = err.message | 215 | return |
216 | } | ||
217 | |||
218 | if (err.message.includes('blocked')) { | ||
219 | this.error = $localize`Your account is blocked.` | ||
220 | return | ||
221 | } | ||
222 | |||
223 | if (err.body?.code === ServerErrorCode.ACCOUNT_WAITING_FOR_APPROVAL) { | ||
224 | this.error = $localize`This account is awaiting approval by moderators.` | ||
225 | return | ||
226 | } | ||
227 | |||
228 | if (err.body?.code === ServerErrorCode.ACCOUNT_APPROVAL_REJECTED) { | ||
229 | this.error = $localize`Registration approval has been rejected for this account.` | ||
230 | return | ||
231 | } | ||
232 | |||
233 | this.error = err.message | ||
213 | } | 234 | } |
214 | } | 235 | } |
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.scss b/client/src/app/+my-library/my-ownership/my-ownership.component.scss index a8450ff1b..98bed226d 100644 --- a/client/src/app/+my-library/my-ownership/my-ownership.component.scss +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.scss | |||
@@ -2,10 +2,6 @@ | |||
2 | @use '_miniature' as *; | 2 | @use '_miniature' as *; |
3 | @use '_mixins' as *; | 3 | @use '_mixins' as *; |
4 | 4 | ||
5 | .chip { | ||
6 | @include chip; | ||
7 | } | ||
8 | |||
9 | .video-table-video { | 5 | .video-table-video { |
10 | display: inline-flex; | 6 | display: inline-flex; |
11 | 7 | ||
diff --git a/client/src/app/+my-library/my-ownership/my-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-ownership.component.ts index 7ea940ceb..8d6a42dfb 100644 --- a/client/src/app/+my-library/my-ownership/my-ownership.component.ts +++ b/client/src/app/+my-library/my-ownership/my-ownership.component.ts | |||
@@ -59,7 +59,7 @@ export class MyOwnershipComponent extends RestTable implements OnInit { | |||
59 | }) | 59 | }) |
60 | } | 60 | } |
61 | 61 | ||
62 | protected reloadData () { | 62 | protected reloadDataInternal () { |
63 | return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) | 63 | return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) |
64 | .subscribe({ | 64 | .subscribe({ |
65 | next: resultList => { | 65 | next: resultList => { |
diff --git a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts index d18e78201..74dbe222d 100644 --- a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts +++ b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts | |||
@@ -68,7 +68,7 @@ export class MyVideoChannelSyncsComponent extends RestTable implements OnInit { | |||
68 | ] | 68 | ] |
69 | } | 69 | } |
70 | 70 | ||
71 | protected reloadData () { | 71 | protected reloadDataInternal () { |
72 | this.error = undefined | 72 | this.error = undefined |
73 | 73 | ||
74 | this.authService.userInformationLoaded | 74 | this.authService.userInformationLoaded |
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts index 46d689bd1..7d82f62b9 100644 --- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts +++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts | |||
@@ -90,7 +90,7 @@ export class MyVideoImportsComponent extends RestTable implements OnInit { | |||
90 | }) | 90 | }) |
91 | } | 91 | } |
92 | 92 | ||
93 | protected reloadData () { | 93 | protected reloadDataInternal () { |
94 | this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search) | 94 | this.videoImportService.getMyVideoImports(this.pagination, this.sort, this.search) |
95 | .subscribe({ | 95 | .subscribe({ |
96 | next: resultList => { | 96 | next: resultList => { |
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html index bafb96a49..86763e801 100644 --- a/client/src/app/+signup/+register/register.component.html +++ b/client/src/app/+signup/+register/register.component.html | |||
@@ -5,29 +5,34 @@ | |||
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <ng-container *ngIf="!signupDisabled"> | 7 | <ng-container *ngIf="!signupDisabled"> |
8 | <h1 i18n class="title-page-v2"> | 8 | <h1 class="title-page-v2"> |
9 | <strong class="underline-orange">{{ instanceName }}</strong> | 9 | <strong class="underline-orange">{{ instanceName }}</strong> |
10 | > | 10 | > |
11 | Create an account | 11 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> |
12 | </h1> | 12 | </h1> |
13 | 13 | ||
14 | <div class="register-content"> | 14 | <div class="register-content"> |
15 | <my-custom-stepper linear> | 15 | <my-custom-stepper linear> |
16 | 16 | ||
17 | <cdk-step i18n-label label="About" [editable]="!signupSuccess"> | 17 | <cdk-step i18n-label label="About" [editable]="!signupSuccess"> |
18 | <my-signup-step-title mascotImageName="about" i18n> | 18 | <my-signup-step-title mascotImageName="about"> |
19 | <strong>Create an account</strong> | 19 | <strong> |
20 | <div>on {{ instanceName }}</div> | 20 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> |
21 | </strong> | ||
22 | |||
23 | <div i18n>on {{ instanceName }}</div> | ||
21 | </my-signup-step-title> | 24 | </my-signup-step-title> |
22 | 25 | ||
23 | <my-register-step-about [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about> | 26 | <my-register-step-about [requiresApproval]="requiresApproval" [videoUploadDisabled]="videoUploadDisabled"></my-register-step-about> |
24 | 27 | ||
25 | <div class="step-buttons"> | 28 | <div class="step-buttons"> |
26 | <a i18n class="skip-step underline-orange" routerLink="/login"> | 29 | <a i18n class="skip-step underline-orange" routerLink="/login"> |
27 | <strong>I already have an account</strong>, I log in | 30 | <strong>I already have an account</strong>, I log in |
28 | </a> | 31 | </a> |
29 | 32 | ||
30 | <button i18n cdkStepperNext>Create an account</button> | 33 | <button cdkStepperNext> |
34 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> | ||
35 | </button> | ||
31 | </div> | 36 | </div> |
32 | </cdk-step> | 37 | </cdk-step> |
33 | 38 | ||
@@ -44,8 +49,8 @@ | |||
44 | ></my-instance-about-accordion> | 49 | ></my-instance-about-accordion> |
45 | 50 | ||
46 | <my-register-step-terms | 51 | <my-register-step-terms |
47 | [hasCodeOfConduct]="!!aboutHtml.codeOfConduct" | 52 | [hasCodeOfConduct]="!!aboutHtml.codeOfConduct" [minimumAge]="minimumAge" [instanceName]="instanceName" |
48 | [minimumAge]="minimumAge" | 53 | [requiresApproval]="requiresApproval" |
49 | (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()" | 54 | (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()" |
50 | ></my-register-step-terms> | 55 | ></my-register-step-terms> |
51 | 56 | ||
@@ -94,14 +99,15 @@ | |||
94 | <div class="skip-step-description" i18n>You will be able to create a channel later</div> | 99 | <div class="skip-step-description" i18n>You will be able to create a channel later</div> |
95 | </div> | 100 | </div> |
96 | 101 | ||
97 | <button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()" i18n> | 102 | <button cdkStepperNext [disabled]="!formStepChannel || !formStepChannel.valid || hasSameChannelAndAccountNames()" (click)="signup()"> |
98 | Create my account | 103 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> |
99 | </button> | 104 | </button> |
100 | </div> | 105 | </div> |
101 | </cdk-step> | 106 | </cdk-step> |
102 | 107 | ||
103 | <cdk-step #lastStep i18n-label label="Done!" [editable]="false"> | 108 | <cdk-step #lastStep i18n-label label="Done!" [editable]="false"> |
104 | <div *ngIf="!signupSuccess && !signupError" class="done-loader"> | 109 | <!-- Account creation can be a little bit long so display a loader --> |
110 | <div *ngIf="!requiresApproval && !signupSuccess && !signupError" class="done-loader"> | ||
105 | <my-loader [loading]="true"></my-loader> | 111 | <my-loader [loading]="true"></my-loader> |
106 | 112 | ||
107 | <div i18n>PeerTube is creating your account...</div> | 113 | <div i18n>PeerTube is creating your account...</div> |
@@ -109,7 +115,10 @@ | |||
109 | 115 | ||
110 | <div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div> | 116 | <div *ngIf="signupError" class="alert alert-danger">{{ signupError }}</div> |
111 | 117 | ||
112 | <my-signup-success *ngIf="signupSuccess" [requiresEmailVerification]="requiresEmailVerification"></my-signup-success> | 118 | <my-signup-success-before-email |
119 | *ngIf="signupSuccess" | ||
120 | [requiresEmailVerification]="requiresEmailVerification" [requiresApproval]="requiresApproval" [instanceName]="instanceName" | ||
121 | ></my-signup-success-before-email> | ||
113 | 122 | ||
114 | <div *ngIf="signupError" class="steps-button"> | 123 | <div *ngIf="signupError" class="steps-button"> |
115 | <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button> | 124 | <button cdkStepperPrevious>{{ defaultPreviousStepButtonLabel }}</button> |
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts index 958770ebf..9259d902c 100644 --- a/client/src/app/+signup/+register/register.component.ts +++ b/client/src/app/+signup/+register/register.component.ts | |||
@@ -5,10 +5,10 @@ import { ActivatedRoute } from '@angular/router' | |||
5 | import { AuthService } from '@app/core' | 5 | import { AuthService } from '@app/core' |
6 | import { HooksService } from '@app/core/plugins/hooks.service' | 6 | import { HooksService } from '@app/core/plugins/hooks.service' |
7 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | 7 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' |
8 | import { UserSignupService } from '@app/shared/shared-users' | ||
9 | import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap' | 8 | import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap' |
10 | import { UserRegister } from '@shared/models' | 9 | import { UserRegister } from '@shared/models' |
11 | import { ServerConfig } from '@shared/models/server' | 10 | import { ServerConfig } from '@shared/models/server' |
11 | import { SignupService } from '../shared/signup.service' | ||
12 | 12 | ||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-register', | 14 | selector: 'my-register', |
@@ -53,7 +53,7 @@ export class RegisterComponent implements OnInit { | |||
53 | constructor ( | 53 | constructor ( |
54 | private route: ActivatedRoute, | 54 | private route: ActivatedRoute, |
55 | private authService: AuthService, | 55 | private authService: AuthService, |
56 | private userSignupService: UserSignupService, | 56 | private signupService: SignupService, |
57 | private hooks: HooksService | 57 | private hooks: HooksService |
58 | ) { } | 58 | ) { } |
59 | 59 | ||
@@ -61,6 +61,10 @@ export class RegisterComponent implements OnInit { | |||
61 | return this.serverConfig.signup.requiresEmailVerification | 61 | return this.serverConfig.signup.requiresEmailVerification |
62 | } | 62 | } |
63 | 63 | ||
64 | get requiresApproval () { | ||
65 | return this.serverConfig.signup.requiresApproval | ||
66 | } | ||
67 | |||
64 | get minimumAge () { | 68 | get minimumAge () { |
65 | return this.serverConfig.signup.minimumAge | 69 | return this.serverConfig.signup.minimumAge |
66 | } | 70 | } |
@@ -132,42 +136,49 @@ export class RegisterComponent implements OnInit { | |||
132 | skipChannelCreation () { | 136 | skipChannelCreation () { |
133 | this.formStepChannel.reset() | 137 | this.formStepChannel.reset() |
134 | this.lastStep.select() | 138 | this.lastStep.select() |
139 | |||
135 | this.signup() | 140 | this.signup() |
136 | } | 141 | } |
137 | 142 | ||
138 | async signup () { | 143 | async signup () { |
139 | this.signupError = undefined | 144 | this.signupError = undefined |
140 | 145 | ||
141 | const body: UserRegister = await this.hooks.wrapObject( | 146 | const termsForm = this.formStepTerms.value |
147 | const userForm = this.formStepUser.value | ||
148 | const channelForm = this.formStepChannel?.value | ||
149 | |||
150 | const channel = this.formStepChannel?.value?.name | ||
151 | ? { name: channelForm?.name, displayName: channelForm?.displayName } | ||
152 | : undefined | ||
153 | |||
154 | const body = await this.hooks.wrapObject( | ||
142 | { | 155 | { |
143 | ...this.formStepUser.value, | 156 | username: userForm.username, |
157 | password: userForm.password, | ||
158 | email: userForm.email, | ||
159 | displayName: userForm.displayName, | ||
160 | |||
161 | registrationReason: termsForm.registrationReason, | ||
144 | 162 | ||
145 | channel: this.formStepChannel?.value?.name | 163 | channel |
146 | ? this.formStepChannel.value | ||
147 | : undefined | ||
148 | }, | 164 | }, |
149 | 'signup', | 165 | 'signup', |
150 | 'filter:api.signup.registration.create.params' | 166 | 'filter:api.signup.registration.create.params' |
151 | ) | 167 | ) |
152 | 168 | ||
153 | this.userSignupService.signup(body).subscribe({ | 169 | const obs = this.requiresApproval |
170 | ? this.signupService.requestSignup(body) | ||
171 | : this.signupService.directSignup(body) | ||
172 | |||
173 | obs.subscribe({ | ||
154 | next: () => { | 174 | next: () => { |
155 | if (this.requiresEmailVerification) { | 175 | if (this.requiresEmailVerification || this.requiresApproval) { |
156 | this.signupSuccess = true | 176 | this.signupSuccess = true |
157 | return | 177 | return |
158 | } | 178 | } |
159 | 179 | ||
160 | // Auto login | 180 | // Auto login |
161 | this.authService.login({ username: body.username, password: body.password }) | 181 | this.autoLogin(body) |
162 | .subscribe({ | ||
163 | next: () => { | ||
164 | this.signupSuccess = true | ||
165 | }, | ||
166 | |||
167 | error: err => { | ||
168 | this.signupError = err.message | ||
169 | } | ||
170 | }) | ||
171 | }, | 182 | }, |
172 | 183 | ||
173 | error: err => { | 184 | error: err => { |
@@ -175,4 +186,17 @@ export class RegisterComponent implements OnInit { | |||
175 | } | 186 | } |
176 | }) | 187 | }) |
177 | } | 188 | } |
189 | |||
190 | private autoLogin (body: UserRegister) { | ||
191 | this.authService.login({ username: body.username, password: body.password }) | ||
192 | .subscribe({ | ||
193 | next: () => { | ||
194 | this.signupSuccess = true | ||
195 | }, | ||
196 | |||
197 | error: err => { | ||
198 | this.signupError = err.message | ||
199 | } | ||
200 | }) | ||
201 | } | ||
178 | } | 202 | } |
diff --git a/client/src/app/+signup/+register/shared/index.ts b/client/src/app/+signup/+register/shared/index.ts new file mode 100644 index 000000000..affb54bf4 --- /dev/null +++ b/client/src/app/+signup/+register/shared/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './register-validators' | |||
diff --git a/client/src/app/+signup/+register/shared/register-validators.ts b/client/src/app/+signup/+register/shared/register-validators.ts new file mode 100644 index 000000000..f14803b68 --- /dev/null +++ b/client/src/app/+signup/+register/shared/register-validators.ts | |||
@@ -0,0 +1,18 @@ | |||
1 | import { Validators } from '@angular/forms' | ||
2 | import { BuildFormValidator } from '@app/shared/form-validators' | ||
3 | |||
4 | export const REGISTER_TERMS_VALIDATOR: BuildFormValidator = { | ||
5 | VALIDATORS: [ Validators.requiredTrue ], | ||
6 | MESSAGES: { | ||
7 | required: $localize`You must agree with the instance terms in order to register on it.` | ||
8 | } | ||
9 | } | ||
10 | |||
11 | export const REGISTER_REASON_VALIDATOR: BuildFormValidator = { | ||
12 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], | ||
13 | MESSAGES: { | ||
14 | required: $localize`Registration reason is required.`, | ||
15 | minlength: $localize`Registration reason must be at least 2 characters long.`, | ||
16 | maxlength: $localize`Registration reason cannot be more than 3000 characters long.` | ||
17 | } | ||
18 | } | ||
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.html b/client/src/app/+signup/+register/steps/register-step-about.component.html index 769fe3127..580e8a92c 100644 --- a/client/src/app/+signup/+register/steps/register-step-about.component.html +++ b/client/src/app/+signup/+register/steps/register-step-about.component.html | |||
@@ -13,6 +13,10 @@ | |||
13 | <li i18n>Have access to your <strong>watch history</strong></li> | 13 | <li i18n>Have access to your <strong>watch history</strong></li> |
14 | <li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li> | 14 | <li *ngIf="!videoUploadDisabled" i18n>Create your channel to <strong>publish videos</strong></li> |
15 | </ul> | 15 | </ul> |
16 | |||
17 | <p *ngIf="requiresApproval" i18n> | ||
18 | Moderators of {{ instanceName }} will have to approve your registration request once you have finished to fill the form. | ||
19 | </p> | ||
16 | </div> | 20 | </div> |
17 | 21 | ||
18 | <div> | 22 | <div> |
diff --git a/client/src/app/+signup/+register/steps/register-step-about.component.ts b/client/src/app/+signup/+register/steps/register-step-about.component.ts index 9a0941016..b176ffa59 100644 --- a/client/src/app/+signup/+register/steps/register-step-about.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-about.component.ts | |||
@@ -7,6 +7,7 @@ import { ServerService } from '@app/core' | |||
7 | styleUrls: [ './register-step-about.component.scss' ] | 7 | styleUrls: [ './register-step-about.component.scss' ] |
8 | }) | 8 | }) |
9 | export class RegisterStepAboutComponent { | 9 | export class RegisterStepAboutComponent { |
10 | @Input() requiresApproval: boolean | ||
10 | @Input() videoUploadDisabled: boolean | 11 | @Input() videoUploadDisabled: boolean |
11 | 12 | ||
12 | constructor (private serverService: ServerService) { | 13 | constructor (private serverService: ServerService) { |
diff --git a/client/src/app/+signup/+register/steps/register-step-channel.component.ts b/client/src/app/+signup/+register/steps/register-step-channel.component.ts index df92c5145..478ca0177 100644 --- a/client/src/app/+signup/+register/steps/register-step-channel.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-channel.component.ts | |||
@@ -2,9 +2,9 @@ import { concat, of } from 'rxjs' | |||
2 | import { pairwise } from 'rxjs/operators' | 2 | import { pairwise } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
4 | import { FormGroup } from '@angular/forms' | 4 | import { FormGroup } from '@angular/forms' |
5 | import { SignupService } from '@app/+signup/shared/signup.service' | ||
5 | import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' | 6 | import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' |
6 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 7 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
7 | import { UserSignupService } from '@app/shared/shared-users' | ||
8 | 8 | ||
9 | @Component({ | 9 | @Component({ |
10 | selector: 'my-register-step-channel', | 10 | selector: 'my-register-step-channel', |
@@ -20,7 +20,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit | |||
20 | 20 | ||
21 | constructor ( | 21 | constructor ( |
22 | protected formReactiveService: FormReactiveService, | 22 | protected formReactiveService: FormReactiveService, |
23 | private userSignupService: UserSignupService | 23 | private signupService: SignupService |
24 | ) { | 24 | ) { |
25 | super() | 25 | super() |
26 | } | 26 | } |
@@ -51,7 +51,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit | |||
51 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { | 51 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { |
52 | const name = this.form.value['name'] || '' | 52 | const name = this.form.value['name'] || '' |
53 | 53 | ||
54 | const newName = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, name) | 54 | const newName = this.signupService.getNewUsername(oldDisplayName, newDisplayName, name) |
55 | this.form.patchValue({ name: newName }) | 55 | this.form.patchValue({ name: newName }) |
56 | } | 56 | } |
57 | } | 57 | } |
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.html b/client/src/app/+signup/+register/steps/register-step-terms.component.html index cbfb32518..1d753a3f2 100644 --- a/client/src/app/+signup/+register/steps/register-step-terms.component.html +++ b/client/src/app/+signup/+register/steps/register-step-terms.component.html | |||
@@ -1,4 +1,16 @@ | |||
1 | <form role="form" [formGroup]="form"> | 1 | <form role="form" [formGroup]="form"> |
2 | |||
3 | <div *ngIf="requiresApproval" class="form-group"> | ||
4 | <label i18n for="registrationReason">Why do you want to join {{ instanceName }}?</label> | ||
5 | |||
6 | <textarea | ||
7 | id="registrationReason" formControlName="registrationReason" class="form-control" rows="4" | ||
8 | [ngClass]="{ 'input-error': formErrors['registrationReason'] }" | ||
9 | ></textarea> | ||
10 | |||
11 | <div *ngIf="formErrors.registrationReason" class="form-error">{{ formErrors.registrationReason }}</div> | ||
12 | </div> | ||
13 | |||
2 | <div class="form-group"> | 14 | <div class="form-group"> |
3 | <my-peertube-checkbox inputName="terms" formControlName="terms"> | 15 | <my-peertube-checkbox inputName="terms" formControlName="terms"> |
4 | <ng-template ptTemplate="label"> | 16 | <ng-template ptTemplate="label"> |
@@ -6,7 +18,7 @@ | |||
6 | I am at least {{ minimumAge }} years old and agree | 18 | I am at least {{ minimumAge }} years old and agree |
7 | to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a> | 19 | to the <a class="link-orange" (click)="onTermsClick($event)" href='#'>Terms</a> |
8 | <ng-container *ngIf="hasCodeOfConduct"> and to the <a class="link-orange" (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> | 20 | <ng-container *ngIf="hasCodeOfConduct"> and to the <a class="link-orange" (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> |
9 | of this instance | 21 | of {{ instanceName }} |
10 | </ng-container> | 22 | </ng-container> |
11 | </ng-template> | 23 | </ng-template> |
12 | </my-peertube-checkbox> | 24 | </my-peertube-checkbox> |
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.ts b/client/src/app/+signup/+register/steps/register-step-terms.component.ts index 2df963b30..1b1fb49ee 100644 --- a/client/src/app/+signup/+register/steps/register-step-terms.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-terms.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
2 | import { FormGroup } from '@angular/forms' | 2 | import { FormGroup } from '@angular/forms' |
3 | import { USER_TERMS_VALIDATOR } from '@app/shared/form-validators/user-validators' | ||
4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 3 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
4 | import { REGISTER_REASON_VALIDATOR, REGISTER_TERMS_VALIDATOR } from '../shared' | ||
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'my-register-step-terms', | 7 | selector: 'my-register-step-terms', |
@@ -10,7 +10,9 @@ import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | |||
10 | }) | 10 | }) |
11 | export class RegisterStepTermsComponent extends FormReactive implements OnInit { | 11 | export class RegisterStepTermsComponent extends FormReactive implements OnInit { |
12 | @Input() hasCodeOfConduct = false | 12 | @Input() hasCodeOfConduct = false |
13 | @Input() requiresApproval: boolean | ||
13 | @Input() minimumAge = 16 | 14 | @Input() minimumAge = 16 |
15 | @Input() instanceName: string | ||
14 | 16 | ||
15 | @Output() formBuilt = new EventEmitter<FormGroup>() | 17 | @Output() formBuilt = new EventEmitter<FormGroup>() |
16 | @Output() termsClick = new EventEmitter<void>() | 18 | @Output() termsClick = new EventEmitter<void>() |
@@ -28,7 +30,11 @@ export class RegisterStepTermsComponent extends FormReactive implements OnInit { | |||
28 | 30 | ||
29 | ngOnInit () { | 31 | ngOnInit () { |
30 | this.buildForm({ | 32 | this.buildForm({ |
31 | terms: USER_TERMS_VALIDATOR | 33 | terms: REGISTER_TERMS_VALIDATOR, |
34 | |||
35 | registrationReason: this.requiresApproval | ||
36 | ? REGISTER_REASON_VALIDATOR | ||
37 | : null | ||
32 | }) | 38 | }) |
33 | 39 | ||
34 | setTimeout(() => this.formBuilt.emit(this.form)) | 40 | setTimeout(() => this.formBuilt.emit(this.form)) |
diff --git a/client/src/app/+signup/+register/steps/register-step-user.component.ts b/client/src/app/+signup/+register/steps/register-step-user.component.ts index 822f8f5c5..0a5d2e437 100644 --- a/client/src/app/+signup/+register/steps/register-step-user.component.ts +++ b/client/src/app/+signup/+register/steps/register-step-user.component.ts | |||
@@ -2,6 +2,7 @@ import { concat, of } from 'rxjs' | |||
2 | import { pairwise } from 'rxjs/operators' | 2 | import { pairwise } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
4 | import { FormGroup } from '@angular/forms' | 4 | import { FormGroup } from '@angular/forms' |
5 | import { SignupService } from '@app/+signup/shared/signup.service' | ||
5 | import { | 6 | import { |
6 | USER_DISPLAY_NAME_REQUIRED_VALIDATOR, | 7 | USER_DISPLAY_NAME_REQUIRED_VALIDATOR, |
7 | USER_EMAIL_VALIDATOR, | 8 | USER_EMAIL_VALIDATOR, |
@@ -9,7 +10,6 @@ import { | |||
9 | USER_USERNAME_VALIDATOR | 10 | USER_USERNAME_VALIDATOR |
10 | } from '@app/shared/form-validators/user-validators' | 11 | } from '@app/shared/form-validators/user-validators' |
11 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 12 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
12 | import { UserSignupService } from '@app/shared/shared-users' | ||
13 | 13 | ||
14 | @Component({ | 14 | @Component({ |
15 | selector: 'my-register-step-user', | 15 | selector: 'my-register-step-user', |
@@ -24,7 +24,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { | |||
24 | 24 | ||
25 | constructor ( | 25 | constructor ( |
26 | protected formReactiveService: FormReactiveService, | 26 | protected formReactiveService: FormReactiveService, |
27 | private userSignupService: UserSignupService | 27 | private signupService: SignupService |
28 | ) { | 28 | ) { |
29 | super() | 29 | super() |
30 | } | 30 | } |
@@ -57,7 +57,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { | |||
57 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { | 57 | private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { |
58 | const username = this.form.value['username'] || '' | 58 | const username = this.form.value['username'] || '' |
59 | 59 | ||
60 | const newUsername = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, username) | 60 | const newUsername = this.signupService.getNewUsername(oldDisplayName, newDisplayName, username) |
61 | this.form.patchValue({ username: newUsername }) | 61 | this.form.patchValue({ username: newUsername }) |
62 | } | 62 | } |
63 | } | 63 | } |
diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts index 06905f678..75b599e0e 100644 --- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { SignupService } from '@app/+signup/shared/signup.service' | ||
2 | import { Notifier, RedirectService, ServerService } from '@app/core' | 3 | import { Notifier, RedirectService, ServerService } from '@app/core' |
3 | import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' | 4 | import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' |
4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { UserSignupService } from '@app/shared/shared-users' | ||
6 | 6 | ||
7 | @Component({ | 7 | @Component({ |
8 | selector: 'my-verify-account-ask-send-email', | 8 | selector: 'my-verify-account-ask-send-email', |
@@ -15,7 +15,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements | |||
15 | 15 | ||
16 | constructor ( | 16 | constructor ( |
17 | protected formReactiveService: FormReactiveService, | 17 | protected formReactiveService: FormReactiveService, |
18 | private userSignupService: UserSignupService, | 18 | private signupService: SignupService, |
19 | private serverService: ServerService, | 19 | private serverService: ServerService, |
20 | private notifier: Notifier, | 20 | private notifier: Notifier, |
21 | private redirectService: RedirectService | 21 | private redirectService: RedirectService |
@@ -34,7 +34,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements | |||
34 | 34 | ||
35 | askSendVerifyEmail () { | 35 | askSendVerifyEmail () { |
36 | const email = this.form.value['verify-email-email'] | 36 | const email = this.form.value['verify-email-email'] |
37 | this.userSignupService.askSendVerifyEmail(email) | 37 | this.signupService.askSendVerifyEmail(email) |
38 | .subscribe({ | 38 | .subscribe({ |
39 | next: () => { | 39 | next: () => { |
40 | this.notifier.success($localize`An email with verification link will be sent to ${email}.`) | 40 | this.notifier.success($localize`An email with verification link will be sent to ${email}.`) |
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html index 122f3c28c..8c8b1098e 100644 --- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.html | |||
@@ -1,14 +1,19 @@ | |||
1 | <div class="margin-content"> | 1 | <div *ngIf="loaded" class="margin-content"> |
2 | <h1 i18n class="title-page">Verify account email confirmation</h1> | 2 | <h1 i18n class="title-page">Verify email</h1> |
3 | 3 | ||
4 | <my-signup-success i18n *ngIf="!isPendingEmail && success" [requiresEmailVerification]="false"> | 4 | <my-signup-success-after-email |
5 | </my-signup-success> | 5 | *ngIf="displaySignupSuccess()" |
6 | [requiresApproval]="isRegistrationRequest() && requiresApproval" | ||
7 | > | ||
8 | </my-signup-success-after-email> | ||
6 | 9 | ||
7 | <div i18n class="alert alert-success" *ngIf="isPendingEmail && success">Email updated.</div> | 10 | <div i18n class="alert alert-success" *ngIf="!isRegistrationRequest() && isPendingEmail && success">Email updated.</div> |
8 | 11 | ||
9 | <div class="alert alert-danger" *ngIf="failed"> | 12 | <div class="alert alert-danger" *ngIf="failed"> |
10 | <span i18n>An error occurred.</span> | 13 | <span i18n>An error occurred.</span> |
11 | 14 | ||
12 | <a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email" [queryParams]="{ isPendingEmail: isPendingEmail }">Request new verification email</a> | 15 | <a i18n class="ms-1 link-orange" routerLink="/verify-account/ask-send-email"> |
16 | Request a new verification email | ||
17 | </a> | ||
13 | </div> | 18 | </div> |
14 | </div> | 19 | </div> |
diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts index 88efce4a1..faf663391 100644 --- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ActivatedRoute } from '@angular/router' | 2 | import { ActivatedRoute } from '@angular/router' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { SignupService } from '@app/+signup/shared/signup.service' |
4 | import { UserSignupService } from '@app/shared/shared-users' | 4 | import { AuthService, Notifier, ServerService } from '@app/core' |
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
7 | selector: 'my-verify-account-email', | 7 | selector: 'my-verify-account-email', |
@@ -13,32 +13,82 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
13 | failed = false | 13 | failed = false |
14 | isPendingEmail = false | 14 | isPendingEmail = false |
15 | 15 | ||
16 | requiresApproval: boolean | ||
17 | loaded = false | ||
18 | |||
16 | private userId: number | 19 | private userId: number |
20 | private registrationId: number | ||
17 | private verificationString: string | 21 | private verificationString: string |
18 | 22 | ||
19 | constructor ( | 23 | constructor ( |
20 | private userSignupService: UserSignupService, | 24 | private signupService: SignupService, |
25 | private server: ServerService, | ||
21 | private authService: AuthService, | 26 | private authService: AuthService, |
22 | private notifier: Notifier, | 27 | private notifier: Notifier, |
23 | private route: ActivatedRoute | 28 | private route: ActivatedRoute |
24 | ) { | 29 | ) { |
25 | } | 30 | } |
26 | 31 | ||
32 | get instanceName () { | ||
33 | return this.server.getHTMLConfig().instance.name | ||
34 | } | ||
35 | |||
27 | ngOnInit () { | 36 | ngOnInit () { |
28 | const queryParams = this.route.snapshot.queryParams | 37 | const queryParams = this.route.snapshot.queryParams |
38 | |||
39 | this.server.getConfig().subscribe(config => { | ||
40 | this.requiresApproval = config.signup.requiresApproval | ||
41 | |||
42 | this.loaded = true | ||
43 | }) | ||
44 | |||
29 | this.userId = queryParams['userId'] | 45 | this.userId = queryParams['userId'] |
46 | this.registrationId = queryParams['registrationId'] | ||
47 | |||
30 | this.verificationString = queryParams['verificationString'] | 48 | this.verificationString = queryParams['verificationString'] |
49 | |||
31 | this.isPendingEmail = queryParams['isPendingEmail'] === 'true' | 50 | this.isPendingEmail = queryParams['isPendingEmail'] === 'true' |
32 | 51 | ||
33 | if (!this.userId || !this.verificationString) { | 52 | if (!this.verificationString) { |
34 | this.notifier.error($localize`Unable to find user id or verification string.`) | 53 | this.notifier.error($localize`Unable to find verification string in URL query.`) |
35 | } else { | 54 | return |
36 | this.verifyEmail() | 55 | } |
56 | |||
57 | if (!this.userId && !this.registrationId) { | ||
58 | this.notifier.error($localize`Unable to find user id or registration id in URL query.`) | ||
59 | return | ||
37 | } | 60 | } |
61 | |||
62 | this.verifyEmail() | ||
63 | } | ||
64 | |||
65 | isRegistrationRequest () { | ||
66 | return !!this.registrationId | ||
67 | } | ||
68 | |||
69 | displaySignupSuccess () { | ||
70 | if (!this.success) return false | ||
71 | if (!this.isRegistrationRequest() && this.isPendingEmail) return false | ||
72 | |||
73 | return true | ||
38 | } | 74 | } |
39 | 75 | ||
40 | verifyEmail () { | 76 | verifyEmail () { |
41 | this.userSignupService.verifyEmail(this.userId, this.verificationString, this.isPendingEmail) | 77 | if (this.isRegistrationRequest()) { |
78 | return this.verifyRegistrationEmail() | ||
79 | } | ||
80 | |||
81 | return this.verifyUserEmail() | ||
82 | } | ||
83 | |||
84 | private verifyUserEmail () { | ||
85 | const options = { | ||
86 | userId: this.userId, | ||
87 | verificationString: this.verificationString, | ||
88 | isPendingEmail: this.isPendingEmail | ||
89 | } | ||
90 | |||
91 | this.signupService.verifyUserEmail(options) | ||
42 | .subscribe({ | 92 | .subscribe({ |
43 | next: () => { | 93 | next: () => { |
44 | if (this.authService.isLoggedIn()) { | 94 | if (this.authService.isLoggedIn()) { |
@@ -55,4 +105,24 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
55 | } | 105 | } |
56 | }) | 106 | }) |
57 | } | 107 | } |
108 | |||
109 | private verifyRegistrationEmail () { | ||
110 | const options = { | ||
111 | registrationId: this.registrationId, | ||
112 | verificationString: this.verificationString | ||
113 | } | ||
114 | |||
115 | this.signupService.verifyRegistrationEmail(options) | ||
116 | .subscribe({ | ||
117 | next: () => { | ||
118 | this.success = true | ||
119 | }, | ||
120 | |||
121 | error: err => { | ||
122 | this.failed = true | ||
123 | |||
124 | this.notifier.error(err.message) | ||
125 | } | ||
126 | }) | ||
127 | } | ||
58 | } | 128 | } |
diff --git a/client/src/app/+signup/shared/shared-signup.module.ts b/client/src/app/+signup/shared/shared-signup.module.ts index 0aa08f3e2..0600f0af8 100644 --- a/client/src/app/+signup/shared/shared-signup.module.ts +++ b/client/src/app/+signup/shared/shared-signup.module.ts | |||
@@ -5,7 +5,9 @@ import { SharedMainModule } from '@app/shared/shared-main' | |||
5 | import { SharedUsersModule } from '@app/shared/shared-users' | 5 | import { SharedUsersModule } from '@app/shared/shared-users' |
6 | import { SignupMascotComponent } from './signup-mascot.component' | 6 | import { SignupMascotComponent } from './signup-mascot.component' |
7 | import { SignupStepTitleComponent } from './signup-step-title.component' | 7 | import { SignupStepTitleComponent } from './signup-step-title.component' |
8 | import { SignupSuccessComponent } from './signup-success.component' | 8 | import { SignupSuccessBeforeEmailComponent } from './signup-success-before-email.component' |
9 | import { SignupSuccessAfterEmailComponent } from './signup-success-after-email.component' | ||
10 | import { SignupService } from './signup.service' | ||
9 | 11 | ||
10 | @NgModule({ | 12 | @NgModule({ |
11 | imports: [ | 13 | imports: [ |
@@ -16,7 +18,8 @@ import { SignupSuccessComponent } from './signup-success.component' | |||
16 | ], | 18 | ], |
17 | 19 | ||
18 | declarations: [ | 20 | declarations: [ |
19 | SignupSuccessComponent, | 21 | SignupSuccessBeforeEmailComponent, |
22 | SignupSuccessAfterEmailComponent, | ||
20 | SignupStepTitleComponent, | 23 | SignupStepTitleComponent, |
21 | SignupMascotComponent | 24 | SignupMascotComponent |
22 | ], | 25 | ], |
@@ -26,12 +29,14 @@ import { SignupSuccessComponent } from './signup-success.component' | |||
26 | SharedFormModule, | 29 | SharedFormModule, |
27 | SharedGlobalIconModule, | 30 | SharedGlobalIconModule, |
28 | 31 | ||
29 | SignupSuccessComponent, | 32 | SignupSuccessBeforeEmailComponent, |
33 | SignupSuccessAfterEmailComponent, | ||
30 | SignupStepTitleComponent, | 34 | SignupStepTitleComponent, |
31 | SignupMascotComponent | 35 | SignupMascotComponent |
32 | ], | 36 | ], |
33 | 37 | ||
34 | providers: [ | 38 | providers: [ |
39 | SignupService | ||
35 | ] | 40 | ] |
36 | }) | 41 | }) |
37 | export class SharedSignupModule { } | 42 | export class SharedSignupModule { } |
diff --git a/client/src/app/+signup/shared/signup-success-after-email.component.html b/client/src/app/+signup/shared/signup-success-after-email.component.html new file mode 100644 index 000000000..1c3536ada --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-after-email.component.html | |||
@@ -0,0 +1,21 @@ | |||
1 | <my-signup-step-title mascotImageName="success"> | ||
2 | <strong i18n>Email verified!</strong> | ||
3 | </my-signup-step-title> | ||
4 | |||
5 | <div class="alert pt-alert-primary"> | ||
6 | <ng-container *ngIf="requiresApproval"> | ||
7 | <p i18n>Your email has been verified and your account request has been sent!</p> | ||
8 | |||
9 | <p i18n> | ||
10 | A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected. | ||
11 | </p> | ||
12 | </ng-container> | ||
13 | |||
14 | <ng-container *ngIf="!requiresApproval"> | ||
15 | <p i18n>Your email has been verified and your account has been created!</p> | ||
16 | |||
17 | <p i18n> | ||
18 | If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
19 | </p> | ||
20 | </ng-container> | ||
21 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success-after-email.component.ts b/client/src/app/+signup/shared/signup-success-after-email.component.ts new file mode 100644 index 000000000..3d72fdae9 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-after-email.component.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-success-after-email', | ||
5 | templateUrl: './signup-success-after-email.component.html', | ||
6 | styleUrls: [ './signup-success.component.scss' ] | ||
7 | }) | ||
8 | export class SignupSuccessAfterEmailComponent { | ||
9 | @Input() requiresApproval: boolean | ||
10 | } | ||
diff --git a/client/src/app/+signup/shared/signup-success-before-email.component.html b/client/src/app/+signup/shared/signup-success-before-email.component.html new file mode 100644 index 000000000..b9668ee82 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-before-email.component.html | |||
@@ -0,0 +1,35 @@ | |||
1 | <my-signup-step-title mascotImageName="success"> | ||
2 | <ng-container *ngIf="requiresApproval"> | ||
3 | <strong i18n>Account request sent</strong> | ||
4 | </ng-container> | ||
5 | |||
6 | <ng-container *ngIf="!requiresApproval" i18n> | ||
7 | <strong>Welcome</strong> | ||
8 | <div>on {{ instanceName }}</div> | ||
9 | </ng-container> | ||
10 | </my-signup-step-title> | ||
11 | |||
12 | <div class="alert pt-alert-primary"> | ||
13 | <p *ngIf="requiresApproval" i18n>Your account request has been sent!</p> | ||
14 | <p *ngIf="!requiresApproval" i18n>Your account has been created!</p> | ||
15 | |||
16 | <ng-container *ngIf="requiresEmailVerification"> | ||
17 | <p i18n *ngIf="requiresApproval"> | ||
18 | <strong>Check your emails</strong> to validate your account and complete your registration request. | ||
19 | </p> | ||
20 | |||
21 | <p i18n *ngIf="!requiresApproval"> | ||
22 | <strong>Check your emails</strong> to validate your account and complete your registration. | ||
23 | </p> | ||
24 | </ng-container> | ||
25 | |||
26 | <ng-container *ngIf="!requiresEmailVerification"> | ||
27 | <p i18n *ngIf="requiresApproval"> | ||
28 | A moderator will check your registration request soon and you'll receive an email when it will be accepted or rejected. | ||
29 | </p> | ||
30 | |||
31 | <p *ngIf="!requiresApproval" i18n> | ||
32 | If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
33 | </p> | ||
34 | </ng-container> | ||
35 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success-before-email.component.ts b/client/src/app/+signup/shared/signup-success-before-email.component.ts new file mode 100644 index 000000000..d72462340 --- /dev/null +++ b/client/src/app/+signup/shared/signup-success-before-email.component.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-success-before-email', | ||
5 | templateUrl: './signup-success-before-email.component.html', | ||
6 | styleUrls: [ './signup-success.component.scss' ] | ||
7 | }) | ||
8 | export class SignupSuccessBeforeEmailComponent { | ||
9 | @Input() requiresApproval: boolean | ||
10 | @Input() requiresEmailVerification: boolean | ||
11 | @Input() instanceName: string | ||
12 | } | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.html b/client/src/app/+signup/shared/signup-success.component.html deleted file mode 100644 index c14889c72..000000000 --- a/client/src/app/+signup/shared/signup-success.component.html +++ /dev/null | |||
@@ -1,22 +0,0 @@ | |||
1 | <my-signup-step-title mascotImageName="success" i18n> | ||
2 | <strong>Welcome</strong> | ||
3 | <div>on {{ instanceName }}</div> | ||
4 | </my-signup-step-title> | ||
5 | |||
6 | <div class="alert pt-alert-primary"> | ||
7 | <p i18n>Your account has been created!</p> | ||
8 | |||
9 | <p i18n *ngIf="requiresEmailVerification"> | ||
10 | <strong>Check your emails</strong> to validate your account and complete your inscription. | ||
11 | </p> | ||
12 | |||
13 | <ng-container *ngIf="!requiresEmailVerification"> | ||
14 | <p i18n> | ||
15 | If you need help to use PeerTube, you can have a look at the <a class="link-orange" href="https://docs.joinpeertube.org/use-setup-account" target="_blank" rel="noopener noreferrer">documentation</a>. | ||
16 | </p> | ||
17 | |||
18 | <p i18n> | ||
19 | To help moderators and other users to know <strong>who you are</strong>, don't forget to <a class="link-orange" routerLink="/my-account/settings">set up your account profile</a> by adding an <strong>avatar</strong> and a <strong>description</strong>. | ||
20 | </p> | ||
21 | </ng-container> | ||
22 | </div> | ||
diff --git a/client/src/app/+signup/shared/signup-success.component.ts b/client/src/app/+signup/shared/signup-success.component.ts deleted file mode 100644 index a03f3819d..000000000 --- a/client/src/app/+signup/shared/signup-success.component.ts +++ /dev/null | |||
@@ -1,19 +0,0 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { ServerService } from '@app/core' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-signup-success', | ||
6 | templateUrl: './signup-success.component.html', | ||
7 | styleUrls: [ './signup-success.component.scss' ] | ||
8 | }) | ||
9 | export class SignupSuccessComponent { | ||
10 | @Input() requiresEmailVerification: boolean | ||
11 | |||
12 | constructor (private serverService: ServerService) { | ||
13 | |||
14 | } | ||
15 | |||
16 | get instanceName () { | ||
17 | return this.serverService.getHTMLConfig().instance.name | ||
18 | } | ||
19 | } | ||
diff --git a/client/src/app/shared/shared-users/user-signup.service.ts b/client/src/app/+signup/shared/signup.service.ts index 46fe34af1..f647298be 100644 --- a/client/src/app/shared/shared-users/user-signup.service.ts +++ b/client/src/app/+signup/shared/signup.service.ts | |||
@@ -2,17 +2,18 @@ import { catchError, tap } from 'rxjs/operators' | |||
2 | import { HttpClient } from '@angular/common/http' | 2 | import { HttpClient } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { RestExtractor, UserService } from '@app/core' | 4 | import { RestExtractor, UserService } from '@app/core' |
5 | import { UserRegister } from '@shared/models' | 5 | import { UserRegister, UserRegistrationRequest } from '@shared/models' |
6 | 6 | ||
7 | @Injectable() | 7 | @Injectable() |
8 | export class UserSignupService { | 8 | export class SignupService { |
9 | |||
9 | constructor ( | 10 | constructor ( |
10 | private authHttp: HttpClient, | 11 | private authHttp: HttpClient, |
11 | private restExtractor: RestExtractor, | 12 | private restExtractor: RestExtractor, |
12 | private userService: UserService | 13 | private userService: UserService |
13 | ) { } | 14 | ) { } |
14 | 15 | ||
15 | signup (userCreate: UserRegister) { | 16 | directSignup (userCreate: UserRegister) { |
16 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) | 17 | return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) |
17 | .pipe( | 18 | .pipe( |
18 | tap(() => this.userService.setSignupInThisSession(true)), | 19 | tap(() => this.userService.setSignupInThisSession(true)), |
@@ -20,8 +21,21 @@ export class UserSignupService { | |||
20 | ) | 21 | ) |
21 | } | 22 | } |
22 | 23 | ||
23 | verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) { | 24 | requestSignup (userCreate: UserRegistrationRequest) { |
24 | const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` | 25 | return this.authHttp.post(UserService.BASE_USERS_URL + 'registrations/request', userCreate) |
26 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
27 | } | ||
28 | |||
29 | // --------------------------------------------------------------------------- | ||
30 | |||
31 | verifyUserEmail (options: { | ||
32 | userId: number | ||
33 | verificationString: string | ||
34 | isPendingEmail: boolean | ||
35 | }) { | ||
36 | const { userId, verificationString, isPendingEmail } = options | ||
37 | |||
38 | const url = `${UserService.BASE_USERS_URL}${userId}/verify-email` | ||
25 | const body = { | 39 | const body = { |
26 | verificationString, | 40 | verificationString, |
27 | isPendingEmail | 41 | isPendingEmail |
@@ -31,13 +45,28 @@ export class UserSignupService { | |||
31 | .pipe(catchError(res => this.restExtractor.handleError(res))) | 45 | .pipe(catchError(res => this.restExtractor.handleError(res))) |
32 | } | 46 | } |
33 | 47 | ||
48 | verifyRegistrationEmail (options: { | ||
49 | registrationId: number | ||
50 | verificationString: string | ||
51 | }) { | ||
52 | const { registrationId, verificationString } = options | ||
53 | |||
54 | const url = `${UserService.BASE_USERS_URL}registrations/${registrationId}/verify-email` | ||
55 | const body = { verificationString } | ||
56 | |||
57 | return this.authHttp.post(url, body) | ||
58 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
59 | } | ||
60 | |||
34 | askSendVerifyEmail (email: string) { | 61 | askSendVerifyEmail (email: string) { |
35 | const url = UserService.BASE_USERS_URL + '/ask-send-verify-email' | 62 | const url = UserService.BASE_USERS_URL + 'ask-send-verify-email' |
36 | 63 | ||
37 | return this.authHttp.post(url, { email }) | 64 | return this.authHttp.post(url, { email }) |
38 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 65 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
39 | } | 66 | } |
40 | 67 | ||
68 | // --------------------------------------------------------------------------- | ||
69 | |||
41 | getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { | 70 | getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { |
42 | // Don't update display name, the user seems to have changed it | 71 | // Don't update display name, the user seems to have changed it |
43 | if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername | 72 | if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 94853423b..84548de97 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -133,8 +133,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
133 | this.loadRouteParams() | 133 | this.loadRouteParams() |
134 | this.loadRouteQuery() | 134 | this.loadRouteQuery() |
135 | 135 | ||
136 | this.initHotkeys() | ||
137 | |||
138 | this.theaterEnabled = getStoredTheater() | 136 | this.theaterEnabled = getStoredTheater() |
139 | 137 | ||
140 | this.hooks.runAction('action:video-watch.init', 'video-watch') | 138 | this.hooks.runAction('action:video-watch.init', 'video-watch') |
@@ -295,6 +293,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
295 | subtitle: queryParams.subtitle, | 293 | subtitle: queryParams.subtitle, |
296 | 294 | ||
297 | playerMode: queryParams.mode, | 295 | playerMode: queryParams.mode, |
296 | playbackRate: queryParams.playbackRate, | ||
298 | peertubeLink: false | 297 | peertubeLink: false |
299 | } | 298 | } |
300 | 299 | ||
@@ -406,6 +405,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
406 | if (res === false) return this.location.back() | 405 | if (res === false) return this.location.back() |
407 | } | 406 | } |
408 | 407 | ||
408 | this.buildHotkeysHelp(video) | ||
409 | |||
409 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) | 410 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) |
410 | .catch(err => logger.error('Cannot build the player', err)) | 411 | .catch(err => logger.error('Cannot build the player', err)) |
411 | 412 | ||
@@ -657,6 +658,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
657 | muted: urlOptions.muted, | 658 | muted: urlOptions.muted, |
658 | loop: urlOptions.loop, | 659 | loop: urlOptions.loop, |
659 | subtitle: urlOptions.subtitle, | 660 | subtitle: urlOptions.subtitle, |
661 | playbackRate: urlOptions.playbackRate, | ||
660 | 662 | ||
661 | peertubeLink: urlOptions.peertubeLink, | 663 | peertubeLink: urlOptions.peertubeLink, |
662 | 664 | ||
@@ -785,33 +787,43 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
785 | this.video.viewers = newViewers | 787 | this.video.viewers = newViewers |
786 | } | 788 | } |
787 | 789 | ||
788 | private initHotkeys () { | 790 | private buildHotkeysHelp (video: Video) { |
791 | if (this.hotkeys.length !== 0) { | ||
792 | this.hotkeysService.remove(this.hotkeys) | ||
793 | } | ||
794 | |||
789 | this.hotkeys = [ | 795 | this.hotkeys = [ |
790 | // These hotkeys are managed by the player | 796 | // These hotkeys are managed by the player |
791 | new Hotkey('f', e => e, undefined, $localize`Enter/exit fullscreen`), | 797 | new Hotkey('f', e => e, undefined, $localize`Enter/exit fullscreen`), |
792 | new Hotkey('space', e => e, undefined, $localize`Play/Pause the video`), | 798 | new Hotkey('space', e => e, undefined, $localize`Play/Pause the video`), |
793 | new Hotkey('m', e => e, undefined, $localize`Mute/unmute the video`), | 799 | new Hotkey('m', e => e, undefined, $localize`Mute/unmute the video`), |
794 | 800 | ||
795 | new Hotkey('0-9', e => e, undefined, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`), | ||
796 | |||
797 | new Hotkey('up', e => e, undefined, $localize`Increase the volume`), | 801 | new Hotkey('up', e => e, undefined, $localize`Increase the volume`), |
798 | new Hotkey('down', e => e, undefined, $localize`Decrease the volume`), | 802 | new Hotkey('down', e => e, undefined, $localize`Decrease the volume`), |
799 | 803 | ||
800 | new Hotkey('right', e => e, undefined, $localize`Seek the video forward`), | ||
801 | new Hotkey('left', e => e, undefined, $localize`Seek the video backward`), | ||
802 | |||
803 | new Hotkey('>', e => e, undefined, $localize`Increase playback rate`), | ||
804 | new Hotkey('<', e => e, undefined, $localize`Decrease playback rate`), | ||
805 | |||
806 | new Hotkey(',', e => e, undefined, $localize`Navigate in the video to the previous frame`), | ||
807 | new Hotkey('.', e => e, undefined, $localize`Navigate in the video to the next frame`), | ||
808 | |||
809 | new Hotkey('t', e => { | 804 | new Hotkey('t', e => { |
810 | this.theaterEnabled = !this.theaterEnabled | 805 | this.theaterEnabled = !this.theaterEnabled |
811 | return false | 806 | return false |
812 | }, undefined, $localize`Toggle theater mode`) | 807 | }, undefined, $localize`Toggle theater mode`) |
813 | ] | 808 | ] |
814 | 809 | ||
810 | if (!video.isLive) { | ||
811 | this.hotkeys = this.hotkeys.concat([ | ||
812 | // These hotkeys are also managed by the player but only for VOD | ||
813 | |||
814 | new Hotkey('0-9', e => e, undefined, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`), | ||
815 | |||
816 | new Hotkey('right', e => e, undefined, $localize`Seek the video forward`), | ||
817 | new Hotkey('left', e => e, undefined, $localize`Seek the video backward`), | ||
818 | |||
819 | new Hotkey('>', e => e, undefined, $localize`Increase playback rate`), | ||
820 | new Hotkey('<', e => e, undefined, $localize`Decrease playback rate`), | ||
821 | |||
822 | new Hotkey(',', e => e, undefined, $localize`Navigate in the video to the previous frame`), | ||
823 | new Hotkey('.', e => e, undefined, $localize`Navigate in the video to the next frame`) | ||
824 | ]) | ||
825 | } | ||
826 | |||
815 | if (this.isUserLoggedIn()) { | 827 | if (this.isUserLoggedIn()) { |
816 | this.hotkeys = this.hotkeys.concat([ | 828 | this.hotkeys = this.hotkeys.concat([ |
817 | new Hotkey('shift+s', () => { | 829 | new Hotkey('shift+s', () => { |
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.ts b/client/src/app/+videos/video-list/videos-list-common-page.component.ts index c8fa8ef30..bafe30fd7 100644 --- a/client/src/app/+videos/video-list/videos-list-common-page.component.ts +++ b/client/src/app/+videos/video-list/videos-list-common-page.component.ts | |||
@@ -177,6 +177,9 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable | |||
177 | case 'best': | 177 | case 'best': |
178 | return '-hot' | 178 | return '-hot' |
179 | 179 | ||
180 | case 'name': | ||
181 | return 'name' | ||
182 | |||
180 | default: | 183 | default: |
181 | return '-' + algorithm as VideoSortField | 184 | return '-' + algorithm as VideoSortField |
182 | } | 185 | } |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 4de28e51e..ed7eabb76 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -5,10 +5,11 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular | |||
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { Router } from '@angular/router' | 6 | import { Router } from '@angular/router' |
7 | import { Notifier } from '@app/core/notification/notifier.service' | 7 | import { Notifier } from '@app/core/notification/notifier.service' |
8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' | 8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage, PluginsManager } from '@root-helpers/index' |
9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' | 9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' |
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | import { RestExtractor } from '../rest/rest-extractor.service' | 11 | import { RestExtractor } from '../rest/rest-extractor.service' |
12 | import { ServerService } from '../server' | ||
12 | import { AuthStatus } from './auth-status.model' | 13 | import { AuthStatus } from './auth-status.model' |
13 | import { AuthUser } from './auth-user.model' | 14 | import { AuthUser } from './auth-user.model' |
14 | 15 | ||
@@ -44,6 +45,7 @@ export class AuthService { | |||
44 | private refreshingTokenObservable: Observable<any> | 45 | private refreshingTokenObservable: Observable<any> |
45 | 46 | ||
46 | constructor ( | 47 | constructor ( |
48 | private serverService: ServerService, | ||
47 | private http: HttpClient, | 49 | private http: HttpClient, |
48 | private notifier: Notifier, | 50 | private notifier: Notifier, |
49 | private hotkeysService: HotkeysService, | 51 | private hotkeysService: HotkeysService, |
@@ -213,25 +215,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular | |||
213 | const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') | 215 | const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') |
214 | 216 | ||
215 | this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) | 217 | this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) |
216 | .pipe( | 218 | .pipe( |
217 | map(res => this.handleRefreshToken(res)), | 219 | map(res => this.handleRefreshToken(res)), |
218 | tap(() => { | 220 | tap(() => { |
219 | this.refreshingTokenObservable = null | 221 | this.refreshingTokenObservable = null |
220 | }), | 222 | }), |
221 | catchError(err => { | 223 | catchError(err => { |
222 | this.refreshingTokenObservable = null | 224 | this.refreshingTokenObservable = null |
223 | 225 | ||
224 | logger.error(err) | 226 | logger.error(err) |
225 | logger.info('Cannot refresh token -> logout...') | 227 | logger.info('Cannot refresh token -> logout...') |
226 | this.logout() | 228 | this.logout() |
227 | this.router.navigate([ '/login' ]) | 229 | |
228 | 230 | const externalLoginUrl = PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverService.getHTMLConfig()) | |
229 | return observableThrowError(() => ({ | 231 | if (externalLoginUrl) window.location.href = externalLoginUrl |
230 | error: $localize`You need to reconnect.` | 232 | else this.router.navigate([ '/login' ]) |
231 | })) | 233 | |
232 | }), | 234 | return observableThrowError(() => ({ |
233 | share() | 235 | error: $localize`You need to reconnect.` |
234 | ) | 236 | })) |
237 | }), | ||
238 | share() | ||
239 | ) | ||
235 | 240 | ||
236 | return this.refreshingTokenObservable | 241 | return this.refreshingTokenObservable |
237 | } | 242 | } |
diff --git a/client/src/app/core/renderer/linkifier.service.ts b/client/src/app/core/renderer/linkifier.service.ts index 78df92cc9..d99591d6c 100644 --- a/client/src/app/core/renderer/linkifier.service.ts +++ b/client/src/app/core/renderer/linkifier.service.ts | |||
@@ -15,7 +15,7 @@ export class LinkifierService { | |||
15 | }, | 15 | }, |
16 | formatHref: { | 16 | formatHref: { |
17 | mention: (href: string) => { | 17 | mention: (href: string) => { |
18 | return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + href.substr(1) | 18 | return getAbsoluteAPIUrl() + '/services/redirect/accounts/' + href.substring(1) |
19 | } | 19 | } |
20 | } | 20 | } |
21 | } | 21 | } |
diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts index a5fd72862..dd23a1b01 100644 --- a/client/src/app/core/renderer/markdown.service.ts +++ b/client/src/app/core/renderer/markdown.service.ts | |||
@@ -64,8 +64,8 @@ export class MarkdownService { | |||
64 | 64 | ||
65 | textMarkdownToHTML (options: { | 65 | textMarkdownToHTML (options: { |
66 | markdown: string | 66 | markdown: string |
67 | withHtml?: boolean | 67 | withHtml?: boolean // default false |
68 | withEmoji?: boolean | 68 | withEmoji?: boolean // default false |
69 | }) { | 69 | }) { |
70 | const { markdown, withHtml = false, withEmoji = false } = options | 70 | const { markdown, withHtml = false, withEmoji = false } = options |
71 | 71 | ||
@@ -76,8 +76,8 @@ export class MarkdownService { | |||
76 | 76 | ||
77 | enhancedMarkdownToHTML (options: { | 77 | enhancedMarkdownToHTML (options: { |
78 | markdown: string | 78 | markdown: string |
79 | withHtml?: boolean | 79 | withHtml?: boolean // default false |
80 | withEmoji?: boolean | 80 | withEmoji?: boolean // default false |
81 | }) { | 81 | }) { |
82 | const { markdown, withHtml = false, withEmoji = false } = options | 82 | const { markdown, withHtml = false, withEmoji = false } = options |
83 | 83 | ||
@@ -99,6 +99,8 @@ export class MarkdownService { | |||
99 | return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags }) | 99 | return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags }) |
100 | } | 100 | } |
101 | 101 | ||
102 | // --------------------------------------------------------------------------- | ||
103 | |||
102 | processVideoTimestamps (videoShortUUID: string, html: string) { | 104 | processVideoTimestamps (videoShortUUID: string, html: string) { |
103 | return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) { | 105 | return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) { |
104 | const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0)) | 106 | const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0)) |
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts index de3f2bfff..daed7f178 100644 --- a/client/src/app/core/rest/rest-extractor.service.ts +++ b/client/src/app/core/rest/rest-extractor.service.ts | |||
@@ -87,7 +87,11 @@ export class RestExtractor { | |||
87 | 87 | ||
88 | if (err.status !== undefined) { | 88 | if (err.status !== undefined) { |
89 | const errorMessage = this.buildServerErrorMessage(err) | 89 | const errorMessage = this.buildServerErrorMessage(err) |
90 | logger.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) | 90 | |
91 | const message = `Backend returned code ${err.status}, errorMessage is: ${errorMessage}` | ||
92 | |||
93 | if (err.status === HttpStatusCode.NOT_FOUND_404) logger.clientError(message) | ||
94 | else logger.error(message) | ||
91 | 95 | ||
92 | return errorMessage | 96 | return errorMessage |
93 | } | 97 | } |
diff --git a/client/src/app/core/rest/rest-table.ts b/client/src/app/core/rest/rest-table.ts index ec5646b5d..707110d7f 100644 --- a/client/src/app/core/rest/rest-table.ts +++ b/client/src/app/core/rest/rest-table.ts | |||
@@ -7,7 +7,7 @@ import { RestPagination } from './rest-pagination' | |||
7 | 7 | ||
8 | const debugLogger = debug('peertube:tables:RestTable') | 8 | const debugLogger = debug('peertube:tables:RestTable') |
9 | 9 | ||
10 | export abstract class RestTable { | 10 | export abstract class RestTable <T = unknown> { |
11 | 11 | ||
12 | abstract totalRecords: number | 12 | abstract totalRecords: number |
13 | abstract sort: SortMeta | 13 | abstract sort: SortMeta |
@@ -17,6 +17,8 @@ export abstract class RestTable { | |||
17 | rowsPerPage = this.rowsPerPageOptions[0] | 17 | rowsPerPage = this.rowsPerPageOptions[0] |
18 | expandedRows = {} | 18 | expandedRows = {} |
19 | 19 | ||
20 | selectedRows: T[] = [] | ||
21 | |||
20 | search: string | 22 | search: string |
21 | 23 | ||
22 | protected route: ActivatedRoute | 24 | protected route: ActivatedRoute |
@@ -75,7 +77,17 @@ export abstract class RestTable { | |||
75 | this.reloadData() | 77 | this.reloadData() |
76 | } | 78 | } |
77 | 79 | ||
78 | protected abstract reloadData (): void | 80 | isInSelectionMode () { |
81 | return this.selectedRows.length !== 0 | ||
82 | } | ||
83 | |||
84 | protected abstract reloadDataInternal (): void | ||
85 | |||
86 | protected reloadData () { | ||
87 | this.selectedRows = [] | ||
88 | |||
89 | this.reloadDataInternal() | ||
90 | } | ||
79 | 91 | ||
80 | private getSortLocalStorageKey () { | 92 | private getSortLocalStorageKey () { |
81 | return 'rest-table-sort-' + this.getIdentifier() | 93 | return 'rest-table-sort-' + this.getIdentifier() |
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index c5d08ab75..15b1a3c4a 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html | |||
@@ -103,7 +103,9 @@ | |||
103 | <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a> | 103 | <a i18n *ngIf="!getExternalLoginHref()" routerLink="/login" class="peertube-button-link orange-button">Login</a> |
104 | <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a> | 104 | <a i18n *ngIf="getExternalLoginHref()" [href]="getExternalLoginHref()" class="peertube-button-link orange-button">Login</a> |
105 | 105 | ||
106 | <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button">Create an account</a> | 106 | <a *ngIf="isRegistrationAllowed()" routerLink="/signup" class="peertube-button-link create-account-button"> |
107 | <my-signup-label [requiresApproval]="requiresApproval"></my-signup-label> | ||
108 | </a> | ||
107 | </div> | 109 | </div> |
108 | 110 | ||
109 | <ng-container *ngFor="let menuSection of menuSections" > | 111 | <ng-container *ngFor="let menuSection of menuSections" > |
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 63f01df92..fc6d74cff 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { HotkeysService } from 'angular2-hotkeys' | 1 | import { HotkeysService } from 'angular2-hotkeys' |
2 | import * as debug from 'debug' | 2 | import * as debug from 'debug' |
3 | import { switchMap } from 'rxjs/operators' | 3 | import { switchMap } from 'rxjs/operators' |
4 | import { environment } from 'src/environments/environment' | ||
4 | import { ViewportScroller } from '@angular/common' | 5 | import { ViewportScroller } from '@angular/common' |
5 | import { Component, OnInit, ViewChild } from '@angular/core' | 6 | import { Component, OnInit, ViewChild } from '@angular/core' |
6 | import { Router } from '@angular/router' | 7 | import { Router } from '@angular/router' |
@@ -91,6 +92,10 @@ export class MenuComponent implements OnInit { | |||
91 | return this.languageChooserModal.getCurrentLanguage() | 92 | return this.languageChooserModal.getCurrentLanguage() |
92 | } | 93 | } |
93 | 94 | ||
95 | get requiresApproval () { | ||
96 | return this.serverConfig.signup.requiresApproval | ||
97 | } | ||
98 | |||
94 | ngOnInit () { | 99 | ngOnInit () { |
95 | this.htmlServerConfig = this.serverService.getHTMLConfig() | 100 | this.htmlServerConfig = this.serverService.getHTMLConfig() |
96 | this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() | 101 | this.currentInterfaceLanguage = this.languageChooserModal.getCurrentLanguage() |
@@ -131,12 +136,7 @@ export class MenuComponent implements OnInit { | |||
131 | } | 136 | } |
132 | 137 | ||
133 | getExternalLoginHref () { | 138 | getExternalLoginHref () { |
134 | if (!this.serverConfig || this.serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined | 139 | return PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverConfig) |
135 | |||
136 | const externalAuths = this.serverConfig.plugin.registeredExternalAuths | ||
137 | if (externalAuths.length !== 1) return undefined | ||
138 | |||
139 | return PluginsManager.getExternalAuthHref(externalAuths[0]) | ||
140 | } | 140 | } |
141 | 141 | ||
142 | isRegistrationAllowed () { | 142 | isRegistrationAllowed () { |
diff --git a/client/src/app/shared/form-validators/form-validator.model.ts b/client/src/app/shared/form-validators/form-validator.model.ts index 31c253b9b..1e4bba86b 100644 --- a/client/src/app/shared/form-validators/form-validator.model.ts +++ b/client/src/app/shared/form-validators/form-validator.model.ts | |||
@@ -12,5 +12,5 @@ export type BuildFormArgument = { | |||
12 | } | 12 | } |
13 | 13 | ||
14 | export type BuildFormDefaultValues = { | 14 | export type BuildFormDefaultValues = { |
15 | [ name: string ]: number | string | string[] | BuildFormDefaultValues | 15 | [ name: string ]: boolean | number | string | string[] | BuildFormDefaultValues |
16 | } | 16 | } |
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts index b93de75ea..ed6e0582e 100644 --- a/client/src/app/shared/form-validators/user-validators.ts +++ b/client/src/app/shared/form-validators/user-validators.ts | |||
@@ -136,13 +136,6 @@ export const USER_DESCRIPTION_VALIDATOR: BuildFormValidator = { | |||
136 | } | 136 | } |
137 | } | 137 | } |
138 | 138 | ||
139 | export const USER_TERMS_VALIDATOR: BuildFormValidator = { | ||
140 | VALIDATORS: [ Validators.requiredTrue ], | ||
141 | MESSAGES: { | ||
142 | required: $localize`You must agree with the instance terms in order to register on it.` | ||
143 | } | ||
144 | } | ||
145 | |||
146 | export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = { | 139 | export const USER_BAN_REASON_VALIDATOR: BuildFormValidator = { |
147 | VALIDATORS: [ | 140 | VALIDATORS: [ |
148 | Validators.minLength(3), | 141 | Validators.minLength(3), |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-details.component.html b/client/src/app/shared/shared-abuse-list/abuse-details.component.html index 089be501d..2d3e26a25 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-details.component.html +++ b/client/src/app/shared/shared-abuse-list/abuse-details.component.html | |||
@@ -8,7 +8,7 @@ | |||
8 | 8 | ||
9 | <span class="moderation-expanded-text"> | 9 | <span class="moderation-expanded-text"> |
10 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" | 10 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" |
11 | class="chip" | 11 | class="chip me-1" |
12 | > | 12 | > |
13 | <my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar> | 13 | <my-actor-avatar size="18" [actor]="abuse.reporterAccount" actorType="account"></my-actor-avatar> |
14 | <div> | 14 | <div> |
@@ -29,7 +29,7 @@ | |||
29 | <span class="moderation-expanded-label" i18n>Reportee</span> | 29 | <span class="moderation-expanded-label" i18n>Reportee</span> |
30 | <span class="moderation-expanded-text"> | 30 | <span class="moderation-expanded-text"> |
31 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" | 31 | <a [routerLink]="[ '.' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.flaggedAccount.displayName + '"' }" |
32 | class="chip" | 32 | class="chip me-1" |
33 | > | 33 | > |
34 | <my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar> | 34 | <my-actor-avatar size="18" [actor]="abuse.flaggedAccount" actorType="account"></my-actor-avatar> |
35 | <div> | 35 | <div> |
@@ -63,7 +63,7 @@ | |||
63 | <div *ngIf="predefinedReasons" class="mt-2 d-flex"> | 63 | <div *ngIf="predefinedReasons" class="mt-2 d-flex"> |
64 | <span> | 64 | <span> |
65 | <a *ngFor="let reason of predefinedReasons" [routerLink]="[ '.' ]" | 65 | <a *ngFor="let reason of predefinedReasons" [routerLink]="[ '.' ]" |
66 | [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" | 66 | [queryParams]="{ 'search': 'tag:' + reason.id }" class="pt-badge badge-secondary" |
67 | > | 67 | > |
68 | <div>{{ reason.label }}</div> | 68 | <div>{{ reason.label }}</div> |
69 | </a> | 69 | </a> |
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts index 569a37b17..d8470e927 100644 --- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts +++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts | |||
@@ -175,7 +175,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit { | |||
175 | return Actor.IS_LOCAL(abuse.reporterAccount.host) | 175 | return Actor.IS_LOCAL(abuse.reporterAccount.host) |
176 | } | 176 | } |
177 | 177 | ||
178 | protected reloadData () { | 178 | protected reloadDataInternal () { |
179 | debugLogger('Loading data.') | 179 | debugLogger('Loading data.') |
180 | 180 | ||
181 | const options = { | 181 | const options = { |
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts b/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts index 4e802b14d..b2ee2d8f2 100644 --- a/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts +++ b/client/src/app/shared/shared-custom-markup/custom-markup-container.component.ts | |||
@@ -6,9 +6,9 @@ import { CustomMarkupService } from './custom-markup.service' | |||
6 | templateUrl: './custom-markup-container.component.html' | 6 | templateUrl: './custom-markup-container.component.html' |
7 | }) | 7 | }) |
8 | export class CustomMarkupContainerComponent implements OnChanges { | 8 | export class CustomMarkupContainerComponent implements OnChanges { |
9 | @ViewChild('contentWrapper') contentWrapper: ElementRef<HTMLInputElement> | 9 | @ViewChild('contentWrapper', { static: true }) contentWrapper: ElementRef<HTMLInputElement> |
10 | 10 | ||
11 | @Input() content: string | 11 | @Input() content: string | HTMLDivElement |
12 | 12 | ||
13 | displayed = false | 13 | displayed = false |
14 | 14 | ||
@@ -17,17 +17,23 @@ export class CustomMarkupContainerComponent implements OnChanges { | |||
17 | ) { } | 17 | ) { } |
18 | 18 | ||
19 | async ngOnChanges () { | 19 | async ngOnChanges () { |
20 | await this.buildElement() | 20 | await this.rebuild() |
21 | } | 21 | } |
22 | 22 | ||
23 | private async buildElement () { | 23 | private async rebuild () { |
24 | if (!this.content) return | 24 | if (this.content instanceof HTMLDivElement) { |
25 | return this.loadElement(this.content) | ||
26 | } | ||
25 | 27 | ||
26 | const { rootElement, componentsLoaded } = await this.customMarkupService.buildElement(this.content) | 28 | const { rootElement, componentsLoaded } = await this.customMarkupService.buildElement(this.content) |
27 | this.contentWrapper.nativeElement.appendChild(rootElement) | ||
28 | |||
29 | await componentsLoaded | 29 | await componentsLoaded |
30 | 30 | ||
31 | return this.loadElement(rootElement) | ||
32 | } | ||
33 | |||
34 | private loadElement (el: HTMLDivElement) { | ||
35 | this.contentWrapper.nativeElement.appendChild(el) | ||
36 | |||
31 | this.displayed = true | 37 | this.displayed = true |
32 | } | 38 | } |
33 | } | 39 | } |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts index 1af060548..264dd9577 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/button-markup.component.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core' |
2 | import { VideoChannel } from '../../shared-main' | 2 | import { VideoChannel } from '../../shared-main' |
3 | import { CustomMarkupComponent } from './shared' | 3 | import { CustomMarkupComponent } from './shared' |
4 | 4 | ||
@@ -9,7 +9,8 @@ import { CustomMarkupComponent } from './shared' | |||
9 | @Component({ | 9 | @Component({ |
10 | selector: 'my-button-markup', | 10 | selector: 'my-button-markup', |
11 | templateUrl: 'button-markup.component.html', | 11 | templateUrl: 'button-markup.component.html', |
12 | styleUrls: [ 'button-markup.component.scss' ] | 12 | styleUrls: [ 'button-markup.component.scss' ], |
13 | changeDetection: ChangeDetectionStrategy.OnPush | ||
13 | }) | 14 | }) |
14 | export class ButtonMarkupComponent implements CustomMarkupComponent { | 15 | export class ButtonMarkupComponent implements CustomMarkupComponent { |
15 | @Input() theme: 'primary' | 'secondary' | 16 | @Input() theme: 'primary' | 'secondary' |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts index ba12b7139..1e7860750 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { from } from 'rxjs' | 1 | import { from } from 'rxjs' |
2 | import { finalize, map, switchMap, tap } from 'rxjs/operators' | 2 | import { finalize, map, switchMap, tap } from 'rxjs/operators' |
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 3 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
4 | import { MarkdownService, Notifier, UserService } from '@app/core' | 4 | import { MarkdownService, Notifier, UserService } from '@app/core' |
5 | import { FindInBulkService } from '@app/shared/shared-search' | 5 | import { FindInBulkService } from '@app/shared/shared-search' |
6 | import { VideoSortField } from '@shared/models' | 6 | import { VideoSortField } from '@shared/models' |
@@ -14,7 +14,8 @@ import { CustomMarkupComponent } from './shared' | |||
14 | @Component({ | 14 | @Component({ |
15 | selector: 'my-channel-miniature-markup', | 15 | selector: 'my-channel-miniature-markup', |
16 | templateUrl: 'channel-miniature-markup.component.html', | 16 | templateUrl: 'channel-miniature-markup.component.html', |
17 | styleUrls: [ 'channel-miniature-markup.component.scss' ] | 17 | styleUrls: [ 'channel-miniature-markup.component.scss' ], |
18 | changeDetection: ChangeDetectionStrategy.OnPush | ||
18 | }) | 19 | }) |
19 | export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { | 20 | export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
20 | @Input() name: string | 21 | @Input() name: string |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts index 07fa6fd2d..ab52e7e37 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/playlist-miniature-markup.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { finalize } from 'rxjs/operators' | 1 | import { finalize } from 'rxjs/operators' |
2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { FindInBulkService } from '@app/shared/shared-search' | 4 | import { FindInBulkService } from '@app/shared/shared-search' |
5 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' | 5 | import { MiniatureDisplayOptions } from '../../shared-video-miniature' |
@@ -13,7 +13,8 @@ import { CustomMarkupComponent } from './shared' | |||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-playlist-miniature-markup', | 14 | selector: 'my-playlist-miniature-markup', |
15 | templateUrl: 'playlist-miniature-markup.component.html', | 15 | templateUrl: 'playlist-miniature-markup.component.html', |
16 | styleUrls: [ 'playlist-miniature-markup.component.scss' ] | 16 | styleUrls: [ 'playlist-miniature-markup.component.scss' ], |
17 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | 18 | }) |
18 | export class PlaylistMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { | 19 | export class PlaylistMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
19 | @Input() uuid: string | 20 | @Input() uuid: string |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts index cbbacf77c..c37666359 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/video-miniature-markup.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { finalize } from 'rxjs/operators' | 1 | import { finalize } from 'rxjs/operators' |
2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { FindInBulkService } from '@app/shared/shared-search' | 4 | import { FindInBulkService } from '@app/shared/shared-search' |
5 | import { Video } from '../../shared-main' | 5 | import { Video } from '../../shared-main' |
@@ -13,7 +13,8 @@ import { CustomMarkupComponent } from './shared' | |||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-video-miniature-markup', | 14 | selector: 'my-video-miniature-markup', |
15 | templateUrl: 'video-miniature-markup.component.html', | 15 | templateUrl: 'video-miniature-markup.component.html', |
16 | styleUrls: [ 'video-miniature-markup.component.scss' ] | 16 | styleUrls: [ 'video-miniature-markup.component.scss' ], |
17 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | 18 | }) |
18 | export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { | 19 | export class VideoMiniatureMarkupComponent implements CustomMarkupComponent, OnInit { |
19 | @Input() uuid: string | 20 | @Input() uuid: string |
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts index 7d3498d4c..70e88ea51 100644 --- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts +++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/videos-list-markup.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { finalize } from 'rxjs/operators' | 1 | import { finalize } from 'rxjs/operators' |
2 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 2 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { VideoSortField } from '@shared/models' | 4 | import { VideoSortField } from '@shared/models' |
5 | import { Video, VideoService } from '../../shared-main' | 5 | import { Video, VideoService } from '../../shared-main' |
@@ -13,7 +13,8 @@ import { CustomMarkupComponent } from './shared' | |||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-videos-list-markup', | 14 | selector: 'my-videos-list-markup', |
15 | templateUrl: 'videos-list-markup.component.html', | 15 | templateUrl: 'videos-list-markup.component.html', |
16 | styleUrls: [ 'videos-list-markup.component.scss' ] | 16 | styleUrls: [ 'videos-list-markup.component.scss' ], |
17 | changeDetection: ChangeDetectionStrategy.OnPush | ||
17 | }) | 18 | }) |
18 | export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit { | 19 | export class VideosListMarkupComponent implements CustomMarkupComponent, OnInit { |
19 | @Input() sort: string | 20 | @Input() sort: string |
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts index e3371f22c..c6527e169 100644 --- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts +++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts | |||
@@ -31,6 +31,8 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
31 | @Input() markdownType: 'text' | 'enhanced' = 'text' | 31 | @Input() markdownType: 'text' | 'enhanced' = 'text' |
32 | @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> | 32 | @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement> |
33 | 33 | ||
34 | @Input() debounceTime = 150 | ||
35 | |||
34 | @Input() markdownVideo: Video | 36 | @Input() markdownVideo: Video |
35 | 37 | ||
36 | @Input() name = 'description' | 38 | @Input() name = 'description' |
@@ -59,7 +61,7 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { | |||
59 | ngOnInit () { | 61 | ngOnInit () { |
60 | this.contentChanged | 62 | this.contentChanged |
61 | .pipe( | 63 | .pipe( |
62 | debounceTime(150), | 64 | debounceTime(this.debounceTime), |
63 | distinctUntilChanged() | 65 | distinctUntilChanged() |
64 | ) | 66 | ) |
65 | .subscribe(() => this.updatePreviews()) | 67 | .subscribe(() => this.updatePreviews()) |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.html b/client/src/app/shared/shared-instance/instance-features-table.component.html index 6c05764df..205f2bc97 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.html +++ b/client/src/app/shared/shared-instance/instance-features-table.component.html | |||
@@ -18,10 +18,9 @@ | |||
18 | </tr> | 18 | </tr> |
19 | 19 | ||
20 | <tr> | 20 | <tr> |
21 | <th i18n class="label" scope="row">User registration allowed</th> | 21 | <th i18n class="label" scope="row">User registration</th> |
22 | <td> | 22 | |
23 | <my-feature-boolean [value]="serverConfig.signup.allowed"></my-feature-boolean> | 23 | <td class="value">{{ buildRegistrationLabel() }}</td> |
24 | </td> | ||
25 | </tr> | 24 | </tr> |
26 | 25 | ||
27 | <tr> | 26 | <tr> |
diff --git a/client/src/app/shared/shared-instance/instance-features-table.component.ts b/client/src/app/shared/shared-instance/instance-features-table.component.ts index e405c5790..c3df7c594 100644 --- a/client/src/app/shared/shared-instance/instance-features-table.component.ts +++ b/client/src/app/shared/shared-instance/instance-features-table.component.ts | |||
@@ -56,6 +56,15 @@ export class InstanceFeaturesTableComponent implements OnInit { | |||
56 | if (policy === 'display') return $localize`Displayed` | 56 | if (policy === 'display') return $localize`Displayed` |
57 | } | 57 | } |
58 | 58 | ||
59 | buildRegistrationLabel () { | ||
60 | const config = this.serverConfig.signup | ||
61 | |||
62 | if (config.allowed !== true) return $localize`Disabled` | ||
63 | if (config.requiresApproval === true) return $localize`Requires approval by moderators` | ||
64 | |||
65 | return $localize`Enabled` | ||
66 | } | ||
67 | |||
59 | getServerVersionAndCommit () { | 68 | getServerVersionAndCommit () { |
60 | return this.serverService.getServerVersionAndCommit() | 69 | return this.serverService.getServerVersionAndCommit() |
61 | } | 70 | } |
diff --git a/client/src/app/shared/shared-instance/instance.service.ts b/client/src/app/shared/shared-instance/instance.service.ts index 89f47db24..f5b2e05db 100644 --- a/client/src/app/shared/shared-instance/instance.service.ts +++ b/client/src/app/shared/shared-instance/instance.service.ts | |||
@@ -7,6 +7,11 @@ import { peertubeTranslate } from '@shared/core-utils/i18n' | |||
7 | import { About } from '@shared/models' | 7 | import { About } from '@shared/models' |
8 | import { environment } from '../../../environments/environment' | 8 | import { environment } from '../../../environments/environment' |
9 | 9 | ||
10 | export type AboutHTML = Pick<About['instance'], | ||
11 | 'terms' | 'codeOfConduct' | 'moderationInformation' | 'administrator' | 'creationReason' | | ||
12 | 'maintenanceLifetime' | 'businessModel' | 'hardwareInformation' | ||
13 | > | ||
14 | |||
10 | @Injectable() | 15 | @Injectable() |
11 | export class InstanceService { | 16 | export class InstanceService { |
12 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config' | 17 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config' |
@@ -39,7 +44,7 @@ export class InstanceService { | |||
39 | } | 44 | } |
40 | 45 | ||
41 | async buildHtml (about: About) { | 46 | async buildHtml (about: About) { |
42 | const html = { | 47 | const html: AboutHTML = { |
43 | terms: '', | 48 | terms: '', |
44 | codeOfConduct: '', | 49 | codeOfConduct: '', |
45 | moderationInformation: '', | 50 | moderationInformation: '', |
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts index b80ddb9f5..dd41a5f05 100644 --- a/client/src/app/shared/shared-main/account/index.ts +++ b/client/src/app/shared/shared-main/account/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './account.model' | 1 | export * from './account.model' |
2 | export * from './account.service' | 2 | export * from './account.service' |
3 | export * from './actor.model' | 3 | export * from './actor.model' |
4 | export * from './signup-label.component' | ||
diff --git a/client/src/app/shared/shared-main/account/signup-label.component.html b/client/src/app/shared/shared-main/account/signup-label.component.html new file mode 100644 index 000000000..35d6c5360 --- /dev/null +++ b/client/src/app/shared/shared-main/account/signup-label.component.html | |||
@@ -0,0 +1,2 @@ | |||
1 | <ng-container i18n *ngIf="requiresApproval">Request an account</ng-container> | ||
2 | <ng-container i18n *ngIf="!requiresApproval">Create an account</ng-container> | ||
diff --git a/client/src/app/shared/shared-main/account/signup-label.component.ts b/client/src/app/shared/shared-main/account/signup-label.component.ts new file mode 100644 index 000000000..caacb9c6f --- /dev/null +++ b/client/src/app/shared/shared-main/account/signup-label.component.ts | |||
@@ -0,0 +1,9 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-signup-label', | ||
5 | templateUrl: './signup-label.component.html' | ||
6 | }) | ||
7 | export class SignupLabelComponent { | ||
8 | @Input() requiresApproval: boolean | ||
9 | } | ||
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts index c1523bc50..eb1642d97 100644 --- a/client/src/app/shared/shared-main/shared-main.module.ts +++ b/client/src/app/shared/shared-main/shared-main.module.ts | |||
@@ -16,7 +16,7 @@ import { | |||
16 | import { LoadingBarModule } from '@ngx-loading-bar/core' | 16 | import { LoadingBarModule } from '@ngx-loading-bar/core' |
17 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' | 17 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' |
18 | import { SharedGlobalIconModule } from '../shared-icons' | 18 | import { SharedGlobalIconModule } from '../shared-icons' |
19 | import { AccountService } from './account' | 19 | import { AccountService, SignupLabelComponent } from './account' |
20 | import { | 20 | import { |
21 | AutofocusDirective, | 21 | AutofocusDirective, |
22 | BytesPipe, | 22 | BytesPipe, |
@@ -113,6 +113,8 @@ import { VideoChannelService } from './video-channel' | |||
113 | UserQuotaComponent, | 113 | UserQuotaComponent, |
114 | UserNotificationsComponent, | 114 | UserNotificationsComponent, |
115 | 115 | ||
116 | SignupLabelComponent, | ||
117 | |||
116 | EmbedComponent, | 118 | EmbedComponent, |
117 | 119 | ||
118 | PluginPlaceholderComponent, | 120 | PluginPlaceholderComponent, |
@@ -171,6 +173,8 @@ import { VideoChannelService } from './video-channel' | |||
171 | UserQuotaComponent, | 173 | UserQuotaComponent, |
172 | UserNotificationsComponent, | 174 | UserNotificationsComponent, |
173 | 175 | ||
176 | SignupLabelComponent, | ||
177 | |||
174 | EmbedComponent, | 178 | EmbedComponent, |
175 | 179 | ||
176 | PluginPlaceholderComponent, | 180 | PluginPlaceholderComponent, |
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts index bf8870a79..96e7b4dd0 100644 --- a/client/src/app/shared/shared-main/users/user-notification.model.ts +++ b/client/src/app/shared/shared-main/users/user-notification.model.ts | |||
@@ -83,6 +83,11 @@ export class UserNotification implements UserNotificationServer { | |||
83 | latestVersion: string | 83 | latestVersion: string |
84 | } | 84 | } |
85 | 85 | ||
86 | registration?: { | ||
87 | id: number | ||
88 | username: string | ||
89 | } | ||
90 | |||
86 | createdAt: string | 91 | createdAt: string |
87 | updatedAt: string | 92 | updatedAt: string |
88 | 93 | ||
@@ -97,6 +102,8 @@ export class UserNotification implements UserNotificationServer { | |||
97 | 102 | ||
98 | accountUrl?: string | 103 | accountUrl?: string |
99 | 104 | ||
105 | registrationsUrl?: string | ||
106 | |||
100 | videoImportIdentifier?: string | 107 | videoImportIdentifier?: string |
101 | videoImportUrl?: string | 108 | videoImportUrl?: string |
102 | 109 | ||
@@ -135,6 +142,7 @@ export class UserNotification implements UserNotificationServer { | |||
135 | 142 | ||
136 | this.plugin = hash.plugin | 143 | this.plugin = hash.plugin |
137 | this.peertube = hash.peertube | 144 | this.peertube = hash.peertube |
145 | this.registration = hash.registration | ||
138 | 146 | ||
139 | this.createdAt = hash.createdAt | 147 | this.createdAt = hash.createdAt |
140 | this.updatedAt = hash.updatedAt | 148 | this.updatedAt = hash.updatedAt |
@@ -208,6 +216,10 @@ export class UserNotification implements UserNotificationServer { | |||
208 | this.accountUrl = this.buildAccountUrl(this.account) | 216 | this.accountUrl = this.buildAccountUrl(this.account) |
209 | break | 217 | break |
210 | 218 | ||
219 | case UserNotificationType.NEW_USER_REGISTRATION_REQUEST: | ||
220 | this.registrationsUrl = '/admin/moderation/registrations/list' | ||
221 | break | ||
222 | |||
211 | case UserNotificationType.NEW_FOLLOW: | 223 | case UserNotificationType.NEW_FOLLOW: |
212 | this.accountUrl = this.buildAccountUrl(this.actorFollow.follower) | 224 | this.accountUrl = this.buildAccountUrl(this.actorFollow.follower) |
213 | break | 225 | break |
diff --git a/client/src/app/shared/shared-main/users/user-notifications.component.html b/client/src/app/shared/shared-main/users/user-notifications.component.html index e7cdb0183..a51e08292 100644 --- a/client/src/app/shared/shared-main/users/user-notifications.component.html +++ b/client/src/app/shared/shared-main/users/user-notifications.component.html | |||
@@ -215,6 +215,14 @@ | |||
215 | </div> | 215 | </div> |
216 | </ng-container> | 216 | </ng-container> |
217 | 217 | ||
218 | <ng-container *ngSwitchCase="20"> <!-- UserNotificationType.NEW_USER_REGISTRATION_REQUEST --> | ||
219 | <my-global-icon iconName="user-add" aria-hidden="true"></my-global-icon> | ||
220 | |||
221 | <div class="message" i18n> | ||
222 | User <a (click)="markAsRead(notification)" [routerLink]="notification.registrationsUrl">{{ notification.registration.username }}</a> wants to register on your instance | ||
223 | </div> | ||
224 | </ng-container> | ||
225 | |||
218 | <ng-container *ngSwitchDefault> | 226 | <ng-container *ngSwitchDefault> |
219 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> | 227 | <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon> |
220 | 228 | ||
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.scss b/client/src/app/shared/shared-moderation/account-blocklist.component.scss index 8b1239d34..00aaf3b9c 100644 --- a/client/src/app/shared/shared-moderation/account-blocklist.component.scss +++ b/client/src/app/shared/shared-moderation/account-blocklist.component.scss | |||
@@ -1,10 +1,6 @@ | |||
1 | @use '_variables' as *; | 1 | @use '_variables' as *; |
2 | @use '_mixins' as *; | 2 | @use '_mixins' as *; |
3 | 3 | ||
4 | .chip { | ||
5 | @include chip; | ||
6 | } | ||
7 | |||
8 | .unblock-button { | 4 | .unblock-button { |
9 | @include peertube-button; | 5 | @include peertube-button; |
10 | @include grey-button; | 6 | @include grey-button; |
diff --git a/client/src/app/shared/shared-moderation/account-blocklist.component.ts b/client/src/app/shared/shared-moderation/account-blocklist.component.ts index 9ed00bc12..38dbbff78 100644 --- a/client/src/app/shared/shared-moderation/account-blocklist.component.ts +++ b/client/src/app/shared/shared-moderation/account-blocklist.component.ts | |||
@@ -48,7 +48,7 @@ export class GenericAccountBlocklistComponent extends RestTable implements OnIni | |||
48 | ) | 48 | ) |
49 | } | 49 | } |
50 | 50 | ||
51 | protected reloadData () { | 51 | protected reloadDataInternal () { |
52 | const operation = this.mode === BlocklistComponentType.Account | 52 | const operation = this.mode === BlocklistComponentType.Account |
53 | ? this.blocklistService.getUserAccountBlocklist({ | 53 | ? this.blocklistService.getUserAccountBlocklist({ |
54 | pagination: this.pagination, | 54 | pagination: this.pagination, |
diff --git a/client/src/app/shared/shared-moderation/moderation.scss b/client/src/app/shared/shared-moderation/moderation.scss index eaf5a8250..7c1e308cf 100644 --- a/client/src/app/shared/shared-moderation/moderation.scss +++ b/client/src/app/shared/shared-moderation/moderation.scss | |||
@@ -40,10 +40,6 @@ | |||
40 | } | 40 | } |
41 | } | 41 | } |
42 | 42 | ||
43 | .chip { | ||
44 | @include chip; | ||
45 | } | ||
46 | |||
47 | my-action-dropdown.show { | 43 | my-action-dropdown.show { |
48 | ::ng-deep .dropdown-root { | 44 | ::ng-deep .dropdown-root { |
49 | display: block !important; | 45 | display: block !important; |
diff --git a/client/src/app/shared/shared-moderation/server-blocklist.component.scss b/client/src/app/shared/shared-moderation/server-blocklist.component.scss index e29668a23..1a6b0435f 100644 --- a/client/src/app/shared/shared-moderation/server-blocklist.component.scss +++ b/client/src/app/shared/shared-moderation/server-blocklist.component.scss | |||
@@ -24,7 +24,3 @@ a { | |||
24 | .block-button { | 24 | .block-button { |
25 | @include create-button; | 25 | @include create-button; |
26 | } | 26 | } |
27 | |||
28 | .chip { | ||
29 | @include chip; | ||
30 | } | ||
diff --git a/client/src/app/shared/shared-moderation/server-blocklist.component.ts b/client/src/app/shared/shared-moderation/server-blocklist.component.ts index 1ba7a1b4d..f1bcbd561 100644 --- a/client/src/app/shared/shared-moderation/server-blocklist.component.ts +++ b/client/src/app/shared/shared-moderation/server-blocklist.component.ts | |||
@@ -75,7 +75,7 @@ export class GenericServerBlocklistComponent extends RestTable implements OnInit | |||
75 | }) | 75 | }) |
76 | } | 76 | } |
77 | 77 | ||
78 | protected reloadData () { | 78 | protected reloadDataInternal () { |
79 | const operation = this.mode === BlocklistComponentType.Account | 79 | const operation = this.mode === BlocklistComponentType.Account |
80 | ? this.blocklistService.getUserServerBlocklist({ | 80 | ? this.blocklistService.getUserServerBlocklist({ |
81 | pagination: this.pagination, | 81 | pagination: this.pagination, |
diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts index c69a45c25..50dccf862 100644 --- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts | |||
@@ -105,7 +105,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { | |||
105 | const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) | 105 | const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) |
106 | if (res === false) return | 106 | if (res === false) return |
107 | 107 | ||
108 | this.userAdminService.removeUser(user) | 108 | this.userAdminService.removeUsers(user) |
109 | .subscribe({ | 109 | .subscribe({ |
110 | next: () => { | 110 | next: () => { |
111 | this.notifier.success($localize`User ${user.username} deleted.`) | 111 | this.notifier.success($localize`User ${user.username} deleted.`) |
diff --git a/client/src/app/shared/shared-users/index.ts b/client/src/app/shared/shared-users/index.ts index 20e60486d..95d90e49e 100644 --- a/client/src/app/shared/shared-users/index.ts +++ b/client/src/app/shared/shared-users/index.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | export * from './user-admin.service' | 1 | export * from './user-admin.service' |
2 | export * from './user-signup.service' | ||
3 | export * from './two-factor.service' | 2 | export * from './two-factor.service' |
4 | 3 | ||
5 | export * from './shared-users.module' | 4 | export * from './shared-users.module' |
diff --git a/client/src/app/shared/shared-users/shared-users.module.ts b/client/src/app/shared/shared-users/shared-users.module.ts index 5a1675dc9..efffc6026 100644 --- a/client/src/app/shared/shared-users/shared-users.module.ts +++ b/client/src/app/shared/shared-users/shared-users.module.ts | |||
@@ -1,9 +1,7 @@ | |||
1 | |||
2 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
3 | import { SharedMainModule } from '../shared-main/shared-main.module' | 2 | import { SharedMainModule } from '../shared-main/shared-main.module' |
4 | import { TwoFactorService } from './two-factor.service' | 3 | import { TwoFactorService } from './two-factor.service' |
5 | import { UserAdminService } from './user-admin.service' | 4 | import { UserAdminService } from './user-admin.service' |
6 | import { UserSignupService } from './user-signup.service' | ||
7 | 5 | ||
8 | @NgModule({ | 6 | @NgModule({ |
9 | imports: [ | 7 | imports: [ |
@@ -15,7 +13,6 @@ import { UserSignupService } from './user-signup.service' | |||
15 | exports: [], | 13 | exports: [], |
16 | 14 | ||
17 | providers: [ | 15 | providers: [ |
18 | UserSignupService, | ||
19 | UserAdminService, | 16 | UserAdminService, |
20 | TwoFactorService | 17 | TwoFactorService |
21 | ] | 18 | ] |
diff --git a/client/src/app/shared/shared-users/user-admin.service.ts b/client/src/app/shared/shared-users/user-admin.service.ts index 0b04023a3..6224f0bd5 100644 --- a/client/src/app/shared/shared-users/user-admin.service.ts +++ b/client/src/app/shared/shared-users/user-admin.service.ts | |||
@@ -64,7 +64,7 @@ export class UserAdminService { | |||
64 | ) | 64 | ) |
65 | } | 65 | } |
66 | 66 | ||
67 | removeUser (usersArg: UserServerModel | UserServerModel[]) { | 67 | removeUsers (usersArg: UserServerModel | UserServerModel[]) { |
68 | const users = arrayify(usersArg) | 68 | const users = arrayify(usersArg) |
69 | 69 | ||
70 | return from(users) | 70 | return from(users) |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html index 6fdf24b2d..227c12130 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html | |||
@@ -53,8 +53,8 @@ | |||
53 | <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> | 53 | <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> |
54 | </div> | 54 | </div> |
55 | 55 | ||
56 | <div *ngIf="containedInPlaylists" class="video-contained-in-playlists"> | 56 | <div *ngIf="containedInPlaylists" class="fs-6"> |
57 | <a *ngFor="let playlist of containedInPlaylists" class="chip rectangular bg-secondary text-light" [routerLink]="['/w/p/', playlist.playlistShortUUID]"> | 57 | <a *ngFor="let playlist of containedInPlaylists" class="pt-badge badge-secondary" [routerLink]="['/w/p/', playlist.playlistShortUUID]"> |
58 | {{ playlist.playlistDisplayName }} | 58 | {{ playlist.playlistDisplayName }} |
59 | </a> | 59 | </a> |
60 | </div> | 60 | </div> |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss index ba2adfc5a..a397efdca 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss | |||
@@ -4,10 +4,6 @@ | |||
4 | 4 | ||
5 | $more-button-width: 40px; | 5 | $more-button-width: 40px; |
6 | 6 | ||
7 | .chip { | ||
8 | @include chip; | ||
9 | } | ||
10 | |||
11 | .video-miniature { | 7 | .video-miniature { |
12 | font-size: 14px; | 8 | font-size: 14px; |
13 | } | 9 | } |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index 85c63c173..706227e66 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts | |||
@@ -314,6 +314,6 @@ export class VideoMiniatureComponent implements OnInit { | |||
314 | this.cd.markForCheck() | 314 | this.cd.markForCheck() |
315 | }) | 315 | }) |
316 | 316 | ||
317 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 317 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
318 | } | 318 | } |
319 | } | 319 | } |
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts index d5cdd958e..7b832263e 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as debug from 'debug' | 1 | import * as debug from 'debug' |
2 | import { fromEvent, Observable, Subject, Subscription } from 'rxjs' | 2 | import { fromEvent, Observable, Subject, Subscription } from 'rxjs' |
3 | import { debounceTime, switchMap } from 'rxjs/operators' | 3 | import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' |
4 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' | 4 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' |
5 | import { ActivatedRoute } from '@angular/router' | 5 | import { ActivatedRoute } from '@angular/router' |
6 | import { | 6 | import { |
@@ -111,6 +111,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
111 | 111 | ||
112 | private lastQueryLength: number | 112 | private lastQueryLength: number |
113 | 113 | ||
114 | private videoRequests = new Subject<{ reset: boolean, obs: Observable<ResultList<Video>> }>() | ||
115 | |||
114 | constructor ( | 116 | constructor ( |
115 | private notifier: Notifier, | 117 | private notifier: Notifier, |
116 | private authService: AuthService, | 118 | private authService: AuthService, |
@@ -124,6 +126,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
124 | } | 126 | } |
125 | 127 | ||
126 | ngOnInit () { | 128 | ngOnInit () { |
129 | this.subscribeToVideoRequests() | ||
130 | |||
127 | const hiddenFilters = this.hideScopeFilter | 131 | const hiddenFilters = this.hideScopeFilter |
128 | ? [ 'scope' ] | 132 | ? [ 'scope' ] |
129 | : [] | 133 | : [] |
@@ -228,30 +232,12 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
228 | } | 232 | } |
229 | 233 | ||
230 | loadMoreVideos (reset = false) { | 234 | loadMoreVideos (reset = false) { |
231 | if (reset) this.hasDoneFirstQuery = false | 235 | if (reset) { |
232 | 236 | this.hasDoneFirstQuery = false | |
233 | this.getVideosObservableFunction(this.pagination, this.filters) | 237 | this.videos = [] |
234 | .subscribe({ | 238 | } |
235 | next: ({ data }) => { | ||
236 | this.hasDoneFirstQuery = true | ||
237 | this.lastQueryLength = data.length | ||
238 | |||
239 | if (reset) this.videos = [] | ||
240 | this.videos = this.videos.concat(data) | ||
241 | |||
242 | if (this.groupByDate) this.buildGroupedDateLabels() | ||
243 | |||
244 | this.onDataSubject.next(data) | ||
245 | this.videosLoaded.emit(this.videos) | ||
246 | }, | ||
247 | |||
248 | error: err => { | ||
249 | const message = $localize`Cannot load more videos. Try again later.` | ||
250 | 239 | ||
251 | logger.error(message, err) | 240 | this.videoRequests.next({ reset, obs: this.getVideosObservableFunction(this.pagination, this.filters) }) |
252 | this.notifier.error(message) | ||
253 | } | ||
254 | }) | ||
255 | } | 241 | } |
256 | 242 | ||
257 | reloadVideos () { | 243 | reloadVideos () { |
@@ -423,4 +409,30 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
423 | this.onFiltersChanged(true) | 409 | this.onFiltersChanged(true) |
424 | }) | 410 | }) |
425 | } | 411 | } |
412 | |||
413 | private subscribeToVideoRequests () { | ||
414 | this.videoRequests | ||
415 | .pipe(concatMap(({ reset, obs }) => obs.pipe(map(({ data }) => ({ data, reset }))))) | ||
416 | .subscribe({ | ||
417 | next: ({ data, reset }) => { | ||
418 | this.hasDoneFirstQuery = true | ||
419 | this.lastQueryLength = data.length | ||
420 | |||
421 | if (reset) this.videos = [] | ||
422 | this.videos = this.videos.concat(data) | ||
423 | |||
424 | if (this.groupByDate) this.buildGroupedDateLabels() | ||
425 | |||
426 | this.onDataSubject.next(data) | ||
427 | this.videosLoaded.emit(this.videos) | ||
428 | }, | ||
429 | |||
430 | error: err => { | ||
431 | const message = $localize`Cannot load more videos. Try again later.` | ||
432 | |||
433 | logger.error(message, err) | ||
434 | this.notifier.error(message) | ||
435 | } | ||
436 | }) | ||
437 | } | ||
426 | } | 438 | } |
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts index 2fc39fc75..f802416a4 100644 --- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts | |||
@@ -81,7 +81,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
81 | .subscribe(result => { | 81 | .subscribe(result => { |
82 | this.playlistsData = result.data | 82 | this.playlistsData = result.data |
83 | 83 | ||
84 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 84 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
85 | }) | 85 | }) |
86 | 86 | ||
87 | this.videoPlaylistSearchChanged | 87 | this.videoPlaylistSearchChanged |
@@ -129,7 +129,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
129 | .subscribe(playlistsResult => { | 129 | .subscribe(playlistsResult => { |
130 | this.playlistsData = playlistsResult.data | 130 | this.playlistsData = playlistsResult.data |
131 | 131 | ||
132 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 132 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
133 | }) | 133 | }) |
134 | } | 134 | } |
135 | 135 | ||
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts index 330a51f91..bc9fb0d74 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts | |||
@@ -206,7 +206,15 @@ export class VideoPlaylistService { | |||
206 | stopTimestamp: body.stopTimestamp | 206 | stopTimestamp: body.stopTimestamp |
207 | }) | 207 | }) |
208 | 208 | ||
209 | this.runPlaylistCheck(body.videoId) | 209 | this.runVideoExistsInPlaylistCheck(body.videoId) |
210 | |||
211 | if (this.myAccountPlaylistCache) { | ||
212 | const playlist = this.myAccountPlaylistCache.data.find(p => p.id === playlistId) | ||
213 | if (!playlist) return | ||
214 | |||
215 | const otherPlaylists = this.myAccountPlaylistCache.data.filter(p => p !== playlist) | ||
216 | this.myAccountPlaylistCache.data = [ playlist, ...otherPlaylists ] | ||
217 | } | ||
210 | }), | 218 | }), |
211 | catchError(err => this.restExtractor.handleError(err)) | 219 | catchError(err => this.restExtractor.handleError(err)) |
212 | ) | 220 | ) |
@@ -225,7 +233,7 @@ export class VideoPlaylistService { | |||
225 | elem.stopTimestamp = body.stopTimestamp | 233 | elem.stopTimestamp = body.stopTimestamp |
226 | } | 234 | } |
227 | 235 | ||
228 | this.runPlaylistCheck(videoId) | 236 | this.runVideoExistsInPlaylistCheck(videoId) |
229 | }), | 237 | }), |
230 | catchError(err => this.restExtractor.handleError(err)) | 238 | catchError(err => this.restExtractor.handleError(err)) |
231 | ) | 239 | ) |
@@ -242,7 +250,7 @@ export class VideoPlaylistService { | |||
242 | .filter(e => e.playlistElementId !== playlistElementId) | 250 | .filter(e => e.playlistElementId !== playlistElementId) |
243 | } | 251 | } |
244 | 252 | ||
245 | this.runPlaylistCheck(videoId) | 253 | this.runVideoExistsInPlaylistCheck(videoId) |
246 | }), | 254 | }), |
247 | catchError(err => this.restExtractor.handleError(err)) | 255 | catchError(err => this.restExtractor.handleError(err)) |
248 | ) | 256 | ) |
@@ -296,7 +304,7 @@ export class VideoPlaylistService { | |||
296 | return obs | 304 | return obs |
297 | } | 305 | } |
298 | 306 | ||
299 | runPlaylistCheck (videoId: number) { | 307 | runVideoExistsInPlaylistCheck (videoId: number) { |
300 | debugLogger('Running playlist check.') | 308 | debugLogger('Running playlist check.') |
301 | 309 | ||
302 | if (this.videoExistsCache[videoId]) { | 310 | if (this.videoExistsCache[videoId]) { |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 56310c4e9..2781850b9 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -11,6 +11,7 @@ import './shared/control-bar/p2p-info-button' | |||
11 | import './shared/control-bar/peertube-link-button' | 11 | import './shared/control-bar/peertube-link-button' |
12 | import './shared/control-bar/peertube-load-progress-bar' | 12 | import './shared/control-bar/peertube-load-progress-bar' |
13 | import './shared/control-bar/theater-button' | 13 | import './shared/control-bar/theater-button' |
14 | import './shared/control-bar/peertube-live-display' | ||
14 | import './shared/settings/resolution-menu-button' | 15 | import './shared/settings/resolution-menu-button' |
15 | import './shared/settings/resolution-menu-item' | 16 | import './shared/settings/resolution-menu-item' |
16 | import './shared/settings/settings-dialog' | 17 | import './shared/settings/settings-dialog' |
@@ -96,6 +97,10 @@ export class PeertubePlayerManager { | |||
96 | videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { | 97 | videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { |
97 | const player = this | 98 | const player = this |
98 | 99 | ||
100 | if (!isNaN(+options.common.playbackRate)) { | ||
101 | player.playbackRate(+options.common.playbackRate) | ||
102 | } | ||
103 | |||
99 | let alreadyFallback = false | 104 | let alreadyFallback = false |
100 | 105 | ||
101 | const handleError = () => { | 106 | const handleError = () => { |
@@ -118,7 +123,7 @@ export class PeertubePlayerManager { | |||
118 | self.addContextMenu(videojsOptionsBuilder, player, options.common) | 123 | self.addContextMenu(videojsOptionsBuilder, player, options.common) |
119 | 124 | ||
120 | if (isMobile()) player.peertubeMobile() | 125 | if (isMobile()) player.peertubeMobile() |
121 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin() | 126 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin({ isLive: options.common.isLive }) |
122 | if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') | 127 | if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') |
123 | 128 | ||
124 | player.bezels() | 129 | player.bezels() |
diff --git a/client/src/assets/player/shared/control-bar/index.ts b/client/src/assets/player/shared/control-bar/index.ts index db5b8938d..e71e90713 100644 --- a/client/src/assets/player/shared/control-bar/index.ts +++ b/client/src/assets/player/shared/control-bar/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export * from './next-previous-video-button' | 1 | export * from './next-previous-video-button' |
2 | export * from './p2p-info-button' | 2 | export * from './p2p-info-button' |
3 | export * from './peertube-link-button' | 3 | export * from './peertube-link-button' |
4 | export * from './peertube-live-display' | ||
4 | export * from './peertube-load-progress-bar' | 5 | export * from './peertube-load-progress-bar' |
5 | export * from './theater-button' | 6 | export * from './theater-button' |
diff --git a/client/src/assets/player/shared/control-bar/peertube-live-display.ts b/client/src/assets/player/shared/control-bar/peertube-live-display.ts new file mode 100644 index 000000000..649eb0b00 --- /dev/null +++ b/client/src/assets/player/shared/control-bar/peertube-live-display.ts | |||
@@ -0,0 +1,93 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { PeerTubeLinkButtonOptions } from '../../types' | ||
3 | |||
4 | const ClickableComponent = videojs.getComponent('ClickableComponent') | ||
5 | |||
6 | class PeerTubeLiveDisplay extends ClickableComponent { | ||
7 | private interval: any | ||
8 | |||
9 | private contentEl_: any | ||
10 | |||
11 | constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) { | ||
12 | super(player, options as any) | ||
13 | |||
14 | this.interval = this.setInterval(() => this.updateClass(), 1000) | ||
15 | |||
16 | this.show() | ||
17 | this.updateSync(true) | ||
18 | } | ||
19 | |||
20 | dispose () { | ||
21 | if (this.interval) { | ||
22 | this.clearInterval(this.interval) | ||
23 | this.interval = undefined | ||
24 | } | ||
25 | |||
26 | this.contentEl_ = null | ||
27 | |||
28 | super.dispose() | ||
29 | } | ||
30 | |||
31 | createEl () { | ||
32 | const el = super.createEl('div', { | ||
33 | className: 'vjs-live-control vjs-control' | ||
34 | }) | ||
35 | |||
36 | this.contentEl_ = videojs.dom.createEl('div', { | ||
37 | className: 'vjs-live-display' | ||
38 | }, { | ||
39 | 'aria-live': 'off' | ||
40 | }) | ||
41 | |||
42 | this.contentEl_.appendChild(videojs.dom.createEl('span', { | ||
43 | className: 'vjs-control-text', | ||
44 | textContent: `${this.localize('Stream Type')}\u00a0` | ||
45 | })) | ||
46 | |||
47 | this.contentEl_.appendChild(document.createTextNode(this.localize('LIVE'))) | ||
48 | |||
49 | el.appendChild(this.contentEl_) | ||
50 | return el | ||
51 | } | ||
52 | |||
53 | handleClick () { | ||
54 | const hlsjs = this.getHLSJS() | ||
55 | if (!hlsjs) return | ||
56 | |||
57 | this.player().currentTime(hlsjs.liveSyncPosition) | ||
58 | this.player().play() | ||
59 | this.updateSync(true) | ||
60 | } | ||
61 | |||
62 | private updateClass () { | ||
63 | const hlsjs = this.getHLSJS() | ||
64 | if (!hlsjs) return | ||
65 | |||
66 | // Not loaded yet | ||
67 | if (this.player().currentTime() === 0) return | ||
68 | |||
69 | const isSync = Math.abs(this.player().currentTime() - hlsjs.liveSyncPosition) < 10 | ||
70 | this.updateSync(isSync) | ||
71 | } | ||
72 | |||
73 | private updateSync (isSync: boolean) { | ||
74 | if (isSync) { | ||
75 | this.addClass('synced-with-live-edge') | ||
76 | this.removeAttribute('title') | ||
77 | this.disable() | ||
78 | } else { | ||
79 | this.removeClass('synced-with-live-edge') | ||
80 | this.setAttribute('title', this.localize('Go back to the live')) | ||
81 | this.enable() | ||
82 | } | ||
83 | } | ||
84 | |||
85 | private getHLSJS () { | ||
86 | const p2pMediaLoader = this.player()?.p2pMediaLoader | ||
87 | if (!p2pMediaLoader) return undefined | ||
88 | |||
89 | return p2pMediaLoader().getHLSJS() | ||
90 | } | ||
91 | } | ||
92 | |||
93 | videojs.registerComponent('PeerTubeLiveDisplay', PeerTubeLiveDisplay) | ||
diff --git a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts index ec1e1038b..f5b4b3919 100644 --- a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts +++ b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts | |||
@@ -4,6 +4,10 @@ type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardE | |||
4 | 4 | ||
5 | const Plugin = videojs.getPlugin('plugin') | 5 | const Plugin = videojs.getPlugin('plugin') |
6 | 6 | ||
7 | export type HotkeysOptions = { | ||
8 | isLive: boolean | ||
9 | } | ||
10 | |||
7 | class PeerTubeHotkeysPlugin extends Plugin { | 11 | class PeerTubeHotkeysPlugin extends Plugin { |
8 | private static readonly VOLUME_STEP = 0.1 | 12 | private static readonly VOLUME_STEP = 0.1 |
9 | private static readonly SEEK_STEP = 5 | 13 | private static readonly SEEK_STEP = 5 |
@@ -12,9 +16,13 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
12 | 16 | ||
13 | private readonly handlers: KeyHandler[] | 17 | private readonly handlers: KeyHandler[] |
14 | 18 | ||
15 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { | 19 | private readonly isLive: boolean |
20 | |||
21 | constructor (player: videojs.Player, options: videojs.PlayerOptions & HotkeysOptions) { | ||
16 | super(player, options) | 22 | super(player, options) |
17 | 23 | ||
24 | this.isLive = options.isLive | ||
25 | |||
18 | this.handlers = this.buildHandlers() | 26 | this.handlers = this.buildHandlers() |
19 | 27 | ||
20 | this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) | 28 | this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) |
@@ -68,28 +76,6 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
68 | } | 76 | } |
69 | }, | 77 | }, |
70 | 78 | ||
71 | // Rewind | ||
72 | { | ||
73 | accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'), | ||
74 | cb: e => { | ||
75 | e.preventDefault() | ||
76 | |||
77 | const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP) | ||
78 | this.player.currentTime(target) | ||
79 | } | ||
80 | }, | ||
81 | |||
82 | // Forward | ||
83 | { | ||
84 | accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'), | ||
85 | cb: e => { | ||
86 | e.preventDefault() | ||
87 | |||
88 | const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP) | ||
89 | this.player.currentTime(target) | ||
90 | } | ||
91 | }, | ||
92 | |||
93 | // Fullscreen | 79 | // Fullscreen |
94 | { | 80 | { |
95 | // f key or Ctrl + Enter | 81 | // f key or Ctrl + Enter |
@@ -116,6 +102,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
116 | { | 102 | { |
117 | accept: e => e.key === '>', | 103 | accept: e => e.key === '>', |
118 | cb: () => { | 104 | cb: () => { |
105 | if (this.isLive) return | ||
106 | |||
119 | const target = Math.min(this.player.playbackRate() + 0.1, 5) | 107 | const target = Math.min(this.player.playbackRate() + 0.1, 5) |
120 | 108 | ||
121 | this.player.playbackRate(parseFloat(target.toFixed(2))) | 109 | this.player.playbackRate(parseFloat(target.toFixed(2))) |
@@ -126,6 +114,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
126 | { | 114 | { |
127 | accept: e => e.key === '<', | 115 | accept: e => e.key === '<', |
128 | cb: () => { | 116 | cb: () => { |
117 | if (this.isLive) return | ||
118 | |||
129 | const target = Math.max(this.player.playbackRate() - 0.1, 0.10) | 119 | const target = Math.max(this.player.playbackRate() - 0.1, 0.10) |
130 | 120 | ||
131 | this.player.playbackRate(parseFloat(target.toFixed(2))) | 121 | this.player.playbackRate(parseFloat(target.toFixed(2))) |
@@ -136,6 +126,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
136 | { | 126 | { |
137 | accept: e => e.key === ',', | 127 | accept: e => e.key === ',', |
138 | cb: () => { | 128 | cb: () => { |
129 | if (this.isLive) return | ||
130 | |||
139 | this.player.pause() | 131 | this.player.pause() |
140 | 132 | ||
141 | // Calculate movement distance (assuming 30 fps) | 133 | // Calculate movement distance (assuming 30 fps) |
@@ -148,6 +140,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
148 | { | 140 | { |
149 | accept: e => e.key === '.', | 141 | accept: e => e.key === '.', |
150 | cb: () => { | 142 | cb: () => { |
143 | if (this.isLive) return | ||
144 | |||
151 | this.player.pause() | 145 | this.player.pause() |
152 | 146 | ||
153 | // Calculate movement distance (assuming 30 fps) | 147 | // Calculate movement distance (assuming 30 fps) |
@@ -157,11 +151,47 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
157 | } | 151 | } |
158 | ] | 152 | ] |
159 | 153 | ||
154 | if (this.isLive) return handlers | ||
155 | |||
156 | return handlers.concat(this.buildVODHandlers()) | ||
157 | } | ||
158 | |||
159 | private buildVODHandlers () { | ||
160 | const handlers: KeyHandler[] = [ | ||
161 | // Rewind | ||
162 | { | ||
163 | accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'), | ||
164 | cb: e => { | ||
165 | if (this.isLive) return | ||
166 | |||
167 | e.preventDefault() | ||
168 | |||
169 | const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP) | ||
170 | this.player.currentTime(target) | ||
171 | } | ||
172 | }, | ||
173 | |||
174 | // Forward | ||
175 | { | ||
176 | accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'), | ||
177 | cb: e => { | ||
178 | if (this.isLive) return | ||
179 | |||
180 | e.preventDefault() | ||
181 | |||
182 | const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP) | ||
183 | this.player.currentTime(target) | ||
184 | } | ||
185 | } | ||
186 | ] | ||
187 | |||
160 | // 0-9 key handlers | 188 | // 0-9 key handlers |
161 | for (let i = 0; i < 10; i++) { | 189 | for (let i = 0; i < 10; i++) { |
162 | handlers.push({ | 190 | handlers.push({ |
163 | accept: e => this.isNakedOrShift(e, i + ''), | 191 | accept: e => this.isNakedOrShift(e, i + ''), |
164 | cb: e => { | 192 | cb: e => { |
193 | if (this.isLive) return | ||
194 | |||
165 | e.preventDefault() | 195 | e.preventDefault() |
166 | 196 | ||
167 | this.player.currentTime(this.player.duration() * i * 0.1) | 197 | this.player.currentTime(this.player.duration() * i * 0.1) |
diff --git a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts index 27f366732..26f923e92 100644 --- a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts +++ b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts | |||
@@ -30,10 +30,7 @@ export class ControlBarOptionsBuilder { | |||
30 | } | 30 | } |
31 | 31 | ||
32 | Object.assign(children, { | 32 | Object.assign(children, { |
33 | currentTimeDisplay: {}, | 33 | ...this.getTimeControls(), |
34 | timeDivider: {}, | ||
35 | durationDisplay: {}, | ||
36 | liveDisplay: {}, | ||
37 | 34 | ||
38 | flexibleWidthSpacer: {}, | 35 | flexibleWidthSpacer: {}, |
39 | 36 | ||
@@ -74,7 +71,9 @@ export class ControlBarOptionsBuilder { | |||
74 | private getSettingsButton () { | 71 | private getSettingsButton () { |
75 | const settingEntries: string[] = [] | 72 | const settingEntries: string[] = [] |
76 | 73 | ||
77 | settingEntries.push('playbackRateMenuButton') | 74 | if (!this.options.isLive) { |
75 | settingEntries.push('playbackRateMenuButton') | ||
76 | } | ||
78 | 77 | ||
79 | if (this.options.captions === true) settingEntries.push('captionsButton') | 78 | if (this.options.captions === true) settingEntries.push('captionsButton') |
80 | 79 | ||
@@ -90,7 +89,23 @@ export class ControlBarOptionsBuilder { | |||
90 | } | 89 | } |
91 | } | 90 | } |
92 | 91 | ||
92 | private getTimeControls () { | ||
93 | if (this.options.isLive) { | ||
94 | return { | ||
95 | peerTubeLiveDisplay: {} | ||
96 | } | ||
97 | } | ||
98 | |||
99 | return { | ||
100 | currentTimeDisplay: {}, | ||
101 | timeDivider: {}, | ||
102 | durationDisplay: {} | ||
103 | } | ||
104 | } | ||
105 | |||
93 | private getProgressControl () { | 106 | private getProgressControl () { |
107 | if (this.options.isLive) return {} | ||
108 | |||
94 | const loadProgressBar = this.mode === 'webtorrent' | 109 | const loadProgressBar = this.mode === 'webtorrent' |
95 | ? 'peerTubeLoadProgressBar' | 110 | ? 'peerTubeLoadProgressBar' |
96 | : 'loadProgressBar' | 111 | : 'loadProgressBar' |
diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts index a14beb347..7f7d90ab9 100644 --- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts | |||
@@ -281,8 +281,8 @@ class Html5Hlsjs { | |||
281 | if (this.errorCounts[data.type]) this.errorCounts[data.type] += 1 | 281 | if (this.errorCounts[data.type]) this.errorCounts[data.type] += 1 |
282 | else this.errorCounts[data.type] = 1 | 282 | else this.errorCounts[data.type] = 1 |
283 | 283 | ||
284 | if (data.fatal) logger.warn(error.message) | 284 | if (data.fatal) logger.error(error.message, { currentTime: this.player.currentTime(), data }) |
285 | else logger.error(error.message, { data }) | 285 | else logger.warn(error.message) |
286 | 286 | ||
287 | if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { | 287 | if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { |
288 | error.code = 2 | 288 | error.code = 2 |
diff --git a/client/src/assets/player/shared/stats/stats-card.ts b/client/src/assets/player/shared/stats/stats-card.ts index f23ae48be..471a5e46c 100644 --- a/client/src/assets/player/shared/stats/stats-card.ts +++ b/client/src/assets/player/shared/stats/stats-card.ts | |||
@@ -182,7 +182,7 @@ class StatsCard extends Component { | |||
182 | let colorSpace = 'unknown' | 182 | let colorSpace = 'unknown' |
183 | let codecs = 'unknown' | 183 | let codecs = 'unknown' |
184 | 184 | ||
185 | if (metadata?.streams[0]) { | 185 | if (metadata?.streams?.[0]) { |
186 | const stream = metadata.streams[0] | 186 | const stream = metadata.streams[0] |
187 | 187 | ||
188 | colorSpace = stream['color_space'] !== 'unknown' | 188 | colorSpace = stream['color_space'] !== 'unknown' |
@@ -193,7 +193,7 @@ class StatsCard extends Component { | |||
193 | } | 193 | } |
194 | 194 | ||
195 | const resolution = videoFile?.resolution.label + videoFile?.fps | 195 | const resolution = videoFile?.resolution.label + videoFile?.fps |
196 | const buffer = this.timeRangesToString(this.player().buffered()) | 196 | const buffer = this.timeRangesToString(this.player_.buffered()) |
197 | const progress = this.player_.webtorrent().getTorrent()?.progress | 197 | const progress = this.player_.webtorrent().getTorrent()?.progress |
198 | 198 | ||
199 | return { | 199 | return { |
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts index 3057a5adb..3fbcec29c 100644 --- a/client/src/assets/player/types/manager-options.ts +++ b/client/src/assets/player/types/manager-options.ts | |||
@@ -29,6 +29,8 @@ export interface CustomizationOptions { | |||
29 | resume?: string | 29 | resume?: string |
30 | 30 | ||
31 | peertubeLink: boolean | 31 | peertubeLink: boolean |
32 | |||
33 | playbackRate?: number | string | ||
32 | } | 34 | } |
33 | 35 | ||
34 | export interface CommonOptions extends CustomizationOptions { | 36 | export interface CommonOptions extends CustomizationOptions { |
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts index c60154f3b..5674f78cb 100644 --- a/client/src/assets/player/types/peertube-videojs-typings.ts +++ b/client/src/assets/player/types/peertube-videojs-typings.ts | |||
@@ -3,6 +3,7 @@ import videojs from 'video.js' | |||
3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' | 3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' |
4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' | 4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | 5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' |
6 | import { HotkeysOptions } from '../shared/hotkeys/peertube-hotkeys-plugin' | ||
6 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' | 7 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' |
7 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' | 8 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' |
8 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' | 9 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' |
@@ -44,7 +45,7 @@ declare module 'video.js' { | |||
44 | 45 | ||
45 | bezels (): void | 46 | bezels (): void |
46 | peertubeMobile (): void | 47 | peertubeMobile (): void |
47 | peerTubeHotkeysPlugin (): void | 48 | peerTubeHotkeysPlugin (options?: HotkeysOptions): void |
48 | 49 | ||
49 | stats (options?: StatsCardOptions): StatsForNerdsPlugin | 50 | stats (options?: StatsCardOptions): StatsForNerdsPlugin |
50 | 51 | ||
diff --git a/client/src/root-helpers/logger.ts b/client/src/root-helpers/logger.ts index d1fdf73aa..618be62cd 100644 --- a/client/src/root-helpers/logger.ts +++ b/client/src/root-helpers/logger.ts | |||
@@ -27,6 +27,10 @@ class Logger { | |||
27 | warn (message: LoggerMessage, meta?: LoggerMeta) { | 27 | warn (message: LoggerMessage, meta?: LoggerMeta) { |
28 | this.runHooks('warn', message, meta) | 28 | this.runHooks('warn', message, meta) |
29 | 29 | ||
30 | this.clientWarn(message, meta) | ||
31 | } | ||
32 | |||
33 | clientWarn (message: LoggerMessage, meta?: LoggerMeta) { | ||
30 | if (meta) console.warn(message, meta) | 34 | if (meta) console.warn(message, meta) |
31 | else console.warn(message) | 35 | else console.warn(message) |
32 | } | 36 | } |
@@ -34,6 +38,10 @@ class Logger { | |||
34 | error (message: LoggerMessage, meta?: LoggerMeta) { | 38 | error (message: LoggerMessage, meta?: LoggerMeta) { |
35 | this.runHooks('error', message, meta) | 39 | this.runHooks('error', message, meta) |
36 | 40 | ||
41 | this.clientError(message, meta) | ||
42 | } | ||
43 | |||
44 | clientError (message: LoggerMessage, meta?: LoggerMeta) { | ||
37 | if (meta) console.error(message, meta) | 45 | if (meta) console.error(message, meta) |
38 | else console.error(message) | 46 | else console.error(message) |
39 | } | 47 | } |
diff --git a/client/src/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts index 6c64e2b01..e5b06a94c 100644 --- a/client/src/root-helpers/plugins-manager.ts +++ b/client/src/root-helpers/plugins-manager.ts | |||
@@ -3,7 +3,7 @@ import * as debug from 'debug' | |||
3 | import { firstValueFrom, ReplaySubject } from 'rxjs' | 3 | import { firstValueFrom, ReplaySubject } from 'rxjs' |
4 | import { first, shareReplay } from 'rxjs/operators' | 4 | import { first, shareReplay } from 'rxjs/operators' |
5 | import { RegisterClientHelpers } from 'src/types/register-client-option.model' | 5 | import { RegisterClientHelpers } from 'src/types/register-client-option.model' |
6 | import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' | 6 | import { getExternalAuthHref, getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' |
7 | import { | 7 | import { |
8 | ClientHookName, | 8 | ClientHookName, |
9 | clientHookObject, | 9 | clientHookObject, |
@@ -16,7 +16,6 @@ import { | |||
16 | RegisterClientRouteOptions, | 16 | RegisterClientRouteOptions, |
17 | RegisterClientSettingsScriptOptions, | 17 | RegisterClientSettingsScriptOptions, |
18 | RegisterClientVideoFieldOptions, | 18 | RegisterClientVideoFieldOptions, |
19 | RegisteredExternalAuthConfig, | ||
20 | ServerConfigPlugin | 19 | ServerConfigPlugin |
21 | } from '@shared/models' | 20 | } from '@shared/models' |
22 | import { environment } from '../environments/environment' | 21 | import { environment } from '../environments/environment' |
@@ -94,9 +93,13 @@ class PluginsManager { | |||
94 | return isTheme ? '/themes' : '/plugins' | 93 | return isTheme ? '/themes' : '/plugins' |
95 | } | 94 | } |
96 | 95 | ||
97 | static getExternalAuthHref (auth: RegisteredExternalAuthConfig) { | 96 | static getDefaultLoginHref (apiUrl: string, serverConfig: HTMLServerConfig) { |
98 | return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` | 97 | if (!serverConfig || serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined |
99 | 98 | ||
99 | const externalAuths = serverConfig.plugin.registeredExternalAuths | ||
100 | if (externalAuths.length !== 1) return undefined | ||
101 | |||
102 | return getExternalAuthHref(apiUrl, externalAuths[0]) | ||
100 | } | 103 | } |
101 | 104 | ||
102 | loadPluginsList (config: HTMLServerConfig) { | 105 | loadPluginsList (config: HTMLServerConfig) { |
diff --git a/client/src/sass/class-helpers.scss b/client/src/sass/class-helpers.scss index bc965331a..feb3a6de2 100644 --- a/client/src/sass/class-helpers.scss +++ b/client/src/sass/class-helpers.scss | |||
@@ -284,3 +284,9 @@ label + .form-group-description { | |||
284 | border: 2px solid pvar(--mainColorLightest); | 284 | border: 2px solid pvar(--mainColorLightest); |
285 | } | 285 | } |
286 | } | 286 | } |
287 | |||
288 | // --------------------------------------------------------------------------- | ||
289 | |||
290 | .chip { | ||
291 | @include chip; | ||
292 | } | ||
diff --git a/client/src/sass/include/_badges.scss b/client/src/sass/include/_badges.scss index 4bc70d4a9..7efd2fb81 100644 --- a/client/src/sass/include/_badges.scss +++ b/client/src/sass/include/_badges.scss | |||
@@ -9,6 +9,10 @@ | |||
9 | font-weight: $font-semibold; | 9 | font-weight: $font-semibold; |
10 | line-height: 1.1; | 10 | line-height: 1.1; |
11 | 11 | ||
12 | &.badge-fs-normal { | ||
13 | font-size: 100%; | ||
14 | } | ||
15 | |||
12 | &.badge-primary { | 16 | &.badge-primary { |
13 | color: pvar(--mainBackgroundColor); | 17 | color: pvar(--mainBackgroundColor); |
14 | background-color: pvar(--mainColor); | 18 | background-color: pvar(--mainColor); |
diff --git a/client/src/sass/include/_fonts.scss b/client/src/sass/include/_fonts.scss index e5a40af34..514261d01 100644 --- a/client/src/sass/include/_fonts.scss +++ b/client/src/sass/include/_fonts.scss | |||
@@ -15,7 +15,3 @@ | |||
15 | font-display: swap; | 15 | font-display: swap; |
16 | src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2'); | 16 | src: url('../fonts/source-sans/WOFF2/VAR/SourceSans3VF-Italic.ttf.woff2') format('woff2'); |
17 | } | 17 | } |
18 | |||
19 | @mixin muted { | ||
20 | color: pvar(--greyForegroundColor) !important; | ||
21 | } | ||
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index b5ccb6598..8816437d9 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss | |||
@@ -36,6 +36,10 @@ | |||
36 | max-height: $font-size * $number-of-lines; | 36 | max-height: $font-size * $number-of-lines; |
37 | } | 37 | } |
38 | 38 | ||
39 | @mixin muted { | ||
40 | color: pvar(--greyForegroundColor) !important; | ||
41 | } | ||
42 | |||
39 | @mixin fade-text ($fade-after, $background-color) { | 43 | @mixin fade-text ($fade-after, $background-color) { |
40 | position: relative; | 44 | position: relative; |
41 | overflow: hidden; | 45 | overflow: hidden; |
@@ -791,51 +795,39 @@ | |||
791 | } | 795 | } |
792 | 796 | ||
793 | @mixin chip { | 797 | @mixin chip { |
794 | --chip-radius: 5rem; | 798 | --avatar-size: 1.2rem; |
795 | --chip-padding: .2rem .4rem; | ||
796 | $avatar-height: 1.2rem; | ||
797 | 799 | ||
798 | align-items: center; | ||
799 | border-radius: var(--chip-radius); | ||
800 | display: inline-flex; | 800 | display: inline-flex; |
801 | font-size: 90%; | ||
802 | color: pvar(--mainForegroundColor); | 801 | color: pvar(--mainForegroundColor); |
803 | height: $avatar-height; | 802 | height: var(--avatar-size); |
804 | line-height: 1rem; | ||
805 | margin: .1rem; | ||
806 | max-width: 320px; | 803 | max-width: 320px; |
807 | overflow: hidden; | 804 | overflow: hidden; |
808 | padding: var(--chip-padding); | ||
809 | text-decoration: none; | 805 | text-decoration: none; |
810 | text-overflow: ellipsis; | 806 | text-overflow: ellipsis; |
811 | vertical-align: middle; | 807 | vertical-align: middle; |
812 | white-space: nowrap; | 808 | white-space: nowrap; |
813 | 809 | ||
814 | &.rectangular { | ||
815 | --chip-radius: .2rem; | ||
816 | --chip-padding: .2rem .3rem; | ||
817 | } | ||
818 | |||
819 | my-actor-avatar { | 810 | my-actor-avatar { |
820 | @include margin-left(-.4rem); | ||
821 | @include margin-right(.2rem); | 811 | @include margin-right(.2rem); |
812 | |||
813 | border-radius: 5rem; | ||
814 | width: var(--avatar-size); | ||
815 | height: var(--avatar-size); | ||
822 | } | 816 | } |
823 | 817 | ||
824 | &.two-lines { | 818 | &.two-lines { |
825 | $avatar-height: 2rem; | 819 | --avatar-size: 2rem; |
826 | 820 | ||
827 | height: $avatar-height; | 821 | font-size: 14px; |
822 | line-height: 1rem; | ||
828 | 823 | ||
829 | my-actor-avatar { | 824 | my-actor-avatar { |
830 | display: inline-block; | 825 | display: inline-block; |
831 | } | 826 | } |
832 | 827 | ||
833 | div { | 828 | > div { |
834 | margin: 0 .1rem; | ||
835 | |||
836 | display: flex; | 829 | display: flex; |
837 | flex-direction: column; | 830 | flex-direction: column; |
838 | height: $avatar-height; | ||
839 | justify-content: center; | 831 | justify-content: center; |
840 | } | 832 | } |
841 | } | 833 | } |
diff --git a/client/src/sass/player/control-bar.scss b/client/src/sass/player/control-bar.scss index 0082378e4..96b3adf66 100644 --- a/client/src/sass/player/control-bar.scss +++ b/client/src/sass/player/control-bar.scss | |||
@@ -153,8 +153,25 @@ | |||
153 | } | 153 | } |
154 | 154 | ||
155 | .vjs-live-control { | 155 | .vjs-live-control { |
156 | line-height: $control-bar-height; | 156 | padding: 5px 7px; |
157 | min-width: 4em; | 157 | border-radius: 3px; |
158 | height: fit-content; | ||
159 | margin: auto 10px; | ||
160 | font-weight: bold; | ||
161 | max-width: fit-content; | ||
162 | opacity: 1 !important; | ||
163 | line-height: normal; | ||
164 | position: relative; | ||
165 | top: -1px; | ||
166 | |||
167 | &.synced-with-live-edge { | ||
168 | background: #d7281c; | ||
169 | } | ||
170 | |||
171 | &:not(.synced-with-live-edge) { | ||
172 | cursor: pointer; | ||
173 | background: #80807f; | ||
174 | } | ||
158 | } | 175 | } |
159 | 176 | ||
160 | .vjs-peertube { | 177 | .vjs-peertube { |
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index 88f6efb6a..ee66a9db3 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss | |||
@@ -294,6 +294,7 @@ body .p-datepicker .p-datepicker-header .p-datepicker-title select:focus { | |||
294 | body .p-datepicker table { | 294 | body .p-datepicker table { |
295 | font-size: 14px; | 295 | font-size: 14px; |
296 | margin: 0.857em 0 0 0; | 296 | margin: 0.857em 0 0 0; |
297 | table-layout: fixed; | ||
297 | } | 298 | } |
298 | body .p-datepicker table th { | 299 | body .p-datepicker table th { |
299 | padding: 0.5em; | 300 | padding: 0.5em; |
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts index b0bdb2dd9..f09c86d14 100644 --- a/client/src/standalone/videos/shared/player-manager-options.ts +++ b/client/src/standalone/videos/shared/player-manager-options.ts | |||
@@ -38,6 +38,7 @@ export class PlayerManagerOptions { | |||
38 | private enableApi = false | 38 | private enableApi = false |
39 | private startTime: number | string = 0 | 39 | private startTime: number | string = 0 |
40 | private stopTime: number | string | 40 | private stopTime: number | string |
41 | private playbackRate: number | string | ||
41 | 42 | ||
42 | private title: boolean | 43 | private title: boolean |
43 | private warningTitle: boolean | 44 | private warningTitle: boolean |
@@ -130,6 +131,7 @@ export class PlayerManagerOptions { | |||
130 | this.subtitle = getParamString(params, 'subtitle') | 131 | this.subtitle = getParamString(params, 'subtitle') |
131 | this.startTime = getParamString(params, 'start') | 132 | this.startTime = getParamString(params, 'start') |
132 | this.stopTime = getParamString(params, 'stop') | 133 | this.stopTime = getParamString(params, 'stop') |
134 | this.playbackRate = getParamString(params, 'playbackRate') | ||
133 | 135 | ||
134 | this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor') | 136 | this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor') |
135 | this.foregroundColor = getParamString(params, 'foregroundColor') | 137 | this.foregroundColor = getParamString(params, 'foregroundColor') |
@@ -210,6 +212,8 @@ export class PlayerManagerOptions { | |||
210 | ? playlistTracker.getCurrentElement().stopTimestamp | 212 | ? playlistTracker.getCurrentElement().stopTimestamp |
211 | : this.stopTime, | 213 | : this.stopTime, |
212 | 214 | ||
215 | playbackRate: this.playbackRate, | ||
216 | |||
213 | videoCaptions, | 217 | videoCaptions, |
214 | inactivityTimeout: 2500, | 218 | inactivityTimeout: 2500, |
215 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), | 219 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), |