diff options
Diffstat (limited to 'client/src')
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' | |||
4 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' | 4 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' |
5 | import { InstanceService } from '@app/shared/instance/instance.service' | 5 | import { InstanceService } from '@app/shared/instance/instance.service' |
6 | import { MarkdownService } from '@app/shared/renderer' | 6 | import { MarkdownService } from '@app/shared/renderer' |
7 | import { forkJoin } from 'rxjs' | ||
8 | import { 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 ❤ 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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { 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 | }) | ||
9 | export 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, | ||
22 | my-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 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | |||
3 | import { AboutRoutingModule } from './about-routing.module' | 2 | import { AboutRoutingModule } from './about-routing.module' |
4 | import { AboutComponent } from './about.component' | 3 | import { AboutComponent } from './about.component' |
5 | import { SharedModule } from '../shared' | 4 | import { SharedModule } from '../shared' |
@@ -7,6 +6,7 @@ import { AboutInstanceComponent } from '@app/+about/about-instance/about-instanc | |||
7 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' | 6 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' |
8 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' | 7 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' |
9 | import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' | 8 | import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' |
9 | import { 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 | |||
4 | input[type=text] { | 8 | input[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' | |||
6 | import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model' | 6 | import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 8 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
9 | import { SelectItem } from 'primeng/api' | ||
10 | import { forkJoin } from 'rxjs' | ||
11 | import { 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' | |||
5 | import { FormReactive, User, UserService } from '../../../shared' | 5 | import { FormReactive, User, UserService } from '../../../shared' |
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
8 | import { Subject } from 'rxjs' | 8 | import { forkJoin, Subject } from 'rxjs' |
9 | import { SelectItem } from 'primeng/api' | 9 | import { SelectItem } from 'primeng/api' |
10 | import { switchMap } from 'rxjs/operators' | 10 | import { 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' |
38 | import { DragDropModule } from '@angular/cdk/drag-drop' | 38 | import { DragDropModule } from '@angular/cdk/drag-drop' |
39 | import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email' | 39 | import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email' |
40 | import { MultiSelectModule } from 'primeng/multiselect' | ||
41 | import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface' | 40 | import { 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 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
2 | import { AuthService } from '@app/core' | 2 | import { AuthService } from '@app/core' |
3 | import { FormReactive, UserService, UserValidatorsService } from '@app/shared' | 3 | import { FormReactive, UserService, UserValidatorsService } from '@app/shared' |
4 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 4 | import { 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 | }) |
14 | export class RegisterStepUserComponent extends FormReactive implements OnInit { | 14 | export 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 | ||
24 | my-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 @@ | |||
1 | import { Component } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' | 2 | import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' |
3 | import { UserService, UserValidatorsService } from '@app/shared' | 3 | import { UserService, UserValidatorsService } from '@app/shared' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { UserRegister } from '@shared/models/users/user-register.model' | 5 | import { UserRegister } from '@shared/models/users/user-register.model' |
6 | import { FormGroup } from '@angular/forms' | 6 | import { FormGroup } from '@angular/forms' |
7 | import { About } from '@shared/models/server' | ||
8 | import { InstanceService } from '@app/shared/instance/instance.service' | ||
9 | import { 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 | }) |
13 | export class RegisterComponent { | 16 | export 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' | |||
7 | import { RegisterStepUserComponent } from './register-step-user.component' | 7 | import { RegisterStepUserComponent } from './register-step-user.component' |
8 | import { CustomStepperComponent } from './custom-stepper.component' | 8 | import { CustomStepperComponent } from './custom-stepper.component' |
9 | import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module' | 9 | import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module' |
10 | import { 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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' | 2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser' |
3 | import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router' | 3 | import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router' |
4 | import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' | 4 | import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' |
5 | import { is18nPath } from '../../../shared/models/i18n' | 5 | import { is18nPath } from '../../../shared/models/i18n' |
6 | import { ScreenService } from '@app/shared/misc/screen.service' | 6 | import { ScreenService } from '@app/shared/misc/screen.service' |
7 | import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators' | 7 | import { debounceTime, filter, map, pairwise, skip, switchMap } from 'rxjs/operators' |
8 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 8 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | 9 | import { I18n } from '@ngx-translate/i18n-polyfill' |
10 | import { fromEvent } from 'rxjs' | 10 | import { fromEvent } from 'rxjs' |
@@ -13,6 +13,11 @@ import { PluginService } from '@app/core/plugins/plugin.service' | |||
13 | import { HooksService } from '@app/core/plugins/hooks.service' | 13 | import { HooksService } from '@app/core/plugins/hooks.service' |
14 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 14 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
15 | import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants' | 15 | import { POP_STATE_MODAL_DISMISS } from '@app/shared/misc/constants' |
16 | import { WelcomeModalComponent } from '@app/modal/welcome-modal.component' | ||
17 | import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component' | ||
18 | import { UserRole } from '@shared/models' | ||
19 | import { User } from '@app/shared' | ||
20 | import { 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 | }) |
22 | export class AppComponent implements OnInit { | 27 | export 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' | |||
18 | import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' | 18 | import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' |
19 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | 19 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' |
20 | import { SearchModule } from '@app/search' | 20 | import { SearchModule } from '@app/search' |
21 | import { WelcomeModalComponent } from '@app/modal/welcome-modal.component' | ||
22 | import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component' | ||
21 | 23 | ||
22 | export function metaFactory (serverService: ServerService): MetaLoader { | 24 | export 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 | |||
12 | li { | ||
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 @@ | |||
1 | import { Component, ElementRef, ViewChild } from '@angular/core' | ||
2 | import { Notifier } from '@app/core' | ||
3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
4 | import { About } from '@shared/models/server' | ||
5 | import { 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 | }) | ||
12 | export 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 | |||
23 | li { | ||
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 @@ | |||
1 | import { Component, ElementRef, ViewChild } from '@angular/core' | ||
2 | import { Notifier } from '@app/core' | ||
3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
4 | import { 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 | }) | ||
11 | export 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 | }) |
6 | export class PeerTubeTemplateDirective { | 6 | export 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 @@ | |||
1 | import { ChangeDetectorRef, Component, forwardRef, Input, OnChanges, SimpleChanges } from '@angular/core' | 1 | import { AfterContentInit, ChangeDetectorRef, Component, ContentChildren, forwardRef, Input, QueryList, TemplateRef } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { 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 | }) |
16 | export class PeertubeCheckboxComponent implements ControlValueAccessor { | 17 | export 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 @@ | |||
1 | import { 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 | }) | ||
8 | export 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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ServerService } from '@app/core' | 2 | import { ServerService } from '@app/core' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { 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 | }) |
10 | export class InstanceFeaturesTableComponent implements OnInit { | 11 | export 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' | |||
4 | import { environment } from '../../../environments/environment' | 4 | import { environment } from '../../../environments/environment' |
5 | import { RestExtractor, RestService } from '../rest' | 5 | import { RestExtractor, RestService } from '../rest' |
6 | import { About } from '../../../../../shared/models/server' | 6 | import { About } from '../../../../../shared/models/server' |
7 | import { MarkdownService } from '@app/shared/renderer' | ||
8 | import { peertubeTranslate } from '@shared/models' | ||
9 | import { ServerService } from '@app/core' | ||
7 | 10 | ||
8 | @Injectable() | 11 | @Injectable() |
9 | export class InstanceService { | 12 | export 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 @@ | |||
1 | import { Component, Input, OnChanges, OnInit } from '@angular/core' | 1 | import { AfterContentInit, Component, ContentChildren, Input, OnChanges, OnInit, QueryList, TemplateRef } from '@angular/core' |
2 | import { I18n } from '@ngx-translate/i18n-polyfill' | 2 | import { I18n } from '@ngx-translate/i18n-polyfill' |
3 | import { MarkdownService } from '@app/shared/renderer' | 3 | import { MarkdownService } from '@app/shared/renderer' |
4 | import { 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 | ||
11 | export class HelpComponent implements OnInit, OnChanges { | 12 | export 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' | |||
6 | import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' | 6 | import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' |
7 | import { HelpComponent } from '@app/shared/misc/help.component' | 7 | import { HelpComponent } from '@app/shared/misc/help.component' |
8 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' | 8 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' |
9 | |||
10 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' | 9 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' |
11 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' | 10 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' |
12 | |||
13 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' | 11 | import { AUTH_INTERCEPTOR_PROVIDER } from './auth' |
14 | import { ButtonComponent } from './buttons/button.component' | 12 | import { ButtonComponent } from './buttons/button.component' |
15 | import { DeleteButtonComponent } from './buttons/delete-button.component' | 13 | import { DeleteButtonComponent } from './buttons/delete-button.component' |
@@ -93,6 +91,8 @@ import { VideoDownloadComponent } from '@app/shared/video/modals/video-download. | |||
93 | import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' | 91 | import { VideoReportComponent } from '@app/shared/video/modals/video-report.component' |
94 | import { ClipboardModule } from 'ngx-clipboard' | 92 | import { ClipboardModule } from 'ngx-clipboard' |
95 | import { FollowService } from '@app/shared/instance/follow.service' | 93 | import { FollowService } from '@app/shared/instance/follow.service' |
94 | import { MultiSelectModule } from 'primeng/multiselect' | ||
95 | import { 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 | ||
3 | class RedundancyUrlManager { | 3 | class 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({ |