diff options
Diffstat (limited to 'client/src/app')
133 files changed, 1666 insertions, 388 deletions
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/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 69f648269..c1377c1ea 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html | |||
@@ -8,6 +8,18 @@ | |||
8 | <div class="actor-names"> | 8 | <div class="actor-names"> |
9 | <div class="actor-display-name">{{ account.displayName }}</div> | 9 | <div class="actor-display-name">{{ account.displayName }}</div> |
10 | <div class="actor-name">{{ account.nameWithHost }}</div> | 10 | <div class="actor-name">{{ account.nameWithHost }}</div> |
11 | |||
12 | <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span> | ||
13 | <span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span> | ||
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 | > | ||
22 | </my-user-moderation-dropdown> | ||
11 | </div> | 23 | </div> |
12 | <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div> | 24 | <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div> |
13 | </div> | 25 | </div> |
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss index 909b65bc7..3cedda889 100644 --- a/client/src/app/+accounts/accounts.component.scss +++ b/client/src/app/+accounts/accounts.component.scss | |||
@@ -3,4 +3,16 @@ | |||
3 | 3 | ||
4 | .sub-menu { | 4 | .sub-menu { |
5 | @include sub-menu-with-actor; | 5 | @include sub-menu-with-actor; |
6 | } | ||
7 | |||
8 | my-user-moderation-dropdown, | ||
9 | .badge { | ||
10 | margin-left: 10px; | ||
11 | |||
12 | position: relative; | ||
13 | top: 3px; | ||
14 | } | ||
15 | |||
16 | .badge { | ||
17 | font-size: 13px; | ||
6 | } \ No newline at end of file | 18 | } \ No newline at end of file |
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index af0451e91..e19927d6b 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts | |||
@@ -1,10 +1,14 @@ | |||
1 | import { Component, OnInit, OnDestroy } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute } from '@angular/router' | 2 | import { ActivatedRoute } from '@angular/router' |
3 | import { AccountService } from '@app/shared/account/account.service' | 3 | import { AccountService } from '@app/shared/account/account.service' |
4 | import { Account } from '@app/shared/account/account.model' | 4 | import { Account } from '@app/shared/account/account.model' |
5 | import { RestExtractor } from '@app/shared' | 5 | import { RestExtractor, UserService } from '@app/shared' |
6 | import { catchError, switchMap, distinctUntilChanged, map } 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' | ||
9 | import { User, UserRight } from '../../../../shared' | ||
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
11 | import { AuthService, RedirectService } from '@app/core' | ||
8 | 12 | ||
9 | @Component({ | 13 | @Component({ |
10 | templateUrl: './accounts.component.html', | 14 | templateUrl: './accounts.component.html', |
@@ -12,13 +16,19 @@ import { Subscription } from 'rxjs' | |||
12 | }) | 16 | }) |
13 | export class AccountsComponent implements OnInit, OnDestroy { | 17 | export class AccountsComponent implements OnInit, OnDestroy { |
14 | account: Account | 18 | account: Account |
19 | user: User | ||
15 | 20 | ||
16 | private routeSub: Subscription | 21 | private routeSub: Subscription |
17 | 22 | ||
18 | constructor ( | 23 | constructor ( |
19 | private route: ActivatedRoute, | 24 | private route: ActivatedRoute, |
25 | private userService: UserService, | ||
20 | private accountService: AccountService, | 26 | private accountService: AccountService, |
21 | private restExtractor: RestExtractor | 27 | private notificationsService: NotificationsService, |
28 | private restExtractor: RestExtractor, | ||
29 | private redirectService: RedirectService, | ||
30 | private authService: AuthService, | ||
31 | private i18n: I18n | ||
22 | ) {} | 32 | ) {} |
23 | 33 | ||
24 | ngOnInit () { | 34 | ngOnInit () { |
@@ -27,12 +37,40 @@ export class AccountsComponent implements OnInit, OnDestroy { | |||
27 | map(params => params[ 'accountId' ]), | 37 | map(params => params[ 'accountId' ]), |
28 | distinctUntilChanged(), | 38 | distinctUntilChanged(), |
29 | switchMap(accountId => this.accountService.getAccount(accountId)), | 39 | switchMap(accountId => this.accountService.getAccount(accountId)), |
40 | tap(account => this.getUserIfNeeded(account)), | ||
30 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) | 41 | catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) |
31 | ) | 42 | ) |
32 | .subscribe(account => this.account = account) | 43 | .subscribe( |
44 | account => this.account = account, | ||
45 | |||
46 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
47 | ) | ||
33 | } | 48 | } |
34 | 49 | ||
35 | ngOnDestroy () { | 50 | ngOnDestroy () { |
36 | if (this.routeSub) this.routeSub.unsubscribe() | 51 | if (this.routeSub) this.routeSub.unsubscribe() |
37 | } | 52 | } |
53 | |||
54 | onUserChanged () { | ||
55 | this.getUserIfNeeded(this.account) | ||
56 | } | ||
57 | |||
58 | onUserDeleted () { | ||
59 | this.redirectService.redirectToHomepage() | ||
60 | } | ||
61 | |||
62 | private getUserIfNeeded (account: Account) { | ||
63 | if (!account.userId) return | ||
64 | if (!this.authService.isLoggedIn()) return | ||
65 | |||
66 | const user = this.authService.getUser() | ||
67 | if (user.hasRight(UserRight.MANAGE_USERS)) { | ||
68 | this.userService.getUser(account.userId) | ||
69 | .subscribe( | ||
70 | user => this.user = user, | ||
71 | |||
72 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
73 | ) | ||
74 | } | ||
75 | } | ||
38 | } | 76 | } |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 5784609ef..c06ae1d60 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -10,12 +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, UserService, UserUpdateComponent } from './users' | 13 | import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' |
14 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' | 14 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' |
15 | import { UserBanModalComponent } from '@app/+admin/users/user-list/user-ban-modal.component' | ||
16 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' | 15 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' |
17 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' | 16 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' |
18 | 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' | ||
19 | 19 | ||
20 | @NgModule({ | 20 | @NgModule({ |
21 | imports: [ | 21 | imports: [ |
@@ -37,12 +37,13 @@ import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service | |||
37 | UserCreateComponent, | 37 | UserCreateComponent, |
38 | UserUpdateComponent, | 38 | UserUpdateComponent, |
39 | UserListComponent, | 39 | UserListComponent, |
40 | UserBanModalComponent, | ||
41 | 40 | ||
42 | ModerationComponent, | 41 | ModerationComponent, |
43 | VideoBlacklistListComponent, | 42 | VideoBlacklistListComponent, |
44 | VideoAbuseListComponent, | 43 | VideoAbuseListComponent, |
45 | ModerationCommentModalComponent, | 44 | ModerationCommentModalComponent, |
45 | InstanceServerBlocklistComponent, | ||
46 | InstanceAccountBlocklistComponent, | ||
46 | 47 | ||
47 | JobsComponent, | 48 | JobsComponent, |
48 | JobsListComponent, | 49 | JobsListComponent, |
@@ -58,7 +59,6 @@ import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service | |||
58 | providers: [ | 59 | providers: [ |
59 | FollowService, | 60 | FollowService, |
60 | RedundancyService, | 61 | RedundancyService, |
61 | UserService, | ||
62 | JobService, | 62 | JobService, |
63 | ConfigService | 63 | ConfigService |
64 | ] | 64 | ] |
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..dfbbfbb29 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 | |||
@@ -112,7 +112,7 @@ | |||
112 | 112 | ||
113 | <my-peertube-checkbox | 113 | <my-peertube-checkbox |
114 | inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled" | 114 | inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled" |
115 | i18n-labelText labelText="Video import with HTTP enabled" | 115 | i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled" |
116 | ></my-peertube-checkbox> | 116 | ></my-peertube-checkbox> |
117 | 117 | ||
118 | <my-peertube-checkbox | 118 | <my-peertube-checkbox |
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..f48b6fc1a 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,6 +1,5 @@ | |||
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 { NotificationsService } from 'angular2-notifications' |
@@ -29,7 +28,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
29 | private notificationsService: NotificationsService, | 28 | private notificationsService: NotificationsService, |
30 | private configService: ConfigService, | 29 | private configService: ConfigService, |
31 | private serverService: ServerService, | 30 | private serverService: ServerService, |
32 | private confirmService: ConfirmService, | ||
33 | private i18n: I18n | 31 | private i18n: I18n |
34 | ) { | 32 | ) { |
35 | super() | 33 | super() |
@@ -64,7 +62,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
64 | } | 62 | } |
65 | 63 | ||
66 | ngOnInit () { | 64 | ngOnInit () { |
67 | const formGroupData = { | 65 | const formGroupData: { [key: string]: any } = { |
68 | instanceName: this.customConfigValidatorsService.INSTANCE_NAME, | 66 | instanceName: this.customConfigValidatorsService.INSTANCE_NAME, |
69 | instanceShortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, | 67 | instanceShortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION, |
70 | instanceDescription: null, | 68 | instanceDescription: null, |
@@ -124,28 +122,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
124 | } | 122 | } |
125 | 123 | ||
126 | async formValidated () { | 124 | async formValidated () { |
127 | const newCustomizationJavascript = this.form.value['customizationJavascript'] | ||
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 = { | 125 | const data: CustomConfig = { |
150 | instance: { | 126 | instance: { |
151 | name: this.form.value['instanceName'], | 127 | name: this.form.value['instanceName'], |
@@ -226,7 +202,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
226 | } | 202 | } |
227 | 203 | ||
228 | private updateForm () { | 204 | private updateForm () { |
229 | const data = { | 205 | const data: { [key: string]: any } = { |
230 | instanceName: this.customConfig.instance.name, | 206 | instanceName: this.customConfig.instance.name, |
231 | instanceShortDescription: this.customConfig.instance.shortDescription, | 207 | instanceShortDescription: this.customConfig.instance.shortDescription, |
232 | instanceDescription: this.customConfig.instance.description, | 208 | instanceDescription: this.customConfig.instance.description, |
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..4a25b7ff3 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 | |||
@@ -28,7 +28,7 @@ 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 () { |
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..9b7029f75 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 | |||
@@ -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) { |
@@ -53,7 +53,7 @@ export class FollowingListComponent extends RestTable implements OnInit { | |||
53 | } | 53 | } |
54 | 54 | ||
55 | protected loadData () { | 55 | protected loadData () { |
56 | this.followService.getFollowing(this.pagination, this.sort) | 56 | this.followService.getFollowing(this.pagination, this.sort, this.search) |
57 | .subscribe( | 57 | .subscribe( |
58 | resultList => { | 58 | resultList => { |
59 | this.following = resultList.data | 59 | this.following = resultList.data |
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/jobs/jobs-list/jobs-list.component.ts b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts index 866ba1b23..44778ab56 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 | |||
@@ -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 () { |
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..3f243aee4 --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts | |||
@@ -0,0 +1,59 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { NotificationsService } from 'angular2-notifications' | ||
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, AccountBlock } 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 notificationsService: NotificationsService, | ||
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.notificationsService.success( | ||
39 | this.i18n('Success'), | ||
40 | this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost }) | ||
41 | ) | ||
42 | |||
43 | this.loadData() | ||
44 | } | ||
45 | ) | ||
46 | } | ||
47 | |||
48 | protected loadData () { | ||
49 | return this.blocklistService.getInstanceAccountBlocklist(this.pagination, this.sort) | ||
50 | .subscribe( | ||
51 | resultList => { | ||
52 | this.blockedAccounts = resultList.data | ||
53 | this.totalRecords = resultList.total | ||
54 | }, | ||
55 | |||
56 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
57 | ) | ||
58 | } | ||
59 | } | ||
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..130009dc7 --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts | |||
@@ -0,0 +1,60 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { NotificationsService } from 'angular2-notifications' | ||
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 notificationsService: NotificationsService, | ||
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.notificationsService.success( | ||
40 | this.i18n('Success'), | ||
41 | this.i18n('Instance {{host}} unmuted by your instance.', { host }) | ||
42 | ) | ||
43 | |||
44 | this.loadData() | ||
45 | } | ||
46 | ) | ||
47 | } | ||
48 | |||
49 | protected loadData () { | ||
50 | return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort) | ||
51 | .subscribe( | ||
52 | resultList => { | ||
53 | this.blockedServers = resultList.data | ||
54 | this.totalRecords = resultList.total | ||
55 | }, | ||
56 | |||
57 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
58 | ) | ||
59 | } | ||
60 | } | ||
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.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/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html index 287ab3e46..0374b70ef 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 | ||
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..7a219c846 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 | |||
@@ -36,7 +36,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
36 | 36 | ||
37 | this.videoAbuseActions = [ | 37 | this.videoAbuseActions = [ |
38 | { | 38 | { |
39 | label: this.i18n('Delete'), | 39 | label: this.i18n('Delete this report'), |
40 | handler: videoAbuse => this.removeVideoAbuse(videoAbuse) | 40 | handler: videoAbuse => this.removeVideoAbuse(videoAbuse) |
41 | }, | 41 | }, |
42 | { | 42 | { |
@@ -57,7 +57,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
57 | } | 57 | } |
58 | 58 | ||
59 | ngOnInit () { | 59 | ngOnInit () { |
60 | this.loadSort() | 60 | this.initialize() |
61 | } | 61 | } |
62 | 62 | ||
63 | openModerationCommentModal (videoAbuse: VideoAbuse) { | 63 | openModerationCommentModal (videoAbuse: VideoAbuse) { |
@@ -85,7 +85,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { | |||
85 | } | 85 | } |
86 | 86 | ||
87 | async removeVideoAbuse (videoAbuse: VideoAbuse) { | 87 | async removeVideoAbuse (videoAbuse: VideoAbuse) { |
88 | const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse?'), this.i18n('Delete')) | 88 | 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 | 89 | if (res === false) return |
90 | 90 | ||
91 | this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( | 91 | this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe( |
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..ff4543b97 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 | |||
@@ -8,7 +8,7 @@ | |||
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 pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> | 10 | <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> |
11 | <th style="width: 50px;"></th> | 11 | <th style="width: 120px;"></th> |
12 | </tr> | 12 | </tr> |
13 | </ng-template> | 13 | </ng-template> |
14 | 14 | ||
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..e491edaca 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 | |||
@@ -39,7 +39,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit { | |||
39 | } | 39 | } |
40 | 40 | ||
41 | ngOnInit () { | 41 | ngOnInit () { |
42 | this.loadSort() | 42 | this.initialize() |
43 | } | 43 | } |
44 | 44 | ||
45 | getVideoUrl (videoBlacklist: VideoBlacklist) { | 45 | getVideoUrl (videoBlacklist: VideoBlacklist) { |
diff --git a/client/src/app/+admin/users/index.ts b/client/src/app/+admin/users/index.ts index efcd0d9cb..156e54d89 100644 --- a/client/src/app/+admin/users/index.ts +++ b/client/src/app/+admin/users/index.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | export * from './shared' | ||
2 | export * from './user-edit' | 1 | export * from './user-edit' |
3 | export * from './user-list' | 2 | export * from './user-list' |
4 | export * from './users.component' | 3 | export * from './users.component' |
diff --git a/client/src/app/+admin/users/shared/index.ts b/client/src/app/+admin/users/shared/index.ts deleted file mode 100644 index 1f1302dc5..000000000 --- a/client/src/app/+admin/users/shared/index.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export * from './user.service' | ||
diff --git a/client/src/app/+admin/users/shared/user.service.ts b/client/src/app/+admin/users/shared/user.service.ts deleted file mode 100644 index 470beef08..000000000 --- a/client/src/app/+admin/users/shared/user.service.ts +++ /dev/null | |||
@@ -1,96 +0,0 @@ | |||
1 | import { catchError, map } from 'rxjs/operators' | ||
2 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { BytesPipe } from 'ngx-pipes' | ||
5 | import { SortMeta } from 'primeng/components/common/sortmeta' | ||
6 | import { Observable } from 'rxjs' | ||
7 | import { ResultList, UserCreate, UserUpdate, User, UserRole } from '../../../../../../shared' | ||
8 | import { environment } from '../../../../environments/environment' | ||
9 | import { RestExtractor, RestPagination, RestService } from '../../../shared' | ||
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
11 | |||
12 | @Injectable() | ||
13 | export class UserService { | ||
14 | private static BASE_USERS_URL = environment.apiUrl + '/api/v1/users/' | ||
15 | private bytesPipe = new BytesPipe() | ||
16 | |||
17 | constructor ( | ||
18 | private authHttp: HttpClient, | ||
19 | private restService: RestService, | ||
20 | private restExtractor: RestExtractor, | ||
21 | private i18n: I18n | ||
22 | ) { } | ||
23 | |||
24 | addUser (userCreate: UserCreate) { | ||
25 | return this.authHttp.post(UserService.BASE_USERS_URL, userCreate) | ||
26 | .pipe( | ||
27 | map(this.restExtractor.extractDataBool), | ||
28 | catchError(err => this.restExtractor.handleError(err)) | ||
29 | ) | ||
30 | } | ||
31 | |||
32 | updateUser (userId: number, userUpdate: UserUpdate) { | ||
33 | return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate) | ||
34 | .pipe( | ||
35 | map(this.restExtractor.extractDataBool), | ||
36 | catchError(err => this.restExtractor.handleError(err)) | ||
37 | ) | ||
38 | } | ||
39 | |||
40 | getUser (userId: number) { | ||
41 | return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId) | ||
42 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
43 | } | ||
44 | |||
45 | getUsers (pagination: RestPagination, sort: SortMeta): Observable<ResultList<User>> { | ||
46 | let params = new HttpParams() | ||
47 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
48 | |||
49 | return this.authHttp.get<ResultList<User>>(UserService.BASE_USERS_URL, { params }) | ||
50 | .pipe( | ||
51 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | ||
52 | map(res => this.restExtractor.applyToResultListData(res, this.formatUser.bind(this))), | ||
53 | catchError(err => this.restExtractor.handleError(err)) | ||
54 | ) | ||
55 | } | ||
56 | |||
57 | removeUser (user: User) { | ||
58 | return this.authHttp.delete(UserService.BASE_USERS_URL + user.id) | ||
59 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
60 | } | ||
61 | |||
62 | banUser (user: User, reason?: string) { | ||
63 | const body = reason ? { reason } : {} | ||
64 | |||
65 | return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/block', body) | ||
66 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
67 | } | ||
68 | |||
69 | unbanUser (user: User) { | ||
70 | return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/unblock', {}) | ||
71 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
72 | } | ||
73 | |||
74 | private formatUser (user: User) { | ||
75 | let videoQuota | ||
76 | if (user.videoQuota === -1) { | ||
77 | videoQuota = this.i18n('Unlimited') | ||
78 | } else { | ||
79 | videoQuota = this.bytesPipe.transform(user.videoQuota, 0) | ||
80 | } | ||
81 | |||
82 | const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0) | ||
83 | |||
84 | const roleLabels: { [ id in UserRole ]: string } = { | ||
85 | [UserRole.USER]: this.i18n('User'), | ||
86 | [UserRole.ADMINISTRATOR]: this.i18n('Administrator'), | ||
87 | [UserRole.MODERATOR]: this.i18n('Moderator') | ||
88 | } | ||
89 | |||
90 | return Object.assign(user, { | ||
91 | roleLabel: roleLabels[user.role], | ||
92 | videoQuota, | ||
93 | videoQuotaUsed | ||
94 | }) | ||
95 | } | ||
96 | } | ||
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 132e280b9..dd8e4efd5 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 { NotificationsService } from 'angular2-notifications' |
4 | import { UserService } from '../shared' | ||
5 | import { ServerService } from '../../../core' | 4 | import { ServerService } from '../../../core' |
6 | import { UserCreate, UserRole } from '../../../../../../shared' | 5 | import { UserCreate, UserRole } from '../../../../../../shared' |
7 | import { UserEdit } from './user-edit' | 6 | import { UserEdit } from './user-edit' |
@@ -9,6 +8,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 { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | 9 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' |
11 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 10 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
11 | import { UserService } from '@app/shared' | ||
12 | 12 | ||
13 | @Component({ | 13 | @Component({ |
14 | selector: 'my-user-create', | 14 | selector: 'my-user-create', |
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..99ce5804b 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/users/user-edit/user-edit.ts | |||
@@ -1,7 +1,6 @@ | |||
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 { |
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 9eb91ac95..cd3885a99 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 | |||
@@ -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 { Subscription } from 'rxjs' | 3 | import { Subscription } from 'rxjs' |
4 | import { NotificationsService } from 'angular2-notifications' | 4 | import { NotificationsService } from 'angular2-notifications' |
5 | import { UserService } from '../shared' | ||
6 | import { ServerService } from '../../../core' | 5 | import { ServerService } from '../../../core' |
7 | import { UserEdit } from './user-edit' | 6 | import { UserEdit } from './user-edit' |
8 | import { User, UserUpdate } from '../../../../../../shared' | 7 | import { User, UserUpdate } from '../../../../../../shared' |
@@ -10,6 +9,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill' | |||
10 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 9 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
11 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | 10 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' |
12 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 11 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
12 | import { UserService } from '@app/shared' | ||
13 | 13 | ||
14 | @Component({ | 14 | @Component({ |
15 | selector: 'my-user-update', | 15 | selector: 'my-user-update', |
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 bb1b26442..eb8d30e17 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 | |||
@@ -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,30 @@ | |||
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 | <td>{{ user.email }}</td> |
39 | <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> | 69 | <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> |
40 | <td>{{ user.roleLabel }}</td> | 70 | <td>{{ user.roleLabel }}</td> |
41 | <td>{{ user.createdAt }}</td> | 71 | <td>{{ user.createdAt }}</td> |
42 | <td class="action-cell"> | 72 | <td class="action-cell"> |
43 | <my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user"></my-action-dropdown> | 73 | <my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> |
74 | </my-user-moderation-dropdown> | ||
44 | </td> | 75 | </td> |
45 | </tr> | 76 | </tr> |
46 | </ng-template> | 77 | </ng-template> |
@@ -55,4 +86,4 @@ | |||
55 | </ng-template> | 86 | </ng-template> |
56 | </p-table> | 87 | </p-table> |
57 | 88 | ||
58 | <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal> \ No newline at end of file | 89 | <my-user-ban-modal #userBanModal (userBanned)="onUsersBanned()"></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..f235769f0 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 | |||
@@ -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 | } | ||
19 | |||
20 | .caption { | ||
21 | justify-content: space-between; | ||
22 | |||
23 | input { | ||
24 | @include peertube-input-text(250px); | ||
25 | } | ||
18 | } \ No newline at end of file | 26 | } \ No newline at end of file |
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 100ffc00e..3859af9ff 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 | |||
@@ -2,13 +2,11 @@ import { Component, OnInit, ViewChild } from '@angular/core' | |||
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { NotificationsService } from 'angular2-notifications' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/components/common/sortmeta' |
4 | import { ConfirmService } from '../../../core' | 4 | import { ConfirmService } from '../../../core' |
5 | import { RestPagination, RestTable } from '../../../shared' | 5 | import { RestPagination, RestTable, UserService } from '../../../shared' |
6 | import { UserService } from '../shared' | ||
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
8 | import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' | ||
9 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | ||
10 | import { UserBanModalComponent } from '@app/+admin/users/user-list/user-ban-modal.component' | ||
11 | 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' | ||
12 | 10 | ||
13 | @Component({ | 11 | @Component({ |
14 | selector: 'my-user-list', | 12 | selector: 'my-user-list', |
@@ -23,9 +21,9 @@ export class UserListComponent extends RestTable implements OnInit { | |||
23 | rowsPerPage = 10 | 21 | rowsPerPage = 10 |
24 | sort: SortMeta = { field: 'createdAt', order: 1 } | 22 | sort: SortMeta = { field: 'createdAt', order: 1 } |
25 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 23 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
26 | userActions: DropdownAction<User>[] = [] | ||
27 | 24 | ||
28 | private openedModal: NgbModalRef | 25 | selectedUsers: User[] = [] |
26 | bulkUserActions: DropdownAction<User[]>[] = [] | ||
29 | 27 | ||
30 | constructor ( | 28 | constructor ( |
31 | private notificationsService: NotificationsService, | 29 | private notificationsService: NotificationsService, |
@@ -34,84 +32,80 @@ export class UserListComponent extends RestTable implements OnInit { | |||
34 | private i18n: I18n | 32 | private i18n: I18n |
35 | ) { | 33 | ) { |
36 | super() | 34 | super() |
35 | } | ||
37 | 36 | ||
38 | this.userActions = [ | 37 | ngOnInit () { |
39 | { | 38 | this.initialize() |
40 | label: this.i18n('Edit'), | 39 | |
41 | linkBuilder: this.getRouterUserEditLink | 40 | this.bulkUserActions = [ |
42 | }, | ||
43 | { | 41 | { |
44 | label: this.i18n('Delete'), | 42 | label: this.i18n('Delete'), |
45 | handler: user => this.removeUser(user) | 43 | handler: users => this.removeUsers(users) |
46 | }, | 44 | }, |
47 | { | 45 | { |
48 | label: this.i18n('Ban'), | 46 | label: this.i18n('Ban'), |
49 | handler: user => this.openBanUserModal(user), | 47 | handler: users => this.openBanUserModal(users), |
50 | isDisplayed: user => !user.blocked | 48 | isDisplayed: users => users.every(u => u.blocked === false) |
51 | }, | 49 | }, |
52 | { | 50 | { |
53 | label: this.i18n('Unban'), | 51 | label: this.i18n('Unban'), |
54 | handler: user => this.unbanUser(user), | 52 | handler: users => this.unbanUsers(users), |
55 | isDisplayed: user => user.blocked | 53 | isDisplayed: users => users.every(u => u.blocked === true) |
56 | } | 54 | } |
57 | ] | 55 | ] |
58 | } | 56 | } |
59 | 57 | ||
60 | ngOnInit () { | 58 | openBanUserModal (users: User[]) { |
61 | this.loadSort() | 59 | for (const user of users) { |
62 | } | 60 | if (user.username === 'root') { |
63 | 61 | this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.')) | |
64 | hideBanUserModal () { | 62 | return |
65 | this.openedModal.close() | 63 | } |
66 | } | ||
67 | |||
68 | openBanUserModal (user: User) { | ||
69 | if (user.username === 'root') { | ||
70 | this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.')) | ||
71 | return | ||
72 | } | 64 | } |
73 | 65 | ||
74 | this.userBanModal.openModal(user) | 66 | this.userBanModal.openModal(users) |
75 | } | 67 | } |
76 | 68 | ||
77 | onUserBanned () { | 69 | onUsersBanned () { |
78 | this.loadData() | 70 | this.loadData() |
79 | } | 71 | } |
80 | 72 | ||
81 | async unbanUser (user: User) { | 73 | async unbanUsers (users: User[]) { |
82 | const message = this.i18n('Do you really want to unban {{username}}?', { username: user.username }) | 74 | const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length }) |
75 | |||
83 | const res = await this.confirmService.confirm(message, this.i18n('Unban')) | 76 | const res = await this.confirmService.confirm(message, this.i18n('Unban')) |
84 | if (res === false) return | 77 | if (res === false) return |
85 | 78 | ||
86 | this.userService.unbanUser(user) | 79 | this.userService.unbanUsers(users) |
87 | .subscribe( | 80 | .subscribe( |
88 | () => { | 81 | () => { |
89 | this.notificationsService.success( | 82 | const message = this.i18n('{{num}} users unbanned.', { num: users.length }) |
90 | this.i18n('Success'), | 83 | |
91 | this.i18n('User {{username}} unbanned.', { username: user.username }) | 84 | this.notificationsService.success(this.i18n('Success'), message) |
92 | ) | 85 | this.loadData() |
93 | this.loadData() | 86 | }, |
94 | }, | 87 | |
95 | 88 | err => this.notificationsService.error(this.i18n('Error'), err.message) | |
96 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 89 | ) |
97 | ) | ||
98 | } | 90 | } |
99 | 91 | ||
100 | async removeUser (user: User) { | 92 | async removeUsers (users: User[]) { |
101 | if (user.username === 'root') { | 93 | for (const user of users) { |
102 | this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.')) | 94 | if (user.username === 'root') { |
103 | return | 95 | this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.')) |
96 | return | ||
97 | } | ||
104 | } | 98 | } |
105 | 99 | ||
106 | const message = this.i18n('If you remove this user, you will not be able to create another with the same username!') | 100 | const message = this.i18n('If you remove these users, you will not be able to create others with the same username!') |
107 | const res = await this.confirmService.confirm(message, this.i18n('Delete')) | 101 | const res = await this.confirmService.confirm(message, this.i18n('Delete')) |
108 | if (res === false) return | 102 | if (res === false) return |
109 | 103 | ||
110 | this.userService.removeUser(user).subscribe( | 104 | this.userService.removeUser(users).subscribe( |
111 | () => { | 105 | () => { |
112 | this.notificationsService.success( | 106 | this.notificationsService.success( |
113 | this.i18n('Success'), | 107 | this.i18n('Success'), |
114 | this.i18n('User {{username}} deleted.', { username: user.username }) | 108 | this.i18n('{{num}} users deleted.', { num: users.length }) |
115 | ) | 109 | ) |
116 | this.loadData() | 110 | this.loadData() |
117 | }, | 111 | }, |
@@ -120,12 +114,14 @@ export class UserListComponent extends RestTable implements OnInit { | |||
120 | ) | 114 | ) |
121 | } | 115 | } |
122 | 116 | ||
123 | getRouterUserEditLink (user: User) { | 117 | isInSelectionMode () { |
124 | return [ '/admin', 'users', 'update', user.id ] | 118 | return this.selectedUsers.length !== 0 |
125 | } | 119 | } |
126 | 120 | ||
127 | protected loadData () { | 121 | protected loadData () { |
128 | this.userService.getUsers(this.pagination, this.sort) | 122 | this.selectedUsers = [] |
123 | |||
124 | this.userService.getUsers(this.pagination, this.sort, this.search) | ||
129 | .subscribe( | 125 | .subscribe( |
130 | resultList => { | 126 | resultList => { |
131 | this.users = resultList.data | 127 | this.users = resultList.data |
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..fbad28410 --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts | |||
@@ -0,0 +1,59 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { NotificationsService } from 'angular2-notifications' | ||
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, AccountBlock } 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 notificationsService: NotificationsService, | ||
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.notificationsService.success( | ||
39 | this.i18n('Success'), | ||
40 | this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost }) | ||
41 | ) | ||
42 | |||
43 | this.loadData() | ||
44 | } | ||
45 | ) | ||
46 | } | ||
47 | |||
48 | protected loadData () { | ||
49 | return this.blocklistService.getUserAccountBlocklist(this.pagination, this.sort) | ||
50 | .subscribe( | ||
51 | resultList => { | ||
52 | this.blockedAccounts = resultList.data | ||
53 | this.totalRecords = resultList.total | ||
54 | }, | ||
55 | |||
56 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
57 | ) | ||
58 | } | ||
59 | } | ||
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..b411d6926 --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts | |||
@@ -0,0 +1,60 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { NotificationsService } from 'angular2-notifications' | ||
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 notificationsService: NotificationsService, | ||
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.notificationsService.success( | ||
40 | this.i18n('Success'), | ||
41 | this.i18n('Instance {{host}} unmuted.', { host }) | ||
42 | ) | ||
43 | |||
44 | this.loadData() | ||
45 | } | ||
46 | ) | ||
47 | } | ||
48 | |||
49 | protected loadData () { | ||
50 | return this.blocklistService.getUserServerBlocklist(this.pagination, this.sort) | ||
51 | .subscribe( | ||
52 | resultList => { | ||
53 | this.blockedServers = resultList.data | ||
54 | this.totalRecords = resultList.total | ||
55 | }, | ||
56 | |||
57 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
58 | ) | ||
59 | } | ||
60 | } | ||
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..0b51ac13c 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 | |||
@@ -31,19 +31,7 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit { | |||
31 | } | 31 | } |
32 | 32 | ||
33 | ngOnInit () { | 33 | ngOnInit () { |
34 | this.loadSort() | 34 | 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 | } | 35 | } |
48 | 36 | ||
49 | createByString (account: Account) { | 37 | createByString (account: Account) { |
@@ -65,4 +53,16 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit { | |||
65 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 53 | err => this.notificationsService.error(this.i18n('Error'), err.message) |
66 | ) | 54 | ) |
67 | } | 55 | } |
56 | |||
57 | protected loadData () { | ||
58 | return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort) | ||
59 | .subscribe( | ||
60 | resultList => { | ||
61 | this.videoChangeOwnerships = resultList.data | ||
62 | this.totalRecords = resultList.total | ||
63 | }, | ||
64 | |||
65 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
66 | ) | ||
67 | } | ||
68 | } | 68 | } |
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..601e517b4 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,8 @@ 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' | ||
14 | 16 | ||
15 | const myAccountRoutes: Routes = [ | 17 | const myAccountRoutes: Routes = [ |
16 | { | 18 | { |
@@ -94,6 +96,24 @@ const myAccountRoutes: Routes = [ | |||
94 | title: 'Ownership changes' | 96 | title: 'Ownership changes' |
95 | } | 97 | } |
96 | } | 98 | } |
99 | }, | ||
100 | { | ||
101 | path: 'blocklist/accounts', | ||
102 | component: MyAccountBlocklistComponent, | ||
103 | data: { | ||
104 | meta: { | ||
105 | title: 'Muted accounts' | ||
106 | } | ||
107 | } | ||
108 | }, | ||
109 | { | ||
110 | path: 'blocklist/servers', | ||
111 | component: MyAccountServerBlocklistComponent, | ||
112 | data: { | ||
113 | meta: { | ||
114 | title: 'Muted instances' | ||
115 | } | ||
116 | } | ||
97 | } | 117 | } |
98 | ] | 118 | ] |
99 | } | 119 | } |
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..8be8a66cc 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 | |||
@@ -16,6 +16,11 @@ | |||
16 | </div> | 16 | </div> |
17 | 17 | ||
18 | <my-peertube-checkbox | 18 | <my-peertube-checkbox |
19 | inputName="webTorrentEnabled" formControlName="webTorrentEnabled" | ||
20 | i18n-labelText labelText="Use WebTorrent to exchange parts of the video with others" | ||
21 | ></my-peertube-checkbox> | ||
22 | |||
23 | <my-peertube-checkbox | ||
19 | inputName="autoPlayVideo" formControlName="autoPlayVideo" | 24 | inputName="autoPlayVideo" formControlName="autoPlayVideo" |
20 | i18n-labelText labelText="Automatically plays video" | 25 | i18n-labelText labelText="Automatically plays video" |
21 | ></my-peertube-checkbox> | 26 | ></my-peertube-checkbox> |
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..6c9a7ce75 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 | |||
@@ -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,9 +44,11 @@ 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 | ||
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..5d43956f2 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,4 +1,4 @@ | |||
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 { NotificationsService } from 'angular2-notifications' |
4 | import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' | 4 | import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' |
@@ -17,11 +17,9 @@ import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators | |||
17 | styleUrls: [ './my-account-video-channel-edit.component.scss' ] | 17 | styleUrls: [ './my-account-video-channel-edit.component.scss' ] |
18 | }) | 18 | }) |
19 | export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy { | 19 | export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy { |
20 | @ViewChild('avatarfileInput') avatarfileInput | ||
21 | |||
22 | error: string | 20 | error: string |
23 | |||
24 | videoChannelToUpdate: VideoChannel | 21 | videoChannelToUpdate: VideoChannel |
22 | |||
25 | private paramsSub: Subscription | 23 | private paramsSub: Subscription |
26 | 24 | ||
27 | constructor ( | 25 | constructor ( |
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..5b920c98d 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 | |||
@@ -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) { |
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..2d88ac760 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 | |||
@@ -169,7 +169,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni | |||
169 | 169 | ||
170 | private spliceVideosById (id: number) { | 170 | private spliceVideosById (id: number) { |
171 | for (const key of Object.keys(this.loadedPages)) { | 171 | for (const key of Object.keys(this.loadedPages)) { |
172 | const videos = this.loadedPages[ key ] | 172 | const videos: Video[] = this.loadedPages[ key ] |
173 | const index = videos.findIndex(v => v.id === id) | 173 | const index = videos.findIndex(v => v.id === id) |
174 | 174 | ||
175 | if (index !== -1) { | 175 | 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..7c0df850d 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 | |||
@@ -22,9 +22,9 @@ | |||
22 | </span> | 22 | </span> |
23 | 23 | ||
24 | <input | 24 | <input |
25 | type="submit" i18n-value value="Submit" class="action-button-submit" | 25 | type="submit" i18n-value value="Submit" class="action-button-submit" |
26 | [disabled]="!form.valid" | 26 | [disabled]="!form.valid" |
27 | (click)="close()" | 27 | (click)="close()" |
28 | /> | 28 | /> |
29 | </div> | 29 | </div> |
30 | </div> | 30 | </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..9f94f3c13 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 | |||
@@ -49,7 +49,7 @@ 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( |
diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html index b602fd69f..41333c25a 100644 --- a/client/src/app/+my-account/my-account.component.html +++ b/client/src/app/+my-account/my-account.component.html | |||
@@ -19,7 +19,21 @@ | |||
19 | </div> | 19 | </div> |
20 | </div> | 20 | </div> |
21 | 21 | ||
22 | <a i18n routerLink="/my-account/ownership" routerLinkActive="active" class="title-page">Ownership changes</a> | 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 | |||
23 | </div> | 37 | </div> |
24 | 38 | ||
25 | <div class="margin-content"> | 39 | <div class="margin-content"> |
diff --git a/client/src/app/+my-account/my-account.component.scss b/client/src/app/+my-account/my-account.component.scss index 20b2639b5..6243c6dcf 100644 --- a/client/src/app/+my-account/my-account.component.scss +++ b/client/src/app/+my-account/my-account.component.scss | |||
@@ -1,4 +1,4 @@ | |||
1 | .my-library { | 1 | .my-library, .misc { |
2 | span[role=button] { | 2 | span[role=button] { |
3 | cursor: pointer; | 3 | cursor: pointer; |
4 | } | 4 | } |
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index bad60a8fb..d728caf07 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts | |||
@@ -13,6 +13,7 @@ import { Subscription } from 'rxjs' | |||
13 | export class MyAccountComponent implements OnInit, OnDestroy { | 13 | export class MyAccountComponent implements OnInit, OnDestroy { |
14 | 14 | ||
15 | libraryLabel = '' | 15 | libraryLabel = '' |
16 | miscLabel = '' | ||
16 | 17 | ||
17 | private routeSub: Subscription | 18 | private routeSub: Subscription |
18 | 19 | ||
@@ -23,11 +24,11 @@ export class MyAccountComponent implements OnInit, OnDestroy { | |||
23 | ) {} | 24 | ) {} |
24 | 25 | ||
25 | ngOnInit () { | 26 | ngOnInit () { |
26 | this.updateLibraryLabel(this.router.url) | 27 | this.updateLabels(this.router.url) |
27 | 28 | ||
28 | this.routeSub = this.router.events | 29 | this.routeSub = this.router.events |
29 | .pipe(filter(event => event instanceof NavigationStart)) | 30 | .pipe(filter(event => event instanceof NavigationStart)) |
30 | .subscribe((event: NavigationStart) => this.updateLibraryLabel(event.url)) | 31 | .subscribe((event: NavigationStart) => this.updateLabels(event.url)) |
31 | } | 32 | } |
32 | 33 | ||
33 | ngOnDestroy () { | 34 | ngOnDestroy () { |
@@ -40,7 +41,7 @@ export class MyAccountComponent implements OnInit, OnDestroy { | |||
40 | return importConfig.http.enabled || importConfig.torrent.enabled | 41 | return importConfig.http.enabled || importConfig.torrent.enabled |
41 | } | 42 | } |
42 | 43 | ||
43 | private updateLibraryLabel (url: string) { | 44 | private updateLabels (url: string) { |
44 | const [ path ] = url.split('?') | 45 | const [ path ] = url.split('?') |
45 | 46 | ||
46 | if (path.startsWith('/my-account/video-channels')) { | 47 | if (path.startsWith('/my-account/video-channels')) { |
@@ -54,5 +55,13 @@ export class MyAccountComponent implements OnInit, OnDestroy { | |||
54 | } else { | 55 | } else { |
55 | this.libraryLabel = '' | 56 | this.libraryLabel = '' |
56 | } | 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 | } | ||
57 | } | 66 | } |
58 | } | 67 | } |
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index ad21162a8..017ebd57d 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts | |||
@@ -19,6 +19,8 @@ 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' | 19 | 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' | 20 | 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' | 21 | import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' |
22 | import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component' | ||
23 | import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component' | ||
22 | 24 | ||
23 | @NgModule({ | 25 | @NgModule({ |
24 | imports: [ | 26 | imports: [ |
@@ -45,7 +47,9 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub | |||
45 | ActorAvatarInfoComponent, | 47 | ActorAvatarInfoComponent, |
46 | MyAccountVideoImportsComponent, | 48 | MyAccountVideoImportsComponent, |
47 | MyAccountDangerZoneComponent, | 49 | MyAccountDangerZoneComponent, |
48 | MyAccountSubscriptionsComponent | 50 | MyAccountSubscriptionsComponent, |
51 | MyAccountBlocklistComponent, | ||
52 | MyAccountServerBlocklistComponent | ||
49 | ], | 53 | ], |
50 | 54 | ||
51 | exports: [ | 55 | 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..54bacc212 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,4 +1,4 @@ | |||
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' | 3 | import { NotificationsService } from 'angular2-notifications' |
4 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 4 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
@@ -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 | ||
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 7cd0fff1b..dc4d0bf6a 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts | |||
@@ -4,9 +4,10 @@ import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router' | |||
4 | import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' | 4 | import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' |
5 | import { is18nPath } from '../../../shared/models/i18n' | 5 | import { is18nPath } from '../../../shared/models/i18n' |
6 | import { ScreenService } from '@app/shared/misc/screen.service' | 6 | import { ScreenService } from '@app/shared/misc/screen.service' |
7 | import { skip } from 'rxjs/operators' | 7 | import { skip, debounceTime } from 'rxjs/operators' |
8 | import { HotkeysService, Hotkey } from 'angular2-hotkeys' | 8 | import { HotkeysService, Hotkey } from 'angular2-hotkeys' |
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | 9 | import { I18n } from '@ngx-translate/i18n-polyfill' |
10 | import { fromEvent } from 'rxjs' | ||
10 | 11 | ||
11 | @Component({ | 12 | @Component({ |
12 | selector: 'my-app', | 13 | selector: 'my-app', |
@@ -28,6 +29,7 @@ export class AppComponent implements OnInit { | |||
28 | } | 29 | } |
29 | 30 | ||
30 | isMenuDisplayed = true | 31 | isMenuDisplayed = true |
32 | isMenuChangedByUser = false | ||
31 | 33 | ||
32 | customCSS: SafeHtml | 34 | customCSS: SafeHtml |
33 | 35 | ||
@@ -165,6 +167,10 @@ export class AppComponent implements OnInit { | |||
165 | return false | 167 | return false |
166 | }, undefined, this.i18n('Toggle Dark theme')) | 168 | }, undefined, this.i18n('Toggle Dark theme')) |
167 | ]) | 169 | ]) |
170 | |||
171 | fromEvent(window, 'resize') | ||
172 | .pipe(debounceTime(200)) | ||
173 | .subscribe(() => this.onResize()) | ||
168 | } | 174 | } |
169 | 175 | ||
170 | isUserLoggedIn () { | 176 | isUserLoggedIn () { |
@@ -173,5 +179,10 @@ export class AppComponent implements OnInit { | |||
173 | 179 | ||
174 | toggleMenu () { | 180 | toggleMenu () { |
175 | this.isMenuDisplayed = !this.isMenuDisplayed | 181 | this.isMenuDisplayed = !this.isMenuDisplayed |
182 | this.isMenuChangedByUser = true | ||
183 | } | ||
184 | |||
185 | onResize () { | ||
186 | this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser | ||
176 | } | 187 | } |
177 | } | 188 | } |
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 34e890b40..371199442 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts | |||
@@ -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..acd13d9c5 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts | |||
@@ -72,6 +72,7 @@ export class AuthUser extends User { | |||
72 | EMAIL: 'email', | 72 | EMAIL: 'email', |
73 | USERNAME: 'username', | 73 | USERNAME: 'username', |
74 | NSFW_POLICY: 'nsfw_policy', | 74 | NSFW_POLICY: 'nsfw_policy', |
75 | WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled', | ||
75 | AUTO_PLAY_VIDEO: 'auto_play_video' | 76 | AUTO_PLAY_VIDEO: 'auto_play_video' |
76 | } | 77 | } |
77 | 78 | ||
@@ -87,6 +88,7 @@ export class AuthUser extends User { | |||
87 | email: peertubeLocalStorage.getItem(this.KEYS.EMAIL), | 88 | email: peertubeLocalStorage.getItem(this.KEYS.EMAIL), |
88 | role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, | 89 | role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, |
89 | nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType, | 90 | nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType, |
91 | webTorrentEnabled: peertubeLocalStorage.getItem(this.KEYS.WEBTORRENT_ENABLED) === 'true', | ||
90 | autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true' | 92 | autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true' |
91 | }, | 93 | }, |
92 | Tokens.load() | 94 | Tokens.load() |
@@ -101,6 +103,7 @@ export class AuthUser extends User { | |||
101 | peertubeLocalStorage.removeItem(this.KEYS.ID) | 103 | peertubeLocalStorage.removeItem(this.KEYS.ID) |
102 | peertubeLocalStorage.removeItem(this.KEYS.ROLE) | 104 | peertubeLocalStorage.removeItem(this.KEYS.ROLE) |
103 | peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY) | 105 | peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY) |
106 | peertubeLocalStorage.removeItem(this.KEYS.WEBTORRENT_ENABLED) | ||
104 | peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) | 107 | peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) |
105 | peertubeLocalStorage.removeItem(this.KEYS.EMAIL) | 108 | peertubeLocalStorage.removeItem(this.KEYS.EMAIL) |
106 | Tokens.flush() | 109 | Tokens.flush() |
@@ -138,6 +141,7 @@ export class AuthUser extends User { | |||
138 | peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email) | 141 | peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email) |
139 | peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) | 142 | peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) |
140 | peertubeLocalStorage.setItem(AuthUser.KEYS.NSFW_POLICY, this.nsfwPolicy.toString()) | 143 | peertubeLocalStorage.setItem(AuthUser.KEYS.NSFW_POLICY, this.nsfwPolicy.toString()) |
144 | peertubeLocalStorage.setItem(AuthUser.KEYS.WEBTORRENT_ENABLED, JSON.stringify(this.webTorrentEnabled)) | ||
141 | peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo)) | 145 | peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo)) |
142 | this.tokens.save() | 146 | this.tokens.save() |
143 | } | 147 | } |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 9c36b946e..443772c9e 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -221,7 +221,7 @@ export class AuthService { | |||
221 | } | 221 | } |
222 | 222 | ||
223 | refreshUserInformation () { | 223 | refreshUserInformation () { |
224 | const obj = { | 224 | const obj: UserLoginWithUsername = { |
225 | access_token: this.user.getAccessToken(), | 225 | access_token: this.user.getAccessToken(), |
226 | refresh_token: null, | 226 | refresh_token: null, |
227 | token_type: this.user.getTokenType(), | 227 | token_type: this.user.getTokenType(), |
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 2f1ef1fc2..da8bd26db 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -154,7 +154,7 @@ export class ServerService { | |||
154 | this.localeObservable | 154 | this.localeObservable |
155 | .pipe( | 155 | .pipe( |
156 | switchMap(translations => { | 156 | switchMap(translations => { |
157 | return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) | 157 | return this.http.get<{ [id: string]: string }>(ServerService.BASE_VIDEO_URL + attributeName) |
158 | .pipe(map(data => ({ data, translations }))) | 158 | .pipe(map(data => ({ data, translations }))) |
159 | }) | 159 | }) |
160 | ) | 160 | ) |
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..c23e0c55d 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html | |||
@@ -1,6 +1,6 @@ | |||
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 | ||
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/search/search.component.ts b/client/src/app/search/search.component.ts index 911d56843..ecffcafc1 100644 --- a/client/src/app/search/search.component.ts +++ b/client/src/app/search/search.component.ts | |||
@@ -1,6 +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 } from '@app/core' |
4 | import { NotificationsService } from 'angular2-notifications' | 4 | import { NotificationsService } from 'angular2-notifications' |
5 | import { forkJoin, Subscription } from 'rxjs' | 5 | import { forkJoin, Subscription } from 'rxjs' |
6 | import { SearchService } from '@app/search/search.service' | 6 | import { SearchService } from '@app/search/search.service' |
@@ -40,7 +40,6 @@ export class SearchComponent implements OnInit, OnDestroy { | |||
40 | private route: ActivatedRoute, | 40 | private route: ActivatedRoute, |
41 | private router: Router, | 41 | private router: Router, |
42 | private metaService: MetaService, | 42 | private metaService: MetaService, |
43 | private redirectService: RedirectService, | ||
44 | private notificationsService: NotificationsService, | 43 | private notificationsService: NotificationsService, |
45 | private searchService: SearchService, | 44 | private searchService: SearchService, |
46 | private authService: AuthService | 45 | private authService: AuthService |
diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts index 5058e372f..c5cd2051c 100644 --- a/client/src/app/shared/account/account.model.ts +++ b/client/src/app/shared/account/account.model.ts | |||
@@ -5,12 +5,24 @@ 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 | ||
12 | |||
13 | userId?: number | ||
8 | 14 | ||
9 | constructor (hash: ServerAccount) { | 15 | constructor (hash: ServerAccount) { |
10 | super(hash) | 16 | super(hash) |
11 | 17 | ||
12 | this.displayName = hash.displayName | 18 | this.displayName = hash.displayName |
13 | this.description = hash.description | 19 | this.description = hash.description |
20 | this.userId = hash.userId | ||
14 | 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 | ||
15 | } | 27 | } |
16 | } | 28 | } |
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 8b7241379..48230d6d8 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.html +++ b/client/src/app/shared/buttons/action-dropdown.component.html | |||
@@ -1,17 +1,21 @@ | |||
1 | <div class="dropdown-root" ngbDropdown [placement]="placement"> | 1 | <div class="dropdown-root" ngbDropdown [placement]="placement"> |
2 | <div class="action-button" 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 | <span *ngIf="!label" class="icon icon-action"></span> | ||
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 action of actions"> |
8 | <div class="dropdown-item" *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> | 12 | <ng-container *ngIf="action.isDisplayed === undefined || action.isDisplayed(entry) === true"> |
9 | <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a> | 13 | <a *ngIf="action.linkBuilder" class="dropdown-item" [routerLink]="action.linkBuilder(entry)">{{ action.label }}</a> |
10 | 14 | ||
11 | <span *ngIf="!action.linkBuilder" class="custom-action" class="dropdown-item" (click)="action.handler(entry)" role="button"> | 15 | <span *ngIf="!action.linkBuilder" class="custom-action dropdown-item" (click)="action.handler(entry)" role="button"> |
12 | {{ action.label }} | 16 | {{ action.label }} |
13 | </span> | 17 | </span> |
14 | </div> | 18 | </ng-container> |
15 | </ng-container> | 19 | </ng-container> |
16 | </div> | 20 | </div> |
17 | </div> \ No newline at end of file | 21 | </div> \ No newline at end of file |
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss index 615511093..92c4d1d2c 100644 --- a/client/src/app/shared/buttons/action-dropdown.component.scss +++ b/client/src/app/shared/buttons/action-dropdown.component.scss | |||
@@ -3,7 +3,14 @@ | |||
3 | 3 | ||
4 | .action-button { | 4 | .action-button { |
5 | @include peertube-button; | 5 | @include peertube-button; |
6 | @include grey-button; | 6 | |
7 | &.grey { | ||
8 | @include grey-button; | ||
9 | } | ||
10 | |||
11 | &.orange { | ||
12 | @include orange-button; | ||
13 | } | ||
7 | 14 | ||
8 | display: inline-block; | 15 | display: inline-block; |
9 | padding: 0 10px; | 16 | padding: 0 10px; |
@@ -22,11 +29,27 @@ | |||
22 | background-image: url('../../../assets/images/video/more.svg'); | 29 | background-image: url('../../../assets/images/video/more.svg'); |
23 | top: -1px; | 30 | top: -1px; |
24 | } | 31 | } |
32 | |||
33 | &.small { | ||
34 | font-size: 14px; | ||
35 | height: 20px; | ||
36 | line-height: 20px; | ||
37 | } | ||
38 | } | ||
39 | |||
40 | .dropdown-toggle::after { | ||
41 | position: relative; | ||
42 | top: 1px; | ||
25 | } | 43 | } |
26 | 44 | ||
27 | .dropdown-menu { | 45 | .dropdown-menu { |
28 | .dropdown-item { | 46 | .dropdown-item { |
29 | cursor: pointer; | 47 | cursor: pointer; |
30 | color: #000 !important; | 48 | color: #000 !important; |
49 | |||
50 | a, span { | ||
51 | display: block; | ||
52 | width: 100%; | ||
53 | } | ||
31 | } | 54 | } |
32 | } \ No newline at end of file | 55 | } \ No newline at end of file |
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts index 17f9cc618..d8026ef41 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({ |
@@ -16,5 +16,8 @@ export type DropdownAction<T> = { | |||
16 | export class ActionDropdownComponent<T> { | 16 | export class ActionDropdownComponent<T> { |
17 | @Input() actions: DropdownAction<T>[] = [] | 17 | @Input() actions: 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' | ||
21 | @Input() label: string | ||
22 | @Input() theme: 'orange' | 'grey' = 'grey' | ||
20 | } | 23 | } |
diff --git a/client/src/app/shared/buttons/button.component.ts b/client/src/app/shared/buttons/button.component.ts index 967cb1409..1a1162f09 100644 --- a/client/src/app/shared/buttons/button.component.ts +++ b/client/src/app/shared/buttons/button.component.ts | |||
@@ -8,9 +8,9 @@ import { Component, Input } from '@angular/core' | |||
8 | 8 | ||
9 | export class ButtonComponent { | 9 | export class ButtonComponent { |
10 | @Input() label = '' | 10 | @Input() label = '' |
11 | @Input() className = undefined | 11 | @Input() className: string = undefined |
12 | @Input() icon = undefined | 12 | @Input() icon: string = undefined |
13 | @Input() title = undefined | 13 | @Input() title: string = undefined |
14 | 14 | ||
15 | getTitle () { | 15 | getTitle () { |
16 | return this.title || this.label | 16 | return this.title || this.label |
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/shared/forms/form-validators/user-validators.service.ts b/client/src/app/shared/forms/form-validators/user-validators.service.ts index 1fd1cdf68..d14fa4777 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 | |||
@@ -101,11 +101,11 @@ export class UserValidatorsService { | |||
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-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..f62ff65f7 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 | |||
@@ -42,22 +42,22 @@ export class VideoChannelValidatorsService { | |||
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/peertube-checkbox.component.html b/client/src/app/shared/forms/peertube-checkbox.component.html index 38691f050..fb3006b53 100644 --- a/client/src/app/shared/forms/peertube-checkbox.component.html +++ b/client/src/app/shared/forms/peertube-checkbox.component.html | |||
@@ -1,4 +1,4 @@ | |||
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]="isDisabled" /> |
4 | <span role="checkbox" [attr.aria-checked]="checked"></span> | 4 | <span role="checkbox" [attr.aria-checked]="checked"></span> |
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/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/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..78be2e5dd 100644 --- a/client/src/app/shared/misc/utils.ts +++ b/client/src/app/shared/misc/utils.ts | |||
@@ -102,7 +102,7 @@ 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 lineFeedToHtml (obj: any, keyToNormalize: string) { |
106 | return immutableAssign(obj, { | 106 | return immutableAssign(obj, { |
107 | [keyToNormalize]: obj[keyToNormalize].replace(/\r?\n|\r/g, '<br />') | 107 | [keyToNormalize]: obj[keyToNormalize].replace(/\r?\n|\r/g, '<br />') |
108 | }) | 108 | }) |
diff --git a/client/src/app/shared/moderation/index.ts b/client/src/app/shared/moderation/index.ts new file mode 100644 index 000000000..9a77c64c0 --- /dev/null +++ b/client/src/app/shared/moderation/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './user-ban-modal.component' | ||
2 | export * from './user-moderation-dropdown.component' | ||
diff --git a/client/src/app/+admin/users/user-list/user-ban-modal.component.html b/client/src/app/shared/moderation/user-ban-modal.component.html index b2958caa4..fa5cb7404 100644 --- a/client/src/app/+admin/users/user-list/user-ban-modal.component.html +++ b/client/src/app/shared/moderation/user-ban-modal.component.html | |||
@@ -1,6 +1,6 @@ | |||
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 | <span class="close" aria-hidden="true" (click)="hideBanUserModal()"></span> |
5 | </div> | 5 | </div> |
6 | 6 | ||
diff --git a/client/src/app/+admin/users/user-list/user-ban-modal.component.scss b/client/src/app/shared/moderation/user-ban-modal.component.scss index 84562f15c..84562f15c 100644 --- a/client/src/app/+admin/users/user-list/user-ban-modal.component.scss +++ b/client/src/app/shared/moderation/user-ban-modal.component.scss | |||
diff --git a/client/src/app/+admin/users/user-list/user-ban-modal.component.ts b/client/src/app/shared/moderation/user-ban-modal.component.ts index 4fd4d561c..60bd442dd 100644 --- a/client/src/app/+admin/users/user-list/user-ban-modal.component.ts +++ b/client/src/app/shared/moderation/user-ban-modal.component.ts | |||
@@ -1,12 +1,12 @@ | |||
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 { NotificationsService } from 'angular2-notifications' |
3 | import { FormReactive, UserValidatorsService } from '../../../shared' | ||
4 | import { UserService } from '../shared' | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill' | 3 | import { I18n } from '@ngx-translate/i18n-polyfill' |
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
8 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 6 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
9 | import { User } from '../../../../../../shared' | 7 | import { FormReactive, UserValidatorsService } from '@app/shared/forms' |
8 | import { UserService } from '@app/shared/users' | ||
9 | import { User } from '../../../../../shared' | ||
10 | 10 | ||
11 | @Component({ | 11 | @Component({ |
12 | selector: 'my-user-ban-modal', | 12 | selector: 'my-user-ban-modal', |
@@ -15,9 +15,9 @@ 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 ( |
@@ -37,28 +37,29 @@ 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 | hideBanUserModal () { |
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.notificationsService.success(this.i18n('Success'), message) |
61 | |||
62 | this.userBanned.emit(this.usersToBan) | ||
62 | this.hideBanUserModal() | 63 | this.hideBanUserModal() |
63 | }, | 64 | }, |
64 | 65 | ||
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.html b/client/src/app/shared/moderation/user-moderation-dropdown.component.html new file mode 100644 index 000000000..7367a7e59 --- /dev/null +++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.html | |||
@@ -0,0 +1,8 @@ | |||
1 | <ng-container *ngIf="userActions.length !== 0"> | ||
2 | <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal> | ||
3 | |||
4 | <my-action-dropdown | ||
5 | [actions]="userActions" [entry]="{ user: user, account: account }" | ||
6 | [buttonSize]="buttonSize" [placement]="placement" | ||
7 | ></my-action-dropdown> | ||
8 | </ng-container> \ No newline at end of file | ||
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.scss b/client/src/app/shared/moderation/user-moderation-dropdown.component.scss new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.scss | |||
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts new file mode 100644 index 000000000..908f0b8e0 --- /dev/null +++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts | |||
@@ -0,0 +1,331 @@ | |||
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' | ||
4 | import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' | ||
5 | import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component' | ||
6 | import { UserService } from '@app/shared/users' | ||
7 | import { AuthService, ConfirmService } from '@app/core' | ||
8 | import { User, UserRight } from '../../../../../shared/models/users' | ||
9 | import { Account } from '@app/shared/account/account.model' | ||
10 | import { BlocklistService } from '@app/shared/blocklist' | ||
11 | |||
12 | @Component({ | ||
13 | selector: 'my-user-moderation-dropdown', | ||
14 | templateUrl: './user-moderation-dropdown.component.html', | ||
15 | styleUrls: [ './user-moderation-dropdown.component.scss' ] | ||
16 | }) | ||
17 | export class UserModerationDropdownComponent implements OnChanges { | ||
18 | @ViewChild('userBanModal') userBanModal: UserBanModalComponent | ||
19 | |||
20 | @Input() user: User | ||
21 | @Input() account: Account | ||
22 | |||
23 | @Input() buttonSize: 'normal' | 'small' = 'normal' | ||
24 | @Input() placement = 'left' | ||
25 | |||
26 | @Output() userChanged = new EventEmitter() | ||
27 | @Output() userDeleted = new EventEmitter() | ||
28 | |||
29 | userActions: DropdownAction<{ user: User, account: Account }>[] = [] | ||
30 | |||
31 | constructor ( | ||
32 | private authService: AuthService, | ||
33 | private notificationsService: NotificationsService, | ||
34 | private confirmService: ConfirmService, | ||
35 | private userService: UserService, | ||
36 | private blocklistService: BlocklistService, | ||
37 | private i18n: I18n | ||
38 | ) { } | ||
39 | |||
40 | ngOnChanges () { | ||
41 | this.buildActions() | ||
42 | } | ||
43 | |||
44 | openBanUserModal (user: User) { | ||
45 | if (user.username === 'root') { | ||
46 | this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.')) | ||
47 | return | ||
48 | } | ||
49 | |||
50 | this.userBanModal.openModal(user) | ||
51 | } | ||
52 | |||
53 | onUserBanned () { | ||
54 | this.userChanged.emit() | ||
55 | } | ||
56 | |||
57 | async unbanUser (user: User) { | ||
58 | const message = this.i18n('Do you really want to unban {{username}}?', { username: user.username }) | ||
59 | const res = await this.confirmService.confirm(message, this.i18n('Unban')) | ||
60 | if (res === false) return | ||
61 | |||
62 | this.userService.unbanUsers(user) | ||
63 | .subscribe( | ||
64 | () => { | ||
65 | this.notificationsService.success( | ||
66 | this.i18n('Success'), | ||
67 | this.i18n('User {{username}} unbanned.', { username: user.username }) | ||
68 | ) | ||
69 | |||
70 | this.userChanged.emit() | ||
71 | }, | ||
72 | |||
73 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
74 | ) | ||
75 | } | ||
76 | |||
77 | async removeUser (user: User) { | ||
78 | if (user.username === 'root') { | ||
79 | this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.')) | ||
80 | return | ||
81 | } | ||
82 | |||
83 | const message = this.i18n('If you remove this user, you will not be able to create another with the same username!') | ||
84 | const res = await this.confirmService.confirm(message, this.i18n('Delete')) | ||
85 | if (res === false) return | ||
86 | |||
87 | this.userService.removeUser(user).subscribe( | ||
88 | () => { | ||
89 | this.notificationsService.success( | ||
90 | this.i18n('Success'), | ||
91 | this.i18n('User {{username}} deleted.', { username: user.username }) | ||
92 | ) | ||
93 | this.userDeleted.emit() | ||
94 | }, | ||
95 | |||
96 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
97 | ) | ||
98 | } | ||
99 | |||
100 | blockAccountByUser (account: Account) { | ||
101 | this.blocklistService.blockAccountByUser(account) | ||
102 | .subscribe( | ||
103 | () => { | ||
104 | this.notificationsService.success( | ||
105 | this.i18n('Success'), | ||
106 | this.i18n('Account {{nameWithHost}} muted.', { nameWithHost: account.nameWithHost }) | ||
107 | ) | ||
108 | |||
109 | this.account.mutedByUser = true | ||
110 | this.userChanged.emit() | ||
111 | }, | ||
112 | |||
113 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
114 | ) | ||
115 | } | ||
116 | |||
117 | unblockAccountByUser (account: Account) { | ||
118 | this.blocklistService.unblockAccountByUser(account) | ||
119 | .subscribe( | ||
120 | () => { | ||
121 | this.notificationsService.success( | ||
122 | this.i18n('Success'), | ||
123 | this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: account.nameWithHost }) | ||
124 | ) | ||
125 | |||
126 | this.account.mutedByUser = false | ||
127 | this.userChanged.emit() | ||
128 | }, | ||
129 | |||
130 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
131 | ) | ||
132 | } | ||
133 | |||
134 | blockServerByUser (host: string) { | ||
135 | this.blocklistService.blockServerByUser(host) | ||
136 | .subscribe( | ||
137 | () => { | ||
138 | this.notificationsService.success( | ||
139 | this.i18n('Success'), | ||
140 | this.i18n('Instance {{host}} muted.', { host }) | ||
141 | ) | ||
142 | |||
143 | this.account.mutedServerByUser = true | ||
144 | this.userChanged.emit() | ||
145 | }, | ||
146 | |||
147 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
148 | ) | ||
149 | } | ||
150 | |||
151 | unblockServerByUser (host: string) { | ||
152 | this.blocklistService.unblockServerByUser(host) | ||
153 | .subscribe( | ||
154 | () => { | ||
155 | this.notificationsService.success( | ||
156 | this.i18n('Success'), | ||
157 | this.i18n('Instance {{host}} unmuted.', { host }) | ||
158 | ) | ||
159 | |||
160 | this.account.mutedServerByUser = false | ||
161 | this.userChanged.emit() | ||
162 | }, | ||
163 | |||
164 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
165 | ) | ||
166 | } | ||
167 | |||
168 | blockAccountByInstance (account: Account) { | ||
169 | this.blocklistService.blockAccountByInstance(account) | ||
170 | .subscribe( | ||
171 | () => { | ||
172 | this.notificationsService.success( | ||
173 | this.i18n('Success'), | ||
174 | this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }) | ||
175 | ) | ||
176 | |||
177 | this.account.mutedByInstance = true | ||
178 | this.userChanged.emit() | ||
179 | }, | ||
180 | |||
181 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
182 | ) | ||
183 | } | ||
184 | |||
185 | unblockAccountByInstance (account: Account) { | ||
186 | this.blocklistService.unblockAccountByInstance(account) | ||
187 | .subscribe( | ||
188 | () => { | ||
189 | this.notificationsService.success( | ||
190 | this.i18n('Success'), | ||
191 | this.i18n('Account {{nameWithHost}} unmuted by the instance.', { nameWithHost: account.nameWithHost }) | ||
192 | ) | ||
193 | |||
194 | this.account.mutedByInstance = false | ||
195 | this.userChanged.emit() | ||
196 | }, | ||
197 | |||
198 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
199 | ) | ||
200 | } | ||
201 | |||
202 | blockServerByInstance (host: string) { | ||
203 | this.blocklistService.blockServerByInstance(host) | ||
204 | .subscribe( | ||
205 | () => { | ||
206 | this.notificationsService.success( | ||
207 | this.i18n('Success'), | ||
208 | this.i18n('Instance {{host}} muted by the instance.', { host }) | ||
209 | ) | ||
210 | |||
211 | this.account.mutedServerByInstance = true | ||
212 | this.userChanged.emit() | ||
213 | }, | ||
214 | |||
215 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
216 | ) | ||
217 | } | ||
218 | |||
219 | unblockServerByInstance (host: string) { | ||
220 | this.blocklistService.unblockServerByInstance(host) | ||
221 | .subscribe( | ||
222 | () => { | ||
223 | this.notificationsService.success( | ||
224 | this.i18n('Success'), | ||
225 | this.i18n('Instance {{host}} unmuted by the instance.', { host }) | ||
226 | ) | ||
227 | |||
228 | this.account.mutedServerByInstance = false | ||
229 | this.userChanged.emit() | ||
230 | }, | ||
231 | |||
232 | err => this.notificationsService.error(this.i18n('Error'), err.message) | ||
233 | ) | ||
234 | } | ||
235 | |||
236 | getRouterUserEditLink (user: User) { | ||
237 | return [ '/admin', 'users', 'update', user.id ] | ||
238 | } | ||
239 | |||
240 | private buildActions () { | ||
241 | this.userActions = [] | ||
242 | |||
243 | if (this.authService.isLoggedIn()) { | ||
244 | const authUser = this.authService.getUser() | ||
245 | |||
246 | if (this.user && authUser.id === this.user.id) return | ||
247 | |||
248 | if (this.user && authUser.hasRight(UserRight.MANAGE_USERS)) { | ||
249 | this.userActions = this.userActions.concat([ | ||
250 | { | ||
251 | label: this.i18n('Edit'), | ||
252 | linkBuilder: ({ user }) => this.getRouterUserEditLink(user) | ||
253 | }, | ||
254 | { | ||
255 | label: this.i18n('Delete'), | ||
256 | handler: ({ user }) => this.removeUser(user) | ||
257 | }, | ||
258 | { | ||
259 | label: this.i18n('Ban'), | ||
260 | handler: ({ user }: { user: User }) => this.openBanUserModal(user), | ||
261 | isDisplayed: ({ user }: { user: User }) => !user.blocked | ||
262 | }, | ||
263 | { | ||
264 | label: this.i18n('Unban'), | ||
265 | handler: ({ user }: { user: User }) => this.unbanUser(user), | ||
266 | isDisplayed: ({ user }: { user: User }) => user.blocked | ||
267 | } | ||
268 | ]) | ||
269 | } | ||
270 | |||
271 | // Actions on accounts/servers | ||
272 | if (this.account) { | ||
273 | // User actions | ||
274 | this.userActions = this.userActions.concat([ | ||
275 | { | ||
276 | label: this.i18n('Mute this account'), | ||
277 | isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === false, | ||
278 | handler: ({ account }: { account: Account }) => this.blockAccountByUser(account) | ||
279 | }, | ||
280 | { | ||
281 | label: this.i18n('Unmute this account'), | ||
282 | isDisplayed: ({ account }: { account: Account }) => account.mutedByUser === true, | ||
283 | handler: ({ account }: { account: Account }) => this.unblockAccountByUser(account) | ||
284 | }, | ||
285 | { | ||
286 | label: this.i18n('Mute the instance'), | ||
287 | isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false, | ||
288 | handler: ({ account }: { account: Account }) => this.blockServerByUser(account.host) | ||
289 | }, | ||
290 | { | ||
291 | label: this.i18n('Unmute the instance'), | ||
292 | isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true, | ||
293 | handler: ({ account }: { account: Account }) => this.unblockServerByUser(account.host) | ||
294 | } | ||
295 | ]) | ||
296 | |||
297 | // Instance actions | ||
298 | if (authUser.hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)) { | ||
299 | this.userActions = this.userActions.concat([ | ||
300 | { | ||
301 | label: this.i18n('Mute this account by your instance'), | ||
302 | isDisplayed: ({ account }: { account: Account }) => account.mutedByInstance === false, | ||
303 | handler: ({ account }: { account: Account }) => this.blockAccountByInstance(account) | ||
304 | }, | ||
305 | { | ||
306 | label: this.i18n('Unmute this account by your instance'), | ||
307 | isDisplayed: ({ account }: { account: Account }) => account.mutedByInstance === true, | ||
308 | handler: ({ account }: { account: Account }) => this.unblockAccountByInstance(account) | ||
309 | } | ||
310 | ]) | ||
311 | } | ||
312 | |||
313 | // Instance actions | ||
314 | if (authUser.hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)) { | ||
315 | this.userActions = this.userActions.concat([ | ||
316 | { | ||
317 | label: this.i18n('Mute the instance by your instance'), | ||
318 | isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === false, | ||
319 | handler: ({ account }: { account: Account }) => this.blockServerByInstance(account.host) | ||
320 | }, | ||
321 | { | ||
322 | label: this.i18n('Unmute the instance by your instance'), | ||
323 | isDisplayed: ({ account }: { account: Account }) => !account.userId && account.mutedServerByInstance === true, | ||
324 | handler: ({ account }: { account: Account }) => this.unblockServerByInstance(account.host) | ||
325 | } | ||
326 | ]) | ||
327 | } | ||
328 | } | ||
329 | } | ||
330 | } | ||
331 | } | ||
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/rest/rest-extractor.service.ts b/client/src/app/shared/rest/rest-extractor.service.ts index 6492aa66d..f149569ef 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 |
@@ -83,7 +83,7 @@ export class RestExtractor { | |||
83 | errorMessage = err | 83 | errorMessage = err |
84 | } | 84 | } |
85 | 85 | ||
86 | const errorObj = { | 86 | const errorObj: { message: string, status: string, body: string } = { |
87 | message: errorMessage, | 87 | message: errorMessage, |
88 | status: undefined, | 88 | status: undefined, |
89 | body: undefined | 89 | 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 076f1d275..0ec2a9b15 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -25,7 +25,7 @@ import { VideoAbuseService } from './video-abuse' | |||
25 | import { VideoBlacklistService } from './video-blacklist' | 25 | import { VideoBlacklistService } from './video-blacklist' |
26 | import { VideoOwnershipService } from './video-ownership' | 26 | import { VideoOwnershipService } from './video-ownership' |
27 | import { VideoMiniatureComponent } from './video/video-miniature.component' | 27 | import { VideoMiniatureComponent } from './video/video-miniature.component' |
28 | import { VideoFeedComponent } from './video/video-feed.component' | 28 | import { FeedComponent } from './video/feed.component' |
29 | import { VideoThumbnailComponent } from './video/video-thumbnail.component' | 29 | import { VideoThumbnailComponent } from './video/video-thumbnail.component' |
30 | import { VideoService } from './video/video.service' | 30 | import { VideoService } from './video/video.service' |
31 | import { AccountService } from '@app/shared/account/account.service' | 31 | import { AccountService } from '@app/shared/account/account.service' |
@@ -56,6 +56,9 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N | |||
56 | import { SubscribeButtonComponent, RemoteSubscribeComponent, UserSubscriptionService } from '@app/shared/user-subscription' | 56 | import { SubscribeButtonComponent, RemoteSubscribeComponent, UserSubscriptionService } from '@app/shared/user-subscription' |
57 | import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' | 57 | import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' |
58 | import { OverviewService } from '@app/shared/overview' | 58 | import { OverviewService } from '@app/shared/overview' |
59 | import { UserBanModalComponent } from '@app/shared/moderation' | ||
60 | import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component' | ||
61 | import { BlocklistService } from '@app/shared/blocklist' | ||
59 | 62 | ||
60 | @NgModule({ | 63 | @NgModule({ |
61 | imports: [ | 64 | imports: [ |
@@ -79,7 +82,7 @@ import { OverviewService } from '@app/shared/overview' | |||
79 | LoaderComponent, | 82 | LoaderComponent, |
80 | VideoThumbnailComponent, | 83 | VideoThumbnailComponent, |
81 | VideoMiniatureComponent, | 84 | VideoMiniatureComponent, |
82 | VideoFeedComponent, | 85 | FeedComponent, |
83 | ButtonComponent, | 86 | ButtonComponent, |
84 | DeleteButtonComponent, | 87 | DeleteButtonComponent, |
85 | EditButtonComponent, | 88 | EditButtonComponent, |
@@ -94,7 +97,9 @@ import { OverviewService } from '@app/shared/overview' | |||
94 | PeertubeCheckboxComponent, | 97 | PeertubeCheckboxComponent, |
95 | SubscribeButtonComponent, | 98 | SubscribeButtonComponent, |
96 | RemoteSubscribeComponent, | 99 | RemoteSubscribeComponent, |
97 | InstanceFeaturesTableComponent | 100 | InstanceFeaturesTableComponent, |
101 | UserBanModalComponent, | ||
102 | UserModerationDropdownComponent | ||
98 | ], | 103 | ], |
99 | 104 | ||
100 | exports: [ | 105 | exports: [ |
@@ -117,7 +122,7 @@ import { OverviewService } from '@app/shared/overview' | |||
117 | LoaderComponent, | 122 | LoaderComponent, |
118 | VideoThumbnailComponent, | 123 | VideoThumbnailComponent, |
119 | VideoMiniatureComponent, | 124 | VideoMiniatureComponent, |
120 | VideoFeedComponent, | 125 | FeedComponent, |
121 | ButtonComponent, | 126 | ButtonComponent, |
122 | DeleteButtonComponent, | 127 | DeleteButtonComponent, |
123 | EditButtonComponent, | 128 | EditButtonComponent, |
@@ -130,6 +135,8 @@ import { OverviewService } from '@app/shared/overview' | |||
130 | SubscribeButtonComponent, | 135 | SubscribeButtonComponent, |
131 | RemoteSubscribeComponent, | 136 | RemoteSubscribeComponent, |
132 | InstanceFeaturesTableComponent, | 137 | InstanceFeaturesTableComponent, |
138 | UserBanModalComponent, | ||
139 | UserModerationDropdownComponent, | ||
133 | 140 | ||
134 | NumberFormatterPipe, | 141 | NumberFormatterPipe, |
135 | ObjectLengthPipe, | 142 | ObjectLengthPipe, |
@@ -166,6 +173,7 @@ import { OverviewService } from '@app/shared/overview' | |||
166 | OverviewService, | 173 | OverviewService, |
167 | VideoChangeOwnershipValidatorsService, | 174 | VideoChangeOwnershipValidatorsService, |
168 | VideoAcceptOwnershipValidatorsService, | 175 | VideoAcceptOwnershipValidatorsService, |
176 | BlocklistService, | ||
169 | 177 | ||
170 | I18nPrimengCalendarService, | 178 | I18nPrimengCalendarService, |
171 | ScreenService, | 179 | ScreenService, |
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 877f1bf3a..7c840ffa7 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts | |||
@@ -18,6 +18,7 @@ export type UserConstructorHash = { | |||
18 | videoQuota?: number, | 18 | videoQuota?: number, |
19 | videoQuotaDaily?: number, | 19 | videoQuotaDaily?: number, |
20 | nsfwPolicy?: NSFWPolicyType, | 20 | nsfwPolicy?: NSFWPolicyType, |
21 | webTorrentEnabled?: boolean, | ||
21 | autoPlayVideo?: boolean, | 22 | autoPlayVideo?: boolean, |
22 | createdAt?: Date, | 23 | createdAt?: Date, |
23 | account?: AccountServerModel, | 24 | account?: AccountServerModel, |
@@ -32,6 +33,7 @@ export class User implements UserServerModel { | |||
32 | email: string | 33 | email: string |
33 | role: UserRole | 34 | role: UserRole |
34 | nsfwPolicy: NSFWPolicyType | 35 | nsfwPolicy: NSFWPolicyType |
36 | webTorrentEnabled: boolean | ||
35 | autoPlayVideo: boolean | 37 | autoPlayVideo: boolean |
36 | videoQuota: number | 38 | videoQuota: number |
37 | videoQuotaDaily: number | 39 | videoQuotaDaily: number |
@@ -52,6 +54,7 @@ export class User implements UserServerModel { | |||
52 | this.videoQuota = hash.videoQuota | 54 | this.videoQuota = hash.videoQuota |
53 | this.videoQuotaDaily = hash.videoQuotaDaily | 55 | this.videoQuotaDaily = hash.videoQuotaDaily |
54 | this.nsfwPolicy = hash.nsfwPolicy | 56 | this.nsfwPolicy = hash.nsfwPolicy |
57 | this.webTorrentEnabled = hash.webTorrentEnabled | ||
55 | this.autoPlayVideo = hash.autoPlayVideo | 58 | this.autoPlayVideo = hash.autoPlayVideo |
56 | this.createdAt = hash.createdAt | 59 | this.createdAt = hash.createdAt |
57 | this.blocked = hash.blocked | 60 | this.blocked = hash.blocked |
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index bd5cd45d4..27a81f0a2 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts | |||
@@ -1,21 +1,27 @@ | |||
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 { UserCreate, UserUpdateMe, UserVideoQuota } from '../../../../../shared' | 5 | import { ResultList, User, UserCreate, UserRole, UserUpdate, UserUpdateMe, UserVideoQuota } from '../../../../../shared' |
6 | import { environment } from '../../../environments/environment' | 6 | import { environment } from '../../../environments/environment' |
7 | import { RestExtractor } from '../rest' | 7 | import { RestExtractor, RestPagination, RestService } from '../rest' |
8 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 8 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
9 | import { SortMeta } from 'primeng/api' | ||
10 | import { BytesPipe } from 'ngx-pipes' | ||
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
9 | 12 | ||
10 | @Injectable() | 13 | @Injectable() |
11 | export class UserService { | 14 | export class UserService { |
12 | static BASE_USERS_URL = environment.apiUrl + '/api/v1/users/' | 15 | static BASE_USERS_URL = environment.apiUrl + '/api/v1/users/' |
13 | 16 | ||
17 | private bytesPipe = new BytesPipe() | ||
18 | |||
14 | constructor ( | 19 | constructor ( |
15 | private authHttp: HttpClient, | 20 | private authHttp: HttpClient, |
16 | private restExtractor: RestExtractor | 21 | private restExtractor: RestExtractor, |
17 | ) { | 22 | private restService: RestService, |
18 | } | 23 | private i18n: I18n |
24 | ) { } | ||
19 | 25 | ||
20 | changePassword (currentPassword: string, newPassword: string) { | 26 | changePassword (currentPassword: string, newPassword: string) { |
21 | const url = UserService.BASE_USERS_URL + 'me' | 27 | const url = UserService.BASE_USERS_URL + 'me' |
@@ -128,4 +134,98 @@ export class UserService { | |||
128 | .get<string[]>(url, { params }) | 134 | .get<string[]>(url, { params }) |
129 | .pipe(catchError(res => this.restExtractor.handleError(res))) | 135 | .pipe(catchError(res => this.restExtractor.handleError(res))) |
130 | } | 136 | } |
137 | |||
138 | /* ###### Admin methods ###### */ | ||
139 | |||
140 | addUser (userCreate: UserCreate) { | ||
141 | return this.authHttp.post(UserService.BASE_USERS_URL, userCreate) | ||
142 | .pipe( | ||
143 | map(this.restExtractor.extractDataBool), | ||
144 | catchError(err => this.restExtractor.handleError(err)) | ||
145 | ) | ||
146 | } | ||
147 | |||
148 | updateUser (userId: number, userUpdate: UserUpdate) { | ||
149 | return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate) | ||
150 | .pipe( | ||
151 | map(this.restExtractor.extractDataBool), | ||
152 | catchError(err => this.restExtractor.handleError(err)) | ||
153 | ) | ||
154 | } | ||
155 | |||
156 | getUser (userId: number) { | ||
157 | return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId) | ||
158 | .pipe(catchError(err => this.restExtractor.handleError(err))) | ||
159 | } | ||
160 | |||
161 | getUsers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<User>> { | ||
162 | let params = new HttpParams() | ||
163 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
164 | |||
165 | if (search) params = params.append('search', search) | ||
166 | |||
167 | return this.authHttp.get<ResultList<User>>(UserService.BASE_USERS_URL, { params }) | ||
168 | .pipe( | ||
169 | map(res => this.restExtractor.convertResultListDateToHuman(res)), | ||
170 | map(res => this.restExtractor.applyToResultListData(res, this.formatUser.bind(this))), | ||
171 | catchError(err => this.restExtractor.handleError(err)) | ||
172 | ) | ||
173 | } | ||
174 | |||
175 | removeUser (usersArg: User | User[]) { | ||
176 | const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] | ||
177 | |||
178 | return from(users) | ||
179 | .pipe( | ||
180 | concatMap(u => this.authHttp.delete(UserService.BASE_USERS_URL + u.id)), | ||
181 | toArray(), | ||
182 | catchError(err => this.restExtractor.handleError(err)) | ||
183 | ) | ||
184 | } | ||
185 | |||
186 | banUsers (usersArg: User | User[], reason?: string) { | ||
187 | const body = reason ? { reason } : {} | ||
188 | const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] | ||
189 | |||
190 | return from(users) | ||
191 | .pipe( | ||
192 | concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/block', body)), | ||
193 | toArray(), | ||
194 | catchError(err => this.restExtractor.handleError(err)) | ||
195 | ) | ||
196 | } | ||
197 | |||
198 | unbanUsers (usersArg: User | User[]) { | ||
199 | const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] | ||
200 | |||
201 | return from(users) | ||
202 | .pipe( | ||
203 | concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/unblock', {})), | ||
204 | toArray(), | ||
205 | catchError(err => this.restExtractor.handleError(err)) | ||
206 | ) | ||
207 | } | ||
208 | |||
209 | private formatUser (user: User) { | ||
210 | let videoQuota | ||
211 | if (user.videoQuota === -1) { | ||
212 | videoQuota = this.i18n('Unlimited') | ||
213 | } else { | ||
214 | videoQuota = this.bytesPipe.transform(user.videoQuota, 0) | ||
215 | } | ||
216 | |||
217 | const videoQuotaUsed = this.bytesPipe.transform(user.videoQuotaUsed, 0) | ||
218 | |||
219 | const roleLabels: { [ id in UserRole ]: string } = { | ||
220 | [UserRole.USER]: this.i18n('User'), | ||
221 | [UserRole.ADMINISTRATOR]: this.i18n('Administrator'), | ||
222 | [UserRole.MODERATOR]: this.i18n('Moderator') | ||
223 | } | ||
224 | |||
225 | return Object.assign(user, { | ||
226 | roleLabel: roleLabels[user.role], | ||
227 | videoQuota, | ||
228 | videoQuotaUsed | ||
229 | }) | ||
230 | } | ||
131 | } | 231 | } |
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index d543ab7c1..29492351b 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html | |||
@@ -1,8 +1,18 @@ | |||
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 | {{ titlePage }} | ||
5 | </div> | ||
6 | <my-feed [syndicationItems]="syndicationItems"></my-feed> | ||
7 | |||
8 | <div class="moderation-block" *ngIf="displayModerationBlock"> | ||
9 | <my-peertube-checkbox | ||
10 | (change)="toggleModerationDisplay()" | ||
11 | inputName="display-unlisted-private" i18n-labelText labelText="Display unlisted and private videos" | ||
12 | > | ||
13 | </my-peertube-checkbox> | ||
14 | </div> | ||
4 | </div> | 15 | </div> |
5 | <my-video-feed [syndicationItems]="syndicationItems"></my-video-feed> | ||
6 | 16 | ||
7 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div> | 17 | <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div> |
8 | <div | 18 | <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..9fb3fd4d6 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 | position: relative; | ||
23 | top: 1px; | ||
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 6a758ebe0..2d32dd6ad 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts | |||
@@ -12,6 +12,7 @@ import { Video } from './video.model' | |||
12 | import { I18n } from '@ngx-translate/i18n-polyfill' | 12 | import { I18n } from '@ngx-translate/i18n-polyfill' |
13 | import { ScreenService } from '@app/shared/misc/screen.service' | 13 | import { ScreenService } from '@app/shared/misc/screen.service' |
14 | import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' | 14 | import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' |
15 | import { Syndication } from '@app/shared/video/syndication.model' | ||
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,6 +38,7 @@ 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 | ||
40 | 42 | ||
41 | protected baseVideoWidth = 215 | 43 | protected baseVideoWidth = 215 |
42 | protected baseVideoHeight = 205 | 44 | protected baseVideoHeight = 205 |
@@ -58,7 +60,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
58 | private resizeSubscription: Subscription | 60 | private resizeSubscription: Subscription |
59 | 61 | ||
60 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}> | 62 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}> |
61 | abstract generateSyndicationList () | 63 | abstract generateSyndicationList (): void |
62 | 64 | ||
63 | get user () { | 65 | get user () { |
64 | return this.authService.getUser() | 66 | return this.authService.getUser() |
@@ -83,7 +85,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
83 | 85 | ||
84 | pageByVideoId (index: number, page: Video[]) { | 86 | pageByVideoId (index: number, page: Video[]) { |
85 | // Video are unique in all pages | 87 | // Video are unique in all pages |
86 | return page[0].id | 88 | return page.length !== 0 ? page[0].id : 0 |
87 | } | 89 | } |
88 | 90 | ||
89 | videoById (index: number, video: Video) { | 91 | videoById (index: number, video: Video) { |
@@ -160,6 +162,10 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
160 | ) | 162 | ) |
161 | } | 163 | } |
162 | 164 | ||
165 | toggleModerationDisplay () { | ||
166 | throw new Error('toggleModerationDisplay is not implemented') | ||
167 | } | ||
168 | |||
163 | protected hasMoreVideos () { | 169 | protected hasMoreVideos () { |
164 | // No results | 170 | // No results |
165 | if (this.pagination.totalItems === 0) return false | 171 | if (this.pagination.totalItems === 0) return false |
@@ -206,7 +212,9 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
206 | protected setNewRouteParams () { | 212 | protected setNewRouteParams () { |
207 | const paramsObject = this.buildRouteParams() | 213 | const paramsObject = this.buildRouteParams() |
208 | 214 | ||
209 | const queryParams = Object.keys(paramsObject).map(p => p + '=' + paramsObject[p]).join('&') | 215 | const queryParams = Object.keys(paramsObject) |
216 | .map(p => p + '=' + paramsObject[p]) | ||
217 | .join('&') | ||
210 | this.location.replaceState(this.currentRoute, queryParams) | 218 | this.location.replaceState(this.currentRoute, queryParams) |
211 | } | 219 | } |
212 | 220 | ||
diff --git a/client/src/app/shared/video/video-feed.component.html b/client/src/app/shared/video/feed.component.html index 16116ba88..16116ba88 100644 --- a/client/src/app/shared/video/video-feed.component.html +++ b/client/src/app/shared/video/feed.component.html | |||
diff --git a/client/src/app/shared/video/video-feed.component.scss b/client/src/app/shared/video/feed.component.scss index 385764be0..385764be0 100644 --- a/client/src/app/shared/video/video-feed.component.scss +++ b/client/src/app/shared/video/feed.component.scss | |||
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-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts index 0046be964..fc772a3cf 100644 --- a/client/src/app/shared/video/video-edit.model.ts +++ b/client/src/app/shared/video/video-edit.model.ts | |||
@@ -49,14 +49,14 @@ export class VideoEdit implements VideoUpdate { | |||
49 | } | 49 | } |
50 | } | 50 | } |
51 | 51 | ||
52 | patch (values: Object) { | 52 | patch (values: { [ id: string ]: string }) { |
53 | Object.keys(values).forEach((key) => { | 53 | Object.keys(values).forEach((key) => { |
54 | this[ key ] = values[ key ] | 54 | this[ key ] = values[ key ] |
55 | }) | 55 | }) |
56 | 56 | ||
57 | // If schedule publication, the video is private and will be changed to public privacy | 57 | // If schedule publication, the video is private and will be changed to public privacy |
58 | if (parseInt(values['privacy'], 10) === VideoEdit.SPECIAL_SCHEDULED_PRIVACY) { | 58 | if (parseInt(values['privacy'], 10) === VideoEdit.SPECIAL_SCHEDULED_PRIVACY) { |
59 | const updateAt = (values['schedulePublicationAt'] as Date) | 59 | const updateAt = new Date(values['schedulePublicationAt']) |
60 | updateAt.setSeconds(0) | 60 | updateAt.setSeconds(0) |
61 | 61 | ||
62 | this.privacy = VideoPrivacy.PRIVATE | 62 | this.privacy = VideoPrivacy.PRIVATE |
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..277a0cf35 100644 --- a/client/src/app/shared/video/video-miniature.component.html +++ b/client/src/app/shared/video/video-miniature.component.html | |||
@@ -8,6 +8,9 @@ | |||
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 | {{ video.name }} | 10 | {{ video.name }} |
11 | |||
12 | <span *ngIf="isUnlistedVideo()" class="badge badge-warning" i18n>Unlisted</span> | ||
13 | <span *ngIf="isPrivateVideo()" class="badge badge-danger" i18n>Private</span> | ||
11 | </a> | 14 | </a> |
12 | 15 | ||
13 | <span i18n class="video-miniature-created-at-views">{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> | 16 | <span i18n class="video-miniature-created-at-views">{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> |
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-thumbnail.component.html b/client/src/app/shared/video/video-thumbnail.component.html index c1d45ea18..d25666916 100644 --- a/client/src/app/shared/video/video-thumbnail.component.html +++ b/client/src/app/shared/video/video-thumbnail.component.html | |||
@@ -2,9 +2,11 @@ | |||
2 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" | 2 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" |
3 | class="video-thumbnail" | 3 | class="video-thumbnail" |
4 | > | 4 | > |
5 | <img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" /> | 5 | <img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" /> |
6 | 6 | ||
7 | <div class="video-thumbnail-overlay"> | 7 | <div class="video-thumbnail-overlay">{{ video.durationLabel }}</div> |
8 | {{ video.durationLabel }} | 8 | |
9 | </div> | 9 | <div class="progress-bar" *ngIf="video.userHistory?.currentTime"> |
10 | <div [ngStyle]="{ 'width.%': getProgressPercent() }"></div> | ||
11 | </div> | ||
10 | </a> | 12 | </a> |
diff --git a/client/src/app/shared/video/video-thumbnail.component.scss b/client/src/app/shared/video/video-thumbnail.component.scss index 1dd8e5338..4772edaf0 100644 --- a/client/src/app/shared/video/video-thumbnail.component.scss +++ b/client/src/app/shared/video/video-thumbnail.component.scss | |||
@@ -29,6 +29,19 @@ | |||
29 | } | 29 | } |
30 | } | 30 | } |
31 | 31 | ||
32 | .progress-bar { | ||
33 | height: 3px; | ||
34 | width: 100%; | ||
35 | position: relative; | ||
36 | top: -3px; | ||
37 | background-color: rgba(0, 0, 0, 0.20); | ||
38 | |||
39 | div { | ||
40 | height: 100%; | ||
41 | background-color: var(--mainColor); | ||
42 | } | ||
43 | } | ||
44 | |||
32 | .video-thumbnail-overlay { | 45 | .video-thumbnail-overlay { |
33 | position: absolute; | 46 | position: absolute; |
34 | right: 5px; | 47 | right: 5px; |
diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts index 86d8f6f74..ca43700c7 100644 --- a/client/src/app/shared/video/video-thumbnail.component.ts +++ b/client/src/app/shared/video/video-thumbnail.component.ts | |||
@@ -22,4 +22,12 @@ export class VideoThumbnailComponent { | |||
22 | 22 | ||
23 | return this.video.thumbnailUrl | 23 | return this.video.thumbnailUrl |
24 | } | 24 | } |
25 | |||
26 | getProgressPercent () { | ||
27 | if (!this.video.userHistory) return 0 | ||
28 | |||
29 | const currentTime = this.video.userHistory.currentTime | ||
30 | |||
31 | return (currentTime / this.video.duration) * 100 | ||
32 | } | ||
25 | } | 33 | } |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 80794faa6..b92c96450 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -66,6 +66,10 @@ export class Video implements VideoServerModel { | |||
66 | avatar: Avatar | 66 | avatar: Avatar |
67 | } | 67 | } |
68 | 68 | ||
69 | userHistory?: { | ||
70 | currentTime: number | ||
71 | } | ||
72 | |||
69 | static buildClientUrl (videoUUID: string) { | 73 | static buildClientUrl (videoUUID: string) { |
70 | return '/videos/watch/' + videoUUID | 74 | return '/videos/watch/' + videoUUID |
71 | } | 75 | } |
@@ -116,6 +120,8 @@ export class Video implements VideoServerModel { | |||
116 | 120 | ||
117 | this.blacklisted = hash.blacklisted | 121 | this.blacklisted = hash.blacklisted |
118 | this.blacklistedReason = hash.blacklistedReason | 122 | this.blacklistedReason = hash.blacklistedReason |
123 | |||
124 | this.userHistory = hash.userHistory | ||
119 | } | 125 | } |
120 | 126 | ||
121 | isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { | 127 | isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { |
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 2255a18a2..65297d7a1 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts | |||
@@ -58,6 +58,10 @@ export class VideoService implements VideosProvider { | |||
58 | return VideoService.BASE_VIDEO_URL + uuid + '/views' | 58 | return VideoService.BASE_VIDEO_URL + uuid + '/views' |
59 | } | 59 | } |
60 | 60 | ||
61 | getUserWatchingVideoUrl (uuid: string) { | ||
62 | return VideoService.BASE_VIDEO_URL + uuid + '/watching' | ||
63 | } | ||
64 | |||
61 | getVideo (uuid: string): Observable<VideoDetails> { | 65 | getVideo (uuid: string): Observable<VideoDetails> { |
62 | return this.serverService.localeObservable | 66 | return this.serverService.localeObservable |
63 | .pipe( | 67 | .pipe( |
@@ -270,9 +274,9 @@ export class VideoService implements VideosProvider { | |||
270 | 274 | ||
271 | loadCompleteDescription (descriptionPath: string) { | 275 | loadCompleteDescription (descriptionPath: string) { |
272 | return this.authHttp | 276 | return this.authHttp |
273 | .get(environment.apiUrl + descriptionPath) | 277 | .get<{ description: string }>(environment.apiUrl + descriptionPath) |
274 | .pipe( | 278 | .pipe( |
275 | map(res => res[ 'description' ]), | 279 | map(res => res.description), |
276 | catchError(err => this.restExtractor.handleError(err)) | 280 | catchError(err => this.restExtractor.handleError(err)) |
277 | ) | 281 | ) |
278 | } | 282 | } |
diff --git a/client/src/app/signup/signup.component.html b/client/src/app/signup/signup.component.html index aad4b5be3..531a97814 100644 --- a/client/src/app/signup/signup.component.html +++ b/client/src/app/signup/signup.component.html | |||
@@ -51,7 +51,7 @@ | |||
51 | <div class="form-group form-group-terms"> | 51 | <div class="form-group form-group-terms"> |
52 | <my-peertube-checkbox | 52 | <my-peertube-checkbox |
53 | inputName="terms" formControlName="terms" | 53 | 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" | 54 | 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> | 55 | ></my-peertube-checkbox> |
56 | 56 | ||
57 | <div *ngIf="formErrors.terms" class="form-error"> | 57 | <div *ngIf="formErrors.terms" class="form-error"> |
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..796fbe531 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 |
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..25db8e8ed 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; |
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 eb9396d70..a56733e57 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 | |||
@@ -48,7 +48,7 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
48 | calendarTimezone: string | 48 | calendarTimezone: string |
49 | calendarDateFormat: string | 49 | calendarDateFormat: string |
50 | 50 | ||
51 | private schedulerInterval | 51 | private schedulerInterval: any |
52 | private firstPatchDone = false | 52 | private firstPatchDone = false |
53 | private initialVideoCaptions: string[] = [] | 53 | private initialVideoCaptions: string[] = [] |
54 | 54 | ||
@@ -77,13 +77,13 @@ export class VideoEditComponent implements OnInit, OnDestroy { | |||
77 | } | 77 | } |
78 | 78 | ||
79 | updateForm () { | 79 | updateForm () { |
80 | const defaultValues = { | 80 | const defaultValues: any = { |
81 | nsfw: 'false', | 81 | nsfw: 'false', |
82 | commentsEnabled: 'true', | 82 | commentsEnabled: 'true', |
83 | waitTranscoding: 'true', | 83 | waitTranscoding: 'true', |
84 | tags: [] | 84 | tags: [] |
85 | } | 85 | } |
86 | const obj = { | 86 | const obj: any = { |
87 | name: this.videoValidatorsService.VIDEO_NAME, | 87 | name: this.videoValidatorsService.VIDEO_NAME, |
88 | privacy: this.videoValidatorsService.VIDEO_PRIVACY, | 88 | privacy: this.videoValidatorsService.VIDEO_PRIVACY, |
89 | channelId: this.videoValidatorsService.VIDEO_CHANNEL, | 89 | channelId: this.videoValidatorsService.VIDEO_CHANNEL, |
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 0f7184ff8..e13c06ce9 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,4 +1,4 @@ | |||
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' | 3 | import { NotificationsService } from 'angular2-notifications' |
4 | import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' | 4 | import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' |
@@ -23,7 +23,7 @@ import { VideoImportService } from '@app/shared/video-import' | |||
23 | }) | 23 | }) |
24 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { | 24 | export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { |
25 | @Output() firstStepDone = new EventEmitter<string>() | 25 | @Output() firstStepDone = new EventEmitter<string>() |
26 | @ViewChild('torrentfileInput') torrentfileInput | 26 | @ViewChild('torrentfileInput') torrentfileInput: ElementRef<HTMLInputElement> |
27 | 27 | ||
28 | videoFileName: string | 28 | videoFileName: string |
29 | magnetUri = '' | 29 | magnetUri = '' |
@@ -64,7 +64,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca | |||
64 | } | 64 | } |
65 | 65 | ||
66 | fileChange () { | 66 | fileChange () { |
67 | const torrentfile = this.torrentfileInput.nativeElement.files[0] as File | 67 | const torrentfile = this.torrentfileInput.nativeElement.files[0] |
68 | if (!torrentfile) return | 68 | if (!torrentfile) return |
69 | 69 | ||
70 | this.importVideo(torrentfile) | 70 | this.importVideo(torrentfile) |
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..1bf22e1a9 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 | |||
@@ -3,7 +3,6 @@ import { LoadingBarService } from '@ngx-loading-bar/core' | |||
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
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' | 7 | import { AuthService, ServerService } from '@app/core' |
9 | import { VideoService } from '@app/shared/video/video.service' | 8 | import { VideoService } from '@app/shared/video/video.service' |
@@ -11,8 +10,9 @@ import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.m | |||
11 | import { VideoCaptionService } from '@app/shared/video-caption' | 10 | import { VideoCaptionService } from '@app/shared/video-caption' |
12 | import { VideoEdit } from '@app/shared/video/video-edit.model' | 11 | import { VideoEdit } from '@app/shared/video/video-edit.model' |
13 | import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' | 12 | import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' |
13 | import { CanComponentDeactivateResult } from '@app/shared/guards/can-deactivate-guard.service' | ||
14 | 14 | ||
15 | export abstract class VideoSend extends FormReactive implements OnInit, CanComponentDeactivate { | 15 | export abstract class VideoSend extends FormReactive implements OnInit { |
16 | userVideoChannels: { id: number, label: string, support: string }[] = [] | 16 | userVideoChannels: { id: number, label: string, support: string }[] = [] |
17 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] | 17 | videoPrivacies: VideoConstant<VideoPrivacy>[] = [] |
18 | videoCaptions: VideoCaptionEdit[] = [] | 18 | videoCaptions: VideoCaptionEdit[] = [] |
@@ -30,7 +30,7 @@ export abstract class VideoSend extends FormReactive implements OnInit, CanCompo | |||
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.ts b/client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts index 941dc5441..8e2d0deaf 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,5 +1,5 @@ | |||
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' | 5 | import { NotificationsService } from 'angular2-notifications' |
@@ -25,7 +25,7 @@ import { VideoCaptionService } from '@app/shared/video-caption' | |||
25 | }) | 25 | }) |
26 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { | 26 | export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { |
27 | @Output() firstStepDone = new EventEmitter<string>() | 27 | @Output() firstStepDone = new EventEmitter<string>() |
28 | @ViewChild('videofileInput') videofileInput | 28 | @ViewChild('videofileInput') videofileInput: ElementRef<HTMLInputElement> |
29 | 29 | ||
30 | // So that it can be accessed in the template | 30 | // So that it can be accessed in the template |
31 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY | 31 | readonly SPECIAL_SCHEDULED_PRIVACY = VideoEdit.SPECIAL_SCHEDULED_PRIVACY |
@@ -110,7 +110,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy | |||
110 | } | 110 | } |
111 | 111 | ||
112 | uploadFirstStep () { | 112 | uploadFirstStep () { |
113 | const videofile = this.videofileInput.nativeElement.files[0] as File | 113 | const videofile = this.videofileInput.nativeElement.files[0] |
114 | if (!videofile) return | 114 | if (!videofile) return |
115 | 115 | ||
116 | // Cannot upload videos > 8GB for now | 116 | // Cannot upload videos > 8GB for now |
diff --git a/client/src/app/videos/+video-watch/comment/linkifier.service.ts b/client/src/app/videos/+video-watch/comment/linkifier.service.ts index 3f4072efd..2529c9eaf 100644 --- a/client/src/app/videos/+video-watch/comment/linkifier.service.ts +++ b/client/src/app/videos/+video-watch/comment/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/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts index fb7de0e04..ba3c0398e 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 | |||
@@ -76,7 +76,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnInit { | |||
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() |
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..00f0460a1 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 | |||
@@ -26,7 +26,7 @@ export class VideoCommentComponent implements OnInit, OnChanges { | |||
26 | @Output() resetReply = new EventEmitter() | 26 | @Output() resetReply = new EventEmitter() |
27 | 27 | ||
28 | sanitizedCommentHTML = '' | 28 | sanitizedCommentHTML = '' |
29 | newParentComments = [] | 29 | newParentComments: VideoComment[] = [] |
30 | 30 | ||
31 | constructor ( | 31 | constructor ( |
32 | private linkifierService: LinkifierService, | 32 | private linkifierService: LinkifierService, |
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..921447d5b 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 | |||
@@ -30,9 +30,9 @@ export class VideoCommentService { | |||
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 = lineFeedToHtml(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 | } |
@@ -41,9 +41,9 @@ export class VideoCommentService { | |||
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 = lineFeedToHtml(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..8850eccd8 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 | |||
@@ -12,6 +12,7 @@ import { VideoDetails } from '../../../shared/video/video-details.model' | |||
12 | import { VideoComment } from './video-comment.model' | 12 | import { VideoComment } from './video-comment.model' |
13 | import { VideoCommentService } from './video-comment.service' | 13 | import { VideoCommentService } from './video-comment.service' |
14 | import { I18n } from '@ngx-translate/i18n-polyfill' | 14 | import { I18n } from '@ngx-translate/i18n-polyfill' |
15 | import { Syndication } from '@app/shared/video/syndication.model' | ||
15 | 16 | ||
16 | @Component({ | 17 | @Component({ |
17 | selector: 'my-video-comments', | 18 | selector: 'my-video-comments', |
@@ -35,7 +36,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { | |||
35 | threadComments: { [ id: number ]: VideoCommentThreadTree } = {} | 36 | threadComments: { [ id: number ]: VideoCommentThreadTree } = {} |
36 | threadLoading: { [ id: number ]: boolean } = {} | 37 | threadLoading: { [ id: number ]: boolean } = {} |
37 | 38 | ||
38 | syndicationItems = [] | 39 | syndicationItems: Syndication[] = [] |
39 | 40 | ||
40 | private sub: Subscription | 41 | private sub: Subscription |
41 | 42 | ||
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..2586a2204 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 | } |
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 ea10b22ad..dda870905 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -7,7 +7,9 @@ import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-supp | |||
7 | import { MetaService } from '@ngx-meta/core' | 7 | import { MetaService } from '@ngx-meta/core' |
8 | import { NotificationsService } from 'angular2-notifications' | 8 | import { NotificationsService } from 'angular2-notifications' |
9 | import { forkJoin, Subscription } from 'rxjs' | 9 | import { forkJoin, Subscription } from 'rxjs' |
10 | import * as videojs from 'video.js' | 10 | // FIXME: something weird with our path definition in tsconfig and typings |
11 | // @ts-ignore | ||
12 | import videojs from 'video.js' | ||
11 | import 'videojs-hotkeys' | 13 | import 'videojs-hotkeys' |
12 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 14 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
13 | import * as WebTorrent from 'webtorrent' | 15 | import * as WebTorrent from 'webtorrent' |
@@ -369,7 +371,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
369 | ) | 371 | ) |
370 | } | 372 | } |
371 | 373 | ||
372 | private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], startTime = 0) { | 374 | private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], startTimeFromUrl: number) { |
373 | this.video = video | 375 | this.video = video |
374 | 376 | ||
375 | // Re init attributes | 377 | // Re init attributes |
@@ -377,6 +379,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
377 | this.completeDescriptionShown = false | 379 | this.completeDescriptionShown = false |
378 | this.remoteServerDown = false | 380 | this.remoteServerDown = false |
379 | 381 | ||
382 | let startTime = startTimeFromUrl || (this.video.userHistory ? this.video.userHistory.currentTime : 0) | ||
383 | // Don't start the video if we are at the end | ||
384 | if (this.video.duration - startTime <= 1) startTime = 0 | ||
385 | |||
380 | if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { | 386 | if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { |
381 | const res = await this.confirmService.confirm( | 387 | const res = await this.confirmService.confirm( |
382 | this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'), | 388 | this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'), |
@@ -414,7 +420,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
414 | poster: this.video.previewUrl, | 420 | poster: this.video.previewUrl, |
415 | startTime, | 421 | startTime, |
416 | theaterMode: true, | 422 | theaterMode: true, |
417 | language: this.localeId | 423 | language: this.localeId, |
424 | |||
425 | userWatching: this.user ? { | ||
426 | url: this.videoService.getUserWatchingVideoUrl(this.video.uuid), | ||
427 | authorizationHeader: this.authService.getRequestHeaderValue() | ||
428 | } : undefined | ||
418 | }) | 429 | }) |
419 | 430 | ||
420 | if (this.videojsLocaleLoaded === false) { | 431 | if (this.videojsLocaleLoaded === false) { |
@@ -424,9 +435,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
424 | 435 | ||
425 | const self = this | 436 | const self = this |
426 | this.zone.runOutsideAngular(async () => { | 437 | this.zone.runOutsideAngular(async () => { |
427 | videojs(this.playerElement, videojsOptions, function () { | 438 | videojs(this.playerElement, videojsOptions, function (this: videojs.Player) { |
428 | self.player = this | 439 | self.player = this |
429 | this.on('customError', (event, data) => self.handleError(data.err)) | 440 | this.on('customError', ({ err }: { err: any }) => self.handleError(err)) |
430 | 441 | ||
431 | addContextMenu(self.player, self.video.embedUrl) | 442 | addContextMenu(self.player, self.video.embedUrl) |
432 | }) | 443 | }) |
@@ -439,7 +450,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
439 | this.checkUserRating() | 450 | this.checkUserRating() |
440 | } | 451 | } |
441 | 452 | ||
442 | private setRating (nextRating) { | 453 | private setRating (nextRating: VideoRateType) { |
443 | let method | 454 | let method |
444 | switch (nextRating) { | 455 | switch (nextRating) { |
445 | case 'like': | 456 | case 'like': |
@@ -461,7 +472,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
461 | this.userRating = nextRating | 472 | this.userRating = nextRating |
462 | }, | 473 | }, |
463 | 474 | ||
464 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 475 | (err: { message: string }) => this.notificationsService.error(this.i18n('Error'), err.message) |
465 | ) | 476 | ) |
466 | } | 477 | } |
467 | 478 | ||
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..9d000cf2e 100644 --- a/client/src/app/videos/video-list/video-local.component.ts +++ b/client/src/app/videos/video-list/video-local.component.ts | |||
@@ -10,6 +10,7 @@ import { VideoService } from '../../shared/video/video.service' | |||
10 | import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' | 10 | import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' |
11 | import { I18n } from '@ngx-translate/i18n-polyfill' | 11 | import { I18n } from '@ngx-translate/i18n-polyfill' |
12 | import { ScreenService } from '@app/shared/misc/screen.service' | 12 | import { ScreenService } from '@app/shared/misc/screen.service' |
13 | import { UserRight } from '../../../../../shared/models/users' | ||
13 | 14 | ||
14 | @Component({ | 15 | @Component({ |
15 | selector: 'my-videos-local', | 16 | selector: 'my-videos-local', |
@@ -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 | } |