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/custom-config-validators.ts9
-rw-r--r--client/src/app/shared/form-validators/user-validators.ts4
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts2
-rw-r--r--client/src/app/shared/shared-custom-markup/button-markup.component.html1
-rw-r--r--client/src/app/shared/shared-custom-markup/button-markup.component.scss3
-rw-r--r--client/src/app/shared/shared-custom-markup/button-markup.component.ts34
-rw-r--r--client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html8
-rw-r--r--client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss9
-rw-r--r--client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts26
-rw-r--r--client/src/app/shared/shared-custom-markup/custom-markup.service.ts161
-rw-r--r--client/src/app/shared/shared-custom-markup/dynamic-element.service.ts57
-rw-r--r--client/src/app/shared/shared-custom-markup/embed-markup.component.ts22
-rw-r--r--client/src/app/shared/shared-custom-markup/index.ts3
-rw-r--r--client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html2
-rw-r--r--client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss7
-rw-r--r--client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts38
-rw-r--r--client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts52
-rw-r--r--client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html6
-rw-r--r--client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss7
-rw-r--r--client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts44
-rw-r--r--client/src/app/shared/shared-custom-markup/videos-list-markup.component.html13
-rw-r--r--client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss9
-rw-r--r--client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts60
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.html1
-rw-r--r--client/src/app/shared/shared-forms/markdown-textarea.component.ts45
-rw-r--r--client/src/app/shared/shared-icons/global-icon.component.ts1
-rw-r--r--client/src/app/shared/shared-main/angular/from-now.pipe.ts29
-rw-r--r--client/src/app/shared/shared-main/custom-page/custom-page.service.ts38
-rw-r--r--client/src/app/shared/shared-main/custom-page/index.ts1
-rw-r--r--client/src/app/shared/shared-main/index.ts3
-rw-r--r--client/src/app/shared/shared-main/plugins/plugin-placeholder.component.scss3
-rw-r--r--client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts2
-rw-r--r--client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts46
-rw-r--r--client/src/app/shared/shared-main/router/index.ts1
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts10
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts4
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts2
-rw-r--r--client/src/app/shared/shared-share-modal/video-share.component.ts4
-rw-r--r--client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts2
-rw-r--r--client/src/app/shared/shared-video-comment/video-comment.model.ts4
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.html8
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.ts2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.ts2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.ts2
46 files changed, 750 insertions, 41 deletions
diff --git a/client/src/app/shared/form-validators/custom-config-validators.ts b/client/src/app/shared/form-validators/custom-config-validators.ts
index ef6e9b456..1ed5700ff 100644
--- a/client/src/app/shared/form-validators/custom-config-validators.ts
+++ b/client/src/app/shared/form-validators/custom-config-validators.ts
@@ -49,6 +49,15 @@ export const SIGNUP_LIMIT_VALIDATOR: BuildFormValidator = {
49 } 49 }
50} 50}
51 51
52export const SIGNUP_MINIMUM_AGE_VALIDATOR: BuildFormValidator = {
53 VALIDATORS: [Validators.required, Validators.min(1), Validators.pattern('[0-9]+')],
54 MESSAGES: {
55 'required': $localize`Signup minimum age is required.`,
56 'min': $localize`Signup minimum age must be greater than 1.`,
57 'pattern': $localize`Signup minimum age must be a number.`
58 }
59}
60
52export const ADMIN_EMAIL_VALIDATOR: BuildFormValidator = { 61export const ADMIN_EMAIL_VALIDATOR: BuildFormValidator = {
53 VALIDATORS: [Validators.required, Validators.email], 62 VALIDATORS: [Validators.required, Validators.email],
54 MESSAGES: { 63 MESSAGES: {
diff --git a/client/src/app/shared/form-validators/user-validators.ts b/client/src/app/shared/form-validators/user-validators.ts
index fee37e95f..976c97b87 100644
--- a/client/src/app/shared/form-validators/user-validators.ts
+++ b/client/src/app/shared/form-validators/user-validators.ts
@@ -1,12 +1,14 @@
1import { Validators } from '@angular/forms' 1import { Validators } from '@angular/forms'
2import { BuildFormValidator } from './form-validator.model' 2import { BuildFormValidator } from './form-validator.model'
3 3
4export const USER_USERNAME_REGEX_CHARACTERS = '[a-z0-9][a-z0-9._]'
5
4export const USER_USERNAME_VALIDATOR: BuildFormValidator = { 6export const USER_USERNAME_VALIDATOR: BuildFormValidator = {
5 VALIDATORS: [ 7 VALIDATORS: [
6 Validators.required, 8 Validators.required,
7 Validators.minLength(1), 9 Validators.minLength(1),
8 Validators.maxLength(50), 10 Validators.maxLength(50),
9 Validators.pattern(/^[a-z0-9][a-z0-9._]*$/) 11 Validators.pattern(new RegExp(`^${USER_USERNAME_REGEX_CHARACTERS}*$`))
10 ], 12 ],
11 MESSAGES: { 13 MESSAGES: {
12 'required': $localize`Username is required.`, 14 'required': $localize`Username is 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 4dc2b4f10..07b9dddba 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
@@ -124,7 +124,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
124 } 124 }
125 125
126 getAccountUrl (abuse: ProcessedAbuse) { 126 getAccountUrl (abuse: ProcessedAbuse) {
127 return '/accounts/' + abuse.flaggedAccount.nameWithHost 127 return '/a/' + abuse.flaggedAccount.nameWithHost
128 } 128 }
129 129
130 getVideoEmbed (abuse: AdminAbuse) { 130 getVideoEmbed (abuse: AdminAbuse) {
diff --git a/client/src/app/shared/shared-custom-markup/button-markup.component.html b/client/src/app/shared/shared-custom-markup/button-markup.component.html
new file mode 100644
index 000000000..619bb9d8c
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/button-markup.component.html
@@ -0,0 +1 @@
<a [href]="href" [ngClass]="getClasses()" [target]="getTarget()">{{ label }}</a>
diff --git a/client/src/app/shared/shared-custom-markup/button-markup.component.scss b/client/src/app/shared/shared-custom-markup/button-markup.component.scss
new file mode 100644
index 000000000..f43d6b400
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/button-markup.component.scss
@@ -0,0 +1,3 @@
1@import '_variables';
2@import '_mixins';
3
diff --git a/client/src/app/shared/shared-custom-markup/button-markup.component.ts b/client/src/app/shared/shared-custom-markup/button-markup.component.ts
new file mode 100644
index 000000000..c0aab2edd
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/button-markup.component.ts
@@ -0,0 +1,34 @@
1import { Component, Input } from '@angular/core'
2import { VideoChannel } from '../shared-main'
3
4/*
5 * Markup component that creates a button
6*/
7
8@Component({
9 selector: 'my-button-markup',
10 templateUrl: 'button-markup.component.html',
11 styleUrls: [ 'button-markup.component.scss' ]
12})
13export class ButtonMarkupComponent {
14 @Input() theme: 'primary' | 'secondary'
15 @Input() href: string
16 @Input() label: string
17 @Input() blankTarget?: boolean
18
19 channel: VideoChannel
20
21 getTarget () {
22 if (this.blankTarget === true) return '_blank'
23
24 return ''
25 }
26
27 getClasses () {
28 const additionalClass = this.theme === 'primary'
29 ? 'orange-button'
30 : 'grey-button'
31
32 return [ 'peertube-button-link', additionalClass ]
33 }
34}
diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html
new file mode 100644
index 000000000..da81006b9
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.html
@@ -0,0 +1,8 @@
1<div *ngIf="channel" class="channel">
2 <my-actor-avatar [channel]="channel" size="34"></my-actor-avatar>
3
4 <div class="display-name">{{ channel.displayName }}</div>
5 <div class="username">{{ channel.name }}</div>
6
7 <div class="description">{{ channel.description }}</div>
8</div>
diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss
new file mode 100644
index 000000000..85018afe2
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.scss
@@ -0,0 +1,9 @@
1@import '_variables';
2@import '_mixins';
3
4.channel {
5 border-radius: 15px;
6 padding: 10px;
7 width: min-content;
8 border: 1px solid pvar(--mainColor);
9}
diff --git a/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts
new file mode 100644
index 000000000..97bb5567e
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/channel-miniature-markup.component.ts
@@ -0,0 +1,26 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { VideoChannel, VideoChannelService } from '../shared-main'
3
4/*
5 * Markup component that creates a channel miniature only
6*/
7
8@Component({
9 selector: 'my-channel-miniature-markup',
10 templateUrl: 'channel-miniature-markup.component.html',
11 styleUrls: [ 'channel-miniature-markup.component.scss' ]
12})
13export class ChannelMiniatureMarkupComponent implements OnInit {
14 @Input() name: string
15
16 channel: VideoChannel
17
18 constructor (
19 private channelService: VideoChannelService
20 ) { }
21
22 ngOnInit () {
23 this.channelService.getVideoChannel(this.name)
24 .subscribe(channel => this.channel = channel)
25 }
26}
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
new file mode 100644
index 000000000..09414da95
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
@@ -0,0 +1,161 @@
1import { ComponentRef, Injectable } from '@angular/core'
2import { MarkdownService } from '@app/core'
3import {
4 ButtonMarkupData,
5 ChannelMiniatureMarkupData,
6 EmbedMarkupData,
7 PlaylistMiniatureMarkupData,
8 VideoMiniatureMarkupData,
9 VideosListMarkupData
10} from '@shared/models'
11import { ButtonMarkupComponent } from './button-markup.component'
12import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
13import { DynamicElementService } from './dynamic-element.service'
14import { EmbedMarkupComponent } from './embed-markup.component'
15import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
16import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
17import { VideosListMarkupComponent } from './videos-list-markup.component'
18
19type BuilderFunction = (el: HTMLElement) => ComponentRef<any>
20
21@Injectable()
22export class CustomMarkupService {
23 private builders: { [ selector: string ]: BuilderFunction } = {
24 'peertube-button': el => this.buttonBuilder(el),
25 'peertube-video-embed': el => this.embedBuilder(el, 'video'),
26 'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
27 'peertube-video-miniature': el => this.videoMiniatureBuilder(el),
28 'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el),
29 'peertube-channel-miniature': el => this.channelMiniatureBuilder(el),
30 'peertube-videos-list': el => this.videosListBuilder(el)
31 }
32
33 constructor (
34 private dynamicElementService: DynamicElementService,
35 private markdown: MarkdownService
36 ) { }
37
38 async buildElement (text: string) {
39 const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags())
40
41 const rootElement = document.createElement('div')
42 rootElement.innerHTML = html
43
44 for (const selector of this.getSupportedTags()) {
45 rootElement.querySelectorAll(selector)
46 .forEach((e: HTMLElement) => {
47 try {
48 const component = this.execBuilder(selector, e)
49
50 this.dynamicElementService.injectElement(e, component)
51 } catch (err) {
52 console.error('Cannot inject component %s.', selector, err)
53 }
54 })
55 }
56
57 return rootElement
58 }
59
60 private getSupportedTags () {
61 return Object.keys(this.builders)
62 }
63
64 private execBuilder (selector: string, el: HTMLElement) {
65 return this.builders[selector](el)
66 }
67
68 private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') {
69 const data = el.dataset as EmbedMarkupData
70 const component = this.dynamicElementService.createElement(EmbedMarkupComponent)
71
72 this.dynamicElementService.setModel(component, { uuid: data.uuid, type })
73
74 return component
75 }
76
77 private videoMiniatureBuilder (el: HTMLElement) {
78 const data = el.dataset as VideoMiniatureMarkupData
79 const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
80
81 this.dynamicElementService.setModel(component, { uuid: data.uuid })
82
83 return component
84 }
85
86 private playlistMiniatureBuilder (el: HTMLElement) {
87 const data = el.dataset as PlaylistMiniatureMarkupData
88 const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
89
90 this.dynamicElementService.setModel(component, { uuid: data.uuid })
91
92 return component
93 }
94
95 private channelMiniatureBuilder (el: HTMLElement) {
96 const data = el.dataset as ChannelMiniatureMarkupData
97 const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
98
99 this.dynamicElementService.setModel(component, { name: data.name })
100
101 return component
102 }
103
104 private buttonBuilder (el: HTMLElement) {
105 const data = el.dataset as ButtonMarkupData
106 const component = this.dynamicElementService.createElement(ButtonMarkupComponent)
107
108 const model = {
109 theme: data.theme,
110 href: data.href,
111 label: data.label,
112 blankTarget: this.buildBoolean(data.blankTarget)
113 }
114 this.dynamicElementService.setModel(component, model)
115
116 return component
117 }
118
119 private videosListBuilder (el: HTMLElement) {
120 const data = el.dataset as VideosListMarkupData
121 const component = this.dynamicElementService.createElement(VideosListMarkupComponent)
122
123 const model = {
124 title: data.title,
125 description: data.description,
126 sort: data.sort,
127 categoryOneOf: this.buildArrayNumber(data.categoryOneOf),
128 languageOneOf: this.buildArrayString(data.languageOneOf),
129 count: this.buildNumber(data.count) || 10
130 }
131
132 this.dynamicElementService.setModel(component, model)
133
134 return component
135 }
136
137 private buildNumber (value: string) {
138 if (!value) return undefined
139
140 return parseInt(value, 10)
141 }
142
143 private buildBoolean (value: string) {
144 if (value === 'true') return true
145 if (value === 'false') return false
146
147 return undefined
148 }
149
150 private buildArrayNumber (value: string) {
151 if (!value) return undefined
152
153 return value.split(',').map(v => parseInt(v, 10))
154 }
155
156 private buildArrayString (value: string) {
157 if (!value) return undefined
158
159 return value.split(',')
160 }
161}
diff --git a/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts b/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts
new file mode 100644
index 000000000..e967e30ac
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/dynamic-element.service.ts
@@ -0,0 +1,57 @@
1import {
2 ApplicationRef,
3 ComponentFactoryResolver,
4 ComponentRef,
5 EmbeddedViewRef,
6 Injectable,
7 Injector,
8 OnChanges,
9 SimpleChange,
10 SimpleChanges,
11 Type
12} from '@angular/core'
13
14@Injectable()
15export class DynamicElementService {
16
17 constructor (
18 private injector: Injector,
19 private applicationRef: ApplicationRef,
20 private componentFactoryResolver: ComponentFactoryResolver
21 ) { }
22
23 createElement <T> (ofComponent: Type<T>) {
24 const div = document.createElement('div')
25
26 const component = this.componentFactoryResolver.resolveComponentFactory(ofComponent)
27 .create(this.injector, [], div)
28
29 return component
30 }
31
32 injectElement <T> (wrapper: HTMLElement, componentRef: ComponentRef<T>) {
33 const hostView = componentRef.hostView as EmbeddedViewRef<any>
34
35 this.applicationRef.attachView(hostView)
36 wrapper.appendChild(hostView.rootNodes[0])
37 }
38
39 setModel <T> (componentRef: ComponentRef<T>, attributes: Partial<T>) {
40 const changes: SimpleChanges = {}
41
42 for (const key of Object.keys(attributes)) {
43 const previousValue = componentRef.instance[key]
44 const newValue = attributes[key]
45
46 componentRef.instance[key] = newValue
47 changes[key] = new SimpleChange(previousValue, newValue, previousValue === undefined)
48 }
49
50 const component = componentRef.instance
51 if (typeof (component as unknown as OnChanges).ngOnChanges === 'function') {
52 (component as unknown as OnChanges).ngOnChanges(changes)
53 }
54
55 componentRef.changeDetectorRef.detectChanges()
56 }
57}
diff --git a/client/src/app/shared/shared-custom-markup/embed-markup.component.ts b/client/src/app/shared/shared-custom-markup/embed-markup.component.ts
new file mode 100644
index 000000000..a854d89f6
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/embed-markup.component.ts
@@ -0,0 +1,22 @@
1import { buildPlaylistLink, buildVideoLink, buildVideoOrPlaylistEmbed } from 'src/assets/player/utils'
2import { environment } from 'src/environments/environment'
3import { Component, ElementRef, Input, OnInit } from '@angular/core'
4
5@Component({
6 selector: 'my-embed-markup',
7 template: ''
8})
9export class EmbedMarkupComponent implements OnInit {
10 @Input() uuid: string
11 @Input() type: 'video' | 'playlist' = 'video'
12
13 constructor (private el: ElementRef) { }
14
15 ngOnInit () {
16 const link = this.type === 'video'
17 ? buildVideoLink({ baseUrl: `${environment.originServerUrl}/videos/embed/${this.uuid}` })
18 : buildPlaylistLink({ baseUrl: `${environment.originServerUrl}/video-playlists/embed/${this.uuid}` })
19
20 this.el.nativeElement.innerHTML = buildVideoOrPlaylistEmbed(link, this.uuid)
21 }
22}
diff --git a/client/src/app/shared/shared-custom-markup/index.ts b/client/src/app/shared/shared-custom-markup/index.ts
new file mode 100644
index 000000000..14bde3ea9
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/index.ts
@@ -0,0 +1,3 @@
1export * from './custom-markup.service'
2export * from './dynamic-element.service'
3export * from './shared-custom-markup.module'
diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html
new file mode 100644
index 000000000..4e1d1a13f
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.html
@@ -0,0 +1,2 @@
1<my-video-playlist-miniature *ngIf="playlist" [playlist]="playlist">
2</my-video-playlist-miniature>
diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss
new file mode 100644
index 000000000..281cef726
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4my-video-playlist-miniature {
5 display: inline-block;
6 width: $video-thumbnail-width;
7}
diff --git a/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts
new file mode 100644
index 000000000..7aee450f1
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/playlist-miniature-markup.component.ts
@@ -0,0 +1,38 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { MiniatureDisplayOptions } from '../shared-video-miniature'
3import { VideoPlaylist, VideoPlaylistService } from '../shared-video-playlist'
4
5/*
6 * Markup component that creates a playlist miniature only
7*/
8
9@Component({
10 selector: 'my-playlist-miniature-markup',
11 templateUrl: 'playlist-miniature-markup.component.html',
12 styleUrls: [ 'playlist-miniature-markup.component.scss' ]
13})
14export class PlaylistMiniatureMarkupComponent implements OnInit {
15 @Input() uuid: string
16
17 playlist: VideoPlaylist
18
19 displayOptions: MiniatureDisplayOptions = {
20 date: true,
21 views: true,
22 by: true,
23 avatar: false,
24 privacyLabel: false,
25 privacyText: false,
26 state: false,
27 blacklistInfo: false
28 }
29
30 constructor (
31 private playlistService: VideoPlaylistService
32 ) { }
33
34 ngOnInit () {
35 this.playlistService.getVideoPlaylist(this.uuid)
36 .subscribe(playlist => this.playlist = playlist)
37 }
38}
diff --git a/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts
new file mode 100644
index 000000000..d03aa856f
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts
@@ -0,0 +1,52 @@
1
2import { CommonModule } from '@angular/common'
3import { NgModule } from '@angular/core'
4import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module'
5import { SharedGlobalIconModule } from '../shared-icons'
6import { SharedMainModule } from '../shared-main'
7import { SharedVideoMiniatureModule } from '../shared-video-miniature'
8import { SharedVideoPlaylistModule } from '../shared-video-playlist'
9import { ButtonMarkupComponent } from './button-markup.component'
10import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
11import { CustomMarkupService } from './custom-markup.service'
12import { DynamicElementService } from './dynamic-element.service'
13import { EmbedMarkupComponent } from './embed-markup.component'
14import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
15import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
16import { VideosListMarkupComponent } from './videos-list-markup.component'
17
18@NgModule({
19 imports: [
20 CommonModule,
21
22 SharedMainModule,
23 SharedGlobalIconModule,
24 SharedVideoMiniatureModule,
25 SharedVideoPlaylistModule,
26 SharedActorImageModule
27 ],
28
29 declarations: [
30 VideoMiniatureMarkupComponent,
31 PlaylistMiniatureMarkupComponent,
32 ChannelMiniatureMarkupComponent,
33 EmbedMarkupComponent,
34 VideosListMarkupComponent,
35 ButtonMarkupComponent
36 ],
37
38 exports: [
39 VideoMiniatureMarkupComponent,
40 PlaylistMiniatureMarkupComponent,
41 ChannelMiniatureMarkupComponent,
42 VideosListMarkupComponent,
43 EmbedMarkupComponent,
44 ButtonMarkupComponent
45 ],
46
47 providers: [
48 CustomMarkupService,
49 DynamicElementService
50 ]
51})
52export class SharedCustomMarkupModule { }
diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html
new file mode 100644
index 000000000..9b4930b6d
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.html
@@ -0,0 +1,6 @@
1<my-video-miniature
2 *ngIf="video"
3 [video]="video" [user]="getUser()" [displayAsRow]="false"
4 [displayVideoActions]="false" [displayOptions]="displayOptions"
5>
6</my-video-miniature>
diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss
new file mode 100644
index 000000000..81e265f29
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.scss
@@ -0,0 +1,7 @@
1@import '_variables';
2@import '_mixins';
3
4my-video-miniature {
5 display: inline-block;
6 width: $video-thumbnail-width;
7}
diff --git a/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts
new file mode 100644
index 000000000..79add0c3b
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/video-miniature-markup.component.ts
@@ -0,0 +1,44 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { AuthService } from '@app/core'
3import { Video, VideoService } from '../shared-main'
4import { MiniatureDisplayOptions } from '../shared-video-miniature'
5
6/*
7 * Markup component that creates a video miniature only
8*/
9
10@Component({
11 selector: 'my-video-miniature-markup',
12 templateUrl: 'video-miniature-markup.component.html',
13 styleUrls: [ 'video-miniature-markup.component.scss' ]
14})
15export class VideoMiniatureMarkupComponent implements OnInit {
16 @Input() uuid: string
17
18 video: Video
19
20 displayOptions: MiniatureDisplayOptions = {
21 date: true,
22 views: true,
23 by: true,
24 avatar: false,
25 privacyLabel: false,
26 privacyText: false,
27 state: false,
28 blacklistInfo: false
29 }
30
31 constructor (
32 private auth: AuthService,
33 private videoService: VideoService
34 ) { }
35
36 getUser () {
37 return this.auth.getUser()
38 }
39
40 ngOnInit () {
41 this.videoService.getVideo({ videoId: this.uuid })
42 .subscribe(video => this.video = video)
43 }
44}
diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html
new file mode 100644
index 000000000..501f35e04
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.html
@@ -0,0 +1,13 @@
1<div class="root">
2 <h4 *ngIf="title">{{ title }}</h4>
3 <div *ngIf="description" class="description">{{ description }}</div>
4
5 <div class="videos">
6 <my-video-miniature
7 *ngFor="let video of videos"
8 [video]="video" [user]="getUser()" [displayAsRow]="false"
9 [displayVideoActions]="false" [displayOptions]="displayOptions"
10 >
11 </my-video-miniature>
12 </div>
13</div>
diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss
new file mode 100644
index 000000000..dcd931090
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.scss
@@ -0,0 +1,9 @@
1@import '_variables';
2@import '_mixins';
3
4my-video-miniature {
5 margin-right: 15px;
6 display: inline-block;
7 min-width: $video-thumbnail-width;
8 max-width: $video-thumbnail-width;
9}
diff --git a/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts
new file mode 100644
index 000000000..cc25d0a51
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/videos-list-markup.component.ts
@@ -0,0 +1,60 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { AuthService } from '@app/core'
3import { VideoSortField } from '@shared/models'
4import { Video, VideoService } from '../shared-main'
5import { MiniatureDisplayOptions } from '../shared-video-miniature'
6
7/*
8 * Markup component list videos depending on criterias
9*/
10
11@Component({
12 selector: 'my-videos-list-markup',
13 templateUrl: 'videos-list-markup.component.html',
14 styleUrls: [ 'videos-list-markup.component.scss' ]
15})
16export class VideosListMarkupComponent implements OnInit {
17 @Input() title: string
18 @Input() description: string
19 @Input() sort = '-publishedAt'
20 @Input() categoryOneOf: number[]
21 @Input() languageOneOf: string[]
22 @Input() count = 10
23
24 videos: Video[]
25
26 displayOptions: MiniatureDisplayOptions = {
27 date: true,
28 views: true,
29 by: true,
30 avatar: false,
31 privacyLabel: false,
32 privacyText: false,
33 state: false,
34 blacklistInfo: false
35 }
36
37 constructor (
38 private auth: AuthService,
39 private videoService: VideoService
40 ) { }
41
42 getUser () {
43 return this.auth.getUser()
44 }
45
46 ngOnInit () {
47 const options = {
48 videoPagination: {
49 currentPage: 1,
50 itemsPerPage: this.count
51 },
52 categoryOneOf: this.categoryOneOf,
53 languageOneOf: this.languageOneOf,
54 sort: this.sort as VideoSortField
55 }
56
57 this.videoService.getVideos(options)
58 .subscribe(({ data }) => this.videos = data)
59 }
60}
diff --git a/client/src/app/shared/shared-forms/markdown-textarea.component.html b/client/src/app/shared/shared-forms/markdown-textarea.component.html
index 513b543cd..6e70e2f37 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.html
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.html
@@ -19,6 +19,7 @@
19 <a ngbNavLink i18n>Complete preview</a> 19 <a ngbNavLink i18n>Complete preview</a>
20 20
21 <ng-template ngbNavContent> 21 <ng-template ngbNavContent>
22 <div #previewElement></div>
22 <div [innerHTML]="previewHTML"></div> 23 <div [innerHTML]="previewHTML"></div>
23 </ng-template> 24 </ng-template>
24 </ng-container> 25 </ng-container>
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 9b3ab9cf3..a233a4205 100644
--- a/client/src/app/shared/shared-forms/markdown-textarea.component.ts
+++ b/client/src/app/shared/shared-forms/markdown-textarea.component.ts
@@ -1,9 +1,10 @@
1import { ViewportScroller } from '@angular/common'
2import truncate from 'lodash-es/truncate' 1import truncate from 'lodash-es/truncate'
3import { Subject } from 'rxjs' 2import { Subject } from 'rxjs'
4import { debounceTime, distinctUntilChanged } from 'rxjs/operators' 3import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
4import { ViewportScroller } from '@angular/common'
5import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core' 5import { Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'
6import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' 6import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
7import { SafeHtml } from '@angular/platform-browser'
7import { MarkdownService, ScreenService } from '@app/core' 8import { MarkdownService, ScreenService } from '@app/core'
8 9
9@Component({ 10@Component({
@@ -21,18 +22,27 @@ import { MarkdownService, ScreenService } from '@app/core'
21 22
22export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit { 23export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
23 @Input() content = '' 24 @Input() content = ''
25
24 @Input() classes: string[] | { [klass: string]: any[] | any } = [] 26 @Input() classes: string[] | { [klass: string]: any[] | any } = []
27
25 @Input() textareaMaxWidth = '100%' 28 @Input() textareaMaxWidth = '100%'
26 @Input() textareaHeight = '150px' 29 @Input() textareaHeight = '150px'
30
27 @Input() truncate: number 31 @Input() truncate: number
32
28 @Input() markdownType: 'text' | 'enhanced' = 'text' 33 @Input() markdownType: 'text' | 'enhanced' = 'text'
34 @Input() customMarkdownRenderer?: (text: string) => Promise<string | HTMLElement>
35
29 @Input() markdownVideo = false 36 @Input() markdownVideo = false
37
30 @Input() name = 'description' 38 @Input() name = 'description'
31 39
32 @ViewChild('textarea') textareaElement: ElementRef 40 @ViewChild('textarea') textareaElement: ElementRef
41 @ViewChild('previewElement') previewElement: ElementRef
42
43 truncatedPreviewHTML: SafeHtml | string = ''
44 previewHTML: SafeHtml | string = ''
33 45
34 truncatedPreviewHTML = ''
35 previewHTML = ''
36 isMaximized = false 46 isMaximized = false
37 47
38 maximizeInText = $localize`Maximize editor` 48 maximizeInText = $localize`Maximize editor`
@@ -115,10 +125,31 @@ export class MarkdownTextareaComponent implements ControlValueAccessor, OnInit {
115 } 125 }
116 126
117 private async markdownRender (text: string) { 127 private async markdownRender (text: string) {
118 const html = this.markdownType === 'text' ? 128 let html: string
119 await this.markdownService.textMarkdownToHTML(text) : 129
120 await this.markdownService.enhancedMarkdownToHTML(text) 130 if (this.customMarkdownRenderer) {
131 const result = await this.customMarkdownRenderer(text)
132
133 if (result instanceof HTMLElement) {
134 html = ''
135
136 const wrapperElement = this.previewElement.nativeElement as HTMLElement
137 wrapperElement.innerHTML = ''
138 wrapperElement.appendChild(result)
139 return
140 }
141
142 html = result
143 } else if (this.markdownType === 'text') {
144 html = await this.markdownService.textMarkdownToHTML(text)
145 } else {
146 html = await this.markdownService.enhancedMarkdownToHTML(text)
147 }
148
149 if (this.markdownVideo) {
150 html = this.markdownService.processVideoTimestamps(html)
151 }
121 152
122 return this.markdownVideo ? this.markdownService.processVideoTimestamps(html) : html 153 return html
123 } 154 }
124} 155}
diff --git a/client/src/app/shared/shared-icons/global-icon.component.ts b/client/src/app/shared/shared-icons/global-icon.component.ts
index 3af517927..a47f07fc3 100644
--- a/client/src/app/shared/shared-icons/global-icon.component.ts
+++ b/client/src/app/shared/shared-icons/global-icon.component.ts
@@ -17,6 +17,7 @@ const icons = {
17 'follower': require('!!raw-loader?!../../../assets/images/misc/account-arrow-left.svg').default, // material ui 17 'follower': require('!!raw-loader?!../../../assets/images/misc/account-arrow-left.svg').default, // material ui
18 'following': require('!!raw-loader?!../../../assets/images/misc/account-arrow-right.svg').default, // material ui 18 'following': require('!!raw-loader?!../../../assets/images/misc/account-arrow-right.svg').default, // material ui
19 'flame': require('!!raw-loader?!../../../assets/images/misc/flame.svg').default, 19 'flame': require('!!raw-loader?!../../../assets/images/misc/flame.svg').default,
20 'local': require('!!raw-loader?!../../../assets/images/misc/local.svg').default,
20 21
21 // feather icons 22 // feather icons
22 'flag': require('!!raw-loader?!../../../assets/images/feather/flag.svg').default, 23 'flag': require('!!raw-loader?!../../../assets/images/feather/flag.svg').default,
diff --git a/client/src/app/shared/shared-main/angular/from-now.pipe.ts b/client/src/app/shared/shared-main/angular/from-now.pipe.ts
index 5e7832807..d62c1f88e 100644
--- a/client/src/app/shared/shared-main/angular/from-now.pipe.ts
+++ b/client/src/app/shared/shared-main/angular/from-now.pipe.ts
@@ -3,32 +3,37 @@ import { Pipe, PipeTransform } from '@angular/core'
3// Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site 3// Thanks: https://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site
4@Pipe({ name: 'myFromNow' }) 4@Pipe({ name: 'myFromNow' })
5export class FromNowPipe implements PipeTransform { 5export class FromNowPipe implements PipeTransform {
6
7 transform (arg: number | Date | string) { 6 transform (arg: number | Date | string) {
8 const argDate = new Date(arg) 7 const argDate = new Date(arg)
9 const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000) 8 const seconds = Math.floor((Date.now() - argDate.getTime()) / 1000)
10 9
11 let interval = Math.round(seconds / 31536000) 10 let interval = Math.floor(seconds / 31536000)
12 if (interval > 1) return $localize`${interval} years ago` 11 if (interval > 1) return $localize`${interval} years ago`
13 if (interval === 1) return $localize`${interval} year ago` 12 if (interval === 1) return $localize`1 year ago`
14 13
15 interval = Math.round(seconds / 2592000) 14 interval = Math.floor(seconds / 2419200)
15 // 12 months = 360 days, but a year ~ 365 days
16 // Display "1 year ago" rather than "12 months ago"
17 if (interval >= 12) return $localize`1 year ago`
16 if (interval > 1) return $localize`${interval} months ago` 18 if (interval > 1) return $localize`${interval} months ago`
17 if (interval === 1) return $localize`${interval} month ago` 19 if (interval === 1) return $localize`1 month ago`
18 20
19 interval = Math.round(seconds / 604800) 21 interval = Math.floor(seconds / 604800)
22 // 4 weeks ~ 28 days, but our month is 30 days
23 // Display "1 month ago" rather than "4 weeks ago"
24 if (interval >= 4) return $localize`1 month ago`
20 if (interval > 1) return $localize`${interval} weeks ago` 25 if (interval > 1) return $localize`${interval} weeks ago`
21 if (interval === 1) return $localize`${interval} week ago` 26 if (interval === 1) return $localize`1 week ago`
22 27
23 interval = Math.round(seconds / 86400) 28 interval = Math.floor(seconds / 86400)
24 if (interval > 1) return $localize`${interval} days ago` 29 if (interval > 1) return $localize`${interval} days ago`
25 if (interval === 1) return $localize`${interval} day ago` 30 if (interval === 1) return $localize`1 day ago`
26 31
27 interval = Math.round(seconds / 3600) 32 interval = Math.floor(seconds / 3600)
28 if (interval > 1) return $localize`${interval} hours ago` 33 if (interval > 1) return $localize`${interval} hours ago`
29 if (interval === 1) return $localize`${interval} hour ago` 34 if (interval === 1) return $localize`1 hour ago`
30 35
31 interval = Math.round(seconds / 60) 36 interval = Math.floor(seconds / 60)
32 if (interval >= 1) return $localize`${interval} min ago` 37 if (interval >= 1) return $localize`${interval} min ago`
33 38
34 return $localize`just now` 39 return $localize`just now`
diff --git a/client/src/app/shared/shared-main/custom-page/custom-page.service.ts b/client/src/app/shared/shared-main/custom-page/custom-page.service.ts
new file mode 100644
index 000000000..e5c2b3cd4
--- /dev/null
+++ b/client/src/app/shared/shared-main/custom-page/custom-page.service.ts
@@ -0,0 +1,38 @@
1import { of } from 'rxjs'
2import { catchError, map } from 'rxjs/operators'
3import { HttpClient } from '@angular/common/http'
4import { Injectable } from '@angular/core'
5import { RestExtractor } from '@app/core'
6import { CustomPage } from '@shared/models'
7import { environment } from '../../../../environments/environment'
8
9@Injectable()
10export class CustomPageService {
11 static BASE_INSTANCE_HOMEPAGE_URL = environment.apiUrl + '/api/v1/custom-pages/homepage/instance'
12
13 constructor (
14 private authHttp: HttpClient,
15 private restExtractor: RestExtractor
16 ) { }
17
18 getInstanceHomepage () {
19 return this.authHttp.get<CustomPage>(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL)
20 .pipe(
21 catchError(err => {
22 if (err.status === 404) {
23 return of({ content: '' })
24 }
25
26 this.restExtractor.handleError(err)
27 })
28 )
29 }
30
31 updateInstanceHomepage (content: string) {
32 return this.authHttp.put(CustomPageService.BASE_INSTANCE_HOMEPAGE_URL, { content })
33 .pipe(
34 map(this.restExtractor.extractDataBool),
35 catchError(err => this.restExtractor.handleError(err))
36 )
37 }
38}
diff --git a/client/src/app/shared/shared-main/custom-page/index.ts b/client/src/app/shared/shared-main/custom-page/index.ts
new file mode 100644
index 000000000..7269ece95
--- /dev/null
+++ b/client/src/app/shared/shared-main/custom-page/index.ts
@@ -0,0 +1 @@
export * from './custom-page.service'
diff --git a/client/src/app/shared/shared-main/index.ts b/client/src/app/shared/shared-main/index.ts
index a4d813c06..3a7fd4c34 100644
--- a/client/src/app/shared/shared-main/index.ts
+++ b/client/src/app/shared/shared-main/index.ts
@@ -5,6 +5,9 @@ export * from './date'
5export * from './feeds' 5export * from './feeds'
6export * from './loaders' 6export * from './loaders'
7export * from './misc' 7export * from './misc'
8export * from './peertube-modal'
9export * from './plugins'
10export * from './router'
8export * from './users' 11export * from './users'
9export * from './video' 12export * from './video'
10export * from './video-caption' 13export * from './video-caption'
diff --git a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.scss b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.scss
new file mode 100644
index 000000000..4e37c5e61
--- /dev/null
+++ b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.scss
@@ -0,0 +1,3 @@
1div {
2 height: 100%;
3}
diff --git a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts
index 4d5381e8d..858eff9ba 100644
--- a/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts
+++ b/client/src/app/shared/shared-main/plugins/plugin-placeholder.component.ts
@@ -4,7 +4,7 @@ import { PluginElementPlaceholder } from '@shared/models'
4@Component({ 4@Component({
5 selector: 'my-plugin-placeholder', 5 selector: 'my-plugin-placeholder',
6 template: '<div [id]="getId()"></div>', 6 template: '<div [id]="getId()"></div>',
7 styles: [ 'div { height: 100%; }' ] 7 styleUrls: [ './plugin-placeholder.component.scss' ]
8}) 8})
9 9
10export class PluginPlaceholderComponent { 10export class PluginPlaceholderComponent {
diff --git a/client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts b/client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts
new file mode 100644
index 000000000..49d61f945
--- /dev/null
+++ b/client/src/app/shared/shared-main/router/actor-redirect-guard.service.ts
@@ -0,0 +1,46 @@
1import { forkJoin, of } from 'rxjs'
2import { catchError, map } from 'rxjs/operators'
3import { Injectable } from '@angular/core'
4import { ActivatedRouteSnapshot, CanActivate, Router } from '@angular/router'
5import { AccountService } from '../account'
6import { VideoChannelService } from '../video-channel'
7
8@Injectable()
9export class ActorRedirectGuard implements CanActivate {
10
11 constructor (
12 private router: Router,
13 private accountService: AccountService,
14 private channelService: VideoChannelService
15 ) {}
16
17 canActivate (route: ActivatedRouteSnapshot) {
18 const actorName = route.params.actorName
19
20 return forkJoin([
21 this.accountService.getAccount(actorName).pipe(this.orUndefined()),
22 this.channelService.getVideoChannel(actorName).pipe(this.orUndefined())
23 ]).pipe(
24 map(([ account, channel ]) => {
25 if (!account && !channel) {
26 this.router.navigate([ '/404' ])
27 return false
28 }
29
30 if (account) {
31 this.router.navigate([ `/a/${actorName}` ], { skipLocationChange: true })
32 }
33
34 if (channel) {
35 this.router.navigate([ `/c/${actorName}` ], { skipLocationChange: true })
36 }
37
38 return true
39 })
40 )
41 }
42
43 private orUndefined () {
44 return catchError(() => of(undefined))
45 }
46}
diff --git a/client/src/app/shared/shared-main/router/index.ts b/client/src/app/shared/shared-main/router/index.ts
new file mode 100644
index 000000000..f4000b674
--- /dev/null
+++ b/client/src/app/shared/shared-main/router/index.ts
@@ -0,0 +1 @@
export * from './actor-redirect-guard.service'
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 772198cb2..c8dd01429 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -4,7 +4,7 @@ import { CommonModule, DatePipe } from '@angular/common'
4import { HttpClientModule } from '@angular/common/http' 4import { HttpClientModule } from '@angular/common/http'
5import { NgModule } from '@angular/core' 5import { NgModule } from '@angular/core'
6import { FormsModule, ReactiveFormsModule } from '@angular/forms' 6import { FormsModule, ReactiveFormsModule } from '@angular/forms'
7import { RouterModule } from '@angular/router' 7import { ActivatedRouteSnapshot, RouterModule } from '@angular/router'
8import { 8import {
9 NgbButtonsModule, 9 NgbButtonsModule,
10 NgbCollapseModule, 10 NgbCollapseModule,
@@ -29,6 +29,7 @@ import {
29} from './angular' 29} from './angular'
30import { AUTH_INTERCEPTOR_PROVIDER } from './auth' 30import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
31import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons' 31import { ActionDropdownComponent, ButtonComponent, DeleteButtonComponent, EditButtonComponent } from './buttons'
32import { CustomPageService } from './custom-page'
32import { DateToggleComponent } from './date' 33import { DateToggleComponent } from './date'
33import { FeedComponent } from './feeds' 34import { FeedComponent } from './feeds'
34import { LoaderComponent, SmallLoaderComponent } from './loaders' 35import { LoaderComponent, SmallLoaderComponent } from './loaders'
@@ -38,6 +39,7 @@ import { UserHistoryService, UserNotificationsComponent, UserNotificationService
38import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video' 39import { RedundancyService, VideoImportService, VideoOwnershipService, VideoService } from './video'
39import { VideoCaptionService } from './video-caption' 40import { VideoCaptionService } from './video-caption'
40import { VideoChannelService } from './video-channel' 41import { VideoChannelService } from './video-channel'
42import { ActorRedirectGuard } from './router'
41 43
42@NgModule({ 44@NgModule({
43 imports: [ 45 imports: [
@@ -171,7 +173,11 @@ import { VideoChannelService } from './video-channel'
171 173
172 VideoCaptionService, 174 VideoCaptionService,
173 175
174 VideoChannelService 176 VideoChannelService,
177
178 CustomPageService,
179
180 ActorRedirectGuard
175 ] 181 ]
176}) 182})
177export class SharedMainModule { } 183export class SharedMainModule { }
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index ed5791794..c80bc13b0 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -238,11 +238,11 @@ export class UserNotification implements UserNotificationServer {
238 } 238 }
239 239
240 private buildVideoUrl (video: { uuid: string }) { 240 private buildVideoUrl (video: { uuid: string }) {
241 return '/videos/watch/' + video.uuid 241 return '/w/' + video.uuid
242 } 242 }
243 243
244 private buildAccountUrl (account: { name: string, host: string }) { 244 private buildAccountUrl (account: { name: string, host: string }) {
245 return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host) 245 return '/a/' + Actor.CREATE_BY_STRING(account.name, account.host)
246 } 246 }
247 247
248 private buildVideoImportUrl () { 248 private buildVideoImportUrl () {
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 526d10e32..e7f739bfe 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -88,7 +88,7 @@ export class Video implements VideoServerModel {
88 pluginData?: any 88 pluginData?: any
89 89
90 static buildClientUrl (videoUUID: string) { 90 static buildClientUrl (videoUUID: string) {
91 return '/videos/watch/' + videoUUID 91 return '/w/' + videoUUID
92 } 92 }
93 93
94 constructor (hash: VideoServerModel, translations = {}) { 94 constructor (hash: VideoServerModel, translations = {}) {
diff --git a/client/src/app/shared/shared-share-modal/video-share.component.ts b/client/src/app/shared/shared-share-modal/video-share.component.ts
index e8760bfcc..2a73e6166 100644
--- a/client/src/app/shared/shared-share-modal/video-share.component.ts
+++ b/client/src/app/shared/shared-share-modal/video-share.component.ts
@@ -98,14 +98,14 @@ export class VideoShareComponent {
98 98
99 getVideoUrl () { 99 getVideoUrl () {
100 let baseUrl = this.customizations.originUrl ? this.video.originInstanceUrl : window.location.origin 100 let baseUrl = this.customizations.originUrl ? this.video.originInstanceUrl : window.location.origin
101 baseUrl += '/videos/watch/' + this.video.uuid 101 baseUrl += '/w/' + this.video.uuid
102 const options = this.getVideoOptions(baseUrl) 102 const options = this.getVideoOptions(baseUrl)
103 103
104 return buildVideoLink(options) 104 return buildVideoLink(options)
105 } 105 }
106 106
107 getPlaylistUrl () { 107 getPlaylistUrl () {
108 const base = window.location.origin + '/videos/watch/playlist/' + this.playlist.uuid 108 const base = window.location.origin + '/w/p/' + this.playlist.uuid
109 109
110 if (!this.includeVideoInPlaylist) return base 110 if (!this.includeVideoInPlaylist) return base
111 111
diff --git a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts
index bdede17a3..d5583c29f 100644
--- a/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts
+++ b/client/src/app/shared/shared-thumbnail/video-thumbnail.component.ts
@@ -57,7 +57,7 @@ export class VideoThumbnailComponent {
57 getVideoRouterLink () { 57 getVideoRouterLink () {
58 if (this.videoRouterLink) return this.videoRouterLink 58 if (this.videoRouterLink) return this.videoRouterLink
59 59
60 return [ '/videos/watch', this.video.uuid ] 60 return [ '/w', this.video.uuid ]
61 } 61 }
62 62
63 onWatchLaterClick (event: Event) { 63 onWatchLaterClick (event: Event) {
diff --git a/client/src/app/shared/shared-video-comment/video-comment.model.ts b/client/src/app/shared/shared-video-comment/video-comment.model.ts
index 9a4e3954e..94d6c5fa8 100644
--- a/client/src/app/shared/shared-video-comment/video-comment.model.ts
+++ b/client/src/app/shared/shared-video-comment/video-comment.model.ts
@@ -85,7 +85,7 @@ export class VideoCommentAdmin implements VideoCommentAdminServerModel {
85 id: hash.video.id, 85 id: hash.video.id,
86 uuid: hash.video.uuid, 86 uuid: hash.video.uuid,
87 name: hash.video.name, 87 name: hash.video.name,
88 localUrl: '/videos/watch/' + hash.video.uuid 88 localUrl: '/w/' + hash.video.uuid
89 } 89 }
90 90
91 this.localUrl = this.video.localUrl + ';threadId=' + this.threadId 91 this.localUrl = this.video.localUrl + ';threadId=' + this.threadId
@@ -95,7 +95,7 @@ export class VideoCommentAdmin implements VideoCommentAdminServerModel {
95 if (this.account) { 95 if (this.account) {
96 this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host) 96 this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host)
97 97
98 this.account.localUrl = '/accounts/' + this.by 98 this.account.localUrl = '/a/' + this.by
99 } 99 }
100 } 100 }
101} 101}
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 645be92bd..6c34123ed 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
@@ -12,12 +12,12 @@
12 <div class="d-flex video-miniature-meta"> 12 <div class="d-flex video-miniature-meta">
13 <my-actor-avatar 13 <my-actor-avatar
14 *ngIf="displayOptions.avatar && displayOwnerVideoChannel()" [title]="channelLinkTitle" 14 *ngIf="displayOptions.avatar && displayOwnerVideoChannel()" [title]="channelLinkTitle"
15 [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/video-channels', video.byVideoChannel ]" 15 [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]"
16 ></my-actor-avatar> 16 ></my-actor-avatar>
17 17
18 <my-actor-avatar 18 <my-actor-avatar
19 *ngIf="displayOptions.avatar && displayOwnerAccount()" [title]="channelLinkTitle" 19 *ngIf="displayOptions.avatar && displayOwnerAccount()" [title]="channelLinkTitle"
20 [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/video-channels', video.byVideoChannel ]" 20 [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]"
21 ></my-actor-avatar> 21 ></my-actor-avatar>
22 22
23 <div class="w-100 d-flex flex-column"> 23 <div class="w-100 d-flex flex-column">
@@ -39,10 +39,10 @@
39 </span> 39 </span>
40 </span> 40 </span>
41 41
42 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> 42 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/c', video.byVideoChannel ]">
43 {{ video.byAccount }} 43 {{ video.byAccount }}
44 </a> 44 </a>
45 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> 45 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/c', video.byVideoChannel ]">
46 {{ video.byVideoChannel }} 46 {{ video.byVideoChannel }}
47 </a> 47 </a>
48 48
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 b58c118be..aac55a6e9 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
@@ -125,7 +125,7 @@ export class VideoMiniatureComponent implements OnInit {
125 125
126 buildVideoLink () { 126 buildVideoLink () {
127 if (this.videoLinkType === 'internal' || !this.video.url) { 127 if (this.videoLinkType === 'internal' || !this.video.url) {
128 this.videoRouterLink = [ '/videos/watch', this.video.uuid ] 128 this.videoRouterLink = [ '/w', this.video.uuid ]
129 return 129 return
130 } 130 }
131 131
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
index ec004a407..e74f58f47 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
@@ -20,7 +20,7 @@
20 [attr.title]="playlistElement.video.name" 20 [attr.title]="playlistElement.video.name"
21 >{{ playlistElement.video.name }}</a> 21 >{{ playlistElement.video.name }}</a>
22 22
23 <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', playlistElement.video.byAccount ]"> 23 <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/a', playlistElement.video.byAccount ]">
24 {{ playlistElement.video.byAccount }} 24 {{ playlistElement.video.byAccount }}
25 </a> 25 </a>
26 <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span> 26 <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span>
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 7c083ae26..86c281a1e 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
@@ -71,7 +71,7 @@ export class VideoPlaylistElementMiniatureComponent implements OnInit {
71 buildRouterLink () { 71 buildRouterLink () {
72 if (!this.playlist) return null 72 if (!this.playlist) return null
73 73
74 return [ '/videos/watch/playlist', this.playlist.uuid ] 74 return [ '/w/p', this.playlist.uuid ]
75 } 75 }
76 76
77 buildRouterQuery () { 77 buildRouterQuery () {
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
index f50f95003..81c36e6fe 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
@@ -19,7 +19,7 @@
19 {{ playlist.displayName }} 19 {{ playlist.displayName }}
20 </a> 20 </a>
21 21
22 <a i18n [routerLink]="[ '/video-channels', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy"> 22 <a i18n [routerLink]="[ '/c', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy">
23 {{ playlist.videoChannelBy }} 23 {{ playlist.videoChannelBy }}
24 </a> 24 </a>
25 25
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 6b0b1056f..9bbec6038 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
@@ -18,6 +18,6 @@ export class VideoPlaylistMiniatureComponent {
18 if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ] 18 if (this.toManage) return [ '/my-library/video-playlists', this.playlist.uuid ]
19 if (this.playlist.videosLength === 0) return null 19 if (this.playlist.videosLength === 0) return null
20 20
21 return [ '/videos/watch/playlist', this.playlist.uuid ] 21 return [ '/w/p', this.playlist.uuid ]
22 } 22 }
23} 23}