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-follows/about-follows.component.html10
-rw-r--r--client/src/app/+about/about-follows/about-follows.component.ts19
-rw-r--r--client/src/app/+about/about-instance/contact-admin-modal.component.ts4
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.html22
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.scss9
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts6
-rw-r--r--client/src/app/+accounts/accounts.component.ts6
-rw-r--r--client/src/app/+admin/admin.module.ts2
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html2
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts4
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss6
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts8
-rw-r--r--client/src/app/+admin/follows/following-list/follow-modal.component.ts4
-rw-r--r--client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts7
-rw-r--r--client/src/app/+admin/overview/comments/video-comment-list.component.ts2
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-create.component.ts4
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.component.html14
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.component.scss19
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-edit.ts36
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-password.component.html4
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-password.component.ts4
-rw-r--r--client/src/app/+admin/overview/users/user-edit/user-update.component.ts27
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.html4
-rw-r--r--client/src/app/+admin/overview/users/user-list/user-list.component.scss2
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.html7
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.ts9
-rw-r--r--client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts4
-rw-r--r--client/src/app/+admin/shared/index.ts3
-rw-r--r--client/src/app/+admin/shared/shared-admin.module.ts20
-rw-r--r--client/src/app/+admin/shared/user-real-quota-info.component.html4
-rw-r--r--client/src/app/+admin/shared/user-real-quota-info.component.scss2
-rw-r--r--client/src/app/+admin/shared/user-real-quota-info.component.ts44
-rw-r--r--client/src/app/+login/login.component.html46
-rw-r--r--client/src/app/+login/login.component.scss4
-rw-r--r--client/src/app/+login/login.component.ts41
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-create.component.ts4
-rw-r--r--client/src/app/+manage/video-channel-edit/video-channel-update.component.ts4
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts11
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts8
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts8
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts4
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.html12
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.scss2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.html12
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts49
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.html54
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.scss16
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts105
-rw-r--r--client/src/app/+my-account/my-account.module.ts8
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts2
-rw-r--r--client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts4
-rw-r--r--client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html7
-rw-r--r--client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts12
-rw-r--r--client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts4
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts4
-rw-r--r--client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts4
-rw-r--r--client/src/app/+my-library/my-videos/modals/video-change-ownership.component.ts4
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.html1
-rw-r--r--client/src/app/+my-library/my-videos/my-videos.component.ts31
-rw-r--r--client/src/app/+reset-password/reset-password.component.ts4
-rw-r--r--client/src/app/+search/search.component.ts2
-rw-r--r--client/src/app/+signup/+register/register.component.ts2
-rw-r--r--client/src/app/+signup/+register/steps/register-step-channel.component.ts4
-rw-r--r--client/src/app/+signup/+register/steps/register-step-terms.component.ts8
-rw-r--r--client/src/app/+signup/+register/steps/register-step-user.component.ts4
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts4
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts13
-rw-r--r--client/src/app/+video-studio/edit/video-studio-edit.component.ts4
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-add-modal.component.ts4
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-caption-edit-modal-content/video-caption-edit-modal-content.component.ts7
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts11
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts8
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts4
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts4
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html8
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss6
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts15
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.ts2
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts15
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts6
-rw-r--r--client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts2
-rw-r--r--client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html2
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html2
-rw-r--r--client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts39
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts89
-rw-r--r--client/src/app/app.component.ts6
-rw-r--r--client/src/app/core/auth/auth-user.model.ts24
-rw-r--r--client/src/app/core/auth/auth.service.ts29
-rw-r--r--client/src/app/core/confirm/confirm.service.ts47
-rw-r--r--client/src/app/core/menu/menu.service.ts9
-rw-r--r--client/src/app/core/plugins/plugin.service.ts9
-rw-r--r--client/src/app/core/renderer/markdown.service.ts27
-rw-r--r--client/src/app/core/rest/rest-extractor.service.ts8
-rw-r--r--client/src/app/core/routing/preload-selected-modules-list.ts4
-rw-r--r--client/src/app/core/users/user-local-storage.service.ts25
-rw-r--r--client/src/app/core/users/user.model.ts14
-rw-r--r--client/src/app/helpers/utils/url.ts5
-rw-r--r--client/src/app/menu/menu.component.html4
-rw-r--r--client/src/app/menu/menu.component.scss21
-rw-r--r--client/src/app/modal/confirm.component.html7
-rw-r--r--client/src/app/modal/confirm.component.ts30
-rw-r--r--client/src/app/shared/form-validators/user-validators.ts9
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts13
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts4
-rw-r--r--client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts4
-rw-r--r--client/src/app/shared/shared-custom-markup/custom-markup.service.ts2
-rw-r--r--client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts6
-rw-r--r--client/src/app/shared/shared-forms/form-reactive.service.ts101
-rw-r--r--client/src/app/shared/shared-forms/form-reactive.ts87
-rw-r--r--client/src/app/shared/shared-forms/form-validator.service.ts2
-rw-r--r--client/src/app/shared/shared-forms/index.ts1
-rw-r--r--client/src/app/shared/shared-forms/input-text.component.ts10
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.ts4
-rw-r--r--client/src/app/shared/shared-forms/shared-form.module.ts5
-rw-r--r--client/src/app/shared/shared-instance/instance.service.ts2
-rw-r--r--client/src/app/shared/shared-main/angular/number-formatter.pipe.ts4
-rw-r--r--client/src/app/shared/shared-main/auth/auth-interceptor.service.ts13
-rw-r--r--client/src/app/shared/shared-main/buttons/action-dropdown.component.html2
-rw-r--r--client/src/app/shared/shared-main/buttons/action-dropdown.component.ts15
-rw-r--r--client/src/app/shared/shared-main/buttons/button.component.ts5
-rw-r--r--client/src/app/shared/shared-main/misc/list-overflow.component.ts2
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts11
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts9
-rw-r--r--client/src/app/shared/shared-main/video/index.ts1
-rw-r--r--client/src/app/shared/shared-main/video/video-file-token.service.ts33
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts3
-rw-r--r--client/src/app/shared/shared-moderation/batch-domains-modal.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/account-report.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/user-ban-modal.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/video-block.component.ts4
-rw-r--r--client/src/app/shared/shared-search/find-in-bulk.service.ts15
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.ts2
-rw-r--r--client/src/app/shared/shared-user-settings/user-interface-settings.component.ts4
-rw-r--r--client/src/app/shared/shared-user-settings/user-video-settings.component.ts4
-rw-r--r--client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts4
-rw-r--r--client/src/app/shared/shared-user-subscription/subscribe-button.component.html2
-rw-r--r--client/src/app/shared/shared-users/index.ts1
-rw-r--r--client/src/app/shared/shared-users/shared-users.module.ts4
-rw-r--r--client/src/app/shared/shared-users/two-factor.service.ts52
-rw-r--r--client/src/app/shared/shared-users/user-admin.service.ts5
-rw-r--r--client/src/app/shared/shared-video-live/live-stream-information.component.html2
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.html5
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.ts21
-rw-r--r--client/src/app/shared/shared-video-miniature/video-filters-header.component.html1
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.html6
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss4
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts15
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.html1
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.ts3
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts8
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist.service.ts12
161 files changed, 1346 insertions, 530 deletions
diff --git a/client/src/app/+about/about-follows/about-follows.component.html b/client/src/app/+about/about-follows/about-follows.component.html
index f16f8bd71..6516b595d 100644
--- a/client/src/app/+about/about-follows/about-follows.component.html
+++ b/client/src/app/+about/about-follows/about-follows.component.html
@@ -2,21 +2,21 @@
2 <h1 class="visually-hidden" i18n>Follows</h1> 2 <h1 class="visually-hidden" i18n>Follows</h1>
3 3
4 <div class="col-xl-6 col-md-12"> 4 <div class="col-xl-6 col-md-12">
5 <h2 i18n class="subtitle">Follower instances ({{ followersPagination.totalItems }})</h2> 5 <h2 i18n class="subtitle">Followers of {{ instanceName }} ({{ followersPagination.totalItems }})</h2>
6 6
7 <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">This instance does not have instances followers.</div> 7 <div i18n class="no-results" *ngIf="followersPagination.totalItems === 0">{{ instanceName }} does not have followers.</div>
8 8
9 <a *ngFor="let follower of followers" [href]="buildLink(follower)" target="_blank" rel="noopener noreferrer"> 9 <a *ngFor="let follower of followers" [href]="buildLink(follower)" target="_blank" rel="noopener noreferrer">
10 {{ follower}} 10 {{ follower }}
11 </a> 11 </a>
12 12
13 <button i18n class="show-more" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button> 13 <button i18n class="show-more" *ngIf="!loadedAllFollowers && canLoadMoreFollowers()" (click)="loadAllFollowers()">Show full list</button>
14 </div> 14 </div>
15 15
16 <div class="col-xl-6 col-md-12"> 16 <div class="col-xl-6 col-md-12">
17 <h2 i18n class="subtitle">Following instances ({{ followingsPagination.totalItems }})</h2> 17 <h2 i18n class="subtitle">Subscriptions of {{ instanceName }} ({{ followingsPagination.totalItems }})</h2>
18 18
19 <div i18n class="no-results" *ngIf="followingsPagination.totalItems === 0">This instance is not following any other.</div> 19 <div i18n class="no-results" *ngIf="followingsPagination.totalItems === 0">{{ instanceName }} does not have subscriptions.</div>
20 20
21 <a *ngFor="let following of followings" [href]="buildLink(following)" target="_blank" rel="noopener noreferrer"> 21 <a *ngFor="let following of followings" [href]="buildLink(following)" target="_blank" rel="noopener noreferrer">
22 {{ following }} 22 {{ following }}
diff --git a/client/src/app/+about/about-follows/about-follows.component.ts b/client/src/app/+about/about-follows/about-follows.component.ts
index 84b47e967..35d810388 100644
--- a/client/src/app/+about/about-follows/about-follows.component.ts
+++ b/client/src/app/+about/about-follows/about-follows.component.ts
@@ -1,7 +1,8 @@
1import { SortMeta } from 'primeng/api' 1import { SortMeta } from 'primeng/api'
2import { Component, OnInit } from '@angular/core' 2import { Component, OnInit } from '@angular/core'
3import { ComponentPagination, hasMoreItems, Notifier, RestService } from '@app/core' 3import { ComponentPagination, hasMoreItems, Notifier, RestService, ServerService } from '@app/core'
4import { InstanceFollowService } from '@app/shared/shared-instance' 4import { InstanceFollowService } from '@app/shared/shared-instance'
5import { Actor } from '@shared/models/actors'
5 6
6@Component({ 7@Component({
7 selector: 'my-about-follows', 8 selector: 'my-about-follows',
@@ -10,6 +11,8 @@ import { InstanceFollowService } from '@app/shared/shared-instance'
10}) 11})
11 12
12export class AboutFollowsComponent implements OnInit { 13export class AboutFollowsComponent implements OnInit {
14 instanceName: string
15
13 followers: string[] = [] 16 followers: string[] = []
14 followings: string[] = [] 17 followings: string[] = []
15 18
@@ -34,6 +37,7 @@ export class AboutFollowsComponent implements OnInit {
34 } 37 }
35 38
36 constructor ( 39 constructor (
40 private server: ServerService,
37 private restService: RestService, 41 private restService: RestService,
38 private notifier: Notifier, 42 private notifier: Notifier,
39 private followService: InstanceFollowService 43 private followService: InstanceFollowService
@@ -43,6 +47,8 @@ export class AboutFollowsComponent implements OnInit {
43 this.loadMoreFollowers() 47 this.loadMoreFollowers()
44 48
45 this.loadMoreFollowings() 49 this.loadMoreFollowings()
50
51 this.instanceName = this.server.getHTMLConfig().instance.name
46 } 52 }
47 53
48 loadAllFollowings () { 54 loadAllFollowings () {
@@ -95,7 +101,7 @@ export class AboutFollowsComponent implements OnInit {
95 next: resultList => { 101 next: resultList => {
96 if (reset) this.followers = [] 102 if (reset) this.followers = []
97 103
98 const newFollowers = resultList.data.map(r => r.follower.host) 104 const newFollowers = resultList.data.map(r => this.formatFollow(r.follower))
99 this.followers = this.followers.concat(newFollowers) 105 this.followers = this.followers.concat(newFollowers)
100 106
101 this.followersPagination.totalItems = resultList.total 107 this.followersPagination.totalItems = resultList.total
@@ -113,7 +119,7 @@ export class AboutFollowsComponent implements OnInit {
113 next: resultList => { 119 next: resultList => {
114 if (reset) this.followings = [] 120 if (reset) this.followings = []
115 121
116 const newFollowings = resultList.data.map(r => r.following.host) 122 const newFollowings = resultList.data.map(r => this.formatFollow(r.following))
117 this.followings = this.followings.concat(newFollowings) 123 this.followings = this.followings.concat(newFollowings)
118 124
119 this.followingsPagination.totalItems = resultList.total 125 this.followingsPagination.totalItems = resultList.total
@@ -123,4 +129,11 @@ export class AboutFollowsComponent implements OnInit {
123 }) 129 })
124 } 130 }
125 131
132 private formatFollow (actor: Actor) {
133 // Instance follow, only display host
134 if (actor.name === 'peertube') return actor.host
135
136 return actor.name + '@' + actor.host
137 }
138
126} 139}
diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.ts b/client/src/app/+about/about-instance/contact-admin-modal.component.ts
index fab9cfc4b..0e2bf51e8 100644
--- a/client/src/app/+about/about-instance/contact-admin-modal.component.ts
+++ b/client/src/app/+about/about-instance/contact-admin-modal.component.ts
@@ -7,7 +7,7 @@ import {
7 FROM_NAME_VALIDATOR, 7 FROM_NAME_VALIDATOR,
8 SUBJECT_VALIDATOR 8 SUBJECT_VALIDATOR
9} from '@app/shared/form-validators/instance-validators' 9} from '@app/shared/form-validators/instance-validators'
10import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 10import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
11import { InstanceService } from '@app/shared/shared-instance' 11import { InstanceService } from '@app/shared/shared-instance'
12import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 12import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
13import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 13import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -32,7 +32,7 @@ export class ContactAdminModalComponent extends FormReactive implements OnInit {
32 private serverConfig: HTMLServerConfig 32 private serverConfig: HTMLServerConfig
33 33
34 constructor ( 34 constructor (
35 protected formValidatorService: FormValidatorService, 35 protected formReactiveService: FormReactiveService,
36 private router: Router, 36 private router: Router,
37 private modalService: NgbModal, 37 private modalService: NgbModal,
38 private instanceService: InstanceService, 38 private instanceService: InstanceService,
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
index 200d9415f..38293b070 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html
@@ -37,16 +37,18 @@
37 37
38 <a i18n class="button-show-channel peertube-button-link orange-button-inverted" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a> 38 <a i18n class="button-show-channel peertube-button-link orange-button-inverted" [routerLink]="getVideoChannelLink(videoChannel)">Show this channel</a>
39 39
40 <div class="videos"> 40 <div class="videos-overflow-workaround">
41 <div class="no-results" i18n *ngIf="getTotalVideosOf(videoChannel) === 0">This channel doesn't have any videos.</div> 41 <div class="videos">
42 42 <div class="no-results" i18n *ngIf="getTotalVideosOf(videoChannel) === 0">This channel doesn't have any videos.</div>
43 <my-video-miniature 43
44 *ngFor="let video of getVideosOf(videoChannel)" 44 <my-video-miniature
45 [video]="video" [user]="userMiniature" [displayVideoActions]="true" [displayOptions]="miniatureDisplayOptions" 45 *ngFor="let video of getVideosOf(videoChannel)"
46 ></my-video-miniature> 46 [video]="video" [user]="userMiniature" [displayVideoActions]="true" [displayOptions]="miniatureDisplayOptions"
47 47 ></my-video-miniature>
48 <div *ngIf="getTotalVideosOf(videoChannel)" class="miniature-show-channel"> 48
49 <a i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a> 49 <div *ngIf="getTotalVideosOf(videoChannel)" class="miniature-show-channel">
50 <a i18n [routerLink]="getVideoChannelLink(videoChannel)">SHOW THIS CHANNEL ></a>
51 </div>
50 </div> 52 </div>
51 </div> 53 </div>
52 </div> 54 </div>
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
index 832ddf973..11ed4c3b1 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
@@ -77,10 +77,8 @@ my-subscribe-button {
77 display: flex; 77 display: flex;
78 grid-column: 1 / 3; 78 grid-column: 1 / 3;
79 grid-row: 2; 79 grid-row: 2;
80 margin-top: 30px;
81 80
82 position: relative; 81 position: relative;
83 overflow: hidden;
84 82
85 my-video-miniature { 83 my-video-miniature {
86 @include margin-right(15px); 84 @include margin-right(15px);
@@ -94,6 +92,11 @@ my-subscribe-button {
94 } 92 }
95} 93}
96 94
95.videos-overflow-workaround {
96 margin-top: 30px;
97 overflow-x: hidden;
98}
99
97.miniature-show-channel { 100.miniature-show-channel {
98 height: 100%; 101 height: 100%;
99 position: absolute; 102 position: absolute;
@@ -112,7 +115,7 @@ my-subscribe-button {
112 display: none; 115 display: none;
113} 116}
114 117
115@media screen and (max-width: $mobile-view) { 118@include on-small-main-col {
116 .channel { 119 .channel {
117 padding: 15px; 120 padding: 15px;
118 } 121 }
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
index 457a432fe..59814a93d 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
@@ -105,7 +105,11 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
105 }) 105 })
106 ) 106 )
107 .subscribe(async ({ videoChannel, videos, total }) => { 107 .subscribe(async ({ videoChannel, videos, total }) => {
108 this.channelsDescriptionHTML[videoChannel.id] = await this.markdown.textMarkdownToHTML(videoChannel.description) 108 this.channelsDescriptionHTML[videoChannel.id] = await this.markdown.textMarkdownToHTML({
109 markdown: videoChannel.description,
110 withEmoji: true,
111 withHtml: true
112 })
109 113
110 this.videoChannels.push(videoChannel) 114 this.videoChannels.push(videoChannel)
111 115
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index cf66b817a..0033fbf59 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -142,7 +142,11 @@ export class AccountsComponent implements OnInit, OnDestroy {
142 } 142 }
143 143
144 private async onAccount (account: Account) { 144 private async onAccount (account: Account) {
145 this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML(account.description) 145 this.accountDescriptionHTML = await this.markdown.textMarkdownToHTML({
146 markdown: account.description,
147 withEmoji: true,
148 withHtml: true
149 })
146 150
147 // After the markdown renderer to avoid layout changes 151 // After the markdown renderer to avoid layout changes
148 this.account = account 152 this.account = account
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 366e29883..f01967ea6 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -49,6 +49,7 @@ import {
49 PluginSearchComponent, 49 PluginSearchComponent,
50 PluginShowInstalledComponent 50 PluginShowInstalledComponent
51} from './plugins' 51} from './plugins'
52import { SharedAdminModule } from './shared'
52import { JobService, LogsComponent, LogsService } from './system' 53import { JobService, LogsComponent, LogsService } from './system'
53import { DebugComponent, DebugService } from './system/debug' 54import { DebugComponent, DebugService } from './system/debug'
54import { JobsComponent } from './system/jobs/jobs.component' 55import { JobsComponent } from './system/jobs/jobs.component'
@@ -69,6 +70,7 @@ import { JobsComponent } from './system/jobs/jobs.component'
69 SharedVideoMiniatureModule, 70 SharedVideoMiniatureModule,
70 SharedTablesModule, 71 SharedTablesModule,
71 SharedUsersModule, 72 SharedUsersModule,
73 SharedAdminModule,
72 74
73 TableModule, 75 TableModule,
74 ChartModule 76 ChartModule
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
index 929ea3a90..43f1438e0 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
@@ -218,6 +218,8 @@
218 [clearable]="false" 218 [clearable]="false"
219 ></my-select-custom-value> 219 ></my-select-custom-value>
220 220
221 <my-user-real-quota-info [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
222
221 <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div> 223 <div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
222 </div> 224 </div>
223 225
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
index 90ed58c99..2122e67b2 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
@@ -60,6 +60,10 @@ export class EditBasicConfigurationComponent implements OnInit, OnChanges {
60 return !!enabled.find((e: string) => e === algorithm) 60 return !!enabled.find((e: string) => e === algorithm)
61 } 61 }
62 62
63 getUserVideoQuota () {
64 return this.form.value['user']['videoQuota']
65 }
66
63 isSignupEnabled () { 67 isSignupEnabled () {
64 return this.form.value['signup']['enabled'] === true 68 return this.form.value['signup']['enabled'] === true
65 } 69 }
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
index dda5d0b5e..764e626ec 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.scss
@@ -150,3 +150,9 @@ ngb-tabset:not(.previews) ::ng-deep {
150 padding: 0 .3em; 150 padding: 0 .3em;
151 } 151 }
152} 152}
153
154my-user-real-quota-info {
155 display: block;
156 margin-top: 5px;
157 font-size: 11px;
158}
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 545e37857..168f4702c 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
@@ -18,15 +18,15 @@ import {
18 MAX_INSTANCE_LIVES_VALIDATOR, 18 MAX_INSTANCE_LIVES_VALIDATOR,
19 MAX_LIVE_DURATION_VALIDATOR, 19 MAX_LIVE_DURATION_VALIDATOR,
20 MAX_USER_LIVES_VALIDATOR, 20 MAX_USER_LIVES_VALIDATOR,
21 MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR,
21 SEARCH_INDEX_URL_VALIDATOR, 22 SEARCH_INDEX_URL_VALIDATOR,
22 SERVICES_TWITTER_USERNAME_VALIDATOR, 23 SERVICES_TWITTER_USERNAME_VALIDATOR,
23 SIGNUP_LIMIT_VALIDATOR, 24 SIGNUP_LIMIT_VALIDATOR,
24 SIGNUP_MINIMUM_AGE_VALIDATOR, 25 SIGNUP_MINIMUM_AGE_VALIDATOR,
25 TRANSCODING_THREADS_VALIDATOR, 26 TRANSCODING_THREADS_VALIDATOR
26 MAX_VIDEO_CHANNELS_PER_USER_VALIDATOR
27} from '@app/shared/form-validators/custom-config-validators' 27} from '@app/shared/form-validators/custom-config-validators'
28import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' 28import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
29import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 29import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
30import { CustomPageService } from '@app/shared/shared-main/custom-page' 30import { CustomPageService } from '@app/shared/shared-main/custom-page'
31import { CustomConfig, CustomPage, HTMLServerConfig } from '@shared/models' 31import { CustomConfig, CustomPage, HTMLServerConfig } from '@shared/models'
32import { EditConfigurationService } from './edit-configuration.service' 32import { EditConfigurationService } from './edit-configuration.service'
@@ -52,9 +52,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
52 categoryItems: SelectOptionsItem[] = [] 52 categoryItems: SelectOptionsItem[] = []
53 53
54 constructor ( 54 constructor (
55 protected formReactiveService: FormReactiveService,
55 private router: Router, 56 private router: Router,
56 private route: ActivatedRoute, 57 private route: ActivatedRoute,
57 protected formValidatorService: FormValidatorService,
58 private notifier: Notifier, 58 private notifier: Notifier,
59 private configService: ConfigService, 59 private configService: ConfigService,
60 private customPage: CustomPageService, 60 private customPage: CustomPageService,
diff --git a/client/src/app/+admin/follows/following-list/follow-modal.component.ts b/client/src/app/+admin/follows/following-list/follow-modal.component.ts
index 07cc75d77..8f74e82a6 100644
--- a/client/src/app/+admin/follows/following-list/follow-modal.component.ts
+++ b/client/src/app/+admin/follows/following-list/follow-modal.component.ts
@@ -2,7 +2,7 @@ import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/cor
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { prepareIcu } from '@app/helpers' 3import { prepareIcu } from '@app/helpers'
4import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' 4import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
6import { InstanceFollowService } from '@app/shared/shared-instance' 6import { InstanceFollowService } from '@app/shared/shared-instance'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -22,7 +22,7 @@ export class FollowModalComponent extends FormReactive implements OnInit {
22 private openedModal: NgbModalRef 22 private openedModal: NgbModalRef
23 23
24 constructor ( 24 constructor (
25 protected formValidatorService: FormValidatorService, 25 protected formReactiveService: FormReactiveService,
26 private modalService: NgbModal, 26 private modalService: NgbModal,
27 private followService: InstanceFollowService, 27 private followService: InstanceFollowService,
28 private notifier: Notifier 28 private notifier: Notifier
diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
index 8d67e9beb..efd99e52b 100644
--- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
+++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
@@ -98,7 +98,10 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
98 98
99 this.videoService.removeVideo(videoBlock.video.id) 99 this.videoService.removeVideo(videoBlock.video.id)
100 .subscribe({ 100 .subscribe({
101 next: () => this.notifier.success($localize`Video deleted.`), 101 next: () => {
102 this.notifier.success($localize`Video deleted.`)
103 this.reloadData()
104 },
102 105
103 error: err => this.notifier.error(err.message) 106 error: err => this.notifier.error(err.message)
104 }) 107 })
@@ -124,7 +127,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit {
124 } 127 }
125 128
126 toHtml (text: string) { 129 toHtml (text: string) {
127 return this.markdownRenderer.textMarkdownToHTML(text) 130 return this.markdownRenderer.textMarkdownToHTML({ markdown: text })
128 } 131 }
129 132
130 async unblockVideo (entry: VideoBlacklist) { 133 async unblockVideo (entry: VideoBlacklist) {
diff --git a/client/src/app/+admin/overview/comments/video-comment-list.component.ts b/client/src/app/+admin/overview/comments/video-comment-list.component.ts
index cfe40b92a..c95d2ffeb 100644
--- a/client/src/app/+admin/overview/comments/video-comment-list.component.ts
+++ b/client/src/app/+admin/overview/comments/video-comment-list.component.ts
@@ -115,7 +115,7 @@ export class VideoCommentListComponent extends RestTable implements OnInit {
115 } 115 }
116 116
117 toHtml (text: string) { 117 toHtml (text: string) {
118 return this.markdownRenderer.textMarkdownToHTML(text, true, true) 118 return this.markdownRenderer.textMarkdownToHTML({ markdown: text, withHtml: true, withEmoji: true })
119 } 119 }
120 120
121 isInSelectionMode () { 121 isInSelectionMode () {
diff --git a/client/src/app/+admin/overview/users/user-edit/user-create.component.ts b/client/src/app/+admin/overview/users/user-edit/user-create.component.ts
index 1713e06ce..0627aa887 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-create.component.ts
+++ b/client/src/app/+admin/overview/users/user-edit/user-create.component.ts
@@ -12,7 +12,7 @@ import {
12 USER_VIDEO_QUOTA_DAILY_VALIDATOR, 12 USER_VIDEO_QUOTA_DAILY_VALIDATOR,
13 USER_VIDEO_QUOTA_VALIDATOR 13 USER_VIDEO_QUOTA_VALIDATOR
14} from '@app/shared/form-validators/user-validators' 14} from '@app/shared/form-validators/user-validators'
15import { FormValidatorService } from '@app/shared/shared-forms' 15import { FormReactiveService } from '@app/shared/shared-forms'
16import { UserAdminService } from '@app/shared/shared-users' 16import { UserAdminService } from '@app/shared/shared-users'
17import { UserCreate, UserRole } from '@shared/models' 17import { UserCreate, UserRole } from '@shared/models'
18import { UserEdit } from './user-edit' 18import { UserEdit } from './user-edit'
@@ -27,7 +27,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
27 27
28 constructor ( 28 constructor (
29 protected serverService: ServerService, 29 protected serverService: ServerService,
30 protected formValidatorService: FormValidatorService, 30 protected formReactiveService: FormReactiveService,
31 protected configService: ConfigService, 31 protected configService: ConfigService,
32 protected screenService: ScreenService, 32 protected screenService: ScreenService,
33 protected auth: AuthService, 33 protected auth: AuthService,
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
index e484ab8b0..e51ccf808 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html
@@ -152,10 +152,7 @@
152 [clearable]="false" 152 [clearable]="false"
153 ></my-select-custom-value> 153 ></my-select-custom-value>
154 154
155 <div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()"> 155 <my-user-real-quota-info [videoQuota]="getUserVideoQuota()"></my-user-real-quota-info>
156 Transcoding is enabled. The video quota only takes into account <strong>original</strong> video size. <br />
157 At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
158 </div>
159 156
160 <div *ngIf="formErrors.videoQuota" class="form-error"> 157 <div *ngIf="formErrors.videoQuota" class="form-error">
161 {{ formErrors.videoQuota }} 158 {{ formErrors.videoQuota }}
@@ -207,7 +204,7 @@
207</div> 204</div>
208 205
209 206
210<div *ngIf="!isCreation() && user && user.pluginAuth === null" class="row mt-4"> <!-- danger zone grid --> 207<div *ngIf="displayDangerZone()" class="row mt-4"> <!-- danger zone grid -->
211 <div class="col-12 col-lg-4 col-xl-3"> 208 <div class="col-12 col-lg-4 col-xl-3">
212 <div class="anchor" id="danger"></div> <!-- danger zone anchor --> 209 <div class="anchor" id="danger"></div> <!-- danger zone anchor -->
213 <div i18n class="account-title account-title-danger">DANGER ZONE</div> 210 <div i18n class="account-title account-title-danger">DANGER ZONE</div>
@@ -216,7 +213,7 @@
216 <div class="col-12 col-lg-8 col-xl-9"> 213 <div class="col-12 col-lg-8 col-xl-9">
217 214
218 <div class="danger-zone"> 215 <div class="danger-zone">
219 <div class="form-group reset-password-email"> 216 <div class="form-group">
220 <label i18n>Send a link to reset the password by email to the user</label> 217 <label i18n>Send a link to reset the password by email to the user</label>
221 <button (click)="resetPassword()" i18n>Ask for new password</button> 218 <button (click)="resetPassword()" i18n>Ask for new password</button>
222 </div> 219 </div>
@@ -225,6 +222,11 @@
225 <label i18n>Manually set the user password</label> 222 <label i18n>Manually set the user password</label>
226 <my-user-password [userId]="user.id"></my-user-password> 223 <my-user-password [userId]="user.id"></my-user-password>
227 </div> 224 </div>
225
226 <div *ngIf="user.twoFactorEnabled" class="form-group">
227 <label i18n>This user has two factor authentication enabled</label>
228 <button (click)="disableTwoFactorAuth()" i18n>Disable two factor authentication</button>
229 </div>
228 </div> 230 </div>
229 231
230 </div> 232 </div>
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
index 254286ae3..698628149 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss
@@ -41,23 +41,20 @@ button {
41 margin-top: 10px; 41 margin-top: 10px;
42} 42}
43 43
44.transcoding-information { 44my-user-real-quota-info {
45 display: block;
45 margin-top: 5px; 46 margin-top: 5px;
46 font-size: 11px; 47 font-size: 11px;
47} 48}
48 49
49.danger-zone { 50.danger-zone {
50 .reset-password-email { 51 button {
51 margin-bottom: 30px; 52 @include peertube-button;
52 53 @include danger-button;
53 button { 54 @include disable-outline;
54 @include peertube-button;
55 @include danger-button;
56 @include disable-outline;
57 55
58 display: block; 56 display: block;
59 margin-top: 0; 57 margin-top: 0;
60 }
61 } 58 }
62} 59}
63 60
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.ts b/client/src/app/+admin/overview/users/user-edit/user-edit.ts
index 395d07423..1edca7fbf 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/overview/users/user-edit/user-edit.ts
@@ -3,7 +3,7 @@ import { ConfigService } from '@app/+admin/config/shared/config.service'
3import { AuthService, ScreenService, ServerService, User } from '@app/core' 3import { AuthService, ScreenService, ServerService, User } from '@app/core'
4import { FormReactive } from '@app/shared/shared-forms' 4import { FormReactive } from '@app/shared/shared-forms'
5import { USER_ROLE_LABELS } from '@shared/core-utils/users' 5import { USER_ROLE_LABELS } from '@shared/core-utils/users'
6import { HTMLServerConfig, UserAdminFlag, UserRole, VideoResolution } from '@shared/models' 6import { HTMLServerConfig, UserAdminFlag, UserRole } from '@shared/models'
7import { SelectOptionsItem } from '../../../../../types/select-options-item.model' 7import { SelectOptionsItem } from '../../../../../types/select-options-item.model'
8 8
9@Directive() 9@Directive()
@@ -49,7 +49,7 @@ export abstract class UserEdit extends FormReactive implements OnInit {
49 buildRoles () { 49 buildRoles () {
50 const authUser = this.auth.getUser() 50 const authUser = this.auth.getUser()
51 51
52 if (authUser.role === UserRole.ADMINISTRATOR) { 52 if (authUser.role.id === UserRole.ADMINISTRATOR) {
53 this.roles = Object.keys(USER_ROLE_LABELS) 53 this.roles = Object.keys(USER_ROLE_LABELS)
54 .map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) 54 .map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
55 return 55 return
@@ -60,33 +60,27 @@ export abstract class UserEdit extends FormReactive implements OnInit {
60 ] 60 ]
61 } 61 }
62 62
63 isTranscodingInformationDisplayed () { 63 displayDangerZone () {
64 const formVideoQuota = parseInt(this.form.value['videoQuota'], 10) 64 if (this.isCreation()) return false
65 if (!this.user) return false
66 if (this.user.pluginAuth) return false
67 if (this.auth.getUser().id === this.user.id) return false
65 68
66 return this.serverConfig.transcoding.enabledResolutions.length !== 0 && 69 return true
67 formVideoQuota > 0
68 } 70 }
69 71
70 computeQuotaWithTranscoding () { 72 resetPassword () {
71 const transcodingConfig = this.serverConfig.transcoding 73 return
72
73 const resolutions = transcodingConfig.enabledResolutions
74 const higherResolution = VideoResolution.H_4K
75 let multiplier = 0
76
77 for (const resolution of resolutions) {
78 multiplier += resolution / higherResolution
79 }
80
81 if (transcodingConfig.hls.enabled) multiplier *= 2
82
83 return multiplier * parseInt(this.form.value['videoQuota'], 10)
84 } 74 }
85 75
86 resetPassword () { 76 disableTwoFactorAuth () {
87 return 77 return
88 } 78 }
89 79
80 getUserVideoQuota () {
81 return this.form.value['videoQuota']
82 }
83
90 protected buildAdminFlags (formValue: any) { 84 protected buildAdminFlags (formValue: any) {
91 return formValue.byPassAutoBlock ? UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST : UserAdminFlag.NONE 85 return formValue.byPassAutoBlock ? UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST : UserAdminFlag.NONE
92 } 86 }
diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.html b/client/src/app/+admin/overview/users/user-edit/user-password.component.html
index 13f57024b..173825957 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-password.component.html
+++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.html
@@ -1,8 +1,10 @@
1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
2 2
3 <div class="input-group"> 3 <div class="input-group">
4 <input id="password" [attr.type]="showPassword ? 'text' : 'password'" class="form-control" 4 <input id="password"
5 [attr.type]="showPassword ? 'text' : 'password'" class="form-control"
5 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" 6 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
7 autocomplete="new-password"
6 > 8 >
7 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button"> 9 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
8 <ng-container *ngIf="!showPassword" i18n>Show</ng-container> 10 <ng-container *ngIf="!showPassword" i18n>Show</ng-container>
diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.ts b/client/src/app/+admin/overview/users/user-edit/user-password.component.ts
index 8999d1f00..d6616e077 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-password.component.ts
+++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.ts
@@ -1,7 +1,7 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' 3import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { UserAdminService } from '@app/shared/shared-users' 5import { UserAdminService } from '@app/shared/shared-users'
6import { UserUpdate } from '@shared/models' 6import { UserUpdate } from '@shared/models'
7 7
@@ -18,7 +18,7 @@ export class UserPasswordComponent extends FormReactive implements OnInit {
18 @Input() userId: number 18 @Input() userId: number
19 19
20 constructor ( 20 constructor (
21 protected formValidatorService: FormValidatorService, 21 protected formReactiveService: FormReactiveService,
22 private notifier: Notifier, 22 private notifier: Notifier,
23 private userAdminService: UserAdminService 23 private userAdminService: UserAdminService
24 ) { 24 ) {
diff --git a/client/src/app/+admin/overview/users/user-edit/user-update.component.ts b/client/src/app/+admin/overview/users/user-edit/user-update.component.ts
index bab288a67..25d02f000 100644
--- a/client/src/app/+admin/overview/users/user-edit/user-update.component.ts
+++ b/client/src/app/+admin/overview/users/user-edit/user-update.component.ts
@@ -9,8 +9,8 @@ import {
9 USER_VIDEO_QUOTA_DAILY_VALIDATOR, 9 USER_VIDEO_QUOTA_DAILY_VALIDATOR,
10 USER_VIDEO_QUOTA_VALIDATOR 10 USER_VIDEO_QUOTA_VALIDATOR
11} from '@app/shared/form-validators/user-validators' 11} from '@app/shared/form-validators/user-validators'
12import { FormValidatorService } from '@app/shared/shared-forms' 12import { FormReactiveService } from '@app/shared/shared-forms'
13import { UserAdminService } from '@app/shared/shared-users' 13import { TwoFactorService, UserAdminService } from '@app/shared/shared-users'
14import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' 14import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models'
15import { UserEdit } from './user-edit' 15import { UserEdit } from './user-edit'
16 16
@@ -25,7 +25,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
25 private paramsSub: Subscription 25 private paramsSub: Subscription
26 26
27 constructor ( 27 constructor (
28 protected formValidatorService: FormValidatorService, 28 protected formReactiveService: FormReactiveService,
29 protected serverService: ServerService, 29 protected serverService: ServerService,
30 protected configService: ConfigService, 30 protected configService: ConfigService,
31 protected screenService: ScreenService, 31 protected screenService: ScreenService,
@@ -34,6 +34,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
34 private router: Router, 34 private router: Router,
35 private notifier: Notifier, 35 private notifier: Notifier,
36 private userService: UserService, 36 private userService: UserService,
37 private twoFactorService: TwoFactorService,
37 private userAdminService: UserAdminService 38 private userAdminService: UserAdminService
38 ) { 39 ) {
39 super() 40 super()
@@ -120,10 +121,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
120 this.notifier.success($localize`An email asking for password reset has been sent to ${this.user.username}.`) 121 this.notifier.success($localize`An email asking for password reset has been sent to ${this.user.username}.`)
121 }, 122 },
122 123
123 error: err => { 124 error: err => this.notifier.error(err.message)
124 this.error = err.message 125 })
125 } 126 }
127
128 disableTwoFactorAuth () {
129 this.twoFactorService.disableTwoFactor({ userId: this.user.id })
130 .subscribe({
131 next: () => {
132 this.user.twoFactorEnabled = false
133
134 this.notifier.success($localize`Two factor authentication of ${this.user.username} disabled.`)
135 },
136
137 error: err => this.notifier.error(err.message)
126 }) 138 })
139
127 } 140 }
128 141
129 private onUserFetched (userJson: UserType) { 142 private onUserFetched (userJson: UserType) {
@@ -131,7 +144,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
131 144
132 this.form.patchValue({ 145 this.form.patchValue({
133 email: userJson.email, 146 email: userJson.email,
134 role: userJson.role.toString(), 147 role: userJson.role.id.toString(),
135 videoQuota: userJson.videoQuota, 148 videoQuota: userJson.videoQuota,
136 videoQuotaDaily: userJson.videoQuotaDaily, 149 videoQuotaDaily: userJson.videoQuotaDaily,
137 pluginAuth: userJson.pluginAuth, 150 pluginAuth: userJson.pluginAuth,
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.html b/client/src/app/+admin/overview/users/user-list/user-list.component.html
index c7af7dfae..a96ce561c 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.html
@@ -106,8 +106,8 @@
106 </td> 106 </td>
107 107
108 <td *ngIf="isSelected('role')"> 108 <td *ngIf="isSelected('role')">
109 <span *ngIf="user.blocked" class="pt-badge badge-banned" i18n-title title="The user was banned">{{ user.roleLabel }}</span> 109 <span *ngIf="user.blocked" class="pt-badge badge-banned" i18n-title title="The user was banned">{{ user.role.label }}</span>
110 <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role)">{{ user.roleLabel }}</span> 110 <span *ngIf="!user.blocked" class="pt-badge" [ngClass]="getRoleClass(user.role.id)">{{ user.role.label }}</span>
111 </td> 111 </td>
112 112
113 <td *ngIf="isSelected('email')" [title]="user.email"> 113 <td *ngIf="isSelected('email')" [title]="user.email">
diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.scss b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
index 3c775cac5..23e0d29ee 100644
--- a/client/src/app/+admin/overview/users/user-list/user-list.component.scss
+++ b/client/src/app/+admin/overview/users/user-list/user-list.component.scss
@@ -1,6 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3@use '~bootstrap/scss/functions' as *; 3@use 'bootstrap/scss/functions' as *;
4 4
5.add-button { 5.add-button {
6 @include create-button; 6 @include create-button;
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html
index 14bbb55e9..a6cd2e257 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.html
+++ b/client/src/app/+admin/overview/videos/video-list.component.html
@@ -85,7 +85,8 @@
85 <td> 85 <td>
86 <span *ngIf="isHLS(video)" class="pt-badge badge-blue">HLS</span> 86 <span *ngIf="isHLS(video)" class="pt-badge badge-blue">HLS</span>
87 <span *ngIf="isWebTorrent(video)" class="pt-badge badge-blue">WebTorrent ({{ video.files.length }})</span> 87 <span *ngIf="isWebTorrent(video)" class="pt-badge badge-blue">WebTorrent ({{ video.files.length }})</span>
88 <span *ngIf="video.isLive" class="pt-badge badge-blue">Live</span> 88 <span i18n *ngIf="video.isLive" class="pt-badge badge-blue">Live</span>
89 <span i18n *ngIf="hasObjectStorage(video)" class="pt-badge badge-purple">Object storage</span>
89 90
90 <span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span> 91 <span *ngIf="!isImport(video) && !video.isLive && video.isLocal">{{ getFilesSize(video) | bytes: 1 }}</span>
91 </td> 92 </td>
@@ -106,7 +107,7 @@
106 107
107 <ul> 108 <ul>
108 <li *ngFor="let file of video.files"> 109 <li *ngFor="let file of video.files">
109 {{ file.resolution.label }}: {{ file.size | bytes: 1 }} 110 <a target="_blank" rel="noopener noreferrer" [href]="file.fileUrl">{{ file.resolution.label }}</a>: {{ file.size | bytes: 1 }}
110 111
111 <my-global-icon 112 <my-global-icon
112 *ngIf="canRemoveOneFile(video)" 113 *ngIf="canRemoveOneFile(video)"
@@ -122,7 +123,7 @@
122 123
123 <ul> 124 <ul>
124 <li *ngFor="let file of video.streamingPlaylists[0].files"> 125 <li *ngFor="let file of video.streamingPlaylists[0].files">
125 {{ file.resolution.label }}: {{ file.size | bytes: 1 }} 126 <a target="_blank" rel="noopener noreferrer" [href]="file.fileUrl">{{ file.resolution.label }}</a>: {{ file.size | bytes: 1 }}
126 127
127 <my-global-icon 128 <my-global-icon
128 *ngIf="canRemoveOneFile(video)" 129 *ngIf="canRemoveOneFile(video)"
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts
index cb693ce12..4d3e9873c 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.ts
+++ b/client/src/app/+admin/overview/videos/video-list.component.ts
@@ -8,6 +8,7 @@ import { AdvancedInputFilter } from '@app/shared/shared-forms'
8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 8import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
9import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' 9import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation'
10import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature' 10import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature'
11import { getAllFiles } from '@shared/core-utils'
11import { UserRight, VideoFile, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models' 12import { UserRight, VideoFile, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
12import { VideoAdminService } from './video-admin.service' 13import { VideoAdminService } from './video-admin.service'
13 14
@@ -166,6 +167,14 @@ export class VideoListComponent extends RestTable implements OnInit {
166 return video.files.length !== 0 167 return video.files.length !== 0
167 } 168 }
168 169
170 hasObjectStorage (video: Video) {
171 if (!video.isLocal) return false
172
173 const files = getAllFiles(video)
174
175 return files.some(f => !f.fileUrl.startsWith(window.location.origin))
176 }
177
169 canRemoveOneFile (video: Video) { 178 canRemoveOneFile (video: Video) {
170 return video.canRemoveOneFile(this.authUser) 179 return video.canRemoveOneFile(this.authUser)
171 } 180 }
diff --git a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
index ec02cfcd9..b1a41567e 100644
--- a/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
+++ b/client/src/app/+admin/plugins/plugin-show-installed/plugin-show-installed.component.ts
@@ -4,7 +4,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
4import { ActivatedRoute } from '@angular/router' 4import { ActivatedRoute } from '@angular/router'
5import { HooksService, Notifier, PluginService } from '@app/core' 5import { HooksService, Notifier, PluginService } from '@app/core'
6import { BuildFormArgument } from '@app/shared/form-validators' 6import { BuildFormArgument } from '@app/shared/form-validators'
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 7import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
8import { PeerTubePlugin, RegisterServerSettingOptions } from '@shared/models' 8import { PeerTubePlugin, RegisterServerSettingOptions } from '@shared/models'
9import { PluginApiService } from '../shared/plugin-api.service' 9import { PluginApiService } from '../shared/plugin-api.service'
10 10
@@ -22,7 +22,7 @@ export class PluginShowInstalledComponent extends FormReactive implements OnInit
22 private npmName: string 22 private npmName: string
23 23
24 constructor ( 24 constructor (
25 protected formValidatorService: FormValidatorService, 25 protected formReactiveService: FormReactiveService,
26 private pluginService: PluginService, 26 private pluginService: PluginService,
27 private pluginAPIService: PluginApiService, 27 private pluginAPIService: PluginApiService,
28 private notifier: Notifier, 28 private notifier: Notifier,
diff --git a/client/src/app/+admin/shared/index.ts b/client/src/app/+admin/shared/index.ts
new file mode 100644
index 000000000..9e3834aae
--- /dev/null
+++ b/client/src/app/+admin/shared/index.ts
@@ -0,0 +1,3 @@
1export * from './user-real-quota-info.component'
2
3export * from './shared-admin.module'
diff --git a/client/src/app/+admin/shared/shared-admin.module.ts b/client/src/app/+admin/shared/shared-admin.module.ts
new file mode 100644
index 000000000..bef7d54ef
--- /dev/null
+++ b/client/src/app/+admin/shared/shared-admin.module.ts
@@ -0,0 +1,20 @@
1import { NgModule } from '@angular/core'
2import { SharedMainModule } from '../../shared/shared-main/shared-main.module'
3import { UserRealQuotaInfoComponent } from './user-real-quota-info.component'
4
5@NgModule({
6 imports: [
7 SharedMainModule
8 ],
9
10 declarations: [
11 UserRealQuotaInfoComponent
12 ],
13
14 exports: [
15 UserRealQuotaInfoComponent
16 ],
17
18 providers: []
19})
20export class SharedAdminModule { }
diff --git a/client/src/app/+admin/shared/user-real-quota-info.component.html b/client/src/app/+admin/shared/user-real-quota-info.component.html
new file mode 100644
index 000000000..b975ab17f
--- /dev/null
+++ b/client/src/app/+admin/shared/user-real-quota-info.component.html
@@ -0,0 +1,4 @@
1<div i18n class="transcoding-information" *ngIf="isTranscodingInformationDisplayed()">
2 The video quota only takes into account <strong>original</strong> video size. <br />
3 Since transcoding is enabled, videos size can be at most ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}.
4</div>
diff --git a/client/src/app/+admin/shared/user-real-quota-info.component.scss b/client/src/app/+admin/shared/user-real-quota-info.component.scss
new file mode 100644
index 000000000..40083bed3
--- /dev/null
+++ b/client/src/app/+admin/shared/user-real-quota-info.component.scss
@@ -0,0 +1,2 @@
1@use '_variables' as *;
2@use '_mixins' as *;
diff --git a/client/src/app/+admin/shared/user-real-quota-info.component.ts b/client/src/app/+admin/shared/user-real-quota-info.component.ts
new file mode 100644
index 000000000..069eeba12
--- /dev/null
+++ b/client/src/app/+admin/shared/user-real-quota-info.component.ts
@@ -0,0 +1,44 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { ServerService } from '@app/core'
3import { HTMLServerConfig, VideoResolution } from '@shared/models/index'
4
5@Component({
6 selector: 'my-user-real-quota-info',
7 templateUrl: './user-real-quota-info.component.html',
8 styleUrls: [ './user-real-quota-info.component.scss' ]
9})
10export class UserRealQuotaInfoComponent implements OnInit {
11 @Input() videoQuota: number | string
12
13 private serverConfig: HTMLServerConfig
14
15 constructor (private server: ServerService) { }
16
17 ngOnInit () {
18 this.serverConfig = this.server.getHTMLConfig()
19 }
20
21 isTranscodingInformationDisplayed () {
22 return this.serverConfig.transcoding.enabledResolutions.length !== 0 && this.getQuotaAsNumber() > 0
23 }
24
25 computeQuotaWithTranscoding () {
26 const transcodingConfig = this.serverConfig.transcoding
27
28 const resolutions = transcodingConfig.enabledResolutions
29 const higherResolution = VideoResolution.H_4K
30 let multiplier = 0
31
32 for (const resolution of resolutions) {
33 multiplier += resolution / higherResolution
34 }
35
36 if (transcodingConfig.hls.enabled) multiplier *= 2
37
38 return multiplier * this.getQuotaAsNumber()
39 }
40
41 private getQuotaAsNumber () {
42 return parseInt(this.videoQuota + '', 10)
43 }
44}
diff --git a/client/src/app/+login/login.component.html b/client/src/app/+login/login.component.html
index f3a2476f9..49b443a20 100644
--- a/client/src/app/+login/login.component.html
+++ b/client/src/app/+login/login.component.html
@@ -39,34 +39,48 @@
39 <div class="login-form-and-externals"> 39 <div class="login-form-and-externals">
40 40
41 <form myPluginSelector pluginSelectorId="login-form" role="form" (ngSubmit)="login()" [formGroup]="form"> 41 <form myPluginSelector pluginSelectorId="login-form" role="form" (ngSubmit)="login()" [formGroup]="form">
42 <div class="form-group"> 42 <ng-container *ngIf="!otpStep">
43 <div> 43 <div class="form-group">
44 <label i18n for="username">Username or email address</label> 44 <div>
45 <input 45 <label i18n for="username">Username or email address</label>
46 type="text" id="username" i18n-placeholder placeholder="Example: john@example.com" required tabindex="1" 46 <input
47 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" myAutofocus 47 type="text" id="username" i18n-placeholder placeholder="Example: john@example.com" required tabindex="1"
48 > 48 formControlName="username" class="form-control" [ngClass]="{ 'input-error': formErrors['username'] }" myAutofocus
49 >
50 </div>
51
52 <div *ngIf="formErrors.username" class="form-error">{{ formErrors.username }}</div>
53
54 <div *ngIf="hasUsernameUppercase()" i18n class="form-warning">
55 ⚠️ Most email addresses do not include capital letters.
56 </div>
49 </div> 57 </div>
50 58
51 <div *ngIf="formErrors.username" class="form-error">{{ formErrors.username }}</div> 59 <div class="form-group">
60 <label i18n for="password">Password</label>
52 61
53 <div *ngIf="hasUsernameUppercase()" i18n class="form-warning"> 62 <my-input-text
54 ⚠️ Most email addresses do not include capital letters. 63 formControlName="password" inputId="password" i18n-placeholder placeholder="Password"
64 [formError]="formErrors['password']" autocomplete="current-password" [tabindex]="2"
65 ></my-input-text>
55 </div> 66 </div>
56 </div> 67 </ng-container>
68
69 <div *ngIf="otpStep" class="form-group">
70 <p i18n>Enter the two-factor code generated by your phone app:</p>
57 71
58 <div class="form-group"> 72 <label i18n for="otp-token">Two factor authentication token</label>
59 <label i18n for="password">Password</label>
60 73
61 <my-input-text 74 <my-input-text
62 formControlName="password" inputId="password" i18n-placeholder placeholder="Password" 75 #otpTokenInput
63 [formError]="formErrors['password']" autocomplete="current-password" [tabindex]="2" 76 [show]="true" formControlName="otp-token" inputId="otp-token"
77 [formError]="formErrors['otp-token']" autocomplete="otp-token"
64 ></my-input-text> 78 ></my-input-text>
65 </div> 79 </div>
66 80
67 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid"> 81 <input type="submit" class="peertube-button orange-button" i18n-value value="Login" [disabled]="!form.valid">
68 82
69 <div class="additional-links"> 83 <div *ngIf="!otpStep" class="additional-links">
70 <a i18n role="button" class="link-orange" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a> 84 <a i18n role="button" class="link-orange" (click)="openForgotPasswordModal()" i18n-title title="Click here to reset your password">I forgot my password</a>
71 85
72 <ng-container *ngIf="signupAllowed"> 86 <ng-container *ngIf="signupAllowed">
diff --git a/client/src/app/+login/login.component.scss b/client/src/app/+login/login.component.scss
index d31d428f7..17e151fd8 100644
--- a/client/src/app/+login/login.component.scss
+++ b/client/src/app/+login/login.component.scss
@@ -1,8 +1,8 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3 3
4@import '~bootstrap/scss/functions'; 4@import 'bootstrap/scss/functions';
5@import '~bootstrap/scss/variables'; 5@import 'bootstrap/scss/variables';
6 6
7label { 7label {
8 display: block; 8 display: block;
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts
index 2ed9be16c..c1705807f 100644
--- a/client/src/app/+login/login.component.ts
+++ b/client/src/app/+login/login.component.ts
@@ -1,10 +1,10 @@
1
2import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' 1import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' 3import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core'
5import { HooksService } from '@app/core/plugins/hooks.service' 4import { HooksService } from '@app/core/plugins/hooks.service'
6import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' 5import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators'
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators'
7import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms'
8import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' 8import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance'
9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 9import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
10import { PluginsManager } from '@root-helpers/plugins-manager' 10import { PluginsManager } from '@root-helpers/plugins-manager'
@@ -20,6 +20,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
20 private static SESSION_STORAGE_REDIRECT_URL_KEY = 'login-previous-url' 20 private static SESSION_STORAGE_REDIRECT_URL_KEY = 'login-previous-url'
21 21
22 @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef 22 @ViewChild('forgotPasswordModal', { static: true }) forgotPasswordModal: ElementRef
23 @ViewChild('otpTokenInput') otpTokenInput: InputTextComponent
23 24
24 accordion: NgbAccordion 25 accordion: NgbAccordion
25 error: string = null 26 error: string = null
@@ -37,11 +38,13 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
37 codeOfConduct: false 38 codeOfConduct: false
38 } 39 }
39 40
41 otpStep = false
42
40 private openedForgotPasswordModal: NgbModalRef 43 private openedForgotPasswordModal: NgbModalRef
41 private serverConfig: ServerConfig 44 private serverConfig: ServerConfig
42 45
43 constructor ( 46 constructor (
44 protected formValidatorService: FormValidatorService, 47 protected formReactiveService: FormReactiveService,
45 private route: ActivatedRoute, 48 private route: ActivatedRoute,
46 private modalService: NgbModal, 49 private modalService: NgbModal,
47 private authService: AuthService, 50 private authService: AuthService,
@@ -82,7 +85,11 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
82 // Avoid undefined errors when accessing form error properties 85 // Avoid undefined errors when accessing form error properties
83 this.buildForm({ 86 this.buildForm({
84 username: LOGIN_USERNAME_VALIDATOR, 87 username: LOGIN_USERNAME_VALIDATOR,
85 password: LOGIN_PASSWORD_VALIDATOR 88 password: LOGIN_PASSWORD_VALIDATOR,
89 'otp-token': {
90 VALIDATORS: [], // Will be set dynamically
91 MESSAGES: USER_OTP_TOKEN_VALIDATOR.MESSAGES
92 }
86 }) 93 })
87 94
88 this.serverConfig = snapshot.data.serverConfig 95 this.serverConfig = snapshot.data.serverConfig
@@ -118,13 +125,20 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni
118 login () { 125 login () {
119 this.error = null 126 this.error = null
120 127
121 const { username, password } = this.form.value 128 const options = {
129 username: this.form.value['username'],
130 password: this.form.value['password'],
131 otpToken: this.form.value['otp-token']
132 }
122 133
123 this.authService.login(username, password) 134 this.authService.login(options)
135 .pipe()
124 .subscribe({ 136 .subscribe({
125 next: () => this.redirectService.redirectToPreviousRoute(), 137 next: () => this.redirectService.redirectToPreviousRoute(),
126 138
127 error: err => this.handleError(err) 139 error: err => {
140 this.handleError(err)
141 }
128 }) 142 })
129 } 143 }
130 144
@@ -162,7 +176,7 @@ The link will expire within 1 hour.`
162 private loadExternalAuthToken (username: string, token: string) { 176 private loadExternalAuthToken (username: string, token: string) {
163 this.isAuthenticatedWithExternalAuth = true 177 this.isAuthenticatedWithExternalAuth = true
164 178
165 this.authService.login(username, null, token) 179 this.authService.login({ username, password: null, token })
166 .subscribe({ 180 .subscribe({
167 next: () => { 181 next: () => {
168 const redirectUrl = this.storage.getItem(LoginComponent.SESSION_STORAGE_REDIRECT_URL_KEY) 182 const redirectUrl = this.storage.getItem(LoginComponent.SESSION_STORAGE_REDIRECT_URL_KEY)
@@ -182,6 +196,17 @@ The link will expire within 1 hour.`
182 } 196 }
183 197
184 private handleError (err: any) { 198 private handleError (err: any) {
199 if (this.authService.isOTPMissingError(err)) {
200 this.otpStep = true
201
202 setTimeout(() => {
203 this.form.get('otp-token').setValidators(USER_OTP_TOKEN_VALIDATOR.VALIDATORS)
204 this.otpTokenInput.focus()
205 })
206
207 return
208 }
209
185 if (err.message.indexOf('credentials are invalid') !== -1) this.error = $localize`Incorrect username or password.` 210 if (err.message.indexOf('credentials are invalid') !== -1) this.error = $localize`Incorrect username or password.`
186 else if (err.message.indexOf('blocked') !== -1) this.error = $localize`Your account is blocked.` 211 else if (err.message.indexOf('blocked') !== -1) this.error = $localize`Your account is blocked.`
187 else this.error = err.message 212 else this.error = err.message
diff --git a/client/src/app/+manage/video-channel-edit/video-channel-create.component.ts b/client/src/app/+manage/video-channel-edit/video-channel-create.component.ts
index 8211451a4..372066890 100644
--- a/client/src/app/+manage/video-channel-edit/video-channel-create.component.ts
+++ b/client/src/app/+manage/video-channel-edit/video-channel-create.component.ts
@@ -9,7 +9,7 @@ import {
9 VIDEO_CHANNEL_NAME_VALIDATOR, 9 VIDEO_CHANNEL_NAME_VALIDATOR,
10 VIDEO_CHANNEL_SUPPORT_VALIDATOR 10 VIDEO_CHANNEL_SUPPORT_VALIDATOR
11} from '@app/shared/form-validators/video-channel-validators' 11} from '@app/shared/form-validators/video-channel-validators'
12import { FormValidatorService } from '@app/shared/shared-forms' 12import { FormReactiveService } from '@app/shared/shared-forms'
13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
14import { HttpStatusCode, VideoChannelCreate } from '@shared/models' 14import { HttpStatusCode, VideoChannelCreate } from '@shared/models'
15import { VideoChannelEdit } from './video-channel-edit' 15import { VideoChannelEdit } from './video-channel-edit'
@@ -26,7 +26,7 @@ export class VideoChannelCreateComponent extends VideoChannelEdit implements OnI
26 private banner: FormData 26 private banner: FormData
27 27
28 constructor ( 28 constructor (
29 protected formValidatorService: FormValidatorService, 29 protected formReactiveService: FormReactiveService,
30 private authService: AuthService, 30 private authService: AuthService,
31 private notifier: Notifier, 31 private notifier: Notifier,
32 private router: Router, 32 private router: Router,
diff --git a/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts b/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts
index 7e8d6ffe6..32f6d650d 100644
--- a/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts
+++ b/client/src/app/+manage/video-channel-edit/video-channel-update.component.ts
@@ -9,7 +9,7 @@ import {
9 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, 9 VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR,
10 VIDEO_CHANNEL_SUPPORT_VALIDATOR 10 VIDEO_CHANNEL_SUPPORT_VALIDATOR
11} from '@app/shared/form-validators/video-channel-validators' 11} from '@app/shared/form-validators/video-channel-validators'
12import { FormValidatorService } from '@app/shared/shared-forms' 12import { FormReactiveService } from '@app/shared/shared-forms'
13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 13import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
14import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models' 14import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models'
15import { VideoChannelEdit } from './video-channel-edit' 15import { VideoChannelEdit } from './video-channel-edit'
@@ -28,7 +28,7 @@ export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnI
28 private serverConfig: HTMLServerConfig 28 private serverConfig: HTMLServerConfig
29 29
30 constructor ( 30 constructor (
31 protected formValidatorService: FormValidatorService, 31 protected formReactiveService: FormReactiveService,
32 private authService: AuthService, 32 private authService: AuthService,
33 private notifier: Notifier, 33 private notifier: Notifier,
34 private route: ActivatedRoute, 34 private route: ActivatedRoute,
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 ef39c1a36..b39b1f6b4 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -7,6 +7,7 @@ import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-b
7import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' 7import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
8import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' 8import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
9import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' 9import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
10import { MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor'
10import { MyAccountComponent } from './my-account.component' 11import { MyAccountComponent } from './my-account.component'
11 12
12const myAccountRoutes: Routes = [ 13const myAccountRoutes: Routes = [
@@ -31,6 +32,16 @@ const myAccountRoutes: Routes = [
31 }, 32 },
32 33
33 { 34 {
35 path: 'two-factor-auth',
36 component: MyAccountTwoFactorComponent,
37 data: {
38 meta: {
39 title: $localize`Two factor authentication`
40 }
41 }
42 },
43
44 {
34 path: 'video-channels', 45 path: 'video-channels',
35 redirectTo: '/my-library/video-channels', 46 redirectTo: '/my-library/video-channels',
36 pathMatch: 'full' 47 pathMatch: 'full'
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html
index d85be846b..30ae9dd55 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.html
@@ -5,7 +5,7 @@
5 <strong>{{ user.pendingEmail }}</strong> is awaiting email verification 5 <strong>{{ user.pendingEmail }}</strong> is awaiting email verification
6</div> 6</div>
7 7
8<form role="form" class="change-email" (ngSubmit)="changeEmail()" [formGroup]="form" *ngIf="user.pluginAuth === null"> 8<form role="form" class="change-email" (ngSubmit)="changeEmail()" [formGroup]="form">
9 9
10 <div class="form-group"> 10 <div class="form-group">
11 <label i18n for="new-email">Change your email</label> 11 <label i18n for="new-email">Change your email</label>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts
index 9b87daa40..235fbec4a 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-change-email/my-account-change-email.component.ts
@@ -3,8 +3,8 @@ import { tap } from 'rxjs/operators'
3import { Component, OnInit } from '@angular/core' 3import { Component, OnInit } from '@angular/core'
4import { AuthService, ServerService, UserService } from '@app/core' 4import { AuthService, ServerService, UserService } from '@app/core'
5import { USER_EMAIL_VALIDATOR, USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' 5import { USER_EMAIL_VALIDATOR, USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7import { User } from '@shared/models' 7import { HttpStatusCode, User } from '@shared/models'
8 8
9@Component({ 9@Component({
10 selector: 'my-account-change-email', 10 selector: 'my-account-change-email',
@@ -17,7 +17,7 @@ export class MyAccountChangeEmailComponent extends FormReactive implements OnIni
17 user: User = null 17 user: User = null
18 18
19 constructor ( 19 constructor (
20 protected formValidatorService: FormValidatorService, 20 protected formReactiveService: FormReactiveService,
21 private authService: AuthService, 21 private authService: AuthService,
22 private userService: UserService, 22 private userService: UserService,
23 private serverService: ServerService 23 private serverService: ServerService
@@ -57,7 +57,7 @@ export class MyAccountChangeEmailComponent extends FormReactive implements OnIni
57 }, 57 },
58 58
59 error: err => { 59 error: err => {
60 if (err.status === 401) { 60 if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
61 this.error = $localize`You current password is invalid.` 61 this.error = $localize`You current password is invalid.`
62 return 62 return
63 } 63 }
diff --git a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts
index 47e54dc23..805d50070 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-change-password/my-account-change-password.component.ts
@@ -6,8 +6,8 @@ import {
6 USER_EXISTING_PASSWORD_VALIDATOR, 6 USER_EXISTING_PASSWORD_VALIDATOR,
7 USER_PASSWORD_VALIDATOR 7 USER_PASSWORD_VALIDATOR
8} from '@app/shared/form-validators/user-validators' 8} from '@app/shared/form-validators/user-validators'
9import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 9import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
10import { User } from '@shared/models' 10import { HttpStatusCode, User } from '@shared/models'
11 11
12@Component({ 12@Component({
13 selector: 'my-account-change-password', 13 selector: 'my-account-change-password',
@@ -19,7 +19,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
19 user: User = null 19 user: User = null
20 20
21 constructor ( 21 constructor (
22 protected formValidatorService: FormValidatorService, 22 protected formReactiveService: FormReactiveService,
23 private notifier: Notifier, 23 private notifier: Notifier,
24 private authService: AuthService, 24 private authService: AuthService,
25 private userService: UserService 25 private userService: UserService
@@ -57,7 +57,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
57 }, 57 },
58 58
59 error: err => { 59 error: err => {
60 if (err.status === 401) { 60 if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
61 this.error = $localize`You current password is invalid.` 61 this.error = $localize`You current password is invalid.`
62 return 62 return
63 } 63 }
diff --git a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts
index 2bae3499e..9619623ee 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-danger-zone/my-account-danger-zone.component.ts
@@ -18,7 +18,7 @@ export class MyAccountDangerZoneComponent {
18 ) { } 18 ) { }
19 19
20 async deleteMe () { 20 async deleteMe () {
21 const res = await this.confirmService.confirmWithInput( 21 const res = await this.confirmService.confirmWithExpectedInput(
22 $localize`Are you sure you want to delete your account?` + 22 $localize`Are you sure you want to delete your account?` +
23 '<br /><br />' + 23 '<br /><br />' +
24 // eslint-disable-next-line max-len 24 // eslint-disable-next-line max-len
diff --git a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
index f395ad73f..8621eb7aa 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
@@ -2,7 +2,7 @@ import { Subject } from 'rxjs'
2import { Component, Input, OnInit } from '@angular/core' 2import { Component, Input, OnInit } from '@angular/core'
3import { Notifier, User, UserService } from '@app/core' 3import { Notifier, User, UserService } from '@app/core'
4import { USER_DESCRIPTION_VALIDATOR, USER_DISPLAY_NAME_REQUIRED_VALIDATOR } from '@app/shared/form-validators/user-validators' 4import { USER_DESCRIPTION_VALIDATOR, USER_DISPLAY_NAME_REQUIRED_VALIDATOR } from '@app/shared/form-validators/user-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
6 6
7@Component({ 7@Component({
8 selector: 'my-account-profile', 8 selector: 'my-account-profile',
@@ -16,7 +16,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit {
16 error: string = null 16 error: string = null
17 17
18 constructor ( 18 constructor (
19 protected formValidatorService: FormValidatorService, 19 protected formReactiveService: FormReactiveService,
20 private notifier: Notifier, 20 private notifier: Notifier,
21 private userService: UserService 21 private userService: UserService
22 ) { 22 ) {
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
index d9e833019..666205de6 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
@@ -62,7 +62,17 @@
62 </div> 62 </div>
63</div> 63</div>
64 64
65<div class="row mt-5"> <!-- email grid --> 65<div class="row mt-5" *ngIf="user.pluginAuth === null"> <!-- two factor auth grid -->
66 <div class="col-12 col-lg-4 col-xl-3">
67 <h2 i18n class="account-title">Two-factor authentication</h2>
68 </div>
69
70 <div class="col-12 col-lg-8 col-xl-9">
71 <my-account-two-factor-button [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-two-factor-button>
72 </div>
73</div>
74
75<div class="row mt-5" *ngIf="user.pluginAuth === null"> <!-- email grid -->
66 <div class="col-12 col-lg-4 col-xl-3"> 76 <div class="col-12 col-lg-4 col-xl-3">
67 <h2 i18n class="account-title">EMAIL</h2> 77 <h2 i18n class="account-title">EMAIL</h2>
68 </div> 78 </div>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss b/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss
index 8206f4dd8..3d686a146 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.scss
@@ -1,6 +1,6 @@
1@use '_variables' as *; 1@use '_variables' as *;
2@use '_mixins' as *; 2@use '_mixins' as *;
3@use '~bootstrap/scss/functions' as *; 3@use 'bootstrap/scss/functions' as *;
4 4
5.account-title { 5.account-title {
6 @include settings-big-title; 6 @include settings-big-title;
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts
new file mode 100644
index 000000000..cc774bde3
--- /dev/null
+++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/index.ts
@@ -0,0 +1,2 @@
1export * from './my-account-two-factor-button.component'
2export * from './my-account-two-factor.component'
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.html b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.html
new file mode 100644
index 000000000..2fcfffbf3
--- /dev/null
+++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.html
@@ -0,0 +1,12 @@
1<div class="two-factor">
2 <ng-container *ngIf="!twoFactorEnabled">
3 <p i18n>Two factor authentication adds an additional layer of security to your account by requiring a numeric code from another device (most commonly mobile phones) when you log in.</p>
4
5 <my-button [routerLink]="[ '/my-account/two-factor-auth' ]" className="orange-button-link" i18n>Enable two-factor authentication</my-button>
6 </ng-container>
7
8 <ng-container *ngIf="twoFactorEnabled">
9 <my-button className="orange-button" (click)="disableTwoFactor()" i18n>Disable two-factor authentication</my-button>
10 </ng-container>
11
12</div>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts
new file mode 100644
index 000000000..97ffb6013
--- /dev/null
+++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor-button.component.ts
@@ -0,0 +1,49 @@
1import { Subject } from 'rxjs'
2import { Component, Input, OnInit } from '@angular/core'
3import { AuthService, ConfirmService, Notifier, User } from '@app/core'
4import { TwoFactorService } from '@app/shared/shared-users'
5
6@Component({
7 selector: 'my-account-two-factor-button',
8 templateUrl: './my-account-two-factor-button.component.html'
9})
10export class MyAccountTwoFactorButtonComponent implements OnInit {
11 @Input() user: User = null
12 @Input() userInformationLoaded: Subject<any>
13
14 twoFactorEnabled = false
15
16 constructor (
17 private notifier: Notifier,
18 private twoFactorService: TwoFactorService,
19 private confirmService: ConfirmService,
20 private auth: AuthService
21 ) {
22 }
23
24 ngOnInit () {
25 this.userInformationLoaded.subscribe(() => {
26 this.twoFactorEnabled = this.user.twoFactorEnabled
27 })
28 }
29
30 async disableTwoFactor () {
31 const message = $localize`Are you sure you want to disable two factor authentication of your account?`
32
33 const { confirmed, password } = await this.confirmService.confirmWithPassword(message, $localize`Disable two factor`)
34 if (confirmed === false) return
35
36 this.twoFactorService.disableTwoFactor({ userId: this.user.id, currentPassword: password })
37 .subscribe({
38 next: () => {
39 this.twoFactorEnabled = false
40
41 this.auth.refreshUserInformation()
42
43 this.notifier.success($localize`Two factor authentication disabled`)
44 },
45
46 error: err => this.notifier.error(err.message)
47 })
48 }
49}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.html b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.html
new file mode 100644
index 000000000..16c344e3b
--- /dev/null
+++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.html
@@ -0,0 +1,54 @@
1<h1>
2 <my-global-icon iconName="cog" aria-hidden="true"></my-global-icon>
3 <ng-container i18n>Two factor authentication</ng-container>
4</h1>
5
6<div i18n *ngIf="twoFactorAlreadyEnabled === true" class="root already-enabled">
7 Two factor authentication is already enabled.
8</div>
9
10<div class="root" *ngIf="twoFactorAlreadyEnabled === false">
11 <ng-container *ngIf="step === 'request'">
12 <form role="form" (ngSubmit)="requestTwoFactor()" [formGroup]="formPassword">
13
14 <label i18n for="current-password">Your password</label>
15 <div class="form-group-description" i18n>Confirm your password to enable two factor authentication</div>
16
17 <my-input-text
18 formControlName="current-password" inputId="current-password" i18n-placeholder placeholder="Current password"
19 [formError]="formErrorsPassword['current-password']" autocomplete="current-password"
20 ></my-input-text>
21
22 <input class="peertube-button orange-button mt-3" type="submit" i18n-value value="Confirm" [disabled]="!formPassword.valid">
23 </form>
24 </ng-container>
25
26 <ng-container *ngIf="step === 'confirm'">
27
28 <p i18n>
29 Scan this QR code into a TOTP app on your phone. This app will generate tokens that you will have to enter when logging in.
30 </p>
31
32 <qrcode [qrdata]="twoFactorURI" [width]="256" level="Q"></qrcode>
33
34 <div i18n>
35 If you can't scan the QR code and need to enter it manually, here is the plain-text secret:
36 </div>
37
38 <div class="secret-plain-text">{{ twoFactorSecret }}</div>
39
40 <form class="mt-3" role="form" (ngSubmit)="confirmTwoFactor()" [formGroup]="formOTP">
41
42 <label i18n for="otp-token">Two-factor code</label>
43 <div class="form-group-description" i18n>Enter the code generated by your authenticator app to confirm</div>
44
45 <my-input-text
46 [show]="true" formControlName="otp-token" inputId="otp-token"
47 [formError]="formErrorsOTP['otp-token']" autocomplete="otp-token"
48 ></my-input-text>
49
50 <input class="peertube-button orange-button mt-3" type="submit" i18n-value value="Confirm" [disabled]="!formOTP.valid">
51 </form>
52 </ng-container>
53
54</div>
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.scss b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.scss
new file mode 100644
index 000000000..cee016bb8
--- /dev/null
+++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.scss
@@ -0,0 +1,16 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4.root {
5 max-width: 600px;
6}
7
8.secret-plain-text {
9 font-family: monospace;
10 font-size: 0.9rem;
11}
12
13qrcode {
14 display: inline-block;
15 margin: auto;
16}
diff --git a/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts
new file mode 100644
index 000000000..259090d64
--- /dev/null
+++ b/client/src/app/+my-account/my-account-settings/my-account-two-factor/my-account-two-factor.component.ts
@@ -0,0 +1,105 @@
1import { Component, OnInit } from '@angular/core'
2import { FormGroup } from '@angular/forms'
3import { Router } from '@angular/router'
4import { AuthService, Notifier, User } from '@app/core'
5import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators'
6import { FormReactiveService } from '@app/shared/shared-forms'
7import { TwoFactorService } from '@app/shared/shared-users'
8
9@Component({
10 selector: 'my-account-two-factor',
11 templateUrl: './my-account-two-factor.component.html',
12 styleUrls: [ './my-account-two-factor.component.scss' ]
13})
14export class MyAccountTwoFactorComponent implements OnInit {
15 twoFactorAlreadyEnabled: boolean
16
17 step: 'request' | 'confirm' | 'confirmed' = 'request'
18
19 twoFactorSecret: string
20 twoFactorURI: string
21
22 inPasswordStep = true
23
24 formPassword: FormGroup
25 formErrorsPassword: any
26
27 formOTP: FormGroup
28 formErrorsOTP: any
29
30 private user: User
31 private requestToken: string
32
33 constructor (
34 private notifier: Notifier,
35 private twoFactorService: TwoFactorService,
36 private formReactiveService: FormReactiveService,
37 private auth: AuthService,
38 private router: Router
39 ) {
40 }
41
42 ngOnInit () {
43 this.buildPasswordForm()
44 this.buildOTPForm()
45
46 this.auth.userInformationLoaded.subscribe(() => {
47 this.user = this.auth.getUser()
48
49 this.twoFactorAlreadyEnabled = this.user.twoFactorEnabled
50 })
51 }
52
53 requestTwoFactor () {
54 this.twoFactorService.requestTwoFactor({
55 userId: this.user.id,
56 currentPassword: this.formPassword.value['current-password']
57 }).subscribe({
58 next: ({ otpRequest }) => {
59 this.requestToken = otpRequest.requestToken
60 this.twoFactorURI = otpRequest.uri
61 this.twoFactorSecret = otpRequest.secret.replace(/(.{4})/g, '$1 ').trim()
62
63 this.step = 'confirm'
64 },
65
66 error: err => this.notifier.error(err.message)
67 })
68 }
69
70 confirmTwoFactor () {
71 this.twoFactorService.confirmTwoFactorRequest({
72 userId: this.user.id,
73 requestToken: this.requestToken,
74 otpToken: this.formOTP.value['otp-token']
75 }).subscribe({
76 next: () => {
77 this.notifier.success($localize`Two factor authentication has been enabled.`)
78
79 this.auth.refreshUserInformation()
80
81 this.router.navigateByUrl('/my-account/settings')
82 },
83
84 error: err => this.notifier.error(err.message)
85 })
86 }
87
88 private buildPasswordForm () {
89 const { form, formErrors } = this.formReactiveService.buildForm({
90 'current-password': USER_EXISTING_PASSWORD_VALIDATOR
91 })
92
93 this.formPassword = form
94 this.formErrorsPassword = formErrors
95 }
96
97 private buildOTPForm () {
98 const { form, formErrors } = this.formReactiveService.buildForm({
99 'otp-token': USER_OTP_TOKEN_VALIDATOR
100 })
101
102 this.formOTP = form
103 this.formErrorsOTP = formErrors
104 }
105}
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 4081e4f01..84b057647 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -1,3 +1,4 @@
1import { QRCodeModule } from 'angularx-qrcode'
1import { AutoCompleteModule } from 'primeng/autocomplete' 2import { AutoCompleteModule } from 'primeng/autocomplete'
2import { TableModule } from 'primeng/table' 3import { TableModule } from 'primeng/table'
3import { DragDropModule } from '@angular/cdk/drag-drop' 4import { DragDropModule } from '@angular/cdk/drag-drop'
@@ -10,6 +11,7 @@ import { SharedMainModule } from '@app/shared/shared-main'
10import { SharedModerationModule } from '@app/shared/shared-moderation' 11import { SharedModerationModule } from '@app/shared/shared-moderation'
11import { SharedShareModal } from '@app/shared/shared-share-modal' 12import { SharedShareModal } from '@app/shared/shared-share-modal'
12import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' 13import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings'
14import { SharedUsersModule } from '@app/shared/shared-users'
13import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' 15import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
14import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' 16import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
15import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' 17import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
@@ -23,12 +25,14 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d
23import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' 25import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences'
24import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' 26import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component'
25import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' 27import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
28import { MyAccountTwoFactorButtonComponent, MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor'
26import { MyAccountComponent } from './my-account.component' 29import { MyAccountComponent } from './my-account.component'
27 30
28@NgModule({ 31@NgModule({
29 imports: [ 32 imports: [
30 MyAccountRoutingModule, 33 MyAccountRoutingModule,
31 34
35 QRCodeModule,
32 AutoCompleteModule, 36 AutoCompleteModule,
33 TableModule, 37 TableModule,
34 DragDropModule, 38 DragDropModule,
@@ -37,6 +41,7 @@ import { MyAccountComponent } from './my-account.component'
37 SharedFormModule, 41 SharedFormModule,
38 SharedModerationModule, 42 SharedModerationModule,
39 SharedUserInterfaceSettingsModule, 43 SharedUserInterfaceSettingsModule,
44 SharedUsersModule,
40 SharedGlobalIconModule, 45 SharedGlobalIconModule,
41 SharedAbuseListModule, 46 SharedAbuseListModule,
42 SharedShareModal, 47 SharedShareModal,
@@ -52,6 +57,9 @@ import { MyAccountComponent } from './my-account.component'
52 MyAccountChangeEmailComponent, 57 MyAccountChangeEmailComponent,
53 MyAccountApplicationsComponent, 58 MyAccountApplicationsComponent,
54 59
60 MyAccountTwoFactorButtonComponent,
61 MyAccountTwoFactorComponent,
62
55 MyAccountDangerZoneComponent, 63 MyAccountDangerZoneComponent,
56 MyAccountBlocklistComponent, 64 MyAccountBlocklistComponent,
57 MyAccountAbusesListComponent, 65 MyAccountAbusesListComponent,
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts
index 205ad7a89..ece59c2ff 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts
@@ -40,7 +40,7 @@ export class MyVideoChannelsComponent {
40 } 40 }
41 41
42 async deleteVideoChannel (videoChannel: VideoChannel) { 42 async deleteVideoChannel (videoChannel: VideoChannel) {
43 const res = await this.confirmService.confirmWithInput( 43 const res = await this.confirmService.confirmWithExpectedInput(
44 $localize`Do you really want to delete ${videoChannel.displayName}? 44 $localize`Do you really want to delete ${videoChannel.displayName}?
45It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another 45It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another
46channel with the same name (${videoChannel.name})!`, 46channel with the same name (${videoChannel.name})!`,
diff --git a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts
index 8ead237c7..ca7eb680b 100644
--- a/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts
+++ b/client/src/app/+my-library/my-ownership/my-accept-ownership/my-accept-ownership.component.ts
@@ -3,7 +3,7 @@ import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '
3import { AuthService, Notifier } from '@app/core' 3import { AuthService, Notifier } from '@app/core'
4import { listUserChannelsForSelect } from '@app/helpers' 4import { listUserChannelsForSelect } from '@app/helpers'
5import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' 5import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7import { VideoOwnershipService } from '@app/shared/shared-main' 7import { VideoOwnershipService } from '@app/shared/shared-main'
8import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 8import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
9import { VideoChangeOwnership } from '@shared/models' 9import { VideoChangeOwnership } from '@shared/models'
@@ -24,7 +24,7 @@ export class MyAcceptOwnershipComponent extends FormReactive implements OnInit {
24 error: string = null 24 error: string = null
25 25
26 constructor ( 26 constructor (
27 protected formValidatorService: FormValidatorService, 27 protected formReactiveService: FormReactiveService,
28 private videoOwnershipService: VideoOwnershipService, 28 private videoOwnershipService: VideoOwnershipService,
29 private notifier: Notifier, 29 private notifier: Notifier,
30 private authService: AuthService, 30 private authService: AuthService,
diff --git a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html
index 5f368d430..538bbd178 100644
--- a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html
+++ b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.html
@@ -36,7 +36,6 @@
36 <th style="width: 10%" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th> 36 <th style="width: 10%" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
37 <th style="width: 10%" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> 37 <th style="width: 10%" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
38 <th style="width: 10%" i18n pSortableColumn="lastSyncAt">Last synchronization at <p-sortIcon field="lastSyncAt"></p-sortIcon></th> 38 <th style="width: 10%" i18n pSortableColumn="lastSyncAt">Last synchronization at <p-sortIcon field="lastSyncAt"></p-sortIcon></th>
39 <th></th>
40 </tr> 39 </tr>
41 </ng-template> 40 </ng-template>
42 41
@@ -79,12 +78,6 @@
79 78
80 <td>{{ videoChannelSync.createdAt | date: 'short' }}</td> 79 <td>{{ videoChannelSync.createdAt | date: 'short' }}</td>
81 <td>{{ videoChannelSync.lastSyncAt | date: 'short' }}</td> 80 <td>{{ videoChannelSync.lastSyncAt | date: 'short' }}</td>
82
83 <td>
84 <a i18n routerLink="/my-library/video-imports" [queryParams]="{ search: 'videoChannelSyncId:' + videoChannelSync.id }" class="peertube-button-link grey-button">
85 List imports
86 </a>
87 </td>
88 </tr> 81 </tr>
89 </ng-template> 82 </ng-template>
90</p-table> 83</p-table>
diff --git a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts
index 290847418..d18e78201 100644
--- a/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts
+++ b/client/src/app/+my-library/my-video-channel-syncs/my-video-channel-syncs.component.ts
@@ -1,10 +1,10 @@
1import { SortMeta } from 'primeng/api'
2import { mergeMap } from 'rxjs'
1import { Component, OnInit } from '@angular/core' 3import { Component, OnInit } from '@angular/core'
2import { AuthService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' 4import { AuthService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
3import { DropdownAction, VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main' 5import { DropdownAction, VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main'
4import { HTMLServerConfig } from '@shared/models/server' 6import { HTMLServerConfig } from '@shared/models/server'
5import { VideoChannelSync, VideoChannelSyncState } from '@shared/models/videos' 7import { VideoChannelSync, VideoChannelSyncState } from '@shared/models/videos'
6import { SortMeta } from 'primeng/api'
7import { mergeMap } from 'rxjs'
8 8
9@Component({ 9@Component({
10 templateUrl: './my-video-channel-syncs.component.html', 10 templateUrl: './my-video-channel-syncs.component.html',
@@ -46,6 +46,14 @@ export class MyVideoChannelSyncsComponent extends RestTable implements OnInit {
46 this.videoChannelSyncActions = [ 46 this.videoChannelSyncActions = [
47 [ 47 [
48 { 48 {
49 label: $localize`List imports`,
50 linkBuilder: () => [ '/my-library/video-imports' ],
51 queryParamsBuilder: sync => ({ search: `videoChannelSyncId:${sync.id}` }),
52 iconName: 'cloud-download'
53 }
54 ],
55 [
56 {
49 label: $localize`Delete`, 57 label: $localize`Delete`,
50 iconName: 'delete', 58 iconName: 'delete',
51 handler: videoChannelSync => this.deleteSync(videoChannelSync) 59 handler: videoChannelSync => this.deleteSync(videoChannelSync)
diff --git a/client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts b/client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts
index 9ceb6dfd1..a14ab5b92 100644
--- a/client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts
+++ b/client/src/app/+my-library/my-video-channel-syncs/video-channel-sync-edit/video-channel-sync-edit.component.ts
@@ -5,7 +5,7 @@ import { Router } from '@angular/router'
5import { AuthService, Notifier } from '@app/core' 5import { AuthService, Notifier } from '@app/core'
6import { listUserChannelsForSelect } from '@app/helpers' 6import { listUserChannelsForSelect } from '@app/helpers'
7import { VIDEO_CHANNEL_EXTERNAL_URL_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' 7import { VIDEO_CHANNEL_EXTERNAL_URL_VALIDATOR } from '@app/shared/form-validators/video-channel-validators'
8import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 8import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
9import { VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main' 9import { VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main'
10import { VideoChannelSyncCreate } from '@shared/models/videos' 10import { VideoChannelSyncCreate } from '@shared/models/videos'
11 11
@@ -20,7 +20,7 @@ export class VideoChannelSyncEditComponent extends FormReactive implements OnIni
20 existingVideosStrategy: string 20 existingVideosStrategy: string
21 21
22 constructor ( 22 constructor (
23 protected formValidatorService: FormValidatorService, 23 protected formReactiveService: FormReactiveService,
24 private authService: AuthService, 24 private authService: AuthService,
25 private router: Router, 25 private router: Router,
26 private notifier: Notifier, 26 private notifier: Notifier,
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts
index 9eb3e9888..63f72df3f 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-create.component.ts
@@ -9,7 +9,7 @@ import {
9 VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, 9 VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
10 VIDEO_PLAYLIST_PRIVACY_VALIDATOR 10 VIDEO_PLAYLIST_PRIVACY_VALIDATOR
11} from '@app/shared/form-validators/video-playlist-validators' 11} from '@app/shared/form-validators/video-playlist-validators'
12import { FormValidatorService } from '@app/shared/shared-forms' 12import { FormReactiveService } from '@app/shared/shared-forms'
13import { VideoPlaylistService } from '@app/shared/shared-video-playlist' 13import { VideoPlaylistService } from '@app/shared/shared-video-playlist'
14import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' 14import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model'
15import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' 15import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model'
@@ -23,7 +23,7 @@ export class MyVideoPlaylistCreateComponent extends MyVideoPlaylistEdit implemen
23 error: string 23 error: string
24 24
25 constructor ( 25 constructor (
26 protected formValidatorService: FormValidatorService, 26 protected formReactiveService: FormReactiveService,
27 private authService: AuthService, 27 private authService: AuthService,
28 private notifier: Notifier, 28 private notifier: Notifier,
29 private router: Router, 29 private router: Router,
diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts
index ef7ba0018..bbe8a5f80 100644
--- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts
+++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-update.component.ts
@@ -11,7 +11,7 @@ import {
11 VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR, 11 VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR,
12 VIDEO_PLAYLIST_PRIVACY_VALIDATOR 12 VIDEO_PLAYLIST_PRIVACY_VALIDATOR
13} from '@app/shared/form-validators/video-playlist-validators' 13} from '@app/shared/form-validators/video-playlist-validators'
14import { FormValidatorService } from '@app/shared/shared-forms' 14import { FormReactiveService } from '@app/shared/shared-forms'
15import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 15import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
16import { VideoPlaylistUpdate } from '@shared/models' 16import { VideoPlaylistUpdate } from '@shared/models'
17import { MyVideoPlaylistEdit } from './my-video-playlist-edit' 17import { MyVideoPlaylistEdit } from './my-video-playlist-edit'
@@ -27,7 +27,7 @@ export class MyVideoPlaylistUpdateComponent extends MyVideoPlaylistEdit implemen
27 private paramsSub: Subscription 27 private paramsSub: Subscription
28 28
29 constructor ( 29 constructor (
30 protected formValidatorService: FormValidatorService, 30 protected formReactiveService: FormReactiveService,
31 private authService: AuthService, 31 private authService: AuthService,
32 private notifier: Notifier, 32 private notifier: Notifier,
33 private router: Router, 33 private router: Router,
diff --git a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.ts b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.ts
index 960c9a4f7..72187e893 100644
--- a/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.ts
+++ b/client/src/app/+my-library/my-videos/modals/video-change-ownership.component.ts
@@ -1,7 +1,7 @@
1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' 1import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
2import { Notifier, UserService } from '@app/core' 2import { Notifier, UserService } from '@app/core'
3import { OWNERSHIP_CHANGE_USERNAME_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' 3import { OWNERSHIP_CHANGE_USERNAME_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { Video, VideoOwnershipService } from '@app/shared/shared-main' 5import { Video, VideoOwnershipService } from '@app/shared/shared-main'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7 7
@@ -20,7 +20,7 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni
20 private video: Video | undefined = undefined 20 private video: Video | undefined = undefined
21 21
22 constructor ( 22 constructor (
23 protected formValidatorService: FormValidatorService, 23 protected formReactiveService: FormReactiveService,
24 private videoOwnershipService: VideoOwnershipService, 24 private videoOwnershipService: VideoOwnershipService,
25 private notifier: Notifier, 25 private notifier: Notifier,
26 private userService: UserService, 26 private userService: UserService,
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html
index 146dcf41e..995f6b75b 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.html
+++ b/client/src/app/+my-library/my-videos/my-videos.component.html
@@ -34,6 +34,7 @@
34</div> 34</div>
35 35
36<my-videos-selection 36<my-videos-selection
37 [videosContainedInPlaylists]="videosContainedInPlaylists"
37 [pagination]="pagination" 38 [pagination]="pagination"
38 [(selection)]="selection" 39 [(selection)]="selection"
39 [(videosModel)]="videos" 40 [(videosModel)]="videos"
diff --git a/client/src/app/+my-library/my-videos/my-videos.component.ts b/client/src/app/+my-library/my-videos/my-videos.component.ts
index 2f1eb84ba..bcfc66099 100644
--- a/client/src/app/+my-library/my-videos/my-videos.component.ts
+++ b/client/src/app/+my-library/my-videos/my-videos.component.ts
@@ -1,10 +1,11 @@
1import { uniqBy } from 'lodash-es'
1import { concat, Observable } from 'rxjs' 2import { concat, Observable } from 'rxjs'
2import { tap, toArray } from 'rxjs/operators' 3import { tap, toArray } from 'rxjs/operators'
3import { Component, OnInit, ViewChild } from '@angular/core' 4import { Component, OnInit, ViewChild } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 5import { ActivatedRoute, Router } from '@angular/router'
5import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' 6import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core'
6import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' 7import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
7import { prepareIcu, immutableAssign } from '@app/helpers' 8import { immutableAssign, prepareIcu } from '@app/helpers'
8import { AdvancedInputFilter } from '@app/shared/shared-forms' 9import { AdvancedInputFilter } from '@app/shared/shared-forms'
9import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 10import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
10import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' 11import { LiveStreamInformationComponent } from '@app/shared/shared-video-live'
@@ -14,7 +15,8 @@ import {
14 VideoActionsDisplayType, 15 VideoActionsDisplayType,
15 VideosSelectionComponent 16 VideosSelectionComponent
16} from '@app/shared/shared-video-miniature' 17} from '@app/shared/shared-video-miniature'
17import { VideoChannel, VideoSortField } from '@shared/models' 18import { VideoPlaylistService } from '@app/shared/shared-video-playlist'
19import { VideoChannel, VideoExistInPlaylist, VideosExistInPlaylists, VideoSortField } from '@shared/models'
18import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' 20import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component'
19 21
20@Component({ 22@Component({
@@ -26,6 +28,7 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
26 @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent 28 @ViewChild('videoChangeOwnershipModal', { static: true }) videoChangeOwnershipModal: VideoChangeOwnershipComponent
27 @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent 29 @ViewChild('liveStreamInformationModal', { static: true }) liveStreamInformationModal: LiveStreamInformationComponent
28 30
31 videosContainedInPlaylists: VideosExistInPlaylists = {}
29 titlePage: string 32 titlePage: string
30 selection: SelectionType = {} 33 selection: SelectionType = {}
31 pagination: ComponentPagination = { 34 pagination: ComponentPagination = {
@@ -40,7 +43,8 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
40 privacyLabel: false, 43 privacyLabel: false,
41 privacyText: true, 44 privacyText: true,
42 state: true, 45 state: true,
43 blacklistInfo: true 46 blacklistInfo: true,
47 forceChannelInBy: true
44 } 48 }
45 videoDropdownDisplayOptions: VideoActionsDisplayType = { 49 videoDropdownDisplayOptions: VideoActionsDisplayType = {
46 playlist: false, 50 playlist: false,
@@ -82,7 +86,8 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
82 protected notifier: Notifier, 86 protected notifier: Notifier,
83 protected screenService: ScreenService, 87 protected screenService: ScreenService,
84 private confirmService: ConfirmService, 88 private confirmService: ConfirmService,
85 private videoService: VideoService 89 private videoService: VideoService,
90 private playlistService: VideoPlaylistService
86 ) { 91 ) {
87 this.titlePage = $localize`My videos` 92 this.titlePage = $localize`My videos`
88 } 93 }
@@ -155,10 +160,20 @@ export class MyVideosComponent implements OnInit, DisableForReuseHook {
155 sort: this.sort, 160 sort: this.sort,
156 userChannels: this.userChannels, 161 userChannels: this.userChannels,
157 search: this.search 162 search: this.search
158 }) 163 }).pipe(
159 .pipe( 164 tap(res => this.pagination.totalItems = res.total),
160 tap(res => this.pagination.totalItems = res.total) 165 tap(({ data }) => this.fetchVideosContainedInPlaylists(data))
161 ) 166 )
167 }
168
169 private fetchVideosContainedInPlaylists (videos: Video[]) {
170 this.playlistService.doVideosExistInPlaylist(videos.map(v => v.id))
171 .subscribe(result => {
172 this.videosContainedInPlaylists = Object.keys(result).reduce((acc, videoId) => ({
173 ...acc,
174 [videoId]: uniqBy(result[videoId], (p: VideoExistInPlaylist) => p.playlistId)
175 }), this.videosContainedInPlaylists)
176 })
162 } 177 }
163 178
164 async deleteSelectedVideos () { 179 async deleteSelectedVideos () {
diff --git a/client/src/app/+reset-password/reset-password.component.ts b/client/src/app/+reset-password/reset-password.component.ts
index 11c5110fd..44216f978 100644
--- a/client/src/app/+reset-password/reset-password.component.ts
+++ b/client/src/app/+reset-password/reset-password.component.ts
@@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router'
3import { Notifier, UserService } from '@app/core' 3import { Notifier, UserService } from '@app/core'
4import { RESET_PASSWORD_CONFIRM_VALIDATOR } from '@app/shared/form-validators/reset-password-validators' 4import { RESET_PASSWORD_CONFIRM_VALIDATOR } from '@app/shared/form-validators/reset-password-validators'
5import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' 5import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7 7
8@Component({ 8@Component({
9 selector: 'my-login', 9 selector: 'my-login',
@@ -16,7 +16,7 @@ export class ResetPasswordComponent extends FormReactive implements OnInit {
16 private verificationString: string 16 private verificationString: string
17 17
18 constructor ( 18 constructor (
19 protected formValidatorService: FormValidatorService, 19 protected formReactiveService: FormReactiveService,
20 private userService: UserService, 20 private userService: UserService,
21 private notifier: Notifier, 21 private notifier: Notifier,
22 private router: Router, 22 private router: Router,
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index 62b1c4446..366fbd459 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -98,7 +98,7 @@ export class SearchComponent implements OnInit, OnDestroy {
98 this.search() 98 this.search()
99 }, 99 },
100 100
101 error: err => this.notifier.error(err.text) 101 error: err => this.notifier.error(err.message)
102 }) 102 })
103 103
104 this.userService.getAnonymousOrLoggedUser() 104 this.userService.getAnonymousOrLoggedUser()
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts
index 4ab327b1b..958770ebf 100644
--- a/client/src/app/+signup/+register/register.component.ts
+++ b/client/src/app/+signup/+register/register.component.ts
@@ -158,7 +158,7 @@ export class RegisterComponent implements OnInit {
158 } 158 }
159 159
160 // Auto login 160 // Auto login
161 this.authService.login(body.username, body.password) 161 this.authService.login({ username: body.username, password: body.password })
162 .subscribe({ 162 .subscribe({
163 next: () => { 163 next: () => {
164 this.signupSuccess = true 164 this.signupSuccess = true
diff --git a/client/src/app/+signup/+register/steps/register-step-channel.component.ts b/client/src/app/+signup/+register/steps/register-step-channel.component.ts
index c10b568ba..df92c5145 100644
--- a/client/src/app/+signup/+register/steps/register-step-channel.component.ts
+++ b/client/src/app/+signup/+register/steps/register-step-channel.component.ts
@@ -3,7 +3,7 @@ import { pairwise } from 'rxjs/operators'
3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 3import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
4import { FormGroup } from '@angular/forms' 4import { FormGroup } from '@angular/forms'
5import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' 5import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7import { UserSignupService } from '@app/shared/shared-users' 7import { UserSignupService } from '@app/shared/shared-users'
8 8
9@Component({ 9@Component({
@@ -19,7 +19,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
19 @Output() formBuilt = new EventEmitter<FormGroup>() 19 @Output() formBuilt = new EventEmitter<FormGroup>()
20 20
21 constructor ( 21 constructor (
22 protected formValidatorService: FormValidatorService, 22 protected formReactiveService: FormReactiveService,
23 private userSignupService: UserSignupService 23 private userSignupService: UserSignupService
24 ) { 24 ) {
25 super() 25 super()
diff --git a/client/src/app/+signup/+register/steps/register-step-terms.component.ts b/client/src/app/+signup/+register/steps/register-step-terms.component.ts
index 87d16696e..2df963b30 100644
--- a/client/src/app/+signup/+register/steps/register-step-terms.component.ts
+++ b/client/src/app/+signup/+register/steps/register-step-terms.component.ts
@@ -1,9 +1,7 @@
1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { FormGroup } from '@angular/forms' 2import { FormGroup } from '@angular/forms'
3import { 3import { USER_TERMS_VALIDATOR } from '@app/shared/form-validators/user-validators'
4 USER_TERMS_VALIDATOR 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5} from '@app/shared/form-validators/user-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
7 5
8@Component({ 6@Component({
9 selector: 'my-register-step-terms', 7 selector: 'my-register-step-terms',
@@ -19,7 +17,7 @@ export class RegisterStepTermsComponent extends FormReactive implements OnInit {
19 @Output() codeOfConductClick = new EventEmitter<void>() 17 @Output() codeOfConductClick = new EventEmitter<void>()
20 18
21 constructor ( 19 constructor (
22 protected formValidatorService: FormValidatorService 20 protected formReactiveService: FormReactiveService
23 ) { 21 ) {
24 super() 22 super()
25 } 23 }
diff --git a/client/src/app/+signup/+register/steps/register-step-user.component.ts b/client/src/app/+signup/+register/steps/register-step-user.component.ts
index b89e38a28..822f8f5c5 100644
--- a/client/src/app/+signup/+register/steps/register-step-user.component.ts
+++ b/client/src/app/+signup/+register/steps/register-step-user.component.ts
@@ -8,7 +8,7 @@ import {
8 USER_PASSWORD_VALIDATOR, 8 USER_PASSWORD_VALIDATOR,
9 USER_USERNAME_VALIDATOR 9 USER_USERNAME_VALIDATOR
10} from '@app/shared/form-validators/user-validators' 10} from '@app/shared/form-validators/user-validators'
11import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 11import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
12import { UserSignupService } from '@app/shared/shared-users' 12import { UserSignupService } from '@app/shared/shared-users'
13 13
14@Component({ 14@Component({
@@ -23,7 +23,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
23 @Output() formBuilt = new EventEmitter<FormGroup>() 23 @Output() formBuilt = new EventEmitter<FormGroup>()
24 24
25 constructor ( 25 constructor (
26 protected formValidatorService: FormValidatorService, 26 protected formReactiveService: FormReactiveService,
27 private userSignupService: UserSignupService 27 private userSignupService: UserSignupService
28 ) { 28 ) {
29 super() 29 super()
diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
index a0ed66a3a..06905f678 100644
--- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
+++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts
@@ -1,7 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { Notifier, RedirectService, ServerService } from '@app/core' 2import { Notifier, RedirectService, ServerService } from '@app/core'
3import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' 3import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { UserSignupService } from '@app/shared/shared-users' 5import { UserSignupService } from '@app/shared/shared-users'
6 6
7@Component({ 7@Component({
@@ -14,7 +14,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements
14 requiresEmailVerification = false 14 requiresEmailVerification = false
15 15
16 constructor ( 16 constructor (
17 protected formValidatorService: FormValidatorService, 17 protected formReactiveService: FormReactiveService,
18 private userSignupService: UserSignupService, 18 private userSignupService: UserSignupService,
19 private serverService: ServerService, 19 private serverService: ServerService,
20 private notifier: Notifier, 20 private notifier: Notifier,
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index c5bcdffe2..afbf96032 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -56,8 +56,17 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
56 ])) 56 ]))
57 ) 57 )
58 .subscribe(async videoChannel => { 58 .subscribe(async videoChannel => {
59 this.channelDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.description) 59 this.channelDescriptionHTML = await this.markdown.textMarkdownToHTML({
60 this.ownerDescriptionHTML = await this.markdown.textMarkdownToHTML(videoChannel.ownerAccount.description) 60 markdown: videoChannel.description,
61 withEmoji: true,
62 withHtml: true
63 })
64
65 this.ownerDescriptionHTML = await this.markdown.textMarkdownToHTML({
66 markdown: videoChannel.ownerAccount.description,
67 withEmoji: true,
68 withHtml: true
69 })
61 70
62 // After the markdown renderer to avoid layout changes 71 // After the markdown renderer to avoid layout changes
63 this.videoChannel = videoChannel 72 this.videoChannel = videoChannel
diff --git a/client/src/app/+video-studio/edit/video-studio-edit.component.ts b/client/src/app/+video-studio/edit/video-studio-edit.component.ts
index bf91c237a..dad083bf9 100644
--- a/client/src/app/+video-studio/edit/video-studio-edit.component.ts
+++ b/client/src/app/+video-studio/edit/video-studio-edit.component.ts
@@ -1,7 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { ConfirmService, Notifier, ServerService } from '@app/core' 3import { ConfirmService, Notifier, ServerService } from '@app/core'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { VideoDetails } from '@app/shared/shared-main' 5import { VideoDetails } from '@app/shared/shared-main'
6import { LoadingBarService } from '@ngx-loading-bar/core' 6import { LoadingBarService } from '@ngx-loading-bar/core'
7import { logger } from '@root-helpers/logger' 7import { logger } from '@root-helpers/logger'
@@ -20,7 +20,7 @@ export class VideoStudioEditComponent extends FormReactive implements OnInit {
20 video: VideoDetails 20 video: VideoDetails
21 21
22 constructor ( 22 constructor (
23 protected formValidatorService: FormValidatorService, 23 protected formReactiveService: FormReactiveService,
24 private serverService: ServerService, 24 private serverService: ServerService,
25 private notifier: Notifier, 25 private notifier: Notifier,
26 private router: Router, 26 private router: Router,
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 95d83b131..4ab2d42db 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
@@ -1,7 +1,7 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { ServerService } from '@app/core' 2import { ServerService } from '@app/core'
3import { VIDEO_CAPTION_FILE_VALIDATOR, VIDEO_CAPTION_LANGUAGE_VALIDATOR } from '@app/shared/form-validators/video-captions-validators' 3import { VIDEO_CAPTION_FILE_VALIDATOR, VIDEO_CAPTION_LANGUAGE_VALIDATOR } from '@app/shared/form-validators/video-captions-validators'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { VideoCaptionEdit } from '@app/shared/shared-main' 5import { VideoCaptionEdit } from '@app/shared/shared-main'
6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
7import { HTMLServerConfig, VideoConstant } from '@shared/models' 7import { HTMLServerConfig, VideoConstant } from '@shared/models'
@@ -26,7 +26,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
26 private closingModal = false 26 private closingModal = false
27 27
28 constructor ( 28 constructor (
29 protected formValidatorService: FormValidatorService, 29 protected formReactiveService: FormReactiveService,
30 private modalService: NgbModal, 30 private modalService: NgbModal,
31 private serverService: ServerService 31 private serverService: ServerService
32 ) { 32 ) {
diff --git a/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal-content/video-caption-edit-modal-content.component.ts b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal-content/video-caption-edit-modal-content.component.ts
index f33353d36..2cb470a24 100644
--- a/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal-content/video-caption-edit-modal-content.component.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-caption-edit-modal-content/video-caption-edit-modal-content.component.ts
@@ -1,8 +1,8 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { VIDEO_CAPTION_FILE_CONTENT_VALIDATOR } from '@app/shared/form-validators/video-captions-validators' 2import { VIDEO_CAPTION_FILE_CONTENT_VALIDATOR } from '@app/shared/form-validators/video-captions-validators'
3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
4import { VideoCaptionEdit, VideoCaptionService, VideoCaptionWithPathEdit } from '@app/shared/shared-main' 4import { VideoCaptionEdit, VideoCaptionService, VideoCaptionWithPathEdit } from '@app/shared/shared-main'
5import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 5import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
6import { HTMLServerConfig, VideoConstant } from '@shared/models' 6import { HTMLServerConfig, VideoConstant } from '@shared/models'
7import { ServerService } from '../../../../core' 7import { ServerService } from '../../../../core'
8 8
@@ -29,8 +29,7 @@ export class VideoCaptionEditModalContentComponent extends FormReactive implemen
29 29
30 constructor ( 30 constructor (
31 protected openedModal: NgbActiveModal, 31 protected openedModal: NgbActiveModal,
32 protected formValidatorService: FormValidatorService, 32 protected formReactiveService: FormReactiveService,
33 private modalService: NgbModal,
34 private videoCaptionService: VideoCaptionService, 33 private videoCaptionService: VideoCaptionService,
35 private serverService: ServerService 34 private serverService: ServerService
36 ) { 35 ) {
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
index 7be5a3736..fa816fd9e 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -141,7 +141,7 @@
141 </ng-template> 141 </ng-template>
142 </my-peertube-checkbox> 142 </my-peertube-checkbox>
143 143
144 <my-peertube-checkbox *ngIf="waitTranscodingEnabled" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right"> 144 <my-peertube-checkbox *ngIf="!hideWaitTranscoding" inputName="waitTranscoding" formControlName="waitTranscoding" helpPlacement="bottom-right">
145 <ng-template ptTemplate="label"> 145 <ng-template ptTemplate="label">
146 <ng-container i18n>Publish after transcoding</ng-container> 146 <ng-container i18n>Publish after transcoding</ng-container>
147 </ng-template> 147 </ng-template>
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 0275f66f5..13359a4d1 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
@@ -22,6 +22,8 @@ import {
22import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' 22import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
23import { InstanceService } from '@app/shared/shared-instance' 23import { InstanceService } from '@app/shared/shared-instance'
24import { VideoCaptionEdit, VideoCaptionWithPathEdit, VideoEdit, VideoService } from '@app/shared/shared-main' 24import { VideoCaptionEdit, VideoCaptionWithPathEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
25import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
26import { logger } from '@root-helpers/logger'
25import { PluginInfo } from '@root-helpers/plugins-manager' 27import { PluginInfo } from '@root-helpers/plugins-manager'
26import { 28import {
27 HTMLServerConfig, 29 HTMLServerConfig,
@@ -33,13 +35,11 @@ import {
33 VideoDetails, 35 VideoDetails,
34 VideoPrivacy 36 VideoPrivacy
35} from '@shared/models' 37} from '@shared/models'
38import { VideoSource } from '@shared/models/videos/video-source'
36import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' 39import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
37import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 40import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
38import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component' 41import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component'
39import { VideoEditType } from './video-edit.type' 42import { VideoEditType } from './video-edit.type'
40import { VideoSource } from '@shared/models/videos/video-source'
41import { logger } from '@root-helpers/logger'
42import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
43 43
44type VideoLanguages = VideoConstant<string> & { group?: string } 44type VideoLanguages = VideoConstant<string> & { group?: string }
45type PluginField = { 45type PluginField = {
@@ -66,7 +66,8 @@ export class VideoEditComponent implements OnInit, OnDestroy {
66 @Input() videoCaptions: VideoCaptionWithPathEdit[] = [] 66 @Input() videoCaptions: VideoCaptionWithPathEdit[] = []
67 @Input() videoSource: VideoSource 67 @Input() videoSource: VideoSource
68 68
69 @Input() waitTranscodingEnabled = true 69 @Input() hideWaitTranscoding = false
70
70 @Input() type: VideoEditType 71 @Input() type: VideoEditType
71 @Input() liveVideo: LiveVideo 72 @Input() liveVideo: LiveVideo
72 73
@@ -140,7 +141,7 @@ export class VideoEditComponent implements OnInit, OnDestroy {
140 nsfw: 'false', 141 nsfw: 'false',
141 commentsEnabled: this.serverConfig.defaults.publish.commentsEnabled, 142 commentsEnabled: this.serverConfig.defaults.publish.commentsEnabled,
142 downloadEnabled: this.serverConfig.defaults.publish.downloadEnabled, 143 downloadEnabled: this.serverConfig.defaults.publish.downloadEnabled,
143 waitTranscoding: 'true', 144 waitTranscoding: true,
144 licence: this.serverConfig.defaults.publish.licence, 145 licence: this.serverConfig.defaults.publish.licence,
145 tags: [] 146 tags: []
146 } 147 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html
index 2fb29303f..e23fd77c7 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.html
@@ -53,7 +53,7 @@
53<form [hidden]="!isInUpdateForm" novalidate [formGroup]="form"> 53<form [hidden]="!isInUpdateForm" novalidate [formGroup]="form">
54 <my-video-edit 54 <my-video-edit
55 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" 55 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
56 [forbidScheduledPublication]="true" [waitTranscodingEnabled]="isWaitTranscodingEnabled()" 56 [forbidScheduledPublication]="true" [hideWaitTranscoding]="true"
57 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" [liveVideo]="liveVideo" 57 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" [liveVideo]="liveVideo"
58 type="go-live" 58 type="go-live"
59 ></my-video-edit> 59 ></my-video-edit>
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
index 344b99ea2..83a6b2229 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
@@ -3,7 +3,7 @@ import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' 4import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
5import { scrollToTop } from '@app/helpers' 5import { scrollToTop } from '@app/helpers'
6import { FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactiveService } from '@app/shared/shared-forms'
7import { Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 7import { Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
8import { LiveVideoService } from '@app/shared/shared-video-live' 8import { LiveVideoService } from '@app/shared/shared-video-live'
9import { LoadingBarService } from '@ngx-loading-bar/core' 9import { LoadingBarService } from '@ngx-loading-bar/core'
@@ -39,7 +39,7 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
39 error: string 39 error: string
40 40
41 constructor ( 41 constructor (
42 protected formValidatorService: FormValidatorService, 42 protected formReactiveService: FormReactiveService,
43 protected loadingBar: LoadingBarService, 43 protected loadingBar: LoadingBarService,
44 protected notifier: Notifier, 44 protected notifier: Notifier,
45 protected authService: AuthService, 45 protected authService: AuthService,
@@ -160,10 +160,6 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
160 return this.serverConfig.live.maxDuration / 1000 160 return this.serverConfig.live.maxDuration / 1000
161 } 161 }
162 162
163 isWaitTranscodingEnabled () {
164 return this.form.value['saveReplay'] === true
165 }
166
167 getNormalLiveDescription () { 163 getNormalLiveDescription () {
168 if (this.isReplayAllowed()) { 164 if (this.isReplayAllowed()) {
169 return $localize`Stream only once, replay will replace your live` 165 return $localize`Stream only once, replay will replace your live`
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 7b9531d27..4a1408a4a 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
@@ -3,7 +3,7 @@ import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, Vie
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' 4import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
5import { scrollToTop } from '@app/helpers' 5import { scrollToTop } from '@app/helpers'
6import { FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactiveService } from '@app/shared/shared-forms'
7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 7import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
8import { LoadingBarService } from '@ngx-loading-bar/core' 8import { LoadingBarService } from '@ngx-loading-bar/core'
9import { logger } from '@root-helpers/logger' 9import { logger } from '@root-helpers/logger'
@@ -35,7 +35,7 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
35 error: string 35 error: string
36 36
37 constructor ( 37 constructor (
38 protected formValidatorService: FormValidatorService, 38 protected formReactiveService: FormReactiveService,
39 protected loadingBar: LoadingBarService, 39 protected loadingBar: LoadingBarService,
40 protected notifier: Notifier, 40 protected notifier: Notifier,
41 protected authService: AuthService, 41 protected authService: AuthService,
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
index 422f0c643..502f3818e 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-url.component.ts
@@ -4,7 +4,7 @@ import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular
4import { Router } from '@angular/router' 4import { Router } from '@angular/router'
5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' 5import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
6import { scrollToTop } from '@app/helpers' 6import { scrollToTop } from '@app/helpers'
7import { FormValidatorService } from '@app/shared/shared-forms' 7import { FormReactiveService } from '@app/shared/shared-forms'
8import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 8import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
9import { LoadingBarService } from '@ngx-loading-bar/core' 9import { LoadingBarService } from '@ngx-loading-bar/core'
10import { logger } from '@root-helpers/logger' 10import { logger } from '@root-helpers/logger'
@@ -34,7 +34,7 @@ export class VideoImportUrlComponent extends VideoSend implements OnInit, AfterV
34 error: string 34 error: string
35 35
36 constructor ( 36 constructor (
37 protected formValidatorService: FormValidatorService, 37 protected formReactiveService: FormReactiveService,
38 protected loadingBar: LoadingBarService, 38 protected loadingBar: LoadingBarService,
39 protected notifier: Notifier, 39 protected notifier: Notifier,
40 protected authService: AuthService, 40 protected authService: AuthService,
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
index 728884986..779d42e0c 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.html
@@ -76,10 +76,8 @@
76 </div> 76 </div>
77 </div> 77 </div>
78 78
79 <div class="btn-group" role="group"> 79 <input type="button" class="peertube-button grey-button ms-1" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" />
80 <input type="button" class="btn" i18n-value="Retry failed upload of a video" value="Retry" (click)="retryUpload()" /> 80 <input type="button" class="peertube-button grey-button ms-1" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" />
81 <input type="button" class="btn" i18n-value="Cancel ongoing upload of a video" value="Cancel" (click)="cancelUpload()" />
82 </div>
83</div> 81</div>
84 82
85<div *ngIf="error && !enableRetryAfterError" class="alert alert-danger"> 83<div *ngIf="error && !enableRetryAfterError" class="alert alert-danger">
@@ -96,7 +94,7 @@
96 <my-video-edit 94 <my-video-edit
97 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions" 95 [form]="form" [formErrors]="formErrors" [videoCaptions]="videoCaptions"
98 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 96 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
99 [waitTranscodingEnabled]="true" [forbidScheduledPublication]="false" 97 [forbidScheduledPublication]="false"
100 type="upload" 98 type="upload"
101 ></my-video-edit> 99 ></my-video-edit>
102 100
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss
index 35c626ec2..52a77f83f 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.scss
@@ -42,10 +42,4 @@
42 } 42 }
43 } 43 }
44 } 44 }
45
46 input {
47 @include peertube-button;
48 @include grey-button;
49 @include margin-left(10px);
50 }
51} 45}
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 66a3967c7..967fa9ed1 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
@@ -5,7 +5,7 @@ import { AfterViewInit, Component, ElementRef, EventEmitter, OnDestroy, OnInit,
5import { ActivatedRoute, Router } from '@angular/router' 5import { ActivatedRoute, Router } from '@angular/router'
6import { AuthService, CanComponentDeactivate, HooksService, MetaService, Notifier, ServerService, UserService } from '@app/core' 6import { AuthService, CanComponentDeactivate, HooksService, MetaService, Notifier, ServerService, UserService } from '@app/core'
7import { genericUploadErrorHandler, scrollToTop } from '@app/helpers' 7import { genericUploadErrorHandler, scrollToTop } from '@app/helpers'
8import { FormValidatorService } from '@app/shared/shared-forms' 8import { FormReactiveService } from '@app/shared/shared-forms'
9import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' 9import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
10import { LoadingBarService } from '@ngx-loading-bar/core' 10import { LoadingBarService } from '@ngx-loading-bar/core'
11import { logger } from '@root-helpers/logger' 11import { logger } from '@root-helpers/logger'
@@ -13,6 +13,7 @@ import { isIOS } from '@root-helpers/web-browser'
13import { HttpStatusCode, VideoCreateResult } from '@shared/models' 13import { HttpStatusCode, VideoCreateResult } from '@shared/models'
14import { UploaderXFormData } from './uploaderx-form-data' 14import { UploaderXFormData } from './uploaderx-form-data'
15import { VideoSend } from './video-send' 15import { VideoSend } from './video-send'
16import { Subscription } from 'rxjs'
16 17
17@Component({ 18@Component({
18 selector: 'my-video-upload', 19 selector: 'my-video-upload',
@@ -56,8 +57,10 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
56 57
57 private alreadyRefreshedToken = false 58 private alreadyRefreshedToken = false
58 59
60 private uploadServiceSubscription: Subscription
61
59 constructor ( 62 constructor (
60 protected formValidatorService: FormValidatorService, 63 protected formReactiveService: FormReactiveService,
61 protected loadingBar: LoadingBarService, 64 protected loadingBar: LoadingBarService,
62 protected notifier: Notifier, 65 protected notifier: Notifier,
63 protected authService: AuthService, 66 protected authService: AuthService,
@@ -87,7 +90,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
87 this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily 90 this.userVideoQuotaUsedDaily = data.videoQuotaUsedDaily
88 }) 91 })
89 92
90 this.resumableUploadService.events 93 this.uploadServiceSubscription = this.resumableUploadService.events
91 .subscribe(state => this.onUploadVideoOngoing(state)) 94 .subscribe(state => this.onUploadVideoOngoing(state))
92 } 95 }
93 96
@@ -96,7 +99,9 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
96 } 99 }
97 100
98 ngOnDestroy () { 101 ngOnDestroy () {
99 this.cancelUpload() 102 this.resumableUploadService.disconnect()
103
104 if (this.uploadServiceSubscription) this.uploadServiceSubscription.unsubscribe()
100 } 105 }
101 106
102 canDeactivate () { 107 canDeactivate () {
@@ -131,7 +136,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
131 onUploadVideoOngoing (state: UploadState) { 136 onUploadVideoOngoing (state: UploadState) {
132 switch (state.status) { 137 switch (state.status) {
133 case 'error': { 138 case 'error': {
134 if (!this.alreadyRefreshedToken && state.response.status === HttpStatusCode.UNAUTHORIZED_401) { 139 if (!this.alreadyRefreshedToken && state.responseStatus === HttpStatusCode.UNAUTHORIZED_401) {
135 this.alreadyRefreshedToken = true 140 this.alreadyRefreshedToken = true
136 141
137 return this.refereshTokenAndRetryUpload() 142 return this.refereshTokenAndRetryUpload()
diff --git a/client/src/app/+videos/+video-edit/video-add.component.ts b/client/src/app/+videos/+video-edit/video-add.component.ts
index 25203de1b..460c37a38 100644
--- a/client/src/app/+videos/+video-edit/video-add.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add.component.ts
@@ -143,7 +143,7 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
143 return text 143 return text
144 } 144 }
145 145
146 canDeactivate (): { canDeactivate: boolean, text?: string} { 146 canDeactivate (): { canDeactivate: boolean, text?: string } {
147 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate() 147 if (this.secondStepType === 'upload') return this.videoUpload.canDeactivate()
148 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate() 148 if (this.secondStepType === 'import-url') return this.videoImportUrl.canDeactivate()
149 if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate() 149 if (this.secondStepType === 'import-torrent') return this.videoImportTorrent.canDeactivate()
diff --git a/client/src/app/+videos/+video-edit/video-update.component.html b/client/src/app/+videos/+video-edit/video-update.component.html
index a33ac3db4..af564aeb0 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.html
+++ b/client/src/app/+videos/+video-edit/video-update.component.html
@@ -9,7 +9,7 @@
9 <my-video-edit 9 <my-video-edit
10 [form]="form" [formErrors]="formErrors" [forbidScheduledPublication]="forbidScheduledPublication" 10 [form]="form" [formErrors]="formErrors" [forbidScheduledPublication]="forbidScheduledPublication"
11 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels" 11 [validationMessages]="validationMessages" [userVideoChannels]="userVideoChannels"
12 [videoCaptions]="videoCaptions" [waitTranscodingEnabled]="isWaitTranscodingEnabled()" 12 [videoCaptions]="videoCaptions" [hideWaitTranscoding]="isWaitTranscodingHidden()"
13 type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()" 13 type="update" (pluginFieldsAdded)="hydratePluginFieldsFromVideo()"
14 [liveVideo]="liveVideo" [videoToUpdate]="videoDetails" 14 [liveVideo]="liveVideo" [videoToUpdate]="videoDetails"
15 [videoSource]="videoSource" 15 [videoSource]="videoSource"
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts
index ed17dff06..02398a036 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -4,7 +4,7 @@ import { SelectChannelItem } from 'src/types/select-options-item.model'
4import { Component, HostListener, OnInit } from '@angular/core' 4import { Component, HostListener, OnInit } from '@angular/core'
5import { ActivatedRoute, Router } from '@angular/router' 5import { ActivatedRoute, Router } from '@angular/router'
6import { Notifier } from '@app/core' 6import { Notifier } from '@app/core'
7import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 7import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
8import { Video, VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' 8import { Video, VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main'
9import { LiveVideoService } from '@app/shared/shared-video-live' 9import { LiveVideoService } from '@app/shared/shared-video-live'
10import { LoadingBarService } from '@ngx-loading-bar/core' 10import { LoadingBarService } from '@ngx-loading-bar/core'
@@ -28,12 +28,11 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
28 28
29 isUpdatingVideo = false 29 isUpdatingVideo = false
30 forbidScheduledPublication = false 30 forbidScheduledPublication = false
31 waitTranscodingEnabled = true
32 31
33 private updateDone = false 32 private updateDone = false
34 33
35 constructor ( 34 constructor (
36 protected formValidatorService: FormValidatorService, 35 protected formReactiveService: FormReactiveService,
37 private route: ActivatedRoute, 36 private route: ActivatedRoute,
38 private router: Router, 37 private router: Router,
39 private notifier: Notifier, 38 private notifier: Notifier,
@@ -96,16 +95,12 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
96 return { canDeactivate: this.formChanged === false, text } 95 return { canDeactivate: this.formChanged === false, text }
97 } 96 }
98 97
99 isWaitTranscodingEnabled () { 98 isWaitTranscodingHidden () {
100 if (this.videoDetails.getFiles().length > 1) { // Already transcoded 99 if (this.videoDetails.getFiles().length > 1) { // Already transcoded
101 return false 100 return true
102 } 101 }
103 102
104 if (this.liveVideo && this.form.value['saveReplay'] !== true) { 103 return false
105 return false
106 }
107
108 return true
109 } 104 }
110 105
111 async update () { 106 async update () {
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts
index fd3614297..9a9bfe710 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment-add.component.ts
@@ -16,7 +16,7 @@ import {
16import { Router } from '@angular/router' 16import { Router } from '@angular/router'
17import { Notifier, User } from '@app/core' 17import { Notifier, User } from '@app/core'
18import { VIDEO_COMMENT_TEXT_VALIDATOR } from '@app/shared/form-validators/video-comment-validators' 18import { VIDEO_COMMENT_TEXT_VALIDATOR } from '@app/shared/form-validators/video-comment-validators'
19import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 19import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
20import { Video } from '@app/shared/shared-main' 20import { Video } from '@app/shared/shared-main'
21import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment' 21import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment'
22import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 22import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@@ -48,7 +48,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
48 private emojiMarkupList: { emoji: string, name: string }[] 48 private emojiMarkupList: { emoji: string, name: string }[]
49 49
50 constructor ( 50 constructor (
51 protected formValidatorService: FormValidatorService, 51 protected formReactiveService: FormReactiveService,
52 private notifier: Notifier, 52 private notifier: Notifier,
53 private videoCommentService: VideoCommentService, 53 private videoCommentService: VideoCommentService,
54 private modalService: NgbModal, 54 private modalService: NgbModal,
@@ -148,7 +148,7 @@ export class VideoCommentAddComponent extends FormReactive implements OnChanges,
148 error: err => { 148 error: err => {
149 this.addingComment = false 149 this.addingComment = false
150 150
151 this.notifier.error(err.text) 151 this.notifier.error(err.message)
152 } 152 }
153 }) 153 })
154 } 154 }
diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts
index cabea7551..191ec4a28 100644
--- a/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/comment/video-comment.component.ts
@@ -160,7 +160,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
160 private async init () { 160 private async init () {
161 // Before HTML rendering restore line feed for markdown list compatibility 161 // Before HTML rendering restore line feed for markdown list compatibility
162 const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n') 162 const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n')
163 const html = await this.markdownService.textMarkdownToHTML(commentText, true, true) 163 const html = await this.markdownService.textMarkdownToHTML({ markdown: commentText, withHtml: true, withEmoji: true })
164 this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(this.video.shortUUID, html) 164 this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(this.video.shortUUID, html)
165 this.newParentComments = this.parentComments.concat([ this.comment ]) 165 this.newParentComments = this.parentComments.concat([ this.comment ])
166 166
diff --git a/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html b/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html
index b64d45564..7677ae836 100644
--- a/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html
+++ b/client/src/app/+videos/+video-watch/shared/information/privacy-concerns.component.html
@@ -1,7 +1,7 @@
1<div class="privacy-concerns" *ngIf="display"> 1<div class="privacy-concerns" *ngIf="display">
2 <div class="privacy-concerns-text"> 2 <div class="privacy-concerns-text">
3 <span class="me-2"> 3 <span class="me-2">
4 <strong i18n>Friendly Reminder: </strong> 4 <strong i18n>Friendly Reminder:</strong>&#32;
5 <ng-container i18n> 5 <ng-container i18n>
6 the sharing system used for this video implies that some technical information about your system (such as a public IP address) can be sent to other peers. 6 the sharing system used for this video implies that some technical information about your system (such as a public IP address) can be sent to other peers.
7 </ng-container> 7 </ng-container>
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html
index fa4dbb3ca..d847daff7 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.html
@@ -1,7 +1,7 @@
1<div class="video-info-description"> 1<div class="video-info-description">
2 <div 2 <div
3 class="video-info-description-html" 3 class="video-info-description-html"
4 [innerHTML]="videoHTMLDescription" 4 [innerHTML]="getHTMLDescription()"
5 (timestampClicked)="onTimestampClicked($event)" 5 (timestampClicked)="onTimestampClicked($event)"
6 myTimestampRouteTransformer 6 myTimestampRouteTransformer
7 ></div> 7 ></div>
diff --git a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts
index b5444facb..d01080611 100644
--- a/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts
+++ b/client/src/app/+videos/+video-watch/shared/metadata/video-description.component.ts
@@ -15,8 +15,10 @@ export class VideoDescriptionComponent implements OnChanges {
15 15
16 descriptionLoading = false 16 descriptionLoading = false
17 completeDescriptionShown = false 17 completeDescriptionShown = false
18 completeVideoDescription: string 18
19 shortVideoDescription: string 19 completeVideoDescriptionLoaded = false
20
21 videoHTMLTruncatedDescription = ''
20 videoHTMLDescription = '' 22 videoHTMLDescription = ''
21 23
22 constructor ( 24 constructor (
@@ -28,22 +30,19 @@ export class VideoDescriptionComponent implements OnChanges {
28 ngOnChanges () { 30 ngOnChanges () {
29 this.descriptionLoading = false 31 this.descriptionLoading = false
30 this.completeDescriptionShown = false 32 this.completeDescriptionShown = false
31 this.completeVideoDescription = undefined
32 33
33 this.setVideoDescriptionHTML() 34 this.setVideoDescriptionHTML()
34 } 35 }
35 36
36 showMoreDescription () { 37 showMoreDescription () {
37 if (this.completeVideoDescription === undefined) { 38 if (!this.completeVideoDescriptionLoaded) {
38 return this.loadCompleteDescription() 39 return this.loadCompleteDescription()
39 } 40 }
40 41
41 this.updateVideoDescription(this.completeVideoDescription)
42 this.completeDescriptionShown = true 42 this.completeDescriptionShown = true
43 } 43 }
44 44
45 showLessDescription () { 45 showLessDescription () {
46 this.updateVideoDescription(this.shortVideoDescription)
47 this.completeDescriptionShown = false 46 this.completeDescriptionShown = false
48 } 47 }
49 48
@@ -56,10 +55,10 @@ export class VideoDescriptionComponent implements OnChanges {
56 this.completeDescriptionShown = true 55 this.completeDescriptionShown = true
57 this.descriptionLoading = false 56 this.descriptionLoading = false
58 57
59 this.shortVideoDescription = this.video.description 58 this.video.description = description
60 this.completeVideoDescription = description
61 59
62 this.updateVideoDescription(this.completeVideoDescription) 60 this.setVideoDescriptionHTML()
61 .catch(err => logger.error(err))
63 }, 62 },
64 63
65 error: err => { 64 error: err => {
@@ -73,15 +72,25 @@ export class VideoDescriptionComponent implements OnChanges {
73 this.timestampClicked.emit(timestamp) 72 this.timestampClicked.emit(timestamp)
74 } 73 }
75 74
76 private updateVideoDescription (description: string) { 75 getHTMLDescription () {
77 this.video.description = description 76 if (this.completeDescriptionShown) {
78 this.setVideoDescriptionHTML() 77 return this.videoHTMLDescription
79 .catch(err => logger.error(err)) 78 }
79
80 return this.videoHTMLTruncatedDescription
80 } 81 }
81 82
82 private async setVideoDescriptionHTML () { 83 private async setVideoDescriptionHTML () {
83 const html = await this.markdownService.textMarkdownToHTML(this.video.description) 84 {
85 const html = await this.markdownService.textMarkdownToHTML({ markdown: this.video.description })
84 86
85 this.videoHTMLDescription = this.markdownService.processVideoTimestamps(this.video.shortUUID, html) 87 this.videoHTMLDescription = this.markdownService.processVideoTimestamps(this.video.shortUUID, html)
88 }
89
90 {
91 const html = await this.markdownService.textMarkdownToHTML({ markdown: this.video.truncatedDescription })
92
93 this.videoHTMLTruncatedDescription = this.markdownService.processVideoTimestamps(this.video.shortUUID, html)
94 }
86 } 95 }
87} 96}
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 9ae6f9f12..94853423b 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -20,12 +20,12 @@ import {
20} from '@app/core' 20} from '@app/core'
21import { HooksService } from '@app/core/plugins/hooks.service' 21import { HooksService } from '@app/core/plugins/hooks.service'
22import { isXPercentInViewport, scrollToTop } from '@app/helpers' 22import { isXPercentInViewport, scrollToTop } from '@app/helpers'
23import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' 23import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main'
24import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 24import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
25import { LiveVideoService } from '@app/shared/shared-video-live' 25import { LiveVideoService } from '@app/shared/shared-video-live'
26import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 26import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
27import { logger } from '@root-helpers/logger' 27import { logger } from '@root-helpers/logger'
28import { isP2PEnabled } from '@root-helpers/video' 28import { isP2PEnabled, videoRequiresAuth } from '@root-helpers/video'
29import { timeToInt } from '@shared/core-utils' 29import { timeToInt } from '@shared/core-utils'
30import { 30import {
31 HTMLServerConfig, 31 HTMLServerConfig,
@@ -78,6 +78,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
78 private nextVideoUUID = '' 78 private nextVideoUUID = ''
79 private nextVideoTitle = '' 79 private nextVideoTitle = ''
80 80
81 private videoFileToken: string
82
81 private currentTime: number 83 private currentTime: number
82 84
83 private paramsSub: Subscription 85 private paramsSub: Subscription
@@ -110,6 +112,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
110 private pluginService: PluginService, 112 private pluginService: PluginService,
111 private peertubeSocket: PeerTubeSocket, 113 private peertubeSocket: PeerTubeSocket,
112 private screenService: ScreenService, 114 private screenService: ScreenService,
115 private videoFileTokenService: VideoFileTokenService,
113 private location: PlatformLocation, 116 private location: PlatformLocation,
114 @Inject(LOCALE_ID) private localeId: string 117 @Inject(LOCALE_ID) private localeId: string
115 ) { } 118 ) { }
@@ -177,7 +180,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
177 } 180 }
178 181
179 onPlaylistVideoFound (videoId: string) { 182 onPlaylistVideoFound (videoId: string) {
180 this.loadVideo(videoId) 183 this.loadVideo({ videoId, forceAutoplay: false })
181 } 184 }
182 185
183 onPlaylistNoVideoFound () { 186 onPlaylistNoVideoFound () {
@@ -209,7 +212,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
209 private loadRouteParams () { 212 private loadRouteParams () {
210 this.paramsSub = this.route.params.subscribe(routeParams => { 213 this.paramsSub = this.route.params.subscribe(routeParams => {
211 const videoId = routeParams['videoId'] 214 const videoId = routeParams['videoId']
212 if (videoId) return this.loadVideo(videoId) 215 if (videoId) return this.loadVideo({ videoId, forceAutoplay: false })
213 216
214 const playlistId = routeParams['playlistId'] 217 const playlistId = routeParams['playlistId']
215 if (playlistId) return this.loadPlaylist(playlistId) 218 if (playlistId) return this.loadPlaylist(playlistId)
@@ -237,7 +240,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
237 }) 240 })
238 } 241 }
239 242
240 private loadVideo (videoId: string) { 243 private loadVideo (options: {
244 videoId: string
245 forceAutoplay: boolean
246 }) {
247 const { videoId, forceAutoplay } = options
248
241 if (this.isSameElement(this.video, videoId)) return 249 if (this.isSameElement(this.video, videoId)) return
242 250
243 if (this.player) this.player.pause() 251 if (this.player) this.player.pause()
@@ -252,12 +260,19 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
252 'filter:api.video-watch.video.get.result' 260 'filter:api.video-watch.video.get.result'
253 ) 261 )
254 262
255 const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo }> = videoObs.pipe( 263 const videoAndLiveObs: Observable<{ video: VideoDetails, live?: LiveVideo, videoFileToken?: string }> = videoObs.pipe(
256 switchMap(video => { 264 switchMap(video => {
257 if (!video.isLive) return of({ video }) 265 if (!video.isLive) return of({ video, live: undefined })
258 266
259 return this.liveVideoService.getVideoLive(video.uuid) 267 return this.liveVideoService.getVideoLive(video.uuid)
260 .pipe(map(live => ({ live, video }))) 268 .pipe(map(live => ({ live, video })))
269 }),
270
271 switchMap(({ video, live }) => {
272 if (!videoRequiresAuth(video)) return of({ video, live, videoFileToken: undefined })
273
274 return this.videoFileTokenService.getVideoFileToken(video.uuid)
275 .pipe(map(({ token }) => ({ video, live, videoFileToken: token })))
261 }) 276 })
262 ) 277 )
263 278
@@ -266,7 +281,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
266 this.videoCaptionService.listCaptions(videoId), 281 this.videoCaptionService.listCaptions(videoId),
267 this.userService.getAnonymousOrLoggedUser() 282 this.userService.getAnonymousOrLoggedUser()
268 ]).subscribe({ 283 ]).subscribe({
269 next: ([ { video, live }, captionsResult, loggedInOrAnonymousUser ]) => { 284 next: ([ { video, live, videoFileToken }, captionsResult, loggedInOrAnonymousUser ]) => {
270 const queryParams = this.route.snapshot.queryParams 285 const queryParams = this.route.snapshot.queryParams
271 286
272 const urlOptions = { 287 const urlOptions = {
@@ -283,8 +298,15 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
283 peertubeLink: false 298 peertubeLink: false
284 } 299 }
285 300
286 this.onVideoFetched({ video, live, videoCaptions: captionsResult.data, loggedInOrAnonymousUser, urlOptions }) 301 this.onVideoFetched({
287 .catch(err => this.handleGlobalError(err)) 302 video,
303 live,
304 videoCaptions: captionsResult.data,
305 videoFileToken,
306 loggedInOrAnonymousUser,
307 urlOptions,
308 forceAutoplay
309 }).catch(err => this.handleGlobalError(err))
288 }, 310 },
289 311
290 error: err => this.handleRequestError(err) 312 error: err => this.handleRequestError(err)
@@ -356,16 +378,20 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
356 video: VideoDetails 378 video: VideoDetails
357 live: LiveVideo 379 live: LiveVideo
358 videoCaptions: VideoCaption[] 380 videoCaptions: VideoCaption[]
381 videoFileToken: string
382
359 urlOptions: URLOptions 383 urlOptions: URLOptions
360 loggedInOrAnonymousUser: User 384 loggedInOrAnonymousUser: User
385 forceAutoplay: boolean
361 }) { 386 }) {
362 const { video, live, videoCaptions, urlOptions, loggedInOrAnonymousUser } = options 387 const { video, live, videoCaptions, urlOptions, videoFileToken, loggedInOrAnonymousUser, forceAutoplay } = options
363 388
364 this.subscribeToLiveEventsIfNeeded(this.video, video) 389 this.subscribeToLiveEventsIfNeeded(this.video, video)
365 390
366 this.video = video 391 this.video = video
367 this.videoCaptions = videoCaptions 392 this.videoCaptions = videoCaptions
368 this.liveVideo = live 393 this.liveVideo = live
394 this.videoFileToken = videoFileToken
369 395
370 // Re init attributes 396 // Re init attributes
371 this.playerPlaceholderImgSrc = undefined 397 this.playerPlaceholderImgSrc = undefined
@@ -380,7 +406,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
380 if (res === false) return this.location.back() 406 if (res === false) return this.location.back()
381 } 407 }
382 408
383 this.buildPlayer(urlOptions, loggedInOrAnonymousUser) 409 this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay })
384 .catch(err => logger.error('Cannot build the player', err)) 410 .catch(err => logger.error('Cannot build the player', err))
385 411
386 this.setOpenGraphTags() 412 this.setOpenGraphTags()
@@ -393,7 +419,13 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
393 this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions) 419 this.hooks.runAction('action:video-watch.video.loaded', 'video-watch', hookOptions)
394 } 420 }
395 421
396 private async buildPlayer (urlOptions: URLOptions, loggedInOrAnonymousUser: User) { 422 private async buildPlayer (options: {
423 urlOptions: URLOptions
424 loggedInOrAnonymousUser: User
425 forceAutoplay: boolean
426 }) {
427 const { urlOptions, loggedInOrAnonymousUser, forceAutoplay } = options
428
397 // Flush old player if needed 429 // Flush old player if needed
398 this.flushPlayer() 430 this.flushPlayer()
399 431
@@ -414,8 +446,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
414 video: this.video, 446 video: this.video,
415 videoCaptions: this.videoCaptions, 447 videoCaptions: this.videoCaptions,
416 liveVideo: this.liveVideo, 448 liveVideo: this.liveVideo,
449 videoFileToken: this.videoFileToken,
417 urlOptions, 450 urlOptions,
418 loggedInOrAnonymousUser, 451 loggedInOrAnonymousUser,
452 forceAutoplay,
419 user: this.user 453 user: this.user
420 } 454 }
421 const { playerMode, playerOptions } = await this.hooks.wrapFun( 455 const { playerMode, playerOptions } = await this.hooks.wrapFun(
@@ -561,11 +595,16 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
561 video: VideoDetails 595 video: VideoDetails
562 liveVideo: LiveVideo 596 liveVideo: LiveVideo
563 videoCaptions: VideoCaption[] 597 videoCaptions: VideoCaption[]
598
599 videoFileToken: string
600
564 urlOptions: CustomizationOptions & { playerMode: PlayerMode } 601 urlOptions: CustomizationOptions & { playerMode: PlayerMode }
602
565 loggedInOrAnonymousUser: User 603 loggedInOrAnonymousUser: User
604 forceAutoplay: boolean
566 user?: AuthUser // Keep for plugins 605 user?: AuthUser // Keep for plugins
567 }) { 606 }) {
568 const { video, liveVideo, videoCaptions, urlOptions, loggedInOrAnonymousUser } = params 607 const { video, liveVideo, videoCaptions, videoFileToken, urlOptions, loggedInOrAnonymousUser, forceAutoplay } = params
569 608
570 const getStartTime = () => { 609 const getStartTime = () => {
571 const byUrl = urlOptions.startTime !== undefined 610 const byUrl = urlOptions.startTime !== undefined
@@ -597,6 +636,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
597 const options: PeertubePlayerManagerOptions = { 636 const options: PeertubePlayerManagerOptions = {
598 common: { 637 common: {
599 autoplay: this.isAutoplay(), 638 autoplay: this.isAutoplay(),
639 forceAutoplay,
600 p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled), 640 p2pEnabled: isP2PEnabled(video, this.serverConfig, loggedInOrAnonymousUser.p2pEnabled),
601 641
602 hasNextVideo: () => this.hasNextVideo(), 642 hasNextVideo: () => this.hasNextVideo(),
@@ -623,13 +663,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
623 theaterButton: true, 663 theaterButton: true,
624 captions: videoCaptions.length !== 0, 664 captions: videoCaptions.length !== 0,
625 665
626 videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE
627 ? this.videoService.getVideoViewUrl(video.uuid)
628 : null,
629 authorizationHeader: this.authService.getRequestHeaderValue(),
630
631 metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
632
633 embedUrl: video.embedUrl, 666 embedUrl: video.embedUrl,
634 embedTitle: video.name, 667 embedTitle: video.name,
635 instanceName: this.serverConfig.instance.name, 668 instanceName: this.serverConfig.instance.name,
@@ -639,7 +672,17 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
639 672
640 language: this.localeId, 673 language: this.localeId,
641 674
642 serverUrl: environment.apiUrl, 675 metricsUrl: environment.apiUrl + '/api/v1/metrics/playback',
676
677 videoViewUrl: video.privacy.id !== VideoPrivacy.PRIVATE
678 ? this.videoService.getVideoViewUrl(video.uuid)
679 : null,
680 authorizationHeader: () => this.authService.getRequestHeaderValue(),
681
682 serverUrl: environment.originServerUrl || window.location.origin,
683
684 videoFileToken: () => videoFileToken,
685 requiresAuth: videoRequiresAuth(video),
643 686
644 videoCaptions: playerCaptions, 687 videoCaptions: playerCaptions,
645 688
@@ -728,7 +771,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
728 771
729 // Reset to force refresh the video 772 // Reset to force refresh the video
730 this.video = undefined 773 this.video = undefined
731 this.loadVideo(videoUUID) 774 this.loadVideo({ videoId: videoUUID, forceAutoplay: true })
732 } 775 }
733 776
734 private handleLiveViewsChange (newViewers: number) { 777 private handleLiveViewsChange (newViewers: number) {
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index a2ad4806c..e621ce432 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -206,7 +206,7 @@ export class AppComponent implements OnInit, AfterViewInit {
206 } 206 }
207 207
208 this.broadcastMessage = { 208 this.broadcastMessage = {
209 message: await this.markdownService.unsafeMarkdownToHTML(messageConfig.message, true), 209 message: await this.markdownService.markdownToUnsafeHTML({ markdown: messageConfig.message }),
210 dismissable: messageConfig.dismissable, 210 dismissable: messageConfig.dismissable,
211 class: classes[messageConfig.level] 211 class: classes[messageConfig.level]
212 } 212 }
@@ -247,12 +247,12 @@ export class AppComponent implements OnInit, AfterViewInit {
247 247
248 // Admin modal 248 // Admin modal
249 userSub.pipe( 249 userSub.pipe(
250 filter(user => user.role === UserRole.ADMINISTRATOR) 250 filter(user => user.role.id === UserRole.ADMINISTRATOR)
251 ).subscribe(user => this.openAdminModalsIfNeeded(user)) 251 ).subscribe(user => this.openAdminModalsIfNeeded(user))
252 252
253 // Account modal 253 // Account modal
254 userSub.pipe( 254 userSub.pipe(
255 filter(user => user.role !== UserRole.ADMINISTRATOR) 255 filter(user => user.role.id !== UserRole.ADMINISTRATOR)
256 ).subscribe(user => this.openAccountModalsIfNeeded(user)) 256 ).subscribe(user => this.openAccountModalsIfNeeded(user))
257 } 257 }
258 258
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts
index cd9665e37..226075265 100644
--- a/client/src/app/core/auth/auth-user.model.ts
+++ b/client/src/app/core/auth/auth-user.model.ts
@@ -1,7 +1,7 @@
1import { Observable, of } from 'rxjs' 1import { Observable, of } from 'rxjs'
2import { map } from 'rxjs/operators' 2import { map } from 'rxjs/operators'
3import { User } from '@app/core/users/user.model' 3import { User } from '@app/core/users/user.model'
4import { UserTokens } from '@root-helpers/users' 4import { OAuthUserTokens } from '@root-helpers/users'
5import { hasUserRight } from '@shared/core-utils/users' 5import { hasUserRight } from '@shared/core-utils/users'
6import { 6import {
7 MyUser as ServerMyUserModel, 7 MyUser as ServerMyUserModel,
@@ -13,46 +13,46 @@ import {
13} from '@shared/models' 13} from '@shared/models'
14 14
15export class AuthUser extends User implements ServerMyUserModel { 15export class AuthUser extends User implements ServerMyUserModel {
16 tokens: UserTokens 16 oauthTokens: OAuthUserTokens
17 specialPlaylists: MyUserSpecialPlaylist[] 17 specialPlaylists: MyUserSpecialPlaylist[]
18 18
19 canSeeVideosLink = true 19 canSeeVideosLink = true
20 20
21 constructor (userHash: Partial<ServerMyUserModel>, hashTokens: Partial<UserTokens>) { 21 constructor (userHash: Partial<ServerMyUserModel>, hashTokens: Partial<OAuthUserTokens>) {
22 super(userHash) 22 super(userHash)
23 23
24 this.tokens = new UserTokens(hashTokens) 24 this.oauthTokens = new OAuthUserTokens(hashTokens)
25 this.specialPlaylists = userHash.specialPlaylists 25 this.specialPlaylists = userHash.specialPlaylists
26 } 26 }
27 27
28 getAccessToken () { 28 getAccessToken () {
29 return this.tokens.accessToken 29 return this.oauthTokens.accessToken
30 } 30 }
31 31
32 getRefreshToken () { 32 getRefreshToken () {
33 return this.tokens.refreshToken 33 return this.oauthTokens.refreshToken
34 } 34 }
35 35
36 getTokenType () { 36 getTokenType () {
37 return this.tokens.tokenType 37 return this.oauthTokens.tokenType
38 } 38 }
39 39
40 refreshTokens (accessToken: string, refreshToken: string) { 40 refreshTokens (accessToken: string, refreshToken: string) {
41 this.tokens.accessToken = accessToken 41 this.oauthTokens.accessToken = accessToken
42 this.tokens.refreshToken = refreshToken 42 this.oauthTokens.refreshToken = refreshToken
43 } 43 }
44 44
45 hasRight (right: UserRight) { 45 hasRight (right: UserRight) {
46 return hasUserRight(this.role, right) 46 return hasUserRight(this.role.id, right)
47 } 47 }
48 48
49 canManage (user: ServerUserModel) { 49 canManage (user: ServerUserModel) {
50 const myRole = this.role 50 const myRole = this.role.id
51 51
52 if (myRole === UserRole.ADMINISTRATOR) return true 52 if (myRole === UserRole.ADMINISTRATOR) return true
53 53
54 // I'm a moderator: I can only manage users 54 // I'm a moderator: I can only manage users
55 return user.role === UserRole.USER 55 return user.role.id === UserRole.USER
56 } 56 }
57 57
58 computeCanSeeVideosLink (quotaObservable: Observable<UserVideoQuota>): Observable<boolean> { 58 computeCanSeeVideosLink (quotaObservable: Observable<UserVideoQuota>): Observable<boolean> {
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts
index ece6bc5d1..4de28e51e 100644
--- a/client/src/app/core/auth/auth.service.ts
+++ b/client/src/app/core/auth/auth.service.ts
@@ -1,11 +1,11 @@
1import { Hotkey, HotkeysService } from 'angular2-hotkeys' 1import { Hotkey, HotkeysService } from 'angular2-hotkeys'
2import { Observable, ReplaySubject, Subject, throwError as observableThrowError } from 'rxjs' 2import { Observable, ReplaySubject, Subject, throwError as observableThrowError } from 'rxjs'
3import { catchError, map, mergeMap, share, tap } from 'rxjs/operators' 3import { catchError, map, mergeMap, share, tap } from 'rxjs/operators'
4import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' 4import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http'
5import { Injectable } from '@angular/core' 5import { Injectable } from '@angular/core'
6import { Router } from '@angular/router' 6import { Router } from '@angular/router'
7import { Notifier } from '@app/core/notification/notifier.service' 7import { Notifier } from '@app/core/notification/notifier.service'
8import { logger, objectToUrlEncoded, peertubeLocalStorage, UserTokens } from '@root-helpers/index' 8import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index'
9import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' 9import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models'
10import { environment } from '../../../environments/environment' 10import { environment } from '../../../environments/environment'
11import { RestExtractor } from '../rest/rest-extractor.service' 11import { RestExtractor } from '../rest/rest-extractor.service'
@@ -74,7 +74,7 @@ export class AuthService {
74 ] 74 ]
75 } 75 }
76 76
77 buildAuthUser (userInfo: Partial<User>, tokens: UserTokens) { 77 buildAuthUser (userInfo: Partial<User>, tokens: OAuthUserTokens) {
78 this.user = new AuthUser(userInfo, tokens) 78 this.user = new AuthUser(userInfo, tokens)
79 } 79 }
80 80
@@ -97,7 +97,7 @@ export class AuthService {
97 let errorMessage = err.message 97 let errorMessage = err.message
98 98
99 if (err.status === HttpStatusCode.FORBIDDEN_403) { 99 if (err.status === HttpStatusCode.FORBIDDEN_403) {
100 errorMessage = $localize`Cannot retrieve OAuth Client credentials: ${err.text}. 100 errorMessage = $localize`Cannot retrieve OAuth Client credentials: ${err.message}.
101Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.` 101Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.`
102 } 102 }
103 103
@@ -141,7 +141,14 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
141 return !!this.getAccessToken() 141 return !!this.getAccessToken()
142 } 142 }
143 143
144 login (username: string, password: string, token?: string) { 144 login (options: {
145 username: string
146 password: string
147 otpToken?: string
148 token?: string
149 }) {
150 const { username, password, token, otpToken } = options
151
145 // Form url encoded 152 // Form url encoded
146 const body = { 153 const body = {
147 client_id: this.clientId, 154 client_id: this.clientId,
@@ -155,7 +162,9 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
155 162
156 if (token) Object.assign(body, { externalAuthToken: token }) 163 if (token) Object.assign(body, { externalAuthToken: token })
157 164
158 const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') 165 let headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
166 if (otpToken) headers = headers.set('x-peertube-otp', otpToken)
167
159 return this.http.post<UserLogin>(AuthService.BASE_TOKEN_URL, objectToUrlEncoded(body), { headers }) 168 return this.http.post<UserLogin>(AuthService.BASE_TOKEN_URL, objectToUrlEncoded(body), { headers })
160 .pipe( 169 .pipe(
161 map(res => Object.assign(res, { username })), 170 map(res => Object.assign(res, { username })),
@@ -245,6 +254,14 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
245 }) 254 })
246 } 255 }
247 256
257 isOTPMissingError (err: HttpErrorResponse) {
258 if (err.status !== HttpStatusCode.UNAUTHORIZED_401) return false
259
260 if (err.headers.get('x-peertube-otp') !== 'required; app') return false
261
262 return true
263 }
264
248 private mergeUserInformation (obj: UserLoginWithUsername): Observable<UserLoginWithUserInformation> { 265 private mergeUserInformation (obj: UserLoginWithUsername): Observable<UserLoginWithUserInformation> {
249 // User is not loaded yet, set manually auth header 266 // User is not loaded yet, set manually auth header
250 const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`) 267 const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`)
diff --git a/client/src/app/core/confirm/confirm.service.ts b/client/src/app/core/confirm/confirm.service.ts
index 338b8762c..89a25f0a5 100644
--- a/client/src/app/core/confirm/confirm.service.ts
+++ b/client/src/app/core/confirm/confirm.service.ts
@@ -1,28 +1,53 @@
1import { firstValueFrom, Subject } from 'rxjs' 1import { firstValueFrom, map, Observable, Subject } from 'rxjs'
2import { Injectable } from '@angular/core' 2import { Injectable } from '@angular/core'
3 3
4type ConfirmOptions = { 4type ConfirmOptions = {
5 title: string 5 title: string
6 message: string 6 message: string
7 inputLabel?: string 7} & (
8 expectedInputValue?: string 8 {
9 confirmButtonText?: string 9 type: 'confirm'
10} 10 confirmButtonText?: string
11 } |
12 {
13 type: 'confirm-password'
14 confirmButtonText?: string
15 } |
16 {
17 type: 'confirm-expected-input'
18 inputLabel?: string
19 expectedInputValue?: string
20 confirmButtonText?: string
21 }
22)
11 23
12@Injectable() 24@Injectable()
13export class ConfirmService { 25export class ConfirmService {
14 showConfirm = new Subject<ConfirmOptions>() 26 showConfirm = new Subject<ConfirmOptions>()
15 confirmResponse = new Subject<boolean>() 27 confirmResponse = new Subject<{ confirmed: boolean, value?: string }>()
16 28
17 confirm (message: string, title = '', confirmButtonText?: string) { 29 confirm (message: string, title = '', confirmButtonText?: string) {
18 this.showConfirm.next({ title, message, confirmButtonText }) 30 this.showConfirm.next({ type: 'confirm', title, message, confirmButtonText })
31
32 return firstValueFrom(this.extractConfirmed(this.confirmResponse.asObservable()))
33 }
19 34
20 return firstValueFrom(this.confirmResponse.asObservable()) 35 confirmWithPassword (message: string, title = '', confirmButtonText?: string) {
36 this.showConfirm.next({ type: 'confirm-password', title, message, confirmButtonText })
37
38 const obs = this.confirmResponse.asObservable()
39 .pipe(map(({ confirmed, value }) => ({ confirmed, password: value })))
40
41 return firstValueFrom(obs)
21 } 42 }
22 43
23 confirmWithInput (message: string, inputLabel: string, expectedInputValue: string, title = '', confirmButtonText?: string) { 44 confirmWithExpectedInput (message: string, inputLabel: string, expectedInputValue: string, title = '', confirmButtonText?: string) {
24 this.showConfirm.next({ title, message, inputLabel, expectedInputValue, confirmButtonText }) 45 this.showConfirm.next({ type: 'confirm-expected-input', title, message, inputLabel, expectedInputValue, confirmButtonText })
46
47 return firstValueFrom(this.extractConfirmed(this.confirmResponse.asObservable()))
48 }
25 49
26 return firstValueFrom(this.confirmResponse.asObservable()) 50 private extractConfirmed (obs: Observable<{ confirmed: boolean }>) {
51 return obs.pipe(map(({ confirmed }) => confirmed))
27 } 52 }
28} 53}
diff --git a/client/src/app/core/menu/menu.service.ts b/client/src/app/core/menu/menu.service.ts
index 81837db7e..d865c7da2 100644
--- a/client/src/app/core/menu/menu.service.ts
+++ b/client/src/app/core/menu/menu.service.ts
@@ -7,6 +7,7 @@ import { ScreenService } from '../wrappers'
7 7
8export type MenuLink = { 8export type MenuLink = {
9 icon: GlobalIconName 9 icon: GlobalIconName
10 iconClass?: string
10 11
11 label: string 12 label: string
12 // Used by the left menu for example 13 // Used by the left menu for example
@@ -71,6 +72,14 @@ export class MenuService {
71 72
72 if (userCanSeeVideosLink) { 73 if (userCanSeeVideosLink) {
73 links.push({ 74 links.push({
75 path: '/my-library/video-channels',
76 icon: 'channel' as GlobalIconName,
77 iconClass: 'channel-icon',
78 shortLabel: $localize`Channels`,
79 label: $localize`My channels`
80 })
81
82 links.push({
74 path: '/my-library/videos', 83 path: '/my-library/videos',
75 icon: 'videos' as GlobalIconName, 84 icon: 'videos' as GlobalIconName,
76 shortLabel: $localize`Videos`, 85 shortLabel: $localize`Videos`,
diff --git a/client/src/app/core/plugins/plugin.service.ts b/client/src/app/core/plugins/plugin.service.ts
index dadc2a41d..bd8c61d9a 100644
--- a/client/src/app/core/plugins/plugin.service.ts
+++ b/client/src/app/core/plugins/plugin.service.ts
@@ -202,6 +202,11 @@ export class PluginService implements ClientHook {
202 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router` 202 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/router`
203 }, 203 },
204 204
205 getBaseWebSocketRoute: () => {
206 const pathPrefix = PluginsManager.getPluginPathPrefix(pluginInfo.isTheme)
207 return environment.apiUrl + `${pathPrefix}/${plugin.name}/${plugin.version}/ws`
208 },
209
205 getBasePluginClientPath: () => { 210 getBasePluginClientPath: () => {
206 return '/p' 211 return '/p'
207 }, 212 },
@@ -254,11 +259,11 @@ export class PluginService implements ClientHook {
254 259
255 markdownRenderer: { 260 markdownRenderer: {
256 textMarkdownToHTML: (textMarkdown: string) => { 261 textMarkdownToHTML: (textMarkdown: string) => {
257 return this.markdownRenderer.textMarkdownToHTML(textMarkdown) 262 return this.markdownRenderer.textMarkdownToHTML({ markdown: textMarkdown })
258 }, 263 },
259 264
260 enhancedMarkdownToHTML: (enhancedMarkdown: string) => { 265 enhancedMarkdownToHTML: (enhancedMarkdown: string) => {
261 return this.markdownRenderer.enhancedMarkdownToHTML(enhancedMarkdown) 266 return this.markdownRenderer.enhancedMarkdownToHTML({ markdown: enhancedMarkdown })
262 } 267 }
263 }, 268 },
264 269
diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts
index 42e8c4a88..a5fd72862 100644
--- a/client/src/app/core/renderer/markdown.service.ts
+++ b/client/src/app/core/renderer/markdown.service.ts
@@ -62,23 +62,40 @@ export class MarkdownService {
62 62
63 constructor (private htmlRenderer: HtmlRendererService) {} 63 constructor (private htmlRenderer: HtmlRendererService) {}
64 64
65 textMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) { 65 textMarkdownToHTML (options: {
66 markdown: string
67 withHtml?: boolean
68 withEmoji?: boolean
69 }) {
70 const { markdown, withHtml = false, withEmoji = false } = options
71
66 if (withHtml) return this.render({ name: 'textWithHTMLMarkdownIt', markdown, withEmoji }) 72 if (withHtml) return this.render({ name: 'textWithHTMLMarkdownIt', markdown, withEmoji })
67 73
68 return this.render({ name: 'textMarkdownIt', markdown, withEmoji }) 74 return this.render({ name: 'textMarkdownIt', markdown, withEmoji })
69 } 75 }
70 76
71 enhancedMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) { 77 enhancedMarkdownToHTML (options: {
78 markdown: string
79 withHtml?: boolean
80 withEmoji?: boolean
81 }) {
82 const { markdown, withHtml = false, withEmoji = false } = options
83
72 if (withHtml) return this.render({ name: 'enhancedWithHTMLMarkdownIt', markdown, withEmoji }) 84 if (withHtml) return this.render({ name: 'enhancedWithHTMLMarkdownIt', markdown, withEmoji })
73 85
74 return this.render({ name: 'enhancedMarkdownIt', markdown, withEmoji }) 86 return this.render({ name: 'enhancedMarkdownIt', markdown, withEmoji })
75 } 87 }
76 88
77 unsafeMarkdownToHTML (markdown: string, _trustedInput: true) { 89 markdownToUnsafeHTML (options: { markdown: string }) {
78 return this.render({ name: 'unsafeMarkdownIt', markdown, withEmoji: true }) 90 return this.render({ name: 'unsafeMarkdownIt', markdown: options.markdown, withEmoji: true })
79 } 91 }
80 92
81 customPageMarkdownToHTML (markdown: string, additionalAllowedTags: string[]) { 93 customPageMarkdownToHTML (options: {
94 markdown: string
95 additionalAllowedTags: string[]
96 }) {
97 const { markdown, additionalAllowedTags } = options
98
82 return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags }) 99 return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags })
83 } 100 }
84 101
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts
index 7eec2eca6..de3f2bfff 100644
--- a/client/src/app/core/rest/rest-extractor.service.ts
+++ b/client/src/app/core/rest/rest-extractor.service.ts
@@ -4,6 +4,7 @@ import { Router } from '@angular/router'
4import { DateFormat, dateToHuman } from '@app/helpers' 4import { DateFormat, dateToHuman } from '@app/helpers'
5import { logger } from '@root-helpers/logger' 5import { logger } from '@root-helpers/logger'
6import { HttpStatusCode, ResultList } from '@shared/models' 6import { HttpStatusCode, ResultList } from '@shared/models'
7import { HttpHeaderResponse } from '@angular/common/http'
7 8
8@Injectable() 9@Injectable()
9export class RestExtractor { 10export class RestExtractor {
@@ -36,6 +37,8 @@ export class RestExtractor {
36 37
37 convertDateToHuman (target: any, fieldsToConvert: string[], format?: DateFormat) { 38 convertDateToHuman (target: any, fieldsToConvert: string[], format?: DateFormat) {
38 fieldsToConvert.forEach(field => { 39 fieldsToConvert.forEach(field => {
40 if (!target[field]) return
41
39 target[field] = dateToHuman(this.localeId, new Date(target[field]), format) 42 target[field] = dateToHuman(this.localeId, new Date(target[field]), format)
40 }) 43 })
41 44
@@ -54,10 +57,11 @@ export class RestExtractor {
54 handleError (err: any) { 57 handleError (err: any) {
55 const errorMessage = this.buildErrorMessage(err) 58 const errorMessage = this.buildErrorMessage(err)
56 59
57 const errorObj: { message: string, status: string, body: string } = { 60 const errorObj: { message: string, status: string, body: string, headers: HttpHeaderResponse } = {
58 message: errorMessage, 61 message: errorMessage,
59 status: undefined, 62 status: undefined,
60 body: undefined 63 body: undefined,
64 headers: err.headers
61 } 65 }
62 66
63 if (err.status) { 67 if (err.status) {
diff --git a/client/src/app/core/routing/preload-selected-modules-list.ts b/client/src/app/core/routing/preload-selected-modules-list.ts
index b5c3195b0..1abcdb015 100644
--- a/client/src/app/core/routing/preload-selected-modules-list.ts
+++ b/client/src/app/core/routing/preload-selected-modules-list.ts
@@ -1,13 +1,13 @@
1import { Observable, of as ofObservable, timer as observableTimer } from 'rxjs' 1import { Observable, of as ofObservable, timer as observableTimer } from 'rxjs'
2import { switchMap } from 'rxjs/operators' 2import { switchMap } from 'rxjs/operators'
3import { PreloadingStrategy, Route } from '@angular/router'
4import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { PreloadingStrategy, Route } from '@angular/router'
5 5
6@Injectable() 6@Injectable()
7export class PreloadSelectedModulesList implements PreloadingStrategy { 7export class PreloadSelectedModulesList implements PreloadingStrategy {
8 8
9 preload (route: Route, load: () => Observable<any>): Observable<any> { 9 preload (route: Route, load: () => Observable<any>): Observable<any> {
10 if (!route.data || !route.data.preload) return ofObservable(null) 10 if (!route.data?.preload) return ofObservable(null)
11 11
12 if (typeof route.data.preload === 'number') { 12 if (typeof route.data.preload === 'number') {
13 return observableTimer(route.data.preload).pipe(switchMap(() => load())) 13 return observableTimer(route.data.preload).pipe(switchMap(() => load()))
diff --git a/client/src/app/core/users/user-local-storage.service.ts b/client/src/app/core/users/user-local-storage.service.ts
index fff649eef..a047efe8e 100644
--- a/client/src/app/core/users/user-local-storage.service.ts
+++ b/client/src/app/core/users/user-local-storage.service.ts
@@ -4,7 +4,7 @@ import { Injectable } from '@angular/core'
4import { AuthService, AuthStatus } from '@app/core/auth' 4import { AuthService, AuthStatus } from '@app/core/auth'
5import { getBoolOrDefault } from '@root-helpers/local-storage-utils' 5import { getBoolOrDefault } from '@root-helpers/local-storage-utils'
6import { logger } from '@root-helpers/logger' 6import { logger } from '@root-helpers/logger'
7import { UserLocalStorageKeys, UserTokens } from '@root-helpers/users' 7import { UserLocalStorageKeys, OAuthUserTokens } from '@root-helpers/users'
8import { UserRole, UserUpdateMe } from '@shared/models' 8import { UserRole, UserUpdateMe } from '@shared/models'
9import { NSFWPolicyType } from '@shared/models/videos' 9import { NSFWPolicyType } from '@shared/models/videos'
10import { ServerService } from '../server' 10import { ServerService } from '../server'
@@ -24,7 +24,7 @@ export class UserLocalStorageService {
24 24
25 this.setLoggedInUser(user) 25 this.setLoggedInUser(user)
26 this.setUserInfo(user) 26 this.setUserInfo(user)
27 this.setTokens(user.tokens) 27 this.setTokens(user.oauthTokens)
28 } 28 }
29 }) 29 })
30 30
@@ -43,7 +43,7 @@ export class UserLocalStorageService {
43 next: () => { 43 next: () => {
44 const user = this.authService.getUser() 44 const user = this.authService.getUser()
45 45
46 this.setTokens(user.tokens) 46 this.setTokens(user.oauthTokens)
47 } 47 }
48 }) 48 })
49 } 49 }
@@ -59,7 +59,10 @@ export class UserLocalStorageService {
59 id: parseInt(this.localStorageService.getItem(UserLocalStorageKeys.ID), 10), 59 id: parseInt(this.localStorageService.getItem(UserLocalStorageKeys.ID), 10),
60 username: this.localStorageService.getItem(UserLocalStorageKeys.USERNAME), 60 username: this.localStorageService.getItem(UserLocalStorageKeys.USERNAME),
61 email: this.localStorageService.getItem(UserLocalStorageKeys.EMAIL), 61 email: this.localStorageService.getItem(UserLocalStorageKeys.EMAIL),
62 role: parseInt(this.localStorageService.getItem(UserLocalStorageKeys.ROLE), 10) as UserRole, 62 role: {
63 id: parseInt(this.localStorageService.getItem(UserLocalStorageKeys.ROLE), 10) as UserRole,
64 label: ''
65 },
63 66
64 ...this.getUserInfo() 67 ...this.getUserInfo()
65 } 68 }
@@ -69,12 +72,14 @@ export class UserLocalStorageService {
69 id: number 72 id: number
70 username: string 73 username: string
71 email: string 74 email: string
72 role: UserRole 75 role: {
76 id: UserRole
77 }
73 }) { 78 }) {
74 this.localStorageService.setItem(UserLocalStorageKeys.ID, user.id.toString()) 79 this.localStorageService.setItem(UserLocalStorageKeys.ID, user.id.toString())
75 this.localStorageService.setItem(UserLocalStorageKeys.USERNAME, user.username) 80 this.localStorageService.setItem(UserLocalStorageKeys.USERNAME, user.username)
76 this.localStorageService.setItem(UserLocalStorageKeys.EMAIL, user.email) 81 this.localStorageService.setItem(UserLocalStorageKeys.EMAIL, user.email)
77 this.localStorageService.setItem(UserLocalStorageKeys.ROLE, user.role.toString()) 82 this.localStorageService.setItem(UserLocalStorageKeys.ROLE, user.role.id.toString())
78 } 83 }
79 84
80 flushLoggedInUser () { 85 flushLoggedInUser () {
@@ -174,14 +179,14 @@ export class UserLocalStorageService {
174 // --------------------------------------------------------------------------- 179 // ---------------------------------------------------------------------------
175 180
176 getTokens () { 181 getTokens () {
177 return UserTokens.getUserTokens(this.localStorageService) 182 return OAuthUserTokens.getUserTokens(this.localStorageService)
178 } 183 }
179 184
180 setTokens (tokens: UserTokens) { 185 setTokens (tokens: OAuthUserTokens) {
181 UserTokens.saveToLocalStorage(this.localStorageService, tokens) 186 OAuthUserTokens.saveToLocalStorage(this.localStorageService, tokens)
182 } 187 }
183 188
184 flushTokens () { 189 flushTokens () {
185 UserTokens.flushLocalStorage(this.localStorageService) 190 OAuthUserTokens.flushLocalStorage(this.localStorageService)
186 } 191 }
187} 192}
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts
index 6ba30e4b8..5534bca33 100644
--- a/client/src/app/core/users/user.model.ts
+++ b/client/src/app/core/users/user.model.ts
@@ -34,8 +34,10 @@ export class User implements UserServerModel {
34 videosHistoryEnabled: boolean 34 videosHistoryEnabled: boolean
35 videoLanguages: string[] 35 videoLanguages: string[]
36 36
37 role: UserRole 37 role: {
38 roleLabel: string 38 id: UserRole
39 label: string
40 }
39 41
40 videoQuota: number 42 videoQuota: number
41 videoQuotaDaily: number 43 videoQuotaDaily: number
@@ -66,6 +68,8 @@ export class User implements UserServerModel {
66 68
67 lastLoginDate: Date | null 69 lastLoginDate: Date | null
68 70
71 twoFactorEnabled: boolean
72
69 createdAt: Date 73 createdAt: Date
70 74
71 constructor (hash: Partial<UserServerModel>) { 75 constructor (hash: Partial<UserServerModel>) {
@@ -108,6 +112,8 @@ export class User implements UserServerModel {
108 112
109 this.notificationSettings = hash.notificationSettings 113 this.notificationSettings = hash.notificationSettings
110 114
115 this.twoFactorEnabled = hash.twoFactorEnabled
116
111 this.createdAt = hash.createdAt 117 this.createdAt = hash.createdAt
112 118
113 this.pluginAuth = hash.pluginAuth 119 this.pluginAuth = hash.pluginAuth
@@ -119,7 +125,7 @@ export class User implements UserServerModel {
119 } 125 }
120 126
121 hasRight (right: UserRight) { 127 hasRight (right: UserRight) {
122 return hasUserRight(this.role, right) 128 return hasUserRight(this.role.id, right)
123 } 129 }
124 130
125 patch (obj: UserServerModel) { 131 patch (obj: UserServerModel) {
@@ -144,6 +150,6 @@ export class User implements UserServerModel {
144 isAutoBlocked (serverConfig: HTMLServerConfig) { 150 isAutoBlocked (serverConfig: HTMLServerConfig) {
145 if (serverConfig.autoBlacklist.videos.ofUsers.enabled !== true) return false 151 if (serverConfig.autoBlacklist.videos.ofUsers.enabled !== true) return false
146 152
147 return this.role === UserRole.USER && this.adminFlags !== UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST 153 return this.role.id === UserRole.USER && this.adminFlags !== UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST
148 } 154 }
149} 155}
diff --git a/client/src/app/helpers/utils/url.ts b/client/src/app/helpers/utils/url.ts
index 08c27e3c1..9e7dc3e6f 100644
--- a/client/src/app/helpers/utils/url.ts
+++ b/client/src/app/helpers/utils/url.ts
@@ -54,8 +54,9 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) {
54} 54}
55 55
56export { 56export {
57 objectToFormData,
58 getAbsoluteAPIUrl, 57 getAbsoluteAPIUrl,
59 getAPIHost, 58 getAPIHost,
60 getAbsoluteEmbedUrl 59 getAbsoluteEmbedUrl,
60
61 objectToFormData
61} 62}
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index c1e5f79a6..c5d08ab75 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -88,7 +88,7 @@
88 </a> 88 </a>
89 89
90 <a class="menu-link" routerLink="/my-library" routerLinkActive="active" #libraryLink (click)="onActiveLinkScrollToAnchor(libraryLink)"> 90 <a class="menu-link" routerLink="/my-library" routerLinkActive="active" #libraryLink (click)="onActiveLinkScrollToAnchor(libraryLink)">
91 <my-global-icon iconName="channel" aria-hidden="true"></my-global-icon> 91 <my-global-icon class="channel-icon" iconName="channel" aria-hidden="true"></my-global-icon>
92 <ng-container i18n>My library</ng-container> 92 <ng-container i18n>My library</ng-container>
93 </a> 93 </a>
94 94
@@ -111,7 +111,7 @@
111 <div i18n class="block-title">{{ menuSection.title }}</div> 111 <div i18n class="block-title">{{ menuSection.title }}</div>
112 112
113 <a class="menu-link" *ngFor="let link of menuSection.links" [routerLink]="link.path" routerLinkActive="active"> 113 <a class="menu-link" *ngFor="let link of menuSection.links" [routerLink]="link.path" routerLinkActive="active">
114 <my-global-icon *ngIf="link.icon" [iconName]="link.icon" aria-hidden="true"></my-global-icon> 114 <my-global-icon *ngIf="link.icon" [iconName]="link.icon" [ngClass]="link.iconClass" aria-hidden="true"></my-global-icon>
115 <ng-container>{{ link.shortLabel }}</ng-container> 115 <ng-container>{{ link.shortLabel }}</ng-container>
116 </a> 116 </a>
117 </div> 117 </div>
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss
index a824c69fe..cd57e134e 100644
--- a/client/src/app/menu/menu.component.scss
+++ b/client/src/app/menu/menu.component.scss
@@ -391,26 +391,17 @@ my-actor-avatar {
391} 391}
392 392
393my-global-icon { 393my-global-icon {
394 &[iconName=playlists] { 394 position: relative;
395 top: -1px;
396
397 .playlist-icon {
395 @include margin-right(16px); 398 @include margin-right(16px);
396 399
397 height: 24px; 400 height: 24px;
398 width: 24px; 401 width: 24px;
399 } 402 }
400 403
401 &[iconName=videos] { 404 &.channel-icon {
402 position: relative; 405 top: -2px;
403 right: -1px;
404 }
405
406 &[iconName=channel] {
407 margin-top: -2px;
408 }
409
410 &[iconName='sign-out'] {
411 position: relative;
412 right: -2px;
413 height: 20px;
414 width: 20px;
415 } 406 }
416} 407}
diff --git a/client/src/app/modal/confirm.component.html b/client/src/app/modal/confirm.component.html
index c59c25770..f364165c4 100644
--- a/client/src/app/modal/confirm.component.html
+++ b/client/src/app/modal/confirm.component.html
@@ -9,9 +9,12 @@
9 <div class="modal-body" > 9 <div class="modal-body" >
10 <div [innerHtml]="message"></div> 10 <div [innerHtml]="message"></div>
11 11
12 <div *ngIf="inputLabel && expectedInputValue" class="form-group mt-3"> 12 <div *ngIf="inputLabel" class="form-group mt-3">
13 <label for="confirmInput">{{ inputLabel }}</label> 13 <label for="confirmInput">{{ inputLabel }}</label>
14 <input type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" /> 14
15 <input *ngIf="!isPasswordInput" type="text" id="confirmInput" name="confirmInput" [(ngModel)]="inputValue" />
16
17 <my-input-text inputId="confirmInput" [(ngModel)]="inputValue"></my-input-text>
15 </div> 18 </div>
16 </div> 19 </div>
17 20
diff --git a/client/src/app/modal/confirm.component.ts b/client/src/app/modal/confirm.component.ts
index ec4e1d60f..3bb8b9b21 100644
--- a/client/src/app/modal/confirm.component.ts
+++ b/client/src/app/modal/confirm.component.ts
@@ -21,6 +21,8 @@ export class ConfirmComponent implements OnInit {
21 inputValue = '' 21 inputValue = ''
22 confirmButtonText = '' 22 confirmButtonText = ''
23 23
24 isPasswordInput = false
25
24 private openedModal: NgbModalRef 26 private openedModal: NgbModalRef
25 27
26 constructor ( 28 constructor (
@@ -31,11 +33,27 @@ export class ConfirmComponent implements OnInit {
31 33
32 ngOnInit () { 34 ngOnInit () {
33 this.confirmService.showConfirm.subscribe( 35 this.confirmService.showConfirm.subscribe(
34 ({ title, message, expectedInputValue, inputLabel, confirmButtonText }) => { 36 payload => {
37 // Reinit fields
38 this.title = ''
39 this.message = ''
40 this.expectedInputValue = ''
41 this.inputLabel = ''
42 this.inputValue = ''
43 this.confirmButtonText = ''
44 this.isPasswordInput = false
45
46 const { type, title, message, confirmButtonText } = payload
47
35 this.title = title 48 this.title = title
36 49
37 this.inputLabel = inputLabel 50 if (type === 'confirm-expected-input') {
38 this.expectedInputValue = expectedInputValue 51 this.inputLabel = payload.inputLabel
52 this.expectedInputValue = payload.expectedInputValue
53 } else if (type === 'confirm-password') {
54 this.inputLabel = $localize`Confirm your password`
55 this.isPasswordInput = true
56 }
39 57
40 this.confirmButtonText = confirmButtonText || $localize`Confirm` 58 this.confirmButtonText = confirmButtonText || $localize`Confirm`
41 59
@@ -66,11 +84,13 @@ export class ConfirmComponent implements OnInit {
66 this.openedModal = this.modalService.open(this.confirmModal, { centered: true }) 84 this.openedModal = this.modalService.open(this.confirmModal, { centered: true })
67 85
68 this.openedModal.result 86 this.openedModal.result
69 .then(() => this.confirmService.confirmResponse.next(true)) 87 .then(() => {
88 this.confirmService.confirmResponse.next({ confirmed: true, value: this.inputValue })
89 })
70 .catch((reason: string) => { 90 .catch((reason: string) => {
71 // If the reason was that the user used the back button, we don't care about the confirm dialog result 91 // If the reason was that the user used the back button, we don't care about the confirm dialog result
72 if (!reason || reason !== POP_STATE_MODAL_DISMISS) { 92 if (!reason || reason !== POP_STATE_MODAL_DISMISS) {
73 this.confirmService.confirmResponse.next(false) 93 this.confirmService.confirmResponse.next({ confirmed: false, value: this.inputValue })
74 } 94 }
75 }) 95 })
76 } 96 }
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts
index 3262853d8..b93de75ea 100644
--- a/client/src/app/shared/form-validators/user-validators.ts
+++ b/client/src/app/shared/form-validators/user-validators.ts
@@ -61,6 +61,15 @@ export const USER_EXISTING_PASSWORD_VALIDATOR: BuildFormValidator = {
61 } 61 }
62} 62}
63 63
64export const USER_OTP_TOKEN_VALIDATOR: BuildFormValidator = {
65 VALIDATORS: [
66 Validators.required
67 ],
68 MESSAGES: {
69 required: $localize`OTP token is required.`
70 }
71}
72
64export const USER_PASSWORD_VALIDATOR = { 73export const USER_PASSWORD_VALIDATOR = {
65 VALIDATORS: [ 74 VALIDATORS: [
66 Validators.required, 75 Validators.required,
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
index 32d3b0093..569a37b17 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -2,7 +2,6 @@ import * as debug from 'debug'
2import truncate from 'lodash-es/truncate' 2import truncate from 'lodash-es/truncate'
3import { SortMeta } from 'primeng/api' 3import { SortMeta } from 'primeng/api'
4import { Component, Input, OnInit, ViewChild } from '@angular/core' 4import { Component, Input, OnInit, ViewChild } from '@angular/core'
5import { DomSanitizer } from '@angular/platform-browser'
6import { ActivatedRoute, Router } from '@angular/router' 5import { ActivatedRoute, Router } from '@angular/router'
7import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' 6import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
8import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' 7import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
@@ -73,8 +72,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
73 private videoService: VideoService, 72 private videoService: VideoService,
74 private videoBlocklistService: VideoBlockService, 73 private videoBlocklistService: VideoBlockService,
75 private confirmService: ConfirmService, 74 private confirmService: ConfirmService,
76 private markdownRenderer: MarkdownService, 75 private markdownRenderer: MarkdownService
77 private sanitizer: DomSanitizer
78 ) { 76 ) {
79 super() 77 super()
80 } 78 }
@@ -216,8 +214,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
216 abuse.truncatedCommentHtml = abuse.commentHtml = $localize`Deleted comment` 214 abuse.truncatedCommentHtml = abuse.commentHtml = $localize`Deleted comment`
217 } else { 215 } else {
218 const truncated = truncate(abuse.comment.text, { length: 100 }) 216 const truncated = truncate(abuse.comment.text, { length: 100 })
219 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true) 217 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML({ markdown: truncated, withHtml: true })
220 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true) 218 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML({ markdown: abuse.comment.text, withHtml: true })
221 } 219 }
222 } 220 }
223 221
@@ -274,7 +272,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
274 }, 272 },
275 { 273 {
276 label: $localize`Delete report`, 274 label: $localize`Delete report`,
277 handler: abuse => this.isAdminView() && this.removeAbuse(abuse) 275 handler: abuse => this.removeAbuse(abuse),
276 isDisplayed: () => this.isAdminView()
278 } 277 }
279 ] 278 ]
280 } 279 }
@@ -452,6 +451,6 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
452 } 451 }
453 452
454 private toHtml (text: string) { 453 private toHtml (text: string) {
455 return this.markdownRenderer.textMarkdownToHTML(text) 454 return this.markdownRenderer.textMarkdownToHTML({ markdown: text })
456 } 455 }
457} 456}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
index d24a5d58d..12d503f56 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
@@ -1,6 +1,6 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { AuthService, HtmlRendererService, Notifier } from '@app/core' 2import { AuthService, HtmlRendererService, Notifier } from '@app/core'
3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { logger } from '@root-helpers/logger' 6import { logger } from '@root-helpers/logger'
@@ -29,7 +29,7 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
29 private abuse: UserAbuse 29 private abuse: UserAbuse
30 30
31 constructor ( 31 constructor (
32 protected formValidatorService: FormValidatorService, 32 protected formReactiveService: FormReactiveService,
33 private modalService: NgbModal, 33 private modalService: NgbModal,
34 private htmlRenderer: HtmlRendererService, 34 private htmlRenderer: HtmlRendererService,
35 private auth: AuthService, 35 private auth: AuthService,
diff --git a/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
index 2600da8da..4ad807d25 100644
--- a/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
+++ b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
@@ -1,6 +1,6 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
4import { AbuseService } from '@app/shared/shared-moderation' 4import { AbuseService } from '@app/shared/shared-moderation'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -20,7 +20,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
20 private openedModal: NgbModalRef 20 private openedModal: NgbModalRef
21 21
22 constructor ( 22 constructor (
23 protected formValidatorService: FormValidatorService, 23 protected formReactiveService: FormReactiveService,
24 private modalService: NgbModal, 24 private modalService: NgbModal,
25 private notifier: Notifier, 25 private notifier: Notifier,
26 private abuseService: AbuseService 26 private abuseService: AbuseService
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
index d738a644e..618c3dd4f 100644
--- a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
+++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
@@ -58,7 +58,7 @@ export class CustomMarkupService {
58 } 58 }
59 59
60 async buildElement (text: string) { 60 async buildElement (text: string) {
61 const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags()) 61 const html = await this.markdown.customPageMarkdownToHTML({ markdown: text, additionalAllowedTags: this.getSupportedTags() })
62 62
63 const rootElement = document.createElement('div') 63 const rootElement = document.createElement('div')
64 rootElement.innerHTML = html 64 rootElement.innerHTML = html
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
index e9c466a90..ba12b7139 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
@@ -42,7 +42,11 @@ export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, O
42 tap(channel => { 42 tap(channel => {
43 this.channel = channel 43 this.channel = channel
44 }), 44 }),
45 switchMap(() => from(this.markdown.textMarkdownToHTML(this.channel.description))), 45 switchMap(() => from(this.markdown.textMarkdownToHTML({
46 markdown: this.channel.description,
47 withEmoji: true,
48 withHtml: true
49 }))),
46 tap(html => { 50 tap(html => {
47 this.descriptionHTML = html 51 this.descriptionHTML = html
48 }), 52 }),
diff --git a/client/src/app/shared/shared-forms/form-reactive.service.ts b/client/src/app/shared/shared-forms/form-reactive.service.ts
new file mode 100644
index 000000000..f1b7e0ef2
--- /dev/null
+++ b/client/src/app/shared/shared-forms/form-reactive.service.ts
@@ -0,0 +1,101 @@
1import { Injectable } from '@angular/core'
2import { AbstractControl, FormGroup } from '@angular/forms'
3import { wait } from '@root-helpers/utils'
4import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
5import { FormValidatorService } from './form-validator.service'
6
7export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors }
8export type FormReactiveValidationMessages = {
9 [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages
10}
11
12@Injectable()
13export class FormReactiveService {
14
15 constructor (private formValidatorService: FormValidatorService) {
16
17 }
18
19 buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) {
20 const { formErrors, validationMessages, form } = this.formValidatorService.buildForm(obj, defaultValues)
21
22 form.statusChanges.subscribe(async () => {
23 // FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed
24 await this.waitPendingCheck(form)
25
26 this.onStatusChanged({ form, formErrors, validationMessages })
27 })
28
29 return { form, formErrors, validationMessages }
30 }
31
32 async waitPendingCheck (form: FormGroup) {
33 if (form.status !== 'PENDING') return
34
35 // FIXME: the following line does not work: https://github.com/angular/angular/issues/41519
36 // return firstValueFrom(form.statusChanges.pipe(filter(status => status !== 'PENDING')))
37 // So we have to fallback to active wait :/
38
39 do {
40 await wait(10)
41 } while (form.status === 'PENDING')
42 }
43
44 markAllAsDirty (controlsArg: { [ key: string ]: AbstractControl }) {
45 const controls = controlsArg
46
47 for (const key of Object.keys(controls)) {
48 const control = controls[key]
49
50 if (control instanceof FormGroup) {
51 this.markAllAsDirty(control.controls)
52 continue
53 }
54
55 control.markAsDirty()
56 }
57 }
58
59 forceCheck (form: FormGroup, formErrors: any, validationMessages: FormReactiveValidationMessages) {
60 this.onStatusChanged({ form, formErrors, validationMessages, onlyDirty: false })
61 }
62
63 private onStatusChanged (options: {
64 form: FormGroup
65 formErrors: FormReactiveErrors
66 validationMessages: FormReactiveValidationMessages
67 onlyDirty?: boolean // default true
68 }) {
69 const { form, formErrors, validationMessages, onlyDirty = true } = options
70
71 for (const field of Object.keys(formErrors)) {
72 if (formErrors[field] && typeof formErrors[field] === 'object') {
73 this.onStatusChanged({
74 form: form.controls[field] as FormGroup,
75 formErrors: formErrors[field] as FormReactiveErrors,
76 validationMessages: validationMessages[field] as FormReactiveValidationMessages,
77 onlyDirty
78 })
79
80 continue
81 }
82
83 // clear previous error message (if any)
84 formErrors[field] = ''
85 const control = form.get(field)
86
87 if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
88
89 const staticMessages = validationMessages[field]
90 for (const key of Object.keys(control.errors)) {
91 const formErrorValue = control.errors[key]
92
93 // Try to find error message in static validation messages first
94 // Then check if the validator returns a string that is the error
95 if (staticMessages[key]) formErrors[field] += staticMessages[key] + ' '
96 else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key]
97 else throw new Error('Form error value of ' + field + ' is invalid')
98 }
99 }
100 }
101}
diff --git a/client/src/app/shared/shared-forms/form-reactive.ts b/client/src/app/shared/shared-forms/form-reactive.ts
index a19ffdd82..d1e7be802 100644
--- a/client/src/app/shared/shared-forms/form-reactive.ts
+++ b/client/src/app/shared/shared-forms/form-reactive.ts
@@ -1,16 +1,9 @@
1 1import { FormGroup } from '@angular/forms'
2import { AbstractControl, FormGroup } from '@angular/forms'
3import { wait } from '@root-helpers/utils'
4import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' 2import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
5import { FormValidatorService } from './form-validator.service' 3import { FormReactiveService, FormReactiveValidationMessages } from './form-reactive.service'
6
7export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors }
8export type FormReactiveValidationMessages = {
9 [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages
10}
11 4
12export abstract class FormReactive { 5export abstract class FormReactive {
13 protected abstract formValidatorService: FormValidatorService 6 protected abstract formReactiveService: FormReactiveService
14 protected formChanged = false 7 protected formChanged = false
15 8
16 form: FormGroup 9 form: FormGroup
@@ -18,86 +11,22 @@ export abstract class FormReactive {
18 validationMessages: FormReactiveValidationMessages 11 validationMessages: FormReactiveValidationMessages
19 12
20 buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) { 13 buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) {
21 const { formErrors, validationMessages, form } = this.formValidatorService.buildForm(obj, defaultValues) 14 const { formErrors, validationMessages, form } = this.formReactiveService.buildForm(obj, defaultValues)
22 15
23 this.form = form 16 this.form = form
24 this.formErrors = formErrors 17 this.formErrors = formErrors
25 this.validationMessages = validationMessages 18 this.validationMessages = validationMessages
26
27 this.form.statusChanges.subscribe(async () => {
28 // FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed
29 await this.waitPendingCheck()
30
31 this.onStatusChanged(this.form, this.formErrors, this.validationMessages)
32 })
33 } 19 }
34 20
35 protected async waitPendingCheck () { 21 protected async waitPendingCheck () {
36 if (this.form.status !== 'PENDING') return 22 return this.formReactiveService.waitPendingCheck(this.form)
37
38 // FIXME: the following line does not work: https://github.com/angular/angular/issues/41519
39 // return firstValueFrom(this.form.statusChanges.pipe(filter(status => status !== 'PENDING')))
40 // So we have to fallback to active wait :/
41
42 do {
43 await wait(10)
44 } while (this.form.status === 'PENDING')
45 } 23 }
46 24
47 protected markAllAsDirty (controlsArg?: { [ key: string ]: AbstractControl }) { 25 protected markAllAsDirty () {
48 const controls = controlsArg || this.form.controls 26 return this.formReactiveService.markAllAsDirty(this.form.controls)
49
50 for (const key of Object.keys(controls)) {
51 const control = controls[key]
52
53 if (control instanceof FormGroup) {
54 this.markAllAsDirty(control.controls)
55 continue
56 }
57
58 control.markAsDirty()
59 }
60 } 27 }
61 28
62 protected forceCheck () { 29 protected forceCheck () {
63 this.onStatusChanged(this.form, this.formErrors, this.validationMessages, false) 30 return this.formReactiveService.forceCheck(this.form, this.formErrors, this.validationMessages)
64 }
65
66 private onStatusChanged (
67 form: FormGroup,
68 formErrors: FormReactiveErrors,
69 validationMessages: FormReactiveValidationMessages,
70 onlyDirty = true
71 ) {
72 for (const field of Object.keys(formErrors)) {
73 if (formErrors[field] && typeof formErrors[field] === 'object') {
74 this.onStatusChanged(
75 form.controls[field] as FormGroup,
76 formErrors[field] as FormReactiveErrors,
77 validationMessages[field] as FormReactiveValidationMessages,
78 onlyDirty
79 )
80 continue
81 }
82
83 // clear previous error message (if any)
84 formErrors[field] = ''
85 const control = form.get(field)
86
87 if (control.dirty) this.formChanged = true
88
89 if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
90
91 const staticMessages = validationMessages[field]
92 for (const key of Object.keys(control.errors)) {
93 const formErrorValue = control.errors[key]
94
95 // Try to find error message in static validation messages first
96 // Then check if the validator returns a string that is the error
97 if (staticMessages[key]) formErrors[field] += staticMessages[key] + ' '
98 else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key]
99 else throw new Error('Form error value of ' + field + ' is invalid')
100 }
101 }
102 } 31 }
103} 32}
diff --git a/client/src/app/shared/shared-forms/form-validator.service.ts b/client/src/app/shared/shared-forms/form-validator.service.ts
index f67d5bb33..897008242 100644
--- a/client/src/app/shared/shared-forms/form-validator.service.ts
+++ b/client/src/app/shared/shared-forms/form-validator.service.ts
@@ -1,7 +1,7 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms' 2import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms'
3import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' 3import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
4import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive' 4import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive.service'
5 5
6@Injectable() 6@Injectable()
7export class FormValidatorService { 7export class FormValidatorService {
diff --git a/client/src/app/shared/shared-forms/index.ts b/client/src/app/shared/shared-forms/index.ts
index 495785e7b..bff9862f2 100644
--- a/client/src/app/shared/shared-forms/index.ts
+++ b/client/src/app/shared/shared-forms/index.ts
@@ -1,4 +1,5 @@
1export * from './advanced-input-filter.component' 1export * from './advanced-input-filter.component'
2export * from './form-reactive.service'
2export * from './form-reactive' 3export * from './form-reactive'
3export * from './form-validator.service' 4export * from './form-validator.service'
4export * from './form-validator.service' 5export * from './form-validator.service'
diff --git a/client/src/app/shared/shared-forms/input-text.component.ts b/client/src/app/shared/shared-forms/input-text.component.ts
index d667ed663..aa4a1cba8 100644
--- a/client/src/app/shared/shared-forms/input-text.component.ts
+++ b/client/src/app/shared/shared-forms/input-text.component.ts
@@ -1,4 +1,4 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4 4
@@ -15,6 +15,8 @@ import { Notifier } from '@app/core'
15 ] 15 ]
16}) 16})
17export class InputTextComponent implements ControlValueAccessor { 17export class InputTextComponent implements ControlValueAccessor {
18 @ViewChild('input') inputElement: ElementRef
19
18 @Input() inputId = Math.random().toString(11).slice(2, 8) // id cannot be left empty or undefined 20 @Input() inputId = Math.random().toString(11).slice(2, 8) // id cannot be left empty or undefined
19 @Input() value = '' 21 @Input() value = ''
20 @Input() autocomplete = 'off' 22 @Input() autocomplete = 'off'
@@ -65,4 +67,10 @@ export class InputTextComponent implements ControlValueAccessor {
65 update () { 67 update () {
66 this.propagateChange(this.value) 68 this.propagateChange(this.value)
67 } 69 }
70
71 focus () {
72 const el: HTMLElement = this.inputElement.nativeElement
73
74 el.focus({ preventScroll: true })
75 }
68} 76}
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
index 089991884..e3371f22c 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
@@ -144,9 +144,9 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
144 144
145 html = result 145 html = result
146 } else if (this.markdownType === 'text') { 146 } else if (this.markdownType === 'text') {
147 html = await this.markdownService.textMarkdownToHTML(text) 147 html = await this.markdownService.textMarkdownToHTML({ markdown: text })
148 } else { 148 } else {
149 html = await this.markdownService.enhancedMarkdownToHTML(text) 149 html = await this.markdownService.enhancedMarkdownToHTML({ markdown: text })
150 } 150 }
151 151
152 if (this.markdownVideo) { 152 if (this.markdownVideo) {
diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts
index 81f076db6..628affb56 100644
--- a/client/src/app/shared/shared-forms/shared-form.module.ts
+++ b/client/src/app/shared/shared-forms/shared-form.module.ts
@@ -1,4 +1,3 @@
1
2import { InputMaskModule } from 'primeng/inputmask' 1import { InputMaskModule } from 'primeng/inputmask'
3import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
4import { FormsModule, ReactiveFormsModule } from '@angular/forms' 3import { FormsModule, ReactiveFormsModule } from '@angular/forms'
@@ -7,6 +6,7 @@ import { SharedGlobalIconModule } from '../shared-icons'
7import { SharedMainModule } from '../shared-main/shared-main.module' 6import { SharedMainModule } from '../shared-main/shared-main.module'
8import { AdvancedInputFilterComponent } from './advanced-input-filter.component' 7import { AdvancedInputFilterComponent } from './advanced-input-filter.component'
9import { DynamicFormFieldComponent } from './dynamic-form-field.component' 8import { DynamicFormFieldComponent } from './dynamic-form-field.component'
9import { FormReactiveService } from './form-reactive.service'
10import { FormValidatorService } from './form-validator.service' 10import { FormValidatorService } from './form-validator.service'
11import { InputSwitchComponent } from './input-switch.component' 11import { InputSwitchComponent } from './input-switch.component'
12import { InputTextComponent } from './input-text.component' 12import { InputTextComponent } from './input-text.component'
@@ -96,7 +96,8 @@ import { TimestampInputComponent } from './timestamp-input.component'
96 ], 96 ],
97 97
98 providers: [ 98 providers: [
99 FormValidatorService 99 FormValidatorService,
100 FormReactiveService
100 ] 101 ]
101}) 102})
102export class SharedFormModule { } 103export class SharedFormModule { }
diff --git a/client/src/app/shared/shared-instance/instance.service.ts b/client/src/app/shared/shared-instance/instance.service.ts
index 0241f56ef..89f47db24 100644
--- a/client/src/app/shared/shared-instance/instance.service.ts
+++ b/client/src/app/shared/shared-instance/instance.service.ts
@@ -51,7 +51,7 @@ export class InstanceService {
51 } 51 }
52 52
53 for (const key of Object.keys(html)) { 53 for (const key of Object.keys(html)) {
54 html[key] = await this.markdownService.textMarkdownToHTML(about.instance[key]) 54 html[key] = await this.markdownService.textMarkdownToHTML({ markdown: about.instance[key] })
55 } 55 }
56 56
57 return html 57 return html
diff --git a/client/src/app/shared/shared-main/angular/number-formatter.pipe.ts b/client/src/app/shared/shared-main/angular/number-formatter.pipe.ts
index 7c18b7f67..e0cb475fc 100644
--- a/client/src/app/shared/shared-main/angular/number-formatter.pipe.ts
+++ b/client/src/app/shared/shared-main/angular/number-formatter.pipe.ts
@@ -13,11 +13,11 @@ export class NumberFormatterPipe implements PipeTransform {
13 static getDecimalForNumber (x: number, n = 1) { 13 static getDecimalForNumber (x: number, n = 1) {
14 const v = x.toString().split('.') 14 const v = x.toString().split('.')
15 const f = v[1] || '' 15 const f = v[1] || ''
16 if (f.length > n) return +f.substr(0, n) 16 if (f.length > n) return +f.substring(0, n)
17 return +f 17 return +f
18 } 18 }
19 19
20 private dictionary: Array<{max: number, type: string}> = [ 20 private dictionary: Array<{ max: number, type: string }> = [
21 { max: 1000, type: '' }, 21 { max: 1000, type: '' },
22 { max: 1000000, type: 'K' }, 22 { max: 1000000, type: 'K' },
23 { max: 1000000000, type: 'M' } 23 { max: 1000000000, type: 'M' }
diff --git a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
index e4b74f3ad..93b3a93d6 100644
--- a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
+++ b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
@@ -27,13 +27,16 @@ export class AuthInterceptor implements HttpInterceptor {
27 .pipe( 27 .pipe(
28 catchError((err: HttpErrorResponse) => { 28 catchError((err: HttpErrorResponse) => {
29 const error = err.error as PeerTubeProblemDocument 29 const error = err.error as PeerTubeProblemDocument
30 const isOTPMissingError = this.authService.isOTPMissingError(err)
30 31
31 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && error && error.code === OAuth2ErrorCode.INVALID_TOKEN) { 32 if (!isOTPMissingError) {
32 return this.handleTokenExpired(req, next) 33 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && error && error.code === OAuth2ErrorCode.INVALID_TOKEN) {
33 } 34 return this.handleTokenExpired(req, next)
35 }
34 36
35 if (err.status === HttpStatusCode.UNAUTHORIZED_401) { 37 if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
36 return this.handleNotAuthenticated(err) 38 return this.handleNotAuthenticated(err)
39 }
37 } 40 }
38 41
39 return observableThrowError(() => err) 42 return observableThrowError(() => err)
diff --git a/client/src/app/shared/shared-main/buttons/action-dropdown.component.html b/client/src/app/shared/shared-main/buttons/action-dropdown.component.html
index 37cf63fcd..474baafd7 100644
--- a/client/src/app/shared/shared-main/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/shared-main/buttons/action-dropdown.component.html
@@ -26,7 +26,7 @@
26 26
27 <a 27 <a
28 *ngIf="action.linkBuilder && !action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }" 28 *ngIf="action.linkBuilder && !action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
29 class="dropdown-item" [routerLink]="action.linkBuilder(entry)" [title]="action.title || ''" 29 class="dropdown-item" [routerLink]="action.linkBuilder(entry)" [queryParams]="getQueryParams(action, entry)" [title]="action.title || ''"
30 > 30 >
31 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container> 31 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
32 </a> 32 </a>
diff --git a/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts b/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts
index 749773f8a..e39fbd66d 100644
--- a/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts
+++ b/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts
@@ -1,4 +1,5 @@
1import { Component, Input } from '@angular/core' 1import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
2import { Params } from '@angular/router'
2import { GlobalIconName } from '@app/shared/shared-icons' 3import { GlobalIconName } from '@app/shared/shared-icons'
3 4
4export type DropdownAction<T> = { 5export type DropdownAction<T> = {
@@ -7,7 +8,10 @@ export type DropdownAction<T> = {
7 description?: string 8 description?: string
8 title?: string 9 title?: string
9 handler?: (a: T) => any 10 handler?: (a: T) => any
11
10 linkBuilder?: (a: T) => (string | number)[] 12 linkBuilder?: (a: T) => (string | number)[]
13 queryParamsBuilder?: (a: T) => Params
14
11 isDisplayed?: (a: T) => boolean 15 isDisplayed?: (a: T) => boolean
12 16
13 class?: string[] 17 class?: string[]
@@ -21,7 +25,8 @@ export type DropdownDirection = 'horizontal' | 'vertical'
21@Component({ 25@Component({
22 selector: 'my-action-dropdown', 26 selector: 'my-action-dropdown',
23 styleUrls: [ './action-dropdown.component.scss' ], 27 styleUrls: [ './action-dropdown.component.scss' ],
24 templateUrl: './action-dropdown.component.html' 28 templateUrl: './action-dropdown.component.html',
29 changeDetection: ChangeDetectionStrategy.OnPush
25}) 30})
26 31
27export class ActionDropdownComponent<T> { 32export class ActionDropdownComponent<T> {
@@ -44,6 +49,12 @@ export class ActionDropdownComponent<T> {
44 return [ this.actions as DropdownAction<T>[] ] 49 return [ this.actions as DropdownAction<T>[] ]
45 } 50 }
46 51
52 getQueryParams (action: DropdownAction<T>, entry: T) {
53 if (action.queryParamsBuilder) return action.queryParamsBuilder(entry)
54
55 return {}
56 }
57
47 areActionsDisplayed (actions: Array<DropdownAction<T> | DropdownAction<T>[]>, entry: T): boolean { 58 areActionsDisplayed (actions: Array<DropdownAction<T> | DropdownAction<T>[]>, entry: T): boolean {
48 return actions.some(a => { 59 return actions.some(a => {
49 if (Array.isArray(a)) return this.areActionsDisplayed(a, entry) 60 if (Array.isArray(a)) return this.areActionsDisplayed(a, entry)
diff --git a/client/src/app/shared/shared-main/buttons/button.component.ts b/client/src/app/shared/shared-main/buttons/button.component.ts
index 10d67831f..1761938ee 100644
--- a/client/src/app/shared/shared-main/buttons/button.component.ts
+++ b/client/src/app/shared/shared-main/buttons/button.component.ts
@@ -1,10 +1,11 @@
1import { Component, Input, OnChanges } from '@angular/core' 1import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'
2import { GlobalIconName } from '@app/shared/shared-icons' 2import { GlobalIconName } from '@app/shared/shared-icons'
3 3
4@Component({ 4@Component({
5 selector: 'my-button', 5 selector: 'my-button',
6 styleUrls: [ './button.component.scss' ], 6 styleUrls: [ './button.component.scss' ],
7 templateUrl: './button.component.html' 7 templateUrl: './button.component.html',
8 changeDetection: ChangeDetectionStrategy.OnPush
8}) 9})
9 10
10export class ButtonComponent implements OnChanges { 11export class ButtonComponent implements OnChanges {
diff --git a/client/src/app/shared/shared-main/misc/list-overflow.component.ts b/client/src/app/shared/shared-main/misc/list-overflow.component.ts
index 7e4e1b1d1..b6ce21641 100644
--- a/client/src/app/shared/shared-main/misc/list-overflow.component.ts
+++ b/client/src/app/shared/shared-main/misc/list-overflow.component.ts
@@ -32,7 +32,7 @@ export interface ListOverflowItem {
32}) 32})
33export class ListOverflowComponent<T extends ListOverflowItem> implements AfterViewInit { 33export class ListOverflowComponent<T extends ListOverflowItem> implements AfterViewInit {
34 @Input() items: T[] 34 @Input() items: T[]
35 @Input() itemTemplate: TemplateRef<{item: T}> 35 @Input() itemTemplate: TemplateRef<{ item: T }>
36 36
37 @ViewChild('modal', { static: true }) modal: ElementRef 37 @ViewChild('modal', { static: true }) modal: ElementRef
38 @ViewChild('itemsParent', { static: true }) parent: ElementRef<HTMLDivElement> 38 @ViewChild('itemsParent', { static: true }) parent: ElementRef<HTMLDivElement>
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index 04b223cc5..c1523bc50 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -44,7 +44,15 @@ import {
44import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins' 44import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins'
45import { ActorRedirectGuard } from './router' 45import { ActorRedirectGuard } from './router'
46import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 46import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
47import { EmbedComponent, RedundancyService, VideoImportService, VideoOwnershipService, VideoResolver, VideoService } from './video' 47import {
48 EmbedComponent,
49 RedundancyService,
50 VideoFileTokenService,
51 VideoImportService,
52 VideoOwnershipService,
53 VideoResolver,
54 VideoService
55} from './video'
48import { VideoCaptionService } from './video-caption' 56import { VideoCaptionService } from './video-caption'
49import { VideoChannelService } from './video-channel' 57import { VideoChannelService } from './video-channel'
50 58
@@ -185,6 +193,7 @@ import { VideoChannelService } from './video-channel'
185 VideoImportService, 193 VideoImportService,
186 VideoOwnershipService, 194 VideoOwnershipService,
187 VideoService, 195 VideoService,
196 VideoFileTokenService,
188 VideoResolver, 197 VideoResolver,
189 198
190 VideoCaptionService, 199 VideoCaptionService,
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
index 5e3985526..08811afec 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
@@ -2,7 +2,7 @@ import { Observable, ReplaySubject } from 'rxjs'
2import { catchError, map, tap } from 'rxjs/operators' 2import { catchError, map, tap } 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 { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' 5import { ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core'
6import { 6import {
7 ActorImage, 7 ActorImage,
8 ResultList, 8 ResultList,
@@ -25,7 +25,8 @@ export class VideoChannelService {
25 constructor ( 25 constructor (
26 private authHttp: HttpClient, 26 private authHttp: HttpClient,
27 private restService: RestService, 27 private restService: RestService,
28 private restExtractor: RestExtractor 28 private restExtractor: RestExtractor,
29 private serverService: ServerService
29 ) { } 30 ) { }
30 31
31 static extractVideoChannels (result: ResultList<VideoChannelServer>) { 32 static extractVideoChannels (result: ResultList<VideoChannelServer>) {
@@ -56,9 +57,11 @@ export class VideoChannelService {
56 }): Observable<ResultList<VideoChannel>> { 57 }): Observable<ResultList<VideoChannel>> {
57 const { account, componentPagination, withStats = false, sort, search } = options 58 const { account, componentPagination, withStats = false, sort, search } = options
58 59
60 const defaultCount = this.serverService.getHTMLConfig().videoChannels.maxPerUser
61
59 const pagination = componentPagination 62 const pagination = componentPagination
60 ? this.restService.componentToRestPagination(componentPagination) 63 ? this.restService.componentToRestPagination(componentPagination)
61 : { start: 0, count: 20 } 64 : { start: 0, count: defaultCount }
62 65
63 let params = new HttpParams() 66 let params = new HttpParams()
64 params = this.restService.addRestGetParams(params, pagination, sort) 67 params = this.restService.addRestGetParams(params, pagination, sort)
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts
index 361601456..a2e47883e 100644
--- a/client/src/app/shared/shared-main/video/index.ts
+++ b/client/src/app/shared/shared-main/video/index.ts
@@ -2,6 +2,7 @@ export * from './embed.component'
2export * from './redundancy.service' 2export * from './redundancy.service'
3export * from './video-details.model' 3export * from './video-details.model'
4export * from './video-edit.model' 4export * from './video-edit.model'
5export * from './video-file-token.service'
5export * from './video-import.service' 6export * from './video-import.service'
6export * from './video-ownership.service' 7export * from './video-ownership.service'
7export * from './video.model' 8export * from './video.model'
diff --git a/client/src/app/shared/shared-main/video/video-file-token.service.ts b/client/src/app/shared/shared-main/video/video-file-token.service.ts
new file mode 100644
index 000000000..791607249
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/video-file-token.service.ts
@@ -0,0 +1,33 @@
1import { catchError, map, of, tap } from 'rxjs'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor } from '@app/core'
5import { VideoToken } from '@shared/models'
6import { VideoService } from './video.service'
7
8@Injectable()
9export class VideoFileTokenService {
10
11 private readonly store = new Map<string, { token: string, expires: Date }>()
12
13 constructor (
14 private authHttp: HttpClient,
15 private restExtractor: RestExtractor
16 ) {}
17
18 getVideoFileToken (videoUUID: string) {
19 const existing = this.store.get(videoUUID)
20 if (existing) return of(existing)
21
22 return this.createVideoFileToken(videoUUID)
23 .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) })))
24 }
25
26 private createVideoFileToken (videoUUID: string) {
27 return this.authHttp.post<VideoToken>(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {})
28 .pipe(
29 map(({ files }) => files),
30 catchError(err => this.restExtractor.handleError(err))
31 )
32 }
33}
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index c9c6b979c..6fdffb394 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -34,6 +34,7 @@ export class Video implements VideoServerModel {
34 language: VideoConstant<string> 34 language: VideoConstant<string>
35 privacy: VideoConstant<VideoPrivacy> 35 privacy: VideoConstant<VideoPrivacy>
36 36
37 truncatedDescription: string
37 description: string 38 description: string
38 39
39 duration: number 40 duration: number
@@ -134,6 +135,8 @@ export class Video implements VideoServerModel {
134 this.privacy = hash.privacy 135 this.privacy = hash.privacy
135 this.waitTranscoding = hash.waitTranscoding 136 this.waitTranscoding = hash.waitTranscoding
136 this.state = hash.state 137 this.state = hash.state
138
139 this.truncatedDescription = hash.truncatedDescription
137 this.description = hash.description 140 this.description = hash.description
138 141
139 this.isLive = hash.isLive 142 this.isLive = hash.isLive
diff --git a/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts b/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
index 20be728f6..ec2fea528 100644
--- a/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
+++ b/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
@@ -1,5 +1,5 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 2import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 4import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
5import { splitAndGetNotEmpty, UNIQUE_HOSTS_VALIDATOR } from '../form-validators/host-validators' 5import { splitAndGetNotEmpty, UNIQUE_HOSTS_VALIDATOR } from '../form-validators/host-validators'
@@ -18,7 +18,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit {
18 private openedModal: NgbModalRef 18 private openedModal: NgbModalRef
19 19
20 constructor ( 20 constructor (
21 protected formValidatorService: FormValidatorService, 21 protected formReactiveService: FormReactiveService,
22 private modalService: NgbModal 22 private modalService: NgbModal
23 ) { 23 ) {
24 super() 24 super()
diff --git a/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
index 78c9b3382..d587a9709 100644
--- a/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
@@ -2,7 +2,7 @@ import { mapValues, pickBy } from 'lodash-es'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' 4import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
6import { Account } from '@app/shared/shared-main' 6import { Account } from '@app/shared/shared-main'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -26,7 +26,7 @@ export class AccountReportComponent extends FormReactive implements OnInit {
26 private openedModal: NgbModalRef 26 private openedModal: NgbModalRef
27 27
28 constructor ( 28 constructor (
29 protected formValidatorService: FormValidatorService, 29 protected formReactiveService: FormReactiveService,
30 private modalService: NgbModal, 30 private modalService: NgbModal,
31 private abuseService: AbuseService, 31 private abuseService: AbuseService,
32 private notifier: Notifier 32 private notifier: Notifier
diff --git a/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
index 7c0907ce4..e35d70c8f 100644
--- a/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
@@ -2,7 +2,7 @@ import { mapValues, pickBy } from 'lodash-es'
2import { Component, Input, OnInit, ViewChild } from '@angular/core' 2import { Component, Input, OnInit, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' 4import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
6import { VideoComment } from '@app/shared/shared-video-comment' 6import { VideoComment } from '@app/shared/shared-video-comment'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -27,7 +27,7 @@ export class CommentReportComponent extends FormReactive implements OnInit {
27 private openedModal: NgbModalRef 27 private openedModal: NgbModalRef
28 28
29 constructor ( 29 constructor (
30 protected formValidatorService: FormValidatorService, 30 protected formReactiveService: FormReactiveService,
31 private modalService: NgbModal, 31 private modalService: NgbModal,
32 private abuseService: AbuseService, 32 private abuseService: AbuseService,
33 private notifier: Notifier 33 private notifier: Notifier
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
index 38dd92910..16be8e0a1 100644
--- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
@@ -3,7 +3,7 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'
3import { DomSanitizer } from '@angular/platform-browser' 3import { DomSanitizer } from '@angular/platform-browser'
4import { Notifier } from '@app/core' 4import { Notifier } from '@app/core'
5import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' 5import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' 9import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
@@ -27,7 +27,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
27 private openedModal: NgbModalRef 27 private openedModal: NgbModalRef
28 28
29 constructor ( 29 constructor (
30 protected formValidatorService: FormValidatorService, 30 protected formReactiveService: FormReactiveService,
31 private modalService: NgbModal, 31 private modalService: NgbModal,
32 private abuseService: AbuseService, 32 private abuseService: AbuseService,
33 private notifier: Notifier, 33 private notifier: Notifier,
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
index 617408f2a..27dcf043a 100644
--- a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
+++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
@@ -2,7 +2,7 @@ import { forkJoin } from 'rxjs'
2import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { prepareIcu } from '@app/helpers' 4import { prepareIcu } from '@app/helpers'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
8import { User } from '@shared/models' 8import { User } from '@shared/models'
@@ -25,7 +25,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
25 modalMessage = '' 25 modalMessage = ''
26 26
27 constructor ( 27 constructor (
28 protected formValidatorService: FormValidatorService, 28 protected formReactiveService: FormReactiveService,
29 private modalService: NgbModal, 29 private modalService: NgbModal,
30 private notifier: Notifier, 30 private notifier: Notifier,
31 private userAdminService: UserAdminService, 31 private userAdminService: UserAdminService,
diff --git a/client/src/app/shared/shared-moderation/video-block.component.ts b/client/src/app/shared/shared-moderation/video-block.component.ts
index f8b22a3f6..3ff53443a 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.ts
+++ b/client/src/app/shared/shared-moderation/video-block.component.ts
@@ -1,7 +1,7 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { prepareIcu } from '@app/helpers' 3import { prepareIcu } from '@app/helpers'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { Video } from '@app/shared/shared-main' 5import { Video } from '@app/shared/shared-main'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -25,7 +25,7 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
25 private openedModal: NgbModalRef 25 private openedModal: NgbModalRef
26 26
27 constructor ( 27 constructor (
28 protected formValidatorService: FormValidatorService, 28 protected formReactiveService: FormReactiveService,
29 private modalService: NgbModal, 29 private modalService: NgbModal,
30 private videoBlocklistService: VideoBlockService, 30 private videoBlocklistService: VideoBlockService,
31 private notifier: Notifier 31 private notifier: Notifier
diff --git a/client/src/app/shared/shared-search/find-in-bulk.service.ts b/client/src/app/shared/shared-search/find-in-bulk.service.ts
index d2f8c3213..d6ee04379 100644
--- a/client/src/app/shared/shared-search/find-in-bulk.service.ts
+++ b/client/src/app/shared/shared-search/find-in-bulk.service.ts
@@ -80,13 +80,18 @@ export class FindInBulkService {
80 map(result => result.response.data), 80 map(result => result.response.data),
81 map(data => data.find(finder)) 81 map(data => data.find(finder))
82 ) 82 )
83 .subscribe(result => { 83 .subscribe({
84 if (!result) { 84 next: result => {
85 obs.error(new Error($localize`Element ${param} not found`)) 85 if (!result) {
86 } else { 86 obs.error(new Error($localize`Element ${param} not found`))
87 return
88 }
89
87 obs.next(result) 90 obs.next(result)
88 obs.complete() 91 obs.complete()
89 } 92 },
93
94 error: err => obs.error(err)
90 }) 95 })
91 96
92 observableObject.notifier.next(param) 97 observableObject.notifier.next(param)
diff --git a/client/src/app/shared/shared-support-modal/support-modal.component.ts b/client/src/app/shared/shared-support-modal/support-modal.component.ts
index 08e997f7b..f330228e1 100644
--- a/client/src/app/shared/shared-support-modal/support-modal.component.ts
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.ts
@@ -27,7 +27,7 @@ export class SupportModalComponent {
27 27
28 const support = this.video?.support || this.videoChannel.support 28 const support = this.video?.support || this.videoChannel.support
29 29
30 this.markdownService.enhancedMarkdownToHTML(support) 30 this.markdownService.enhancedMarkdownToHTML({ markdown: support })
31 .then(r => { 31 .then(r => {
32 this.htmlSupport = r 32 this.htmlSupport = r
33 }) 33 })
diff --git a/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts b/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts
index 13e2e5424..c2c30d38b 100644
--- a/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts
+++ b/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts
@@ -1,7 +1,7 @@
1import { Subject, Subscription } from 'rxjs' 1import { Subject, Subscription } from 'rxjs'
2import { Component, Input, OnDestroy, OnInit } from '@angular/core' 2import { Component, Input, OnDestroy, OnInit } from '@angular/core'
3import { AuthService, Notifier, ServerService, ThemeService, UserService } from '@app/core' 3import { AuthService, Notifier, ServerService, ThemeService, UserService } from '@app/core'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { HTMLServerConfig, User, UserUpdateMe } from '@shared/models' 5import { HTMLServerConfig, User, UserUpdateMe } from '@shared/models'
6import { SelectOptionsItem } from 'src/types' 6import { SelectOptionsItem } from 'src/types'
7 7
@@ -22,7 +22,7 @@ export class UserInterfaceSettingsComponent extends FormReactive implements OnIn
22 private serverConfig: HTMLServerConfig 22 private serverConfig: HTMLServerConfig
23 23
24 constructor ( 24 constructor (
25 protected formValidatorService: FormValidatorService, 25 protected formReactiveService: FormReactiveService,
26 private authService: AuthService, 26 private authService: AuthService,
27 private notifier: Notifier, 27 private notifier: Notifier,
28 private userService: UserService, 28 private userService: UserService,
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.ts b/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
index 7d6b69469..af0870f12 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
@@ -3,7 +3,7 @@ import { Subject, Subscription } from 'rxjs'
3import { first } from 'rxjs/operators' 3import { first } from 'rxjs/operators'
4import { Component, Input, OnDestroy, OnInit } from '@angular/core' 4import { Component, Input, OnDestroy, OnInit } from '@angular/core'
5import { AuthService, Notifier, ServerService, User, UserService } from '@app/core' 5import { AuthService, Notifier, ServerService, User, UserService } from '@app/core'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7import { UserUpdateMe } from '@shared/models' 7import { UserUpdateMe } from '@shared/models'
8import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' 8import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
9 9
@@ -22,7 +22,7 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
22 formValuesWatcher: Subscription 22 formValuesWatcher: Subscription
23 23
24 constructor ( 24 constructor (
25 protected formValidatorService: FormValidatorService, 25 protected formReactiveService: FormReactiveService,
26 private authService: AuthService, 26 private authService: AuthService,
27 private notifier: Notifier, 27 private notifier: Notifier,
28 private userService: UserService, 28 private userService: UserService,
diff --git a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
index 7bcfdd8aa..61bcd5345 100644
--- a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
+++ b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
@@ -1,6 +1,6 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
4import { logger } from '@root-helpers/logger' 4import { logger } from '@root-helpers/logger'
5import { USER_HANDLE_VALIDATOR } from '../form-validators/user-validators' 5import { USER_HANDLE_VALIDATOR } from '../form-validators/user-validators'
6 6
@@ -15,7 +15,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
15 @Input() showHelp = false 15 @Input() showHelp = false
16 16
17 constructor ( 17 constructor (
18 protected formValidatorService: FormValidatorService, 18 protected formReactiveService: FormReactiveService,
19 private notifier: Notifier 19 private notifier: Notifier
20 ) { 20 ) {
21 super() 21 super()
diff --git a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
index 0e09c2697..341b83a04 100644
--- a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
+++ b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
@@ -37,7 +37,7 @@
37 class="btn-group" ngbDropdown autoClose="outside" placement="bottom-right bottom-left bottom auto" 37 class="btn-group" ngbDropdown autoClose="outside" placement="bottom-right bottom-left bottom auto"
38 role="group" aria-label="Multiple ways to subscribe to the current channel" i18n-aria-label 38 role="group" aria-label="Multiple ways to subscribe to the current channel" i18n-aria-label
39 > 39 >
40 <button class="btn dropdown-toggle-split" ngbDropdownToggle aria-label="Open subscription dropdown" i18n-aria-label> 40 <button class="btn dropdown-toggle-split last-in-group" ngbDropdownToggle aria-label="Open subscription dropdown" i18n-aria-label>
41 <ng-container 41 <ng-container
42 *ngIf="!isUserLoggedIn(); then userLoggedOut"> 42 *ngIf="!isUserLoggedIn(); then userLoggedOut">
43 </ng-container> 43 </ng-container>
diff --git a/client/src/app/shared/shared-users/index.ts b/client/src/app/shared/shared-users/index.ts
index 8f90f2515..20e60486d 100644
--- a/client/src/app/shared/shared-users/index.ts
+++ b/client/src/app/shared/shared-users/index.ts
@@ -1,4 +1,5 @@
1export * from './user-admin.service' 1export * from './user-admin.service'
2export * from './user-signup.service' 2export * from './user-signup.service'
3export * from './two-factor.service'
3 4
4export * from './shared-users.module' 5export * from './shared-users.module'
diff --git a/client/src/app/shared/shared-users/shared-users.module.ts b/client/src/app/shared/shared-users/shared-users.module.ts
index 2a1dadf20..5a1675dc9 100644
--- a/client/src/app/shared/shared-users/shared-users.module.ts
+++ b/client/src/app/shared/shared-users/shared-users.module.ts
@@ -1,6 +1,7 @@
1 1
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { SharedMainModule } from '../shared-main/shared-main.module' 3import { SharedMainModule } from '../shared-main/shared-main.module'
4import { TwoFactorService } from './two-factor.service'
4import { UserAdminService } from './user-admin.service' 5import { UserAdminService } from './user-admin.service'
5import { UserSignupService } from './user-signup.service' 6import { UserSignupService } from './user-signup.service'
6 7
@@ -15,7 +16,8 @@ import { UserSignupService } from './user-signup.service'
15 16
16 providers: [ 17 providers: [
17 UserSignupService, 18 UserSignupService,
18 UserAdminService 19 UserAdminService,
20 TwoFactorService
19 ] 21 ]
20}) 22})
21export class SharedUsersModule { } 23export class SharedUsersModule { }
diff --git a/client/src/app/shared/shared-users/two-factor.service.ts b/client/src/app/shared/shared-users/two-factor.service.ts
new file mode 100644
index 000000000..9ff916f15
--- /dev/null
+++ b/client/src/app/shared/shared-users/two-factor.service.ts
@@ -0,0 +1,52 @@
1import { catchError } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor, UserService } from '@app/core'
5import { TwoFactorEnableResult } from '@shared/models'
6
7@Injectable()
8export class TwoFactorService {
9 constructor (
10 private authHttp: HttpClient,
11 private restExtractor: RestExtractor
12 ) { }
13
14 // ---------------------------------------------------------------------------
15
16 requestTwoFactor (options: {
17 userId: number
18 currentPassword: string
19 }) {
20 const { userId, currentPassword } = options
21
22 const url = UserService.BASE_USERS_URL + userId + '/two-factor/request'
23
24 return this.authHttp.post<TwoFactorEnableResult>(url, { currentPassword })
25 .pipe(catchError(err => this.restExtractor.handleError(err)))
26 }
27
28 confirmTwoFactorRequest (options: {
29 userId: number
30 requestToken: string
31 otpToken: string
32 }) {
33 const { userId, requestToken, otpToken } = options
34
35 const url = UserService.BASE_USERS_URL + userId + '/two-factor/confirm-request'
36
37 return this.authHttp.post(url, { requestToken, otpToken })
38 .pipe(catchError(err => this.restExtractor.handleError(err)))
39 }
40
41 disableTwoFactor (options: {
42 userId: number
43 currentPassword?: string
44 }) {
45 const { userId, currentPassword } = options
46
47 const url = UserService.BASE_USERS_URL + userId + '/two-factor/disable'
48
49 return this.authHttp.post(url, { currentPassword })
50 .pipe(catchError(err => this.restExtractor.handleError(err)))
51 }
52}
diff --git a/client/src/app/shared/shared-users/user-admin.service.ts b/client/src/app/shared/shared-users/user-admin.service.ts
index 4128358dc..0b04023a3 100644
--- a/client/src/app/shared/shared-users/user-admin.service.ts
+++ b/client/src/app/shared/shared-users/user-admin.service.ts
@@ -125,7 +125,10 @@ export class UserAdminService {
125 } 125 }
126 126
127 return Object.assign(user, { 127 return Object.assign(user, {
128 roleLabel: roleLabels[user.role], 128 role: {
129 id: user.role.id,
130 label: roleLabels[user.role.id]
131 },
129 videoQuota, 132 videoQuota,
130 videoQuotaUsed, 133 videoQuotaUsed,
131 rawVideoQuota: user.videoQuota, 134 rawVideoQuota: user.videoQuota,
diff --git a/client/src/app/shared/shared-video-live/live-stream-information.component.html b/client/src/app/shared/shared-video-live/live-stream-information.component.html
index cf30c1ce1..8e61bdbb3 100644
--- a/client/src/app/shared/shared-video-live/live-stream-information.component.html
+++ b/client/src/app/shared/shared-video-live/live-stream-information.component.html
@@ -32,7 +32,7 @@
32 <div class="form-group-description" i18n>⚠️ Never share your stream key with anyone.</div> 32 <div class="form-group-description" i18n>⚠️ Never share your stream key with anyone.</div>
33 </div> 33 </div>
34 34
35 <div class="journal"> 35 <div class="journal" *ngIf="latestLiveSessions.length !== 0">
36 <label i18n>Latest live sessions</label> 36 <label i18n>Latest live sessions</label>
37 37
38 <div class="journal-session" *ngFor="let session of latestLiveSessions"> 38 <div class="journal-session" *ngFor="let session of latestLiveSessions">
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html
index 1c7458b4b..1f622933d 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.html
@@ -48,10 +48,7 @@
48 48
49 <ng-template ngbNavContent> 49 <ng-template ngbNavContent>
50 <div class="nav-content"> 50 <div class="nav-content">
51 <my-input-text 51 <my-input-text [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"></my-input-text>
52 *ngIf="!isConfidentialVideo()"
53 [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"
54 ></my-input-text>
55 </div> 52 </div>
56 </ng-template> 53 </ng-template>
57 </ng-container> 54 </ng-container>
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts
index 47482caaa..4135542dc 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.ts
@@ -2,14 +2,15 @@ import { mapValues, pick } from 'lodash-es'
2import { firstValueFrom } from 'rxjs' 2import { firstValueFrom } from 'rxjs'
3import { tap } from 'rxjs/operators' 3import { tap } from 'rxjs/operators'
4import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' 4import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
5import { AuthService, HooksService, Notifier } from '@app/core' 5import { HooksService } from '@app/core'
6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
7import { logger } from '@root-helpers/logger' 7import { logger } from '@root-helpers/logger'
8import { videoRequiresAuth } from '@root-helpers/video'
8import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' 9import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
9import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' 10import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main'
10 11
11type DownloadType = 'video' | 'subtitles' 12type DownloadType = 'video' | 'subtitles'
12type FileMetadata = { [key: string]: { label: string, value: string }} 13type FileMetadata = { [key: string]: { label: string, value: string } }
13 14
14@Component({ 15@Component({
15 selector: 'my-video-download', 16 selector: 'my-video-download',
@@ -32,6 +33,8 @@ export class VideoDownloadComponent {
32 33
33 type: DownloadType = 'video' 34 type: DownloadType = 'video'
34 35
36 videoFileToken: string
37
35 private activeModal: NgbModalRef 38 private activeModal: NgbModalRef
36 39
37 private bytesPipe: BytesPipe 40 private bytesPipe: BytesPipe
@@ -42,10 +45,9 @@ export class VideoDownloadComponent {
42 45
43 constructor ( 46 constructor (
44 @Inject(LOCALE_ID) private localeId: string, 47 @Inject(LOCALE_ID) private localeId: string,
45 private notifier: Notifier,
46 private modalService: NgbModal, 48 private modalService: NgbModal,
47 private videoService: VideoService, 49 private videoService: VideoService,
48 private auth: AuthService, 50 private videoFileTokenService: VideoFileTokenService,
49 private hooks: HooksService 51 private hooks: HooksService
50 ) { 52 ) {
51 this.bytesPipe = new BytesPipe() 53 this.bytesPipe = new BytesPipe()
@@ -71,6 +73,8 @@ export class VideoDownloadComponent {
71 } 73 }
72 74
73 show (video: VideoDetails, videoCaptions?: VideoCaption[]) { 75 show (video: VideoDetails, videoCaptions?: VideoCaption[]) {
76 this.videoFileToken = undefined
77
74 this.video = video 78 this.video = video
75 this.videoCaptions = videoCaptions 79 this.videoCaptions = videoCaptions
76 80
@@ -84,6 +88,11 @@ export class VideoDownloadComponent {
84 this.subtitleLanguageId = this.videoCaptions[0].language.id 88 this.subtitleLanguageId = this.videoCaptions[0].language.id
85 } 89 }
86 90
91 if (videoRequiresAuth(this.video)) {
92 this.videoFileTokenService.getVideoFileToken(this.video.uuid)
93 .subscribe(({ token }) => this.videoFileToken = token)
94 }
95
87 this.activeModal.shown.subscribe(() => { 96 this.activeModal.shown.subscribe(() => {
88 this.hooks.runAction('action:modal.video-download.shown', 'common') 97 this.hooks.runAction('action:modal.video-download.shown', 'common')
89 }) 98 })
@@ -155,7 +164,7 @@ export class VideoDownloadComponent {
155 if (!file) return '' 164 if (!file) return ''
156 165
157 const suffix = this.isConfidentialVideo() 166 const suffix = this.isConfidentialVideo()
158 ? '?access_token=' + this.auth.getAccessToken() 167 ? '?videoFileToken=' + this.videoFileToken
159 : '' 168 : ''
160 169
161 switch (this.downloadType) { 170 switch (this.downloadType) {
diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
index 9ddfd7dda..1e92e1952 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
@@ -47,6 +47,7 @@
47 <ng-option i18n value="-publishedAt">Sort by <strong>"Recently Added"</strong></ng-option> 47 <ng-option i18n value="-publishedAt">Sort by <strong>"Recently Added"</strong></ng-option>
48 <ng-option i18n value="-originallyPublishedAt">Sort by <strong>"Original Publication Date"</strong></ng-option> 48 <ng-option i18n value="-originallyPublishedAt">Sort by <strong>"Original Publication Date"</strong></ng-option>
49 49
50 <ng-option i18n value="name">Sort by <strong>"Name"</strong></ng-option>
50 <ng-option i18n *ngIf="isTrendingSortEnabled('most-viewed')" value="-trending">Sort by <strong>"Recent Views"</strong></ng-option> 51 <ng-option i18n *ngIf="isTrendingSortEnabled('most-viewed')" value="-trending">Sort by <strong>"Recent Views"</strong></ng-option>
51 <ng-option i18n *ngIf="isTrendingSortEnabled('hot')" value="-hot">Sort by <strong>"Hot"</strong></ng-option> 52 <ng-option i18n *ngIf="isTrendingSortEnabled('hot')" value="-hot">Sort by <strong>"Hot"</strong></ng-option>
52 <ng-option i18n *ngIf="isTrendingSortEnabled('most-liked')" value="-likes">Sort by <strong>"Likes"</strong></ng-option> 53 <ng-option i18n *ngIf="isTrendingSortEnabled('most-liked')" value="-likes">Sort by <strong>"Likes"</strong></ng-option>
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index e8d2ca1c4..6fdf24b2d 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -52,6 +52,12 @@
52 <ng-container *ngIf="displayOptions.privacyText && displayOptions.state && getStateLabel(video)"> - </ng-container> 52 <ng-container *ngIf="displayOptions.privacyText && displayOptions.state && getStateLabel(video)"> - </ng-container>
53 <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> 53 <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container>
54 </div> 54 </div>
55
56 <div *ngIf="containedInPlaylists" class="video-contained-in-playlists">
57 <a *ngFor="let playlist of containedInPlaylists" class="chip rectangular bg-secondary text-light" [routerLink]="['/w/p/', playlist.playlistShortUUID]">
58 {{ playlist.playlistDisplayName }}
59 </a>
60 </div>
55 </div> 61 </div>
56 </div> 62 </div>
57 63
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
index a397efdca..ba2adfc5a 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
@@ -4,6 +4,10 @@
4 4
5$more-button-width: 40px; 5$more-button-width: 40px;
6 6
7.chip {
8 @include chip;
9}
10
7.video-miniature { 11.video-miniature {
8 font-size: 14px; 12 font-size: 14px;
9} 13}
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
index 534a78b3f..85c63c173 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
@@ -11,7 +11,7 @@ import {
11 Output 11 Output
12} from '@angular/core' 12} from '@angular/core'
13import { AuthService, ScreenService, ServerService, User } from '@app/core' 13import { AuthService, ScreenService, ServerService, User } from '@app/core'
14import { HTMLServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models' 14import { HTMLServerConfig, VideoExistInPlaylist, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models'
15import { LinkType } from '../../../types/link.type' 15import { LinkType } from '../../../types/link.type'
16import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component' 16import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component'
17import { Video } from '../shared-main' 17import { Video } from '../shared-main'
@@ -21,13 +21,15 @@ import { VideoActionsDisplayType } from './video-actions-dropdown.component'
21export type MiniatureDisplayOptions = { 21export type MiniatureDisplayOptions = {
22 date?: boolean 22 date?: boolean
23 views?: boolean 23 views?: boolean
24 by?: boolean
25 avatar?: boolean 24 avatar?: boolean
26 privacyLabel?: boolean 25 privacyLabel?: boolean
27 privacyText?: boolean 26 privacyText?: boolean
28 state?: boolean 27 state?: boolean
29 blacklistInfo?: boolean 28 blacklistInfo?: boolean
30 nsfw?: boolean 29 nsfw?: boolean
30
31 by?: boolean
32 forceChannelInBy?: boolean
31} 33}
32@Component({ 34@Component({
33 selector: 'my-video-miniature', 35 selector: 'my-video-miniature',
@@ -38,6 +40,7 @@ export type MiniatureDisplayOptions = {
38export class VideoMiniatureComponent implements OnInit { 40export class VideoMiniatureComponent implements OnInit {
39 @Input() user: User 41 @Input() user: User
40 @Input() video: Video 42 @Input() video: Video
43 @Input() containedInPlaylists: VideoExistInPlaylist[]
41 44
42 @Input() displayOptions: MiniatureDisplayOptions = { 45 @Input() displayOptions: MiniatureDisplayOptions = {
43 date: true, 46 date: true,
@@ -47,7 +50,8 @@ export class VideoMiniatureComponent implements OnInit {
47 privacyLabel: false, 50 privacyLabel: false,
48 privacyText: false, 51 privacyText: false,
49 state: false, 52 state: false,
50 blacklistInfo: false 53 blacklistInfo: false,
54 forceChannelInBy: false
51 } 55 }
52 56
53 @Input() displayVideoActions = true 57 @Input() displayVideoActions = true
@@ -267,6 +271,11 @@ export class VideoMiniatureComponent implements OnInit {
267 } 271 }
268 272
269 private setUpBy () { 273 private setUpBy () {
274 if (this.displayOptions.forceChannelInBy) {
275 this.ownerDisplayType = 'videoChannel'
276 return
277 }
278
270 const accountName = this.video.account.name 279 const accountName = this.video.account.name
271 280
272 // If the video channel name is an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) 281 // If the video channel name is an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.html b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
index 6ea2661e4..6c6db4b96 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.html
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
@@ -12,6 +12,7 @@
12 </div> 12 </div>
13 13
14 <my-video-miniature 14 <my-video-miniature
15 [containedInPlaylists]="videosContainedInPlaylists ? videosContainedInPlaylists[video.id] : undefined"
15 [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" 16 [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions"
16 [displayVideoActions]="false" [user]="user" 17 [displayVideoActions]="false" [user]="user"
17 ></my-video-miniature> 18 ></my-video-miniature>
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
index fa3c79bbb..460a0080e 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
@@ -2,7 +2,7 @@ import { Observable, Subject } from 'rxjs'
2import { AfterContentInit, Component, ContentChildren, EventEmitter, Input, Output, QueryList, TemplateRef } from '@angular/core' 2import { AfterContentInit, Component, ContentChildren, EventEmitter, Input, Output, QueryList, TemplateRef } from '@angular/core'
3import { ComponentPagination, Notifier, User } from '@app/core' 3import { ComponentPagination, Notifier, User } from '@app/core'
4import { logger } from '@root-helpers/logger' 4import { logger } from '@root-helpers/logger'
5import { ResultList, VideoSortField } from '@shared/models' 5import { ResultList, VideosExistInPlaylists, VideoSortField } from '@shared/models'
6import { PeerTubeTemplateDirective, Video } from '../shared-main' 6import { PeerTubeTemplateDirective, Video } from '../shared-main'
7import { MiniatureDisplayOptions } from './video-miniature.component' 7import { MiniatureDisplayOptions } from './video-miniature.component'
8 8
@@ -14,6 +14,7 @@ export type SelectionType = { [ id: number ]: boolean }
14 styleUrls: [ './videos-selection.component.scss' ] 14 styleUrls: [ './videos-selection.component.scss' ]
15}) 15})
16export class VideosSelectionComponent implements AfterContentInit { 16export class VideosSelectionComponent implements AfterContentInit {
17 @Input() videosContainedInPlaylists: VideosExistInPlaylists
17 @Input() user: User 18 @Input() user: User
18 @Input() pagination: ComponentPagination 19 @Input() pagination: ComponentPagination
19 20
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
index e019fdd26..2fc39fc75 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
@@ -3,11 +3,11 @@ import { Subject, Subscription } from 'rxjs'
3import { debounceTime, filter } from 'rxjs/operators' 3import { debounceTime, filter } from 'rxjs/operators'
4import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' 4import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'
5import { AuthService, DisableForReuseHook, Notifier } from '@app/core' 5import { AuthService, DisableForReuseHook, Notifier } from '@app/core'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7import { secondsToTime } from '@shared/core-utils' 7import { secondsToTime } from '@shared/core-utils'
8import { 8import {
9 CachedVideoExistInPlaylist,
9 Video, 10 Video,
10 VideoExistInPlaylist,
11 VideoPlaylistCreate, 11 VideoPlaylistCreate,
12 VideoPlaylistElementCreate, 12 VideoPlaylistElementCreate,
13 VideoPlaylistElementUpdate, 13 VideoPlaylistElementUpdate,
@@ -59,7 +59,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
59 private pendingAddId: number 59 private pendingAddId: number
60 60
61 constructor ( 61 constructor (
62 protected formValidatorService: FormValidatorService, 62 protected formReactiveService: FormReactiveService,
63 private authService: AuthService, 63 private authService: AuthService,
64 private notifier: Notifier, 64 private notifier: Notifier,
65 private videoPlaylistService: VideoPlaylistService, 65 private videoPlaylistService: VideoPlaylistService,
@@ -330,7 +330,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
330 } 330 }
331 } 331 }
332 332
333 private rebuildPlaylists (existResult: VideoExistInPlaylist[]) { 333 private rebuildPlaylists (existResult: CachedVideoExistInPlaylist[]) {
334 debugLogger('Got existing results for %d.', this.video.id, existResult) 334 debugLogger('Got existing results for %d.', this.video.id, existResult)
335 335
336 const oldPlaylists = this.videoPlaylists 336 const oldPlaylists = this.videoPlaylists
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
index 7a2574345..79b7b9a50 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts
@@ -74,7 +74,7 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit {
74 } 74 }
75 75
76 buildRouterQuery () { 76 buildRouterQuery () {
77 if (!this.playlistElement || !this.playlistElement.video) return {} 77 if (!this.playlistElement?.video) return {}
78 78
79 return { 79 return {
80 playlistPosition: this.playlistElement.position, 80 playlistPosition: this.playlistElement.position,
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
index dd9fe0a5a..225c4eb64 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
@@ -32,7 +32,7 @@ export class VideoPlaylistMiniatureComponent implements OnInit {
32 async ngOnInit () { 32 async ngOnInit () {
33 this.buildPlaylistUrl() 33 this.buildPlaylistUrl()
34 if (this.displayDescription) { 34 if (this.displayDescription) {
35 this.playlistDescription = await this.markdownService.textMarkdownToHTML(this.playlist.description) 35 this.playlistDescription = await this.markdownService.textMarkdownToHTML({ markdown: this.playlist.description })
36 } 36 }
37 } 37 }
38 38
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
index d71f8f72e..330a51f91 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
@@ -8,6 +8,8 @@ import { buildBulkObservable, objectToFormData } from '@app/helpers'
8import { Account, AccountService, VideoChannel, VideoChannelService } from '@app/shared/shared-main' 8import { Account, AccountService, VideoChannel, VideoChannelService } from '@app/shared/shared-main'
9import { NGX_LOADING_BAR_IGNORED } from '@ngx-loading-bar/http-client' 9import { NGX_LOADING_BAR_IGNORED } from '@ngx-loading-bar/http-client'
10import { 10import {
11 CachedVideoExistInPlaylist,
12 CachedVideosExistInPlaylists,
11 ResultList, 13 ResultList,
12 VideoExistInPlaylist, 14 VideoExistInPlaylist,
13 VideoPlaylist as VideoPlaylistServerModel, 15 VideoPlaylist as VideoPlaylistServerModel,
@@ -34,11 +36,11 @@ export class VideoPlaylistService {
34 36
35 // Use a replay subject because we "next" a value before subscribing 37 // Use a replay subject because we "next" a value before subscribing
36 private videoExistsInPlaylistNotifier = new ReplaySubject<number>(1) 38 private videoExistsInPlaylistNotifier = new ReplaySubject<number>(1)
37 private videoExistsInPlaylistCacheSubject = new Subject<VideosExistInPlaylists>() 39 private videoExistsInPlaylistCacheSubject = new Subject<CachedVideosExistInPlaylists>()
38 private readonly videoExistsInPlaylistObservable: Observable<VideosExistInPlaylists> 40 private readonly videoExistsInPlaylistObservable: Observable<CachedVideosExistInPlaylists>
39 41
40 private videoExistsObservableCache: { [ id: number ]: Observable<VideoExistInPlaylist[]> } = {} 42 private videoExistsObservableCache: { [ id: number ]: Observable<CachedVideoExistInPlaylist[]> } = {}
41 private videoExistsCache: { [ id: number ]: VideoExistInPlaylist[] } = {} 43 private videoExistsCache: { [ id: number ]: CachedVideoExistInPlaylist[] } = {}
42 44
43 private myAccountPlaylistCache: ResultList<CachedPlaylist> = undefined 45 private myAccountPlaylistCache: ResultList<CachedPlaylist> = undefined
44 private myAccountPlaylistCacheRunning: Observable<ResultList<CachedPlaylist>> 46 private myAccountPlaylistCacheRunning: Observable<ResultList<CachedPlaylist>>
@@ -346,7 +348,7 @@ export class VideoPlaylistService {
346 ) 348 )
347 } 349 }
348 350
349 private doVideosExistInPlaylist (videoIds: number[]): Observable<VideosExistInPlaylists> { 351 doVideosExistInPlaylist (videoIds: number[]): Observable<VideosExistInPlaylists> {
350 const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' 352 const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
351 353
352 let params = new HttpParams() 354 let params = new HttpParams()