diff options
Diffstat (limited to 'client/src/app')
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 @@ | |||
1 | import { SortMeta } from 'primeng/api' | 1 | import { SortMeta } from 'primeng/api' |
2 | import { Component, OnInit } from '@angular/core' | 2 | import { Component, OnInit } from '@angular/core' |
3 | import { ComponentPagination, hasMoreItems, Notifier, RestService } from '@app/core' | 3 | import { ComponentPagination, hasMoreItems, Notifier, RestService, ServerService } from '@app/core' |
4 | import { InstanceFollowService } from '@app/shared/shared-instance' | 4 | import { InstanceFollowService } from '@app/shared/shared-instance' |
5 | import { 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 | ||
12 | export class AboutFollowsComponent implements OnInit { | 13 | export 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' |
10 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 10 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
11 | import { InstanceService } from '@app/shared/shared-instance' | 11 | import { InstanceService } from '@app/shared/shared-instance' |
12 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 12 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
13 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 13 | import { 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' |
52 | import { SharedAdminModule } from './shared' | ||
52 | import { JobService, LogsComponent, LogsService } from './system' | 53 | import { JobService, LogsComponent, LogsService } from './system' |
53 | import { DebugComponent, DebugService } from './system/debug' | 54 | import { DebugComponent, DebugService } from './system/debug' |
54 | import { JobsComponent } from './system/jobs/jobs.component' | 55 | import { 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 | |||
154 | my-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' |
28 | import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' | 28 | import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' |
29 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 29 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
30 | import { CustomPageService } from '@app/shared/shared-main/custom-page' | 30 | import { CustomPageService } from '@app/shared/shared-main/custom-page' |
31 | import { CustomConfig, CustomPage, HTMLServerConfig } from '@shared/models' | 31 | import { CustomConfig, CustomPage, HTMLServerConfig } from '@shared/models' |
32 | import { EditConfigurationService } from './edit-configuration.service' | 32 | import { 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 | |||
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | 3 | import { prepareIcu } from '@app/helpers' |
4 | import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' | 4 | import { splitAndGetNotEmpty, UNIQUE_HOSTS_OR_HANDLE_VALIDATOR } from '@app/shared/form-validators/host-validators' |
5 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
6 | import { InstanceFollowService } from '@app/shared/shared-instance' | 6 | import { InstanceFollowService } from '@app/shared/shared-instance' |
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
@@ -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' |
15 | import { FormValidatorService } from '@app/shared/shared-forms' | 15 | import { FormReactiveService } from '@app/shared/shared-forms' |
16 | import { UserAdminService } from '@app/shared/shared-users' | 16 | import { UserAdminService } from '@app/shared/shared-users' |
17 | import { UserCreate, UserRole } from '@shared/models' | 17 | import { UserCreate, UserRole } from '@shared/models' |
18 | import { UserEdit } from './user-edit' | 18 | import { 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 { | 44 | my-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' | |||
3 | import { AuthService, ScreenService, ServerService, User } from '@app/core' | 3 | import { AuthService, ScreenService, ServerService, User } from '@app/core' |
4 | import { FormReactive } from '@app/shared/shared-forms' | 4 | import { FormReactive } from '@app/shared/shared-forms' |
5 | import { USER_ROLE_LABELS } from '@shared/core-utils/users' | 5 | import { USER_ROLE_LABELS } from '@shared/core-utils/users' |
6 | import { HTMLServerConfig, UserAdminFlag, UserRole, VideoResolution } from '@shared/models' | 6 | import { HTMLServerConfig, UserAdminFlag, UserRole } from '@shared/models' |
7 | import { SelectOptionsItem } from '../../../../../types/select-options-item.model' | 7 | import { 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 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' | 3 | import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' |
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { UserAdminService } from '@app/shared/shared-users' | 5 | import { UserAdminService } from '@app/shared/shared-users' |
6 | import { UserUpdate } from '@shared/models' | 6 | import { 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' |
12 | import { FormValidatorService } from '@app/shared/shared-forms' | 12 | import { FormReactiveService } from '@app/shared/shared-forms' |
13 | import { UserAdminService } from '@app/shared/shared-users' | 13 | import { TwoFactorService, UserAdminService } from '@app/shared/shared-users' |
14 | import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' | 14 | import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' |
15 | import { UserEdit } from './user-edit' | 15 | import { 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' | |||
8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 8 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
9 | import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' | 9 | import { VideoBlockComponent, VideoBlockService } from '@app/shared/shared-moderation' |
10 | import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature' | 10 | import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature' |
11 | import { getAllFiles } from '@shared/core-utils' | ||
11 | import { UserRight, VideoFile, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models' | 12 | import { UserRight, VideoFile, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models' |
12 | import { VideoAdminService } from './video-admin.service' | 13 | import { 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' | |||
4 | import { ActivatedRoute } from '@angular/router' | 4 | import { ActivatedRoute } from '@angular/router' |
5 | import { HooksService, Notifier, PluginService } from '@app/core' | 5 | import { HooksService, Notifier, PluginService } from '@app/core' |
6 | import { BuildFormArgument } from '@app/shared/form-validators' | 6 | import { BuildFormArgument } from '@app/shared/form-validators' |
7 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
8 | import { PeerTubePlugin, RegisterServerSettingOptions } from '@shared/models' | 8 | import { PeerTubePlugin, RegisterServerSettingOptions } from '@shared/models' |
9 | import { PluginApiService } from '../shared/plugin-api.service' | 9 | import { 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 @@ | |||
1 | export * from './user-real-quota-info.component' | ||
2 | |||
3 | export * 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 @@ | |||
1 | import { NgModule } from '@angular/core' | ||
2 | import { SharedMainModule } from '../../shared/shared-main/shared-main.module' | ||
3 | import { 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 | }) | ||
20 | export 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 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { ServerService } from '@app/core' | ||
3 | import { 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 | }) | ||
10 | export 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 | ||
7 | label { | 7 | label { |
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 | |||
2 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 1 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
3 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
4 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' | 3 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' |
5 | import { HooksService } from '@app/core/plugins/hooks.service' | 4 | import { HooksService } from '@app/core/plugins/hooks.service' |
6 | import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' | 5 | import { LOGIN_PASSWORD_VALIDATOR, LOGIN_USERNAME_VALIDATOR } from '@app/shared/form-validators/login-validators' |
7 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators' |
7 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' | ||
8 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | 8 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' |
9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
10 | import { PluginsManager } from '@root-helpers/plugins-manager' | 10 | import { 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' |
12 | import { FormValidatorService } from '@app/shared/shared-forms' | 12 | import { FormReactiveService } from '@app/shared/shared-forms' |
13 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 13 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
14 | import { HttpStatusCode, VideoChannelCreate } from '@shared/models' | 14 | import { HttpStatusCode, VideoChannelCreate } from '@shared/models' |
15 | import { VideoChannelEdit } from './video-channel-edit' | 15 | import { 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' |
12 | import { FormValidatorService } from '@app/shared/shared-forms' | 12 | import { FormReactiveService } from '@app/shared/shared-forms' |
13 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 13 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
14 | import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models' | 14 | import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models' |
15 | import { VideoChannelEdit } from './video-channel-edit' | 15 | import { 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 | |||
7 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' | 7 | import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' |
8 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' | 8 | import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' |
9 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' | 9 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' |
10 | import { MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor' | ||
10 | import { MyAccountComponent } from './my-account.component' | 11 | import { MyAccountComponent } from './my-account.component' |
11 | 12 | ||
12 | const myAccountRoutes: Routes = [ | 13 | const 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' | |||
3 | import { Component, OnInit } from '@angular/core' | 3 | import { Component, OnInit } from '@angular/core' |
4 | import { AuthService, ServerService, UserService } from '@app/core' | 4 | import { AuthService, ServerService, UserService } from '@app/core' |
5 | import { USER_EMAIL_VALIDATOR, USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' | 5 | import { USER_EMAIL_VALIDATOR, USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' |
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
7 | import { User } from '@shared/models' | 7 | import { 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' |
9 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 9 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
10 | import { User } from '@shared/models' | 10 | import { 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' | |||
2 | import { Component, Input, OnInit } from '@angular/core' | 2 | import { Component, Input, OnInit } from '@angular/core' |
3 | import { Notifier, User, UserService } from '@app/core' | 3 | import { Notifier, User, UserService } from '@app/core' |
4 | import { USER_DESCRIPTION_VALIDATOR, USER_DISPLAY_NAME_REQUIRED_VALIDATOR } from '@app/shared/form-validators/user-validators' | 4 | import { USER_DESCRIPTION_VALIDATOR, USER_DISPLAY_NAME_REQUIRED_VALIDATOR } from '@app/shared/form-validators/user-validators' |
5 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 5 | import { 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 @@ | |||
1 | export * from './my-account-two-factor-button.component' | ||
2 | export * 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 @@ | |||
1 | import { Subject } from 'rxjs' | ||
2 | import { Component, Input, OnInit } from '@angular/core' | ||
3 | import { AuthService, ConfirmService, Notifier, User } from '@app/core' | ||
4 | import { 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 | }) | ||
10 | export 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 | |||
13 | qrcode { | ||
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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { FormGroup } from '@angular/forms' | ||
3 | import { Router } from '@angular/router' | ||
4 | import { AuthService, Notifier, User } from '@app/core' | ||
5 | import { USER_EXISTING_PASSWORD_VALIDATOR, USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-validators' | ||
6 | import { FormReactiveService } from '@app/shared/shared-forms' | ||
7 | import { 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 | }) | ||
14 | export 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 @@ | |||
1 | import { QRCodeModule } from 'angularx-qrcode' | ||
1 | import { AutoCompleteModule } from 'primeng/autocomplete' | 2 | import { AutoCompleteModule } from 'primeng/autocomplete' |
2 | import { TableModule } from 'primeng/table' | 3 | import { TableModule } from 'primeng/table' |
3 | import { DragDropModule } from '@angular/cdk/drag-drop' | 4 | import { DragDropModule } from '@angular/cdk/drag-drop' |
@@ -10,6 +11,7 @@ import { SharedMainModule } from '@app/shared/shared-main' | |||
10 | import { SharedModerationModule } from '@app/shared/shared-moderation' | 11 | import { SharedModerationModule } from '@app/shared/shared-moderation' |
11 | import { SharedShareModal } from '@app/shared/shared-share-modal' | 12 | import { SharedShareModal } from '@app/shared/shared-share-modal' |
12 | import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' | 13 | import { SharedUserInterfaceSettingsModule } from '@app/shared/shared-user-settings' |
14 | import { SharedUsersModule } from '@app/shared/shared-users' | ||
13 | import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' | 15 | import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module' |
14 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' | 16 | import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' |
15 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' | 17 | import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' |
@@ -23,12 +25,14 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d | |||
23 | import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' | 25 | import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' |
24 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' | 26 | import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' |
25 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' | 27 | import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' |
28 | import { MyAccountTwoFactorButtonComponent, MyAccountTwoFactorComponent } from './my-account-settings/my-account-two-factor' | ||
26 | import { MyAccountComponent } from './my-account.component' | 29 | import { 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}? |
45 | It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another | 45 | It will delete ${videoChannel.videosCount} videos uploaded in this channel, and you will not be able to create another |
46 | channel with the same name (${videoChannel.name})!`, | 46 | channel 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 ' | |||
3 | import { AuthService, Notifier } from '@app/core' | 3 | import { AuthService, Notifier } from '@app/core' |
4 | import { listUserChannelsForSelect } from '@app/helpers' | 4 | import { listUserChannelsForSelect } from '@app/helpers' |
5 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' | 5 | import { OWNERSHIP_CHANGE_CHANNEL_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' |
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
7 | import { VideoOwnershipService } from '@app/shared/shared-main' | 7 | import { VideoOwnershipService } from '@app/shared/shared-main' |
8 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 8 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
9 | import { VideoChangeOwnership } from '@shared/models' | 9 | import { 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 @@ | |||
1 | import { SortMeta } from 'primeng/api' | ||
2 | import { mergeMap } from 'rxjs' | ||
1 | import { Component, OnInit } from '@angular/core' | 3 | import { Component, OnInit } from '@angular/core' |
2 | import { AuthService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' | 4 | import { AuthService, Notifier, RestPagination, RestTable, ServerService } from '@app/core' |
3 | import { DropdownAction, VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main' | 5 | import { DropdownAction, VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main' |
4 | import { HTMLServerConfig } from '@shared/models/server' | 6 | import { HTMLServerConfig } from '@shared/models/server' |
5 | import { VideoChannelSync, VideoChannelSyncState } from '@shared/models/videos' | 7 | import { VideoChannelSync, VideoChannelSyncState } from '@shared/models/videos' |
6 | import { SortMeta } from 'primeng/api' | ||
7 | import { 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' | |||
5 | import { AuthService, Notifier } from '@app/core' | 5 | import { AuthService, Notifier } from '@app/core' |
6 | import { listUserChannelsForSelect } from '@app/helpers' | 6 | import { listUserChannelsForSelect } from '@app/helpers' |
7 | import { VIDEO_CHANNEL_EXTERNAL_URL_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' | 7 | import { VIDEO_CHANNEL_EXTERNAL_URL_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' |
8 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 8 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
9 | import { VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main' | 9 | import { VideoChannelService, VideoChannelSyncService } from '@app/shared/shared-main' |
10 | import { VideoChannelSyncCreate } from '@shared/models/videos' | 10 | import { 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' |
12 | import { FormValidatorService } from '@app/shared/shared-forms' | 12 | import { FormReactiveService } from '@app/shared/shared-forms' |
13 | import { VideoPlaylistService } from '@app/shared/shared-video-playlist' | 13 | import { VideoPlaylistService } from '@app/shared/shared-video-playlist' |
14 | import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' | 14 | import { VideoPlaylistCreate } from '@shared/models/videos/playlist/video-playlist-create.model' |
15 | import { VideoPlaylistPrivacy } from '@shared/models/videos/playlist/video-playlist-privacy.model' | 15 | import { 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' |
14 | import { FormValidatorService } from '@app/shared/shared-forms' | 14 | import { FormReactiveService } from '@app/shared/shared-forms' |
15 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 15 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
16 | import { VideoPlaylistUpdate } from '@shared/models' | 16 | import { VideoPlaylistUpdate } from '@shared/models' |
17 | import { MyVideoPlaylistEdit } from './my-video-playlist-edit' | 17 | import { 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 @@ | |||
1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
2 | import { Notifier, UserService } from '@app/core' | 2 | import { Notifier, UserService } from '@app/core' |
3 | import { OWNERSHIP_CHANGE_USERNAME_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' | 3 | import { OWNERSHIP_CHANGE_USERNAME_VALIDATOR } from '@app/shared/form-validators/video-ownership-change-validators' |
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { Video, VideoOwnershipService } from '@app/shared/shared-main' | 5 | import { Video, VideoOwnershipService } from '@app/shared/shared-main' |
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { 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 @@ | |||
1 | import { uniqBy } from 'lodash-es' | ||
1 | import { concat, Observable } from 'rxjs' | 2 | import { concat, Observable } from 'rxjs' |
2 | import { tap, toArray } from 'rxjs/operators' | 3 | import { tap, toArray } from 'rxjs/operators' |
3 | import { Component, OnInit, ViewChild } from '@angular/core' | 4 | import { Component, OnInit, ViewChild } from '@angular/core' |
4 | import { ActivatedRoute, Router } from '@angular/router' | 5 | import { ActivatedRoute, Router } from '@angular/router' |
5 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' | 6 | import { AuthService, ComponentPagination, ConfirmService, Notifier, ScreenService, ServerService, User } from '@app/core' |
6 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' | 7 | import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook' |
7 | import { prepareIcu, immutableAssign } from '@app/helpers' | 8 | import { immutableAssign, prepareIcu } from '@app/helpers' |
8 | import { AdvancedInputFilter } from '@app/shared/shared-forms' | 9 | import { AdvancedInputFilter } from '@app/shared/shared-forms' |
9 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 10 | import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' |
10 | import { LiveStreamInformationComponent } from '@app/shared/shared-video-live' | 11 | import { 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' |
17 | import { VideoChannel, VideoSortField } from '@shared/models' | 18 | import { VideoPlaylistService } from '@app/shared/shared-video-playlist' |
19 | import { VideoChannel, VideoExistInPlaylist, VideosExistInPlaylists, VideoSortField } from '@shared/models' | ||
18 | import { VideoChangeOwnershipComponent } from './modals/video-change-ownership.component' | 20 | import { 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' | |||
3 | import { Notifier, UserService } from '@app/core' | 3 | import { Notifier, UserService } from '@app/core' |
4 | import { RESET_PASSWORD_CONFIRM_VALIDATOR } from '@app/shared/form-validators/reset-password-validators' | 4 | import { RESET_PASSWORD_CONFIRM_VALIDATOR } from '@app/shared/form-validators/reset-password-validators' |
5 | import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' | 5 | import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' |
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { 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' | |||
3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 3 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
4 | import { FormGroup } from '@angular/forms' | 4 | import { FormGroup } from '@angular/forms' |
5 | import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' | 5 | import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' |
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
7 | import { UserSignupService } from '@app/shared/shared-users' | 7 | import { 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 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' |
2 | import { FormGroup } from '@angular/forms' | 2 | import { FormGroup } from '@angular/forms' |
3 | import { | 3 | import { USER_TERMS_VALIDATOR } from '@app/shared/form-validators/user-validators' |
4 | USER_TERMS_VALIDATOR | 4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | } from '@app/shared/form-validators/user-validators' | ||
6 | import { 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' |
11 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 11 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
12 | import { UserSignupService } from '@app/shared/shared-users' | 12 | import { 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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Notifier, RedirectService, ServerService } from '@app/core' | 2 | import { Notifier, RedirectService, ServerService } from '@app/core' |
3 | import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' | 3 | import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' |
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { UserSignupService } from '@app/shared/shared-users' | 5 | import { 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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { ConfirmService, Notifier, ServerService } from '@app/core' | 3 | import { ConfirmService, Notifier, ServerService } from '@app/core' |
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { VideoDetails } from '@app/shared/shared-main' | 5 | import { VideoDetails } from '@app/shared/shared-main' |
6 | import { LoadingBarService } from '@ngx-loading-bar/core' | 6 | import { LoadingBarService } from '@ngx-loading-bar/core' |
7 | import { logger } from '@root-helpers/logger' | 7 | import { 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 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { ServerService } from '@app/core' | 2 | import { ServerService } from '@app/core' |
3 | import { VIDEO_CAPTION_FILE_VALIDATOR, VIDEO_CAPTION_LANGUAGE_VALIDATOR } from '@app/shared/form-validators/video-captions-validators' | 3 | import { VIDEO_CAPTION_FILE_VALIDATOR, VIDEO_CAPTION_LANGUAGE_VALIDATOR } from '@app/shared/form-validators/video-captions-validators' |
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { VideoCaptionEdit } from '@app/shared/shared-main' | 5 | import { VideoCaptionEdit } from '@app/shared/shared-main' |
6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
7 | import { HTMLServerConfig, VideoConstant } from '@shared/models' | 7 | import { 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 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { VIDEO_CAPTION_FILE_CONTENT_VALIDATOR } from '@app/shared/form-validators/video-captions-validators' | 2 | import { VIDEO_CAPTION_FILE_CONTENT_VALIDATOR } from '@app/shared/form-validators/video-captions-validators' |
3 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 3 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
4 | import { VideoCaptionEdit, VideoCaptionService, VideoCaptionWithPathEdit } from '@app/shared/shared-main' | 4 | import { VideoCaptionEdit, VideoCaptionService, VideoCaptionWithPathEdit } from '@app/shared/shared-main' |
5 | import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | 5 | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' |
6 | import { HTMLServerConfig, VideoConstant } from '@shared/models' | 6 | import { HTMLServerConfig, VideoConstant } from '@shared/models' |
7 | import { ServerService } from '../../../../core' | 7 | import { 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 { | |||
22 | import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' | 22 | import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' |
23 | import { InstanceService } from '@app/shared/shared-instance' | 23 | import { InstanceService } from '@app/shared/shared-instance' |
24 | import { VideoCaptionEdit, VideoCaptionWithPathEdit, VideoEdit, VideoService } from '@app/shared/shared-main' | 24 | import { VideoCaptionEdit, VideoCaptionWithPathEdit, VideoEdit, VideoService } from '@app/shared/shared-main' |
25 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
26 | import { logger } from '@root-helpers/logger' | ||
25 | import { PluginInfo } from '@root-helpers/plugins-manager' | 27 | import { PluginInfo } from '@root-helpers/plugins-manager' |
26 | import { | 28 | import { |
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' |
38 | import { VideoSource } from '@shared/models/videos/video-source' | ||
36 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' | 39 | import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' |
37 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' | 40 | import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' |
38 | import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component' | 41 | import { VideoCaptionEditModalContentComponent } from './video-caption-edit-modal-content/video-caption-edit-modal-content.component' |
39 | import { VideoEditType } from './video-edit.type' | 42 | import { VideoEditType } from './video-edit.type' |
40 | import { VideoSource } from '@shared/models/videos/video-source' | ||
41 | import { logger } from '@root-helpers/logger' | ||
42 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||
43 | 43 | ||
44 | type VideoLanguages = VideoConstant<string> & { group?: string } | 44 | type VideoLanguages = VideoConstant<string> & { group?: string } |
45 | type PluginField = { | 45 | type 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 | |||
3 | import { Router } from '@angular/router' | 3 | import { Router } from '@angular/router' |
4 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' | 4 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' |
5 | import { scrollToTop } from '@app/helpers' | 5 | import { scrollToTop } from '@app/helpers' |
6 | import { FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactiveService } from '@app/shared/shared-forms' |
7 | import { Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 7 | import { Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
8 | import { LiveVideoService } from '@app/shared/shared-video-live' | 8 | import { LiveVideoService } from '@app/shared/shared-video-live' |
9 | import { LoadingBarService } from '@ngx-loading-bar/core' | 9 | import { 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 | |||
3 | import { Router } from '@angular/router' | 3 | import { Router } from '@angular/router' |
4 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' | 4 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' |
5 | import { scrollToTop } from '@app/helpers' | 5 | import { scrollToTop } from '@app/helpers' |
6 | import { FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactiveService } from '@app/shared/shared-forms' |
7 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' | 7 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' |
8 | import { LoadingBarService } from '@ngx-loading-bar/core' | 8 | import { LoadingBarService } from '@ngx-loading-bar/core' |
9 | import { logger } from '@root-helpers/logger' | 9 | import { 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 | |||
4 | import { Router } from '@angular/router' | 4 | import { Router } from '@angular/router' |
5 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' | 5 | import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core' |
6 | import { scrollToTop } from '@app/helpers' | 6 | import { scrollToTop } from '@app/helpers' |
7 | import { FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormReactiveService } from '@app/shared/shared-forms' |
8 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' | 8 | import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' |
9 | import { LoadingBarService } from '@ngx-loading-bar/core' | 9 | import { LoadingBarService } from '@ngx-loading-bar/core' |
10 | import { logger } from '@root-helpers/logger' | 10 | import { 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, | |||
5 | import { ActivatedRoute, Router } from '@angular/router' | 5 | import { ActivatedRoute, Router } from '@angular/router' |
6 | import { AuthService, CanComponentDeactivate, HooksService, MetaService, Notifier, ServerService, UserService } from '@app/core' | 6 | import { AuthService, CanComponentDeactivate, HooksService, MetaService, Notifier, ServerService, UserService } from '@app/core' |
7 | import { genericUploadErrorHandler, scrollToTop } from '@app/helpers' | 7 | import { genericUploadErrorHandler, scrollToTop } from '@app/helpers' |
8 | import { FormValidatorService } from '@app/shared/shared-forms' | 8 | import { FormReactiveService } from '@app/shared/shared-forms' |
9 | import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' | 9 | import { BytesPipe, Video, VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main' |
10 | import { LoadingBarService } from '@ngx-loading-bar/core' | 10 | import { LoadingBarService } from '@ngx-loading-bar/core' |
11 | import { logger } from '@root-helpers/logger' | 11 | import { logger } from '@root-helpers/logger' |
@@ -13,6 +13,7 @@ import { isIOS } from '@root-helpers/web-browser' | |||
13 | import { HttpStatusCode, VideoCreateResult } from '@shared/models' | 13 | import { HttpStatusCode, VideoCreateResult } from '@shared/models' |
14 | import { UploaderXFormData } from './uploaderx-form-data' | 14 | import { UploaderXFormData } from './uploaderx-form-data' |
15 | import { VideoSend } from './video-send' | 15 | import { VideoSend } from './video-send' |
16 | import { 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' | |||
4 | import { Component, HostListener, OnInit } from '@angular/core' | 4 | import { Component, HostListener, OnInit } from '@angular/core' |
5 | import { ActivatedRoute, Router } from '@angular/router' | 5 | import { ActivatedRoute, Router } from '@angular/router' |
6 | import { Notifier } from '@app/core' | 6 | import { Notifier } from '@app/core' |
7 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 7 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
8 | import { Video, VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' | 8 | import { Video, VideoCaptionEdit, VideoCaptionService, VideoDetails, VideoEdit, VideoService } from '@app/shared/shared-main' |
9 | import { LiveVideoService } from '@app/shared/shared-video-live' | 9 | import { LiveVideoService } from '@app/shared/shared-video-live' |
10 | import { LoadingBarService } from '@ngx-loading-bar/core' | 10 | import { 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 { | |||
16 | import { Router } from '@angular/router' | 16 | import { Router } from '@angular/router' |
17 | import { Notifier, User } from '@app/core' | 17 | import { Notifier, User } from '@app/core' |
18 | import { VIDEO_COMMENT_TEXT_VALIDATOR } from '@app/shared/form-validators/video-comment-validators' | 18 | import { VIDEO_COMMENT_TEXT_VALIDATOR } from '@app/shared/form-validators/video-comment-validators' |
19 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 19 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
20 | import { Video } from '@app/shared/shared-main' | 20 | import { Video } from '@app/shared/shared-main' |
21 | import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment' | 21 | import { VideoComment, VideoCommentService } from '@app/shared/shared-video-comment' |
22 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 22 | import { 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>  |
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' |
21 | import { HooksService } from '@app/core/plugins/hooks.service' | 21 | import { HooksService } from '@app/core/plugins/hooks.service' |
22 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' | 22 | import { isXPercentInViewport, scrollToTop } from '@app/helpers' |
23 | import { Video, VideoCaptionService, VideoDetails, VideoService } from '@app/shared/shared-main' | 23 | import { Video, VideoCaptionService, VideoDetails, VideoFileTokenService, VideoService } from '@app/shared/shared-main' |
24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' | 24 | import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' |
25 | import { LiveVideoService } from '@app/shared/shared-video-live' | 25 | import { LiveVideoService } from '@app/shared/shared-video-live' |
26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' | 26 | import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' |
27 | import { logger } from '@root-helpers/logger' | 27 | import { logger } from '@root-helpers/logger' |
28 | import { isP2PEnabled } from '@root-helpers/video' | 28 | import { isP2PEnabled, videoRequiresAuth } from '@root-helpers/video' |
29 | import { timeToInt } from '@shared/core-utils' | 29 | import { timeToInt } from '@shared/core-utils' |
30 | import { | 30 | import { |
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 @@ | |||
1 | import { Observable, of } from 'rxjs' | 1 | import { Observable, of } from 'rxjs' |
2 | import { map } from 'rxjs/operators' | 2 | import { map } from 'rxjs/operators' |
3 | import { User } from '@app/core/users/user.model' | 3 | import { User } from '@app/core/users/user.model' |
4 | import { UserTokens } from '@root-helpers/users' | 4 | import { OAuthUserTokens } from '@root-helpers/users' |
5 | import { hasUserRight } from '@shared/core-utils/users' | 5 | import { hasUserRight } from '@shared/core-utils/users' |
6 | import { | 6 | import { |
7 | MyUser as ServerMyUserModel, | 7 | MyUser as ServerMyUserModel, |
@@ -13,46 +13,46 @@ import { | |||
13 | } from '@shared/models' | 13 | } from '@shared/models' |
14 | 14 | ||
15 | export class AuthUser extends User implements ServerMyUserModel { | 15 | export 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 @@ | |||
1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' | 1 | import { Hotkey, HotkeysService } from 'angular2-hotkeys' |
2 | import { Observable, ReplaySubject, Subject, throwError as observableThrowError } from 'rxjs' | 2 | import { Observable, ReplaySubject, Subject, throwError as observableThrowError } from 'rxjs' |
3 | import { catchError, map, mergeMap, share, tap } from 'rxjs/operators' | 3 | import { catchError, map, mergeMap, share, tap } from 'rxjs/operators' |
4 | import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' | 4 | import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular/common/http' |
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { Router } from '@angular/router' | 6 | import { Router } from '@angular/router' |
7 | import { Notifier } from '@app/core/notification/notifier.service' | 7 | import { Notifier } from '@app/core/notification/notifier.service' |
8 | import { logger, objectToUrlEncoded, peertubeLocalStorage, UserTokens } from '@root-helpers/index' | 8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' |
9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' | 9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' |
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | import { RestExtractor } from '../rest/rest-extractor.service' | 11 | import { 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}. |
101 | Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.` | 101 | Ensure 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 @@ | |||
1 | import { firstValueFrom, Subject } from 'rxjs' | 1 | import { firstValueFrom, map, Observable, Subject } from 'rxjs' |
2 | import { Injectable } from '@angular/core' | 2 | import { Injectable } from '@angular/core' |
3 | 3 | ||
4 | type ConfirmOptions = { | 4 | type 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() |
13 | export class ConfirmService { | 25 | export 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 | ||
8 | export type MenuLink = { | 8 | export 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' | |||
4 | import { DateFormat, dateToHuman } from '@app/helpers' | 4 | import { DateFormat, dateToHuman } from '@app/helpers' |
5 | import { logger } from '@root-helpers/logger' | 5 | import { logger } from '@root-helpers/logger' |
6 | import { HttpStatusCode, ResultList } from '@shared/models' | 6 | import { HttpStatusCode, ResultList } from '@shared/models' |
7 | import { HttpHeaderResponse } from '@angular/common/http' | ||
7 | 8 | ||
8 | @Injectable() | 9 | @Injectable() |
9 | export class RestExtractor { | 10 | export 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 @@ | |||
1 | import { Observable, of as ofObservable, timer as observableTimer } from 'rxjs' | 1 | import { Observable, of as ofObservable, timer as observableTimer } from 'rxjs' |
2 | import { switchMap } from 'rxjs/operators' | 2 | import { switchMap } from 'rxjs/operators' |
3 | import { PreloadingStrategy, Route } from '@angular/router' | ||
4 | import { Injectable } from '@angular/core' | 3 | import { Injectable } from '@angular/core' |
4 | import { PreloadingStrategy, Route } from '@angular/router' | ||
5 | 5 | ||
6 | @Injectable() | 6 | @Injectable() |
7 | export class PreloadSelectedModulesList implements PreloadingStrategy { | 7 | export 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' | |||
4 | import { AuthService, AuthStatus } from '@app/core/auth' | 4 | import { AuthService, AuthStatus } from '@app/core/auth' |
5 | import { getBoolOrDefault } from '@root-helpers/local-storage-utils' | 5 | import { getBoolOrDefault } from '@root-helpers/local-storage-utils' |
6 | import { logger } from '@root-helpers/logger' | 6 | import { logger } from '@root-helpers/logger' |
7 | import { UserLocalStorageKeys, UserTokens } from '@root-helpers/users' | 7 | import { UserLocalStorageKeys, OAuthUserTokens } from '@root-helpers/users' |
8 | import { UserRole, UserUpdateMe } from '@shared/models' | 8 | import { UserRole, UserUpdateMe } from '@shared/models' |
9 | import { NSFWPolicyType } from '@shared/models/videos' | 9 | import { NSFWPolicyType } from '@shared/models/videos' |
10 | import { ServerService } from '../server' | 10 | import { 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 | ||
56 | export { | 56 | export { |
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 | ||
393 | my-global-icon { | 393 | my-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 | ||
64 | export const USER_OTP_TOKEN_VALIDATOR: BuildFormValidator = { | ||
65 | VALIDATORS: [ | ||
66 | Validators.required | ||
67 | ], | ||
68 | MESSAGES: { | ||
69 | required: $localize`OTP token is required.` | ||
70 | } | ||
71 | } | ||
72 | |||
64 | export const USER_PASSWORD_VALIDATOR = { | 73 | export 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' | |||
2 | import truncate from 'lodash-es/truncate' | 2 | import truncate from 'lodash-es/truncate' |
3 | import { SortMeta } from 'primeng/api' | 3 | import { SortMeta } from 'primeng/api' |
4 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 4 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
5 | import { DomSanitizer } from '@angular/platform-browser' | ||
6 | import { ActivatedRoute, Router } from '@angular/router' | 5 | import { ActivatedRoute, Router } from '@angular/router' |
7 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' | 6 | import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' |
8 | import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' | 7 | import { 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 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { AuthService, HtmlRendererService, Notifier } from '@app/core' | 2 | import { AuthService, HtmlRendererService, Notifier } from '@app/core' |
3 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 3 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 4 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 5 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
6 | import { logger } from '@root-helpers/logger' | 6 | import { 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 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 3 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
4 | import { AbuseService } from '@app/shared/shared-moderation' | 4 | import { AbuseService } from '@app/shared/shared-moderation' |
5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 5 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
6 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 6 | import { 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 @@ | |||
1 | import { Injectable } from '@angular/core' | ||
2 | import { AbstractControl, FormGroup } from '@angular/forms' | ||
3 | import { wait } from '@root-helpers/utils' | ||
4 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' | ||
5 | import { FormValidatorService } from './form-validator.service' | ||
6 | |||
7 | export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors } | ||
8 | export type FormReactiveValidationMessages = { | ||
9 | [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages | ||
10 | } | ||
11 | |||
12 | @Injectable() | ||
13 | export 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 | 1 | import { FormGroup } from '@angular/forms' | |
2 | import { AbstractControl, FormGroup } from '@angular/forms' | ||
3 | import { wait } from '@root-helpers/utils' | ||
4 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' | 2 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' |
5 | import { FormValidatorService } from './form-validator.service' | 3 | import { FormReactiveService, FormReactiveValidationMessages } from './form-reactive.service' |
6 | |||
7 | export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors } | ||
8 | export type FormReactiveValidationMessages = { | ||
9 | [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages | ||
10 | } | ||
11 | 4 | ||
12 | export abstract class FormReactive { | 5 | export 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 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms' | 2 | import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms' |
3 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' | 3 | import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' |
4 | import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive' | 4 | import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive.service' |
5 | 5 | ||
6 | @Injectable() | 6 | @Injectable() |
7 | export class FormValidatorService { | 7 | export 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 @@ | |||
1 | export * from './advanced-input-filter.component' | 1 | export * from './advanced-input-filter.component' |
2 | export * from './form-reactive.service' | ||
2 | export * from './form-reactive' | 3 | export * from './form-reactive' |
3 | export * from './form-validator.service' | 4 | export * from './form-validator.service' |
4 | export * from './form-validator.service' | 5 | export * 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 @@ | |||
1 | import { Component, forwardRef, Input } from '@angular/core' | 1 | import { Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core' |
2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' | 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | 4 | ||
@@ -15,6 +15,8 @@ import { Notifier } from '@app/core' | |||
15 | ] | 15 | ] |
16 | }) | 16 | }) |
17 | export class InputTextComponent implements ControlValueAccessor { | 17 | export 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 | |||
2 | import { InputMaskModule } from 'primeng/inputmask' | 1 | import { InputMaskModule } from 'primeng/inputmask' |
3 | import { NgModule } from '@angular/core' | 2 | import { NgModule } from '@angular/core' |
4 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' | 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' |
@@ -7,6 +6,7 @@ import { SharedGlobalIconModule } from '../shared-icons' | |||
7 | import { SharedMainModule } from '../shared-main/shared-main.module' | 6 | import { SharedMainModule } from '../shared-main/shared-main.module' |
8 | import { AdvancedInputFilterComponent } from './advanced-input-filter.component' | 7 | import { AdvancedInputFilterComponent } from './advanced-input-filter.component' |
9 | import { DynamicFormFieldComponent } from './dynamic-form-field.component' | 8 | import { DynamicFormFieldComponent } from './dynamic-form-field.component' |
9 | import { FormReactiveService } from './form-reactive.service' | ||
10 | import { FormValidatorService } from './form-validator.service' | 10 | import { FormValidatorService } from './form-validator.service' |
11 | import { InputSwitchComponent } from './input-switch.component' | 11 | import { InputSwitchComponent } from './input-switch.component' |
12 | import { InputTextComponent } from './input-text.component' | 12 | import { 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 | }) |
102 | export class SharedFormModule { } | 103 | export 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 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core' |
2 | import { Params } from '@angular/router' | ||
2 | import { GlobalIconName } from '@app/shared/shared-icons' | 3 | import { GlobalIconName } from '@app/shared/shared-icons' |
3 | 4 | ||
4 | export type DropdownAction<T> = { | 5 | export 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 | ||
27 | export class ActionDropdownComponent<T> { | 32 | export 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 @@ | |||
1 | import { Component, Input, OnChanges } from '@angular/core' | 1 | import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core' |
2 | import { GlobalIconName } from '@app/shared/shared-icons' | 2 | import { 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 | ||
10 | export class ButtonComponent implements OnChanges { | 11 | export 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 | }) |
33 | export class ListOverflowComponent<T extends ListOverflowItem> implements AfterViewInit { | 33 | export 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 { | |||
44 | import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins' | 44 | import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins' |
45 | import { ActorRedirectGuard } from './router' | 45 | import { ActorRedirectGuard } from './router' |
46 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' | 46 | import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' |
47 | import { EmbedComponent, RedundancyService, VideoImportService, VideoOwnershipService, VideoResolver, VideoService } from './video' | 47 | import { |
48 | EmbedComponent, | ||
49 | RedundancyService, | ||
50 | VideoFileTokenService, | ||
51 | VideoImportService, | ||
52 | VideoOwnershipService, | ||
53 | VideoResolver, | ||
54 | VideoService | ||
55 | } from './video' | ||
48 | import { VideoCaptionService } from './video-caption' | 56 | import { VideoCaptionService } from './video-caption' |
49 | import { VideoChannelService } from './video-channel' | 57 | import { 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' | |||
2 | import { catchError, map, tap } from 'rxjs/operators' | 2 | import { catchError, map, tap } from 'rxjs/operators' |
3 | import { HttpClient, HttpParams } from '@angular/common/http' | 3 | import { HttpClient, HttpParams } from '@angular/common/http' |
4 | import { Injectable } from '@angular/core' | 4 | import { Injectable } from '@angular/core' |
5 | import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' | 5 | import { ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core' |
6 | import { | 6 | import { |
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' | |||
2 | export * from './redundancy.service' | 2 | export * from './redundancy.service' |
3 | export * from './video-details.model' | 3 | export * from './video-details.model' |
4 | export * from './video-edit.model' | 4 | export * from './video-edit.model' |
5 | export * from './video-file-token.service' | ||
5 | export * from './video-import.service' | 6 | export * from './video-import.service' |
6 | export * from './video-ownership.service' | 7 | export * from './video-ownership.service' |
7 | export * from './video.model' | 8 | export * 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 @@ | |||
1 | import { catchError, map, of, tap } from 'rxjs' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor } from '@app/core' | ||
5 | import { VideoToken } from '@shared/models' | ||
6 | import { VideoService } from './video.service' | ||
7 | |||
8 | @Injectable() | ||
9 | export 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 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 2 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 3 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
4 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 4 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
5 | import { splitAndGetNotEmpty, UNIQUE_HOSTS_VALIDATOR } from '../form-validators/host-validators' | 5 | import { 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' | |||
2 | import { Component, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, OnInit, ViewChild } from '@angular/core' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' | 4 | import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' |
5 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
6 | import { Account } from '@app/shared/shared-main' | 6 | import { Account } from '@app/shared/shared-main' |
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
@@ -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' | |||
2 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 2 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' | 4 | import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' |
5 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
6 | import { VideoComment } from '@app/shared/shared-video-comment' | 6 | import { VideoComment } from '@app/shared/shared-video-comment' |
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
@@ -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' | |||
3 | import { DomSanitizer } from '@angular/platform-browser' | 3 | import { DomSanitizer } from '@angular/platform-browser' |
4 | import { Notifier } from '@app/core' | 4 | import { Notifier } from '@app/core' |
5 | import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' | 5 | import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' |
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 7 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 8 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
9 | import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' | 9 | import { 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' | |||
2 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 2 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
3 | import { Notifier } from '@app/core' | 3 | import { Notifier } from '@app/core' |
4 | import { prepareIcu } from '@app/helpers' | 4 | import { prepareIcu } from '@app/helpers' |
5 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 5 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' |
8 | import { User } from '@shared/models' | 8 | import { 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 @@ | |||
1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { prepareIcu } from '@app/helpers' | 3 | import { prepareIcu } from '@app/helpers' |
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { Video } from '@app/shared/shared-main' | 5 | import { Video } from '@app/shared/shared-main' |
6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal } from '@ng-bootstrap/ng-bootstrap' |
7 | import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' | 7 | import { 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 @@ | |||
1 | import { Subject, Subscription } from 'rxjs' | 1 | import { Subject, Subscription } from 'rxjs' |
2 | import { Component, Input, OnDestroy, OnInit } from '@angular/core' | 2 | import { Component, Input, OnDestroy, OnInit } from '@angular/core' |
3 | import { AuthService, Notifier, ServerService, ThemeService, UserService } from '@app/core' | 3 | import { AuthService, Notifier, ServerService, ThemeService, UserService } from '@app/core' |
4 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 4 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
5 | import { HTMLServerConfig, User, UserUpdateMe } from '@shared/models' | 5 | import { HTMLServerConfig, User, UserUpdateMe } from '@shared/models' |
6 | import { SelectOptionsItem } from 'src/types' | 6 | import { 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' | |||
3 | import { first } from 'rxjs/operators' | 3 | import { first } from 'rxjs/operators' |
4 | import { Component, Input, OnDestroy, OnInit } from '@angular/core' | 4 | import { Component, Input, OnDestroy, OnInit } from '@angular/core' |
5 | import { AuthService, Notifier, ServerService, User, UserService } from '@app/core' | 5 | import { AuthService, Notifier, ServerService, User, UserService } from '@app/core' |
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
7 | import { UserUpdateMe } from '@shared/models' | 7 | import { UserUpdateMe } from '@shared/models' |
8 | import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' | 8 | import { 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 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { Notifier } from '@app/core' | 2 | import { Notifier } from '@app/core' |
3 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 3 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
4 | import { logger } from '@root-helpers/logger' | 4 | import { logger } from '@root-helpers/logger' |
5 | import { USER_HANDLE_VALIDATOR } from '../form-validators/user-validators' | 5 | import { 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 @@ | |||
1 | export * from './user-admin.service' | 1 | export * from './user-admin.service' |
2 | export * from './user-signup.service' | 2 | export * from './user-signup.service' |
3 | export * from './two-factor.service' | ||
3 | 4 | ||
4 | export * from './shared-users.module' | 5 | export * 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 | ||
2 | import { NgModule } from '@angular/core' | 2 | import { NgModule } from '@angular/core' |
3 | import { SharedMainModule } from '../shared-main/shared-main.module' | 3 | import { SharedMainModule } from '../shared-main/shared-main.module' |
4 | import { TwoFactorService } from './two-factor.service' | ||
4 | import { UserAdminService } from './user-admin.service' | 5 | import { UserAdminService } from './user-admin.service' |
5 | import { UserSignupService } from './user-signup.service' | 6 | import { 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 | }) |
21 | export class SharedUsersModule { } | 23 | export 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 @@ | |||
1 | import { catchError } from 'rxjs/operators' | ||
2 | import { HttpClient } from '@angular/common/http' | ||
3 | import { Injectable } from '@angular/core' | ||
4 | import { RestExtractor, UserService } from '@app/core' | ||
5 | import { TwoFactorEnableResult } from '@shared/models' | ||
6 | |||
7 | @Injectable() | ||
8 | export 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' | |||
2 | import { firstValueFrom } from 'rxjs' | 2 | import { firstValueFrom } from 'rxjs' |
3 | import { tap } from 'rxjs/operators' | 3 | import { tap } from 'rxjs/operators' |
4 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' | 4 | import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' |
5 | import { AuthService, HooksService, Notifier } from '@app/core' | 5 | import { HooksService } from '@app/core' |
6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 6 | import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
7 | import { logger } from '@root-helpers/logger' | 7 | import { logger } from '@root-helpers/logger' |
8 | import { videoRequiresAuth } from '@root-helpers/video' | ||
8 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' | 9 | import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' |
9 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' | 10 | import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main' |
10 | 11 | ||
11 | type DownloadType = 'video' | 'subtitles' | 12 | type DownloadType = 'video' | 'subtitles' |
12 | type FileMetadata = { [key: string]: { label: string, value: string }} | 13 | type 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' |
13 | import { AuthService, ScreenService, ServerService, User } from '@app/core' | 13 | import { AuthService, ScreenService, ServerService, User } from '@app/core' |
14 | import { HTMLServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models' | 14 | import { HTMLServerConfig, VideoExistInPlaylist, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models' |
15 | import { LinkType } from '../../../types/link.type' | 15 | import { LinkType } from '../../../types/link.type' |
16 | import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component' | 16 | import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component' |
17 | import { Video } from '../shared-main' | 17 | import { Video } from '../shared-main' |
@@ -21,13 +21,15 @@ import { VideoActionsDisplayType } from './video-actions-dropdown.component' | |||
21 | export type MiniatureDisplayOptions = { | 21 | export 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 = { | |||
38 | export class VideoMiniatureComponent implements OnInit { | 40 | export 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' | |||
2 | import { AfterContentInit, Component, ContentChildren, EventEmitter, Input, Output, QueryList, TemplateRef } from '@angular/core' | 2 | import { AfterContentInit, Component, ContentChildren, EventEmitter, Input, Output, QueryList, TemplateRef } from '@angular/core' |
3 | import { ComponentPagination, Notifier, User } from '@app/core' | 3 | import { ComponentPagination, Notifier, User } from '@app/core' |
4 | import { logger } from '@root-helpers/logger' | 4 | import { logger } from '@root-helpers/logger' |
5 | import { ResultList, VideoSortField } from '@shared/models' | 5 | import { ResultList, VideosExistInPlaylists, VideoSortField } from '@shared/models' |
6 | import { PeerTubeTemplateDirective, Video } from '../shared-main' | 6 | import { PeerTubeTemplateDirective, Video } from '../shared-main' |
7 | import { MiniatureDisplayOptions } from './video-miniature.component' | 7 | import { 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 | }) |
16 | export class VideosSelectionComponent implements AfterContentInit { | 16 | export 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' | |||
3 | import { debounceTime, filter } from 'rxjs/operators' | 3 | import { debounceTime, filter } from 'rxjs/operators' |
4 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' | 4 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' |
5 | import { AuthService, DisableForReuseHook, Notifier } from '@app/core' | 5 | import { AuthService, DisableForReuseHook, Notifier } from '@app/core' |
6 | import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' | 6 | import { FormReactive, FormReactiveService } from '@app/shared/shared-forms' |
7 | import { secondsToTime } from '@shared/core-utils' | 7 | import { secondsToTime } from '@shared/core-utils' |
8 | import { | 8 | import { |
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' | |||
8 | import { Account, AccountService, VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 8 | import { Account, AccountService, VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
9 | import { NGX_LOADING_BAR_IGNORED } from '@ngx-loading-bar/http-client' | 9 | import { NGX_LOADING_BAR_IGNORED } from '@ngx-loading-bar/http-client' |
10 | import { | 10 | import { |
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() |