aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app')
-rw-r--r--client/src/app/+about/about-peertube/about-peertube-contributors.component.ts2
-rw-r--r--client/src/app/+about/about-routing.module.ts8
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts2
-rw-r--r--client/src/app/+accounts/accounts-routing.module.ts2
-rw-r--r--client/src/app/+admin/admin-routing.module.ts3
-rw-r--r--client/src/app/+admin/admin.module.ts8
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html41
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts27
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html14
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts52
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html28
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts25
-rw-r--r--client/src/app/+admin/config/edit-custom-config/index.ts1
-rw-r--r--client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts3
-rw-r--r--client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts3
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html2
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html2
-rw-r--r--client/src/app/+home/home-routing.module.ts16
-rw-r--r--client/src/app/+home/home.component.html4
-rw-r--r--client/src/app/+home/home.component.scss3
-rw-r--r--client/src/app/+home/home.component.ts26
-rw-r--r--client/src/app/+home/home.module.ts25
-rw-r--r--client/src/app/+home/index.ts3
-rw-r--r--client/src/app/+login/login-routing.module.ts4
-rw-r--r--client/src/app/+my-account/my-account-routing.module.ts5
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.html4
-rw-r--r--client/src/app/+my-library/my-library-routing.module.ts3
-rw-r--r--client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html6
-rw-r--r--client/src/app/+my-library/my-video-imports/my-video-imports.component.ts2
-rw-r--r--client/src/app/+page-not-found/page-not-found.component.ts3
-rw-r--r--client/src/app/+remote-interaction/remote-interaction.component.ts4
-rw-r--r--client/src/app/+reset-password/reset-password-routing.module.ts4
-rw-r--r--client/src/app/+search/search-routing.module.ts4
-rw-r--r--client/src/app/+search/search.component.ts10
-rw-r--r--client/src/app/+search/video-lazy-load.resolver.ts2
-rw-r--r--client/src/app/+signup/+register/register-routing.module.ts3
-rw-r--r--client/src/app/+signup/+register/register-step-terms.component.html4
-rw-r--r--client/src/app/+signup/+register/register-step-terms.component.ts1
-rw-r--r--client/src/app/+signup/+register/register.component.html1
-rw-r--r--client/src/app/+signup/+register/register.component.ts4
-rw-r--r--client/src/app/+signup/+verify-account/verify-account-routing.module.ts4
-rw-r--r--client/src/app/+video-channels/video-channels-routing.module.ts2
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/shared/video-edit.component.ts11
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts10
-rw-r--r--client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts2
-rw-r--r--client/src/app/+videos/+video-edit/video-add-routing.module.ts3
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.html10
-rw-r--r--client/src/app/+videos/+video-edit/video-add.component.ts20
-rw-r--r--client/src/app/+videos/+video-edit/video-update-routing.module.ts3
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.html2
-rw-r--r--client/src/app/+videos/+video-edit/video-update.component.ts2
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment.component.html6
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment.component.ts2
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.html4
-rw-r--r--client/src/app/+videos/+video-watch/video-watch-routing.module.ts9
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.html6
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.ts6
-rw-r--r--client/src/app/+videos/video-list/overview/video-overview.component.html2
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending-header.component.ts12
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending.component.ts7
-rw-r--r--client/src/app/+videos/videos-routing.module.ts27
-rw-r--r--client/src/app/app-routing.module.ts123
-rw-r--r--client/src/app/app.component.ts4
-rw-r--r--client/src/app/app.module.ts20
-rw-r--r--client/src/app/core/core.module.ts7
-rw-r--r--client/src/app/core/menu/menu.service.ts58
-rw-r--r--client/src/app/core/renderer/html-renderer.service.ts10
-rw-r--r--client/src/app/core/renderer/markdown.service.ts53
-rw-r--r--client/src/app/core/routing/index.ts2
-rw-r--r--client/src/app/core/routing/meta-guard.service.ts23
-rw-r--r--client/src/app/core/routing/meta.service.ts40
-rw-r--r--client/src/app/core/routing/redirect.service.ts35
-rw-r--r--client/src/app/core/server/server.service.ts35
-rw-r--r--client/src/app/core/theme/theme.service.ts14
-rw-r--r--client/src/app/menu/menu.component.html23
-rw-r--r--client/src/app/menu/menu.component.ts23
-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/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.ts136
-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.ts49
-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
122 files changed, 1380 insertions, 335 deletions
diff --git a/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts
index c45269be4..dd774a4ef 100644
--- a/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts
+++ b/client/src/app/+about/about-peertube/about-peertube-contributors.component.ts
@@ -14,6 +14,6 @@ export class AboutPeertubeContributorsComponent implements OnInit {
14 constructor (private markdownService: MarkdownService) { } 14 constructor (private markdownService: MarkdownService) { }
15 15
16 async ngOnInit () { 16 async ngOnInit () {
17 this.creditsHtml = await this.markdownService.completeMarkdownToHTML(this.markdown) 17 this.creditsHtml = await this.markdownService.unsafeMarkdownToHTML(this.markdown, true)
18 } 18 }
19} 19}
diff --git a/client/src/app/+about/about-routing.module.ts b/client/src/app/+about/about-routing.module.ts
index 96a737555..880bf4a39 100644
--- a/client/src/app/+about/about-routing.module.ts
+++ b/client/src/app/+about/about-routing.module.ts
@@ -1,17 +1,15 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { AboutComponent } from './about.component'
5import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
7import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component' 3import { AboutFollowsComponent } from '@app/+about/about-follows/about-follows.component'
4import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
8import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver' 5import { AboutInstanceResolver } from '@app/+about/about-instance/about-instance.resolver'
6import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
7import { AboutComponent } from './about.component'
9 8
10const aboutRoutes: Routes = [ 9const aboutRoutes: Routes = [
11 { 10 {
12 path: '', 11 path: '',
13 component: AboutComponent, 12 component: AboutComponent,
14 canActivateChild: [ MetaGuard ],
15 children: [ 13 children: [
16 { 14 {
17 path: '', 15 path: '',
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
index 7e916e122..e146a5cd2 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
@@ -139,6 +139,6 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
139 } 139 }
140 140
141 getVideoChannelLink (videoChannel: VideoChannel) { 141 getVideoChannelLink (videoChannel: VideoChannel) {
142 return [ '/video-channels', videoChannel.nameWithHost ] 142 return [ '/c', videoChannel.nameWithHost ]
143 } 143 }
144} 144}
diff --git a/client/src/app/+accounts/accounts-routing.module.ts b/client/src/app/+accounts/accounts-routing.module.ts
index 3bf0f7185..2f3792a8d 100644
--- a/client/src/app/+accounts/accounts-routing.module.ts
+++ b/client/src/app/+accounts/accounts-routing.module.ts
@@ -1,6 +1,5 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { AccountSearchComponent } from './account-search/account-search.component' 3import { AccountSearchComponent } from './account-search/account-search.component'
5import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component' 4import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
6import { AccountVideosComponent } from './account-videos/account-videos.component' 5import { AccountVideosComponent } from './account-videos/account-videos.component'
@@ -14,7 +13,6 @@ const accountsRoutes: Routes = [
14 { 13 {
15 path: ':accountId', 14 path: ':accountId',
16 component: AccountsComponent, 15 component: AccountsComponent,
17 canActivateChild: [ MetaGuard ],
18 children: [ 16 children: [
19 { 17 {
20 path: '', 18 path: '',
diff --git a/client/src/app/+admin/admin-routing.module.ts b/client/src/app/+admin/admin-routing.module.ts
index 986dae8eb..d029661d3 100644
--- a/client/src/app/+admin/admin-routing.module.ts
+++ b/client/src/app/+admin/admin-routing.module.ts
@@ -4,7 +4,6 @@ import { ConfigRoutes } from '@app/+admin/config'
4import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes' 4import { ModerationRoutes } from '@app/+admin/moderation/moderation.routes'
5import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes' 5import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes'
6import { SystemRoutes } from '@app/+admin/system' 6import { SystemRoutes } from '@app/+admin/system'
7import { MetaGuard } from '@ngx-meta/core'
8import { AdminComponent } from './admin.component' 7import { AdminComponent } from './admin.component'
9import { FollowsRoutes } from './follows' 8import { FollowsRoutes } from './follows'
10import { UsersRoutes } from './users' 9import { UsersRoutes } from './users'
@@ -13,8 +12,6 @@ const adminRoutes: Routes = [
13 { 12 {
14 path: '', 13 path: '',
15 component: AdminComponent, 14 component: AdminComponent,
16 canActivate: [ MetaGuard ],
17 canActivateChild: [ MetaGuard ],
18 children: [ 15 children: [
19 { 16 {
20 path: '', 17 path: '',
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts
index 45366f9ec..a7fe20b07 100644
--- a/client/src/app/+admin/admin.module.ts
+++ b/client/src/app/+admin/admin.module.ts
@@ -4,12 +4,13 @@ import { TableModule } from 'primeng/table'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list' 5import { SharedAbuseListModule } from '@app/shared/shared-abuse-list'
6import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit' 6import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
7import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
8import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup'
7import { SharedFormModule } from '@app/shared/shared-forms' 9import { SharedFormModule } from '@app/shared/shared-forms'
8import { SharedGlobalIconModule } from '@app/shared/shared-icons' 10import { SharedGlobalIconModule } from '@app/shared/shared-icons'
9import { SharedMainModule } from '@app/shared/shared-main' 11import { SharedMainModule } from '@app/shared/shared-main'
10import { SharedModerationModule } from '@app/shared/shared-moderation' 12import { SharedModerationModule } from '@app/shared/shared-moderation'
11import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' 13import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
12import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
13import { AdminRoutingModule } from './admin-routing.module' 14import { AdminRoutingModule } from './admin-routing.module'
14import { AdminComponent } from './admin.component' 15import { AdminComponent } from './admin.component'
15import { 16import {
@@ -18,6 +19,7 @@ import {
18 EditBasicConfigurationComponent, 19 EditBasicConfigurationComponent,
19 EditConfigurationService, 20 EditConfigurationService,
20 EditCustomConfigComponent, 21 EditCustomConfigComponent,
22 EditHomepageComponent,
21 EditInstanceInformationComponent, 23 EditInstanceInformationComponent,
22 EditLiveConfigurationComponent, 24 EditLiveConfigurationComponent,
23 EditVODTranscodingComponent 25 EditVODTranscodingComponent
@@ -53,6 +55,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
53 SharedVideoCommentModule, 55 SharedVideoCommentModule,
54 SharedActorImageModule, 56 SharedActorImageModule,
55 SharedActorImageEditModule, 57 SharedActorImageEditModule,
58 SharedCustomMarkupModule,
56 59
57 TableModule, 60 TableModule,
58 SelectButtonModule, 61 SelectButtonModule,
@@ -100,7 +103,8 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
100 EditVODTranscodingComponent, 103 EditVODTranscodingComponent,
101 EditLiveConfigurationComponent, 104 EditLiveConfigurationComponent,
102 EditAdvancedConfigurationComponent, 105 EditAdvancedConfigurationComponent,
103 EditInstanceInformationComponent 106 EditInstanceInformationComponent,
107 EditHomepageComponent
104 ], 108 ],
105 109
106 exports: [ 110 exports: [
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
index 84a793ae4..1f542e458 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html
@@ -26,22 +26,13 @@
26 <div class="form-group" formGroupName="instance"> 26 <div class="form-group" formGroupName="instance">
27 <label i18n for="instanceDefaultClientRoute">Landing page</label> 27 <label i18n for="instanceDefaultClientRoute">Landing page</label>
28 28
29 <div class="peertube-select-container"> 29 <my-select-custom-value
30 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control"> 30 id="instanceDefaultClientRoute"
31 <option i18n value="/videos/overview">Discover videos</option> 31 [items]="defaultLandingPageOptions"
32 32 formControlName="defaultClientRoute"
33 <optgroup i18n-label label="Trending pages"> 33 inputType="text"
34 <option i18n value="/videos/trending">Default trending page</option> 34 [clearable]="false"
35 <option i18n value="/videos/trending?alg=best" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('best')">Best videos</option> 35 ></my-select-custom-value>
36 <option i18n value="/videos/trending?alg=hot" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('hot')">Hot videos</option>
37 <option i18n value="/videos/trending?alg=most-viewed" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-viewed')">Most viewed videos</option>
38 <option i18n value="/videos/trending?alg=most-liked" [disabled]="!doesTrendingVideosAlgorithmsEnabledInclude('most-liked')">Most liked videos</option>
39 </optgroup>
40
41 <option i18n value="/videos/recently-added">Recently added videos</option>
42 <option i18n value="/videos/local">Local videos</option>
43 </select>
44 </div>
45 36
46 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div> 37 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
47 </div> 38 </div>
@@ -167,6 +158,20 @@
167 158
168 <small i18n *ngIf="hasUnlimitedSignup()" class="text-muted">Signup won't be limited to a fixed number of users.</small> 159 <small i18n *ngIf="hasUnlimitedSignup()" class="text-muted">Signup won't be limited to a fixed number of users.</small>
169 </div> 160 </div>
161
162 <div [ngClass]="getDisabledSignupClass()" class="mt-3">
163 <label i18n for="signupMinimumAge">Minimum required age to create an account</label>
164
165 <div class="number-with-unit">
166 <input
167 type="number" min="1" id="signupMinimumAge" class="form-control"
168 formControlName="minimumAge" [ngClass]="{ 'input-error': formErrors['signup.minimumAge'] }"
169 >
170 <span i18n>{form.value['signup']['minimumAge'], plural, =1 {year old} other {years old}}</span>
171 </div>
172
173 <div *ngIf="formErrors.signup.minimumAge" class="form-error">{{ formErrors.signup.minimumAge }}</div>
174 </div>
170 </ng-container> 175 </ng-container>
171 </my-peertube-checkbox> 176 </my-peertube-checkbox>
172 </div> 177 </div>
@@ -478,7 +483,7 @@
478 <ng-container formGroupName="twitter"> 483 <ng-container formGroupName="twitter">
479 484
480 <div class="form-group"> 485 <div class="form-group">
481 <label i18n for="signupLimit">Your Twitter username</label> 486 <label for="servicesTwitterUsername" i18n>Your Twitter username</label>
482 487
483 <input 488 <input
484 type="text" id="servicesTwitterUsername" class="form-control" 489 type="text" id="servicesTwitterUsername" class="form-control"
@@ -498,7 +503,7 @@
498 <ng-container i18n> 503 <ng-container i18n>
499 If your instance is explicitly allowed by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br /> 504 If your instance is explicitly allowed by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
500 If the instance is not, we use an image link card that will redirect to your PeerTube instance.<br /><br /> 505 If the instance is not, we use an image link card that will redirect to your PeerTube instance.<br /><br />
501 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on 506 Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/w/blabla) on
502 <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> 507 <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a>
503 to see if you instance is allowed. 508 to see if you instance is allowed.
504 </ng-container> 509 </ng-container>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
index 34d05f9f3..74fdb87a1 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.ts
@@ -1,7 +1,8 @@
1
2import { pairwise } from 'rxjs/operators' 1import { pairwise } from 'rxjs/operators'
3import { Component, Input, OnInit } from '@angular/core' 2import { SelectOptionsItem } from 'src/types/select-options-item.model'
3import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'
4import { FormGroup } from '@angular/forms' 4import { FormGroup } from '@angular/forms'
5import { MenuService } from '@app/core'
5import { ServerConfig } from '@shared/models' 6import { ServerConfig } from '@shared/models'
6import { ConfigService } from '../shared/config.service' 7import { ConfigService } from '../shared/config.service'
7 8
@@ -10,22 +11,31 @@ import { ConfigService } from '../shared/config.service'
10 templateUrl: './edit-basic-configuration.component.html', 11 templateUrl: './edit-basic-configuration.component.html',
11 styleUrls: [ './edit-custom-config.component.scss' ] 12 styleUrls: [ './edit-custom-config.component.scss' ]
12}) 13})
13export class EditBasicConfigurationComponent implements OnInit { 14export class EditBasicConfigurationComponent implements OnInit, OnChanges {
14 @Input() form: FormGroup 15 @Input() form: FormGroup
15 @Input() formErrors: any 16 @Input() formErrors: any
16 17
17 @Input() serverConfig: ServerConfig 18 @Input() serverConfig: ServerConfig
18 19
19 signupAlertMessage: string 20 signupAlertMessage: string
21 defaultLandingPageOptions: SelectOptionsItem[] = []
20 22
21 constructor ( 23 constructor (
22 private configService: ConfigService 24 private configService: ConfigService,
25 private menuService: MenuService
23 ) { } 26 ) { }
24 27
25 ngOnInit () { 28 ngOnInit () {
29 this.buildLandingPageOptions()
26 this.checkSignupField() 30 this.checkSignupField()
27 } 31 }
28 32
33 ngOnChanges (changes: SimpleChanges) {
34 if (changes['serverConfig']) {
35 this.buildLandingPageOptions()
36 }
37 }
38
29 getVideoQuotaOptions () { 39 getVideoQuotaOptions () {
30 return this.configService.videoQuotaOptions 40 return this.configService.videoQuotaOptions
31 } 41 }
@@ -70,6 +80,15 @@ export class EditBasicConfigurationComponent implements OnInit {
70 return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true 80 return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
71 } 81 }
72 82
83 buildLandingPageOptions () {
84 this.defaultLandingPageOptions = this.menuService.buildCommonLinks(this.serverConfig)
85 .map(o => ({
86 id: o.path,
87 label: o.label,
88 description: o.path
89 }))
90 }
91
73 private checkSignupField () { 92 private checkSignupField () {
74 const signupControl = this.form.get('signup.enabled') 93 const signupControl = this.form.get('signup.enabled')
75 94
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index b6365614d..3ceea02ca 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -3,8 +3,16 @@
3 3
4 <div ngbNav #nav="ngbNav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" class="nav-tabs"> 4 <div ngbNav #nav="ngbNav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" class="nav-tabs">
5 5
6 <ng-container ngbNavItem="instance-homepage">
7 <a ngbNavLink i18n>Homepage</a>
8
9 <ng-template ngbNavContent>
10 <my-edit-homepage [form]="form" [formErrors]="formErrors"></my-edit-homepage>
11 </ng-template>
12 </ng-container>
13
6 <ng-container ngbNavItem="instance-information"> 14 <ng-container ngbNavItem="instance-information">
7 <a ngbNavLink i18n>Instance information</a> 15 <a ngbNavLink i18n>Information</a>
8 16
9 <ng-template ngbNavContent> 17 <ng-template ngbNavContent>
10 <my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems"> 18 <my-edit-instance-information [form]="form" [formErrors]="formErrors" [languageItems]="languageItems" [categoryItems]="categoryItems">
@@ -13,7 +21,7 @@
13 </ng-container> 21 </ng-container>
14 22
15 <ng-container ngbNavItem="basic-configuration"> 23 <ng-container ngbNavItem="basic-configuration">
16 <a ngbNavLink i18n>Basic configuration</a> 24 <a ngbNavLink i18n>Basic</a>
17 25
18 <ng-template ngbNavContent> 26 <ng-template ngbNavContent>
19 <my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig"> 27 <my-edit-basic-configuration [form]="form" [formErrors]="formErrors" [serverConfig]="serverConfig">
@@ -40,7 +48,7 @@
40 </ng-container> 48 </ng-container>
41 49
42 <ng-container ngbNavItem="advanced-configuration"> 50 <ng-container ngbNavItem="advanced-configuration">
43 <a ngbNavLink i18n>Advanced configuration</a> 51 <a ngbNavLink i18n>Advanced</a>
44 52
45 <ng-template ngbNavContent> 53 <ng-template ngbNavContent>
46 <my-edit-advanced-configuration [form]="form" [formErrors]="formErrors"> 54 <my-edit-advanced-configuration [form]="form" [formErrors]="formErrors">
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 4b35d65fc..cb65ca6e7 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -1,4 +1,5 @@
1 1
2import omit from 'lodash-es/omit'
2import { forkJoin } from 'rxjs' 3import { forkJoin } from 'rxjs'
3import { SelectOptionsItem } from 'src/types/select-options-item.model' 4import { SelectOptionsItem } from 'src/types/select-options-item.model'
4import { Component, OnInit } from '@angular/core' 5import { Component, OnInit } from '@angular/core'
@@ -20,13 +21,19 @@ import {
20 SEARCH_INDEX_URL_VALIDATOR, 21 SEARCH_INDEX_URL_VALIDATOR,
21 SERVICES_TWITTER_USERNAME_VALIDATOR, 22 SERVICES_TWITTER_USERNAME_VALIDATOR,
22 SIGNUP_LIMIT_VALIDATOR, 23 SIGNUP_LIMIT_VALIDATOR,
24 SIGNUP_MINIMUM_AGE_VALIDATOR,
23 TRANSCODING_THREADS_VALIDATOR 25 TRANSCODING_THREADS_VALIDATOR
24} from '@app/shared/form-validators/custom-config-validators' 26} from '@app/shared/form-validators/custom-config-validators'
25import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' 27import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators'
26import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' 28import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
27import { CustomConfig, ServerConfig } from '@shared/models' 29import { CustomPageService } from '@app/shared/shared-main/custom-page'
30import { CustomConfig, CustomPage, ServerConfig } from '@shared/models'
28import { EditConfigurationService } from './edit-configuration.service' 31import { EditConfigurationService } from './edit-configuration.service'
29 32
33type ComponentCustomConfig = CustomConfig & {
34 instanceCustomHomepage: CustomPage
35}
36
30@Component({ 37@Component({
31 selector: 'my-edit-custom-config', 38 selector: 'my-edit-custom-config',
32 templateUrl: './edit-custom-config.component.html', 39 templateUrl: './edit-custom-config.component.html',
@@ -35,9 +42,11 @@ import { EditConfigurationService } from './edit-configuration.service'
35export class EditCustomConfigComponent extends FormReactive implements OnInit { 42export class EditCustomConfigComponent extends FormReactive implements OnInit {
36 activeNav: string 43 activeNav: string
37 44
38 customConfig: CustomConfig 45 customConfig: ComponentCustomConfig
39 serverConfig: ServerConfig 46 serverConfig: ServerConfig
40 47
48 homepage: CustomPage
49
41 languageItems: SelectOptionsItem[] = [] 50 languageItems: SelectOptionsItem[] = []
42 categoryItems: SelectOptionsItem[] = [] 51 categoryItems: SelectOptionsItem[] = []
43 52
@@ -47,6 +56,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
47 protected formValidatorService: FormValidatorService, 56 protected formValidatorService: FormValidatorService,
48 private notifier: Notifier, 57 private notifier: Notifier,
49 private configService: ConfigService, 58 private configService: ConfigService,
59 private customPage: CustomPageService,
50 private serverService: ServerService, 60 private serverService: ServerService,
51 private editConfigurationService: EditConfigurationService 61 private editConfigurationService: EditConfigurationService
52 ) { 62 ) {
@@ -56,11 +66,9 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
56 ngOnInit () { 66 ngOnInit () {
57 this.serverConfig = this.serverService.getTmpConfig() 67 this.serverConfig = this.serverService.getTmpConfig()
58 this.serverService.getConfig() 68 this.serverService.getConfig()
59 .subscribe(config => { 69 .subscribe(config => this.serverConfig = config)
60 this.serverConfig = config
61 })
62 70
63 const formGroupData: { [key in keyof CustomConfig ]: any } = { 71 const formGroupData: { [key in keyof ComponentCustomConfig ]: any } = {
64 instance: { 72 instance: {
65 name: INSTANCE_NAME_VALIDATOR, 73 name: INSTANCE_NAME_VALIDATOR,
66 shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR, 74 shortDescription: INSTANCE_SHORT_DESCRIPTION_VALIDATOR,
@@ -113,7 +121,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
113 signup: { 121 signup: {
114 enabled: null, 122 enabled: null,
115 limit: SIGNUP_LIMIT_VALIDATOR, 123 limit: SIGNUP_LIMIT_VALIDATOR,
116 requiresEmailVerification: null 124 requiresEmailVerification: null,
125 minimumAge: SIGNUP_MINIMUM_AGE_VALIDATOR
117 }, 126 },
118 import: { 127 import: {
119 videos: { 128 videos: {
@@ -215,6 +224,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
215 disableLocalSearch: null, 224 disableLocalSearch: null,
216 isDefaultSearch: null 225 isDefaultSearch: null
217 } 226 }
227 },
228
229 instanceCustomHomepage: {
230 content: null
218 } 231 }
219 } 232 }
220 233
@@ -250,15 +263,23 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
250 } 263 }
251 264
252 async formValidated () { 265 async formValidated () {
253 const value: CustomConfig = this.form.getRawValue() 266 const value: ComponentCustomConfig = this.form.getRawValue()
254 267
255 this.configService.updateCustomConfig(value) 268 forkJoin([
269 this.configService.updateCustomConfig(omit(value, 'instanceCustomHomepage')),
270 this.customPage.updateInstanceHomepage(value.instanceCustomHomepage.content)
271 ])
256 .subscribe( 272 .subscribe(
257 res => { 273 ([ resConfig ]) => {
258 this.customConfig = res 274 const instanceCustomHomepage = {
275 content: value.instanceCustomHomepage.content
276 }
277
278 this.customConfig = { ...resConfig, instanceCustomHomepage }
259 279
260 // Reload general configuration 280 // Reload general configuration
261 this.serverService.resetConfig() 281 this.serverService.resetConfig()
282 .subscribe(config => this.serverConfig = config)
262 283
263 this.updateForm() 284 this.updateForm()
264 285
@@ -317,9 +338,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
317 } 338 }
318 339
319 private loadConfigAndUpdateForm () { 340 private loadConfigAndUpdateForm () {
320 this.configService.getCustomConfig() 341 forkJoin([
321 .subscribe(config => { 342 this.configService.getCustomConfig(),
322 this.customConfig = config 343 this.customPage.getInstanceHomepage()
344 ])
345 .subscribe(([ config, homepage ]) => {
346 this.customConfig = { ...config, instanceCustomHomepage: homepage }
323 347
324 this.updateForm() 348 this.updateForm()
325 // Force form validation 349 // Force form validation
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
new file mode 100644
index 000000000..c48fa5bf8
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.html
@@ -0,0 +1,28 @@
1<ng-container [formGroup]="form">
2
3 <ng-container formGroupName="instanceCustomHomepage">
4
5 <div class="form-row mt-5"> <!-- homepage grid -->
6 <div class="form-group col-12 col-lg-4 col-xl-3">
7 <div i18n class="inner-form-title">INSTANCE HOMEPAGE</div>
8 </div>
9
10 <div class="form-group form-group-right col-12 col-lg-8 col-xl-9">
11
12 <div class="form-group">
13 <label i18n for="instanceCustomHomepageContent">Homepage</label>
14
15 <my-markdown-textarea
16 name="instanceCustomHomepageContent" formControlName="content" textareaMaxWidth="90%" textareaHeight="300px"
17 [customMarkdownRenderer]="customMarkdownRenderer"
18 [classes]="{ 'input-error': formErrors['instanceCustomHomepage.content'] }"
19 ></my-markdown-textarea>
20
21 <div *ngIf="formErrors.instanceCustomHomepage.content" class="form-error">{{ formErrors.instanceCustomHomepage.content }}</div>
22 </div>
23 </div>
24 </div>
25
26 </ng-container>
27
28</ng-container>
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts
new file mode 100644
index 000000000..7decf8f75
--- /dev/null
+++ b/client/src/app/+admin/config/edit-custom-config/edit-homepage.component.ts
@@ -0,0 +1,25 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { FormGroup } from '@angular/forms'
3import { CustomMarkupService } from '@app/shared/shared-custom-markup'
4
5@Component({
6 selector: 'my-edit-homepage',
7 templateUrl: './edit-homepage.component.html',
8 styleUrls: [ './edit-custom-config.component.scss' ]
9})
10export class EditHomepageComponent implements OnInit {
11 @Input() form: FormGroup
12 @Input() formErrors: any
13
14 customMarkdownRenderer: (text: string) => Promise<HTMLElement>
15
16 constructor (private customMarkup: CustomMarkupService) {
17
18 }
19
20 ngOnInit () {
21 this.customMarkdownRenderer = async (text: string) => {
22 return this.customMarkup.buildElement(text)
23 }
24 }
25}
diff --git a/client/src/app/+admin/config/edit-custom-config/index.ts b/client/src/app/+admin/config/edit-custom-config/index.ts
index 95fcc8f52..4281ad09b 100644
--- a/client/src/app/+admin/config/edit-custom-config/index.ts
+++ b/client/src/app/+admin/config/edit-custom-config/index.ts
@@ -2,6 +2,7 @@ export * from './edit-advanced-configuration.component'
2export * from './edit-basic-configuration.component' 2export * from './edit-basic-configuration.component'
3export * from './edit-configuration.service' 3export * from './edit-configuration.service'
4export * from './edit-custom-config.component' 4export * from './edit-custom-config.component'
5export * from './edit-homepage.component'
5export * from './edit-instance-information.component' 6export * from './edit-instance-information.component'
6export * from './edit-live-configuration.component' 7export * from './edit-live-configuration.component'
7export * from './edit-vod-transcoding.component' 8export * from './edit-vod-transcoding.component'
diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
index 1a95980ae..6af224920 100644
--- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
+++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts
@@ -5,8 +5,7 @@ import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
5import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core' 5import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core'
6import { PluginService } from '@app/core/plugins/plugin.service' 6import { PluginService } from '@app/core/plugins/plugin.service'
7import { compareSemVer } from '@shared/core-utils/miscs/miscs' 7import { compareSemVer } from '@shared/core-utils/miscs/miscs'
8import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model' 8import { PeerTubePlugin, PluginType } from '@shared/models'
9import { PluginType } from '@shared/models/plugins/plugin.type'
10 9
11@Component({ 10@Component({
12 selector: 'my-plugin-list-installed', 11 selector: 'my-plugin-list-installed',
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts
index d2c179aba..0a6e57904 100644
--- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts
+++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts
@@ -4,8 +4,7 @@ import { Component, OnInit } from '@angular/core'
4import { ActivatedRoute, Router } from '@angular/router' 4import { ActivatedRoute, Router } from '@angular/router'
5import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' 5import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
6import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core' 6import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core'
7import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model' 7import { PeerTubePluginIndex, PluginType } from '@shared/models'
8import { PluginType } from '@shared/models/plugins/plugin.type'
9 8
10@Component({ 9@Component({
11 selector: 'my-plugin-search', 10 selector: 'my-plugin-search',
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 5e92c0f36..772ebf272 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -10,7 +10,7 @@
10 <ng-container *ngIf="!isCreation()"> 10 <ng-container *ngIf="!isCreation()">
11 <li class="breadcrumb-item active" i18n>Edit</li> 11 <li class="breadcrumb-item active" i18n>Edit</li>
12 <li class="breadcrumb-item active" aria-current="page"> 12 <li class="breadcrumb-item active" aria-current="page">
13 <a *ngIf="user" [routerLink]="[ '/accounts', user?.username ]">{{ user?.username }}</a> 13 <a *ngIf="user" [routerLink]="[ '/a', user?.username ]">{{ user?.username }}</a>
14 </li> 14 </li>
15 </ng-container> 15 </ng-container>
16 </ol> 16 </ol>
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index 44d8a7e87..5b4f35c77 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -87,7 +87,7 @@
87 </td> 87 </td>
88 88
89 <td *ngIf="isSelected('username')"> 89 <td *ngIf="isSelected('username')">
90 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]"> 90 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/a/' + user.username ]">
91 <div class="chip two-lines"> 91 <div class="chip two-lines">
92 <my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar> 92 <my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar>
93 <div> 93 <div>
diff --git a/client/src/app/+home/home-routing.module.ts b/client/src/app/+home/home-routing.module.ts
new file mode 100644
index 000000000..a2085f191
--- /dev/null
+++ b/client/src/app/+home/home-routing.module.ts
@@ -0,0 +1,16 @@
1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router'
3import { HomeComponent } from './home.component'
4
5const homeRoutes: Routes = [
6 {
7 path: '',
8 component: HomeComponent
9 }
10]
11
12@NgModule({
13 imports: [ RouterModule.forChild(homeRoutes) ],
14 exports: [ RouterModule ]
15})
16export class HomeRoutingModule {}
diff --git a/client/src/app/+home/home.component.html b/client/src/app/+home/home.component.html
new file mode 100644
index 000000000..645b9dc69
--- /dev/null
+++ b/client/src/app/+home/home.component.html
@@ -0,0 +1,4 @@
1<div class="root margin-content">
2 <div #contentWrapper></div>
3</div>
4
diff --git a/client/src/app/+home/home.component.scss b/client/src/app/+home/home.component.scss
new file mode 100644
index 000000000..6c73e9248
--- /dev/null
+++ b/client/src/app/+home/home.component.scss
@@ -0,0 +1,3 @@
1.root {
2 padding-top: 20px;
3}
diff --git a/client/src/app/+home/home.component.ts b/client/src/app/+home/home.component.ts
new file mode 100644
index 000000000..16d3a6df7
--- /dev/null
+++ b/client/src/app/+home/home.component.ts
@@ -0,0 +1,26 @@
1
2import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'
3import { CustomMarkupService } from '@app/shared/shared-custom-markup'
4import { CustomPageService } from '@app/shared/shared-main/custom-page'
5
6@Component({
7 templateUrl: './home.component.html',
8 styleUrls: [ './home.component.scss' ]
9})
10
11export class HomeComponent implements OnInit {
12 @ViewChild('contentWrapper') contentWrapper: ElementRef<HTMLInputElement>
13
14 constructor (
15 private customMarkupService: CustomMarkupService,
16 private customPageService: CustomPageService
17 ) { }
18
19 async ngOnInit () {
20 this.customPageService.getInstanceHomepage()
21 .subscribe(async ({ content }) => {
22 const element = await this.customMarkupService.buildElement(content)
23 this.contentWrapper.nativeElement.appendChild(element)
24 })
25 }
26}
diff --git a/client/src/app/+home/home.module.ts b/client/src/app/+home/home.module.ts
new file mode 100644
index 000000000..102cdc296
--- /dev/null
+++ b/client/src/app/+home/home.module.ts
@@ -0,0 +1,25 @@
1import { NgModule } from '@angular/core'
2import { SharedCustomMarkupModule } from '@app/shared/shared-custom-markup'
3import { SharedMainModule } from '@app/shared/shared-main'
4import { HomeRoutingModule } from './home-routing.module'
5import { HomeComponent } from './home.component'
6
7@NgModule({
8 imports: [
9 HomeRoutingModule,
10
11 SharedMainModule,
12 SharedCustomMarkupModule
13 ],
14
15 declarations: [
16 HomeComponent
17 ],
18
19 exports: [
20 HomeComponent
21 ],
22
23 providers: [ ]
24})
25export class HomeModule { }
diff --git a/client/src/app/+home/index.ts b/client/src/app/+home/index.ts
new file mode 100644
index 000000000..7c77cf9fd
--- /dev/null
+++ b/client/src/app/+home/index.ts
@@ -0,0 +1,3 @@
1export * from './home-routing.module'
2export * from './home.component'
3export * from './home.module'
diff --git a/client/src/app/+login/login-routing.module.ts b/client/src/app/+login/login-routing.module.ts
index 258ddc5c1..c5f0f23c2 100644
--- a/client/src/app/+login/login-routing.module.ts
+++ b/client/src/app/+login/login-routing.module.ts
@@ -1,14 +1,12 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { LoginComponent } from './login.component'
5import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service' 3import { ServerConfigResolver } from '@app/core/routing/server-config-resolver.service'
4import { LoginComponent } from './login.component'
6 5
7const loginRoutes: Routes = [ 6const loginRoutes: Routes = [
8 { 7 {
9 path: '', 8 path: '',
10 component: LoginComponent, 9 component: LoginComponent,
11 canActivate: [ MetaGuard ],
12 data: { 10 data: {
13 meta: { 11 meta: {
14 title: $localize`Login` 12 title: $localize`Login`
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts
index e2f8660fb..ef39c1a36 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -1,20 +1,19 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { LoginGuard } from '../core' 3import { LoginGuard } from '../core'
5import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component' 4import { MyAccountAbusesListComponent } from './my-account-abuses/my-account-abuses-list.component'
5import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
6import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component' 6import { MyAccountBlocklistComponent } from './my-account-blocklist/my-account-blocklist.component'
7import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component' 7import { MyAccountServerBlocklistComponent } from './my-account-blocklist/my-account-server-blocklist.component'
8import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component' 8import { MyAccountNotificationsComponent } from './my-account-notifications/my-account-notifications.component'
9import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' 9import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
10import { MyAccountComponent } from './my-account.component' 10import { MyAccountComponent } from './my-account.component'
11import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component'
12 11
13const myAccountRoutes: Routes = [ 12const myAccountRoutes: Routes = [
14 { 13 {
15 path: '', 14 path: '',
16 component: MyAccountComponent, 15 component: MyAccountComponent,
17 canActivateChild: [ MetaGuard, LoginGuard ], 16 canActivateChild: [ LoginGuard ],
18 children: [ 17 children: [
19 { 18 {
20 path: '', 19 path: '',
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
index e41cbe921..9f139b4f2 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
@@ -17,10 +17,10 @@
17 17
18<div class="video-channels"> 18<div class="video-channels">
19 <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> 19 <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel">
20 <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/video-channels', videoChannel.nameWithHost ]"></my-actor-avatar> 20 <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar>
21 21
22 <div class="video-channel-info"> 22 <div class="video-channel-info">
23 <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> 23 <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
24 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div> 24 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
25 <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div> 25 <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
26 </a> 26 </a>
diff --git a/client/src/app/+my-library/my-library-routing.module.ts b/client/src/app/+my-library/my-library-routing.module.ts
index d8e5aa562..76894bed8 100644
--- a/client/src/app/+my-library/my-library-routing.module.ts
+++ b/client/src/app/+my-library/my-library-routing.module.ts
@@ -1,6 +1,5 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { LoginGuard } from '../core' 3import { LoginGuard } from '../core'
5import { MyHistoryComponent } from './my-history/my-history.component' 4import { MyHistoryComponent } from './my-history/my-history.component'
6import { MyLibraryComponent } from './my-library.component' 5import { MyLibraryComponent } from './my-library.component'
@@ -17,7 +16,7 @@ const myLibraryRoutes: Routes = [
17 { 16 {
18 path: '', 17 path: '',
19 component: MyLibraryComponent, 18 component: MyLibraryComponent,
20 canActivateChild: [ MetaGuard, LoginGuard ], 19 canActivateChild: [ LoginGuard ],
21 children: [ 20 children: [
22 { 21 {
23 path: '', 22 path: '',
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
index f91cebacf..1bd459059 100644
--- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
+++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
@@ -14,17 +14,17 @@
14 14
15<div class="video-channels" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> 15<div class="video-channels" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
16 <div *ngFor="let videoChannel of videoChannels" class="video-channel"> 16 <div *ngFor="let videoChannel of videoChannels" class="video-channel">
17 <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/video-channels', videoChannel.nameWithHost ]"></my-actor-avatar> 17 <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar>
18 18
19 <div class="video-channel-info"> 19 <div class="video-channel-info">
20 <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> 20 <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
21 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div> 21 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
22 <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div> 22 <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
23 </a> 23 </a>
24 24
25 <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div> 25 <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
26 26
27 <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner"> 27 <a [routerLink]="[ '/a', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner">
28 <span i18n>Created by {{ videoChannel.ownerBy }}</span> 28 <span i18n>Created by {{ videoChannel.ownerBy }}</span>
29 29
30 <my-actor-avatar [account]="videoChannel.ownerAccount" size="18"></my-actor-avatar> 30 <my-actor-avatar [account]="videoChannel.ownerAccount" size="18"></my-actor-avatar>
diff --git a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts
index 359535526..bb9d70524 100644
--- a/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts
+++ b/client/src/app/+my-library/my-video-imports/my-video-imports.component.ts
@@ -55,7 +55,7 @@ export class MyVideoImportsComponent extends RestTable implements OnInit {
55 } 55 }
56 56
57 getVideoUrl (video: { uuid: string }) { 57 getVideoUrl (video: { uuid: string }) {
58 return '/videos/watch/' + video.uuid 58 return '/w/' + video.uuid
59 } 59 }
60 60
61 getEditVideoUrl (video: { uuid: string }) { 61 getEditVideoUrl (video: { uuid: string }) {
diff --git a/client/src/app/+page-not-found/page-not-found.component.ts b/client/src/app/+page-not-found/page-not-found.component.ts
index 94b4c8d27..639e5db78 100644
--- a/client/src/app/+page-not-found/page-not-found.component.ts
+++ b/client/src/app/+page-not-found/page-not-found.component.ts
@@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core'
2import { Title } from '@angular/platform-browser' 2import { Title } from '@angular/platform-browser'
3import { Router } from '@angular/router' 3import { Router } from '@angular/router'
4import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 4import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
5
6@Component({ 5@Component({
7 selector: 'my-page-not-found', 6 selector: 'my-page-not-found',
8 templateUrl: './page-not-found.component.html', 7 templateUrl: './page-not-found.component.html',
@@ -10,7 +9,7 @@ import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
10}) 9})
11export class PageNotFoundComponent implements OnInit { 10export class PageNotFoundComponent implements OnInit {
12 status = HttpStatusCode.NOT_FOUND_404 11 status = HttpStatusCode.NOT_FOUND_404
13 type: string 12 type: 'video' | 'other' = 'other'
14 13
15 public constructor ( 14 public constructor (
16 private titleService: Title, 15 private titleService: Title,
diff --git a/client/src/app/+remote-interaction/remote-interaction.component.ts b/client/src/app/+remote-interaction/remote-interaction.component.ts
index e24607b24..6ddf5b58d 100644
--- a/client/src/app/+remote-interaction/remote-interaction.component.ts
+++ b/client/src/app/+remote-interaction/remote-interaction.component.ts
@@ -39,11 +39,11 @@ export class RemoteInteractionComponent implements OnInit {
39 if (videoResult.data.length !== 0) { 39 if (videoResult.data.length !== 0) {
40 const video = videoResult.data[0] 40 const video = videoResult.data[0]
41 41
42 redirectUrl = '/videos/watch/' + video.uuid 42 redirectUrl = '/w/' + video.uuid
43 } else if (channelResult.data.length !== 0) { 43 } else if (channelResult.data.length !== 0) {
44 const channel = new VideoChannel(channelResult.data[0]) 44 const channel = new VideoChannel(channelResult.data[0])
45 45
46 redirectUrl = '/video-channels/' + channel.nameWithHost 46 redirectUrl = '/c/' + channel.nameWithHost
47 } else { 47 } else {
48 this.error = $localize`Cannot access to the remote resource` 48 this.error = $localize`Cannot access to the remote resource`
49 return 49 return
diff --git a/client/src/app/+reset-password/reset-password-routing.module.ts b/client/src/app/+reset-password/reset-password-routing.module.ts
index 7f1ba2f68..3532cdbc1 100644
--- a/client/src/app/+reset-password/reset-password-routing.module.ts
+++ b/client/src/app/+reset-password/reset-password-routing.module.ts
@@ -1,16 +1,14 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { ResetPasswordComponent } from './reset-password.component' 3import { ResetPasswordComponent } from './reset-password.component'
5 4
6const resetPasswordRoutes: Routes = [ 5const resetPasswordRoutes: Routes = [
7 { 6 {
8 path: '', 7 path: '',
9 component: ResetPasswordComponent, 8 component: ResetPasswordComponent,
10 canActivate: [ MetaGuard ],
11 data: { 9 data: {
12 meta: { 10 meta: {
13 title: `Reset password` 11 title: $localize`Reset password`
14 } 12 }
15 } 13 }
16 } 14 }
diff --git a/client/src/app/+search/search-routing.module.ts b/client/src/app/+search/search-routing.module.ts
index e5d7d1ede..0d778af0d 100644
--- a/client/src/app/+search/search-routing.module.ts
+++ b/client/src/app/+search/search-routing.module.ts
@@ -1,6 +1,5 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver' 3import { ChannelLazyLoadResolver } from './channel-lazy-load.resolver'
5import { SearchComponent } from './search.component' 4import { SearchComponent } from './search.component'
6import { VideoLazyLoadResolver } from './video-lazy-load.resolver' 5import { VideoLazyLoadResolver } from './video-lazy-load.resolver'
@@ -9,7 +8,6 @@ const searchRoutes: Routes = [
9 { 8 {
10 path: '', 9 path: '',
11 component: SearchComponent, 10 component: SearchComponent,
12 canActivate: [ MetaGuard ],
13 data: { 11 data: {
14 meta: { 12 meta: {
15 title: $localize`Search` 13 title: $localize`Search`
@@ -19,7 +17,6 @@ const searchRoutes: Routes = [
19 { 17 {
20 path: 'lazy-load-video', 18 path: 'lazy-load-video',
21 component: SearchComponent, 19 component: SearchComponent,
22 canActivate: [ MetaGuard ],
23 resolve: { 20 resolve: {
24 data: VideoLazyLoadResolver 21 data: VideoLazyLoadResolver
25 } 22 }
@@ -27,7 +24,6 @@ const searchRoutes: Routes = [
27 { 24 {
28 path: 'lazy-load-channel', 25 path: 'lazy-load-channel',
29 component: SearchComponent, 26 component: SearchComponent,
30 canActivate: [ MetaGuard ],
31 resolve: { 27 resolve: {
32 data: ChannelLazyLoadResolver 28 data: ChannelLazyLoadResolver
33 } 29 }
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index ecede19a3..4381659e1 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -1,12 +1,11 @@
1import { forkJoin, of, Subscription } from 'rxjs' 1import { forkJoin, of, Subscription } from 'rxjs'
2import { Component, OnDestroy, OnInit } from '@angular/core' 2import { Component, OnDestroy, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ComponentPagination, HooksService, Notifier, ServerService, User, UserService } from '@app/core' 4import { AuthService, ComponentPagination, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core'
5import { immutableAssign } from '@app/helpers' 5import { immutableAssign } from '@app/helpers'
6import { Video, VideoChannel } from '@app/shared/shared-main' 6import { Video, VideoChannel } from '@app/shared/shared-main'
7import { AdvancedSearch, SearchService } from '@app/shared/shared-search' 7import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
8import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature' 8import { MiniatureDisplayOptions, VideoLinkType } from '@app/shared/shared-video-miniature'
9import { MetaService } from '@ngx-meta/core'
10import { SearchTargetType, ServerConfig } from '@shared/models' 9import { SearchTargetType, ServerConfig } from '@shared/models'
11 10
12@Component({ 11@Component({
@@ -214,7 +213,7 @@ export class SearchComponent implements OnInit, OnDestroy {
214 const linkType = this.getVideoLinkType() 213 const linkType = this.getVideoLinkType()
215 214
216 if (linkType === 'internal') { 215 if (linkType === 'internal') {
217 return [ '/video-channels', channel.nameWithHost ] 216 return [ '/c', channel.nameWithHost ]
218 } 217 }
219 218
220 if (linkType === 'lazy-load') { 219 if (linkType === 'lazy-load') {
@@ -238,7 +237,10 @@ export class SearchComponent implements OnInit, OnDestroy {
238 } 237 }
239 238
240 private updateTitle () { 239 private updateTitle () {
241 const suffix = this.currentSearch ? ' ' + this.currentSearch : '' 240 const suffix = this.currentSearch
241 ? ' ' + this.currentSearch
242 : ''
243
242 this.metaService.setTitle($localize`Search` + suffix) 244 this.metaService.setTitle($localize`Search` + suffix)
243 } 245 }
244 246
diff --git a/client/src/app/+search/video-lazy-load.resolver.ts b/client/src/app/+search/video-lazy-load.resolver.ts
index d4fe6ed79..e43e0089b 100644
--- a/client/src/app/+search/video-lazy-load.resolver.ts
+++ b/client/src/app/+search/video-lazy-load.resolver.ts
@@ -28,7 +28,7 @@ export class VideoLazyLoadResolver implements Resolve<any> {
28 28
29 const video = result.data[0] 29 const video = result.data[0]
30 30
31 return this.router.navigateByUrl('/videos/watch/' + video.uuid) 31 return this.router.navigateByUrl('/w/' + video.uuid)
32 }) 32 })
33 ) 33 )
34 } 34 }
diff --git a/client/src/app/+signup/+register/register-routing.module.ts b/client/src/app/+signup/+register/register-routing.module.ts
index 61a2fa42d..dabe79fa5 100644
--- a/client/src/app/+signup/+register/register-routing.module.ts
+++ b/client/src/app/+signup/+register/register-routing.module.ts
@@ -1,14 +1,13 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { ServerConfigResolver, UnloggedGuard } from '@app/core' 3import { ServerConfigResolver, UnloggedGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core'
5import { RegisterComponent } from './register.component' 4import { RegisterComponent } from './register.component'
6 5
7const registerRoutes: Routes = [ 6const registerRoutes: Routes = [
8 { 7 {
9 path: '', 8 path: '',
10 component: RegisterComponent, 9 component: RegisterComponent,
11 canActivate: [ MetaGuard, UnloggedGuard ], 10 canActivate: [ UnloggedGuard ],
12 data: { 11 data: {
13 meta: { 12 meta: {
14 title: $localize`Register` 13 title: $localize`Register`
diff --git a/client/src/app/+signup/+register/register-step-terms.component.html b/client/src/app/+signup/+register/register-step-terms.component.html
index 1cfdc0a3a..28a6e0021 100644
--- a/client/src/app/+signup/+register/register-step-terms.component.html
+++ b/client/src/app/+signup/+register/register-step-terms.component.html
@@ -2,8 +2,8 @@
2 <div class="form-group form-group-terms"> 2 <div class="form-group form-group-terms">
3 <my-peertube-checkbox inputName="terms" formControlName="terms"> 3 <my-peertube-checkbox inputName="terms" formControlName="terms">
4 <ng-template ptTemplate="label"> 4 <ng-template ptTemplate="label">
5 <ng-container i18n> 5 <ng-container i18n>
6 I am at least 16 years old and agree 6 I am at least {{ minimumAge }} years old and agree
7 to the <a class="terms-anchor" (click)="onTermsClick($event)" href='#'>Terms</a> 7 to the <a class="terms-anchor" (click)="onTermsClick($event)" href='#'>Terms</a>
8 <ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container> 8 <ng-container *ngIf="hasCodeOfConduct"> and to the <a (click)="onCodeOfConductClick($event)" href='#'>Code of Conduct</a></ng-container>
9 of this instance 9 of this instance
diff --git a/client/src/app/+signup/+register/register-step-terms.component.ts b/client/src/app/+signup/+register/register-step-terms.component.ts
index db834c68d..20c1ae1c4 100644
--- a/client/src/app/+signup/+register/register-step-terms.component.ts
+++ b/client/src/app/+signup/+register/register-step-terms.component.ts
@@ -12,6 +12,7 @@ import { FormReactive, FormValidatorService } from '@app/shared/shared-forms'
12}) 12})
13export class RegisterStepTermsComponent extends FormReactive implements OnInit { 13export class RegisterStepTermsComponent extends FormReactive implements OnInit {
14 @Input() hasCodeOfConduct = false 14 @Input() hasCodeOfConduct = false
15 @Input() minimumAge = 16
15 16
16 @Output() formBuilt = new EventEmitter<FormGroup>() 17 @Output() formBuilt = new EventEmitter<FormGroup>()
17 @Output() termsClick = new EventEmitter<void>() 18 @Output() termsClick = new EventEmitter<void>()
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html
index dc1c7496f..de72065d3 100644
--- a/client/src/app/+signup/+register/register.component.html
+++ b/client/src/app/+signup/+register/register.component.html
@@ -17,6 +17,7 @@
17 17
18 <my-register-step-terms 18 <my-register-step-terms
19 [hasCodeOfConduct]="!!aboutHtml.codeOfConduct" 19 [hasCodeOfConduct]="!!aboutHtml.codeOfConduct"
20 [minimumAge]="minimumAge"
20 (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()" 21 (formBuilt)="onTermsFormBuilt($event)" (termsClick)="onTermsClick()" (codeOfConductClick)="onCodeOfConductClick()"
21 ></my-register-step-terms> 22 ></my-register-step-terms>
22 23
diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts
index 8e89bb01a..241ca04c6 100644
--- a/client/src/app/+signup/+register/register.component.ts
+++ b/client/src/app/+signup/+register/register.component.ts
@@ -56,6 +56,10 @@ export class RegisterComponent implements OnInit {
56 return this.serverConfig.signup.requiresEmailVerification 56 return this.serverConfig.signup.requiresEmailVerification
57 } 57 }
58 58
59 get minimumAge () {
60 return this.serverConfig.signup.minimumAge
61 }
62
59 ngOnInit (): void { 63 ngOnInit (): void {
60 this.serverConfig = this.route.snapshot.data.serverConfig 64 this.serverConfig = this.route.snapshot.data.serverConfig
61 65
diff --git a/client/src/app/+signup/+verify-account/verify-account-routing.module.ts b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
index 67c80ae93..1bc636345 100644
--- a/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
+++ b/client/src/app/+signup/+verify-account/verify-account-routing.module.ts
@@ -1,13 +1,11 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
5import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component' 3import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component'
4import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component'
6 5
7const verifyAccountRoutes: Routes = [ 6const verifyAccountRoutes: Routes = [
8 { 7 {
9 path: '', 8 path: '',
10 canActivateChild: [ MetaGuard ],
11 children: [ 9 children: [
12 { 10 {
13 path: 'email', 11 path: 'email',
diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts
index fcaad8934..4ee052873 100644
--- a/client/src/app/+video-channels/video-channels-routing.module.ts
+++ b/client/src/app/+video-channels/video-channels-routing.module.ts
@@ -1,6 +1,5 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component' 3import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-channel-playlists.component'
5import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' 4import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
6import { VideoChannelsComponent } from './video-channels.component' 5import { VideoChannelsComponent } from './video-channels.component'
@@ -9,7 +8,6 @@ const videoChannelsRoutes: Routes = [
9 { 8 {
10 path: ':videoChannelName', 9 path: ':videoChannelName',
11 component: VideoChannelsComponent, 10 component: VideoChannelsComponent,
12 canActivateChild: [ MetaGuard ],
13 children: [ 11 children: [
14 { 12 {
15 path: '', 13 path: '',
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index 41fdb5e79..3833d9c54 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -112,7 +112,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
112 } 112 }
113 113
114 getAccountUrl () { 114 getAccountUrl () {
115 return [ '/accounts', this.videoChannel.ownerBy ] 115 return [ '/a', this.videoChannel.ownerBy ]
116 } 116 }
117 117
118 private loadChannelVideosCount () { 118 private loadChannelVideosCount () {
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.html b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
index 16233f9e0..50d030ac9 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.html
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.html
@@ -76,7 +76,7 @@
76 <my-help> 76 <my-help>
77 <ng-template ptTemplate="customHtml"> 77 <ng-template ptTemplate="customHtml">
78 <ng-container i18n> 78 <ng-container i18n>
79 <a href="https://chooser-beta.creativecommons.org/" target="_blank" rel="noopener noreferrer">Choose</a> the appropriate license for your work. 79 <a href="https://chooser-beta.creativecommons.org/" target="_blank" rel="noopener noreferrer">Choose</a> the appropriate licence for your work.
80 </ng-container> 80 </ng-container>
81 </ng-template> 81 </ng-template>
82 </my-help> 82 </my-help>
diff --git a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
index 34119f7ab..3d916dbce 100644
--- a/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
+++ b/client/src/app/+videos/+video-edit/shared/video-edit.component.ts
@@ -21,8 +21,15 @@ import {
21import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms' 21import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
22import { InstanceService } from '@app/shared/shared-instance' 22import { InstanceService } from '@app/shared/shared-instance'
23import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main' 23import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
24import { LiveVideo, ServerConfig, VideoConstant, VideoDetails, VideoPrivacy } from '@shared/models' 24import {
25import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model' 25 LiveVideo,
26 RegisterClientFormFieldOptions,
27 RegisterClientVideoFieldOptions,
28 ServerConfig,
29 VideoConstant,
30 VideoDetails,
31 VideoPrivacy
32} from '@shared/models'
26import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service' 33import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
27import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component' 34import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
28import { VideoEditType } from './video-edit.type' 35import { VideoEditType } from './video-edit.type'
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
index 8e035b6bb..727bbc32f 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-go-live.component.ts
@@ -127,7 +127,7 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
127 () => { 127 () => {
128 this.notifier.success($localize`Live published.`) 128 this.notifier.success($localize`Live published.`)
129 129
130 this.router.navigate(['/videos/watch', video.uuid]) 130 this.router.navigate(['/w', video.uuid])
131 }, 131 },
132 132
133 err => { 133 err => {
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
index 3aae24732..23bd5ef76 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -5,7 +5,7 @@ import { scrollToTop } from '@app/helpers'
5import { FormValidatorService } from '@app/shared/shared-forms' 5import { FormValidatorService } from '@app/shared/shared-forms'
6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main' 6import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
7import { LoadingBarService } from '@ngx-loading-bar/core' 7import { LoadingBarService } from '@ngx-loading-bar/core'
8import { VideoPrivacy, VideoUpdate } from '@shared/models' 8import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models'
9import { hydrateFormFromVideo } from '../shared/video-edit-utils' 9import { hydrateFormFromVideo } from '../shared/video-edit-utils'
10import { VideoSend } from './video-send' 10import { VideoSend } from './video-send'
11 11
@@ -113,7 +113,13 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
113 this.loadingBar.useRef().complete() 113 this.loadingBar.useRef().complete()
114 this.isImportingVideo = false 114 this.isImportingVideo = false
115 this.firstStepError.emit() 115 this.firstStepError.emit()
116 this.notifier.error(err.message) 116
117 let message = err.message
118 if (err.body?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) {
119 message = $localize`Torrents with only 1 file are supported.`
120 }
121
122 this.notifier.error(message)
117 } 123 }
118 ) 124 )
119 } 125 }
diff --git a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
index bca1b6eb6..e20f08879 100644
--- a/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add-components/video-upload.component.ts
@@ -244,7 +244,7 @@ export class VideoUploadComponent extends VideoSend implements OnInit, OnDestroy
244 this.isUploadingVideo = false 244 this.isUploadingVideo = false
245 245
246 this.notifier.success($localize`Video published.`) 246 this.notifier.success($localize`Video published.`)
247 this.router.navigate([ '/videos/watch', video.uuid ]) 247 this.router.navigate([ '/w', video.uuid ])
248 }, 248 },
249 249
250 err => { 250 err => {
diff --git a/client/src/app/+videos/+video-edit/video-add-routing.module.ts b/client/src/app/+videos/+video-edit/video-add-routing.module.ts
index 9ff66bea0..3b9a5ab3a 100644
--- a/client/src/app/+videos/+video-edit/video-add-routing.module.ts
+++ b/client/src/app/+videos/+video-edit/video-add-routing.module.ts
@@ -1,14 +1,13 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { CanDeactivateGuard, LoginGuard } from '@app/core' 3import { CanDeactivateGuard, LoginGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core'
5import { VideoAddComponent } from './video-add.component' 4import { VideoAddComponent } from './video-add.component'
6 5
7const videoAddRoutes: Routes = [ 6const videoAddRoutes: Routes = [
8 { 7 {
9 path: '', 8 path: '',
10 component: VideoAddComponent, 9 component: VideoAddComponent,
11 canActivate: [ MetaGuard, LoginGuard ], 10 canActivate: [ LoginGuard ],
12 canDeactivate: [ CanDeactivateGuard ] 11 canDeactivate: [ CanDeactivateGuard ]
13 } 12 }
14] 13]
diff --git a/client/src/app/+videos/+video-edit/video-add.component.html b/client/src/app/+videos/+video-edit/video-add.component.html
index dc8c2f21d..ac75d9ff8 100644
--- a/client/src/app/+videos/+video-edit/video-add.component.html
+++ b/client/src/app/+videos/+video-edit/video-add.component.html
@@ -20,8 +20,8 @@
20 <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container> 20 <ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
21 </div> 21 </div>
22 22
23 <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [ngClass]="{ 'hide-nav': secondStepType !== undefined }"> 23 <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" [ngClass]="{ 'hide-nav': !!secondStepType }">
24 <ng-container ngbNavItem> 24 <ng-container ngbNavItem="upload">
25 <a ngbNavLink> 25 <a ngbNavLink>
26 <span i18n>Upload a file</span> 26 <span i18n>Upload a file</span>
27 </a> 27 </a>
@@ -31,7 +31,7 @@
31 </ng-template> 31 </ng-template>
32 </ng-container> 32 </ng-container>
33 33
34 <ng-container ngbNavItem *ngIf="isVideoImportHttpEnabled()"> 34 <ng-container ngbNavItem="import-url" *ngIf="isVideoImportHttpEnabled()">
35 <a ngbNavLink> 35 <a ngbNavLink>
36 <span i18n>Import with URL</span> 36 <span i18n>Import with URL</span>
37 </a> 37 </a>
@@ -41,7 +41,7 @@
41 </ng-template> 41 </ng-template>
42 </ng-container> 42 </ng-container>
43 43
44 <ng-container ngbNavItem *ngIf="isVideoImportTorrentEnabled()"> 44 <ng-container ngbNavItem="import-torrent" *ngIf="isVideoImportTorrentEnabled()">
45 <a ngbNavLink> 45 <a ngbNavLink>
46 <span i18n>Import with torrent</span> 46 <span i18n>Import with torrent</span>
47 </a> 47 </a>
@@ -51,7 +51,7 @@
51 </ng-template> 51 </ng-template>
52 </ng-container> 52 </ng-container>
53 53
54 <ng-container ngbNavItem *ngIf="isVideoLiveEnabled()"> 54 <ng-container ngbNavItem="go-live" *ngIf="isVideoLiveEnabled()">
55 <a ngbNavLink> 55 <a ngbNavLink>
56 <span i18n>Go live</span> 56 <span i18n>Go live</span>
57 </a> 57 </a>
diff --git a/client/src/app/+videos/+video-edit/video-add.component.ts b/client/src/app/+videos/+video-edit/video-add.component.ts
index 441d5a3db..d735c936c 100644
--- a/client/src/app/+videos/+video-edit/video-add.component.ts
+++ b/client/src/app/+videos/+video-edit/video-add.component.ts
@@ -1,4 +1,5 @@
1import { Component, HostListener, OnInit, ViewChild } from '@angular/core' 1import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
2import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core' 3import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core'
3import { ServerConfig } from '@shared/models' 4import { ServerConfig } from '@shared/models'
4import { VideoEditType } from './shared/video-edit.type' 5import { VideoEditType } from './shared/video-edit.type'
@@ -22,11 +23,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
22 23
23 secondStepType: VideoEditType 24 secondStepType: VideoEditType
24 videoName: string 25 videoName: string
25 serverConfig: ServerConfig 26
27 activeNav: string
28
29 private serverConfig: ServerConfig
26 30
27 constructor ( 31 constructor (
28 private auth: AuthService, 32 private auth: AuthService,
29 private serverService: ServerService 33 private serverService: ServerService,
34 private route: ActivatedRoute,
35 private router: Router
30 ) {} 36 ) {}
31 37
32 get userInformationLoaded () { 38 get userInformationLoaded () {
@@ -42,6 +48,16 @@ export class VideoAddComponent implements OnInit, CanComponentDeactivate {
42 .subscribe(config => this.serverConfig = config) 48 .subscribe(config => this.serverConfig = config)
43 49
44 this.user = this.auth.getUser() 50 this.user = this.auth.getUser()
51
52 if (this.route.snapshot.fragment) {
53 this.onNavChange(this.route.snapshot.fragment)
54 }
55 }
56
57 onNavChange (newActiveNav: string) {
58 this.activeNav = newActiveNav
59
60 this.router.navigate([], { fragment: this.activeNav })
45 } 61 }
46 62
47 onFirstStepDone (type: VideoEditType, videoName: string) { 63 onFirstStepDone (type: VideoEditType, videoName: string) {
diff --git a/client/src/app/+videos/+video-edit/video-update-routing.module.ts b/client/src/app/+videos/+video-edit/video-update-routing.module.ts
index a04351b05..ba9167dd0 100644
--- a/client/src/app/+videos/+video-edit/video-update-routing.module.ts
+++ b/client/src/app/+videos/+video-edit/video-update-routing.module.ts
@@ -1,7 +1,6 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { CanDeactivateGuard, LoginGuard } from '@app/core' 3import { CanDeactivateGuard, LoginGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core'
5import { VideoUpdateComponent } from './video-update.component' 4import { VideoUpdateComponent } from './video-update.component'
6import { VideoUpdateResolver } from './video-update.resolver' 5import { VideoUpdateResolver } from './video-update.resolver'
7 6
@@ -9,7 +8,7 @@ const videoUpdateRoutes: Routes = [
9 { 8 {
10 path: '', 9 path: '',
11 component: VideoUpdateComponent, 10 component: VideoUpdateComponent,
12 canActivate: [ MetaGuard, LoginGuard ], 11 canActivate: [ LoginGuard ],
13 canDeactivate: [ CanDeactivateGuard ], 12 canDeactivate: [ CanDeactivateGuard ],
14 resolve: { 13 resolve: {
15 videoData: VideoUpdateResolver 14 videoData: VideoUpdateResolver
diff --git a/client/src/app/+videos/+video-edit/video-update.component.html b/client/src/app/+videos/+video-edit/video-update.component.html
index 3ce3e623e..9629081e3 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.html
+++ b/client/src/app/+videos/+video-edit/video-update.component.html
@@ -1,7 +1,7 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <div class="title-page title-page-single"> 2 <div class="title-page title-page-single">
3 <span class="mr-1" i18n>Update</span> 3 <span class="mr-1" i18n>Update</span>
4 <a [routerLink]="[ '/videos/watch', video.uuid ]">{{ video?.name }}</a> 4 <a [routerLink]="[ '/w', video.uuid ]">{{ video?.name }}</a>
5 </div> 5 </div>
6 6
7 <form novalidate [formGroup]="form"> 7 <form novalidate [formGroup]="form">
diff --git a/client/src/app/+videos/+video-edit/video-update.component.ts b/client/src/app/+videos/+video-edit/video-update.component.ts
index 2973c6840..574669a23 100644
--- a/client/src/app/+videos/+video-edit/video-update.component.ts
+++ b/client/src/app/+videos/+video-edit/video-update.component.ts
@@ -156,7 +156,7 @@ export class VideoUpdateComponent extends FormReactive implements OnInit {
156 this.isUpdatingVideo = false 156 this.isUpdatingVideo = false
157 this.loadingBar.useRef().complete() 157 this.loadingBar.useRef().complete()
158 this.notifier.success($localize`Video updated.`) 158 this.notifier.success($localize`Video updated.`)
159 this.router.navigate([ '/videos/watch', this.video.uuid ]) 159 this.router.navigate([ '/w', this.video.uuid ])
160 }, 160 },
161 161
162 err => { 162 err => {
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/comment/video-comment.component.html
index d7ba40ef6..06548edc8 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment.component.html
+++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.html
@@ -11,7 +11,7 @@
11 11
12 <div class="comment-account-date"> 12 <div class="comment-account-date">
13 <div class="comment-account"> 13 <div class="comment-account">
14 <a [routerLink]="[ '/accounts', comment.by ]"> 14 <a [routerLink]="[ '/a', comment.by ]">
15 <span class="comment-account-name" [ngClass]="{ 'video-author': video.account.id === comment.account.id }"> 15 <span class="comment-account-name" [ngClass]="{ 'video-author': video.account.id === comment.account.id }">
16 {{ comment.account.displayName }} 16 {{ comment.account.displayName }}
17 </span> 17 </span>
@@ -20,7 +20,7 @@
20 </a> 20 </a>
21 </div> 21 </div>
22 22
23 <a [routerLink]="['/videos/watch', video.uuid, { 'threadId': comment.threadId }]" class="comment-date" [title]="comment.createdAt"> 23 <a [routerLink]="['/w', video.uuid, { 'threadId': comment.threadId }]" class="comment-date" [title]="comment.createdAt">
24 {{ comment.createdAt | myFromNow }} 24 {{ comment.createdAt | myFromNow }}
25 </a> 25 </a>
26 </div> 26 </div>
@@ -45,7 +45,7 @@
45 <ng-container *ngIf="comment.isDeleted"> 45 <ng-container *ngIf="comment.isDeleted">
46 <div class="comment-account-date"> 46 <div class="comment-account-date">
47 <span class="comment-account" i18n>Deleted</span> 47 <span class="comment-account" i18n>Deleted</span>
48 <a [routerLink]="['/videos/watch', video.uuid, { 'threadId': comment.threadId }]" 48 <a [routerLink]="['/w', video.uuid, { 'threadId': comment.threadId }]"
49 class="comment-date">{{ comment.createdAt | myFromNow }}</a> 49 class="comment-date">{{ comment.createdAt | myFromNow }}</a>
50 </div> 50 </div>
51 51
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts
index fd379e80e..04f8f0d58 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment.component.ts
+++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.ts
@@ -161,7 +161,7 @@ export class VideoCommentComponent implements OnInit, OnChanges {
161 // Before HTML rendering restore line feed for markdown list compatibility 161 // Before HTML rendering restore line feed for markdown list compatibility
162 const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n') 162 const commentText = this.comment.text.replace(/<br.?\/?>/g, '\r\n')
163 const html = await this.markdownService.textMarkdownToHTML(commentText, true, true) 163 const html = await this.markdownService.textMarkdownToHTML(commentText, true, true)
164 this.sanitizedCommentHTML = await this.markdownService.processVideoTimestamps(html) 164 this.sanitizedCommentHTML = this.markdownService.processVideoTimestamps(html)
165 this.newParentComments = this.parentComments.concat([ this.comment ]) 165 this.newParentComments = this.parentComments.concat([ this.comment ])
166 166
167 if (this.comment.account) { 167 if (this.comment.account) {
diff --git a/client/src/app/+videos/+video-watch/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
index 5f149cbd1..5a7221858 100644
--- a/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
+++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
@@ -1,11 +1,11 @@
1<div class="wrapper" [ngClass]="{ 'generic-channel': genericChannel }"> 1<div class="wrapper" [ngClass]="{ 'generic-channel': genericChannel }">
2 <my-actor-avatar 2 <my-actor-avatar
3 class="channel" [channel]="video.channel" 3 class="channel" [channel]="video.channel"
4 [internalHref]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle" 4 [internalHref]="[ '/c', video.byVideoChannel ]" [title]="channelLinkTitle"
5 ></my-actor-avatar> 5 ></my-actor-avatar>
6 6
7 <my-actor-avatar 7 <my-actor-avatar
8 class="account" [account]="video.account" 8 class="account" [account]="video.account"
9 [internalHref]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> 9 [internalHref]="[ '/a', video.byAccount ]" [title]="accountLinkTitle">
10 </my-actor-avatar> 10 </my-actor-avatar>
11</div> 11</div>
diff --git a/client/src/app/+videos/+video-watch/video-watch-routing.module.ts b/client/src/app/+videos/+video-watch/video-watch-routing.module.ts
index d8fecb87d..657fd10f8 100644
--- a/client/src/app/+videos/+video-watch/video-watch-routing.module.ts
+++ b/client/src/app/+videos/+video-watch/video-watch-routing.module.ts
@@ -1,13 +1,11 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { MetaGuard } from '@ngx-meta/core'
4import { VideoWatchComponent } from './video-watch.component' 3import { VideoWatchComponent } from './video-watch.component'
5 4
6const videoWatchRoutes: Routes = [ 5const videoWatchRoutes: Routes = [
7 { 6 {
8 path: 'playlist/:playlistId', 7 path: 'p/:playlistId',
9 component: VideoWatchComponent, 8 component: VideoWatchComponent
10 canActivate: [ MetaGuard ]
11 }, 9 },
12 { 10 {
13 path: ':videoId/comments/:commentId', 11 path: ':videoId/comments/:commentId',
@@ -15,8 +13,7 @@ const videoWatchRoutes: Routes = [
15 }, 13 },
16 { 14 {
17 path: ':videoId', 15 path: ':videoId',
18 component: VideoWatchComponent, 16 component: VideoWatchComponent
19 canActivate: [ MetaGuard ]
20 } 17 }
21] 18]
22 19
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html
index 4779602d2..bb41fba77 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -183,16 +183,16 @@
183 183
184 <div class="video-info-channel-left-links ml-1"> 184 <div class="video-info-channel-left-links ml-1">
185 <ng-container *ngIf="!isChannelDisplayNameGeneric()"> 185 <ng-container *ngIf="!isChannelDisplayNameGeneric()">
186 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" i18n-title title="Channel page"> 186 <a [routerLink]="[ '/c', video.byVideoChannel ]" i18n-title title="Channel page">
187 {{ video.channel.displayName }} 187 {{ video.channel.displayName }}
188 </a> 188 </a>
189 <a [routerLink]="[ '/accounts', video.byAccount ]" i18n-title title="Account page"> 189 <a [routerLink]="[ '/a', video.byAccount ]" i18n-title title="Account page">
190 <span i18n>By {{ video.byAccount }}</span> 190 <span i18n>By {{ video.byAccount }}</span>
191 </a> 191 </a>
192 </ng-container> 192 </ng-container>
193 193
194 <ng-container *ngIf="isChannelDisplayNameGeneric()"> 194 <ng-container *ngIf="isChannelDisplayNameGeneric()">
195 <a [routerLink]="[ '/accounts', video.byAccount ]" class="single-link" i18n-title title="Account page"> 195 <a [routerLink]="[ '/a', video.byAccount ]" class="single-link" i18n-title title="Account page">
196 <span i18n>{{ video.byAccount }}</span> 196 <span i18n>{{ video.byAccount }}</span>
197 </a> 197 </a>
198 </ng-container> 198 </ng-container>
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts
index 116139d47..0acd44524 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/+videos/+video-watch/video-watch.component.ts
@@ -9,6 +9,7 @@ import {
9 AuthUser, 9 AuthUser,
10 ConfirmService, 10 ConfirmService,
11 MarkdownService, 11 MarkdownService,
12 MetaService,
12 Notifier, 13 Notifier,
13 PeerTubeSocket, 14 PeerTubeSocket,
14 RestExtractor, 15 RestExtractor,
@@ -25,7 +26,6 @@ import { SupportModalComponent } from '@app/shared/shared-support-modal'
25import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' 26import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
26import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature' 27import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/shared-video-miniature'
27import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist' 28import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
28import { MetaService } from '@ngx-meta/core'
29import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage' 29import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' 30import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
31import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models' 31import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
@@ -509,7 +509,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
509 509
510 private async setVideoDescriptionHTML () { 510 private async setVideoDescriptionHTML () {
511 const html = await this.markdownService.textMarkdownToHTML(this.video.description) 511 const html = await this.markdownService.textMarkdownToHTML(this.video.description)
512 this.videoHTMLDescription = await this.markdownService.processVideoTimestamps(html) 512 this.videoHTMLDescription = this.markdownService.processVideoTimestamps(html)
513 } 513 }
514 514
515 private setVideoLikesBarTooltipText () { 515 private setVideoLikesBarTooltipText () {
@@ -690,7 +690,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
690 if (this.playlist) { 690 if (this.playlist) {
691 this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo()) 691 this.zone.run(() => this.videoWatchPlaylist.navigateToNextPlaylistVideo())
692 } else if (this.nextVideoUuid) { 692 } else if (this.nextVideoUuid) {
693 this.router.navigate([ '/videos/watch', this.nextVideoUuid ]) 693 this.router.navigate([ '/w', this.nextVideoUuid ])
694 } 694 }
695 } 695 }
696 696
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.html b/client/src/app/+videos/video-list/overview/video-overview.component.html
index e21bffb6c..d3c602aa5 100644
--- a/client/src/app/+videos/video-list/overview/video-overview.component.html
+++ b/client/src/app/+videos/video-list/overview/video-overview.component.html
@@ -32,7 +32,7 @@
32 32
33 <div class="section channel videos" *ngFor="let object of overview.channels"> 33 <div class="section channel videos" *ngFor="let object of overview.channels">
34 <div class="section-title"> 34 <div class="section-title">
35 <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]"> 35 <a [routerLink]="[ '/c', buildVideoChannelBy(object) ]">
36 <my-actor-avatar [channel]="buildVideoChannel(object)"></my-actor-avatar> 36 <my-actor-avatar [channel]="buildVideoChannel(object)"></my-actor-avatar>
37 37
38 <h2 class="section-title">{{ object.channel.displayName }}</h2> 38 <h2 class="section-title">{{ object.channel.displayName }}</h2>
diff --git a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
index 55040f3c9..bbb02a236 100644
--- a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
+++ b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
@@ -31,7 +31,8 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
31 private route: ActivatedRoute, 31 private route: ActivatedRoute,
32 private router: Router, 32 private router: Router,
33 private auth: AuthService, 33 private auth: AuthService,
34 private serverService: ServerService 34 private serverService: ServerService,
35 private redirectService: RedirectService
35 ) { 36 ) {
36 super(data) 37 super(data)
37 38
@@ -84,12 +85,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
84 85
85 this.algorithmChangeSub = this.route.queryParams.subscribe( 86 this.algorithmChangeSub = this.route.queryParams.subscribe(
86 queryParams => { 87 queryParams => {
87 const algorithm = queryParams['alg'] 88 this.data.model = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
88 if (algorithm) {
89 this.data.model = algorithm
90 } else {
91 this.data.model = RedirectService.DEFAULT_TRENDING_ALGORITHM
92 }
93 } 89 }
94 ) 90 )
95 } 91 }
@@ -99,7 +95,7 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
99 } 95 }
100 96
101 setSort () { 97 setSort () {
102 const alg = this.data.model !== RedirectService.DEFAULT_TRENDING_ALGORITHM 98 const alg = this.data.model !== this.redirectService.getDefaultTrendingAlgorithm()
103 ? this.data.model 99 ? this.data.model
104 : undefined 100 : undefined
105 101
diff --git a/client/src/app/+videos/video-list/trending/video-trending.component.ts b/client/src/app/+videos/video-list/trending/video-trending.component.ts
index e50d6ec3a..ebec672f3 100644
--- a/client/src/app/+videos/video-list/trending/video-trending.component.ts
+++ b/client/src/app/+videos/video-list/trending/video-trending.component.ts
@@ -35,11 +35,12 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
35 protected storageService: LocalStorageService, 35 protected storageService: LocalStorageService,
36 protected cfr: ComponentFactoryResolver, 36 protected cfr: ComponentFactoryResolver,
37 private videoService: VideoService, 37 private videoService: VideoService,
38 private redirectService: RedirectService,
38 private hooks: HooksService 39 private hooks: HooksService
39 ) { 40 ) {
40 super() 41 super()
41 42
42 this.defaultSort = this.parseAlgorithm(RedirectService.DEFAULT_TRENDING_ALGORITHM) 43 this.defaultSort = this.parseAlgorithm(this.redirectService.getDefaultTrendingAlgorithm())
43 44
44 this.headerComponentInjector = this.getInjector() 45 this.headerComponentInjector = this.getInjector()
45 } 46 }
@@ -106,7 +107,7 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
106 } 107 }
107 108
108 protected loadPageRouteParams (queryParams: Params) { 109 protected loadPageRouteParams (queryParams: Params) {
109 const algorithm = queryParams['alg'] || RedirectService.DEFAULT_TRENDING_ALGORITHM 110 const algorithm = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
110 111
111 this.sort = this.parseAlgorithm(algorithm) 112 this.sort = this.parseAlgorithm(algorithm)
112 } 113 }
@@ -115,8 +116,10 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
115 switch (algorithm) { 116 switch (algorithm) {
116 case 'most-viewed': 117 case 'most-viewed':
117 return '-trending' 118 return '-trending'
119
118 case 'most-liked': 120 case 'most-liked':
119 return '-likes' 121 return '-likes'
122
120 default: 123 default:
121 return '-' + algorithm as VideoSortField 124 return '-' + algorithm as VideoSortField
122 } 125 }
diff --git a/client/src/app/+videos/videos-routing.module.ts b/client/src/app/+videos/videos-routing.module.ts
index 16e3b9bb2..926dfaab0 100644
--- a/client/src/app/+videos/videos-routing.module.ts
+++ b/client/src/app/+videos/videos-routing.module.ts
@@ -1,7 +1,6 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { LoginGuard } from '@app/core' 3import { LoginGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core'
5import { VideoTrendingComponent } from './video-list' 4import { VideoTrendingComponent } from './video-list'
6import { VideoOverviewComponent } from './video-list/overview/video-overview.component' 5import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
7import { VideoLocalComponent } from './video-list/video-local.component' 6import { VideoLocalComponent } from './video-list/video-local.component'
@@ -13,7 +12,6 @@ const videosRoutes: Routes = [
13 { 12 {
14 path: '', 13 path: '',
15 component: VideosComponent, 14 component: VideosComponent,
16 canActivateChild: [ MetaGuard ],
17 children: [ 15 children: [
18 { 16 {
19 path: 'overview', 17 path: 'overview',
@@ -76,31 +74,6 @@ const videosRoutes: Routes = [
76 key: 'local-videos-list' 74 key: 'local-videos-list'
77 } 75 }
78 } 76 }
79 },
80 {
81 path: 'upload',
82 loadChildren: () => import('@app/+videos/+video-edit/video-add.module').then(m => m.VideoAddModule),
83 data: {
84 meta: {
85 title: $localize`Upload a video`
86 }
87 }
88 },
89 {
90 path: 'update/:uuid',
91 loadChildren: () => import('@app/+videos/+video-edit/video-update.module').then(m => m.VideoUpdateModule),
92 data: {
93 meta: {
94 title: $localize`Edit a video`
95 }
96 }
97 },
98 {
99 path: 'watch',
100 loadChildren: () => import('@app/+videos/+video-watch/video-watch.module').then(m => m.VideoWatchModule),
101 data: {
102 preload: 3000
103 }
104 } 77 }
105 ] 78 ]
106 } 79 }
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index 3ea5b7e5e..e35f540be 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -1,70 +1,159 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router' 2import { RouteReuseStrategy, RouterModule, Routes, UrlMatchResult, UrlSegment } from '@angular/router'
3import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' 3import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
4import { MenuGuards } from '@app/core/routing/menu-guard.service' 4import { MenuGuards } from '@app/core/routing/menu-guard.service'
5import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n' 5import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
6import { PreloadSelectedModulesList } from './core' 6import { MetaGuard, PreloadSelectedModulesList } from './core'
7import { EmptyComponent } from './empty.component' 7import { EmptyComponent } from './empty.component'
8import { USER_USERNAME_REGEX_CHARACTERS } from './shared/form-validators/user-validators'
9import { ActorRedirectGuard } from './shared/shared-main'
8 10
9const routes: Routes = [ 11const routes: Routes = [
10 { 12 {
11 path: 'admin', 13 path: 'admin',
12 canActivate: [ MenuGuards.close() ], 14 canActivate: [ MenuGuards.close() ],
13 canDeactivate: [ MenuGuards.open() ], 15 canDeactivate: [ MenuGuards.open() ],
14 loadChildren: () => import('./+admin/admin.module').then(m => m.AdminModule) 16 loadChildren: () => import('./+admin/admin.module').then(m => m.AdminModule),
17 canActivateChild: [ MetaGuard ]
18 },
19 {
20 path: 'home',
21 loadChildren: () => import('./+home/home.module').then(m => m.HomeModule),
22 canActivateChild: [ MetaGuard ]
15 }, 23 },
16 { 24 {
17 path: 'my-account', 25 path: 'my-account',
18 loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule) 26 loadChildren: () => import('./+my-account/my-account.module').then(m => m.MyAccountModule),
27 canActivateChild: [ MetaGuard ]
19 }, 28 },
20 { 29 {
21 path: 'my-library', 30 path: 'my-library',
22 loadChildren: () => import('./+my-library/my-library.module').then(m => m.MyLibraryModule) 31 loadChildren: () => import('./+my-library/my-library.module').then(m => m.MyLibraryModule),
32 canActivateChild: [ MetaGuard ]
23 }, 33 },
24 { 34 {
25 path: 'verify-account', 35 path: 'verify-account',
26 loadChildren: () => import('./+signup/+verify-account/verify-account.module').then(m => m.VerifyAccountModule) 36 loadChildren: () => import('./+signup/+verify-account/verify-account.module').then(m => m.VerifyAccountModule),
37 canActivateChild: [ MetaGuard ]
27 }, 38 },
39
28 { 40 {
29 path: 'accounts', 41 path: 'accounts',
30 loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule) 42 redirectTo: 'a'
31 }, 43 },
32 { 44 {
45 path: 'a',
46 loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule),
47 canActivateChild: [ MetaGuard ]
48 },
49
50 {
33 path: 'video-channels', 51 path: 'video-channels',
34 loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule) 52 redirectTo: 'c'
53 },
54 {
55 path: 'c',
56 loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule),
57 canActivateChild: [ MetaGuard ]
35 }, 58 },
59
36 { 60 {
37 path: 'about', 61 path: 'about',
38 loadChildren: () => import('./+about/about.module').then(m => m.AboutModule) 62 loadChildren: () => import('./+about/about.module').then(m => m.AboutModule),
63 canActivateChild: [ MetaGuard ]
39 }, 64 },
40 { 65 {
41 path: 'signup', 66 path: 'signup',
42 loadChildren: () => import('./+signup/+register/register.module').then(m => m.RegisterModule) 67 loadChildren: () => import('./+signup/+register/register.module').then(m => m.RegisterModule),
68 canActivateChild: [ MetaGuard ]
43 }, 69 },
44 { 70 {
45 path: 'reset-password', 71 path: 'reset-password',
46 loadChildren: () => import('./+reset-password/reset-password.module').then(m => m.ResetPasswordModule) 72 loadChildren: () => import('./+reset-password/reset-password.module').then(m => m.ResetPasswordModule),
73 canActivateChild: [ MetaGuard ]
47 }, 74 },
48 { 75 {
49 path: 'login', 76 path: 'login',
50 loadChildren: () => import('./+login/login.module').then(m => m.LoginModule) 77 loadChildren: () => import('./+login/login.module').then(m => m.LoginModule),
78 canActivateChild: [ MetaGuard ]
51 }, 79 },
52 { 80 {
53 path: 'search', 81 path: 'search',
54 loadChildren: () => import('./+search/search.module').then(m => m.SearchModule) 82 loadChildren: () => import('./+search/search.module').then(m => m.SearchModule),
83 canActivateChild: [ MetaGuard ]
55 }, 84 },
85
56 { 86 {
57 path: 'videos', 87 path: 'videos/upload',
58 loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule) 88 loadChildren: () => import('@app/+videos/+video-edit/video-add.module').then(m => m.VideoAddModule),
89 data: {
90 meta: {
91 title: $localize`Upload a video`
92 }
93 }
59 }, 94 },
60 { 95 {
61 path: 'remote-interaction', 96 path: 'videos/update/:uuid',
62 loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule) 97 loadChildren: () => import('@app/+videos/+video-edit/video-update.module').then(m => m.VideoUpdateModule),
98 data: {
99 meta: {
100 title: $localize`Edit a video`
101 }
102 }
103 },
104
105 {
106 path: 'videos/watch/playlist',
107 redirectTo: 'w/p'
108 },
109 {
110 path: 'videos/watch',
111 redirectTo: 'w'
112 },
113 {
114 path: 'w',
115 loadChildren: () => import('@app/+videos/+video-watch/video-watch.module').then(m => m.VideoWatchModule),
116 data: {
117 preload: 3000
118 }
119 },
120 {
121 path: 'videos',
122 loadChildren: () => import('./+videos/videos.module').then(m => m.VideosModule),
123 canActivateChild: [ MetaGuard ]
63 }, 124 },
64 { 125 {
65 path: 'video-playlists/watch', 126 path: 'video-playlists/watch',
66 redirectTo: 'videos/watch/playlist' 127 redirectTo: 'videos/watch/playlist'
67 }, 128 },
129
130 {
131 path: 'remote-interaction',
132 loadChildren: () => import('./+remote-interaction/remote-interaction.module').then(m => m.RemoteInteractionModule),
133 canActivateChild: [ MetaGuard ]
134 },
135
136 // Matches /@:actorName
137 {
138 matcher: (url): UrlMatchResult => {
139 const regex = new RegExp(`^@(${USER_USERNAME_REGEX_CHARACTERS}+)$`)
140 if (url.length !== 1) return null
141
142 const matchResult = url[0].path.match(regex)
143 if (!matchResult) return null
144
145 return {
146 consumed: url,
147 posParams: {
148 actorName: new UrlSegment(matchResult[1], {})
149 }
150 }
151 },
152 pathMatch: 'full',
153 canActivate: [ ActorRedirectGuard ],
154 component: EmptyComponent
155 },
156
68 { 157 {
69 path: '', 158 path: '',
70 component: EmptyComponent // Avoid 404, app component will redirect dynamically 159 component: EmptyComponent // Avoid 404, app component will redirect dynamically
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index 66d871b4a..863c3f3b5 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -67,7 +67,7 @@ export class AppComponent implements OnInit, AfterViewInit {
67 } 67 }
68 68
69 goToDefaultRoute () { 69 goToDefaultRoute () {
70 return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE) 70 return this.router.navigateByUrl(this.redirectService.getDefaultRoute())
71 } 71 }
72 72
73 ngOnInit () { 73 ngOnInit () {
@@ -231,7 +231,7 @@ export class AppComponent implements OnInit, AfterViewInit {
231 } 231 }
232 232
233 this.broadcastMessage = { 233 this.broadcastMessage = {
234 message: await this.markdownService.completeMarkdownToHTML(messageConfig.message), 234 message: await this.markdownService.unsafeMarkdownToHTML(messageConfig.message, true),
235 dismissable: messageConfig.dismissable, 235 dismissable: messageConfig.dismissable,
236 class: classes[messageConfig.level] 236 class: classes[messageConfig.level]
237 } 237 }
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 3cec6d739..9f46d49a2 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -4,9 +4,7 @@ import { APP_BASE_HREF, registerLocaleData } from '@angular/common'
4import { NgModule } from '@angular/core' 4import { NgModule } from '@angular/core'
5import { BrowserModule } from '@angular/platform-browser' 5import { BrowserModule } from '@angular/platform-browser'
6import { ServiceWorkerModule } from '@angular/service-worker' 6import { ServiceWorkerModule } from '@angular/service-worker'
7import { ServerService } from '@app/core'
8import localeOc from '@app/helpers/locales/oc' 7import localeOc from '@app/helpers/locales/oc'
9import { MetaLoader, MetaModule, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
10import { AppRoutingModule } from './app-routing.module' 8import { AppRoutingModule } from './app-routing.module'
11import { AppComponent } from './app.component' 9import { AppComponent } from './app.component'
12import { CoreModule } from './core' 10import { CoreModule } from './core'
@@ -19,12 +17,12 @@ import { CustomModalComponent } from './modal/custom-modal.component'
19import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component' 17import { InstanceConfigWarningModalComponent } from './modal/instance-config-warning-modal.component'
20import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component' 18import { QuickSettingsModalComponent } from './modal/quick-settings-modal.component'
21import { WelcomeModalComponent } from './modal/welcome-modal.component' 19import { WelcomeModalComponent } from './modal/welcome-modal.component'
20import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
22import { SharedFormModule } from './shared/shared-forms' 21import { SharedFormModule } from './shared/shared-forms'
23import { SharedGlobalIconModule } from './shared/shared-icons' 22import { SharedGlobalIconModule } from './shared/shared-icons'
24import { SharedInstanceModule } from './shared/shared-instance' 23import { SharedInstanceModule } from './shared/shared-instance'
25import { SharedMainModule } from './shared/shared-main' 24import { SharedMainModule } from './shared/shared-main'
26import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings' 25import { SharedUserInterfaceSettingsModule } from './shared/shared-user-settings'
27import { SharedActorImageModule } from './shared/shared-actor-image/shared-actor-image.module'
28 26
29registerLocaleData(localeOc, 'oc') 27registerLocaleData(localeOc, 'oc')
30 28
@@ -62,22 +60,6 @@ registerLocaleData(localeOc, 'oc')
62 SharedInstanceModule, 60 SharedInstanceModule,
63 SharedActorImageModule, 61 SharedActorImageModule,
64 62
65 MetaModule.forRoot({
66 provide: MetaLoader,
67 useFactory: (serverService: ServerService) => {
68 return new MetaStaticLoader({
69 pageTitlePositioning: PageTitlePositioning.PrependPageTitle,
70 pageTitleSeparator: ' - ',
71 get applicationName () { return serverService.getTmpConfig().instance.name },
72 defaults: {
73 get title () { return serverService.getTmpConfig().instance.name },
74 get description () { return serverService.getTmpConfig().instance.shortDescription }
75 }
76 })
77 },
78 deps: [ ServerService ]
79 }),
80
81 AppRoutingModule // Put it after all the module because it has the 404 route 63 AppRoutingModule // Put it after all the module because it has the 404 route
82 ], 64 ],
83 65
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index 3152a7003..de3274544 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -14,7 +14,7 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
14import { Notifier } from './notification' 14import { Notifier } from './notification'
15import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer' 15import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
16import { RestExtractor, RestService } from './rest' 16import { RestExtractor, RestService } from './rest'
17import { LoginGuard, RedirectService, UnloggedGuard, UserRightGuard } from './routing' 17import { LoginGuard, MetaGuard, MetaService, RedirectService, UnloggedGuard, UserRightGuard } from './routing'
18import { CanDeactivateGuard } from './routing/can-deactivate-guard.service' 18import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
19import { ServerConfigResolver } from './routing/server-config-resolver.service' 19import { ServerConfigResolver } from './routing/server-config-resolver.service'
20import { ScopedTokensService } from './scoped-tokens' 20import { ScopedTokensService } from './scoped-tokens'
@@ -77,7 +77,10 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra
77 MessageService, 77 MessageService,
78 PeerTubeSocket, 78 PeerTubeSocket,
79 ServerConfigResolver, 79 ServerConfigResolver,
80 CanDeactivateGuard 80 CanDeactivateGuard,
81
82 MetaService,
83 MetaGuard
81 ] 84 ]
82}) 85})
83export class CoreModule { 86export class CoreModule {
diff --git a/client/src/app/core/menu/menu.service.ts b/client/src/app/core/menu/menu.service.ts
index 502d3bb2f..77592cbb6 100644
--- a/client/src/app/core/menu/menu.service.ts
+++ b/client/src/app/core/menu/menu.service.ts
@@ -1,8 +1,19 @@
1import { fromEvent } from 'rxjs' 1import { fromEvent } from 'rxjs'
2import { debounceTime } from 'rxjs/operators' 2import { debounceTime } from 'rxjs/operators'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { GlobalIconName } from '@app/shared/shared-icons'
5import { sortObjectComparator } from '@shared/core-utils/miscs/miscs'
6import { ServerConfig } from '@shared/models/server'
4import { ScreenService } from '../wrappers' 7import { ScreenService } from '../wrappers'
5 8
9export type MenuLink = {
10 icon: GlobalIconName
11 label: string
12 menuLabel: string
13 path: string
14 priority: number
15}
16
6@Injectable() 17@Injectable()
7export class MenuService { 18export class MenuService {
8 isMenuDisplayed = true 19 isMenuDisplayed = true
@@ -48,6 +59,53 @@ export class MenuService {
48 this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser 59 this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
49 } 60 }
50 61
62 buildCommonLinks (config: ServerConfig) {
63 let entries: MenuLink[] = [
64 {
65 icon: 'globe' as 'globe',
66 label: $localize`Discover videos`,
67 menuLabel: $localize`Discover`,
68 path: '/videos/overview',
69 priority: 150
70 },
71 {
72 icon: 'trending' as 'trending',
73 label: $localize`Trending videos`,
74 menuLabel: $localize`Trending`,
75 path: '/videos/trending',
76 priority: 140
77 },
78 {
79 icon: 'recently-added' as 'recently-added',
80 label: $localize`Recently added videos`,
81 menuLabel: $localize`Recently added`,
82 path: '/videos/recently-added',
83 priority: 130
84 },
85 {
86 icon: 'octagon' as 'octagon',
87 label: $localize`Local videos`,
88 menuLabel: $localize`Local videos`,
89 path: '/videos/local',
90 priority: 120
91 }
92 ]
93
94 if (config.homepage.enabled) {
95 entries.push({
96 icon: 'home' as 'home',
97 label: $localize`Home`,
98 menuLabel: $localize`Home`,
99 path: '/home',
100 priority: 160
101 })
102 }
103
104 entries = entries.sort(sortObjectComparator('priority', 'desc'))
105
106 return entries
107 }
108
51 private handleWindowResize () { 109 private handleWindowResize () {
52 // On touch screens, do not handle window resize event since opened menu is handled with a content overlay 110 // On touch screens, do not handle window resize event since opened menu is handled with a content overlay
53 if (this.screenService.isInTouchScreen()) return 111 if (this.screenService.isInTouchScreen()) return
diff --git a/client/src/app/core/renderer/html-renderer.service.ts b/client/src/app/core/renderer/html-renderer.service.ts
index 3176cf6a4..418d8603e 100644
--- a/client/src/app/core/renderer/html-renderer.service.ts
+++ b/client/src/app/core/renderer/html-renderer.service.ts
@@ -1,6 +1,6 @@
1import { Injectable } from '@angular/core' 1import { Injectable } from '@angular/core'
2import { LinkifierService } from './linkifier.service' 2import { LinkifierService } from './linkifier.service'
3import { SANITIZE_OPTIONS } from '@shared/core-utils/renderer/html' 3import { getCustomMarkupSanitizeOptions, getSanitizeOptions } from '@shared/core-utils/renderer/html'
4 4
5@Injectable() 5@Injectable()
6export class HtmlRendererService { 6export class HtmlRendererService {
@@ -20,7 +20,7 @@ export class HtmlRendererService {
20 }) 20 })
21 } 21 }
22 22
23 async toSafeHtml (text: string) { 23 async toSafeHtml (text: string, additionalAllowedTags: string[] = []) {
24 const [ html ] = await Promise.all([ 24 const [ html ] = await Promise.all([
25 // Convert possible markdown to html 25 // Convert possible markdown to html
26 this.linkifier.linkify(text), 26 this.linkifier.linkify(text),
@@ -28,7 +28,11 @@ export class HtmlRendererService {
28 this.loadSanitizeHtml() 28 this.loadSanitizeHtml()
29 ]) 29 ])
30 30
31 return this.sanitizeHtml(html, SANITIZE_OPTIONS) 31 const options = additionalAllowedTags.length !== 0
32 ? getCustomMarkupSanitizeOptions(additionalAllowedTags)
33 : getSanitizeOptions()
34
35 return this.sanitizeHtml(html, options)
32 } 36 }
33 37
34 private async loadSanitizeHtml () { 38 private async loadSanitizeHtml () {
diff --git a/client/src/app/core/renderer/markdown.service.ts b/client/src/app/core/renderer/markdown.service.ts
index edddb0a66..ca1bf4eb9 100644
--- a/client/src/app/core/renderer/markdown.service.ts
+++ b/client/src/app/core/renderer/markdown.service.ts
@@ -17,12 +17,15 @@ type MarkdownParsers = {
17 enhancedMarkdownIt: MarkdownIt 17 enhancedMarkdownIt: MarkdownIt
18 enhancedWithHTMLMarkdownIt: MarkdownIt 18 enhancedWithHTMLMarkdownIt: MarkdownIt
19 19
20 completeMarkdownIt: MarkdownIt 20 unsafeMarkdownIt: MarkdownIt
21
22 customPageMarkdownIt: MarkdownIt
21} 23}
22 24
23type MarkdownConfig = { 25type MarkdownConfig = {
24 rules: string[] 26 rules: string[]
25 html: boolean 27 html: boolean
28 breaks: boolean
26 escape?: boolean 29 escape?: boolean
27} 30}
28 31
@@ -35,18 +38,24 @@ export class MarkdownService {
35 private markdownParsers: MarkdownParsers = { 38 private markdownParsers: MarkdownParsers = {
36 textMarkdownIt: null, 39 textMarkdownIt: null,
37 textWithHTMLMarkdownIt: null, 40 textWithHTMLMarkdownIt: null,
41
38 enhancedMarkdownIt: null, 42 enhancedMarkdownIt: null,
39 enhancedWithHTMLMarkdownIt: null, 43 enhancedWithHTMLMarkdownIt: null,
40 completeMarkdownIt: null 44
45 unsafeMarkdownIt: null,
46
47 customPageMarkdownIt: null
41 } 48 }
42 private parsersConfig: MarkdownParserConfigs = { 49 private parsersConfig: MarkdownParserConfigs = {
43 textMarkdownIt: { rules: TEXT_RULES, html: false }, 50 textMarkdownIt: { rules: TEXT_RULES, breaks: true, html: false },
44 textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, html: true, escape: true }, 51 textWithHTMLMarkdownIt: { rules: TEXT_WITH_HTML_RULES, breaks: true, html: true, escape: true },
45 52
46 enhancedMarkdownIt: { rules: ENHANCED_RULES, html: false }, 53 enhancedMarkdownIt: { rules: ENHANCED_RULES, breaks: true, html: false },
47 enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, html: true, escape: true }, 54 enhancedWithHTMLMarkdownIt: { rules: ENHANCED_WITH_HTML_RULES, breaks: true, html: true, escape: true },
48 55
49 completeMarkdownIt: { rules: COMPLETE_RULES, html: true } 56 unsafeMarkdownIt: { rules: COMPLETE_RULES, breaks: true, html: true, escape: false },
57
58 customPageMarkdownIt: { rules: COMPLETE_RULES, breaks: false, html: true, escape: true }
50 } 59 }
51 60
52 private emojiModule: any 61 private emojiModule: any
@@ -54,22 +63,26 @@ export class MarkdownService {
54 constructor (private htmlRenderer: HtmlRendererService) {} 63 constructor (private htmlRenderer: HtmlRendererService) {}
55 64
56 textMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) { 65 textMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) {
57 if (withHtml) return this.render('textWithHTMLMarkdownIt', markdown, withEmoji) 66 if (withHtml) return this.render({ name: 'textWithHTMLMarkdownIt', markdown, withEmoji })
58 67
59 return this.render('textMarkdownIt', markdown, withEmoji) 68 return this.render({ name: 'textMarkdownIt', markdown, withEmoji })
60 } 69 }
61 70
62 enhancedMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) { 71 enhancedMarkdownToHTML (markdown: string, withHtml = false, withEmoji = false) {
63 if (withHtml) return this.render('enhancedWithHTMLMarkdownIt', markdown, withEmoji) 72 if (withHtml) return this.render({ name: 'enhancedWithHTMLMarkdownIt', markdown, withEmoji })
73
74 return this.render({ name: 'enhancedMarkdownIt', markdown, withEmoji })
75 }
64 76
65 return this.render('enhancedMarkdownIt', markdown, withEmoji) 77 unsafeMarkdownToHTML (markdown: string, _trustedInput: true) {
78 return this.render({ name: 'unsafeMarkdownIt', markdown, withEmoji: true })
66 } 79 }
67 80
68 completeMarkdownToHTML (markdown: string) { 81 customPageMarkdownToHTML (markdown: string, additionalAllowedTags: string[]) {
69 return this.render('completeMarkdownIt', markdown, true) 82 return this.render({ name: 'customPageMarkdownIt', markdown, withEmoji: true, additionalAllowedTags })
70 } 83 }
71 84
72 async processVideoTimestamps (html: string) { 85 processVideoTimestamps (html: string) {
73 return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) { 86 return html.replace(/((\d{1,2}):)?(\d{1,2}):(\d{1,2})/g, function (str, _, h, m, s) {
74 const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0)) 87 const t = (3600 * +(h || 0)) + (60 * +(m || 0)) + (+(s || 0))
75 const url = buildVideoLink({ startTime: t }) 88 const url = buildVideoLink({ startTime: t })
@@ -77,7 +90,13 @@ export class MarkdownService {
77 }) 90 })
78 } 91 }
79 92
80 private async render (name: keyof MarkdownParsers, markdown: string, withEmoji = false) { 93 private async render (options: {
94 name: keyof MarkdownParsers
95 markdown: string
96 withEmoji: boolean
97 additionalAllowedTags?: string[]
98 }) {
99 const { name, markdown, withEmoji, additionalAllowedTags } = options
81 if (!markdown) return '' 100 if (!markdown) return ''
82 101
83 const config = this.parsersConfig[ name ] 102 const config = this.parsersConfig[ name ]
@@ -96,7 +115,7 @@ export class MarkdownService {
96 let html = this.markdownParsers[ name ].render(markdown) 115 let html = this.markdownParsers[ name ].render(markdown)
97 html = this.avoidTruncatedTags(html) 116 html = this.avoidTruncatedTags(html)
98 117
99 if (config.escape) return this.htmlRenderer.toSafeHtml(html) 118 if (config.escape) return this.htmlRenderer.toSafeHtml(html, additionalAllowedTags)
100 119
101 return html 120 return html
102 } 121 }
@@ -105,7 +124,7 @@ export class MarkdownService {
105 // FIXME: import('...') returns a struct module, containing a "default" field 124 // FIXME: import('...') returns a struct module, containing a "default" field
106 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default 125 const MarkdownItClass: typeof import ('markdown-it') = (await import('markdown-it') as any).default
107 126
108 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: true, html: config.html }) 127 const markdownIt = new MarkdownItClass('zero', { linkify: true, breaks: config.breaks, html: config.html })
109 128
110 for (const rule of config.rules) { 129 for (const rule of config.rules) {
111 markdownIt.enable(rule) 130 markdownIt.enable(rule)
diff --git a/client/src/app/core/routing/index.ts b/client/src/app/core/routing/index.ts
index 239c27caf..4314ea475 100644
--- a/client/src/app/core/routing/index.ts
+++ b/client/src/app/core/routing/index.ts
@@ -3,6 +3,8 @@ export * from './custom-reuse-strategy'
3export * from './disable-for-reuse-hook' 3export * from './disable-for-reuse-hook'
4export * from './login-guard.service' 4export * from './login-guard.service'
5export * from './menu-guard.service' 5export * from './menu-guard.service'
6export * from './meta-guard.service'
7export * from './meta.service'
6export * from './preload-selected-modules-list' 8export * from './preload-selected-modules-list'
7export * from './redirect.service' 9export * from './redirect.service'
8export * from './server-config-resolver.service' 10export * from './server-config-resolver.service'
diff --git a/client/src/app/core/routing/meta-guard.service.ts b/client/src/app/core/routing/meta-guard.service.ts
new file mode 100644
index 000000000..bedb3450e
--- /dev/null
+++ b/client/src/app/core/routing/meta-guard.service.ts
@@ -0,0 +1,23 @@
1import { Injectable } from '@angular/core'
2import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot } from '@angular/router'
3import { MetaService } from './meta.service'
4
5@Injectable()
6export class MetaGuard implements CanActivate, CanActivateChild {
7
8 constructor (private meta: MetaService) { }
9
10 canActivate (route: ActivatedRouteSnapshot): boolean {
11 const metaSettings = route.data?.meta
12
13 if (metaSettings) {
14 this.meta.update(metaSettings)
15 }
16
17 return true
18 }
19
20 canActivateChild (route: ActivatedRouteSnapshot): boolean {
21 return this.canActivate(route)
22 }
23}
diff --git a/client/src/app/core/routing/meta.service.ts b/client/src/app/core/routing/meta.service.ts
new file mode 100644
index 000000000..a5ac778dc
--- /dev/null
+++ b/client/src/app/core/routing/meta.service.ts
@@ -0,0 +1,40 @@
1import { Injectable } from '@angular/core'
2import { Meta, Title } from '@angular/platform-browser'
3import { HTMLServerConfig } from '@shared/models/server'
4import { ServerService } from '../server'
5
6export interface MetaSettings {
7 title?: string
8}
9
10@Injectable()
11export class MetaService {
12 private config: HTMLServerConfig
13
14 constructor (
15 private titleService: Title,
16 private meta: Meta,
17 private server: ServerService
18 ) {
19 this.config = this.server.getTmpConfig()
20 this.server.getConfig()
21 .subscribe(config => this.config = config)
22 }
23
24 setTitle (subTitle?: string) {
25 let title = ''
26 if (subTitle) title += `${subTitle} - `
27
28 title += this.config.instance.name
29
30 this.titleService.setTitle(title)
31 }
32
33 setTag (name: string, value: string) {
34 this.meta.addTag({ name, content: value })
35 }
36
37 update (meta: MetaSettings) {
38 this.setTitle(meta.title)
39 }
40}
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts
index 6d26fb504..cf690a4d0 100644
--- a/client/src/app/core/routing/redirect.service.ts
+++ b/client/src/app/core/routing/redirect.service.ts
@@ -6,14 +6,14 @@ import { ServerService } from '../server'
6export class RedirectService { 6export class RedirectService {
7 // Default route could change according to the instance configuration 7 // Default route could change according to the instance configuration
8 static INIT_DEFAULT_ROUTE = '/videos/trending' 8 static INIT_DEFAULT_ROUTE = '/videos/trending'
9 static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
10 static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed' 9 static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed'
11 static DEFAULT_TRENDING_ALGORITHM = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
12 10
13 private previousUrl: string 11 private previousUrl: string
14 private currentUrl: string 12 private currentUrl: string
15 13
16 private redirectingToHomepage = false 14 private redirectingToHomepage = false
15 private defaultTrendingAlgorithm = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
16 private defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
17 17
18 constructor ( 18 constructor (
19 private router: Router, 19 private router: Router,
@@ -22,10 +22,10 @@ export class RedirectService {
22 // The config is first loaded from the cache so try to get the default route 22 // The config is first loaded from the cache so try to get the default route
23 const tmpConfig = this.serverService.getTmpConfig() 23 const tmpConfig = this.serverService.getTmpConfig()
24 if (tmpConfig?.instance?.defaultClientRoute) { 24 if (tmpConfig?.instance?.defaultClientRoute) {
25 RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute 25 this.defaultRoute = tmpConfig.instance.defaultClientRoute
26 } 26 }
27 if (tmpConfig?.trending?.videos?.algorithms?.default) { 27 if (tmpConfig?.trending?.videos?.algorithms?.default) {
28 RedirectService.DEFAULT_TRENDING_ALGORITHM = tmpConfig.trending.videos.algorithms.default 28 this.defaultTrendingAlgorithm = tmpConfig.trending.videos.algorithms.default
29 } 29 }
30 30
31 // Load default route 31 // Load default route
@@ -34,13 +34,8 @@ export class RedirectService {
34 const defaultRouteConfig = config.instance.defaultClientRoute 34 const defaultRouteConfig = config.instance.defaultClientRoute
35 const defaultTrendingConfig = config.trending.videos.algorithms.default 35 const defaultTrendingConfig = config.trending.videos.algorithms.default
36 36
37 if (defaultRouteConfig) { 37 if (defaultRouteConfig) this.defaultRoute = defaultRouteConfig
38 RedirectService.DEFAULT_ROUTE = defaultRouteConfig 38 if (defaultTrendingConfig) this.defaultTrendingAlgorithm = defaultTrendingConfig
39 }
40
41 if (defaultTrendingConfig) {
42 RedirectService.DEFAULT_TRENDING_ALGORITHM = defaultTrendingConfig
43 }
44 }) 39 })
45 40
46 // Track previous url 41 // Track previous url
@@ -53,6 +48,14 @@ export class RedirectService {
53 }) 48 })
54 } 49 }
55 50
51 getDefaultRoute () {
52 return this.defaultRoute
53 }
54
55 getDefaultTrendingAlgorithm () {
56 return this.defaultTrendingAlgorithm
57 }
58
56 redirectToPreviousRoute () { 59 redirectToPreviousRoute () {
57 const exceptions = [ 60 const exceptions = [
58 '/verify-account', 61 '/verify-account',
@@ -72,21 +75,21 @@ export class RedirectService {
72 75
73 this.redirectingToHomepage = true 76 this.redirectingToHomepage = true
74 77
75 console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE) 78 console.log('Redirecting to %s...', this.defaultRoute)
76 79
77 this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange }) 80 this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
78 .then(() => this.redirectingToHomepage = false) 81 .then(() => this.redirectingToHomepage = false)
79 .catch(() => { 82 .catch(() => {
80 this.redirectingToHomepage = false 83 this.redirectingToHomepage = false
81 84
82 console.error( 85 console.error(
83 'Cannot navigate to %s, resetting default route to %s.', 86 'Cannot navigate to %s, resetting default route to %s.',
84 RedirectService.DEFAULT_ROUTE, 87 this.defaultRoute,
85 RedirectService.INIT_DEFAULT_ROUTE 88 RedirectService.INIT_DEFAULT_ROUTE
86 ) 89 )
87 90
88 RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE 91 this.defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
89 return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange }) 92 return this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
90 }) 93 })
91 94
92 } 95 }
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 906191ae1..6918957f4 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -3,7 +3,6 @@ import { first, map, share, shareReplay, switchMap, tap } from 'rxjs/operators'
3import { HttpClient } from '@angular/common/http' 3import { HttpClient } from '@angular/common/http'
4import { Inject, Injectable, LOCALE_ID } from '@angular/core' 4import { Inject, Injectable, LOCALE_ID } from '@angular/core'
5import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers' 5import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers'
6import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
7import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n' 6import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
8import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models' 7import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models'
9import { environment } from '../../../environments/environment' 8import { environment } from '../../../environments/environment'
@@ -16,8 +15,6 @@ export class ServerService {
16 private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' 15 private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
17 private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats' 16 private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
18 17
19 private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
20
21 configReloaded = new Subject<ServerConfig>() 18 configReloaded = new Subject<ServerConfig>()
22 19
23 private localeObservable: Observable<any> 20 private localeObservable: Observable<any>
@@ -63,7 +60,8 @@ export class ServerService {
63 signup: { 60 signup: {
64 allowed: false, 61 allowed: false,
65 allowedForCurrentIP: false, 62 allowedForCurrentIP: false,
66 requiresEmailVerification: false 63 requiresEmailVerification: false,
64 minimumAge: 16
67 }, 65 },
68 transcoding: { 66 transcoding: {
69 profile: 'default', 67 profile: 'default',
@@ -176,6 +174,9 @@ export class ServerService {
176 disableLocalSearch: false, 174 disableLocalSearch: false,
177 isDefaultSearch: false 175 isDefaultSearch: false
178 } 176 }
177 },
178 homepage: {
179 enabled: false
179 } 180 }
180 } 181 }
181 182
@@ -201,9 +202,7 @@ export class ServerService {
201 this.configReset = true 202 this.configReset = true
202 203
203 // Notify config update 204 // Notify config update
204 this.getConfig().subscribe(() => { 205 return this.getConfig()
205 // empty, to fire a reset config event
206 })
207 } 206 }
208 207
209 getConfig () { 208 getConfig () {
@@ -212,7 +211,6 @@ export class ServerService {
212 if (!this.configObservable) { 211 if (!this.configObservable) {
213 this.configObservable = this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL) 212 this.configObservable = this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL)
214 .pipe( 213 .pipe(
215 tap(config => this.saveConfigLocally(config)),
216 tap(config => { 214 tap(config => {
217 this.config = config 215 this.config = config
218 this.configLoaded = true 216 this.configLoaded = true
@@ -343,20 +341,15 @@ export class ServerService {
343 ) 341 )
344 } 342 }
345 343
346 private saveConfigLocally (config: ServerConfig) {
347 peertubeLocalStorage.setItem(ServerService.CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config))
348 }
349
350 private loadConfigLocally () { 344 private loadConfigLocally () {
351 const configString = peertubeLocalStorage.getItem(ServerService.CONFIG_LOCAL_STORAGE_KEY) 345 const configString = window['PeerTubeServerConfig']
352 346 if (!configString) return
353 if (configString) { 347
354 try { 348 try {
355 const parsed = JSON.parse(configString) 349 const parsed = JSON.parse(configString)
356 Object.assign(this.config, parsed) 350 Object.assign(this.config, parsed)
357 } catch (err) { 351 } catch (err) {
358 console.error('Cannot parse config saved in local storage.', err) 352 console.error('Cannot parse config saved in from index.html.', err)
359 }
360 } 353 }
361 } 354 }
362} 355}
diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts
index 4c4611d01..e7a5ae17a 100644
--- a/client/src/app/core/theme/theme.service.ts
+++ b/client/src/app/core/theme/theme.service.ts
@@ -82,7 +82,19 @@ export class ThemeService {
82 : this.userService.getAnonymousUser().theme 82 : this.userService.getAnonymousUser().theme
83 83
84 if (theme !== 'instance-default') return theme 84 if (theme !== 'instance-default') return theme
85 return this.serverConfig.theme.default 85
86 const instanceTheme = this.serverConfig.theme.default
87 if (instanceTheme !== 'default') return instanceTheme
88
89 // Default to dark theme if available and wanted by the user
90 if (
91 this.themes.find(t => t.name === 'dark') &&
92 window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
93 ) {
94 return 'dark'
95 }
96
97 return instanceTheme
86 } 98 }
87 99
88 private loadTheme (name: string) { 100 private loadTheme (name: string) {
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index eaaaae314..099ee8f36 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -18,7 +18,7 @@
18 </div> 18 </div>
19 19
20 <div ngbDropdownMenu> 20 <div ngbDropdownMenu>
21 <a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/accounts', user.account.nameWithHost ]" 21 <a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/a', user.account.nameWithHost ]"
22 #profile (click)="onActiveLinkScrollToAnchor(profile)"> 22 #profile (click)="onActiveLinkScrollToAnchor(profile)">
23 <my-global-icon iconName="go" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container> 23 <my-global-icon iconName="go" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container>
24 </a> 24 </a>
@@ -123,24 +123,9 @@
123 <div class="on-instance"> 123 <div class="on-instance">
124 <div i18n class="block-title">ON {{instanceName}}</div> 124 <div i18n class="block-title">ON {{instanceName}}</div>
125 125
126 <a class="menu-link" routerLink="/videos/overview" routerLinkActive="active"> 126 <a class="menu-link" *ngFor="let commonLink of commonMenuLinks" [routerLink]="commonLink.path" routerLinkActive="active">
127 <my-global-icon iconName="globe" aria-hidden="true"></my-global-icon> 127 <my-global-icon [iconName]="commonLink.icon" aria-hidden="true"></my-global-icon>
128 <ng-container i18n>Discover</ng-container> 128 <ng-container>{{ commonLink.menuLabel }}</ng-container>
129 </a>
130
131 <a class="menu-link" routerLink="/videos/trending" routerLinkActive="active">
132 <my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
133 <ng-container i18n>Trending</ng-container>
134 </a>
135
136 <a class="menu-link" routerLink="/videos/recently-added" routerLinkActive="active">
137 <my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon>
138 <ng-container i18n>Recently added</ng-container>
139 </a>
140
141 <a class="menu-link" routerLink="/videos/local" routerLinkActive="active">
142 <my-global-icon iconName="home" aria-hidden="true"></my-global-icon>
143 <ng-container i18n>Local videos</ng-container>
144 </a> 129 </a>
145 </div> 130 </div>
146 </div> 131 </div>
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts
index 8fa1de326..2f7e0cf07 100644
--- a/client/src/app/menu/menu.component.ts
+++ b/client/src/app/menu/menu.component.ts
@@ -4,7 +4,17 @@ import { switchMap } from 'rxjs/operators'
4import { ViewportScroller } from '@angular/common' 4import { ViewportScroller } from '@angular/common'
5import { Component, OnInit, ViewChild } from '@angular/core' 5import { Component, OnInit, ViewChild } from '@angular/core'
6import { Router } from '@angular/router' 6import { Router } from '@angular/router'
7import { AuthService, AuthStatus, AuthUser, MenuService, RedirectService, ScreenService, ServerService, UserService } from '@app/core' 7import {
8 AuthService,
9 AuthStatus,
10 AuthUser,
11 MenuLink,
12 MenuService,
13 RedirectService,
14 ScreenService,
15 ServerService,
16 UserService
17} from '@app/core'
8import { scrollToTop } from '@app/helpers' 18import { scrollToTop } from '@app/helpers'
9import { LanguageChooserComponent } from '@app/menu/language-chooser.component' 19import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
10import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component' 20import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
@@ -35,6 +45,8 @@ export class MenuComponent implements OnInit {
35 45
36 currentInterfaceLanguage: string 46 currentInterfaceLanguage: string
37 47
48 commonMenuLinks: MenuLink[] = []
49
38 private languages: VideoConstant<string>[] = [] 50 private languages: VideoConstant<string>[] = []
39 private serverConfig: ServerConfig 51 private serverConfig: ServerConfig
40 private routesPerRight: { [role in UserRight]?: string } = { 52 private routesPerRight: { [role in UserRight]?: string } = {
@@ -80,7 +92,10 @@ export class MenuComponent implements OnInit {
80 ngOnInit () { 92 ngOnInit () {
81 this.serverConfig = this.serverService.getTmpConfig() 93 this.serverConfig = this.serverService.getTmpConfig()
82 this.serverService.getConfig() 94 this.serverService.getConfig()
83 .subscribe(config => this.serverConfig = config) 95 .subscribe(config => {
96 this.serverConfig = config
97 this.buildMenuLinks()
98 })
84 99
85 this.isLoggedIn = this.authService.isLoggedIn() 100 this.isLoggedIn = this.authService.isLoggedIn()
86 if (this.isLoggedIn === true) { 101 if (this.isLoggedIn === true) {
@@ -241,6 +256,10 @@ export class MenuComponent implements OnInit {
241 } 256 }
242 } 257 }
243 258
259 private buildMenuLinks () {
260 this.commonMenuLinks = this.menuService.buildCommonLinks(this.serverConfig)
261 }
262
244 private buildUserLanguages () { 263 private buildUserLanguages () {
245 if (!this.user) { 264 if (!this.user) {
246 this.videoLanguages = [] 265 this.videoLanguages = []
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/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..ffaf15710
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/custom-markup.service.ts
@@ -0,0 +1,136 @@
1import { ComponentRef, Injectable } from '@angular/core'
2import { MarkdownService } from '@app/core'
3import {
4 ChannelMiniatureMarkupData,
5 EmbedMarkupData,
6 PlaylistMiniatureMarkupData,
7 VideoMiniatureMarkupData,
8 VideosListMarkupData
9} from '@shared/models'
10import { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
11import { DynamicElementService } from './dynamic-element.service'
12import { EmbedMarkupComponent } from './embed-markup.component'
13import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
14import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
15import { VideosListMarkupComponent } from './videos-list-markup.component'
16
17type BuilderFunction = (el: HTMLElement) => ComponentRef<any>
18
19@Injectable()
20export class CustomMarkupService {
21 private builders: { [ selector: string ]: BuilderFunction } = {
22 'peertube-video-embed': el => this.embedBuilder(el, 'video'),
23 'peertube-playlist-embed': el => this.embedBuilder(el, 'playlist'),
24 'peertube-video-miniature': el => this.videoMiniatureBuilder(el),
25 'peertube-playlist-miniature': el => this.playlistMiniatureBuilder(el),
26 'peertube-channel-miniature': el => this.channelMiniatureBuilder(el),
27 'peertube-videos-list': el => this.videosListBuilder(el)
28 }
29
30 constructor (
31 private dynamicElementService: DynamicElementService,
32 private markdown: MarkdownService
33 ) { }
34
35 async buildElement (text: string) {
36 const html = await this.markdown.customPageMarkdownToHTML(text, this.getSupportedTags())
37
38 const rootElement = document.createElement('div')
39 rootElement.innerHTML = html
40
41 for (const selector of this.getSupportedTags()) {
42 rootElement.querySelectorAll(selector)
43 .forEach((e: HTMLElement) => {
44 try {
45 const component = this.execBuilder(selector, e)
46
47 this.dynamicElementService.injectElement(e, component)
48 } catch (err) {
49 console.error('Cannot inject component %s.', selector, err)
50 }
51 })
52 }
53
54 return rootElement
55 }
56
57 private getSupportedTags () {
58 return Object.keys(this.builders)
59 }
60
61 private execBuilder (selector: string, el: HTMLElement) {
62 return this.builders[selector](el)
63 }
64
65 private embedBuilder (el: HTMLElement, type: 'video' | 'playlist') {
66 const data = el.dataset as EmbedMarkupData
67 const component = this.dynamicElementService.createElement(EmbedMarkupComponent)
68
69 this.dynamicElementService.setModel(component, { uuid: data.uuid, type })
70
71 return component
72 }
73
74 private videoMiniatureBuilder (el: HTMLElement) {
75 const data = el.dataset as VideoMiniatureMarkupData
76 const component = this.dynamicElementService.createElement(VideoMiniatureMarkupComponent)
77
78 this.dynamicElementService.setModel(component, { uuid: data.uuid })
79
80 return component
81 }
82
83 private playlistMiniatureBuilder (el: HTMLElement) {
84 const data = el.dataset as PlaylistMiniatureMarkupData
85 const component = this.dynamicElementService.createElement(PlaylistMiniatureMarkupComponent)
86
87 this.dynamicElementService.setModel(component, { uuid: data.uuid })
88
89 return component
90 }
91
92 private channelMiniatureBuilder (el: HTMLElement) {
93 const data = el.dataset as ChannelMiniatureMarkupData
94 const component = this.dynamicElementService.createElement(ChannelMiniatureMarkupComponent)
95
96 this.dynamicElementService.setModel(component, { name: data.name })
97
98 return component
99 }
100
101 private videosListBuilder (el: HTMLElement) {
102 const data = el.dataset as VideosListMarkupData
103 const component = this.dynamicElementService.createElement(VideosListMarkupComponent)
104
105 const model = {
106 title: data.title,
107 description: data.description,
108 sort: data.sort,
109 categoryOneOf: this.buildArrayNumber(data.categoryOneOf),
110 languageOneOf: this.buildArrayString(data.languageOneOf),
111 count: this.buildNumber(data.count) || 10
112 }
113
114 this.dynamicElementService.setModel(component, model)
115
116 return component
117 }
118
119 private buildNumber (value: string) {
120 if (!value) return undefined
121
122 return parseInt(value, 10)
123 }
124
125 private buildArrayNumber (value: string) {
126 if (!value) return undefined
127
128 return value.split(',').map(v => parseInt(v, 10))
129 }
130
131 private buildArrayString (value: string) {
132 if (!value) return undefined
133
134 return value.split(',')
135 }
136}
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..4bbb71588
--- /dev/null
+++ b/client/src/app/shared/shared-custom-markup/shared-custom-markup.module.ts
@@ -0,0 +1,49 @@
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 { ChannelMiniatureMarkupComponent } from './channel-miniature-markup.component'
10import { CustomMarkupService } from './custom-markup.service'
11import { DynamicElementService } from './dynamic-element.service'
12import { EmbedMarkupComponent } from './embed-markup.component'
13import { PlaylistMiniatureMarkupComponent } from './playlist-miniature-markup.component'
14import { VideoMiniatureMarkupComponent } from './video-miniature-markup.component'
15import { VideosListMarkupComponent } from './videos-list-markup.component'
16
17@NgModule({
18 imports: [
19 CommonModule,
20
21 SharedMainModule,
22 SharedGlobalIconModule,
23 SharedVideoMiniatureModule,
24 SharedVideoPlaylistModule,
25 SharedActorImageModule
26 ],
27
28 declarations: [
29 VideoMiniatureMarkupComponent,
30 PlaylistMiniatureMarkupComponent,
31 ChannelMiniatureMarkupComponent,
32 EmbedMarkupComponent,
33 VideosListMarkupComponent
34 ],
35
36 exports: [
37 VideoMiniatureMarkupComponent,
38 PlaylistMiniatureMarkupComponent,
39 ChannelMiniatureMarkupComponent,
40 VideosListMarkupComponent,
41 EmbedMarkupComponent
42 ],
43
44 providers: [
45 CustomMarkupService,
46 DynamicElementService
47 ]
48})
49export 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..a4dd72db6 100644
--- a/client/src/app/shared/shared-icons/global-icon.component.ts
+++ b/client/src/app/shared/shared-icons/global-icon.component.ts
@@ -72,6 +72,7 @@ const icons = {
72 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default, 72 'repeat': require('!!raw-loader?!../../../assets/images/feather/repeat.svg').default,
73 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default, 73 'message-circle': require('!!raw-loader?!../../../assets/images/feather/message-circle.svg').default,
74 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default, 74 'codesandbox': require('!!raw-loader?!../../../assets/images/feather/codesandbox.svg').default,
75 'octagon': require('!!raw-loader?!../../../assets/images/feather/octagon.svg').default,
75 'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default 76 'award': require('!!raw-loader?!../../../assets/images/feather/award.svg').default
76} 77}
77 78
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}