aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-instance/about-instance.component.html63
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html12
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts26
-rw-r--r--client/src/app/+my-account/my-account.component.html37
-rw-r--r--client/src/app/+my-account/my-account.component.scss15
-rw-r--r--client/src/app/+my-account/my-account.component.ts97
-rw-r--r--client/src/app/core/core.module.ts2
-rw-r--r--client/src/app/core/server/server.service.ts3
-rw-r--r--client/src/app/login/login.component.html7
-rw-r--r--client/src/app/login/login.component.ts9
-rw-r--r--client/src/app/menu/language-chooser.component.html5
-rw-r--r--client/src/app/menu/language-chooser.component.scss7
-rw-r--r--client/src/app/search/search.component.ts3
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html20
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss6
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.ts8
-rw-r--r--client/src/app/shared/forms/form-validators/user-validators.service.ts20
-rw-r--r--client/src/app/shared/forms/form-validators/video-channel-validators.service.ts20
-rw-r--r--client/src/app/shared/menu/top-menu-dropdown.component.html18
-rw-r--r--client/src/app/shared/menu/top-menu-dropdown.component.scss14
-rw-r--r--client/src/app/shared/menu/top-menu-dropdown.component.ts75
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts83
-rw-r--r--client/src/app/shared/shared.module.ts5
-rw-r--r--client/src/app/shared/users/user.model.ts2
-rw-r--r--client/src/app/shared/users/user.service.ts9
-rw-r--r--client/src/app/signup/signup.component.html2
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts6
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.html5
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.scss4
-rw-r--r--client/src/app/videos/+video-watch/modal/video-report.component.ts16
30 files changed, 406 insertions, 193 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..37ff795f5 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,48 @@
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 i18n class="about-instance-title">
4 About {{ instanceName }} instance
5 </div>
4 6
5<div class="short-description"> 7 <div class="short-description">
6 <div>{{ shortDescription }}</div> 8 <div>{{ shortDescription }}</div>
7</div> 9 </div>
8 10
9<div class="description"> 11 <div class="description">
10 <div i18n class="section-title">Description</div> 12 <div i18n class="section-title">Description</div>
11 13
12 <div [innerHTML]="descriptionHTML"></div> 14 <div [innerHTML]="descriptionHTML"></div>
13</div> 15 </div>
14 16
15<div class="terms" id="terms-section"> 17 <div class="terms" id="terms-section">
16 <div i18n class="section-title">Terms</div> 18 <div i18n class="section-title">Terms</div>
17 19
18 <div [innerHTML]="termsHTML"></div> 20 <div [innerHTML]="termsHTML"></div>
19</div> 21 </div>
22
23 <div class="signup">
24 <div i18n class="section-title">Signup</div>
20 25
21<div class="signup"> 26 <div *ngIf="isSignupAllowed">
22 <div i18n class="section-title">Signup</div> 27 <ng-container i18n>User registration is allowed and</ng-container>
23 28
24 <div *ngIf="isSignupAllowed"> 29 <ng-container i18n *ngIf="userVideoQuota !== -1">
25 <ng-container i18n>User registration is allowed and</ng-container> 30 this instance provides a baseline quota of {{ userVideoQuota | bytes: 0 }} space for the videos of its users.
31 </ng-container>
26 32
27 <ng-container i18n *ngIf="userVideoQuota !== -1"> 33 <ng-container i18n *ngIf="userVideoQuota === -1">
28 this instance provides a baseline quota of {{ userVideoQuota | bytes: 0 }} space for the videos of its users. 34 this instance provides unlimited space for the videos of its users.
29 </ng-container> 35 </ng-container>
36 </div>
30 37
31 <ng-container i18n *ngIf="userVideoQuota === -1"> 38 <div i18n *ngIf="isSignupAllowed === false">
32 this instance provides unlimited space for the videos of its users. 39 User registration is currently not allowed.
33 </ng-container> 40 </div>
41 </div>
34 </div> 42 </div>
35 43
36 <div i18n *ngIf="isSignupAllowed === false"> 44 <div class="col-md-12 col-xl-6">
37 User registration is currently not allowed. 45 <label>Features found on this instance</label>
46 <my-instance-features-table></my-instance-features-table>
38 </div> 47 </div>
39</div> \ No newline at end of file 48</div>
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 5684004a5..556ab3c5d 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
@@ -65,7 +65,17 @@
65 <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span> 65 <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
66 </a> 66 </a>
67 </td> 67 </td>
68 <td>{{ user.email }}</td> 68 <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td>
69 <ng-template #emailWithVerificationStatus>
70 <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
71 <em>? {{ user.email }}</em>
72 </td>
73 <ng-template #emailVerifiedNotFalse>
74 <td i18n-title title="User's email is verified / User can login without email verification">
75 &#x2713; {{ user.email }}
76 </td>
77 </ng-template>
78 </ng-template>
69 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> 79 <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
70 <td>{{ user.roleLabel }}</td> 80 <td>{{ user.roleLabel }}</td>
71 <td>{{ user.createdAt }}</td> 81 <td>{{ user.createdAt }}</td>
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 31e783622..fb085c133 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,7 +1,7 @@
1import { Component, OnInit, ViewChild } from '@angular/core' 1import { Component, OnInit, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { NotificationsService } from 'angular2-notifications'
3import { SortMeta } from 'primeng/components/common/sortmeta' 3import { SortMeta } from 'primeng/components/common/sortmeta'
4import { ConfirmService } from '../../../core' 4import { ConfirmService, ServerService } from '../../../core'
5import { RestPagination, RestTable, UserService } from '../../../shared' 5import { RestPagination, RestTable, UserService } from '../../../shared'
6import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
7import { User } from '../../../../../../shared' 7import { User } from '../../../../../../shared'
@@ -28,12 +28,17 @@ export class UserListComponent extends RestTable implements OnInit {
28 constructor ( 28 constructor (
29 private notificationsService: NotificationsService, 29 private notificationsService: NotificationsService,
30 private confirmService: ConfirmService, 30 private confirmService: ConfirmService,
31 private serverService: ServerService,
31 private userService: UserService, 32 private userService: UserService,
32 private i18n: I18n 33 private i18n: I18n
33 ) { 34 ) {
34 super() 35 super()
35 } 36 }
36 37
38 get requiresEmailVerification () {
39 return this.serverService.getConfig().signup.requiresEmailVerification
40 }
41
37 ngOnInit () { 42 ngOnInit () {
38 this.initialize() 43 this.initialize()
39 44
@@ -51,6 +56,11 @@ export class UserListComponent extends RestTable implements OnInit {
51 label: this.i18n('Unban'), 56 label: this.i18n('Unban'),
52 handler: users => this.unbanUsers(users), 57 handler: users => this.unbanUsers(users),
53 isDisplayed: users => users.every(u => u.blocked === true) 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)
54 } 64 }
55 ] 65 ]
56 } 66 }
@@ -114,6 +124,20 @@ export class UserListComponent extends RestTable implements OnInit {
114 ) 124 )
115 } 125 }
116 126
127 async setEmailsAsVerified (users: User[]) {
128 this.userService.updateUsers(users, { emailVerified: true }).subscribe(
129 () => {
130 this.notificationsService.success(
131 this.i18n('Success'),
132 this.i18n('{{num}} users email set as verified.', { num: users.length })
133 )
134 this.loadData()
135 },
136
137 err => this.notificationsService.error(this.i18n('Error'), err.message)
138 )
139 }
140
117 isInSelectionMode () { 141 isInSelectionMode () {
118 return this.selectedUsers.length !== 0 142 return this.selectedUsers.length !== 0
119 } 143 }
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html
index 41333c25a..3999252be 100644
--- a/client/src/app/+my-account/my-account.component.html
+++ b/client/src/app/+my-account/my-account.component.html
@@ -1,40 +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 <div ngbDropdown class="misc">
23 <span role="button" class="title-page" [ngClass]="{ active: miscLabel !== '' }" ngbDropdownToggle>
24 <ng-container i18n>Misc</ng-container>
25 <ng-container *ngIf="miscLabel"> - {{ miscLabel }}</ng-container>
26 </span>
27
28 <div ngbDropdownMenu>
29 <a class="dropdown-item" i18n routerLink="/my-account/blocklist/accounts">Muted accounts</a>
30
31 <a class="dropdown-item" i18n routerLink="/my-account/blocklist/servers">Muted instances</a>
32
33 <a class="dropdown-item" i18n routerLink="/my-account/ownership">Ownership changes</a>
34 </div>
35 </div>
36
37 </div>
38 3
39 <div class="margin-content"> 4 <div class="margin-content">
40 <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 6243c6dcf..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, .misc { 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 d728caf07..d9381ebfa 100644
--- a/client/src/app/+my-account/my-account.component.ts
+++ b/client/src/app/+my-account/my-account.component.ts
@@ -1,38 +1,72 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { NavigationStart, Router } from '@angular/router'
4import { filter } from 'rxjs/operators'
5import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
6import { Subscription } from 'rxjs' 4import { 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})
13export class MyAccountComponent implements OnInit, OnDestroy { 11export class MyAccountComponent {
14 12 menuEntries: TopMenuDropdownParam[] = []
15 libraryLabel = ''
16 miscLabel = ''
17
18 private routeSub: Subscription
19 13
20 constructor ( 14 constructor (
21 private serverService: ServerService, 15 private serverService: ServerService,
22 private router: Router,
23 private i18n: I18n 16 private i18n: I18n
24 ) {} 17 ) {
18
19 const libraryEntries: TopMenuDropdownParam = {
20 label: this.i18n('My library'),
21 children: [
22 {
23 label: this.i18n('My channels'),
24 routerLink: '/my-account/videos'
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 }
25 36
26 ngOnInit () { 37 if (this.isVideoImportEnabled()) {
27 this.updateLabels(this.router.url) 38 libraryEntries.children.push({
39 label: 'My imports',
40 routerLink: '/my-account/video-imports'
41 })
42 }
28 43
29 this.routeSub = this.router.events 44 const miscEntries: TopMenuDropdownParam = {
30 .pipe(filter(event => event instanceof NavigationStart)) 45 label: this.i18n('Misc'),
31 .subscribe((event: NavigationStart) => this.updateLabels(event.url)) 46 children: [
32 } 47 {
48 label: this.i18n('Muted accounts'),
49 routerLink: '/my-account/blocklist/accounts'
50 },
51 {
52 label: this.i18n('Muted instances'),
53 routerLink: '/my-account/blocklist/servers'
54 },
55 {
56 label: this.i18n('Ownership changes'),
57 routerLink: '/my-account/ownership'
58 }
59 ]
60 }
33 61
34 ngOnDestroy () { 62 this.menuEntries = [
35 if (this.routeSub) this.routeSub.unsubscribe() 63 {
64 label: this.i18n('My settings'),
65 routerLink: '/my-account/settings'
66 },
67 libraryEntries,
68 miscEntries
69 ]
36 } 70 }
37 71
38 isVideoImportEnabled () { 72 isVideoImportEnabled () {
@@ -41,27 +75,4 @@ export class MyAccountComponent implements OnInit, OnDestroy {
41 return importConfig.http.enabled || importConfig.torrent.enabled 75 return importConfig.http.enabled || importConfig.torrent.enabled
42 } 76 }
43 77
44 private updateLabels (url: string) {
45 const [ path ] = url.split('?')
46
47 if (path.startsWith('/my-account/video-channels')) {
48 this.libraryLabel = this.i18n('Channels')
49 } else if (path.startsWith('/my-account/videos')) {
50 this.libraryLabel = this.i18n('Videos')
51 } else if (path.startsWith('/my-account/subscriptions')) {
52 this.libraryLabel = this.i18n('Subscriptions')
53 } else if (path.startsWith('/my-account/video-imports')) {
54 this.libraryLabel = this.i18n('Video imports')
55 } else {
56 this.libraryLabel = ''
57 }
58
59 if (path.startsWith('/my-account/blocklist/accounts')) {
60 this.miscLabel = this.i18n('Muted accounts')
61 } else if (path.startsWith('/my-account/blocklist/servers')) {
62 this.miscLabel = this.i18n('Muted instances')
63 } else {
64 this.miscLabel = ''
65 }
66 }
67} 78}
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index df2ec696d..8a6654aa1 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -29,7 +29,7 @@ import { CheatSheetComponent } from '@app/core/hotkeys'
29 29
30 LoadingBarHttpClientModule, 30 LoadingBarHttpClientModule,
31 LoadingBarRouterModule, 31 LoadingBarRouterModule,
32 LoadingBarModule.forRoot(), 32 LoadingBarModule,
33 33
34 HotkeyModule.forRoot({ 34 HotkeyModule.forRoot({
35 cheatSheetCloseEsc: true 35 cheatSheetCloseEsc: true
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index da8bd26db..6eccb8336 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -37,6 +37,9 @@ export class ServerService {
37 css: '' 37 css: ''
38 } 38 }
39 }, 39 },
40 email: {
41 enabled: false
42 },
40 serverVersion: 'Unknown', 43 serverVersion: 'Unknown',
41 signup: { 44 signup: {
42 allowed: false, 45 allowed: false,
diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html
index 93dbed525..9b8146624 100644
--- a/client/src/app/login/login.component.html
+++ b/client/src/app/login/login.component.html
@@ -59,7 +59,12 @@
59 </div> 59 </div>
60 60
61 <div class="modal-body"> 61 <div class="modal-body">
62 <div class="form-group"> 62
63 <div *ngIf="isEmailDisabled()" class="alert alert-danger" i18n>
64 We are sorry, you cannot recover you password because your instance administrator did not configure the PeerTube email system.
65 </div>
66
67 <div class="form-group" [hidden]="isEmailDisabled()">
63 <label i18n for="forgot-password-email">Email</label> 68 <label i18n for="forgot-password-email">Email</label>
64 <input 69 <input
65 type="email" id="forgot-password-email" i18n-placeholder placeholder="Email address" required 70 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..212a8ff1f 100644
--- a/client/src/app/login/login.component.ts
+++ b/client/src/app/login/login.component.ts
@@ -19,7 +19,6 @@ import { Router } from '@angular/router'
19export class LoginComponent extends FormReactive implements OnInit { 19export class LoginComponent extends FormReactive implements OnInit {
20 @ViewChild('emailInput') input: ElementRef 20 @ViewChild('emailInput') input: ElementRef
21 @ViewChild('forgotPasswordModal') forgotPasswordModal: ElementRef 21 @ViewChild('forgotPasswordModal') forgotPasswordModal: ElementRef
22 @ViewChild('forgotPasswordEmailInput') forgotPasswordEmailInput: ElementRef
23 22
24 error: string = null 23 error: string = null
25 forgotPasswordEmail = '' 24 forgotPasswordEmail = ''
@@ -45,6 +44,10 @@ export class LoginComponent extends FormReactive implements OnInit {
45 return this.serverService.getConfig().signup.allowed === true 44 return this.serverService.getConfig().signup.allowed === true
46 } 45 }
47 46
47 isEmailDisabled () {
48 return this.serverService.getConfig().email.enabled === false
49 }
50
48 ngOnInit () { 51 ngOnInit () {
49 this.buildForm({ 52 this.buildForm({
50 username: this.loginValidatorsService.LOGIN_USERNAME, 53 username: this.loginValidatorsService.LOGIN_USERNAME,
@@ -96,10 +99,6 @@ export class LoginComponent extends FormReactive implements OnInit {
96 ) 99 )
97 } 100 }
98 101
99 onForgotPasswordModalShown () {
100 this.forgotPasswordEmailInput.nativeElement.focus()
101 }
102
103 openForgotPasswordModal () { 102 openForgotPasswordModal () {
104 this.openedForgotPasswordModal = this.modalService.open(this.forgotPasswordModal) 103 this.openedForgotPasswordModal = this.modalService.open(this.forgotPasswordModal)
105 } 104 }
diff --git a/client/src/app/menu/language-chooser.component.html b/client/src/app/menu/language-chooser.component.html
index c37bf2826..c79609898 100644
--- a/client/src/app/menu/language-chooser.component.html
+++ b/client/src/app/menu/language-chooser.component.html
@@ -4,6 +4,11 @@
4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span> 4 <span class="close" aria-label="Close" role="button" (click)="hide()"></span>
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/search/search.component.ts b/client/src/app/search/search.component.ts
index ecffcafc1..3d17e6d96 100644
--- a/client/src/app/search/search.component.ts
+++ b/client/src/app/search/search.component.ts
@@ -146,7 +146,8 @@ export class SearchComponent implements OnInit, OnDestroy {
146 } 146 }
147 147
148 private updateTitle () { 148 private updateTitle () {
149 this.metaService.setTitle(this.i18n('Search') + ' ' + this.currentSearch) 149 const suffix = this.currentSearch ? ' ' + this.currentSearch : ''
150 this.metaService.setTitle(this.i18n('Search') + suffix)
150 } 151 }
151 152
152 private updateUrlFromAdvancedSearch () { 153 private updateUrlFromAdvancedSearch () {
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html
index 48230d6d8..90651f217 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/buttons/action-dropdown.component.html
@@ -8,14 +8,20 @@
8 </div> 8 </div>
9 9
10 <div ngbDropdownMenu class="dropdown-menu"> 10 <div ngbDropdownMenu class="dropdown-menu">
11 <ng-container *ngFor="let action of actions"> 11 <ng-container *ngFor="let actions of getActions()">
12 <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
13 <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a>
14 12
15 <span *ngIf="!action.linkBuilder" class="custom-action dropdown-item" (click)="action.handler(entry)" role="button"> 13 <ng-container *ngFor="let action of actions">
16 {{ action.label }} 14 <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true">
17 </span> 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>
18 </ng-container> 21 </ng-container>
22
23 <div class="dropdown-divider"></div>
24
19 </ng-container> 25 </ng-container>
20 </div> 26 </div>
21</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 92c4d1d2c..a4fcceeee 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.scss
+++ b/client/src/app/shared/buttons/action-dropdown.component.scss
@@ -1,6 +1,10 @@
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 10
@@ -52,4 +56,4 @@
52 width: 100%; 56 width: 100%;
53 } 57 }
54 } 58 }
55} \ No newline at end of file 59}
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts
index d8026ef41..275e2b51e 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.ts
+++ b/client/src/app/shared/buttons/action-dropdown.component.ts
@@ -14,10 +14,16 @@ export type DropdownAction<T> = {
14}) 14})
15 15
16export class ActionDropdownComponent<T> { 16export 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 = 'bottom-left' 19 @Input() placement = 'bottom-left'
20 @Input() buttonSize: 'normal' | 'small' = 'normal' 20 @Input() buttonSize: 'normal' | 'small' = 'normal'
21 @Input() label: string 21 @Input() label: string
22 @Input() theme: 'orange' | 'grey' = 'grey' 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 }
23} 29}
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 d14fa4777..b1c61d6df 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; underscores are allowed.')
35 } 35 }
36 } 36 }
37 37
@@ -88,13 +88,13 @@ 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
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 f62ff65f7..e657f36cf 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,28 +14,28 @@ 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; 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
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..2d6d1c4bf
--- /dev/null
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.html
@@ -0,0 +1,18 @@
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 (mouseenter)="openDropdownOnHover(dropdown)" role="button" class="title-page" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" ngbDropdownToggle>
8 <ng-container i18n>{{ menuEntry.label }}</ng-container>
9 <ng-container *ngIf="!!suffixLabels[menuEntry.label]"> - {{ suffixLabels[menuEntry.label] }}</ng-container>
10 </span>
11
12 <div ngbDropdownMenu>
13 <a *ngFor="let menuChild of menuEntry.children" class="dropdown-item" [routerLink]="menuChild.routerLink">{{ menuChild.label }}</a>
14 </div>
15 </div>
16
17 </ng-container>
18</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..f3ef8f814
--- /dev/null
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.scss
@@ -0,0 +1,14 @@
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}
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..272b721b2
--- /dev/null
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.ts
@@ -0,0 +1,75 @@
1import { Component, Input, OnDestroy, OnInit } from '@angular/core'
2import { filter, take } from 'rxjs/operators'
3import { NavigationStart, Router } from '@angular/router'
4import { Subscription } from 'rxjs'
5import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
6import { drop } from 'lodash-es'
7
8export type TopMenuDropdownParam = {
9 label: string
10 routerLink?: string
11
12 children?: {
13 label: string
14 routerLink: string
15 }[]
16}
17
18@Component({
19 selector: 'my-top-menu-dropdown',
20 templateUrl: './top-menu-dropdown.component.html',
21 styleUrls: [ './top-menu-dropdown.component.scss' ]
22})
23export class TopMenuDropdownComponent implements OnInit, OnDestroy {
24 @Input() menuEntries: TopMenuDropdownParam[] = []
25
26 suffixLabels: { [ parentLabel: string ]: string }
27
28 private openedOnHover = false
29 private routeSub: Subscription
30
31 constructor (private router: Router) {}
32
33 ngOnInit () {
34 this.updateChildLabels(window.location.pathname)
35
36 this.routeSub = this.router.events
37 .pipe(filter(event => event instanceof NavigationStart))
38 .subscribe(() => this.updateChildLabels(window.location.pathname))
39 }
40
41 ngOnDestroy () {
42 if (this.routeSub) this.routeSub.unsubscribe()
43 }
44
45 openDropdownOnHover (dropdown: NgbDropdown) {
46 this.openedOnHover = true
47 dropdown.open()
48
49 // Menu was closed
50 dropdown.openChange
51 .pipe(take(1))
52 .subscribe(e => this.openedOnHover = false)
53 }
54
55 closeDropdownIfHovered (dropdown: NgbDropdown) {
56 if (this.openedOnHover === false) return
57
58 dropdown.close()
59 this.openedOnHover = false
60 }
61
62 private updateChildLabels (path: string) {
63 this.suffixLabels = {}
64
65 for (const entry of this.menuEntries) {
66 if (!entry.children) continue
67
68 for (const child of entry.children) {
69 if (path.startsWith(child.routerLink)) {
70 this.suffixLabels[entry.label] = child.label
71 }
72 }
73 }
74 }
75}
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 908f0b8e0..e3c9db923 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
@@ -4,7 +4,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' 4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
5import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component' 5import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
6import { UserService } from '@app/shared/users' 6import { UserService } from '@app/shared/users'
7import { AuthService, ConfirmService } from '@app/core' 7import { AuthService, ConfirmService, ServerService } from '@app/core'
8import { User, UserRight } from '../../../../../shared/models/users' 8import { User, UserRight } from '../../../../../shared/models/users'
9import { Account } from '@app/shared/account/account.model' 9import { Account } from '@app/shared/account/account.model'
10import { BlocklistService } from '@app/shared/blocklist' 10import { BlocklistService } from '@app/shared/blocklist'
@@ -26,17 +26,22 @@ export class UserModerationDropdownComponent implements OnChanges {
26 @Output() userChanged = new EventEmitter() 26 @Output() userChanged = new EventEmitter()
27 @Output() userDeleted = new EventEmitter() 27 @Output() userDeleted = new EventEmitter()
28 28
29 userActions: DropdownAction<{ user: User, account: Account }>[] = [] 29 userActions: DropdownAction<{ user: User, account: Account }>[][] = []
30 30
31 constructor ( 31 constructor (
32 private authService: AuthService, 32 private authService: AuthService,
33 private notificationsService: NotificationsService, 33 private notificationsService: NotificationsService,
34 private confirmService: ConfirmService, 34 private confirmService: ConfirmService,
35 private serverService: ServerService,
35 private userService: UserService, 36 private userService: UserService,
36 private blocklistService: BlocklistService, 37 private blocklistService: BlocklistService,
37 private i18n: I18n 38 private i18n: I18n
38 ) { } 39 ) { }
39 40
41 get requiresEmailVerification () {
42 return this.serverService.getConfig().signup.requiresEmailVerification
43 }
44
40 ngOnChanges () { 45 ngOnChanges () {
41 this.buildActions() 46 this.buildActions()
42 } 47 }
@@ -97,6 +102,21 @@ export class UserModerationDropdownComponent implements OnChanges {
97 ) 102 )
98 } 103 }
99 104
105 setEmailAsVerified (user: User) {
106 this.userService.updateUser(user.id, { emailVerified: true }).subscribe(
107 () => {
108 this.notificationsService.success(
109 this.i18n('Success'),
110 this.i18n('User {{username}} email set as verified', { username: user.username })
111 )
112
113 this.userChanged.emit()
114 },
115
116 err => this.notificationsService.error(this.i18n('Error'), err.message)
117 )
118 }
119
100 blockAccountByUser (account: Account) { 120 blockAccountByUser (account: Account) {
101 this.blocklistService.blockAccountByUser(account) 121 this.blocklistService.blockAccountByUser(account)
102 .subscribe( 122 .subscribe(
@@ -246,7 +266,7 @@ export class UserModerationDropdownComponent implements OnChanges {
246 if (this.user && authUser.id === this.user.id) return 266 if (this.user && authUser.id === this.user.id) return
247 267
248 if (this.user && authUser.hasRight(UserRight.MANAGE_USERS)) { 268 if (this.user && authUser.hasRight(UserRight.MANAGE_USERS)) {
249 this.userActions = this.userActions.concat([ 269 this.userActions.push([
250 { 270 {
251 label: this.i18n('Edit'), 271 label: this.i18n('Edit'),
252 linkBuilder: ({ user }) => this.getRouterUserEditLink(user) 272 linkBuilder: ({ user }) => this.getRouterUserEditLink(user)
@@ -257,13 +277,18 @@ export class UserModerationDropdownComponent implements OnChanges {
257 }, 277 },
258 { 278 {
259 label: this.i18n('Ban'), 279 label: this.i18n('Ban'),
260 handler: ({ user }: { user: User }) => this.openBanUserModal(user), 280 handler: ({ user }) => this.openBanUserModal(user),
261 isDisplayed: ({ user }: { user: User }) => !user.blocked 281 isDisplayed: ({ user }) => !user.blocked
262 }, 282 },
263 { 283 {
264 label: this.i18n('Unban'), 284 label: this.i18n('Unban'),
265 handler: ({ user }: { user: User }) => this.unbanUser(user), 285 handler: ({ user }) => this.unbanUser(user),
266 isDisplayed: ({ user }: { user: User }) => user.blocked 286 isDisplayed: ({ user }) => user.blocked
287 },
288 {
289 label: this.i18n('Set Email as Verified'),
290 handler: ({ user }) => this.setEmailAsVerified(user),
291 isDisplayed: ({ user }) => this.requiresEmailVerification && !user.blocked && user.emailVerified === false
267 } 292 }
268 ]) 293 ])
269 } 294 }
@@ -271,60 +296,66 @@ export class UserModerationDropdownComponent implements OnChanges {
271 // Actions on accounts/servers 296 // Actions on accounts/servers
272 if (this.account) { 297 if (this.account) {
273 // User actions 298 // User actions
274 this.userActions = this.userActions.concat([ 299 this.userActions.push([
275 { 300 {
276 label: this.i18n('Mute this account'), 301 label: this.i18n('Mute this account'),
277 isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === false, 302 isDisplayed: ({ account }) => account.mutedByUser === false,
278 handler: ({ account }: { account: Account }) => this.blockAccountByUser(account) 303 handler: ({ account }) => this.blockAccountByUser(account)
279 }, 304 },
280 { 305 {
281 label: this.i18n('Unmute this account'), 306 label: this.i18n('Unmute this account'),
282 isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === true, 307 isDisplayed: ({ account }) => account.mutedByUser === true,
283 handler: ({ account }: { account: Account }) => this.unblockAccountByUser(account) 308 handler: ({ account }) => this.unblockAccountByUser(account)
284 }, 309 },
285 { 310 {
286 label: this.i18n('Mute the instance'), 311 label: this.i18n('Mute the instance'),
287 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false, 312 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false,
288 handler: ({ account }: { account: Account }) => this.blockServerByUser(account.host) 313 handler: ({ account }) => this.blockServerByUser(account.host)
289 }, 314 },
290 { 315 {
291 label: this.i18n('Unmute the instance'), 316 label: this.i18n('Unmute the instance'),
292 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true, 317 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
293 handler: ({ account }: { account: Account }) => this.unblockServerByUser(account.host) 318 handler: ({ account }) => this.unblockServerByUser(account.host)
294 } 319 }
295 ]) 320 ])
296 321
322 let instanceActions: DropdownAction<{ user: User, account: Account }>[] = []
323
297 // Instance actions 324 // Instance actions
298 if (authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) { 325 if (authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) {
299 this.userActions = this.userActions.concat([ 326 instanceActions = instanceActions.concat([
300 { 327 {
301 label: this.i18n('Mute this account by your instance'), 328 label: this.i18n('Mute this account by your instance'),
302 isDisplayed: ({ account }: { account: Account }) => account.mutedByInstance === false, 329 isDisplayed: ({ account }) => account.mutedByInstance === false,
303 handler: ({ account }: { account: Account }) => this.blockAccountByInstance(account) 330 handler: ({ account }) => this.blockAccountByInstance(account)
304 }, 331 },
305 { 332 {
306 label: this.i18n('Unmute this account by your instance'), 333 label: this.i18n('Unmute this account by your instance'),
307 isDisplayed: ({ account }: { account: Account }) => account.mutedByInstance === true, 334 isDisplayed: ({ account }) => account.mutedByInstance === true,
308 handler: ({ account }: { account: Account }) => this.unblockAccountByInstance(account) 335 handler: ({ account }) => this.unblockAccountByInstance(account)
309 } 336 }
310 ]) 337 ])
311 } 338 }
312 339
313 // Instance actions 340 // Instance actions
314 if (authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) { 341 if (authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) {
315 this.userActions = this.userActions.concat([ 342 instanceActions = instanceActions.concat([
316 { 343 {
317 label: this.i18n('Mute the instance by your instance'), 344 label: this.i18n('Mute the instance by your instance'),
318 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false, 345 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === false,
319 handler: ({ account }: { account: Account }) => this.blockServerByInstance(account.host) 346 handler: ({ account }) => this.blockServerByInstance(account.host)
320 }, 347 },
321 { 348 {
322 label: this.i18n('Unmute the instance by your instance'), 349 label: this.i18n('Unmute the instance by your instance'),
323 isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true, 350 isDisplayed: ({ account }) => !account.userId && account.mutedServerByInstance === true,
324 handler: ({ account }: { account: Account }) => this.unblockServerByInstance(account.host) 351 handler: ({ account }) => this.unblockServerByInstance(account.host)
325 } 352 }
326 ]) 353 ])
327 } 354 }
355
356 if (instanceActions.length !== 0) {
357 this.userActions.push(instanceActions)
358 }
328 } 359 }
329 } 360 }
330 } 361 }
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index a2fa27b72..9810e9485 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -61,6 +61,7 @@ import { OverviewService } from '@app/shared/overview'
61import { UserBanModalComponent } from '@app/shared/moderation' 61import { UserBanModalComponent } from '@app/shared/moderation'
62import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component' 62import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component'
63import { BlocklistService } from '@app/shared/blocklist' 63import { BlocklistService } from '@app/shared/blocklist'
64import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.component'
64 65
65@NgModule({ 66@NgModule({
66 imports: [ 67 imports: [
@@ -102,7 +103,8 @@ import { BlocklistService } from '@app/shared/blocklist'
102 RemoteSubscribeComponent, 103 RemoteSubscribeComponent,
103 InstanceFeaturesTableComponent, 104 InstanceFeaturesTableComponent,
104 UserBanModalComponent, 105 UserBanModalComponent,
105 UserModerationDropdownComponent 106 UserModerationDropdownComponent,
107 TopMenuDropdownComponent
106 ], 108 ],
107 109
108 exports: [ 110 exports: [
@@ -141,6 +143,7 @@ import { BlocklistService } from '@app/shared/blocklist'
141 InstanceFeaturesTableComponent, 143 InstanceFeaturesTableComponent,
142 UserBanModalComponent, 144 UserBanModalComponent,
143 UserModerationDropdownComponent, 145 UserModerationDropdownComponent,
146 TopMenuDropdownComponent,
144 147
145 NumberFormatterPipe, 148 NumberFormatterPipe,
146 ObjectLengthPipe, 149 ObjectLengthPipe,
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
index 7c840ffa7..9819829fd 100644
--- a/client/src/app/shared/users/user.model.ts
+++ b/client/src/app/shared/users/user.model.ts
@@ -15,6 +15,7 @@ export type UserConstructorHash = {
15 username: string, 15 username: string,
16 email: string, 16 email: string,
17 role: UserRole, 17 role: UserRole,
18 emailVerified?: boolean,
18 videoQuota?: number, 19 videoQuota?: number,
19 videoQuotaDaily?: number, 20 videoQuotaDaily?: number,
20 nsfwPolicy?: NSFWPolicyType, 21 nsfwPolicy?: NSFWPolicyType,
@@ -31,6 +32,7 @@ export class User implements UserServerModel {
31 id: number 32 id: number
32 username: string 33 username: string
33 email: string 34 email: string
35 emailVerified: boolean
34 role: UserRole 36 role: UserRole
35 nsfwPolicy: NSFWPolicyType 37 nsfwPolicy: NSFWPolicyType
36 webTorrentEnabled: boolean 38 webTorrentEnabled: boolean
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
index 27a81f0a2..cc5c051f1 100644
--- a/client/src/app/shared/users/user.service.ts
+++ b/client/src/app/shared/users/user.service.ts
@@ -153,6 +153,15 @@ 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)))
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html
index 0207a166e..07d24b381 100644
--- a/client/src/app/signup/signup.component.html
+++ b/client/src/app/signup/signup.component.html
@@ -64,7 +64,7 @@
64 </form> 64 </form>
65 65
66 <div> 66 <div>
67 <label for="email" i18n>Features found on this instance</label> 67 <label i18n>Features found on this instance</label>
68 <my-instance-features-table></my-instance-features-table> 68 <my-instance-features-table></my-instance-features-table>
69 </div> 69 </div>
70 </div> 70 </div>
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 3fcb71ac3..7ea3691fa 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
@@ -117,12 +117,6 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
117 const videofile = this.videofileInput.nativeElement.files[0] 117 const videofile = this.videofileInput.nativeElement.files[0]
118 if (!videofile) return 118 if (!videofile) return
119 119
120 // Cannot upload videos > 8GB for now
121 if (videofile.size > 8 * 1024 * 1024 * 1024) {
122 this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 8GB'))
123 return
124 }
125
126 const bytePipes = new BytesPipe() 120 const bytePipes = new BytesPipe()
127 const videoQuota = this.authService.getUser().videoQuota 121 const videoQuota = this.authService.getUser().videoQuota
128 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) { 122 if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
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..733c01be0 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
@@ -6,6 +6,11 @@
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
4textarea { 8textarea {
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..023387984 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
@@ -33,6 +33,18 @@ export class VideoReportComponent extends FormReactive implements OnInit {
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
@@ -61,4 +73,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
61 err => this.notificationsService.error(this.i18n('Error'), err.message) 73 err => this.notificationsService.error(this.i18n('Error'), err.message)
62 ) 74 )
63 } 75 }
76
77 isRemoteVideo () {
78 return !this.video.isLocal
79 }
64} 80}