aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-peertube/about-peertube.component.html4
-rw-r--r--client/src/app/+accounts/accounts.component.html12
-rw-r--r--client/src/app/+accounts/accounts.component.scss12
-rw-r--r--client/src/app/+accounts/accounts.component.ts48
-rw-r--r--client/src/app/+admin/admin.module.ts8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html2
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts28
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.html9
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.scss10
-rw-r--r--client/src/app/+admin/follows/followers-list/followers-list.component.ts2
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.html11
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.scss11
-rw-r--r--client/src/app/+admin/follows/following-list/following-list.component.ts4
-rw-r--r--client/src/app/+admin/follows/shared/follow.service.ts8
-rw-r--r--client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts2
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/index.ts2
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html22
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss7
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts59
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html23
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss7
-rw-r--r--client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts60
-rw-r--r--client/src/app/+admin/moderation/moderation.component.html4
-rw-r--r--client/src/app/+admin/moderation/moderation.component.ts8
-rw-r--r--client/src/app/+admin/moderation/moderation.routes.ts23
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html2
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts6
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html2
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts2
-rw-r--r--client/src/app/+admin/users/index.ts1
-rw-r--r--client/src/app/+admin/users/shared/index.ts1
-rw-r--r--client/src/app/+admin/users/shared/user.service.ts96
-rw-r--r--client/src/app/+admin/users/user-edit/user-create.component.ts2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts1
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts2
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html41
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.scss8
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.ts106
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html26
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss7
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts59
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html27
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss7
-rw-r--r--client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts60
-rw-r--r--client/src/app/+my-account/my-account-ownership/my-account-ownership.component.ts26
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts20
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html5
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts4
-rw-r--r--client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts6
-rw-r--r--client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html6
-rw-r--r--client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts2
-rw-r--r--client/src/app/+my-account/my-account.component.html16
-rw-r--r--client/src/app/+my-account/my-account.component.scss2
-rw-r--r--client/src/app/+my-account/my-account.component.ts15
-rw-r--r--client/src/app/+my-account/my-account.module.ts6
-rw-r--r--client/src/app/+my-account/shared/actor-avatar-info.component.ts4
-rw-r--r--client/src/app/app.component.ts13
-rw-r--r--client/src/app/app.module.ts2
-rw-r--r--client/src/app/core/auth/auth-user.model.ts4
-rw-r--r--client/src/app/core/auth/auth.service.ts2
-rw-r--r--client/src/app/core/server/server.service.ts2
-rw-r--r--client/src/app/core/theme/theme.service.ts4
-rw-r--r--client/src/app/header/header.component.html4
-rw-r--r--client/src/app/menu/menu.component.ts2
-rw-r--r--client/src/app/search/search.component.ts3
-rw-r--r--client/src/app/shared/account/account.model.ts12
-rw-r--r--client/src/app/shared/blocklist/account-block.model.ts14
-rw-r--r--client/src/app/shared/blocklist/blocklist.service.ts135
-rw-r--r--client/src/app/shared/blocklist/index.ts2
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html14
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss25
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.ts11
-rw-r--r--client/src/app/shared/buttons/button.component.ts6
-rw-r--r--client/src/app/shared/buttons/edit-button.component.ts2
-rw-r--r--client/src/app/shared/forms/form-validators/user-validators.service.ts4
-rw-r--r--client/src/app/shared/forms/form-validators/video-change-ownership-validators.service.ts15
-rw-r--r--client/src/app/shared/forms/form-validators/video-channel-validators.service.ts8
-rw-r--r--client/src/app/shared/forms/form-validators/video-validators.service.ts4
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.html2
-rw-r--r--client/src/app/shared/forms/peertube-checkbox.component.scss6
-rw-r--r--client/src/app/shared/guards/can-deactivate-guard.service.ts4
-rw-r--r--client/src/app/shared/misc/peertube-local-storage.ts6
-rw-r--r--client/src/app/shared/misc/utils.ts2
-rw-r--r--client/src/app/shared/moderation/index.ts2
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.html (renamed from client/src/app/+admin/users/user-list/user-ban-modal.component.html)2
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.scss (renamed from client/src/app/+admin/users/user-list/user-ban-modal.component.scss)0
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.ts (renamed from client/src/app/+admin/users/user-list/user-ban-modal.component.ts)29
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.html8
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.scss0
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts331
-rw-r--r--client/src/app/shared/overview/videos-overview.model.ts1
-rw-r--r--client/src/app/shared/rest/rest-extractor.service.ts4
-rw-r--r--client/src/app/shared/rest/rest-table.ts29
-rw-r--r--client/src/app/shared/rest/rest.service.ts2
-rw-r--r--client/src/app/shared/shared.module.ts16
-rw-r--r--client/src/app/shared/users/user.model.ts3
-rw-r--r--client/src/app/shared/users/user.service.ts114
-rw-r--r--client/src/app/shared/video/abstract-video-list.html16
-rw-r--r--client/src/app/shared/video/abstract-video-list.scss25
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts16
-rw-r--r--client/src/app/shared/video/feed.component.html (renamed from client/src/app/shared/video/video-feed.component.html)0
-rw-r--r--client/src/app/shared/video/feed.component.scss (renamed from client/src/app/shared/video/video-feed.component.scss)0
-rw-r--r--client/src/app/shared/video/feed.component.ts11
-rw-r--r--client/src/app/shared/video/syndication.model.ts7
-rw-r--r--client/src/app/shared/video/video-edit.model.ts4
-rw-r--r--client/src/app/shared/video/video-feed.component.ts10
-rw-r--r--client/src/app/shared/video/video-miniature.component.html3
-rw-r--r--client/src/app/shared/video/video-miniature.component.ts9
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.html10
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.scss13
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.ts8
-rw-r--r--client/src/app/shared/video/video.model.ts6
-rw-r--r--client/src/app/shared/video/video.service.ts8
-rw-r--r--client/src/app/signup/signup.component.html2
-rw-r--r--client/src/app/videos/+video-edit/shared/video-caption-add-modal.component.ts3
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.scss5
-rw-r--r--client/src/app/videos/+video-edit/shared/video-edit.component.ts6
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts6
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-send.ts6
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts6
-rw-r--r--client/src/app/videos/+video-watch/comment/linkifier.service.ts7
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.ts2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.model.ts2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.service.ts8
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.html2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.scss2
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.ts3
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss2
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts25
-rw-r--r--client/src/app/videos/video-list/video-local.component.ts12
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
8my-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 @@
1import { Component, OnInit, OnDestroy } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute } from '@angular/router' 2import { ActivatedRoute } from '@angular/router'
3import { AccountService } from '@app/shared/account/account.service' 3import { AccountService } from '@app/shared/account/account.service'
4import { Account } from '@app/shared/account/account.model' 4import { Account } from '@app/shared/account/account.model'
5import { RestExtractor } from '@app/shared' 5import { RestExtractor, UserService } from '@app/shared'
6import { catchError, switchMap, distinctUntilChanged, map } from 'rxjs/operators' 6import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
7import { Subscription } from 'rxjs' 7import { Subscription } from 'rxjs'
8import { NotificationsService } from 'angular2-notifications'
9import { User, UserRight } from '../../../../shared'
10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { 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})
13export class AccountsComponent implements OnInit, OnDestroy { 17export 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.
10import { JobsComponent } from './jobs/job.component' 10import { JobsComponent } from './jobs/job.component'
11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' 11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
12import { JobService } from './jobs/shared/job.service' 12import { JobService } from './jobs/shared/job.service'
13import { UserCreateComponent, UserListComponent, UsersComponent, UserService, UserUpdateComponent } from './users' 13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users'
14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' 14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
15import { UserBanModalComponent } from '@app/+admin/users/user-list/user-ban-modal.component'
16import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 15import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
17import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
18import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' 17import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
18import { 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 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ConfigService } from '@app/+admin/config/shared/config.service' 2import { ConfigService } from '@app/+admin/config/shared/config.service'
3import { ConfirmService } from '@app/core'
4import { ServerService } from '@app/core/server/server.service' 3import { ServerService } from '@app/core/server/server.service'
5import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared' 4import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared'
6import { NotificationsService } from 'angular2-notifications' 5import { 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
4my-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 @@
1export * from './instance-account-blocklist.component'
2export * 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 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { 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})
13export 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 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { BlocklistService } from '@app/shared/blocklist'
7import { 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})
14export 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'
4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list' 4import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list' 5import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list'
6import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 6import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
7import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
7 8
8export const ModerationRoutes: Routes = [ 9export 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 @@
1export * from './shared'
2export * from './user-edit' 1export * from './user-edit'
3export * from './user-list' 2export * from './user-list'
4export * from './users.component' 3export * 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 @@
1export * 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 @@
1import { catchError, map } from 'rxjs/operators'
2import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { BytesPipe } from 'ngx-pipes'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { Observable } from 'rxjs'
7import { ResultList, UserCreate, UserUpdate, User, UserRole } from '../../../../../../shared'
8import { environment } from '../../../../environments/environment'
9import { RestExtractor, RestPagination, RestService } from '../../../shared'
10import { I18n } from '@ngx-translate/i18n-polyfill'
11
12@Injectable()
13export 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 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
4import { UserService } from '../shared'
5import { ServerService } from '../../../core' 4import { ServerService } from '../../../core'
6import { UserCreate, UserRole } from '../../../../../../shared' 5import { UserCreate, UserRole } from '../../../../../../shared'
7import { UserEdit } from './user-edit' 6import { UserEdit } from './user-edit'
@@ -9,6 +8,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
9import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 8import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
10import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' 9import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
11import { ConfigService } from '@app/+admin/config/shared/config.service' 10import { ConfigService } from '@app/+admin/config/shared/config.service'
11import { 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 @@
1import { ServerService } from '../../../core' 1import { ServerService } from '../../../core'
2import { FormReactive } from '../../../shared' 2import { FormReactive } from '../../../shared'
3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' 3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared'
4import { EditCustomConfigComponent } from '../../../+admin/config/edit-custom-config/'
5import { ConfigService } from '@app/+admin/config/shared/config.service' 4import { ConfigService } from '@app/+admin/config/shared/config.service'
6 5
7export abstract class UserEdit extends FormReactive { 6export 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'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Subscription } from 'rxjs' 3import { Subscription } from 'rxjs'
4import { NotificationsService } from 'angular2-notifications' 4import { NotificationsService } from 'angular2-notifications'
5import { UserService } from '../shared'
6import { ServerService } from '../../../core' 5import { ServerService } from '../../../core'
7import { UserEdit } from './user-edit' 6import { UserEdit } from './user-edit'
8import { User, UserUpdate } from '../../../../../../shared' 7import { User, UserUpdate } from '../../../../../../shared'
@@ -10,6 +9,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
10import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 9import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
11import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' 10import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
12import { ConfigService } from '@app/+admin/config/shared/config.service' 11import { ConfigService } from '@app/+admin/config/shared/config.service'
12import { 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'
2import { NotificationsService } from 'angular2-notifications' 2import { NotificationsService } from 'angular2-notifications'
3import { SortMeta } from 'primeng/components/common/sortmeta' 3import { SortMeta } from 'primeng/components/common/sortmeta'
4import { ConfirmService } from '../../../core' 4import { ConfirmService } from '../../../core'
5import { RestPagination, RestTable } from '../../../shared' 5import { RestPagination, RestTable, UserService } from '../../../shared'
6import { UserService } from '../shared'
7import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
8import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
9import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
10import { UserBanModalComponent } from '@app/+admin/users/user-list/user-ban-modal.component'
11import { User } from '../../../../../../shared' 7import { User } from '../../../../../../shared'
8import { UserBanModalComponent } from '@app/shared/moderation'
9import { 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 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { 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})
13export 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 @@
1import { Component, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { RestPagination, RestTable } from '@app/shared'
5import { SortMeta } from 'primeng/components/common/sortmeta'
6import { ServerBlock } from '../../../../../shared'
7import { 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})
14export 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
11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' 11import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
12import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' 12import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
13import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component' 13import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component'
14import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
15import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
14 16
15const myAccountRoutes: Routes = [ 17const 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 @@
1import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
4import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit' 4import { 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})
19export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy { 19export 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'
13export class MyAccountComponent implements OnInit, OnDestroy { 13export 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
19import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' 19import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
20import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' 20import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone'
21import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component' 21import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
22import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
23import { 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 @@
1import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'
2import { ServerService } from '../../core/server' 2import { ServerService } from '../../core/server'
3import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
4import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 4import { 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})
12export class ActorAvatarInfoComponent { 12export 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'
4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core' 4import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
5import { is18nPath } from '../../../shared/models/i18n' 5import { is18nPath } from '../../../shared/models/i18n'
6import { ScreenService } from '@app/shared/misc/screen.service' 6import { ScreenService } from '@app/shared/misc/screen.service'
7import { skip } from 'rxjs/operators' 7import { skip, debounceTime } from 'rxjs/operators'
8import { HotkeysService, Hotkey } from 'angular2-hotkeys' 8import { HotkeysService, Hotkey } from 'angular2-hotkeys'
9import { I18n } from '@ngx-translate/i18n-polyfill' 9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { 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'
5export class ThemeService { 5export 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 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, RedirectService } from '@app/core' 3import { AuthService } from '@app/core'
4import { NotificationsService } from 'angular2-notifications' 4import { NotificationsService } from 'angular2-notifications'
5import { forkJoin, Subscription } from 'rxjs' 5import { forkJoin, Subscription } from 'rxjs'
6import { SearchService } from '@app/search/search.service' 6import { 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 @@
1import { AccountBlock as AccountBlockServer } from '../../../../../shared'
2import { Account } from '../account/account.model'
3
4export 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 @@
1import { Injectable } from '@angular/core'
2import { environment } from '../../../environments/environment'
3import { HttpClient, HttpParams } from '@angular/common/http'
4import { RestExtractor, RestPagination, RestService } from '../rest'
5import { SortMeta } from 'primeng/api'
6import { catchError, map } from 'rxjs/operators'
7import { AccountBlock as AccountBlockServer, ResultList, ServerBlock } from '../../../../../shared'
8import { Account } from '@app/shared/account/account.model'
9import { AccountBlock } from '@app/shared/blocklist/account-block.model'
10
11@Injectable()
12export 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 @@
1export * from './blocklist.service'
2export * 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
3export type DropdownAction<T> = { 3export 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> = {
16export class ActionDropdownComponent<T> { 16export 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
9export class ButtonComponent { 9export 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
9export class EditButtonComponent { 9export 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 @@
1import { I18n } from '@ngx-translate/i18n-polyfill' 1import { I18n } from '@ngx-translate/i18n-polyfill'
2import { Validators } from '@angular/forms' 2import { AbstractControl, ValidationErrors, Validators } from '@angular/forms'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { BuildFormValidator } from '@app/shared' 4import { 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'
4import { ConfirmService } from '../../core/index' 4import { ConfirmService } from '../../core/index'
5import { I18n } from '@ngx-translate/i18n-polyfill' 5import { I18n } from '@ngx-translate/i18n-polyfill'
6 6
7export type CanComponentDeactivateResult = { text?: string, canDeactivate: Observable<boolean> | boolean }
8
7export interface CanComponentDeactivate { 9export 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
105function lineFeedToHtml (obj: object, keyToNormalize: string) { 105function 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 @@
1export * from './user-ban-modal.component'
2export * 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 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications' 2import { NotificationsService } from 'angular2-notifications'
3import { FormReactive, UserValidatorsService } from '../../../shared'
4import { UserService } from '../shared'
5import { I18n } from '@ngx-translate/i18n-polyfill' 3import { I18n } from '@ngx-translate/i18n-polyfill'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
8import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
9import { User } from '../../../../../../shared' 7import { FormReactive, UserValidatorsService } from '@app/shared/forms'
8import { UserService } from '@app/shared/users'
9import { 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})
16export class UserBanModalComponent extends FormReactive implements OnInit { 16export 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 @@
1import { Component, EventEmitter, Input, OnChanges, Output, ViewChild } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { I18n } from '@ngx-translate/i18n-polyfill'
4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
5import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
6import { UserService } from '@app/shared/users'
7import { AuthService, ConfirmService } from '@app/core'
8import { User, UserRight } from '../../../../../shared/models/users'
9import { Account } from '@app/shared/account/account.model'
10import { 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})
17export 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 @@
1import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' 1import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
2import { LazyLoadEvent } from 'primeng/components/common/lazyloadevent' 2import { LazyLoadEvent } from 'primeng/components/common/lazyloadevent'
3import { SortMeta } from 'primeng/components/common/sortmeta' 3import { SortMeta } from 'primeng/components/common/sortmeta'
4
5import { RestPagination } from './rest-pagination' 4import { RestPagination } from './rest-pagination'
5import { Subject } from 'rxjs'
6import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
6 7
7export abstract class RestTable { 8export 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'
25import { VideoBlacklistService } from './video-blacklist' 25import { VideoBlacklistService } from './video-blacklist'
26import { VideoOwnershipService } from './video-ownership' 26import { VideoOwnershipService } from './video-ownership'
27import { VideoMiniatureComponent } from './video/video-miniature.component' 27import { VideoMiniatureComponent } from './video/video-miniature.component'
28import { VideoFeedComponent } from './video/video-feed.component' 28import { FeedComponent } from './video/feed.component'
29import { VideoThumbnailComponent } from './video/video-thumbnail.component' 29import { VideoThumbnailComponent } from './video/video-thumbnail.component'
30import { VideoService } from './video/video.service' 30import { VideoService } from './video/video.service'
31import { AccountService } from '@app/shared/account/account.service' 31import { AccountService } from '@app/shared/account/account.service'
@@ -56,6 +56,9 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
56import { SubscribeButtonComponent, RemoteSubscribeComponent, UserSubscriptionService } from '@app/shared/user-subscription' 56import { SubscribeButtonComponent, RemoteSubscribeComponent, UserSubscriptionService } from '@app/shared/user-subscription'
57import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component' 57import { InstanceFeaturesTableComponent } from '@app/shared/instance/instance-features-table.component'
58import { OverviewService } from '@app/shared/overview' 58import { OverviewService } from '@app/shared/overview'
59import { UserBanModalComponent } from '@app/shared/moderation'
60import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component'
61import { 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 @@
1import { Observable } from 'rxjs' 1import { from, Observable } from 'rxjs'
2import { catchError, map } from 'rxjs/operators' 2import { catchError, concatMap, map, toArray } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { UserCreate, UserUpdateMe, UserVideoQuota } from '../../../../../shared' 5import { ResultList, User, UserCreate, UserRole, UserUpdate, UserUpdateMe, UserVideoQuota } from '../../../../../shared'
6import { environment } from '../../../environments/environment' 6import { environment } from '../../../environments/environment'
7import { RestExtractor } from '../rest' 7import { RestExtractor, RestPagination, RestService } from '../rest'
8import { Avatar } from '../../../../../shared/models/avatars/avatar.model' 8import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
9import { SortMeta } from 'primeng/api'
10import { BytesPipe } from 'ngx-pipes'
11import { I18n } from '@ngx-translate/i18n-polyfill'
9 12
10@Injectable() 13@Injectable()
11export class UserService { 14export 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
15my-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'
12import { I18n } from '@ngx-translate/i18n-polyfill' 12import { I18n } from '@ngx-translate/i18n-polyfill'
13import { ScreenService } from '@app/shared/misc/screen.service' 13import { ScreenService } from '@app/shared/misc/screen.service'
14import { OwnerDisplayType } from '@app/shared/video/video-miniature.component' 14import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
15import { Syndication } from '@app/shared/video/syndication.model'
15 16
16export abstract class AbstractVideoList implements OnInit, OnDestroy { 17export 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 @@
1import { Component, Input } from '@angular/core'
2import { 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})
9export 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 @@
1import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum'
2
3export 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 @@
1import { 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})
8export 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
2import { User } from '../users' 2import { User } from '../users'
3import { Video } from './video.model' 3import { Video } from './video.model'
4import { ServerService } from '@app/core' 4import { ServerService } from '@app/core'
5import { VideoPrivacy } from '../../../../../shared'
5 6
6export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto' 7export 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
5import { ServerService } from '@app/core' 5import { ServerService } from '@app/core'
6import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model' 6import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.model'
7import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
8import { 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
8my-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 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
4import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos' 4import { VideoPrivacy, VideoUpdate } from '../../../../../../shared/models/videos'
@@ -23,7 +23,7 @@ import { VideoImportService } from '@app/shared/video-import'
23}) 23})
24export class VideoImportTorrentComponent extends VideoSend implements OnInit, CanComponentDeactivate { 24export 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'
3import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
4import { catchError, switchMap, tap } from 'rxjs/operators' 4import { catchError, switchMap, tap } from 'rxjs/operators'
5import { FormReactive } from '@app/shared' 5import { FormReactive } from '@app/shared'
6import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.service'
7import { VideoConstant, VideoPrivacy } from '../../../../../../shared' 6import { VideoConstant, VideoPrivacy } from '../../../../../../shared'
8import { AuthService, ServerService } from '@app/core' 7import { AuthService, ServerService } from '@app/core'
9import { VideoService } from '@app/shared/video/video.service' 8import { VideoService } from '@app/shared/video/video.service'
@@ -11,8 +10,9 @@ import { VideoCaptionEdit } from '@app/shared/video-caption/video-caption-edit.m
11import { VideoCaptionService } from '@app/shared/video-caption' 10import { VideoCaptionService } from '@app/shared/video-caption'
12import { VideoEdit } from '@app/shared/video/video-edit.model' 11import { VideoEdit } from '@app/shared/video/video-edit.model'
13import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils' 12import { populateAsyncUserVideoChannels } from '@app/shared/misc/utils'
13import { CanComponentDeactivateResult } from '@app/shared/guards/can-deactivate-guard.service'
14 14
15export abstract class VideoSend extends FormReactive implements OnInit, CanComponentDeactivate { 15export 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 @@
1import { HttpEventType, HttpResponse } from '@angular/common/http' 1import { HttpEventType, HttpResponse } from '@angular/common/http'
2import { Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, ElementRef, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { LoadingBarService } from '@ngx-loading-bar/core' 4import { LoadingBarService } from '@ngx-loading-bar/core'
5import { NotificationsService } from 'angular2-notifications' 5import { NotificationsService } from 'angular2-notifications'
@@ -25,7 +25,7 @@ import { VideoCaptionService } from '@app/shared/video-caption'
25}) 25})
26export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy, CanComponentDeactivate { 26export 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 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { getAbsoluteAPIUrl } from '@app/shared/misc/utils' 2import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
3import * as linkify from 'linkifyjs' 3// FIXME: use @types/linkify when https://github.com/DefinitelyTyped/DefinitelyTyped/pull/29682/files is merged?
4import * as linkifyHtml from 'linkifyjs/html' 4const linkify = require('linkifyjs')
5const linkifyHtml = require('linkifyjs/html')
5 6
6@Injectable() 7@Injectable()
7export class LinkifierService { 8export 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
26my-video-feed { 26my-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'
12import { VideoComment } from './video-comment.model' 12import { VideoComment } from './video-comment.model'
13import { VideoCommentService } from './video-comment.service' 13import { VideoCommentService } from './video-comment.service'
14import { I18n } from '@ngx-translate/i18n-polyfill' 14import { I18n } from '@ngx-translate/i18n-polyfill'
15import { 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
7import { MetaService } from '@ngx-meta/core' 7import { MetaService } from '@ngx-meta/core'
8import { NotificationsService } from 'angular2-notifications' 8import { NotificationsService } from 'angular2-notifications'
9import { forkJoin, Subscription } from 'rxjs' 9import { forkJoin, Subscription } from 'rxjs'
10import * as videojs from 'video.js' 10// FIXME: something weird with our path definition in tsconfig and typings
11// @ts-ignore
12import videojs from 'video.js'
11import 'videojs-hotkeys' 13import 'videojs-hotkeys'
12import { Hotkey, HotkeysService } from 'angular2-hotkeys' 14import { Hotkey, HotkeysService } from 'angular2-hotkeys'
13import * as WebTorrent from 'webtorrent' 15import * 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'
10import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' 10import { VideoFilter } from '../../../../../shared/models/videos/video-query.type'
11import { I18n } from '@ngx-translate/i18n-polyfill' 11import { I18n } from '@ngx-translate/i18n-polyfill'
12import { ScreenService } from '@app/shared/misc/screen.service' 12import { ScreenService } from '@app/shared/misc/screen.service'
13import { 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}