aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html79
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.scss35
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.ts49
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html337
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss13
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts83
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts6
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html21
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts47
-rw-r--r--client/src/app/+my-account/my-account.module.ts4
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.html15
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.ts16
-rw-r--r--client/src/app/+signup/+register/register.component.html63
-rw-r--r--client/src/app/+signup/+register/register.component.scss37
-rw-r--r--client/src/app/+signup/+register/register.component.ts40
-rw-r--r--client/src/app/+signup/+register/register.module.ts4
-rw-r--r--client/src/app/app.component.html5
-rw-r--r--client/src/app/app.component.ts49
-rw-r--r--client/src/app/app.module.ts7
-rw-r--r--client/src/app/login/login.component.html9
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.html45
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.scss22
-rw-r--r--client/src/app/modal/instance-config-warning-modal.component.ts47
-rw-r--r--client/src/app/modal/welcome-modal.component.html67
-rw-r--r--client/src/app/modal/welcome-modal.component.scss56
-rw-r--r--client/src/app/modal/welcome-modal.component.ts38
-rw-r--r--client/src/app/shared/angular/peertube-template.directive.ts4
-rw-r--r--client/src/app/shared/forms/form-validators/custom-config-validators.service.ts9
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.html11
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.scss2
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.ts24
-rw-r--r--client/src/app/shared/instance/feature-boolean.component.html3
-rw-r--r--client/src/app/shared/instance/feature-boolean.component.scss10
-rw-r--r--client/src/app/shared/instance/feature-boolean.component.ts10
-rw-r--r--client/src/app/shared/instance/instance-features-table.component.html71
-rw-r--r--client/src/app/shared/instance/instance-features-table.component.scss26
-rw-r--r--client/src/app/shared/instance/instance-features-table.component.ts36
-rw-r--r--client/src/app/shared/instance/instance.service.ts46
-rw-r--r--client/src/app/shared/misc/help.component.html28
-rw-r--r--client/src/app/shared/misc/help.component.ts36
-rw-r--r--client/src/app/shared/shared.module.ts9
-rw-r--r--client/src/app/shared/user-subscription/remote-subscribe.component.html22
-rw-r--r--client/src/app/shared/users/user-notification.model.ts9
-rw-r--r--client/src/app/shared/users/user-notifications.component.html10
-rw-r--r--client/src/app/shared/users/user.model.ts33
-rw-r--r--client/src/app/shared/video/videos-selection.component.ts2
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.html54
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html12
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html14
-rw-r--r--client/src/sass/include/_mixins.scss1
-rw-r--r--client/src/sass/player/peertube-skin.scss5
-rw-r--r--client/src/standalone/videos/embed.ts2
52 files changed, 1373 insertions, 310 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 7c27ec760..9499bbe4e 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -1,27 +1,92 @@
1<div class="row"> 1<div class="row">
2 <div class="col-md-12 col-xl-6"> 2 <div class="col-md-12 col-xl-6">
3
3 <div class="about-instance-title"> 4 <div class="about-instance-title">
4 <div i18n>About {{ instanceName }} instance</div> 5 <div i18n class="title">About {{ instanceName }} instance</div>
5 6
6 <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div> 7 <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div>
7 </div> 8 </div>
8 9
10 <div class="block instance-badges">
11 <span *ngFor="let category of categories" class="badge badge-primary category">{{ category }}</span>
12
13 <span *ngFor="let language of languages" class="badge badge-secondary language">{{ language }}</span>
14 </div>
15
9 <div class="short-description"> 16 <div class="short-description">
10 <div>{{ shortDescription }}</div> 17 <div class="block short-description">{{ shortDescription }}</div>
18
19 <div *ngIf="isNSFW" class="block dedicated-to-nsfw">This instance is dedicated to sensitive/NSFW content.</div>
20 </div>
21
22 <div class="middle-title" *ngIf="html.administrator || maintenanceLifetime || businessModel">
23 Administrators & sustainability
24 </div>
25
26 <div class="block administrator" *ngIf="html.administrator">
27 <div i18n class="section-title">Who we are</div>
11 28
12 <div *ngIf="isNSFW" class="dedicated-to-nsfw">This instance is dedicated to sensitive/NSFW content.</div> 29 <div [innerHTML]="html.administrator"></div>
13 </div> 30 </div>
14 31
15 <div class="description"> 32 <div class="block creation-reason" *ngIf="creationReason">
33 <div i18n class="section-title">Why we created this instance</div>
34
35 <p>{{ creationReason }}</p>
36 </div>
37
38 <div class="block maintenance-lifetime" *ngIf="maintenanceLifetime">
39 <div i18n class="section-title">How long we plan to maintain this instance</div>
40
41 <p>{{ maintenanceLifetime }}</p>
42 </div>
43
44 <div class="block business-model" *ngIf="businessModel">
45 <div i18n class="section-title">How we will pay this instance</div>
46
47 <p>{{ businessModel }}</p>
48 </div>
49
50 <div class="middle-title" *ngIf="html.description">
51 Information
52 </div>
53
54 <div class="block description">
16 <div i18n class="section-title">Description</div> 55 <div i18n class="section-title">Description</div>
17 56
18 <div [innerHTML]="descriptionHTML"></div> 57 <div [innerHTML]="html.description"></div>
58 </div>
59
60 <div class="middle-title" *ngIf="html.moderationInformation || html.codeOfConduct || html.terms">
61 Moderation
62 </div>
63
64 <div class="block moderation-information" *ngIf="html.moderationInformation">
65 <div i18n class="section-title">Moderation information</div>
66
67 <div [innerHTML]="html.moderationInformation"></div>
19 </div> 68 </div>
20 69
21 <div class="terms" id="terms-section"> 70 <div class="block code-of-conduct" *ngIf="html.codeOfConduct">
71 <div i18n class="section-title">Code of conduct</div>
72
73 <div [innerHTML]="html.codeOfConduct"></div>
74 </div>
75
76 <div class="block terms">
22 <div i18n class="section-title">Terms</div> 77 <div i18n class="section-title">Terms</div>
23 78
24 <div [innerHTML]="termsHTML"></div> 79 <div [innerHTML]="html.terms"></div>
80 </div>
81
82 <div class="middle-title" *ngIf="html.hardwareInformation">
83 Other information
84 </div>
85
86 <div class="block hardware-information">
87 <div i18n class="section-title">Hardware information</div>
88
89 <div [innerHTML]="html.hardwareInformation"></div>
25 </div> 90 </div>
26 </div> 91 </div>
27 92
diff --git a/client/src/app/+about/about-instance/about-instance.component.scss b/client/src/app/+about/about-instance/about-instance.component.scss
index 0296ae8e9..909ae5c21 100644
--- a/client/src/app/+about/about-instance/about-instance.component.scss
+++ b/client/src/app/+about/about-instance/about-instance.component.scss
@@ -5,13 +5,12 @@
5 display: flex; 5 display: flex;
6 justify-content: space-between; 6 justify-content: space-between;
7 7
8 & > div { 8 .title {
9 font-size: 20px; 9 font-size: 20px;
10 font-weight: bold; 10 font-weight: $font-semibold;
11 margin-bottom: 15px;
12 } 11 }
13 12
14 & > .contact-admin { 13 .contact-admin {
15 @include peertube-button; 14 @include peertube-button;
16 @include orange-button; 15 @include orange-button;
17 16
@@ -19,14 +18,38 @@
19 } 18 }
20} 19}
21 20
21.instance-badges {
22 font-size: 16px;
23
24 .badge {
25 font-size: 12px;
26 font-weight: $font-semibold;
27 margin-right: 5px;
28
29 &.category {
30 background-color: var(--mainColor);
31 }
32 }
33}
34
22.section-title { 35.section-title {
23 font-weight: $font-semibold; 36 font-weight: $font-semibold;
24 font-size: 20px; 37 font-size: 16px;
25 margin-bottom: 5px; 38 margin-bottom: 5px;
39 display: flex;
40 align-items: center;
41}
42
43.middle-title {
44 @include in-content-small-title;
45
46 margin-top: 45px;
47 margin-bottom: 25px;
26} 48}
27 49
28.short-description, .description, .terms, .signup { 50.block {
29 margin-bottom: 30px; 51 margin-bottom: 30px;
52 font-size: 15px;
30} 53}
31 54
32.short-description .dedicated-to-nsfw { 55.short-description .dedicated-to-nsfw {
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 a5204de27..16ccae2e2 100644
--- a/client/src/app/+about/about-instance/about-instance.component.ts
+++ b/client/src/app/+about/about-instance/about-instance.component.ts
@@ -4,6 +4,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
4import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' 4import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
5import { InstanceService } from '@app/shared/instance/instance.service' 5import { InstanceService } from '@app/shared/instance/instance.service'
6import { MarkdownService } from '@app/shared/renderer' 6import { MarkdownService } from '@app/shared/renderer'
7import { forkJoin } from 'rxjs'
8import { first } from 'rxjs/operators'
7 9
8@Component({ 10@Component({
9 selector: 'my-about-instance', 11 selector: 'my-about-instance',
@@ -14,8 +16,22 @@ export class AboutInstanceComponent implements OnInit {
14 @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent 16 @ViewChild('contactAdminModal', { static: true }) contactAdminModal: ContactAdminModalComponent
15 17
16 shortDescription = '' 18 shortDescription = ''
17 descriptionHTML = '' 19
18 termsHTML = '' 20 html = {
21 description: '',
22 terms: '',
23 codeOfConduct: '',
24 moderationInformation: '',
25 administrator: '',
26 hardwareInformation: ''
27 }
28
29 creationReason = ''
30 maintenanceLifetime = ''
31 businessModel = ''
32
33 languages: string[] = []
34 categories: string[] = []
19 35
20 constructor ( 36 constructor (
21 private notifier: Notifier, 37 private notifier: Notifier,
@@ -38,21 +54,30 @@ export class AboutInstanceComponent implements OnInit {
38 } 54 }
39 55
40 ngOnInit () { 56 ngOnInit () {
41 this.instanceService.getAbout() 57 forkJoin([
42 .subscribe( 58 this.instanceService.getAbout(),
43 async res => { 59 this.serverService.localeObservable.pipe(first()),
44 this.shortDescription = res.instance.shortDescription 60 this.serverService.videoLanguagesLoaded.pipe(first()),
61 this.serverService.videoCategoriesLoaded.pipe(first())
62 ]).subscribe(
63 async ([ about, translations ]) => {
64 this.shortDescription = about.instance.shortDescription
45 65
46 this.descriptionHTML = await this.markdownService.textMarkdownToHTML(res.instance.description) 66 this.creationReason = about.instance.creationReason
47 this.termsHTML = await this.markdownService.textMarkdownToHTML(res.instance.terms) 67 this.maintenanceLifetime = about.instance.maintenanceLifetime
48 }, 68 this.businessModel = about.instance.businessModel
49 69
50 () => this.notifier.error(this.i18n('Cannot get about information from server')) 70 this.html = await this.instanceService.buildHtml(about)
51 ) 71
72 this.languages = this.instanceService.buildTranslatedLanguages(about, translations)
73 this.categories = this.instanceService.buildTranslatedCategories(about, translations)
74 },
75
76 () => this.notifier.error(this.i18n('Cannot get about information from server'))
77 )
52 } 78 }
53 79
54 openContactModal () { 80 openContactModal () {
55 return this.contactAdminModal.show() 81 return this.contactAdminModal.show()
56 } 82 }
57
58} 83}
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index fe9d856d0..54115055a 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -2,12 +2,13 @@
2 2
3 <ngb-tabset class="root-tabset bootstrap"> 3 <ngb-tabset class="root-tabset bootstrap">
4 4
5 <ngb-tab i18n-title title="Basic configuration"> 5 <ngb-tab i18n-title title="Instance information">
6 <ng-template ngbTabContent> 6 <ng-template ngbTabContent>
7 7
8 <div i18n class="inner-form-title">Instance</div>
9
10 <ng-container formGroupName="instance"> 8 <ng-container formGroupName="instance">
9
10 <div i18n class="inner-form-title">Instance</div>
11
11 <div class="form-group"> 12 <div class="form-group">
12 <label i18n for="instanceName">Name</label> 13 <label i18n for="instanceName">Name</label>
13 <input 14 <input
@@ -20,7 +21,7 @@
20 <div class="form-group"> 21 <div class="form-group">
21 <label i18n for="instanceShortDescription">Short description</label> 22 <label i18n for="instanceShortDescription">Short description</label>
22 <textarea 23 <textarea
23 id="instanceShortDescription" formControlName="shortDescription" 24 id="instanceShortDescription" formControlName="shortDescription" class="small"
24 [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }" 25 [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
25 ></textarea> 26 ></textarea>
26 <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div> 27 <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
@@ -36,42 +37,56 @@
36 </div> 37 </div>
37 38
38 <div class="form-group"> 39 <div class="form-group">
39 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help> 40 <label i18n for="instanceCategories">Main instance categories</label>
40 <my-markdown-textarea 41
41 id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true" 42 <div>
42 [ngClass]="{ 'input-error': formErrors['instance.terms'] }" 43 <p-multiSelect
43 ></my-markdown-textarea> 44 inputId="instanceCategories" [options]="categoryItems" formControlName="categories" showToggleAll="false"
44 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div> 45 [defaultLabel]="getDefaultCategoryLabel()" [selectedItemsLabel]="getSelectedCategoryLabel()"
46 emptyFilterMessage="No results found" i18n-emptyFilterMessage
47 ></p-multiSelect>
48 </div>
45 </div> 49 </div>
46 50
47 <div class="form-group"> 51 <div class="form-group">
48 <my-peertube-checkbox 52 <label i18n for="instanceLanguages">Main languages you/your moderators speak</label>
49 inputName="instanceIsNSFW" formControlName="isNSFW" 53
50 i18n-labelText labelText="Dedicated to sensitive or NSFW content" 54 <div>
51 i18n-helpHtml helpHtml="Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br /> 55 <p-multiSelect
52 Moreover, the NSFW checkbox on video upload will be automatically checked by default." 56 inputId="instanceLanguages" [options]="languageItems" formControlName="languages" showToggleAll="false"
53 ></my-peertube-checkbox> 57 [defaultLabel]="getDefaultLanguageLabel()" [selectedItemsLabel]="getSelectedLanguageLabel()"
58 emptyFilterMessage="No results found" i18n-emptyFilterMessage
59 ></p-multiSelect>
60 </div>
54 </div> 61 </div>
55 62
63 <div i18n class="inner-form-title">Moderation & NSFW</div>
64
56 <div class="form-group"> 65 <div class="form-group">
57 <label i18n for="instanceDefaultClientRoute">Default client route</label> 66 <my-peertube-checkbox inputName="instanceIsNSFW" formControlName="isNSFW">
58 <div class="peertube-select-container"> 67 <ng-template ptTemplate="label">
59 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute"> 68 <ng-container i18n>This instance is dedicated to sensitive or NSFW content</ng-container>
60 <option i18n value="/videos/overview">Videos Overview</option> 69 </ng-template>
61 <option i18n value="/videos/trending">Videos Trending</option> 70
62 <option i18n value="/videos/recently-added">Videos Recently Added</option> 71 <ng-template ptTemplate="help">
63 <option i18n value="/videos/local">Local videos</option> 72 <ng-container i18n>
64 </select> 73 Enabling it will allow other administrators to know that you are mainly federating sensitive content.<br /><br />
65 </div> 74 Moreover, the NSFW checkbox on video upload will be automatically checked by default.
66 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div> 75 </ng-container>
76 </ng-template>
77 </my-peertube-checkbox>
67 </div> 78 </div>
68 79
69 <div class="form-group"> 80 <div class="form-group">
70 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label> 81 <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
71 <my-help 82
72 helpType="custom" i18n-customHtml 83 <my-help>
73 customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video." 84 <ng-template ptTemplate="customHtml">
74 ></my-help> 85 <ng-container i18n>
86 With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
87 </ng-container>
88 </ng-template>
89 </my-help>
75 90
76 <div class="peertube-select-container"> 91 <div class="peertube-select-container">
77 <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy"> 92 <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy">
@@ -82,10 +97,105 @@
82 </div> 97 </div>
83 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div> 98 <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
84 </div> 99 </div>
100
101 <div class="form-group">
102 <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
103 <my-markdown-textarea
104 id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
105 [ngClass]="{ 'input-error': formErrors['instance.terms'] }"
106 ></my-markdown-textarea>
107 <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
108 </div>
109
110 <div class="form-group">
111 <label i18n for="instanceCodeOfConduct">Code of conduct</label><my-help helpType="markdownText"></my-help>
112 <my-markdown-textarea
113 id="instanceCodeOfConduct" formControlName="codeOfConduct" textareaWidth="500px" [previewColumn]="true"
114 [ngClass]="{ 'input-error': formErrors['instance.codeOfConduct'] }"
115 ></my-markdown-textarea>
116 <div *ngIf="formErrors.instance.codeOfConduct" class="form-error">{{ formErrors.instance.codeOfConduct }}</div>
117 </div>
118
119 <div class="form-group">
120 <label i18n for="instanceModerationInformation">Moderation information</label><my-help helpType="markdownText"></my-help>
121 <div class="label-small-info">Who moderates the instance? What is the policy regarding NSFW videos? Political videos? etc</div>
122
123 <my-markdown-textarea
124 id="instanceModerationInformation" formControlName="moderationInformation" textareaWidth="500px" [previewColumn]="true"
125 [ngClass]="{ 'input-error': formErrors['instance.moderationInformation'] }"
126 ></my-markdown-textarea>
127 <div *ngIf="formErrors.instance.moderationInformation" class="form-error">{{ formErrors.instance.moderationInformation }}</div>
128 </div>
129
130 <div i18n class="inner-form-title">You and your instance</div>
131
132 <div class="form-group">
133 <label i18n for="instanceAdministrator">Who is behind the instance?</label>
134 <div class="label-small-info">A single person? A non profit? A company?</div>
135
136 <my-markdown-textarea
137 id="instanceAdministrator" formControlName="administrator" textareaWidth="500px" textareaHeight="75px" [previewColumn]="true"
138 [classes]="{ 'input-error': formErrors['instance.administrator'] }"
139 ></my-markdown-textarea>
140
141 <div *ngIf="formErrors.instance.administrator" class="form-error">{{ formErrors.instance.administrator }}</div>
142 </div>
143
144 <div class="form-group">
145 <label i18n for="instanceCreationReason">Why did you create this instance?</label>
146 <div class="label-small-info">To share your personal videos? To open registrations and allow people to upload what they want?</div>
147
148 <textarea
149 id="instanceCreationReason" formControlName="creationReason" class="small"
150 [ngClass]="{ 'input-error': formErrors['instance.creationReason'] }"
151 ></textarea>
152 <div *ngIf="formErrors.instance.creationReason" class="form-error">{{ formErrors.instance.creationReason }}</div>
153 </div>
154
155 <div class="form-group">
156 <label i18n for="instanceMaintenanceLifetime">How long do you plan to maintain this instance?</label>
157 <div class="label-small-info">It's important to know for users who want to register on your instance</div>
158
159 <textarea
160 id="instanceMaintenanceLifetime" formControlName="maintenanceLifetime" class="small"
161 [ngClass]="{ 'input-error': formErrors['instance.maintenanceLifetime'] }"
162 ></textarea>
163 <div *ngIf="formErrors.instance.maintenanceLifetime" class="form-error">{{ formErrors.instance.maintenanceLifetime }}</div>
164 </div>
165
166 <div class="form-group">
167 <label i18n for="instanceBusinessModel">How will you pay the PeerTube instance server?</label>
168 <div class="label-small-info">With you own funds? With users donations? Advertising?</div>
169
170 <textarea
171 id="instanceBusinessModel" formControlName="businessModel" class="small"
172 [ngClass]="{ 'input-error': formErrors['instance.businessModel'] }"
173 ></textarea>
174 <div *ngIf="formErrors.instance.businessModel" class="form-error">{{ formErrors.instance.businessModel }}</div>
175 </div>
176
177 <div i18n class="inner-form-title">Other information</div>
178
179 <div class="form-group">
180 <label i18n for="instanceHardwareInformation">On what server/hardware the instance runs?</label>
181 <div class="label-small-info">2vCore 2GB RAM/or directly the link to the server you rent etc</div>
182
183 <my-markdown-textarea
184 id="instanceHardwareInformation" formControlName="hardwareInformation" textareaWidth="500px" textareaHeight="75px" [previewColumn]="true"
185 [classes]="{ 'input-error': formErrors['instance.hardwareInformation'] }"
186 ></my-markdown-textarea>
187
188 <div *ngIf="formErrors.instance.hardwareInformation" class="form-error">{{ formErrors.instance.hardwareInformation }}</div>
189 </div>
190
85 </ng-container> 191 </ng-container>
192 </ng-template>
193 </ngb-tab>
86 194
195 <ngb-tab i18n-title title="Basic configuration">
196 <ng-template ngbTabContent>
87 197
88 <div i18n class="inner-form-title">Theme</div> 198 <div i18n class="inner-form-title">Theme & Default route</div>
89 199
90 <ng-container formGroupName="theme"> 200 <ng-container formGroupName="theme">
91 <div class="form-group"> 201 <div class="form-group">
@@ -102,6 +212,19 @@
102 </ng-container> 212 </ng-container>
103 213
104 214
215 <div class="form-group" formGroupName="instance">
216 <label i18n for="instanceDefaultClientRoute">Default client route</label>
217 <div class="peertube-select-container">
218 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
219 <option i18n value="/videos/overview">Videos Discover</option>
220 <option i18n value="/videos/trending">Videos Trending</option>
221 <option i18n value="/videos/recently-added">Videos Recently Added</option>
222 <option i18n value="/videos/local">Local videos</option>
223 </select>
224 </div>
225 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
226 </div>
227
105 <div i18n class="inner-form-title">Signup</div> 228 <div i18n class="inner-form-title">Signup</div>
106 229
107 <ng-container formGroupName="signup"> 230 <ng-container formGroupName="signup">
@@ -221,6 +344,41 @@
221 </ng-container> 344 </ng-container>
222 </ng-container> 345 </ng-container>
223 346
347 <div i18n class="inner-form-title">Instance followings</div>
348
349 <ng-container formGroupName="followings">
350 <ng-container formGroupName="instance">
351
352 <ng-container formGroupName="autoFollowBack">
353 <div class="form-group">
354 <my-peertube-checkbox
355 inputName="followingsInstanceAutoFollowBackEnabled" formControlName="enabled"
356 i18n-labelText labelText="Automatically follow other instances that follow you"
357 ></my-peertube-checkbox>
358 </div>
359 </ng-container>
360
361 <ng-container formGroupName="autoFollowIndex">
362 <div class="form-group">
363 <my-peertube-checkbox
364 inputName="followingsInstanceAutoFollowIndexEnabled" formControlName="enabled"
365 i18n-labelText labelText="Automatically follow instance of the public index (below)"
366 ></my-peertube-checkbox>
367 </div>
368
369 <div class="form-group">
370 <label i18n for="followingsInstanceAutoFollowIndexUrl">Index URL</label>
371 <input
372 type="text" id="followingsInstanceAutoFollowIndexUrl"
373 formControlName="indexUrl" [ngClass]="{ 'input-error': formErrors['followings.instance.autoFollowIndex.indexUrl'] }"
374 >
375 <div *ngIf="formErrors.followings.instance.autoFollowIndex.indexUrl" class="form-error">{{ formErrors.followings.instance.autoFollowIndex.indexUrl }}</div>
376 </div>
377
378 </ng-container>
379 </ng-container>
380 </ng-container>
381
224 382
225 <div i18n class="inner-form-title">Administrator</div> 383 <div i18n class="inner-form-title">Administrator</div>
226 384
@@ -252,10 +410,13 @@
252 410
253 <div class="form-group"> 411 <div class="form-group">
254 <label i18n for="signupLimit">Your Twitter username</label> 412 <label i18n for="signupLimit">Your Twitter username</label>
255 <my-help 413
256 helpType="custom" i18n-customHtml 414 <my-help>
257 customHtml="Indicates the Twitter account for the website or platform on which the content was published." 415 <ng-template ptTemplate="customHtml">
258 ></my-help> 416 <ng-container i18n>Indicates the Twitter account for the website or platform on which the content was published.</ng-container>
417 </ng-template>
418 </my-help>
419
259 <input 420 <input
260 type="text" id="servicesTwitterUsername" 421 type="text" id="servicesTwitterUsername"
261 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }" 422 formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
@@ -264,13 +425,21 @@
264 </div> 425 </div>
265 426
266 <div class="form-group"> 427 <div class="form-group">
267 <my-peertube-checkbox 428 <my-peertube-checkbox inputName="servicesTwitterWhitelisted" formControlName="whitelisted">
268 inputName="servicesTwitterWhitelisted" formControlName="whitelisted" 429 <ng-template ptTemplate="label">
269 i18n-labelText labelText="Instance whitelisted by Twitter" 430 <ng-container i18n>Instance whitelisted by Twitter</ng-container>
270 i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br /> 431 </ng-template>
271 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br /> 432
272 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted." 433 <ng-template ptTemplate="help">
273 ></my-peertube-checkbox> 434 <ng-container i18n>
435 If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
436 If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
437 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on
438 <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a>
439 to see if you instance is whitelisted.
440 </ng-container>
441 </ng-template>
442 </my-peertube-checkbox>
274 </div> 443 </div>
275 444
276 </ng-container> 445 </ng-container>
@@ -286,11 +455,15 @@
286 455
287 <ng-container formGroupName="transcoding"> 456 <ng-container formGroupName="transcoding">
288 <div class="form-group"> 457 <div class="form-group">
289 <my-peertube-checkbox 458 <my-peertube-checkbox inputName="transcodingEnabled" formControlName="enabled">
290 inputName="transcodingEnabled" formControlName="enabled" 459 <ng-template ptTemplate="label">
291 i18n-labelText labelText="Transcoding enabled" 460 <ng-container i18n>Transcoding enabled</ng-container>
292 i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!" 461 </ng-template>
293 ></my-peertube-checkbox> 462
463 <ng-template ptTemplate="help">
464 <ng-container i18n>If you disable transcoding, many videos from your users will not work!</ng-container>
465 </ng-template>
466 </my-peertube-checkbox>
294 </div> 467 </div>
295 468
296 <ng-container *ngIf="isTranscodingEnabled()"> 469 <ng-container *ngIf="isTranscodingEnabled()">
@@ -299,16 +472,22 @@
299 <my-peertube-checkbox 472 <my-peertube-checkbox
300 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions" 473 inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
301 i18n-labelText labelText="Allow additional extensions" 474 i18n-labelText labelText="Allow additional extensions"
302 i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos" 475 >
303 ></my-peertube-checkbox> 476 <ng-template ptTemplate="help">
477 <ng-container i18n>Allow your users to upload .mkv, .mov, .avi, .flv videos</ng-container>
478 </ng-template>
479 </my-peertube-checkbox>
304 </div> 480 </div>
305 481
306 <div class="form-group"> 482 <div class="form-group">
307 <my-peertube-checkbox 483 <my-peertube-checkbox
308 inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles" 484 inputName="transcodingAllowAudioFiles" formControlName="allowAudioFiles"
309 i18n-labelText labelText="Allow audio files upload" 485 i18n-labelText labelText="Allow audio files upload"
310 i18n-helpHtml helpHtml="Allow your users to upload audio files that will be merged with the preview file on upload" 486 >
311 ></my-peertube-checkbox> 487 <ng-template ptTemplate="help">
488 <ng-container i18n>Allow your users to upload audio files that will be merged with the preview file on upload</ng-container>
489 </ng-template>
490 </my-peertube-checkbox>
312 </div> 491 </div>
313 492
314 <div class="form-group"> 493 <div class="form-group">
@@ -338,10 +517,11 @@
338 <div i18n class="inner-form-title"> 517 <div i18n class="inner-form-title">
339 Cache 518 Cache
340 519
341 <my-help 520 <my-help>
342 helpType="custom" i18n-customHtml 521 <ng-template ptTemplate="customHtml">
343 customHtml="Some files are not federated (previews, captions). We fetch them directly from the origin instance and cache them." 522 <ng-container i18n>Some files are not federated (previews, captions). We fetch them directly from the origin instance and cache them.</ng-container>
344 ></my-help> 523 </ng-template>
524 </my-help>
345 </div> 525 </div>
346 526
347 <ng-container formGroupName="cache"> 527 <ng-container formGroupName="cache">
@@ -370,38 +550,45 @@
370 <ng-container formGroupName="customizations"> 550 <ng-container formGroupName="customizations">
371 <div class="form-group"> 551 <div class="form-group">
372 <label i18n for="customizationJavascript">JavaScript</label> 552 <label i18n for="customizationJavascript">JavaScript</label>
373 <my-help 553 <my-help>
374 helpType="custom" i18n-customHtml 554 <ng-template ptTemplate="customHtml">
375 customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>" 555 <ng-container i18n>
376 ></my-help> 556 Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>
557 </ng-container>
558 </ng-template>
559 </my-help>
560
377 <textarea 561 <textarea
378 id="customizationJavascript" formControlName="javascript" 562 id="customizationJavascript" formControlName="javascript"
379 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }" 563 [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
380 ></textarea> 564 ></textarea>
565
381 <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div> 566 <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
382 </div> 567 </div>
383 568
384 <div class="form-group"> 569 <div class="form-group">
385 <label for="customizationCSS">CSS</label> 570 <label for="customizationCSS">CSS</label>
386 <my-help 571
387 helpType="custom" 572 <my-help>
388 i18n-customHtml 573 <ng-template ptTemplate="customHtml">
389 customHtml=" 574 <ng-container i18n>
390 Write directly CSS code. Example:<br /><br /> 575 Write directly CSS code. Example:<br /><br />
391 <pre> 576<pre>
392 #custom-css {{ '{' }} 577#custom-css {{ '{' }}
393 color: red; 578 color: red;
394 {{ '}' }} 579{{ '}' }}
395 </pre> 580</pre>
396 581
397 Prepend with <em>#custom-css</em> to override styles. Example:<br /><br /> 582 Prepend with <em>#custom-css</em> to override styles. Example:<br /><br />
398 <pre> 583<pre>
399 #custom-css .logged-in-email {{ '{' }} 584#custom-css .logged-in-email {{ '{' }}
400 color: red; 585 color: red;
401 {{ '}' }} 586{{ '}' }}
402 </pre> 587</pre>
403 " 588 </ng-container>
404 ></my-help> 589 </ng-template>
590 </my-help>
591
405 <textarea 592 <textarea
406 id="customizationCSS" formControlName="css" 593 id="customizationCSS" formControlName="css"
407 [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }" 594 [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
index c90bd5141..2b4d0da2c 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
@@ -1,6 +1,10 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.form-group {
5 margin-bottom: 25px;
6}
7
4input[type=text] { 8input[type=text] {
5 @include peertube-input-text(340px); 9 @include peertube-input-text(340px);
6 display: block; 10 display: block;
@@ -40,7 +44,12 @@ textarea {
40 44
41 display: block; 45 display: block;
42 46
43 &#instanceShortDescription { 47 &.small {
44 height: 100px; 48 height: 75px;
45 } 49 }
46} 50}
51
52.label-small-info {
53 font-style: italic;
54 margin-bottom: 10px;
55}
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 8bd7f7cf6..0a69f3481 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
@@ -6,6 +6,9 @@ import { Notifier } from '@app/core'
6import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model' 6import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 8import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
9import { SelectItem } from 'primeng/api'
10import { forkJoin } from 'rxjs'
11import { first } from 'rxjs/operators'
9 12
10@Component({ 13@Component({
11 selector: 'my-edit-custom-config', 14 selector: 'my-edit-custom-config',
@@ -18,6 +21,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
18 resolutions: { id: string, label: string }[] = [] 21 resolutions: { id: string, label: string }[] = []
19 transcodingThreadOptions: { label: string, value: number }[] = [] 22 transcodingThreadOptions: { label: string, value: number }[] = []
20 23
24 languageItems: SelectItem[] = []
25 categoryItems: SelectItem[] = []
26
21 constructor ( 27 constructor (
22 protected formValidatorService: FormValidatorService, 28 protected formValidatorService: FormValidatorService,
23 private customConfigValidatorsService: CustomConfigValidatorsService, 29 private customConfigValidatorsService: CustomConfigValidatorsService,
@@ -88,10 +94,26 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
88 name: this.customConfigValidatorsService.INSTANCE_NAME, 94 name: this.customConfigValidatorsService.INSTANCE_NAME,
89 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, 95 shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
90 description: null, 96 description: null,
91 terms: null, 97
92 defaultClientRoute: null,
93 isNSFW: false, 98 isNSFW: false,
94 defaultNSFWPolicy: null, 99 defaultNSFWPolicy: null,
100
101 terms: null,
102 codeOfConduct: null,
103
104 creationReason: null,
105 moderationInformation: null,
106 administrator: null,
107 maintenanceLifetime: null,
108 businessModel: null,
109
110 hardwareInformation: null,
111
112 categories: null,
113 languages: null,
114
115 defaultClientRoute: null,
116
95 customizations: { 117 customizations: {
96 javascript: null, 118 javascript: null,
97 css: null 119 css: null
@@ -158,6 +180,17 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
158 enabled: null, 180 enabled: null,
159 manualApproval: null 181 manualApproval: null
160 } 182 }
183 },
184 followings: {
185 instance: {
186 autoFollowBack: {
187 enabled: null
188 },
189 autoFollowIndex: {
190 enabled: null,
191 indexUrl: this.customConfigValidatorsService.INDEX_URL
192 }
193 }
161 } 194 }
162 } 195 }
163 196
@@ -173,18 +206,27 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
173 206
174 this.buildForm(formGroupData) 207 this.buildForm(formGroupData)
175 208
176 this.configService.getCustomConfig() 209 forkJoin([
177 .subscribe( 210 this.configService.getCustomConfig(),
178 res => { 211 this.serverService.videoLanguagesLoaded.pipe(first()), // First so the observable completes
179 this.customConfig = res 212 this.serverService.videoCategoriesLoaded.pipe(first())
213 ]).subscribe(
214 ([ config ]) => {
215 this.customConfig = config
180 216
181 this.updateForm() 217 const languages = this.serverService.getVideoLanguages()
182 // Force form validation 218 this.languageItems = languages.map(l => ({ label: l.label, value: l.id }))
183 this.forceCheck()
184 },
185 219
186 err => this.notifier.error(err.message) 220 const categories = this.serverService.getVideoCategories()
187 ) 221 this.categoryItems = categories.map(l => ({ label: l.label, value: l.id }))
222
223 this.updateForm()
224 // Force form validation
225 this.forceCheck()
226 },
227
228 err => this.notifier.error(err.message)
229 )
188 } 230 }
189 231
190 isTranscodingEnabled () { 232 isTranscodingEnabled () {
@@ -213,8 +255,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
213 ) 255 )
214 } 256 }
215 257
258 getSelectedLanguageLabel () {
259 return this.i18n('{{\'{0} languages selected')
260 }
261
262 getDefaultLanguageLabel () {
263 return this.i18n('No language')
264 }
265
266 getSelectedCategoryLabel () {
267 return this.i18n('{{\'{0} categories selected')
268 }
269
270 getDefaultCategoryLabel () {
271 return this.i18n('No category')
272 }
273
216 private updateForm () { 274 private updateForm () {
217 this.form.patchValue(this.customConfig) 275 this.form.patchValue(this.customConfig)
218 } 276 }
219
220} 277}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
index 34febc457..76fabb19d 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
@@ -43,7 +43,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
43 newUserRegistration: this.i18n('A new user registered on your instance'), 43 newUserRegistration: this.i18n('A new user registered on your instance'),
44 newFollow: this.i18n('You or your channel(s) has a new follower'), 44 newFollow: this.i18n('You or your channel(s) has a new follower'),
45 commentMention: this.i18n('Someone mentioned you in video comments'), 45 commentMention: this.i18n('Someone mentioned you in video comments'),
46 newInstanceFollower: this.i18n('Your instance has a new follower') 46 newInstanceFollower: this.i18n('Your instance has a new follower'),
47 autoInstanceFollowing: this.i18n('Your instance auto followed another instance')
47 } 48 }
48 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] 49 this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
49 50
@@ -51,7 +52,8 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
51 videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES, 52 videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
52 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST, 53 videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
53 newUserRegistration: UserRight.MANAGE_USERS, 54 newUserRegistration: UserRight.MANAGE_USERS,
54 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW 55 newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
56 autoInstanceFollowing: UserRight.MANAGE_CONFIGURATION
55 } 57 }
56 58
57 this.emailEnabled = this.serverService.getConfig().email.enabled 59 this.emailEnabled = this.serverService.getConfig().email.enabled
diff --git a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
index 2796dd2db..a11238925 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
@@ -1,10 +1,13 @@
1<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form"> 1<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
2 <div class="form-group"> 2 <div class="form-group">
3 <label i18n for="nsfwPolicy">Default policy on videos containing sensitive content</label> 3 <label i18n for="nsfwPolicy">Default policy on videos containing sensitive content</label>
4 <my-help 4 <my-help>
5 helpType="custom" i18n-customHtml 5 <ng-template ptTemplate="customHtml">
6 customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video." 6 <ng-container i18n>
7 ></my-help> 7 With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video.
8 </ng-container>
9 </ng-template>
10 </my-help>
8 11
9 <div class="peertube-select-container"> 12 <div class="peertube-select-container">
10 <select id="nsfwPolicy" formControlName="nsfwPolicy"> 13 <select id="nsfwPolicy" formControlName="nsfwPolicy">
@@ -17,13 +20,15 @@
17 20
18 <div class="form-group"> 21 <div class="form-group">
19 <label i18n for="videoLanguages">Only display videos in the following languages</label> 22 <label i18n for="videoLanguages">Only display videos in the following languages</label>
20 <my-help i18n-customHtml 23 <my-help>
21 customHtml="In Recently added, Trending, Local and Search pages" 24 <ng-template ptTemplate="customHtml">
22 ></my-help> 25 <ng-container i18n>In Recently added, Trending, Local and Search pages</ng-container>
26 </ng-template>
27 </my-help>
23 28
24 <div> 29 <div>
25 <p-multiSelect 30 <p-multiSelect
26 [options]="languageItems" formControlName="videoLanguages" showToggleAll="true" 31 inputId="videoLanguages" [options]="languageItems" formControlName="videoLanguages" showToggleAll="true"
27 [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()" 32 [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()"
28 emptyFilterMessage="No results found" i18n-emptyFilterMessage 33 emptyFilterMessage="No results found" i18n-emptyFilterMessage
29 ></p-multiSelect> 34 ></p-multiSelect>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
index 77febf179..4fb828082 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
@@ -5,9 +5,9 @@ import { AuthService } from '../../../core'
5import { FormReactive, User, UserService } from '../../../shared' 5import { FormReactive, User, UserService } from '../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
8import { Subject } from 'rxjs' 8import { forkJoin, Subject } from 'rxjs'
9import { SelectItem } from 'primeng/api' 9import { SelectItem } from 'primeng/api'
10import { switchMap } from 'rxjs/operators' 10import { first } from 'rxjs/operators'
11 11
12@Component({ 12@Component({
13 selector: 'my-account-video-settings', 13 selector: 'my-account-video-settings',
@@ -39,30 +39,31 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
39 videoLanguages: null 39 videoLanguages: null
40 }) 40 })
41 41
42 this.serverService.videoLanguagesLoaded 42 forkJoin([
43 .pipe(switchMap(() => this.userInformationLoaded)) 43 this.serverService.videoLanguagesLoaded.pipe(first()),
44 .subscribe(() => { 44 this.userInformationLoaded.pipe(first())
45 const languages = this.serverService.getVideoLanguages() 45 ]).subscribe(() => {
46 46 const languages = this.serverService.getVideoLanguages()
47 this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ] 47
48 this.languageItems = this.languageItems 48 this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ]
49 .concat(languages.map(l => ({ label: l.label, value: l.id }))) 49 this.languageItems = this.languageItems
50 50 .concat(languages.map(l => ({ label: l.label, value: l.id })))
51 const videoLanguages = this.user.videoLanguages 51
52 ? this.user.videoLanguages 52 const videoLanguages = this.user.videoLanguages
53 : this.languageItems.map(l => l.value) 53 ? this.user.videoLanguages
54 54 : this.languageItems.map(l => l.value)
55 this.form.patchValue({ 55
56 nsfwPolicy: this.user.nsfwPolicy, 56 this.form.patchValue({
57 webTorrentEnabled: this.user.webTorrentEnabled, 57 nsfwPolicy: this.user.nsfwPolicy,
58 autoPlayVideo: this.user.autoPlayVideo === true, 58 webTorrentEnabled: this.user.webTorrentEnabled,
59 videoLanguages 59 autoPlayVideo: this.user.autoPlayVideo === true,
60 }) 60 videoLanguages
61 }) 61 })
62 })
62 } 63 }
63 64
64 updateDetails () { 65 updateDetails () {
65 const nsfwPolicy = this.form.value['nsfwPolicy'] 66 const nsfwPolicy = this.form.value[ 'nsfwPolicy' ]
66 const webTorrentEnabled = this.form.value['webTorrentEnabled'] 67 const webTorrentEnabled = this.form.value['webTorrentEnabled']
67 const autoPlayVideo = this.form.value['autoPlayVideo'] 68 const autoPlayVideo = this.form.value['autoPlayVideo']
68 69
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 571f46de9..6cf1499d3 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -37,7 +37,6 @@ import {
37} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component' 37} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component'
38import { DragDropModule } from '@angular/cdk/drag-drop' 38import { DragDropModule } from '@angular/cdk/drag-drop'
39import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email' 39import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email'
40import { MultiSelectModule } from 'primeng/multiselect'
41import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' 40import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
42 41
43@NgModule({ 42@NgModule({
@@ -48,8 +47,7 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account
48 SharedModule, 47 SharedModule,
49 TableModule, 48 TableModule,
50 InputSwitchModule, 49 InputSwitchModule,
51 DragDropModule, 50 DragDropModule
52 MultiSelectModule
53 ], 51 ],
54 52
55 declarations: [ 53 declarations: [
diff --git a/client/src/app/+signup/+register/register-step-user.component.html b/client/src/app/+signup/+register/register-step-user.component.html
index 47b3be8cc..4381702ae 100644
--- a/client/src/app/+signup/+register/register-step-user.component.html
+++ b/client/src/app/+signup/+register/register-step-user.component.html
@@ -60,11 +60,16 @@
60 </div> 60 </div>
61 61
62 <div class="form-group form-group-terms"> 62 <div class="form-group form-group-terms">
63 <my-peertube-checkbox 63 <my-peertube-checkbox inputName="terms" formControlName="terms">
64 inputName="terms" formControlName="terms" 64 <ng-template ptTemplate="label">
65 i18n-labelHtml 65 <ng-container i18n>
66 labelHtml="I am at least 16 years old and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance" 66 I am at least 16 years old and agree
67 ></my-peertube-checkbox> 67 to the <a (click)="onTermsClick($event)" href='#'>Terms</a>
68 <ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
69 of this instance
70 </ng-container>
71 </ng-template>
72 </my-peertube-checkbox>
68 73
69 <div *ngIf="formErrors.terms" class="form-error"> 74 <div *ngIf="formErrors.terms" class="form-error">
70 {{ formErrors.terms }} 75 {{ formErrors.terms }}
diff --git a/client/src/app/+signup/+register/register-step-user.component.ts b/client/src/app/+signup/+register/register-step-user.component.ts
index 3b71fd3c4..6c96f20b4 100644
--- a/client/src/app/+signup/+register/register-step-user.component.ts
+++ b/client/src/app/+signup/+register/register-step-user.component.ts
@@ -1,4 +1,4 @@
1import { Component, EventEmitter, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { AuthService } from '@app/core' 2import { AuthService } from '@app/core'
3import { FormReactive, UserService, UserValidatorsService } from '@app/shared' 3import { FormReactive, UserService, UserValidatorsService } from '@app/shared'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
@@ -12,7 +12,11 @@ import { concat, of } from 'rxjs'
12 styleUrls: [ './register.component.scss' ] 12 styleUrls: [ './register.component.scss' ]
13}) 13})
14export class RegisterStepUserComponent extends FormReactive implements OnInit { 14export class RegisterStepUserComponent extends FormReactive implements OnInit {
15 @Input() hasCodeOfConduct = false
16
15 @Output() formBuilt = new EventEmitter<FormGroup>() 17 @Output() formBuilt = new EventEmitter<FormGroup>()
18 @Output() termsClick = new EventEmitter<void>()
19 @Output() codeOfConductClick = new EventEmitter<void>()
16 20
17 constructor ( 21 constructor (
18 protected formValidatorService: FormValidatorService, 22 protected formValidatorService: FormValidatorService,
@@ -45,6 +49,16 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
45 .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue)) 49 .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
46 } 50 }
47 51
52 onTermsClick (event: Event) {
53 event.preventDefault()
54 this.termsClick.emit()
55 }
56
57 onCodeOfConductClick (event: Event) {
58 event.preventDefault()
59 this.codeOfConductClick.emit()
60 }
61
48 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { 62 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
49 const username = this.form.value['username'] || '' 63 const username = this.form.value['username'] || ''
50 64
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html
index e4d647fef..906e29aed 100644
--- a/client/src/app/+signup/+register/register.component.html
+++ b/client/src/app/+signup/+register/register.component.html
@@ -7,11 +7,15 @@
7 <my-signup-success *ngIf="signupDone" [message]="success"></my-signup-success> 7 <my-signup-success *ngIf="signupDone" [message]="success"></my-signup-success>
8 <div *ngIf="info" class="alert alert-info">{{ info }}</div> 8 <div *ngIf="info" class="alert alert-info">{{ info }}</div>
9 9
10 <div class="wrapper" *ngIf="!signupDone"> 10 <div class="wrapper" [hidden]="signupDone">
11 <div> 11 <div class="register-form">
12 <my-custom-stepper linear *ngIf="!signupDone"> 12 <my-custom-stepper linear *ngIf="!signupDone">
13 <cdk-step [stepControl]="formStepUser" i18n-label label="User"> 13 <cdk-step [stepControl]="formStepUser" i18n-label label="User">
14 <my-register-step-user (formBuilt)="onUserFormBuilt($event)"></my-register-step-user> 14 <my-register-step-user
15 [hasCodeOfConduct]="!!aboutHtml.codeOfConduct"
16 (formBuilt)="onUserFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
17 >
18 </my-register-step-user>
15 19
16 <button i18n cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid">Next</button> 20 <button i18n cdkStepperNext [disabled]="!formStepUser || !formStepUser.valid">Next</button>
17 </cdk-step> 21 </cdk-step>
@@ -38,9 +42,56 @@
38 </my-custom-stepper> 42 </my-custom-stepper>
39 </div> 43 </div>
40 44
41 <div> 45 <div class="instance-information">
42 <label i18n>Features found on this instance</label> 46 <ngb-accordion [closeOthers]="true" #accordion="ngbAccordion">
43 <my-instance-features-table></my-instance-features-table> 47 <ngb-panel id="instance-features" i18n-title title="Features found on this instance">
48 <ng-template ngbPanelContent>
49 <my-instance-features-table></my-instance-features-table>
50 </ng-template>
51 </ngb-panel>
52
53 <ng-container *ngIf="about">
54 <ngb-panel
55 *ngIf="aboutHtml.administrator || about.instance.maintenanceLifetime || about.instance.businessModel"
56 id="admin-sustainability" i18n-title title="Administrators & Sustainability"
57 >
58 <ng-template ngbPanelContent>
59 <div class="block">
60 <strong i18n>Who are we?</strong>
61 <div [innerHTML]="aboutHtml.administrator"></div>
62 </div>
63
64 <div class="block">
65 <strong i18n>How long do we plan to maintain this instance?</strong>
66 <div [innerHTML]="about.instance.maintenanceLifetime"></div>
67 </div>
68
69 <div class="block">
70 <strong i18n>How will we pay this instance?</strong>
71 <div [innerHTML]="about.instance.businessModel"></div>
72 </div>
73 </ng-template>
74 </ngb-panel>
75
76 <ngb-panel *ngIf="aboutHtml.moderationInformation" id="moderation-information" i18n-title title="Moderation information">
77 <ng-template ngbPanelContent>
78 <div class="block" [innerHTML]="aboutHtml.moderationInformation"></div>
79 </ng-template>
80 </ngb-panel>
81
82 <ngb-panel *ngIf="aboutHtml.codeOfConduct" id="code-of-conduct" i18n-title title="Code of conduct">
83 <ng-template ngbPanelContent>
84 <div class="block" [innerHTML]="aboutHtml.codeOfConduct"></div>
85 </ng-template>
86 </ngb-panel>
87
88 <ngb-panel *ngIf="aboutHtml.terms" id="terms" i18n-title title="Terms">
89 <ng-template ngbPanelContent>
90 <div class="block" [innerHTML]="aboutHtml.terms"></div>
91 </ng-template>
92 </ngb-panel>
93 </ng-container>
94 </ngb-accordion>
44 </div> 95 </div>
45 </div> 96 </div>
46 97
diff --git a/client/src/app/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss
index 9405b5293..2f62dd59d 100644
--- a/client/src/app/+signup/+register/register.component.scss
+++ b/client/src/app/+signup/+register/register.component.scss
@@ -1,5 +1,9 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3@import "./_bootstrap-variables";
4
5@import '~bootstrap/scss/functions';
6@import '~bootstrap/scss/variables';
3 7
4.alert { 8.alert {
5 font-size: 15px; 9 font-size: 15px;
@@ -13,7 +17,32 @@
13 17
14 & > div { 18 & > div {
15 margin-bottom: 40px; 19 margin-bottom: 40px;
16 width: 450px; 20
21 &.register-form {
22 width: 450px;
23 }
24
25 &.instance-information {
26 width: 600px;
27 margin-bottom: 40px;
28
29 .block {
30 font-size: 15px;
31 margin-bottom: 15px;
32 padding: 0 $btn-padding-x;
33 }
34
35 @media screen and (max-width: 1500px) {
36 width: 450px;
37 }
38
39 ngb-accordion ::ng-deep {
40 .btn {
41 font-weight: $font-semibold !important;
42 color: var(--mainForegroundColor) !important;
43 }
44 }
45 }
17 46
18 @media screen and (max-width: 500px) { 47 @media screen and (max-width: 500px) {
19 width: auto; 48 width: auto;
@@ -21,12 +50,6 @@
21 } 50 }
22} 51}
23 52
24my-instance-features-table {
25 display: block;
26
27 margin-bottom: 40px;
28}
29
30.form-group-terms { 53.form-group-terms {
31 margin: 30px 0; 54 margin: 30px 0;
32} 55}
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts
index cd6059728..d470ef4dc 100644
--- a/client/src/app/+signup/+register/register.component.ts
+++ b/client/src/app/+signup/+register/register.component.ts
@@ -1,21 +1,35 @@
1import { Component } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' 2import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
3import { UserService, UserValidatorsService } from '@app/shared' 3import { UserService, UserValidatorsService } from '@app/shared'
4import { I18n } from '@ngx-translate/i18n-polyfill' 4import { I18n } from '@ngx-translate/i18n-polyfill'
5import { UserRegister } from '@shared/models/users/user-register.model' 5import { UserRegister } from '@shared/models/users/user-register.model'
6import { FormGroup } from '@angular/forms' 6import { FormGroup } from '@angular/forms'
7import { About } from '@shared/models/server'
8import { InstanceService } from '@app/shared/instance/instance.service'
9import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'
7 10
8@Component({ 11@Component({
9 selector: 'my-register', 12 selector: 'my-register',
10 templateUrl: './register.component.html', 13 templateUrl: './register.component.html',
11 styleUrls: [ './register.component.scss' ] 14 styleUrls: [ './register.component.scss' ]
12}) 15})
13export class RegisterComponent { 16export class RegisterComponent implements OnInit {
17 @ViewChild('accordion', { static: true }) accordion: NgbAccordion
18
14 info: string = null 19 info: string = null
15 error: string = null 20 error: string = null
16 success: string = null 21 success: string = null
17 signupDone = false 22 signupDone = false
18 23
24 about: About
25 aboutHtml = {
26 description: '',
27 terms: '',
28 codeOfConduct: '',
29 moderationInformation: '',
30 administrator: ''
31 }
32
19 formStepUser: FormGroup 33 formStepUser: FormGroup
20 formStepChannel: FormGroup 34 formStepChannel: FormGroup
21 35
@@ -26,6 +40,7 @@ export class RegisterComponent {
26 private userService: UserService, 40 private userService: UserService,
27 private serverService: ServerService, 41 private serverService: ServerService,
28 private redirectService: RedirectService, 42 private redirectService: RedirectService,
43 private instanceService: InstanceService,
29 private i18n: I18n 44 private i18n: I18n
30 ) { 45 ) {
31 } 46 }
@@ -34,6 +49,19 @@ export class RegisterComponent {
34 return this.serverService.getConfig().signup.requiresEmailVerification 49 return this.serverService.getConfig().signup.requiresEmailVerification
35 } 50 }
36 51
52 ngOnInit (): void {
53 this.instanceService.getAbout()
54 .subscribe(
55 async about => {
56 this.about = about
57
58 this.aboutHtml = await this.instanceService.buildHtml(about)
59 },
60
61 err => this.notifier.error(err.message)
62 )
63 }
64
37 hasSameChannelAndAccountNames () { 65 hasSameChannelAndAccountNames () {
38 return this.getUsername() === this.getChannelName() 66 return this.getUsername() === this.getChannelName()
39 } 67 }
@@ -58,6 +86,14 @@ export class RegisterComponent {
58 this.formStepChannel = form 86 this.formStepChannel = form
59 } 87 }
60 88
89 onTermsClick () {
90 if (this.accordion) this.accordion.toggle('terms')
91 }
92
93 onCodeOfConductClick () {
94 if (this.accordion) this.accordion.toggle('code-of-conduct')
95 }
96
61 signup () { 97 signup () {
62 this.error = null 98 this.error = null
63 99
diff --git a/client/src/app/+signup/+register/register.module.ts b/client/src/app/+signup/+register/register.module.ts
index 46336cbd0..e55f83990 100644
--- a/client/src/app/+signup/+register/register.module.ts
+++ b/client/src/app/+signup/+register/register.module.ts
@@ -7,13 +7,15 @@ import { RegisterStepChannelComponent } from './register-step-channel.component'
7import { RegisterStepUserComponent } from './register-step-user.component' 7import { RegisterStepUserComponent } from './register-step-user.component'
8import { CustomStepperComponent } from './custom-stepper.component' 8import { CustomStepperComponent } from './custom-stepper.component'
9import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module' 9import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module'
10import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
10 11
11@NgModule({ 12@NgModule({
12 imports: [ 13 imports: [
13 RegisterRoutingModule, 14 RegisterRoutingModule,
14 SharedModule, 15 SharedModule,
15 CdkStepperModule, 16 CdkStepperModule,
16 SignupSharedModule 17 SignupSharedModule,
18 NgbAccordionModule
17 ], 19 ],
18 20
19 declarations: [ 21 declarations: [
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index 07a576083..81b4351c5 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -54,3 +54,8 @@
54 </div> 54 </div>
55 </ng-template> 55 </ng-template>
56</p-toast> 56</p-toast>
57
58<ng-template [ngIf]="isUserLoggedIn()">
59 <my-welcome-modal #welcomeModal></my-welcome-modal>
60 <my-instance-config-warning-modal #instanceConfigWarningModal></my-instance-config-warning-modal>
61</ng-template>
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 64bfb9671..6b18e5feb 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -1,10 +1,10 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { DomSanitizer, SafeHtml } from '@angular/platform-browser' 2import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
3import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router' 3import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router'
4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' 4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
5import { is18nPath } from '../../../shared/models/i18n' 5import { is18nPath } from '../../../shared/models/i18n'
6import { ScreenService } from '@app/shared/misc/screen.service' 6import { ScreenService } from '@app/shared/misc/screen.service'
7import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators' 7import { debounceTime, filter, map, pairwise, skip, switchMap } from 'rxjs/operators'
8import { Hotkey, HotkeysService } from 'angular2-hotkeys' 8import { Hotkey, HotkeysService } from 'angular2-hotkeys'
9import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { fromEvent } from 'rxjs' 10import { fromEvent } from 'rxjs'
@@ -13,6 +13,11 @@ import { PluginService } from '@app/core/plugins/plugin.service'
13import { HooksService } from '@app/core/plugins/hooks.service' 13import { HooksService } from '@app/core/plugins/hooks.service'
14import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 14import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
15import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants' 15import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
16import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
17import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
18import { UserRole } from '@shared/models'
19import { User } from '@app/shared'
20import { InstanceService } from '@app/shared/instance/instance.service'
16 21
17@Component({ 22@Component({
18 selector: 'my-app', 23 selector: 'my-app',
@@ -20,6 +25,9 @@ import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants'
20 styleUrls: [ './app.component.scss' ] 25 styleUrls: [ './app.component.scss' ]
21}) 26})
22export class AppComponent implements OnInit { 27export class AppComponent implements OnInit {
28 @ViewChild('welcomeModal', { static: false }) welcomeModal: WelcomeModalComponent
29 @ViewChild('instanceConfigWarningModal', { static: false }) instanceConfigWarningModal: InstanceConfigWarningModalComponent
30
23 isMenuDisplayed = true 31 isMenuDisplayed = true
24 isMenuChangedByUser = false 32 isMenuChangedByUser = false
25 33
@@ -32,6 +40,7 @@ export class AppComponent implements OnInit {
32 private authService: AuthService, 40 private authService: AuthService,
33 private serverService: ServerService, 41 private serverService: ServerService,
34 private pluginService: PluginService, 42 private pluginService: PluginService,
43 private instanceService: InstanceService,
35 private domSanitizer: DomSanitizer, 44 private domSanitizer: DomSanitizer,
36 private redirectService: RedirectService, 45 private redirectService: RedirectService,
37 private screenService: ScreenService, 46 private screenService: ScreenService,
@@ -96,6 +105,8 @@ export class AppComponent implements OnInit {
96 .subscribe(() => this.onResize()) 105 .subscribe(() => this.onResize())
97 106
98 this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS)) 107 this.location.onPopState(() => this.modalService.dismissAll(POP_STATE_MODAL_DISMISS))
108
109 this.openModalsIfNeeded()
99 } 110 }
100 111
101 isUserLoggedIn () { 112 isUserLoggedIn () {
@@ -220,32 +231,66 @@ export class AppComponent implements OnInit {
220 this.hooks.runAction('action:application.init', 'common') 231 this.hooks.runAction('action:application.init', 'common')
221 } 232 }
222 233
234 private async openModalsIfNeeded () {
235 this.serverService.configLoaded
236 .pipe(
237 switchMap(() => this.authService.userInformationLoaded),
238 map(() => this.authService.getUser()),
239 filter(user => user.role === UserRole.ADMINISTRATOR)
240 ).subscribe(user => setTimeout(() => this.openAdminModals(user))) // setTimeout because of ngIf in template
241 }
242
243 private async openAdminModals (user: User) {
244 if (user.noWelcomeModal !== true) return this.welcomeModal.show()
245
246 const config = this.serverService.getConfig()
247 if (user.noInstanceConfigWarningModal === true || !config.signup.allowed) return
248
249 this.instanceService.getAbout()
250 .subscribe(about => {
251 if (
252 config.instance.name.toLowerCase() === 'peertube' ||
253 !about.instance.terms ||
254 !about.instance.administrator ||
255 !about.instance.maintenanceLifetime
256 ) {
257 this.instanceConfigWarningModal.show(about)
258 }
259 })
260 }
261
223 private initHotkeys () { 262 private initHotkeys () {
224 this.hotkeysService.add([ 263 this.hotkeysService.add([
225 new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => { 264 new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
226 document.getElementById('search-video').focus() 265 document.getElementById('search-video').focus()
227 return false 266 return false
228 }, undefined, this.i18n('Focus the search bar')), 267 }, undefined, this.i18n('Focus the search bar')),
268
229 new Hotkey('b', (event: KeyboardEvent): boolean => { 269 new Hotkey('b', (event: KeyboardEvent): boolean => {
230 this.toggleMenu() 270 this.toggleMenu()
231 return false 271 return false
232 }, undefined, this.i18n('Toggle the left menu')), 272 }, undefined, this.i18n('Toggle the left menu')),
273
233 new Hotkey('g o', (event: KeyboardEvent): boolean => { 274 new Hotkey('g o', (event: KeyboardEvent): boolean => {
234 this.router.navigate([ '/videos/overview' ]) 275 this.router.navigate([ '/videos/overview' ])
235 return false 276 return false
236 }, undefined, this.i18n('Go to the discover videos page')), 277 }, undefined, this.i18n('Go to the discover videos page')),
278
237 new Hotkey('g t', (event: KeyboardEvent): boolean => { 279 new Hotkey('g t', (event: KeyboardEvent): boolean => {
238 this.router.navigate([ '/videos/trending' ]) 280 this.router.navigate([ '/videos/trending' ])
239 return false 281 return false
240 }, undefined, this.i18n('Go to the trending videos page')), 282 }, undefined, this.i18n('Go to the trending videos page')),
283
241 new Hotkey('g r', (event: KeyboardEvent): boolean => { 284 new Hotkey('g r', (event: KeyboardEvent): boolean => {
242 this.router.navigate([ '/videos/recently-added' ]) 285 this.router.navigate([ '/videos/recently-added' ])
243 return false 286 return false
244 }, undefined, this.i18n('Go to the recently added videos page')), 287 }, undefined, this.i18n('Go to the recently added videos page')),
288
245 new Hotkey('g l', (event: KeyboardEvent): boolean => { 289 new Hotkey('g l', (event: KeyboardEvent): boolean => {
246 this.router.navigate([ '/videos/local' ]) 290 this.router.navigate([ '/videos/local' ])
247 return false 291 return false
248 }, undefined, this.i18n('Go to the local videos page')), 292 }, undefined, this.i18n('Go to the local videos page')),
293
249 new Hotkey('g u', (event: KeyboardEvent): boolean => { 294 new Hotkey('g u', (event: KeyboardEvent): boolean => {
250 this.router.navigate([ '/videos/upload' ]) 295 this.router.navigate([ '/videos/upload' ])
251 return false 296 return false
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 1e2936a37..a3ea33ca9 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -18,6 +18,8 @@ import { VideosModule } from './videos'
18import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' 18import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
19import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' 19import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
20import { SearchModule } from '@app/search' 20import { SearchModule } from '@app/search'
21import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
22import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
21 23
22export function metaFactory (serverService: ServerService): MetaLoader { 24export function metaFactory (serverService: ServerService): MetaLoader {
23 return new MetaStaticLoader({ 25 return new MetaStaticLoader({
@@ -39,7 +41,10 @@ export function metaFactory (serverService: ServerService): MetaLoader {
39 MenuComponent, 41 MenuComponent,
40 LanguageChooserComponent, 42 LanguageChooserComponent,
41 AvatarNotificationComponent, 43 AvatarNotificationComponent,
42 HeaderComponent 44 HeaderComponent,
45
46 WelcomeModalComponent,
47 InstanceConfigWarningModalComponent
43 ], 48 ],
44 imports: [ 49 imports: [
45 BrowserModule, 50 BrowserModule,
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html
index 4efe3fb22..683355960 100644
--- a/client/src/app/login/login.component.html
+++ b/client/src/app/login/login.component.html
@@ -23,10 +23,11 @@
23 or create an account on another instance 23 or create an account on another instance
24 </a> 24 </a>
25 25
26 <my-help 26 <my-help *ngIf="signupAllowed === false">
27 *ngIf="signupAllowed === false" helpType="custom" i18n-customHtml 27 <ng-template ptTemplate="customHtml">
28 customHtml="User registration is not allowed on this instance, but you can register on many others!" 28 <ng-container i18n>User registration is not allowed on this instance, but you can register on many others!</ng-container>
29 ></my-help> 29 </ng-template>
30 </my-help>
30 </div> 31 </div>
31 32
32 <div *ngIf="formErrors.username" class="form-error"> 33 <div *ngIf="formErrors.username" class="form-error">
diff --git a/client/src/app/modal/instance-config-warning-modal.component.html b/client/src/app/modal/instance-config-warning-modal.component.html
new file mode 100644
index 000000000..64f14e69b
--- /dev/null
+++ b/client/src/app/modal/instance-config-warning-modal.component.html
@@ -0,0 +1,45 @@
1<ng-template #modal let-hide="close">
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Configuration warning!</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8
9 <p i18n>Hello dear administrator. You enabled user registration on your instance but you did not configure the following fields:</p>
10
11 <ul>
12 <li i18n *ngIf="about.instance.name.toLowerCase() === 'peertube'">Instance name</li>
13 <li i18n *ngIf="isDefaultShortDescription(about.instance.shortDescription)">Instance short description</li>
14
15 <li i18n *ngIf="!about.instance.administrator">Who you are</li>
16 <li i18n *ngIf="!about.instance.maintenanceLifetime">How long you plan to maintain your instance</li>
17 <li i18n *ngIf="!about.instance.businessModel">How you plan to pay your instance</li>
18
19 <li i18n *ngIf="!about.instance.moderationInformation">How you will moderate your instance</li>
20 <li i18n *ngIf="!about.instance.terms">Instance terms</li>
21 </ul>
22
23 <p>
24 Please consider to configure these fields to help people to choose <strong>the appropriate instance</strong>.
25 Without them, your instance may not be referenced on <a target="_blank" rel="noopener noreferrer" href="https://joinpeertube.org">JoinPeerTube website</a>.
26 </p>
27
28 <div class="configure-instance">
29 <a i18n href="/admin/config/edit-custom" target="_blank" rel="noopener noreferrer">Configure these fields</a>
30 </div>
31
32 </div>
33
34 <div class="modal-footer inputs">
35 <my-peertube-checkbox
36 inputName="stopDisplayModal" [(ngModel)]="stopDisplayModal"
37 i18n-labelText labelText="Don't show me this warning anymore"
38 >
39
40 </my-peertube-checkbox>
41
42 <span i18n class="action-button action-button-cancel" (click)="hide()">Close</span>
43 </div>
44
45</ng-template>
diff --git a/client/src/app/modal/instance-config-warning-modal.component.scss b/client/src/app/modal/instance-config-warning-modal.component.scss
new file mode 100644
index 000000000..ff62a1b9c
--- /dev/null
+++ b/client/src/app/modal/instance-config-warning-modal.component.scss
@@ -0,0 +1,22 @@
1@import '_mixins';
2@import '_variables';
3
4.action-button-cancel {
5 margin-right: 0 !important;
6}
7
8.modal-body {
9 font-size: 15px;
10}
11
12li {
13 margin-bottom: 10px;
14}
15
16.configure-instance {
17 text-align: center;
18 font-weight: 600;
19 font-size: 18px;
20 margin-top: 40px;
21 margin-bottom: 10px;
22}
diff --git a/client/src/app/modal/instance-config-warning-modal.component.ts b/client/src/app/modal/instance-config-warning-modal.component.ts
new file mode 100644
index 000000000..742a7dd41
--- /dev/null
+++ b/client/src/app/modal/instance-config-warning-modal.component.ts
@@ -0,0 +1,47 @@
1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { About } from '@shared/models/server'
5import { UserService } from '@app/shared'
6
7@Component({
8 selector: 'my-instance-config-warning-modal',
9 templateUrl: './instance-config-warning-modal.component.html',
10 styleUrls: [ './instance-config-warning-modal.component.scss' ]
11})
12export class InstanceConfigWarningModalComponent {
13 @ViewChild('modal', { static: true }) modal: ElementRef
14
15 stopDisplayModal = false
16 about: About
17
18 constructor (
19 private userService: UserService,
20 private modalService: NgbModal,
21 private notifier: Notifier
22 ) { }
23
24 show (about: About) {
25 this.about = about
26
27 const ref = this.modalService.open(this.modal)
28
29 ref.result.finally(() => {
30 if (this.stopDisplayModal === true) this.doNotOpenAgain()
31 })
32 }
33
34 isDefaultShortDescription (description: string) {
35 return description === 'PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly ' +
36 'in the web browser with WebTorrent and Angular.'
37 }
38
39 private doNotOpenAgain () {
40 this.userService.updateMyProfile({ noInstanceConfigWarningModal: true })
41 .subscribe(
42 () => console.log('We will not open the instance config warning modal again.'),
43
44 err => this.notifier.error(err.message)
45 )
46 }
47}
diff --git a/client/src/app/modal/welcome-modal.component.html b/client/src/app/modal/welcome-modal.component.html
new file mode 100644
index 000000000..09ff2163b
--- /dev/null
+++ b/client/src/app/modal/welcome-modal.component.html
@@ -0,0 +1,67 @@
1<ng-template #modal let-hide="close">
2 <div class="modal-header">
3 <h4 i18n class="modal-title">Welcome on PeerTube dear administrator!</h4>
4 <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
5 </div>
6
7 <div class="modal-body">
8
9 <div class="block-documentation">
10 <div i18n class="subtitle">Documentation</div>
11
12 <div class="columns">
13 <a class="link-block" href="https://docs.joinpeertube.org/#/maintain-tools" target="_blank" rel="noopener noreferrer">
14 <a class="link-title" href="https://docs.joinpeertube.org/#/maintain-tools" target="_blank" rel="noopener noreferrer">CLI</a>
15
16 <div>Upload or import videos, parse logs, prune storage directories, reset user password...</div>
17 </a>
18
19 <a class="link-block" href="https://docs.joinpeertube.org/#/admin-following-instances" target="_blank" rel="noopener noreferrer">
20 <a class="link-title" href="https://docs.joinpeertube.org/#/admin-following-instances" target="_blank" rel="noopener noreferrer">Administer</a>
21
22 <div>Managing users, following other instances, dealing with spammers...</div>
23 </a>
24
25 <a class="link-block" href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">
26 <a class="link-title" href="https://docs.joinpeertube.org/#/use-setup-account" target="_blank" rel="noopener noreferrer">Use</a>
27
28 <div>Setup your account, managing video playlists, discover third-party applications...</div>
29 </a>
30 </div>
31 </div>
32
33 <div class="block-configuration">
34 <div i18n class="subtitle">It's time to configure your instance!</div>
35
36 <p i18n>
37 Choosing your <strong>instance name</strong>, <strong>setting up a description</strong>, specifying <strong>who you are</strong>,
38 why <strong>you created your instance</strong> and <strong>how long</strong> you plan to <strong>maintain your it</strong>
39 is very important for visitors to understand on what type of instance they are.
40 </p>
41
42 <p i18n>
43 If you want to open registrations, please decide what are <strong>your moderation rules</strong>, fill your <strong>instance terms</strong>
44 and specify the categories and languages you speak. This way, you will help users to register on <strong>the appropriate</strong> PeerTube instance.
45 </p>
46
47 <div class="configure-instance">
48 <a i18n href="/admin/config/edit-custom" target="_blank" rel="noopener noreferrer">Configure your instance</a>
49 </div>
50 </div>
51
52 <div class="block-links">
53 <div i18n class="subtitle">Useful links</div>
54
55 <ul>
56 <li>Official PeerTube website (news, support, contribute...): <a href="https://joinpeertube.org" target="_blank" rel="noopener noreferrer">https://joinpeertube.org</a></li>
57
58 <li>Put your instance on the public PeerTube index: <a href="https://instances.joinpeertube.org/instances">https://instances.joinpeertube.org/instances</a></li>
59 </ul>
60 </div>
61 </div>
62
63 <div class="modal-footer inputs">
64 <span i18n class="action-button action-button-submit" (click)="hide()">Understood!</span>
65 </div>
66
67</ng-template>
diff --git a/client/src/app/modal/welcome-modal.component.scss b/client/src/app/modal/welcome-modal.component.scss
new file mode 100644
index 000000000..8bb6973f4
--- /dev/null
+++ b/client/src/app/modal/welcome-modal.component.scss
@@ -0,0 +1,56 @@
1@import '_mixins';
2@import '_variables';
3
4.modal-body {
5 font-size: 15px;
6}
7
8.subtitle {
9 font-weight: $font-semibold;
10 margin-bottom: 10px;
11 font-size: 16px;
12}
13
14.block-documentation .subtitle {
15 margin-bottom: 20px;
16}
17
18.block-configuration,
19.block-instance {
20 margin-top: 30px;
21}
22
23li {
24 margin-bottom: 10px;
25}
26
27.configure-instance {
28 text-align: center;
29 font-weight: 600;
30 font-size: 18px;
31 margin: 20px 0 40px 0;
32}
33
34.columns {
35 display: flex;
36
37 .link-block {
38 @include disable-default-a-behaviour;
39
40 color: var(--mainForegroundColor);
41 padding: 10px;
42 transition: background-color 0.2s ease-in;
43
44 &:hover {
45 background-color: rgba(0, 0, 0, 0.05);
46 }
47
48 .link-title {
49 font-size: 16px;
50 font-weight: $font-semibold;
51 display: flex;
52 justify-content: center;
53 margin-bottom: 5px;
54 }
55 }
56}
diff --git a/client/src/app/modal/welcome-modal.component.ts b/client/src/app/modal/welcome-modal.component.ts
new file mode 100644
index 000000000..05412a4cd
--- /dev/null
+++ b/client/src/app/modal/welcome-modal.component.ts
@@ -0,0 +1,38 @@
1import { Component, ElementRef, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { UserService } from '@app/shared'
5
6@Component({
7 selector: 'my-welcome-modal',
8 templateUrl: './welcome-modal.component.html',
9 styleUrls: [ './welcome-modal.component.scss' ]
10})
11export class WelcomeModalComponent {
12 @ViewChild('modal', { static: true }) modal: ElementRef
13
14 constructor (
15 private userService: UserService,
16 private modalService: NgbModal,
17 private notifier: Notifier
18 ) { }
19
20 show () {
21 const ref = this.modalService.open(this.modal,{
22 backdrop: 'static',
23 keyboard: false,
24 size: 'lg'
25 })
26
27 ref.result.finally(() => this.doNotOpenAgain())
28 }
29
30 private doNotOpenAgain () {
31 this.userService.updateMyProfile({ noWelcomeModal: true })
32 .subscribe(
33 () => console.log('We will not open the welcome modal again.'),
34
35 err => this.notifier.error(err.message)
36 )
37 }
38}
diff --git a/client/src/app/shared/angular/peertube-template.directive.ts b/client/src/app/shared/angular/peertube-template.directive.ts
index a514b6057..e04c25d9a 100644
--- a/client/src/app/shared/angular/peertube-template.directive.ts
+++ b/client/src/app/shared/angular/peertube-template.directive.ts
@@ -3,8 +3,8 @@ import { Directive, Input, TemplateRef } from '@angular/core'
3@Directive({ 3@Directive({
4 selector: '[ptTemplate]' 4 selector: '[ptTemplate]'
5}) 5})
6export class PeerTubeTemplateDirective { 6export class PeerTubeTemplateDirective <T extends string> {
7 @Input('ptTemplate') name: string 7 @Input('ptTemplate') name: T
8 8
9 constructor (public template: TemplateRef<any>) { 9 constructor (public template: TemplateRef<any>) {
10 // empty 10 // empty
diff --git a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
index 882e39453..767e3f026 100644
--- a/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/custom-config-validators.service.ts
@@ -13,6 +13,7 @@ export class CustomConfigValidatorsService {
13 readonly SIGNUP_LIMIT: BuildFormValidator 13 readonly SIGNUP_LIMIT: BuildFormValidator
14 readonly ADMIN_EMAIL: BuildFormValidator 14 readonly ADMIN_EMAIL: BuildFormValidator
15 readonly TRANSCODING_THREADS: BuildFormValidator 15 readonly TRANSCODING_THREADS: BuildFormValidator
16 readonly INDEX_URL: BuildFormValidator
16 17
17 constructor (private i18n: I18n) { 18 constructor (private i18n: I18n) {
18 this.INSTANCE_NAME = { 19 this.INSTANCE_NAME = {
@@ -78,5 +79,13 @@ export class CustomConfigValidatorsService {
78 'min': this.i18n('Transcoding threads must be greater or equal to 0.') 79 'min': this.i18n('Transcoding threads must be greater or equal to 0.')
79 } 80 }
80 } 81 }
82
83 this.INDEX_URL = {
84 VALIDATORS: [ Validators.required, Validators.pattern(/^https:\/\//) ],
85 MESSAGES: {
86 'required': this.i18n('Index URL is required.'),
87 'pattern': this.i18n('Index URL should be a URL')
88 }
89 }
81 } 90 }
82} 91}
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.html b/client/src/app/shared/forms/peertube-checkbox.component.html
index 571a1a673..f1e3bf0bf 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.html
+++ b/client/src/app/shared/forms/peertube-checkbox.component.html
@@ -3,8 +3,15 @@
3 <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="disabled" /> 3 <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="disabled" />
4 <span role="checkbox" [attr.aria-checked]="checked"></span> 4 <span role="checkbox" [attr.aria-checked]="checked"></span>
5 <span *ngIf="labelText">{{ labelText }}</span> 5 <span *ngIf="labelText">{{ labelText }}</span>
6 <span *ngIf="labelHtml" [innerHTML]="labelHtml"></span> 6
7 <span *ngIf="labelTemplate">
8 <ng-container *ngTemplateOutlet="labelTemplate"></ng-container>
9 </span>
7 </label> 10 </label>
8 11
9 <my-help *ngIf="helpHtml" [tooltipPlacement]="helpPlacement" helpType="custom" i18n-customHtml [customHtml]="helpHtml"></my-help> 12 <my-help *ngIf="helpTemplate" [tooltipPlacement]="helpPlacement" helpType="custom">
13 <ng-template ptTemplate="customHtml">
14 <ng-template *ngTemplateOutlet="helpTemplate"></ng-template>
15 </ng-template>
16 </my-help>
10</div> 17</div>
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.scss b/client/src/app/shared/forms/peertube-checkbox.component.scss
index 84ea788af..51f98b0bc 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.scss
+++ b/client/src/app/shared/forms/peertube-checkbox.component.scss
@@ -7,7 +7,7 @@
7 .form-group-checkbox { 7 .form-group-checkbox {
8 display: flex; 8 display: flex;
9 9
10 span { 10 .label-text {
11 font-weight: $font-regular; 11 font-weight: $font-regular;
12 margin: 0; 12 margin: 0;
13 } 13 }
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.ts b/client/src/app/shared/forms/peertube-checkbox.component.ts
index a4b72aa37..3b8f39ed0 100644
--- a/client/src/app/shared/forms/peertube-checkbox.component.ts
+++ b/client/src/app/shared/forms/peertube-checkbox.component.ts
@@ -1,5 +1,6 @@
1import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core' 1import { AfterContentInit, ChangeDetectorRef, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
3 4
4@Component({ 5@Component({
5 selector: 'my-peertube-checkbox', 6 selector: 'my-peertube-checkbox',
@@ -13,20 +14,35 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
13 } 14 }
14 ] 15 ]
15}) 16})
16export class PeertubeCheckboxComponent implements ControlValueAccessor { 17export class PeertubeCheckboxComponent implements ControlValueAccessor, AfterContentInit {
17 @Input() checked = false 18 @Input() checked = false
18 @Input() inputName: string 19 @Input() inputName: string
19 @Input() labelText: string 20 @Input() labelText: string
20 @Input() labelHtml: string
21 @Input() helpHtml: string
22 @Input() helpPlacement = 'top' 21 @Input() helpPlacement = 'top'
23 @Input() disabled = false 22 @Input() disabled = false
24 23
24 @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'label' | 'help'>>
25
25 // FIXME: https://github.com/angular/angular/issues/10816#issuecomment-307567836 26 // FIXME: https://github.com/angular/angular/issues/10816#issuecomment-307567836
26 @Input() onPushWorkaround = false 27 @Input() onPushWorkaround = false
27 28
29 labelTemplate: TemplateRef<any>
30 helpTemplate: TemplateRef<any>
31
28 constructor (private cdr: ChangeDetectorRef) { } 32 constructor (private cdr: ChangeDetectorRef) { }
29 33
34 ngAfterContentInit () {
35 {
36 const t = this.templates.find(t => t.name === 'label')
37 if (t) this.labelTemplate = t.template
38 }
39
40 {
41 const t = this.templates.find(t => t.name === 'help')
42 if (t) this.helpTemplate = t.template
43 }
44 }
45
30 propagateChange = (_: any) => { /* empty */ } 46 propagateChange = (_: any) => { /* empty */ }
31 47
32 writeValue (checked: boolean) { 48 writeValue (checked: boolean) {
diff --git a/client/src/app/shared/instance/feature-boolean.component.html b/client/src/app/shared/instance/feature-boolean.component.html
new file mode 100644
index 000000000..ac208fc13
--- /dev/null
+++ b/client/src/app/shared/instance/feature-boolean.component.html
@@ -0,0 +1,3 @@
1<span *ngIf="value === true" class="glyphicon glyphicon-ok"></span>
2<span *ngIf="value === false" class="glyphicon glyphicon-remove"></span>
3
diff --git a/client/src/app/shared/instance/feature-boolean.component.scss b/client/src/app/shared/instance/feature-boolean.component.scss
new file mode 100644
index 000000000..56d08af06
--- /dev/null
+++ b/client/src/app/shared/instance/feature-boolean.component.scss
@@ -0,0 +1,10 @@
1@import '_variables';
2@import '_mixins';
3
4.glyphicon-ok {
5 color: $green;
6}
7
8.glyphicon-remove {
9 color: $red;
10}
diff --git a/client/src/app/shared/instance/feature-boolean.component.ts b/client/src/app/shared/instance/feature-boolean.component.ts
new file mode 100644
index 000000000..d02d513d6
--- /dev/null
+++ b/client/src/app/shared/instance/feature-boolean.component.ts
@@ -0,0 +1,10 @@
1import { Component, Input } from '@angular/core'
2
3@Component({
4 selector: 'my-feature-boolean',
5 templateUrl: './feature-boolean.component.html',
6 styleUrls: [ './feature-boolean.component.scss' ]
7})
8export class FeatureBooleanComponent {
9 @Input() value: boolean
10}
diff --git a/client/src/app/shared/instance/instance-features-table.component.html b/client/src/app/shared/instance/instance-features-table.component.html
index 2987bd00e..d1cb8fcbe 100644
--- a/client/src/app/shared/instance/instance-features-table.component.html
+++ b/client/src/app/shared/instance/instance-features-table.component.html
@@ -1,28 +1,53 @@
1<div class="feature-table"> 1<div class="feature-table">
2 2
3 <table class="table"> 3 <table class="table" *ngIf="config">
4 <tr> 4 <tr>
5 <td i18n class="label">Default NSFW/sensitive videos policy (can be redefined by the users)</td> 5 <td i18n class="label">
6 <div>Default NSFW/sensitive videos policy</div>
7 <div class="more-info">can be redefined by the users</div>
8 </td>
6 9
7 <td class="value">{{ buildNSFWLabel() }}</td> 10 <td class="value">{{ buildNSFWLabel() }}</td>
8 </tr> 11 </tr>
9 12
10 <tr *ngFor="let feature of features"> 13 <tr>
11 <td class="label">{{ feature.label }}</td> 14 <td i18n class="label">User registration allowed</td>
12 <td> 15 <td>
13 <span *ngIf="feature.value === true" class="glyphicon glyphicon-ok"></span> 16 <my-feature-boolean [value]="config.signup.allowed"></my-feature-boolean>
14 <span *ngIf="feature.value === false" class="glyphicon glyphicon-remove"></span>
15 </td> 17 </td>
16 </tr> 18 </tr>
17 19
18 <tr> 20 <tr>
19 <td i18n class="label">Video quota</td> 21 <td i18n class="label" colspan="2">Video uploads</td>
22 </tr>
23
24 <tr>
25 <td i18n class="sub-label">Transcoding in multiple resolutions</td>
26 <td>
27 <my-feature-boolean [value]="config.transcoding.enabledResolutions.length !== 0"></my-feature-boolean>
28 </td>
29 </tr>
30
31 <tr>
32 <td i18n class="sub-label">Video uploads</td>
33 <td>
34 <span *ngIf="config.autoBlacklist.videos.ofUsers.enabled">Requires manual validation by moderators</span>
35 <span *ngIf="!config.autoBlacklist.videos.ofUsers.enabled">Automatically published</span>
36 </td>
37 </tr>
38
39 <tr>
40 <td i18n class="sub-label">Video quota</td>
20 41
21 <td class="value"> 42 <td class="value">
22 <ng-container *ngIf="initialUserVideoQuota !== -1"> 43 <ng-container *ngIf="initialUserVideoQuota !== -1">
23 {{ initialUserVideoQuota | bytes: 0 }} <ng-container *ngIf="dailyUserVideoQuota !== -1">({{ dailyUserVideoQuota | bytes: 0 }} per day)</ng-container> 44 {{ initialUserVideoQuota | bytes: 0 }} <ng-container *ngIf="dailyUserVideoQuota !== -1">({{ dailyUserVideoQuota | bytes: 0 }} per day)</ng-container>
24 45
25 <my-help tooltipPlacement="auto" helpType="custom" [customHtml]="quotaHelpIndication"></my-help> 46 <my-help tooltipPlacement="auto" helpType="custom">
47 <ng-template ptTemplate="customHtml">
48 <div [innerHTML]="quotaHelpIndication"></div>
49 </ng-template>
50 </my-help>
26 </ng-container> 51 </ng-container>
27 52
28 <ng-container i18n *ngIf="initialUserVideoQuota === -1"> 53 <ng-container i18n *ngIf="initialUserVideoQuota === -1">
@@ -30,5 +55,35 @@
30 </ng-container> 55 </ng-container>
31 </td> 56 </td>
32 </tr> 57 </tr>
58
59 <tr>
60 <td i18n class="label" colspan="2">Import</td>
61 </tr>
62
63 <tr>
64 <td i18n class="sub-label">HTTP import (YouTube, Vimeo, direct URL...)</td>
65 <td>
66 <my-feature-boolean [value]="config.import.videos.http.enabled"></my-feature-boolean>
67 </td>
68 </tr>
69
70 <tr>
71 <td i18n class="sub-label">Torrent import</td>
72 <td>
73 <my-feature-boolean [value]="config.import.videos.torrent.enabled"></my-feature-boolean>
74 </td>
75 </tr>
76
77
78 <tr>
79 <td i18n class="label" colspan="2">Player</td>
80 </tr>
81
82 <tr>
83 <td i18n class="sub-label">P2P enabled</td>
84 <td>
85 <my-feature-boolean [value]="config.tracker.enabled"></my-feature-boolean>
86 </td>
87 </tr>
33 </table> 88 </table>
34</div> 89</div>
diff --git a/client/src/app/shared/instance/instance-features-table.component.scss b/client/src/app/shared/instance/instance-features-table.component.scss
index f9bec038d..67f2b6c84 100644
--- a/client/src/app/shared/instance/instance-features-table.component.scss
+++ b/client/src/app/shared/instance/instance-features-table.component.scss
@@ -5,16 +5,28 @@ table {
5 font-size: 14px; 5 font-size: 14px;
6 color: var(--mainForegroundColor); 6 color: var(--mainForegroundColor);
7 7
8 .label { 8 .label,
9 font-weight: $font-semibold; 9 .sub-label {
10 min-width: 330px; 10 min-width: 330px;
11 }
12 11
13 .glyphicon-ok { 12 &.label {
14 color: $green; 13 font-weight: $font-semibold;
14 }
15
16 &.sub-label {
17 padding-left: 30px;
18 }
19
20 .more-info {
21 font-style: italic;
22 font-weight: initial;
23 font-size: 14px
24 }
15 } 25 }
16 26
17 .glyphicon-remove { 27 td {
18 color: $red; 28 vertical-align: middle;
19 } 29 }
20} 30}
31
32
diff --git a/client/src/app/shared/instance/instance-features-table.component.ts b/client/src/app/shared/instance/instance-features-table.component.ts
index a53082a93..46df4d0b2 100644
--- a/client/src/app/shared/instance/instance-features-table.component.ts
+++ b/client/src/app/shared/instance/instance-features-table.component.ts
@@ -1,6 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { ServerConfig } from '@shared/models'
4 5
5@Component({ 6@Component({
6 selector: 'my-instance-features-table', 7 selector: 'my-instance-features-table',
@@ -8,8 +9,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
8 styleUrls: [ './instance-features-table.component.scss' ] 9 styleUrls: [ './instance-features-table.component.scss' ]
9}) 10})
10export class InstanceFeaturesTableComponent implements OnInit { 11export class InstanceFeaturesTableComponent implements OnInit {
11 features: { label: string, value?: boolean }[] = []
12 quotaHelpIndication = '' 12 quotaHelpIndication = ''
13 config: ServerConfig
13 14
14 constructor ( 15 constructor (
15 private i18n: I18n, 16 private i18n: I18n,
@@ -28,7 +29,7 @@ export class InstanceFeaturesTableComponent implements OnInit {
28 ngOnInit () { 29 ngOnInit () {
29 this.serverService.configLoaded 30 this.serverService.configLoaded
30 .subscribe(() => { 31 .subscribe(() => {
31 this.buildFeatures() 32 this.config = this.serverService.getConfig()
32 this.buildQuotaHelpIndication() 33 this.buildQuotaHelpIndication()
33 }) 34 })
34 } 35 }
@@ -41,37 +42,6 @@ export class InstanceFeaturesTableComponent implements OnInit {
41 if (policy === 'display') return this.i18n('Displayed') 42 if (policy === 'display') return this.i18n('Displayed')
42 } 43 }
43 44
44 private buildFeatures () {
45 const config = this.serverService.getConfig()
46
47 this.features = [
48 {
49 label: this.i18n('User registration allowed'),
50 value: config.signup.allowed
51 },
52 {
53 label: this.i18n('Video uploads require manual validation by moderators'),
54 value: config.autoBlacklist.videos.ofUsers.enabled
55 },
56 {
57 label: this.i18n('Transcode your videos in multiple resolutions'),
58 value: config.transcoding.enabledResolutions.length !== 0
59 },
60 {
61 label: this.i18n('HTTP import (YouTube, Vimeo, direct URL...)'),
62 value: config.import.videos.http.enabled
63 },
64 {
65 label: this.i18n('Torrent import'),
66 value: config.import.videos.torrent.enabled
67 },
68 {
69 label: this.i18n('P2P enabled'),
70 value: config.tracker.enabled
71 }
72 ]
73 }
74
75 private getApproximateTime (seconds: number) { 45 private getApproximateTime (seconds: number) {
76 const hours = Math.floor(seconds / 3600) 46 const hours = Math.floor(seconds / 3600)
77 let pluralSuffix = '' 47 let pluralSuffix = ''
diff --git a/client/src/app/shared/instance/instance.service.ts b/client/src/app/shared/instance/instance.service.ts
index d0c96941d..44b413fa4 100644
--- a/client/src/app/shared/instance/instance.service.ts
+++ b/client/src/app/shared/instance/instance.service.ts
@@ -4,6 +4,9 @@ import { Injectable } from '@angular/core'
4import { environment } from '../../../environments/environment' 4import { environment } from '../../../environments/environment'
5import { RestExtractor, RestService } from '../rest' 5import { RestExtractor, RestService } from '../rest'
6import { About } from '../../../../../shared/models/server' 6import { About } from '../../../../../shared/models/server'
7import { MarkdownService } from '@app/shared/renderer'
8import { peertubeTranslate } from '@shared/models'
9import { ServerService } from '@app/core'
7 10
8@Injectable() 11@Injectable()
9export class InstanceService { 12export class InstanceService {
@@ -13,7 +16,9 @@ export class InstanceService {
13 constructor ( 16 constructor (
14 private authHttp: HttpClient, 17 private authHttp: HttpClient,
15 private restService: RestService, 18 private restService: RestService,
16 private restExtractor: RestExtractor 19 private restExtractor: RestExtractor,
20 private markdownService: MarkdownService,
21 private serverService: ServerService
17 ) { 22 ) {
18 } 23 }
19 24
@@ -34,4 +39,43 @@ export class InstanceService {
34 .pipe(catchError(res => this.restExtractor.handleError(res))) 39 .pipe(catchError(res => this.restExtractor.handleError(res)))
35 40
36 } 41 }
42
43 async buildHtml (about: About) {
44 const html = {
45 description: '',
46 terms: '',
47 codeOfConduct: '',
48 moderationInformation: '',
49 administrator: '',
50 hardwareInformation: ''
51 }
52
53 for (const key of Object.keys(html)) {
54 html[ key ] = await this.markdownService.textMarkdownToHTML(about.instance[ key ])
55 }
56
57 return html
58 }
59
60 buildTranslatedLanguages (about: About, translations: any) {
61 const languagesArray = this.serverService.getVideoLanguages()
62
63 return about.instance.languages
64 .map(l => {
65 const languageObj = languagesArray.find(la => la.id === l)
66
67 return peertubeTranslate(languageObj.label, translations)
68 })
69 }
70
71 buildTranslatedCategories (about: About, translations: any) {
72 const categoriesArray = this.serverService.getVideoCategories()
73
74 return about.instance.categories
75 .map(c => {
76 const categoryObj = categoriesArray.find(ca => ca.id === c)
77
78 return peertubeTranslate(categoryObj.label, translations)
79 })
80 }
37} 81}
diff --git a/client/src/app/shared/misc/help.component.html b/client/src/app/shared/misc/help.component.html
index e31eef06a..9a6d3e48e 100644
--- a/client/src/app/shared/misc/help.component.html
+++ b/client/src/app/shared/misc/help.component.html
@@ -1,15 +1,25 @@
1<ng-template #tooltipTemplate> 1<ng-template #tooltipTemplate>
2 <ng-template [ngIf]="preHtml"> 2 <p *ngIf="preHtmlTemplate">
3 <p [innerHTML]="preHtml"></p> 3 <ng-template *ngTemplateOutlet="preHtmlTemplate"></ng-template>
4 <br /> 4 </p>
5 </ng-template>
6 5
7 <p [innerHTML]="mainHtml"></p> 6 <ng-container *ngIf="preHtmlTemplate && (customHtmlTemplate || mainHtml || postHtmlTemplate)">
7 <br /><br />
8 </ng-container>
8 9
9 <ng-template [ngIf]="postHtml"> 10 <p *ngIf="customHtmlTemplate">
10 <br /> 11 <ng-template *ngTemplateOutlet="customHtmlTemplate"></ng-template>
11 <p [innerHTML]="postHtml"></p> 12 </p>
12 </ng-template> 13
14 <p *ngIf="mainHtml" [innerHTML]="mainHtml"></p>
15
16 <ng-container *ngIf="(customHtmlTemplate || mainHtml) && postHtmlTemplate">
17 <br /><br />
18 </ng-container>
19
20 <p *ngIf="postHtmlTemplate">
21 <ng-template *ngTemplateOutlet="postHtmlTemplate"></ng-template>
22 </p>
13</ng-template> 23</ng-template>
14 24
15<span 25<span
diff --git a/client/src/app/shared/misc/help.component.ts b/client/src/app/shared/misc/help.component.ts
index f3426f70f..18ba8ad5e 100644
--- a/client/src/app/shared/misc/help.component.ts
+++ b/client/src/app/shared/misc/help.component.ts
@@ -1,6 +1,7 @@
1import { Component, Input, OnChanges, OnInit } from '@angular/core' 1import { AfterContentInit, Component, ContentChildren, Input, OnChanges, OnInit, QueryList, TemplateRef } from '@angular/core'
2import { I18n } from '@ngx-translate/i18n-polyfill' 2import { I18n } from '@ngx-translate/i18n-polyfill'
3import { MarkdownService } from '@app/shared/renderer' 3import { MarkdownService } from '@app/shared/renderer'
4import { PeerTubeTemplateDirective } from '@app/shared/angular/peertube-template.directive'
4 5
5@Component({ 6@Component({
6 selector: 'my-help', 7 selector: 'my-help',
@@ -8,22 +9,42 @@ import { MarkdownService } from '@app/shared/renderer'
8 templateUrl: './help.component.html' 9 templateUrl: './help.component.html'
9}) 10})
10 11
11export class HelpComponent implements OnInit, OnChanges { 12export class HelpComponent implements OnInit, OnChanges, AfterContentInit {
12 @Input() preHtml = ''
13 @Input() postHtml = ''
14 @Input() customHtml = ''
15 @Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom' 13 @Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom'
16 @Input() tooltipPlacement = 'right' 14 @Input() tooltipPlacement = 'right'
17 15
16 @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'preHtml' | 'customHtml' | 'postHtml'>>
17
18 isPopoverOpened = false 18 isPopoverOpened = false
19 mainHtml = '' 19 mainHtml = ''
20 20
21 preHtmlTemplate: TemplateRef<any>
22 customHtmlTemplate: TemplateRef<any>
23 postHtmlTemplate: TemplateRef<any>
24
21 constructor (private i18n: I18n) { } 25 constructor (private i18n: I18n) { }
22 26
23 ngOnInit () { 27 ngOnInit () {
24 this.init() 28 this.init()
25 } 29 }
26 30
31 ngAfterContentInit () {
32 {
33 const t = this.templates.find(t => t.name === 'preHtml')
34 if (t) this.preHtmlTemplate = t.template
35 }
36
37 {
38 const t = this.templates.find(t => t.name === 'customHtml')
39 if (t) this.customHtmlTemplate = t.template
40 }
41
42 {
43 const t = this.templates.find(t => t.name === 'postHtml')
44 if (t) this.postHtmlTemplate = t.template
45 }
46 }
47
27 ngOnChanges () { 48 ngOnChanges () {
28 this.init() 49 this.init()
29 } 50 }
@@ -37,11 +58,6 @@ export class HelpComponent implements OnInit, OnChanges {
37 } 58 }
38 59
39 private init () { 60 private init () {
40 if (this.helpType === 'custom') {
41 this.mainHtml = this.customHtml
42 return
43 }
44
45 if (this.helpType === 'markdownText') { 61 if (this.helpType === 'markdownText') {
46 this.mainHtml = this.formatMarkdownSupport(MarkdownService.TEXT_RULES) 62 this.mainHtml = this.formatMarkdownSupport(MarkdownService.TEXT_RULES)
47 return 63 return
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index eb57a2fff..65e0f21a4 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -6,10 +6,8 @@ import { RouterModule } from '@angular/router'
6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' 6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
7import { HelpComponent } from '@app/shared/misc/help.component' 7import { HelpComponent } from '@app/shared/misc/help.component'
8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' 8import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
9
10import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' 9import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
11import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' 10import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
12
13import { AUTH_INTERCEPTOR_PROVIDER } from './auth' 11import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
14import { ButtonComponent } from './buttons/button.component' 12import { ButtonComponent } from './buttons/button.component'
15import { DeleteButtonComponent } from './buttons/delete-button.component' 13import { DeleteButtonComponent } from './buttons/delete-button.component'
@@ -93,6 +91,8 @@ import { VideoDownloadComponent } from '@app/shared/video/modals/video-download.
93import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' 91import { VideoReportComponent } from '@app/shared/video/modals/video-report.component'
94import { ClipboardModule } from 'ngx-clipboard' 92import { ClipboardModule } from 'ngx-clipboard'
95import { FollowService } from '@app/shared/instance/follow.service' 93import { FollowService } from '@app/shared/instance/follow.service'
94import { MultiSelectModule } from 'primeng/multiselect'
95import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.component'
96 96
97@NgModule({ 97@NgModule({
98 imports: [ 98 imports: [
@@ -113,7 +113,8 @@ import { FollowService } from '@app/shared/instance/follow.service'
113 113
114 PrimeSharedModule, 114 PrimeSharedModule,
115 InputMaskModule, 115 InputMaskModule,
116 NgPipesModule 116 NgPipesModule,
117 MultiSelectModule
117 ], 118 ],
118 119
119 declarations: [ 120 declarations: [
@@ -156,6 +157,7 @@ import { FollowService } from '@app/shared/instance/follow.service'
156 SubscribeButtonComponent, 157 SubscribeButtonComponent,
157 RemoteSubscribeComponent, 158 RemoteSubscribeComponent,
158 InstanceFeaturesTableComponent, 159 InstanceFeaturesTableComponent,
160 FeatureBooleanComponent,
159 UserBanModalComponent, 161 UserBanModalComponent,
160 UserModerationDropdownComponent, 162 UserModerationDropdownComponent,
161 TopMenuDropdownComponent, 163 TopMenuDropdownComponent,
@@ -186,6 +188,7 @@ import { FollowService } from '@app/shared/instance/follow.service'
186 InputMaskModule, 188 InputMaskModule,
187 BytesPipe, 189 BytesPipe,
188 KeysPipe, 190 KeysPipe,
191 MultiSelectModule,
189 192
190 LoaderComponent, 193 LoaderComponent,
191 SmallLoaderComponent, 194 SmallLoaderComponent,
diff --git a/client/src/app/shared/user-subscription/remote-subscribe.component.html b/client/src/app/shared/user-subscription/remote-subscribe.component.html
index ec3636b3e..59ee1cb04 100644
--- a/client/src/app/shared/user-subscription/remote-subscribe.component.html
+++ b/client/src/app/shared/user-subscription/remote-subscribe.component.html
@@ -12,13 +12,21 @@
12 <span *ngIf="interact">Remote interact</span> 12 <span *ngIf="interact">Remote interact</span>
13 </button> 13 </button>
14 14
15 <my-help *ngIf="!interact && showHelp" 15 <my-help *ngIf="!interact && showHelp">
16 helpType="custom" 16 <ng-template ptTemplate="customHtml">
17 i18n-customHtml customHtml="You can subscribe to the channel via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type the channel URL in the search box and subscribe there."> 17 <ng-container i18n>
18 You can subscribe to the channel via any ActivityPub-capable fediverse instance.<br /><br />
19 For instance with Mastodon or Pleroma you can type the channel URL in the search box and subscribe there.
20 </ng-container>
21 </ng-template>
18 </my-help> 22 </my-help>
19 23
20 <my-help *ngIf="showHelp && interact" 24 <my-help *ngIf="showHelp && interact">
21 helpType="custom" 25 <ng-template ptTemplate="customHtml">
22 i18n-customHtml customHtml="You can interact with this via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there."> 26 <ng-container i18n>
27 You can interact with this via any ActivityPub-capable fediverse instance.<br /><br />
28 For instance with Mastodon or Pleroma you can type the current URL in the search box and interact with it there.
29 </ng-container>
30 </ng-template>
23 </my-help> 31 </my-help>
24</form> \ No newline at end of file 32</form>
diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts
index 06eace71c..b4ac075c5 100644
--- a/client/src/app/shared/users/user-notification.model.ts
+++ b/client/src/app/shared/users/user-notification.model.ts
@@ -42,9 +42,10 @@ export class UserNotification implements UserNotificationServer {
42 state: FollowState 42 state: FollowState
43 follower: ActorInfo & { avatarUrl?: string } 43 follower: ActorInfo & { avatarUrl?: string }
44 following: { 44 following: {
45 type: 'account' | 'channel' 45 type: 'account' | 'channel' | 'instance'
46 name: string 46 name: string
47 displayName: string 47 displayName: string
48 host: string
48 } 49 }
49 } 50 }
50 51
@@ -112,7 +113,7 @@ export class UserNotification implements UserNotificationServer {
112 113
113 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS: 114 case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
114 this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list' 115 this.videoAutoBlacklistUrl = '/admin/moderation/video-auto-blacklist/list'
115 this.videoUrl = this.buildVideoUrl(this.video) 116 this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
116 break 117 break
117 118
118 case UserNotificationType.BLACKLIST_ON_MY_VIDEO: 119 case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
@@ -146,6 +147,10 @@ export class UserNotification implements UserNotificationServer {
146 case UserNotificationType.NEW_INSTANCE_FOLLOWER: 147 case UserNotificationType.NEW_INSTANCE_FOLLOWER:
147 this.instanceFollowUrl = '/admin/follows/followers-list' 148 this.instanceFollowUrl = '/admin/follows/followers-list'
148 break 149 break
150
151 case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
152 this.instanceFollowUrl = '/admin/follows/following-list'
153 break
149 } 154 }
150 } catch (err) { 155 } catch (err) {
151 this.type = null 156 this.type = null
diff --git a/client/src/app/shared/users/user-notifications.component.html b/client/src/app/shared/users/user-notifications.component.html
index 292813426..0702d3b5e 100644
--- a/client/src/app/shared/users/user-notifications.component.html
+++ b/client/src/app/shared/users/user-notifications.component.html
@@ -40,7 +40,7 @@
40 <my-global-icon iconName="no"></my-global-icon> 40 <my-global-icon iconName="no"></my-global-icon>
41 41
42 <div class="message"> 42 <div class="message">
43 The recently added video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been <a (click)="markAsRead(notification)" [routerLink]="notification.videoAutoBlacklistUrl">auto-blacklisted</a> 43 The recently added video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been <a (click)="markAsRead(notification)" [routerLink]="notification.videoAutoBlacklistUrl">auto-blacklisted</a>
44 </div> 44 </div>
45 </ng-container> 45 </ng-container>
46 46
@@ -111,6 +111,14 @@
111 <ng-container *ngIf="notification.actorFollow.state === 'pending'"> awaiting your approval</ng-container> 111 <ng-container *ngIf="notification.actorFollow.state === 'pending'"> awaiting your approval</ng-container>
112 </div> 112 </div>
113 </ng-container> 113 </ng-container>
114
115 <ng-container i18n *ngSwitchCase="UserNotificationType.AUTO_INSTANCE_FOLLOWING">
116 <my-global-icon iconName="users"></my-global-icon>
117
118 <div class="message">
119 Your instance automatically followed <a (click)="markAsRead(notification)" [routerLink]="notification.instanceFollowUrl">{{ notification.actorFollow.following.host }}</a>
120 </div>
121 </ng-container>
114 </ng-container> 122 </ng-container>
115 123
116 <div class="from-date">{{ notification.createdAt | myFromNow }}</div> 124 <div class="from-date">{{ notification.createdAt | myFromNow }}</div>
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
index 53809f82c..656b73dd2 100644
--- a/client/src/app/shared/users/user.model.ts
+++ b/client/src/app/shared/users/user.model.ts
@@ -9,31 +9,38 @@ export class User implements UserServerModel {
9 username: string 9 username: string
10 email: string 10 email: string
11 pendingEmail: string | null 11 pendingEmail: string | null
12
12 emailVerified: boolean 13 emailVerified: boolean
13 nsfwPolicy: NSFWPolicyType 14 nsfwPolicy: NSFWPolicyType
14 15
15 role: UserRole 16 adminFlags?: UserAdminFlag
16 roleLabel: string
17 17
18 webTorrentEnabled: boolean
19 autoPlayVideo: boolean 18 autoPlayVideo: boolean
19 webTorrentEnabled: boolean
20 videosHistoryEnabled: boolean 20 videosHistoryEnabled: boolean
21 videoLanguages: string[] 21 videoLanguages: string[]
22 22
23 role: UserRole
24 roleLabel: string
25
23 videoQuota: number 26 videoQuota: number
24 videoQuotaDaily: number 27 videoQuotaDaily: number
25 account: Account 28 videoQuotaUsed?: number
26 videoChannels: VideoChannel[] 29 videoQuotaUsedDaily?: number
27 createdAt: Date
28 30
29 theme: string 31 theme: string
30 32
31 adminFlags?: UserAdminFlag 33 account: Account
34 notificationSettings?: UserNotificationSetting
35 videoChannels?: VideoChannel[]
32 36
33 blocked: boolean 37 blocked: boolean
34 blockedReason?: string 38 blockedReason?: string
35 39
36 notificationSettings?: UserNotificationSetting 40 noInstanceConfigWarningModal: boolean
41 noWelcomeModal: boolean
42
43 createdAt: Date
37 44
38 constructor (hash: Partial<UserServerModel>) { 45 constructor (hash: Partial<UserServerModel>) {
39 this.id = hash.id 46 this.id = hash.id
@@ -43,13 +50,16 @@ export class User implements UserServerModel {
43 this.role = hash.role 50 this.role = hash.role
44 51
45 this.videoChannels = hash.videoChannels 52 this.videoChannels = hash.videoChannels
53
46 this.videoQuota = hash.videoQuota 54 this.videoQuota = hash.videoQuota
47 this.videoQuotaDaily = hash.videoQuotaDaily 55 this.videoQuotaDaily = hash.videoQuotaDaily
56 this.videoQuotaUsed = hash.videoQuotaUsed
57 this.videoQuotaUsedDaily = hash.videoQuotaUsedDaily
58
48 this.nsfwPolicy = hash.nsfwPolicy 59 this.nsfwPolicy = hash.nsfwPolicy
49 this.webTorrentEnabled = hash.webTorrentEnabled 60 this.webTorrentEnabled = hash.webTorrentEnabled
50 this.videosHistoryEnabled = hash.videosHistoryEnabled 61 this.videosHistoryEnabled = hash.videosHistoryEnabled
51 this.autoPlayVideo = hash.autoPlayVideo 62 this.autoPlayVideo = hash.autoPlayVideo
52 this.createdAt = hash.createdAt
53 63
54 this.theme = hash.theme 64 this.theme = hash.theme
55 65
@@ -58,8 +68,13 @@ export class User implements UserServerModel {
58 this.blocked = hash.blocked 68 this.blocked = hash.blocked
59 this.blockedReason = hash.blockedReason 69 this.blockedReason = hash.blockedReason
60 70
71 this.noInstanceConfigWarningModal = hash.noInstanceConfigWarningModal
72 this.noWelcomeModal = hash.noWelcomeModal
73
61 this.notificationSettings = hash.notificationSettings 74 this.notificationSettings = hash.notificationSettings
62 75
76 this.createdAt = hash.createdAt
77
63 if (hash.account !== undefined) { 78 if (hash.account !== undefined) {
64 this.account = new Account(hash.account) 79 this.account = new Account(hash.account)
65 } 80 }
diff --git a/client/src/app/shared/video/videos-selection.component.ts b/client/src/app/shared/video/videos-selection.component.ts
index 994e0fa1e..064420056 100644
--- a/client/src/app/shared/video/videos-selection.component.ts
+++ b/client/src/app/shared/video/videos-selection.component.ts
@@ -35,7 +35,7 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
35 @Input() titlePage: string 35 @Input() titlePage: string
36 @Input() miniatureDisplayOptions: MiniatureDisplayOptions 36 @Input() miniatureDisplayOptions: MiniatureDisplayOptions
37 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>> 37 @Input() getVideosObservableFunction: (page: number, sort?: VideoSortField) => Observable<ResultList<Video>>
38 @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective> 38 @ContentChildren(PeerTubeTemplateDirective) templates: QueryList<PeerTubeTemplateDirective<'rowButtons' | 'globalButtons'>>
39 39
40 @Output() selectionChange = new EventEmitter<SelectionType>() 40 @Output() selectionChange = new EventEmitter<SelectionType>()
41 @Output() videosModelChange = new EventEmitter<Video[]>() 41 @Output() videosModelChange = new EventEmitter<Video[]>()
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html
index 217cadc66..245ae42b6 100644
--- a/client/src/app/videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html
@@ -15,7 +15,16 @@
15 15
16 <div class="form-group"> 16 <div class="form-group">
17 <label i18n class="label-tags">Tags</label> 17 <label i18n class="label-tags">Tags</label>
18 <my-help i18n-preHtml preHtml="Tags could be used to suggest relevant recommendations.</br>Press Enter to add a new tag."></my-help> 18
19 <my-help>
20 <ng-template ptTemplate="customHtml">
21 <ng-container i18n>
22 Tags could be used to suggest relevant recommendations. <br />
23 Press Enter to add a new tag.
24 </ng-container>
25 </ng-template>
26 </my-help>
27
19 <tag-input 28 <tag-input
20 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages" 29 [validators]="tagValidators" [errorMessages]="tagValidatorsMessages"
21 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag" 30 i18n-placeholder placeholder="+ Tag" i18n-secondaryPlaceholder secondaryPlaceholder="Enter a new tag"
@@ -25,7 +34,15 @@
25 34
26 <div class="form-group"> 35 <div class="form-group">
27 <label i18n for="description">Description</label> 36 <label i18n for="description">Description</label>
28 <my-help helpType="markdownText" i18n-preHtml preHtml="Video descriptions are truncated by default and require manual action to expand them."></my-help> 37
38 <my-help helpType="markdownText">
39 <ng-template ptTemplate="preHtml">
40 <ng-container i18n>
41 Video descriptions are truncated by default and require manual action to expand them.
42 </ng-container>
43 </ng-template>
44 </my-help>
45
29 <my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea> 46 <my-markdown-textarea truncate="250" formControlName="description"></my-markdown-textarea>
30 47
31 <div *ngIf="formErrors.description" class="form-error"> 48 <div *ngIf="formErrors.description" class="form-error">
@@ -114,20 +131,25 @@
114 </div> 131 </div>
115 </div> 132 </div>
116 133
117 <my-peertube-checkbox 134 <my-peertube-checkbox inputName="nsfw" formControlName="nsfw" helpPlacement="bottom-right">
118 inputName="nsfw" formControlName="nsfw" 135 <ng-template ptTemplate="label">
119 i18n-labelText labelText="This video contains mature or explicit content" 136 <ng-container i18n>This video contains mature or explicit content</ng-container>
120 i18n-helpHtml helpHtml="Some instances do not list videos containing mature or explicit content by default." 137 </ng-template>
121 helpPlacement="bottom-right" 138
122 ></my-peertube-checkbox> 139 <ng-template ptTemplate="help">
123 140 <ng-container i18n>Some instances do not list videos containing mature or explicit content by default.</ng-container>
124 <my-peertube-checkbox 141 </ng-template>
125 *ngIf="waitTranscodingEnabled" 142 </my-peertube-checkbox>
126 inputName="waitTranscoding" formControlName="waitTranscoding" 143
127 i18n-labelText labelText="Wait transcoding before publishing the video" 144 <my-peertube-checkbox *ngIf="waitTranscodingEnabled" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right">
128 i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends." 145 <ng-template ptTemplate="label">
129 helpPlacement="bottom-right" 146 <ng-container i18n>Wait transcoding before publishing the video</ng-container>
130 ></my-peertube-checkbox> 147 </ng-template>
148
149 <ng-template ptTemplate="help">
150 <ng-container i18n>If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends.</ng-container>
151 </ng-template>
152 </my-peertube-checkbox>
131 153
132 </div> 154 </div>
133 </div> 155 </div>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
index 7a495fea5..c290fd4b1 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.html
@@ -12,10 +12,14 @@
12 12
13 <div class="form-group form-group-magnet-uri"> 13 <div class="form-group form-group-magnet-uri">
14 <label i18n for="magnetUri">Paste magnet URI</label> 14 <label i18n for="magnetUri">Paste magnet URI</label>
15 <my-help 15 <my-help>
16 helpType="custom" i18n-customHtml 16 <ng-template ptTemplate="customHtml">
17 customHtml="You can import any torrent file that points to a mp4 file. You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance." 17 <ng-container i18n>
18 ></my-help> 18 You can import any torrent file that points to a mp4 file.
19 You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance.
20 </ng-container>
21 </ng-template>
22 </my-help>
19 23
20 <input type="text" id="magnetUri" [(ngModel)]="magnetUri" /> 24 <input type="text" id="magnetUri" [(ngModel)]="magnetUri" />
21 </div> 25 </div>
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
index e4f19faa8..09d0b8272 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.html
@@ -4,10 +4,16 @@
4 4
5 <div class="form-group"> 5 <div class="form-group">
6 <label i18n for="targetUrl">URL</label> 6 <label i18n for="targetUrl">URL</label>
7 <my-help 7
8 helpType="custom" i18n-customHtml 8 <my-help>
9 customHtml="You can import any URL <a href='https://rg3.github.io/youtube-dl/supportedsites.html' target='_blank' rel='noopener noreferrer'>supported by youtube-dl</a> or URL that points to a raw MP4 file. You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance." 9 <ng-template ptTemplate="customHtml">
10 ></my-help> 10 <ng-container i18n>
11 You can import any URL <a href='https://rg3.github.io/youtube-dl/supportedsites.html' target='_blank' rel='noopener noreferrer'>supported by youtube-dl</a>
12 or URL that points to a raw MP4 file.
13 You should make sure you have diffusion rights over the content it points to, otherwise it could cause legal trouble to yourself and your instance.
14 </ng-container>
15 </ng-template>
16 </my-help>
11 17
12 <input type="text" id="targetUrl" [(ngModel)]="targetUrl" /> 18 <input type="text" id="targetUrl" [(ngModel)]="targetUrl" />
13 </div> 19 </div>
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index abbc137b2..ebd3c0cc4 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -343,6 +343,7 @@
343 & + span { 343 & + span {
344 position: relative; 344 position: relative;
345 width: 18px; 345 width: 18px;
346 min-width: 18px;
346 height: 18px; 347 height: 18px;
347 border: $border-width solid var(--mainForegroundColor); 348 border: $border-width solid var(--mainForegroundColor);
348 border-radius: 3px; 349 border-radius: 3px;
diff --git a/client/src/sass/player/peertube-skin.scss b/client/src/sass/player/peertube-skin.scss
index 1a5144b11..4bf48a570 100644
--- a/client/src/sass/player/peertube-skin.scss
+++ b/client/src/sass/player/peertube-skin.scss
@@ -26,11 +26,6 @@ body {
26 .vjs-dock-description { 26 .vjs-dock-description {
27 font-size: 11px; 27 font-size: 11px;
28 28
29 .text::before, .text::after {
30 display: inline-block;
31 content: '\1F308';
32 }
33
34 .text::before { 29 .text::before {
35 margin-right: 4px; 30 margin-right: 4px;
36 } 31 }
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 6ff3efef1..19d2a1d02 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -239,7 +239,7 @@ export class PeerTubeEmbed {
239 239
240 const config: ServerConfig = await configResponse.json() 240 const config: ServerConfig = await configResponse.json()
241 const description = config.tracker.enabled && this.warningTitle 241 const description = config.tracker.enabled && this.warningTitle
242 ? '<span class="text">' + this.player.localize('Uses P2P, others may know your IP is downloading this video.') + '</span>' 242 ? '<span class="text">' + this.player.localize('Watching this video may reveal your IP address to others.') + '</span>'
243 : undefined 243 : undefined
244 244
245 this.player.dock({ 245 this.player.dock({