diff options
author | Chocobozzz <me@florianbigard.com> | 2019-02-11 11:52:34 +0100 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-02-11 11:52:34 +0100 |
commit | 88108880bbdba473cfe36ecbebc1c3c4f972e102 (patch) | |
tree | b242efb3b4f0d7e49d88f2d1f2063b5b3b0489c0 /client/src/app | |
parent | 53a94c7cfa8368da4cd248d65df8346905938f0c (diff) | |
parent | 9b712a2017e4ab3cf12cd6bd58278905520159d0 (diff) | |
download | PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.gz PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.tar.zst PeerTube-88108880bbdba473cfe36ecbebc1c3c4f972e102.zip |
Merge branch 'develop' into pr/1217
Diffstat (limited to 'client/src/app')
286 files changed, 4918 insertions, 1800 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 5970cac01..8c700752e 100644 --- a/client/src/app/+about/about-instance/about-instance.component.html +++ b/client/src/app/+about/about-instance/about-instance.component.html | |||
@@ -1,39 +1,52 @@ | |||
1 | <div i18n class="about-instance-title"> | 1 | <div class="row"> |
2 | About {{ instanceName }} instance | 2 | <div class="col-md-12 col-xl-6"> |
3 | </div> | 3 | <div class="about-instance-title"> |
4 | <div i18n>About {{ instanceName }} instance</div> | ||
4 | 5 | ||
5 | <div class="short-description"> | 6 | <div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div> |
6 | <div>{{ shortDescription }}</div> | 7 | </div> |
7 | </div> | ||
8 | 8 | ||
9 | <div class="description"> | 9 | <div class="short-description"> |
10 | <div i18n class="section-title">Description</div> | 10 | <div>{{ shortDescription }}</div> |
11 | </div> | ||
11 | 12 | ||
12 | <div [innerHTML]="descriptionHTML"></div> | 13 | <div class="description"> |
13 | </div> | 14 | <div i18n class="section-title">Description</div> |
14 | 15 | ||
15 | <div class="terms" id="terms-section"> | 16 | <div [innerHTML]="descriptionHTML"></div> |
16 | <div i18n class="section-title">Terms</div> | 17 | </div> |
17 | 18 | ||
18 | <div [innerHTML]="termsHTML"></div> | 19 | <div class="terms" id="terms-section"> |
19 | </div> | 20 | <div i18n class="section-title">Terms</div> |
20 | 21 | ||
21 | <div class="signup"> | 22 | <div [innerHTML]="termsHTML"></div> |
22 | <div i18n class="section-title">Signup</div> | 23 | </div> |
23 | 24 | ||
24 | <div *ngIf="isSignupAllowed"> | 25 | <div class="signup"> |
25 | <ng-container i18n>User registration is allowed and</ng-container> | 26 | <div i18n class="section-title">Signup</div> |
26 | 27 | ||
27 | <ng-container i18n *ngIf="userVideoQuota !== -1"> | 28 | <div *ngIf="isSignupAllowed"> |
28 | this instance provides a baseline quota of {{ userVideoQuota | bytes: 0 }} space for the videos of its users. | 29 | <ng-container i18n>User registration is allowed and</ng-container> |
29 | </ng-container> | ||
30 | 30 | ||
31 | <ng-container i18n *ngIf="userVideoQuota === -1"> | 31 | <ng-container i18n *ngIf="userVideoQuota !== -1"> |
32 | this instance provides unlimited space for the videos of its users. | 32 | this instance provides a baseline quota of {{ userVideoQuota | bytes: 0 }} space for the videos of its users. |
33 | </ng-container> | 33 | </ng-container> |
34 | |||
35 | <ng-container i18n *ngIf="userVideoQuota === -1"> | ||
36 | this instance provides unlimited space for the videos of its users. | ||
37 | </ng-container> | ||
38 | </div> | ||
39 | |||
40 | <div i18n *ngIf="isSignupAllowed === false"> | ||
41 | User registration is currently not allowed. | ||
42 | </div> | ||
43 | </div> | ||
34 | </div> | 44 | </div> |
35 | 45 | ||
36 | <div i18n *ngIf="isSignupAllowed === false"> | 46 | <div class="col-md-12 col-xl-6"> |
37 | User registration is currently not allowed. | 47 | <label>Features found on this instance</label> |
48 | <my-instance-features-table></my-instance-features-table> | ||
38 | </div> | 49 | </div> |
39 | </div> \ No newline at end of file | 50 | </div> |
51 | |||
52 | <my-contact-admin-modal #contactAdminModal></my-contact-admin-modal> | ||
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 b451e85aa..75cf57322 100644 --- a/client/src/app/+about/about-instance/about-instance.component.scss +++ b/client/src/app/+about/about-instance/about-instance.component.scss | |||
@@ -2,9 +2,19 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .about-instance-title { | 4 | .about-instance-title { |
5 | font-size: 20px; | 5 | display: flex; |
6 | font-weight: bold; | 6 | justify-content: space-between; |
7 | margin-bottom: 15px; | 7 | |
8 | & > div { | ||
9 | font-size: 20px; | ||
10 | font-weight: bold; | ||
11 | margin-bottom: 15px; | ||
12 | } | ||
13 | |||
14 | & > .contact-admin { | ||
15 | @include peertube-button; | ||
16 | @include orange-button; | ||
17 | } | ||
8 | } | 18 | } |
9 | 19 | ||
10 | .section-title { | 20 | .section-title { |
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 354f52ce7..a1b30fa8c 100644 --- a/client/src/app/+about/about-instance/about-instance.component.ts +++ b/client/src/app/+about/about-instance/about-instance.component.ts | |||
@@ -1,23 +1,26 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { ServerService } from '@app/core' | 2 | import { Notifier, ServerService } from '@app/core' |
3 | import { MarkdownService } from '@app/videos/shared' | ||
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' | ||
5 | import { InstanceService } from '@app/shared/instance/instance.service' | ||
6 | import { MarkdownService } from '@app/shared/renderer' | ||
6 | 7 | ||
7 | @Component({ | 8 | @Component({ |
8 | selector: 'my-about-instance', | 9 | selector: 'my-about-instance', |
9 | templateUrl: './about-instance.component.html', | 10 | templateUrl: './about-instance.component.html', |
10 | styleUrls: [ './about-instance.component.scss' ] | 11 | styleUrls: [ './about-instance.component.scss' ] |
11 | }) | 12 | }) |
12 | |||
13 | export class AboutInstanceComponent implements OnInit { | 13 | export class AboutInstanceComponent implements OnInit { |
14 | @ViewChild('contactAdminModal') contactAdminModal: ContactAdminModalComponent | ||
15 | |||
14 | shortDescription = '' | 16 | shortDescription = '' |
15 | descriptionHTML = '' | 17 | descriptionHTML = '' |
16 | termsHTML = '' | 18 | termsHTML = '' |
17 | 19 | ||
18 | constructor ( | 20 | constructor ( |
19 | private notificationsService: NotificationsService, | 21 | private notifier: Notifier, |
20 | private serverService: ServerService, | 22 | private serverService: ServerService, |
23 | private instanceService: InstanceService, | ||
21 | private markdownService: MarkdownService, | 24 | private markdownService: MarkdownService, |
22 | private i18n: I18n | 25 | private i18n: I18n |
23 | ) {} | 26 | ) {} |
@@ -34,8 +37,12 @@ export class AboutInstanceComponent implements OnInit { | |||
34 | return this.serverService.getConfig().signup.allowed | 37 | return this.serverService.getConfig().signup.allowed |
35 | } | 38 | } |
36 | 39 | ||
40 | get isContactFormEnabled () { | ||
41 | return this.serverService.getConfig().email.enabled && this.serverService.getConfig().contactForm.enabled | ||
42 | } | ||
43 | |||
37 | ngOnInit () { | 44 | ngOnInit () { |
38 | this.serverService.getAbout() | 45 | this.instanceService.getAbout() |
39 | .subscribe( | 46 | .subscribe( |
40 | res => { | 47 | res => { |
41 | this.shortDescription = res.instance.shortDescription | 48 | this.shortDescription = res.instance.shortDescription |
@@ -43,8 +50,12 @@ export class AboutInstanceComponent implements OnInit { | |||
43 | this.termsHTML = this.markdownService.textMarkdownToHTML(res.instance.terms) | 50 | this.termsHTML = this.markdownService.textMarkdownToHTML(res.instance.terms) |
44 | }, | 51 | }, |
45 | 52 | ||
46 | err => this.notificationsService.error(this.i18n('Error getting about from server'), err) | 53 | () => this.notifier.error(this.i18n('Cannot get about information from server')) |
47 | ) | 54 | ) |
48 | } | 55 | } |
49 | 56 | ||
57 | openContactModal () { | ||
58 | return this.contactAdminModal.show() | ||
59 | } | ||
60 | |||
50 | } | 61 | } |
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.html b/client/src/app/+about/about-instance/contact-admin-modal.component.html new file mode 100644 index 000000000..b2cbd0873 --- /dev/null +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.html | |||
@@ -0,0 +1,50 @@ | |||
1 | <ng-template #modal> | ||
2 | <div class="modal-header"> | ||
3 | <h4 i18n class="modal-title">Contact {{ instanceName }} 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 | <form novalidate [formGroup]="form" (ngSubmit)="sendForm()"> | ||
10 | <div class="form-group"> | ||
11 | <label i18n for="fromName">Your name</label> | ||
12 | <input | ||
13 | type="text" id="fromName" | ||
14 | formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }" | ||
15 | > | ||
16 | <div *ngIf="formErrors.fromName" class="form-error">{{ formErrors.fromName }}</div> | ||
17 | </div> | ||
18 | |||
19 | <div class="form-group"> | ||
20 | <label i18n for="fromEmail">Your email</label> | ||
21 | <input | ||
22 | type="text" id="fromEmail" | ||
23 | formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }" | ||
24 | > | ||
25 | <div *ngIf="formErrors.fromEmail" class="form-error">{{ formErrors.fromEmail }}</div> | ||
26 | </div> | ||
27 | |||
28 | <div class="form-group"> | ||
29 | <label i18n for="body">Your message</label> | ||
30 | <textarea id="body" formControlName="body" [ngClass]="{ 'input-error': formErrors['body'] }"> | ||
31 | </textarea> | ||
32 | <div *ngIf="formErrors.body" class="form-error">{{ formErrors.body }}</div> | ||
33 | </div> | ||
34 | |||
35 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | ||
36 | |||
37 | <div class="form-group inputs"> | ||
38 | <span i18n class="action-button action-button-cancel" (click)="hide()"> | ||
39 | Cancel | ||
40 | </span> | ||
41 | |||
42 | <input | ||
43 | type="submit" i18n-value value="Submit" class="action-button-submit" | ||
44 | [disabled]="!form.valid" | ||
45 | > | ||
46 | </div> | ||
47 | </form> | ||
48 | |||
49 | </div> | ||
50 | </ng-template> | ||
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.scss b/client/src/app/+about/about-instance/contact-admin-modal.component.scss new file mode 100644 index 000000000..260d77888 --- /dev/null +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.scss | |||
@@ -0,0 +1,11 @@ | |||
1 | @import 'variables'; | ||
2 | @import 'mixins'; | ||
3 | |||
4 | input[type=text] { | ||
5 | @include peertube-input-text(340px); | ||
6 | display: block; | ||
7 | } | ||
8 | |||
9 | textarea { | ||
10 | @include peertube-textarea(100%, 200px); | ||
11 | } | ||
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.ts b/client/src/app/+about/about-instance/contact-admin-modal.component.ts new file mode 100644 index 000000000..7d79c2215 --- /dev/null +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.ts | |||
@@ -0,0 +1,77 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | ||
2 | import { Notifier, ServerService } from '@app/core' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
6 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
7 | import { FormReactive, InstanceValidatorsService } from '@app/shared' | ||
8 | import { InstanceService } from '@app/shared/instance/instance.service' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-contact-admin-modal', | ||
12 | templateUrl: './contact-admin-modal.component.html', | ||
13 | styleUrls: [ './contact-admin-modal.component.scss' ] | ||
14 | }) | ||
15 | export class ContactAdminModalComponent extends FormReactive implements OnInit { | ||
16 | @ViewChild('modal') modal: NgbModal | ||
17 | |||
18 | error: string | ||
19 | |||
20 | private openedModal: NgbModalRef | ||
21 | |||
22 | constructor ( | ||
23 | protected formValidatorService: FormValidatorService, | ||
24 | private modalService: NgbModal, | ||
25 | private instanceValidatorsService: InstanceValidatorsService, | ||
26 | private instanceService: InstanceService, | ||
27 | private serverService: ServerService, | ||
28 | private notifier: Notifier, | ||
29 | private i18n: I18n | ||
30 | ) { | ||
31 | super() | ||
32 | } | ||
33 | |||
34 | get instanceName () { | ||
35 | return this.serverService.getConfig().instance.name | ||
36 | } | ||
37 | |||
38 | ngOnInit () { | ||
39 | this.buildForm({ | ||
40 | fromName: this.instanceValidatorsService.FROM_NAME, | ||
41 | fromEmail: this.instanceValidatorsService.FROM_EMAIL, | ||
42 | body: this.instanceValidatorsService.BODY | ||
43 | }) | ||
44 | } | ||
45 | |||
46 | show () { | ||
47 | this.openedModal = this.modalService.open(this.modal, { keyboard: false }) | ||
48 | } | ||
49 | |||
50 | hide () { | ||
51 | this.form.reset() | ||
52 | this.error = undefined | ||
53 | |||
54 | this.openedModal.close() | ||
55 | this.openedModal = null | ||
56 | } | ||
57 | |||
58 | sendForm () { | ||
59 | const fromName = this.form.value['fromName'] | ||
60 | const fromEmail = this.form.value[ 'fromEmail' ] | ||
61 | const body = this.form.value[ 'body' ] | ||
62 | |||
63 | this.instanceService.contactAdministrator(fromEmail, fromName, body) | ||
64 | .subscribe( | ||
65 | () => { | ||
66 | this.notifier.success(this.i18n('Your message has been sent.')) | ||
67 | this.hide() | ||
68 | }, | ||
69 | |||
70 | err => { | ||
71 | this.error = err.status === 403 | ||
72 | ? this.i18n('You already sent this form recently') | ||
73 | : err.message | ||
74 | } | ||
75 | ) | ||
76 | } | ||
77 | } | ||
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 13ce89f75..d3fc9a828 100644 --- a/client/src/app/+about/about-peertube/about-peertube.component.html +++ b/client/src/app/+about/about-peertube/about-peertube.component.html | |||
@@ -83,7 +83,7 @@ | |||
83 | <h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6> | 83 | <h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6> |
84 | 84 | ||
85 | <p i18n> | 85 | <p i18n> |
86 | PeerTube is only in beta, and want to deliver the best countermeasures possible by the time the stable is released. | 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: | 87 | In the meantime, we want to test different ideas related to this issue: |
88 | </p> | 88 | </p> |
89 | 89 | ||
@@ -94,4 +94,4 @@ | |||
94 | <li i18n>Disable P2P from the administration interface</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> | 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> | 96 | </ul> |
97 | </div> \ No newline at end of file | 97 | </div> |
diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts index ff6e8ef41..9c6b29740 100644 --- a/client/src/app/+about/about.module.ts +++ b/client/src/app/+about/about.module.ts | |||
@@ -5,6 +5,7 @@ import { AboutComponent } from './about.component' | |||
5 | import { SharedModule } from '../shared' | 5 | import { SharedModule } from '../shared' |
6 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' | 6 | import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component' |
7 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' | 7 | import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component' |
8 | import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' | ||
8 | 9 | ||
9 | @NgModule({ | 10 | @NgModule({ |
10 | imports: [ | 11 | imports: [ |
@@ -15,7 +16,8 @@ import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertub | |||
15 | declarations: [ | 16 | declarations: [ |
16 | AboutComponent, | 17 | AboutComponent, |
17 | AboutInstanceComponent, | 18 | AboutInstanceComponent, |
18 | AboutPeertubeComponent | 19 | AboutPeertubeComponent, |
20 | ContactAdminModalComponent | ||
19 | ], | 21 | ], |
20 | 22 | ||
21 | exports: [ | 23 | exports: [ |
diff --git a/client/src/app/+accounts/account-about/account-about.component.ts b/client/src/app/+accounts/account-about/account-about.component.ts index 6f3e6caa0..13890a0ee 100644 --- a/client/src/app/+accounts/account-about/account-about.component.ts +++ b/client/src/app/+accounts/account-about/account-about.component.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { Component, OnInit, OnDestroy } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { Account } from '@app/shared/account/account.model' | 2 | import { Account } from '@app/shared/account/account.model' |
3 | import { AccountService } from '@app/shared/account/account.service' | 3 | import { AccountService } from '@app/shared/account/account.service' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { Subscription } from 'rxjs' | 5 | import { Subscription } from 'rxjs' |
6 | import { MarkdownService } from '@app/videos/shared' | 6 | import { MarkdownService } from '@app/shared/renderer' |
7 | 7 | ||
8 | @Component({ | 8 | @Component({ |
9 | selector: 'my-account-about', | 9 | selector: 'my-account-about', |
diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts index e5c1f58b0..13b634a01 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-videos.component.ts | |||
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core' | |||
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Location } from '@angular/common' | 3 | import { Location } from '@angular/common' |
4 | import { immutableAssign } from '@app/shared/misc/utils' | 4 | import { immutableAssign } from '@app/shared/misc/utils' |
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
7 | import { ConfirmService } from '../../core/confirm' | 6 | import { ConfirmService } from '../../core/confirm' |
8 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 7 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
@@ -13,6 +12,7 @@ import { tap } from 'rxjs/operators' | |||
13 | import { I18n } from '@ngx-translate/i18n-polyfill' | 12 | import { I18n } from '@ngx-translate/i18n-polyfill' |
14 | import { Subscription } from 'rxjs' | 13 | import { Subscription } from 'rxjs' |
15 | import { ScreenService } from '@app/shared/misc/screen.service' | 14 | import { ScreenService } from '@app/shared/misc/screen.service' |
15 | import { Notifier } from '@app/core' | ||
16 | 16 | ||
17 | @Component({ | 17 | @Component({ |
18 | selector: 'my-account-videos', | 18 | selector: 'my-account-videos', |
@@ -35,7 +35,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit, | |||
35 | protected router: Router, | 35 | protected router: Router, |
36 | protected route: ActivatedRoute, | 36 | protected route: ActivatedRoute, |
37 | protected authService: AuthService, | 37 | protected authService: AuthService, |
38 | protected notificationsService: NotificationsService, | 38 | protected notifier: Notifier, |
39 | protected confirmService: ConfirmService, | 39 | protected confirmService: ConfirmService, |
40 | protected location: Location, | 40 | protected location: Location, |
41 | protected screenService: ScreenService, | 41 | protected screenService: ScreenService, |
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 036e794d2..c1377c1ea 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -10,8 +10,15 @@ | |||
10 | <div class="actor-name">{{ account.nameWithHost }}</div> | 10 | <div class="actor-name">{{ account.nameWithHost }}</div> |
11 | 11 | ||
12 | <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span> | 12 | <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span> |
13 | 13 | <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> | |
14 | <my-user-moderation-dropdown buttonSize="small" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"> | 14 | <span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Muted by your instance</span> |
15 | <span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Instance muted</span> | ||
16 | <span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span> | ||
17 | |||
18 | <my-user-moderation-dropdown | ||
19 | buttonSize="small" [account]="account" [user]="user" | ||
20 | (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()" | ||
21 | > | ||
15 | </my-user-moderation-dropdown> | 22 | </my-user-moderation-dropdown> |
16 | </div> | 23 | </div> |
17 | <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div> | 24 | <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div> |
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index e19927d6b..e8339b78b 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -5,10 +5,9 @@ import { Account } from '@app/shared/account/account.model' | |||
5 | import { RestExtractor, UserService } from '@app/shared' | 5 | import { RestExtractor, UserService } from '@app/shared' |
6 | import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' | 6 | import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators' |
7 | import { Subscription } from 'rxjs' | 7 | import { Subscription } from 'rxjs' |
8 | import { NotificationsService } from 'angular2-notifications' | 8 | import { AuthService, Notifier, RedirectService } from '@app/core' |
9 | import { User, UserRight } from '../../../../shared' | 9 | import { User, UserRight } from '../../../../shared' |
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { AuthService, RedirectService } from '@app/core' | ||
12 | 11 | ||
13 | @Component({ | 12 | @Component({ |
14 | templateUrl: './accounts.component.html', | 13 | templateUrl: './accounts.component.html', |
@@ -24,11 +23,10 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
24 | private route: ActivatedRoute, | 23 | private route: ActivatedRoute, |
25 | private userService: UserService, | 24 | private userService: UserService, |
26 | private accountService: AccountService, | 25 | private accountService: AccountService, |
27 | private notificationsService: NotificationsService, | 26 | private notifier: Notifier, |
28 | private restExtractor: RestExtractor, | 27 | private restExtractor: RestExtractor, |
29 | private redirectService: RedirectService, | 28 | private redirectService: RedirectService, |
30 | private authService: AuthService, | 29 | private authService: AuthService |
31 | private i18n: I18n | ||
32 | ) {} | 30 | ) {} |
33 | 31 | ||
34 | ngOnInit () { | 32 | ngOnInit () { |
@@ -43,7 +41,7 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
43 | .subscribe( | 41 | .subscribe( |
44 | account => this.account = account, | 42 | account => this.account = account, |
45 | 43 | ||
46 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 44 | err => this.notifier.error(err.message) |
47 | ) | 45 | ) |
48 | } | 46 | } |
49 | 47 | ||
@@ -69,7 +67,7 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
69 | .subscribe( | 67 | .subscribe( |
70 | user => this.user = user, | 68 | user => this.user = user, |
71 | 69 | ||
72 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 70 | err => this.notifier.error(err.message) |
73 | ) | 71 | ) |
74 | } | 72 | } |
75 | } | 73 | } |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 8c6db98d9..f7f347105 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -10,11 +10,12 @@ import { FollowingListComponent } from './follows/following-list/following-list. | |||
10 | import { JobsComponent } from './jobs/job.component' | 10 | import { JobsComponent } from './jobs/job.component' |
11 | import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' | 11 | import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' |
12 | import { JobService } from './jobs/shared/job.service' | 12 | import { JobService } from './jobs/shared/job.service' |
13 | import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' | 13 | import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users' |
14 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' | 14 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' |
15 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' | 15 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' |
16 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' | 16 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' |
17 | import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' | 17 | import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' |
18 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' | ||
18 | 19 | ||
19 | @NgModule({ | 20 | @NgModule({ |
20 | imports: [ | 21 | imports: [ |
@@ -35,12 +36,15 @@ import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service | |||
35 | UsersComponent, | 36 | UsersComponent, |
36 | UserCreateComponent, | 37 | UserCreateComponent, |
37 | UserUpdateComponent, | 38 | UserUpdateComponent, |
39 | UserPasswordComponent, | ||
38 | UserListComponent, | 40 | UserListComponent, |
39 | 41 | ||
40 | ModerationComponent, | 42 | ModerationComponent, |
41 | VideoBlacklistListComponent, | 43 | VideoBlacklistListComponent, |
42 | VideoAbuseListComponent, | 44 | VideoAbuseListComponent, |
43 | ModerationCommentModalComponent, | 45 | ModerationCommentModalComponent, |
46 | InstanceServerBlocklistComponent, | ||
47 | InstanceAccountBlocklistComponent, | ||
44 | 48 | ||
45 | JobsComponent, | 49 | JobsComponent, |
46 | JobsListComponent, | 50 | JobsListComponent, |
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 e2cbd35ca..52eb00d93 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 | |||
@@ -7,161 +7,169 @@ | |||
7 | 7 | ||
8 | <div i18n class="inner-form-title">Instance</div> | 8 | <div i18n class="inner-form-title">Instance</div> |
9 | 9 | ||
10 | <div class="form-group"> | 10 | <ng-container formGroupName="instance"> |
11 | <label i18n for="instanceName">Name</label> | 11 | <div class="form-group"> |
12 | <input | 12 | <label i18n for="instanceName">Name</label> |
13 | type="text" id="instanceName" | 13 | <input |
14 | formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }" | 14 | type="text" id="instanceName" |
15 | > | 15 | formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }" |
16 | <div *ngIf="formErrors.instanceName" class="form-error"> | 16 | > |
17 | {{ formErrors.instanceName }} | 17 | <div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div> |
18 | </div> | 18 | </div> |
19 | </div> | ||
20 | 19 | ||
21 | <div class="form-group"> | 20 | <div class="form-group"> |
22 | <label i18n for="instanceShortDescription">Short description</label> | 21 | <label i18n for="instanceShortDescription">Short description</label> |
23 | <textarea | 22 | <textarea |
24 | id="instanceShortDescription" formControlName="instanceShortDescription" | 23 | id="instanceShortDescription" formControlName="shortDescription" |
25 | [ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }" | 24 | [ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }" |
26 | ></textarea> | 25 | ></textarea> |
27 | <div *ngIf="formErrors.instanceShortDescription" class="form-error"> | 26 | <div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div> |
28 | {{ formErrors.instanceShortDescription }} | ||
29 | </div> | 27 | </div> |
30 | </div> | ||
31 | 28 | ||
32 | <div class="form-group"> | 29 | <div class="form-group"> |
33 | <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help> | 30 | <label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help> |
34 | <my-markdown-textarea | 31 | <my-markdown-textarea |
35 | id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true" | 32 | id="instanceDescription" formControlName="description" textareaWidth="500px" [previewColumn]="true" |
36 | [classes]="{ 'input-error': formErrors['instanceDescription'] }" | 33 | [classes]="{ 'input-error': formErrors['instance.description'] }" |
37 | ></my-markdown-textarea> | 34 | ></my-markdown-textarea> |
38 | <div *ngIf="formErrors.instanceDescription" class="form-error"> | 35 | <div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div> |
39 | {{ formErrors.instanceDescription }} | ||
40 | </div> | 36 | </div> |
41 | </div> | ||
42 | 37 | ||
43 | <div class="form-group"> | 38 | <div class="form-group"> |
44 | <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help> | 39 | <label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help> |
45 | <my-markdown-textarea | 40 | <my-markdown-textarea |
46 | id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true" | 41 | id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true" |
47 | [ngClass]="{ 'input-error': formErrors['instanceTerms'] }" | 42 | [ngClass]="{ 'input-error': formErrors['instance.terms'] }" |
48 | ></my-markdown-textarea> | 43 | ></my-markdown-textarea> |
49 | <div *ngIf="formErrors.instanceTerms" class="form-error"> | 44 | <div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div> |
50 | {{ formErrors.instanceTerms }} | ||
51 | </div> | 45 | </div> |
52 | </div> | ||
53 | 46 | ||
54 | <div class="form-group"> | 47 | <div class="form-group"> |
55 | <label i18n for="instanceDefaultClientRoute">Default client route</label> | 48 | <label i18n for="instanceDefaultClientRoute">Default client route</label> |
56 | <div class="peertube-select-container"> | 49 | <div class="peertube-select-container"> |
57 | <select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute"> | 50 | <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute"> |
58 | <option i18n value="/videos/overview">Videos Overview</option> | 51 | <option i18n value="/videos/overview">Videos Overview</option> |
59 | <option i18n value="/videos/trending">Videos Trending</option> | 52 | <option i18n value="/videos/trending">Videos Trending</option> |
60 | <option i18n value="/videos/recently-added">Videos Recently Added</option> | 53 | <option i18n value="/videos/recently-added">Videos Recently Added</option> |
61 | <option i18n value="/videos/local">Local videos</option> | 54 | <option i18n value="/videos/local">Local videos</option> |
62 | </select> | 55 | </select> |
56 | </div> | ||
57 | <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div> | ||
63 | </div> | 58 | </div> |
64 | <div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error"> | 59 | |
65 | {{ formErrors.instanceDefaultClientRoute }} | 60 | <div class="form-group"> |
61 | <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label> | ||
62 | <my-help | ||
63 | helpType="custom" i18n-customHtml | ||
64 | customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video." | ||
65 | ></my-help> | ||
66 | |||
67 | <div class="peertube-select-container"> | ||
68 | <select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy"> | ||
69 | <option i18n value="do_not_list">Do not list</option> | ||
70 | <option i18n value="blur">Blur thumbnails</option> | ||
71 | <option i18n value="display">Display</option> | ||
72 | </select> | ||
73 | </div> | ||
74 | <div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div> | ||
66 | </div> | 75 | </div> |
67 | </div> | 76 | </ng-container> |
68 | 77 | ||
69 | <div class="form-group"> | 78 | <div i18n class="inner-form-title">Signup</div> |
70 | <label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label> | ||
71 | <my-help | ||
72 | helpType="custom" i18n-customHtml | ||
73 | customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video." | ||
74 | ></my-help> | ||
75 | 79 | ||
76 | <div class="peertube-select-container"> | 80 | <ng-container formGroupName="signup"> |
77 | <select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy"> | 81 | <div class="form-group"> |
78 | <option i18n value="do_not_list">Do not list</option> | 82 | <my-peertube-checkbox |
79 | <option i18n value="blur">Blur thumbnails</option> | 83 | inputName="signupEnabled" formControlName="enabled" |
80 | <option i18n value="display">Display</option> | 84 | i18n-labelText labelText="Signup enabled" |
81 | </select> | 85 | ></my-peertube-checkbox> |
82 | </div> | 86 | </div> |
83 | <div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error"> | 87 | |
84 | {{ formErrors.instanceDefaultNSFWPolicy }} | 88 | <div class="form-group"> |
89 | <my-peertube-checkbox *ngIf="isSignupEnabled()" | ||
90 | inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification" | ||
91 | i18n-labelText labelText="Signup requires email verification" | ||
92 | ></my-peertube-checkbox> | ||
85 | </div> | 93 | </div> |
86 | </div> | ||
87 | 94 | ||
88 | <div i18n class="inner-form-title">Signup</div> | 95 | <div *ngIf="isSignupEnabled()" class="form-group"> |
96 | <label i18n for="signupLimit">Signup limit</label> | ||
97 | <input | ||
98 | type="text" id="signupLimit" | ||
99 | formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }" | ||
100 | > | ||
101 | <div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div> | ||
102 | </div> | ||
103 | </ng-container> | ||
89 | 104 | ||
90 | <my-peertube-checkbox | 105 | <div i18n class="inner-form-title">Users</div> |
91 | inputName="signupEnabled" formControlName="signupEnabled" | ||
92 | i18n-labelText labelText="Signup enabled" | ||
93 | ></my-peertube-checkbox> | ||
94 | 106 | ||
95 | <my-peertube-checkbox *ngIf="isSignupEnabled()" | 107 | <ng-container formGroupName="user"> |
96 | inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification" | 108 | <div class="form-group"> |
97 | i18n-labelText labelText="Signup requires email verification" | 109 | <label i18n for="userVideoQuota">User default video quota</label> |
98 | ></my-peertube-checkbox> | 110 | <div class="peertube-select-container"> |
111 | <select id="userVideoQuota" formControlName="videoQuota"> | ||
112 | <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value"> | ||
113 | {{ videoQuotaOption.label }} | ||
114 | </option> | ||
115 | </select> | ||
116 | </div> | ||
117 | <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div> | ||
118 | </div> | ||
99 | 119 | ||
100 | <div *ngIf="isSignupEnabled()" class="form-group"> | 120 | <div class="form-group"> |
101 | <label i18n for="signupLimit">Signup limit</label> | 121 | <label i18n for="userVideoQuotaDaily">User default daily upload limit</label> |
102 | <input | 122 | <div class="peertube-select-container"> |
103 | type="text" id="signupLimit" | 123 | <select id="userVideoQuotaDaily" formControlName="videoQuotaDaily"> |
104 | formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }" | 124 | <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value"> |
105 | > | 125 | {{ videoQuotaDailyOption.label }} |
106 | <div *ngIf="formErrors.signupLimit" class="form-error"> | 126 | </option> |
107 | {{ formErrors.signupLimit }} | 127 | </select> |
128 | </div> | ||
129 | <div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div> | ||
108 | </div> | 130 | </div> |
109 | </div> | 131 | </ng-container> |
110 | 132 | ||
111 | <div i18n class="inner-form-title">Import</div> | 133 | <div i18n class="inner-form-title">Import</div> |
112 | 134 | ||
113 | <my-peertube-checkbox | 135 | <ng-container formGroupName="import"> |
114 | inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled" | 136 | <ng-container formGroupName="videos"> |
115 | i18n-labelText labelText="Video import with HTTP enabled" | ||
116 | ></my-peertube-checkbox> | ||
117 | 137 | ||
118 | <my-peertube-checkbox | 138 | <div class="form-group" formGroupName="http"> |
119 | inputName="importVideosTorrentEnabled" formControlName="importVideosTorrentEnabled" | 139 | <my-peertube-checkbox |
120 | i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled" | 140 | inputName="importVideosHttpEnabled" formControlName="enabled" |
121 | ></my-peertube-checkbox> | 141 | i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled" |
142 | ></my-peertube-checkbox> | ||
143 | </div> | ||
144 | |||
145 | <div class="form-group" formGroupName="torrent"> | ||
146 | <my-peertube-checkbox | ||
147 | inputName="importVideosTorrentEnabled" formControlName="enabled" | ||
148 | i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled" | ||
149 | ></my-peertube-checkbox> | ||
150 | </div> | ||
151 | |||
152 | </ng-container> | ||
153 | </ng-container> | ||
122 | 154 | ||
123 | <div i18n class="inner-form-title">Administrator</div> | 155 | <div i18n class="inner-form-title">Administrator</div> |
124 | 156 | ||
125 | <div class="form-group"> | 157 | <div class="form-group" formGroupName="admin"> |
126 | <label i18n for="adminEmail">Admin email</label> | 158 | <label i18n for="adminEmail">Admin email</label> |
127 | <input | 159 | <input |
128 | type="text" id="adminEmail" | 160 | type="text" id="adminEmail" |
129 | formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }" | 161 | formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }" |
130 | > | 162 | > |
131 | <div *ngIf="formErrors.adminEmail" class="form-error"> | 163 | <div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div> |
132 | {{ formErrors.adminEmail }} | ||
133 | </div> | ||
134 | </div> | 164 | </div> |
135 | 165 | ||
136 | <div i18n class="inner-form-title">Users</div> | 166 | <div class="form-group" formGroupName="contactForm"> |
137 | 167 | <my-peertube-checkbox | |
138 | <div class="form-group"> | 168 | inputName="enableContactForm" formControlName="enabled" |
139 | <label i18n for="userVideoQuota">User default video quota</label> | 169 | i18n-labelText labelText="Enable contact form" |
140 | <div class="peertube-select-container"> | 170 | ></my-peertube-checkbox> |
141 | <select id="userVideoQuota" formControlName="userVideoQuota"> | ||
142 | <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value"> | ||
143 | {{ videoQuotaOption.label }} | ||
144 | </option> | ||
145 | </select> | ||
146 | </div> | ||
147 | <div *ngIf="formErrors.userVideoQuota" class="form-error"> | ||
148 | {{ formErrors.userVideoQuota }} | ||
149 | </div> | ||
150 | </div> | 171 | </div> |
151 | 172 | ||
152 | <div class="form-group"> | ||
153 | <label i18n for="userVideoQuotaDaily">User default daily upload limit</label> | ||
154 | <div class="peertube-select-container"> | ||
155 | <select id="userVideoQuotaDaily" formControlName="userVideoQuotaDaily"> | ||
156 | <option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value"> | ||
157 | {{ videoQuotaDailyOption.label }} | ||
158 | </option> | ||
159 | </select> | ||
160 | </div> | ||
161 | <div *ngIf="formErrors.userVideoQuotaDaily" class="form-error"> | ||
162 | {{ formErrors.userVideoQuotaDaily }} | ||
163 | </div> | ||
164 | </div> | ||
165 | </ng-template> | 173 | </ng-template> |
166 | </ngb-tab> | 174 | </ngb-tab> |
167 | 175 | ||
@@ -169,28 +177,35 @@ | |||
169 | <ng-template ngbTabContent> | 177 | <ng-template ngbTabContent> |
170 | <div i18n class="inner-form-title">Twitter</div> | 178 | <div i18n class="inner-form-title">Twitter</div> |
171 | 179 | ||
172 | <div class="form-group"> | 180 | <ng-container formGroupName="services"> |
173 | <label i18n for="signupLimit">Your Twitter username</label> | 181 | <ng-container formGroupName="twitter"> |
174 | <my-help | 182 | |
175 | helpType="custom" i18n-customHtml | 183 | <div class="form-group"> |
176 | customHtml="Indicates the Twitter account for the website or platform on which the content was published." | 184 | <label i18n for="signupLimit">Your Twitter username</label> |
177 | ></my-help> | 185 | <my-help |
178 | <input | 186 | helpType="custom" i18n-customHtml |
179 | type="text" id="servicesTwitterUsername" | 187 | customHtml="Indicates the Twitter account for the website or platform on which the content was published." |
180 | formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }" | 188 | ></my-help> |
181 | > | 189 | <input |
182 | <div *ngIf="formErrors.servicesTwitterUsername" class="form-error"> | 190 | type="text" id="servicesTwitterUsername" |
183 | {{ formErrors.servicesTwitterUsername }} | 191 | formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }" |
184 | </div> | 192 | > |
185 | </div> | 193 | <div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div> |
194 | </div> | ||
195 | |||
196 | <div class="form-group"> | ||
197 | <my-peertube-checkbox | ||
198 | inputName="servicesTwitterWhitelisted" formControlName="whitelisted" | ||
199 | i18n-labelText labelText="Instance whitelisted by Twitter" | ||
200 | 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 /> | ||
201 | If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br /> | ||
202 | 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." | ||
203 | ></my-peertube-checkbox> | ||
204 | </div> | ||
205 | |||
206 | </ng-container> | ||
207 | </ng-container> | ||
186 | 208 | ||
187 | <my-peertube-checkbox | ||
188 | inputName="servicesTwitterWhitelisted" formControlName="servicesTwitterWhitelisted" | ||
189 | i18n-labelText labelText="Instance whitelisted by Twitter" | ||
190 | 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 /> | ||
191 | If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br /> | ||
192 | 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." | ||
193 | ></my-peertube-checkbox> | ||
194 | </ng-template> | 209 | </ng-template> |
195 | </ngb-tab> | 210 | </ngb-tab> |
196 | 211 | ||
@@ -199,36 +214,48 @@ | |||
199 | 214 | ||
200 | <div i18n class="inner-form-title">Transcoding</div> | 215 | <div i18n class="inner-form-title">Transcoding</div> |
201 | 216 | ||
202 | <my-peertube-checkbox | 217 | <ng-container formGroupName="transcoding"> |
203 | inputName="transcodingEnabled" formControlName="transcodingEnabled" | 218 | <div class="form-group"> |
204 | i18n-labelText labelText="Transcoding enabled" | 219 | <my-peertube-checkbox |
205 | i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!" | 220 | inputName="transcodingEnabled" formControlName="enabled" |
206 | ></my-peertube-checkbox> | 221 | i18n-labelText labelText="Transcoding enabled" |
222 | i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!" | ||
223 | ></my-peertube-checkbox> | ||
224 | </div> | ||
207 | 225 | ||
208 | <ng-template [ngIf]="isTranscodingEnabled()"> | 226 | <ng-container *ngIf="isTranscodingEnabled()"> |
209 | 227 | ||
210 | <div class="form-group"> | 228 | <div class="form-group"> |
211 | <label i18n for="transcodingThreads">Transcoding threads</label> | 229 | <my-peertube-checkbox |
212 | <div class="peertube-select-container"> | 230 | inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions" |
213 | <select id="transcodingThreads" formControlName="transcodingThreads"> | 231 | i18n-labelText labelText="Allow additional extensions" |
214 | <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value"> | 232 | i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos" |
215 | {{ transcodingThreadOption.label }} | 233 | ></my-peertube-checkbox> |
216 | </option> | ||
217 | </select> | ||
218 | </div> | 234 | </div> |
219 | <div *ngIf="formErrors.transcodingThreads" class="form-error"> | 235 | |
220 | {{ formErrors.transcodingThreads }} | 236 | <div class="form-group"> |
237 | <label i18n for="transcodingThreads">Transcoding threads</label> | ||
238 | <div class="peertube-select-container"> | ||
239 | <select id="transcodingThreads" formControlName="threads"> | ||
240 | <option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value"> | ||
241 | {{ transcodingThreadOption.label }} | ||
242 | </option> | ||
243 | </select> | ||
244 | </div> | ||
245 | <div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div> | ||
221 | </div> | 246 | </div> |
222 | </div> | ||
223 | 247 | ||
224 | <div class="form-group" *ngFor="let resolution of resolutions"> | 248 | <ng-container formGroupName="resolutions"> |
225 | <my-peertube-checkbox | 249 | <div class="form-group" *ngFor="let resolution of resolutions"> |
226 | [inputName]="getResolutionKey(resolution)" [formControlName]="getResolutionKey(resolution)" | 250 | <my-peertube-checkbox |
227 | i18n-labelText labelText="Resolution {{resolution}} enabled" | 251 | [inputName]="getResolutionKey(resolution)" [formControlName]="resolution" |
228 | ></my-peertube-checkbox> | 252 | i18n-labelText labelText="Resolution {{resolution}} enabled" |
253 | ></my-peertube-checkbox> | ||
254 | </div> | ||
255 | </ng-container> | ||
229 | 256 | ||
230 | </div> | 257 | </ng-container> |
231 | </ng-template> | 258 | </ng-container> |
232 | 259 | ||
233 | <div i18n class="inner-form-title"> | 260 | <div i18n class="inner-form-title"> |
234 | Cache | 261 | Cache |
@@ -239,74 +266,73 @@ | |||
239 | ></my-help> | 266 | ></my-help> |
240 | </div> | 267 | </div> |
241 | 268 | ||
242 | <div class="form-group"> | 269 | <ng-container formGroupName="cache"> |
243 | <label i18n for="cachePreviewsSize">Previews cache size</label> | 270 | <div class="form-group" formGroupName="previews"> |
244 | <input | 271 | <label i18n for="cachePreviewsSize">Previews cache size</label> |
245 | type="text" id="cachePreviewsSize" | 272 | <input |
246 | formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }" | 273 | type="text" id="cachePreviewsSize" |
247 | > | 274 | formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }" |
248 | <div *ngIf="formErrors.cachePreviewsSize" class="form-error"> | 275 | > |
249 | {{ formErrors.cachePreviewsSize }} | 276 | <div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div> |
250 | </div> | 277 | </div> |
251 | </div> | ||
252 | 278 | ||
253 | <div class="form-group"> | 279 | <div class="form-group" formGroupName="captions"> |
254 | <label i18n for="cachePreviewsSize">Video captions cache size</label> | 280 | <label i18n for="cacheCaptionsSize">Video captions cache size</label> |
255 | <input | 281 | <input |
256 | type="text" id="cacheCaptionsSize" | 282 | type="text" id="cacheCaptionsSize" |
257 | formControlName="cacheCaptionsSize" [ngClass]="{ 'input-error': formErrors['cacheCaptionsSize'] }" | 283 | formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }" |
258 | > | 284 | > |
259 | <div *ngIf="formErrors.cacheCaptionsSize" class="form-error"> | 285 | <div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div> |
260 | {{ formErrors.cacheCaptionsSize }} | ||
261 | </div> | 286 | </div> |
262 | </div> | 287 | </ng-container> |
263 | 288 | ||
264 | <div i18n class="inner-form-title">Customizations</div> | 289 | <div i18n class="inner-form-title">Customizations</div> |
265 | 290 | ||
266 | <div class="form-group"> | 291 | <ng-container formGroupName="instance"> |
267 | <label i18n for="customizationJavascript">JavaScript</label> | 292 | <ng-container formGroupName="customizations"> |
268 | <my-help | 293 | <div class="form-group"> |
269 | helpType="custom" i18n-customHtml | 294 | <label i18n for="customizationJavascript">JavaScript</label> |
270 | customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>" | 295 | <my-help |
271 | ></my-help> | 296 | helpType="custom" i18n-customHtml |
272 | <textarea | 297 | customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>" |
273 | id="customizationJavascript" formControlName="customizationJavascript" | 298 | ></my-help> |
274 | [ngClass]="{ 'input-error': formErrors['customizationJavascript'] }" | 299 | <textarea |
275 | ></textarea> | 300 | id="customizationJavascript" formControlName="javascript" |
276 | <div *ngIf="formErrors.customizationJavascript" class="form-error"> | 301 | [ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }" |
277 | {{ formErrors.customizationJavascript }} | 302 | ></textarea> |
278 | </div> | 303 | <div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div> |
279 | </div> | 304 | </div> |
305 | |||
306 | <div class="form-group"> | ||
307 | <label for="customizationCSS">CSS</label> | ||
308 | <my-help | ||
309 | helpType="custom" | ||
310 | i18n-customHtml | ||
311 | customHtml=" | ||
312 | Write directly CSS code. Example:<br /> | ||
313 | <pre> | ||
314 | body {{ '{' }} | ||
315 | background-color: red; | ||
316 | {{ '}' }} | ||
317 | </pre> | ||
318 | |||
319 | Prepend with <em>#custom-css</em> to override styles. Example: | ||
320 | <pre> | ||
321 | #custom-css .logged-in-email {{ '{' }} | ||
322 | color: red; | ||
323 | {{ '}' }} | ||
324 | </pre> | ||
325 | " | ||
326 | ></my-help> | ||
327 | <textarea | ||
328 | id="customizationCSS" formControlName="css" | ||
329 | [ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }" | ||
330 | ></textarea> | ||
331 | <div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div> | ||
332 | </div> | ||
333 | </ng-container> | ||
334 | </ng-container> | ||
280 | 335 | ||
281 | <div class="form-group"> | ||
282 | <label for="customizationCSS">CSS</label> | ||
283 | <my-help | ||
284 | helpType="custom" | ||
285 | i18n-customHtml | ||
286 | customHtml=" | ||
287 | Write directly CSS code. Example:<br /> | ||
288 | <pre> | ||
289 | body {{ '{' }} | ||
290 | background-color: red; | ||
291 | {{ '}' }} | ||
292 | </pre> | ||
293 | |||
294 | Prepend with <em>#custom-css</em> to override styles. Example: | ||
295 | <pre> | ||
296 | #custom-css .logged-in-email {{ '{' }} | ||
297 | color: red; | ||
298 | {{ '}' }} | ||
299 | </pre> | ||
300 | " | ||
301 | ></my-help> | ||
302 | <textarea | ||
303 | id="customizationCSS" formControlName="customizationCSS" | ||
304 | [ngClass]="{ 'input-error': formErrors['customizationCSS'] }" | ||
305 | ></textarea> | ||
306 | <div *ngIf="formErrors.customizationCSS" class="form-error"> | ||
307 | {{ formErrors.customizationCSS }} | ||
308 | </div> | ||
309 | </div> | ||
310 | </ng-template> | 336 | </ng-template> |
311 | </ngb-tab> | 337 | </ngb-tab> |
312 | </ngb-tabset> | 338 | </ngb-tabset> |
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 4983b0425..654a076b0 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 | |||
@@ -1,9 +1,8 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 2 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
3 | import { ConfirmService } from '@app/core' | ||
4 | import { ServerService } from '@app/core/server/server.service' | 3 | import { ServerService } from '@app/core/server/server.service' |
5 | import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared' | 4 | import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared' |
6 | import { NotificationsService } from 'angular2-notifications' | 5 | import { Notifier } from '@app/core' |
7 | import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model' | 6 | import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model' |
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
9 | import { BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 8 | import { BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
@@ -19,17 +18,13 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
19 | resolutions: string[] = [] | 18 | resolutions: string[] = [] |
20 | transcodingThreadOptions: { label: string, value: number }[] = [] | 19 | transcodingThreadOptions: { label: string, value: number }[] = [] |
21 | 20 | ||
22 | private oldCustomJavascript: string | ||
23 | private oldCustomCSS: string | ||
24 | |||
25 | constructor ( | 21 | constructor ( |
26 | protected formValidatorService: FormValidatorService, | 22 | protected formValidatorService: FormValidatorService, |
27 | private customConfigValidatorsService: CustomConfigValidatorsService, | 23 | private customConfigValidatorsService: CustomConfigValidatorsService, |
28 | private userValidatorsService: UserValidatorsService, | 24 | private userValidatorsService: UserValidatorsService, |
29 | private notificationsService: NotificationsService, | 25 | private notifier: Notifier, |
30 | private configService: ConfigService, | 26 | private configService: ConfigService, |
31 | private serverService: ServerService, | 27 | private serverService: ServerService, |
32 | private confirmService: ConfirmService, | ||
33 | private i18n: I18n | 28 | private i18n: I18n |
34 | ) { | 29 | ) { |
35 | super() | 30 | super() |
@@ -60,40 +55,78 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
60 | } | 55 | } |
61 | 56 | ||
62 | getResolutionKey (resolution: string) { | 57 | getResolutionKey (resolution: string) { |
63 | return 'transcodingResolution' + resolution | 58 | return 'transcoding.resolutions.' + resolution |
64 | } | 59 | } |
65 | 60 | ||
66 | ngOnInit () { | 61 | ngOnInit () { |
67 | const formGroupData = { | 62 | const formGroupData: { [key in keyof CustomConfig ]: any } = { |
68 | instanceName: this.customConfigValidatorsService.INSTANCE_NAME, | 63 | instance: { |
69 | instanceShortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, | 64 | name: this.customConfigValidatorsService.INSTANCE_NAME, |
70 | instanceDescription: null, | 65 | shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, |
71 | instanceTerms: null, | 66 | description: null, |
72 | instanceDefaultClientRoute: null, | 67 | terms: null, |
73 | instanceDefaultNSFWPolicy: null, | 68 | defaultClientRoute: null, |
74 | servicesTwitterUsername: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME, | 69 | defaultNSFWPolicy: null, |
75 | servicesTwitterWhitelisted: null, | 70 | customizations: { |
76 | cachePreviewsSize: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE, | 71 | javascript: null, |
77 | cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE, | 72 | css: null |
78 | signupEnabled: null, | 73 | } |
79 | signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT, | 74 | }, |
80 | signupRequiresEmailVerification: null, | 75 | services: { |
81 | importVideosHttpEnabled: null, | 76 | twitter: { |
82 | importVideosTorrentEnabled: null, | 77 | username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME, |
83 | adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL, | 78 | whitelisted: null |
84 | userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, | 79 | } |
85 | userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY, | 80 | }, |
86 | transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS, | 81 | cache: { |
87 | transcodingEnabled: null, | 82 | previews: { |
88 | customizationJavascript: null, | 83 | size: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE |
89 | customizationCSS: null | 84 | }, |
85 | captions: { | ||
86 | size: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE | ||
87 | } | ||
88 | }, | ||
89 | signup: { | ||
90 | enabled: null, | ||
91 | limit: this.customConfigValidatorsService.SIGNUP_LIMIT, | ||
92 | requiresEmailVerification: null | ||
93 | }, | ||
94 | import: { | ||
95 | videos: { | ||
96 | http: { | ||
97 | enabled: null | ||
98 | }, | ||
99 | torrent: { | ||
100 | enabled: null | ||
101 | } | ||
102 | } | ||
103 | }, | ||
104 | admin: { | ||
105 | email: this.customConfigValidatorsService.ADMIN_EMAIL | ||
106 | }, | ||
107 | contactForm: { | ||
108 | enabled: null | ||
109 | }, | ||
110 | user: { | ||
111 | videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA, | ||
112 | videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY | ||
113 | }, | ||
114 | transcoding: { | ||
115 | enabled: null, | ||
116 | threads: this.customConfigValidatorsService.TRANSCODING_THREADS, | ||
117 | allowAdditionalExtensions: null, | ||
118 | resolutions: {} | ||
119 | } | ||
90 | } | 120 | } |
91 | 121 | ||
92 | const defaultValues: BuildFormDefaultValues = {} | 122 | const defaultValues = { |
123 | transcoding: { | ||
124 | resolutions: {} | ||
125 | } | ||
126 | } | ||
93 | for (const resolution of this.resolutions) { | 127 | for (const resolution of this.resolutions) { |
94 | const key = this.getResolutionKey(resolution) | 128 | defaultValues.transcoding.resolutions[resolution] = 'false' |
95 | defaultValues[key] = 'false' | 129 | formGroupData.transcoding.resolutions[resolution] = null |
96 | formGroupData[key] = null | ||
97 | } | 130 | } |
98 | 131 | ||
99 | this.buildForm(formGroupData) | 132 | this.buildForm(formGroupData) |
@@ -103,112 +136,25 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
103 | res => { | 136 | res => { |
104 | this.customConfig = res | 137 | this.customConfig = res |
105 | 138 | ||
106 | this.oldCustomCSS = this.customConfig.instance.customizations.css | ||
107 | this.oldCustomJavascript = this.customConfig.instance.customizations.javascript | ||
108 | |||
109 | this.updateForm() | 139 | this.updateForm() |
110 | // Force form validation | 140 | // Force form validation |
111 | this.forceCheck() | 141 | this.forceCheck() |
112 | }, | 142 | }, |
113 | 143 | ||
114 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 144 | err => this.notifier.error(err.message) |
115 | ) | 145 | ) |
116 | } | 146 | } |
117 | 147 | ||
118 | isTranscodingEnabled () { | 148 | isTranscodingEnabled () { |
119 | return this.form.value['transcodingEnabled'] === true | 149 | return this.form.value['transcoding']['enabled'] === true |
120 | } | 150 | } |
121 | 151 | ||
122 | isSignupEnabled () { | 152 | isSignupEnabled () { |
123 | return this.form.value['signupEnabled'] === true | 153 | return this.form.value['signup']['enabled'] === true |
124 | } | 154 | } |
125 | 155 | ||
126 | async formValidated () { | 156 | async formValidated () { |
127 | const newCustomizationJavascript = this.form.value['customizationJavascript'] | 157 | this.configService.updateCustomConfig(this.form.value) |
128 | const newCustomizationCSS = this.form.value['customizationCSS'] | ||
129 | |||
130 | const customizations = [] | ||
131 | if (newCustomizationJavascript && newCustomizationJavascript !== this.oldCustomJavascript) customizations.push('JavaScript') | ||
132 | if (newCustomizationCSS && newCustomizationCSS !== this.oldCustomCSS) customizations.push('CSS') | ||
133 | |||
134 | if (customizations.length !== 0) { | ||
135 | const customizationsText = customizations.join('/') | ||
136 | |||
137 | // FIXME: i18n service does not support string concatenation | ||
138 | const message = this.i18n('You set custom {{customizationsText}}. ', { customizationsText }) + | ||
139 | this.i18n('This could lead to security issues or bugs if you do not understand it. ') + | ||
140 | this.i18n('Are you sure you want to update the configuration?') | ||
141 | |||
142 | const label = this.i18n('Please type') + ` "I understand the ${customizationsText} I set" ` + this.i18n('to confirm.') | ||
143 | const expectedInputValue = `I understand the ${customizationsText} I set` | ||
144 | |||
145 | const confirmRes = await this.confirmService.confirmWithInput(message, label, expectedInputValue) | ||
146 | if (confirmRes === false) return | ||
147 | } | ||
148 | |||
149 | const data: CustomConfig = { | ||
150 | instance: { | ||
151 | name: this.form.value['instanceName'], | ||
152 | shortDescription: this.form.value['instanceShortDescription'], | ||
153 | description: this.form.value['instanceDescription'], | ||
154 | terms: this.form.value['instanceTerms'], | ||
155 | defaultClientRoute: this.form.value['instanceDefaultClientRoute'], | ||
156 | defaultNSFWPolicy: this.form.value['instanceDefaultNSFWPolicy'], | ||
157 | customizations: { | ||
158 | javascript: this.form.value['customizationJavascript'], | ||
159 | css: this.form.value['customizationCSS'] | ||
160 | } | ||
161 | }, | ||
162 | services: { | ||
163 | twitter: { | ||
164 | username: this.form.value['servicesTwitterUsername'], | ||
165 | whitelisted: this.form.value['servicesTwitterWhitelisted'] | ||
166 | } | ||
167 | }, | ||
168 | cache: { | ||
169 | previews: { | ||
170 | size: this.form.value['cachePreviewsSize'] | ||
171 | }, | ||
172 | captions: { | ||
173 | size: this.form.value['cacheCaptionsSize'] | ||
174 | } | ||
175 | }, | ||
176 | signup: { | ||
177 | enabled: this.form.value['signupEnabled'], | ||
178 | limit: this.form.value['signupLimit'], | ||
179 | requiresEmailVerification: this.form.value['signupRequiresEmailVerification'] | ||
180 | }, | ||
181 | admin: { | ||
182 | email: this.form.value['adminEmail'] | ||
183 | }, | ||
184 | user: { | ||
185 | videoQuota: this.form.value['userVideoQuota'], | ||
186 | videoQuotaDaily: this.form.value['userVideoQuotaDaily'] | ||
187 | }, | ||
188 | transcoding: { | ||
189 | enabled: this.form.value['transcodingEnabled'], | ||
190 | threads: this.form.value['transcodingThreads'], | ||
191 | resolutions: { | ||
192 | '240p': this.form.value[this.getResolutionKey('240p')], | ||
193 | '360p': this.form.value[this.getResolutionKey('360p')], | ||
194 | '480p': this.form.value[this.getResolutionKey('480p')], | ||
195 | '720p': this.form.value[this.getResolutionKey('720p')], | ||
196 | '1080p': this.form.value[this.getResolutionKey('1080p')] | ||
197 | } | ||
198 | }, | ||
199 | import: { | ||
200 | videos: { | ||
201 | http: { | ||
202 | enabled: this.form.value['importVideosHttpEnabled'] | ||
203 | }, | ||
204 | torrent: { | ||
205 | enabled: this.form.value['importVideosTorrentEnabled'] | ||
206 | } | ||
207 | } | ||
208 | } | ||
209 | } | ||
210 | |||
211 | this.configService.updateCustomConfig(data) | ||
212 | .subscribe( | 158 | .subscribe( |
213 | res => { | 159 | res => { |
214 | this.customConfig = res | 160 | this.customConfig = res |
@@ -218,45 +164,15 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
218 | 164 | ||
219 | this.updateForm() | 165 | this.updateForm() |
220 | 166 | ||
221 | this.notificationsService.success(this.i18n('Success'), this.i18n('Configuration updated.')) | 167 | this.notifier.success(this.i18n('Configuration updated.')) |
222 | }, | 168 | }, |
223 | 169 | ||
224 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 170 | err => this.notifier.error(err.message) |
225 | ) | 171 | ) |
226 | } | 172 | } |
227 | 173 | ||
228 | private updateForm () { | 174 | private updateForm () { |
229 | const data = { | 175 | this.form.patchValue(this.customConfig) |
230 | instanceName: this.customConfig.instance.name, | ||
231 | instanceShortDescription: this.customConfig.instance.shortDescription, | ||
232 | instanceDescription: this.customConfig.instance.description, | ||
233 | instanceTerms: this.customConfig.instance.terms, | ||
234 | instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute, | ||
235 | instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy, | ||
236 | servicesTwitterUsername: this.customConfig.services.twitter.username, | ||
237 | servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted, | ||
238 | cachePreviewsSize: this.customConfig.cache.previews.size, | ||
239 | cacheCaptionsSize: this.customConfig.cache.captions.size, | ||
240 | signupEnabled: this.customConfig.signup.enabled, | ||
241 | signupLimit: this.customConfig.signup.limit, | ||
242 | signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification, | ||
243 | adminEmail: this.customConfig.admin.email, | ||
244 | userVideoQuota: this.customConfig.user.videoQuota, | ||
245 | userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily, | ||
246 | transcodingThreads: this.customConfig.transcoding.threads, | ||
247 | transcodingEnabled: this.customConfig.transcoding.enabled, | ||
248 | customizationJavascript: this.customConfig.instance.customizations.javascript, | ||
249 | customizationCSS: this.customConfig.instance.customizations.css, | ||
250 | importVideosHttpEnabled: this.customConfig.import.videos.http.enabled, | ||
251 | importVideosTorrentEnabled: this.customConfig.import.videos.torrent.enabled | ||
252 | } | ||
253 | |||
254 | for (const resolution of this.resolutions) { | ||
255 | const key = this.getResolutionKey(resolution) | ||
256 | data[key] = this.customConfig.transcoding.resolutions[resolution] | ||
257 | } | ||
258 | |||
259 | this.form.patchValue(data) | ||
260 | } | 176 | } |
261 | 177 | ||
262 | } | 178 | } |
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html index 5645a60cc..fc022bdb4 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.html +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html | |||
@@ -2,6 +2,15 @@ | |||
2 | [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | 2 | [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" |
3 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" | 3 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" |
4 | > | 4 | > |
5 | <ng-template pTemplate="caption"> | ||
6 | <div class="caption"> | ||
7 | <input | ||
8 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
9 | (keyup)="onSearch($event.target.value)" | ||
10 | > | ||
11 | </div> | ||
12 | </ng-template> | ||
13 | |||
5 | <ng-template pTemplate="header"> | 14 | <ng-template pTemplate="header"> |
6 | <tr> | 15 | <tr> |
7 | <th i18n style="width: 60px">ID</th> | 16 | <th i18n style="width: 60px">ID</th> |
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.scss b/client/src/app/+admin/follows/followers-list/followers-list.component.scss index e69de29bb..a6f0656b8 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.scss +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.scss | |||
@@ -0,0 +1,10 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .caption { | ||
5 | justify-content: flex-end; | ||
6 | |||
7 | input { | ||
8 | @include peertube-input-text(250px); | ||
9 | } | ||
10 | } \ No newline at end of file | ||
diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts index ca993dcd3..9a8848bfb 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | 2 | ||
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { Notifier } from '@app/core' |
4 | import { SortMeta } from 'primeng/primeng' | 4 | import { SortMeta } from 'primeng/primeng' |
5 | import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' | 5 | import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' |
6 | import { RestPagination, RestTable } from '../../../shared' | 6 | import { RestPagination, RestTable } from '../../../shared' |
@@ -20,7 +20,7 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
20 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 20 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
21 | 21 | ||
22 | constructor ( | 22 | constructor ( |
23 | private notificationsService: NotificationsService, | 23 | private notifier: Notifier, |
24 | private followService: FollowService, | 24 | private followService: FollowService, |
25 | private i18n: I18n | 25 | private i18n: I18n |
26 | ) { | 26 | ) { |
@@ -28,18 +28,18 @@ export class FollowersListComponent extends RestTable implements OnInit { | |||
28 | } | 28 | } |
29 | 29 | ||
30 | ngOnInit () { | 30 | ngOnInit () { |
31 | this.loadSort() | 31 | this.initialize() |
32 | } | 32 | } |
33 | 33 | ||
34 | protected loadData () { | 34 | protected loadData () { |
35 | this.followService.getFollowers(this.pagination, this.sort) | 35 | this.followService.getFollowers(this.pagination, this.sort, this.search) |
36 | .subscribe( | 36 | .subscribe( |
37 | resultList => { | 37 | resultList => { |
38 | this.followers = resultList.data | 38 | this.followers = resultList.data |
39 | this.totalRecords = resultList.total | 39 | this.totalRecords = resultList.total |
40 | }, | 40 | }, |
41 | 41 | ||
42 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 42 | err => this.notifier.error(err.message) |
43 | ) | 43 | ) |
44 | } | 44 | } |
45 | } | 45 | } |
diff --git a/client/src/app/+admin/follows/following-add/following-add.component.ts b/client/src/app/+admin/follows/following-add/following-add.component.ts index bd9cc022b..2bb249746 100644 --- a/client/src/app/+admin/follows/following-add/following-add.component.ts +++ b/client/src/app/+admin/follows/following-add/following-add.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component } from '@angular/core' | 1 | import { Component } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { Notifier } from '@app/core' |
4 | import { ConfirmService } from '../../../core' | 4 | import { ConfirmService } from '../../../core' |
5 | import { validateHost } from '../../../shared' | 5 | import { validateHost } from '../../../shared' |
6 | import { FollowService } from '../shared' | 6 | import { FollowService } from '../shared' |
@@ -18,7 +18,7 @@ export class FollowingAddComponent { | |||
18 | 18 | ||
19 | constructor ( | 19 | constructor ( |
20 | private router: Router, | 20 | private router: Router, |
21 | private notificationsService: NotificationsService, | 21 | private notifier: Notifier, |
22 | private confirmService: ConfirmService, | 22 | private confirmService: ConfirmService, |
23 | private followService: FollowService, | 23 | private followService: FollowService, |
24 | private i18n: I18n | 24 | private i18n: I18n |
@@ -64,12 +64,12 @@ export class FollowingAddComponent { | |||
64 | 64 | ||
65 | this.followService.follow(hosts).subscribe( | 65 | this.followService.follow(hosts).subscribe( |
66 | () => { | 66 | () => { |
67 | this.notificationsService.success(this.i18n('Success'), this.i18n('Follow request(s) sent!')) | 67 | this.notifier.success(this.i18n('Follow request(s) sent!')) |
68 | 68 | ||
69 | setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500) | 69 | setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500) |
70 | }, | 70 | }, |
71 | 71 | ||
72 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 72 | err => this.notifier.error(err.message) |
73 | ) | 73 | ) |
74 | } | 74 | } |
75 | 75 | ||
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html index 8af624ac5..5bc8fbc2d 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.html +++ b/client/src/app/+admin/follows/following-list/following-list.component.html | |||
@@ -2,6 +2,17 @@ | |||
2 | [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | 2 | [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" |
3 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" | 3 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" |
4 | > | 4 | > |
5 | <ng-template pTemplate="caption"> | ||
6 | <div class="caption"> | ||
7 | <div> | ||
8 | <input | ||
9 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
10 | (keyup)="onSearch($event.target.value)" | ||
11 | > | ||
12 | </div> | ||
13 | </div> | ||
14 | </ng-template> | ||
15 | |||
5 | <ng-template pTemplate="header"> | 16 | <ng-template pTemplate="header"> |
6 | <tr> | 17 | <tr> |
7 | <th i18n style="width: 60px">ID</th> | 18 | <th i18n style="width: 60px">ID</th> |
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.scss b/client/src/app/+admin/follows/following-list/following-list.component.scss index bfcdcaa49..a6f0656b8 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.scss +++ b/client/src/app/+admin/follows/following-list/following-list.component.scss | |||
@@ -1,13 +1,10 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | my-redundancy-checkbox /deep/ my-peertube-checkbox { | 4 | .caption { |
5 | .form-group { | 5 | justify-content: flex-end; |
6 | margin-bottom: 0; | ||
7 | align-items: center; | ||
8 | } | ||
9 | 6 | ||
10 | label { | 7 | input { |
11 | margin: 0; | 8 | @include peertube-input-text(250px); |
12 | } | 9 | } |
13 | } \ No newline at end of file | 10 | } \ No newline at end of file |
diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts index dd57884c6..4517a721e 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.ts +++ b/client/src/app/+admin/follows/following-list/following-list.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { SortMeta } from 'primeng/primeng' | 3 | import { SortMeta } from 'primeng/primeng' |
4 | import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' | 4 | import { ActorFollow } from '../../../../../../shared/models/actors/follow.model' |
5 | import { ConfirmService } from '../../../core/confirm/confirm.service' | 5 | import { ConfirmService } from '../../../core/confirm/confirm.service' |
@@ -20,7 +20,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
20 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 20 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
21 | 21 | ||
22 | constructor ( | 22 | constructor ( |
23 | private notificationsService: NotificationsService, | 23 | private notifier: Notifier, |
24 | private confirmService: ConfirmService, | 24 | private confirmService: ConfirmService, |
25 | private followService: FollowService, | 25 | private followService: FollowService, |
26 | private i18n: I18n | 26 | private i18n: I18n |
@@ -29,7 +29,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
29 | } | 29 | } |
30 | 30 | ||
31 | ngOnInit () { | 31 | ngOnInit () { |
32 | this.loadSort() | 32 | this.initialize() |
33 | } | 33 | } |
34 | 34 | ||
35 | async removeFollowing (follow: ActorFollow) { | 35 | async removeFollowing (follow: ActorFollow) { |
@@ -41,26 +41,23 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
41 | 41 | ||
42 | this.followService.unfollow(follow).subscribe( | 42 | this.followService.unfollow(follow).subscribe( |
43 | () => { | 43 | () => { |
44 | this.notificationsService.success( | 44 | this.notifier.success(this.i18n('You are not following {{host}} anymore.', { host: follow.following.host })) |
45 | this.i18n('Success'), | ||
46 | this.i18n('You are not following {{host}} anymore.', { host: follow.following.host }) | ||
47 | ) | ||
48 | this.loadData() | 45 | this.loadData() |
49 | }, | 46 | }, |
50 | 47 | ||
51 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 48 | err => this.notifier.error(err.message) |
52 | ) | 49 | ) |
53 | } | 50 | } |
54 | 51 | ||
55 | protected loadData () { | 52 | protected loadData () { |
56 | this.followService.getFollowing(this.pagination, this.sort) | 53 | this.followService.getFollowing(this.pagination, this.sort, this.search) |
57 | .subscribe( | 54 | .subscribe( |
58 | resultList => { | 55 | resultList => { |
59 | this.following = resultList.data | 56 | this.following = resultList.data |
60 | this.totalRecords = resultList.total | 57 | this.totalRecords = resultList.total |
61 | }, | 58 | }, |
62 | 59 | ||
63 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 60 | err => this.notifier.error(err.message) |
64 | ) | 61 | ) |
65 | } | 62 | } |
66 | } | 63 | } |
diff --git a/client/src/app/+admin/follows/shared/follow.service.ts b/client/src/app/+admin/follows/shared/follow.service.ts index 27169a9cd..a2904179e 100644 --- a/client/src/app/+admin/follows/shared/follow.service.ts +++ b/client/src/app/+admin/follows/shared/follow.service.ts | |||
@@ -18,10 +18,12 @@ export class FollowService { | |||
18 | ) { | 18 | ) { |
19 | } | 19 | } |
20 | 20 | ||
21 | getFollowing (pagination: RestPagination, sort: SortMeta): Observable<ResultList<ActorFollow>> { | 21 | getFollowing (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<ActorFollow>> { |
22 | let params = new HttpParams() | 22 | let params = new HttpParams() |
23 | params = this.restService.addRestGetParams(params, pagination, sort) | 23 | params = this.restService.addRestGetParams(params, pagination, sort) |
24 | 24 | ||
25 | if (search) params = params.append('search', search) | ||
26 | |||
25 | return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/following', { params }) | 27 | return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/following', { params }) |
26 | .pipe( | 28 | .pipe( |
27 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | 29 | map(res => this.restExtractor.convertResultListDateToHuman(res)), |
@@ -29,10 +31,12 @@ export class FollowService { | |||
29 | ) | 31 | ) |
30 | } | 32 | } |
31 | 33 | ||
32 | getFollowers (pagination: RestPagination, sort: SortMeta): Observable<ResultList<ActorFollow>> { | 34 | getFollowers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<ActorFollow>> { |
33 | let params = new HttpParams() | 35 | let params = new HttpParams() |
34 | params = this.restService.addRestGetParams(params, pagination, sort) | 36 | params = this.restService.addRestGetParams(params, pagination, sort) |
35 | 37 | ||
38 | if (search) params = params.append('search', search) | ||
39 | |||
36 | return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/followers', { params }) | 40 | return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/followers', { params }) |
37 | .pipe( | 41 | .pipe( |
38 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | 42 | map(res => this.restExtractor.convertResultListDateToHuman(res)), |
diff --git a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts index 6d77a0eb4..fa1da26bf 100644 --- a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts +++ b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' | 4 | import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' |
5 | 5 | ||
@@ -13,24 +13,21 @@ export class RedundancyCheckboxComponent { | |||
13 | @Input() host: string | 13 | @Input() host: string |
14 | 14 | ||
15 | constructor ( | 15 | constructor ( |
16 | private notificationsService: NotificationsService, | 16 | private notifier: Notifier, |
17 | private redundancyService: RedundancyService, | 17 | private redundancyService: RedundancyService, |
18 | private i18n: I18n | 18 | private i18n: I18n |
19 | ) { } | 19 | ) { } |
20 | 20 | ||
21 | updateRedundancyState () { | 21 | updateRedundancyState () { |
22 | this.redundancyService.updateRedundancy(this.host, this.redundancyAllowed) | 22 | this.redundancyService.updateRedundancy(this.host, this.redundancyAllowed) |
23 | .subscribe( | 23 | .subscribe( |
24 | () => { | 24 | () => { |
25 | const stateLabel = this.redundancyAllowed ? this.i18n('enabled') : this.i18n('disabled') | 25 | const stateLabel = this.redundancyAllowed ? this.i18n('enabled') : this.i18n('disabled') |
26 | 26 | ||
27 | this.notificationsService.success( | 27 | this.notifier.success(this.i18n('Redundancy for {{host}} is {{stateLabel}}', { host: this.host, stateLabel })) |
28 | this.i18n('Success'), | 28 | }, |
29 | this.i18n('Redundancy for {{host}} is {{stateLabel}}', { host: this.host, stateLabel }) | ||
30 | ) | ||
31 | }, | ||
32 | 29 | ||
33 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 30 | err => this.notifier.error(err.message) |
34 | ) | 31 | ) |
35 | } | 32 | } |
36 | } | 33 | } |
diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts index 866ba1b23..b265e1dd6 100644 --- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts +++ b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | 2 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { Notifier } from '@app/core' |
4 | import { SortMeta } from 'primeng/primeng' | 4 | import { SortMeta } from 'primeng/primeng' |
5 | import { Job } from '../../../../../../shared/index' | 5 | import { Job } from '../../../../../../shared/index' |
6 | import { JobState } from '../../../../../../shared/models' | 6 | import { JobState } from '../../../../../../shared/models' |
@@ -25,7 +25,7 @@ export class JobsListComponent extends RestTable implements OnInit { | |||
25 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 25 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
26 | 26 | ||
27 | constructor ( | 27 | constructor ( |
28 | private notificationsService: NotificationsService, | 28 | private notifier: Notifier, |
29 | private jobsService: JobService, | 29 | private jobsService: JobService, |
30 | private i18n: I18n | 30 | private i18n: I18n |
31 | ) { | 31 | ) { |
@@ -34,7 +34,7 @@ export class JobsListComponent extends RestTable implements OnInit { | |||
34 | 34 | ||
35 | ngOnInit () { | 35 | ngOnInit () { |
36 | this.loadJobState() | 36 | this.loadJobState() |
37 | this.loadSort() | 37 | this.initialize() |
38 | } | 38 | } |
39 | 39 | ||
40 | onJobStateChanged () { | 40 | onJobStateChanged () { |
@@ -53,7 +53,7 @@ export class JobsListComponent extends RestTable implements OnInit { | |||
53 | this.totalRecords = resultList.total | 53 | this.totalRecords = resultList.total |
54 | }, | 54 | }, |
55 | 55 | ||
56 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 56 | err => this.notifier.error(err.message) |
57 | ) | 57 | ) |
58 | } | 58 | } |
59 | 59 | ||
diff --git a/client/src/app/+admin/moderation/instance-blocklist/index.ts b/client/src/app/+admin/moderation/instance-blocklist/index.ts new file mode 100644 index 000000000..3e7a344bb --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './instance-account-blocklist.component' | ||
2 | export * from './instance-server-blocklist.component' | ||
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html new file mode 100644 index 000000000..7797bc56e --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html | |||
@@ -0,0 +1,22 @@ | |||
1 | <p-table | ||
2 | [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
3 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" | ||
4 | > | ||
5 | |||
6 | <ng-template pTemplate="header"> | ||
7 | <tr> | ||
8 | <th i18n>Account</th> | ||
9 | <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
10 | </tr> | ||
11 | </ng-template> | ||
12 | |||
13 | <ng-template pTemplate="body" let-accountBlock> | ||
14 | <tr> | ||
15 | <td>{{ accountBlock.blockedAccount.nameWithHost }}</td> | ||
16 | <td>{{ accountBlock.createdAt }}</td> | ||
17 | <td class="action-cell"> | ||
18 | <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button> | ||
19 | </td> | ||
20 | </tr> | ||
21 | </ng-template> | ||
22 | </p-table> | ||
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss new file mode 100644 index 000000000..6028b75ea --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss | |||
@@ -0,0 +1,7 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .unblock-button { | ||
5 | @include peertube-button; | ||
6 | @include grey-button; | ||
7 | } \ No newline at end of file | ||
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts new file mode 100644 index 000000000..032bf745a --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts | |||
@@ -0,0 +1,58 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Notifier } from '@app/core' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { RestPagination, RestTable } from '@app/shared' | ||
5 | import { SortMeta } from 'primeng/components/common/sortmeta' | ||
6 | import { AccountBlock, BlocklistService } from '@app/shared/blocklist' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-instance-account-blocklist', | ||
10 | styleUrls: [ './instance-account-blocklist.component.scss' ], | ||
11 | templateUrl: './instance-account-blocklist.component.html' | ||
12 | }) | ||
13 | export class InstanceAccountBlocklistComponent extends RestTable implements OnInit { | ||
14 | blockedAccounts: AccountBlock[] = [] | ||
15 | totalRecords = 0 | ||
16 | rowsPerPage = 10 | ||
17 | sort: SortMeta = { field: 'createdAt', order: -1 } | ||
18 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
19 | |||
20 | constructor ( | ||
21 | private notifier: Notifier, | ||
22 | private blocklistService: BlocklistService, | ||
23 | private i18n: I18n | ||
24 | ) { | ||
25 | super() | ||
26 | } | ||
27 | |||
28 | ngOnInit () { | ||
29 | this.initialize() | ||
30 | } | ||
31 | |||
32 | unblockAccount (accountBlock: AccountBlock) { | ||
33 | const blockedAccount = accountBlock.blockedAccount | ||
34 | |||
35 | this.blocklistService.unblockAccountByInstance(blockedAccount) | ||
36 | .subscribe( | ||
37 | () => { | ||
38 | this.notifier.success( | ||
39 | this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost }) | ||
40 | ) | ||
41 | |||
42 | this.loadData() | ||
43 | } | ||
44 | ) | ||
45 | } | ||
46 | |||
47 | protected loadData () { | ||
48 | return this.blocklistService.getInstanceAccountBlocklist(this.pagination, this.sort) | ||
49 | .subscribe( | ||
50 | resultList => { | ||
51 | this.blockedAccounts = resultList.data | ||
52 | this.totalRecords = resultList.total | ||
53 | }, | ||
54 | |||
55 | err => this.notifier.error(err.message) | ||
56 | ) | ||
57 | } | ||
58 | } | ||
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html new file mode 100644 index 000000000..f634ba834 --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html | |||
@@ -0,0 +1,23 @@ | |||
1 | <p-table | ||
2 | [value]="blockedServers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
3 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" | ||
4 | > | ||
5 | |||
6 | <ng-template pTemplate="header"> | ||
7 | <tr> | ||
8 | <th i18n>Instance</th> | ||
9 | <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
10 | <th></th> | ||
11 | </tr> | ||
12 | </ng-template> | ||
13 | |||
14 | <ng-template pTemplate="body" let-serverBlock> | ||
15 | <tr> | ||
16 | <td>{{ serverBlock.blockedServer.host }}</td> | ||
17 | <td>{{ serverBlock.createdAt }}</td> | ||
18 | <td class="action-cell"> | ||
19 | <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button> | ||
20 | </td> | ||
21 | </tr> | ||
22 | </ng-template> | ||
23 | </p-table> | ||
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss new file mode 100644 index 000000000..6028b75ea --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss | |||
@@ -0,0 +1,7 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .unblock-button { | ||
5 | @include peertube-button; | ||
6 | @include grey-button; | ||
7 | } \ No newline at end of file | ||
diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts new file mode 100644 index 000000000..db3dfcd1c --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts | |||
@@ -0,0 +1,57 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Notifier } from '@app/core' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { RestPagination, RestTable } from '@app/shared' | ||
5 | import { SortMeta } from 'primeng/components/common/sortmeta' | ||
6 | import { BlocklistService } from '@app/shared/blocklist' | ||
7 | import { ServerBlock } from '../../../../../../shared' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-instance-server-blocklist', | ||
11 | styleUrls: [ './instance-server-blocklist.component.scss' ], | ||
12 | templateUrl: './instance-server-blocklist.component.html' | ||
13 | }) | ||
14 | export class InstanceServerBlocklistComponent extends RestTable implements OnInit { | ||
15 | blockedServers: ServerBlock[] = [] | ||
16 | totalRecords = 0 | ||
17 | rowsPerPage = 10 | ||
18 | sort: SortMeta = { field: 'createdAt', order: -1 } | ||
19 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
20 | |||
21 | constructor ( | ||
22 | private notifier: Notifier, | ||
23 | private blocklistService: BlocklistService, | ||
24 | private i18n: I18n | ||
25 | ) { | ||
26 | super() | ||
27 | } | ||
28 | |||
29 | ngOnInit () { | ||
30 | this.initialize() | ||
31 | } | ||
32 | |||
33 | unblockServer (serverBlock: ServerBlock) { | ||
34 | const host = serverBlock.blockedServer.host | ||
35 | |||
36 | this.blocklistService.unblockServerByInstance(host) | ||
37 | .subscribe( | ||
38 | () => { | ||
39 | this.notifier.success(this.i18n('Instance {{host}} unmuted by your instance.', { host })) | ||
40 | |||
41 | this.loadData() | ||
42 | } | ||
43 | ) | ||
44 | } | ||
45 | |||
46 | protected loadData () { | ||
47 | return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort) | ||
48 | .subscribe( | ||
49 | resultList => { | ||
50 | this.blockedServers = resultList.data | ||
51 | this.totalRecords = resultList.total | ||
52 | }, | ||
53 | |||
54 | err => this.notifier.error(err.message) | ||
55 | ) | ||
56 | } | ||
57 | } | ||
diff --git a/client/src/app/+admin/moderation/moderation.component.html b/client/src/app/+admin/moderation/moderation.component.html index 91e87fcd4..01457936c 100644 --- a/client/src/app/+admin/moderation/moderation.component.html +++ b/client/src/app/+admin/moderation/moderation.component.html | |||
@@ -5,6 +5,10 @@ | |||
5 | <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a> | 5 | <a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a> |
6 | 6 | ||
7 | <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a> | 7 | <a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a> |
8 | |||
9 | <a *ngIf="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a> | ||
10 | |||
11 | <a *ngIf="hasServersBlocklistRight()" i18n routerLink="blocklist/servers" routerLinkActive="active">Muted servers</a> | ||
8 | </div> | 12 | </div> |
9 | </div> | 13 | </div> |
10 | 14 | ||
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss index 02ccfc8ca..13b019c5b 100644 --- a/client/src/app/+admin/moderation/moderation.component.scss +++ b/client/src/app/+admin/moderation/moderation.component.scss | |||
@@ -10,6 +10,7 @@ | |||
10 | font-weight: $font-semibold; | 10 | font-weight: $font-semibold; |
11 | min-width: 200px; | 11 | min-width: 200px; |
12 | display: inline-block; | 12 | display: inline-block; |
13 | vertical-align: top; | ||
13 | } | 14 | } |
14 | 15 | ||
15 | .moderation-expanded-text { | 16 | .moderation-expanded-text { |
diff --git a/client/src/app/+admin/moderation/moderation.component.ts b/client/src/app/+admin/moderation/moderation.component.ts index 0f4efb970..2b2618933 100644 --- a/client/src/app/+admin/moderation/moderation.component.ts +++ b/client/src/app/+admin/moderation/moderation.component.ts | |||
@@ -16,4 +16,12 @@ export class ModerationComponent { | |||
16 | hasVideoBlacklistRight () { | 16 | hasVideoBlacklistRight () { |
17 | return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) | 17 | return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) |
18 | } | 18 | } |
19 | |||
20 | hasAccountsBlocklistRight () { | ||
21 | return this.auth.getUser().hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST) | ||
22 | } | ||
23 | |||
24 | hasServersBlocklistRight () { | ||
25 | return this.auth.getUser().hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST) | ||
26 | } | ||
19 | } | 27 | } |
diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index 6d81b9b36..bc6dd49d5 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts | |||
@@ -4,6 +4,7 @@ import { UserRightGuard } from '@app/core' | |||
4 | import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' | 4 | import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' |
5 | import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list' | 5 | import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list' |
6 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' | 6 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' |
7 | import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist' | ||
7 | 8 | ||
8 | export const ModerationRoutes: Routes = [ | 9 | export const ModerationRoutes: Routes = [ |
9 | { | 10 | { |
@@ -46,6 +47,28 @@ export const ModerationRoutes: Routes = [ | |||
46 | title: 'Blacklisted videos' | 47 | title: 'Blacklisted videos' |
47 | } | 48 | } |
48 | } | 49 | } |
50 | }, | ||
51 | { | ||
52 | path: 'blocklist/accounts', | ||
53 | component: InstanceAccountBlocklistComponent, | ||
54 | canActivate: [ UserRightGuard ], | ||
55 | data: { | ||
56 | userRight: UserRight.MANAGE_ACCOUNTS_BLOCKLIST, | ||
57 | meta: { | ||
58 | title: 'Muted accounts' | ||
59 | } | ||
60 | } | ||
61 | }, | ||
62 | { | ||
63 | path: 'blocklist/servers', | ||
64 | component: InstanceServerBlocklistComponent, | ||
65 | canActivate: [ UserRightGuard ], | ||
66 | data: { | ||
67 | userRight: UserRight.MANAGE_SERVER_REDUNDANCY, | ||
68 | meta: { | ||
69 | title: 'Muted instances' | ||
70 | } | ||
71 | } | ||
49 | } | 72 | } |
50 | ] | 73 | ] |
51 | } | 74 | } |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html index 3a8424f68..303a788d2 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Moderation comment</h4> | 3 | <h4 i18n class="modal-title">Moderation comment</h4> |
4 | <span class="close" aria-hidden="true" (click)="hideModerationCommentModal()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body"> | 8 | <div class="modal-body"> |
@@ -14,12 +15,12 @@ | |||
14 | </div> | 15 | </div> |
15 | </div> | 16 | </div> |
16 | 17 | ||
17 | <div i18n> | 18 | <div class="form-group" i18n> |
18 | This comment can only be seen by you or the other moderators. | 19 | This comment can only be seen by you or the other moderators. |
19 | </div> | 20 | </div> |
20 | 21 | ||
21 | <div class="form-group inputs"> | 22 | <div class="form-group inputs"> |
22 | <span i18n class="action-button action-button-cancel" (click)="hideModerationCommentModal()">Cancel</span> | 23 | <span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span> |
23 | 24 | ||
24 | <input | 25 | <input |
25 | type="submit" i18n-value value="Update this comment" class="action-button-submit" | 26 | type="submit" i18n-value value="Update this comment" class="action-button-submit" |
@@ -29,4 +30,4 @@ | |||
29 | </form> | 30 | </form> |
30 | </div> | 31 | </div> |
31 | 32 | ||
32 | </ng-template> \ No newline at end of file | 33 | </ng-template> |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts index 34ab384d1..f915978ee 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { FormReactive, VideoAbuseService, VideoAbuseValidatorsService } from '../../../shared' | 3 | import { FormReactive, VideoAbuseService, VideoAbuseValidatorsService } from '../../../shared' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
@@ -22,7 +22,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
22 | constructor ( | 22 | constructor ( |
23 | protected formValidatorService: FormValidatorService, | 23 | protected formValidatorService: FormValidatorService, |
24 | private modalService: NgbModal, | 24 | private modalService: NgbModal, |
25 | private notificationsService: NotificationsService, | 25 | private notifier: Notifier, |
26 | private videoAbuseService: VideoAbuseService, | 26 | private videoAbuseService: VideoAbuseService, |
27 | private videoAbuseValidatorsService: VideoAbuseValidatorsService, | 27 | private videoAbuseValidatorsService: VideoAbuseValidatorsService, |
28 | private i18n: I18n | 28 | private i18n: I18n |
@@ -45,29 +45,26 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI | |||
45 | }) | 45 | }) |
46 | } | 46 | } |
47 | 47 | ||
48 | hideModerationCommentModal () { | 48 | hide () { |
49 | this.abuseToComment = undefined | 49 | this.abuseToComment = undefined |
50 | this.openedModal.close() | 50 | this.openedModal.close() |
51 | this.form.reset() | 51 | this.form.reset() |
52 | } | 52 | } |
53 | 53 | ||
54 | async banUser () { | 54 | async banUser () { |
55 | const moderationComment: string = this.form.value['moderationComment'] | 55 | const moderationComment: string = this.form.value[ 'moderationComment' ] |
56 | 56 | ||
57 | this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment }) | 57 | this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment }) |
58 | .subscribe( | 58 | .subscribe( |
59 | () => { | 59 | () => { |
60 | this.notificationsService.success( | 60 | this.notifier.success(this.i18n('Comment updated.')) |
61 | this.i18n('Success'), | ||
62 | this.i18n('Comment updated.') | ||
63 | ) | ||
64 | 61 | ||
65 | this.commentUpdated.emit(moderationComment) | 62 | this.commentUpdated.emit(moderationComment) |
66 | this.hideModerationCommentModal() | 63 | this.hide() |
67 | }, | 64 | }, |
68 | 65 | ||
69 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 66 | err => this.notifier.error(err.message) |
70 | ) | 67 | ) |
71 | } | 68 | } |
72 | 69 | ||
73 | } | 70 | } |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html index 287ab3e46..05b549de6 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html | |||
@@ -9,7 +9,7 @@ | |||
9 | <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> | 9 | <th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> |
10 | <th i18n>Video</th> | 10 | <th i18n>Video</th> |
11 | <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> | 11 | <th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th> |
12 | <th style="width: 50px;"></th> | 12 | <th style="width: 120px;"></th> |
13 | </tr> | 13 | </tr> |
14 | </ng-template> | 14 | </ng-template> |
15 | 15 | ||
@@ -41,7 +41,7 @@ | |||
41 | </td> | 41 | </td> |
42 | 42 | ||
43 | <td class="action-cell"> | 43 | <td class="action-cell"> |
44 | <my-action-dropdown i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown> | 44 | <my-action-dropdown placement="bottom-right" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown> |
45 | </td> | 45 | </td> |
46 | </tr> | 46 | </tr> |
47 | </ng-template> | 47 | </ng-template> |
@@ -51,15 +51,15 @@ | |||
51 | <td class="moderation-expanded" colspan="6"> | 51 | <td class="moderation-expanded" colspan="6"> |
52 | <div> | 52 | <div> |
53 | <span i18n class="moderation-expanded-label">Reason:</span> | 53 | <span i18n class="moderation-expanded-label">Reason:</span> |
54 | <span class="moderation-expanded-text">{{ videoAbuse.reason }}</span> | 54 | <span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.reason)"></span> |
55 | </div> | 55 | </div> |
56 | <div *ngIf="videoAbuse.moderationComment"> | 56 | <div *ngIf="videoAbuse.moderationComment"> |
57 | <span i18n class="moderation-expanded-label">Moderation comment:</span> | 57 | <span i18n class="moderation-expanded-label">Moderation comment:</span> |
58 | <span class="moderation-expanded-text">{{ videoAbuse.moderationComment }}</span> | 58 | <span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.moderationComment)"></span> |
59 | </div> | 59 | </div> |
60 | </td> | 60 | </td> |
61 | </tr> | 61 | </tr> |
62 | </ng-template> | 62 | </ng-template> |
63 | </p-table> | 63 | </p-table> |
64 | 64 | ||
65 | <my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal> \ No newline at end of file | 65 | <my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal> |
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts index 681db7434..00c871659 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { Account } from '../../../shared/account/account.model' | 2 | import { Account } from '../../../shared/account/account.model' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { Notifier } from '@app/core' |
4 | import { SortMeta } from 'primeng/components/common/sortmeta' | 4 | import { SortMeta } from 'primeng/components/common/sortmeta' |
5 | import { VideoAbuse, VideoAbuseState } from '../../../../../../shared' | 5 | import { VideoAbuse, VideoAbuseState } from '../../../../../../shared' |
6 | import { RestPagination, RestTable, VideoAbuseService } from '../../../shared' | 6 | import { RestPagination, RestTable, VideoAbuseService } from '../../../shared' |
@@ -9,6 +9,7 @@ import { DropdownAction } from '../../../shared/buttons/action-dropdown.componen | |||
9 | import { ConfirmService } from '../../../core/index' | 9 | import { ConfirmService } from '../../../core/index' |
10 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' | 10 | import { ModerationCommentModalComponent } from './moderation-comment-modal.component' |
11 | import { Video } from '../../../shared/video/video.model' | 11 | import { Video } from '../../../shared/video/video.model' |
12 | import { MarkdownService } from '@app/shared/renderer' | ||
12 | 13 | ||
13 | @Component({ | 14 | @Component({ |
14 | selector: 'my-video-abuse-list', | 15 | selector: 'my-video-abuse-list', |
@@ -27,16 +28,17 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
27 | videoAbuseActions: DropdownAction<VideoAbuse>[] = [] | 28 | videoAbuseActions: DropdownAction<VideoAbuse>[] = [] |
28 | 29 | ||
29 | constructor ( | 30 | constructor ( |
30 | private notificationsService: NotificationsService, | 31 | private notifier: Notifier, |
31 | private videoAbuseService: VideoAbuseService, | 32 | private videoAbuseService: VideoAbuseService, |
32 | private confirmService: ConfirmService, | 33 | private confirmService: ConfirmService, |
33 | private i18n: I18n | 34 | private i18n: I18n, |
35 | private markdownRenderer: MarkdownService | ||
34 | ) { | 36 | ) { |
35 | super() | 37 | super() |
36 | 38 | ||
37 | this.videoAbuseActions = [ | 39 | this.videoAbuseActions = [ |
38 | { | 40 | { |
39 | label: this.i18n('Delete'), | 41 | label: this.i18n('Delete this report'), |
40 | handler: videoAbuse => this.removeVideoAbuse(videoAbuse) | 42 | handler: videoAbuse => this.removeVideoAbuse(videoAbuse) |
41 | }, | 43 | }, |
42 | { | 44 | { |
@@ -57,7 +59,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
57 | } | 59 | } |
58 | 60 | ||
59 | ngOnInit () { | 61 | ngOnInit () { |
60 | this.loadSort() | 62 | this.initialize() |
61 | } | 63 | } |
62 | 64 | ||
63 | openModerationCommentModal (videoAbuse: VideoAbuse) { | 65 | openModerationCommentModal (videoAbuse: VideoAbuse) { |
@@ -85,19 +87,16 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
85 | } | 87 | } |
86 | 88 | ||
87 | async removeVideoAbuse (videoAbuse: VideoAbuse) { | 89 | async removeVideoAbuse (videoAbuse: VideoAbuse) { |
88 | const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse?'), this.i18n('Delete')) | 90 | const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete')) |
89 | if (res === false) return | 91 | if (res === false) return |
90 | 92 | ||
91 | this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( | 93 | this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( |
92 | () => { | 94 | () => { |
93 | this.notificationsService.success( | 95 | this.notifier.success(this.i18n('Abuse deleted.')) |
94 | this.i18n('Success'), | ||
95 | this.i18n('Abuse deleted.') | ||
96 | ) | ||
97 | this.loadData() | 96 | this.loadData() |
98 | }, | 97 | }, |
99 | 98 | ||
100 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 99 | err => this.notifier.error(err.message) |
101 | ) | 100 | ) |
102 | } | 101 | } |
103 | 102 | ||
@@ -106,11 +105,15 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
106 | .subscribe( | 105 | .subscribe( |
107 | () => this.loadData(), | 106 | () => this.loadData(), |
108 | 107 | ||
109 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 108 | err => this.notifier.error(err.message) |
110 | ) | 109 | ) |
111 | 110 | ||
112 | } | 111 | } |
113 | 112 | ||
113 | toHtml (text: string) { | ||
114 | return this.markdownRenderer.textMarkdownToHTML(text) | ||
115 | } | ||
116 | |||
114 | protected loadData () { | 117 | protected loadData () { |
115 | return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort) | 118 | return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort) |
116 | .subscribe( | 119 | .subscribe( |
@@ -119,7 +122,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
119 | this.totalRecords = resultList.total | 122 | this.totalRecords = resultList.total |
120 | }, | 123 | }, |
121 | 124 | ||
122 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 125 | err => this.notifier.error(err.message) |
123 | ) | 126 | ) |
124 | } | 127 | } |
125 | } | 128 | } |
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html index 0585e0490..247f441c1 100644 --- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html +++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html | |||
@@ -7,8 +7,9 @@ | |||
7 | <th style="width: 40px"></th> | 7 | <th style="width: 40px"></th> |
8 | <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th> | 8 | <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th> |
9 | <th i18n>Sensitive</th> | 9 | <th i18n>Sensitive</th> |
10 | <th i18n>Unfederated</th> | ||
10 | <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> | 11 | <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> |
11 | <th style="width: 50px;"></th> | 12 | <th style="width: 120px;"></th> |
12 | </tr> | 13 | </tr> |
13 | </ng-template> | 14 | </ng-template> |
14 | 15 | ||
@@ -26,20 +27,21 @@ | |||
26 | </a> | 27 | </a> |
27 | </td> | 28 | </td> |
28 | 29 | ||
29 | <td>{{ videoBlacklist.video.nsfw }}</td> | 30 | <td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td> |
31 | <td>{{ booleanToText(videoBlacklist.unfederated) }}</td> | ||
30 | <td>{{ videoBlacklist.createdAt }}</td> | 32 | <td>{{ videoBlacklist.createdAt }}</td> |
31 | 33 | ||
32 | <td class="action-cell"> | 34 | <td class="action-cell"> |
33 | <my-action-dropdown i18n-label label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown> | 35 | <my-action-dropdown i18n-label placement="bottom-right" label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown> |
34 | </td> | 36 | </td> |
35 | </tr> | 37 | </tr> |
36 | </ng-template> | 38 | </ng-template> |
37 | 39 | ||
38 | <ng-template pTemplate="rowexpansion" let-videoBlacklist> | 40 | <ng-template pTemplate="rowexpansion" let-videoBlacklist> |
39 | <tr> | 41 | <tr> |
40 | <td class="moderation-expanded" colspan="5"> | 42 | <td class="moderation-expanded" colspan="6"> |
41 | <span i18n class="moderation-expanded-label">Blacklist reason:</span> | 43 | <span i18n class="moderation-expanded-label">Blacklist reason:</span> |
42 | <span class="moderation-expanded-text">{{ videoBlacklist.reason }}</span> | 44 | <span class="moderation-expanded-text" [innerHTML]="toHtml(videoBlacklist.reason)"></span> |
43 | </td> | 45 | </td> |
44 | </tr> | 46 | </tr> |
45 | </ng-template> | 47 | </ng-template> |
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts index bb051d00f..b27bbbfef 100644 --- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts +++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts | |||
@@ -1,12 +1,13 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { SortMeta } from 'primeng/components/common/sortmeta' | 2 | import { SortMeta } from 'primeng/components/common/sortmeta' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { Notifier } from '@app/core' |
4 | import { ConfirmService } from '../../../core' | 4 | import { ConfirmService } from '../../../core' |
5 | import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared' | 5 | import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared' |
6 | import { VideoBlacklist } from '../../../../../../shared' | 6 | import { VideoBlacklist } from '../../../../../../shared' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' | 8 | import { DropdownAction } from '../../../shared/buttons/action-dropdown.component' |
9 | import { Video } from '../../../shared/video/video.model' | 9 | import { Video } from '../../../shared/video/video.model' |
10 | import { MarkdownService } from '@app/shared/renderer' | ||
10 | 11 | ||
11 | @Component({ | 12 | @Component({ |
12 | selector: 'my-video-blacklist-list', | 13 | selector: 'my-video-blacklist-list', |
@@ -23,9 +24,10 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit { | |||
23 | videoBlacklistActions: DropdownAction<VideoBlacklist>[] = [] | 24 | videoBlacklistActions: DropdownAction<VideoBlacklist>[] = [] |
24 | 25 | ||
25 | constructor ( | 26 | constructor ( |
26 | private notificationsService: NotificationsService, | 27 | private notifier: Notifier, |
27 | private confirmService: ConfirmService, | 28 | private confirmService: ConfirmService, |
28 | private videoBlacklistService: VideoBlacklistService, | 29 | private videoBlacklistService: VideoBlacklistService, |
30 | private markdownRenderer: MarkdownService, | ||
29 | private i18n: I18n | 31 | private i18n: I18n |
30 | ) { | 32 | ) { |
31 | super() | 33 | super() |
@@ -39,13 +41,23 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit { | |||
39 | } | 41 | } |
40 | 42 | ||
41 | ngOnInit () { | 43 | ngOnInit () { |
42 | this.loadSort() | 44 | this.initialize() |
43 | } | 45 | } |
44 | 46 | ||
45 | getVideoUrl (videoBlacklist: VideoBlacklist) { | 47 | getVideoUrl (videoBlacklist: VideoBlacklist) { |
46 | return Video.buildClientUrl(videoBlacklist.video.uuid) | 48 | return Video.buildClientUrl(videoBlacklist.video.uuid) |
47 | } | 49 | } |
48 | 50 | ||
51 | booleanToText (value: boolean) { | ||
52 | if (value === true) return this.i18n('yes') | ||
53 | |||
54 | return this.i18n('no') | ||
55 | } | ||
56 | |||
57 | toHtml (text: string) { | ||
58 | return this.markdownRenderer.textMarkdownToHTML(text) | ||
59 | } | ||
60 | |||
49 | async removeVideoFromBlacklist (entry: VideoBlacklist) { | 61 | async removeVideoFromBlacklist (entry: VideoBlacklist) { |
50 | const confirmMessage = this.i18n( | 62 | const confirmMessage = this.i18n( |
51 | 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.' | 63 | 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.' |
@@ -56,14 +68,11 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit { | |||
56 | 68 | ||
57 | this.videoBlacklistService.removeVideoFromBlacklist(entry.video.id).subscribe( | 69 | this.videoBlacklistService.removeVideoFromBlacklist(entry.video.id).subscribe( |
58 | () => { | 70 | () => { |
59 | this.notificationsService.success( | 71 | this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name })) |
60 | this.i18n('Success'), | ||
61 | this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name }) | ||
62 | ) | ||
63 | this.loadData() | 72 | this.loadData() |
64 | }, | 73 | }, |
65 | 74 | ||
66 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 75 | err => this.notifier.error(err.message) |
67 | ) | 76 | ) |
68 | } | 77 | } |
69 | 78 | ||
@@ -75,7 +84,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit { | |||
75 | this.totalRecords = resultList.total | 84 | this.totalRecords = resultList.total |
76 | }, | 85 | }, |
77 | 86 | ||
78 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 87 | err => this.notifier.error(err.message) |
79 | ) | 88 | ) |
80 | } | 89 | } |
81 | } | 90 | } |
diff --git a/client/src/app/+admin/users/user-edit/index.ts b/client/src/app/+admin/users/user-edit/index.ts index fd80a02e0..ec734ef92 100644 --- a/client/src/app/+admin/users/user-edit/index.ts +++ b/client/src/app/+admin/users/user-edit/index.ts | |||
@@ -1,2 +1,3 @@ | |||
1 | export * from './user-create.component' | 1 | export * from './user-create.component' |
2 | export * from './user-update.component' | 2 | export * from './user-update.component' |
3 | export * from './user-password.component' | ||
diff --git a/client/src/app/+admin/users/user-edit/user-create.component.ts b/client/src/app/+admin/users/user-edit/user-create.component.ts index dd8e4efd5..137ecfcbd 100644 --- a/client/src/app/+admin/users/user-edit/user-create.component.ts +++ b/client/src/app/+admin/users/user-edit/user-create.component.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { Notifier, ServerService } from '@app/core' |
4 | import { ServerService } from '../../../core' | ||
5 | import { UserCreate, UserRole } from '../../../../../../shared' | 4 | import { UserCreate, UserRole } from '../../../../../../shared' |
6 | import { UserEdit } from './user-edit' | 5 | import { UserEdit } from './user-edit' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -24,7 +23,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
24 | protected configService: ConfigService, | 23 | protected configService: ConfigService, |
25 | private userValidatorsService: UserValidatorsService, | 24 | private userValidatorsService: UserValidatorsService, |
26 | private router: Router, | 25 | private router: Router, |
27 | private notificationsService: NotificationsService, | 26 | private notifier: Notifier, |
28 | private userService: UserService, | 27 | private userService: UserService, |
29 | private i18n: I18n | 28 | private i18n: I18n |
30 | ) { | 29 | ) { |
@@ -60,10 +59,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
60 | 59 | ||
61 | this.userService.addUser(userCreate).subscribe( | 60 | this.userService.addUser(userCreate).subscribe( |
62 | () => { | 61 | () => { |
63 | this.notificationsService.success( | 62 | this.notifier.success(this.i18n('User {{username}} created.', { username: userCreate.username })) |
64 | this.i18n('Success'), | ||
65 | this.i18n('User {{username}} created.', { username: userCreate.username }) | ||
66 | ) | ||
67 | this.router.navigate([ '/admin/users/list' ]) | 63 | this.router.navigate([ '/admin/users/list' ]) |
68 | }, | 64 | }, |
69 | 65 | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 56cf7d17d..c6566da24 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -81,3 +81,17 @@ | |||
81 | 81 | ||
82 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | 82 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> |
83 | </form> | 83 | </form> |
84 | |||
85 | <div *ngIf="!isCreation()" class="danger-zone"> | ||
86 | <div class="account-title" i18n>Danger Zone</div> | ||
87 | |||
88 | <div class="form-group reset-password-email"> | ||
89 | <label i18n>Send a link to reset the password by email to the user</label> | ||
90 | <button (click)="resetPassword()" i18n>Ask for new password</button> | ||
91 | </div> | ||
92 | |||
93 | <div class="form-group"> | ||
94 | <label i18n>Manually set the user password</label> | ||
95 | <my-user-password [userId]="userId"></my-user-password> | ||
96 | </div> | ||
97 | </div> | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss index 6675f65cc..c1cc4ca45 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss | |||
@@ -14,7 +14,7 @@ input:not([type=submit]) { | |||
14 | @include peertube-select-container(340px); | 14 | @include peertube-select-container(340px); |
15 | } | 15 | } |
16 | 16 | ||
17 | input[type=submit] { | 17 | input[type=submit], button { |
18 | @include peertube-button; | 18 | @include peertube-button; |
19 | @include orange-button; | 19 | @include orange-button; |
20 | 20 | ||
@@ -25,3 +25,23 @@ input[type=submit] { | |||
25 | margin-top: 5px; | 25 | margin-top: 5px; |
26 | font-size: 11px; | 26 | font-size: 11px; |
27 | } | 27 | } |
28 | |||
29 | .account-title { | ||
30 | @include in-content-small-title; | ||
31 | |||
32 | margin-top: 55px; | ||
33 | margin-bottom: 30px; | ||
34 | } | ||
35 | |||
36 | .danger-zone { | ||
37 | .reset-password-email { | ||
38 | margin-bottom: 30px; | ||
39 | padding-bottom: 30px; | ||
40 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); | ||
41 | |||
42 | button { | ||
43 | display: block; | ||
44 | margin-top: 0; | ||
45 | } | ||
46 | } | ||
47 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts index 07b087b5b..649b35b0c 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/users/user-edit/user-edit.ts | |||
@@ -1,14 +1,14 @@ | |||
1 | import { ServerService } from '../../../core' | 1 | import { ServerService } from '../../../core' |
2 | import { FormReactive } from '../../../shared' | 2 | import { FormReactive } from '../../../shared' |
3 | import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' | 3 | import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' |
4 | import { EditCustomConfigComponent } from '../../../+admin/config/edit-custom-config/' | ||
5 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 4 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
6 | 5 | ||
7 | export abstract class UserEdit extends FormReactive { | 6 | export abstract class UserEdit extends FormReactive { |
8 | |||
9 | videoQuotaOptions: { value: string, label: string }[] = [] | 7 | videoQuotaOptions: { value: string, label: string }[] = [] |
10 | videoQuotaDailyOptions: { value: string, label: string }[] = [] | 8 | videoQuotaDailyOptions: { value: string, label: string }[] = [] |
11 | roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) | 9 | roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) |
10 | username: string | ||
11 | userId: number | ||
12 | 12 | ||
13 | protected abstract serverService: ServerService | 13 | protected abstract serverService: ServerService |
14 | protected abstract configService: ConfigService | 14 | protected abstract configService: ConfigService |
@@ -23,7 +23,9 @@ export abstract class UserEdit extends FormReactive { | |||
23 | } | 23 | } |
24 | 24 | ||
25 | computeQuotaWithTranscoding () { | 25 | computeQuotaWithTranscoding () { |
26 | const resolutions = this.serverService.getConfig().transcoding.enabledResolutions | 26 | const transcodingConfig = this.serverService.getConfig().transcoding |
27 | |||
28 | const resolutions = transcodingConfig.enabledResolutions | ||
27 | const higherResolution = VideoResolution.H_1080P | 29 | const higherResolution = VideoResolution.H_1080P |
28 | let multiplier = 0 | 30 | let multiplier = 0 |
29 | 31 | ||
@@ -31,9 +33,15 @@ export abstract class UserEdit extends FormReactive { | |||
31 | multiplier += resolution / higherResolution | 33 | multiplier += resolution / higherResolution |
32 | } | 34 | } |
33 | 35 | ||
36 | if (transcodingConfig.hls.enabled) multiplier *= 2 | ||
37 | |||
34 | return multiplier * parseInt(this.form.value['videoQuota'], 10) | 38 | return multiplier * parseInt(this.form.value['videoQuota'], 10) |
35 | } | 39 | } |
36 | 40 | ||
41 | resetPassword () { | ||
42 | return | ||
43 | } | ||
44 | |||
37 | protected buildQuotaOptions () { | 45 | protected buildQuotaOptions () { |
38 | // These are used by a HTML select, so convert key into strings | 46 | // These are used by a HTML select, so convert key into strings |
39 | this.videoQuotaOptions = this.configService | 47 | this.videoQuotaOptions = this.configService |
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.html b/client/src/app/+admin/users/user-edit/user-password.component.html new file mode 100644 index 000000000..a1e1f6216 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.html | |||
@@ -0,0 +1,21 @@ | |||
1 | <form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> | ||
2 | <div class="form-group"> | ||
3 | |||
4 | <div class="input-group"> | ||
5 | <input id="password" [attr.type]="showPassword ? 'text' : 'password'" | ||
6 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
7 | > | ||
8 | <div class="input-group-append"> | ||
9 | <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button"> | ||
10 | <ng-container *ngIf="!showPassword" i18n>Show</ng-container> | ||
11 | <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container> | ||
12 | </button> | ||
13 | </div> | ||
14 | </div> | ||
15 | <div *ngIf="formErrors.password" class="form-error"> | ||
16 | {{ formErrors.password }} | ||
17 | </div> | ||
18 | </div> | ||
19 | |||
20 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | ||
21 | </form> | ||
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss new file mode 100644 index 000000000..217d585af --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.scss | |||
@@ -0,0 +1,22 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | input:not([type=submit]):not([type=checkbox]) { | ||
5 | @include peertube-input-text(340px); | ||
6 | |||
7 | display: block; | ||
8 | border-top-right-radius: 0; | ||
9 | border-bottom-right-radius: 0; | ||
10 | border-right: none; | ||
11 | } | ||
12 | |||
13 | input[type=submit] { | ||
14 | @include peertube-button; | ||
15 | @include orange-button; | ||
16 | |||
17 | margin-top: 10px; | ||
18 | } | ||
19 | |||
20 | .input-group-append { | ||
21 | height: 30px; | ||
22 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts new file mode 100644 index 000000000..5b3040440 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.ts | |||
@@ -0,0 +1,64 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { UserService } from '@app/shared/users/user.service' | ||
4 | import { Notifier } from '../../../core' | ||
5 | import { User, UserUpdate } from '../../../../../../shared' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | ||
9 | import { FormReactive } from '../../../shared' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-user-password', | ||
13 | templateUrl: './user-password.component.html', | ||
14 | styleUrls: [ './user-password.component.scss' ] | ||
15 | }) | ||
16 | export class UserPasswordComponent extends FormReactive implements OnInit { | ||
17 | error: string | ||
18 | username: string | ||
19 | showPassword = false | ||
20 | |||
21 | @Input() userId: number | ||
22 | |||
23 | constructor ( | ||
24 | protected formValidatorService: FormValidatorService, | ||
25 | private userValidatorsService: UserValidatorsService, | ||
26 | private route: ActivatedRoute, | ||
27 | private router: Router, | ||
28 | private notifier: Notifier, | ||
29 | private userService: UserService, | ||
30 | private i18n: I18n | ||
31 | ) { | ||
32 | super() | ||
33 | } | ||
34 | |||
35 | ngOnInit () { | ||
36 | this.buildForm({ | ||
37 | password: this.userValidatorsService.USER_PASSWORD | ||
38 | }) | ||
39 | } | ||
40 | |||
41 | formValidated () { | ||
42 | this.error = undefined | ||
43 | |||
44 | const userUpdate: UserUpdate = this.form.value | ||
45 | |||
46 | this.userService.updateUser(this.userId, userUpdate).subscribe( | ||
47 | () => { | ||
48 | this.notifier.success( | ||
49 | this.i18n('Password changed for user {{username}}.', { username: this.username }) | ||
50 | ) | ||
51 | }, | ||
52 | |||
53 | err => this.error = err.message | ||
54 | ) | ||
55 | } | ||
56 | |||
57 | togglePasswordVisibility () { | ||
58 | this.showPassword = !this.showPassword | ||
59 | } | ||
60 | |||
61 | getFormButtonTitle () { | ||
62 | return this.i18n('Update user password') | ||
63 | } | ||
64 | } | ||
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts index cd3885a99..94ef87b08 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Subscription } from 'rxjs' | 3 | import { Subscription } from 'rxjs' |
4 | import { NotificationsService } from 'angular2-notifications' | 4 | import { Notifier } from '@app/core' |
5 | import { ServerService } from '../../../core' | 5 | import { ServerService } from '../../../core' |
6 | import { UserEdit } from './user-edit' | 6 | import { UserEdit } from './user-edit' |
7 | import { User, UserUpdate } from '../../../../../../shared' | 7 | import { User, UserUpdate } from '../../../../../../shared' |
@@ -19,6 +19,7 @@ import { UserService } from '@app/shared' | |||
19 | export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | 19 | export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { |
20 | error: string | 20 | error: string |
21 | userId: number | 21 | userId: number |
22 | userEmail: string | ||
22 | username: string | 23 | username: string |
23 | 24 | ||
24 | private paramsSub: Subscription | 25 | private paramsSub: Subscription |
@@ -30,7 +31,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
30 | private userValidatorsService: UserValidatorsService, | 31 | private userValidatorsService: UserValidatorsService, |
31 | private route: ActivatedRoute, | 32 | private route: ActivatedRoute, |
32 | private router: Router, | 33 | private router: Router, |
33 | private notificationsService: NotificationsService, | 34 | private notifier: Notifier, |
34 | private userService: UserService, | 35 | private userService: UserService, |
35 | private i18n: I18n | 36 | private i18n: I18n |
36 | ) { | 37 | ) { |
@@ -73,10 +74,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
73 | 74 | ||
74 | this.userService.updateUser(this.userId, userUpdate).subscribe( | 75 | this.userService.updateUser(this.userId, userUpdate).subscribe( |
75 | () => { | 76 | () => { |
76 | this.notificationsService.success( | 77 | this.notifier.success(this.i18n('User {{username}} updated.', { username: this.username })) |
77 | this.i18n('Success'), | ||
78 | this.i18n('User {{username}} updated.', { username: this.username }) | ||
79 | ) | ||
80 | this.router.navigate([ '/admin/users/list' ]) | 78 | this.router.navigate([ '/admin/users/list' ]) |
81 | }, | 79 | }, |
82 | 80 | ||
@@ -92,9 +90,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
92 | return this.i18n('Update user') | 90 | return this.i18n('Update user') |
93 | } | 91 | } |
94 | 92 | ||
93 | resetPassword () { | ||
94 | this.userService.askResetPassword(this.userEmail).subscribe( | ||
95 | () => { | ||
96 | this.notifier.success( | ||
97 | this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username }) | ||
98 | ) | ||
99 | }, | ||
100 | |||
101 | err => this.error = err.message | ||
102 | ) | ||
103 | } | ||
104 | |||
95 | private onUserFetched (userJson: User) { | 105 | private onUserFetched (userJson: User) { |
96 | this.userId = userJson.id | 106 | this.userId = userJson.id |
97 | this.username = userJson.username | 107 | this.username = userJson.username |
108 | this.userEmail = userJson.email | ||
98 | 109 | ||
99 | this.form.patchValue({ | 110 | this.form.patchValue({ |
100 | email: userJson.email, | 111 | email: userJson.email, |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index cca057ba1..69a4616a3 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -2,7 +2,7 @@ | |||
2 | <div i18n class="form-sub-title">Users list</div> | 2 | <div i18n class="form-sub-title">Users list</div> |
3 | 3 | ||
4 | <a class="add-button" routerLink="/admin/users/create"> | 4 | <a class="add-button" routerLink="/admin/users/create"> |
5 | <span class="icon icon-add"></span> | 5 | <my-global-icon iconName="add"></my-global-icon> |
6 | <ng-container i18n>Create user</ng-container> | 6 | <ng-container i18n>Create user</ng-container> |
7 | </a> | 7 | </a> |
8 | </div> | 8 | </div> |
@@ -10,9 +10,32 @@ | |||
10 | <p-table | 10 | <p-table |
11 | [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | 11 | [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" |
12 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" | 12 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" |
13 | [(selection)]="selectedUsers" | ||
13 | > | 14 | > |
15 | <ng-template pTemplate="caption"> | ||
16 | <div class="caption"> | ||
17 | <div> | ||
18 | <my-action-dropdown | ||
19 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | ||
20 | [actions]="bulkUserActions" [entry]="selectedUsers" | ||
21 | > | ||
22 | </my-action-dropdown> | ||
23 | </div> | ||
24 | |||
25 | <div> | ||
26 | <input | ||
27 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
28 | (keyup)="onSearch($event.target.value)" | ||
29 | > | ||
30 | </div> | ||
31 | </div> | ||
32 | </ng-template> | ||
33 | |||
14 | <ng-template pTemplate="header"> | 34 | <ng-template pTemplate="header"> |
15 | <tr> | 35 | <tr> |
36 | <th style="width: 40px"> | ||
37 | <p-tableHeaderCheckbox></p-tableHeaderCheckbox> | ||
38 | </th> | ||
16 | <th style="width: 40px"></th> | 39 | <th style="width: 40px"></th> |
17 | <th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th> | 40 | <th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th> |
18 | <th i18n>Email</th> | 41 | <th i18n>Email</th> |
@@ -25,22 +48,42 @@ | |||
25 | 48 | ||
26 | <ng-template pTemplate="body" let-expanded="expanded" let-user> | 49 | <ng-template pTemplate="body" let-expanded="expanded" let-user> |
27 | 50 | ||
28 | <tr [ngClass]="{ banned: user.blocked }"> | 51 | <tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }"> |
52 | <td> | ||
53 | <p-tableCheckbox [value]="user"></p-tableCheckbox> | ||
54 | </td> | ||
55 | |||
29 | <td> | 56 | <td> |
30 | <span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user"> | 57 | <span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user"> |
31 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | 58 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> |
32 | </span> | 59 | </span> |
33 | </td> | 60 | </td> |
61 | |||
34 | <td> | 62 | <td> |
35 | {{ user.username }} | 63 | <a i18n-title title="Go to the account page" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]"> |
36 | <span *ngIf="user.blocked" class="banned-info">(banned)</span> | 64 | {{ user.username }} |
65 | <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span> | ||
66 | </a> | ||
37 | </td> | 67 | </td> |
38 | <td>{{ user.email }}</td> | 68 | |
69 | <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td> | ||
70 | |||
71 | <ng-template #emailWithVerificationStatus> | ||
72 | <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login"> | ||
73 | <em>? {{ user.email }}</em> | ||
74 | </td> | ||
75 | <ng-template #emailVerifiedNotFalse> | ||
76 | <td i18n-title title="User's email is verified / User can login without email verification"> | ||
77 | ✓ {{ user.email }} | ||
78 | </td> | ||
79 | </ng-template> | ||
80 | </ng-template> | ||
81 | |||
39 | <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> | 82 | <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> |
40 | <td>{{ user.roleLabel }}</td> | 83 | <td>{{ user.roleLabel }}</td> |
41 | <td>{{ user.createdAt }}</td> | 84 | <td>{{ user.createdAt }}</td> |
42 | <td class="action-cell"> | 85 | <td class="action-cell"> |
43 | <my-user-moderation-dropdown [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> | 86 | <my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> |
44 | </my-user-moderation-dropdown> | 87 | </my-user-moderation-dropdown> |
45 | </td> | 88 | </td> |
46 | </tr> | 89 | </tr> |
@@ -56,3 +99,4 @@ | |||
56 | </ng-template> | 99 | </ng-template> |
57 | </p-table> | 100 | </p-table> |
58 | 101 | ||
102 | <my-user-ban-modal #userBanModal (userBanned)="onUserChanged()"></my-user-ban-modal> | ||
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss index 47291918d..5274be01c 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/users/user-list/user-list.component.scss | |||
@@ -2,7 +2,7 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .add-button { | 4 | .add-button { |
5 | @include create-button('../../../../assets/images/global/add.svg'); | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | tr.banned { | 8 | tr.banned { |
@@ -15,4 +15,12 @@ tr.banned { | |||
15 | 15 | ||
16 | .ban-reason-label { | 16 | .ban-reason-label { |
17 | font-weight: $font-semibold; | 17 | font-weight: $font-semibold; |
18 | } \ No newline at end of file | 18 | } |
19 | |||
20 | .caption { | ||
21 | justify-content: space-between; | ||
22 | |||
23 | input { | ||
24 | @include peertube-input-text(250px); | ||
25 | } | ||
26 | } | ||
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index dee3ed643..66ab796f9 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts | |||
@@ -1,10 +1,12 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/components/common/sortmeta' |
4 | import { ConfirmService } from '../../../core' | 4 | import { ConfirmService, ServerService } from '../../../core' |
5 | import { RestPagination, RestTable, UserService } from '../../../shared' | 5 | import { RestPagination, RestTable, UserService } from '../../../shared' |
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { User } from '../../../../../../shared' | 7 | import { User } from '../../../../../../shared' |
8 | import { UserBanModalComponent } from '@app/shared/moderation' | ||
9 | import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' | ||
8 | 10 | ||
9 | @Component({ | 11 | @Component({ |
10 | selector: 'my-user-list', | 12 | selector: 'my-user-list', |
@@ -12,38 +14,139 @@ import { User } from '../../../../../../shared' | |||
12 | styleUrls: [ './user-list.component.scss' ] | 14 | styleUrls: [ './user-list.component.scss' ] |
13 | }) | 15 | }) |
14 | export class UserListComponent extends RestTable implements OnInit { | 16 | export class UserListComponent extends RestTable implements OnInit { |
17 | @ViewChild('userBanModal') userBanModal: UserBanModalComponent | ||
18 | |||
15 | users: User[] = [] | 19 | users: User[] = [] |
16 | totalRecords = 0 | 20 | totalRecords = 0 |
17 | rowsPerPage = 10 | 21 | rowsPerPage = 10 |
18 | sort: SortMeta = { field: 'createdAt', order: 1 } | 22 | sort: SortMeta = { field: 'createdAt', order: 1 } |
19 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 23 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
20 | 24 | ||
25 | selectedUsers: User[] = [] | ||
26 | bulkUserActions: DropdownAction<User[]>[] = [] | ||
27 | |||
21 | constructor ( | 28 | constructor ( |
22 | private notificationsService: NotificationsService, | 29 | private notifier: Notifier, |
23 | private confirmService: ConfirmService, | 30 | private confirmService: ConfirmService, |
31 | private serverService: ServerService, | ||
24 | private userService: UserService, | 32 | private userService: UserService, |
25 | private i18n: I18n | 33 | private i18n: I18n |
26 | ) { | 34 | ) { |
27 | super() | 35 | super() |
28 | } | 36 | } |
29 | 37 | ||
38 | get requiresEmailVerification () { | ||
39 | return this.serverService.getConfig().signup.requiresEmailVerification | ||
40 | } | ||
41 | |||
30 | ngOnInit () { | 42 | ngOnInit () { |
31 | this.loadSort() | 43 | this.initialize() |
44 | |||
45 | this.bulkUserActions = [ | ||
46 | { | ||
47 | label: this.i18n('Delete'), | ||
48 | handler: users => this.removeUsers(users) | ||
49 | }, | ||
50 | { | ||
51 | label: this.i18n('Ban'), | ||
52 | handler: users => this.openBanUserModal(users), | ||
53 | isDisplayed: users => users.every(u => u.blocked === false) | ||
54 | }, | ||
55 | { | ||
56 | label: this.i18n('Unban'), | ||
57 | handler: users => this.unbanUsers(users), | ||
58 | isDisplayed: users => users.every(u => u.blocked === true) | ||
59 | }, | ||
60 | { | ||
61 | label: this.i18n('Set Email as Verified'), | ||
62 | handler: users => this.setEmailsAsVerified(users), | ||
63 | isDisplayed: users => this.requiresEmailVerification && users.every(u => !u.blocked && u.emailVerified === false) | ||
64 | } | ||
65 | ] | ||
66 | } | ||
67 | |||
68 | openBanUserModal (users: User[]) { | ||
69 | for (const user of users) { | ||
70 | if (user.username === 'root') { | ||
71 | this.notifier.error(this.i18n('You cannot ban root.')) | ||
72 | return | ||
73 | } | ||
74 | } | ||
75 | |||
76 | this.userBanModal.openModal(users) | ||
32 | } | 77 | } |
33 | 78 | ||
34 | onUserChanged () { | 79 | onUserChanged () { |
35 | this.loadData() | 80 | this.loadData() |
36 | } | 81 | } |
37 | 82 | ||
83 | async unbanUsers (users: User[]) { | ||
84 | const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length }) | ||
85 | |||
86 | const res = await this.confirmService.confirm(message, this.i18n('Unban')) | ||
87 | if (res === false) return | ||
88 | |||
89 | this.userService.unbanUsers(users) | ||
90 | .subscribe( | ||
91 | () => { | ||
92 | const message = this.i18n('{{num}} users unbanned.', { num: users.length }) | ||
93 | |||
94 | this.notifier.success(message) | ||
95 | this.loadData() | ||
96 | }, | ||
97 | |||
98 | err => this.notifier.error(err.message) | ||
99 | ) | ||
100 | } | ||
101 | |||
102 | async removeUsers (users: User[]) { | ||
103 | for (const user of users) { | ||
104 | if (user.username === 'root') { | ||
105 | this.notifier.error(this.i18n('You cannot delete root.')) | ||
106 | return | ||
107 | } | ||
108 | } | ||
109 | |||
110 | const message = this.i18n('If you remove these users, you will not be able to create others with the same username!') | ||
111 | const res = await this.confirmService.confirm(message, this.i18n('Delete')) | ||
112 | if (res === false) return | ||
113 | |||
114 | this.userService.removeUser(users).subscribe( | ||
115 | () => { | ||
116 | this.notifier.success(this.i18n('{{num}} users deleted.', { num: users.length })) | ||
117 | this.loadData() | ||
118 | }, | ||
119 | |||
120 | err => this.notifier.error(err.message) | ||
121 | ) | ||
122 | } | ||
123 | |||
124 | async setEmailsAsVerified (users: User[]) { | ||
125 | this.userService.updateUsers(users, { emailVerified: true }).subscribe( | ||
126 | () => { | ||
127 | this.notifier.success(this.i18n('{{num}} users email set as verified.', { num: users.length })) | ||
128 | this.loadData() | ||
129 | }, | ||
130 | |||
131 | err => this.notifier.error(err.message) | ||
132 | ) | ||
133 | } | ||
134 | |||
135 | isInSelectionMode () { | ||
136 | return this.selectedUsers.length !== 0 | ||
137 | } | ||
138 | |||
38 | protected loadData () { | 139 | protected loadData () { |
39 | this.userService.getUsers(this.pagination, this.sort) | 140 | this.selectedUsers = [] |
40 | .subscribe( | 141 | |
41 | resultList => { | 142 | this.userService.getUsers(this.pagination, this.sort, this.search) |
42 | this.users = resultList.data | 143 | .subscribe( |
43 | this.totalRecords = resultList.total | 144 | resultList => { |
44 | }, | 145 | this.users = resultList.data |
45 | 146 | this.totalRecords = resultList.total | |
46 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 147 | }, |
47 | ) | 148 | |
149 | err => this.notifier.error(err.message) | ||
150 | ) | ||
48 | } | 151 | } |
49 | } | 152 | } |
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html new file mode 100644 index 000000000..a96a11f5e --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html | |||
@@ -0,0 +1,26 @@ | |||
1 | <div class="admin-sub-header"> | ||
2 | <div i18n class="form-sub-title">Muted accounts</div> | ||
3 | </div> | ||
4 | |||
5 | <p-table | ||
6 | [value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
7 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" | ||
8 | > | ||
9 | |||
10 | <ng-template pTemplate="header"> | ||
11 | <tr> | ||
12 | <th i18n>Account</th> | ||
13 | <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
14 | </tr> | ||
15 | </ng-template> | ||
16 | |||
17 | <ng-template pTemplate="body" let-accountBlock> | ||
18 | <tr> | ||
19 | <td>{{ accountBlock.blockedAccount.nameWithHost }}</td> | ||
20 | <td>{{ accountBlock.createdAt }}</td> | ||
21 | <td class="action-cell"> | ||
22 | <button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button> | ||
23 | </td> | ||
24 | </tr> | ||
25 | </ng-template> | ||
26 | </p-table> | ||
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss new file mode 100644 index 000000000..6028b75ea --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss | |||
@@ -0,0 +1,7 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .unblock-button { | ||
5 | @include peertube-button; | ||
6 | @include grey-button; | ||
7 | } \ No newline at end of file | ||
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts new file mode 100644 index 000000000..e3025dec4 --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts | |||
@@ -0,0 +1,56 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Notifier } from '@app/core' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { RestPagination, RestTable } from '@app/shared' | ||
5 | import { SortMeta } from 'primeng/components/common/sortmeta' | ||
6 | import { AccountBlock, BlocklistService } from '@app/shared/blocklist' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-account-blocklist', | ||
10 | styleUrls: [ './my-account-blocklist.component.scss' ], | ||
11 | templateUrl: './my-account-blocklist.component.html' | ||
12 | }) | ||
13 | export class MyAccountBlocklistComponent extends RestTable implements OnInit { | ||
14 | blockedAccounts: AccountBlock[] = [] | ||
15 | totalRecords = 0 | ||
16 | rowsPerPage = 10 | ||
17 | sort: SortMeta = { field: 'createdAt', order: -1 } | ||
18 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
19 | |||
20 | constructor ( | ||
21 | private notifier: Notifier, | ||
22 | private blocklistService: BlocklistService, | ||
23 | private i18n: I18n | ||
24 | ) { | ||
25 | super() | ||
26 | } | ||
27 | |||
28 | ngOnInit () { | ||
29 | this.initialize() | ||
30 | } | ||
31 | |||
32 | unblockAccount (accountBlock: AccountBlock) { | ||
33 | const blockedAccount = accountBlock.blockedAccount | ||
34 | |||
35 | this.blocklistService.unblockAccountByUser(blockedAccount) | ||
36 | .subscribe( | ||
37 | () => { | ||
38 | this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost })) | ||
39 | |||
40 | this.loadData() | ||
41 | } | ||
42 | ) | ||
43 | } | ||
44 | |||
45 | protected loadData () { | ||
46 | return this.blocklistService.getUserAccountBlocklist(this.pagination, this.sort) | ||
47 | .subscribe( | ||
48 | resultList => { | ||
49 | this.blockedAccounts = resultList.data | ||
50 | this.totalRecords = resultList.total | ||
51 | }, | ||
52 | |||
53 | err => this.notifier.error(err.message) | ||
54 | ) | ||
55 | } | ||
56 | } | ||
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html new file mode 100644 index 000000000..329cfb08f --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html | |||
@@ -0,0 +1,27 @@ | |||
1 | <div class="admin-sub-header"> | ||
2 | <div i18n class="form-sub-title">Muted instances</div> | ||
3 | </div> | ||
4 | |||
5 | <p-table | ||
6 | [value]="blockedServers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | ||
7 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" | ||
8 | > | ||
9 | |||
10 | <ng-template pTemplate="header"> | ||
11 | <tr> | ||
12 | <th i18n>Instance</th> | ||
13 | <th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th> | ||
14 | <th></th> | ||
15 | </tr> | ||
16 | </ng-template> | ||
17 | |||
18 | <ng-template pTemplate="body" let-serverBlock> | ||
19 | <tr> | ||
20 | <td>{{ serverBlock.blockedServer.host }}</td> | ||
21 | <td>{{ serverBlock.createdAt }}</td> | ||
22 | <td class="action-cell"> | ||
23 | <button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button> | ||
24 | </td> | ||
25 | </tr> | ||
26 | </ng-template> | ||
27 | </p-table> | ||
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss new file mode 100644 index 000000000..6028b75ea --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss | |||
@@ -0,0 +1,7 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .unblock-button { | ||
5 | @include peertube-button; | ||
6 | @include grey-button; | ||
7 | } \ No newline at end of file | ||
diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts new file mode 100644 index 000000000..4c5cc28b8 --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts | |||
@@ -0,0 +1,57 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { Notifier } from '@app/core' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { RestPagination, RestTable } from '@app/shared' | ||
5 | import { SortMeta } from 'primeng/components/common/sortmeta' | ||
6 | import { ServerBlock } from '../../../../../shared' | ||
7 | import { BlocklistService } from '@app/shared/blocklist' | ||
8 | |||
9 | @Component({ | ||
10 | selector: 'my-account-server-blocklist', | ||
11 | styleUrls: [ './my-account-server-blocklist.component.scss' ], | ||
12 | templateUrl: './my-account-server-blocklist.component.html' | ||
13 | }) | ||
14 | export class MyAccountServerBlocklistComponent extends RestTable implements OnInit { | ||
15 | blockedServers: ServerBlock[] = [] | ||
16 | totalRecords = 0 | ||
17 | rowsPerPage = 10 | ||
18 | sort: SortMeta = { field: 'createdAt', order: -1 } | ||
19 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | ||
20 | |||
21 | constructor ( | ||
22 | private notifier: Notifier, | ||
23 | private blocklistService: BlocklistService, | ||
24 | private i18n: I18n | ||
25 | ) { | ||
26 | super() | ||
27 | } | ||
28 | |||
29 | ngOnInit () { | ||
30 | this.initialize() | ||
31 | } | ||
32 | |||
33 | unblockServer (serverBlock: ServerBlock) { | ||
34 | const host = serverBlock.blockedServer.host | ||
35 | |||
36 | this.blocklistService.unblockServerByUser(host) | ||
37 | .subscribe( | ||
38 | () => { | ||
39 | this.notifier.success(this.i18n('Instance {{host}} unmuted.', { host })) | ||
40 | |||
41 | this.loadData() | ||
42 | } | ||
43 | ) | ||
44 | } | ||
45 | |||
46 | protected loadData () { | ||
47 | return this.blocklistService.getUserServerBlocklist(this.pagination, this.sort) | ||
48 | .subscribe( | ||
49 | resultList => { | ||
50 | this.blockedServers = resultList.data | ||
51 | this.totalRecords = resultList.total | ||
52 | }, | ||
53 | |||
54 | err => this.notifier.error(err.message) | ||
55 | ) | ||
56 | } | ||
57 | } | ||
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.html b/client/src/app/+my-account/my-account-history/my-account-history.component.html new file mode 100644 index 000000000..d42af37d4 --- /dev/null +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.html | |||
@@ -0,0 +1,27 @@ | |||
1 | <div class="top-buttons"> | ||
2 | <div class="history-switch"> | ||
3 | <p-inputSwitch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></p-inputSwitch> | ||
4 | <label i18n>History enabled</label> | ||
5 | </div> | ||
6 | |||
7 | <div class="delete-history"> | ||
8 | <button (click)="deleteHistory()" i18n>Delete history</button> | ||
9 | </div> | ||
10 | </div> | ||
11 | |||
12 | |||
13 | <div class="no-history" i18n *ngIf="pagination.totalItems === 0">You don't have videos history yet.</div> | ||
14 | |||
15 | <div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" class="videos" #videosElement> | ||
16 | <div *ngFor="let videos of videoPages;" class="videos-page"> | ||
17 | <div class="video" *ngFor="let video of videos"> | ||
18 | <my-video-thumbnail [video]="video"></my-video-thumbnail> | ||
19 | |||
20 | <div class="video-info"> | ||
21 | <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> | ||
22 | <span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span> | ||
23 | <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a> | ||
24 | </div> | ||
25 | </div> | ||
26 | </div> | ||
27 | </div> | ||
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.scss b/client/src/app/+my-account/my-account-history/my-account-history.component.scss new file mode 100644 index 000000000..e7c6863f1 --- /dev/null +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.scss | |||
@@ -0,0 +1,99 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .no-history { | ||
5 | display: flex; | ||
6 | justify-content: center; | ||
7 | margin-top: 50px; | ||
8 | font-weight: $font-semibold; | ||
9 | font-size: 16px; | ||
10 | } | ||
11 | |||
12 | .top-buttons { | ||
13 | margin-bottom: 20px; | ||
14 | display: flex; | ||
15 | |||
16 | .history-switch { | ||
17 | display: flex; | ||
18 | flex-grow: 1; | ||
19 | |||
20 | label { | ||
21 | margin: 0 0 0 5px; | ||
22 | } | ||
23 | } | ||
24 | |||
25 | .delete-history { | ||
26 | font-size: 15px; | ||
27 | |||
28 | button { | ||
29 | @include peertube-button; | ||
30 | @include grey-button; | ||
31 | } | ||
32 | } | ||
33 | } | ||
34 | |||
35 | .video { | ||
36 | @include row-blocks; | ||
37 | |||
38 | my-video-thumbnail { | ||
39 | margin-right: 10px; | ||
40 | } | ||
41 | |||
42 | .video-info { | ||
43 | flex-grow: 1; | ||
44 | |||
45 | .video-info-name { | ||
46 | @include disable-default-a-behaviour; | ||
47 | |||
48 | color: var(--mainForegroundColor); | ||
49 | display: block; | ||
50 | width: fit-content; | ||
51 | font-size: 18px; | ||
52 | font-weight: $font-semibold; | ||
53 | } | ||
54 | |||
55 | .video-info-date-views { | ||
56 | font-size: 14px; | ||
57 | } | ||
58 | |||
59 | .video-info-account { | ||
60 | @include disable-default-a-behaviour; | ||
61 | |||
62 | display: block; | ||
63 | width: fit-content; | ||
64 | overflow: hidden; | ||
65 | text-overflow: ellipsis; | ||
66 | white-space: nowrap; | ||
67 | font-size: 14px; | ||
68 | color: $grey-foreground-color; | ||
69 | |||
70 | &:hover { | ||
71 | color: $grey-foreground-hover-color; | ||
72 | } | ||
73 | } | ||
74 | } | ||
75 | } | ||
76 | |||
77 | @media screen and (max-width: $small-view) { | ||
78 | .video { | ||
79 | flex-direction: column; | ||
80 | height: auto; | ||
81 | text-align: center; | ||
82 | |||
83 | .video-info-name { | ||
84 | margin: auto; | ||
85 | } | ||
86 | |||
87 | input[type=checkbox] { | ||
88 | display: none; | ||
89 | } | ||
90 | |||
91 | my-video-thumbnail { | ||
92 | margin-right: 0; | ||
93 | } | ||
94 | |||
95 | .video-buttons { | ||
96 | margin-top: 10px; | ||
97 | } | ||
98 | } | ||
99 | } | ||
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.ts b/client/src/app/+my-account/my-account-history/my-account-history.component.ts new file mode 100644 index 000000000..394091bad --- /dev/null +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.ts | |||
@@ -0,0 +1,107 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Location } from '@angular/common' | ||
4 | import { immutableAssign } from '@app/shared/misc/utils' | ||
5 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | ||
6 | import { AuthService } from '../../core/auth' | ||
7 | import { ConfirmService } from '../../core/confirm' | ||
8 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | ||
9 | import { VideoService } from '../../shared/video/video.service' | ||
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
11 | import { ScreenService } from '@app/shared/misc/screen.service' | ||
12 | import { UserHistoryService } from '@app/shared/users/user-history.service' | ||
13 | import { UserService } from '@app/shared' | ||
14 | import { Notifier } from '@app/core' | ||
15 | |||
16 | @Component({ | ||
17 | selector: 'my-account-history', | ||
18 | templateUrl: './my-account-history.component.html', | ||
19 | styleUrls: [ './my-account-history.component.scss' ] | ||
20 | }) | ||
21 | export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy { | ||
22 | titlePage: string | ||
23 | currentRoute = '/my-account/history/videos' | ||
24 | pagination: ComponentPagination = { | ||
25 | currentPage: 1, | ||
26 | itemsPerPage: 5, | ||
27 | totalItems: null | ||
28 | } | ||
29 | videosHistoryEnabled: boolean | ||
30 | |||
31 | protected baseVideoWidth = -1 | ||
32 | protected baseVideoHeight = 155 | ||
33 | |||
34 | constructor ( | ||
35 | protected router: Router, | ||
36 | protected route: ActivatedRoute, | ||
37 | protected authService: AuthService, | ||
38 | protected userService: UserService, | ||
39 | protected notifier: Notifier, | ||
40 | protected location: Location, | ||
41 | protected screenService: ScreenService, | ||
42 | protected i18n: I18n, | ||
43 | private confirmService: ConfirmService, | ||
44 | private videoService: VideoService, | ||
45 | private userHistoryService: UserHistoryService | ||
46 | ) { | ||
47 | super() | ||
48 | |||
49 | this.titlePage = this.i18n('My videos history') | ||
50 | } | ||
51 | |||
52 | ngOnInit () { | ||
53 | super.ngOnInit() | ||
54 | |||
55 | this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled | ||
56 | } | ||
57 | |||
58 | ngOnDestroy () { | ||
59 | super.ngOnDestroy() | ||
60 | } | ||
61 | |||
62 | getVideosObservable (page: number) { | ||
63 | const newPagination = immutableAssign(this.pagination, { currentPage: page }) | ||
64 | |||
65 | return this.userHistoryService.getUserVideosHistory(newPagination) | ||
66 | } | ||
67 | |||
68 | generateSyndicationList () { | ||
69 | throw new Error('Method not implemented.') | ||
70 | } | ||
71 | |||
72 | onVideosHistoryChange () { | ||
73 | this.userService.updateMyProfile({ videosHistoryEnabled: this.videosHistoryEnabled }) | ||
74 | .subscribe( | ||
75 | () => { | ||
76 | const message = this.videosHistoryEnabled === true ? | ||
77 | this.i18n('Videos history is enabled') : | ||
78 | this.i18n('Videos history is disabled') | ||
79 | |||
80 | this.notifier.success(message) | ||
81 | |||
82 | this.authService.refreshUserInformation() | ||
83 | }, | ||
84 | |||
85 | err => this.notifier.error(err.message) | ||
86 | ) | ||
87 | } | ||
88 | |||
89 | async deleteHistory () { | ||
90 | const title = this.i18n('Delete videos history') | ||
91 | const message = this.i18n('Are you sure you want to delete all your videos history?') | ||
92 | |||
93 | const res = await this.confirmService.confirm(message, title) | ||
94 | if (res !== true) return | ||
95 | |||
96 | this.userHistoryService.deleteUserVideosHistory() | ||
97 | .subscribe( | ||
98 | () => { | ||
99 | this.notifier.success(this.i18n('Videos history deleted')) | ||
100 | |||
101 | this.reloadVideos() | ||
102 | }, | ||
103 | |||
104 | err => this.notifier.error(err.message) | ||
105 | ) | ||
106 | } | ||
107 | } | ||
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html new file mode 100644 index 000000000..d518b22ec --- /dev/null +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html | |||
@@ -0,0 +1,13 @@ | |||
1 | <div class="header"> | ||
2 | <a routerLink="/my-account/settings" fragment="notifications" i18n> | ||
3 | <my-global-icon iconName="cog"></my-global-icon> | ||
4 | Notification preferences | ||
5 | </a> | ||
6 | |||
7 | <button (click)="markAllAsRead()" i18n> | ||
8 | <my-global-icon iconName="circle-tick"></my-global-icon> | ||
9 | Mark all as read | ||
10 | </button> | ||
11 | </div> | ||
12 | |||
13 | <my-user-notifications #userNotification></my-user-notifications> | ||
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss new file mode 100644 index 000000000..43d1f82ab --- /dev/null +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss | |||
@@ -0,0 +1,25 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .header { | ||
5 | display: flex; | ||
6 | justify-content: space-between; | ||
7 | font-size: 15px; | ||
8 | margin-bottom: 20px; | ||
9 | |||
10 | a { | ||
11 | @include peertube-button-link; | ||
12 | @include grey-button; | ||
13 | @include button-with-icon(18px, 3px, -1px); | ||
14 | } | ||
15 | |||
16 | button { | ||
17 | @include peertube-button; | ||
18 | @include grey-button; | ||
19 | @include button-with-icon(20px, 3px, -1px); | ||
20 | } | ||
21 | } | ||
22 | |||
23 | my-user-notifications { | ||
24 | font-size: 15px; | ||
25 | } | ||
diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts new file mode 100644 index 000000000..3e197088d --- /dev/null +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import { Component, ViewChild } from '@angular/core' | ||
2 | import { UserNotificationsComponent } from '@app/shared' | ||
3 | |||
4 | @Component({ | ||
5 | templateUrl: './my-account-notifications.component.html', | ||
6 | styleUrls: [ './my-account-notifications.component.scss' ] | ||
7 | }) | ||
8 | export class MyAccountNotificationsComponent { | ||
9 | @ViewChild('userNotification') userNotification: UserNotificationsComponent | ||
10 | |||
11 | markAllAsRead () { | ||
12 | this.userNotification.markAllAsRead() | ||
13 | } | ||
14 | } | ||
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html index fd7d7d23b..674a4e8a2 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html +++ b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> | 1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Accept ownership</h4> | 3 | <h4 i18n class="modal-title">Accept ownership</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body" [formGroup]="form"> | 8 | <div class="modal-body" [formGroup]="form"> |
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts index a68b452ec..79d29b139 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts +++ b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { AuthService, Notifier } from '@app/core' |
3 | import { FormReactive } from '@app/shared' | 3 | import { FormReactive } 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' |
5 | import { VideoOwnershipService } from '@app/shared/video-ownership' | 5 | import { VideoOwnershipService } from '@app/shared/video-ownership' |
@@ -8,7 +8,6 @@ import { VideoAcceptOwnershipValidatorsService } from '@app/shared/forms/form-va | |||
8 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 8 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
9 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | 9 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' |
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { AuthService } from '@app/core' | ||
12 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 11 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
13 | 12 | ||
14 | @Component({ | 13 | @Component({ |
@@ -31,7 +30,7 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O | |||
31 | protected formValidatorService: FormValidatorService, | 30 | protected formValidatorService: FormValidatorService, |
32 | private videoChangeOwnershipValidatorsService: VideoAcceptOwnershipValidatorsService, | 31 | private videoChangeOwnershipValidatorsService: VideoAcceptOwnershipValidatorsService, |
33 | private videoOwnershipService: VideoOwnershipService, | 32 | private videoOwnershipService: VideoOwnershipService, |
34 | private notificationsService: NotificationsService, | 33 | private notifier: Notifier, |
35 | private authService: AuthService, | 34 | private authService: AuthService, |
36 | private videoChannelService: VideoChannelService, | 35 | private videoChannelService: VideoChannelService, |
37 | private modalService: NgbModal, | 36 | private modalService: NgbModal, |
@@ -68,12 +67,12 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O | |||
68 | .acceptOwnership(videoChangeOwnership.id, { channelId: channel }) | 67 | .acceptOwnership(videoChangeOwnership.id, { channelId: channel }) |
69 | .subscribe( | 68 | .subscribe( |
70 | () => { | 69 | () => { |
71 | this.notificationsService.success(this.i18n('Success'), this.i18n('Ownership accepted')) | 70 | this.notifier.success(this.i18n('Ownership accepted')) |
72 | if (this.accepted) this.accepted.emit() | 71 | if (this.accepted) this.accepted.emit() |
73 | this.videoChangeOwnership = undefined | 72 | this.videoChangeOwnership = undefined |
74 | }, | 73 | }, |
75 | 74 | ||
76 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 75 | err => this.notifier.error(err.message) |
77 | ) | 76 | ) |
78 | } | 77 | } |
79 | } | 78 | } |
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html index 379fd8bb1..5709e9f54 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.html | |||
@@ -40,10 +40,10 @@ | |||
40 | <td class="action-cell"> | 40 | <td class="action-cell"> |
41 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> | 41 | <ng-container *ngIf="videoChangeOwnership.status === 'WAITING'"> |
42 | <my-button i18n label="Accept" | 42 | <my-button i18n label="Accept" |
43 | icon="icon-tick" | 43 | icon="tick" |
44 | (click)="openAcceptModal(videoChangeOwnership)"></my-button> | 44 | (click)="openAcceptModal(videoChangeOwnership)"></my-button> |
45 | <my-button i18n label="Refuse" | 45 | <my-button i18n label="Refuse" |
46 | icon="icon-cross" | 46 | icon="cross" |
47 | (click)="refuse(videoChangeOwnership)">Refuse</my-button> | 47 | (click)="refuse(videoChangeOwnership)">Refuse</my-button> |
48 | </ng-container> | 48 | </ng-container> |
49 | </td> | 49 | </td> |
diff --git a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts index 13517b9f4..77857c4fd 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts +++ b/client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts | |||
@@ -1,13 +1,11 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { RestPagination, RestTable } from '@app/shared' | 3 | import { RestPagination, RestTable } from '@app/shared' |
5 | import { SortMeta } from 'primeng/components/common/sortmeta' | 4 | import { SortMeta } from 'primeng/components/common/sortmeta' |
6 | import { VideoChangeOwnership } from '../../../../../shared' | 5 | import { VideoChangeOwnership } from '../../../../../shared' |
7 | import { VideoOwnershipService } from '@app/shared/video-ownership' | 6 | import { VideoOwnershipService } from '@app/shared/video-ownership' |
8 | import { Account } from '@app/shared/account/account.model' | 7 | import { Account } from '@app/shared/account/account.model' |
9 | import { MyAccountAcceptOwnershipComponent } | 8 | import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component' |
10 | from '@app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component' | ||
11 | 9 | ||
12 | @Component({ | 10 | @Component({ |
13 | selector: 'my-account-ownership', | 11 | selector: 'my-account-ownership', |
@@ -23,27 +21,14 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit { | |||
23 | @ViewChild('myAccountAcceptOwnershipComponent') myAccountAcceptOwnershipComponent: MyAccountAcceptOwnershipComponent | 21 | @ViewChild('myAccountAcceptOwnershipComponent') myAccountAcceptOwnershipComponent: MyAccountAcceptOwnershipComponent |
24 | 22 | ||
25 | constructor ( | 23 | constructor ( |
26 | private notificationsService: NotificationsService, | 24 | private notifier: Notifier, |
27 | private videoOwnershipService: VideoOwnershipService, | 25 | private videoOwnershipService: VideoOwnershipService |
28 | private i18n: I18n | ||
29 | ) { | 26 | ) { |
30 | super() | 27 | super() |
31 | } | 28 | } |
32 | 29 | ||
33 | ngOnInit () { | 30 | ngOnInit () { |
34 | this.loadSort() | 31 | this.initialize() |
35 | } | ||
36 | |||
37 | protected loadData () { | ||
38 | return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) | ||
39 | .subscribe( | ||
40 | resultList => { | ||
41 | this.videoChangeOwnerships = resultList.data | ||
42 | this.totalRecords = resultList.total | ||
43 | }, | ||
44 | |||
45 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
46 | ) | ||
47 | } | 32 | } |
48 | 33 | ||
49 | createByString (account: Account) { | 34 | createByString (account: Account) { |
@@ -62,7 +47,19 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit { | |||
62 | this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id) | 47 | this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id) |
63 | .subscribe( | 48 | .subscribe( |
64 | () => this.loadData(), | 49 | () => this.loadData(), |
65 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 50 | err => this.notifier.error(err.message) |
51 | ) | ||
52 | } | ||
53 | |||
54 | protected loadData () { | ||
55 | return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) | ||
56 | .subscribe( | ||
57 | resultList => { | ||
58 | this.videoChangeOwnerships = resultList.data | ||
59 | this.totalRecords = resultList.total | ||
60 | }, | ||
61 | |||
62 | err => this.notifier.error(err.message) | ||
66 | ) | 63 | ) |
67 | } | 64 | } |
68 | } | 65 | } |
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts index 4b2168e35..9996218ca 100644 --- a/client/src/app/+my-account/my-account-routing.module.ts +++ b/client/src/app/+my-account/my-account-routing.module.ts | |||
@@ -11,6 +11,10 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun | |||
11 | import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' | 11 | import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' |
12 | import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' | 12 | import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' |
13 | import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component' | 13 | import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component' |
14 | import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component' | ||
15 | import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' | ||
16 | import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' | ||
17 | import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' | ||
14 | 18 | ||
15 | const myAccountRoutes: Routes = [ | 19 | const myAccountRoutes: Routes = [ |
16 | { | 20 | { |
@@ -94,6 +98,42 @@ const myAccountRoutes: Routes = [ | |||
94 | title: 'Ownership changes' | 98 | title: 'Ownership changes' |
95 | } | 99 | } |
96 | } | 100 | } |
101 | }, | ||
102 | { | ||
103 | path: 'blocklist/accounts', | ||
104 | component: MyAccountBlocklistComponent, | ||
105 | data: { | ||
106 | meta: { | ||
107 | title: 'Muted accounts' | ||
108 | } | ||
109 | } | ||
110 | }, | ||
111 | { | ||
112 | path: 'blocklist/servers', | ||
113 | component: MyAccountServerBlocklistComponent, | ||
114 | data: { | ||
115 | meta: { | ||
116 | title: 'Muted instances' | ||
117 | } | ||
118 | } | ||
119 | }, | ||
120 | { | ||
121 | path: 'history/videos', | ||
122 | component: MyAccountHistoryComponent, | ||
123 | data: { | ||
124 | meta: { | ||
125 | title: 'Videos history' | ||
126 | } | ||
127 | } | ||
128 | }, | ||
129 | { | ||
130 | path: 'notifications', | ||
131 | component: MyAccountNotificationsComponent, | ||
132 | data: { | ||
133 | meta: { | ||
134 | title: 'Notifications' | ||
135 | } | ||
136 | } | ||
97 | } | 137 | } |
98 | ] | 138 | ] |
99 | } | 139 | } |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts index e5343b33d..cbb068c7c 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts | |||
@@ -1,11 +1,10 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { AuthService, Notifier } from '@app/core' |
3 | import { FormReactive, UserService } from '../../../shared' | 3 | import { FormReactive, UserService } from '../../../shared' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 5 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
6 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | 6 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' |
7 | import { filter } from 'rxjs/operators' | 7 | import { filter } from 'rxjs/operators' |
8 | import { AuthService } from '@app/core' | ||
9 | import { User } from '../../../../../../shared' | 8 | import { User } from '../../../../../../shared' |
10 | 9 | ||
11 | @Component({ | 10 | @Component({ |
@@ -20,7 +19,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On | |||
20 | constructor ( | 19 | constructor ( |
21 | protected formValidatorService: FormValidatorService, | 20 | protected formValidatorService: FormValidatorService, |
22 | private userValidatorsService: UserValidatorsService, | 21 | private userValidatorsService: UserValidatorsService, |
23 | private notificationsService: NotificationsService, | 22 | private notifier: Notifier, |
24 | private authService: AuthService, | 23 | private authService: AuthService, |
25 | private userService: UserService, | 24 | private userService: UserService, |
26 | private i18n: I18n | 25 | private i18n: I18n |
@@ -50,7 +49,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On | |||
50 | 49 | ||
51 | this.userService.changePassword(currentPassword, newPassword).subscribe( | 50 | this.userService.changePassword(currentPassword, newPassword).subscribe( |
52 | () => { | 51 | () => { |
53 | this.notificationsService.success(this.i18n('Success'), this.i18n('Password updated.')) | 52 | this.notifier.success(this.i18n('Password updated.')) |
54 | 53 | ||
55 | this.form.reset() | 54 | this.form.reset() |
56 | this.error = null | 55 | this.error = null |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts index 63a121f64..3f79efe20 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { AuthService, ConfirmService, RedirectService } from '../../../core' | 3 | import { AuthService, ConfirmService, RedirectService } from '../../../core' |
4 | import { UserService } from '../../../shared' | 4 | import { UserService } from '../../../shared' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -15,7 +15,7 @@ export class MyAccountDangerZoneComponent { | |||
15 | 15 | ||
16 | constructor ( | 16 | constructor ( |
17 | private authService: AuthService, | 17 | private authService: AuthService, |
18 | private notificationsService: NotificationsService, | 18 | private notifier: Notifier, |
19 | private userService: UserService, | 19 | private userService: UserService, |
20 | private confirmService: ConfirmService, | 20 | private confirmService: ConfirmService, |
21 | private redirectService: RedirectService, | 21 | private redirectService: RedirectService, |
@@ -34,13 +34,13 @@ export class MyAccountDangerZoneComponent { | |||
34 | 34 | ||
35 | this.userService.deleteMe().subscribe( | 35 | this.userService.deleteMe().subscribe( |
36 | () => { | 36 | () => { |
37 | this.notificationsService.success(this.i18n('Success'), this.i18n('Your account is deleted.')) | 37 | this.notifier.success(this.i18n('Your account is deleted.')) |
38 | 38 | ||
39 | this.authService.logout() | 39 | this.authService.logout() |
40 | this.redirectService.redirectToHomepage() | 40 | this.redirectService.redirectToHomepage() |
41 | }, | 41 | }, |
42 | 42 | ||
43 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 43 | err => this.notifier.error(err.message) |
44 | ) | 44 | ) |
45 | } | 45 | } |
46 | } | 46 | } |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts new file mode 100644 index 000000000..5e1d51339 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './my-account-notification-preferences.component' | |||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html new file mode 100644 index 000000000..59422d682 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.html | |||
@@ -0,0 +1,19 @@ | |||
1 | <div class="custom-row"> | ||
2 | <div i18n>Activities</div> | ||
3 | <div i18n>Web</div> | ||
4 | <div i18n *ngIf="emailEnabled">Email</div> | ||
5 | </div> | ||
6 | |||
7 | <div class="custom-row" *ngFor="let notificationType of notificationSettingKeys"> | ||
8 | <ng-container *ngIf="hasUserRight(notificationType)"> | ||
9 | <div>{{ labelNotifications[notificationType] }}</div> | ||
10 | |||
11 | <div> | ||
12 | <p-inputSwitch [(ngModel)]="webNotifications[notificationType]" (onChange)="updateWebSetting(notificationType, $event.checked)"></p-inputSwitch> | ||
13 | </div> | ||
14 | |||
15 | <div *ngIf="emailEnabled"> | ||
16 | <p-inputSwitch [(ngModel)]="emailNotifications[notificationType]" (onChange)="updateEmailSetting(notificationType, $event.checked)"></p-inputSwitch> | ||
17 | </div> | ||
18 | </ng-container> | ||
19 | </div> | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss new file mode 100644 index 000000000..6feb16ab1 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.scss | |||
@@ -0,0 +1,25 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .custom-row { | ||
5 | display: flex; | ||
6 | align-items: center; | ||
7 | border-bottom: 1px solid rgba(0, 0, 0, 0.10); | ||
8 | |||
9 | &:first-child { | ||
10 | font-size: 16px; | ||
11 | |||
12 | & > div { | ||
13 | font-weight: $font-semibold; | ||
14 | } | ||
15 | } | ||
16 | |||
17 | & > div { | ||
18 | width: 350px; | ||
19 | } | ||
20 | |||
21 | & > div { | ||
22 | padding: 10px | ||
23 | } | ||
24 | } | ||
25 | |||
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 new file mode 100644 index 000000000..519bdfab4 --- /dev/null +++ b/client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts | |||
@@ -0,0 +1,99 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { User } from '@app/shared' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | import { Subject } from 'rxjs' | ||
5 | import { UserNotificationSetting, UserNotificationSettingValue, UserRight } from '../../../../../../shared' | ||
6 | import { Notifier, ServerService } from '@app/core' | ||
7 | import { debounce } from 'lodash-es' | ||
8 | import { UserNotificationService } from '@app/shared/users/user-notification.service' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-account-notification-preferences', | ||
12 | templateUrl: './my-account-notification-preferences.component.html', | ||
13 | styleUrls: [ './my-account-notification-preferences.component.scss' ] | ||
14 | }) | ||
15 | export class MyAccountNotificationPreferencesComponent implements OnInit { | ||
16 | @Input() user: User = null | ||
17 | @Input() userInformationLoaded: Subject<any> | ||
18 | |||
19 | notificationSettingKeys: (keyof UserNotificationSetting)[] = [] | ||
20 | emailNotifications: { [ id in keyof UserNotificationSetting ]: boolean } = {} as any | ||
21 | webNotifications: { [ id in keyof UserNotificationSetting ]: boolean } = {} as any | ||
22 | labelNotifications: { [ id in keyof UserNotificationSetting ]: string } = {} as any | ||
23 | rightNotifications: { [ id in keyof Partial<UserNotificationSetting> ]: UserRight } = {} as any | ||
24 | emailEnabled: boolean | ||
25 | |||
26 | private savePreferences = debounce(this.savePreferencesImpl.bind(this), 500) | ||
27 | |||
28 | constructor ( | ||
29 | private i18n: I18n, | ||
30 | private userNotificationService: UserNotificationService, | ||
31 | private serverService: ServerService, | ||
32 | private notifier: Notifier | ||
33 | ) { | ||
34 | this.labelNotifications = { | ||
35 | newVideoFromSubscription: this.i18n('New video from your subscriptions'), | ||
36 | newCommentOnMyVideo: this.i18n('New comment on your video'), | ||
37 | videoAbuseAsModerator: this.i18n('New video abuse on local video'), | ||
38 | blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'), | ||
39 | myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'), | ||
40 | myVideoImportFinished: this.i18n('Video import finished'), | ||
41 | newUserRegistration: this.i18n('A new user registered on your instance'), | ||
42 | newFollow: this.i18n('You or your channel(s) has a new follower'), | ||
43 | commentMention: this.i18n('Someone mentioned you in video comments') | ||
44 | } | ||
45 | this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[] | ||
46 | |||
47 | this.rightNotifications = { | ||
48 | videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES, | ||
49 | newUserRegistration: UserRight.MANAGE_USERS | ||
50 | } | ||
51 | |||
52 | this.emailEnabled = this.serverService.getConfig().email.enabled | ||
53 | } | ||
54 | |||
55 | ngOnInit () { | ||
56 | this.userInformationLoaded.subscribe(() => this.loadNotificationSettings()) | ||
57 | } | ||
58 | |||
59 | hasUserRight (field: keyof UserNotificationSetting) { | ||
60 | const rightToHave = this.rightNotifications[field] | ||
61 | if (!rightToHave) return true // No rights needed | ||
62 | |||
63 | return this.user.hasRight(rightToHave) | ||
64 | } | ||
65 | |||
66 | updateEmailSetting (field: keyof UserNotificationSetting, value: boolean) { | ||
67 | if (value === true) this.user.notificationSettings[field] |= UserNotificationSettingValue.EMAIL | ||
68 | else this.user.notificationSettings[field] &= ~UserNotificationSettingValue.EMAIL | ||
69 | |||
70 | this.savePreferences() | ||
71 | } | ||
72 | |||
73 | updateWebSetting (field: keyof UserNotificationSetting, value: boolean) { | ||
74 | if (value === true) this.user.notificationSettings[field] |= UserNotificationSettingValue.WEB | ||
75 | else this.user.notificationSettings[field] &= ~UserNotificationSettingValue.WEB | ||
76 | |||
77 | this.savePreferences() | ||
78 | } | ||
79 | |||
80 | private savePreferencesImpl () { | ||
81 | this.userNotificationService.updateNotificationSettings(this.user, this.user.notificationSettings) | ||
82 | .subscribe( | ||
83 | () => { | ||
84 | this.notifier.success(this.i18n('Preferences saved'), undefined, 2000) | ||
85 | }, | ||
86 | |||
87 | err => this.notifier.error(err.message) | ||
88 | ) | ||
89 | } | ||
90 | |||
91 | private loadNotificationSettings () { | ||
92 | for (const key of Object.keys(this.user.notificationSettings)) { | ||
93 | const value = this.user.notificationSettings[key] | ||
94 | this.emailNotifications[key] = value & UserNotificationSettingValue.EMAIL | ||
95 | |||
96 | this.webNotifications[key] = value & UserNotificationSettingValue.WEB | ||
97 | } | ||
98 | } | ||
99 | } | ||
diff --git a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts index 967e21f0b..a9503ed1b 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { FormReactive, UserService } from '../../../shared' | 3 | import { FormReactive, UserService } from '../../../shared' |
4 | import { User } from '@app/shared' | 4 | import { User } from '@app/shared' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -21,7 +21,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit { | |||
21 | constructor ( | 21 | constructor ( |
22 | protected formValidatorService: FormValidatorService, | 22 | protected formValidatorService: FormValidatorService, |
23 | private userValidatorsService: UserValidatorsService, | 23 | private userValidatorsService: UserValidatorsService, |
24 | private notificationsService: NotificationsService, | 24 | private notifier: Notifier, |
25 | private userService: UserService, | 25 | private userService: UserService, |
26 | private i18n: I18n | 26 | private i18n: I18n |
27 | ) { | 27 | ) { |
@@ -53,7 +53,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit { | |||
53 | this.user.account.displayName = displayName | 53 | this.user.account.displayName = displayName |
54 | this.user.account.description = description | 54 | this.user.account.description = description |
55 | 55 | ||
56 | this.notificationsService.success(this.i18n('Success'), this.i18n('Profile updated.')) | 56 | this.notifier.success(this.i18n('Profile updated.')) |
57 | }, | 57 | }, |
58 | 58 | ||
59 | err => this.error = err.message | 59 | err => this.error = err.message |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html index c7e23cd1f..ad64f28fe 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html | |||
@@ -4,10 +4,11 @@ | |||
4 | <span i18n class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }} | 4 | <span i18n class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }} |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <ng-template [ngIf]="user && user.account"> | 7 | <div i18n class="account-title">Profile</div> |
8 | <div i18n class="account-title">Profile</div> | 8 | <my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile> |
9 | <my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile> | 9 | |
10 | </ng-template> | 10 | <div i18n class="account-title" id="notifications">Notifications</div> |
11 | <my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences> | ||
11 | 12 | ||
12 | <div i18n class="account-title">Password</div> | 13 | <div i18n class="account-title">Password</div> |
13 | <my-account-change-password></my-account-change-password> | 14 | <my-account-change-password></my-account-change-password> |
@@ -16,4 +17,4 @@ | |||
16 | <my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings> | 17 | <my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings> |
17 | 18 | ||
18 | <div i18n class="account-title">Danger zone</div> | 19 | <div i18n class="account-title">Danger zone</div> |
19 | <my-account-danger-zone [user]="user"></my-account-danger-zone> \ No newline at end of file | 20 | <my-account-danger-zone [user]="user"></my-account-danger-zone> |
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index 62053d97b..f4b954e54 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { BytesPipe } from 'ngx-pipes' | 3 | import { BytesPipe } from 'ngx-pipes' |
4 | import { AuthService } from '../../core' | 4 | import { AuthService } from '../../core' |
5 | import { User } from '../../shared' | 5 | import { User } from '../../shared' |
@@ -19,7 +19,7 @@ export class MyAccountSettingsComponent implements OnInit { | |||
19 | constructor ( | 19 | constructor ( |
20 | private userService: UserService, | 20 | private userService: UserService, |
21 | private authService: AuthService, | 21 | private authService: AuthService, |
22 | private notificationsService: NotificationsService, | 22 | private notifier: Notifier, |
23 | private i18n: I18n | 23 | private i18n: I18n |
24 | ) {} | 24 | ) {} |
25 | 25 | ||
@@ -48,12 +48,12 @@ export class MyAccountSettingsComponent implements OnInit { | |||
48 | this.userService.changeAvatar(formData) | 48 | this.userService.changeAvatar(formData) |
49 | .subscribe( | 49 | .subscribe( |
50 | data => { | 50 | data => { |
51 | this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.')) | 51 | this.notifier.success(this.i18n('Avatar changed.')) |
52 | 52 | ||
53 | this.user.updateAccountAvatar(data.avatar) | 53 | this.user.updateAccountAvatar(data.avatar) |
54 | }, | 54 | }, |
55 | 55 | ||
56 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 56 | err => this.notifier.error(err.message) |
57 | ) | 57 | ) |
58 | } | 58 | } |
59 | } | 59 | } |
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 96629940f..049119fa8 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 | |||
@@ -15,10 +15,19 @@ | |||
15 | </div> | 15 | </div> |
16 | </div> | 16 | </div> |
17 | 17 | ||
18 | <my-peertube-checkbox | 18 | <div class="form-group"> |
19 | inputName="autoPlayVideo" formControlName="autoPlayVideo" | 19 | <my-peertube-checkbox |
20 | i18n-labelText labelText="Automatically plays video" | 20 | inputName="webTorrentEnabled" formControlName="webTorrentEnabled" |
21 | ></my-peertube-checkbox> | 21 | i18n-labelText labelText="Use WebTorrent to exchange parts of the video with others" |
22 | ></my-peertube-checkbox> | ||
23 | </div> | ||
24 | |||
25 | <div class="form-group"> | ||
26 | <my-peertube-checkbox | ||
27 | inputName="autoPlayVideo" formControlName="autoPlayVideo" | ||
28 | i18n-labelText labelText="Automatically plays video" | ||
29 | ></my-peertube-checkbox> | ||
30 | </div> | ||
22 | 31 | ||
23 | <input type="submit" i18n-value value="Save" [disabled]="!form.valid"> | 32 | <input type="submit" i18n-value value="Save" [disabled]="!form.valid"> |
24 | </form> | 33 | </form> |
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 7089b2057..b8f80bc1a 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 | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { UserUpdateMe } from '../../../../../../shared' | 3 | import { UserUpdateMe } from '../../../../../../shared' |
4 | import { AuthService } from '../../../core' | 4 | import { AuthService } from '../../../core' |
5 | import { FormReactive, User, UserService } from '../../../shared' | 5 | import { FormReactive, User, UserService } from '../../../shared' |
@@ -19,7 +19,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI | |||
19 | constructor ( | 19 | constructor ( |
20 | protected formValidatorService: FormValidatorService, | 20 | protected formValidatorService: FormValidatorService, |
21 | private authService: AuthService, | 21 | private authService: AuthService, |
22 | private notificationsService: NotificationsService, | 22 | private notifier: Notifier, |
23 | private userService: UserService, | 23 | private userService: UserService, |
24 | private i18n: I18n | 24 | private i18n: I18n |
25 | ) { | 25 | ) { |
@@ -29,12 +29,14 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI | |||
29 | ngOnInit () { | 29 | ngOnInit () { |
30 | this.buildForm({ | 30 | this.buildForm({ |
31 | nsfwPolicy: null, | 31 | nsfwPolicy: null, |
32 | webTorrentEnabled: null, | ||
32 | autoPlayVideo: null | 33 | autoPlayVideo: null |
33 | }) | 34 | }) |
34 | 35 | ||
35 | this.userInformationLoaded.subscribe(() => { | 36 | this.userInformationLoaded.subscribe(() => { |
36 | this.form.patchValue({ | 37 | this.form.patchValue({ |
37 | nsfwPolicy: this.user.nsfwPolicy, | 38 | nsfwPolicy: this.user.nsfwPolicy, |
39 | webTorrentEnabled: this.user.webTorrentEnabled, | ||
38 | autoPlayVideo: this.user.autoPlayVideo === true | 40 | autoPlayVideo: this.user.autoPlayVideo === true |
39 | }) | 41 | }) |
40 | }) | 42 | }) |
@@ -42,20 +44,22 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI | |||
42 | 44 | ||
43 | updateDetails () { | 45 | updateDetails () { |
44 | const nsfwPolicy = this.form.value['nsfwPolicy'] | 46 | const nsfwPolicy = this.form.value['nsfwPolicy'] |
47 | const webTorrentEnabled = this.form.value['webTorrentEnabled'] | ||
45 | const autoPlayVideo = this.form.value['autoPlayVideo'] | 48 | const autoPlayVideo = this.form.value['autoPlayVideo'] |
46 | const details: UserUpdateMe = { | 49 | const details: UserUpdateMe = { |
47 | nsfwPolicy, | 50 | nsfwPolicy, |
51 | webTorrentEnabled, | ||
48 | autoPlayVideo | 52 | autoPlayVideo |
49 | } | 53 | } |
50 | 54 | ||
51 | this.userService.updateMyProfile(details).subscribe( | 55 | this.userService.updateMyProfile(details).subscribe( |
52 | () => { | 56 | () => { |
53 | this.notificationsService.success(this.i18n('Success'), this.i18n('Information updated.')) | 57 | this.notifier.success(this.i18n('Information updated.')) |
54 | 58 | ||
55 | this.authService.refreshUserInformation() | 59 | this.authService.refreshUserInformation() |
56 | }, | 60 | }, |
57 | 61 | ||
58 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 62 | err => this.notifier.error(err.message) |
59 | ) | 63 | ) |
60 | } | 64 | } |
61 | } | 65 | } |
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts index 9517a3705..9d2dccdf0 100644 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 3 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { UserSubscriptionService } from '@app/shared/user-subscription' | 5 | import { UserSubscriptionService } from '@app/shared/user-subscription' |
@@ -21,7 +21,7 @@ export class MyAccountSubscriptionsComponent implements OnInit { | |||
21 | 21 | ||
22 | constructor ( | 22 | constructor ( |
23 | private userSubscriptionService: UserSubscriptionService, | 23 | private userSubscriptionService: UserSubscriptionService, |
24 | private notificationsService: NotificationsService, | 24 | private notifier: Notifier, |
25 | private i18n: I18n | 25 | private i18n: I18n |
26 | ) {} | 26 | ) {} |
27 | 27 | ||
@@ -37,7 +37,7 @@ export class MyAccountSubscriptionsComponent implements OnInit { | |||
37 | this.pagination.totalItems = res.total | 37 | this.pagination.totalItems = res.total |
38 | }, | 38 | }, |
39 | 39 | ||
40 | error => this.notificationsService.error(this.i18n('Error'), error.message) | 40 | error => this.notifier.error(error.message) |
41 | ) | 41 | ) |
42 | } | 42 | } |
43 | 43 | ||
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts index 81608d837..a68f79b47 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts | |||
@@ -1,10 +1,9 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' | 4 | import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' |
5 | import { VideoChannelCreate } from '../../../../../shared/models/videos' | 5 | import { VideoChannelCreate } from '../../../../../shared/models/videos' |
6 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | 6 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' |
7 | import { AuthService } from '@app/core' | ||
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | 7 | import { I18n } from '@ngx-translate/i18n-polyfill' |
9 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 8 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
10 | import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service' | 9 | import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service' |
@@ -21,7 +20,7 @@ export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelE | |||
21 | protected formValidatorService: FormValidatorService, | 20 | protected formValidatorService: FormValidatorService, |
22 | private authService: AuthService, | 21 | private authService: AuthService, |
23 | private videoChannelValidatorsService: VideoChannelValidatorsService, | 22 | private videoChannelValidatorsService: VideoChannelValidatorsService, |
24 | private notificationsService: NotificationsService, | 23 | private notifier: Notifier, |
25 | private router: Router, | 24 | private router: Router, |
26 | private videoChannelService: VideoChannelService, | 25 | private videoChannelService: VideoChannelService, |
27 | private i18n: I18n | 26 | private i18n: I18n |
@@ -56,8 +55,8 @@ export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelE | |||
56 | this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe( | 55 | this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe( |
57 | () => { | 56 | () => { |
58 | this.authService.refreshUserInformation() | 57 | this.authService.refreshUserInformation() |
59 | this.notificationsService.success( | 58 | |
60 | this.i18n('Success'), | 59 | this.notifier.success( |
61 | this.i18n('Video channel {{videoChannelName}} created.', { videoChannelName: videoChannelCreate.displayName }) | 60 | this.i18n('Video channel {{videoChannelName}} created.', { videoChannelName: videoChannelCreate.displayName }) |
62 | ) | 61 | ) |
63 | this.router.navigate([ '/my-account', 'video-channels' ]) | 62 | this.router.navigate([ '/my-account', 'video-channels' ]) |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts index ccdd9a3dc..4dc65dd99 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts | |||
@@ -4,7 +4,11 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | |||
4 | export abstract class MyAccountVideoChannelEdit extends FormReactive { | 4 | export abstract class MyAccountVideoChannelEdit extends FormReactive { |
5 | // We need it even in the create component because it's used in the edit template | 5 | // We need it even in the create component because it's used in the edit template |
6 | videoChannelToUpdate: VideoChannel | 6 | videoChannelToUpdate: VideoChannel |
7 | instanceHost: string | ||
7 | 8 | ||
8 | abstract isCreation (): boolean | 9 | abstract isCreation (): boolean |
9 | abstract getFormButtonTitle (): string | 10 | abstract getFormButtonTitle (): string |
11 | |||
12 | // FIXME: We need this method so angular does not complain in the child template | ||
13 | onAvatarChange (formData: FormData) { /* empty */ } | ||
10 | } | 14 | } |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts index 56697030b..da4fb645a 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts | |||
@@ -1,12 +1,11 @@ | |||
1 | import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { AuthService, Notifier, ServerService } from '@app/core' |
4 | import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' | 4 | import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' |
5 | import { VideoChannelUpdate } from '../../../../../shared/models/videos' | 5 | import { VideoChannelUpdate } from '../../../../../shared/models/videos' |
6 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' | 6 | import { VideoChannelService } from '@app/shared/video-channel/video-channel.service' |
7 | import { Subscription } from 'rxjs' | 7 | import { Subscription } from 'rxjs' |
8 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 8 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
9 | import { AuthService, ServerService } from '@app/core' | ||
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 9 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 10 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
12 | import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service' | 11 | import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service' |
@@ -17,18 +16,16 @@ import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators | |||
17 | styleUrls: [ './my-account-video-channel-edit.component.scss' ] | 16 | styleUrls: [ './my-account-video-channel-edit.component.scss' ] |
18 | }) | 17 | }) |
19 | export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy { | 18 | export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy { |
20 | @ViewChild('avatarfileInput') avatarfileInput | ||
21 | |||
22 | error: string | 19 | error: string |
23 | |||
24 | videoChannelToUpdate: VideoChannel | 20 | videoChannelToUpdate: VideoChannel |
21 | |||
25 | private paramsSub: Subscription | 22 | private paramsSub: Subscription |
26 | 23 | ||
27 | constructor ( | 24 | constructor ( |
28 | protected formValidatorService: FormValidatorService, | 25 | protected formValidatorService: FormValidatorService, |
29 | private authService: AuthService, | 26 | private authService: AuthService, |
30 | private videoChannelValidatorsService: VideoChannelValidatorsService, | 27 | private videoChannelValidatorsService: VideoChannelValidatorsService, |
31 | private notificationsService: NotificationsService, | 28 | private notifier: Notifier, |
32 | private router: Router, | 29 | private router: Router, |
33 | private route: ActivatedRoute, | 30 | private route: ActivatedRoute, |
34 | private videoChannelService: VideoChannelService, | 31 | private videoChannelService: VideoChannelService, |
@@ -81,10 +78,11 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
81 | this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( | 78 | this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe( |
82 | () => { | 79 | () => { |
83 | this.authService.refreshUserInformation() | 80 | this.authService.refreshUserInformation() |
84 | this.notificationsService.success( | 81 | |
85 | this.i18n('Success'), | 82 | this.notifier.success( |
86 | this.i18n('Video channel {{videoChannelName}} updated.', { videoChannelName: videoChannelUpdate.displayName }) | 83 | this.i18n('Video channel {{videoChannelName}} updated.', { videoChannelName: videoChannelUpdate.displayName }) |
87 | ) | 84 | ) |
85 | |||
88 | this.router.navigate([ '/my-account', 'video-channels' ]) | 86 | this.router.navigate([ '/my-account', 'video-channels' ]) |
89 | }, | 87 | }, |
90 | 88 | ||
@@ -96,12 +94,12 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE | |||
96 | this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) | 94 | this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData) |
97 | .subscribe( | 95 | .subscribe( |
98 | data => { | 96 | data => { |
99 | this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.')) | 97 | this.notifier.success(this.i18n('Avatar changed.')) |
100 | 98 | ||
101 | this.videoChannelToUpdate.updateAvatar(data.avatar) | 99 | this.videoChannelToUpdate.updateAvatar(data.avatar) |
102 | }, | 100 | }, |
103 | 101 | ||
104 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 102 | err => this.notifier.error(err.message) |
105 | ) | 103 | ) |
106 | } | 104 | } |
107 | 105 | ||
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html index df74b19b6..51db2e75d 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html | |||
@@ -1,6 +1,6 @@ | |||
1 | <div class="video-channels-header"> | 1 | <div class="video-channels-header"> |
2 | <a class="create-button" routerLink="create"> | 2 | <a class="create-button" routerLink="create"> |
3 | <span class="icon icon-add"></span> | 3 | <my-global-icon iconName="add"></my-global-icon> |
4 | <ng-container i18n>Create another video channel</ng-container> | 4 | <ng-container i18n>Create another video channel</ng-container> |
5 | </a> | 5 | </a> |
6 | </div> | 6 | </div> |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss index 472cbb723..77fce138b 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss | |||
@@ -2,7 +2,7 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .create-button { | 4 | .create-button { |
5 | @include create-button('../../../assets/images/global/add.svg'); | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | /deep/ .action-button { | 8 | /deep/ .action-button { |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts index 6d1098865..da2c5bcd3 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { AuthService } from '../../core/auth' | 3 | import { AuthService } from '../../core/auth' |
4 | import { ConfirmService } from '../../core/confirm' | 4 | import { ConfirmService } from '../../core/confirm' |
5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
@@ -20,7 +20,7 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
20 | 20 | ||
21 | constructor ( | 21 | constructor ( |
22 | private authService: AuthService, | 22 | private authService: AuthService, |
23 | private notificationsService: NotificationsService, | 23 | private notifier: Notifier, |
24 | private confirmService: ConfirmService, | 24 | private confirmService: ConfirmService, |
25 | private videoChannelService: VideoChannelService, | 25 | private videoChannelService: VideoChannelService, |
26 | private i18n: I18n | 26 | private i18n: I18n |
@@ -35,10 +35,14 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
35 | async deleteVideoChannel (videoChannel: VideoChannel) { | 35 | async deleteVideoChannel (videoChannel: VideoChannel) { |
36 | const res = await this.confirmService.confirmWithInput( | 36 | const res = await this.confirmService.confirmWithInput( |
37 | this.i18n( | 37 | this.i18n( |
38 | 'Do you really want to delete {{videoChannelName}}? It will delete all videos uploaded in this channel too.', | 38 | 'Do you really want to delete {{channelDisplayName}}? It will delete all videos uploaded in this channel, ' + |
39 | { videoChannelName: videoChannel.displayName } | 39 | 'and you will not be able to create another channel with the same name ({{channelName}})!', |
40 | { channelDisplayName: videoChannel.displayName, channelName: videoChannel.name } | ||
41 | ), | ||
42 | this.i18n( | ||
43 | 'Please type the display name of the video channel ({{displayName}}) to confirm', | ||
44 | { displayName: videoChannel.displayName } | ||
40 | ), | 45 | ), |
41 | this.i18n('Please type the name of the video channel to confirm'), | ||
42 | videoChannel.displayName, | 46 | videoChannel.displayName, |
43 | this.i18n('Delete') | 47 | this.i18n('Delete') |
44 | ) | 48 | ) |
@@ -46,15 +50,14 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
46 | 50 | ||
47 | this.videoChannelService.removeVideoChannel(videoChannel) | 51 | this.videoChannelService.removeVideoChannel(videoChannel) |
48 | .subscribe( | 52 | .subscribe( |
49 | status => { | 53 | () => { |
50 | this.loadVideoChannels() | 54 | this.loadVideoChannels() |
51 | this.notificationsService.success( | 55 | this.notifier.success( |
52 | this.i18n('Success'), | ||
53 | this.i18n('Video channel {{videoChannelName}} deleted.', { videoChannelName: videoChannel.displayName }) | 56 | this.i18n('Video channel {{videoChannelName}} deleted.', { videoChannelName: videoChannel.displayName }) |
54 | ) | 57 | ) |
55 | }, | 58 | }, |
56 | 59 | ||
57 | error => this.notificationsService.error(this.i18n('Error'), error.message) | 60 | error => this.notifier.error(error.message) |
58 | ) | 61 | ) |
59 | } | 62 | } |
60 | 63 | ||
diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts index d9fb20446..21a10c8ff 100644 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { RestPagination, RestTable } from '@app/shared' | 2 | import { RestPagination, RestTable } from '@app/shared' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/components/common/sortmeta' |
4 | import { NotificationsService } from 'angular2-notifications' | 4 | import { Notifier } from '@app/core' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { VideoImport, VideoImportState } from '../../../../../shared/models/videos' | 6 | import { VideoImport, VideoImportState } from '../../../../../shared/models/videos' |
7 | import { VideoImportService } from '@app/shared/video-import' | 7 | import { VideoImportService } from '@app/shared/video-import' |
@@ -19,7 +19,7 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit | |||
19 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 19 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
20 | 20 | ||
21 | constructor ( | 21 | constructor ( |
22 | private notificationsService: NotificationsService, | 22 | private notifier: Notifier, |
23 | private videoImportService: VideoImportService, | 23 | private videoImportService: VideoImportService, |
24 | private i18n: I18n | 24 | private i18n: I18n |
25 | ) { | 25 | ) { |
@@ -27,7 +27,7 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit | |||
27 | } | 27 | } |
28 | 28 | ||
29 | ngOnInit () { | 29 | ngOnInit () { |
30 | this.loadSort() | 30 | this.initialize() |
31 | } | 31 | } |
32 | 32 | ||
33 | isVideoImportSuccess (videoImport: VideoImport) { | 33 | isVideoImportSuccess (videoImport: VideoImport) { |
@@ -58,7 +58,7 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit | |||
58 | this.totalRecords = resultList.total | 58 | this.totalRecords = resultList.total |
59 | }, | 59 | }, |
60 | 60 | ||
61 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 61 | err => this.notifier.error(err.message) |
62 | ) | 62 | ) |
63 | } | 63 | } |
64 | } | 64 | } |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index a6911e4bf..69748ef37 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html | |||
@@ -32,7 +32,7 @@ | |||
32 | </span> | 32 | </span> |
33 | 33 | ||
34 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> | 34 | <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> |
35 | <span class="icon icon-delete-white"></span> | 35 | <my-global-icon iconName="delete"></my-global-icon> |
36 | <ng-container i18n>Delete</ng-container> | 36 | <ng-container i18n>Delete</ng-container> |
37 | </span> | 37 | </span> |
38 | </div> | 38 | </div> |
@@ -45,7 +45,7 @@ | |||
45 | 45 | ||
46 | <my-button i18n-label label="Change ownership" | 46 | <my-button i18n-label label="Change ownership" |
47 | className="action-button-change-ownership" | 47 | className="action-button-change-ownership" |
48 | icon="icon-im-with-her" | 48 | icon="im-with-her" |
49 | (click)="changeOwnership($event, video)" | 49 | (click)="changeOwnership($event, video)" |
50 | ></my-button> | 50 | ></my-button> |
51 | </div> | 51 | </div> |
@@ -53,4 +53,4 @@ | |||
53 | </div> | 53 | </div> |
54 | </div> | 54 | </div> |
55 | 55 | ||
56 | <my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership> \ No newline at end of file | 56 | <my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership> |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss index 2db81a3fe..39d0cf2f7 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss | |||
@@ -23,14 +23,11 @@ | |||
23 | .action-button-delete-selection { | 23 | .action-button-delete-selection { |
24 | @include peertube-button; | 24 | @include peertube-button; |
25 | @include orange-button; | 25 | @include orange-button; |
26 | } | 26 | @include button-with-icon(21px); |
27 | |||
28 | .icon.icon-delete-white { | ||
29 | @include icon(21px); | ||
30 | 27 | ||
31 | position: relative; | 28 | my-global-icon { |
32 | top: -2px; | 29 | @include apply-svg-color(#fff); |
33 | background-image: url('../../../assets/images/global/delete-white.svg'); | 30 | } |
34 | } | 31 | } |
35 | } | 32 | } |
36 | } | 33 | } |
@@ -97,7 +94,7 @@ | |||
97 | } | 94 | } |
98 | } | 95 | } |
99 | 96 | ||
100 | @media screen and (max-width: 800px) { | 97 | @media screen and (max-width: $small-view) { |
101 | .video { | 98 | .video { |
102 | flex-direction: column; | 99 | flex-direction: column; |
103 | height: auto; | 100 | height: auto; |
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts index 7560f0128..41608f796 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts | |||
@@ -5,7 +5,7 @@ import { ActivatedRoute, Router } from '@angular/router' | |||
5 | import { Location } from '@angular/common' | 5 | import { Location } from '@angular/common' |
6 | import { immutableAssign } from '@app/shared/misc/utils' | 6 | import { immutableAssign } from '@app/shared/misc/utils' |
7 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 7 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
8 | import { NotificationsService } from 'angular2-notifications' | 8 | import { Notifier } from '@app/core' |
9 | import { AuthService } from '../../core/auth' | 9 | import { AuthService } from '../../core/auth' |
10 | import { ConfirmService } from '../../core/confirm' | 10 | import { ConfirmService } from '../../core/confirm' |
11 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 11 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
@@ -40,7 +40,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni | |||
40 | protected router: Router, | 40 | protected router: Router, |
41 | protected route: ActivatedRoute, | 41 | protected route: ActivatedRoute, |
42 | protected authService: AuthService, | 42 | protected authService: AuthService, |
43 | protected notificationsService: NotificationsService, | 43 | protected notifier: Notifier, |
44 | protected location: Location, | 44 | protected location: Location, |
45 | protected screenService: ScreenService, | 45 | protected screenService: ScreenService, |
46 | protected i18n: I18n, | 46 | protected i18n: I18n, |
@@ -102,16 +102,13 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni | |||
102 | .pipe(concatAll()) | 102 | .pipe(concatAll()) |
103 | .subscribe( | 103 | .subscribe( |
104 | res => { | 104 | res => { |
105 | this.notificationsService.success( | 105 | this.notifier.success(this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length })) |
106 | this.i18n('Success'), | ||
107 | this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length }) | ||
108 | ) | ||
109 | 106 | ||
110 | this.abortSelectionMode() | 107 | this.abortSelectionMode() |
111 | this.reloadVideos() | 108 | this.reloadVideos() |
112 | }, | 109 | }, |
113 | 110 | ||
114 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 111 | err => this.notifier.error(err.message) |
115 | ) | 112 | ) |
116 | } | 113 | } |
117 | 114 | ||
@@ -124,15 +121,12 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni | |||
124 | 121 | ||
125 | this.videoService.removeVideo(video.id) | 122 | this.videoService.removeVideo(video.id) |
126 | .subscribe( | 123 | .subscribe( |
127 | status => { | 124 | () => { |
128 | this.notificationsService.success( | 125 | this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: video.name })) |
129 | this.i18n('Success'), | ||
130 | this.i18n('Video {{videoName}} deleted.', { videoName: video.name }) | ||
131 | ) | ||
132 | this.reloadVideos() | 126 | this.reloadVideos() |
133 | }, | 127 | }, |
134 | 128 | ||
135 | error => this.notificationsService.error(this.i18n('Error'), error.message) | 129 | error => this.notifier.error(error.message) |
136 | ) | 130 | ) |
137 | } | 131 | } |
138 | 132 | ||
@@ -169,7 +163,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni | |||
169 | 163 | ||
170 | private spliceVideosById (id: number) { | 164 | private spliceVideosById (id: number) { |
171 | for (const key of Object.keys(this.loadedPages)) { | 165 | for (const key of Object.keys(this.loadedPages)) { |
172 | const videos = this.loadedPages[ key ] | 166 | const videos: Video[] = this.loadedPages[ key ] |
173 | const index = videos.findIndex(v => v.id === id) | 167 | const index = videos.findIndex(v => v.id === id) |
174 | 168 | ||
175 | if (index !== -1) { | 169 | if (index !== -1) { |
diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html index 69b198faa..22f127904 100644 --- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html +++ b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> | 1 | <ng-template #modal let-close="close" let-dismiss="dismiss"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Change ownership</h4> | 3 | <h4 i18n class="modal-title">Change ownership</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body" [formGroup]="form"> | 8 | <div class="modal-body" [formGroup]="form"> |
@@ -22,9 +23,9 @@ | |||
22 | </span> | 23 | </span> |
23 | 24 | ||
24 | <input | 25 | <input |
25 | type="submit" i18n-value value="Submit" class="action-button-submit" | 26 | type="submit" i18n-value value="Submit" class="action-button-submit" |
26 | [disabled]="!form.valid" | 27 | [disabled]="!form.valid" |
27 | (click)="close()" | 28 | (click)="close()" |
28 | /> | 29 | /> |
29 | </div> | 30 | </div> |
30 | </div> | 31 | </div> |
diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts index 7437b939a..37d7cf2a4 100644 --- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts +++ b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
4 | import { FormReactive, UserService } from '../../../shared/index' | 4 | import { FormReactive, UserService } from '../../../shared/index' |
5 | import { Video } from '@app/shared/video/video.model' | 5 | import { Video } from '@app/shared/video/video.model' |
@@ -25,7 +25,7 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni | |||
25 | protected formValidatorService: FormValidatorService, | 25 | protected formValidatorService: FormValidatorService, |
26 | private videoChangeOwnershipValidatorsService: VideoChangeOwnershipValidatorsService, | 26 | private videoChangeOwnershipValidatorsService: VideoChangeOwnershipValidatorsService, |
27 | private videoOwnershipService: VideoOwnershipService, | 27 | private videoOwnershipService: VideoOwnershipService, |
28 | private notificationsService: NotificationsService, | 28 | private notifier: Notifier, |
29 | private userService: UserService, | 29 | private userService: UserService, |
30 | private modalService: NgbModal, | 30 | private modalService: NgbModal, |
31 | private i18n: I18n | 31 | private i18n: I18n |
@@ -49,15 +49,13 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni | |||
49 | .catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing | 49 | .catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing |
50 | } | 50 | } |
51 | 51 | ||
52 | search (event) { | 52 | search (event: { query: string }) { |
53 | const query = event.query | 53 | const query = event.query |
54 | this.userService.autocomplete(query) | 54 | this.userService.autocomplete(query) |
55 | .subscribe( | 55 | .subscribe( |
56 | usernames => { | 56 | usernames => this.usernamePropositions = usernames, |
57 | this.usernamePropositions = usernames | ||
58 | }, | ||
59 | 57 | ||
60 | err => this.notificationsService.error('Error', err.message) | 58 | err => this.notifier.error(err.message) |
61 | ) | 59 | ) |
62 | } | 60 | } |
63 | 61 | ||
@@ -67,9 +65,9 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni | |||
67 | this.videoOwnershipService | 65 | this.videoOwnershipService |
68 | .changeOwnership(this.video.id, username) | 66 | .changeOwnership(this.video.id, username) |
69 | .subscribe( | 67 | .subscribe( |
70 | () => this.notificationsService.success(this.i18n('Success'), this.i18n('Ownership change request sent.')), | 68 | () => this.notifier.success(this.i18n('Ownership change request sent.')), |
71 | 69 | ||
72 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 70 | err => this.notifier.error(err.message) |
73 | ) | 71 | ) |
74 | } | 72 | } |
75 | } | 73 | } |
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html index b602fd69f..3999252be 100644 --- a/client/src/app/+my-account/my-account.component.html +++ b/client/src/app/+my-account/my-account.component.html | |||
@@ -1,26 +1,5 @@ | |||
1 | <div class="row"> | 1 | <div class="row"> |
2 | <div class="sub-menu"> | 2 | <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown> |
3 | <a i18n routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My settings</a> | ||
4 | |||
5 | <div ngbDropdown class="my-library"> | ||
6 | <span role="button" class="title-page" [ngClass]="{ active: libraryLabel !== '' }" ngbDropdownToggle> | ||
7 | <ng-container i18n>My library</ng-container> | ||
8 | <ng-container *ngIf="libraryLabel"> - {{ libraryLabel }}</ng-container> | ||
9 | </span> | ||
10 | |||
11 | <div ngbDropdownMenu> | ||
12 | <a class="dropdown-item" i18n routerLink="/my-account/video-channels">My channels</a> | ||
13 | |||
14 | <a class="dropdown-item" i18n routerLink="/my-account/videos">My videos</a> | ||
15 | |||
16 | <a class="dropdown-item" i18n routerLink="/my-account/subscriptions">My subscriptions</a> | ||
17 | |||
18 | <a class="dropdown-item" *ngIf="isVideoImportEnabled()" i18n routerLink="/my-account/video-imports">My imports</a> | ||
19 | </div> | ||
20 | </div> | ||
21 | |||
22 | <a i18n routerLink="/my-account/ownership" routerLinkActive="active" class="title-page">Ownership changes</a> | ||
23 | </div> | ||
24 | 3 | ||
25 | <div class="margin-content"> | 4 | <div class="margin-content"> |
26 | <router-outlet></router-outlet> | 5 | <router-outlet></router-outlet> |
diff --git a/client/src/app/+my-account/my-account.component.scss b/client/src/app/+my-account/my-account.component.scss index 20b2639b5..4f111efdf 100644 --- a/client/src/app/+my-account/my-account.component.scss +++ b/client/src/app/+my-account/my-account.component.scss | |||
@@ -1,14 +1,3 @@ | |||
1 | .my-library { | 1 | .row { |
2 | span[role=button] { | 2 | flex-direction: column; |
3 | cursor: pointer; | ||
4 | } | ||
5 | |||
6 | a { | ||
7 | display: block; | ||
8 | } | ||
9 | } | 3 | } |
10 | |||
11 | /deep/ .dropdown-toggle::after { | ||
12 | position: relative; | ||
13 | top: 2px; | ||
14 | } \ No newline at end of file | ||
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index bad60a8fb..8a4102d80 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts | |||
@@ -1,37 +1,80 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component } from '@angular/core' |
2 | import { ServerService } from '@app/core' | 2 | import { ServerService } from '@app/core' |
3 | import { NavigationStart, Router } from '@angular/router' | ||
4 | import { filter } from 'rxjs/operators' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { Subscription } from 'rxjs' | 4 | import { TopMenuDropdownParam } from '@app/shared/menu/top-menu-dropdown.component' |
7 | 5 | ||
8 | @Component({ | 6 | @Component({ |
9 | selector: 'my-my-account', | 7 | selector: 'my-my-account', |
10 | templateUrl: './my-account.component.html', | 8 | templateUrl: './my-account.component.html', |
11 | styleUrls: [ './my-account.component.scss' ] | 9 | styleUrls: [ './my-account.component.scss' ] |
12 | }) | 10 | }) |
13 | export class MyAccountComponent implements OnInit, OnDestroy { | 11 | export class MyAccountComponent { |
14 | 12 | menuEntries: TopMenuDropdownParam[] = [] | |
15 | libraryLabel = '' | ||
16 | |||
17 | private routeSub: Subscription | ||
18 | 13 | ||
19 | constructor ( | 14 | constructor ( |
20 | private serverService: ServerService, | 15 | private serverService: ServerService, |
21 | private router: Router, | ||
22 | private i18n: I18n | 16 | private i18n: I18n |
23 | ) {} | 17 | ) { |
24 | 18 | ||
25 | ngOnInit () { | 19 | const libraryEntries: TopMenuDropdownParam = { |
26 | this.updateLibraryLabel(this.router.url) | 20 | label: this.i18n('My library'), |
21 | children: [ | ||
22 | { | ||
23 | label: this.i18n('My channels'), | ||
24 | routerLink: '/my-account/video-channels' | ||
25 | }, | ||
26 | { | ||
27 | label: this.i18n('My videos'), | ||
28 | routerLink: '/my-account/videos' | ||
29 | }, | ||
30 | { | ||
31 | label: this.i18n('My subscriptions'), | ||
32 | routerLink: '/my-account/subscriptions' | ||
33 | }, | ||
34 | { | ||
35 | label: this.i18n('My history'), | ||
36 | routerLink: '/my-account/history/videos' | ||
37 | } | ||
38 | ] | ||
39 | } | ||
27 | 40 | ||
28 | this.routeSub = this.router.events | 41 | if (this.isVideoImportEnabled()) { |
29 | .pipe(filter(event => event instanceof NavigationStart)) | 42 | libraryEntries.children.push({ |
30 | .subscribe((event: NavigationStart) => this.updateLibraryLabel(event.url)) | 43 | label: 'My imports', |
31 | } | 44 | routerLink: '/my-account/video-imports' |
45 | }) | ||
46 | } | ||
32 | 47 | ||
33 | ngOnDestroy () { | 48 | const miscEntries: TopMenuDropdownParam = { |
34 | if (this.routeSub) this.routeSub.unsubscribe() | 49 | label: this.i18n('Misc'), |
50 | children: [ | ||
51 | { | ||
52 | label: this.i18n('Muted accounts'), | ||
53 | routerLink: '/my-account/blocklist/accounts' | ||
54 | }, | ||
55 | { | ||
56 | label: this.i18n('Muted instances'), | ||
57 | routerLink: '/my-account/blocklist/servers' | ||
58 | }, | ||
59 | { | ||
60 | label: this.i18n('Ownership changes'), | ||
61 | routerLink: '/my-account/ownership' | ||
62 | } | ||
63 | ] | ||
64 | } | ||
65 | |||
66 | this.menuEntries = [ | ||
67 | { | ||
68 | label: this.i18n('My settings'), | ||
69 | routerLink: '/my-account/settings' | ||
70 | }, | ||
71 | { | ||
72 | label: this.i18n('My notifications'), | ||
73 | routerLink: '/my-account/notifications' | ||
74 | }, | ||
75 | libraryEntries, | ||
76 | miscEntries | ||
77 | ] | ||
35 | } | 78 | } |
36 | 79 | ||
37 | isVideoImportEnabled () { | 80 | isVideoImportEnabled () { |
@@ -40,19 +83,4 @@ export class MyAccountComponent implements OnInit, OnDestroy { | |||
40 | return importConfig.http.enabled || importConfig.torrent.enabled | 83 | return importConfig.http.enabled || importConfig.torrent.enabled |
41 | } | 84 | } |
42 | 85 | ||
43 | private updateLibraryLabel (url: string) { | ||
44 | const [ path ] = url.split('?') | ||
45 | |||
46 | if (path.startsWith('/my-account/video-channels')) { | ||
47 | this.libraryLabel = this.i18n('Channels') | ||
48 | } else if (path.startsWith('/my-account/videos')) { | ||
49 | this.libraryLabel = this.i18n('Videos') | ||
50 | } else if (path.startsWith('/my-account/subscriptions')) { | ||
51 | this.libraryLabel = this.i18n('Subscriptions') | ||
52 | } else if (path.startsWith('/my-account/video-imports')) { | ||
53 | this.libraryLabel = this.i18n('Video imports') | ||
54 | } else { | ||
55 | this.libraryLabel = '' | ||
56 | } | ||
57 | } | ||
58 | } | 86 | } |
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index ad21162a8..18f51f171 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { TableModule } from 'primeng/table' | 1 | import { TableModule } from 'primeng/table' |
2 | import { NgModule } from '@angular/core' | 2 | import { NgModule } from '@angular/core' |
3 | import { AutoCompleteModule } from 'primeng/autocomplete' | 3 | import { AutoCompleteModule } from 'primeng/autocomplete' |
4 | import { InputSwitchModule } from 'primeng/inputswitch' | ||
4 | import { SharedModule } from '../shared' | 5 | import { SharedModule } from '../shared' |
5 | import { MyAccountRoutingModule } from './my-account-routing.module' | 6 | import { MyAccountRoutingModule } from './my-account-routing.module' |
6 | import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component' | 7 | import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component' |
@@ -19,6 +20,11 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i | |||
19 | import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' | 20 | import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' |
20 | import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' | 21 | import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' |
21 | import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' | 22 | import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' |
23 | import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component' | ||
24 | import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' | ||
25 | import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component' | ||
26 | import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component' | ||
27 | import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences' | ||
22 | 28 | ||
23 | @NgModule({ | 29 | @NgModule({ |
24 | imports: [ | 30 | imports: [ |
@@ -26,7 +32,8 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub | |||
26 | MyAccountRoutingModule, | 32 | MyAccountRoutingModule, |
27 | AutoCompleteModule, | 33 | AutoCompleteModule, |
28 | SharedModule, | 34 | SharedModule, |
29 | TableModule | 35 | TableModule, |
36 | InputSwitchModule | ||
30 | ], | 37 | ], |
31 | 38 | ||
32 | declarations: [ | 39 | declarations: [ |
@@ -45,7 +52,12 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub | |||
45 | ActorAvatarInfoComponent, | 52 | ActorAvatarInfoComponent, |
46 | MyAccountVideoImportsComponent, | 53 | MyAccountVideoImportsComponent, |
47 | MyAccountDangerZoneComponent, | 54 | MyAccountDangerZoneComponent, |
48 | MyAccountSubscriptionsComponent | 55 | MyAccountSubscriptionsComponent, |
56 | MyAccountBlocklistComponent, | ||
57 | MyAccountServerBlocklistComponent, | ||
58 | MyAccountHistoryComponent, | ||
59 | MyAccountNotificationsComponent, | ||
60 | MyAccountNotificationPreferencesComponent | ||
49 | ], | 61 | ], |
50 | 62 | ||
51 | exports: [ | 63 | exports: [ |
diff --git a/client/src/app/+my-account/shared/actor-avatar-info.component.ts b/client/src/app/+my-account/shared/actor-avatar-info.component.ts index 7b80b1ed4..72c815a0c 100644 --- a/client/src/app/+my-account/shared/actor-avatar-info.component.ts +++ b/client/src/app/+my-account/shared/actor-avatar-info.component.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core' |
2 | import { ServerService } from '../../core/server' | 2 | import { ServerService } from '../../core/server' |
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 3 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
5 | import { Account } from '@app/shared/account/account.model' | 4 | import { Account } from '@app/shared/account/account.model' |
5 | import { Notifier } from '@app/core' | ||
6 | 6 | ||
7 | @Component({ | 7 | @Component({ |
8 | selector: 'my-actor-avatar-info', | 8 | selector: 'my-actor-avatar-info', |
@@ -10,7 +10,7 @@ import { Account } from '@app/shared/account/account.model' | |||
10 | styleUrls: [ './actor-avatar-info.component.scss' ] | 10 | styleUrls: [ './actor-avatar-info.component.scss' ] |
11 | }) | 11 | }) |
12 | export class ActorAvatarInfoComponent { | 12 | export class ActorAvatarInfoComponent { |
13 | @ViewChild('avatarfileInput') avatarfileInput | 13 | @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> |
14 | 14 | ||
15 | @Input() actor: VideoChannel | Account | 15 | @Input() actor: VideoChannel | Account |
16 | 16 | ||
@@ -18,13 +18,13 @@ export class ActorAvatarInfoComponent { | |||
18 | 18 | ||
19 | constructor ( | 19 | constructor ( |
20 | private serverService: ServerService, | 20 | private serverService: ServerService, |
21 | private notificationsService: NotificationsService | 21 | private notifier: Notifier |
22 | ) {} | 22 | ) {} |
23 | 23 | ||
24 | onAvatarChange () { | 24 | onAvatarChange () { |
25 | const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ] | 25 | const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ] |
26 | if (avatarfile.size > this.maxAvatarSize) { | 26 | if (avatarfile.size > this.maxAvatarSize) { |
27 | this.notificationsService.error('Error', 'This image is too large.') | 27 | this.notifier.error('Error', 'This image is too large.') |
28 | return | 28 | return |
29 | } | 29 | } |
30 | 30 | ||
diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts index 995f42ffc..cfd471fa4 100644 --- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts +++ b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts | |||
@@ -1,9 +1,8 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { I18n } from '@ngx-translate/i18n-polyfill' | 2 | import { I18n } from '@ngx-translate/i18n-polyfill' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { Notifier, RedirectService } from '@app/core' |
4 | import { ServerService } from '@app/core/server' | 4 | import { ServerService } from '@app/core/server' |
5 | import { RedirectService } from '@app/core' | 5 | import { FormReactive, UserService } from '@app/shared' |
6 | import { UserService, FormReactive } from '@app/shared' | ||
7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | 7 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' |
9 | 8 | ||
@@ -20,7 +19,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements | |||
20 | private userValidatorsService: UserValidatorsService, | 19 | private userValidatorsService: UserValidatorsService, |
21 | private userService: UserService, | 20 | private userService: UserService, |
22 | private serverService: ServerService, | 21 | private serverService: ServerService, |
23 | private notificationsService: NotificationsService, | 22 | private notifier: Notifier, |
24 | private redirectService: RedirectService, | 23 | private redirectService: RedirectService, |
25 | private i18n: I18n | 24 | private i18n: I18n |
26 | ) { | 25 | ) { |
@@ -46,12 +45,12 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements | |||
46 | 'An email with verification link will be sent to {{email}}.', | 45 | 'An email with verification link will be sent to {{email}}.', |
47 | { email } | 46 | { email } |
48 | ) | 47 | ) |
49 | this.notificationsService.success(this.i18n('Success'), message) | 48 | this.notifier.success(message) |
50 | this.redirectService.redirectToHomepage() | 49 | this.redirectService.redirectToHomepage() |
51 | }, | 50 | }, |
52 | 51 | ||
53 | err => { | 52 | err => { |
54 | this.notificationsService.error(this.i18n('Error'), err.message) | 53 | this.notifier.error(err.message) |
55 | } | 54 | } |
56 | ) | 55 | ) |
57 | } | 56 | } |
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html index 30ace5e10..a83d4a3c2 100644 --- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html +++ b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html | |||
@@ -9,7 +9,7 @@ | |||
9 | <ng-template #verificationError> | 9 | <ng-template #verificationError> |
10 | <div> | 10 | <div> |
11 | <span i18n>An error occurred. </span> | 11 | <span i18n>An error occurred. </span> |
12 | <a i18n routerLink="/verify-account/ask-email">Request new verification email.</a> | 12 | <a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a> |
13 | </div> | 13 | </div> |
14 | </ng-template> | 14 | </ng-template> |
15 | </div> | 15 | </div> |
diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts index 26b3bf4b1..f9ecf664b 100644 --- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts +++ b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { NotificationsService } from 'angular2-notifications' | 4 | import { Notifier } from '@app/core' |
5 | import { UserService } from '@app/shared' | 5 | import { UserService } from '@app/shared' |
6 | 6 | ||
7 | @Component({ | 7 | @Component({ |
@@ -17,7 +17,7 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
17 | 17 | ||
18 | constructor ( | 18 | constructor ( |
19 | private userService: UserService, | 19 | private userService: UserService, |
20 | private notificationsService: NotificationsService, | 20 | private notifier: Notifier, |
21 | private router: Router, | 21 | private router: Router, |
22 | private route: ActivatedRoute, | 22 | private route: ActivatedRoute, |
23 | private i18n: I18n | 23 | private i18n: I18n |
@@ -25,12 +25,11 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
25 | } | 25 | } |
26 | 26 | ||
27 | ngOnInit () { | 27 | ngOnInit () { |
28 | |||
29 | this.userId = this.route.snapshot.queryParams['userId'] | 28 | this.userId = this.route.snapshot.queryParams['userId'] |
30 | this.verificationString = this.route.snapshot.queryParams['verificationString'] | 29 | this.verificationString = this.route.snapshot.queryParams['verificationString'] |
31 | 30 | ||
32 | if (!this.userId || !this.verificationString) { | 31 | if (!this.userId || !this.verificationString) { |
33 | this.notificationsService.error(this.i18n('Error'), this.i18n('Unable to find user id or verification string.')) | 32 | this.notifier.error(this.i18n('Unable to find user id or verification string.')) |
34 | } else { | 33 | } else { |
35 | this.verifyEmail() | 34 | this.verifyEmail() |
36 | } | 35 | } |
@@ -47,7 +46,7 @@ export class VerifyAccountEmailComponent implements OnInit { | |||
47 | }, | 46 | }, |
48 | 47 | ||
49 | err => { | 48 | err => { |
50 | this.notificationsService.error(this.i18n('Error'), err.message) | 49 | this.notifier.error(err.message) |
51 | } | 50 | } |
52 | ) | 51 | ) |
53 | } | 52 | } |
diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts index ea7b0e118..895b19064 100644 --- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts +++ b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts | |||
@@ -3,7 +3,7 @@ import { VideoChannelService } from '@app/shared/video-channel/video-channel.ser | |||
3 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 3 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { Subscription } from 'rxjs' | 5 | import { Subscription } from 'rxjs' |
6 | import { MarkdownService } from '@app/videos/shared' | 6 | import { MarkdownService } from '@app/shared/renderer' |
7 | 7 | ||
8 | @Component({ | 8 | @Component({ |
9 | selector: 'my-video-channel-about', | 9 | selector: 'my-video-channel-about', |
diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts index 800d97b7f..dea378a6e 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts | |||
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core' | |||
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Location } from '@angular/common' | 3 | import { Location } from '@angular/common' |
4 | import { immutableAssign } from '@app/shared/misc/utils' | 4 | import { immutableAssign } from '@app/shared/misc/utils' |
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
7 | import { ConfirmService } from '../../core/confirm' | 6 | import { ConfirmService } from '../../core/confirm' |
8 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 7 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
@@ -13,6 +12,7 @@ import { tap } from 'rxjs/operators' | |||
13 | import { I18n } from '@ngx-translate/i18n-polyfill' | 12 | import { I18n } from '@ngx-translate/i18n-polyfill' |
14 | import { Subscription } from 'rxjs' | 13 | import { Subscription } from 'rxjs' |
15 | import { ScreenService } from '@app/shared/misc/screen.service' | 14 | import { ScreenService } from '@app/shared/misc/screen.service' |
15 | import { Notifier } from '@app/core' | ||
16 | 16 | ||
17 | @Component({ | 17 | @Component({ |
18 | selector: 'my-video-channel-videos', | 18 | selector: 'my-video-channel-videos', |
@@ -25,7 +25,7 @@ import { ScreenService } from '@app/shared/misc/screen.service' | |||
25 | export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { | 25 | export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { |
26 | titlePage: string | 26 | titlePage: string |
27 | marginContent = false // Disable margin | 27 | marginContent = false // Disable margin |
28 | currentRoute = '/video-channel/videos' | 28 | currentRoute = '/video-channels/videos' |
29 | loadOnInit = false | 29 | loadOnInit = false |
30 | 30 | ||
31 | private videoChannel: VideoChannel | 31 | private videoChannel: VideoChannel |
@@ -35,7 +35,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
35 | protected router: Router, | 35 | protected router: Router, |
36 | protected route: ActivatedRoute, | 36 | protected route: ActivatedRoute, |
37 | protected authService: AuthService, | 37 | protected authService: AuthService, |
38 | protected notificationsService: NotificationsService, | 38 | protected notifier: Notifier, |
39 | protected confirmService: ConfirmService, | 39 | protected confirmService: ConfirmService, |
40 | protected location: Location, | 40 | protected location: Location, |
41 | protected screenService: ScreenService, | 41 | protected screenService: ScreenService, |
@@ -55,7 +55,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On | |||
55 | this.videoChannelSub = this.videoChannelService.videoChannelLoaded | 55 | this.videoChannelSub = this.videoChannelService.videoChannelLoaded |
56 | .subscribe(videoChannel => { | 56 | .subscribe(videoChannel => { |
57 | this.videoChannel = videoChannel | 57 | this.videoChannel = videoChannel |
58 | this.currentRoute = '/video-channel/' + this.videoChannel.uuid + '/videos' | 58 | this.currentRoute = '/video-channels/' + this.videoChannel.nameWithHost + '/videos' |
59 | 59 | ||
60 | this.reloadVideos() | 60 | this.reloadVideos() |
61 | this.generateSyndicationList() | 61 | this.generateSyndicationList() |
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts index 935578d2a..3ac3533d9 100644 --- a/client/src/app/+video-channels/video-channels-routing.module.ts +++ b/client/src/app/+video-channels/video-channels-routing.module.ts | |||
@@ -7,7 +7,7 @@ import { VideoChannelAboutComponent } from './video-channel-about/video-channel- | |||
7 | 7 | ||
8 | const videoChannelsRoutes: Routes = [ | 8 | const videoChannelsRoutes: Routes = [ |
9 | { | 9 | { |
10 | path: ':videoChannelId', | 10 | path: ':videoChannelName', |
11 | component: VideoChannelsComponent, | 11 | component: VideoChannelsComponent, |
12 | canActivateChild: [ MetaGuard ], | 12 | canActivateChild: [ MetaGuard ], |
13 | children: [ | 13 | children: [ |
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts index 0c5c814c7..41ff82e98 100644 --- a/client/src/app/+video-channels/video-channels.component.ts +++ b/client/src/app/+video-channels/video-channels.component.ts | |||
@@ -34,9 +34,9 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { | |||
34 | ngOnInit () { | 34 | ngOnInit () { |
35 | this.routeSub = this.route.params | 35 | this.routeSub = this.route.params |
36 | .pipe( | 36 | .pipe( |
37 | map(params => params[ 'videoChannelId' ]), | 37 | map(params => params[ 'videoChannelName' ]), |
38 | distinctUntilChanged(), | 38 | distinctUntilChanged(), |
39 | switchMap(videoChannelId => this.videoChannelService.getVideoChannel(videoChannelId)), | 39 | switchMap(videoChannelName => this.videoChannelService.getVideoChannel(videoChannelName)), |
40 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) | 40 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) |
41 | ) | 41 | ) |
42 | .subscribe(videoChannel => this.videoChannel = videoChannel) | 42 | .subscribe(videoChannel => this.videoChannel = videoChannel) |
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 545d6aeda..cff37a7d6 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts | |||
@@ -43,7 +43,8 @@ const routes: Routes = [ | |||
43 | imports: [ | 43 | imports: [ |
44 | RouterModule.forRoot(routes, { | 44 | RouterModule.forRoot(routes, { |
45 | useHash: Boolean(history.pushState) === false, | 45 | useHash: Boolean(history.pushState) === false, |
46 | preloadingStrategy: PreloadSelectedModulesList | 46 | preloadingStrategy: PreloadSelectedModulesList, |
47 | anchorScrolling: 'enabled' | ||
47 | }) | 48 | }) |
48 | ], | 49 | ], |
49 | providers: [ | 50 | providers: [ |
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 03f7e88ed..d398d4f35 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html | |||
@@ -30,12 +30,27 @@ | |||
30 | 30 | ||
31 | <footer class="row"> | 31 | <footer class="row"> |
32 | <a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a> - | 32 | <a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a> - |
33 | <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2018</a> | 33 | <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2019</a> |
34 | </footer> | 34 | </footer> |
35 | </div> | 35 | </div> |
36 | </div> | 36 | </div> |
37 | </div> | 37 | </div> |
38 | 38 | ||
39 | <ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar> | 39 | <ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar> |
40 | |||
40 | <my-confirm></my-confirm> | 41 | <my-confirm></my-confirm> |
41 | <simple-notifications [options]="notificationOptions"></simple-notifications> | 42 | |
43 | <p-toast position="bottom-right"> | ||
44 | <ng-template let-message pTemplate="message"> | ||
45 | <div class="notification-block"> | ||
46 | <div class="message"> | ||
47 | <h3>{{ message.summary }}</h3> | ||
48 | <p>{{ message.detail }}</p> | ||
49 | </div> | ||
50 | |||
51 | <span *ngIf="message.severity === 'success'" class="glyphicon glyphicon-ok"></span> | ||
52 | <span *ngIf="message.severity === 'info'" class="glyphicon glyphicon-info-sign"></span> | ||
53 | <span *ngIf="message.severity === 'error'" class="glyphicon glyphicon-remove"></span> | ||
54 | </div> | ||
55 | </ng-template> | ||
56 | </p-toast> | ||
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index b51a81eb1..881f3ff31 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss | |||
@@ -91,8 +91,3 @@ footer { | |||
91 | height: $footer-height; | 91 | height: $footer-height; |
92 | justify-content: center; | 92 | justify-content: center; |
93 | } | 93 | } |
94 | |||
95 | simple-notifications { | ||
96 | position: relative; | ||
97 | z-index: 1500; | ||
98 | } | ||
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index dc4d0bf6a..7583fdee8 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts | |||
@@ -15,19 +15,6 @@ import { fromEvent } from 'rxjs' | |||
15 | styleUrls: [ './app.component.scss' ] | 15 | styleUrls: [ './app.component.scss' ] |
16 | }) | 16 | }) |
17 | export class AppComponent implements OnInit { | 17 | export class AppComponent implements OnInit { |
18 | notificationOptions = { | ||
19 | timeOut: 5000, | ||
20 | lastOnBottom: true, | ||
21 | clickToClose: true, | ||
22 | maxLength: 0, | ||
23 | maxStack: 7, | ||
24 | showProgressBar: false, | ||
25 | pauseOnHover: false, | ||
26 | preventDuplicates: false, | ||
27 | preventLastDuplicates: 'visible', | ||
28 | rtl: false | ||
29 | } | ||
30 | |||
31 | isMenuDisplayed = true | 18 | isMenuDisplayed = true |
32 | isMenuChangedByUser = false | 19 | isMenuChangedByUser = false |
33 | 20 | ||
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 34e890b40..0bbc2e08b 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -12,13 +12,12 @@ import { AppComponent } from './app.component' | |||
12 | import { CoreModule } from './core' | 12 | import { CoreModule } from './core' |
13 | import { HeaderComponent } from './header' | 13 | import { HeaderComponent } from './header' |
14 | import { LoginModule } from './login' | 14 | import { LoginModule } from './login' |
15 | import { MenuComponent } from './menu' | 15 | import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' |
16 | import { SharedModule } from './shared' | 16 | import { SharedModule } from './shared' |
17 | import { SignupModule } from './signup' | 17 | import { SignupModule } from './signup' |
18 | import { VideosModule } from './videos' | 18 | import { VideosModule } from './videos' |
19 | import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' | 19 | import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' |
20 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | 20 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' |
21 | import { LanguageChooserComponent } from '@app/menu/language-chooser.component' | ||
22 | import { SearchModule } from '@app/search' | 21 | import { SearchModule } from '@app/search' |
23 | 22 | ||
24 | export function metaFactory (serverService: ServerService): MetaLoader { | 23 | export function metaFactory (serverService: ServerService): MetaLoader { |
@@ -40,6 +39,7 @@ export function metaFactory (serverService: ServerService): MetaLoader { | |||
40 | 39 | ||
41 | MenuComponent, | 40 | MenuComponent, |
42 | LanguageChooserComponent, | 41 | LanguageChooserComponent, |
42 | AvatarNotificationComponent, | ||
43 | HeaderComponent | 43 | HeaderComponent |
44 | ], | 44 | ], |
45 | imports: [ | 45 | imports: [ |
@@ -69,7 +69,7 @@ export function metaFactory (serverService: ServerService): MetaLoader { | |||
69 | providers: [ | 69 | providers: [ |
70 | { | 70 | { |
71 | provide: TRANSLATIONS, | 71 | provide: TRANSLATIONS, |
72 | useFactory: (locale) => { | 72 | useFactory: (locale: string) => { |
73 | // On dev mode, test localization | 73 | // On dev mode, test localization |
74 | if (isOnDevLocale()) { | 74 | if (isOnDevLocale()) { |
75 | locale = buildFileLocale(getDevLocale()) | 75 | locale = buildFileLocale(getDevLocale()) |
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index 74ed1c580..abb11fdc2 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | 1 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' |
2 | import { UserRight } from '../../../../../shared/models/users/user-right.enum' | 2 | import { UserRight } from '../../../../../shared/models/users/user-right.enum' |
3 | import { User as ServerUserModel } from '../../../../../shared/models/users/user.model' | ||
3 | // Do not use the barrel (dependency loop) | 4 | // Do not use the barrel (dependency loop) |
4 | import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' | 5 | import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' |
5 | import { User, UserConstructorHash } from '../../shared/users/user.model' | 6 | import { User } from '../../shared/users/user.model' |
6 | import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' | 7 | import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' |
7 | 8 | ||
8 | export type TokenOptions = { | 9 | export type TokenOptions = { |
@@ -70,8 +71,10 @@ export class AuthUser extends User { | |||
70 | ID: 'id', | 71 | ID: 'id', |
71 | ROLE: 'role', | 72 | ROLE: 'role', |
72 | EMAIL: 'email', | 73 | EMAIL: 'email', |
74 | VIDEOS_HISTORY_ENABLED: 'videos-history-enabled', | ||
73 | USERNAME: 'username', | 75 | USERNAME: 'username', |
74 | NSFW_POLICY: 'nsfw_policy', | 76 | NSFW_POLICY: 'nsfw_policy', |
77 | WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled', | ||
75 | AUTO_PLAY_VIDEO: 'auto_play_video' | 78 | AUTO_PLAY_VIDEO: 'auto_play_video' |
76 | } | 79 | } |
77 | 80 | ||
@@ -87,7 +90,9 @@ export class AuthUser extends User { | |||
87 | email: peertubeLocalStorage.getItem(this.KEYS.EMAIL), | 90 | email: peertubeLocalStorage.getItem(this.KEYS.EMAIL), |
88 | role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, | 91 | role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, |
89 | nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType, | 92 | nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType, |
90 | autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true' | 93 | webTorrentEnabled: peertubeLocalStorage.getItem(this.KEYS.WEBTORRENT_ENABLED) === 'true', |
94 | autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true', | ||
95 | videosHistoryEnabled: peertubeLocalStorage.getItem(this.KEYS.VIDEOS_HISTORY_ENABLED) === 'true' | ||
91 | }, | 96 | }, |
92 | Tokens.load() | 97 | Tokens.load() |
93 | ) | 98 | ) |
@@ -101,12 +106,14 @@ export class AuthUser extends User { | |||
101 | peertubeLocalStorage.removeItem(this.KEYS.ID) | 106 | peertubeLocalStorage.removeItem(this.KEYS.ID) |
102 | peertubeLocalStorage.removeItem(this.KEYS.ROLE) | 107 | peertubeLocalStorage.removeItem(this.KEYS.ROLE) |
103 | peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY) | 108 | peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY) |
109 | peertubeLocalStorage.removeItem(this.KEYS.WEBTORRENT_ENABLED) | ||
110 | peertubeLocalStorage.removeItem(this.KEYS.VIDEOS_HISTORY_ENABLED) | ||
104 | peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) | 111 | peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) |
105 | peertubeLocalStorage.removeItem(this.KEYS.EMAIL) | 112 | peertubeLocalStorage.removeItem(this.KEYS.EMAIL) |
106 | Tokens.flush() | 113 | Tokens.flush() |
107 | } | 114 | } |
108 | 115 | ||
109 | constructor (userHash: UserConstructorHash, hashTokens: TokenOptions) { | 116 | constructor (userHash: Partial<ServerUserModel>, hashTokens: TokenOptions) { |
110 | super(userHash) | 117 | super(userHash) |
111 | this.tokens = new Tokens(hashTokens) | 118 | this.tokens = new Tokens(hashTokens) |
112 | } | 119 | } |
@@ -138,6 +145,7 @@ export class AuthUser extends User { | |||
138 | peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email) | 145 | peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email) |
139 | peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) | 146 | peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) |
140 | peertubeLocalStorage.setItem(AuthUser.KEYS.NSFW_POLICY, this.nsfwPolicy.toString()) | 147 | peertubeLocalStorage.setItem(AuthUser.KEYS.NSFW_POLICY, this.nsfwPolicy.toString()) |
148 | peertubeLocalStorage.setItem(AuthUser.KEYS.WEBTORRENT_ENABLED, JSON.stringify(this.webTorrentEnabled)) | ||
141 | peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo)) | 149 | peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo)) |
142 | this.tokens.save() | 150 | this.tokens.save() |
143 | } | 151 | } |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 9c36b946e..eaa822e0f 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -3,18 +3,18 @@ import { catchError, map, mergeMap, share, tap } from 'rxjs/operators' | |||
3 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' | 3 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' |
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { Router } from '@angular/router' | 5 | import { Router } from '@angular/router' |
6 | import { NotificationsService } from 'angular2-notifications' | 6 | import { Notifier } from '@app/core/notification/notifier.service' |
7 | import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared' | 7 | import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared' |
8 | import { User } from '../../../../../shared/models/users' | 8 | import { User } from '../../../../../shared/models/users' |
9 | import { UserLogin } from '../../../../../shared/models/users/user-login.model' | 9 | import { UserLogin } from '../../../../../shared/models/users/user-login.model' |
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | import { RestExtractor } from '../../shared/rest' | 11 | import { RestExtractor } from '../../shared/rest/rest-extractor.service' |
12 | import { AuthStatus } from './auth-status.model' | 12 | import { AuthStatus } from './auth-status.model' |
13 | import { AuthUser } from './auth-user.model' | 13 | import { AuthUser } from './auth-user.model' |
14 | import { objectToUrlEncoded } from '@app/shared/misc/utils' | 14 | import { objectToUrlEncoded } from '@app/shared/misc/utils' |
15 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | 15 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' |
16 | import { I18n } from '@ngx-translate/i18n-polyfill' | 16 | import { I18n } from '@ngx-translate/i18n-polyfill' |
17 | import { HotkeysService, Hotkey } from 'angular2-hotkeys' | 17 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
18 | 18 | ||
19 | interface UserLoginWithUsername extends UserLogin { | 19 | interface UserLoginWithUsername extends UserLogin { |
20 | access_token: string | 20 | access_token: string |
@@ -38,7 +38,6 @@ export class AuthService { | |||
38 | loginChangedSource: Observable<AuthStatus> | 38 | loginChangedSource: Observable<AuthStatus> |
39 | userInformationLoaded = new ReplaySubject<boolean>(1) | 39 | userInformationLoaded = new ReplaySubject<boolean>(1) |
40 | hotkeys: Hotkey[] | 40 | hotkeys: Hotkey[] |
41 | redirectUrl: string | ||
42 | 41 | ||
43 | private clientId: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID) | 42 | private clientId: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID) |
44 | private clientSecret: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET) | 43 | private clientSecret: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET) |
@@ -48,7 +47,7 @@ export class AuthService { | |||
48 | 47 | ||
49 | constructor ( | 48 | constructor ( |
50 | private http: HttpClient, | 49 | private http: HttpClient, |
51 | private notificationsService: NotificationsService, | 50 | private notifier: Notifier, |
52 | private hotkeysService: HotkeysService, | 51 | private hotkeysService: HotkeysService, |
53 | private restExtractor: RestExtractor, | 52 | private restExtractor: RestExtractor, |
54 | private router: Router, | 53 | private router: Router, |
@@ -106,9 +105,8 @@ export class AuthService { | |||
106 | ) | 105 | ) |
107 | } | 106 | } |
108 | 107 | ||
109 | // We put a bigger timeout | 108 | // We put a bigger timeout: this is an important message |
110 | // This is an important message | 109 | this.notifier.error(errorMessage, this.i18n('Error'), 7000) |
111 | this.notificationsService.error(this.i18n('Error'), errorMessage, { timeOut: 7000 }) | ||
112 | } | 110 | } |
113 | ) | 111 | ) |
114 | } | 112 | } |
@@ -178,8 +176,6 @@ export class AuthService { | |||
178 | this.setStatus(AuthStatus.LoggedOut) | 176 | this.setStatus(AuthStatus.LoggedOut) |
179 | 177 | ||
180 | this.hotkeysService.remove(this.hotkeys) | 178 | this.hotkeysService.remove(this.hotkeys) |
181 | |||
182 | this.redirectUrl = null | ||
183 | } | 179 | } |
184 | 180 | ||
185 | refreshAccessToken () { | 181 | refreshAccessToken () { |
@@ -221,7 +217,7 @@ export class AuthService { | |||
221 | } | 217 | } |
222 | 218 | ||
223 | refreshUserInformation () { | 219 | refreshUserInformation () { |
224 | const obj = { | 220 | const obj: UserLoginWithUsername = { |
225 | access_token: this.user.getAccessToken(), | 221 | access_token: this.user.getAccessToken(), |
226 | refresh_token: null, | 222 | refresh_token: null, |
227 | token_type: this.user.getTokenType(), | 223 | token_type: this.user.getTokenType(), |
diff --git a/client/src/app/core/auth/index.ts b/client/src/app/core/auth/index.ts index bc7bfec0e..8e5caa7ed 100644 --- a/client/src/app/core/auth/index.ts +++ b/client/src/app/core/auth/index.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | export * from './auth-status.model' | 1 | export * from './auth-status.model' |
2 | export * from './auth-user.model' | 2 | export * from './auth-user.model' |
3 | export * from './auth.service' | 3 | export * from './auth.service' |
4 | export * from '../routing/login-guard.service' | ||
diff --git a/client/src/app/core/confirm/index.ts b/client/src/app/core/confirm/index.ts index 44aabfc13..aca591e1a 100644 --- a/client/src/app/core/confirm/index.ts +++ b/client/src/app/core/confirm/index.ts | |||
@@ -1,2 +1 @@ | |||
1 | export * from './confirm.component' | ||
2 | export * from './confirm.service' | export * from './confirm.service' | |
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index df2ec696d..4ef3b1e73 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts | |||
@@ -7,16 +7,18 @@ import { LoadingBarModule } from '@ngx-loading-bar/core' | |||
7 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' | 7 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' |
8 | import { LoadingBarRouterModule } from '@ngx-loading-bar/router' | 8 | import { LoadingBarRouterModule } from '@ngx-loading-bar/router' |
9 | 9 | ||
10 | import { SimpleNotificationsModule } from 'angular2-notifications' | ||
11 | |||
12 | import { AuthService } from './auth' | 10 | import { AuthService } from './auth' |
13 | import { ConfirmComponent, ConfirmService } from './confirm' | 11 | import { ConfirmService } from './confirm' |
14 | import { throwIfAlreadyLoaded } from './module-import-guard' | 12 | import { throwIfAlreadyLoaded } from './module-import-guard' |
15 | import { LoginGuard, RedirectService, UserRightGuard } from './routing' | 13 | import { LoginGuard, RedirectService, UserRightGuard } from './routing' |
16 | import { ServerService } from './server' | 14 | import { ServerService } from './server' |
17 | import { ThemeService } from './theme' | 15 | import { ThemeService } from './theme' |
18 | import { HotkeyModule } from 'angular2-hotkeys' | 16 | import { HotkeyModule } from 'angular2-hotkeys' |
19 | import { CheatSheetComponent } from '@app/core/hotkeys' | 17 | import { CheatSheetComponent } from './hotkeys' |
18 | import { ToastModule } from 'primeng/toast' | ||
19 | import { Notifier } from './notification' | ||
20 | import { MessageService } from 'primeng/api' | ||
21 | import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' | ||
20 | 22 | ||
21 | @NgModule({ | 23 | @NgModule({ |
22 | imports: [ | 24 | imports: [ |
@@ -25,11 +27,10 @@ import { CheatSheetComponent } from '@app/core/hotkeys' | |||
25 | FormsModule, | 27 | FormsModule, |
26 | BrowserAnimationsModule, | 28 | BrowserAnimationsModule, |
27 | 29 | ||
28 | SimpleNotificationsModule.forRoot(), | ||
29 | |||
30 | LoadingBarHttpClientModule, | 30 | LoadingBarHttpClientModule, |
31 | LoadingBarRouterModule, | 31 | LoadingBarRouterModule, |
32 | LoadingBarModule.forRoot(), | 32 | LoadingBarModule, |
33 | ToastModule, | ||
33 | 34 | ||
34 | HotkeyModule.forRoot({ | 35 | HotkeyModule.forRoot({ |
35 | cheatSheetCloseEsc: true | 36 | cheatSheetCloseEsc: true |
@@ -37,16 +38,15 @@ import { CheatSheetComponent } from '@app/core/hotkeys' | |||
37 | ], | 38 | ], |
38 | 39 | ||
39 | declarations: [ | 40 | declarations: [ |
40 | ConfirmComponent, | ||
41 | CheatSheetComponent | 41 | CheatSheetComponent |
42 | ], | 42 | ], |
43 | 43 | ||
44 | exports: [ | 44 | exports: [ |
45 | SimpleNotificationsModule, | ||
46 | LoadingBarHttpClientModule, | 45 | LoadingBarHttpClientModule, |
47 | LoadingBarModule, | 46 | LoadingBarModule, |
48 | 47 | ||
49 | ConfirmComponent, | 48 | ToastModule, |
49 | |||
50 | CheatSheetComponent | 50 | CheatSheetComponent |
51 | ], | 51 | ], |
52 | 52 | ||
@@ -57,7 +57,10 @@ import { CheatSheetComponent } from '@app/core/hotkeys' | |||
57 | ThemeService, | 57 | ThemeService, |
58 | LoginGuard, | 58 | LoginGuard, |
59 | UserRightGuard, | 59 | UserRightGuard, |
60 | RedirectService | 60 | RedirectService, |
61 | Notifier, | ||
62 | MessageService, | ||
63 | UserNotificationSocket | ||
61 | ] | 64 | ] |
62 | }) | 65 | }) |
63 | export class CoreModule { | 66 | export class CoreModule { |
diff --git a/client/src/app/core/index.ts b/client/src/app/core/index.ts index 524589d74..f664aff41 100644 --- a/client/src/app/core/index.ts +++ b/client/src/app/core/index.ts | |||
@@ -2,6 +2,7 @@ export * from './auth' | |||
2 | export * from './confirm' | 2 | export * from './confirm' |
3 | export * from './routing' | 3 | export * from './routing' |
4 | export * from './server' | 4 | export * from './server' |
5 | export * from './notification' | ||
5 | export * from './theme' | 6 | export * from './theme' |
6 | 7 | ||
7 | export * from './core.module' | 8 | export * from './core.module' |
diff --git a/client/src/app/core/notification/index.ts b/client/src/app/core/notification/index.ts new file mode 100644 index 000000000..3e8d9ea65 --- /dev/null +++ b/client/src/app/core/notification/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './notifier.service' | ||
2 | export * from './user-notification-socket.service' | ||
diff --git a/client/src/app/core/notification/notifier.service.ts b/client/src/app/core/notification/notifier.service.ts new file mode 100644 index 000000000..9833c65a0 --- /dev/null +++ b/client/src/app/core/notification/notifier.service.ts | |||
@@ -0,0 +1,41 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { MessageService } from 'primeng/api' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
4 | |||
5 | @Injectable() | ||
6 | export class Notifier { | ||
7 | readonly TIMEOUT = 5000 | ||
8 | |||
9 | constructor ( | ||
10 | private i18n: I18n, | ||
11 | private messageService: MessageService) { | ||
12 | } | ||
13 | |||
14 | info (text: string, title?: string, timeout?: number) { | ||
15 | if (!title) title = this.i18n('Info') | ||
16 | |||
17 | return this.notify('info', text, title, timeout) | ||
18 | } | ||
19 | |||
20 | error (text: string, title?: string, timeout?: number) { | ||
21 | if (!title) title = this.i18n('Error') | ||
22 | |||
23 | return this.notify('error', text, title, timeout) | ||
24 | } | ||
25 | |||
26 | success (text: string, title?: string, timeout?: number) { | ||
27 | if (!title) title = this.i18n('Success') | ||
28 | |||
29 | return this.notify('success', text, title, timeout) | ||
30 | } | ||
31 | |||
32 | private notify (severity: 'success' | 'info' | 'warn' | 'error', text: string, title: string, timeout?: number) { | ||
33 | this.messageService.add({ | ||
34 | severity, | ||
35 | summary: title, | ||
36 | detail: text, | ||
37 | closable: true, | ||
38 | life: timeout || this.TIMEOUT | ||
39 | }) | ||
40 | } | ||
41 | } | ||
diff --git a/client/src/app/core/notification/user-notification-socket.service.ts b/client/src/app/core/notification/user-notification-socket.service.ts new file mode 100644 index 000000000..f367d9ae4 --- /dev/null +++ b/client/src/app/core/notification/user-notification-socket.service.ts | |||
@@ -0,0 +1,41 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { environment } from '../../../environments/environment' | ||
3 | import { UserNotification as UserNotificationServer } from '../../../../../shared' | ||
4 | import { Subject } from 'rxjs' | ||
5 | import * as io from 'socket.io-client' | ||
6 | import { AuthService } from '../auth' | ||
7 | |||
8 | export type NotificationEvent = 'new' | 'read' | 'read-all' | ||
9 | |||
10 | @Injectable() | ||
11 | export class UserNotificationSocket { | ||
12 | private notificationSubject = new Subject<{ type: NotificationEvent, notification?: UserNotificationServer }>() | ||
13 | |||
14 | private socket: SocketIOClient.Socket | ||
15 | |||
16 | constructor ( | ||
17 | private auth: AuthService | ||
18 | ) {} | ||
19 | |||
20 | dispatch (type: NotificationEvent, notification?: UserNotificationServer) { | ||
21 | this.notificationSubject.next({ type, notification }) | ||
22 | } | ||
23 | |||
24 | getMyNotificationsSocket () { | ||
25 | const socket = this.getSocket() | ||
26 | |||
27 | socket.on('new-notification', (n: UserNotificationServer) => this.dispatch('new', n)) | ||
28 | |||
29 | return this.notificationSubject.asObservable() | ||
30 | } | ||
31 | |||
32 | private getSocket () { | ||
33 | if (this.socket) return this.socket | ||
34 | |||
35 | this.socket = io(environment.apiUrl + '/user-notifications', { | ||
36 | query: { accessToken: this.auth.getAccessToken() } | ||
37 | }) | ||
38 | |||
39 | return this.socket | ||
40 | } | ||
41 | } | ||
diff --git a/client/src/app/core/routing/login-guard.service.ts b/client/src/app/core/routing/login-guard.service.ts index 40ff8f505..7b1c37ee8 100644 --- a/client/src/app/core/routing/login-guard.service.ts +++ b/client/src/app/core/routing/login-guard.service.ts | |||
@@ -1,11 +1,5 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { | 2 | import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router' |
3 | ActivatedRouteSnapshot, | ||
4 | CanActivateChild, | ||
5 | RouterStateSnapshot, | ||
6 | CanActivate, | ||
7 | Router | ||
8 | } from '@angular/router' | ||
9 | 3 | ||
10 | import { AuthService } from '../auth/auth.service' | 4 | import { AuthService } from '../auth/auth.service' |
11 | 5 | ||
@@ -20,8 +14,6 @@ export class LoginGuard implements CanActivate, CanActivateChild { | |||
20 | canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | 14 | canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { |
21 | if (this.auth.isLoggedIn() === true) return true | 15 | if (this.auth.isLoggedIn() === true) return true |
22 | 16 | ||
23 | this.auth.redirectUrl = state.url | ||
24 | |||
25 | this.router.navigate([ '/login' ]) | 17 | this.router.navigate([ '/login' ]) |
26 | return false | 18 | return false |
27 | } | 19 | } |
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts index 1881be117..e1db4097b 100644 --- a/client/src/app/core/routing/redirect.service.ts +++ b/client/src/app/core/routing/redirect.service.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { NavigationEnd, Router } from '@angular/router' |
3 | import { ServerService } from '../server' | 3 | import { ServerService } from '../server' |
4 | 4 | ||
5 | @Injectable() | 5 | @Injectable() |
@@ -8,6 +8,9 @@ export class RedirectService { | |||
8 | static INIT_DEFAULT_ROUTE = '/videos/trending' | 8 | static INIT_DEFAULT_ROUTE = '/videos/trending' |
9 | static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE | 9 | static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE |
10 | 10 | ||
11 | private previousUrl: string | ||
12 | private currentUrl: string | ||
13 | |||
11 | constructor ( | 14 | constructor ( |
12 | private router: Router, | 15 | private router: Router, |
13 | private serverService: ServerService | 16 | private serverService: ServerService |
@@ -18,6 +21,7 @@ export class RedirectService { | |||
18 | RedirectService.DEFAULT_ROUTE = config.instance.defaultClientRoute | 21 | RedirectService.DEFAULT_ROUTE = config.instance.defaultClientRoute |
19 | } | 22 | } |
20 | 23 | ||
24 | // Load default route | ||
21 | this.serverService.configLoaded | 25 | this.serverService.configLoaded |
22 | .subscribe(() => { | 26 | .subscribe(() => { |
23 | const defaultRouteConfig = this.serverService.getConfig().instance.defaultClientRoute | 27 | const defaultRouteConfig = this.serverService.getConfig().instance.defaultClientRoute |
@@ -26,6 +30,21 @@ export class RedirectService { | |||
26 | RedirectService.DEFAULT_ROUTE = defaultRouteConfig | 30 | RedirectService.DEFAULT_ROUTE = defaultRouteConfig |
27 | } | 31 | } |
28 | }) | 32 | }) |
33 | |||
34 | // Track previous url | ||
35 | this.currentUrl = this.router.url | ||
36 | router.events.subscribe(event => { | ||
37 | if (event instanceof NavigationEnd) { | ||
38 | this.previousUrl = this.currentUrl | ||
39 | this.currentUrl = event.url | ||
40 | } | ||
41 | }) | ||
42 | } | ||
43 | |||
44 | redirectToPreviousRoute () { | ||
45 | if (this.previousUrl) return this.router.navigateByUrl(this.previousUrl) | ||
46 | |||
47 | return this.redirectToHomepage() | ||
29 | } | 48 | } |
30 | 49 | ||
31 | redirectToHomepage (skipLocationChange = false) { | 50 | redirectToHomepage (skipLocationChange = false) { |
diff --git a/client/src/app/core/routing/user-right-guard.service.ts b/client/src/app/core/routing/user-right-guard.service.ts index 65d029977..50c3d8c19 100644 --- a/client/src/app/core/routing/user-right-guard.service.ts +++ b/client/src/app/core/routing/user-right-guard.service.ts | |||
@@ -7,7 +7,7 @@ import { | |||
7 | Router | 7 | Router |
8 | } from '@angular/router' | 8 | } from '@angular/router' |
9 | 9 | ||
10 | import { AuthService } from '../auth' | 10 | import { AuthService } from '../auth/auth.service' |
11 | 11 | ||
12 | @Injectable() | 12 | @Injectable() |
13 | export class UserRightGuard implements CanActivate, CanActivateChild { | 13 | export class UserRightGuard implements CanActivate, CanActivateChild { |
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 2f1ef1fc2..c868ccdcc 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -13,6 +13,7 @@ import { sortBy } from '@app/shared/misc/utils' | |||
13 | 13 | ||
14 | @Injectable() | 14 | @Injectable() |
15 | export class ServerService { | 15 | export class ServerService { |
16 | private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/' | ||
16 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' | 17 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' |
17 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' | 18 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' |
18 | private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' | 19 | private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' |
@@ -37,6 +38,12 @@ export class ServerService { | |||
37 | css: '' | 38 | css: '' |
38 | } | 39 | } |
39 | }, | 40 | }, |
41 | email: { | ||
42 | enabled: false | ||
43 | }, | ||
44 | contactForm: { | ||
45 | enabled: false | ||
46 | }, | ||
40 | serverVersion: 'Unknown', | 47 | serverVersion: 'Unknown', |
41 | signup: { | 48 | signup: { |
42 | allowed: false, | 49 | allowed: false, |
@@ -44,7 +51,10 @@ export class ServerService { | |||
44 | requiresEmailVerification: false | 51 | requiresEmailVerification: false |
45 | }, | 52 | }, |
46 | transcoding: { | 53 | transcoding: { |
47 | enabledResolutions: [] | 54 | enabledResolutions: [], |
55 | hls: { | ||
56 | enabled: false | ||
57 | } | ||
48 | }, | 58 | }, |
49 | avatar: { | 59 | avatar: { |
50 | file: { | 60 | file: { |
@@ -80,6 +90,11 @@ export class ServerService { | |||
80 | enabled: false | 90 | enabled: false |
81 | } | 91 | } |
82 | } | 92 | } |
93 | }, | ||
94 | trending: { | ||
95 | videos: { | ||
96 | intervalDays: 0 | ||
97 | } | ||
83 | } | 98 | } |
84 | } | 99 | } |
85 | private videoCategories: Array<VideoConstant<number>> = [] | 100 | private videoCategories: Array<VideoConstant<number>> = [] |
@@ -141,10 +156,6 @@ export class ServerService { | |||
141 | return this.videoPrivacies | 156 | return this.videoPrivacies |
142 | } | 157 | } |
143 | 158 | ||
144 | getAbout () { | ||
145 | return this.http.get<About>(ServerService.BASE_CONFIG_URL + '/about') | ||
146 | } | ||
147 | |||
148 | private loadVideoAttributeEnum ( | 159 | private loadVideoAttributeEnum ( |
149 | attributeName: 'categories' | 'licences' | 'languages' | 'privacies', | 160 | attributeName: 'categories' | 'licences' | 'languages' | 'privacies', |
150 | hashToPopulate: VideoConstant<string | number>[], | 161 | hashToPopulate: VideoConstant<string | number>[], |
@@ -154,7 +165,7 @@ export class ServerService { | |||
154 | this.localeObservable | 165 | this.localeObservable |
155 | .pipe( | 166 | .pipe( |
156 | switchMap(translations => { | 167 | switchMap(translations => { |
157 | return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) | 168 | return this.http.get<{ [id: string]: string }>(ServerService.BASE_VIDEO_URL + attributeName) |
158 | .pipe(map(data => ({ data, translations }))) | 169 | .pipe(map(data => ({ data, translations }))) |
159 | }) | 170 | }) |
160 | ) | 171 | ) |
diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts index a6eef0898..50c19ecac 100644 --- a/client/src/app/core/theme/theme.service.ts +++ b/client/src/app/core/theme/theme.service.ts | |||
@@ -5,7 +5,7 @@ import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | |||
5 | export class ThemeService { | 5 | export class ThemeService { |
6 | private theme = document.querySelector('body') | 6 | private theme = document.querySelector('body') |
7 | private darkTheme = false | 7 | private darkTheme = false |
8 | private previousTheme = {} | 8 | private previousTheme: { [ id: string ]: string } = {} |
9 | 9 | ||
10 | constructor () { | 10 | constructor () { |
11 | // initialise the alternative theme with dark theme colors | 11 | // initialise the alternative theme with dark theme colors |
@@ -33,7 +33,7 @@ export class ThemeService { | |||
33 | } | 33 | } |
34 | } | 34 | } |
35 | 35 | ||
36 | private switchProperty (property, newValue?) { | 36 | private switchProperty (property: string, newValue?: string) { |
37 | const propertyOldvalue = window.getComputedStyle(this.theme).getPropertyValue('--' + property) | 37 | const propertyOldvalue = window.getComputedStyle(this.theme).getPropertyValue('--' + property) |
38 | this.theme.style.setProperty('--' + property, (newValue) ? newValue : this.previousTheme[property]) | 38 | this.theme.style.setProperty('--' + property, (newValue) ? newValue : this.previousTheme[property]) |
39 | this.previousTheme[property] = propertyOldvalue | 39 | this.previousTheme[property] = propertyOldvalue |
diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index a04354db5..46a87c79c 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html | |||
@@ -1,10 +1,10 @@ | |||
1 | <input | 1 | <input |
2 | type="text" id="search-video" name="search-video" i18n-placeholder placeholder="Search..." | 2 | type="text" id="search-video" name="search-video" i18n-placeholder placeholder="Search..." |
3 | [(ngModel)]="searchValue" (keyup.enter)="doSearch()" | 3 | [(ngModel)]="searchValue" (keyup.enter)="doSearch()" |
4 | > | 4 | > |
5 | <span (click)="doSearch()" class="icon icon-search"></span> | 5 | <span (click)="doSearch()" class="icon icon-search"></span> |
6 | 6 | ||
7 | <a class="upload-button" routerLink="/videos/upload"> | 7 | <a class="upload-button" routerLink="/videos/upload"> |
8 | <span class="icon icon-upload"></span> | 8 | <my-global-icon iconName="upload"></my-global-icon> |
9 | <span i18n class="upload-button-label">Upload</span> | 9 | <span i18n class="upload-button-label">Upload</span> |
10 | </a> | 10 | </a> |
diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss index bd03c338a..cea415d9b 100644 --- a/client/src/app/header/header.component.scss +++ b/client/src/app/header/header.component.scss | |||
@@ -6,6 +6,7 @@ | |||
6 | padding-left: 10px; | 6 | padding-left: 10px; |
7 | margin-right: 15px; | 7 | margin-right: 15px; |
8 | padding-right: 40px; // For the search icon | 8 | padding-right: 40px; // For the search icon |
9 | font-size: 14px; | ||
9 | 10 | ||
10 | &::placeholder { | 11 | &::placeholder { |
11 | color: var(--inputPlaceholderColor); | 12 | color: var(--inputPlaceholderColor); |
@@ -40,6 +41,7 @@ | |||
40 | .upload-button { | 41 | .upload-button { |
41 | @include peertube-button-link; | 42 | @include peertube-button-link; |
42 | @include orange-button; | 43 | @include orange-button; |
44 | @include button-with-icon(22px, 3px, -1px); | ||
43 | 45 | ||
44 | margin-right: 25px; | 46 | margin-right: 25px; |
45 | 47 | ||
@@ -47,15 +49,6 @@ | |||
47 | margin-right: 0; | 49 | margin-right: 0; |
48 | } | 50 | } |
49 | 51 | ||
50 | .icon.icon-upload { | ||
51 | @include icon(22px); | ||
52 | |||
53 | background-image: url('../../assets/images/header/upload.svg'); | ||
54 | height: 24px; | ||
55 | vertical-align: middle; | ||
56 | margin-right: 6px; | ||
57 | } | ||
58 | |||
59 | @media screen and (max-width: 600px) { | 52 | @media screen and (max-width: 600px) { |
60 | margin-right: 10px; | 53 | margin-right: 10px; |
61 | padding: 0 10px; | 54 | padding: 0 10px; |
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html index 93dbed525..4efe3fb22 100644 --- a/client/src/app/login/login.component.html +++ b/client/src/app/login/login.component.html | |||
@@ -55,11 +55,17 @@ | |||
55 | <ng-template #forgotPasswordModal> | 55 | <ng-template #forgotPasswordModal> |
56 | <div class="modal-header"> | 56 | <div class="modal-header"> |
57 | <h4 i18n class="modal-title">Forgot your password</h4> | 57 | <h4 i18n class="modal-title">Forgot your password</h4> |
58 | <span class="close" aria-hidden="true" (click)="hideForgotPasswordModal()"></span> | 58 | |
59 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideForgotPasswordModal()"></my-global-icon> | ||
59 | </div> | 60 | </div> |
60 | 61 | ||
61 | <div class="modal-body"> | 62 | <div class="modal-body"> |
62 | <div class="form-group"> | 63 | |
64 | <div *ngIf="isEmailDisabled()" class="alert alert-danger" i18n> | ||
65 | We are sorry, you cannot recover you password because your instance administrator did not configure the PeerTube email system. | ||
66 | </div> | ||
67 | |||
68 | <div class="form-group" [hidden]="isEmailDisabled()"> | ||
63 | <label i18n for="forgot-password-email">Email</label> | 69 | <label i18n for="forgot-password-email">Email</label> |
64 | <input | 70 | <input |
65 | type="email" id="forgot-password-email" i18n-placeholder placeholder="Email address" required | 71 | type="email" id="forgot-password-email" i18n-placeholder placeholder="Email address" required |
diff --git a/client/src/app/login/login.component.ts b/client/src/app/login/login.component.ts index 7553e6456..fc2442c0e 100644 --- a/client/src/app/login/login.component.ts +++ b/client/src/app/login/login.component.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
2 | import { RedirectService, ServerService } from '@app/core' | 2 | import { Notifier, RedirectService, ServerService } from '@app/core' |
3 | import { UserService } from '@app/shared' | 3 | import { UserService } from '@app/shared' |
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { AuthService } from '../core' | 4 | import { AuthService } from '../core' |
6 | import { FormReactive } from '../shared' | 5 | import { FormReactive } from '../shared' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -19,7 +18,6 @@ import { Router } from '@angular/router' | |||
19 | export class LoginComponent extends FormReactive implements OnInit { | 18 | export class LoginComponent extends FormReactive implements OnInit { |
20 | @ViewChild('emailInput') input: ElementRef | 19 | @ViewChild('emailInput') input: ElementRef |
21 | @ViewChild('forgotPasswordModal') forgotPasswordModal: ElementRef | 20 | @ViewChild('forgotPasswordModal') forgotPasswordModal: ElementRef |
22 | @ViewChild('forgotPasswordEmailInput') forgotPasswordEmailInput: ElementRef | ||
23 | 21 | ||
24 | error: string = null | 22 | error: string = null |
25 | forgotPasswordEmail = '' | 23 | forgotPasswordEmail = '' |
@@ -35,7 +33,7 @@ export class LoginComponent extends FormReactive implements OnInit { | |||
35 | private userService: UserService, | 33 | private userService: UserService, |
36 | private serverService: ServerService, | 34 | private serverService: ServerService, |
37 | private redirectService: RedirectService, | 35 | private redirectService: RedirectService, |
38 | private notificationsService: NotificationsService, | 36 | private notifier: Notifier, |
39 | private i18n: I18n | 37 | private i18n: I18n |
40 | ) { | 38 | ) { |
41 | super() | 39 | super() |
@@ -45,6 +43,10 @@ export class LoginComponent extends FormReactive implements OnInit { | |||
45 | return this.serverService.getConfig().signup.allowed === true | 43 | return this.serverService.getConfig().signup.allowed === true |
46 | } | 44 | } |
47 | 45 | ||
46 | isEmailDisabled () { | ||
47 | return this.serverService.getConfig().email.enabled === false | ||
48 | } | ||
49 | |||
48 | ngOnInit () { | 50 | ngOnInit () { |
49 | this.buildForm({ | 51 | this.buildForm({ |
50 | username: this.loginValidatorsService.LOGIN_USERNAME, | 52 | username: this.loginValidatorsService.LOGIN_USERNAME, |
@@ -61,7 +63,7 @@ export class LoginComponent extends FormReactive implements OnInit { | |||
61 | 63 | ||
62 | this.authService.login(username, password) | 64 | this.authService.login(username, password) |
63 | .subscribe( | 65 | .subscribe( |
64 | () => this.redirect(), | 66 | () => this.redirectService.redirectToPreviousRoute(), |
65 | 67 | ||
66 | err => { | 68 | err => { |
67 | if (err.message.indexOf('credentials are invalid') !== -1) this.error = this.i18n('Incorrect username or password.') | 69 | if (err.message.indexOf('credentials are invalid') !== -1) this.error = this.i18n('Incorrect username or password.') |
@@ -71,15 +73,6 @@ export class LoginComponent extends FormReactive implements OnInit { | |||
71 | ) | 73 | ) |
72 | } | 74 | } |
73 | 75 | ||
74 | redirect () { | ||
75 | const redirect = this.authService.redirectUrl | ||
76 | if (redirect) { | ||
77 | this.router.navigate([ redirect ]) | ||
78 | } else { | ||
79 | this.redirectService.redirectToHomepage() | ||
80 | } | ||
81 | } | ||
82 | |||
83 | askResetPassword () { | 76 | askResetPassword () { |
84 | this.userService.askResetPassword(this.forgotPasswordEmail) | 77 | this.userService.askResetPassword(this.forgotPasswordEmail) |
85 | .subscribe( | 78 | .subscribe( |
@@ -88,18 +81,14 @@ export class LoginComponent extends FormReactive implements OnInit { | |||
88 | 'An email with the reset password instructions will be sent to {{email}}.', | 81 | 'An email with the reset password instructions will be sent to {{email}}.', |
89 | { email: this.forgotPasswordEmail } | 82 | { email: this.forgotPasswordEmail } |
90 | ) | 83 | ) |
91 | this.notificationsService.success(this.i18n('Success'), message) | 84 | this.notifier.success(message) |
92 | this.hideForgotPasswordModal() | 85 | this.hideForgotPasswordModal() |
93 | }, | 86 | }, |
94 | 87 | ||
95 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 88 | err => this.notifier.error(err.message) |
96 | ) | 89 | ) |
97 | } | 90 | } |
98 | 91 | ||
99 | onForgotPasswordModalShown () { | ||
100 | this.forgotPasswordEmailInput.nativeElement.focus() | ||
101 | } | ||
102 | |||
103 | openForgotPasswordModal () { | 92 | openForgotPasswordModal () { |
104 | this.openedForgotPasswordModal = this.modalService.open(this.forgotPasswordModal) | 93 | this.openedForgotPasswordModal = this.modalService.open(this.forgotPasswordModal) |
105 | } | 94 | } |
diff --git a/client/src/app/menu/avatar-notification.component.html b/client/src/app/menu/avatar-notification.component.html new file mode 100644 index 000000000..4ef3f0e89 --- /dev/null +++ b/client/src/app/menu/avatar-notification.component.html | |||
@@ -0,0 +1,23 @@ | |||
1 | <div | ||
2 | [ngbPopover]="popContent" autoClose="outside" placement="bottom-left" container="body" popoverClass="popover-notifications" | ||
3 | i18n-title title="View your notifications" class="notification-avatar" #popover="ngbPopover" | ||
4 | > | ||
5 | <div *ngIf="unreadNotifications > 0" class="unread-notifications">{{ unreadNotifications }}</div> | ||
6 | |||
7 | <img [src]="user.accountAvatarUrl" alt="Avatar" /> | ||
8 | </div> | ||
9 | |||
10 | <ng-template #popContent> | ||
11 | <div class="notifications-header"> | ||
12 | <div i18n>Notifications</div> | ||
13 | |||
14 | <a | ||
15 | i18n-title title="Update your notification preferences" class="glyphicon glyphicon-cog" | ||
16 | routerLink="/my-account/settings" fragment="notifications" | ||
17 | ></a> | ||
18 | </div> | ||
19 | |||
20 | <my-user-notifications [ignoreLoadingBar]="true" [infiniteScroll]="false" itemsPerPage="10"></my-user-notifications> | ||
21 | |||
22 | <a class="all-notifications" routerLink="/my-account/notifications" i18n>See all your notifications</a> | ||
23 | </ng-template> | ||
diff --git a/client/src/app/menu/avatar-notification.component.scss b/client/src/app/menu/avatar-notification.component.scss new file mode 100644 index 000000000..e785db788 --- /dev/null +++ b/client/src/app/menu/avatar-notification.component.scss | |||
@@ -0,0 +1,91 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | /deep/ { | ||
5 | .popover-notifications.popover { | ||
6 | max-width: none; | ||
7 | |||
8 | .popover-body { | ||
9 | padding: 0; | ||
10 | font-size: 14px; | ||
11 | font-family: $main-fonts; | ||
12 | overflow-y: auto; | ||
13 | max-height: 500px; | ||
14 | width: 400px; | ||
15 | box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30); | ||
16 | |||
17 | .notifications-header { | ||
18 | display: flex; | ||
19 | justify-content: space-between; | ||
20 | |||
21 | background-color: rgba(0, 0, 0, 0.10); | ||
22 | align-items: center; | ||
23 | padding: 0 10px; | ||
24 | font-size: 16px; | ||
25 | height: 50px; | ||
26 | |||
27 | a { | ||
28 | @include disable-default-a-behaviour; | ||
29 | |||
30 | color: rgba(20, 20, 20, 0.5); | ||
31 | |||
32 | &:hover { | ||
33 | color: rgba(20, 20, 20, 0.8); | ||
34 | } | ||
35 | } | ||
36 | } | ||
37 | |||
38 | .all-notifications { | ||
39 | display: flex; | ||
40 | align-items: center; | ||
41 | justify-content: center; | ||
42 | font-weight: $font-semibold; | ||
43 | color: var(--mainForegroundColor); | ||
44 | padding: 7px 0; | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | } | ||
49 | |||
50 | .notification-avatar { | ||
51 | cursor: pointer; | ||
52 | position: relative; | ||
53 | |||
54 | img, | ||
55 | .unread-notifications { | ||
56 | margin-left: 20px; | ||
57 | } | ||
58 | |||
59 | img { | ||
60 | @include avatar(34px); | ||
61 | |||
62 | margin-right: 10px; | ||
63 | } | ||
64 | |||
65 | .unread-notifications { | ||
66 | position: absolute; | ||
67 | top: -5px; | ||
68 | left: -5px; | ||
69 | |||
70 | display: flex; | ||
71 | align-items: center; | ||
72 | justify-content: center; | ||
73 | |||
74 | background-color: var(--mainColor); | ||
75 | color: var(#fff); | ||
76 | font-size: 10px; | ||
77 | font-weight: $font-semibold; | ||
78 | |||
79 | border-radius: 15px; | ||
80 | width: 15px; | ||
81 | height: 15px; | ||
82 | } | ||
83 | } | ||
84 | |||
85 | @media screen and (max-width: $mobile-view) { | ||
86 | /deep/ { | ||
87 | .popover-notifications.popover .popover-body { | ||
88 | width: 400px; | ||
89 | } | ||
90 | } | ||
91 | } | ||
diff --git a/client/src/app/menu/avatar-notification.component.ts b/client/src/app/menu/avatar-notification.component.ts new file mode 100644 index 000000000..f1af08096 --- /dev/null +++ b/client/src/app/menu/avatar-notification.component.ts | |||
@@ -0,0 +1,65 @@ | |||
1 | import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core' | ||
2 | import { User } from '../shared/users/user.model' | ||
3 | import { UserNotificationService } from '@app/shared/users/user-notification.service' | ||
4 | import { Subscription } from 'rxjs' | ||
5 | import { Notifier, UserNotificationSocket } from '@app/core' | ||
6 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | ||
7 | import { NavigationEnd, Router } from '@angular/router' | ||
8 | import { filter } from 'rxjs/operators' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-avatar-notification', | ||
12 | templateUrl: './avatar-notification.component.html', | ||
13 | styleUrls: [ './avatar-notification.component.scss' ] | ||
14 | }) | ||
15 | export class AvatarNotificationComponent implements OnInit, OnDestroy { | ||
16 | @ViewChild('popover') popover: NgbPopover | ||
17 | @Input() user: User | ||
18 | |||
19 | unreadNotifications = 0 | ||
20 | |||
21 | private notificationSub: Subscription | ||
22 | private routeSub: Subscription | ||
23 | |||
24 | constructor ( | ||
25 | private userNotificationService: UserNotificationService, | ||
26 | private userNotificationSocket: UserNotificationSocket, | ||
27 | private notifier: Notifier, | ||
28 | private router: Router | ||
29 | ) {} | ||
30 | |||
31 | ngOnInit () { | ||
32 | this.userNotificationService.countUnreadNotifications() | ||
33 | .subscribe( | ||
34 | result => { | ||
35 | this.unreadNotifications = Math.min(result, 99) // Limit number to 99 | ||
36 | this.subscribeToNotifications() | ||
37 | }, | ||
38 | |||
39 | err => this.notifier.error(err.message) | ||
40 | ) | ||
41 | |||
42 | this.routeSub = this.router.events | ||
43 | .pipe(filter(event => event instanceof NavigationEnd)) | ||
44 | .subscribe(() => this.closePopover()) | ||
45 | } | ||
46 | |||
47 | ngOnDestroy () { | ||
48 | if (this.notificationSub) this.notificationSub.unsubscribe() | ||
49 | if (this.routeSub) this.routeSub.unsubscribe() | ||
50 | } | ||
51 | |||
52 | closePopover () { | ||
53 | this.popover.close() | ||
54 | } | ||
55 | |||
56 | private subscribeToNotifications () { | ||
57 | this.notificationSub = this.userNotificationSocket.getMyNotificationsSocket() | ||
58 | .subscribe(data => { | ||
59 | if (data.type === 'new') return this.unreadNotifications++ | ||
60 | if (data.type === 'read') return this.unreadNotifications-- | ||
61 | if (data.type === 'read-all') return this.unreadNotifications = 0 | ||
62 | }) | ||
63 | } | ||
64 | |||
65 | } | ||
diff --git a/client/src/app/menu/index.ts b/client/src/app/menu/index.ts index 421271c12..39dbde750 100644 --- a/client/src/app/menu/index.ts +++ b/client/src/app/menu/index.ts | |||
@@ -1 +1,3 @@ | |||
1 | export * from './language-chooser.component' | ||
2 | export * from './avatar-notification.component' | ||
1 | export * from './menu.component' | 3 | export * from './menu.component' |
diff --git a/client/src/app/menu/language-chooser.component.html b/client/src/app/menu/language-chooser.component.html index c37bf2826..a62b33dda 100644 --- a/client/src/app/menu/language-chooser.component.html +++ b/client/src/app/menu/language-chooser.component.html | |||
@@ -1,9 +1,14 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Change the language</h4> | 3 | <h4 i18n class="modal-title">Change the language</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | |||
8 | <a i18n class="help-to-translate" target="_blank" rel="noreferrer noopener" href="https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/translation.md"> | ||
9 | Help to translate PeerTube! | ||
10 | </a> | ||
11 | |||
7 | <div class="modal-body"> | 12 | <div class="modal-body"> |
8 | <a *ngFor="let lang of languages" [href]="buildLanguageLink(lang)">{{ lang.label }}</a> | 13 | <a *ngFor="let lang of languages" [href]="buildLanguageLink(lang)">{{ lang.label }}</a> |
9 | </div> | 14 | </div> |
diff --git a/client/src/app/menu/language-chooser.component.scss b/client/src/app/menu/language-chooser.component.scss index 944e86f46..72deb3952 100644 --- a/client/src/app/menu/language-chooser.component.scss +++ b/client/src/app/menu/language-chooser.component.scss | |||
@@ -1,6 +1,11 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .help-to-translate { | ||
5 | @include peertube-button-link; | ||
6 | @include orange-button; | ||
7 | } | ||
8 | |||
4 | .modal-body { | 9 | .modal-body { |
5 | text-align: center; | 10 | text-align: center; |
6 | 11 | ||
@@ -9,4 +14,4 @@ | |||
9 | font-size: 16px; | 14 | font-size: 16px; |
10 | margin: 15px; | 15 | margin: 15px; |
11 | } | 16 | } |
12 | } \ No newline at end of file | 17 | } |
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index e04bdf3d6..aa5bfa9c9 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html | |||
@@ -2,9 +2,7 @@ | |||
2 | <menu> | 2 | <menu> |
3 | <div class="top-menu"> | 3 | <div class="top-menu"> |
4 | <div *ngIf="isLoggedIn" class="logged-in-block"> | 4 | <div *ngIf="isLoggedIn" class="logged-in-block"> |
5 | <a routerLink="/my-account/settings"> | 5 | <my-avatar-notification [user]="user"></my-avatar-notification> |
6 | <img [src]="user.accountAvatarUrl" alt="Avatar" /> | ||
7 | </a> | ||
8 | 6 | ||
9 | <div class="logged-in-info"> | 7 | <div class="logged-in-info"> |
10 | <a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a> | 8 | <a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a> |
@@ -97,4 +95,4 @@ | |||
97 | </menu> | 95 | </menu> |
98 | </div> | 96 | </div> |
99 | 97 | ||
100 | <my-language-chooser #languageChooserModal></my-language-chooser> \ No newline at end of file | 98 | <my-language-chooser #languageChooserModal></my-language-chooser> |
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index a842765ba..f30b89413 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss | |||
@@ -16,7 +16,7 @@ menu { | |||
16 | height: 100%; | 16 | height: 100%; |
17 | white-space: nowrap; | 17 | white-space: nowrap; |
18 | text-overflow: ellipsis; | 18 | text-overflow: ellipsis; |
19 | overflow: hidden; | 19 | overflow: auto; |
20 | color: var(--menuForegroundColor); | 20 | color: var(--menuForegroundColor); |
21 | display: flex; | 21 | display: flex; |
22 | flex-direction: column; | 22 | flex-direction: column; |
@@ -39,13 +39,6 @@ menu { | |||
39 | justify-content: center; | 39 | justify-content: center; |
40 | margin-bottom: 35px; | 40 | margin-bottom: 35px; |
41 | 41 | ||
42 | img { | ||
43 | @include avatar(34px); | ||
44 | |||
45 | margin-left: 20px; | ||
46 | margin-right: 10px; | ||
47 | } | ||
48 | |||
49 | .logged-in-info { | 42 | .logged-in-info { |
50 | flex-grow: 1; | 43 | flex-grow: 1; |
51 | 44 | ||
@@ -131,10 +124,14 @@ menu { | |||
131 | transition: background-color .1s ease-in-out; | 124 | transition: background-color .1s ease-in-out; |
132 | @include disable-default-a-behaviour; | 125 | @include disable-default-a-behaviour; |
133 | 126 | ||
134 | &:hover, &.focus-visible { | 127 | &.active { |
135 | background-color: rgba(255, 255, 255, 0.15); | 128 | background-color: rgba(255, 255, 255, 0.15); |
136 | } | 129 | } |
137 | 130 | ||
131 | &:hover, &.focus-visible { | ||
132 | background-color: rgba(255, 255, 255, 0.10); | ||
133 | } | ||
134 | |||
138 | .icon { | 135 | .icon { |
139 | @include icon(22px); | 136 | @include icon(22px); |
140 | 137 | ||
@@ -246,7 +243,7 @@ menu { | |||
246 | } | 243 | } |
247 | } | 244 | } |
248 | 245 | ||
249 | @media screen and (max-width: 400px) { | 246 | @media screen and (max-width: $mobile-view) { |
250 | .menu-wrapper { | 247 | .menu-wrapper { |
251 | width: 100% !important; | 248 | width: 100% !important; |
252 | } | 249 | } |
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 95926f5f0..371beb4a5 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -18,7 +18,7 @@ export class MenuComponent implements OnInit { | |||
18 | userHasAdminAccess = false | 18 | userHasAdminAccess = false |
19 | helpVisible = false | 19 | helpVisible = false |
20 | 20 | ||
21 | private routesPerRight = { | 21 | private routesPerRight: { [ role in UserRight ]?: string } = { |
22 | [UserRight.MANAGE_USERS]: '/admin/users', | 22 | [UserRight.MANAGE_USERS]: '/admin/users', |
23 | [UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends', | 23 | [UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends', |
24 | [UserRight.MANAGE_VIDEO_ABUSES]: '/admin/moderation/video-abuses', | 24 | [UserRight.MANAGE_VIDEO_ABUSES]: '/admin/moderation/video-abuses', |
diff --git a/client/src/app/reset-password/reset-password.component.ts b/client/src/app/reset-password/reset-password.component.ts index af1298de6..07b93ee73 100644 --- a/client/src/app/reset-password/reset-password.component.ts +++ b/client/src/app/reset-password/reset-password.component.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { UserService, UserValidatorsService } from '@app/shared' | 3 | import { UserService, UserValidatorsService, FormReactive } from '@app/shared' |
4 | import { NotificationsService } from 'angular2-notifications' | 4 | import { Notifier } from '@app/core' |
5 | import { FormReactive } from '../shared' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
8 | import { ResetPasswordValidatorsService } from '@app/shared/forms/form-validators/reset-password-validators.service' | 7 | import { ResetPasswordValidatorsService } from '@app/shared/forms/form-validators/reset-password-validators.service' |
@@ -22,7 +21,7 @@ export class ResetPasswordComponent extends FormReactive implements OnInit { | |||
22 | private resetPasswordValidatorsService: ResetPasswordValidatorsService, | 21 | private resetPasswordValidatorsService: ResetPasswordValidatorsService, |
23 | private userValidatorsService: UserValidatorsService, | 22 | private userValidatorsService: UserValidatorsService, |
24 | private userService: UserService, | 23 | private userService: UserService, |
25 | private notificationsService: NotificationsService, | 24 | private notifier: Notifier, |
26 | private router: Router, | 25 | private router: Router, |
27 | private route: ActivatedRoute, | 26 | private route: ActivatedRoute, |
28 | private i18n: I18n | 27 | private i18n: I18n |
@@ -40,7 +39,7 @@ export class ResetPasswordComponent extends FormReactive implements OnInit { | |||
40 | this.verificationString = this.route.snapshot.queryParams['verificationString'] | 39 | this.verificationString = this.route.snapshot.queryParams['verificationString'] |
41 | 40 | ||
42 | if (!this.userId || !this.verificationString) { | 41 | if (!this.userId || !this.verificationString) { |
43 | this.notificationsService.error(this.i18n('Error'), this.i18n('Unable to find user id or verification string.')) | 42 | this.notifier.error(this.i18n('Unable to find user id or verification string.')) |
44 | this.router.navigate([ '/' ]) | 43 | this.router.navigate([ '/' ]) |
45 | } | 44 | } |
46 | } | 45 | } |
@@ -49,11 +48,11 @@ export class ResetPasswordComponent extends FormReactive implements OnInit { | |||
49 | this.userService.resetPassword(this.userId, this.verificationString, this.form.value.password) | 48 | this.userService.resetPassword(this.userId, this.verificationString, this.form.value.password) |
50 | .subscribe( | 49 | .subscribe( |
51 | () => { | 50 | () => { |
52 | this.notificationsService.success(this.i18n('Success'), this.i18n('Your password has been successfully reset!')) | 51 | this.notifier.success(this.i18n('Your password has been successfully reset!')) |
53 | this.router.navigate([ '/login' ]) | 52 | this.router.navigate([ '/login' ]) |
54 | }, | 53 | }, |
55 | 54 | ||
56 | err => this.notificationsService.error('Error', err.message) | 55 | err => this.notifier.error(err.message) |
57 | ) | 56 | ) |
58 | } | 57 | } |
59 | 58 | ||
diff --git a/client/src/app/search/search-filters.component.ts b/client/src/app/search/search-filters.component.ts index 8d7f84ac1..3fdc6df35 100644 --- a/client/src/app/search/search-filters.component.ts +++ b/client/src/app/search/search-filters.component.ts | |||
@@ -1,10 +1,6 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
2 | import { ActivatedRoute } from '@angular/router' | 2 | import { ServerService } from '@app/core' |
3 | import { RedirectService, ServerService } from '@app/core' | ||
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { SearchService } from '@app/search/search.service' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { MetaService } from '@ngx-meta/core' | ||
8 | import { AdvancedSearch } from '@app/search/advanced-search.model' | 4 | import { AdvancedSearch } from '@app/search/advanced-search.model' |
9 | import { VideoConstant } from '../../../../shared' | 5 | import { VideoConstant } from '../../../../shared' |
10 | 6 | ||
diff --git a/client/src/app/search/search.component.html b/client/src/app/search/search.component.html index 3a87ea1de..82a5f0f26 100644 --- a/client/src/app/search/search.component.html +++ b/client/src/app/search/search.component.html | |||
@@ -48,7 +48,7 @@ | |||
48 | </div> | 48 | </div> |
49 | 49 | ||
50 | <div *ngIf="isVideo(result)" class="entry video"> | 50 | <div *ngIf="isVideo(result)" class="entry video"> |
51 | <my-video-thumbnail [video]="result"></my-video-thumbnail> | 51 | <my-video-thumbnail [video]="result" [nsfw]="isVideoBlur(result)"></my-video-thumbnail> |
52 | 52 | ||
53 | <div class="video-info"> | 53 | <div class="video-info"> |
54 | <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', result.uuid]" [attr.title]="result.name">{{ result.name }}</a> | 54 | <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', result.uuid]" [attr.title]="result.name">{{ result.name }}</a> |
diff --git a/client/src/app/search/search.component.scss b/client/src/app/search/search.component.scss index 3e074621b..6de13d276 100644 --- a/client/src/app/search/search.component.scss +++ b/client/src/app/search/search.component.scss | |||
@@ -87,10 +87,10 @@ | |||
87 | text-overflow: ellipsis; | 87 | text-overflow: ellipsis; |
88 | white-space: nowrap; | 88 | white-space: nowrap; |
89 | font-size: 14px; | 89 | font-size: 14px; |
90 | color: #585858; | 90 | color: $grey-foreground-color; |
91 | 91 | ||
92 | &:hover { | 92 | &:hover { |
93 | color: #303030; | 93 | color: $grey-foreground-hover-color; |
94 | } | 94 | } |
95 | } | 95 | } |
96 | } | 96 | } |
diff --git a/client/src/app/search/search.component.ts b/client/src/app/search/search.component.ts index 911d56843..c4a4b1fde 100644 --- a/client/src/app/search/search.component.ts +++ b/client/src/app/search/search.component.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { AuthService, RedirectService } from '@app/core' | 3 | import { AuthService, Notifier, ServerService } from '@app/core' |
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { forkJoin, Subscription } from 'rxjs' | 4 | import { forkJoin, Subscription } from 'rxjs' |
6 | import { SearchService } from '@app/search/search.service' | 5 | import { SearchService } from '@app/search/search.service' |
7 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 6 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
@@ -40,10 +39,10 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
40 | private route: ActivatedRoute, | 39 | private route: ActivatedRoute, |
41 | private router: Router, | 40 | private router: Router, |
42 | private metaService: MetaService, | 41 | private metaService: MetaService, |
43 | private redirectService: RedirectService, | 42 | private notifier: Notifier, |
44 | private notificationsService: NotificationsService, | ||
45 | private searchService: SearchService, | 43 | private searchService: SearchService, |
46 | private authService: AuthService | 44 | private authService: AuthService, |
45 | private serverService: ServerService | ||
47 | ) { } | 46 | ) { } |
48 | 47 | ||
49 | ngOnInit () { | 48 | ngOnInit () { |
@@ -69,7 +68,7 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
69 | this.search() | 68 | this.search() |
70 | }, | 69 | }, |
71 | 70 | ||
72 | err => this.notificationsService.error('Error', err.text) | 71 | err => this.notifier.error(err.text) |
73 | ) | 72 | ) |
74 | } | 73 | } |
75 | 74 | ||
@@ -77,6 +76,10 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
77 | if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe() | 76 | if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe() |
78 | } | 77 | } |
79 | 78 | ||
79 | isVideoBlur (video: Video) { | ||
80 | return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig()) | ||
81 | } | ||
82 | |||
80 | isVideoChannel (d: VideoChannel | Video): d is VideoChannel { | 83 | isVideoChannel (d: VideoChannel | Video): d is VideoChannel { |
81 | return d instanceof VideoChannel | 84 | return d instanceof VideoChannel |
82 | } | 85 | } |
@@ -113,9 +116,7 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
113 | this.firstSearch = false | 116 | this.firstSearch = false |
114 | }, | 117 | }, |
115 | 118 | ||
116 | error => { | 119 | err => this.notifier.error(err.message) |
117 | this.notificationsService.error(this.i18n('Error'), error.message) | ||
118 | } | ||
119 | ) | 120 | ) |
120 | 121 | ||
121 | } | 122 | } |
@@ -147,7 +148,8 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
147 | } | 148 | } |
148 | 149 | ||
149 | private updateTitle () { | 150 | private updateTitle () { |
150 | this.metaService.setTitle(this.i18n('Search') + ' ' + this.currentSearch) | 151 | const suffix = this.currentSearch ? ' ' + this.currentSearch : '' |
152 | this.metaService.setTitle(this.i18n('Search') + suffix) | ||
151 | } | 153 | } |
152 | 154 | ||
153 | private updateUrlFromAdvancedSearch () { | 155 | private updateUrlFromAdvancedSearch () { |
diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts index 42f2cfeaf..c5cd2051c 100644 --- a/client/src/app/shared/account/account.model.ts +++ b/client/src/app/shared/account/account.model.ts | |||
@@ -5,6 +5,10 @@ export class Account extends Actor implements ServerAccount { | |||
5 | displayName: string | 5 | displayName: string |
6 | description: string | 6 | description: string |
7 | nameWithHost: string | 7 | nameWithHost: string |
8 | mutedByUser: boolean | ||
9 | mutedByInstance: boolean | ||
10 | mutedServerByUser: boolean | ||
11 | mutedServerByInstance: boolean | ||
8 | 12 | ||
9 | userId?: number | 13 | userId?: number |
10 | 14 | ||
@@ -15,5 +19,10 @@ export class Account extends Actor implements ServerAccount { | |||
15 | this.description = hash.description | 19 | this.description = hash.description |
16 | this.userId = hash.userId | 20 | this.userId = hash.userId |
17 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) | 21 | this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) |
22 | |||
23 | this.mutedByUser = false | ||
24 | this.mutedByInstance = false | ||
25 | this.mutedServerByUser = false | ||
26 | this.mutedServerByInstance = false | ||
18 | } | 27 | } |
19 | } | 28 | } |
diff --git a/client/src/app/shared/actor/actor.model.ts b/client/src/app/shared/actor/actor.model.ts index 811afb449..adecec1fc 100644 --- a/client/src/app/shared/actor/actor.model.ts +++ b/client/src/app/shared/actor/actor.model.ts | |||
@@ -16,7 +16,7 @@ export abstract class Actor implements ActorServer { | |||
16 | 16 | ||
17 | avatarUrl: string | 17 | avatarUrl: string |
18 | 18 | ||
19 | static GET_ACTOR_AVATAR_URL (actor: { avatar: Avatar }) { | 19 | static GET_ACTOR_AVATAR_URL (actor: { avatar?: { path: string } }) { |
20 | const absoluteAPIUrl = getAbsoluteAPIUrl() | 20 | const absoluteAPIUrl = getAbsoluteAPIUrl() |
21 | 21 | ||
22 | if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path | 22 | if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path |
diff --git a/client/src/app/shared/blocklist/account-block.model.ts b/client/src/app/shared/blocklist/account-block.model.ts new file mode 100644 index 000000000..e7b433d88 --- /dev/null +++ b/client/src/app/shared/blocklist/account-block.model.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import { AccountBlock as AccountBlockServer } from '../../../../../shared' | ||
2 | import { Account } from '../account/account.model' | ||
3 | |||
4 | export class AccountBlock implements AccountBlockServer { | ||
5 | byAccount: Account | ||
6 | blockedAccount: Account | ||
7 | createdAt: Date | string | ||
8 | |||
9 | constructor (block: AccountBlockServer) { | ||
10 | this.byAccount = new Account(block.byAccount) | ||
11 | this.blockedAccount = new Account(block.blockedAccount) | ||
12 | this.createdAt = block.createdAt | ||
13 | } | ||
14 | } | ||
diff --git a/client/src/app/shared/blocklist/blocklist.service.ts b/client/src/app/shared/blocklist/blocklist.service.ts new file mode 100644 index 000000000..c1f7312f0 --- /dev/null +++ b/client/src/app/shared/blocklist/blocklist.service.ts | |||
@@ -0,0 +1,135 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { environment } from '../../../environments/environment' | ||
3 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
4 | import { RestExtractor, RestPagination, RestService } from '../rest' | ||
5 | import { SortMeta } from 'primeng/api' | ||
6 | import { catchError, map } from 'rxjs/operators' | ||
7 | import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '../../../../../shared' | ||
8 | import { Account } from '@app/shared/account/account.model' | ||
9 | import { AccountBlock } from '@app/shared/blocklist/account-block.model' | ||
10 | |||
11 | @Injectable() | ||
12 | export class BlocklistService { | ||
13 | static BASE_USER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/users/me/blocklist' | ||
14 | static BASE_SERVER_BLOCKLIST_URL = environment.apiUrl + '/api/v1/server/blocklist' | ||
15 | |||
16 | constructor ( | ||
17 | private authHttp: HttpClient, | ||
18 | private restExtractor: RestExtractor, | ||
19 | private restService: RestService | ||
20 | ) { } | ||
21 | |||
22 | /*********************** User -> Account blocklist ***********************/ | ||
23 | |||
24 | getUserAccountBlocklist (pagination: RestPagination, sort: SortMeta) { | ||
25 | let params = new HttpParams() | ||
26 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
27 | |||
28 | return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', { params }) | ||
29 | .pipe( | ||
30 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | ||
31 | map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))), | ||
32 | catchError(err => this.restExtractor.handleError(err)) | ||
33 | ) | ||
34 | } | ||
35 | |||
36 | blockAccountByUser (account: Account) { | ||
37 | const body = { accountName: account.nameWithHost } | ||
38 | |||
39 | return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts', body) | ||
40 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
41 | } | ||
42 | |||
43 | unblockAccountByUser (account: Account) { | ||
44 | const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/accounts/' + account.nameWithHost | ||
45 | |||
46 | return this.authHttp.delete(path) | ||
47 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
48 | } | ||
49 | |||
50 | /*********************** User -> Server blocklist ***********************/ | ||
51 | |||
52 | getUserServerBlocklist (pagination: RestPagination, sort: SortMeta) { | ||
53 | let params = new HttpParams() | ||
54 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
55 | |||
56 | return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', { params }) | ||
57 | .pipe( | ||
58 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | ||
59 | catchError(err => this.restExtractor.handleError(err)) | ||
60 | ) | ||
61 | } | ||
62 | |||
63 | blockServerByUser (host: string) { | ||
64 | const body = { host } | ||
65 | |||
66 | return this.authHttp.post(BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers', body) | ||
67 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
68 | } | ||
69 | |||
70 | unblockServerByUser (host: string) { | ||
71 | const path = BlocklistService.BASE_USER_BLOCKLIST_URL + '/servers/' + host | ||
72 | |||
73 | return this.authHttp.delete(path) | ||
74 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
75 | } | ||
76 | |||
77 | /*********************** Instance -> Account blocklist ***********************/ | ||
78 | |||
79 | getInstanceAccountBlocklist (pagination: RestPagination, sort: SortMeta) { | ||
80 | let params = new HttpParams() | ||
81 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
82 | |||
83 | return this.authHttp.get<ResultList<AccountBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', { params }) | ||
84 | .pipe( | ||
85 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | ||
86 | map(res => this.restExtractor.applyToResultListData(res, this.formatAccountBlock.bind(this))), | ||
87 | catchError(err => this.restExtractor.handleError(err)) | ||
88 | ) | ||
89 | } | ||
90 | |||
91 | blockAccountByInstance (account: Account) { | ||
92 | const body = { accountName: account.nameWithHost } | ||
93 | |||
94 | return this.authHttp.post(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts', body) | ||
95 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
96 | } | ||
97 | |||
98 | unblockAccountByInstance (account: Account) { | ||
99 | const path = BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/accounts/' + account.nameWithHost | ||
100 | |||
101 | return this.authHttp.delete(path) | ||
102 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
103 | } | ||
104 | |||
105 | /*********************** Instance -> Server blocklist ***********************/ | ||
106 | |||
107 | getInstanceServerBlocklist (pagination: RestPagination, sort: SortMeta) { | ||
108 | let params = new HttpParams() | ||
109 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
110 | |||
111 | return this.authHttp.get<ResultList<ServerBlock>>(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', { params }) | ||
112 | .pipe( | ||
113 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | ||
114 | catchError(err => this.restExtractor.handleError(err)) | ||
115 | ) | ||
116 | } | ||
117 | |||
118 | blockServerByInstance (host: string) { | ||
119 | const body = { host } | ||
120 | |||
121 | return this.authHttp.post(BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers', body) | ||
122 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
123 | } | ||
124 | |||
125 | unblockServerByInstance (host: string) { | ||
126 | const path = BlocklistService.BASE_SERVER_BLOCKLIST_URL + '/servers/' + host | ||
127 | |||
128 | return this.authHttp.delete(path) | ||
129 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
130 | } | ||
131 | |||
132 | private formatAccountBlock (accountBlock: AccountBlockServer) { | ||
133 | return new AccountBlock(accountBlock) | ||
134 | } | ||
135 | } | ||
diff --git a/client/src/app/shared/blocklist/index.ts b/client/src/app/shared/blocklist/index.ts new file mode 100644 index 000000000..5886ca07e --- /dev/null +++ b/client/src/app/shared/blocklist/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './blocklist.service' | ||
2 | export * from './account-block.model' | ||
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html index 8110e2515..114b1d71f 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.html +++ b/client/src/app/shared/buttons/action-dropdown.component.html | |||
@@ -1,17 +1,27 @@ | |||
1 | <div class="dropdown-root" ngbDropdown [placement]="placement"> | 1 | <div class="dropdown-root" ngbDropdown [placement]="placement"> |
2 | <div class="action-button" [ngClass]="{ small: buttonSize === 'small' }" ngbDropdownToggle role="button"> | 2 | <div |
3 | <span class="icon icon-action"></span> | 3 | class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }" |
4 | ngbDropdownToggle role="button" | ||
5 | > | ||
6 | <my-global-icon *ngIf="!label" class="more-icon" iconName="more"></my-global-icon> | ||
7 | <span *ngIf="label" class="dropdown-toggle">{{ label }}</span> | ||
4 | </div> | 8 | </div> |
5 | 9 | ||
6 | <div ngbDropdownMenu class="dropdown-menu"> | 10 | <div ngbDropdownMenu class="dropdown-menu"> |
7 | <ng-container *ngFor="let action of actions"> | 11 | <ng-container *ngFor="let actions of getActions()"> |
8 | <div class="dropdown-item" *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> | 12 | |
9 | <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a> | 13 | <ng-container *ngFor="let action of actions"> |
14 | <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> | ||
15 | <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a> | ||
16 | |||
17 | <span *ngIf="!action.linkBuilder" class="custom-action dropdown-item" (click)="action.handler(entry)" role="button"> | ||
18 | {{ action.label }} | ||
19 | </span> | ||
20 | </ng-container> | ||
21 | </ng-container> | ||
22 | |||
23 | <div class="dropdown-divider"></div> | ||
10 | 24 | ||
11 | <span *ngIf="!action.linkBuilder" class="custom-action" class="dropdown-item" (click)="action.handler(entry)" role="button"> | ||
12 | {{ action.label }} | ||
13 | </span> | ||
14 | </div> | ||
15 | </ng-container> | 25 | </ng-container> |
16 | </div> | 26 | </div> |
17 | </div> \ No newline at end of file | 27 | </div> |
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss index 00f120fb8..985b2ca88 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.scss +++ b/client/src/app/shared/buttons/action-dropdown.component.scss | |||
@@ -1,9 +1,20 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .dropdown-divider:last-child { | ||
5 | display: none; | ||
6 | } | ||
7 | |||
4 | .action-button { | 8 | .action-button { |
5 | @include peertube-button; | 9 | @include peertube-button; |
6 | @include grey-button; | 10 | |
11 | &.grey { | ||
12 | @include grey-button; | ||
13 | } | ||
14 | |||
15 | &.orange { | ||
16 | @include orange-button; | ||
17 | } | ||
7 | 18 | ||
8 | display: inline-block; | 19 | display: inline-block; |
9 | padding: 0 10px; | 20 | padding: 0 10px; |
@@ -13,14 +24,11 @@ | |||
13 | } | 24 | } |
14 | 25 | ||
15 | &:hover, &:active, &:focus { | 26 | &:hover, &:active, &:focus { |
16 | background-color: $grey-color; | 27 | background-color: $grey-background-color; |
17 | } | 28 | } |
18 | 29 | ||
19 | .icon-action { | 30 | .more-icon { |
20 | @include icon(21px); | 31 | width: 21px; |
21 | |||
22 | background-image: url('../../../assets/images/video/more.svg'); | ||
23 | top: -1px; | ||
24 | } | 32 | } |
25 | 33 | ||
26 | &.small { | 34 | &.small { |
@@ -30,9 +38,19 @@ | |||
30 | } | 38 | } |
31 | } | 39 | } |
32 | 40 | ||
41 | .dropdown-toggle::after { | ||
42 | position: relative; | ||
43 | top: 1px; | ||
44 | } | ||
45 | |||
33 | .dropdown-menu { | 46 | .dropdown-menu { |
34 | .dropdown-item { | 47 | .dropdown-item { |
35 | cursor: pointer; | 48 | cursor: pointer; |
36 | color: #000 !important; | 49 | color: #000 !important; |
50 | |||
51 | a, span { | ||
52 | display: block; | ||
53 | width: 100%; | ||
54 | } | ||
37 | } | 55 | } |
38 | } \ No newline at end of file | 56 | } |
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts index 1838ff697..275e2b51e 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.ts +++ b/client/src/app/shared/buttons/action-dropdown.component.ts | |||
@@ -2,9 +2,9 @@ import { Component, Input } from '@angular/core' | |||
2 | 2 | ||
3 | export type DropdownAction<T> = { | 3 | export type DropdownAction<T> = { |
4 | label?: string | 4 | label?: string |
5 | handler?: (T) => any | 5 | handler?: (a: T) => any |
6 | linkBuilder?: (T) => (string | number)[] | 6 | linkBuilder?: (a: T) => (string | number)[] |
7 | isDisplayed?: (T) => boolean | 7 | isDisplayed?: (a: T) => boolean |
8 | } | 8 | } |
9 | 9 | ||
10 | @Component({ | 10 | @Component({ |
@@ -14,8 +14,16 @@ export type DropdownAction<T> = { | |||
14 | }) | 14 | }) |
15 | 15 | ||
16 | export class ActionDropdownComponent<T> { | 16 | export class ActionDropdownComponent<T> { |
17 | @Input() actions: DropdownAction<T>[] = [] | 17 | @Input() actions: DropdownAction<T>[] | DropdownAction<T>[][] = [] |
18 | @Input() entry: T | 18 | @Input() entry: T |
19 | @Input() placement = 'left' | 19 | @Input() placement = 'bottom-left' |
20 | @Input() buttonSize: 'normal' | 'small' = 'normal' | 20 | @Input() buttonSize: 'normal' | 'small' = 'normal' |
21 | @Input() label: string | ||
22 | @Input() theme: 'orange' | 'grey' = 'grey' | ||
23 | |||
24 | getActions () { | ||
25 | if (this.actions.length !== 0 && Array.isArray(this.actions[0])) return this.actions | ||
26 | |||
27 | return [ this.actions ] | ||
28 | } | ||
21 | } | 29 | } |
diff --git a/client/src/app/shared/buttons/button.component.html b/client/src/app/shared/buttons/button.component.html index 87a8daccf..b6df67102 100644 --- a/client/src/app/shared/buttons/button.component.html +++ b/client/src/app/shared/buttons/button.component.html | |||
@@ -1,4 +1,4 @@ | |||
1 | <span class="action-button" [ngClass]="className" [title]="getTitle()"> | 1 | <span class="action-button" [ngClass]="className" [title]="getTitle()"> |
2 | <span class="icon" [ngClass]="icon"></span> | 2 | <my-global-icon [iconName]="icon"></my-global-icon> |
3 | <span class="button-label">{{ label }}</span> | 3 | <span class="button-label">{{ label }}</span> |
4 | </span> | 4 | </span> |
diff --git a/client/src/app/shared/buttons/button.component.scss b/client/src/app/shared/buttons/button.component.scss index 168102f09..04199a2a9 100644 --- a/client/src/app/shared/buttons/button.component.scss +++ b/client/src/app/shared/buttons/button.component.scss | |||
@@ -3,41 +3,18 @@ | |||
3 | 3 | ||
4 | .action-button { | 4 | .action-button { |
5 | @include peertube-button-link; | 5 | @include peertube-button-link; |
6 | @include button-with-icon(21px, 0, -2px); | ||
6 | 7 | ||
7 | font-size: 15px; | ||
8 | font-weight: $font-semibold; | 8 | font-weight: $font-semibold; |
9 | color: #585858; | 9 | color: $grey-foreground-color; |
10 | background-color: #E5E5E5; | 10 | background-color: $grey-background-color; |
11 | 11 | ||
12 | &:hover { | 12 | &:hover { |
13 | background-color: #EFEFEF; | 13 | background-color: $grey-background-hover-color; |
14 | } | 14 | } |
15 | 15 | ||
16 | .icon { | 16 | my-global-icon { |
17 | @include icon(21px); | 17 | @include apply-svg-color($grey-foreground-color); |
18 | |||
19 | position: relative; | ||
20 | top: -2px; | ||
21 | |||
22 | &.icon-edit { | ||
23 | background-image: url('../../../assets/images/global/edit-grey.svg'); | ||
24 | } | ||
25 | |||
26 | &.icon-delete-grey { | ||
27 | background-image: url('../../../assets/images/global/delete-grey.svg'); | ||
28 | } | ||
29 | |||
30 | &.icon-im-with-her { | ||
31 | background-image: url('../../../assets/images/global/im-with-her.svg'); | ||
32 | } | ||
33 | |||
34 | &.icon-tick { | ||
35 | background-image: url('../../../assets/images/global/tick.svg'); | ||
36 | } | ||
37 | |||
38 | &.icon-cross { | ||
39 | background-image: url('../../../assets/images/global/cross.svg'); | ||
40 | } | ||
41 | } | 18 | } |
42 | } | 19 | } |
43 | 20 | ||
diff --git a/client/src/app/shared/buttons/button.component.ts b/client/src/app/shared/buttons/button.component.ts index 967cb1409..a91e9c7eb 100644 --- a/client/src/app/shared/buttons/button.component.ts +++ b/client/src/app/shared/buttons/button.component.ts | |||
@@ -1,4 +1,5 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { GlobalIconName } from '@app/shared/icons/global-icon.component' | ||
2 | 3 | ||
3 | @Component({ | 4 | @Component({ |
4 | selector: 'my-button', | 5 | selector: 'my-button', |
@@ -8,9 +9,9 @@ import { Component, Input } from '@angular/core' | |||
8 | 9 | ||
9 | export class ButtonComponent { | 10 | export class ButtonComponent { |
10 | @Input() label = '' | 11 | @Input() label = '' |
11 | @Input() className = undefined | 12 | @Input() className: string = undefined |
12 | @Input() icon = undefined | 13 | @Input() icon: GlobalIconName = undefined |
13 | @Input() title = undefined | 14 | @Input() title: string = undefined |
14 | 15 | ||
15 | getTitle () { | 16 | getTitle () { |
16 | return this.title || this.label | 17 | return this.title || this.label |
diff --git a/client/src/app/shared/buttons/delete-button.component.html b/client/src/app/shared/buttons/delete-button.component.html index 6c55d8104..4d12a84c0 100644 --- a/client/src/app/shared/buttons/delete-button.component.html +++ b/client/src/app/shared/buttons/delete-button.component.html | |||
@@ -1,5 +1,5 @@ | |||
1 | <span class="action-button action-button-delete" [title]="getTitle()" role="button"> | 1 | <span class="action-button action-button-delete" [title]="getTitle()" role="button"> |
2 | <span class="icon icon-delete-grey"></span> | 2 | <my-global-icon iconName="delete"></my-global-icon> |
3 | 3 | ||
4 | <span class="button-label" *ngIf="label">{{ label }}</span> | 4 | <span class="button-label" *ngIf="label">{{ label }}</span> |
5 | <span class="button-label" i18n *ngIf="!label">Delete</span> | 5 | <span class="button-label" i18n *ngIf="!label">Delete</span> |
diff --git a/client/src/app/shared/buttons/edit-button.component.html b/client/src/app/shared/buttons/edit-button.component.html index cecb780f3..da3addbae 100644 --- a/client/src/app/shared/buttons/edit-button.component.html +++ b/client/src/app/shared/buttons/edit-button.component.html | |||
@@ -1,5 +1,5 @@ | |||
1 | <a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit"> | 1 | <a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit"> |
2 | <span class="icon icon-edit"></span> | 2 | <my-global-icon iconName="edit"></my-global-icon> |
3 | 3 | ||
4 | <span class="button-label" *ngIf="label">{{ label }}</span> | 4 | <span class="button-label" *ngIf="label">{{ label }}</span> |
5 | <span i18n class="button-label" *ngIf="!label">Edit</span> | 5 | <span i18n class="button-label" *ngIf="!label">Edit</span> |
diff --git a/client/src/app/shared/buttons/edit-button.component.ts b/client/src/app/shared/buttons/edit-button.component.ts index 7abaacc26..1fe4f7b30 100644 --- a/client/src/app/shared/buttons/edit-button.component.ts +++ b/client/src/app/shared/buttons/edit-button.component.ts | |||
@@ -8,5 +8,5 @@ import { Component, Input } from '@angular/core' | |||
8 | 8 | ||
9 | export class EditButtonComponent { | 9 | export class EditButtonComponent { |
10 | @Input() label: string | 10 | @Input() label: string |
11 | @Input() routerLink = [] | 11 | @Input() routerLink: string[] = [] |
12 | } | 12 | } |
diff --git a/client/src/app/core/confirm/confirm.component.html b/client/src/app/shared/confirm/confirm.component.html index 43f0c6190..65df1cd4d 100644 --- a/client/src/app/core/confirm/confirm.component.html +++ b/client/src/app/shared/confirm/confirm.component.html | |||
@@ -2,7 +2,8 @@ | |||
2 | 2 | ||
3 | <div class="modal-header"> | 3 | <div class="modal-header"> |
4 | <h4 class="modal-title">{{ title }}</h4> | 4 | <h4 class="modal-title">{{ title }}</h4> |
5 | <span class="close" aria-label="Close" role="button" (click)="dismiss()"></span> | 5 | |
6 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> | ||
6 | </div> | 7 | </div> |
7 | 8 | ||
8 | <div class="modal-body" > | 9 | <div class="modal-body" > |
diff --git a/client/src/app/core/confirm/confirm.component.scss b/client/src/app/shared/confirm/confirm.component.scss index 93dd7926b..93dd7926b 100644 --- a/client/src/app/core/confirm/confirm.component.scss +++ b/client/src/app/shared/confirm/confirm.component.scss | |||
diff --git a/client/src/app/core/confirm/confirm.component.ts b/client/src/app/shared/confirm/confirm.component.ts index 5138b7848..63c163da6 100644 --- a/client/src/app/core/confirm/confirm.component.ts +++ b/client/src/app/shared/confirm/confirm.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core' |
2 | import { ConfirmService } from './confirm.service' | 2 | import { ConfirmService } from '@app/core/confirm/confirm.service' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
diff --git a/client/src/app/shared/forms/form-reactive.ts b/client/src/app/shared/forms/form-reactive.ts index 0bb7d25e6..b9873af2c 100644 --- a/client/src/app/shared/forms/form-reactive.ts +++ b/client/src/app/shared/forms/form-reactive.ts | |||
@@ -1,11 +1,9 @@ | |||
1 | import { FormGroup } from '@angular/forms' | 1 | import { FormGroup } from '@angular/forms' |
2 | import { BuildFormArgument, BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 2 | import { BuildFormArgument, BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
3 | 3 | ||
4 | export type FormReactiveErrors = { [ id: string ]: string } | 4 | export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors } |
5 | export type FormReactiveValidationMessages = { | 5 | export type FormReactiveValidationMessages = { |
6 | [ id: string ]: { | 6 | [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages |
7 | [ name: string ]: string | ||
8 | } | ||
9 | } | 7 | } |
10 | 8 | ||
11 | export abstract class FormReactive { | 9 | export abstract class FormReactive { |
@@ -13,7 +11,7 @@ export abstract class FormReactive { | |||
13 | protected formChanged = false | 11 | protected formChanged = false |
14 | 12 | ||
15 | form: FormGroup | 13 | form: FormGroup |
16 | formErrors: FormReactiveErrors | 14 | formErrors: any // To avoid casting in template because of string | FormReactiveErrors |
17 | validationMessages: FormReactiveValidationMessages | 15 | validationMessages: FormReactiveValidationMessages |
18 | 16 | ||
19 | buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) { | 17 | buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) { |
@@ -23,29 +21,49 @@ export abstract class FormReactive { | |||
23 | this.formErrors = formErrors | 21 | this.formErrors = formErrors |
24 | this.validationMessages = validationMessages | 22 | this.validationMessages = validationMessages |
25 | 23 | ||
26 | this.form.valueChanges.subscribe(() => this.onValueChanged(false)) | 24 | this.form.valueChanges.subscribe(() => this.onValueChanged(this.form, this.formErrors, this.validationMessages, false)) |
25 | } | ||
26 | |||
27 | protected forceCheck () { | ||
28 | return this.onValueChanged(this.form, this.formErrors, this.validationMessages, true) | ||
29 | } | ||
30 | |||
31 | protected check () { | ||
32 | return this.onValueChanged(this.form, this.formErrors, this.validationMessages, false) | ||
27 | } | 33 | } |
28 | 34 | ||
29 | protected onValueChanged (forceCheck = false) { | 35 | private onValueChanged ( |
30 | for (const field in this.formErrors) { | 36 | form: FormGroup, |
37 | formErrors: FormReactiveErrors, | ||
38 | validationMessages: FormReactiveValidationMessages, | ||
39 | forceCheck = false | ||
40 | ) { | ||
41 | for (const field of Object.keys(formErrors)) { | ||
42 | if (formErrors[field] && typeof formErrors[field] === 'object') { | ||
43 | this.onValueChanged( | ||
44 | form.controls[field] as FormGroup, | ||
45 | formErrors[field] as FormReactiveErrors, | ||
46 | validationMessages[field] as FormReactiveValidationMessages, | ||
47 | forceCheck | ||
48 | ) | ||
49 | continue | ||
50 | } | ||
51 | |||
31 | // clear previous error message (if any) | 52 | // clear previous error message (if any) |
32 | this.formErrors[ field ] = '' | 53 | formErrors[ field ] = '' |
33 | const control = this.form.get(field) | 54 | const control = form.get(field) |
34 | 55 | ||
35 | if (control.dirty) this.formChanged = true | 56 | if (control.dirty) this.formChanged = true |
36 | 57 | ||
37 | // Don't care if dirty on force check | 58 | // Don't care if dirty on force check |
38 | const isDirty = control.dirty || forceCheck === true | 59 | const isDirty = control.dirty || forceCheck === true |
39 | if (control && isDirty && !control.valid) { | 60 | if (control && isDirty && !control.valid) { |
40 | const messages = this.validationMessages[ field ] | 61 | const messages = validationMessages[ field ] |
41 | for (const key in control.errors) { | 62 | for (const key in control.errors) { |
42 | this.formErrors[ field ] += messages[ key ] + ' ' | 63 | formErrors[ field ] += messages[ key ] + ' ' |
43 | } | 64 | } |
44 | } | 65 | } |
45 | } | 66 | } |
46 | } | 67 | } |
47 | 68 | ||
48 | protected forceCheck () { | ||
49 | return this.onValueChanged(true) | ||
50 | } | ||
51 | } | 69 | } |
diff --git a/client/src/app/shared/forms/form-validators/form-validator.service.ts b/client/src/app/shared/forms/form-validators/form-validator.service.ts index 19a8bef25..249fdf119 100644 --- a/client/src/app/shared/forms/form-validators/form-validator.service.ts +++ b/client/src/app/shared/forms/form-validators/form-validator.service.ts | |||
@@ -7,10 +7,10 @@ export type BuildFormValidator = { | |||
7 | MESSAGES: { [ name: string ]: string } | 7 | MESSAGES: { [ name: string ]: string } |
8 | } | 8 | } |
9 | export type BuildFormArgument = { | 9 | export type BuildFormArgument = { |
10 | [ id: string ]: BuildFormValidator | 10 | [ id: string ]: BuildFormValidator | BuildFormArgument |
11 | } | 11 | } |
12 | export type BuildFormDefaultValues = { | 12 | export type BuildFormDefaultValues = { |
13 | [ name: string ]: string | string[] | 13 | [ name: string ]: string | string[] | BuildFormDefaultValues |
14 | } | 14 | } |
15 | 15 | ||
16 | @Injectable() | 16 | @Injectable() |
@@ -29,7 +29,16 @@ export class FormValidatorService { | |||
29 | formErrors[name] = '' | 29 | formErrors[name] = '' |
30 | 30 | ||
31 | const field = obj[name] | 31 | const field = obj[name] |
32 | if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES | 32 | if (this.isRecursiveField(field)) { |
33 | const result = this.buildForm(field as BuildFormArgument, defaultValues[name] as BuildFormDefaultValues) | ||
34 | group[name] = result.form | ||
35 | formErrors[name] = result.formErrors | ||
36 | validationMessages[name] = result.validationMessages | ||
37 | |||
38 | continue | ||
39 | } | ||
40 | |||
41 | if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string } | ||
33 | 42 | ||
34 | const defaultValue = defaultValues[name] || '' | 43 | const defaultValue = defaultValues[name] || '' |
35 | 44 | ||
@@ -52,13 +61,27 @@ export class FormValidatorService { | |||
52 | formErrors[name] = '' | 61 | formErrors[name] = '' |
53 | 62 | ||
54 | const field = obj[name] | 63 | const field = obj[name] |
55 | if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES | 64 | if (this.isRecursiveField(field)) { |
65 | this.updateForm( | ||
66 | form[name], | ||
67 | formErrors[name] as FormReactiveErrors, | ||
68 | validationMessages[name] as FormReactiveValidationMessages, | ||
69 | obj[name] as BuildFormArgument, | ||
70 | defaultValues[name] as BuildFormDefaultValues | ||
71 | ) | ||
72 | continue | ||
73 | } | ||
74 | |||
75 | if (field && field.MESSAGES) validationMessages[name] = field.MESSAGES as { [ name: string ]: string } | ||
56 | 76 | ||
57 | const defaultValue = defaultValues[name] || '' | 77 | const defaultValue = defaultValues[name] || '' |
58 | 78 | ||
59 | if (field && field.VALIDATORS) form.addControl(name, new FormControl(defaultValue, field.VALIDATORS)) | 79 | if (field && field.VALIDATORS) form.addControl(name, new FormControl(defaultValue, field.VALIDATORS as ValidatorFn[])) |
60 | else form.addControl(name, new FormControl(defaultValue)) | 80 | else form.addControl(name, new FormControl(defaultValue)) |
61 | } | 81 | } |
62 | } | 82 | } |
63 | 83 | ||
84 | private isRecursiveField (field: any) { | ||
85 | return field && typeof field === 'object' && !field.MESSAGES && !field.VALIDATORS | ||
86 | } | ||
64 | } | 87 | } |
diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts index 74e385b3d..fdcbedb71 100644 --- a/client/src/app/shared/forms/form-validators/index.ts +++ b/client/src/app/shared/forms/form-validators/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | export * from './custom-config-validators.service' | 1 | export * from './custom-config-validators.service' |
2 | export * from './form-validator.service' | 2 | export * from './form-validator.service' |
3 | export * from './host' | 3 | export * from './host' |
4 | export * from './instance-validators.service' | ||
4 | export * from './login-validators.service' | 5 | export * from './login-validators.service' |
5 | export * from './reset-password-validators.service' | 6 | export * from './reset-password-validators.service' |
6 | export * from './user-validators.service' | 7 | export * from './user-validators.service' |
diff --git a/client/src/app/shared/forms/form-validators/instance-validators.service.ts b/client/src/app/shared/forms/form-validators/instance-validators.service.ts new file mode 100644 index 000000000..5bb852858 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/instance-validators.service.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
2 | import { Validators } from '@angular/forms' | ||
3 | import { BuildFormValidator } from '@app/shared' | ||
4 | import { Injectable } from '@angular/core' | ||
5 | |||
6 | @Injectable() | ||
7 | export class InstanceValidatorsService { | ||
8 | readonly FROM_EMAIL: BuildFormValidator | ||
9 | readonly FROM_NAME: BuildFormValidator | ||
10 | readonly BODY: BuildFormValidator | ||
11 | |||
12 | constructor (private i18n: I18n) { | ||
13 | |||
14 | this.FROM_EMAIL = { | ||
15 | VALIDATORS: [ Validators.required, Validators.email ], | ||
16 | MESSAGES: { | ||
17 | 'required': this.i18n('Email is required.'), | ||
18 | 'email': this.i18n('Email must be valid.') | ||
19 | } | ||
20 | } | ||
21 | |||
22 | this.FROM_NAME = { | ||
23 | VALIDATORS: [ | ||
24 | Validators.required, | ||
25 | Validators.minLength(1), | ||
26 | Validators.maxLength(120) | ||
27 | ], | ||
28 | MESSAGES: { | ||
29 | 'required': this.i18n('Your name is required.'), | ||
30 | 'minlength': this.i18n('Your name must be at least 1 character long.'), | ||
31 | 'maxlength': this.i18n('Your name cannot be more than 120 characters long.') | ||
32 | } | ||
33 | } | ||
34 | |||
35 | this.BODY = { | ||
36 | VALIDATORS: [ | ||
37 | Validators.required, | ||
38 | Validators.minLength(3), | ||
39 | Validators.maxLength(5000) | ||
40 | ], | ||
41 | MESSAGES: { | ||
42 | 'required': this.i18n('A message is required.'), | ||
43 | 'minlength': this.i18n('The message must be at least 3 characters long.'), | ||
44 | 'maxlength': this.i18n('The message cannot be more than 5000 characters long.') | ||
45 | } | ||
46 | } | ||
47 | } | ||
48 | } | ||
diff --git a/client/src/app/shared/forms/form-validators/user-validators.service.ts b/client/src/app/shared/forms/form-validators/user-validators.service.ts index 1fd1cdf68..6589b2580 100644 --- a/client/src/app/shared/forms/form-validators/user-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/user-validators.service.ts | |||
@@ -23,15 +23,15 @@ export class UserValidatorsService { | |||
23 | this.USER_USERNAME = { | 23 | this.USER_USERNAME = { |
24 | VALIDATORS: [ | 24 | VALIDATORS: [ |
25 | Validators.required, | 25 | Validators.required, |
26 | Validators.minLength(3), | 26 | Validators.minLength(1), |
27 | Validators.maxLength(20), | 27 | Validators.maxLength(50), |
28 | Validators.pattern(/^[a-z0-9._]+$/) | 28 | Validators.pattern(/^[a-z0-9][a-z0-9._]*$/) |
29 | ], | 29 | ], |
30 | MESSAGES: { | 30 | MESSAGES: { |
31 | 'required': this.i18n('Username is required.'), | 31 | 'required': this.i18n('Username is required.'), |
32 | 'minlength': this.i18n('Username must be at least 3 characters long.'), | 32 | 'minlength': this.i18n('Username must be at least 1 character long.'), |
33 | 'maxlength': this.i18n('Username cannot be more than 20 characters long.'), | 33 | 'maxlength': this.i18n('Username cannot be more than 50 characters long.'), |
34 | 'pattern': this.i18n('Username should be only lowercase alphanumeric characters.') | 34 | 'pattern': this.i18n('Username should be lowercase alphanumeric; dots and underscores are allowed.') |
35 | } | 35 | } |
36 | } | 36 | } |
37 | 37 | ||
@@ -88,24 +88,24 @@ export class UserValidatorsService { | |||
88 | this.USER_DISPLAY_NAME = { | 88 | this.USER_DISPLAY_NAME = { |
89 | VALIDATORS: [ | 89 | VALIDATORS: [ |
90 | Validators.required, | 90 | Validators.required, |
91 | Validators.minLength(3), | 91 | Validators.minLength(1), |
92 | Validators.maxLength(120) | 92 | Validators.maxLength(50) |
93 | ], | 93 | ], |
94 | MESSAGES: { | 94 | MESSAGES: { |
95 | 'required': this.i18n('Display name is required.'), | 95 | 'required': this.i18n('Display name is required.'), |
96 | 'minlength': this.i18n('Display name must be at least 3 characters long.'), | 96 | 'minlength': this.i18n('Display name must be at least 1 character long.'), |
97 | 'maxlength': this.i18n('Display name cannot be more than 120 characters long.') | 97 | 'maxlength': this.i18n('Display name cannot be more than 50 characters long.') |
98 | } | 98 | } |
99 | } | 99 | } |
100 | 100 | ||
101 | this.USER_DESCRIPTION = { | 101 | this.USER_DESCRIPTION = { |
102 | VALIDATORS: [ | 102 | VALIDATORS: [ |
103 | Validators.minLength(3), | 103 | Validators.minLength(3), |
104 | Validators.maxLength(250) | 104 | Validators.maxLength(1000) |
105 | ], | 105 | ], |
106 | MESSAGES: { | 106 | MESSAGES: { |
107 | 'minlength': this.i18n('Description must be at least 3 characters long.'), | 107 | 'minlength': this.i18n('Description must be at least 3 characters long.'), |
108 | 'maxlength': this.i18n('Description cannot be more than 250 characters long.') | 108 | 'maxlength': this.i18n('Description cannot be more than 1000 characters long.') |
109 | } | 109 | } |
110 | } | 110 | } |
111 | 111 | ||
diff --git a/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts b/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts index 6e9806611..fcc966b84 100644 --- a/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/video-abuse-validators.service.ts | |||
@@ -10,20 +10,20 @@ export class VideoAbuseValidatorsService { | |||
10 | 10 | ||
11 | constructor (private i18n: I18n) { | 11 | constructor (private i18n: I18n) { |
12 | this.VIDEO_ABUSE_REASON = { | 12 | this.VIDEO_ABUSE_REASON = { |
13 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], | 13 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], |
14 | MESSAGES: { | 14 | MESSAGES: { |
15 | 'required': this.i18n('Report reason is required.'), | 15 | 'required': this.i18n('Report reason is required.'), |
16 | 'minlength': this.i18n('Report reason must be at least 2 characters long.'), | 16 | 'minlength': this.i18n('Report reason must be at least 2 characters long.'), |
17 | 'maxlength': this.i18n('Report reason cannot be more than 300 characters long.') | 17 | 'maxlength': this.i18n('Report reason cannot be more than 3000 characters long.') |
18 | } | 18 | } |
19 | } | 19 | } |
20 | 20 | ||
21 | this.VIDEO_ABUSE_MODERATION_COMMENT = { | 21 | this.VIDEO_ABUSE_MODERATION_COMMENT = { |
22 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ], | 22 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], |
23 | MESSAGES: { | 23 | MESSAGES: { |
24 | 'required': this.i18n('Moderation comment is required.'), | 24 | 'required': this.i18n('Moderation comment is required.'), |
25 | 'minlength': this.i18n('Moderation comment must be at least 2 characters long.'), | 25 | 'minlength': this.i18n('Moderation comment must be at least 2 characters long.'), |
26 | 'maxlength': this.i18n('Moderation comment cannot be more than 300 characters long.') | 26 | 'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.') |
27 | } | 27 | } |
28 | } | 28 | } |
29 | } | 29 | } |
diff --git a/client/src/app/shared/forms/form-validators/video-change-ownership-validators.service.ts b/client/src/app/shared/forms/form-validators/video-change-ownership-validators.service.ts index 087b80b44..c6fbb7538 100644 --- a/client/src/app/shared/forms/form-validators/video-change-ownership-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/video-change-ownership-validators.service.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { I18n } from '@ngx-translate/i18n-polyfill' | 1 | import { I18n } from '@ngx-translate/i18n-polyfill' |
2 | import { Validators } from '@angular/forms' | 2 | import { AbstractControl, ValidationErrors, Validators } from '@angular/forms' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { BuildFormValidator } from '@app/shared' | 4 | import { BuildFormValidator } from '@app/shared' |
5 | 5 | ||
@@ -9,10 +9,19 @@ export class VideoChangeOwnershipValidatorsService { | |||
9 | 9 | ||
10 | constructor (private i18n: I18n) { | 10 | constructor (private i18n: I18n) { |
11 | this.USERNAME = { | 11 | this.USERNAME = { |
12 | VALIDATORS: [ Validators.required ], | 12 | VALIDATORS: [ Validators.required, this.localAccountValidator ], |
13 | MESSAGES: { | 13 | MESSAGES: { |
14 | 'required': this.i18n('The username is required.') | 14 | 'required': this.i18n('The username is required.'), |
15 | 'localAccountOnly': this.i18n('You can only transfer ownership to a local account') | ||
15 | } | 16 | } |
16 | } | 17 | } |
17 | } | 18 | } |
19 | |||
20 | localAccountValidator (control: AbstractControl): ValidationErrors { | ||
21 | if (control.value.includes('@')) { | ||
22 | return { 'localAccountOnly': true } | ||
23 | } | ||
24 | |||
25 | return null | ||
26 | } | ||
18 | } | 27 | } |
diff --git a/client/src/app/shared/forms/form-validators/video-channel-validators.service.ts b/client/src/app/shared/forms/form-validators/video-channel-validators.service.ts index 1ce3a0dca..1c519c10a 100644 --- a/client/src/app/shared/forms/form-validators/video-channel-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/video-channel-validators.service.ts | |||
@@ -14,50 +14,50 @@ export class VideoChannelValidatorsService { | |||
14 | this.VIDEO_CHANNEL_NAME = { | 14 | this.VIDEO_CHANNEL_NAME = { |
15 | VALIDATORS: [ | 15 | VALIDATORS: [ |
16 | Validators.required, | 16 | Validators.required, |
17 | Validators.minLength(3), | 17 | Validators.minLength(1), |
18 | Validators.maxLength(20), | 18 | Validators.maxLength(50), |
19 | Validators.pattern(/^[a-z0-9._]+$/) | 19 | Validators.pattern(/^[a-z0-9][a-z0-9._]*$/) |
20 | ], | 20 | ], |
21 | MESSAGES: { | 21 | MESSAGES: { |
22 | 'required': this.i18n('Name is required.'), | 22 | 'required': this.i18n('Name is required.'), |
23 | 'minlength': this.i18n('Name must be at least 3 characters long.'), | 23 | 'minlength': this.i18n('Name must be at least 1 character long.'), |
24 | 'maxlength': this.i18n('Name cannot be more than 20 characters long.'), | 24 | 'maxlength': this.i18n('Name cannot be more than 50 characters long.'), |
25 | 'pattern': this.i18n('Name should be only lowercase alphanumeric characters.') | 25 | 'pattern': this.i18n('Name should be lowercase alphanumeric; dots and underscores are allowed.') |
26 | } | 26 | } |
27 | } | 27 | } |
28 | 28 | ||
29 | this.VIDEO_CHANNEL_DISPLAY_NAME = { | 29 | this.VIDEO_CHANNEL_DISPLAY_NAME = { |
30 | VALIDATORS: [ | 30 | VALIDATORS: [ |
31 | Validators.required, | 31 | Validators.required, |
32 | Validators.minLength(3), | 32 | Validators.minLength(1), |
33 | Validators.maxLength(120) | 33 | Validators.maxLength(50) |
34 | ], | 34 | ], |
35 | MESSAGES: { | 35 | MESSAGES: { |
36 | 'required': i18n('Display name is required.'), | 36 | 'required': i18n('Display name is required.'), |
37 | 'minlength': i18n('Display name must be at least 3 characters long.'), | 37 | 'minlength': i18n('Display name must be at least 1 character long.'), |
38 | 'maxlength': i18n('Display name cannot be more than 120 characters long.') | 38 | 'maxlength': i18n('Display name cannot be more than 50 characters long.') |
39 | } | 39 | } |
40 | } | 40 | } |
41 | 41 | ||
42 | this.VIDEO_CHANNEL_DESCRIPTION = { | 42 | this.VIDEO_CHANNEL_DESCRIPTION = { |
43 | VALIDATORS: [ | 43 | VALIDATORS: [ |
44 | Validators.minLength(3), | 44 | Validators.minLength(3), |
45 | Validators.maxLength(500) | 45 | Validators.maxLength(1000) |
46 | ], | 46 | ], |
47 | MESSAGES: { | 47 | MESSAGES: { |
48 | 'minlength': i18n('Description must be at least 3 characters long.'), | 48 | 'minlength': i18n('Description must be at least 3 characters long.'), |
49 | 'maxlength': i18n('Description cannot be more than 500 characters long.') | 49 | 'maxlength': i18n('Description cannot be more than 1000 characters long.') |
50 | } | 50 | } |
51 | } | 51 | } |
52 | 52 | ||
53 | this.VIDEO_CHANNEL_SUPPORT = { | 53 | this.VIDEO_CHANNEL_SUPPORT = { |
54 | VALIDATORS: [ | 54 | VALIDATORS: [ |
55 | Validators.minLength(3), | 55 | Validators.minLength(3), |
56 | Validators.maxLength(500) | 56 | Validators.maxLength(1000) |
57 | ], | 57 | ], |
58 | MESSAGES: { | 58 | MESSAGES: { |
59 | 'minlength': i18n('Support text must be at least 3 characters long.'), | 59 | 'minlength': i18n('Support text must be at least 3 characters long.'), |
60 | 'maxlength': i18n('Support text cannot be more than 500 characters long.') | 60 | 'maxlength': i18n('Support text cannot be more than 1000 characters long.') |
61 | } | 61 | } |
62 | } | 62 | } |
63 | } | 63 | } |
diff --git a/client/src/app/shared/forms/form-validators/video-validators.service.ts b/client/src/app/shared/forms/form-validators/video-validators.service.ts index 396be6f3b..81ed0666f 100644 --- a/client/src/app/shared/forms/form-validators/video-validators.service.ts +++ b/client/src/app/shared/forms/form-validators/video-validators.service.ts | |||
@@ -79,10 +79,10 @@ export class VideoValidatorsService { | |||
79 | } | 79 | } |
80 | 80 | ||
81 | this.VIDEO_SUPPORT = { | 81 | this.VIDEO_SUPPORT = { |
82 | VALIDATORS: [ Validators.minLength(3), Validators.maxLength(500) ], | 82 | VALIDATORS: [ Validators.minLength(3), Validators.maxLength(1000) ], |
83 | MESSAGES: { | 83 | MESSAGES: { |
84 | 'minlength': this.i18n('Video support must be at least 3 characters long.'), | 84 | 'minlength': this.i18n('Video support must be at least 3 characters long.'), |
85 | 'maxlength': this.i18n('Video support cannot be more than 500 characters long.') | 85 | 'maxlength': this.i18n('Video support cannot be more than 1000 characters long.') |
86 | } | 86 | } |
87 | } | 87 | } |
88 | 88 | ||
diff --git a/client/src/app/shared/forms/index.ts b/client/src/app/shared/forms/index.ts index 41c321c4c..8febbfee9 100644 --- a/client/src/app/shared/forms/index.ts +++ b/client/src/app/shared/forms/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './form-validators' | 1 | export * from './form-validators' |
2 | export * from './form-reactive' | 2 | export * from './form-reactive' |
3 | export * from './reactive-file.component' | 3 | export * from './reactive-file.component' |
4 | export * from './textarea-autoresize.directive' | ||
diff --git a/client/src/app/shared/forms/markdown-textarea.component.ts b/client/src/app/shared/forms/markdown-textarea.component.ts index b99169ed2..e87aca0d4 100644 --- a/client/src/app/shared/forms/markdown-textarea.component.ts +++ b/client/src/app/shared/forms/markdown-textarea.component.ts | |||
@@ -1,10 +1,10 @@ | |||
1 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | 1 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' |
2 | import { Component, forwardRef, Input, OnInit } from '@angular/core' | 2 | import { Component, forwardRef, Input, OnInit } from '@angular/core' |
3 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 3 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
4 | import { MarkdownService } from '@app/videos/shared' | ||
5 | import { Subject } from 'rxjs' | 4 | import { Subject } from 'rxjs' |
6 | import truncate from 'lodash-es/truncate' | 5 | import truncate from 'lodash-es/truncate' |
7 | import { ScreenService } from '@app/shared/misc/screen.service' | 6 | import { ScreenService } from '@app/shared/misc/screen.service' |
7 | import { MarkdownService } from '@app/shared/renderer' | ||
8 | 8 | ||
9 | @Component({ | 9 | @Component({ |
10 | selector: 'my-markdown-textarea', | 10 | selector: 'my-markdown-textarea', |
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.html b/client/src/app/shared/forms/peertube-checkbox.component.html index 38691f050..7b8bcf601 100644 --- a/client/src/app/shared/forms/peertube-checkbox.component.html +++ b/client/src/app/shared/forms/peertube-checkbox.component.html | |||
@@ -1,10 +1,10 @@ | |||
1 | <div class="form-group"> | 1 | <div class="root"> |
2 | <label class="form-group-checkbox"> | 2 | <label class="form-group-checkbox"> |
3 | <input type="checkbox" [(ngModel)]="checked" (ngModelChange)="onModelChange()" [id]="inputName" [disabled]="isDisabled" /> | 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 | <span *ngIf="labelHtml" [innerHTML]="labelHtml"></span> |
7 | </label> | 7 | </label> |
8 | 8 | ||
9 | <my-help *ngIf="helpHtml" tooltipPlacement="top" helpType="custom" i18n-customHtml [customHtml]="helpHtml"></my-help> | 9 | <my-help *ngIf="helpHtml" tooltipPlacement="top" helpType="custom" i18n-customHtml [customHtml]="helpHtml"></my-help> |
10 | </div> \ No newline at end of file | 10 | </div> |
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.scss b/client/src/app/shared/forms/peertube-checkbox.component.scss index ee133f190..6e4e20775 100644 --- a/client/src/app/shared/forms/peertube-checkbox.component.scss +++ b/client/src/app/shared/forms/peertube-checkbox.component.scss | |||
@@ -1,7 +1,7 @@ | |||
1 | @import '_variables'; | 1 | @import '_variables'; |
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .form-group { | 4 | .root { |
5 | display: flex; | 5 | display: flex; |
6 | 6 | ||
7 | .form-group-checkbox { | 7 | .form-group-checkbox { |
@@ -20,6 +20,10 @@ | |||
20 | } | 20 | } |
21 | } | 21 | } |
22 | 22 | ||
23 | label { | ||
24 | margin-bottom: 0; | ||
25 | } | ||
26 | |||
23 | my-help { | 27 | my-help { |
24 | position: relative; | 28 | position: relative; |
25 | top: -2px; | 29 | top: -2px; |
diff --git a/client/src/app/shared/forms/peertube-checkbox.component.ts b/client/src/app/shared/forms/peertube-checkbox.component.ts index bbc9904df..c1a6915e8 100644 --- a/client/src/app/shared/forms/peertube-checkbox.component.ts +++ b/client/src/app/shared/forms/peertube-checkbox.component.ts | |||
@@ -19,8 +19,7 @@ export class PeertubeCheckboxComponent implements ControlValueAccessor { | |||
19 | @Input() labelText: string | 19 | @Input() labelText: string |
20 | @Input() labelHtml: string | 20 | @Input() labelHtml: string |
21 | @Input() helpHtml: string | 21 | @Input() helpHtml: string |
22 | 22 | @Input() disabled = false | |
23 | isDisabled = false | ||
24 | 23 | ||
25 | propagateChange = (_: any) => { /* empty */ } | 24 | propagateChange = (_: any) => { /* empty */ } |
26 | 25 | ||
@@ -41,6 +40,6 @@ export class PeertubeCheckboxComponent implements ControlValueAccessor { | |||
41 | } | 40 | } |
42 | 41 | ||
43 | setDisabledState (isDisabled: boolean) { | 42 | setDisabledState (isDisabled: boolean) { |
44 | this.isDisabled = isDisabled | 43 | this.disabled = isDisabled |
45 | } | 44 | } |
46 | } | 45 | } |
diff --git a/client/src/app/shared/forms/reactive-file.component.ts b/client/src/app/shared/forms/reactive-file.component.ts index 8d22aa56c..f60c38e8d 100644 --- a/client/src/app/shared/forms/reactive-file.component.ts +++ b/client/src/app/shared/forms/reactive-file.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { Notifier } from '@app/core' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | 5 | ||
6 | @Component({ | 6 | @Component({ |
@@ -30,7 +30,7 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor { | |||
30 | private file: File | 30 | private file: File |
31 | 31 | ||
32 | constructor ( | 32 | constructor ( |
33 | private notificationsService: NotificationsService, | 33 | private notifier: Notifier, |
34 | private i18n: I18n | 34 | private i18n: I18n |
35 | ) {} | 35 | ) {} |
36 | 36 | ||
@@ -49,7 +49,18 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor { | |||
49 | const [ file ] = event.target.files | 49 | const [ file ] = event.target.files |
50 | 50 | ||
51 | if (file.size > this.maxFileSize) { | 51 | if (file.size > this.maxFileSize) { |
52 | this.notificationsService.error(this.i18n('Error'), this.i18n('This file is too large.')) | 52 | this.notifier.error(this.i18n('This file is too large.')) |
53 | return | ||
54 | } | ||
55 | |||
56 | const extension = '.' + file.name.split('.').pop() | ||
57 | if (this.extensions.includes(extension) === false) { | ||
58 | const message = this.i18n( | ||
59 | 'PeerTube cannot handle this kind of file. Accepted extensions are {{extensions}}.', | ||
60 | { extensions: this.allowedExtensionsMessage } | ||
61 | ) | ||
62 | this.notifier.error(message) | ||
63 | |||
53 | return | 64 | return |
54 | } | 65 | } |
55 | 66 | ||
diff --git a/client/src/app/shared/forms/textarea-autoresize.directive.ts b/client/src/app/shared/forms/textarea-autoresize.directive.ts new file mode 100644 index 000000000..f8c855c16 --- /dev/null +++ b/client/src/app/shared/forms/textarea-autoresize.directive.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | // Thanks: https://github.com/evseevdev/ngx-textarea-autosize | ||
2 | import { AfterViewInit, Directive, ElementRef, HostBinding, HostListener } from '@angular/core' | ||
3 | |||
4 | @Directive({ | ||
5 | selector: 'textarea[myAutoResize]' | ||
6 | }) | ||
7 | export class TextareaAutoResizeDirective implements AfterViewInit { | ||
8 | @HostBinding('attr.rows') rows = '1' | ||
9 | @HostBinding('style.overflow') overflow = 'hidden' | ||
10 | |||
11 | constructor (private elem: ElementRef) { } | ||
12 | |||
13 | public ngAfterViewInit () { | ||
14 | this.resize() | ||
15 | } | ||
16 | |||
17 | @HostListener('input') | ||
18 | resize () { | ||
19 | const textarea = this.elem.nativeElement as HTMLTextAreaElement | ||
20 | // Reset textarea height to auto that correctly calculate the new height | ||
21 | textarea.style.height = 'auto' | ||
22 | // Set new height | ||
23 | textarea.style.height = `${textarea.scrollHeight}px` | ||
24 | } | ||
25 | } | ||
diff --git a/client/src/app/shared/guards/can-deactivate-guard.service.ts b/client/src/app/shared/guards/can-deactivate-guard.service.ts index e2a79e8c4..3a35fcfb3 100644 --- a/client/src/app/shared/guards/can-deactivate-guard.service.ts +++ b/client/src/app/shared/guards/can-deactivate-guard.service.ts | |||
@@ -4,8 +4,10 @@ import { Observable } from 'rxjs' | |||
4 | import { ConfirmService } from '../../core/index' | 4 | import { ConfirmService } from '../../core/index' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | 6 | ||
7 | export type CanComponentDeactivateResult = { text?: string, canDeactivate: Observable<boolean> | boolean } | ||
8 | |||
7 | export interface CanComponentDeactivate { | 9 | export interface CanComponentDeactivate { |
8 | canDeactivate: () => { text?: string, canDeactivate: Observable<boolean> | boolean } | 10 | canDeactivate: () => CanComponentDeactivateResult |
9 | } | 11 | } |
10 | 12 | ||
11 | @Injectable() | 13 | @Injectable() |
diff --git a/client/src/app/shared/icons/global-icon.component.html b/client/src/app/shared/icons/global-icon.component.html new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/client/src/app/shared/icons/global-icon.component.html | |||
diff --git a/client/src/app/shared/icons/global-icon.component.scss b/client/src/app/shared/icons/global-icon.component.scss new file mode 100644 index 000000000..6805fb6f7 --- /dev/null +++ b/client/src/app/shared/icons/global-icon.component.scss | |||
@@ -0,0 +1,4 @@ | |||
1 | /deep/ svg { | ||
2 | width: inherit; | ||
3 | height: inherit; | ||
4 | } | ||
diff --git a/client/src/app/shared/icons/global-icon.component.ts b/client/src/app/shared/icons/global-icon.component.ts new file mode 100644 index 000000000..e8ada0324 --- /dev/null +++ b/client/src/app/shared/icons/global-icon.component.ts | |||
@@ -0,0 +1,48 @@ | |||
1 | import { Component, ElementRef, Input, OnInit } from '@angular/core' | ||
2 | |||
3 | const icons = { | ||
4 | 'add': require('../../../assets/images/global/add.html'), | ||
5 | 'syndication': require('../../../assets/images/global/syndication.html'), | ||
6 | 'help': require('../../../assets/images/global/help.html'), | ||
7 | 'sparkle': require('../../../assets/images/global/sparkle.html'), | ||
8 | 'alert': require('../../../assets/images/global/alert.html'), | ||
9 | 'cloud-error': require('../../../assets/images/global/cloud-error.html'), | ||
10 | 'user-add': require('../../../assets/images/global/user-add.html'), | ||
11 | 'no': require('../../../assets/images/global/no.html'), | ||
12 | 'cloud-download': require('../../../assets/images/global/cloud-download.html'), | ||
13 | 'undo': require('../../../assets/images/global/undo.html'), | ||
14 | 'circle-tick': require('../../../assets/images/global/circle-tick.html'), | ||
15 | 'cog': require('../../../assets/images/global/cog.html'), | ||
16 | 'download': require('../../../assets/images/global/download.html'), | ||
17 | 'edit': require('../../../assets/images/global/edit.html'), | ||
18 | 'im-with-her': require('../../../assets/images/global/im-with-her.html'), | ||
19 | 'delete': require('../../../assets/images/global/delete.html'), | ||
20 | 'cross': require('../../../assets/images/global/cross.html'), | ||
21 | 'validate': require('../../../assets/images/global/validate.html'), | ||
22 | 'tick': require('../../../assets/images/global/tick.html'), | ||
23 | 'dislike': require('../../../assets/images/video/dislike.html'), | ||
24 | 'heart': require('../../../assets/images/video/heart.html'), | ||
25 | 'like': require('../../../assets/images/video/like.html'), | ||
26 | 'more': require('../../../assets/images/video/more.html'), | ||
27 | 'share': require('../../../assets/images/video/share.html'), | ||
28 | 'upload': require('../../../assets/images/video/upload.html') | ||
29 | } | ||
30 | |||
31 | export type GlobalIconName = keyof typeof icons | ||
32 | |||
33 | @Component({ | ||
34 | selector: 'my-global-icon', | ||
35 | template: '', | ||
36 | styleUrls: [ './global-icon.component.scss' ] | ||
37 | }) | ||
38 | export class GlobalIconComponent implements OnInit { | ||
39 | @Input() iconName: GlobalIconName | ||
40 | |||
41 | constructor (private el: ElementRef) {} | ||
42 | |||
43 | ngOnInit () { | ||
44 | const nativeElement = this.el.nativeElement | ||
45 | |||
46 | nativeElement.innerHTML = icons[this.iconName] | ||
47 | } | ||
48 | } | ||
diff --git a/client/src/app/shared/instance/instance.service.ts b/client/src/app/shared/instance/instance.service.ts new file mode 100644 index 000000000..61321ecce --- /dev/null +++ b/client/src/app/shared/instance/instance.service.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | import { catchError } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { environment } from '../../../environments/environment' | ||
5 | import { RestExtractor, RestService } from '../rest' | ||
6 | import { About } from '../../../../../shared/models/server' | ||
7 | |||
8 | @Injectable() | ||
9 | export class InstanceService { | ||
10 | private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config' | ||
11 | private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server' | ||
12 | |||
13 | constructor ( | ||
14 | private authHttp: HttpClient, | ||
15 | private restService: RestService, | ||
16 | private restExtractor: RestExtractor | ||
17 | ) { | ||
18 | } | ||
19 | |||
20 | getAbout () { | ||
21 | return this.authHttp.get<About>(InstanceService.BASE_CONFIG_URL + '/about') | ||
22 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
23 | } | ||
24 | |||
25 | contactAdministrator (fromEmail: string, fromName: string, message: string) { | ||
26 | const body = { | ||
27 | fromEmail, | ||
28 | fromName, | ||
29 | body: message | ||
30 | } | ||
31 | |||
32 | return this.authHttp.post(InstanceService.BASE_SERVER_URL + '/contact', body) | ||
33 | .pipe(catchError(res => this.restExtractor.handleError(res))) | ||
34 | |||
35 | } | ||
36 | } | ||
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.html b/client/src/app/shared/menu/top-menu-dropdown.component.html new file mode 100644 index 000000000..d3c896019 --- /dev/null +++ b/client/src/app/shared/menu/top-menu-dropdown.component.html | |||
@@ -0,0 +1,21 @@ | |||
1 | <div class="sub-menu"> | ||
2 | <ng-container *ngFor="let menuEntry of menuEntries"> | ||
3 | |||
4 | <a *ngIf="menuEntry.routerLink" [routerLink]="menuEntry.routerLink" routerLinkActive="active" class="title-page">{{ menuEntry.label }}</a> | ||
5 | |||
6 | <div *ngIf="!menuEntry.routerLink" ngbDropdown class="parent-entry" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)"> | ||
7 | <span | ||
8 | (mouseenter)="openDropdownOnHover(dropdown)" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" ngbDropdownAnchor | ||
9 | (click)="dropdownAnchorClicked(dropdown)" role="button" class="title-page" | ||
10 | > | ||
11 | <ng-container i18n>{{ menuEntry.label }}</ng-container> | ||
12 | <ng-container *ngIf="!!suffixLabels[menuEntry.label]"> - {{ suffixLabels[menuEntry.label] }}</ng-container> | ||
13 | </span> | ||
14 | |||
15 | <div ngbDropdownMenu> | ||
16 | <a *ngFor="let menuChild of menuEntry.children" class="dropdown-item" [routerLink]="menuChild.routerLink">{{ menuChild.label }}</a> | ||
17 | </div> | ||
18 | </div> | ||
19 | |||
20 | </ng-container> | ||
21 | </div> | ||
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.scss b/client/src/app/shared/menu/top-menu-dropdown.component.scss new file mode 100644 index 000000000..77159532f --- /dev/null +++ b/client/src/app/shared/menu/top-menu-dropdown.component.scss | |||
@@ -0,0 +1,18 @@ | |||
1 | .parent-entry { | ||
2 | span[role=button] { | ||
3 | cursor: pointer; | ||
4 | } | ||
5 | |||
6 | a { | ||
7 | display: block; | ||
8 | } | ||
9 | } | ||
10 | |||
11 | /deep/ .dropdown-toggle::after { | ||
12 | position: relative; | ||
13 | top: 2px; | ||
14 | } | ||
15 | |||
16 | /deep/ .dropdown-menu { | ||
17 | margin-top: 0 !important; | ||
18 | } | ||
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.ts b/client/src/app/shared/menu/top-menu-dropdown.component.ts new file mode 100644 index 000000000..e859c30dd --- /dev/null +++ b/client/src/app/shared/menu/top-menu-dropdown.component.ts | |||
@@ -0,0 +1,83 @@ | |||
1 | import { Component, Input, OnDestroy, OnInit } from '@angular/core' | ||
2 | import { filter, take } from 'rxjs/operators' | ||
3 | import { NavigationEnd, Router } from '@angular/router' | ||
4 | import { Subscription } from 'rxjs' | ||
5 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | ||
6 | |||
7 | export type TopMenuDropdownParam = { | ||
8 | label: string | ||
9 | routerLink?: string | ||
10 | |||
11 | children?: { | ||
12 | label: string | ||
13 | routerLink: string | ||
14 | }[] | ||
15 | } | ||
16 | |||
17 | @Component({ | ||
18 | selector: 'my-top-menu-dropdown', | ||
19 | templateUrl: './top-menu-dropdown.component.html', | ||
20 | styleUrls: [ './top-menu-dropdown.component.scss' ] | ||
21 | }) | ||
22 | export class TopMenuDropdownComponent implements OnInit, OnDestroy { | ||
23 | @Input() menuEntries: TopMenuDropdownParam[] = [] | ||
24 | |||
25 | suffixLabels: { [ parentLabel: string ]: string } | ||
26 | |||
27 | private openedOnHover = false | ||
28 | private routeSub: Subscription | ||
29 | |||
30 | constructor (private router: Router) {} | ||
31 | |||
32 | ngOnInit () { | ||
33 | this.updateChildLabels(window.location.pathname) | ||
34 | |||
35 | this.routeSub = this.router.events | ||
36 | .pipe(filter(event => event instanceof NavigationEnd)) | ||
37 | .subscribe(() => this.updateChildLabels(window.location.pathname)) | ||
38 | } | ||
39 | |||
40 | ngOnDestroy () { | ||
41 | if (this.routeSub) this.routeSub.unsubscribe() | ||
42 | } | ||
43 | |||
44 | openDropdownOnHover (dropdown: NgbDropdown) { | ||
45 | this.openedOnHover = true | ||
46 | dropdown.open() | ||
47 | |||
48 | // Menu was closed | ||
49 | dropdown.openChange | ||
50 | .pipe(take(1)) | ||
51 | .subscribe(e => this.openedOnHover = false) | ||
52 | } | ||
53 | |||
54 | dropdownAnchorClicked (dropdown: NgbDropdown) { | ||
55 | if (this.openedOnHover) { | ||
56 | this.openedOnHover = false | ||
57 | return | ||
58 | } | ||
59 | |||
60 | return dropdown.toggle() | ||
61 | } | ||
62 | |||
63 | closeDropdownIfHovered (dropdown: NgbDropdown) { | ||
64 | if (this.openedOnHover === false) return | ||
65 | |||
66 | dropdown.close() | ||
67 | this.openedOnHover = false | ||
68 | } | ||
69 | |||
70 | private updateChildLabels (path: string) { | ||
71 | this.suffixLabels = {} | ||
72 | |||
73 | for (const entry of this.menuEntries) { | ||
74 | if (!entry.children) continue | ||
75 | |||
76 | for (const child of entry.children) { | ||
77 | if (path.startsWith(child.routerLink)) { | ||
78 | this.suffixLabels[entry.label] = child.label | ||
79 | } | ||
80 | } | ||
81 | } | ||
82 | } | ||
83 | } | ||
diff --git a/client/src/app/shared/misc/from-now.pipe.ts b/client/src/app/shared/misc/from-now.pipe.ts index 33e6d25fe..00b5be6c9 100644 --- a/client/src/app/shared/misc/from-now.pipe.ts +++ b/client/src/app/shared/misc/from-now.pipe.ts | |||
@@ -7,8 +7,9 @@ export class FromNowPipe implements PipeTransform { | |||
7 | 7 | ||
8 | constructor (private i18n: I18n) { } | 8 | constructor (private i18n: I18n) { } |
9 | 9 | ||
10 | transform (value: number) { | 10 | transform (arg: number | Date | string) { |
11 | const seconds = Math.floor((Date.now() - value) / 1000) | 11 | const argDate = new Date(arg) |
12 | const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) | ||
12 | 13 | ||
13 | let interval = Math.floor(seconds / 31536000) | 14 | let interval = Math.floor(seconds / 31536000) |
14 | if (interval > 1) { | 15 | if (interval > 1) { |
diff --git a/client/src/app/shared/misc/help.component.html b/client/src/app/shared/misc/help.component.html index 28ccb1e26..444425c9f 100644 --- a/client/src/app/shared/misc/help.component.html +++ b/client/src/app/shared/misc/help.component.html | |||
@@ -18,10 +18,13 @@ | |||
18 | container="body" | 18 | container="body" |
19 | title="Get help" | 19 | title="Get help" |
20 | i18n-title | 20 | i18n-title |
21 | popoverClass="help-popover" | ||
21 | [attr.aria-pressed]="isPopoverOpened" | 22 | [attr.aria-pressed]="isPopoverOpened" |
22 | [ngbPopover]="tooltipTemplate" | 23 | [ngbPopover]="tooltipTemplate" |
23 | [placement]="tooltipPlacement" | 24 | [placement]="tooltipPlacement" |
24 | [autoClose]="true" | 25 | [autoClose]="true" |
25 | (onHidden)="onPopoverHidden()" | 26 | (onHidden)="onPopoverHidden()" |
26 | (onShown)="onPopoverShown()" | 27 | (onShown)="onPopoverShown()" |
27 | ></span> | 28 | > |
29 | <my-global-icon iconName="help"></my-global-icon> | ||
30 | </span> | ||
diff --git a/client/src/app/shared/misc/help.component.scss b/client/src/app/shared/misc/help.component.scss index 5c73a8031..3898f3cda 100644 --- a/client/src/app/shared/misc/help.component.scss +++ b/client/src/app/shared/misc/help.component.scss | |||
@@ -2,29 +2,40 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .help-tooltip-button { | 4 | .help-tooltip-button { |
5 | @include icon(17px); | 5 | cursor: pointer; |
6 | |||
7 | position: relative; | ||
8 | top: -2px; | ||
9 | background-image: url('../../../assets/images/global/help.svg'); | ||
10 | border: none; | 6 | border: none; |
11 | margin: 5px; | 7 | |
8 | my-global-icon { | ||
9 | width: 17px; | ||
10 | position: relative; | ||
11 | top: -2px; | ||
12 | margin: 5px; | ||
13 | |||
14 | @include apply-svg-color(var(--mainForegroundColor)) | ||
15 | } | ||
12 | } | 16 | } |
13 | 17 | ||
14 | /deep/ { | 18 | /deep/ { |
15 | .popover-body { | 19 | .help-popover { |
16 | text-align: left; | ||
17 | padding: 10px; | ||
18 | max-width: 300px; | 20 | max-width: 300px; |
19 | 21 | ||
20 | font-size: 13px; | 22 | .popover-body { |
21 | font-family: $main-fonts; | 23 | font-family: $main-fonts; |
22 | background-color: #fff; | 24 | text-align: left; |
23 | color: #000; | 25 | padding: 10px; |
24 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); | 26 | font-size: 13px; |
27 | background-color: var(--mainBackgroundColor); | ||
28 | color: var(--mainForegroundColor); | ||
29 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); | ||
30 | |||
31 | p { | ||
32 | margin-bottom: 0; | ||
33 | } | ||
25 | 34 | ||
26 | ul { | 35 | ul { |
27 | padding-left: 20px; | 36 | padding-left: 20px; |
37 | margin-bottom: 0; | ||
38 | } | ||
28 | } | 39 | } |
29 | } | 40 | } |
30 | } | 41 | } |
diff --git a/client/src/app/shared/misc/help.component.ts b/client/src/app/shared/misc/help.component.ts index ba0452e77..f3426f70f 100644 --- a/client/src/app/shared/misc/help.component.ts +++ b/client/src/app/shared/misc/help.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, Input, OnChanges, OnInit } from '@angular/core' | 1 | import { Component, Input, OnChanges, OnInit } from '@angular/core' |
2 | import { MarkdownService } from '@app/videos/shared' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 2 | import { I18n } from '@ngx-translate/i18n-polyfill' |
3 | import { MarkdownService } from '@app/shared/renderer' | ||
4 | 4 | ||
5 | @Component({ | 5 | @Component({ |
6 | selector: 'my-help', | 6 | selector: 'my-help', |
diff --git a/client/src/app/shared/misc/peertube-local-storage.ts b/client/src/app/shared/misc/peertube-local-storage.ts index 260f994b6..fb5c45acf 100644 --- a/client/src/app/shared/misc/peertube-local-storage.ts +++ b/client/src/app/shared/misc/peertube-local-storage.ts | |||
@@ -6,7 +6,7 @@ class MemoryStorage { | |||
6 | [key: string]: any | 6 | [key: string]: any |
7 | [index: number]: string | 7 | [index: number]: string |
8 | 8 | ||
9 | getItem (key) { | 9 | getItem (key: any) { |
10 | const stringKey = String(key) | 10 | const stringKey = String(key) |
11 | if (valuesMap.has(key)) { | 11 | if (valuesMap.has(key)) { |
12 | return String(valuesMap.get(stringKey)) | 12 | return String(valuesMap.get(stringKey)) |
@@ -15,11 +15,11 @@ class MemoryStorage { | |||
15 | return null | 15 | return null |
16 | } | 16 | } |
17 | 17 | ||
18 | setItem (key, val) { | 18 | setItem (key: any, val: any) { |
19 | valuesMap.set(String(key), String(val)) | 19 | valuesMap.set(String(key), String(val)) |
20 | } | 20 | } |
21 | 21 | ||
22 | removeItem (key) { | 22 | removeItem (key: any) { |
23 | valuesMap.delete(key) | 23 | valuesMap.delete(key) |
24 | } | 24 | } |
25 | 25 | ||
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts index c8b7ebc67..7cc6055c2 100644 --- a/client/src/app/shared/misc/utils.ts +++ b/client/src/app/shared/misc/utils.ts | |||
@@ -102,12 +102,18 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) { | |||
102 | return fd | 102 | return fd |
103 | } | 103 | } |
104 | 104 | ||
105 | function lineFeedToHtml (obj: object, keyToNormalize: string) { | 105 | function objectLineFeedToHtml (obj: any, keyToNormalize: string) { |
106 | return immutableAssign(obj, { | 106 | return immutableAssign(obj, { |
107 | [keyToNormalize]: obj[keyToNormalize].replace(/\r?\n|\r/g, '<br />') | 107 | [keyToNormalize]: lineFeedToHtml(obj[keyToNormalize]) |
108 | }) | 108 | }) |
109 | } | 109 | } |
110 | 110 | ||
111 | function lineFeedToHtml (text: string) { | ||
112 | if (!text) return text | ||
113 | |||
114 | return text.replace(/\r?\n|\r/g, '<br />') | ||
115 | } | ||
116 | |||
111 | function removeElementFromArray <T> (arr: T[], elem: T) { | 117 | function removeElementFromArray <T> (arr: T[], elem: T) { |
112 | const index = arr.indexOf(elem) | 118 | const index = arr.indexOf(elem) |
113 | if (index !== -1) arr.splice(index, 1) | 119 | if (index !== -1) arr.splice(index, 1) |
@@ -124,9 +130,14 @@ function sortBy (obj: any[], key1: string, key2?: string) { | |||
124 | }) | 130 | }) |
125 | } | 131 | } |
126 | 132 | ||
133 | function scrollToTop () { | ||
134 | window.scroll(0, 0) | ||
135 | } | ||
136 | |||
127 | export { | 137 | export { |
128 | sortBy, | 138 | sortBy, |
129 | durationToString, | 139 | durationToString, |
140 | lineFeedToHtml, | ||
130 | objectToUrlEncoded, | 141 | objectToUrlEncoded, |
131 | getParameterByName, | 142 | getParameterByName, |
132 | populateAsyncUserVideoChannels, | 143 | populateAsyncUserVideoChannels, |
@@ -134,6 +145,7 @@ export { | |||
134 | dateToHuman, | 145 | dateToHuman, |
135 | immutableAssign, | 146 | immutableAssign, |
136 | objectToFormData, | 147 | objectToFormData, |
137 | lineFeedToHtml, | 148 | objectLineFeedToHtml, |
138 | removeElementFromArray | 149 | removeElementFromArray, |
150 | scrollToTop | ||
139 | } | 151 | } |
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.html b/client/src/app/shared/moderation/user-ban-modal.component.html index b2958caa4..f38ea543d 100644 --- a/client/src/app/shared/moderation/user-ban-modal.component.html +++ b/client/src/app/shared/moderation/user-ban-modal.component.html | |||
@@ -1,7 +1,8 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Ban {{ userToBan.username }}</h4> | 3 | <h4 i18n class="modal-title">Ban</h4> |
4 | <span class="close" aria-hidden="true" (click)="hideBanUserModal()"></span> | 4 | |
5 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> | ||
5 | </div> | 6 | </div> |
6 | 7 | ||
7 | <div class="modal-body"> | 8 | <div class="modal-body"> |
@@ -19,7 +20,7 @@ | |||
19 | </div> | 20 | </div> |
20 | 21 | ||
21 | <div class="form-group inputs"> | 22 | <div class="form-group inputs"> |
22 | <span i18n class="action-button action-button-cancel" (click)="hideBanUserModal()">Cancel</span> | 23 | <span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span> |
23 | 24 | ||
24 | <input | 25 | <input |
25 | type="submit" i18n-value value="Ban this user" class="action-button-submit" | 26 | type="submit" i18n-value value="Ban this user" class="action-button-submit" |
@@ -29,4 +30,4 @@ | |||
29 | </form> | 30 | </form> |
30 | </div> | 31 | </div> |
31 | 32 | ||
32 | </ng-template> \ No newline at end of file | 33 | </ng-template> |
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.ts b/client/src/app/shared/moderation/user-ban-modal.component.ts index 67ae38e48..942765301 100644 --- a/client/src/app/shared/moderation/user-ban-modal.component.ts +++ b/client/src/app/shared/moderation/user-ban-modal.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
@@ -15,15 +15,15 @@ import { User } from '../../../../../shared' | |||
15 | }) | 15 | }) |
16 | export class UserBanModalComponent extends FormReactive implements OnInit { | 16 | export class UserBanModalComponent extends FormReactive implements OnInit { |
17 | @ViewChild('modal') modal: NgbModal | 17 | @ViewChild('modal') modal: NgbModal |
18 | @Output() userBanned = new EventEmitter<User>() | 18 | @Output() userBanned = new EventEmitter<User | User[]>() |
19 | 19 | ||
20 | private userToBan: User | 20 | private usersToBan: User | User[] |
21 | private openedModal: NgbModalRef | 21 | private openedModal: NgbModalRef |
22 | 22 | ||
23 | constructor ( | 23 | constructor ( |
24 | protected formValidatorService: FormValidatorService, | 24 | protected formValidatorService: FormValidatorService, |
25 | private modalService: NgbModal, | 25 | private modalService: NgbModal, |
26 | private notificationsService: NotificationsService, | 26 | private notifier: Notifier, |
27 | private userService: UserService, | 27 | private userService: UserService, |
28 | private userValidatorsService: UserValidatorsService, | 28 | private userValidatorsService: UserValidatorsService, |
29 | private i18n: I18n | 29 | private i18n: I18n |
@@ -37,32 +37,33 @@ export class UserBanModalComponent extends FormReactive implements OnInit { | |||
37 | }) | 37 | }) |
38 | } | 38 | } |
39 | 39 | ||
40 | openModal (user: User) { | 40 | openModal (user: User | User[]) { |
41 | this.userToBan = user | 41 | this.usersToBan = user |
42 | this.openedModal = this.modalService.open(this.modal) | 42 | this.openedModal = this.modalService.open(this.modal) |
43 | } | 43 | } |
44 | 44 | ||
45 | hideBanUserModal () { | 45 | hide () { |
46 | this.userToBan = undefined | 46 | this.usersToBan = undefined |
47 | this.openedModal.close() | 47 | this.openedModal.close() |
48 | } | 48 | } |
49 | 49 | ||
50 | async banUser () { | 50 | async banUser () { |
51 | const reason = this.form.value['reason'] || undefined | 51 | const reason = this.form.value['reason'] || undefined |
52 | 52 | ||
53 | this.userService.banUser(this.userToBan, reason) | 53 | this.userService.banUsers(this.usersToBan, reason) |
54 | .subscribe( | 54 | .subscribe( |
55 | () => { | 55 | () => { |
56 | this.notificationsService.success( | 56 | const message = Array.isArray(this.usersToBan) |
57 | this.i18n('Success'), | 57 | ? this.i18n('{{num}} users banned.', { num: this.usersToBan.length }) |
58 | this.i18n('User {{username}} banned.', { username: this.userToBan.username }) | 58 | : this.i18n('User {{username}} banned.', { username: this.usersToBan.username }) |
59 | ) | ||
60 | 59 | ||
61 | this.userBanned.emit(this.userToBan) | 60 | this.notifier.success(message) |
62 | this.hideBanUserModal() | 61 | |
62 | this.userBanned.emit(this.usersToBan) | ||
63 | this.hide() | ||
63 | }, | 64 | }, |
64 | 65 | ||
65 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 66 | err => this.notifier.error(err.message) |
66 | ) | 67 | ) |
67 | } | 68 | } |
68 | 69 | ||
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.html b/client/src/app/shared/moderation/user-moderation-dropdown.component.html index ed1a4c863..7367a7e59 100644 --- a/client/src/app/shared/moderation/user-moderation-dropdown.component.html +++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.html | |||
@@ -1,5 +1,8 @@ | |||
1 | <ng-container *ngIf="user && userActions.length !== 0"> | 1 | <ng-container *ngIf="userActions.length !== 0"> |
2 | <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal> | 2 | <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal> |
3 | 3 | ||
4 | <my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user" [buttonSize]="buttonSize"></my-action-dropdown> | 4 | <my-action-dropdown |
5 | [actions]="userActions" [entry]="{ user: user, account: account }" | ||
6 | [buttonSize]="buttonSize" [placement]="placement" | ||
7 | ></my-action-dropdown> | ||
5 | </ng-container> \ No newline at end of file | 8 | </ng-container> \ No newline at end of file |
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts index 4f88456de..9a2461ebf 100644 --- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts | |||
@@ -1,50 +1,53 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | ||
3 | import { I18n } from '@ngx-translate/i18n-polyfill' | 2 | import { I18n } from '@ngx-translate/i18n-polyfill' |
4 | import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' | 3 | import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' |
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
6 | import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component' | 4 | import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component' |
7 | import { UserService } from '@app/shared/users' | 5 | import { UserService } from '@app/shared/users' |
8 | import { AuthService, ConfirmService } from '@app/core' | 6 | import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' |
9 | import { User, UserRight } from '../../../../../shared/models/users' | 7 | import { User, UserRight } from '../../../../../shared/models/users' |
8 | import { Account } from '@app/shared/account/account.model' | ||
9 | import { BlocklistService } from '@app/shared/blocklist' | ||
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
12 | selector: 'my-user-moderation-dropdown', | 12 | selector: 'my-user-moderation-dropdown', |
13 | templateUrl: './user-moderation-dropdown.component.html', | 13 | templateUrl: './user-moderation-dropdown.component.html', |
14 | styleUrls: [ './user-moderation-dropdown.component.scss' ] | 14 | styleUrls: [ './user-moderation-dropdown.component.scss' ] |
15 | }) | 15 | }) |
16 | export class UserModerationDropdownComponent implements OnInit { | 16 | export class UserModerationDropdownComponent implements OnChanges { |
17 | @ViewChild('userBanModal') userBanModal: UserBanModalComponent | 17 | @ViewChild('userBanModal') userBanModal: UserBanModalComponent |
18 | 18 | ||
19 | @Input() user: User | 19 | @Input() user: User |
20 | @Input() account: Account | ||
21 | |||
20 | @Input() buttonSize: 'normal' | 'small' = 'normal' | 22 | @Input() buttonSize: 'normal' | 'small' = 'normal' |
23 | @Input() placement = 'left' | ||
21 | 24 | ||
22 | @Output() userChanged = new EventEmitter() | 25 | @Output() userChanged = new EventEmitter() |
23 | @Output() userDeleted = new EventEmitter() | 26 | @Output() userDeleted = new EventEmitter() |
24 | 27 | ||
25 | userActions: DropdownAction<User>[] = [] | 28 | userActions: DropdownAction<{ user: User, account: Account }>[][] = [] |
26 | |||
27 | private openedModal: NgbModalRef | ||
28 | 29 | ||
29 | constructor ( | 30 | constructor ( |
30 | private authService: AuthService, | 31 | private authService: AuthService, |
31 | private notificationsService: NotificationsService, | 32 | private notifier: Notifier, |
32 | private confirmService: ConfirmService, | 33 | private confirmService: ConfirmService, |
34 | private serverService: ServerService, | ||
33 | private userService: UserService, | 35 | private userService: UserService, |
36 | private blocklistService: BlocklistService, | ||
34 | private i18n: I18n | 37 | private i18n: I18n |
35 | ) { } | 38 | ) { } |
36 | 39 | ||
37 | ngOnInit () { | 40 | get requiresEmailVerification () { |
38 | this.buildActions() | 41 | return this.serverService.getConfig().signup.requiresEmailVerification |
39 | } | 42 | } |
40 | 43 | ||
41 | hideBanUserModal () { | 44 | ngOnChanges () { |
42 | this.openedModal.close() | 45 | this.buildActions() |
43 | } | 46 | } |
44 | 47 | ||
45 | openBanUserModal (user: User) { | 48 | openBanUserModal (user: User) { |
46 | if (user.username === 'root') { | 49 | if (user.username === 'root') { |
47 | this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.')) | 50 | this.notifier.error(this.i18n('You cannot ban root.')) |
48 | return | 51 | return |
49 | } | 52 | } |
50 | 53 | ||
@@ -60,24 +63,21 @@ export class UserModerationDropdownComponent implements OnInit { | |||
60 | const res = await this.confirmService.confirm(message, this.i18n('Unban')) | 63 | const res = await this.confirmService.confirm(message, this.i18n('Unban')) |
61 | if (res === false) return | 64 | if (res === false) return |
62 | 65 | ||
63 | this.userService.unbanUser(user) | 66 | this.userService.unbanUsers(user) |
64 | .subscribe( | 67 | .subscribe( |
65 | () => { | 68 | () => { |
66 | this.notificationsService.success( | 69 | this.notifier.success(this.i18n('User {{username}} unbanned.', { username: user.username })) |
67 | this.i18n('Success'), | ||
68 | this.i18n('User {{username}} unbanned.', { username: user.username }) | ||
69 | ) | ||
70 | 70 | ||
71 | this.userChanged.emit() | 71 | this.userChanged.emit() |
72 | }, | 72 | }, |
73 | 73 | ||
74 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 74 | err => this.notifier.error(err.message) |
75 | ) | 75 | ) |
76 | } | 76 | } |
77 | 77 | ||
78 | async removeUser (user: User) { | 78 | async removeUser (user: User) { |
79 | if (user.username === 'root') { | 79 | if (user.username === 'root') { |
80 | this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.')) | 80 | this.notifier.error(this.i18n('You cannot delete root.')) |
81 | return | 81 | return |
82 | } | 82 | } |
83 | 83 | ||
@@ -87,17 +87,138 @@ export class UserModerationDropdownComponent implements OnInit { | |||
87 | 87 | ||
88 | this.userService.removeUser(user).subscribe( | 88 | this.userService.removeUser(user).subscribe( |
89 | () => { | 89 | () => { |
90 | this.notificationsService.success( | 90 | this.notifier.success(this.i18n('User {{username}} deleted.', { username: user.username })) |
91 | this.i18n('Success'), | ||
92 | this.i18n('User {{username}} deleted.', { username: user.username }) | ||
93 | ) | ||
94 | this.userDeleted.emit() | 91 | this.userDeleted.emit() |
95 | }, | 92 | }, |
96 | 93 | ||
97 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 94 | err => this.notifier.error(err.message) |
98 | ) | 95 | ) |
99 | } | 96 | } |
100 | 97 | ||
98 | setEmailAsVerified (user: User) { | ||
99 | this.userService.updateUser(user.id, { emailVerified: true }).subscribe( | ||
100 | () => { | ||
101 | this.notifier.success(this.i18n('User {{username}} email set as verified', { username: user.username })) | ||
102 | |||
103 | this.userChanged.emit() | ||
104 | }, | ||
105 | |||
106 | err => this.notifier.error(err.message) | ||
107 | ) | ||
108 | } | ||
109 | |||
110 | blockAccountByUser (account: Account) { | ||
111 | this.blocklistService.blockAccountByUser(account) | ||
112 | .subscribe( | ||
113 | () => { | ||
114 | this.notifier.success(this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost })) | ||
115 | |||
116 | this.account.mutedByUser = true | ||
117 | this.userChanged.emit() | ||
118 | }, | ||
119 | |||
120 | err => this.notifier.error(err.message) | ||
121 | ) | ||
122 | } | ||
123 | |||
124 | unblockAccountByUser (account: Account) { | ||
125 | this.blocklistService.unblockAccountByUser(account) | ||
126 | .subscribe( | ||
127 | () => { | ||
128 | this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost })) | ||
129 | |||
130 | this.account.mutedByUser = false | ||
131 | this.userChanged.emit() | ||
132 | }, | ||
133 | |||
134 | err => this.notifier.error(err.message) | ||
135 | ) | ||
136 | } | ||
137 | |||
138 | blockServerByUser (host: string) { | ||
139 | this.blocklistService.blockServerByUser(host) | ||
140 | .subscribe( | ||
141 | () => { | ||
142 | this.notifier.success(this.i18n('Instance {{host}} muted.', { host })) | ||
143 | |||
144 | this.account.mutedServerByUser = true | ||
145 | this.userChanged.emit() | ||
146 | }, | ||
147 | |||
148 | err => this.notifier.error(err.message) | ||
149 | ) | ||
150 | } | ||
151 | |||
152 | unblockServerByUser (host: string) { | ||
153 | this.blocklistService.unblockServerByUser(host) | ||
154 | .subscribe( | ||
155 | () => { | ||
156 | this.notifier.success(this.i18n('Instance {{host}} unmuted.', { host })) | ||
157 | |||
158 | this.account.mutedServerByUser = false | ||
159 | this.userChanged.emit() | ||
160 | }, | ||
161 | |||
162 | err => this.notifier.error(err.message) | ||
163 | ) | ||
164 | } | ||
165 | |||
166 | blockAccountByInstance (account: Account) { | ||
167 | this.blocklistService.blockAccountByInstance(account) | ||
168 | .subscribe( | ||
169 | () => { | ||
170 | this.notifier.success(this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })) | ||
171 | |||
172 | this.account.mutedByInstance = true | ||
173 | this.userChanged.emit() | ||
174 | }, | ||
175 | |||
176 | err => this.notifier.error(err.message) | ||
177 | ) | ||
178 | } | ||
179 | |||
180 | unblockAccountByInstance (account: Account) { | ||
181 | this.blocklistService.unblockAccountByInstance(account) | ||
182 | .subscribe( | ||
183 | () => { | ||
184 | this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted by the instance.', { nameWithHost: account.nameWithHost })) | ||
185 | |||
186 | this.account.mutedByInstance = false | ||
187 | this.userChanged.emit() | ||
188 | }, | ||
189 | |||
190 | err => this.notifier.error(err.message) | ||
191 | ) | ||
192 | } | ||
193 | |||
194 | blockServerByInstance (host: string) { | ||
195 | this.blocklistService.blockServerByInstance(host) | ||
196 | .subscribe( | ||
197 | () => { | ||
198 | this.notifier.success(this.i18n('Instance {{host}} muted by the instance.', { host })) | ||
199 | |||
200 | this.account.mutedServerByInstance = true | ||
201 | this.userChanged.emit() | ||
202 | }, | ||
203 | |||
204 | err => this.notifier.error(err.message) | ||
205 | ) | ||
206 | } | ||
207 | |||
208 | unblockServerByInstance (host: string) { | ||
209 | this.blocklistService.unblockServerByInstance(host) | ||
210 | .subscribe( | ||
211 | () => { | ||
212 | this.notifier.success(this.i18n('Instance {{host}} unmuted by the instance.', { host })) | ||
213 | |||
214 | this.account.mutedServerByInstance = false | ||
215 | this.userChanged.emit() | ||
216 | }, | ||
217 | |||
218 | err => this.notifier.error(err.message) | ||
219 | ) | ||
220 | } | ||
221 | |||
101 | getRouterUserEditLink (user: User) { | 222 | getRouterUserEditLink (user: User) { |
102 | return [ '/admin', 'users', 'update', user.id ] | 223 | return [ '/admin', 'users', 'update', user.id ] |
103 | } | 224 | } |
@@ -108,28 +229,100 @@ export class UserModerationDropdownComponent implements OnInit { | |||
108 | if (this.authService.isLoggedIn()) { | 229 | if (this.authService.isLoggedIn()) { |
109 | const authUser = this.authService.getUser() | 230 | const authUser = this.authService.getUser() |
110 | 231 | ||
111 | if (authUser.hasRight(UserRight.MANAGE_USERS)) { | 232 | if (this.user && authUser.id === this.user.id) return |
112 | this.userActions = this.userActions.concat([ | 233 | |
234 | if (this.user && authUser.hasRight(UserRight.MANAGE_USERS)) { | ||
235 | this.userActions.push([ | ||
113 | { | 236 | { |
114 | label: this.i18n('Edit'), | 237 | label: this.i18n('Edit'), |
115 | linkBuilder: this.getRouterUserEditLink | 238 | linkBuilder: ({ user }) => this.getRouterUserEditLink(user) |
116 | }, | 239 | }, |
117 | { | 240 | { |
118 | label: this.i18n('Delete'), | 241 | label: this.i18n('Delete'), |
119 | handler: user => this.removeUser(user) | 242 | handler: ({ user }) => this.removeUser(user) |
120 | }, | 243 | }, |
121 | { | 244 | { |
122 | label: this.i18n('Ban'), | 245 | label: this.i18n('Ban'), |
123 | handler: user => this.openBanUserModal(user), | 246 | handler: ({ user }) => this.openBanUserModal(user), |
124 | isDisplayed: user => !user.blocked | 247 | isDisplayed: ({ user }) => !user.blocked |
125 | }, | 248 | }, |
126 | { | 249 | { |
127 | label: this.i18n('Unban'), | 250 | label: this.i18n('Unban'), |
128 | handler: user => this.unbanUser(user), | 251 | handler: ({ user }) => this.unbanUser(user), |
129 | isDisplayed: user => user.blocked | 252 | isDisplayed: ({ user }) => user.blocked |
253 | }, | ||
254 | { | ||
255 | label: this.i18n('Set Email as Verified'), | ||
256 | handler: ({ user }) => this.setEmailAsVerified(user), | ||
257 | isDisplayed: ({ user }) => this.requiresEmailVerification && !user.blocked && user.emailVerified === false | ||
130 | } | 258 | } |
131 | ]) | 259 | ]) |
132 | } | 260 | } |
261 | |||
262 | // Actions on accounts/servers | ||
263 | if (this.account) { | ||
264 | // User actions | ||
265 | this.userActions.push([ | ||
266 | { | ||
267 | label: this.i18n('Mute this account'), | ||
268 | isDisplayed: ({ account }) => account.mutedByUser === false, | ||
269 | handler: ({ account }) => this.blockAccountByUser(account) | ||
270 | }, | ||
271 | { | ||
272 | label: this.i18n('Unmute this account'), | ||
273 | isDisplayed: ({ account }) => account.mutedByUser === true, | ||
274 | handler: ({ account }) => this.unblockAccountByUser(account) | ||
275 | }, | ||
276 | { | ||
277 | label: this.i18n('Mute the instance'), | ||
278 | isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false, | ||
279 | handler: ({ account }) => this.blockServerByUser(account.host) | ||
280 | }, | ||
281 | { | ||
282 | label: this.i18n('Unmute the instance'), | ||
283 | isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true, | ||
284 | handler: ({ account }) => this.unblockServerByUser(account.host) | ||
285 | } | ||
286 | ]) | ||
287 | |||
288 | let instanceActions: DropdownAction<{ user: User, account: Account }>[] = [] | ||
289 | |||
290 | // Instance actions | ||
291 | if (authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) { | ||
292 | instanceActions = instanceActions.concat([ | ||
293 | { | ||
294 | label: this.i18n('Mute this account by your instance'), | ||
295 | isDisplayed: ({ account }) => account.mutedByInstance === false, | ||
296 | handler: ({ account }) => this.blockAccountByInstance(account) | ||
297 | }, | ||
298 | { | ||
299 | label: this.i18n('Unmute this account by your instance'), | ||
300 | isDisplayed: ({ account }) => account.mutedByInstance === true, | ||
301 | handler: ({ account }) => this.unblockAccountByInstance(account) | ||
302 | } | ||
303 | ]) | ||
304 | } | ||
305 | |||
306 | // Instance actions | ||
307 | if (authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) { | ||
308 | instanceActions = instanceActions.concat([ | ||
309 | { | ||
310 | label: this.i18n('Mute the instance by your instance'), | ||
311 | isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false, | ||
312 | handler: ({ account }) => this.blockServerByInstance(account.host) | ||
313 | }, | ||
314 | { | ||
315 | label: this.i18n('Unmute the instance by your instance'), | ||
316 | isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true, | ||
317 | handler: ({ account }) => this.unblockServerByInstance(account.host) | ||
318 | } | ||
319 | ]) | ||
320 | } | ||
321 | |||
322 | if (instanceActions.length !== 0) { | ||
323 | this.userActions.push(instanceActions) | ||
324 | } | ||
325 | } | ||
133 | } | 326 | } |
134 | } | 327 | } |
135 | } | 328 | } |
diff --git a/client/src/app/shared/overview/videos-overview.model.ts b/client/src/app/shared/overview/videos-overview.model.ts index cf02bdb3d..c8eafc8e8 100644 --- a/client/src/app/shared/overview/videos-overview.model.ts +++ b/client/src/app/shared/overview/videos-overview.model.ts | |||
@@ -16,4 +16,5 @@ export class VideosOverview implements VideosOverviewServer { | |||
16 | tag: string | 16 | tag: string |
17 | videos: Video[] | 17 | videos: Video[] |
18 | }[] | 18 | }[] |
19 | [key: string]: any | ||
19 | } | 20 | } |
diff --git a/client/src/app/shared/renderer/html-renderer.service.ts b/client/src/app/shared/renderer/html-renderer.service.ts new file mode 100644 index 000000000..d49df9b6d --- /dev/null +++ b/client/src/app/shared/renderer/html-renderer.service.ts | |||
@@ -0,0 +1,35 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { LinkifierService } from '@app/shared/renderer/linkifier.service' | ||
3 | import * as sanitizeHtml from 'sanitize-html' | ||
4 | |||
5 | @Injectable() | ||
6 | export class HtmlRendererService { | ||
7 | |||
8 | constructor (private linkifier: LinkifierService) { | ||
9 | |||
10 | } | ||
11 | |||
12 | toSafeHtml (text: string) { | ||
13 | // Convert possible markdown to html | ||
14 | const html = this.linkifier.linkify(text) | ||
15 | |||
16 | return sanitizeHtml(html, { | ||
17 | allowedTags: [ 'a', 'p', 'span', 'br' ], | ||
18 | allowedSchemes: [ 'http', 'https' ], | ||
19 | allowedAttributes: { | ||
20 | 'a': [ 'href', 'class', 'target' ] | ||
21 | }, | ||
22 | transformTags: { | ||
23 | a: (tagName, attribs) => { | ||
24 | return { | ||
25 | tagName, | ||
26 | attribs: Object.assign(attribs, { | ||
27 | target: '_blank', | ||
28 | rel: 'noopener noreferrer' | ||
29 | }) | ||
30 | } | ||
31 | } | ||
32 | } | ||
33 | }) | ||
34 | } | ||
35 | } | ||
diff --git a/client/src/app/shared/renderer/index.ts b/client/src/app/shared/renderer/index.ts new file mode 100644 index 000000000..39202b385 --- /dev/null +++ b/client/src/app/shared/renderer/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './html-renderer.service' | ||
2 | export * from './linkifier.service' | ||
3 | export * from './markdown.service' | ||
diff --git a/client/src/app/videos/+video-watch/comment/linkifier.service.ts b/client/src/app/shared/renderer/linkifier.service.ts index 3f4072efd..2529c9eaf 100644 --- a/client/src/app/videos/+video-watch/comment/linkifier.service.ts +++ b/client/src/app/shared/renderer/linkifier.service.ts | |||
@@ -1,7 +1,8 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' | 2 | import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' |
3 | import * as linkify from 'linkifyjs' | 3 | // FIXME: use @types/linkify when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29682/files is merged? |
4 | import * as linkifyHtml from 'linkifyjs/html' | 4 | const linkify = require('linkifyjs') |
5 | const linkifyHtml = require('linkifyjs/html') | ||
5 | 6 | ||
6 | @Injectable() | 7 | @Injectable() |
7 | export class LinkifierService { | 8 | export class LinkifierService { |
@@ -40,7 +41,7 @@ export class LinkifierService { | |||
40 | const TT_UNDERSCORE = TT.UNDERSCORE | 41 | const TT_UNDERSCORE = TT.UNDERSCORE |
41 | const TT_DOT = TT.DOT | 42 | const TT_DOT = TT.DOT |
42 | 43 | ||
43 | function MENTION (value) { | 44 | function MENTION (this: any, value: any) { |
44 | this.v = value | 45 | this.v = value |
45 | } | 46 | } |
46 | 47 | ||
diff --git a/client/src/app/videos/shared/markdown.service.ts b/client/src/app/shared/renderer/markdown.service.ts index 07017eca5..07017eca5 100644 --- a/client/src/app/videos/shared/markdown.service.ts +++ b/client/src/app/shared/renderer/markdown.service.ts | |||
diff --git a/client/src/app/shared/rest/component-pagination.model.ts b/client/src/app/shared/rest/component-pagination.model.ts index 0b8ecc318..85160d445 100644 --- a/client/src/app/shared/rest/component-pagination.model.ts +++ b/client/src/app/shared/rest/component-pagination.model.ts | |||
@@ -3,3 +3,14 @@ export interface ComponentPagination { | |||
3 | itemsPerPage: number | 3 | itemsPerPage: number |
4 | totalItems?: number | 4 | totalItems?: number |
5 | } | 5 | } |
6 | |||
7 | export function hasMoreItems (componentPagination: ComponentPagination) { | ||
8 | // No results | ||
9 | if (componentPagination.totalItems === 0) return false | ||
10 | |||
11 | // Not loaded yet | ||
12 | if (!componentPagination.totalItems) return true | ||
13 | |||
14 | const maxPage = componentPagination.totalItems / componentPagination.itemsPerPage | ||
15 | return maxPage > componentPagination.currentPage | ||
16 | } | ||
diff --git a/client/src/app/shared/rest/rest-extractor.service.ts b/client/src/app/shared/rest/rest-extractor.service.ts index 6492aa66d..e6518dd1d 100644 --- a/client/src/app/shared/rest/rest-extractor.service.ts +++ b/client/src/app/shared/rest/rest-extractor.service.ts | |||
@@ -33,7 +33,7 @@ export class RestExtractor { | |||
33 | return this.applyToResultListData(result, this.convertDateToHuman, [ fieldsToConvert ]) | 33 | return this.applyToResultListData(result, this.convertDateToHuman, [ fieldsToConvert ]) |
34 | } | 34 | } |
35 | 35 | ||
36 | convertDateToHuman (target: object, fieldsToConvert: string[]) { | 36 | convertDateToHuman (target: { [ id: string ]: string }, fieldsToConvert: string[]) { |
37 | fieldsToConvert.forEach(field => target[field] = dateToHuman(target[field])) | 37 | fieldsToConvert.forEach(field => target[field] = dateToHuman(target[field])) |
38 | 38 | ||
39 | return target | 39 | return target |
@@ -80,10 +80,11 @@ export class RestExtractor { | |||
80 | errorMessage = errorMessage ? errorMessage : 'Unknown error.' | 80 | errorMessage = errorMessage ? errorMessage : 'Unknown error.' |
81 | console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) | 81 | console.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) |
82 | } else { | 82 | } else { |
83 | console.error(err) | ||
83 | errorMessage = err | 84 | errorMessage = err |
84 | } | 85 | } |
85 | 86 | ||
86 | const errorObj = { | 87 | const errorObj: { message: string, status: string, body: string } = { |
87 | message: errorMessage, | 88 | message: errorMessage, |
88 | status: undefined, | 89 | status: undefined, |
89 | body: undefined | 90 | body: undefined |
diff --git a/client/src/app/shared/rest/rest-table.ts b/client/src/app/shared/rest/rest-table.ts index fe1a91d2d..884588207 100644 --- a/client/src/app/shared/rest/rest-table.ts +++ b/client/src/app/shared/rest/rest-table.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | 1 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' |
2 | import { LazyLoadEvent } from 'primeng/components/common/lazyloadevent' | 2 | import { LazyLoadEvent } from 'primeng/components/common/lazyloadevent' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/components/common/sortmeta' |
4 | |||
5 | import { RestPagination } from './rest-pagination' | 4 | import { RestPagination } from './rest-pagination' |
5 | import { Subject } from 'rxjs' | ||
6 | import { debounceTime, distinctUntilChanged } from 'rxjs/operators' | ||
6 | 7 | ||
7 | export abstract class RestTable { | 8 | export abstract class RestTable { |
8 | 9 | ||
@@ -11,9 +12,14 @@ export abstract class RestTable { | |||
11 | abstract sort: SortMeta | 12 | abstract sort: SortMeta |
12 | abstract pagination: RestPagination | 13 | abstract pagination: RestPagination |
13 | 14 | ||
15 | protected search: string | ||
16 | private searchStream: Subject<string> | ||
14 | private sortLocalStorageKey = 'rest-table-sort-' + this.constructor.name | 17 | private sortLocalStorageKey = 'rest-table-sort-' + this.constructor.name |
15 | 18 | ||
16 | protected abstract loadData (): void | 19 | initialize () { |
20 | this.loadSort() | ||
21 | this.initSearch() | ||
22 | } | ||
17 | 23 | ||
18 | loadSort () { | 24 | loadSort () { |
19 | const result = peertubeLocalStorage.getItem(this.sortLocalStorageKey) | 25 | const result = peertubeLocalStorage.getItem(this.sortLocalStorageKey) |
@@ -46,4 +52,23 @@ export abstract class RestTable { | |||
46 | peertubeLocalStorage.setItem(this.sortLocalStorageKey, JSON.stringify(this.sort)) | 52 | peertubeLocalStorage.setItem(this.sortLocalStorageKey, JSON.stringify(this.sort)) |
47 | } | 53 | } |
48 | 54 | ||
55 | initSearch () { | ||
56 | this.searchStream = new Subject() | ||
57 | |||
58 | this.searchStream | ||
59 | .pipe( | ||
60 | debounceTime(400), | ||
61 | distinctUntilChanged() | ||
62 | ) | ||
63 | .subscribe(search => { | ||
64 | this.search = search | ||
65 | this.loadData() | ||
66 | }) | ||
67 | } | ||
68 | |||
69 | onSearch (search: string) { | ||
70 | this.searchStream.next(search) | ||
71 | } | ||
72 | |||
73 | protected abstract loadData (): void | ||
49 | } | 74 | } |
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts index 4560c2024..e6d4e6e5e 100644 --- a/client/src/app/shared/rest/rest.service.ts +++ b/client/src/app/shared/rest/rest.service.ts | |||
@@ -32,7 +32,7 @@ export class RestService { | |||
32 | return newParams | 32 | return newParams |
33 | } | 33 | } |
34 | 34 | ||
35 | addObjectParams (params: HttpParams, object: object) { | 35 | addObjectParams (params: HttpParams, object: { [ name: string ]: any }) { |
36 | for (const name of Object.keys(object)) { | 36 | for (const name of Object.keys(object)) { |
37 | const value = object[name] | 37 | const value = object[name] |
38 | if (!value) continue | 38 | if (!value) continue |
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 9647a7966..6f8625c7e 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -6,7 +6,6 @@ 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 | import { MarkdownService } from '@app/videos/shared' | ||
10 | 9 | ||
11 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' | 10 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' |
12 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' | 11 | import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' |
@@ -25,7 +24,7 @@ import { VideoAbuseService } from './video-abuse' | |||
25 | import { VideoBlacklistService } from './video-blacklist' | 24 | import { VideoBlacklistService } from './video-blacklist' |
26 | import { VideoOwnershipService } from './video-ownership' | 25 | import { VideoOwnershipService } from './video-ownership' |
27 | import { VideoMiniatureComponent } from './video/video-miniature.component' | 26 | import { VideoMiniatureComponent } from './video/video-miniature.component' |
28 | import { VideoFeedComponent } from './video/video-feed.component' | 27 | import { FeedComponent } from './video/feed.component' |
29 | import { VideoThumbnailComponent } from './video/video-thumbnail.component' | 28 | import { VideoThumbnailComponent } from './video/video-thumbnail.component' |
30 | import { VideoService } from './video/video.service' | 29 | import { VideoService } from './video/video.service' |
31 | import { AccountService } from '@app/shared/account/account.service' | 30 | import { AccountService } from '@app/shared/account/account.service' |
@@ -34,16 +33,19 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
34 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 33 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
35 | import { | 34 | import { |
36 | CustomConfigValidatorsService, | 35 | CustomConfigValidatorsService, |
36 | InstanceValidatorsService, | ||
37 | LoginValidatorsService, | 37 | LoginValidatorsService, |
38 | ReactiveFileComponent, | 38 | ReactiveFileComponent, |
39 | ResetPasswordValidatorsService, | 39 | ResetPasswordValidatorsService, |
40 | TextareaAutoResizeDirective, | ||
40 | UserValidatorsService, | 41 | UserValidatorsService, |
41 | VideoAbuseValidatorsService, | 42 | VideoAbuseValidatorsService, |
43 | VideoAcceptOwnershipValidatorsService, | ||
42 | VideoBlacklistValidatorsService, | 44 | VideoBlacklistValidatorsService, |
45 | VideoChangeOwnershipValidatorsService, | ||
43 | VideoChannelValidatorsService, | 46 | VideoChannelValidatorsService, |
44 | VideoCommentValidatorsService, | 47 | VideoCommentValidatorsService, |
45 | VideoValidatorsService, | 48 | VideoValidatorsService |
46 | VideoChangeOwnershipValidatorsService, VideoAcceptOwnershipValidatorsService | ||
47 | } from '@app/shared/forms' | 49 | } from '@app/shared/forms' |
48 | import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' | 50 | import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar' |
49 | import { ScreenService } from '@app/shared/misc/screen.service' | 51 | import { ScreenService } from '@app/shared/misc/screen.service' |
@@ -53,11 +55,20 @@ import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.c | |||
53 | import { VideoImportService } from '@app/shared/video-import/video-import.service' | 55 | import { VideoImportService } from '@app/shared/video-import/video-import.service' |
54 | import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component' | 56 | import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component' |
55 | import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' | 57 | import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' |
56 | import { SubscribeButtonComponent, RemoteSubscribeComponent, UserSubscriptionService } from '@app/shared/user-subscription' | 58 | import { RemoteSubscribeComponent, SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription' |
57 | import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' | 59 | import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' |
58 | import { OverviewService } from '@app/shared/overview' | 60 | import { OverviewService } from '@app/shared/overview' |
59 | import { UserBanModalComponent } from '@app/shared/moderation' | 61 | import { UserBanModalComponent } from '@app/shared/moderation' |
60 | import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component' | 62 | import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component' |
63 | import { BlocklistService } from '@app/shared/blocklist' | ||
64 | import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.component' | ||
65 | import { UserHistoryService } from '@app/shared/users/user-history.service' | ||
66 | import { UserNotificationService } from '@app/shared/users/user-notification.service' | ||
67 | import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component' | ||
68 | import { InstanceService } from '@app/shared/instance/instance.service' | ||
69 | import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer' | ||
70 | import { ConfirmComponent } from '@app/shared/confirm/confirm.component' | ||
71 | import { GlobalIconComponent } from '@app/shared/icons/global-icon.component' | ||
61 | 72 | ||
62 | @NgModule({ | 73 | @NgModule({ |
63 | imports: [ | 74 | imports: [ |
@@ -81,7 +92,7 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod | |||
81 | LoaderComponent, | 92 | LoaderComponent, |
82 | VideoThumbnailComponent, | 93 | VideoThumbnailComponent, |
83 | VideoMiniatureComponent, | 94 | VideoMiniatureComponent, |
84 | VideoFeedComponent, | 95 | FeedComponent, |
85 | ButtonComponent, | 96 | ButtonComponent, |
86 | DeleteButtonComponent, | 97 | DeleteButtonComponent, |
87 | EditButtonComponent, | 98 | EditButtonComponent, |
@@ -91,6 +102,7 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod | |||
91 | FromNowPipe, | 102 | FromNowPipe, |
92 | MarkdownTextareaComponent, | 103 | MarkdownTextareaComponent, |
93 | InfiniteScrollerDirective, | 104 | InfiniteScrollerDirective, |
105 | TextareaAutoResizeDirective, | ||
94 | HelpComponent, | 106 | HelpComponent, |
95 | ReactiveFileComponent, | 107 | ReactiveFileComponent, |
96 | PeertubeCheckboxComponent, | 108 | PeertubeCheckboxComponent, |
@@ -98,7 +110,11 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod | |||
98 | RemoteSubscribeComponent, | 110 | RemoteSubscribeComponent, |
99 | InstanceFeaturesTableComponent, | 111 | InstanceFeaturesTableComponent, |
100 | UserBanModalComponent, | 112 | UserBanModalComponent, |
101 | UserModerationDropdownComponent | 113 | UserModerationDropdownComponent, |
114 | TopMenuDropdownComponent, | ||
115 | UserNotificationsComponent, | ||
116 | ConfirmComponent, | ||
117 | GlobalIconComponent | ||
102 | ], | 118 | ], |
103 | 119 | ||
104 | exports: [ | 120 | exports: [ |
@@ -121,13 +137,14 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod | |||
121 | LoaderComponent, | 137 | LoaderComponent, |
122 | VideoThumbnailComponent, | 138 | VideoThumbnailComponent, |
123 | VideoMiniatureComponent, | 139 | VideoMiniatureComponent, |
124 | VideoFeedComponent, | 140 | FeedComponent, |
125 | ButtonComponent, | 141 | ButtonComponent, |
126 | DeleteButtonComponent, | 142 | DeleteButtonComponent, |
127 | EditButtonComponent, | 143 | EditButtonComponent, |
128 | ActionDropdownComponent, | 144 | ActionDropdownComponent, |
129 | MarkdownTextareaComponent, | 145 | MarkdownTextareaComponent, |
130 | InfiniteScrollerDirective, | 146 | InfiniteScrollerDirective, |
147 | TextareaAutoResizeDirective, | ||
131 | HelpComponent, | 148 | HelpComponent, |
132 | ReactiveFileComponent, | 149 | ReactiveFileComponent, |
133 | PeertubeCheckboxComponent, | 150 | PeertubeCheckboxComponent, |
@@ -136,6 +153,10 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod | |||
136 | InstanceFeaturesTableComponent, | 153 | InstanceFeaturesTableComponent, |
137 | UserBanModalComponent, | 154 | UserBanModalComponent, |
138 | UserModerationDropdownComponent, | 155 | UserModerationDropdownComponent, |
156 | TopMenuDropdownComponent, | ||
157 | UserNotificationsComponent, | ||
158 | ConfirmComponent, | ||
159 | GlobalIconComponent, | ||
139 | 160 | ||
140 | NumberFormatterPipe, | 161 | NumberFormatterPipe, |
141 | ObjectLengthPipe, | 162 | ObjectLengthPipe, |
@@ -152,7 +173,6 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod | |||
152 | UserService, | 173 | UserService, |
153 | VideoService, | 174 | VideoService, |
154 | AccountService, | 175 | AccountService, |
155 | MarkdownService, | ||
156 | VideoChannelService, | 176 | VideoChannelService, |
157 | VideoCaptionService, | 177 | VideoCaptionService, |
158 | VideoImportService, | 178 | VideoImportService, |
@@ -172,10 +192,20 @@ import { UserModerationDropdownComponent } from '@app/shared/moderation/user-mod | |||
172 | OverviewService, | 192 | OverviewService, |
173 | VideoChangeOwnershipValidatorsService, | 193 | VideoChangeOwnershipValidatorsService, |
174 | VideoAcceptOwnershipValidatorsService, | 194 | VideoAcceptOwnershipValidatorsService, |
195 | InstanceValidatorsService, | ||
196 | BlocklistService, | ||
197 | UserHistoryService, | ||
198 | InstanceService, | ||
199 | |||
200 | MarkdownService, | ||
201 | LinkifierService, | ||
202 | HtmlRendererService, | ||
175 | 203 | ||
176 | I18nPrimengCalendarService, | 204 | I18nPrimengCalendarService, |
177 | ScreenService, | 205 | ScreenService, |
178 | 206 | ||
207 | UserNotificationService, | ||
208 | |||
179 | I18n | 209 | I18n |
180 | ] | 210 | ] |
181 | }) | 211 | }) |
diff --git a/client/src/app/shared/user-subscription/remote-subscribe.component.ts b/client/src/app/shared/user-subscription/remote-subscribe.component.ts index 7a81108cd..ba2a45df1 100644 --- a/client/src/app/shared/user-subscription/remote-subscribe.component.ts +++ b/client/src/app/shared/user-subscription/remote-subscribe.component.ts | |||
@@ -29,7 +29,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit { | |||
29 | } | 29 | } |
30 | 30 | ||
31 | onValidKey () { | 31 | onValidKey () { |
32 | this.onValueChanged() | 32 | this.check() |
33 | if (!this.form.valid) return | 33 | if (!this.form.valid) return |
34 | 34 | ||
35 | this.formValidated() | 35 | this.formValidated() |
@@ -37,7 +37,24 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit { | |||
37 | 37 | ||
38 | formValidated () { | 38 | formValidated () { |
39 | const address = this.form.value['text'] | 39 | const address = this.form.value['text'] |
40 | const [ , hostname ] = address.split('@') | 40 | const [ username, hostname ] = address.split('@') |
41 | window.open(`https://${hostname}/authorize_interaction?acct=${this.account}`) | 41 | |
42 | fetch(`https://${hostname}/.well-known/webfinger?resource=acct:${username}@${hostname}`) | ||
43 | .then(response => response.json()) | ||
44 | .then(data => new Promise((resolve, reject) => { | ||
45 | if (data && Array.isArray(data.links)) { | ||
46 | const link: { | ||
47 | template: string | ||
48 | } = data.links.find((link: any) => | ||
49 | link && typeof link.template === 'string' && link.rel === 'http://ostatus.org/schema/1.0/subscribe') | ||
50 | |||
51 | if (link && link.template.includes('{uri}')) { | ||
52 | resolve(link.template.replace('{uri}', `acct:${this.account}`)) | ||
53 | } | ||
54 | } | ||
55 | reject() | ||
56 | })) | ||
57 | .then(window.open) | ||
58 | .catch(() => window.open(`https://${hostname}/authorize_interaction?acct=${this.account}`)) | ||
42 | } | 59 | } |
43 | } | 60 | } |
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.ts b/client/src/app/shared/user-subscription/subscribe-button.component.ts index 315ea5037..8f1754c7f 100644 --- a/client/src/app/shared/user-subscription/subscribe-button.component.ts +++ b/client/src/app/shared/user-subscription/subscribe-button.component.ts | |||
@@ -1,9 +1,8 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { AuthService } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' | 4 | import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' |
5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
6 | import { NotificationsService } from 'angular2-notifications' | ||
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { VideoService } from '@app/shared/video/video.service' | 7 | import { VideoService } from '@app/shared/video/video.service' |
9 | import { FeedFormat } from '../../../../../shared/models/feeds' | 8 | import { FeedFormat } from '../../../../../shared/models/feeds' |
@@ -23,7 +22,7 @@ export class SubscribeButtonComponent implements OnInit { | |||
23 | constructor ( | 22 | constructor ( |
24 | private authService: AuthService, | 23 | private authService: AuthService, |
25 | private router: Router, | 24 | private router: Router, |
26 | private notificationsService: NotificationsService, | 25 | private notifier: Notifier, |
27 | private userSubscriptionService: UserSubscriptionService, | 26 | private userSubscriptionService: UserSubscriptionService, |
28 | private i18n: I18n, | 27 | private i18n: I18n, |
29 | private videoService: VideoService | 28 | private videoService: VideoService |
@@ -43,18 +42,17 @@ export class SubscribeButtonComponent implements OnInit { | |||
43 | .subscribe( | 42 | .subscribe( |
44 | res => this.subscribed = res[this.uri], | 43 | res => this.subscribed = res[this.uri], |
45 | 44 | ||
46 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 45 | err => this.notifier.error(err.message) |
47 | ) | 46 | ) |
48 | } | 47 | } |
49 | } | 48 | } |
50 | 49 | ||
51 | subscribe () { | 50 | subscribe () { |
52 | if (this.isUserLoggedIn()) { | 51 | if (this.isUserLoggedIn()) { |
53 | this.localSubscribe() | 52 | return this.localSubscribe() |
54 | } else { | ||
55 | this.authService.redirectUrl = this.router.url | ||
56 | this.gotoLogin() | ||
57 | } | 53 | } |
54 | |||
55 | return this.gotoLogin() | ||
58 | } | 56 | } |
59 | 57 | ||
60 | localSubscribe () { | 58 | localSubscribe () { |
@@ -63,13 +61,13 @@ export class SubscribeButtonComponent implements OnInit { | |||
63 | () => { | 61 | () => { |
64 | this.subscribed = true | 62 | this.subscribed = true |
65 | 63 | ||
66 | this.notificationsService.success( | 64 | this.notifier.success( |
67 | this.i18n('Subscribed'), | 65 | this.i18n('Subscribed to {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName }), |
68 | this.i18n('Subscribed to {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName }) | 66 | this.i18n('Subscribed') |
69 | ) | 67 | ) |
70 | }, | 68 | }, |
71 | 69 | ||
72 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 70 | err => this.notifier.error(err.message) |
73 | ) | 71 | ) |
74 | } | 72 | } |
75 | 73 | ||
@@ -85,13 +83,13 @@ export class SubscribeButtonComponent implements OnInit { | |||
85 | () => { | 83 | () => { |
86 | this.subscribed = false | 84 | this.subscribed = false |
87 | 85 | ||
88 | this.notificationsService.success( | 86 | this.notifier.success( |
89 | this.i18n('Unsubscribed'), | 87 | this.i18n('Unsubscribed from {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName }), |
90 | this.i18n('Unsubscribed from {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName }) | 88 | this.i18n('Unsubscribed') |
91 | ) | 89 | ) |
92 | }, | 90 | }, |
93 | 91 | ||
94 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 92 | err => this.notifier.error(err.message) |
95 | ) | 93 | ) |
96 | } | 94 | } |
97 | 95 | ||
diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts index 7b5a67bc7..ebd715fb1 100644 --- a/client/src/app/shared/users/index.ts +++ b/client/src/app/shared/users/index.ts | |||
@@ -1,2 +1,3 @@ | |||
1 | export * from './user.model' | 1 | export * from './user.model' |
2 | export * from './user.service' | 2 | export * from './user.service' |
3 | export * from './user-notifications.component' | ||
diff --git a/client/src/app/shared/users/user-history.service.ts b/client/src/app/shared/users/user-history.service.ts new file mode 100644 index 000000000..9ed25bfc7 --- /dev/null +++ b/client/src/app/shared/users/user-history.service.ts | |||
@@ -0,0 +1,45 @@ | |||
1 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
2 | import { Injectable } from '@angular/core' | ||
3 | import { environment } from '../../../environments/environment' | ||
4 | import { RestExtractor } from '../rest/rest-extractor.service' | ||
5 | import { RestService } from '../rest/rest.service' | ||
6 | import { Video } from '../video/video.model' | ||
7 | import { catchError, map, switchMap } from 'rxjs/operators' | ||
8 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | ||
9 | import { VideoService } from '@app/shared/video/video.service' | ||
10 | import { ResultList } from '../../../../../shared' | ||
11 | |||
12 | @Injectable() | ||
13 | export class UserHistoryService { | ||
14 | static BASE_USER_VIDEOS_HISTORY_URL = environment.apiUrl + '/api/v1/users/me/history/videos' | ||
15 | |||
16 | constructor ( | ||
17 | private authHttp: HttpClient, | ||
18 | private restExtractor: RestExtractor, | ||
19 | private restService: RestService, | ||
20 | private videoService: VideoService | ||
21 | ) {} | ||
22 | |||
23 | getUserVideosHistory (historyPagination: ComponentPagination) { | ||
24 | const pagination = this.restService.componentPaginationToRestPagination(historyPagination) | ||
25 | |||
26 | let params = new HttpParams() | ||
27 | params = this.restService.addRestGetParams(params, pagination) | ||
28 | |||
29 | return this.authHttp | ||
30 | .get<ResultList<Video>>(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL, { params }) | ||
31 | .pipe( | ||
32 | switchMap(res => this.videoService.extractVideos(res)), | ||
33 | catchError(err => this.restExtractor.handleError(err)) | ||
34 | ) | ||
35 | } | ||
36 | |||
37 | deleteUserVideosHistory () { | ||
38 | return this.authHttp | ||
39 | .post(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/remove', {}) | ||
40 | .pipe( | ||
41 | map(() => this.restExtractor.extractDataBool()), | ||
42 | catchError(err => this.restExtractor.handleError(err)) | ||
43 | ) | ||
44 | } | ||
45 | } | ||
diff --git a/client/src/app/shared/users/user-notification.model.ts b/client/src/app/shared/users/user-notification.model.ts new file mode 100644 index 000000000..125d2120c --- /dev/null +++ b/client/src/app/shared/users/user-notification.model.ts | |||
@@ -0,0 +1,155 @@ | |||
1 | import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo, ActorInfo } from '../../../../../shared' | ||
2 | import { Actor } from '@app/shared/actor/actor.model' | ||
3 | |||
4 | export class UserNotification implements UserNotificationServer { | ||
5 | id: number | ||
6 | type: UserNotificationType | ||
7 | read: boolean | ||
8 | |||
9 | video?: VideoInfo & { | ||
10 | channel: ActorInfo & { avatarUrl?: string } | ||
11 | } | ||
12 | |||
13 | videoImport?: { | ||
14 | id: number | ||
15 | video?: VideoInfo | ||
16 | torrentName?: string | ||
17 | magnetUri?: string | ||
18 | targetUrl?: string | ||
19 | } | ||
20 | |||
21 | comment?: { | ||
22 | id: number | ||
23 | threadId: number | ||
24 | account: ActorInfo & { avatarUrl?: string } | ||
25 | video: VideoInfo | ||
26 | } | ||
27 | |||
28 | videoAbuse?: { | ||
29 | id: number | ||
30 | video: VideoInfo | ||
31 | } | ||
32 | |||
33 | videoBlacklist?: { | ||
34 | id: number | ||
35 | video: VideoInfo | ||
36 | } | ||
37 | |||
38 | account?: ActorInfo & { avatarUrl?: string } | ||
39 | |||
40 | actorFollow?: { | ||
41 | id: number | ||
42 | follower: ActorInfo & { avatarUrl?: string } | ||
43 | following: { | ||
44 | type: 'account' | 'channel' | ||
45 | name: string | ||
46 | displayName: string | ||
47 | } | ||
48 | } | ||
49 | |||
50 | createdAt: string | ||
51 | updatedAt: string | ||
52 | |||
53 | // Additional fields | ||
54 | videoUrl?: string | ||
55 | commentUrl?: any[] | ||
56 | videoAbuseUrl?: string | ||
57 | accountUrl?: string | ||
58 | videoImportIdentifier?: string | ||
59 | videoImportUrl?: string | ||
60 | |||
61 | constructor (hash: UserNotificationServer) { | ||
62 | this.id = hash.id | ||
63 | this.type = hash.type | ||
64 | this.read = hash.read | ||
65 | |||
66 | this.video = hash.video | ||
67 | if (this.video) this.setAvatarUrl(this.video.channel) | ||
68 | |||
69 | this.videoImport = hash.videoImport | ||
70 | |||
71 | this.comment = hash.comment | ||
72 | if (this.comment) this.setAvatarUrl(this.comment.account) | ||
73 | |||
74 | this.videoAbuse = hash.videoAbuse | ||
75 | |||
76 | this.videoBlacklist = hash.videoBlacklist | ||
77 | |||
78 | this.account = hash.account | ||
79 | if (this.account) this.setAvatarUrl(this.account) | ||
80 | |||
81 | this.actorFollow = hash.actorFollow | ||
82 | if (this.actorFollow) this.setAvatarUrl(this.actorFollow.follower) | ||
83 | |||
84 | this.createdAt = hash.createdAt | ||
85 | this.updatedAt = hash.updatedAt | ||
86 | |||
87 | switch (this.type) { | ||
88 | case UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION: | ||
89 | this.videoUrl = this.buildVideoUrl(this.video) | ||
90 | break | ||
91 | |||
92 | case UserNotificationType.UNBLACKLIST_ON_MY_VIDEO: | ||
93 | this.videoUrl = this.buildVideoUrl(this.video) | ||
94 | break | ||
95 | |||
96 | case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO: | ||
97 | case UserNotificationType.COMMENT_MENTION: | ||
98 | this.accountUrl = this.buildAccountUrl(this.comment.account) | ||
99 | this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ] | ||
100 | break | ||
101 | |||
102 | case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS: | ||
103 | this.videoAbuseUrl = '/admin/moderation/video-abuses/list' | ||
104 | this.videoUrl = this.buildVideoUrl(this.videoAbuse.video) | ||
105 | break | ||
106 | |||
107 | case UserNotificationType.BLACKLIST_ON_MY_VIDEO: | ||
108 | this.videoUrl = this.buildVideoUrl(this.videoBlacklist.video) | ||
109 | break | ||
110 | |||
111 | case UserNotificationType.MY_VIDEO_PUBLISHED: | ||
112 | this.videoUrl = this.buildVideoUrl(this.video) | ||
113 | break | ||
114 | |||
115 | case UserNotificationType.MY_VIDEO_IMPORT_SUCCESS: | ||
116 | this.videoImportUrl = this.buildVideoImportUrl() | ||
117 | this.videoImportIdentifier = this.buildVideoImportIdentifier(this.videoImport) | ||
118 | this.videoUrl = this.buildVideoUrl(this.videoImport.video) | ||
119 | break | ||
120 | |||
121 | case UserNotificationType.MY_VIDEO_IMPORT_ERROR: | ||
122 | this.videoImportUrl = this.buildVideoImportUrl() | ||
123 | this.videoImportIdentifier = this.buildVideoImportIdentifier(this.videoImport) | ||
124 | break | ||
125 | |||
126 | case UserNotificationType.NEW_USER_REGISTRATION: | ||
127 | this.accountUrl = this.buildAccountUrl(this.account) | ||
128 | break | ||
129 | |||
130 | case UserNotificationType.NEW_FOLLOW: | ||
131 | this.accountUrl = this.buildAccountUrl(this.actorFollow.follower) | ||
132 | break | ||
133 | } | ||
134 | } | ||
135 | |||
136 | private buildVideoUrl (video: { uuid: string }) { | ||
137 | return '/videos/watch/' + video.uuid | ||
138 | } | ||
139 | |||
140 | private buildAccountUrl (account: { name: string, host: string }) { | ||
141 | return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host) | ||
142 | } | ||
143 | |||
144 | private buildVideoImportUrl () { | ||
145 | return '/my-account/video-imports' | ||
146 | } | ||
147 | |||
148 | private buildVideoImportIdentifier (videoImport: { targetUrl?: string, magnetUri?: string, torrentName?: string }) { | ||
149 | return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName | ||
150 | } | ||
151 | |||
152 | private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { path: string } }) { | ||
153 | actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor) | ||
154 | } | ||
155 | } | ||
diff --git a/client/src/app/shared/users/user-notification.service.ts b/client/src/app/shared/users/user-notification.service.ts new file mode 100644 index 000000000..f8a30955d --- /dev/null +++ b/client/src/app/shared/users/user-notification.service.ts | |||
@@ -0,0 +1,86 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
3 | import { RestExtractor, RestService } from '../rest' | ||
4 | import { catchError, map, tap } from 'rxjs/operators' | ||
5 | import { environment } from '../../../environments/environment' | ||
6 | import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '../../../../../shared' | ||
7 | import { UserNotification } from './user-notification.model' | ||
8 | import { AuthService } from '../../core' | ||
9 | import { ComponentPagination } from '../rest/component-pagination.model' | ||
10 | import { User } from '..' | ||
11 | import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' | ||
12 | |||
13 | @Injectable() | ||
14 | export class UserNotificationService { | ||
15 | static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications' | ||
16 | static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings' | ||
17 | |||
18 | constructor ( | ||
19 | private auth: AuthService, | ||
20 | private authHttp: HttpClient, | ||
21 | private restExtractor: RestExtractor, | ||
22 | private restService: RestService, | ||
23 | private userNotificationSocket: UserNotificationSocket | ||
24 | ) {} | ||
25 | |||
26 | listMyNotifications (pagination: ComponentPagination, unread?: boolean, ignoreLoadingBar = false) { | ||
27 | let params = new HttpParams() | ||
28 | params = this.restService.addRestGetParams(params, this.restService.componentPaginationToRestPagination(pagination)) | ||
29 | |||
30 | if (unread) params = params.append('unread', `${unread}`) | ||
31 | |||
32 | const headers = ignoreLoadingBar ? { ignoreLoadingBar: '' } : undefined | ||
33 | |||
34 | return this.authHttp.get<ResultList<UserNotification>>(UserNotificationService.BASE_NOTIFICATIONS_URL, { params, headers }) | ||
35 | .pipe( | ||
36 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | ||
37 | map(res => this.restExtractor.applyToResultListData(res, this.formatNotification.bind(this))), | ||
38 | catchError(err => this.restExtractor.handleError(err)) | ||
39 | ) | ||
40 | } | ||
41 | |||
42 | countUnreadNotifications () { | ||
43 | return this.listMyNotifications({ currentPage: 1, itemsPerPage: 0 }, true) | ||
44 | .pipe(map(n => n.total)) | ||
45 | } | ||
46 | |||
47 | markAsRead (notification: UserNotification) { | ||
48 | const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read' | ||
49 | |||
50 | const body = { ids: [ notification.id ] } | ||
51 | const headers = { ignoreLoadingBar: '' } | ||
52 | |||
53 | return this.authHttp.post(url, body, { headers }) | ||
54 | .pipe( | ||
55 | map(this.restExtractor.extractDataBool), | ||
56 | tap(() => this.userNotificationSocket.dispatch('read')), | ||
57 | catchError(res => this.restExtractor.handleError(res)) | ||
58 | ) | ||
59 | } | ||
60 | |||
61 | markAllAsRead () { | ||
62 | const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read-all' | ||
63 | const headers = { ignoreLoadingBar: '' } | ||
64 | |||
65 | return this.authHttp.post(url, {}, { headers }) | ||
66 | .pipe( | ||
67 | map(this.restExtractor.extractDataBool), | ||
68 | tap(() => this.userNotificationSocket.dispatch('read-all')), | ||
69 | catchError(res => this.restExtractor.handleError(res)) | ||
70 | ) | ||
71 | } | ||
72 | |||
73 | updateNotificationSettings (user: User, settings: UserNotificationSetting) { | ||
74 | const url = UserNotificationService.BASE_NOTIFICATION_SETTINGS | ||
75 | |||
76 | return this.authHttp.put(url, settings) | ||
77 | .pipe( | ||
78 | map(this.restExtractor.extractDataBool), | ||
79 | catchError(res => this.restExtractor.handleError(res)) | ||
80 | ) | ||
81 | } | ||
82 | |||
83 | private formatNotification (notification: UserNotificationServer) { | ||
84 | return new UserNotification(notification) | ||
85 | } | ||
86 | } | ||
diff --git a/client/src/app/shared/users/user-notifications.component.html b/client/src/app/shared/users/user-notifications.component.html new file mode 100644 index 000000000..0d69e0feb --- /dev/null +++ b/client/src/app/shared/users/user-notifications.component.html | |||
@@ -0,0 +1,101 @@ | |||
1 | <div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div> | ||
2 | |||
3 | <div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"> | ||
4 | <div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)"> | ||
5 | |||
6 | <ng-container [ngSwitch]="notification.type"> | ||
7 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION"> | ||
8 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" /> | ||
9 | |||
10 | <div class="message"> | ||
11 | {{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a> | ||
12 | </div> | ||
13 | </ng-container> | ||
14 | |||
15 | <ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO"> | ||
16 | <my-global-icon iconName="undo"></my-global-icon> | ||
17 | |||
18 | <div class="message"> | ||
19 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted | ||
20 | </div> | ||
21 | </ng-container> | ||
22 | |||
23 | <ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO"> | ||
24 | <my-global-icon iconName="no"></my-global-icon> | ||
25 | |||
26 | <div class="message"> | ||
27 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted | ||
28 | </div> | ||
29 | </ng-container> | ||
30 | |||
31 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS"> | ||
32 | <my-global-icon iconName="alert"></my-global-icon> | ||
33 | |||
34 | <div class="message"> | ||
35 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a> | ||
36 | </div> | ||
37 | </ng-container> | ||
38 | |||
39 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO"> | ||
40 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> | ||
41 | |||
42 | <div class="message"> | ||
43 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a> | ||
44 | </div> | ||
45 | </ng-container> | ||
46 | |||
47 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED"> | ||
48 | <my-global-icon iconName="sparkle"></my-global-icon> | ||
49 | |||
50 | <div class="message"> | ||
51 | Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published | ||
52 | </div> | ||
53 | </ng-container> | ||
54 | |||
55 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS"> | ||
56 | <my-global-icon iconName="cloud-download"></my-global-icon> | ||
57 | |||
58 | <div class="message"> | ||
59 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded | ||
60 | </div> | ||
61 | </ng-container> | ||
62 | |||
63 | <ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR"> | ||
64 | <my-global-icon iconName="cloud-error"></my-global-icon> | ||
65 | |||
66 | <div class="message"> | ||
67 | <a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed | ||
68 | </div> | ||
69 | </ng-container> | ||
70 | |||
71 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION"> | ||
72 | <my-global-icon iconName="user-add"></my-global-icon> | ||
73 | |||
74 | <div class="message"> | ||
75 | User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance | ||
76 | </div> | ||
77 | </ng-container> | ||
78 | |||
79 | <ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW"> | ||
80 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" /> | ||
81 | |||
82 | <div class="message"> | ||
83 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following | ||
84 | |||
85 | <ng-container *ngIf="notification.actorFollow.following.type === 'channel'">your channel {{ notification.actorFollow.following.displayName }}</ng-container> | ||
86 | <ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container> | ||
87 | </div> | ||
88 | </ng-container> | ||
89 | |||
90 | <ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION"> | ||
91 | <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" /> | ||
92 | |||
93 | <div class="message"> | ||
94 | <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a> | ||
95 | </div> | ||
96 | </ng-container> | ||
97 | </ng-container> | ||
98 | |||
99 | <div class="from-date">{{ notification.createdAt | myFromNow }}</div> | ||
100 | </div> | ||
101 | </div> | ||
diff --git a/client/src/app/shared/users/user-notifications.component.scss b/client/src/app/shared/users/user-notifications.component.scss new file mode 100644 index 000000000..315d504c9 --- /dev/null +++ b/client/src/app/shared/users/user-notifications.component.scss | |||
@@ -0,0 +1,51 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .no-notification { | ||
5 | display: flex; | ||
6 | justify-content: center; | ||
7 | align-items: center; | ||
8 | padding: 20px 0; | ||
9 | } | ||
10 | |||
11 | .notification { | ||
12 | display: flex; | ||
13 | align-items: center; | ||
14 | font-size: inherit; | ||
15 | padding: 15px 5px 15px 10px; | ||
16 | border-bottom: 1px solid rgba(0, 0, 0, 0.10); | ||
17 | |||
18 | &.unread { | ||
19 | background-color: rgba(0, 0, 0, 0.05); | ||
20 | } | ||
21 | |||
22 | my-global-icon { | ||
23 | width: 24px; | ||
24 | margin-right: 11px; | ||
25 | margin-left: 3px; | ||
26 | |||
27 | @include apply-svg-color(#333); | ||
28 | } | ||
29 | |||
30 | .avatar { | ||
31 | @include avatar(30px); | ||
32 | |||
33 | margin-right: 10px; | ||
34 | } | ||
35 | |||
36 | .message { | ||
37 | flex-grow: 1; | ||
38 | |||
39 | a { | ||
40 | font-weight: $font-semibold; | ||
41 | } | ||
42 | } | ||
43 | |||
44 | .from-date { | ||
45 | font-size: 0.85em; | ||
46 | color: $grey-foreground-color; | ||
47 | padding-left: 5px; | ||
48 | min-width: 70px; | ||
49 | text-align: right; | ||
50 | } | ||
51 | } | ||
diff --git a/client/src/app/shared/users/user-notifications.component.ts b/client/src/app/shared/users/user-notifications.component.ts new file mode 100644 index 000000000..b5f9fd399 --- /dev/null +++ b/client/src/app/shared/users/user-notifications.component.ts | |||
@@ -0,0 +1,87 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { UserNotificationService } from '@app/shared/users/user-notification.service' | ||
3 | import { UserNotificationType } from '../../../../../shared' | ||
4 | import { ComponentPagination, hasMoreItems } from '@app/shared/rest/component-pagination.model' | ||
5 | import { Notifier } from '@app/core' | ||
6 | import { UserNotification } from '@app/shared/users/user-notification.model' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-user-notifications', | ||
10 | templateUrl: 'user-notifications.component.html', | ||
11 | styleUrls: [ 'user-notifications.component.scss' ] | ||
12 | }) | ||
13 | export class UserNotificationsComponent implements OnInit { | ||
14 | @Input() ignoreLoadingBar = false | ||
15 | @Input() infiniteScroll = true | ||
16 | @Input() itemsPerPage = 20 | ||
17 | |||
18 | notifications: UserNotification[] = [] | ||
19 | |||
20 | // So we can access it in the template | ||
21 | UserNotificationType = UserNotificationType | ||
22 | |||
23 | componentPagination: ComponentPagination | ||
24 | |||
25 | constructor ( | ||
26 | private userNotificationService: UserNotificationService, | ||
27 | private notifier: Notifier | ||
28 | ) { } | ||
29 | |||
30 | ngOnInit () { | ||
31 | this.componentPagination = { | ||
32 | currentPage: 1, | ||
33 | itemsPerPage: this.itemsPerPage, // Reset items per page, because of the @Input() variable | ||
34 | totalItems: null | ||
35 | } | ||
36 | |||
37 | this.loadMoreNotifications() | ||
38 | } | ||
39 | |||
40 | loadMoreNotifications () { | ||
41 | this.userNotificationService.listMyNotifications(this.componentPagination, undefined, this.ignoreLoadingBar) | ||
42 | .subscribe( | ||
43 | result => { | ||
44 | this.notifications = this.notifications.concat(result.data) | ||
45 | this.componentPagination.totalItems = result.total | ||
46 | }, | ||
47 | |||
48 | err => this.notifier.error(err.message) | ||
49 | ) | ||
50 | } | ||
51 | |||
52 | onNearOfBottom () { | ||
53 | if (this.infiniteScroll === false) return | ||
54 | |||
55 | this.componentPagination.currentPage++ | ||
56 | |||
57 | if (hasMoreItems(this.componentPagination)) { | ||
58 | this.loadMoreNotifications() | ||
59 | } | ||
60 | } | ||
61 | |||
62 | markAsRead (notification: UserNotification) { | ||
63 | if (notification.read) return | ||
64 | |||
65 | this.userNotificationService.markAsRead(notification) | ||
66 | .subscribe( | ||
67 | () => { | ||
68 | notification.read = true | ||
69 | }, | ||
70 | |||
71 | err => this.notifier.error(err.message) | ||
72 | ) | ||
73 | } | ||
74 | |||
75 | markAllAsRead () { | ||
76 | this.userNotificationService.markAllAsRead() | ||
77 | .subscribe( | ||
78 | () => { | ||
79 | for (const notification of this.notifications) { | ||
80 | notification.read = true | ||
81 | } | ||
82 | }, | ||
83 | |||
84 | err => this.notifier.error(err.message) | ||
85 | ) | ||
86 | } | ||
87 | } | ||
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 877f1bf3a..c15f1de8c 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts | |||
@@ -1,38 +1,20 @@ | |||
1 | import { | 1 | import { hasUserRight, User as UserServerModel, UserNotificationSetting, UserRight, UserRole, VideoChannel } from '../../../../../shared' |
2 | Account as AccountServerModel, | ||
3 | hasUserRight, | ||
4 | User as UserServerModel, | ||
5 | UserRight, | ||
6 | UserRole, | ||
7 | VideoChannel | ||
8 | } from '../../../../../shared' | ||
9 | import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' | 2 | import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' |
10 | import { Account } from '@app/shared/account/account.model' | 3 | import { Account } from '@app/shared/account/account.model' |
11 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 4 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
12 | 5 | ||
13 | export type UserConstructorHash = { | ||
14 | id: number, | ||
15 | username: string, | ||
16 | email: string, | ||
17 | role: UserRole, | ||
18 | videoQuota?: number, | ||
19 | videoQuotaDaily?: number, | ||
20 | nsfwPolicy?: NSFWPolicyType, | ||
21 | autoPlayVideo?: boolean, | ||
22 | createdAt?: Date, | ||
23 | account?: AccountServerModel, | ||
24 | videoChannels?: VideoChannel[] | ||
25 | |||
26 | blocked?: boolean | ||
27 | blockedReason?: string | ||
28 | } | ||
29 | export class User implements UserServerModel { | 6 | export class User implements UserServerModel { |
30 | id: number | 7 | id: number |
31 | username: string | 8 | username: string |
32 | email: string | 9 | email: string |
10 | emailVerified: boolean | ||
33 | role: UserRole | 11 | role: UserRole |
34 | nsfwPolicy: NSFWPolicyType | 12 | nsfwPolicy: NSFWPolicyType |
13 | |||
14 | webTorrentEnabled: boolean | ||
35 | autoPlayVideo: boolean | 15 | autoPlayVideo: boolean |
16 | videosHistoryEnabled: boolean | ||
17 | |||
36 | videoQuota: number | 18 | videoQuota: number |
37 | videoQuotaDaily: number | 19 | videoQuotaDaily: number |
38 | account: Account | 20 | account: Account |
@@ -42,7 +24,9 @@ export class User implements UserServerModel { | |||
42 | blocked: boolean | 24 | blocked: boolean |
43 | blockedReason?: string | 25 | blockedReason?: string |
44 | 26 | ||
45 | constructor (hash: UserConstructorHash) { | 27 | notificationSettings?: UserNotificationSetting |
28 | |||
29 | constructor (hash: Partial<UserServerModel>) { | ||
46 | this.id = hash.id | 30 | this.id = hash.id |
47 | this.username = hash.username | 31 | this.username = hash.username |
48 | this.email = hash.email | 32 | this.email = hash.email |
@@ -52,11 +36,15 @@ export class User implements UserServerModel { | |||
52 | this.videoQuota = hash.videoQuota | 36 | this.videoQuota = hash.videoQuota |
53 | this.videoQuotaDaily = hash.videoQuotaDaily | 37 | this.videoQuotaDaily = hash.videoQuotaDaily |
54 | this.nsfwPolicy = hash.nsfwPolicy | 38 | this.nsfwPolicy = hash.nsfwPolicy |
39 | this.webTorrentEnabled = hash.webTorrentEnabled | ||
40 | this.videosHistoryEnabled = hash.videosHistoryEnabled | ||
55 | this.autoPlayVideo = hash.autoPlayVideo | 41 | this.autoPlayVideo = hash.autoPlayVideo |
56 | this.createdAt = hash.createdAt | 42 | this.createdAt = hash.createdAt |
57 | this.blocked = hash.blocked | 43 | this.blocked = hash.blocked |
58 | this.blockedReason = hash.blockedReason | 44 | this.blockedReason = hash.blockedReason |
59 | 45 | ||
46 | this.notificationSettings = hash.notificationSettings | ||
47 | |||
60 | if (hash.account !== undefined) { | 48 | if (hash.account !== undefined) { |
61 | this.account = new Account(hash.account) | 49 | this.account = new Account(hash.account) |
62 | } | 50 | } |
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index d9b81c181..cc5c051f1 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Observable } from 'rxjs' | 1 | import { from, Observable } from 'rxjs' |
2 | import { catchError, map } from 'rxjs/operators' | 2 | import { catchError, concatMap, map, toArray } from 'rxjs/operators' |
3 | import { HttpClient, HttpParams } from '@angular/common/http' | 3 | import { HttpClient, HttpParams } from '@angular/common/http' |
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { ResultList, User, UserCreate, UserRole, UserUpdate, UserUpdateMe, UserVideoQuota } from '../../../../../shared' | 5 | import { ResultList, User, UserCreate, UserRole, UserUpdate, UserUpdateMe, UserVideoQuota } from '../../../../../shared' |
@@ -153,15 +153,26 @@ export class UserService { | |||
153 | ) | 153 | ) |
154 | } | 154 | } |
155 | 155 | ||
156 | updateUsers (users: User[], userUpdate: UserUpdate) { | ||
157 | return from(users) | ||
158 | .pipe( | ||
159 | concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)), | ||
160 | toArray(), | ||
161 | catchError(err => this.restExtractor.handleError(err)) | ||
162 | ) | ||
163 | } | ||
164 | |||
156 | getUser (userId: number) { | 165 | getUser (userId: number) { |
157 | return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId) | 166 | return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId) |
158 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 167 | .pipe(catchError(err => this.restExtractor.handleError(err))) |
159 | } | 168 | } |
160 | 169 | ||
161 | getUsers (pagination: RestPagination, sort: SortMeta): Observable<ResultList<User>> { | 170 | getUsers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<User>> { |
162 | let params = new HttpParams() | 171 | let params = new HttpParams() |
163 | params = this.restService.addRestGetParams(params, pagination, sort) | 172 | params = this.restService.addRestGetParams(params, pagination, sort) |
164 | 173 | ||
174 | if (search) params = params.append('search', search) | ||
175 | |||
165 | return this.authHttp.get<ResultList<User>>(UserService.BASE_USERS_URL, { params }) | 176 | return this.authHttp.get<ResultList<User>>(UserService.BASE_USERS_URL, { params }) |
166 | .pipe( | 177 | .pipe( |
167 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | 178 | map(res => this.restExtractor.convertResultListDateToHuman(res)), |
@@ -170,21 +181,38 @@ export class UserService { | |||
170 | ) | 181 | ) |
171 | } | 182 | } |
172 | 183 | ||
173 | removeUser (user: { id: number }) { | 184 | removeUser (usersArg: User | User[]) { |
174 | return this.authHttp.delete(UserService.BASE_USERS_URL + user.id) | 185 | const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] |
175 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 186 | |
187 | return from(users) | ||
188 | .pipe( | ||
189 | concatMap(u => this.authHttp.delete(UserService.BASE_USERS_URL + u.id)), | ||
190 | toArray(), | ||
191 | catchError(err => this.restExtractor.handleError(err)) | ||
192 | ) | ||
176 | } | 193 | } |
177 | 194 | ||
178 | banUser (user: { id: number }, reason?: string) { | 195 | banUsers (usersArg: User | User[], reason?: string) { |
179 | const body = reason ? { reason } : {} | 196 | const body = reason ? { reason } : {} |
197 | const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] | ||
180 | 198 | ||
181 | return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/block', body) | 199 | return from(users) |
182 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 200 | .pipe( |
201 | concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/block', body)), | ||
202 | toArray(), | ||
203 | catchError(err => this.restExtractor.handleError(err)) | ||
204 | ) | ||
183 | } | 205 | } |
184 | 206 | ||
185 | unbanUser (user: { id: number }) { | 207 | unbanUsers (usersArg: User | User[]) { |
186 | return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/unblock', {}) | 208 | const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] |
187 | .pipe(catchError(err => this.restExtractor.handleError(err))) | 209 | |
210 | return from(users) | ||
211 | .pipe( | ||
212 | concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/unblock', {})), | ||
213 | toArray(), | ||
214 | catchError(err => this.restExtractor.handleError(err)) | ||
215 | ) | ||
188 | } | 216 | } |
189 | 217 | ||
190 | private formatUser (user: User) { | 218 | private formatUser (user: User) { |
diff --git a/client/src/app/shared/video-abuse/video-abuse.service.ts b/client/src/app/shared/video-abuse/video-abuse.service.ts index 61b7e1b98..b0b59ea0c 100644 --- a/client/src/app/shared/video-abuse/video-abuse.service.ts +++ b/client/src/app/shared/video-abuse/video-abuse.service.ts | |||
@@ -32,9 +32,7 @@ export class VideoAbuseService { | |||
32 | 32 | ||
33 | reportVideo (id: number, reason: string) { | 33 | reportVideo (id: number, reason: string) { |
34 | const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse' | 34 | const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse' |
35 | const body = { | 35 | const body = { reason } |
36 | reason | ||
37 | } | ||
38 | 36 | ||
39 | return this.authHttp.post(url, body) | 37 | return this.authHttp.post(url, body) |
40 | .pipe( | 38 | .pipe( |
diff --git a/client/src/app/shared/video-blacklist/video-blacklist.service.ts b/client/src/app/shared/video-blacklist/video-blacklist.service.ts index 7d39fd4f2..94e46d7c2 100644 --- a/client/src/app/shared/video-blacklist/video-blacklist.service.ts +++ b/client/src/app/shared/video-blacklist/video-blacklist.service.ts | |||
@@ -36,8 +36,11 @@ export class VideoBlacklistService { | |||
36 | ) | 36 | ) |
37 | } | 37 | } |
38 | 38 | ||
39 | blacklistVideo (videoId: number, reason?: string) { | 39 | blacklistVideo (videoId: number, reason: string, unfederate: boolean) { |
40 | const body = reason ? { reason } : {} | 40 | const body = { |
41 | unfederate, | ||
42 | reason | ||
43 | } | ||
41 | 44 | ||
42 | return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', body) | 45 | return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', body) |
43 | .pipe( | 46 | .pipe( |
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index d543ab7c1..1f97bc389 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html | |||
@@ -1,8 +1,21 @@ | |||
1 | <div [ngClass]="{ 'margin-content': marginContent }"> | 1 | <div [ngClass]="{ 'margin-content': marginContent }"> |
2 | <div *ngIf="titlePage" class="title-page title-page-single"> | 2 | <div class="videos-header"> |
3 | {{ titlePage }} | 3 | <div *ngIf="titlePage" class="title-page title-page-single"> |
4 | <div placement="bottom" [ngbTooltip]="titleTooltip" container="body"> | ||
5 | {{ titlePage }} | ||
6 | </div> | ||
7 | </div> | ||
8 | |||
9 | <my-feed [syndicationItems]="syndicationItems"></my-feed> | ||
10 | |||
11 | <div class="moderation-block" *ngIf="displayModerationBlock"> | ||
12 | <my-peertube-checkbox | ||
13 | (change)="toggleModerationDisplay()" | ||
14 | inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos" | ||
15 | > | ||
16 | </my-peertube-checkbox> | ||
17 | </div> | ||
4 | </div> | 18 | </div> |
5 | <my-video-feed [syndicationItems]="syndicationItems"></my-video-feed> | ||
6 | 19 | ||
7 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div> | 20 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div> |
8 | <div | 21 | <div |
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss index 3f9c73a29..292ede698 100644 --- a/client/src/app/shared/video/abstract-video-list.scss +++ b/client/src/app/shared/video/abstract-video-list.scss | |||
@@ -8,12 +8,27 @@ | |||
8 | } | 8 | } |
9 | } | 9 | } |
10 | 10 | ||
11 | .title-page.title-page-single { | 11 | .videos-header { |
12 | margin-right: 5px; | 12 | display: flex; |
13 | } | 13 | height: 80px; |
14 | align-items: center; | ||
15 | |||
16 | .title-page.title-page-single { | ||
17 | margin: 0 5px 0 0; | ||
18 | } | ||
14 | 19 | ||
15 | my-video-feed { | 20 | my-feed { |
16 | display: inline-block; | 21 | display: inline-block; |
22 | top: 1px; | ||
23 | min-width: 60px; | ||
24 | } | ||
25 | |||
26 | .moderation-block { | ||
27 | display: flex; | ||
28 | flex-grow: 1; | ||
29 | justify-content: flex-end; | ||
30 | align-items: center; | ||
31 | } | ||
17 | } | 32 | } |
18 | 33 | ||
19 | @media screen and (max-width: 500px) { | 34 | @media screen and (max-width: 500px) { |
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index 763791165..b0633be4a 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts | |||
@@ -3,7 +3,6 @@ import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core' | |||
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { Location } from '@angular/common' | 4 | import { Location } from '@angular/common' |
5 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' | 5 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' |
6 | import { NotificationsService } from 'angular2-notifications' | ||
7 | import { fromEvent, Observable, Subscription } from 'rxjs' | 6 | import { fromEvent, Observable, Subscription } from 'rxjs' |
8 | import { AuthService } from '../../core/auth' | 7 | import { AuthService } from '../../core/auth' |
9 | import { ComponentPagination } from '../rest/component-pagination.model' | 8 | import { ComponentPagination } from '../rest/component-pagination.model' |
@@ -12,6 +11,8 @@ import { Video } from './video.model' | |||
12 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
13 | import { ScreenService } from '@app/shared/misc/screen.service' | 12 | import { ScreenService } from '@app/shared/misc/screen.service' |
14 | import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' | 13 | import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' |
14 | import { Syndication } from '@app/shared/video/syndication.model' | ||
15 | import { Notifier } from '@app/core' | ||
15 | 16 | ||
16 | export abstract class AbstractVideoList implements OnInit, OnDestroy { | 17 | export abstract class AbstractVideoList implements OnInit, OnDestroy { |
17 | private static LINES_PER_PAGE = 4 | 18 | private static LINES_PER_PAGE = 4 |
@@ -27,7 +28,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
27 | sort: VideoSortField = '-publishedAt' | 28 | sort: VideoSortField = '-publishedAt' |
28 | categoryOneOf?: number | 29 | categoryOneOf?: number |
29 | defaultSort: VideoSortField = '-publishedAt' | 30 | defaultSort: VideoSortField = '-publishedAt' |
30 | syndicationItems = [] | 31 | syndicationItems: Syndication[] = [] |
31 | 32 | ||
32 | loadOnInit = true | 33 | loadOnInit = true |
33 | marginContent = true | 34 | marginContent = true |
@@ -37,11 +38,13 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
37 | videoPages: Video[][] = [] | 38 | videoPages: Video[][] = [] |
38 | ownerDisplayType: OwnerDisplayType = 'account' | 39 | ownerDisplayType: OwnerDisplayType = 'account' |
39 | firstLoadedPage: number | 40 | firstLoadedPage: number |
41 | displayModerationBlock = false | ||
42 | titleTooltip: string | ||
40 | 43 | ||
41 | protected baseVideoWidth = 215 | 44 | protected baseVideoWidth = 215 |
42 | protected baseVideoHeight = 205 | 45 | protected baseVideoHeight = 205 |
43 | 46 | ||
44 | protected abstract notificationsService: NotificationsService | 47 | protected abstract notifier: Notifier |
45 | protected abstract authService: AuthService | 48 | protected abstract authService: AuthService |
46 | protected abstract router: Router | 49 | protected abstract router: Router |
47 | protected abstract route: ActivatedRoute | 50 | protected abstract route: ActivatedRoute |
@@ -58,7 +61,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
58 | private resizeSubscription: Subscription | 61 | private resizeSubscription: Subscription |
59 | 62 | ||
60 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}> | 63 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}> |
61 | abstract generateSyndicationList () | 64 | abstract generateSyndicationList (): void |
62 | 65 | ||
63 | get user () { | 66 | get user () { |
64 | return this.authService.getUser() | 67 | return this.authService.getUser() |
@@ -155,11 +158,15 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
155 | }, | 158 | }, |
156 | error => { | 159 | error => { |
157 | this.loadingPage[page] = false | 160 | this.loadingPage[page] = false |
158 | this.notificationsService.error(this.i18n('Error'), error.message) | 161 | this.notifier.error(error.message) |
159 | } | 162 | } |
160 | ) | 163 | ) |
161 | } | 164 | } |
162 | 165 | ||
166 | toggleModerationDisplay () { | ||
167 | throw new Error('toggleModerationDisplay is not implemented') | ||
168 | } | ||
169 | |||
163 | protected hasMoreVideos () { | 170 | protected hasMoreVideos () { |
164 | // No results | 171 | // No results |
165 | if (this.pagination.totalItems === 0) return false | 172 | if (this.pagination.totalItems === 0) return false |
@@ -206,7 +213,9 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
206 | protected setNewRouteParams () { | 213 | protected setNewRouteParams () { |
207 | const paramsObject = this.buildRouteParams() | 214 | const paramsObject = this.buildRouteParams() |
208 | 215 | ||
209 | const queryParams = Object.keys(paramsObject).map(p => p + '=' + paramsObject[p]).join('&') | 216 | const queryParams = Object.keys(paramsObject) |
217 | .map(p => p + '=' + paramsObject[p]) | ||
218 | .join('&') | ||
210 | this.location.replaceState(this.currentRoute, queryParams) | 219 | this.location.replaceState(this.currentRoute, queryParams) |
211 | } | 220 | } |
212 | 221 | ||
diff --git a/client/src/app/shared/video/video-feed.component.html b/client/src/app/shared/video/feed.component.html index 16116ba88..f7624ec01 100644 --- a/client/src/app/shared/video/video-feed.component.html +++ b/client/src/app/shared/video/feed.component.html | |||
@@ -1,10 +1,11 @@ | |||
1 | <div class="video-feed"> | 1 | <div class="video-feed"> |
2 | <span | 2 | <my-global-icon |
3 | *ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom" | 3 | *ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom" |
4 | class="icon icon-syndication" role="button" | 4 | class="icon-syndication" role="button" iconName="syndication" |
5 | ></span> | 5 | > |
6 | </my-global-icon> | ||
6 | 7 | ||
7 | <ng-template #feedsList> | 8 | <ng-template #feedsList> |
8 | <a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a> | 9 | <a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a> |
9 | </ng-template> | 10 | </ng-template> |
10 | </div> \ No newline at end of file | 11 | </div> |
diff --git a/client/src/app/shared/video/feed.component.scss b/client/src/app/shared/video/feed.component.scss new file mode 100644 index 000000000..ed1dc17d3 --- /dev/null +++ b/client/src/app/shared/video/feed.component.scss | |||
@@ -0,0 +1,18 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .video-feed { | ||
5 | a { | ||
6 | color: black; | ||
7 | display: block; | ||
8 | } | ||
9 | |||
10 | my-global-icon { | ||
11 | cursor: pointer; | ||
12 | width: 12px; | ||
13 | position: relative; | ||
14 | top: -2px; | ||
15 | |||
16 | @include apply-svg-color(var(--mainForegroundColor)) | ||
17 | } | ||
18 | } | ||
diff --git a/client/src/app/shared/video/feed.component.ts b/client/src/app/shared/video/feed.component.ts new file mode 100644 index 000000000..12507458f --- /dev/null +++ b/client/src/app/shared/video/feed.component.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | import { Syndication } from '@app/shared/video/syndication.model' | ||
3 | |||
4 | @Component({ | ||
5 | selector: 'my-feed', | ||
6 | styleUrls: [ './feed.component.scss' ], | ||
7 | templateUrl: './feed.component.html' | ||
8 | }) | ||
9 | export class FeedComponent { | ||
10 | @Input() syndicationItems: Syndication[] | ||
11 | } | ||
diff --git a/client/src/app/shared/video/syndication.model.ts b/client/src/app/shared/video/syndication.model.ts new file mode 100644 index 000000000..c59ab01e8 --- /dev/null +++ b/client/src/app/shared/video/syndication.model.ts | |||
@@ -0,0 +1,7 @@ | |||
1 | import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' | ||
2 | |||
3 | export interface Syndication { | ||
4 | format: FeedFormat, | ||
5 | label: string, | ||
6 | url: string | ||
7 | } | ||
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index 5ff3926c4..388357343 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts | |||
@@ -3,6 +3,8 @@ import { AuthUser } from '../../core' | |||
3 | import { Video } from '../../shared/video/video.model' | 3 | import { Video } from '../../shared/video/video.model' |
4 | import { Account } from '@app/shared/account/account.model' | 4 | import { Account } from '@app/shared/account/account.model' |
5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 5 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
6 | import { VideoStreamingPlaylist } from '../../../../../shared/models/videos/video-streaming-playlist.model' | ||
7 | import { VideoStreamingPlaylistType } from '../../../../../shared/models/videos/video-streaming-playlist.type' | ||
6 | 8 | ||
7 | export class VideoDetails extends Video implements VideoDetailsServerModel { | 9 | export class VideoDetails extends Video implements VideoDetailsServerModel { |
8 | descriptionPath: string | 10 | descriptionPath: string |
@@ -20,6 +22,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
20 | likesPercent: number | 22 | likesPercent: number |
21 | dislikesPercent: number | 23 | dislikesPercent: number |
22 | 24 | ||
25 | trackerUrls: string[] | ||
26 | |||
27 | streamingPlaylists: VideoStreamingPlaylist[] | ||
28 | |||
23 | constructor (hash: VideoDetailsServerModel, translations = {}) { | 29 | constructor (hash: VideoDetailsServerModel, translations = {}) { |
24 | super(hash, translations) | 30 | super(hash, translations) |
25 | 31 | ||
@@ -32,6 +38,9 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
32 | this.commentsEnabled = hash.commentsEnabled | 38 | this.commentsEnabled = hash.commentsEnabled |
33 | this.downloadEnabled = hash.downloadEnabled | 39 | this.downloadEnabled = hash.downloadEnabled |
34 | 40 | ||
41 | this.trackerUrls = hash.trackerUrls | ||
42 | this.streamingPlaylists = hash.streamingPlaylists | ||
43 | |||
35 | this.buildLikeAndDislikePercents() | 44 | this.buildLikeAndDislikePercents() |
36 | } | 45 | } |
37 | 46 | ||
@@ -55,4 +64,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
55 | this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 | 64 | this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100 |
56 | this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 | 65 | this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100 |
57 | } | 66 | } |
67 | |||
68 | getHlsPlaylist () { | ||
69 | return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS) | ||
70 | } | ||
58 | } | 71 | } |
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts index 5bb0510f9..18c62a1f9 100644 --- a/client/src/app/shared/video/video-edit.model.ts +++ b/client/src/app/shared/video/video-edit.model.ts | |||
@@ -59,14 +59,14 @@ export class VideoEdit implements VideoUpdate { | |||
59 | } | 59 | } |
60 | } | 60 | } |
61 | 61 | ||
62 | patch (values: Object) { | 62 | patch (values: { [ id: string ]: string }) { |
63 | Object.keys(values).forEach((key) => { | 63 | Object.keys(values).forEach((key) => { |
64 | this[ key ] = values[ key ] | 64 | this[ key ] = values[ key ] |
65 | }) | 65 | }) |
66 | 66 | ||
67 | // If schedule publication, the video is private and will be changed to public privacy | 67 | // If schedule publication, the video is private and will be changed to public privacy |
68 | if (parseInt(values['privacy'], 10) === VideoEdit.SPECIAL_SCHEDULED_PRIVACY) { | 68 | if (parseInt(values['privacy'], 10) === VideoEdit.SPECIAL_SCHEDULED_PRIVACY) { |
69 | const updateAt = (values['schedulePublicationAt'] as Date) | 69 | const updateAt = new Date(values['schedulePublicationAt']) |
70 | updateAt.setSeconds(0) | 70 | updateAt.setSeconds(0) |
71 | 71 | ||
72 | this.privacy = VideoPrivacy.PRIVATE | 72 | this.privacy = VideoPrivacy.PRIVATE |
diff --git a/client/src/app/shared/video/video-feed.component.scss b/client/src/app/shared/video/video-feed.component.scss deleted file mode 100644 index 385764be0..000000000 --- a/client/src/app/shared/video/video-feed.component.scss +++ /dev/null | |||
@@ -1,19 +0,0 @@ | |||
1 | @import '_mixins'; | ||
2 | |||
3 | .video-feed { | ||
4 | a { | ||
5 | color: black; | ||
6 | display: block; | ||
7 | } | ||
8 | |||
9 | .icon { | ||
10 | @include icon(12px); | ||
11 | |||
12 | &.icon-syndication { | ||
13 | position: relative; | ||
14 | top: -2px; | ||
15 | background-color: var(--mainForegroundColor); | ||
16 | mask-image: url('../../../assets/images/global/syndication.svg'); | ||
17 | } | ||
18 | } | ||
19 | } \ No newline at end of file | ||
diff --git a/client/src/app/shared/video/video-feed.component.ts b/client/src/app/shared/video/video-feed.component.ts deleted file mode 100644 index 6922153c0..000000000 --- a/client/src/app/shared/video/video-feed.component.ts +++ /dev/null | |||
@@ -1,10 +0,0 @@ | |||
1 | import { Component, Input } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-video-feed', | ||
5 | styleUrls: [ './video-feed.component.scss' ], | ||
6 | templateUrl: './video-feed.component.html' | ||
7 | }) | ||
8 | export class VideoFeedComponent { | ||
9 | @Input() syndicationItems | ||
10 | } | ||
diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html index cfc483018..2c635fa2f 100644 --- a/client/src/app/shared/video/video-miniature.component.html +++ b/client/src/app/shared/video/video-miniature.component.html | |||
@@ -7,6 +7,9 @@ | |||
7 | class="video-miniature-name" | 7 | class="video-miniature-name" |
8 | [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" | 8 | [routerLink]="[ '/videos/watch', video.uuid ]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur }" |
9 | > | 9 | > |
10 | <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span> | ||
11 | <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span> | ||
12 | |||
10 | {{ video.name }} | 13 | {{ video.name }} |
11 | </a> | 14 | </a> |
12 | 15 | ||
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss index 895879adc..f44bdf9a9 100644 --- a/client/src/app/shared/video/video-miniature.component.scss +++ b/client/src/app/shared/video/video-miniature.component.scss | |||
@@ -50,10 +50,10 @@ | |||
50 | text-overflow: ellipsis; | 50 | text-overflow: ellipsis; |
51 | white-space: nowrap; | 51 | white-space: nowrap; |
52 | font-size: 13px; | 52 | font-size: 13px; |
53 | color: #585858; | 53 | color: $grey-foreground-color; |
54 | 54 | ||
55 | &:hover { | 55 | &:hover { |
56 | color: #303030; | 56 | color: $grey-foreground-hover-color; |
57 | } | 57 | } |
58 | } | 58 | } |
59 | } | 59 | } |
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts index 7e8692b0b..2f951a1f1 100644 --- a/client/src/app/shared/video/video-miniature.component.ts +++ b/client/src/app/shared/video/video-miniature.component.ts | |||
@@ -2,6 +2,7 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core | |||
2 | import { User } from '../users' | 2 | import { User } from '../users' |
3 | import { Video } from './video.model' | 3 | import { Video } from './video.model' |
4 | import { ServerService } from '@app/core' | 4 | import { ServerService } from '@app/core' |
5 | import { VideoPrivacy } from '../../../../../shared' | ||
5 | 6 | ||
6 | export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' | 7 | export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' |
7 | 8 | ||
@@ -49,4 +50,12 @@ export class VideoMiniatureComponent implements OnInit { | |||
49 | displayOwnerVideoChannel () { | 50 | displayOwnerVideoChannel () { |
50 | return this.ownerDisplayTypeChosen === 'videoChannel' | 51 | return this.ownerDisplayTypeChosen === 'videoChannel' |
51 | } | 52 | } |
53 | |||
54 | isUnlistedVideo () { | ||
55 | return this.video.privacy.id === VideoPrivacy.UNLISTED | ||
56 | } | ||
57 | |||
58 | isPrivateVideo () { | ||
59 | return this.video.privacy.id === VideoPrivacy.PRIVATE | ||
60 | } | ||
52 | } | 61 | } |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index b92c96450..6ea83d13b 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -53,7 +53,7 @@ export class Video implements VideoServerModel { | |||
53 | displayName: string | 53 | displayName: string |
54 | url: string | 54 | url: string |
55 | host: string | 55 | host: string |
56 | avatar: Avatar | 56 | avatar?: Avatar |
57 | } | 57 | } |
58 | 58 | ||
59 | channel: { | 59 | channel: { |
@@ -63,7 +63,7 @@ export class Video implements VideoServerModel { | |||
63 | displayName: string | 63 | displayName: string |
64 | url: string | 64 | url: string |
65 | host: string | 65 | host: string |
66 | avatar: Avatar | 66 | avatar?: Avatar |
67 | } | 67 | } |
68 | 68 | ||
69 | userHistory?: { | 69 | userHistory?: { |
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index c9d6da7a4..565aad93b 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts | |||
@@ -6,11 +6,11 @@ import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } fr | |||
6 | import { ResultList } from '../../../../../shared/models/result-list.model' | 6 | import { ResultList } from '../../../../../shared/models/result-list.model' |
7 | import { | 7 | import { |
8 | UserVideoRate, | 8 | UserVideoRate, |
9 | UserVideoRateType, | ||
9 | UserVideoRateUpdate, | 10 | UserVideoRateUpdate, |
10 | VideoConstant, | 11 | VideoConstant, |
11 | VideoFilter, | 12 | VideoFilter, |
12 | VideoPrivacy, | 13 | VideoPrivacy, |
13 | VideoRateType, | ||
14 | VideoUpdate | 14 | VideoUpdate |
15 | } from '../../../../../shared/models/videos' | 15 | } from '../../../../../shared/models/videos' |
16 | import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' | 16 | import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' |
@@ -275,9 +275,9 @@ export class VideoService implements VideosProvider { | |||
275 | 275 | ||
276 | loadCompleteDescription (descriptionPath: string) { | 276 | loadCompleteDescription (descriptionPath: string) { |
277 | return this.authHttp | 277 | return this.authHttp |
278 | .get(environment.apiUrl + descriptionPath) | 278 | .get<{ description: string }>(environment.apiUrl + descriptionPath) |
279 | .pipe( | 279 | .pipe( |
280 | map(res => res[ 'description' ]), | 280 | map(res => res.description), |
281 | catchError(err => this.restExtractor.handleError(err)) | 281 | catchError(err => this.restExtractor.handleError(err)) |
282 | ) | 282 | ) |
283 | } | 283 | } |
@@ -333,7 +333,7 @@ export class VideoService implements VideosProvider { | |||
333 | return privacies | 333 | return privacies |
334 | } | 334 | } |
335 | 335 | ||
336 | private setVideoRate (id: number, rateType: VideoRateType) { | 336 | private setVideoRate (id: number, rateType: UserVideoRateType) { |
337 | const url = VideoService.BASE_VIDEO_URL + id + '/rate' | 337 | const url = VideoService.BASE_VIDEO_URL + id + '/rate' |
338 | const body: UserVideoRateUpdate = { | 338 | const body: UserVideoRateUpdate = { |
339 | rating: rateType | 339 | rating: rateType |
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html index aad4b5be3..07d24b381 100644 --- a/client/src/app/signup/signup.component.html +++ b/client/src/app/signup/signup.component.html | |||
@@ -4,6 +4,7 @@ | |||
4 | Create an account | 4 | Create an account |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div *ngIf="info" class="alert alert-info">{{ info }}</div> | ||
7 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> | 8 | <div *ngIf="error" class="alert alert-danger">{{ error }}</div> |
8 | 9 | ||
9 | <div class="d-flex justify-content-left flex-wrap"> | 10 | <div class="d-flex justify-content-left flex-wrap"> |
@@ -51,7 +52,7 @@ | |||
51 | <div class="form-group form-group-terms"> | 52 | <div class="form-group form-group-terms"> |
52 | <my-peertube-checkbox | 53 | <my-peertube-checkbox |
53 | inputName="terms" formControlName="terms" | 54 | inputName="terms" formControlName="terms" |
54 | i18n-labelHtml labelHtml="I have read and agree to the <a href='/about/instance#terms-section' target='_blank'rel='noopener noreferrer'>Terms</a> of this instance" | 55 | i18n-labelHtml 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" |
55 | ></my-peertube-checkbox> | 56 | ></my-peertube-checkbox> |
56 | 57 | ||
57 | <div *ngIf="formErrors.terms" class="form-error"> | 58 | <div *ngIf="formErrors.terms" class="form-error"> |
@@ -59,11 +60,11 @@ | |||
59 | </div> | 60 | </div> |
60 | </div> | 61 | </div> |
61 | 62 | ||
62 | <input type="submit" i18n-value value="Signup" [disabled]="!form.valid"> | 63 | <input type="submit" i18n-value value="Signup" [disabled]="!form.valid || signupDone"> |
63 | </form> | 64 | </form> |
64 | 65 | ||
65 | <div> | 66 | <div> |
66 | <label for="email" i18n>Features found on this instance</label> | 67 | <label i18n>Features found on this instance</label> |
67 | <my-instance-features-table></my-instance-features-table> | 68 | <my-instance-features-table></my-instance-features-table> |
68 | </div> | 69 | </div> |
69 | </div> | 70 | </div> |
diff --git a/client/src/app/signup/signup.component.ts b/client/src/app/signup/signup.component.ts index cf2657b85..13941ec79 100644 --- a/client/src/app/signup/signup.component.ts +++ b/client/src/app/signup/signup.component.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { AuthService, Notifier, RedirectService, ServerService } from '@app/core' |
3 | import { UserCreate } from '../../../../shared' | 3 | import { UserCreate } from '../../../../shared' |
4 | import { FormReactive, UserService, UserValidatorsService } from '../shared' | 4 | import { FormReactive, UserService, UserValidatorsService } from '../shared' |
5 | import { RedirectService, ServerService } from '@app/core' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
8 | 7 | ||
@@ -12,12 +11,15 @@ import { FormValidatorService } from '@app/shared/forms/form-validators/form-val | |||
12 | styleUrls: [ './signup.component.scss' ] | 11 | styleUrls: [ './signup.component.scss' ] |
13 | }) | 12 | }) |
14 | export class SignupComponent extends FormReactive implements OnInit { | 13 | export class SignupComponent extends FormReactive implements OnInit { |
14 | info: string = null | ||
15 | error: string = null | 15 | error: string = null |
16 | signupDone = false | ||
16 | 17 | ||
17 | constructor ( | 18 | constructor ( |
18 | protected formValidatorService: FormValidatorService, | 19 | protected formValidatorService: FormValidatorService, |
20 | private authService: AuthService, | ||
19 | private userValidatorsService: UserValidatorsService, | 21 | private userValidatorsService: UserValidatorsService, |
20 | private notificationsService: NotificationsService, | 22 | private notifier: Notifier, |
21 | private userService: UserService, | 23 | private userService: UserService, |
22 | private serverService: ServerService, | 24 | private serverService: ServerService, |
23 | private redirectService: RedirectService, | 25 | private redirectService: RedirectService, |
@@ -50,18 +52,24 @@ export class SignupComponent extends FormReactive implements OnInit { | |||
50 | 52 | ||
51 | this.userService.signup(userCreate).subscribe( | 53 | this.userService.signup(userCreate).subscribe( |
52 | () => { | 54 | () => { |
55 | this.signupDone = true | ||
56 | |||
53 | if (this.requiresEmailVerification) { | 57 | if (this.requiresEmailVerification) { |
54 | this.notificationsService.alert( | 58 | this.info = this.i18n('Welcome! Now please check your emails to verify your account and complete signup.') |
55 | this.i18n('Welcome'), | 59 | return |
56 | this.i18n('Please check your email to verify your account and complete signup.') | ||
57 | ) | ||
58 | } else { | ||
59 | this.notificationsService.success( | ||
60 | this.i18n('Success'), | ||
61 | this.i18n('Registration for {{username}} complete.', { username: userCreate.username }) | ||
62 | ) | ||
63 | } | 60 | } |
64 | this.redirectService.redirectToHomepage() | 61 | |
62 | // Auto login | ||
63 | this.authService.login(userCreate.username, userCreate.password) | ||
64 | .subscribe( | ||
65 | () => { | ||
66 | this.notifier.success(this.i18n('You are now logged in as {{username}}!', { username: userCreate.username })) | ||
67 | |||
68 | this.redirectService.redirectToHomepage() | ||
69 | }, | ||
70 | |||
71 | err => this.error = err.message | ||
72 | ) | ||
65 | }, | 73 | }, |
66 | 74 | ||
67 | err => this.error = err.message | 75 | err => this.error = err.message |
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html index 30aefdbfc..19043eee6 100644 --- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html +++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | 3 | ||
4 | <div class="modal-header"> | 4 | <div class="modal-header"> |
5 | <h4 i18n class="modal-title">Add caption</h4> | 5 | <h4 i18n class="modal-title">Add caption</h4> |
6 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 6 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
7 | </div> | 7 | </div> |
8 | 8 | ||
9 | <div class="modal-body"> | 9 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts index 07c33030a..1413e7262 100644 --- a/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts | |||
@@ -5,6 +5,7 @@ import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validator | |||
5 | import { ServerService } from '@app/core' | 5 | import { ServerService } from '@app/core' |
6 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' | 6 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' |
7 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
8 | import { VideoConstant } from '../../../../../../shared' | ||
8 | 9 | ||
9 | @Component({ | 10 | @Component({ |
10 | selector: 'my-video-caption-add-modal', | 11 | selector: 'my-video-caption-add-modal', |
@@ -19,7 +20,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni | |||
19 | 20 | ||
20 | @ViewChild('modal') modal: ElementRef | 21 | @ViewChild('modal') modal: ElementRef |
21 | 22 | ||
22 | videoCaptionLanguages = [] | 23 | videoCaptionLanguages: VideoConstant<string>[] = [] |
23 | 24 | ||
24 | private openedModal: NgbModalRef | 25 | private openedModal: NgbModalRef |
25 | private closingModal = false | 26 | private closingModal = false |
@@ -59,6 +60,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni | |||
59 | hide () { | 60 | hide () { |
60 | this.closingModal = true | 61 | this.closingModal = true |
61 | this.openedModal.close() | 62 | this.openedModal.close() |
63 | this.form.reset() | ||
62 | } | 64 | } |
63 | 65 | ||
64 | isReplacingExistingCaption () { | 66 | isReplacingExistingCaption () { |
@@ -70,8 +72,6 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni | |||
70 | } | 72 | } |
71 | 73 | ||
72 | async addCaption () { | 74 | async addCaption () { |
73 | this.hide() | ||
74 | |||
75 | const languageId = this.form.value[ 'language' ] | 75 | const languageId = this.form.value[ 'language' ] |
76 | const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId) | 76 | const languageObject = this.videoCaptionLanguages.find(l => l.id === languageId) |
77 | 77 | ||
@@ -80,6 +80,6 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni | |||
80 | captionfile: this.form.value[ 'captionfile' ] | 80 | captionfile: this.form.value[ 'captionfile' ] |
81 | }) | 81 | }) |
82 | 82 | ||
83 | this.form.reset() | 83 | this.hide() |
84 | } | 84 | } |
85 | } | 85 | } |
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 8c74a1ca6..7fd9af208 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 | |||
@@ -127,10 +127,11 @@ | |||
127 | 127 | ||
128 | <my-peertube-checkbox | 128 | <my-peertube-checkbox |
129 | inputName="downloadEnabled" formControlName="downloadEnabled" | 129 | inputName="downloadEnabled" formControlName="downloadEnabled" |
130 | i18n-labelText labelText="Disable downloading" | 130 | i18n-labelText labelText="Download enabled" |
131 | ></my-peertube-checkbox> | 131 | ></my-peertube-checkbox> |
132 | 132 | ||
133 | <my-peertube-checkbox | 133 | <my-peertube-checkbox |
134 | *ngIf="waitTranscodingEnabled" | ||
134 | inputName="waitTranscoding" formControlName="waitTranscoding" | 135 | inputName="waitTranscoding" formControlName="waitTranscoding" |
135 | i18n-labelText labelText="Wait transcoding before publishing the video" | 136 | i18n-labelText labelText="Wait transcoding before publishing the video" |
136 | i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends." | 137 | i18n-helpHtml helpHtml="If you decide not to wait for transcoding before publishing the video, it could be unplayable until transcoding ends." |
@@ -147,7 +148,7 @@ | |||
147 | 148 | ||
148 | <div class="captions-header"> | 149 | <div class="captions-header"> |
149 | <a (click)="openAddCaptionModal()" class="create-caption"> | 150 | <a (click)="openAddCaptionModal()" class="create-caption"> |
150 | <span class="icon icon-add"></span> | 151 | <my-global-icon iconName="add"></my-global-icon> |
151 | <ng-container i18n>Add another caption</ng-container> | 152 | <ng-container i18n>Add another caption</ng-container> |
152 | </a> | 153 | </a> |
153 | </div> | 154 | </div> |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss index b039d7ad4..bb775cb0a 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss | |||
@@ -5,6 +5,11 @@ | |||
5 | @include peertube-select-container(auto); | 5 | @include peertube-select-container(auto); |
6 | } | 6 | } |
7 | 7 | ||
8 | my-peertube-checkbox { | ||
9 | display: block; | ||
10 | margin-bottom: 1rem; | ||
11 | } | ||
12 | |||
8 | .video-edit { | 13 | .video-edit { |
9 | height: 100%; | 14 | height: 100%; |
10 | min-height: 300px; | 15 | min-height: 300px; |
@@ -18,10 +23,6 @@ | |||
18 | display: block; | 23 | display: block; |
19 | } | 24 | } |
20 | 25 | ||
21 | input, select { | ||
22 | font-size: 15px | ||
23 | } | ||
24 | |||
25 | .label-tags + span { | 26 | .label-tags + span { |
26 | font-size: 15px; | 27 | font-size: 15px; |
27 | } | 28 | } |
@@ -37,7 +38,7 @@ | |||
37 | text-align: right; | 38 | text-align: right; |
38 | 39 | ||
39 | .create-caption { | 40 | .create-caption { |
40 | @include create-button('../../../../assets/images/global/add.svg'); | 41 | @include create-button; |
41 | } | 42 | } |
42 | } | 43 | } |
43 | 44 | ||
@@ -95,13 +96,14 @@ | |||
95 | display: inline-block; | 96 | display: inline-block; |
96 | margin-right: 25px; | 97 | margin-right: 25px; |
97 | 98 | ||
98 | color: #585858; | 99 | color: $grey-foreground-color; |
99 | font-size: 15px; | 100 | font-size: 15px; |
100 | } | 101 | } |
101 | 102 | ||
102 | .submit-button { | 103 | .submit-button { |
103 | @include peertube-button; | 104 | @include peertube-button; |
104 | @include orange-button; | 105 | @include orange-button; |
106 | @include button-with-icon(20px, 1px); | ||
105 | 107 | ||
106 | display: inline-block; | 108 | display: inline-block; |
107 | 109 | ||
@@ -114,16 +116,6 @@ | |||
114 | color: inherit; | 116 | color: inherit; |
115 | font-weight: $font-semibold; | 117 | font-weight: $font-semibold; |
116 | } | 118 | } |
117 | |||
118 | .icon.icon-validate { | ||
119 | @include icon(20px); | ||
120 | |||
121 | cursor: inherit; | ||
122 | position: relative; | ||
123 | top: -1px; | ||
124 | margin-right: 4px; | ||
125 | background-image: url('../../../../assets/images/global/validate.svg'); | ||
126 | } | ||
127 | } | 119 | } |
128 | } | 120 | } |
129 | 121 | ||
@@ -171,10 +163,10 @@ p-calendar { | |||
171 | } | 163 | } |
172 | 164 | ||
173 | tag { | 165 | tag { |
174 | background-color: var(--inputColor) !important; | 166 | background-color: $grey-background-color !important; |
167 | color: #000 !important; | ||
175 | border-radius: 3px !important; | 168 | border-radius: 3px !important; |
176 | font-size: 15px !important; | 169 | font-size: 15px !important; |
177 | color: var(--mainForegroundColor) !important; | ||
178 | height: 30px !important; | 170 | height: 30px !important; |
179 | line-height: 30px !important; | 171 | line-height: 30px !important; |
180 | margin: 0 5px 0 0 !important; | 172 | margin: 0 5px 0 0 !important; |
@@ -197,7 +189,10 @@ p-calendar { | |||
197 | top: -1px; | 189 | top: -1px; |
198 | height: auto !important; | 190 | height: auto !important; |
199 | vertical-align: middle !important; | 191 | vertical-align: middle !important; |
200 | fill: #585858 !important; | 192 | |
193 | path { | ||
194 | fill: $grey-foreground-color !important; | ||
195 | } | ||
201 | } | 196 | } |
202 | 197 | ||
203 | &:hover { | 198 | &:hover { |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.ts b/client/src/app/videos/+video-edit/shared/video-edit.component.ts index 1dae931e2..3ed7a4a10 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.ts +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.ts | |||
@@ -2,7 +2,7 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core' | |||
2 | import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' | 2 | import { FormArray, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' | 4 | import { FormReactiveValidationMessages, VideoValidatorsService } from '@app/shared' |
5 | import { NotificationsService } from 'angular2-notifications' | 5 | import { Notifier } from '@app/core' |
6 | import { ServerService } from '../../../core/server' | 6 | import { ServerService } from '../../../core/server' |
7 | import { VideoEdit } from '../../../shared/video/video-edit.model' | 7 | import { VideoEdit } from '../../../shared/video/video-edit.model' |
8 | import { map } from 'rxjs/operators' | 8 | import { map } from 'rxjs/operators' |
@@ -27,6 +27,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
27 | @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] | 27 | @Input() userVideoChannels: { id: number, label: string, support: string }[] = [] |
28 | @Input() schedulePublicationPossible = true | 28 | @Input() schedulePublicationPossible = true |
29 | @Input() videoCaptions: VideoCaptionEdit[] = [] | 29 | @Input() videoCaptions: VideoCaptionEdit[] = [] |
30 | @Input() waitTranscodingEnabled = true | ||
30 | 31 | ||
31 | @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent | 32 | @ViewChild('videoCaptionAddModal') videoCaptionAddModal: VideoCaptionAddModalComponent |
32 | 33 | ||
@@ -48,7 +49,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
48 | calendarTimezone: string | 49 | calendarTimezone: string |
49 | calendarDateFormat: string | 50 | calendarDateFormat: string |
50 | 51 | ||
51 | private schedulerInterval | 52 | private schedulerInterval: any |
52 | private firstPatchDone = false | 53 | private firstPatchDone = false |
53 | private initialVideoCaptions: string[] = [] | 54 | private initialVideoCaptions: string[] = [] |
54 | 55 | ||
@@ -58,7 +59,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
58 | private videoCaptionService: VideoCaptionService, | 59 | private videoCaptionService: VideoCaptionService, |
59 | private route: ActivatedRoute, | 60 | private route: ActivatedRoute, |
60 | private router: Router, | 61 | private router: Router, |
61 | private notificationsService: NotificationsService, | 62 | private notifier: Notifier, |
62 | private serverService: ServerService, | 63 | private serverService: ServerService, |
63 | private i18nPrimengCalendarService: I18nPrimengCalendarService | 64 | private i18nPrimengCalendarService: I18nPrimengCalendarService |
64 | ) { | 65 | ) { |
@@ -77,14 +78,14 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
77 | } | 78 | } |
78 | 79 | ||
79 | updateForm () { | 80 | updateForm () { |
80 | const defaultValues = { | 81 | const defaultValues: any = { |
81 | nsfw: 'false', | 82 | nsfw: 'false', |
82 | commentsEnabled: 'true', | 83 | commentsEnabled: 'true', |
83 | downloadEnabled: 'true', | 84 | downloadEnabled: 'true', |
84 | waitTranscoding: 'true', | 85 | waitTranscoding: 'true', |
85 | tags: [] | 86 | tags: [] |
86 | } | 87 | } |
87 | const obj = { | 88 | const obj: any = { |
88 | name: this.videoValidatorsService.VIDEO_NAME, | 89 | name: this.videoValidatorsService.VIDEO_NAME, |
89 | privacy: this.videoValidatorsService.VIDEO_PRIVACY, | 90 | privacy: this.videoValidatorsService.VIDEO_PRIVACY, |
90 | channelId: this.videoValidatorsService.VIDEO_CHANNEL, | 91 | channelId: this.videoValidatorsService.VIDEO_CHANNEL, |
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 a933a64f0..28eb143c9 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 | |||
@@ -1,6 +1,6 @@ | |||
1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> | 1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> |
2 | <div class="import-video-torrent"> | 2 | <div class="first-step-block"> |
3 | <div class="icon icon-upload"></div> | 3 | <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> |
4 | 4 | ||
5 | <div class="button-file"> | 5 | <div class="button-file"> |
6 | <span i18n>Select the torrent to import</span> | 6 | <span i18n>Select the torrent to import</span> |
@@ -45,7 +45,12 @@ | |||
45 | </div> | 45 | </div> |
46 | </div> | 46 | </div> |
47 | 47 | ||
48 | <div *ngIf="hasImportedVideo" class="alert alert-info" i18n> | 48 | <div *ngIf="error" class="alert alert-danger"> |
49 | <div i18n>Sorry, but something went wrong</div> | ||
50 | {{ error }} | ||
51 | </div> | ||
52 | |||
53 | <div *ngIf="hasImportedVideo && !error" class="alert alert-info" i18n> | ||
49 | Congratulations, the video will be imported with BitTorrent! You can already add information about this video. | 54 | Congratulations, the video will be imported with BitTorrent! You can already add information about this video. |
50 | </div> | 55 | </div> |
51 | 56 | ||
@@ -61,7 +66,7 @@ | |||
61 | (click)="updateSecondStep()" | 66 | (click)="updateSecondStep()" |
62 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" | 67 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" |
63 | > | 68 | > |
64 | <span class="icon icon-validate"></span> | 69 | <my-global-icon iconName="validate"></my-global-icon> |
65 | <input type="button" i18n-value value="Update" /> | 70 | <input type="button" i18n-value value="Update" /> |
66 | </div> | 71 | </div> |
67 | </div> | 72 | </div> |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss index 262b0b68e..6d59ed834 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.scss | |||
@@ -1,37 +1,7 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | $width-size: 190px; | 4 | .first-step-block { |
5 | |||
6 | .peertube-select-container { | ||
7 | @include peertube-select-container($width-size); | ||
8 | } | ||
9 | |||
10 | .import-video-torrent { | ||
11 | display: flex; | ||
12 | flex-direction: column; | ||
13 | align-items: center; | ||
14 | |||
15 | .icon.icon-upload { | ||
16 | @include icon(90px); | ||
17 | margin-bottom: 25px; | ||
18 | cursor: default; | ||
19 | |||
20 | background-image: url('../../../../assets/images/video/upload.svg'); | ||
21 | } | ||
22 | |||
23 | .button-file { | ||
24 | @include peertube-button-file(auto); | ||
25 | |||
26 | min-width: 190px; | ||
27 | } | ||
28 | |||
29 | .button-file-extension { | ||
30 | display: block; | ||
31 | font-size: 12px; | ||
32 | margin-top: 5px; | ||
33 | } | ||
34 | |||
35 | .torrent-or-magnet { | 5 | .torrent-or-magnet { |
36 | margin: 10px 0; | 6 | margin: 10px 0; |
37 | } | 7 | } |
@@ -39,19 +9,6 @@ $width-size: 190px; | |||
39 | .form-group-magnet-uri { | 9 | .form-group-magnet-uri { |
40 | margin-bottom: 40px; | 10 | margin-bottom: 40px; |
41 | } | 11 | } |
42 | |||
43 | input[type=text] { | ||
44 | @include peertube-input-text($width-size); | ||
45 | display: block; | ||
46 | } | ||
47 | |||
48 | input[type=button] { | ||
49 | @include peertube-button; | ||
50 | @include orange-button; | ||
51 | |||
52 | width: $width-size; | ||
53 | margin-top: 30px; | ||
54 | } | ||
55 | } | 12 | } |
56 | 13 | ||
57 | 14 | ||
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts index 0f6fd17a9..c12a1d653 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' | 3 | import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' |
5 | import { AuthService, ServerService } from '../../../core' | 4 | import { AuthService, Notifier, ServerService } from '../../../core' |
6 | import { VideoService } from '../../../shared/video/video.service' | 5 | import { VideoService } from '../../../shared/video/video.service' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { LoadingBarService } from '@ngx-loading-bar/core' | 7 | import { LoadingBarService } from '@ngx-loading-bar/core' |
@@ -12,20 +11,22 @@ import { VideoEdit } from '@app/shared/video/video-edit.model' | |||
12 | import { FormValidatorService } from '@app/shared' | 11 | import { FormValidatorService } from '@app/shared' |
13 | import { VideoCaptionService } from '@app/shared/video-caption' | 12 | import { VideoCaptionService } from '@app/shared/video-caption' |
14 | import { VideoImportService } from '@app/shared/video-import' | 13 | import { VideoImportService } from '@app/shared/video-import' |
14 | import { scrollToTop } from '@app/shared/misc/utils' | ||
15 | 15 | ||
16 | @Component({ | 16 | @Component({ |
17 | selector: 'my-video-import-torrent', | 17 | selector: 'my-video-import-torrent', |
18 | templateUrl: './video-import-torrent.component.html', | 18 | templateUrl: './video-import-torrent.component.html', |
19 | styleUrls: [ | 19 | styleUrls: [ |
20 | '../shared/video-edit.component.scss', | 20 | '../shared/video-edit.component.scss', |
21 | './video-import-torrent.component.scss' | 21 | './video-import-torrent.component.scss', |
22 | './video-send.scss' | ||
22 | ] | 23 | ] |
23 | }) | 24 | }) |
24 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 25 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { |
25 | @Output() firstStepDone = new EventEmitter<string>() | 26 | @Output() firstStepDone = new EventEmitter<string>() |
26 | @ViewChild('torrentfileInput') torrentfileInput | 27 | @Output() firstStepError = new EventEmitter<void>() |
28 | @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement> | ||
27 | 29 | ||
28 | videoFileName: string | ||
29 | magnetUri = '' | 30 | magnetUri = '' |
30 | 31 | ||
31 | isImportingVideo = false | 32 | isImportingVideo = false |
@@ -33,13 +34,14 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca | |||
33 | isUpdatingVideo = false | 34 | isUpdatingVideo = false |
34 | 35 | ||
35 | video: VideoEdit | 36 | video: VideoEdit |
37 | error: string | ||
36 | 38 | ||
37 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC | 39 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC |
38 | 40 | ||
39 | constructor ( | 41 | constructor ( |
40 | protected formValidatorService: FormValidatorService, | 42 | protected formValidatorService: FormValidatorService, |
41 | protected loadingBar: LoadingBarService, | 43 | protected loadingBar: LoadingBarService, |
42 | protected notificationsService: NotificationsService, | 44 | protected notifier: Notifier, |
43 | protected authService: AuthService, | 45 | protected authService: AuthService, |
44 | protected serverService: ServerService, | 46 | protected serverService: ServerService, |
45 | protected videoService: VideoService, | 47 | protected videoService: VideoService, |
@@ -64,7 +66,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca | |||
64 | } | 66 | } |
65 | 67 | ||
66 | fileChange () { | 68 | fileChange () { |
67 | const torrentfile = this.torrentfileInput.nativeElement.files[0] as File | 69 | const torrentfile = this.torrentfileInput.nativeElement.files[0] |
68 | if (!torrentfile) return | 70 | if (!torrentfile) return |
69 | 71 | ||
70 | this.importVideo(torrentfile) | 72 | this.importVideo(torrentfile) |
@@ -106,7 +108,8 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca | |||
106 | err => { | 108 | err => { |
107 | this.loadingBar.complete() | 109 | this.loadingBar.complete() |
108 | this.isImportingVideo = false | 110 | this.isImportingVideo = false |
109 | this.notificationsService.error(this.i18n('Error'), err.message) | 111 | this.firstStepError.emit() |
112 | this.notifier.error(err.message) | ||
110 | } | 113 | } |
111 | ) | 114 | ) |
112 | } | 115 | } |
@@ -125,14 +128,14 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca | |||
125 | .subscribe( | 128 | .subscribe( |
126 | () => { | 129 | () => { |
127 | this.isUpdatingVideo = false | 130 | this.isUpdatingVideo = false |
128 | this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.')) | 131 | this.notifier.success(this.i18n('Video to import updated.')) |
129 | 132 | ||
130 | this.router.navigate([ '/my-account', 'video-imports' ]) | 133 | this.router.navigate([ '/my-account', 'video-imports' ]) |
131 | }, | 134 | }, |
132 | 135 | ||
133 | err => { | 136 | err => { |
134 | this.isUpdatingVideo = false | 137 | this.error = err.message |
135 | this.notificationsService.error(this.i18n('Error'), err.message) | 138 | scrollToTop() |
136 | console.error(err) | 139 | console.error(err) |
137 | } | 140 | } |
138 | ) | 141 | ) |
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 9f5fc6d22..3550c3585 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 | |||
@@ -1,6 +1,6 @@ | |||
1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> | 1 | <div *ngIf="!hasImportedVideo" class="upload-video-container"> |
2 | <div class="import-video-url"> | 2 | <div class="first-step-block"> |
3 | <div class="icon icon-upload"></div> | 3 | <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> |
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> |
@@ -37,7 +37,13 @@ | |||
37 | </div> | 37 | </div> |
38 | </div> | 38 | </div> |
39 | 39 | ||
40 | <div *ngIf="hasImportedVideo" class="alert alert-info" i18n> | 40 | |
41 | <div *ngIf="error" class="alert alert-danger"> | ||
42 | <div i18n>Sorry, but something went wrong</div> | ||
43 | {{ error }} | ||
44 | </div> | ||
45 | |||
46 | <div *ngIf="!error && hasImportedVideo" class="alert alert-info" i18n> | ||
41 | Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video. | 47 | Congratulations, the video behind {{ targetUrl }} will be imported! You can already add information about this video. |
42 | </div> | 48 | </div> |
43 | 49 | ||
@@ -53,7 +59,7 @@ | |||
53 | (click)="updateSecondStep()" | 59 | (click)="updateSecondStep()" |
54 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" | 60 | [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }" |
55 | > | 61 | > |
56 | <span class="icon icon-validate"></span> | 62 | <my-global-icon iconName="validate"></my-global-icon> |
57 | <input type="button" i18n-value value="Update" /> | 63 | <input type="button" i18n-value value="Update" /> |
58 | </div> | 64 | </div> |
59 | </div> | 65 | </div> |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss deleted file mode 100644 index 7c6deda1d..000000000 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.scss +++ /dev/null | |||
@@ -1,37 +0,0 @@ | |||
1 | @import 'variables'; | ||
2 | @import 'mixins'; | ||
3 | |||
4 | $width-size: 190px; | ||
5 | |||
6 | .peertube-select-container { | ||
7 | @include peertube-select-container($width-size); | ||
8 | } | ||
9 | |||
10 | .import-video-url { | ||
11 | display: flex; | ||
12 | flex-direction: column; | ||
13 | align-items: center; | ||
14 | |||
15 | .icon.icon-upload { | ||
16 | @include icon(90px); | ||
17 | margin-bottom: 25px; | ||
18 | cursor: default; | ||
19 | |||
20 | background-image: url('../../../../assets/images/video/upload.svg'); | ||
21 | } | ||
22 | |||
23 | input[type=text] { | ||
24 | @include peertube-input-text($width-size); | ||
25 | display: block; | ||
26 | } | ||
27 | |||
28 | input[type=button] { | ||
29 | @include peertube-button; | ||
30 | @include orange-button; | ||
31 | |||
32 | width: $width-size; | ||
33 | margin-top: 30px; | ||
34 | } | ||
35 | } | ||
36 | |||
37 | |||
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts index fbc85c74f..d11685916 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-import-url.component.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' | 3 | import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' |
5 | import { AuthService, ServerService } from '../../../core' | 4 | import { AuthService, Notifier, ServerService } from '../../../core' |
6 | import { VideoService } from '../../../shared/video/video.service' | 5 | import { VideoService } from '../../../shared/video/video.service' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { LoadingBarService } from '@ngx-loading-bar/core' | 7 | import { LoadingBarService } from '@ngx-loading-bar/core' |
@@ -12,33 +11,35 @@ import { VideoEdit } from '@app/shared/video/video-edit.model' | |||
12 | import { FormValidatorService } from '@app/shared' | 11 | import { FormValidatorService } from '@app/shared' |
13 | import { VideoCaptionService } from '@app/shared/video-caption' | 12 | import { VideoCaptionService } from '@app/shared/video-caption' |
14 | import { VideoImportService } from '@app/shared/video-import' | 13 | import { VideoImportService } from '@app/shared/video-import' |
14 | import { scrollToTop } from '@app/shared/misc/utils' | ||
15 | 15 | ||
16 | @Component({ | 16 | @Component({ |
17 | selector: 'my-video-import-url', | 17 | selector: 'my-video-import-url', |
18 | templateUrl: './video-import-url.component.html', | 18 | templateUrl: './video-import-url.component.html', |
19 | styleUrls: [ | 19 | styleUrls: [ |
20 | '../shared/video-edit.component.scss', | 20 | '../shared/video-edit.component.scss', |
21 | './video-import-url.component.scss' | 21 | './video-send.scss' |
22 | ] | 22 | ] |
23 | }) | 23 | }) |
24 | export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 24 | export class VideoImportUrlComponent extends VideoSend implements OnInit, CanComponentDeactivate { |
25 | @Output() firstStepDone = new EventEmitter<string>() | 25 | @Output() firstStepDone = new EventEmitter<string>() |
26 | @Output() firstStepError = new EventEmitter<void>() | ||
26 | 27 | ||
27 | targetUrl = '' | 28 | targetUrl = '' |
28 | videoFileName: string | ||
29 | 29 | ||
30 | isImportingVideo = false | 30 | isImportingVideo = false |
31 | hasImportedVideo = false | 31 | hasImportedVideo = false |
32 | isUpdatingVideo = false | 32 | isUpdatingVideo = false |
33 | 33 | ||
34 | video: VideoEdit | 34 | video: VideoEdit |
35 | error: string | ||
35 | 36 | ||
36 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC | 37 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC |
37 | 38 | ||
38 | constructor ( | 39 | constructor ( |
39 | protected formValidatorService: FormValidatorService, | 40 | protected formValidatorService: FormValidatorService, |
40 | protected loadingBar: LoadingBarService, | 41 | protected loadingBar: LoadingBarService, |
41 | protected notificationsService: NotificationsService, | 42 | protected notifier: Notifier, |
42 | protected authService: AuthService, | 43 | protected authService: AuthService, |
43 | protected serverService: ServerService, | 44 | protected serverService: ServerService, |
44 | protected videoService: VideoService, | 45 | protected videoService: VideoService, |
@@ -98,7 +99,8 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom | |||
98 | err => { | 99 | err => { |
99 | this.loadingBar.complete() | 100 | this.loadingBar.complete() |
100 | this.isImportingVideo = false | 101 | this.isImportingVideo = false |
101 | this.notificationsService.error(this.i18n('Error'), err.message) | 102 | this.firstStepError.emit() |
103 | this.notifier.error(err.message) | ||
102 | } | 104 | } |
103 | ) | 105 | ) |
104 | } | 106 | } |
@@ -117,14 +119,14 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, CanCom | |||
117 | .subscribe( | 119 | .subscribe( |
118 | () => { | 120 | () => { |
119 | this.isUpdatingVideo = false | 121 | this.isUpdatingVideo = false |
120 | this.notificationsService.success(this.i18n('Success'), this.i18n('Video to import updated.')) | 122 | this.notifier.success(this.i18n('Video to import updated.')) |
121 | 123 | ||
122 | this.router.navigate([ '/my-account', 'video-imports' ]) | 124 | this.router.navigate([ '/my-account', 'video-imports' ]) |
123 | }, | 125 | }, |
124 | 126 | ||
125 | err => { | 127 | err => { |
126 | this.isUpdatingVideo = false | 128 | this.error = err.message |
127 | this.notificationsService.error(this.i18n('Error'), err.message) | 129 | scrollToTop() |
128 | console.error(err) | 130 | console.error(err) |
129 | } | 131 | } |
130 | ) | 132 | ) |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.scss b/client/src/app/videos/+video-edit/video-add-components/video-send.scss new file mode 100644 index 000000000..8769dd302 --- /dev/null +++ b/client/src/app/videos/+video-edit/video-add-components/video-send.scss | |||
@@ -0,0 +1,54 @@ | |||
1 | @import 'variables'; | ||
2 | @import 'mixins'; | ||
3 | |||
4 | $width-size: 190px; | ||
5 | |||
6 | .alert.alert-danger { | ||
7 | text-align: center; | ||
8 | |||
9 | & > div { | ||
10 | font-weight: $font-semibold; | ||
11 | } | ||
12 | } | ||
13 | |||
14 | .first-step-block { | ||
15 | display: flex; | ||
16 | flex-direction: column; | ||
17 | align-items: center; | ||
18 | |||
19 | .upload-icon { | ||
20 | width: 90px; | ||
21 | margin-bottom: 25px; | ||
22 | |||
23 | @include apply-svg-color(#C6C6C6); | ||
24 | } | ||
25 | |||
26 | .peertube-select-container { | ||
27 | @include peertube-select-container($width-size); | ||
28 | } | ||
29 | |||
30 | input[type=text] { | ||
31 | @include peertube-input-text($width-size); | ||
32 | display: block; | ||
33 | } | ||
34 | |||
35 | input[type=button] { | ||
36 | @include peertube-button; | ||
37 | @include orange-button; | ||
38 | |||
39 | width: $width-size; | ||
40 | margin-top: 30px; | ||
41 | } | ||
42 | |||
43 | .button-file { | ||
44 | @include peertube-button-file(auto); | ||
45 | |||
46 | min-width: 190px; | ||
47 | } | ||
48 | |||
49 | .button-file-extension { | ||
50 | display: block; | ||
51 | font-size: 12px; | ||
52 | margin-top: 5px; | ||
53 | } | ||
54 | } | ||
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-send.ts b/client/src/app/videos/+video-edit/video-add-components/video-send.ts index 6d1bac3f2..580c123a0 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-send.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-send.ts | |||
@@ -1,18 +1,17 @@ | |||
1 | import { EventEmitter, OnInit } from '@angular/core' | 1 | import { EventEmitter, OnInit } from '@angular/core' |
2 | import { LoadingBarService } from '@ngx-loading-bar/core' | 2 | import { LoadingBarService } from '@ngx-loading-bar/core' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { AuthService, Notifier, ServerService } from '@app/core' |
4 | import { catchError, switchMap, tap } from 'rxjs/operators' | 4 | import { catchError, switchMap, tap } from 'rxjs/operators' |
5 | import { FormReactive } from '@app/shared' | 5 | import { FormReactive } from '@app/shared' |
6 | import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' | ||
7 | import { VideoConstant, VideoPrivacy } from '../../../../../../shared' | 6 | import { VideoConstant, VideoPrivacy } from '../../../../../../shared' |
8 | import { AuthService, ServerService } from '@app/core' | ||
9 | import { VideoService } from '@app/shared/video/video.service' | 7 | import { VideoService } from '@app/shared/video/video.service' |
10 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' | 8 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' |
11 | import { VideoCaptionService } from '@app/shared/video-caption' | 9 | import { VideoCaptionService } from '@app/shared/video-caption' |
12 | import { VideoEdit } from '@app/shared/video/video-edit.model' | 10 | import { VideoEdit } from '@app/shared/video/video-edit.model' |
13 | import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' | 11 | import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' |
12 | import { CanComponentDeactivateResult } from '@app/shared/guards/can-deactivate-guard.service' | ||
14 | 13 | ||
15 | export abstract class VideoSend extends FormReactive implements OnInit, CanComponentDeactivate { | 14 | export abstract class VideoSend extends FormReactive implements OnInit { |
16 | userVideoChannels: { id: number, label: string, support: string }[] = [] | 15 | userVideoChannels: { id: number, label: string, support: string }[] = [] |
17 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] | 16 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] |
18 | videoCaptions: VideoCaptionEdit[] = [] | 17 | videoCaptions: VideoCaptionEdit[] = [] |
@@ -21,16 +20,17 @@ export abstract class VideoSend extends FormReactive implements OnInit, CanCompo | |||
21 | firstStepChannelId = 0 | 20 | firstStepChannelId = 0 |
22 | 21 | ||
23 | abstract firstStepDone: EventEmitter<string> | 22 | abstract firstStepDone: EventEmitter<string> |
23 | abstract firstStepError: EventEmitter<void> | ||
24 | protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy | 24 | protected abstract readonly DEFAULT_VIDEO_PRIVACY: VideoPrivacy |
25 | 25 | ||
26 | protected loadingBar: LoadingBarService | 26 | protected loadingBar: LoadingBarService |
27 | protected notificationsService: NotificationsService | 27 | protected notifier: Notifier |
28 | protected authService: AuthService | 28 | protected authService: AuthService |
29 | protected serverService: ServerService | 29 | protected serverService: ServerService |
30 | protected videoService: VideoService | 30 | protected videoService: VideoService |
31 | protected videoCaptionService: VideoCaptionService | 31 | protected videoCaptionService: VideoCaptionService |
32 | 32 | ||
33 | abstract canDeactivate () | 33 | abstract canDeactivate (): CanComponentDeactivateResult |
34 | 34 | ||
35 | ngOnInit () { | 35 | ngOnInit () { |
36 | this.buildForm({}) | 36 | this.buildForm({}) |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html index fa57c8cb5..b252cd60a 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.html | |||
@@ -1,12 +1,12 @@ | |||
1 | <div *ngIf="!isUploadingVideo" class="upload-video-container"> | 1 | <div *ngIf="!isUploadingVideo" class="upload-video-container"> |
2 | <div class="upload-video"> | 2 | <div class="first-step-block"> |
3 | <div class="icon icon-upload"></div> | 3 | <my-global-icon class="upload-icon" iconName="upload"></my-global-icon> |
4 | 4 | ||
5 | <div class="button-file"> | 5 | <div class="button-file"> |
6 | <span i18n>Select the file to upload</span> | 6 | <span i18n>Select the file to upload</span> |
7 | <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" /> | 7 | <input #videofileInput type="file" name="videofile" id="videofile" [accept]="videoExtensions" (change)="fileChange()" /> |
8 | </div> | 8 | </div> |
9 | <span class="button-file-extension">(.mp4, .webm, .ogv)</span> | 9 | <span class="button-file-extension">({{ videoExtensions }})</span> |
10 | 10 | ||
11 | <div class="form-group form-group-channel"> | 11 | <div class="form-group form-group-channel"> |
12 | <label i18n for="first-step-channel">Channel</label> | 12 | <label i18n for="first-step-channel">Channel</label> |
@@ -29,7 +29,7 @@ | |||
29 | </div> | 29 | </div> |
30 | </div> | 30 | </div> |
31 | 31 | ||
32 | <div *ngIf="isUploadingVideo" class="upload-progress-cancel"> | 32 | <div *ngIf="isUploadingVideo && !error" class="upload-progress-cancel"> |
33 | <p-progressBar | 33 | <p-progressBar |
34 | [value]="videoUploadPercents" | 34 | [value]="videoUploadPercents" |
35 | [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }" | 35 | [ngClass]="{ processing: videoUploadPercents === 100 && videoUploaded === false }" |
@@ -37,11 +37,21 @@ | |||
37 | <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" /> | 37 | <input *ngIf="videoUploaded === false" type="button" value="Cancel" (click)="cancelUpload()" /> |
38 | </div> | 38 | </div> |
39 | 39 | ||
40 | <div *ngIf="error" class="alert alert-danger"> | ||
41 | <div i18n>Sorry, but something went wrong</div> | ||
42 | {{ error }} | ||
43 | </div> | ||
44 | |||
45 | <div *ngIf="videoUploaded && !error" class="alert alert-info" i18n> | ||
46 | Congratulations! Your video is now available in your private library. | ||
47 | </div> | ||
48 | |||
40 | <!-- Hidden because we want to load the component --> | 49 | <!-- Hidden because we want to load the component --> |
41 | <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> | 50 | <form [hidden]="!isUploadingVideo" novalidate [formGroup]="form"> |
42 | <my-video-edit | 51 | <my-video-edit |
43 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" | 52 | [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" |
44 | [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" | 53 | [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" |
54 | [waitTranscodingEnabled]="waitTranscodingEnabled" | ||
45 | ></my-video-edit> | 55 | ></my-video-edit> |
46 | 56 | ||
47 | <div class="submit-container"> | 57 | <div class="submit-container"> |
@@ -51,8 +61,8 @@ | |||
51 | (click)="updateSecondStep()" | 61 | (click)="updateSecondStep()" |
52 | [ngClass]="{ disabled: isPublishingButtonDisabled() }" | 62 | [ngClass]="{ disabled: isPublishingButtonDisabled() }" |
53 | > | 63 | > |
54 | <span class="icon icon-validate"></span> | 64 | <my-global-icon iconName="validate"></my-global-icon> |
55 | <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> | 65 | <input [disabled]="isPublishingButtonDisabled()" type="button" i18n-value value="Publish" /> |
56 | </div> | 66 | </div> |
57 | </div> | 67 | </div> |
58 | </form> \ No newline at end of file | 68 | </form> |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss index dbae5230d..8adf8f169 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.scss | |||
@@ -1,39 +1,9 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | .peertube-select-container { | 4 | .first-step-block .form-group-channel { |
5 | @include peertube-select-container(190px); | 5 | margin-bottom: 20px; |
6 | } | 6 | margin-top: 35px; |
7 | |||
8 | .upload-video { | ||
9 | display: flex; | ||
10 | flex-direction: column; | ||
11 | align-items: center; | ||
12 | |||
13 | .form-group-channel { | ||
14 | margin-bottom: 20px; | ||
15 | margin-top: 35px; | ||
16 | } | ||
17 | |||
18 | .icon.icon-upload { | ||
19 | @include icon(90px); | ||
20 | margin-bottom: 25px; | ||
21 | cursor: default; | ||
22 | |||
23 | background-image: url('../../../../assets/images/video/upload.svg'); | ||
24 | } | ||
25 | |||
26 | .button-file { | ||
27 | @include peertube-button-file(auto); | ||
28 | |||
29 | min-width: 190px; | ||
30 | } | ||
31 | |||
32 | .button-file-extension { | ||
33 | display: block; | ||
34 | font-size: 12px; | ||
35 | margin-top: 5px; | ||
36 | } | ||
37 | } | 7 | } |
38 | 8 | ||
39 | .upload-progress-cancel { | 9 | .upload-progress-cancel { |
@@ -46,9 +16,7 @@ | |||
46 | 16 | ||
47 | /deep/ .ui-progressbar { | 17 | /deep/ .ui-progressbar { |
48 | font-size: 15px !important; | 18 | font-size: 15px !important; |
49 | color: #fff !important; | ||
50 | height: 30px !important; | 19 | height: 30px !important; |
51 | line-height: 30px !important; | ||
52 | border-radius: 3px !important; | 20 | border-radius: 3px !important; |
53 | background-color: rgba(11, 204, 41, 0.16) !important; | 21 | background-color: rgba(11, 204, 41, 0.16) !important; |
54 | 22 | ||
@@ -60,6 +28,8 @@ | |||
60 | text-align: left; | 28 | text-align: left; |
61 | padding-left: 18px; | 29 | padding-left: 18px; |
62 | margin-top: 0 !important; | 30 | margin-top: 0 !important; |
31 | color: #fff !important; | ||
32 | line-height: 30px !important; | ||
63 | } | 33 | } |
64 | } | 34 | } |
65 | 35 | ||
@@ -82,4 +52,4 @@ | |||
82 | 52 | ||
83 | margin-left: 10px; | 53 | margin-left: 10px; |
84 | } | 54 | } |
85 | } \ No newline at end of file | 55 | } |
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts index ac6c1786f..9cadf52cb 100644 --- a/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts +++ b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts | |||
@@ -1,12 +1,11 @@ | |||
1 | import { HttpEventType, HttpResponse } from '@angular/common/http' | 1 | import { HttpEventType, HttpResponse } from '@angular/common/http' |
2 | import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' | 2 | import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' |
3 | import { Router } from '@angular/router' | 3 | import { Router } from '@angular/router' |
4 | import { LoadingBarService } from '@ngx-loading-bar/core' | 4 | import { LoadingBarService } from '@ngx-loading-bar/core' |
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | import { BytesPipe } from 'ngx-pipes' | 5 | import { BytesPipe } from 'ngx-pipes' |
7 | import { Subscription } from 'rxjs' | 6 | import { Subscription } from 'rxjs' |
8 | import { VideoPrivacy } from '../../../../../../shared/models/videos' | 7 | import { VideoPrivacy } from '../../../../../../shared/models/videos' |
9 | import { AuthService, ServerService } from '../../../core' | 8 | import { AuthService, Notifier, ServerService } from '../../../core' |
10 | import { VideoEdit } from '../../../shared/video/video-edit.model' | 9 | import { VideoEdit } from '../../../shared/video/video-edit.model' |
11 | import { VideoService } from '../../../shared/video/video.service' | 10 | import { VideoService } from '../../../shared/video/video.service' |
12 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -14,18 +13,21 @@ import { VideoSend } from '@app/videos/+video-edit/video-add-components/video-se | |||
14 | import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' | 13 | import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' |
15 | import { FormValidatorService, UserService } from '@app/shared' | 14 | import { FormValidatorService, UserService } from '@app/shared' |
16 | import { VideoCaptionService } from '@app/shared/video-caption' | 15 | import { VideoCaptionService } from '@app/shared/video-caption' |
16 | import { scrollToTop } from '@app/shared/misc/utils' | ||
17 | 17 | ||
18 | @Component({ | 18 | @Component({ |
19 | selector: 'my-video-upload', | 19 | selector: 'my-video-upload', |
20 | templateUrl: './video-upload.component.html', | 20 | templateUrl: './video-upload.component.html', |
21 | styleUrls: [ | 21 | styleUrls: [ |
22 | '../shared/video-edit.component.scss', | 22 | '../shared/video-edit.component.scss', |
23 | './video-upload.component.scss' | 23 | './video-upload.component.scss', |
24 | './video-send.scss' | ||
24 | ] | 25 | ] |
25 | }) | 26 | }) |
26 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { | 27 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { |
27 | @Output() firstStepDone = new EventEmitter<string>() | 28 | @Output() firstStepDone = new EventEmitter<string>() |
28 | @ViewChild('videofileInput') videofileInput | 29 | @Output() firstStepError = new EventEmitter<void>() |
30 | @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> | ||
29 | 31 | ||
30 | // So that it can be accessed in the template | 32 | // So that it can be accessed in the template |
31 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY | 33 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY |
@@ -42,13 +44,16 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
42 | id: 0, | 44 | id: 0, |
43 | uuid: '' | 45 | uuid: '' |
44 | } | 46 | } |
47 | waitTranscodingEnabled = true | ||
48 | |||
49 | error: string | ||
45 | 50 | ||
46 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC | 51 | protected readonly DEFAULT_VIDEO_PRIVACY = VideoPrivacy.PUBLIC |
47 | 52 | ||
48 | constructor ( | 53 | constructor ( |
49 | protected formValidatorService: FormValidatorService, | 54 | protected formValidatorService: FormValidatorService, |
50 | protected loadingBar: LoadingBarService, | 55 | protected loadingBar: LoadingBarService, |
51 | protected notificationsService: NotificationsService, | 56 | protected notifier: Notifier, |
52 | protected authService: AuthService, | 57 | protected authService: AuthService, |
53 | protected serverService: ServerService, | 58 | protected serverService: ServerService, |
54 | protected videoService: VideoService, | 59 | protected videoService: VideoService, |
@@ -105,20 +110,15 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
105 | this.isUploadingVideo = false | 110 | this.isUploadingVideo = false |
106 | this.videoUploadPercents = 0 | 111 | this.videoUploadPercents = 0 |
107 | this.videoUploadObservable = null | 112 | this.videoUploadObservable = null |
108 | this.notificationsService.info(this.i18n('Info'), this.i18n('Upload cancelled')) | 113 | this.notifier.info(this.i18n('Upload cancelled')) |
109 | } | 114 | } |
110 | } | 115 | } |
111 | 116 | ||
112 | uploadFirstStep () { | 117 | uploadFirstStep () { |
113 | const videofile = this.videofileInput.nativeElement.files[0] as File | 118 | const videofile = this.videofileInput.nativeElement.files[0] |
114 | if (!videofile) return | 119 | if (!videofile) return |
115 | 120 | ||
116 | // Cannot upload videos > 8GB for now | 121 | // Check global user quota |
117 | if (videofile.size > 8 * 1024 * 1024 * 1024) { | ||
118 | this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB')) | ||
119 | return | ||
120 | } | ||
121 | |||
122 | const bytePipes = new BytesPipe() | 122 | const bytePipes = new BytesPipe() |
123 | const videoQuota = this.authService.getUser().videoQuota | 123 | const videoQuota = this.authService.getUser().videoQuota |
124 | if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { | 124 | if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { |
@@ -130,10 +130,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
130 | videoQuota: bytePipes.transform(videoQuota, 0) | 130 | videoQuota: bytePipes.transform(videoQuota, 0) |
131 | } | 131 | } |
132 | ) | 132 | ) |
133 | this.notificationsService.error(this.i18n('Error'), msg) | 133 | this.notifier.error(msg) |
134 | return | 134 | return |
135 | } | 135 | } |
136 | 136 | ||
137 | // Check daily user quota | ||
137 | const videoQuotaDaily = this.authService.getUser().videoQuotaDaily | 138 | const videoQuotaDaily = this.authService.getUser().videoQuotaDaily |
138 | if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) { | 139 | if (videoQuotaDaily !== -1 && (this.userVideoQuotaUsedDaily + videofile.size) > videoQuotaDaily) { |
139 | const msg = this.i18n( | 140 | const msg = this.i18n( |
@@ -144,10 +145,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
144 | quotaDaily: bytePipes.transform(videoQuotaDaily, 0) | 145 | quotaDaily: bytePipes.transform(videoQuotaDaily, 0) |
145 | } | 146 | } |
146 | ) | 147 | ) |
147 | this.notificationsService.error(this.i18n('Error'), msg) | 148 | this.notifier.error(msg) |
148 | return | 149 | return |
149 | } | 150 | } |
150 | 151 | ||
152 | // Build name field | ||
151 | const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') | 153 | const nameWithoutExtension = videofile.name.replace(/\.[^/.]+$/, '') |
152 | let name: string | 154 | let name: string |
153 | 155 | ||
@@ -155,6 +157,11 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
155 | if (nameWithoutExtension.length < 3) name = videofile.name | 157 | if (nameWithoutExtension.length < 3) name = videofile.name |
156 | else name = nameWithoutExtension | 158 | else name = nameWithoutExtension |
157 | 159 | ||
160 | // Force user to wait transcoding for unsupported video types in web browsers | ||
161 | if (!videofile.name.endsWith('.mp4') && !videofile.name.endsWith('.webm') && !videofile.name.endsWith('.ogv')) { | ||
162 | this.waitTranscodingEnabled = false | ||
163 | } | ||
164 | |||
158 | const privacy = this.firstStepPrivacyId.toString() | 165 | const privacy = this.firstStepPrivacyId.toString() |
159 | const nsfw = false | 166 | const nsfw = false |
160 | const waitTranscoding = true | 167 | const waitTranscoding = true |
@@ -203,7 +210,8 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
203 | this.isUploadingVideo = false | 210 | this.isUploadingVideo = false |
204 | this.videoUploadPercents = 0 | 211 | this.videoUploadPercents = 0 |
205 | this.videoUploadObservable = null | 212 | this.videoUploadObservable = null |
206 | this.notificationsService.error(this.i18n('Error'), err.message) | 213 | this.firstStepError.emit() |
214 | this.notifier.error(err.message) | ||
207 | } | 215 | } |
208 | ) | 216 | ) |
209 | } | 217 | } |
@@ -232,13 +240,13 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
232 | this.isUpdatingVideo = false | 240 | this.isUpdatingVideo = false |
233 | this.isUploadingVideo = false | 241 | this.isUploadingVideo = false |
234 | 242 | ||
235 | this.notificationsService.success(this.i18n('Success'), this.i18n('Video published.')) | 243 | this.notifier.success(this.i18n('Video published.')) |
236 | this.router.navigate([ '/videos/watch', video.uuid ]) | 244 | this.router.navigate([ '/videos/watch', video.uuid ]) |
237 | }, | 245 | }, |
238 | 246 | ||
239 | err => { | 247 | err => { |
240 | this.isUpdatingVideo = false | 248 | this.error = err.message |
241 | this.notificationsService.error(this.i18n('Error'), err.message) | 249 | scrollToTop() |
242 | console.error(err) | 250 | console.error(err) |
243 | } | 251 | } |
244 | ) | 252 | ) |
diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html index e14e23aed..72a233b72 100644 --- a/client/src/app/videos/+video-edit/video-add.component.html +++ b/client/src/app/videos/+video-edit/video-add.component.html | |||
@@ -6,24 +6,24 @@ | |||
6 | 6 | ||
7 | <ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }"> | 7 | <ngb-tabset class="video-add-tabset root-tabset bootstrap" [ngClass]="{ 'hide-nav': secondStepType !== undefined }"> |
8 | 8 | ||
9 | <ngb-tab i18n-title title=""> | 9 | <ngb-tab> |
10 | <ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template> | 10 | <ng-template ngbTabTitle><span i18n>Upload a file</span></ng-template> |
11 | <ng-template ngbTabContent> | 11 | <ng-template ngbTabContent> |
12 | <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)"></my-video-upload> | 12 | <my-video-upload #videoUpload (firstStepDone)="onFirstStepDone('upload', $event)" (firstStepError)="onError()"></my-video-upload> |
13 | </ng-template> | 13 | </ng-template> |
14 | </ngb-tab> | 14 | </ngb-tab> |
15 | 15 | ||
16 | <ngb-tab *ngIf="isVideoImportHttpEnabled()"> | 16 | <ngb-tab *ngIf="isVideoImportHttpEnabled()"> |
17 | <ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template> | 17 | <ng-template ngbTabTitle><span i18n>Import with URL</span></ng-template> |
18 | <ng-template ngbTabContent> | 18 | <ng-template ngbTabContent> |
19 | <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)"></my-video-import-url> | 19 | <my-video-import-url #videoImportUrl (firstStepDone)="onFirstStepDone('import-url', $event)" (firstStepError)="onError()"></my-video-import-url> |
20 | </ng-template> | 20 | </ng-template> |
21 | </ngb-tab> | 21 | </ngb-tab> |
22 | 22 | ||
23 | <ngb-tab *ngIf="isVideoImportTorrentEnabled()"> | 23 | <ngb-tab *ngIf="isVideoImportTorrentEnabled()"> |
24 | <ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template> | 24 | <ng-template ngbTabTitle><span i18n>Import with torrent</span></ng-template> |
25 | <ng-template ngbTabContent> | 25 | <ng-template ngbTabContent> |
26 | <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)"></my-video-import-torrent> | 26 | <my-video-import-torrent #videoImportTorrent (firstStepDone)="onFirstStepDone('import-torrent', $event)" (firstStepError)="onError()"></my-video-import-torrent> |
27 | </ng-template> | 27 | </ng-template> |
28 | </ngb-tab> | 28 | </ngb-tab> |
29 | </ngb-tabset> | 29 | </ngb-tabset> |
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 1a9247dbe..01fdfcb66 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { Component, ViewChild } from '@angular/core' | 1 | import { Component, HostListener, ViewChild } from '@angular/core' |
2 | import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' | 2 | import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service' |
3 | import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-add-components/video-import-url.component' | 3 | import { VideoImportUrlComponent } from '@app/videos/+video-edit/video-add-components/video-import-url.component' |
4 | import { VideoUploadComponent } from '@app/videos/+video-edit/video-add-components/video-upload.component' | 4 | import { VideoUploadComponent } from '@app/videos/+video-edit/video-add-components/video-upload.component' |
@@ -27,7 +27,22 @@ export class VideoAddComponent implements CanComponentDeactivate { | |||
27 | this.videoName = videoName | 27 | this.videoName = videoName |
28 | } | 28 | } |
29 | 29 | ||
30 | canDeactivate () { | 30 | onError () { |
31 | this.videoName = undefined | ||
32 | this.secondStepType = undefined | ||
33 | } | ||
34 | |||
35 | @HostListener('window:beforeunload', [ '$event' ]) | ||
36 | onUnload (event: any) { | ||
37 | const { text, canDeactivate } = this.canDeactivate() | ||
38 | |||
39 | if (canDeactivate) return | ||
40 | |||
41 | event.returnValue = text | ||
42 | return text | ||
43 | } | ||
44 | |||
45 | canDeactivate (): { canDeactivate: boolean, text?: string} { | ||
31 | if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() | 46 | if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() |
32 | if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() | 47 | if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() |
33 | if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate() | 48 | if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate() |
diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html index 9242c30a0..4992bb369 100644 --- a/client/src/app/videos/+video-edit/video-update.component.html +++ b/client/src/app/videos/+video-edit/video-update.component.html | |||
@@ -8,12 +8,12 @@ | |||
8 | <my-video-edit | 8 | <my-video-edit |
9 | [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" | 9 | [form]="form" [formErrors]="formErrors" [schedulePublicationPossible]="schedulePublicationPossible" |
10 | [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" | 10 | [validationMessages]="validationMessages" [videoPrivacies]="videoPrivacies" [userVideoChannels]="userVideoChannels" |
11 | [videoCaptions]="videoCaptions" | 11 | [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="waitTranscodingEnabled" |
12 | ></my-video-edit> | 12 | ></my-video-edit> |
13 | 13 | ||
14 | <div class="submit-container"> | 14 | <div class="submit-container"> |
15 | <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"> | 15 | <div class="submit-button" (click)="update()" [ngClass]="{ disabled: !form.valid || isUpdatingVideo === true }"> |
16 | <span class="icon icon-validate"></span> | 16 | <my-global-icon iconName="validate"></my-global-icon> |
17 | <input type="button" i18n-value value="Update" /> | 17 | <input type="button" i18n-value value="Update" /> |
18 | </div> | 18 | </div> |
19 | </div> | 19 | </div> |
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts index 3a0f3a39a..9e849014e 100644 --- a/client/src/app/videos/+video-edit/video-update.component.ts +++ b/client/src/app/videos/+video-edit/video-update.component.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { map, switchMap } from 'rxjs/operators' | 1 | import { map, switchMap } from 'rxjs/operators' |
2 | import { Component, OnInit } from '@angular/core' | 2 | import { Component, HostListener, OnInit } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { LoadingBarService } from '@ngx-loading-bar/core' | 4 | import { LoadingBarService } from '@ngx-loading-bar/core' |
5 | import { NotificationsService } from 'angular2-notifications' | 5 | import { Notifier } from '@app/core' |
6 | import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' | 6 | import { VideoConstant, VideoPrivacy } from '../../../../../shared/models/videos' |
7 | import { ServerService } from '../../core' | 7 | import { ServerService } from '../../core' |
8 | import { FormReactive } from '../../shared' | 8 | import { FormReactive } from '../../shared' |
@@ -12,6 +12,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
12 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 12 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
13 | import { VideoCaptionService } from '@app/shared/video-caption' | 13 | import { VideoCaptionService } from '@app/shared/video-caption' |
14 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' | 14 | import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' |
15 | import { VideoDetails } from '@app/shared/video/video-details.model' | ||
15 | 16 | ||
16 | @Component({ | 17 | @Component({ |
17 | selector: 'my-videos-update', | 18 | selector: 'my-videos-update', |
@@ -26,6 +27,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
26 | userVideoChannels: { id: number, label: string, support: string }[] = [] | 27 | userVideoChannels: { id: number, label: string, support: string }[] = [] |
27 | schedulePublicationPossible = false | 28 | schedulePublicationPossible = false |
28 | videoCaptions: VideoCaptionEdit[] = [] | 29 | videoCaptions: VideoCaptionEdit[] = [] |
30 | waitTranscodingEnabled = true | ||
29 | 31 | ||
30 | private updateDone = false | 32 | private updateDone = false |
31 | 33 | ||
@@ -33,7 +35,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
33 | protected formValidatorService: FormValidatorService, | 35 | protected formValidatorService: FormValidatorService, |
34 | private route: ActivatedRoute, | 36 | private route: ActivatedRoute, |
35 | private router: Router, | 37 | private router: Router, |
36 | private notificationsService: NotificationsService, | 38 | private notifier: Notifier, |
37 | private serverService: ServerService, | 39 | private serverService: ServerService, |
38 | private videoService: VideoService, | 40 | private videoService: VideoService, |
39 | private loadingBar: LoadingBarService, | 41 | private loadingBar: LoadingBarService, |
@@ -65,25 +67,42 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
65 | 67 | ||
66 | this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) | 68 | this.videoPrivacies = this.videoService.explainedPrivacyLabels(this.videoPrivacies) |
67 | 69 | ||
70 | const videoFiles = (video as VideoDetails).files | ||
71 | if (videoFiles.length > 1) { // Already transcoded | ||
72 | this.waitTranscodingEnabled = false | ||
73 | } | ||
74 | |||
68 | // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout | 75 | // FIXME: Angular does not detect the change inside this subscription, so use the patched setTimeout |
69 | setTimeout(() => this.hydrateFormFromVideo()) | 76 | setTimeout(() => this.hydrateFormFromVideo()) |
70 | }, | 77 | }, |
71 | 78 | ||
72 | err => { | 79 | err => { |
73 | console.error(err) | 80 | console.error(err) |
74 | this.notificationsService.error(this.i18n('Error'), err.message) | 81 | this.notifier.error(err.message) |
75 | } | 82 | } |
76 | ) | 83 | ) |
77 | } | 84 | } |
78 | 85 | ||
79 | canDeactivate () { | 86 | @HostListener('window:beforeunload', [ '$event' ]) |
87 | onUnload (event: any) { | ||
88 | const { text, canDeactivate } = this.canDeactivate() | ||
89 | |||
90 | if (canDeactivate) return | ||
91 | |||
92 | event.returnValue = text | ||
93 | return text | ||
94 | } | ||
95 | |||
96 | canDeactivate (): { canDeactivate: boolean, text?: string } { | ||
80 | if (this.updateDone === true) return { canDeactivate: true } | 97 | if (this.updateDone === true) return { canDeactivate: true } |
81 | 98 | ||
99 | const text = this.i18n('You have unsaved changes! If you leave, your changes will be lost.') | ||
100 | |||
82 | for (const caption of this.videoCaptions) { | 101 | for (const caption of this.videoCaptions) { |
83 | if (caption.action) return { canDeactivate: false } | 102 | if (caption.action) return { canDeactivate: false, text } |
84 | } | 103 | } |
85 | 104 | ||
86 | return { canDeactivate: this.formChanged === false } | 105 | return { canDeactivate: this.formChanged === false, text } |
87 | } | 106 | } |
88 | 107 | ||
89 | checkForm () { | 108 | checkForm () { |
@@ -114,14 +133,14 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { | |||
114 | this.updateDone = true | 133 | this.updateDone = true |
115 | this.isUpdatingVideo = false | 134 | this.isUpdatingVideo = false |
116 | this.loadingBar.complete() | 135 | this.loadingBar.complete() |
117 | this.notificationsService.success(this.i18n('Success'), this.i18n('Video updated.')) | 136 | this.notifier.success(this.i18n('Video updated.')) |
118 | this.router.navigate([ '/videos/watch', this.video.uuid ]) | 137 | this.router.navigate([ '/videos/watch', this.video.uuid ]) |
119 | }, | 138 | }, |
120 | 139 | ||
121 | err => { | 140 | err => { |
122 | this.loadingBar.complete() | 141 | this.loadingBar.complete() |
123 | this.isUpdatingVideo = false | 142 | this.isUpdatingVideo = false |
124 | this.notificationsService.error(this.i18n('Error'), err.message) | 143 | this.notifier.error(err.message) |
125 | console.error(err) | 144 | console.error(err) |
126 | } | 145 | } |
127 | ) | 146 | ) |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html index b58a56596..d8a7a78c4 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html +++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html | |||
@@ -3,7 +3,7 @@ | |||
3 | <img [src]="getAvatarUrl()" alt="Avatar" /> | 3 | <img [src]="getAvatarUrl()" alt="Avatar" /> |
4 | 4 | ||
5 | <div class="form-group"> | 5 | <div class="form-group"> |
6 | <textarea i18n-placeholder placeholder="Add comment..." autosize | 6 | <textarea i18n-placeholder placeholder="Add comment..." myAutoResize |
7 | [readonly]="(user === null) ? true : false" | 7 | [readonly]="(user === null) ? true : false" |
8 | (click)="openVisitorModal($event)" | 8 | (click)="openVisitorModal($event)" |
9 | formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" | 9 | formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts index fb7de0e04..fd85c28f2 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { Notifier } from '@app/core' |
4 | import { Observable } from 'rxjs' | 4 | import { Observable } from 'rxjs' |
5 | import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model' | 5 | import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model' |
6 | import { FormReactive } from '../../../shared' | 6 | import { FormReactive } from '../../../shared' |
@@ -29,14 +29,14 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit { | |||
29 | @Output() commentCreated = new EventEmitter<VideoCommentCreate>() | 29 | @Output() commentCreated = new EventEmitter<VideoCommentCreate>() |
30 | 30 | ||
31 | @ViewChild('visitorModal') visitorModal: NgbModal | 31 | @ViewChild('visitorModal') visitorModal: NgbModal |
32 | @ViewChild('textarea') private textareaElement: ElementRef | 32 | @ViewChild('textarea') textareaElement: ElementRef |
33 | 33 | ||
34 | private addingComment = false | 34 | addingComment = false |
35 | 35 | ||
36 | constructor ( | 36 | constructor ( |
37 | protected formValidatorService: FormValidatorService, | 37 | protected formValidatorService: FormValidatorService, |
38 | private videoCommentValidatorsService: VideoCommentValidatorsService, | 38 | private videoCommentValidatorsService: VideoCommentValidatorsService, |
39 | private notificationsService: NotificationsService, | 39 | private notifier: Notifier, |
40 | private videoCommentService: VideoCommentService, | 40 | private videoCommentService: VideoCommentService, |
41 | private authService: AuthService, | 41 | private authService: AuthService, |
42 | private modalService: NgbModal, | 42 | private modalService: NgbModal, |
@@ -70,13 +70,13 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit { | |||
70 | } | 70 | } |
71 | 71 | ||
72 | onValidKey () { | 72 | onValidKey () { |
73 | this.onValueChanged() | 73 | this.check() |
74 | if (!this.form.valid) return | 74 | if (!this.form.valid) return |
75 | 75 | ||
76 | this.formValidated() | 76 | this.formValidated() |
77 | } | 77 | } |
78 | 78 | ||
79 | openVisitorModal (event) { | 79 | openVisitorModal (event: any) { |
80 | if (this.user === null) { // we only open it for visitors | 80 | if (this.user === null) { // we only open it for visitors |
81 | // fixing ng-bootstrap ModalService and the "Expression Changed After It Has Been Checked" Error | 81 | // fixing ng-bootstrap ModalService and the "Expression Changed After It Has Been Checked" Error |
82 | event.srcElement.blur() | 82 | event.srcElement.blur() |
@@ -115,7 +115,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit { | |||
115 | err => { | 115 | err => { |
116 | this.addingComment = false | 116 | this.addingComment = false |
117 | 117 | ||
118 | this.notificationsService.error(this.i18n('Error'), err.text) | 118 | this.notifier.error(err.text) |
119 | } | 119 | } |
120 | ) | 120 | ) |
121 | } | 121 | } |
@@ -135,7 +135,6 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit { | |||
135 | 135 | ||
136 | gotoLogin () { | 136 | gotoLogin () { |
137 | this.hideVisitorModal() | 137 | this.hideVisitorModal() |
138 | this.authService.redirectUrl = this.router.url | ||
139 | this.router.navigate([ '/login' ]) | 138 | this.router.navigate([ '/login' ]) |
140 | } | 139 | } |
141 | 140 | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.scss b/client/src/app/videos/+video-watch/comment/video-comment.component.scss index 84da5727e..731ecbf8f 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.scss +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.scss | |||
@@ -41,7 +41,7 @@ | |||
41 | } | 41 | } |
42 | 42 | ||
43 | .comment-date { | 43 | .comment-date { |
44 | color: #585858; | 44 | color: $grey-foreground-color; |
45 | margin-left: 10px; | 45 | margin-left: 10px; |
46 | } | 46 | } |
47 | } | 47 | } |
@@ -69,7 +69,7 @@ | |||
69 | 69 | ||
70 | .comment-action-reply, | 70 | .comment-action-reply, |
71 | .comment-action-delete { | 71 | .comment-action-delete { |
72 | color: #585858; | 72 | color: $grey-foreground-color; |
73 | cursor: pointer; | 73 | cursor: pointer; |
74 | margin-right: 10px; | 74 | margin-right: 10px; |
75 | 75 | ||
@@ -108,4 +108,4 @@ | |||
108 | .root-comment { | 108 | .root-comment { |
109 | font-size: 14px; | 109 | font-size: 14px; |
110 | } | 110 | } |
111 | } \ No newline at end of file | 111 | } |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts index e90008de9..aba7f9d1c 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts | |||
@@ -1,11 +1,10 @@ | |||
1 | import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core' |
2 | import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service' | ||
3 | import * as sanitizeHtml from 'sanitize-html' | ||
4 | import { UserRight } from '../../../../../../shared/models/users' | 2 | import { UserRight } from '../../../../../../shared/models/users' |
5 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' | 3 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' |
6 | import { AuthService } from '../../../core/auth' | 4 | import { AuthService } from '../../../core/auth' |
7 | import { Video } from '../../../shared/video/video.model' | 5 | import { Video } from '../../../shared/video/video.model' |
8 | import { VideoComment } from './video-comment.model' | 6 | import { VideoComment } from './video-comment.model' |
7 | import { HtmlRendererService } from '@app/shared/renderer' | ||
9 | 8 | ||
10 | @Component({ | 9 | @Component({ |
11 | selector: 'my-video-comment', | 10 | selector: 'my-video-comment', |
@@ -26,10 +25,10 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
26 | @Output() resetReply = new EventEmitter() | 25 | @Output() resetReply = new EventEmitter() |
27 | 26 | ||
28 | sanitizedCommentHTML = '' | 27 | sanitizedCommentHTML = '' |
29 | newParentComments = [] | 28 | newParentComments: VideoComment[] = [] |
30 | 29 | ||
31 | constructor ( | 30 | constructor ( |
32 | private linkifierService: LinkifierService, | 31 | private htmlRenderer: HtmlRendererService, |
33 | private authService: AuthService | 32 | private authService: AuthService |
34 | ) {} | 33 | ) {} |
35 | 34 | ||
@@ -87,27 +86,7 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
87 | } | 86 | } |
88 | 87 | ||
89 | private init () { | 88 | private init () { |
90 | // Convert possible markdown to html | 89 | this.sanitizedCommentHTML = this.htmlRenderer.toSafeHtml(this.comment.text) |
91 | const html = this.linkifierService.linkify(this.comment.text) | ||
92 | |||
93 | this.sanitizedCommentHTML = sanitizeHtml(html, { | ||
94 | allowedTags: [ 'a', 'p', 'span', 'br' ], | ||
95 | allowedSchemes: [ 'http', 'https' ], | ||
96 | allowedAttributes: { | ||
97 | 'a': [ 'href', 'class', 'target' ] | ||
98 | }, | ||
99 | transformTags: { | ||
100 | a: (tagName, attribs) => { | ||
101 | return { | ||
102 | tagName, | ||
103 | attribs: Object.assign(attribs, { | ||
104 | target: '_blank', | ||
105 | rel: 'noopener noreferrer' | ||
106 | }) | ||
107 | } | ||
108 | } | ||
109 | } | ||
110 | }) | ||
111 | 90 | ||
112 | this.newParentComments = this.parentComments.concat([ this.comment ]) | 91 | this.newParentComments = this.parentComments.concat([ this.comment ]) |
113 | } | 92 | } |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.model.ts b/client/src/app/videos/+video-watch/comment/video-comment.model.ts index fe591811e..824fb24c3 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.model.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.model.ts | |||
@@ -14,7 +14,7 @@ export class VideoComment implements VideoCommentServerModel { | |||
14 | account: AccountInterface | 14 | account: AccountInterface |
15 | totalReplies: number | 15 | totalReplies: number |
16 | by: string | 16 | by: string |
17 | accountAvatarUrl | 17 | accountAvatarUrl: string |
18 | 18 | ||
19 | constructor (hash: VideoCommentServerModel) { | 19 | constructor (hash: VideoCommentServerModel) { |
20 | this.id = hash.id | 20 | this.id = hash.id |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts index 9bcb4b7de..b8e5878c5 100644 --- a/client/src/app/videos/+video-watch/comment/video-comment.service.ts +++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { catchError, map } from 'rxjs/operators' | 1 | import { catchError, map } from 'rxjs/operators' |
2 | import { HttpClient, HttpParams } from '@angular/common/http' | 2 | import { HttpClient, HttpParams } from '@angular/common/http' |
3 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { lineFeedToHtml } from '@app/shared/misc/utils' | 4 | import { objectLineFeedToHtml } from '@app/shared/misc/utils' |
5 | import { Observable } from 'rxjs' | 5 | import { Observable } from 'rxjs' |
6 | import { ResultList, FeedFormat } from '../../../../../../shared/models' | 6 | import { ResultList, FeedFormat } from '../../../../../../shared/models' |
7 | import { | 7 | import { |
@@ -28,22 +28,22 @@ export class VideoCommentService { | |||
28 | 28 | ||
29 | addCommentThread (videoId: number | string, comment: VideoCommentCreate) { | 29 | addCommentThread (videoId: number | string, comment: VideoCommentCreate) { |
30 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' | 30 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' |
31 | const normalizedComment = lineFeedToHtml(comment, 'text') | 31 | const normalizedComment = objectLineFeedToHtml(comment, 'text') |
32 | 32 | ||
33 | return this.authHttp.post(url, normalizedComment) | 33 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) |
34 | .pipe( | 34 | .pipe( |
35 | map(data => this.extractVideoComment(data['comment'])), | 35 | map(data => this.extractVideoComment(data.comment)), |
36 | catchError(err => this.restExtractor.handleError(err)) | 36 | catchError(err => this.restExtractor.handleError(err)) |
37 | ) | 37 | ) |
38 | } | 38 | } |
39 | 39 | ||
40 | addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) { | 40 | addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) { |
41 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId | 41 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId |
42 | const normalizedComment = lineFeedToHtml(comment, 'text') | 42 | const normalizedComment = objectLineFeedToHtml(comment, 'text') |
43 | 43 | ||
44 | return this.authHttp.post(url, normalizedComment) | 44 | return this.authHttp.post<{ comment: VideoCommentServerModel }>(url, normalizedComment) |
45 | .pipe( | 45 | .pipe( |
46 | map(data => this.extractVideoComment(data[ 'comment' ])), | 46 | map(data => this.extractVideoComment(data.comment)), |
47 | catchError(err => this.restExtractor.handleError(err)) | 47 | catchError(err => this.restExtractor.handleError(err)) |
48 | ) | 48 | ) |
49 | } | 49 | } |
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html index 42e129d65..44016d8ad 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.html +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html | |||
@@ -4,7 +4,7 @@ | |||
4 | Comments | 4 | Comments |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <my-video-feed [syndicationItems]="syndicationItems"></my-video-feed> | 7 | <my-feed [syndicationItems]="syndicationItems"></my-feed> |
8 | </div> | 8 | </div> |
9 | 9 | ||
10 | <ng-template [ngIf]="video.commentsEnabled === true"> | 10 | <ng-template [ngIf]="video.commentsEnabled === true"> |
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.scss b/client/src/app/videos/+video-watch/comment/video-comments.component.scss index dbb44c66c..575e331e4 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.scss +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.scss | |||
@@ -23,7 +23,7 @@ | |||
23 | margin-right: 0; | 23 | margin-right: 0; |
24 | } | 24 | } |
25 | 25 | ||
26 | my-video-feed { | 26 | my-feed { |
27 | display: inline-block; | 27 | display: inline-block; |
28 | margin-left: 5px; | 28 | margin-left: 5px; |
29 | } | 29 | } |
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts index c864d82b7..2616820d2 100644 --- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts | |||
@@ -1,17 +1,17 @@ | |||
1 | import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, ElementRef } from '@angular/core' | 1 | import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core' |
2 | import { ActivatedRoute } from '@angular/router' | 2 | import { ActivatedRoute } from '@angular/router' |
3 | import { ConfirmService } from '@app/core' | 3 | import { ConfirmService, Notifier } from '@app/core' |
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { Subscription } from 'rxjs' | 4 | import { Subscription } from 'rxjs' |
6 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' | 5 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' |
7 | import { AuthService } from '../../../core/auth' | 6 | import { AuthService } from '../../../core/auth' |
8 | import { ComponentPagination } from '../../../shared/rest/component-pagination.model' | 7 | import { ComponentPagination, hasMoreItems } from '../../../shared/rest/component-pagination.model' |
9 | import { User } from '../../../shared/users' | 8 | import { User } from '../../../shared/users' |
10 | import { VideoSortField } from '../../../shared/video/sort-field.type' | 9 | import { VideoSortField } from '../../../shared/video/sort-field.type' |
11 | import { VideoDetails } from '../../../shared/video/video-details.model' | 10 | import { VideoDetails } from '../../../shared/video/video-details.model' |
12 | import { VideoComment } from './video-comment.model' | 11 | import { VideoComment } from './video-comment.model' |
13 | import { VideoCommentService } from './video-comment.service' | 12 | import { VideoCommentService } from './video-comment.service' |
14 | import { I18n } from '@ngx-translate/i18n-polyfill' | 13 | import { I18n } from '@ngx-translate/i18n-polyfill' |
14 | import { Syndication } from '@app/shared/video/syndication.model' | ||
15 | 15 | ||
16 | @Component({ | 16 | @Component({ |
17 | selector: 'my-video-comments', | 17 | selector: 'my-video-comments', |
@@ -35,13 +35,13 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | |||
35 | threadComments: { [ id: number ]: VideoCommentThreadTree } = {} | 35 | threadComments: { [ id: number ]: VideoCommentThreadTree } = {} |
36 | threadLoading: { [ id: number ]: boolean } = {} | 36 | threadLoading: { [ id: number ]: boolean } = {} |
37 | 37 | ||
38 | syndicationItems = [] | 38 | syndicationItems: Syndication[] = [] |
39 | 39 | ||
40 | private sub: Subscription | 40 | private sub: Subscription |
41 | 41 | ||
42 | constructor ( | 42 | constructor ( |
43 | private authService: AuthService, | 43 | private authService: AuthService, |
44 | private notificationsService: NotificationsService, | 44 | private notifier: Notifier, |
45 | private confirmService: ConfirmService, | 45 | private confirmService: ConfirmService, |
46 | private videoCommentService: VideoCommentService, | 46 | private videoCommentService: VideoCommentService, |
47 | private activatedRoute: ActivatedRoute, | 47 | private activatedRoute: ActivatedRoute, |
@@ -83,15 +83,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | |||
83 | this.highlightedThread = new VideoComment(res.comment) | 83 | this.highlightedThread = new VideoComment(res.comment) |
84 | 84 | ||
85 | // Scroll to the highlighted thread | 85 | // Scroll to the highlighted thread |
86 | setTimeout(() => { | 86 | setTimeout(() => this.commentHighlightBlock.nativeElement.scrollIntoView(), 0) |
87 | // -60 because of the fixed header | ||
88 | const scrollY = this.commentHighlightBlock.nativeElement.offsetTop - 60 | ||
89 | window.scroll(0, scrollY) | ||
90 | }, 500) | ||
91 | } | 87 | } |
92 | }, | 88 | }, |
93 | 89 | ||
94 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 90 | err => this.notifier.error(err.message) |
95 | ) | 91 | ) |
96 | } | 92 | } |
97 | 93 | ||
@@ -103,7 +99,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | |||
103 | this.componentPagination.totalItems = res.totalComments | 99 | this.componentPagination.totalItems = res.totalComments |
104 | }, | 100 | }, |
105 | 101 | ||
106 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 102 | err => this.notifier.error(err.message) |
107 | ) | 103 | ) |
108 | } | 104 | } |
109 | 105 | ||
@@ -154,7 +150,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | |||
154 | if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined | 150 | if (this.highlightedThread.id === commentToDelete.id) this.highlightedThread = undefined |
155 | }, | 151 | }, |
156 | 152 | ||
157 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 153 | err => this.notifier.error(err.message) |
158 | ) | 154 | ) |
159 | } | 155 | } |
160 | 156 | ||
@@ -165,22 +161,11 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | |||
165 | onNearOfBottom () { | 161 | onNearOfBottom () { |
166 | this.componentPagination.currentPage++ | 162 | this.componentPagination.currentPage++ |
167 | 163 | ||
168 | if (this.hasMoreComments()) { | 164 | if (hasMoreItems(this.componentPagination)) { |
169 | this.loadMoreComments() | 165 | this.loadMoreComments() |
170 | } | 166 | } |
171 | } | 167 | } |
172 | 168 | ||
173 | private hasMoreComments () { | ||
174 | // No results | ||
175 | if (this.componentPagination.totalItems === 0) return false | ||
176 | |||
177 | // Not loaded yet | ||
178 | if (!this.componentPagination.totalItems) return true | ||
179 | |||
180 | const maxPage = this.componentPagination.totalItems / this.componentPagination.itemsPerPage | ||
181 | return maxPage > this.componentPagination.currentPage | ||
182 | } | ||
183 | |||
184 | private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) { | 169 | private deleteLocalCommentThread (parentComment: VideoCommentThreadTree, commentToDelete: VideoComment) { |
185 | for (const commentChild of parentComment.children) { | 170 | for (const commentChild of parentComment.children) { |
186 | if (commentChild.comment.id === commentToDelete.id) { | 171 | if (commentChild.comment.id === commentToDelete.id) { |
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html index c436501b4..1a87bdcd4 100644 --- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.html +++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Blacklist video</h4> | 3 | <h4 i18n class="modal-title">Blacklist video</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
@@ -15,6 +15,13 @@ | |||
15 | </div> | 15 | </div> |
16 | </div> | 16 | </div> |
17 | 17 | ||
18 | <div class="form-group" *ngIf="video.isLocal"> | ||
19 | <my-peertube-checkbox | ||
20 | inputName="unfederate" formControlName="unfederate" | ||
21 | i18n-labelText labelText="Unfederate the video (ask for its deletion from the remote instances)" | ||
22 | ></my-peertube-checkbox> | ||
23 | </div> | ||
24 | |||
18 | <div class="form-group inputs"> | 25 | <div class="form-group inputs"> |
19 | <span i18n class="action-button action-button-cancel" (click)="hide()"> | 26 | <span i18n class="action-button action-button-cancel" (click)="hide()"> |
20 | Cancel | 27 | Cancel |
diff --git a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts index 2c123ebed..50a7cadd1 100644 --- a/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-blacklist.component.ts | |||
@@ -1,12 +1,11 @@ | |||
1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier, RedirectService } from '@app/core' |
3 | import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index' | 3 | import { FormReactive, VideoBlacklistService, VideoBlacklistValidatorsService } from '../../../shared/index' |
4 | import { VideoDetails } from '../../../shared/video/video-details.model' | 4 | import { VideoDetails } from '../../../shared/video/video-details.model' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
9 | import { RedirectService } from '@app/core' | ||
10 | 9 | ||
11 | @Component({ | 10 | @Component({ |
12 | selector: 'my-video-blacklist', | 11 | selector: 'my-video-blacklist', |
@@ -27,7 +26,7 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit { | |||
27 | private modalService: NgbModal, | 26 | private modalService: NgbModal, |
28 | private videoBlacklistValidatorsService: VideoBlacklistValidatorsService, | 27 | private videoBlacklistValidatorsService: VideoBlacklistValidatorsService, |
29 | private videoBlacklistService: VideoBlacklistService, | 28 | private videoBlacklistService: VideoBlacklistService, |
30 | private notificationsService: NotificationsService, | 29 | private notifier: Notifier, |
31 | private redirectService: RedirectService, | 30 | private redirectService: RedirectService, |
32 | private i18n: I18n | 31 | private i18n: I18n |
33 | ) { | 32 | ) { |
@@ -35,9 +34,12 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit { | |||
35 | } | 34 | } |
36 | 35 | ||
37 | ngOnInit () { | 36 | ngOnInit () { |
37 | const defaultValues = { unfederate: 'true' } | ||
38 | |||
38 | this.buildForm({ | 39 | this.buildForm({ |
39 | reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON | 40 | reason: this.videoBlacklistValidatorsService.VIDEO_BLACKLIST_REASON, |
40 | }) | 41 | unfederate: null |
42 | }, defaultValues) | ||
41 | } | 43 | } |
42 | 44 | ||
43 | show () { | 45 | show () { |
@@ -51,16 +53,17 @@ export class VideoBlacklistComponent extends FormReactive implements OnInit { | |||
51 | 53 | ||
52 | blacklist () { | 54 | blacklist () { |
53 | const reason = this.form.value[ 'reason' ] || undefined | 55 | const reason = this.form.value[ 'reason' ] || undefined |
56 | const unfederate = this.video.isLocal ? this.form.value[ 'unfederate' ] : undefined | ||
54 | 57 | ||
55 | this.videoBlacklistService.blacklistVideo(this.video.id, reason) | 58 | this.videoBlacklistService.blacklistVideo(this.video.id, reason, unfederate) |
56 | .subscribe( | 59 | .subscribe( |
57 | () => { | 60 | () => { |
58 | this.notificationsService.success(this.i18n('Success'), this.i18n('Video blacklisted.')) | 61 | this.notifier.success(this.i18n('Video blacklisted.')) |
59 | this.hide() | 62 | this.hide() |
60 | this.redirectService.redirectToHomepage() | 63 | this.redirectService.redirectToHomepage() |
61 | }, | 64 | }, |
62 | 65 | ||
63 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 66 | err => this.notifier.error(err.message) |
64 | ) | 67 | ) |
65 | } | 68 | } |
66 | } | 69 | } |
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.html b/client/src/app/videos/+video-watch/modal/video-download.component.html index f46f92a17..2bb5d6d37 100644 --- a/client/src/app/videos/+video-watch/modal/video-download.component.html +++ b/client/src/app/videos/+video-watch/modal/video-download.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Download video</h4> | 3 | <h4 i18n class="modal-title">Download video</h4> |
4 | <span class="close" aria-hidden="true" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.ts b/client/src/app/videos/+video-watch/modal/video-download.component.ts index b1b2c0623..834385771 100644 --- a/client/src/app/videos/+video-watch/modal/video-download.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-download.component.ts | |||
@@ -2,7 +2,7 @@ import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core' | |||
2 | import { VideoDetails } from '../../../shared/video/video-details.model' | 2 | import { VideoDetails } from '../../../shared/video/video-details.model' |
3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 4 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { NotificationsService } from 'angular2-notifications' | 5 | import { Notifier } from '@app/core' |
6 | 6 | ||
7 | @Component({ | 7 | @Component({ |
8 | selector: 'my-video-download', | 8 | selector: 'my-video-download', |
@@ -18,7 +18,7 @@ export class VideoDownloadComponent implements OnInit { | |||
18 | resolutionId: number | string = -1 | 18 | resolutionId: number | string = -1 |
19 | 19 | ||
20 | constructor ( | 20 | constructor ( |
21 | private notificationsService: NotificationsService, | 21 | private notifier: Notifier, |
22 | private modalService: NgbModal, | 22 | private modalService: NgbModal, |
23 | private i18n: I18n | 23 | private i18n: I18n |
24 | ) { } | 24 | ) { } |
@@ -63,6 +63,6 @@ export class VideoDownloadComponent implements OnInit { | |||
63 | } | 63 | } |
64 | 64 | ||
65 | activateCopiedMessage () { | 65 | activateCopiedMessage () { |
66 | this.notificationsService.success(this.i18n('Success'), this.i18n('Copied')) | 66 | this.notifier.success(this.i18n('Copied')) |
67 | } | 67 | } |
68 | } | 68 | } |
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.html b/client/src/app/videos/+video-watch/modal/video-report.component.html index 8d9a49276..b9434da26 100644 --- a/client/src/app/videos/+video-watch/modal/video-report.component.html +++ b/client/src/app/videos/+video-watch/modal/video-report.component.html | |||
@@ -1,11 +1,16 @@ | |||
1 | <ng-template #modal> | 1 | <ng-template #modal> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Report video</h4> | 3 | <h4 i18n class="modal-title">Report video</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
8 | 8 | ||
9 | <div i18n class="information"> | ||
10 | Your report will be sent to moderators of {{ currentHost }}. | ||
11 | <ng-container *ngIf="isRemoteVideo()"> It will be forwarded to origin instance {{ originHost }} too.</ng-container> | ||
12 | </div> | ||
13 | |||
9 | <form novalidate [formGroup]="form" (ngSubmit)="report()"> | 14 | <form novalidate [formGroup]="form" (ngSubmit)="report()"> |
10 | <div class="form-group"> | 15 | <div class="form-group"> |
11 | <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }"> | 16 | <textarea i18n-placeholder placeholder="Reason..." formControlName="reason" [ngClass]="{ 'input-error': formErrors['reason'] }"> |
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.scss b/client/src/app/videos/+video-watch/modal/video-report.component.scss index afcdb9a16..4713660a2 100644 --- a/client/src/app/videos/+video-watch/modal/video-report.component.scss +++ b/client/src/app/videos/+video-watch/modal/video-report.component.scss | |||
@@ -1,6 +1,10 @@ | |||
1 | @import 'variables'; | 1 | @import 'variables'; |
2 | @import 'mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | .information { | ||
5 | margin-bottom: 20px; | ||
6 | } | ||
7 | |||
4 | textarea { | 8 | textarea { |
5 | @include peertube-textarea(100%, 100px); | 9 | @include peertube-textarea(100%, 100px); |
6 | } | 10 | } |
diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.ts b/client/src/app/videos/+video-watch/modal/video-report.component.ts index 297afb19f..911f3b447 100644 --- a/client/src/app/videos/+video-watch/modal/video-report.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-report.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { FormReactive, VideoAbuseService } from '../../../shared/index' | 3 | import { FormReactive, VideoAbuseService } from '../../../shared/index' |
4 | import { VideoDetails } from '../../../shared/video/video-details.model' | 4 | import { VideoDetails } from '../../../shared/video/video-details.model' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -27,12 +27,24 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
27 | private modalService: NgbModal, | 27 | private modalService: NgbModal, |
28 | private videoAbuseValidatorsService: VideoAbuseValidatorsService, | 28 | private videoAbuseValidatorsService: VideoAbuseValidatorsService, |
29 | private videoAbuseService: VideoAbuseService, | 29 | private videoAbuseService: VideoAbuseService, |
30 | private notificationsService: NotificationsService, | 30 | private notifier: Notifier, |
31 | private i18n: I18n | 31 | private i18n: I18n |
32 | ) { | 32 | ) { |
33 | super() | 33 | super() |
34 | } | 34 | } |
35 | 35 | ||
36 | get currentHost () { | ||
37 | return window.location.host | ||
38 | } | ||
39 | |||
40 | get originHost () { | ||
41 | if (this.isRemoteVideo()) { | ||
42 | return this.video.account.host | ||
43 | } | ||
44 | |||
45 | return '' | ||
46 | } | ||
47 | |||
36 | ngOnInit () { | 48 | ngOnInit () { |
37 | this.buildForm({ | 49 | this.buildForm({ |
38 | reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON | 50 | reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON |
@@ -54,11 +66,15 @@ export class VideoReportComponent extends FormReactive implements OnInit { | |||
54 | this.videoAbuseService.reportVideo(this.video.id, reason) | 66 | this.videoAbuseService.reportVideo(this.video.id, reason) |
55 | .subscribe( | 67 | .subscribe( |
56 | () => { | 68 | () => { |
57 | this.notificationsService.success(this.i18n('Success'), this.i18n('Video reported.')) | 69 | this.notifier.success(this.i18n('Video reported.')) |
58 | this.hide() | 70 | this.hide() |
59 | }, | 71 | }, |
60 | 72 | ||
61 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 73 | err => this.notifier.error(err.message) |
62 | ) | 74 | ) |
63 | } | 75 | } |
76 | |||
77 | isRemoteVideo () { | ||
78 | return !this.video.isLocal | ||
79 | } | ||
64 | } | 80 | } |
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html index 301f67f2d..9f3c37fe8 100644 --- a/client/src/app/videos/+video-watch/modal/video-share.component.html +++ b/client/src/app/videos/+video-watch/modal/video-share.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Share</h4> | 3 | <h4 i18n class="modal-title">Share</h4> |
4 | <span class="close" aria-hidden="true" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body"> | 7 | <div class="modal-body"> |
diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.ts b/client/src/app/videos/+video-watch/modal/video-share.component.ts index 17e2b31e1..c6205e355 100644 --- a/client/src/app/videos/+video-watch/modal/video-share.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-share.component.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { Component, ElementRef, Input, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, Input, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { VideoDetails } from '../../../shared/video/video-details.model' | 3 | import { VideoDetails } from '../../../shared/video/video-details.model' |
4 | import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' | 4 | import { buildVideoEmbed, buildVideoLink } from '../../../../assets/player/utils' |
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 5 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -23,7 +23,7 @@ export class VideoShareComponent { | |||
23 | 23 | ||
24 | constructor ( | 24 | constructor ( |
25 | private modalService: NgbModal, | 25 | private modalService: NgbModal, |
26 | private notificationsService: NotificationsService, | 26 | private notifier: Notifier, |
27 | private i18n: I18n | 27 | private i18n: I18n |
28 | ) { } | 28 | ) { } |
29 | 29 | ||
@@ -49,7 +49,7 @@ export class VideoShareComponent { | |||
49 | } | 49 | } |
50 | 50 | ||
51 | activateCopiedMessage () { | 51 | activateCopiedMessage () { |
52 | this.notificationsService.success(this.i18n('Success'), this.i18n('Copied')) | 52 | this.notifier.success(this.i18n('Copied')) |
53 | } | 53 | } |
54 | 54 | ||
55 | getStartCheckboxLabel () { | 55 | getStartCheckboxLabel () { |
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.html b/client/src/app/videos/+video-watch/modal/video-support.component.html index 00c304709..2a05224a8 100644 --- a/client/src/app/videos/+video-watch/modal/video-support.component.html +++ b/client/src/app/videos/+video-watch/modal/video-support.component.html | |||
@@ -1,7 +1,7 @@ | |||
1 | <ng-template #modal let-hide="close"> | 1 | <ng-template #modal let-hide="close"> |
2 | <div class="modal-header"> | 2 | <div class="modal-header"> |
3 | <h4 i18n class="modal-title">Support</h4> | 3 | <h4 i18n class="modal-title">Support</h4> |
4 | <span class="close" aria-label="Close" role="button" (click)="hide()"></span> | 4 | <my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> |
5 | </div> | 5 | </div> |
6 | 6 | ||
7 | <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> | 7 | <div class="modal-body" [innerHTML]="videoHTMLSupport"></div> |
diff --git a/client/src/app/videos/+video-watch/modal/video-support.component.ts b/client/src/app/videos/+video-watch/modal/video-support.component.ts index 154002120..deb8fbc67 100644 --- a/client/src/app/videos/+video-watch/modal/video-support.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-support.component.ts | |||
@@ -1,8 +1,7 @@ | |||
1 | import { Component, Input, ViewChild } from '@angular/core' | 1 | import { Component, Input, ViewChild } from '@angular/core' |
2 | import { MarkdownService } from '@app/videos/shared' | ||
3 | |||
4 | import { VideoDetails } from '../../../shared/video/video-details.model' | 2 | import { VideoDetails } from '../../../shared/video/video-details.model' |
5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
4 | import { MarkdownService } from '@app/shared/renderer' | ||
6 | 5 | ||
7 | @Component({ | 6 | @Component({ |
8 | selector: 'my-video-support', | 7 | selector: 'my-video-support', |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index ba04d638f..1875230d8 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html | |||
@@ -53,55 +53,57 @@ | |||
53 | <div | 53 | <div |
54 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" | 54 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'like' }" (click)="setLike()" |
55 | class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'" | 55 | class="action-button action-button-like" role="button" [attr.aria-pressed]="userRating === 'like'" |
56 | i18n-title title="Like this video" | ||
56 | > | 57 | > |
57 | <span class="icon icon-like" i18n-title title="Like this video" ></span> | 58 | <my-global-icon iconName="like"></my-global-icon> |
58 | </div> | 59 | </div> |
59 | 60 | ||
60 | <div | 61 | <div |
61 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" | 62 | *ngIf="isUserLoggedIn()" [ngClass]="{ 'activated': userRating === 'dislike' }" (click)="setDislike()" |
62 | class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'" | 63 | class="action-button action-button-dislike" role="button" [attr.aria-pressed]="userRating === 'dislike'" |
64 | i18n-title title="Dislike this video" | ||
63 | > | 65 | > |
64 | <span class="icon icon-dislike" i18n-title title="Dislike this video"></span> | 66 | <my-global-icon iconName="dislike"></my-global-icon> |
65 | </div> | 67 | </div> |
66 | 68 | ||
67 | <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> | 69 | <div *ngIf="video.support" (click)="showSupportModal()" class="action-button action-button-support"> |
68 | <span class="icon icon-support"></span> | 70 | <my-global-icon iconName="heart"></my-global-icon> |
69 | <span class="icon-text" i18n>Support</span> | 71 | <span class="icon-text" i18n>Support</span> |
70 | </div> | 72 | </div> |
71 | 73 | ||
72 | <div (click)="showShareModal()" class="action-button action-button-share" role="button"> | 74 | <div (click)="showShareModal()" class="action-button action-button-share" role="button"> |
73 | <span class="icon icon-share"></span> | 75 | <my-global-icon iconName="share"></my-global-icon> |
74 | <span class="icon-text" i18n>Share</span> | 76 | <span class="icon-text" i18n>Share</span> |
75 | </div> | 77 | </div> |
76 | 78 | ||
77 | <div class="action-more" ngbDropdown placement="top" role="button"> | 79 | <div class="action-more" ngbDropdown placement="top" role="button"> |
78 | <div class="action-button" ngbDropdownToggle role="button"> | 80 | <div class="action-button" ngbDropdownToggle role="button"> |
79 | <span class="icon icon-more"></span> | 81 | <my-global-icon class="more-icon" iconName="more"></my-global-icon> |
80 | </div> | 82 | </div> |
81 | 83 | ||
82 | <div ngbDropdownMenu> | 84 | <div ngbDropdownMenu> |
83 | <a *ngIf="isVideoDownloadable()" class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> | 85 | <a *ngIf="isVideoDownloadable()" class="dropdown-item" i18n-title title="Download the video" href="#" (click)="showDownloadModal($event)"> |
84 | <span class="icon icon-download"></span> <ng-container i18n>Download</ng-container> | 86 | <my-global-icon iconName="download"></my-global-icon> <ng-container i18n>Download</ng-container> |
85 | </a> | 87 | </a> |
86 | 88 | ||
87 | <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> | 89 | <a *ngIf="isUserLoggedIn()" class="dropdown-item" i18n-title title="Report this video" href="#" (click)="showReportModal($event)"> |
88 | <span class="icon icon-alert"></span> <ng-container i18n>Report</ng-container> | 90 | <my-global-icon iconName="alert"></my-global-icon> <ng-container i18n>Report</ng-container> |
89 | </a> | 91 | </a> |
90 | 92 | ||
91 | <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> | 93 | <a *ngIf="isVideoUpdatable()" class="dropdown-item" i18n-title title="Update this video" href="#" [routerLink]="[ '/videos/update', video.uuid ]"> |
92 | <span class="icon icon-edit"></span> <ng-container i18n>Update</ng-container> | 94 | <my-global-icon iconName="edit"></my-global-icon> <ng-container i18n>Update</ng-container> |
93 | </a> | 95 | </a> |
94 | 96 | ||
95 | <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> | 97 | <a *ngIf="isVideoBlacklistable()" class="dropdown-item" i18n-title title="Blacklist this video" href="#" (click)="showBlacklistModal($event)"> |
96 | <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container> | 98 | <my-global-icon iconName="no"></my-global-icon> <ng-container i18n>Blacklist</ng-container> |
97 | </a> | 99 | </a> |
98 | 100 | ||
99 | <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> | 101 | <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)"> |
100 | <span class="icon icon-unblacklist"></span> <ng-container i18n>Unblacklist</ng-container> | 102 | <my-global-icon iconName="undo"></my-global-icon> <ng-container i18n>Unblacklist</ng-container> |
101 | </a> | 103 | </a> |
102 | 104 | ||
103 | <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> | 105 | <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> |
104 | <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container> | 106 | <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete</ng-container> |
105 | </a> | 107 | </a> |
106 | </div> | 108 | </div> |
107 | </div> | 109 | </div> |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss index f31e4694a..b03ed197d 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss | |||
@@ -162,7 +162,7 @@ $other-videos-width: 260px; | |||
162 | } | 162 | } |
163 | } | 163 | } |
164 | 164 | ||
165 | my-video-feed { | 165 | my-feed { |
166 | margin-left: 5px; | 166 | margin-left: 5px; |
167 | margin-top: 1px; | 167 | margin-top: 1px; |
168 | } | 168 | } |
@@ -183,6 +183,8 @@ $other-videos-width: 260px; | |||
183 | .action-button { | 183 | .action-button { |
184 | @include peertube-button; | 184 | @include peertube-button; |
185 | @include grey-button; | 185 | @include grey-button; |
186 | @include button-with-icon(21px, 0, -1px); | ||
187 | @include apply-svg-color($grey-foreground-color); | ||
186 | 188 | ||
187 | font-size: 15px; | 189 | font-size: 15px; |
188 | font-weight: $font-semibold; | 190 | font-weight: $font-semibold; |
@@ -194,53 +196,25 @@ $other-videos-width: 260px; | |||
194 | display: none; | 196 | display: none; |
195 | } | 197 | } |
196 | 198 | ||
197 | .icon { | ||
198 | @include icon(21px); | ||
199 | |||
200 | position: relative; | ||
201 | top: -2px; | ||
202 | |||
203 | &.icon-like { | ||
204 | background-image: url('../../../assets/images/video/like-grey.svg'); | ||
205 | } | ||
206 | |||
207 | &.icon-dislike { | ||
208 | background-image: url('../../../assets/images/video/dislike-grey.svg'); | ||
209 | } | ||
210 | |||
211 | &.icon-support { | ||
212 | background-image: url('../../../assets/images/video/heart.svg'); | ||
213 | } | ||
214 | |||
215 | &.icon-share { | ||
216 | background-image: url('../../../assets/images/video/share.svg'); | ||
217 | } | ||
218 | |||
219 | &.icon-more { | ||
220 | background-image: url('../../../assets/images/video/more.svg'); | ||
221 | top: -1px; | ||
222 | } | ||
223 | } | ||
224 | |||
225 | .icon-text { | ||
226 | margin-left: 3px; | ||
227 | } | ||
228 | |||
229 | &.action-button-like.activated { | 199 | &.action-button-like.activated { |
230 | background-color: $green; | 200 | background-color: $green; |
231 | 201 | ||
232 | .icon-like { | 202 | my-global-icon { |
233 | background-image: url('../../../assets/images/video/like-white.svg'); | 203 | @include apply-svg-color(#fff); |
234 | } | 204 | } |
235 | } | 205 | } |
236 | 206 | ||
237 | &.action-button-dislike.activated { | 207 | &.action-button-dislike.activated { |
238 | background-color: $red; | 208 | background-color: $red; |
239 | 209 | ||
240 | .icon-dislike { | 210 | my-global-icon { |
241 | background-image: url('../../../assets/images/video/dislike-white.svg'); | 211 | @include apply-svg-color(#fff); |
242 | } | 212 | } |
243 | } | 213 | } |
214 | |||
215 | .icon-text { | ||
216 | margin-left: 3px; | ||
217 | } | ||
244 | } | 218 | } |
245 | 219 | ||
246 | .action-more { | 220 | .action-more { |
@@ -249,36 +223,12 @@ $other-videos-width: 260px; | |||
249 | .dropdown-menu .dropdown-item { | 223 | .dropdown-menu .dropdown-item { |
250 | padding: 6px 24px; | 224 | padding: 6px 24px; |
251 | 225 | ||
252 | .icon { | 226 | my-global-icon { |
253 | @include icon(24px); | 227 | width: 24px; |
254 | 228 | ||
255 | margin-right: 10px; | 229 | margin-right: 10px; |
256 | position: relative; | 230 | position: relative; |
257 | top: -1px; | 231 | top: -2px; |
258 | |||
259 | &.icon-download { | ||
260 | background-image: url('../../../assets/images/video/download-black.svg'); | ||
261 | } | ||
262 | |||
263 | &.icon-edit { | ||
264 | background-image: url('../../../assets/images/global/edit-black.svg'); | ||
265 | } | ||
266 | |||
267 | &.icon-alert { | ||
268 | background-image: url('../../../assets/images/video/alert.svg'); | ||
269 | } | ||
270 | |||
271 | &.icon-blacklist { | ||
272 | background-image: url('../../../assets/images/video/blacklist.svg'); | ||
273 | } | ||
274 | |||
275 | &.icon-unblacklist { | ||
276 | background-image: url('../../../assets/images/global/undo.svg'); | ||
277 | } | ||
278 | |||
279 | &.icon-delete { | ||
280 | background-image: url('../../../assets/images/global/delete-black.svg'); | ||
281 | } | ||
282 | } | 232 | } |
283 | } | 233 | } |
284 | } | 234 | } |
@@ -320,7 +270,7 @@ $other-videos-width: 260px; | |||
320 | .video-info-description-more { | 270 | .video-info-description-more { |
321 | cursor: pointer; | 271 | cursor: pointer; |
322 | font-weight: $font-semibold; | 272 | font-weight: $font-semibold; |
323 | color: #585858; | 273 | color: $grey-foreground-color; |
324 | font-size: 14px; | 274 | font-size: 14px; |
325 | 275 | ||
326 | .glyphicon { | 276 | .glyphicon { |
@@ -339,7 +289,7 @@ $other-videos-width: 260px; | |||
339 | min-width: 91px; | 289 | min-width: 91px; |
340 | padding-right: 5px; | 290 | padding-right: 5px; |
341 | display: inline-block; | 291 | display: inline-block; |
342 | color: #585858; | 292 | color: $grey-foreground-color; |
343 | font-weight: $font-bold; | 293 | font-weight: $font-bold; |
344 | } | 294 | } |
345 | 295 | ||
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index a7fd45695..4dbfa41e5 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -5,30 +5,29 @@ import { RedirectService } from '@app/core/routing/redirect.service' | |||
5 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' | 5 | import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' |
6 | import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' | 6 | import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' |
7 | import { MetaService } from '@ngx-meta/core' | 7 | import { MetaService } from '@ngx-meta/core' |
8 | import { NotificationsService } from 'angular2-notifications' | 8 | import { Notifier, ServerService } from '@app/core' |
9 | import { forkJoin, Subscription } from 'rxjs' | 9 | import { forkJoin, Subscription } from 'rxjs' |
10 | import * as videojs from 'video.js' | ||
11 | import 'videojs-hotkeys' | ||
12 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 10 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
13 | import * as WebTorrent from 'webtorrent' | 11 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared' |
14 | import { UserVideoRateType, VideoCaption, VideoPrivacy, VideoRateType, VideoState } from '../../../../../shared' | ||
15 | import '../../../assets/player/peertube-videojs-plugin' | ||
16 | import { AuthService, ConfirmService } from '../../core' | 12 | import { AuthService, ConfirmService } from '../../core' |
17 | import { RestExtractor, VideoBlacklistService } from '../../shared' | 13 | import { RestExtractor, VideoBlacklistService } from '../../shared' |
18 | import { VideoDetails } from '../../shared/video/video-details.model' | 14 | import { VideoDetails } from '../../shared/video/video-details.model' |
19 | import { VideoService } from '../../shared/video/video.service' | 15 | import { VideoService } from '../../shared/video/video.service' |
20 | import { MarkdownService } from '../shared' | ||
21 | import { VideoDownloadComponent } from './modal/video-download.component' | 16 | import { VideoDownloadComponent } from './modal/video-download.component' |
22 | import { VideoReportComponent } from './modal/video-report.component' | 17 | import { VideoReportComponent } from './modal/video-report.component' |
23 | import { VideoShareComponent } from './modal/video-share.component' | 18 | import { VideoShareComponent } from './modal/video-share.component' |
24 | import { VideoBlacklistComponent } from './modal/video-blacklist.component' | 19 | import { VideoBlacklistComponent } from './modal/video-blacklist.component' |
25 | import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' | 20 | import { SubscribeButtonComponent } from '@app/shared/user-subscription/subscribe-button.component' |
26 | import { addContextMenu, getVideojsOptions, loadLocaleInVideoJS } from '../../../assets/player/peertube-player' | ||
27 | import { ServerService } from '@app/core' | ||
28 | import { I18n } from '@ngx-translate/i18n-polyfill' | 21 | import { I18n } from '@ngx-translate/i18n-polyfill' |
29 | import { environment } from '../../../environments/environment' | 22 | import { environment } from '../../../environments/environment' |
30 | import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' | ||
31 | import { VideoCaptionService } from '@app/shared/video-caption' | 23 | import { VideoCaptionService } from '@app/shared/video-caption' |
24 | import { MarkdownService } from '@app/shared/renderer' | ||
25 | import { | ||
26 | P2PMediaLoaderOptions, | ||
27 | PeertubePlayerManager, | ||
28 | PeertubePlayerManagerOptions, | ||
29 | PlayerMode | ||
30 | } from '../../../assets/player/peertube-player-manager' | ||
32 | 31 | ||
33 | @Component({ | 32 | @Component({ |
34 | selector: 'my-video-watch', | 33 | selector: 'my-video-watch', |
@@ -45,7 +44,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
45 | @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent | 44 | @ViewChild('videoBlacklistModal') videoBlacklistModal: VideoBlacklistComponent |
46 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent | 45 | @ViewChild('subscribeButton') subscribeButton: SubscribeButtonComponent |
47 | 46 | ||
48 | player: videojs.Player | 47 | player: any |
49 | playerElement: HTMLVideoElement | 48 | playerElement: HTMLVideoElement |
50 | userRating: UserVideoRateType = null | 49 | userRating: UserVideoRateType = null |
51 | video: VideoDetails = null | 50 | video: VideoDetails = null |
@@ -60,7 +59,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
60 | remoteServerDown = false | 59 | remoteServerDown = false |
61 | hotkeys: Hotkey[] | 60 | hotkeys: Hotkey[] |
62 | 61 | ||
63 | private videojsLocaleLoaded = false | ||
64 | private paramsSub: Subscription | 62 | private paramsSub: Subscription |
65 | 63 | ||
66 | constructor ( | 64 | constructor ( |
@@ -75,7 +73,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
75 | private authService: AuthService, | 73 | private authService: AuthService, |
76 | private serverService: ServerService, | 74 | private serverService: ServerService, |
77 | private restExtractor: RestExtractor, | 75 | private restExtractor: RestExtractor, |
78 | private notificationsService: NotificationsService, | 76 | private notifier: Notifier, |
79 | private markdownService: MarkdownService, | 77 | private markdownService: MarkdownService, |
80 | private zone: NgZone, | 78 | private zone: NgZone, |
81 | private redirectService: RedirectService, | 79 | private redirectService: RedirectService, |
@@ -91,7 +89,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
91 | 89 | ||
92 | ngOnInit () { | 90 | ngOnInit () { |
93 | if ( | 91 | if ( |
94 | WebTorrent.WEBRTC_SUPPORT === false || | 92 | !!((window as any).RTCPeerConnection || (window as any).mozRTCPeerConnection || (window as any).webkitRTCPeerConnection) === false || |
95 | peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true' | 93 | peertubeLocalStorage.getItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY) === 'true' |
96 | ) { | 94 | ) { |
97 | this.hasAlreadyAcceptedPrivacyConcern = true | 95 | this.hasAlreadyAcceptedPrivacyConcern = true |
@@ -112,11 +110,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
112 | ) | 110 | ) |
113 | .pipe( | 111 | .pipe( |
114 | // If 401, the video is private or blacklisted so redirect to 404 | 112 | // If 401, the video is private or blacklisted so redirect to 404 |
115 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 404 ])) | 113 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 403, 404 ])) |
116 | ) | 114 | ) |
117 | .subscribe(([ video, captionsResult ]) => { | 115 | .subscribe(([ video, captionsResult ]) => { |
118 | const startTime = this.route.snapshot.queryParams.start | 116 | const startTime = this.route.snapshot.queryParams.start |
119 | this.onVideoFetched(video, captionsResult.data, startTime) | 117 | const subtitle = this.route.snapshot.queryParams.subtitle |
118 | const playerMode = this.route.snapshot.queryParams.mode | ||
119 | |||
120 | this.onVideoFetched(video, captionsResult.data, { startTime, subtitle, playerMode }) | ||
120 | .catch(err => this.handleError(err)) | 121 | .catch(err => this.handleError(err)) |
121 | }) | 122 | }) |
122 | }) | 123 | }) |
@@ -201,7 +202,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
201 | 202 | ||
202 | error => { | 203 | error => { |
203 | this.descriptionLoading = false | 204 | this.descriptionLoading = false |
204 | this.notificationsService.error(this.i18n('Error'), error.message) | 205 | this.notifier.error(error.message) |
205 | } | 206 | } |
206 | ) | 207 | ) |
207 | } | 208 | } |
@@ -243,16 +244,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
243 | 244 | ||
244 | this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe( | 245 | this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe( |
245 | () => { | 246 | () => { |
246 | this.notificationsService.success( | 247 | this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name })) |
247 | this.i18n('Success'), | ||
248 | this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name }) | ||
249 | ) | ||
250 | 248 | ||
251 | this.video.blacklisted = false | 249 | this.video.blacklisted = false |
252 | this.video.blacklistedReason = null | 250 | this.video.blacklistedReason = null |
253 | }, | 251 | }, |
254 | 252 | ||
255 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 253 | err => this.notifier.error(err.message) |
256 | ) | 254 | ) |
257 | } | 255 | } |
258 | 256 | ||
@@ -290,17 +288,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
290 | 288 | ||
291 | this.videoService.removeVideo(this.video.id) | 289 | this.videoService.removeVideo(this.video.id) |
292 | .subscribe( | 290 | .subscribe( |
293 | status => { | 291 | () => { |
294 | this.notificationsService.success( | 292 | this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name })) |
295 | this.i18n('Success'), | ||
296 | this.i18n('Video {{videoName}} deleted.', { videoName: this.video.name }) | ||
297 | ) | ||
298 | 293 | ||
299 | // Go back to the video-list. | 294 | // Go back to the video-list. |
300 | this.redirectService.redirectToHomepage() | 295 | this.redirectService.redirectToHomepage() |
301 | }, | 296 | }, |
302 | 297 | ||
303 | error => this.notificationsService.error(this.i18n('Error'), error.message) | 298 | error => this.notifier.error(error.message) |
304 | ) | 299 | ) |
305 | } | 300 | } |
306 | 301 | ||
@@ -354,7 +349,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
354 | return | 349 | return |
355 | } | 350 | } |
356 | 351 | ||
357 | this.notificationsService.error(this.i18n('Error'), errorMessage) | 352 | this.notifier.error(errorMessage) |
358 | } | 353 | } |
359 | 354 | ||
360 | private checkUserRating () { | 355 | private checkUserRating () { |
@@ -369,11 +364,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
369 | } | 364 | } |
370 | }, | 365 | }, |
371 | 366 | ||
372 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 367 | err => this.notifier.error(err.message) |
373 | ) | 368 | ) |
374 | } | 369 | } |
375 | 370 | ||
376 | private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], startTimeFromUrl: number) { | 371 | private async onVideoFetched ( |
372 | video: VideoDetails, | ||
373 | videoCaptions: VideoCaption[], | ||
374 | urlOptions: { startTime?: number, subtitle?: string, playerMode?: string } | ||
375 | ) { | ||
377 | this.video = video | 376 | this.video = video |
378 | 377 | ||
379 | // Re init attributes | 378 | // Re init attributes |
@@ -381,8 +380,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
381 | this.completeDescriptionShown = false | 380 | this.completeDescriptionShown = false |
382 | this.remoteServerDown = false | 381 | this.remoteServerDown = false |
383 | 382 | ||
384 | let startTime = startTimeFromUrl || (this.video.userHistory ? this.video.userHistory.currentTime : 0) | 383 | let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0) |
385 | // Don't start the video if we are at the end | 384 | // If we are at the end of the video, reset the timer |
386 | if (this.video.duration - startTime <= 1) startTime = 0 | 385 | if (this.video.duration - startTime <= 1) startTime = 0 |
387 | 386 | ||
388 | if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { | 387 | if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { |
@@ -409,40 +408,64 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
409 | src: environment.apiUrl + c.captionPath | 408 | src: environment.apiUrl + c.captionPath |
410 | })) | 409 | })) |
411 | 410 | ||
412 | const videojsOptions = getVideojsOptions({ | 411 | const options: PeertubePlayerManagerOptions = { |
413 | autoplay: this.isAutoplay(), | 412 | common: { |
414 | inactivityTimeout: 2500, | 413 | autoplay: this.isAutoplay(), |
415 | videoFiles: this.video.files, | 414 | |
416 | videoCaptions: playerCaptions, | 415 | playerElement: this.playerElement, |
417 | playerElement: this.playerElement, | 416 | onPlayerElementChange: (element: HTMLVideoElement) => this.playerElement = element, |
418 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, | 417 | |
419 | videoDuration: this.video.duration, | 418 | videoDuration: this.video.duration, |
420 | enableHotkeys: true, | 419 | enableHotkeys: true, |
421 | peertubeLink: false, | 420 | inactivityTimeout: 2500, |
422 | poster: this.video.previewUrl, | 421 | poster: this.video.previewUrl, |
423 | startTime, | 422 | startTime, |
424 | theaterMode: true, | 423 | |
425 | language: this.localeId, | 424 | theaterMode: true, |
426 | 425 | captions: videoCaptions.length !== 0, | |
427 | userWatching: this.user ? { | 426 | peertubeLink: false, |
428 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), | 427 | |
429 | authorizationHeader: this.authService.getRequestHeaderValue() | 428 | videoViewUrl: this.video.privacy.id !== VideoPrivacy.PRIVATE ? this.videoService.getVideoViewUrl(this.video.uuid) : null, |
430 | } : undefined | 429 | embedUrl: this.video.embedUrl, |
431 | }) | 430 | |
431 | language: this.localeId, | ||
432 | |||
433 | subtitle: urlOptions.subtitle, | ||
434 | |||
435 | userWatching: this.user && this.user.videosHistoryEnabled === true ? { | ||
436 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), | ||
437 | authorizationHeader: this.authService.getRequestHeaderValue() | ||
438 | } : undefined, | ||
432 | 439 | ||
433 | if (this.videojsLocaleLoaded === false) { | 440 | serverUrl: environment.apiUrl, |
434 | await loadLocaleInVideoJS(environment.apiUrl, videojs, isOnDevLocale() ? getDevLocale() : this.localeId) | 441 | |
435 | this.videojsLocaleLoaded = true | 442 | videoCaptions: playerCaptions |
443 | }, | ||
444 | |||
445 | webtorrent: { | ||
446 | videoFiles: this.video.files | ||
447 | } | ||
436 | } | 448 | } |
437 | 449 | ||
438 | const self = this | 450 | const mode: PlayerMode = urlOptions.playerMode === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent' |
439 | this.zone.runOutsideAngular(async () => { | 451 | |
440 | videojs(this.playerElement, videojsOptions, function () { | 452 | if (mode === 'p2p-media-loader') { |
441 | self.player = this | 453 | const hlsPlaylist = this.video.getHlsPlaylist() |
442 | this.on('customError', (event, data) => self.handleError(data.err)) | ||
443 | 454 | ||
444 | addContextMenu(self.player, self.video.embedUrl) | 455 | const p2pMediaLoader = { |
445 | }) | 456 | playlistUrl: hlsPlaylist.playlistUrl, |
457 | segmentsSha256Url: hlsPlaylist.segmentsSha256Url, | ||
458 | redundancyBaseUrls: hlsPlaylist.redundancies.map(r => r.baseUrl), | ||
459 | trackerAnnounce: this.video.trackerUrls, | ||
460 | videoFiles: this.video.files | ||
461 | } as P2PMediaLoaderOptions | ||
462 | |||
463 | Object.assign(options, { p2pMediaLoader }) | ||
464 | } | ||
465 | |||
466 | this.zone.runOutsideAngular(async () => { | ||
467 | this.player = await PeertubePlayerManager.initialize(mode, options) | ||
468 | this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) | ||
446 | }) | 469 | }) |
447 | 470 | ||
448 | this.setVideoDescriptionHTML() | 471 | this.setVideoDescriptionHTML() |
@@ -452,7 +475,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
452 | this.checkUserRating() | 475 | this.checkUserRating() |
453 | } | 476 | } |
454 | 477 | ||
455 | private setRating (nextRating) { | 478 | private setRating (nextRating: UserVideoRateType) { |
456 | let method | 479 | let method |
457 | switch (nextRating) { | 480 | switch (nextRating) { |
458 | case 'like': | 481 | case 'like': |
@@ -474,11 +497,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
474 | this.userRating = nextRating | 497 | this.userRating = nextRating |
475 | }, | 498 | }, |
476 | 499 | ||
477 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 500 | (err: { message: string }) => this.notifier.error(err.message) |
478 | ) | 501 | ) |
479 | } | 502 | } |
480 | 503 | ||
481 | private updateVideoRating (oldRating: UserVideoRateType, newRating: VideoRateType) { | 504 | private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) { |
482 | let likesToIncrement = 0 | 505 | let likesToIncrement = 0 |
483 | let dislikesToIncrement = 0 | 506 | let dislikesToIncrement = 0 |
484 | 507 | ||
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts index 5582ab40f..2f448db78 100644 --- a/client/src/app/videos/+video-watch/video-watch.module.ts +++ b/client/src/app/videos/+video-watch/video-watch.module.ts | |||
@@ -1,9 +1,7 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { LinkifierService } from '@app/videos/+video-watch/comment/linkifier.service' | ||
3 | import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' | 2 | import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component' |
4 | import { ClipboardModule } from 'ngx-clipboard' | 3 | import { ClipboardModule } from 'ngx-clipboard' |
5 | import { SharedModule } from '../../shared' | 4 | import { SharedModule } from '../../shared' |
6 | import { MarkdownService } from '../shared' | ||
7 | import { VideoCommentAddComponent } from './comment/video-comment-add.component' | 5 | import { VideoCommentAddComponent } from './comment/video-comment-add.component' |
8 | import { VideoCommentComponent } from './comment/video-comment.component' | 6 | import { VideoCommentComponent } from './comment/video-comment.component' |
9 | import { VideoCommentService } from './comment/video-comment.service' | 7 | import { VideoCommentService } from './comment/video-comment.service' |
@@ -17,7 +15,6 @@ import { NgxQRCodeModule } from 'ngx-qrcode2' | |||
17 | import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' | 15 | import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap' |
18 | import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component' | 16 | import { VideoBlacklistComponent } from '@app/videos/+video-watch/modal/video-blacklist.component' |
19 | import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module' | 17 | import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module' |
20 | import { TextareaAutosizeModule } from 'ngx-textarea-autosize' | ||
21 | 18 | ||
22 | @NgModule({ | 19 | @NgModule({ |
23 | imports: [ | 20 | imports: [ |
@@ -26,7 +23,6 @@ import { TextareaAutosizeModule } from 'ngx-textarea-autosize' | |||
26 | ClipboardModule, | 23 | ClipboardModule, |
27 | NgbTooltipModule, | 24 | NgbTooltipModule, |
28 | NgxQRCodeModule, | 25 | NgxQRCodeModule, |
29 | TextareaAutosizeModule, | ||
30 | RecommendationsModule | 26 | RecommendationsModule |
31 | ], | 27 | ], |
32 | 28 | ||
@@ -48,8 +44,6 @@ import { TextareaAutosizeModule } from 'ngx-textarea-autosize' | |||
48 | ], | 44 | ], |
49 | 45 | ||
50 | providers: [ | 46 | providers: [ |
51 | MarkdownService, | ||
52 | LinkifierService, | ||
53 | VideoCommentService | 47 | VideoCommentService |
54 | ] | 48 | ] |
55 | }) | 49 | }) |
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts deleted file mode 100644 index 7a66944b9..000000000 --- a/client/src/app/videos/shared/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './markdown.service' | ||
diff --git a/client/src/app/videos/video-list/video-local.component.ts b/client/src/app/videos/video-list/video-local.component.ts index c91c639ca..c0be4b885 100644 --- a/client/src/app/videos/video-list/video-local.component.ts +++ b/client/src/app/videos/video-list/video-local.component.ts | |||
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core' | |||
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { immutableAssign } from '@app/shared/misc/utils' | 3 | import { immutableAssign } from '@app/shared/misc/utils' |
4 | import { Location } from '@angular/common' | 4 | import { Location } from '@angular/common' |
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
7 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 6 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
8 | import { VideoSortField } from '../../shared/video/sort-field.type' | 7 | import { VideoSortField } from '../../shared/video/sort-field.type' |
@@ -10,6 +9,8 @@ import { VideoService } from '../../shared/video/video.service' | |||
10 | import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' | 9 | import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
12 | import { ScreenService } from '@app/shared/misc/screen.service' | 11 | import { ScreenService } from '@app/shared/misc/screen.service' |
12 | import { UserRight } from '../../../../../shared/models/users' | ||
13 | import { Notifier } from '@app/core' | ||
13 | 14 | ||
14 | @Component({ | 15 | @Component({ |
15 | selector: 'my-videos-local', | 16 | selector: 'my-videos-local', |
@@ -25,7 +26,7 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On | |||
25 | constructor ( | 26 | constructor ( |
26 | protected router: Router, | 27 | protected router: Router, |
27 | protected route: ActivatedRoute, | 28 | protected route: ActivatedRoute, |
28 | protected notificationsService: NotificationsService, | 29 | protected notifier: Notifier, |
29 | protected authService: AuthService, | 30 | protected authService: AuthService, |
30 | protected location: Location, | 31 | protected location: Location, |
31 | protected i18n: I18n, | 32 | protected i18n: I18n, |
@@ -40,6 +41,11 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On | |||
40 | ngOnInit () { | 41 | ngOnInit () { |
41 | super.ngOnInit() | 42 | super.ngOnInit() |
42 | 43 | ||
44 | if (this.authService.isLoggedIn()) { | ||
45 | const user = this.authService.getUser() | ||
46 | this.displayModerationBlock = user.hasRight(UserRight.SEE_ALL_VIDEOS) | ||
47 | } | ||
48 | |||
43 | this.generateSyndicationList() | 49 | this.generateSyndicationList() |
44 | } | 50 | } |
45 | 51 | ||
@@ -56,4 +62,10 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On | |||
56 | generateSyndicationList () { | 62 | generateSyndicationList () { |
57 | this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, this.filter, this.categoryOneOf) | 63 | this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, this.filter, this.categoryOneOf) |
58 | } | 64 | } |
65 | |||
66 | toggleModerationDisplay () { | ||
67 | this.filter = this.filter === 'local' ? 'all-local' as 'all-local' : 'local' as 'local' | ||
68 | |||
69 | this.reloadVideos() | ||
70 | } | ||
59 | } | 71 | } |
diff --git a/client/src/app/videos/video-list/video-overview.component.ts b/client/src/app/videos/video-list/video-overview.component.ts index 2c6054721..7ff52b259 100644 --- a/client/src/app/videos/video-list/video-overview.component.ts +++ b/client/src/app/videos/video-list/video-overview.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { AuthService } from '@app/core' | 2 | import { AuthService, Notifier } from '@app/core' |
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
5 | import { VideosOverview } from '@app/shared/overview/videos-overview.model' | 4 | import { VideosOverview } from '@app/shared/overview/videos-overview.model' |
6 | import { OverviewService } from '@app/shared/overview' | 5 | import { OverviewService } from '@app/shared/overview' |
@@ -21,7 +20,7 @@ export class VideoOverviewComponent implements OnInit { | |||
21 | 20 | ||
22 | constructor ( | 21 | constructor ( |
23 | private i18n: I18n, | 22 | private i18n: I18n, |
24 | private notificationsService: NotificationsService, | 23 | private notifier: Notifier, |
25 | private authService: AuthService, | 24 | private authService: AuthService, |
26 | private overviewService: OverviewService | 25 | private overviewService: OverviewService |
27 | ) { } | 26 | ) { } |
@@ -43,10 +42,7 @@ export class VideoOverviewComponent implements OnInit { | |||
43 | ) this.notResults = true | 42 | ) this.notResults = true |
44 | }, | 43 | }, |
45 | 44 | ||
46 | err => { | 45 | err => this.notifier.error(err.message) |
47 | console.log(err) | ||
48 | this.notificationsService.error('Error', err.text) | ||
49 | } | ||
50 | ) | 46 | ) |
51 | } | 47 | } |
52 | 48 | ||
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts index ac1fcfff3..f99c8abb6 100644 --- a/client/src/app/videos/video-list/video-recently-added.component.ts +++ b/client/src/app/videos/video-list/video-recently-added.component.ts | |||
@@ -2,13 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core' | |||
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Location } from '@angular/common' | 3 | import { Location } from '@angular/common' |
4 | import { immutableAssign } from '@app/shared/misc/utils' | 4 | import { immutableAssign } from '@app/shared/misc/utils' |
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
7 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 6 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
8 | import { VideoSortField } from '../../shared/video/sort-field.type' | 7 | import { VideoSortField } from '../../shared/video/sort-field.type' |
9 | import { VideoService } from '../../shared/video/video.service' | 8 | import { VideoService } from '../../shared/video/video.service' |
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 9 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { ScreenService } from '@app/shared/misc/screen.service' | 10 | import { ScreenService } from '@app/shared/misc/screen.service' |
11 | import { Notifier } from '@app/core' | ||
12 | 12 | ||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-videos-recently-added', | 14 | selector: 'my-videos-recently-added', |
@@ -24,7 +24,7 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On | |||
24 | protected router: Router, | 24 | protected router: Router, |
25 | protected route: ActivatedRoute, | 25 | protected route: ActivatedRoute, |
26 | protected location: Location, | 26 | protected location: Location, |
27 | protected notificationsService: NotificationsService, | 27 | protected notifier: Notifier, |
28 | protected authService: AuthService, | 28 | protected authService: AuthService, |
29 | protected i18n: I18n, | 29 | protected i18n: I18n, |
30 | protected screenService: ScreenService, | 30 | protected screenService: ScreenService, |
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts index 8f3d3842b..6fd74e67a 100644 --- a/client/src/app/videos/video-list/video-trending.component.ts +++ b/client/src/app/videos/video-list/video-trending.component.ts | |||
@@ -2,13 +2,13 @@ import { Component, OnDestroy, OnInit } from '@angular/core' | |||
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Location } from '@angular/common' | 3 | import { Location } from '@angular/common' |
4 | import { immutableAssign } from '@app/shared/misc/utils' | 4 | import { immutableAssign } from '@app/shared/misc/utils' |
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
7 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 6 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
8 | import { VideoSortField } from '../../shared/video/sort-field.type' | 7 | import { VideoSortField } from '../../shared/video/sort-field.type' |
9 | import { VideoService } from '../../shared/video/video.service' | 8 | import { VideoService } from '../../shared/video/video.service' |
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 9 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { ScreenService } from '@app/shared/misc/screen.service' | 10 | import { ScreenService } from '@app/shared/misc/screen.service' |
11 | import { Notifier, ServerService } from '@app/core' | ||
12 | 12 | ||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-videos-trending', | 14 | selector: 'my-videos-trending', |
@@ -23,22 +23,37 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit, | |||
23 | constructor ( | 23 | constructor ( |
24 | protected router: Router, | 24 | protected router: Router, |
25 | protected route: ActivatedRoute, | 25 | protected route: ActivatedRoute, |
26 | protected notificationsService: NotificationsService, | 26 | protected notifier: Notifier, |
27 | protected authService: AuthService, | 27 | protected authService: AuthService, |
28 | protected location: Location, | 28 | protected location: Location, |
29 | protected screenService: ScreenService, | 29 | protected screenService: ScreenService, |
30 | private serverService: ServerService, | ||
30 | protected i18n: I18n, | 31 | protected i18n: I18n, |
31 | private videoService: VideoService | 32 | private videoService: VideoService |
32 | ) { | 33 | ) { |
33 | super() | 34 | super() |
34 | |||
35 | this.titlePage = i18n('Trending') | ||
36 | } | 35 | } |
37 | 36 | ||
38 | ngOnInit () { | 37 | ngOnInit () { |
39 | super.ngOnInit() | 38 | super.ngOnInit() |
40 | 39 | ||
41 | this.generateSyndicationList() | 40 | this.generateSyndicationList() |
41 | |||
42 | this.serverService.configLoaded.subscribe( | ||
43 | () => { | ||
44 | const trendingDays = this.serverService.getConfig().trending.videos.intervalDays | ||
45 | |||
46 | if (trendingDays === 1) { | ||
47 | this.titlePage = this.i18n('Trending for the last 24 hours') | ||
48 | this.titleTooltip = this.i18n('Trending videos are those totalizing the greatest number of views during the last 24 hours.') | ||
49 | } else { | ||
50 | this.titlePage = this.i18n('Trending for the last {{days}} days', { days: trendingDays }) | ||
51 | this.titleTooltip = this.i18n( | ||
52 | 'Trending videos are those totalizing the greatest number of views during the last {{days}} days.', | ||
53 | { days: trendingDays } | ||
54 | ) | ||
55 | } | ||
56 | }) | ||
42 | } | 57 | } |
43 | 58 | ||
44 | ngOnDestroy () { | 59 | ngOnDestroy () { |
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts index 6e8959c54..bee828e12 100644 --- a/client/src/app/videos/video-list/video-user-subscriptions.component.ts +++ b/client/src/app/videos/video-list/video-user-subscriptions.component.ts | |||
@@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core' | |||
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { immutableAssign } from '@app/shared/misc/utils' | 3 | import { immutableAssign } from '@app/shared/misc/utils' |
4 | import { Location } from '@angular/common' | 4 | import { Location } from '@angular/common' |
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
7 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' | 6 | import { AbstractVideoList } from '../../shared/video/abstract-video-list' |
8 | import { VideoSortField } from '../../shared/video/sort-field.type' | 7 | import { VideoSortField } from '../../shared/video/sort-field.type' |
@@ -10,6 +9,7 @@ import { VideoService } from '../../shared/video/video.service' | |||
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 9 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { ScreenService } from '@app/shared/misc/screen.service' | 10 | import { ScreenService } from '@app/shared/misc/screen.service' |
12 | import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' | 11 | import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' |
12 | import { Notifier } from '@app/core' | ||
13 | 13 | ||
14 | @Component({ | 14 | @Component({ |
15 | selector: 'my-videos-user-subscriptions', | 15 | selector: 'my-videos-user-subscriptions', |
@@ -25,7 +25,7 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement | |||
25 | constructor ( | 25 | constructor ( |
26 | protected router: Router, | 26 | protected router: Router, |
27 | protected route: ActivatedRoute, | 27 | protected route: ActivatedRoute, |
28 | protected notificationsService: NotificationsService, | 28 | protected notifier: Notifier, |
29 | protected authService: AuthService, | 29 | protected authService: AuthService, |
30 | protected location: Location, | 30 | protected location: Location, |
31 | protected i18n: I18n, | 31 | protected i18n: I18n, |