aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html83
-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/+about/about-peertube/about-peertube-contributors.component.html13
-rw-r--r--client/src/app/+about/about-peertube/about-peertube-contributors.component.scss15
-rw-r--r--client/src/app/+about/about-peertube/about-peertube-contributors.component.ts19
-rw-r--r--client/src/app/+about/about-peertube/about-peertube.component.html163
-rw-r--r--client/src/app/+about/about-peertube/about-peertube.component.scss43
-rw-r--r--client/src/app/+about/about.module.ts3
-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/+admin/system/debug/debug.component.html2
-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/renderer/markdown.service.ts19
-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.ts12
-rw-r--r--client/src/app/shared/users/user-notifications.component.html12
-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/assets/images/framasoft.pngbin0 -> 5232 bytes
-rw-r--r--client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts2
-rw-r--r--client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts17
-rw-r--r--client/src/sass/include/_mixins.scss2
-rw-r--r--client/src/sass/player/peertube-skin.scss5
-rw-r--r--client/src/standalone/videos/embed.ts2
63 files changed, 1571 insertions, 418 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..25d416740 100644
--- a/client/src/app/+about/about-instance/about-instance.component.html
+++ b/client/src/app/+about/about-instance/about-instance.component.html
@@ -1,32 +1,97 @@
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 i18n *ngIf="isContactFormEnabled" (click)="openContactModal()" role="button" class="contact-admin">Contact administrator</div>
8 </div>
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>
7 </div> 14 </div>
8 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 i18n *ngIf="isNSFW" class="block dedicated-to-nsfw">This instance is dedicated to sensitive/NSFW content.</div>
20 </div>
21
22 <div i18n 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 i18n 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 i18n 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 i18n 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
28 <div class="col-md-12 col-xl-6"> 93 <div class="col-md-12 col-xl-6">
29 <label>Features found on this instance</label> 94 <label i18n>Features found on this instance</label>
30 <my-instance-features-table></my-instance-features-table> 95 <my-instance-features-table></my-instance-features-table>
31 </div> 96 </div>
32</div> 97</div>
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/+about/about-peertube/about-peertube-contributors.component.html b/client/src/app/+about/about-peertube/about-peertube-contributors.component.html
new file mode 100644
index 000000000..997a6a3e1
--- /dev/null
+++ b/client/src/app/+about/about-peertube/about-peertube-contributors.component.html
@@ -0,0 +1,13 @@
1<h3 i18n class="section-title">Who made this software?</h3>
2
3<p align="center">
4 <strong>Developed with &#10084; by <a target="_blank" rel="noopener noreferrer" href="https://framasoft.org">Framasoft</a></strong>
5</p>
6
7<p align="center">
8 <a target="_blank" rel="noopener noreferrer" href="https://framasoft.org">
9 <img width="150px" src="/client/assets/images/framasoft.png" alt="Framasoft logo"/>
10 </a>
11</p>
12
13<div [innerHTML]="creditsHtml"></div>
diff --git a/client/src/app/+about/about-peertube/about-peertube-contributors.component.scss b/client/src/app/+about/about-peertube/about-peertube-contributors.component.scss
new file mode 100644
index 000000000..9c3b0a46b
--- /dev/null
+++ b/client/src/app/+about/about-peertube/about-peertube-contributors.component.scss
@@ -0,0 +1,15 @@
1@import '_variables';
2@import '_mixins';
3
4/deep/ h1 {
5 font-size: 1rem;
6}
7
8/deep/ ul {
9 padding: 0;
10
11 li {
12 display: inline-block;
13 margin-right: 10px;
14 }
15}
diff --git a/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts
new file mode 100644
index 000000000..fa2c0daa0
--- /dev/null
+++ b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts
@@ -0,0 +1,19 @@
1import { Component, OnInit } from '@angular/core'
2import { MarkdownService } from '@app/shared/renderer'
3
4@Component({
5 selector: 'my-about-peertube-contributors',
6 templateUrl: './about-peertube-contributors.component.html',
7 styleUrls: [ './about-peertube-contributors.component.scss' ]
8})
9export class AboutPeertubeContributorsComponent implements OnInit {
10 creditsHtml: string
11
12 private markdown = require('raw-loader!../../../../../CREDITS.md')
13
14 constructor (private markdownService: MarkdownService) { }
15
16 async ngOnInit () {
17 this.creditsHtml = await this.markdownService.completeMarkdownToHTML(this.markdown)
18 }
19}
diff --git a/client/src/app/+about/about-peertube/about-peertube.component.html b/client/src/app/+about/about-peertube/about-peertube.component.html
index d3fc9a828..423f7bce7 100644
--- a/client/src/app/+about/about-peertube/about-peertube.component.html
+++ b/client/src/app/+about/about-peertube/about-peertube.component.html
@@ -14,84 +14,89 @@
14 </p> 14 </p>
15</div> 15</div>
16 16
17<div id="p2p-privacy"> 17<div class="privacy-contributors">
18 <h3 i18n class="section-title">P2P & Privacy</h3> 18 <my-about-peertube-contributors></my-about-peertube-contributors>
19
20 <div class="p2p-privacy">
21 <h3 i18n class="section-title">P2P & Privacy</h3>
22
23 <p i18n>
24 PeerTube uses the BitTorrent protocol to share bandwidth between users.
25 This implies that your IP address is stored in the instance's BitTorrent tracker as long as you download or watch the video.
26 </p>
27
28 <h6 i18n class="p2p-privacy-title">What are the consequences?</h6>
29
30 <p i18n>
31 In theory, someone with enough technical skills could create a script that tracks which IP is downloading which video.
32 In practice, this is much more difficult because:
33 </p>
34
35 <ul>
36 <li i18n>
37 An HTTP request has to be sent on each tracker for each video to spy.
38 If we want to spy all PeerTube's videos, we have to send as many requests as there are videos (so potentially a lot)
39 </li>
40
41 <li i18n>
42 For each request sent, the tracker returns random peers at a limited number.
43 For instance, if there are 1000 peers in the swarm and the tracker sends only 20 peers for each request, there must be at least 50 requests sent to know every peers in the swarm
44 </li>
45
46 <li i18n>
47 Those requests have to be sent regularly to know who starts/stops watching a video. It is easy to detect that kind of behaviour
48 </li>
49
50 <li i18n>
51 If an IP address is stored in the tracker, it doesn't mean that the person behind the IP (if this person exists) has watched the video
52 </li>
53
54 <li i18n>
55 The IP address is a vague information : usually, it regularly changes and can represent many persons or entities
56 </li>
57
58 <li i18n>
59 Web peers are not publicly accessible: because we use WebRTC inside the web browser (<a href="https://webtorrent.io/">with the WebTorrent library</a>), the protocol is different from classic BitTorrent.
60 When you are in a web browser, you send a signal containing your IP address to the tracker that will randomly choose other peers to forward the information to.
61 See <a href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information
62 </li>
63 </ul>
64
65 <p i18n>
66 The worst-case scenario of an average person spying on their friends is quite unlikely.
67 There are much more effective ways to get that kind of information.
68 </p>
69
70 <h6 i18n class="p2p-privacy-title">How does PeerTube compare with YouTube?</h6>
71
72 <p i18n>
73 The threats to privacy in YouTube are different from PeerTube's.
74 In YouTube's case, the platform gathers a huge amount of your personal information (not only your IP) to analyze them and track you.
75 Moreover, YouTube is owned by Google/Alphabet, a company that tracks you across many websites (via AdSense or Google Analytics).
76 </p>
77
78 <h6 i18n class="p2p-privacy-title">What can I do to limit the exposure of my IP address?</h6>
79
80 <p i18n>
81 Your IP address is public so every time you consult a website, there is a number of actors (in addition to the final website) seeing your IP in their connection logs: ISP/routers/trackers/CDN and more.
82 PeerTube is transparent about it: we warn you that if you want to keep your IP private, you must use a VPN or Tor Browser.
83 Thinking that removing P2P from PeerTube will give you back anonymity doesn't make sense.
84 </p>
85
86 <h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6>
87
88 <p i18n>
89 PeerTube is in its early stages, and want to deliver the best countermeasures possible by the time the stable is released.
90 In the meantime, we want to test different ideas related to this issue:
91 </p>
92
93 <ul>
94 <li i18n>Set a limit to the number of peers sent by the tracker</li>
95 <li i18n>Set a limit on the request frequency received by the tracker (being tested)</li>
96 <li i18n>Ring a bell if there are unusual requests (being tested)</li>
97 <li i18n>Disable P2P from the administration interface</li>
98 <li i18n>An automatic video redundancy program: we wouldn't know if the IP downloaded the video on purpose or if it was the automatized program</li>
99 </ul>
100 </div>
19 101
20 <p i18n>
21 PeerTube uses the BitTorrent protocol to share bandwidth between users.
22 This implies that your IP address is stored in the instance's BitTorrent tracker as long as you download or watch the video.
23 </p>
24
25 <h6 i18n class="p2p-privacy-title">What are the consequences?</h6>
26
27 <p i18n>
28 In theory, someone with enough technical skills could create a script that tracks which IP is downloading which video.
29 In practice, this is much more difficult because:
30 </p>
31
32 <ul>
33 <li i18n>
34 An HTTP request has to be sent on each tracker for each video to spy.
35 If we want to spy all PeerTube's videos, we have to send as many requests as there are videos (so potentially a lot)
36 </li>
37
38 <li i18n>
39 For each request sent, the tracker returns random peers at a limited number.
40 For instance, if there are 1000 peers in the swarm and the tracker sends only 20 peers for each request, there must be at least 50 requests sent to know every peers in the swarm
41 </li>
42
43 <li i18n>
44 Those requests have to be sent regularly to know who starts/stops watching a video. It is easy to detect that kind of behaviour
45 </li>
46
47 <li i18n>
48 If an IP address is stored in the tracker, it doesn't mean that the person behind the IP (if this person exists) has watched the video
49 </li>
50
51 <li i18n>
52 The IP address is a vague information : usually, it regularly changes and can represent many persons or entities
53 </li>
54
55 <li i18n>
56 Web peers are not publicly accessible: because we use WebRTC inside the web browser (<a href="https://webtorrent.io/">with the WebTorrent library</a>), the protocol is different from classic BitTorrent.
57 When you are in a web browser, you send a signal containing your IP address to the tracker that will randomly choose other peers to forward the information to.
58 See <a href="https://github.com/yciabaud/webtorrent/blob/beps/bep_webrtc.rst">this document</a> for more information
59 </li>
60 </ul>
61
62 <p i18n>
63 The worst-case scenario of an average person spying on their friends is quite unlikely.
64 There are much more effective ways to get that kind of information.
65 </p>
66
67 <h6 i18n class="p2p-privacy-title">How does PeerTube compare with YouTube?</h6>
68
69 <p i18n>
70 The threats to privacy in YouTube are different from PeerTube's.
71 In YouTube's case, the platform gathers a huge amount of your personal information (not only your IP) to analyze them and track you.
72 Moreover, YouTube is owned by Google/Alphabet, a company that tracks you across many websites (via AdSense or Google Analytics).
73 </p>
74
75 <h6 i18n class="p2p-privacy-title">What can I do to limit the exposure of my IP address?</h6>
76
77 <p i18n>
78 Your IP address is public so every time you consult a website, there is a number of actors (in addition to the final website) seeing your IP in their connection logs: ISP/routers/trackers/CDN and more.
79 PeerTube is transparent about it: we warn you that if you want to keep your IP private, you must use a VPN or Tor Browser.
80 Thinking that removing P2P from PeerTube will give you back anonymity doesn't make sense.
81 </p>
82
83 <h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6>
84
85 <p i18n>
86 PeerTube is in its early stages, and want to deliver the best countermeasures possible by the time the stable is released.
87 In the meantime, we want to test different ideas related to this issue:
88 </p>
89
90 <ul>
91 <li i18n>Set a limit to the number of peers sent by the tracker</li>
92 <li i18n>Set a limit on the request frequency received by the tracker (being tested)</li>
93 <li i18n>Ring a bell if there are unusual requests (being tested)</li>
94 <li i18n>Disable P2P from the administration interface</li>
95 <li i18n>An automatic video redundancy program: we wouldn't know if the IP downloaded the video on purpose or if it was the automatized program</li>
96 </ul>
97</div> 102</div>
diff --git a/client/src/app/+about/about-peertube/about-peertube.component.scss b/client/src/app/+about/about-peertube/about-peertube.component.scss
index 0d2e2bb68..8fca53e90 100644
--- a/client/src/app/+about/about-peertube/about-peertube.component.scss
+++ b/client/src/app/+about/about-peertube/about-peertube.component.scss
@@ -2,12 +2,12 @@
2@import '_mixins'; 2@import '_mixins';
3 3
4.about-peertube-title { 4.about-peertube-title {
5 font-size: 25px; 5 font-size: 20px;
6 font-weight: bold; 6 font-weight: $font-semibold;
7 margin-bottom: 15px; 7 margin-bottom: 15px;
8} 8}
9 9
10.section-title { 10/deep/ .section-title {
11 font-weight: $font-semibold; 11 font-weight: $font-semibold;
12 font-size: 20px; 12 font-size: 20px;
13 margin-bottom: 5px; 13 margin-bottom: 5px;
@@ -17,6 +17,41 @@
17 margin-bottom: 30px; 17 margin-bottom: 30px;
18} 18}
19 19
20.description,
21.p2p-privacy,
22my-about-peertube-contributors {
23 /deep/ {
24 p, li {
25 font-size: 15px;
26 }
27 }
28}
29
20.p2p-privacy-title { 30.p2p-privacy-title {
21 margin-top: 15px; 31 margin-top: 15px;
22} \ No newline at end of file 32}
33
34.privacy-contributors {
35 display: flex;
36 flex-direction: row;
37
38 > div,
39 > my-about-peertube-contributors {
40 flex-basis: 100%;
41 display: block;
42 }
43
44 .p2p-privacy {
45 h6 {
46 font-size: 20px;
47 }
48 }
49
50 my-about-peertube-contributors {
51 margin: 0 40px 40px 0;
52 }
53
54 @media screen and (max-width: $small-view) {
55 flex-direction: column;
56 }
57}
diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts
index 49a7a52f8..14bf76e55 100644
--- a/client/src/app/+about/about.module.ts
+++ b/client/src/app/+about/about.module.ts
@@ -1,5 +1,4 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2
3import { AboutRoutingModule } from './about-routing.module' 2import { AboutRoutingModule } from './about-routing.module'
4import { AboutComponent } from './about.component' 3import { AboutComponent } from './about.component'
5import { SharedModule } from '../shared' 4import { SharedModule } from '../shared'
@@ -7,6 +6,7 @@ import { AboutInstanceComponent } from '@app/+about/about-instance/about-instanc
7import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' 6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
8import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' 7import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
9import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' 8import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
9import { AboutPeertubeContributorsComponent } from '@app/+about/about-peertube/about-peertube-contributors.component'
10 10
11@NgModule({ 11@NgModule({
12 imports: [ 12 imports: [
@@ -19,6 +19,7 @@ import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.c
19 AboutInstanceComponent, 19 AboutInstanceComponent,
20 AboutPeertubeComponent, 20 AboutPeertubeComponent,
21 AboutFollowsComponent, 21 AboutFollowsComponent,
22 AboutPeertubeContributorsComponent,
22 ContactAdminModalComponent 23 ContactAdminModalComponent
23 ], 24 ],
24 25
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/+admin/system/debug/debug.component.html b/client/src/app/+admin/system/debug/debug.component.html
index f35414b37..75f3df601 100644
--- a/client/src/app/+admin/system/debug/debug.component.html
+++ b/client/src/app/+admin/system/debug/debug.component.html
@@ -1,7 +1,7 @@
1<div class="root"> 1<div class="root">
2 <h4>IP</h4> 2 <h4>IP</h4>
3 3
4 <p>PeerTube thinks your public IP is <strong>{{ debug?.ip }}</strong>.</p> 4 <p>PeerTube thinks your web browser public IP is <strong>{{ debug?.ip }}</strong>.</p>
5 5
6 <p>If this is not your correct public IP, please consider fixing it because:</p> 6 <p>If this is not your correct public IP, please consider fixing it because:</p>
7 <ul> 7 <ul>
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/renderer/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts
index 9a9066351..0e24f3085 100644
--- a/client/src/app/shared/renderer/markdown.service.ts
+++ b/client/src/app/shared/renderer/markdown.service.ts
@@ -13,9 +13,11 @@ export class MarkdownService {
13 'list' 13 'list'
14 ] 14 ]
15 static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ]) 15 static ENHANCED_RULES = MarkdownService.TEXT_RULES.concat([ 'image' ])
16 static COMPLETE_RULES = MarkdownService.ENHANCED_RULES.concat([ 'block', 'inline', 'heading', 'html_inline', 'html_block', 'paragraph' ])
16 17
17 private textMarkdownIt: MarkdownIt 18 private textMarkdownIt: MarkdownIt
18 private enhancedMarkdownIt: MarkdownIt 19 private enhancedMarkdownIt: MarkdownIt
20 private completeMarkdownIt: MarkdownIt
19 21
20 async textMarkdownToHTML (markdown: string) { 22 async textMarkdownToHTML (markdown: string) {
21 if (!markdown) return '' 23 if (!markdown) return ''
@@ -39,11 +41,22 @@ export class MarkdownService {
39 return this.avoidTruncatedTags(html) 41 return this.avoidTruncatedTags(html)
40 } 42 }
41 43
42 private async createMarkdownIt (rules: string[]) { 44 async completeMarkdownToHTML (markdown: string) {
43 // FIXME: import('..') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function 45 if (!markdown) return ''
46
47 if (!this.completeMarkdownIt) {
48 this.completeMarkdownIt = await this.createMarkdownIt(MarkdownService.COMPLETE_RULES, true)
49 }
50
51 const html = this.completeMarkdownIt.render(markdown)
52 return this.avoidTruncatedTags(html)
53 }
54
55 private async createMarkdownIt (rules: string[], html = false) {
56 // FIXME: import('...') returns a struct module, containing a "default" field corresponding to our sanitizeHtml function
44 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default 57 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
45 58
46 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true }) 59 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html })
47 60
48 for (const rule of rules) { 61 for (const rule of rules) {
49 markdownIt.enable(rule) 62 markdownIt.enable(rule)
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..c3f4bf429 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,10 @@ 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 // Backward compatibility where we did not assign videoBlacklist to this type of notification before
117 if (!this.videoBlacklist) this.videoBlacklist = { id: null, video: this.video }
118
119 this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video)
116 break 120 break
117 121
118 case UserNotificationType.BLACKLIST_ON_MY_VIDEO: 122 case UserNotificationType.BLACKLIST_ON_MY_VIDEO:
@@ -146,6 +150,10 @@ export class UserNotification implements UserNotificationServer {
146 case UserNotificationType.NEW_INSTANCE_FOLLOWER: 150 case UserNotificationType.NEW_INSTANCE_FOLLOWER:
147 this.instanceFollowUrl = '/admin/follows/followers-list' 151 this.instanceFollowUrl = '/admin/follows/followers-list'
148 break 152 break
153
154 case UserNotificationType.AUTO_INSTANCE_FOLLOWING:
155 this.instanceFollowUrl = '/admin/follows/following-list'
156 break
149 } 157 }
150 } catch (err) { 158 } catch (err) {
151 this.type = null 159 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..a0f8e6df5 100644
--- a/client/src/app/shared/users/user-notifications.component.html
+++ b/client/src/app/shared/users/user-notifications.component.html
@@ -8,7 +8,7 @@
8 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" /> 8 <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" />
9 9
10 <div class="message"> 10 <div class="message">
11 {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a> 11 {{ notification.video.channel.displayName }} published a new video: <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a>
12 </div> 12 </div>
13 </ng-container> 13 </ng-container>
14 14
@@ -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/assets/images/framasoft.png b/client/src/assets/images/framasoft.png
new file mode 100644
index 000000000..57be8c219
--- /dev/null
+++ b/client/src/assets/images/framasoft.png
Binary files differ
diff --git a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
index 0c8c612ee..c44c184d5 100644
--- a/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
+++ b/client/src/assets/player/p2p-media-loader/p2p-media-loader-plugin.ts
@@ -92,7 +92,7 @@ class P2pMediaLoaderPlugin extends Plugin {
92 this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => { 92 this.p2pEngine.on(Events.SegmentError, (segment: Segment, err) => {
93 console.error('Segment error.', segment, err) 93 console.error('Segment error.', segment, err)
94 94
95 this.options.redundancyUrlManager.removeByOriginUrl(segment.url) 95 this.options.redundancyUrlManager.removeBySegmentUrl(segment.requestUrl)
96 }) 96 })
97 97
98 this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls() 98 this.statsP2PBytes.numPeers = 1 + this.options.redundancyUrlManager.countBaseUrls()
diff --git a/client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts b/client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts
index 7fc2b6ab1..abab8aa99 100644
--- a/client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts
+++ b/client/src/assets/player/p2p-media-loader/redundancy-url-manager.ts
@@ -2,9 +2,6 @@ import { basename, dirname } from 'path'
2 2
3class RedundancyUrlManager { 3class RedundancyUrlManager {
4 4
5 // Remember by what new URL we replaced an origin URL
6 private replacedSegmentUrls: { [originUrl: string]: string } = {}
7
8 constructor (private baseUrls: string[] = []) { 5 constructor (private baseUrls: string[] = []) {
9 // empty 6 // empty
10 } 7 }
@@ -17,16 +14,7 @@ class RedundancyUrlManager {
17 this.baseUrls = this.baseUrls.filter(u => u !== baseUrl && u !== baseUrl + '/') 14 this.baseUrls = this.baseUrls.filter(u => u !== baseUrl && u !== baseUrl + '/')
18 } 15 }
19 16
20 removeByOriginUrl (originUrl: string) {
21 const replaced = this.replacedSegmentUrls[originUrl]
22 if (!replaced) return
23
24 return this.removeBySegmentUrl(replaced)
25 }
26
27 buildUrl (url: string) { 17 buildUrl (url: string) {
28 delete this.replacedSegmentUrls[url]
29
30 const max = this.baseUrls.length + 1 18 const max = this.baseUrls.length + 1
31 const i = this.getRandomInt(max) 19 const i = this.getRandomInt(max)
32 20
@@ -35,10 +23,7 @@ class RedundancyUrlManager {
35 const newBaseUrl = this.baseUrls[i] 23 const newBaseUrl = this.baseUrls[i]
36 const slashPart = newBaseUrl.endsWith('/') ? '' : '/' 24 const slashPart = newBaseUrl.endsWith('/') ? '' : '/'
37 25
38 const newUrl = newBaseUrl + slashPart + basename(url) 26 return newBaseUrl + slashPart + basename(url)
39 this.replacedSegmentUrls[url] = newUrl
40
41 return newUrl
42 } 27 }
43 28
44 countBaseUrls () { 29 countBaseUrls () {
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index abbc137b2..26ba490c7 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;
@@ -395,6 +396,7 @@
395 border-radius: 50%; 396 border-radius: 50%;
396 width: $size; 397 width: $size;
397 height: $size; 398 height: $size;
399 min-width: $size;
398} 400}
399 401
400@mixin chevron ($size, $border-width) { 402@mixin chevron ($size, $border-width) {
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({