aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared')
-rw-r--r--client/src/app/shared/form-validators/user-validators.ts9
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts13
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts4
-rw-r--r--client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts4
-rw-r--r--client/src/app/shared/shared-custom-markup/custom-markup.service.ts2
-rw-r--r--client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts6
-rw-r--r--client/src/app/shared/shared-forms/form-reactive.service.ts101
-rw-r--r--client/src/app/shared/shared-forms/form-reactive.ts87
-rw-r--r--client/src/app/shared/shared-forms/form-validator.service.ts2
-rw-r--r--client/src/app/shared/shared-forms/index.ts1
-rw-r--r--client/src/app/shared/shared-forms/input-text.component.ts10
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.ts4
-rw-r--r--client/src/app/shared/shared-forms/shared-form.module.ts5
-rw-r--r--client/src/app/shared/shared-instance/instance.service.ts2
-rw-r--r--client/src/app/shared/shared-main/auth/auth-interceptor.service.ts13
-rw-r--r--client/src/app/shared/shared-main/buttons/action-dropdown.component.html2
-rw-r--r--client/src/app/shared/shared-main/buttons/action-dropdown.component.ts15
-rw-r--r--client/src/app/shared/shared-main/buttons/button.component.ts5
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts11
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts9
-rw-r--r--client/src/app/shared/shared-main/video/index.ts1
-rw-r--r--client/src/app/shared/shared-main/video/video-file-token.service.ts33
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts3
-rw-r--r--client/src/app/shared/shared-moderation/batch-domains-modal.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/account-report.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/report-modals/video-report.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/user-ban-modal.component.ts4
-rw-r--r--client/src/app/shared/shared-moderation/video-block.component.ts4
-rw-r--r--client/src/app/shared/shared-search/find-in-bulk.service.ts15
-rw-r--r--client/src/app/shared/shared-support-modal/support-modal.component.ts2
-rw-r--r--client/src/app/shared/shared-user-settings/user-interface-settings.component.ts4
-rw-r--r--client/src/app/shared/shared-user-settings/user-video-settings.component.ts4
-rw-r--r--client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts4
-rw-r--r--client/src/app/shared/shared-user-subscription/subscribe-button.component.html2
-rw-r--r--client/src/app/shared/shared-users/index.ts1
-rw-r--r--client/src/app/shared/shared-users/shared-users.module.ts4
-rw-r--r--client/src/app/shared/shared-users/two-factor.service.ts52
-rw-r--r--client/src/app/shared/shared-users/user-admin.service.ts5
-rw-r--r--client/src/app/shared/shared-video-live/live-stream-information.component.html2
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.html5
-rw-r--r--client/src/app/shared/shared-video-miniature/video-download.component.ts19
-rw-r--r--client/src/app/shared/shared-video-miniature/video-filters-header.component.html1
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.html6
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.scss4
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts15
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.html1
-rw-r--r--client/src/app/shared/shared-video-miniature/videos-selection.component.ts3
-rw-r--r--client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts8
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist.service.ts12
51 files changed, 373 insertions, 164 deletions
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts
index 3262853d8..b93de75ea 100644
--- a/client/src/app/shared/form-validators/user-validators.ts
+++ b/client/src/app/shared/form-validators/user-validators.ts
@@ -61,6 +61,15 @@ export const USER_EXISTING_PASSWORD_VALIDATOR: BuildFormValidator = {
61 } 61 }
62} 62}
63 63
64export const USER_OTP_TOKEN_VALIDATOR: BuildFormValidator = {
65 VALIDATORS: [
66 Validators.required
67 ],
68 MESSAGES: {
69 required: $localize`OTP token is required.`
70 }
71}
72
64export const USER_PASSWORD_VALIDATOR = { 73export const USER_PASSWORD_VALIDATOR = {
65 VALIDATORS: [ 74 VALIDATORS: [
66 Validators.required, 75 Validators.required,
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
index 32d3b0093..569a37b17 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -2,7 +2,6 @@ import * as debug from 'debug'
2import truncate from 'lodash-es/truncate' 2import truncate from 'lodash-es/truncate'
3import { SortMeta } from 'primeng/api' 3import { SortMeta } from 'primeng/api'
4import { Component, Input, OnInit, ViewChild } from '@angular/core' 4import { Component, Input, OnInit, ViewChild } from '@angular/core'
5import { DomSanitizer } from '@angular/platform-browser'
6import { ActivatedRoute, Router } from '@angular/router' 5import { ActivatedRoute, Router } from '@angular/router'
7import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core' 6import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
8import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main' 7import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
@@ -73,8 +72,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
73 private videoService: VideoService, 72 private videoService: VideoService,
74 private videoBlocklistService: VideoBlockService, 73 private videoBlocklistService: VideoBlockService,
75 private confirmService: ConfirmService, 74 private confirmService: ConfirmService,
76 private markdownRenderer: MarkdownService, 75 private markdownRenderer: MarkdownService
77 private sanitizer: DomSanitizer
78 ) { 76 ) {
79 super() 77 super()
80 } 78 }
@@ -216,8 +214,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
216 abuse.truncatedCommentHtml = abuse.commentHtml = $localize`Deleted comment` 214 abuse.truncatedCommentHtml = abuse.commentHtml = $localize`Deleted comment`
217 } else { 215 } else {
218 const truncated = truncate(abuse.comment.text, { length: 100 }) 216 const truncated = truncate(abuse.comment.text, { length: 100 })
219 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML(truncated, true) 217 abuse.truncatedCommentHtml = await this.markdownRenderer.textMarkdownToHTML({ markdown: truncated, withHtml: true })
220 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML(abuse.comment.text, true) 218 abuse.commentHtml = await this.markdownRenderer.textMarkdownToHTML({ markdown: abuse.comment.text, withHtml: true })
221 } 219 }
222 } 220 }
223 221
@@ -274,7 +272,8 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
274 }, 272 },
275 { 273 {
276 label: $localize`Delete report`, 274 label: $localize`Delete report`,
277 handler: abuse => this.isAdminView() && this.removeAbuse(abuse) 275 handler: abuse => this.removeAbuse(abuse),
276 isDisplayed: () => this.isAdminView()
278 } 277 }
279 ] 278 ]
280 } 279 }
@@ -452,6 +451,6 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
452 } 451 }
453 452
454 private toHtml (text: string) { 453 private toHtml (text: string) {
455 return this.markdownRenderer.textMarkdownToHTML(text) 454 return this.markdownRenderer.textMarkdownToHTML({ markdown: text })
456 } 455 }
457} 456}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
index d24a5d58d..12d503f56 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-message-modal.component.ts
@@ -1,6 +1,6 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { AuthService, HtmlRendererService, Notifier } from '@app/core' 2import { AuthService, HtmlRendererService, Notifier } from '@app/core'
3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
4import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 4import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { logger } from '@root-helpers/logger' 6import { logger } from '@root-helpers/logger'
@@ -29,7 +29,7 @@ export class AbuseMessageModalComponent extends FormReactive implements OnInit {
29 private abuse: UserAbuse 29 private abuse: UserAbuse
30 30
31 constructor ( 31 constructor (
32 protected formValidatorService: FormValidatorService, 32 protected formReactiveService: FormReactiveService,
33 private modalService: NgbModal, 33 private modalService: NgbModal,
34 private htmlRenderer: HtmlRendererService, 34 private htmlRenderer: HtmlRendererService,
35 private auth: AuthService, 35 private auth: AuthService,
diff --git a/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
index 2600da8da..4ad807d25 100644
--- a/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
+++ b/client/src/app/shared/shared-abuse-list/moderation-comment-modal.component.ts
@@ -1,6 +1,6 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
4import { AbuseService } from '@app/shared/shared-moderation' 4import { AbuseService } from '@app/shared/shared-moderation'
5import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 5import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 6import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -20,7 +20,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
20 private openedModal: NgbModalRef 20 private openedModal: NgbModalRef
21 21
22 constructor ( 22 constructor (
23 protected formValidatorService: FormValidatorService, 23 protected formReactiveService: FormReactiveService,
24 private modalService: NgbModal, 24 private modalService: NgbModal,
25 private notifier: Notifier, 25 private notifier: Notifier,
26 private abuseService: AbuseService 26 private abuseService: AbuseService
diff --git a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
index d738a644e..618c3dd4f 100644
--- a/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
+++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
@@ -58,7 +58,7 @@ export class CustomMarkupService {
58 } 58 }
59 59
60 async buildElement (text: string) { 60 async buildElement (text: string) {
61 const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags()) 61 const html = await this.markdown.customPageMarkdownToHTML({ markdown: text, additionalAllowedTags: this.getSupportedTags() })
62 62
63 const rootElement = document.createElement('div') 63 const rootElement = document.createElement('div')
64 rootElement.innerHTML = html 64 rootElement.innerHTML = html
diff --git a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
index e9c466a90..ba12b7139 100644
--- a/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
+++ b/client/src/app/shared/shared-custom-markup/peertube-custom-tags/channel-miniature-markup.component.ts
@@ -42,7 +42,11 @@ export class ChannelMiniatureMarkupComponent implements CustomMarkupComponent, O
42 tap(channel => { 42 tap(channel => {
43 this.channel = channel 43 this.channel = channel
44 }), 44 }),
45 switchMap(() => from(this.markdown.textMarkdownToHTML(this.channel.description))), 45 switchMap(() => from(this.markdown.textMarkdownToHTML({
46 markdown: this.channel.description,
47 withEmoji: true,
48 withHtml: true
49 }))),
46 tap(html => { 50 tap(html => {
47 this.descriptionHTML = html 51 this.descriptionHTML = html
48 }), 52 }),
diff --git a/client/src/app/shared/shared-forms/form-reactive.service.ts b/client/src/app/shared/shared-forms/form-reactive.service.ts
new file mode 100644
index 000000000..f1b7e0ef2
--- /dev/null
+++ b/client/src/app/shared/shared-forms/form-reactive.service.ts
@@ -0,0 +1,101 @@
1import { Injectable } from '@angular/core'
2import { AbstractControl, FormGroup } from '@angular/forms'
3import { wait } from '@root-helpers/utils'
4import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
5import { FormValidatorService } from './form-validator.service'
6
7export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors }
8export type FormReactiveValidationMessages = {
9 [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages
10}
11
12@Injectable()
13export class FormReactiveService {
14
15 constructor (private formValidatorService: FormValidatorService) {
16
17 }
18
19 buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) {
20 const { formErrors, validationMessages, form } = this.formValidatorService.buildForm(obj, defaultValues)
21
22 form.statusChanges.subscribe(async () => {
23 // FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed
24 await this.waitPendingCheck(form)
25
26 this.onStatusChanged({ form, formErrors, validationMessages })
27 })
28
29 return { form, formErrors, validationMessages }
30 }
31
32 async waitPendingCheck (form: FormGroup) {
33 if (form.status !== 'PENDING') return
34
35 // FIXME: the following line does not work: https://github.com/angular/angular/issues/41519
36 // return firstValueFrom(form.statusChanges.pipe(filter(status => status !== 'PENDING')))
37 // So we have to fallback to active wait :/
38
39 do {
40 await wait(10)
41 } while (form.status === 'PENDING')
42 }
43
44 markAllAsDirty (controlsArg: { [ key: string ]: AbstractControl }) {
45 const controls = controlsArg
46
47 for (const key of Object.keys(controls)) {
48 const control = controls[key]
49
50 if (control instanceof FormGroup) {
51 this.markAllAsDirty(control.controls)
52 continue
53 }
54
55 control.markAsDirty()
56 }
57 }
58
59 forceCheck (form: FormGroup, formErrors: any, validationMessages: FormReactiveValidationMessages) {
60 this.onStatusChanged({ form, formErrors, validationMessages, onlyDirty: false })
61 }
62
63 private onStatusChanged (options: {
64 form: FormGroup
65 formErrors: FormReactiveErrors
66 validationMessages: FormReactiveValidationMessages
67 onlyDirty?: boolean // default true
68 }) {
69 const { form, formErrors, validationMessages, onlyDirty = true } = options
70
71 for (const field of Object.keys(formErrors)) {
72 if (formErrors[field] && typeof formErrors[field] === 'object') {
73 this.onStatusChanged({
74 form: form.controls[field] as FormGroup,
75 formErrors: formErrors[field] as FormReactiveErrors,
76 validationMessages: validationMessages[field] as FormReactiveValidationMessages,
77 onlyDirty
78 })
79
80 continue
81 }
82
83 // clear previous error message (if any)
84 formErrors[field] = ''
85 const control = form.get(field)
86
87 if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
88
89 const staticMessages = validationMessages[field]
90 for (const key of Object.keys(control.errors)) {
91 const formErrorValue = control.errors[key]
92
93 // Try to find error message in static validation messages first
94 // Then check if the validator returns a string that is the error
95 if (staticMessages[key]) formErrors[field] += staticMessages[key] + ' '
96 else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key]
97 else throw new Error('Form error value of ' + field + ' is invalid')
98 }
99 }
100 }
101}
diff --git a/client/src/app/shared/shared-forms/form-reactive.ts b/client/src/app/shared/shared-forms/form-reactive.ts
index a19ffdd82..d1e7be802 100644
--- a/client/src/app/shared/shared-forms/form-reactive.ts
+++ b/client/src/app/shared/shared-forms/form-reactive.ts
@@ -1,16 +1,9 @@
1 1import { FormGroup } from '@angular/forms'
2import { AbstractControl, FormGroup } from '@angular/forms'
3import { wait } from '@root-helpers/utils'
4import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' 2import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
5import { FormValidatorService } from './form-validator.service' 3import { FormReactiveService, FormReactiveValidationMessages } from './form-reactive.service'
6
7export type FormReactiveErrors = { [ id: string ]: string | FormReactiveErrors }
8export type FormReactiveValidationMessages = {
9 [ id: string ]: { [ name: string ]: string } | FormReactiveValidationMessages
10}
11 4
12export abstract class FormReactive { 5export abstract class FormReactive {
13 protected abstract formValidatorService: FormValidatorService 6 protected abstract formReactiveService: FormReactiveService
14 protected formChanged = false 7 protected formChanged = false
15 8
16 form: FormGroup 9 form: FormGroup
@@ -18,86 +11,22 @@ export abstract class FormReactive {
18 validationMessages: FormReactiveValidationMessages 11 validationMessages: FormReactiveValidationMessages
19 12
20 buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) { 13 buildForm (obj: BuildFormArgument, defaultValues: BuildFormDefaultValues = {}) {
21 const { formErrors, validationMessages, form } = this.formValidatorService.buildForm(obj, defaultValues) 14 const { formErrors, validationMessages, form } = this.formReactiveService.buildForm(obj, defaultValues)
22 15
23 this.form = form 16 this.form = form
24 this.formErrors = formErrors 17 this.formErrors = formErrors
25 this.validationMessages = validationMessages 18 this.validationMessages = validationMessages
26
27 this.form.statusChanges.subscribe(async () => {
28 // FIXME: remove when https://github.com/angular/angular/issues/41519 is fixed
29 await this.waitPendingCheck()
30
31 this.onStatusChanged(this.form, this.formErrors, this.validationMessages)
32 })
33 } 19 }
34 20
35 protected async waitPendingCheck () { 21 protected async waitPendingCheck () {
36 if (this.form.status !== 'PENDING') return 22 return this.formReactiveService.waitPendingCheck(this.form)
37
38 // FIXME: the following line does not work: https://github.com/angular/angular/issues/41519
39 // return firstValueFrom(this.form.statusChanges.pipe(filter(status => status !== 'PENDING')))
40 // So we have to fallback to active wait :/
41
42 do {
43 await wait(10)
44 } while (this.form.status === 'PENDING')
45 } 23 }
46 24
47 protected markAllAsDirty (controlsArg?: { [ key: string ]: AbstractControl }) { 25 protected markAllAsDirty () {
48 const controls = controlsArg || this.form.controls 26 return this.formReactiveService.markAllAsDirty(this.form.controls)
49
50 for (const key of Object.keys(controls)) {
51 const control = controls[key]
52
53 if (control instanceof FormGroup) {
54 this.markAllAsDirty(control.controls)
55 continue
56 }
57
58 control.markAsDirty()
59 }
60 } 27 }
61 28
62 protected forceCheck () { 29 protected forceCheck () {
63 this.onStatusChanged(this.form, this.formErrors, this.validationMessages, false) 30 return this.formReactiveService.forceCheck(this.form, this.formErrors, this.validationMessages)
64 }
65
66 private onStatusChanged (
67 form: FormGroup,
68 formErrors: FormReactiveErrors,
69 validationMessages: FormReactiveValidationMessages,
70 onlyDirty = true
71 ) {
72 for (const field of Object.keys(formErrors)) {
73 if (formErrors[field] && typeof formErrors[field] === 'object') {
74 this.onStatusChanged(
75 form.controls[field] as FormGroup,
76 formErrors[field] as FormReactiveErrors,
77 validationMessages[field] as FormReactiveValidationMessages,
78 onlyDirty
79 )
80 continue
81 }
82
83 // clear previous error message (if any)
84 formErrors[field] = ''
85 const control = form.get(field)
86
87 if (control.dirty) this.formChanged = true
88
89 if (!control || (onlyDirty && !control.dirty) || !control.enabled || !control.errors) continue
90
91 const staticMessages = validationMessages[field]
92 for (const key of Object.keys(control.errors)) {
93 const formErrorValue = control.errors[key]
94
95 // Try to find error message in static validation messages first
96 // Then check if the validator returns a string that is the error
97 if (staticMessages[key]) formErrors[field] += staticMessages[key] + ' '
98 else if (typeof formErrorValue === 'string') formErrors[field] += control.errors[key]
99 else throw new Error('Form error value of ' + field + ' is invalid')
100 }
101 }
102 } 31 }
103} 32}
diff --git a/client/src/app/shared/shared-forms/form-validator.service.ts b/client/src/app/shared/shared-forms/form-validator.service.ts
index f67d5bb33..897008242 100644
--- a/client/src/app/shared/shared-forms/form-validator.service.ts
+++ b/client/src/app/shared/shared-forms/form-validator.service.ts
@@ -1,7 +1,7 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms' 2import { AsyncValidatorFn, FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn } from '@angular/forms'
3import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model' 3import { BuildFormArgument, BuildFormDefaultValues } from '../form-validators/form-validator.model'
4import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive' 4import { FormReactiveErrors, FormReactiveValidationMessages } from './form-reactive.service'
5 5
6@Injectable() 6@Injectable()
7export class FormValidatorService { 7export class FormValidatorService {
diff --git a/client/src/app/shared/shared-forms/index.ts b/client/src/app/shared/shared-forms/index.ts
index 495785e7b..bff9862f2 100644
--- a/client/src/app/shared/shared-forms/index.ts
+++ b/client/src/app/shared/shared-forms/index.ts
@@ -1,4 +1,5 @@
1export * from './advanced-input-filter.component' 1export * from './advanced-input-filter.component'
2export * from './form-reactive.service'
2export * from './form-reactive' 3export * from './form-reactive'
3export * from './form-validator.service' 4export * from './form-validator.service'
4export * from './form-validator.service' 5export * from './form-validator.service'
diff --git a/client/src/app/shared/shared-forms/input-text.component.ts b/client/src/app/shared/shared-forms/input-text.component.ts
index d667ed663..aa4a1cba8 100644
--- a/client/src/app/shared/shared-forms/input-text.component.ts
+++ b/client/src/app/shared/shared-forms/input-text.component.ts
@@ -1,4 +1,4 @@
1import { Component, forwardRef, Input } from '@angular/core' 1import { Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'
2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 2import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4 4
@@ -15,6 +15,8 @@ import { Notifier } from '@app/core'
15 ] 15 ]
16}) 16})
17export class InputTextComponent implements ControlValueAccessor { 17export class InputTextComponent implements ControlValueAccessor {
18 @ViewChild('input') inputElement: ElementRef
19
18 @Input() inputId = Math.random().toString(11).slice(2, 8) // id cannot be left empty or undefined 20 @Input() inputId = Math.random().toString(11).slice(2, 8) // id cannot be left empty or undefined
19 @Input() value = '' 21 @Input() value = ''
20 @Input() autocomplete = 'off' 22 @Input() autocomplete = 'off'
@@ -65,4 +67,10 @@ export class InputTextComponent implements ControlValueAccessor {
65 update () { 67 update () {
66 this.propagateChange(this.value) 68 this.propagateChange(this.value)
67 } 69 }
70
71 focus () {
72 const el: HTMLElement = this.inputElement.nativeElement
73
74 el.focus({ preventScroll: true })
75 }
68} 76}
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.ts b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
index 089991884..e3371f22c 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
@@ -144,9 +144,9 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
144 144
145 html = result 145 html = result
146 } else if (this.markdownType === 'text') { 146 } else if (this.markdownType === 'text') {
147 html = await this.markdownService.textMarkdownToHTML(text) 147 html = await this.markdownService.textMarkdownToHTML({ markdown: text })
148 } else { 148 } else {
149 html = await this.markdownService.enhancedMarkdownToHTML(text) 149 html = await this.markdownService.enhancedMarkdownToHTML({ markdown: text })
150 } 150 }
151 151
152 if (this.markdownVideo) { 152 if (this.markdownVideo) {
diff --git a/client/src/app/shared/shared-forms/shared-form.module.ts b/client/src/app/shared/shared-forms/shared-form.module.ts
index 81f076db6..628affb56 100644
--- a/client/src/app/shared/shared-forms/shared-form.module.ts
+++ b/client/src/app/shared/shared-forms/shared-form.module.ts
@@ -1,4 +1,3 @@
1
2import { InputMaskModule } from 'primeng/inputmask' 1import { InputMaskModule } from 'primeng/inputmask'
3import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
4import { FormsModule, ReactiveFormsModule } from '@angular/forms' 3import { FormsModule, ReactiveFormsModule } from '@angular/forms'
@@ -7,6 +6,7 @@ import { SharedGlobalIconModule } from '../shared-icons'
7import { SharedMainModule } from '../shared-main/shared-main.module' 6import { SharedMainModule } from '../shared-main/shared-main.module'
8import { AdvancedInputFilterComponent } from './advanced-input-filter.component' 7import { AdvancedInputFilterComponent } from './advanced-input-filter.component'
9import { DynamicFormFieldComponent } from './dynamic-form-field.component' 8import { DynamicFormFieldComponent } from './dynamic-form-field.component'
9import { FormReactiveService } from './form-reactive.service'
10import { FormValidatorService } from './form-validator.service' 10import { FormValidatorService } from './form-validator.service'
11import { InputSwitchComponent } from './input-switch.component' 11import { InputSwitchComponent } from './input-switch.component'
12import { InputTextComponent } from './input-text.component' 12import { InputTextComponent } from './input-text.component'
@@ -96,7 +96,8 @@ import { TimestampInputComponent } from './timestamp-input.component'
96 ], 96 ],
97 97
98 providers: [ 98 providers: [
99 FormValidatorService 99 FormValidatorService,
100 FormReactiveService
100 ] 101 ]
101}) 102})
102export class SharedFormModule { } 103export class SharedFormModule { }
diff --git a/client/src/app/shared/shared-instance/instance.service.ts b/client/src/app/shared/shared-instance/instance.service.ts
index 0241f56ef..89f47db24 100644
--- a/client/src/app/shared/shared-instance/instance.service.ts
+++ b/client/src/app/shared/shared-instance/instance.service.ts
@@ -51,7 +51,7 @@ export class InstanceService {
51 } 51 }
52 52
53 for (const key of Object.keys(html)) { 53 for (const key of Object.keys(html)) {
54 html[key] = await this.markdownService.textMarkdownToHTML(about.instance[key]) 54 html[key] = await this.markdownService.textMarkdownToHTML({ markdown: about.instance[key] })
55 } 55 }
56 56
57 return html 57 return html
diff --git a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
index e4b74f3ad..93b3a93d6 100644
--- a/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
+++ b/client/src/app/shared/shared-main/auth/auth-interceptor.service.ts
@@ -27,13 +27,16 @@ export class AuthInterceptor implements HttpInterceptor {
27 .pipe( 27 .pipe(
28 catchError((err: HttpErrorResponse) => { 28 catchError((err: HttpErrorResponse) => {
29 const error = err.error as PeerTubeProblemDocument 29 const error = err.error as PeerTubeProblemDocument
30 const isOTPMissingError = this.authService.isOTPMissingError(err)
30 31
31 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && error && error.code === OAuth2ErrorCode.INVALID_TOKEN) { 32 if (!isOTPMissingError) {
32 return this.handleTokenExpired(req, next) 33 if (err.status === HttpStatusCode.UNAUTHORIZED_401 && error && error.code === OAuth2ErrorCode.INVALID_TOKEN) {
33 } 34 return this.handleTokenExpired(req, next)
35 }
34 36
35 if (err.status === HttpStatusCode.UNAUTHORIZED_401) { 37 if (err.status === HttpStatusCode.UNAUTHORIZED_401) {
36 return this.handleNotAuthenticated(err) 38 return this.handleNotAuthenticated(err)
39 }
37 } 40 }
38 41
39 return observableThrowError(() => err) 42 return observableThrowError(() => err)
diff --git a/client/src/app/shared/shared-main/buttons/action-dropdown.component.html b/client/src/app/shared/shared-main/buttons/action-dropdown.component.html
index 37cf63fcd..474baafd7 100644
--- a/client/src/app/shared/shared-main/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/shared-main/buttons/action-dropdown.component.html
@@ -26,7 +26,7 @@
26 26
27 <a 27 <a
28 *ngIf="action.linkBuilder && !action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }" 28 *ngIf="action.linkBuilder && !action.isHeader" [ngClass]="{ 'with-icon': !!action.iconName }"
29 class="dropdown-item" [routerLink]="action.linkBuilder(entry)" [title]="action.title || ''" 29 class="dropdown-item" [routerLink]="action.linkBuilder(entry)" [queryParams]="getQueryParams(action, entry)" [title]="action.title || ''"
30 > 30 >
31 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container> 31 <ng-container *ngTemplateOutlet="templateActionLabel; context:{ $implicit: action }"></ng-container>
32 </a> 32 </a>
diff --git a/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts b/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts
index 749773f8a..e39fbd66d 100644
--- a/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts
+++ b/client/src/app/shared/shared-main/buttons/action-dropdown.component.ts
@@ -1,4 +1,5 @@
1import { Component, Input } from '@angular/core' 1import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
2import { Params } from '@angular/router'
2import { GlobalIconName } from '@app/shared/shared-icons' 3import { GlobalIconName } from '@app/shared/shared-icons'
3 4
4export type DropdownAction<T> = { 5export type DropdownAction<T> = {
@@ -7,7 +8,10 @@ export type DropdownAction<T> = {
7 description?: string 8 description?: string
8 title?: string 9 title?: string
9 handler?: (a: T) => any 10 handler?: (a: T) => any
11
10 linkBuilder?: (a: T) => (string | number)[] 12 linkBuilder?: (a: T) => (string | number)[]
13 queryParamsBuilder?: (a: T) => Params
14
11 isDisplayed?: (a: T) => boolean 15 isDisplayed?: (a: T) => boolean
12 16
13 class?: string[] 17 class?: string[]
@@ -21,7 +25,8 @@ export type DropdownDirection = 'horizontal' | 'vertical'
21@Component({ 25@Component({
22 selector: 'my-action-dropdown', 26 selector: 'my-action-dropdown',
23 styleUrls: [ './action-dropdown.component.scss' ], 27 styleUrls: [ './action-dropdown.component.scss' ],
24 templateUrl: './action-dropdown.component.html' 28 templateUrl: './action-dropdown.component.html',
29 changeDetection: ChangeDetectionStrategy.OnPush
25}) 30})
26 31
27export class ActionDropdownComponent<T> { 32export class ActionDropdownComponent<T> {
@@ -44,6 +49,12 @@ export class ActionDropdownComponent<T> {
44 return [ this.actions as DropdownAction<T>[] ] 49 return [ this.actions as DropdownAction<T>[] ]
45 } 50 }
46 51
52 getQueryParams (action: DropdownAction<T>, entry: T) {
53 if (action.queryParamsBuilder) return action.queryParamsBuilder(entry)
54
55 return {}
56 }
57
47 areActionsDisplayed (actions: Array<DropdownAction<T> | DropdownAction<T>[]>, entry: T): boolean { 58 areActionsDisplayed (actions: Array<DropdownAction<T> | DropdownAction<T>[]>, entry: T): boolean {
48 return actions.some(a => { 59 return actions.some(a => {
49 if (Array.isArray(a)) return this.areActionsDisplayed(a, entry) 60 if (Array.isArray(a)) return this.areActionsDisplayed(a, entry)
diff --git a/client/src/app/shared/shared-main/buttons/button.component.ts b/client/src/app/shared/shared-main/buttons/button.component.ts
index 10d67831f..1761938ee 100644
--- a/client/src/app/shared/shared-main/buttons/button.component.ts
+++ b/client/src/app/shared/shared-main/buttons/button.component.ts
@@ -1,10 +1,11 @@
1import { Component, Input, OnChanges } from '@angular/core' 1import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'
2import { GlobalIconName } from '@app/shared/shared-icons' 2import { GlobalIconName } from '@app/shared/shared-icons'
3 3
4@Component({ 4@Component({
5 selector: 'my-button', 5 selector: 'my-button',
6 styleUrls: [ './button.component.scss' ], 6 styleUrls: [ './button.component.scss' ],
7 templateUrl: './button.component.html' 7 templateUrl: './button.component.html',
8 changeDetection: ChangeDetectionStrategy.OnPush
8}) 9})
9 10
10export class ButtonComponent implements OnChanges { 11export class ButtonComponent implements OnChanges {
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index 04b223cc5..c1523bc50 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -44,7 +44,15 @@ import {
44import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins' 44import { PluginPlaceholderComponent, PluginSelectorDirective } from './plugins'
45import { ActorRedirectGuard } from './router' 45import { ActorRedirectGuard } from './router'
46import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users' 46import { UserHistoryService, UserNotificationsComponent, UserNotificationService, UserQuotaComponent } from './users'
47import { EmbedComponent, RedundancyService, VideoImportService, VideoOwnershipService, VideoResolver, VideoService } from './video' 47import {
48 EmbedComponent,
49 RedundancyService,
50 VideoFileTokenService,
51 VideoImportService,
52 VideoOwnershipService,
53 VideoResolver,
54 VideoService
55} from './video'
48import { VideoCaptionService } from './video-caption' 56import { VideoCaptionService } from './video-caption'
49import { VideoChannelService } from './video-channel' 57import { VideoChannelService } from './video-channel'
50 58
@@ -185,6 +193,7 @@ import { VideoChannelService } from './video-channel'
185 VideoImportService, 193 VideoImportService,
186 VideoOwnershipService, 194 VideoOwnershipService,
187 VideoService, 195 VideoService,
196 VideoFileTokenService,
188 VideoResolver, 197 VideoResolver,
189 198
190 VideoCaptionService, 199 VideoCaptionService,
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
index 5e3985526..08811afec 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
@@ -2,7 +2,7 @@ import { Observable, ReplaySubject } from 'rxjs'
2import { catchError, map, tap } from 'rxjs/operators' 2import { catchError, map, tap } from 'rxjs/operators'
3import { HttpClient, HttpParams } from '@angular/common/http' 3import { HttpClient, HttpParams } from '@angular/common/http'
4import { Injectable } from '@angular/core' 4import { Injectable } from '@angular/core'
5import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core' 5import { ComponentPaginationLight, RestExtractor, RestService, ServerService } from '@app/core'
6import { 6import {
7 ActorImage, 7 ActorImage,
8 ResultList, 8 ResultList,
@@ -25,7 +25,8 @@ export class VideoChannelService {
25 constructor ( 25 constructor (
26 private authHttp: HttpClient, 26 private authHttp: HttpClient,
27 private restService: RestService, 27 private restService: RestService,
28 private restExtractor: RestExtractor 28 private restExtractor: RestExtractor,
29 private serverService: ServerService
29 ) { } 30 ) { }
30 31
31 static extractVideoChannels (result: ResultList<VideoChannelServer>) { 32 static extractVideoChannels (result: ResultList<VideoChannelServer>) {
@@ -56,9 +57,11 @@ export class VideoChannelService {
56 }): Observable<ResultList<VideoChannel>> { 57 }): Observable<ResultList<VideoChannel>> {
57 const { account, componentPagination, withStats = false, sort, search } = options 58 const { account, componentPagination, withStats = false, sort, search } = options
58 59
60 const defaultCount = this.serverService.getHTMLConfig().videoChannels.maxPerUser
61
59 const pagination = componentPagination 62 const pagination = componentPagination
60 ? this.restService.componentToRestPagination(componentPagination) 63 ? this.restService.componentToRestPagination(componentPagination)
61 : { start: 0, count: 20 } 64 : { start: 0, count: defaultCount }
62 65
63 let params = new HttpParams() 66 let params = new HttpParams()
64 params = this.restService.addRestGetParams(params, pagination, sort) 67 params = this.restService.addRestGetParams(params, pagination, sort)
diff --git a/client/src/app/shared/shared-main/video/index.ts b/client/src/app/shared/shared-main/video/index.ts
index 361601456..a2e47883e 100644
--- a/client/src/app/shared/shared-main/video/index.ts
+++ b/client/src/app/shared/shared-main/video/index.ts
@@ -2,6 +2,7 @@ export * from './embed.component'
2export * from './redundancy.service' 2export * from './redundancy.service'
3export * from './video-details.model' 3export * from './video-details.model'
4export * from './video-edit.model' 4export * from './video-edit.model'
5export * from './video-file-token.service'
5export * from './video-import.service' 6export * from './video-import.service'
6export * from './video-ownership.service' 7export * from './video-ownership.service'
7export * from './video.model' 8export * from './video.model'
diff --git a/client/src/app/shared/shared-main/video/video-file-token.service.ts b/client/src/app/shared/shared-main/video/video-file-token.service.ts
new file mode 100644
index 000000000..791607249
--- /dev/null
+++ b/client/src/app/shared/shared-main/video/video-file-token.service.ts
@@ -0,0 +1,33 @@
1import { catchError, map, of, tap } from 'rxjs'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor } from '@app/core'
5import { VideoToken } from '@shared/models'
6import { VideoService } from './video.service'
7
8@Injectable()
9export class VideoFileTokenService {
10
11 private readonly store = new Map<string, { token: string, expires: Date }>()
12
13 constructor (
14 private authHttp: HttpClient,
15 private restExtractor: RestExtractor
16 ) {}
17
18 getVideoFileToken (videoUUID: string) {
19 const existing = this.store.get(videoUUID)
20 if (existing) return of(existing)
21
22 return this.createVideoFileToken(videoUUID)
23 .pipe(tap(result => this.store.set(videoUUID, { token: result.token, expires: new Date(result.expires) })))
24 }
25
26 private createVideoFileToken (videoUUID: string) {
27 return this.authHttp.post<VideoToken>(`${VideoService.BASE_VIDEO_URL}/${videoUUID}/token`, {})
28 .pipe(
29 map(({ files }) => files),
30 catchError(err => this.restExtractor.handleError(err))
31 )
32 }
33}
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index c9c6b979c..6fdffb394 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -34,6 +34,7 @@ export class Video implements VideoServerModel {
34 language: VideoConstant<string> 34 language: VideoConstant<string>
35 privacy: VideoConstant<VideoPrivacy> 35 privacy: VideoConstant<VideoPrivacy>
36 36
37 truncatedDescription: string
37 description: string 38 description: string
38 39
39 duration: number 40 duration: number
@@ -134,6 +135,8 @@ export class Video implements VideoServerModel {
134 this.privacy = hash.privacy 135 this.privacy = hash.privacy
135 this.waitTranscoding = hash.waitTranscoding 136 this.waitTranscoding = hash.waitTranscoding
136 this.state = hash.state 137 this.state = hash.state
138
139 this.truncatedDescription = hash.truncatedDescription
137 this.description = hash.description 140 this.description = hash.description
138 141
139 this.isLive = hash.isLive 142 this.isLive = hash.isLive
diff --git a/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts b/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
index 20be728f6..ec2fea528 100644
--- a/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
+++ b/client/src/app/shared/shared-moderation/batch-domains-modal.component.ts
@@ -1,5 +1,5 @@
1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
2import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 2import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
3import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 3import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
4import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 4import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
5import { splitAndGetNotEmpty, UNIQUE_HOSTS_VALIDATOR } from '../form-validators/host-validators' 5import { splitAndGetNotEmpty, UNIQUE_HOSTS_VALIDATOR } from '../form-validators/host-validators'
@@ -18,7 +18,7 @@ export class BatchDomainsModalComponent extends FormReactive implements OnInit {
18 private openedModal: NgbModalRef 18 private openedModal: NgbModalRef
19 19
20 constructor ( 20 constructor (
21 protected formValidatorService: FormValidatorService, 21 protected formReactiveService: FormReactiveService,
22 private modalService: NgbModal 22 private modalService: NgbModal
23 ) { 23 ) {
24 super() 24 super()
diff --git a/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
index 78c9b3382..d587a9709 100644
--- a/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/account-report.component.ts
@@ -2,7 +2,7 @@ import { mapValues, pickBy } from 'lodash-es'
2import { Component, OnInit, ViewChild } from '@angular/core' 2import { Component, OnInit, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' 4import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
6import { Account } from '@app/shared/shared-main' 6import { Account } from '@app/shared/shared-main'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -26,7 +26,7 @@ export class AccountReportComponent extends FormReactive implements OnInit {
26 private openedModal: NgbModalRef 26 private openedModal: NgbModalRef
27 27
28 constructor ( 28 constructor (
29 protected formValidatorService: FormValidatorService, 29 protected formReactiveService: FormReactiveService,
30 private modalService: NgbModal, 30 private modalService: NgbModal,
31 private abuseService: AbuseService, 31 private abuseService: AbuseService,
32 private notifier: Notifier 32 private notifier: Notifier
diff --git a/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
index 7c0907ce4..e35d70c8f 100644
--- a/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/comment-report.component.ts
@@ -2,7 +2,7 @@ import { mapValues, pickBy } from 'lodash-es'
2import { Component, Input, OnInit, ViewChild } from '@angular/core' 2import { Component, Input, OnInit, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' 4import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
6import { VideoComment } from '@app/shared/shared-video-comment' 6import { VideoComment } from '@app/shared/shared-video-comment'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -27,7 +27,7 @@ export class CommentReportComponent extends FormReactive implements OnInit {
27 private openedModal: NgbModalRef 27 private openedModal: NgbModalRef
28 28
29 constructor ( 29 constructor (
30 protected formValidatorService: FormValidatorService, 30 protected formReactiveService: FormReactiveService,
31 private modalService: NgbModal, 31 private modalService: NgbModal,
32 private abuseService: AbuseService, 32 private abuseService: AbuseService,
33 private notifier: Notifier 33 private notifier: Notifier
diff --git a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
index 38dd92910..16be8e0a1 100644
--- a/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
+++ b/client/src/app/shared/shared-moderation/report-modals/video-report.component.ts
@@ -3,7 +3,7 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'
3import { DomSanitizer } from '@angular/platform-browser' 3import { DomSanitizer } from '@angular/platform-browser'
4import { Notifier } from '@app/core' 4import { Notifier } from '@app/core'
5import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators' 5import { ABUSE_REASON_VALIDATOR } from '@app/shared/form-validators/abuse-validators'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 7import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 8import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
9import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse' 9import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
@@ -27,7 +27,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
27 private openedModal: NgbModalRef 27 private openedModal: NgbModalRef
28 28
29 constructor ( 29 constructor (
30 protected formValidatorService: FormValidatorService, 30 protected formReactiveService: FormReactiveService,
31 private modalService: NgbModal, 31 private modalService: NgbModal,
32 private abuseService: AbuseService, 32 private abuseService: AbuseService,
33 private notifier: Notifier, 33 private notifier: Notifier,
diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
index 617408f2a..27dcf043a 100644
--- a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
+++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts
@@ -2,7 +2,7 @@ import { forkJoin } from 'rxjs'
2import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 2import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
3import { Notifier } from '@app/core' 3import { Notifier } from '@app/core'
4import { prepareIcu } from '@app/helpers' 4import { prepareIcu } from '@app/helpers'
5import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 5import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
8import { User } from '@shared/models' 8import { User } from '@shared/models'
@@ -25,7 +25,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
25 modalMessage = '' 25 modalMessage = ''
26 26
27 constructor ( 27 constructor (
28 protected formValidatorService: FormValidatorService, 28 protected formReactiveService: FormReactiveService,
29 private modalService: NgbModal, 29 private modalService: NgbModal,
30 private notifier: Notifier, 30 private notifier: Notifier,
31 private userAdminService: UserAdminService, 31 private userAdminService: UserAdminService,
diff --git a/client/src/app/shared/shared-moderation/video-block.component.ts b/client/src/app/shared/shared-moderation/video-block.component.ts
index f8b22a3f6..3ff53443a 100644
--- a/client/src/app/shared/shared-moderation/video-block.component.ts
+++ b/client/src/app/shared/shared-moderation/video-block.component.ts
@@ -1,7 +1,7 @@
1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { prepareIcu } from '@app/helpers' 3import { prepareIcu } from '@app/helpers'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { Video } from '@app/shared/shared-main' 5import { Video } from '@app/shared/shared-main'
6import { NgbModal } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 7import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
@@ -25,7 +25,7 @@ export class VideoBlockComponent extends FormReactive implements OnInit {
25 private openedModal: NgbModalRef 25 private openedModal: NgbModalRef
26 26
27 constructor ( 27 constructor (
28 protected formValidatorService: FormValidatorService, 28 protected formReactiveService: FormReactiveService,
29 private modalService: NgbModal, 29 private modalService: NgbModal,
30 private videoBlocklistService: VideoBlockService, 30 private videoBlocklistService: VideoBlockService,
31 private notifier: Notifier 31 private notifier: Notifier
diff --git a/client/src/app/shared/shared-search/find-in-bulk.service.ts b/client/src/app/shared/shared-search/find-in-bulk.service.ts
index d2f8c3213..d6ee04379 100644
--- a/client/src/app/shared/shared-search/find-in-bulk.service.ts
+++ b/client/src/app/shared/shared-search/find-in-bulk.service.ts
@@ -80,13 +80,18 @@ export class FindInBulkService {
80 map(result => result.response.data), 80 map(result => result.response.data),
81 map(data => data.find(finder)) 81 map(data => data.find(finder))
82 ) 82 )
83 .subscribe(result => { 83 .subscribe({
84 if (!result) { 84 next: result => {
85 obs.error(new Error($localize`Element ${param} not found`)) 85 if (!result) {
86 } else { 86 obs.error(new Error($localize`Element ${param} not found`))
87 return
88 }
89
87 obs.next(result) 90 obs.next(result)
88 obs.complete() 91 obs.complete()
89 } 92 },
93
94 error: err => obs.error(err)
90 }) 95 })
91 96
92 observableObject.notifier.next(param) 97 observableObject.notifier.next(param)
diff --git a/client/src/app/shared/shared-support-modal/support-modal.component.ts b/client/src/app/shared/shared-support-modal/support-modal.component.ts
index 08e997f7b..f330228e1 100644
--- a/client/src/app/shared/shared-support-modal/support-modal.component.ts
+++ b/client/src/app/shared/shared-support-modal/support-modal.component.ts
@@ -27,7 +27,7 @@ export class SupportModalComponent {
27 27
28 const support = this.video?.support || this.videoChannel.support 28 const support = this.video?.support || this.videoChannel.support
29 29
30 this.markdownService.enhancedMarkdownToHTML(support) 30 this.markdownService.enhancedMarkdownToHTML({ markdown: support })
31 .then(r => { 31 .then(r => {
32 this.htmlSupport = r 32 this.htmlSupport = r
33 }) 33 })
diff --git a/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts b/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts
index 13e2e5424..c2c30d38b 100644
--- a/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts
+++ b/client/src/app/shared/shared-user-settings/user-interface-settings.component.ts
@@ -1,7 +1,7 @@
1import { Subject, Subscription } from 'rxjs' 1import { Subject, Subscription } from 'rxjs'
2import { Component, Input, OnDestroy, OnInit } from '@angular/core' 2import { Component, Input, OnDestroy, OnInit } from '@angular/core'
3import { AuthService, Notifier, ServerService, ThemeService, UserService } from '@app/core' 3import { AuthService, Notifier, ServerService, ThemeService, UserService } from '@app/core'
4import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 4import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
5import { HTMLServerConfig, User, UserUpdateMe } from '@shared/models' 5import { HTMLServerConfig, User, UserUpdateMe } from '@shared/models'
6import { SelectOptionsItem } from 'src/types' 6import { SelectOptionsItem } from 'src/types'
7 7
@@ -22,7 +22,7 @@ export class UserInterfaceSettingsComponent extends FormReactive implements OnIn
22 private serverConfig: HTMLServerConfig 22 private serverConfig: HTMLServerConfig
23 23
24 constructor ( 24 constructor (
25 protected formValidatorService: FormValidatorService, 25 protected formReactiveService: FormReactiveService,
26 private authService: AuthService, 26 private authService: AuthService,
27 private notifier: Notifier, 27 private notifier: Notifier,
28 private userService: UserService, 28 private userService: UserService,
diff --git a/client/src/app/shared/shared-user-settings/user-video-settings.component.ts b/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
index 7d6b69469..af0870f12 100644
--- a/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
+++ b/client/src/app/shared/shared-user-settings/user-video-settings.component.ts
@@ -3,7 +3,7 @@ import { Subject, Subscription } from 'rxjs'
3import { first } from 'rxjs/operators' 3import { first } from 'rxjs/operators'
4import { Component, Input, OnDestroy, OnInit } from '@angular/core' 4import { Component, Input, OnDestroy, OnInit } from '@angular/core'
5import { AuthService, Notifier, ServerService, User, UserService } from '@app/core' 5import { AuthService, Notifier, ServerService, User, UserService } from '@app/core'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7import { UserUpdateMe } from '@shared/models' 7import { UserUpdateMe } from '@shared/models'
8import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type' 8import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
9 9
@@ -22,7 +22,7 @@ export class UserVideoSettingsComponent extends FormReactive implements OnInit,
22 formValuesWatcher: Subscription 22 formValuesWatcher: Subscription
23 23
24 constructor ( 24 constructor (
25 protected formValidatorService: FormValidatorService, 25 protected formReactiveService: FormReactiveService,
26 private authService: AuthService, 26 private authService: AuthService,
27 private notifier: Notifier, 27 private notifier: Notifier,
28 private userService: UserService, 28 private userService: UserService,
diff --git a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
index 7bcfdd8aa..61bcd5345 100644
--- a/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
+++ b/client/src/app/shared/shared-user-subscription/remote-subscribe.component.ts
@@ -1,6 +1,6 @@
1import { Component, Input, OnInit } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { Notifier } from '@app/core' 2import { Notifier } from '@app/core'
3import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 3import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
4import { logger } from '@root-helpers/logger' 4import { logger } from '@root-helpers/logger'
5import { USER_HANDLE_VALIDATOR } from '../form-validators/user-validators' 5import { USER_HANDLE_VALIDATOR } from '../form-validators/user-validators'
6 6
@@ -15,7 +15,7 @@ export class RemoteSubscribeComponent extends FormReactive implements OnInit {
15 @Input() showHelp = false 15 @Input() showHelp = false
16 16
17 constructor ( 17 constructor (
18 protected formValidatorService: FormValidatorService, 18 protected formReactiveService: FormReactiveService,
19 private notifier: Notifier 19 private notifier: Notifier
20 ) { 20 ) {
21 super() 21 super()
diff --git a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
index 0e09c2697..341b83a04 100644
--- a/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
+++ b/client/src/app/shared/shared-user-subscription/subscribe-button.component.html
@@ -37,7 +37,7 @@
37 class="btn-group" ngbDropdown autoClose="outside" placement="bottom-right bottom-left bottom auto" 37 class="btn-group" ngbDropdown autoClose="outside" placement="bottom-right bottom-left bottom auto"
38 role="group" aria-label="Multiple ways to subscribe to the current channel" i18n-aria-label 38 role="group" aria-label="Multiple ways to subscribe to the current channel" i18n-aria-label
39 > 39 >
40 <button class="btn dropdown-toggle-split" ngbDropdownToggle aria-label="Open subscription dropdown" i18n-aria-label> 40 <button class="btn dropdown-toggle-split last-in-group" ngbDropdownToggle aria-label="Open subscription dropdown" i18n-aria-label>
41 <ng-container 41 <ng-container
42 *ngIf="!isUserLoggedIn(); then userLoggedOut"> 42 *ngIf="!isUserLoggedIn(); then userLoggedOut">
43 </ng-container> 43 </ng-container>
diff --git a/client/src/app/shared/shared-users/index.ts b/client/src/app/shared/shared-users/index.ts
index 8f90f2515..20e60486d 100644
--- a/client/src/app/shared/shared-users/index.ts
+++ b/client/src/app/shared/shared-users/index.ts
@@ -1,4 +1,5 @@
1export * from './user-admin.service' 1export * from './user-admin.service'
2export * from './user-signup.service' 2export * from './user-signup.service'
3export * from './two-factor.service'
3 4
4export * from './shared-users.module' 5export * from './shared-users.module'
diff --git a/client/src/app/shared/shared-users/shared-users.module.ts b/client/src/app/shared/shared-users/shared-users.module.ts
index 2a1dadf20..5a1675dc9 100644
--- a/client/src/app/shared/shared-users/shared-users.module.ts
+++ b/client/src/app/shared/shared-users/shared-users.module.ts
@@ -1,6 +1,7 @@
1 1
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { SharedMainModule } from '../shared-main/shared-main.module' 3import { SharedMainModule } from '../shared-main/shared-main.module'
4import { TwoFactorService } from './two-factor.service'
4import { UserAdminService } from './user-admin.service' 5import { UserAdminService } from './user-admin.service'
5import { UserSignupService } from './user-signup.service' 6import { UserSignupService } from './user-signup.service'
6 7
@@ -15,7 +16,8 @@ import { UserSignupService } from './user-signup.service'
15 16
16 providers: [ 17 providers: [
17 UserSignupService, 18 UserSignupService,
18 UserAdminService 19 UserAdminService,
20 TwoFactorService
19 ] 21 ]
20}) 22})
21export class SharedUsersModule { } 23export class SharedUsersModule { }
diff --git a/client/src/app/shared/shared-users/two-factor.service.ts b/client/src/app/shared/shared-users/two-factor.service.ts
new file mode 100644
index 000000000..9ff916f15
--- /dev/null
+++ b/client/src/app/shared/shared-users/two-factor.service.ts
@@ -0,0 +1,52 @@
1import { catchError } from 'rxjs/operators'
2import { HttpClient } from '@angular/common/http'
3import { Injectable } from '@angular/core'
4import { RestExtractor, UserService } from '@app/core'
5import { TwoFactorEnableResult } from '@shared/models'
6
7@Injectable()
8export class TwoFactorService {
9 constructor (
10 private authHttp: HttpClient,
11 private restExtractor: RestExtractor
12 ) { }
13
14 // ---------------------------------------------------------------------------
15
16 requestTwoFactor (options: {
17 userId: number
18 currentPassword: string
19 }) {
20 const { userId, currentPassword } = options
21
22 const url = UserService.BASE_USERS_URL + userId + '/two-factor/request'
23
24 return this.authHttp.post<TwoFactorEnableResult>(url, { currentPassword })
25 .pipe(catchError(err => this.restExtractor.handleError(err)))
26 }
27
28 confirmTwoFactorRequest (options: {
29 userId: number
30 requestToken: string
31 otpToken: string
32 }) {
33 const { userId, requestToken, otpToken } = options
34
35 const url = UserService.BASE_USERS_URL + userId + '/two-factor/confirm-request'
36
37 return this.authHttp.post(url, { requestToken, otpToken })
38 .pipe(catchError(err => this.restExtractor.handleError(err)))
39 }
40
41 disableTwoFactor (options: {
42 userId: number
43 currentPassword?: string
44 }) {
45 const { userId, currentPassword } = options
46
47 const url = UserService.BASE_USERS_URL + userId + '/two-factor/disable'
48
49 return this.authHttp.post(url, { currentPassword })
50 .pipe(catchError(err => this.restExtractor.handleError(err)))
51 }
52}
diff --git a/client/src/app/shared/shared-users/user-admin.service.ts b/client/src/app/shared/shared-users/user-admin.service.ts
index 4128358dc..0b04023a3 100644
--- a/client/src/app/shared/shared-users/user-admin.service.ts
+++ b/client/src/app/shared/shared-users/user-admin.service.ts
@@ -125,7 +125,10 @@ export class UserAdminService {
125 } 125 }
126 126
127 return Object.assign(user, { 127 return Object.assign(user, {
128 roleLabel: roleLabels[user.role], 128 role: {
129 id: user.role.id,
130 label: roleLabels[user.role.id]
131 },
129 videoQuota, 132 videoQuota,
130 videoQuotaUsed, 133 videoQuotaUsed,
131 rawVideoQuota: user.videoQuota, 134 rawVideoQuota: user.videoQuota,
diff --git a/client/src/app/shared/shared-video-live/live-stream-information.component.html b/client/src/app/shared/shared-video-live/live-stream-information.component.html
index cf30c1ce1..8e61bdbb3 100644
--- a/client/src/app/shared/shared-video-live/live-stream-information.component.html
+++ b/client/src/app/shared/shared-video-live/live-stream-information.component.html
@@ -32,7 +32,7 @@
32 <div class="form-group-description" i18n>⚠️ Never share your stream key with anyone.</div> 32 <div class="form-group-description" i18n>⚠️ Never share your stream key with anyone.</div>
33 </div> 33 </div>
34 34
35 <div class="journal"> 35 <div class="journal" *ngIf="latestLiveSessions.length !== 0">
36 <label i18n>Latest live sessions</label> 36 <label i18n>Latest live sessions</label>
37 37
38 <div class="journal-session" *ngFor="let session of latestLiveSessions"> 38 <div class="journal-session" *ngFor="let session of latestLiveSessions">
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.html b/client/src/app/shared/shared-video-miniature/video-download.component.html
index 1c7458b4b..1f622933d 100644
--- a/client/src/app/shared/shared-video-miniature/video-download.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-download.component.html
@@ -48,10 +48,7 @@
48 48
49 <ng-template ngbNavContent> 49 <ng-template ngbNavContent>
50 <div class="nav-content"> 50 <div class="nav-content">
51 <my-input-text 51 <my-input-text [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"></my-input-text>
52 *ngIf="!isConfidentialVideo()"
53 [show]="true" [readonly]="true" [withCopy]="true" [withToggle]="false" [value]="getLink()"
54 ></my-input-text>
55 </div> 52 </div>
56 </ng-template> 53 </ng-template>
57 </ng-container> 54 </ng-container>
diff --git a/client/src/app/shared/shared-video-miniature/video-download.component.ts b/client/src/app/shared/shared-video-miniature/video-download.component.ts
index 47482caaa..667cb107f 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,11 +2,12 @@ import { mapValues, pick } from 'lodash-es'
2import { firstValueFrom } from 'rxjs' 2import { firstValueFrom } from 'rxjs'
3import { tap } from 'rxjs/operators' 3import { tap } from 'rxjs/operators'
4import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core' 4import { Component, ElementRef, Inject, LOCALE_ID, ViewChild } from '@angular/core'
5import { AuthService, HooksService, Notifier } from '@app/core' 5import { HooksService } from '@app/core'
6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' 6import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
7import { logger } from '@root-helpers/logger' 7import { logger } from '@root-helpers/logger'
8import { videoRequiresAuth } from '@root-helpers/video'
8import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models' 9import { VideoCaption, VideoFile, VideoPrivacy } from '@shared/models'
9import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoService } from '../shared-main' 10import { BytesPipe, NumberFormatterPipe, VideoDetails, VideoFileTokenService, VideoService } from '../shared-main'
10 11
11type DownloadType = 'video' | 'subtitles' 12type DownloadType = 'video' | 'subtitles'
12type FileMetadata = { [key: string]: { label: string, value: string }} 13type FileMetadata = { [key: string]: { label: string, value: string }}
@@ -32,6 +33,8 @@ export class VideoDownloadComponent {
32 33
33 type: DownloadType = 'video' 34 type: DownloadType = 'video'
34 35
36 videoFileToken: string
37
35 private activeModal: NgbModalRef 38 private activeModal: NgbModalRef
36 39
37 private bytesPipe: BytesPipe 40 private bytesPipe: BytesPipe
@@ -42,10 +45,9 @@ export class VideoDownloadComponent {
42 45
43 constructor ( 46 constructor (
44 @Inject(LOCALE_ID) private localeId: string, 47 @Inject(LOCALE_ID) private localeId: string,
45 private notifier: Notifier,
46 private modalService: NgbModal, 48 private modalService: NgbModal,
47 private videoService: VideoService, 49 private videoService: VideoService,
48 private auth: AuthService, 50 private videoFileTokenService: VideoFileTokenService,
49 private hooks: HooksService 51 private hooks: HooksService
50 ) { 52 ) {
51 this.bytesPipe = new BytesPipe() 53 this.bytesPipe = new BytesPipe()
@@ -71,6 +73,8 @@ export class VideoDownloadComponent {
71 } 73 }
72 74
73 show (video: VideoDetails, videoCaptions?: VideoCaption[]) { 75 show (video: VideoDetails, videoCaptions?: VideoCaption[]) {
76 this.videoFileToken = undefined
77
74 this.video = video 78 this.video = video
75 this.videoCaptions = videoCaptions 79 this.videoCaptions = videoCaptions
76 80
@@ -84,6 +88,11 @@ export class VideoDownloadComponent {
84 this.subtitleLanguageId = this.videoCaptions[0].language.id 88 this.subtitleLanguageId = this.videoCaptions[0].language.id
85 } 89 }
86 90
91 if (videoRequiresAuth(this.video)) {
92 this.videoFileTokenService.getVideoFileToken(this.video.uuid)
93 .subscribe(({ token }) => this.videoFileToken = token)
94 }
95
87 this.activeModal.shown.subscribe(() => { 96 this.activeModal.shown.subscribe(() => {
88 this.hooks.runAction('action:modal.video-download.shown', 'common') 97 this.hooks.runAction('action:modal.video-download.shown', 'common')
89 }) 98 })
@@ -155,7 +164,7 @@ export class VideoDownloadComponent {
155 if (!file) return '' 164 if (!file) return ''
156 165
157 const suffix = this.isConfidentialVideo() 166 const suffix = this.isConfidentialVideo()
158 ? '?access_token=' + this.auth.getAccessToken() 167 ? '?videoFileToken=' + this.videoFileToken
159 : '' 168 : ''
160 169
161 switch (this.downloadType) { 170 switch (this.downloadType) {
diff --git a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
index 9ddfd7dda..1e92e1952 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-filters-header.component.html
@@ -47,6 +47,7 @@
47 <ng-option i18n value="-publishedAt">Sort by <strong>"Recently Added"</strong></ng-option> 47 <ng-option i18n value="-publishedAt">Sort by <strong>"Recently Added"</strong></ng-option>
48 <ng-option i18n value="-originallyPublishedAt">Sort by <strong>"Original Publication Date"</strong></ng-option> 48 <ng-option i18n value="-originallyPublishedAt">Sort by <strong>"Original Publication Date"</strong></ng-option>
49 49
50 <ng-option i18n value="name">Sort by <strong>"Name"</strong></ng-option>
50 <ng-option i18n *ngIf="isTrendingSortEnabled('most-viewed')" value="-trending">Sort by <strong>"Recent Views"</strong></ng-option> 51 <ng-option i18n *ngIf="isTrendingSortEnabled('most-viewed')" value="-trending">Sort by <strong>"Recent Views"</strong></ng-option>
51 <ng-option i18n *ngIf="isTrendingSortEnabled('hot')" value="-hot">Sort by <strong>"Hot"</strong></ng-option> 52 <ng-option i18n *ngIf="isTrendingSortEnabled('hot')" value="-hot">Sort by <strong>"Hot"</strong></ng-option>
52 <ng-option i18n *ngIf="isTrendingSortEnabled('most-liked')" value="-likes">Sort by <strong>"Likes"</strong></ng-option> 53 <ng-option i18n *ngIf="isTrendingSortEnabled('most-liked')" value="-likes">Sort by <strong>"Likes"</strong></ng-option>
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index e8d2ca1c4..6fdf24b2d 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -52,6 +52,12 @@
52 <ng-container *ngIf="displayOptions.privacyText && displayOptions.state && getStateLabel(video)"> - </ng-container> 52 <ng-container *ngIf="displayOptions.privacyText && displayOptions.state && getStateLabel(video)"> - </ng-container>
53 <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container> 53 <ng-container *ngIf="displayOptions.state">{{ getStateLabel(video) }}</ng-container>
54 </div> 54 </div>
55
56 <div *ngIf="containedInPlaylists" class="video-contained-in-playlists">
57 <a *ngFor="let playlist of containedInPlaylists" class="chip rectangular bg-secondary text-light" [routerLink]="['/w/p/', playlist.playlistShortUUID]">
58 {{ playlist.playlistDisplayName }}
59 </a>
60 </div>
55 </div> 61 </div>
56 </div> 62 </div>
57 63
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
index a397efdca..ba2adfc5a 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss
@@ -4,6 +4,10 @@
4 4
5$more-button-width: 40px; 5$more-button-width: 40px;
6 6
7.chip {
8 @include chip;
9}
10
7.video-miniature { 11.video-miniature {
8 font-size: 14px; 12 font-size: 14px;
9} 13}
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
index 534a78b3f..85c63c173 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts
@@ -11,7 +11,7 @@ import {
11 Output 11 Output
12} from '@angular/core' 12} from '@angular/core'
13import { AuthService, ScreenService, ServerService, User } from '@app/core' 13import { AuthService, ScreenService, ServerService, User } from '@app/core'
14import { HTMLServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models' 14import { HTMLServerConfig, VideoExistInPlaylist, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models'
15import { LinkType } from '../../../types/link.type' 15import { LinkType } from '../../../types/link.type'
16import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component' 16import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component'
17import { Video } from '../shared-main' 17import { Video } from '../shared-main'
@@ -21,13 +21,15 @@ import { VideoActionsDisplayType } from './video-actions-dropdown.component'
21export type MiniatureDisplayOptions = { 21export type MiniatureDisplayOptions = {
22 date?: boolean 22 date?: boolean
23 views?: boolean 23 views?: boolean
24 by?: boolean
25 avatar?: boolean 24 avatar?: boolean
26 privacyLabel?: boolean 25 privacyLabel?: boolean
27 privacyText?: boolean 26 privacyText?: boolean
28 state?: boolean 27 state?: boolean
29 blacklistInfo?: boolean 28 blacklistInfo?: boolean
30 nsfw?: boolean 29 nsfw?: boolean
30
31 by?: boolean
32 forceChannelInBy?: boolean
31} 33}
32@Component({ 34@Component({
33 selector: 'my-video-miniature', 35 selector: 'my-video-miniature',
@@ -38,6 +40,7 @@ export type MiniatureDisplayOptions = {
38export class VideoMiniatureComponent implements OnInit { 40export class VideoMiniatureComponent implements OnInit {
39 @Input() user: User 41 @Input() user: User
40 @Input() video: Video 42 @Input() video: Video
43 @Input() containedInPlaylists: VideoExistInPlaylist[]
41 44
42 @Input() displayOptions: MiniatureDisplayOptions = { 45 @Input() displayOptions: MiniatureDisplayOptions = {
43 date: true, 46 date: true,
@@ -47,7 +50,8 @@ export class VideoMiniatureComponent implements OnInit {
47 privacyLabel: false, 50 privacyLabel: false,
48 privacyText: false, 51 privacyText: false,
49 state: false, 52 state: false,
50 blacklistInfo: false 53 blacklistInfo: false,
54 forceChannelInBy: false
51 } 55 }
52 56
53 @Input() displayVideoActions = true 57 @Input() displayVideoActions = true
@@ -267,6 +271,11 @@ export class VideoMiniatureComponent implements OnInit {
267 } 271 }
268 272
269 private setUpBy () { 273 private setUpBy () {
274 if (this.displayOptions.forceChannelInBy) {
275 this.ownerDisplayType = 'videoChannel'
276 return
277 }
278
270 const accountName = this.video.account.name 279 const accountName = this.video.account.name
271 280
272 // If the video channel name is an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12) 281 // If the video channel name is an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.html b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
index 6ea2661e4..6c6db4b96 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.html
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.html
@@ -12,6 +12,7 @@
12 </div> 12 </div>
13 13
14 <my-video-miniature 14 <my-video-miniature
15 [containedInPlaylists]="videosContainedInPlaylists ? videosContainedInPlaylists[video.id] : undefined"
15 [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions" 16 [video]="video" [displayAsRow]="true" [displayOptions]="miniatureDisplayOptions"
16 [displayVideoActions]="false" [user]="user" 17 [displayVideoActions]="false" [user]="user"
17 ></my-video-miniature> 18 ></my-video-miniature>
diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
index fa3c79bbb..460a0080e 100644
--- a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
+++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts
@@ -2,7 +2,7 @@ import { Observable, Subject } from 'rxjs'
2import { AfterContentInit, Component, ContentChildren, EventEmitter, Input, Output, QueryList, TemplateRef } from '@angular/core' 2import { AfterContentInit, Component, ContentChildren, EventEmitter, Input, Output, QueryList, TemplateRef } from '@angular/core'
3import { ComponentPagination, Notifier, User } from '@app/core' 3import { ComponentPagination, Notifier, User } from '@app/core'
4import { logger } from '@root-helpers/logger' 4import { logger } from '@root-helpers/logger'
5import { ResultList, VideoSortField } from '@shared/models' 5import { ResultList, VideosExistInPlaylists, VideoSortField } from '@shared/models'
6import { PeerTubeTemplateDirective, Video } from '../shared-main' 6import { PeerTubeTemplateDirective, Video } from '../shared-main'
7import { MiniatureDisplayOptions } from './video-miniature.component' 7import { MiniatureDisplayOptions } from './video-miniature.component'
8 8
@@ -14,6 +14,7 @@ export type SelectionType = { [ id: number ]: boolean }
14 styleUrls: [ './videos-selection.component.scss' ] 14 styleUrls: [ './videos-selection.component.scss' ]
15}) 15})
16export class VideosSelectionComponent implements AfterContentInit { 16export class VideosSelectionComponent implements AfterContentInit {
17 @Input() videosContainedInPlaylists: VideosExistInPlaylists
17 @Input() user: User 18 @Input() user: User
18 @Input() pagination: ComponentPagination 19 @Input() pagination: ComponentPagination
19 20
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
index e019fdd26..2fc39fc75 100644
--- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts
@@ -3,11 +3,11 @@ import { Subject, Subscription } from 'rxjs'
3import { debounceTime, filter } from 'rxjs/operators' 3import { debounceTime, filter } from 'rxjs/operators'
4import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core' 4import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'
5import { AuthService, DisableForReuseHook, Notifier } from '@app/core' 5import { AuthService, DisableForReuseHook, Notifier } from '@app/core'
6import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 6import { FormReactive, FormReactiveService } from '@app/shared/shared-forms'
7import { secondsToTime } from '@shared/core-utils' 7import { secondsToTime } from '@shared/core-utils'
8import { 8import {
9 CachedVideoExistInPlaylist,
9 Video, 10 Video,
10 VideoExistInPlaylist,
11 VideoPlaylistCreate, 11 VideoPlaylistCreate,
12 VideoPlaylistElementCreate, 12 VideoPlaylistElementCreate,
13 VideoPlaylistElementUpdate, 13 VideoPlaylistElementUpdate,
@@ -59,7 +59,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
59 private pendingAddId: number 59 private pendingAddId: number
60 60
61 constructor ( 61 constructor (
62 protected formValidatorService: FormValidatorService, 62 protected formReactiveService: FormReactiveService,
63 private authService: AuthService, 63 private authService: AuthService,
64 private notifier: Notifier, 64 private notifier: Notifier,
65 private videoPlaylistService: VideoPlaylistService, 65 private videoPlaylistService: VideoPlaylistService,
@@ -330,7 +330,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
330 } 330 }
331 } 331 }
332 332
333 private rebuildPlaylists (existResult: VideoExistInPlaylist[]) { 333 private rebuildPlaylists (existResult: CachedVideoExistInPlaylist[]) {
334 debugLogger('Got existing results for %d.', this.video.id, existResult) 334 debugLogger('Got existing results for %d.', this.video.id, existResult)
335 335
336 const oldPlaylists = this.videoPlaylists 336 const oldPlaylists = this.videoPlaylists
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
index dd9fe0a5a..225c4eb64 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts
@@ -32,7 +32,7 @@ export class VideoPlaylistMiniatureComponent implements OnInit {
32 async ngOnInit () { 32 async ngOnInit () {
33 this.buildPlaylistUrl() 33 this.buildPlaylistUrl()
34 if (this.displayDescription) { 34 if (this.displayDescription) {
35 this.playlistDescription = await this.markdownService.textMarkdownToHTML(this.playlist.description) 35 this.playlistDescription = await this.markdownService.textMarkdownToHTML({ markdown: this.playlist.description })
36 } 36 }
37 } 37 }
38 38
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
index d71f8f72e..330a51f91 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
+++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts
@@ -8,6 +8,8 @@ import { buildBulkObservable, objectToFormData } from '@app/helpers'
8import { Account, AccountService, VideoChannel, VideoChannelService } from '@app/shared/shared-main' 8import { Account, AccountService, VideoChannel, VideoChannelService } from '@app/shared/shared-main'
9import { NGX_LOADING_BAR_IGNORED } from '@ngx-loading-bar/http-client' 9import { NGX_LOADING_BAR_IGNORED } from '@ngx-loading-bar/http-client'
10import { 10import {
11 CachedVideoExistInPlaylist,
12 CachedVideosExistInPlaylists,
11 ResultList, 13 ResultList,
12 VideoExistInPlaylist, 14 VideoExistInPlaylist,
13 VideoPlaylist as VideoPlaylistServerModel, 15 VideoPlaylist as VideoPlaylistServerModel,
@@ -34,11 +36,11 @@ export class VideoPlaylistService {
34 36
35 // Use a replay subject because we "next" a value before subscribing 37 // Use a replay subject because we "next" a value before subscribing
36 private videoExistsInPlaylistNotifier = new ReplaySubject<number>(1) 38 private videoExistsInPlaylistNotifier = new ReplaySubject<number>(1)
37 private videoExistsInPlaylistCacheSubject = new Subject<VideosExistInPlaylists>() 39 private videoExistsInPlaylistCacheSubject = new Subject<CachedVideosExistInPlaylists>()
38 private readonly videoExistsInPlaylistObservable: Observable<VideosExistInPlaylists> 40 private readonly videoExistsInPlaylistObservable: Observable<CachedVideosExistInPlaylists>
39 41
40 private videoExistsObservableCache: { [ id: number ]: Observable<VideoExistInPlaylist[]> } = {} 42 private videoExistsObservableCache: { [ id: number ]: Observable<CachedVideoExistInPlaylist[]> } = {}
41 private videoExistsCache: { [ id: number ]: VideoExistInPlaylist[] } = {} 43 private videoExistsCache: { [ id: number ]: CachedVideoExistInPlaylist[] } = {}
42 44
43 private myAccountPlaylistCache: ResultList<CachedPlaylist> = undefined 45 private myAccountPlaylistCache: ResultList<CachedPlaylist> = undefined
44 private myAccountPlaylistCacheRunning: Observable<ResultList<CachedPlaylist>> 46 private myAccountPlaylistCacheRunning: Observable<ResultList<CachedPlaylist>>
@@ -346,7 +348,7 @@ export class VideoPlaylistService {
346 ) 348 )
347 } 349 }
348 350
349 private doVideosExistInPlaylist (videoIds: number[]): Observable<VideosExistInPlaylists> { 351 doVideosExistInPlaylist (videoIds: number[]): Observable<VideosExistInPlaylists> {
350 const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' 352 const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
351 353
352 let params = new HttpParams() 354 let params = new HttpParams()