aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
Diffstat (limited to 'client')
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html32
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts18
-rw-r--r--client/src/app/+videos/video-list/trending/index.ts4
-rw-r--r--client/src/app/+videos/video-list/trending/video-hot.component.ts85
-rw-r--r--client/src/app/+videos/video-list/trending/video-most-liked.component.ts81
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending-header.component.html10
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending-header.component.ts67
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending.component.ts (renamed from client/src/app/+videos/video-list/trending/video-most-viewed.component.ts)42
-rw-r--r--client/src/app/+videos/videos-routing.module.ts42
-rw-r--r--client/src/app/+videos/videos.module.ts10
-rw-r--r--client/src/app/app.component.html2
-rw-r--r--client/src/app/app.component.ts4
-rw-r--r--client/src/app/core/core.module.ts3
-rw-r--r--client/src/app/core/routing/index.ts1
-rw-r--r--client/src/app/core/routing/redirect.service.ts34
-rw-r--r--client/src/app/core/routing/trending-guard.service.ts14
-rw-r--r--client/src/app/core/server/server.service.ts12
-rw-r--r--client/src/app/menu/menu.component.html6
-rw-r--r--client/src/app/shared/shared-video-miniature/video-list-header.component.ts10
19 files changed, 141 insertions, 336 deletions
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 83b1c6a31..dd62a4aab 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
@@ -271,10 +271,9 @@
271 <option i18n value="/videos/overview">Discover videos</option> 271 <option i18n value="/videos/overview">Discover videos</option>
272 <optgroup i18n-label label="Trending pages"> 272 <optgroup i18n-label label="Trending pages">
273 <option i18n value="/videos/trending">Default trending page</option> 273 <option i18n value="/videos/trending">Default trending page</option>
274 <option i18n value="/videos/hot" *ngIf="isTrendingHotEnabled()">Hot videos</option> 274 <option i18n value="/videos/trending?alg=hot" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('hot')">Hot videos</option>
275 <option i18n value="/videos/hot" *ngIf="!isTrendingHotEnabled()" disabled>Hot videos</option> 275 <option i18n value="/videos/trending?alg=most-viewed" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-viewed')">Most viewed videos</option>
276 <option i18n value="/videos/most-viewed">Most viewed videos</option> 276 <option i18n value="/videos/trending?alg=most-liked" [disabled]="!trendingVideosAlgorithmsEnabledIncludes('most-liked')">Most liked videos</option>
277 <option i18n value="/videos/most-liked">Most liked videos</option>
278 </optgroup> 277 </optgroup>
279 <option i18n value="/videos/recently-added">Recently added videos</option> 278 <option i18n value="/videos/recently-added">Recently added videos</option>
280 <option i18n value="/videos/local">Local videos</option> 279 <option i18n value="/videos/local">Local videos</option>
@@ -283,17 +282,20 @@
283 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div> 282 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
284 </div> 283 </div>
285 284
286 <div class="form-group" formGroupName="instance"> 285 <div class="form-group" formGroupName="trending">
287 <label i18n for="instanceDefaultTrendingRoute">Default trending page</label> 286 <ng-container formGroupName="videos">
288 <div class="peertube-select-container"> 287 <ng-container formGroupName="algorithms">
289 <select id="instanceDefaultTrendingRoute" formControlName="defaultTrendingRoute" class="form-control"> 288 <label i18n for="trendingVideosAlgorithmsDefault">Default trending page</label>
290 <option i18n value="/videos/hot" *ngIf="isTrendingHotEnabled()">Hot videos</option> 289 <div class="peertube-select-container">
291 <option i18n value="/videos/hot" *ngIf="!isTrendingHotEnabled()" disabled>Hot videos</option> 290 <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control">
292 <option i18n value="/videos/trending">Most viewed videos</option> 291 <option i18n value="hot">Hot videos</option>
293 <option i18n value="/videos/most-liked">Most liked videos</option> 292 <option i18n value="most-viewed">Most viewed videos</option>
294 </select> 293 <option i18n value="most-liked">Most liked videos</option>
295 </div> 294 </select>
296 <div *ngIf="formErrors.instance.defaultTrendingRoute" class="form-error">{{ formErrors.instance.defaultTrendingRoute }}</div> 295 </div>
296 <div *ngIf="formErrors.trending.videos.algorithms.default" class="form-error">{{ formErrors.trending.videos.algorithms.default }}</div>
297 </ng-container>
298 </ng-container>
297 </div> 299 </div>
298 300
299 </div> 301 </div>
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 e6fc4582b..9a46a2e59 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
@@ -186,12 +186,6 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
186 languages: null, 186 languages: null,
187 187
188 defaultClientRoute: null, 188 defaultClientRoute: null,
189 defaultTrendingRoute: null,
190 pages: {
191 hot: {
192 enabled: null
193 }
194 },
195 189
196 customizations: { 190 customizations: {
197 javascript: null, 191 javascript: null,
@@ -230,6 +224,14 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
230 } 224 }
231 } 225 }
232 }, 226 },
227 trending: {
228 videos: {
229 algorithms: {
230 enabled: null,
231 default: null
232 }
233 }
234 },
233 admin: { 235 admin: {
234 email: ADMIN_EMAIL_VALIDATOR 236 email: ADMIN_EMAIL_VALIDATOR
235 }, 237 },
@@ -370,8 +372,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
370 return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true 372 return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
371 } 373 }
372 374
373 isTrendingHotEnabled () { 375 trendingVideosAlgorithmsEnabledIncludes (algorithm: string) {
374 return this.form.value['instance']['pages']['hot']['enabled'] === true 376 return this.form.value['trending']['videos']['algorithms']['enabled'].find((e: string) => e === algorithm)
375 } 377 }
376 378
377 async formValidated () { 379 async formValidated () {
diff --git a/client/src/app/+videos/video-list/trending/index.ts b/client/src/app/+videos/video-list/trending/index.ts
index 93f4b1df6..70835885a 100644
--- a/client/src/app/+videos/video-list/trending/index.ts
+++ b/client/src/app/+videos/video-list/trending/index.ts
@@ -1,4 +1,2 @@
1export * from './video-trending-header.component' 1export * from './video-trending-header.component'
2export * from './video-hot.component' 2export * from './video-trending.component'
3export * from './video-most-viewed.component'
4export * from './video-most-liked.component'
diff --git a/client/src/app/+videos/video-list/trending/video-hot.component.ts b/client/src/app/+videos/video-list/trending/video-hot.component.ts
deleted file mode 100644
index 1617eb21e..000000000
--- a/client/src/app/+videos/video-list/trending/video-hot.component.ts
+++ /dev/null
@@ -1,85 +0,0 @@
1import { Component, ComponentFactoryResolver, Injector, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
4import { HooksService } from '@app/core/plugins/hooks.service'
5import { immutableAssign } from '@app/helpers'
6import { VideoService } from '@app/shared/shared-main'
7import { AbstractVideoList } from '@app/shared/shared-video-miniature'
8import { VideoSortField } from '@shared/models'
9import { VideoTrendingHeaderComponent } from './video-trending-header.component'
10
11@Component({
12 selector: 'my-videos-hot',
13 styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
14 templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
15})
16export class VideoHotComponent extends AbstractVideoList implements OnInit, OnDestroy {
17 HeaderComponent = VideoTrendingHeaderComponent
18 titlePage: string
19 defaultSort: VideoSortField = '-hot'
20
21 useUserVideoPreferences = true
22
23 constructor (
24 protected router: Router,
25 protected serverService: ServerService,
26 protected route: ActivatedRoute,
27 protected notifier: Notifier,
28 protected authService: AuthService,
29 protected userService: UserService,
30 protected screenService: ScreenService,
31 protected storageService: LocalStorageService,
32 protected cfr: ComponentFactoryResolver,
33 private videoService: VideoService,
34 private hooks: HooksService
35 ) {
36 super()
37
38 this.headerComponentInjector = this.getInjector()
39 }
40
41 ngOnInit () {
42 super.ngOnInit()
43
44 this.generateSyndicationList()
45 }
46
47 ngOnDestroy () {
48 super.ngOnDestroy()
49 }
50
51 getVideosObservable (page: number) {
52 const newPagination = immutableAssign(this.pagination, { currentPage: page })
53 const params = {
54 videoPagination: newPagination,
55 sort: this.sort,
56 categoryOneOf: this.categoryOneOf,
57 languageOneOf: this.languageOneOf,
58 nsfwPolicy: this.nsfwPolicy,
59 skipCount: true
60 }
61
62 return this.hooks.wrapObsFun(
63 this.videoService.getVideos.bind(this.videoService),
64 params,
65 'common',
66 'filter:api.trending-videos.videos.list.params',
67 'filter:api.trending-videos.videos.list.result'
68 )
69 }
70
71 generateSyndicationList () {
72 this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, undefined, this.categoryOneOf)
73 }
74
75 getInjector () {
76 return Injector.create({
77 providers: [{
78 provide: 'data',
79 useValue: {
80 model: this.defaultSort
81 }
82 }]
83 })
84 }
85}
diff --git a/client/src/app/+videos/video-list/trending/video-most-liked.component.ts b/client/src/app/+videos/video-list/trending/video-most-liked.component.ts
deleted file mode 100644
index 1781cc6aa..000000000
--- a/client/src/app/+videos/video-list/trending/video-most-liked.component.ts
+++ /dev/null
@@ -1,81 +0,0 @@
1import { Component, ComponentFactoryResolver, Injector, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core'
4import { HooksService } from '@app/core/plugins/hooks.service'
5import { immutableAssign } from '@app/helpers'
6import { VideoService } from '@app/shared/shared-main'
7import { AbstractVideoList } from '@app/shared/shared-video-miniature'
8import { VideoSortField } from '@shared/models'
9import { VideoTrendingHeaderComponent } from './video-trending-header.component'
10
11@Component({
12 selector: 'my-videos-most-liked',
13 styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
14 templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
15})
16export class VideoMostLikedComponent extends AbstractVideoList implements OnInit {
17 HeaderComponent = VideoTrendingHeaderComponent
18 titlePage: string
19 defaultSort: VideoSortField = '-likes'
20
21 useUserVideoPreferences = true
22
23 constructor (
24 protected router: Router,
25 protected serverService: ServerService,
26 protected route: ActivatedRoute,
27 protected notifier: Notifier,
28 protected authService: AuthService,
29 protected userService: UserService,
30 protected screenService: ScreenService,
31 protected storageService: LocalStorageService,
32 protected cfr: ComponentFactoryResolver,
33 private videoService: VideoService,
34 private hooks: HooksService
35 ) {
36 super()
37
38 this.headerComponentInjector = this.getInjector()
39 }
40
41 ngOnInit () {
42 super.ngOnInit()
43
44 this.generateSyndicationList()
45 }
46
47 getVideosObservable (page: number) {
48 const newPagination = immutableAssign(this.pagination, { currentPage: page })
49 const params = {
50 videoPagination: newPagination,
51 sort: this.sort,
52 categoryOneOf: this.categoryOneOf,
53 languageOneOf: this.languageOneOf,
54 nsfwPolicy: this.nsfwPolicy,
55 skipCount: true
56 }
57
58 return this.hooks.wrapObsFun(
59 this.videoService.getVideos.bind(this.videoService),
60 params,
61 'common',
62 'filter:api.most-liked-videos.videos.list.params',
63 'filter:api.most-liked-videos.videos.list.result'
64 )
65 }
66
67 generateSyndicationList () {
68 this.syndicationItems = this.videoService.getVideoFeedUrls(this.sort, undefined, this.categoryOneOf)
69 }
70
71 getInjector () {
72 return Injector.create({
73 providers: [{
74 provide: 'data',
75 useValue: {
76 model: this.defaultSort
77 }
78 }]
79 })
80 }
81}
diff --git a/client/src/app/+videos/video-list/trending/video-trending-header.component.html b/client/src/app/+videos/video-list/trending/video-trending-header.component.html
index a025bf1a2..7eb1e4f95 100644
--- a/client/src/app/+videos/video-list/trending/video-trending-header.component.html
+++ b/client/src/app/+videos/video-list/trending/video-trending-header.component.html
@@ -1,6 +1,8 @@
1<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" [(ngModel)]="data.model" (ngModelChange)="setSort()"> 1<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" [(ngModel)]="data.model" (ngModelChange)="setSort()">
2 <label *ngFor="let button of visibleButtons" ngbButtonLabel class="btn-light" placement="bottom" [ngbTooltip]="button.tooltip" container="body"> 2 <ng-container *ngFor="let button of buttons">
3 <my-global-icon [iconName]="button.iconName"></my-global-icon> 3 <label *ngIf="!button.hidden" ngbButtonLabel class="btn-light" placement="bottom" [ngbTooltip]="button.tooltip" container="body">
4 <input ngbButton type="radio" [value]="button.value"> {{ button.label }} 4 <my-global-icon [iconName]="button.iconName"></my-global-icon>
5 </label> 5 <input ngbButton type="radio" [value]="button.value"> {{ button.label }}
6 </label>
7 </ng-container>
6</div> \ No newline at end of file 8</div> \ No newline at end of file
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 e49b61c68..33eaa2c1e 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
@@ -1,30 +1,34 @@
1import { Component, Inject, OnInit } from '@angular/core' 1import { Component, HostBinding, Inject, OnDestroy, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { VideoListHeaderComponent } from '@app/shared/shared-video-miniature' 3import { VideoListHeaderComponent } from '@app/shared/shared-video-miniature'
4import { GlobalIconName } from '@app/shared/shared-icons' 4import { GlobalIconName } from '@app/shared/shared-icons'
5import { VideoSortField } from '@shared/models'
6import { ServerService } from '@app/core/server/server.service' 5import { ServerService } from '@app/core/server/server.service'
6import { Subscription } from 'rxjs'
7import { RedirectService } from '@app/core'
7 8
8interface VideoTrendingHeaderItem { 9interface VideoTrendingHeaderItem {
9 label: string 10 label: string
10 iconName: GlobalIconName 11 iconName: GlobalIconName
11 value: VideoSortField 12 value: string
12 path: string
13 tooltip?: string 13 tooltip?: string
14 hidden?: boolean 14 hidden?: boolean
15} 15}
16 16
17@Component({ 17@Component({
18 selector: 'video-trending-title-page', 18 selector: 'video-trending-title-page',
19 host: { 'class': 'title-page title-page-single' },
20 styleUrls: [ './video-trending-header.component.scss' ], 19 styleUrls: [ './video-trending-header.component.scss' ],
21 templateUrl: './video-trending-header.component.html' 20 templateUrl: './video-trending-header.component.html'
22}) 21})
23export class VideoTrendingHeaderComponent extends VideoListHeaderComponent implements OnInit { 22export class VideoTrendingHeaderComponent extends VideoListHeaderComponent implements OnInit, OnDestroy {
23 @HostBinding('class') class = 'title-page title-page-single'
24
24 buttons: VideoTrendingHeaderItem[] 25 buttons: VideoTrendingHeaderItem[]
25 26
27 private algorithmChangeSub: Subscription
28
26 constructor ( 29 constructor (
27 @Inject('data') public data: any, 30 @Inject('data') public data: any,
31 private route: ActivatedRoute,
28 private router: Router, 32 private router: Router,
29 private serverService: ServerService 33 private serverService: ServerService
30 ) { 34 ) {
@@ -34,23 +38,20 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
34 { 38 {
35 label: $localize`:A variant of Trending videos based on the number of recent interactions:Hot`, 39 label: $localize`:A variant of Trending videos based on the number of recent interactions:Hot`,
36 iconName: 'flame', 40 iconName: 'flame',
37 value: '-hot', 41 value: 'hot',
38 path: 'hot',
39 tooltip: $localize`Videos totalizing the most interactions for recent videos`, 42 tooltip: $localize`Videos totalizing the most interactions for recent videos`,
40 hidden: true 43 hidden: true
41 }, 44 },
42 { 45 {
43 label: $localize`:Main variant of Trending videos based on number of recent views:Views`, 46 label: $localize`:Main variant of Trending videos based on number of recent views:Views`,
44 iconName: 'trending', 47 iconName: 'trending',
45 value: '-trending', 48 value: 'most-viewed',
46 path: 'most-viewed', 49 tooltip: $localize`Videos totalizing the most views during the last 24 hours`
47 tooltip: $localize`Videos totalizing the most views during the last 24 hours`,
48 }, 50 },
49 { 51 {
50 label: $localize`:A variant of Trending videos based on the number of likes:Likes`, 52 label: $localize`:A variant of Trending videos based on the number of likes:Likes`,
51 iconName: 'like', 53 iconName: 'like',
52 value: '-likes', 54 value: 'most-liked',
53 path: 'most-liked',
54 tooltip: $localize`Videos that have the most likes` 55 tooltip: $localize`Videos that have the most likes`
55 } 56 }
56 ] 57 ]
@@ -59,20 +60,40 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent imple
59 ngOnInit () { 60 ngOnInit () {
60 this.serverService.getConfig() 61 this.serverService.getConfig()
61 .subscribe(config => { 62 .subscribe(config => {
62 // don't filter if auto-blacklist is not enabled as this will be the only list 63 this.buttons = this.buttons.map(b => {
63 if (config.instance.pages.hot.enabled) { 64 b.hidden = !config.trending.videos.algorithms.enabled.includes(b.value)
64 const index = this.buttons.findIndex(b => b.path === 'hot') 65 return b
65 this.buttons[index].hidden = false 66 })
66 }
67 }) 67 })
68
69 this.algorithmChangeSub = this.route.queryParams.subscribe(
70 queryParams => {
71 const algorithm = queryParams['alg']
72 if (algorithm) {
73 this.data.model = algorithm
74 } else {
75 this.data.model = RedirectService.DEFAULT_TRENDING_ALGORITHM
76 }
77 }
78 )
68 } 79 }
69 80
70 get visibleButtons () { 81 ngOnDestroy () {
71 return this.buttons.filter(b => !b.hidden) 82 if (this.algorithmChangeSub) this.algorithmChangeSub.unsubscribe()
72 } 83 }
73 84
74 setSort () { 85 setSort () {
75 const path = this.buttons.find(b => b.value === this.data.model).path 86 const alg = this.data.model !== RedirectService.DEFAULT_TRENDING_ALGORITHM
76 this.router.navigate([ `/videos/${path}` ]) 87 ? this.data.model
88 : undefined
89
90 this.router.navigate(
91 [],
92 {
93 relativeTo: this.route,
94 queryParams: { alg },
95 queryParamsHandling: 'merge'
96 }
97 )
77 } 98 }
78} 99}
diff --git a/client/src/app/+videos/video-list/trending/video-most-viewed.component.ts b/client/src/app/+videos/video-list/trending/video-trending.component.ts
index 98ced42d6..6128c4acd 100644
--- a/client/src/app/+videos/video-list/trending/video-most-viewed.component.ts
+++ b/client/src/app/+videos/video-list/trending/video-trending.component.ts
@@ -1,25 +1,28 @@
1import { Component, ComponentFactoryResolver, Injector, OnDestroy, OnInit } from '@angular/core' 1import { Component, ComponentFactoryResolver, Injector, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' 3import { AuthService, LocalStorageService, Notifier, RedirectService, ScreenService, ServerService, UserService } from '@app/core'
4import { HooksService } from '@app/core/plugins/hooks.service' 4import { HooksService } from '@app/core/plugins/hooks.service'
5import { immutableAssign } from '@app/helpers' 5import { immutableAssign } from '@app/helpers'
6import { VideoService } from '@app/shared/shared-main' 6import { VideoService } from '@app/shared/shared-main'
7import { AbstractVideoList } from '@app/shared/shared-video-miniature' 7import { AbstractVideoList } from '@app/shared/shared-video-miniature'
8import { VideoSortField } from '@shared/models' 8import { VideoSortField } from '@shared/models'
9import { Subscription } from 'rxjs'
9import { VideoTrendingHeaderComponent } from './video-trending-header.component' 10import { VideoTrendingHeaderComponent } from './video-trending-header.component'
10 11
11@Component({ 12@Component({
12 selector: 'my-videos-most-viewed', 13 selector: 'my-videos-hot',
13 styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ], 14 styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
14 templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html' 15 templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
15}) 16})
16export class VideoMostViewedComponent extends AbstractVideoList implements OnInit, OnDestroy { 17export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
17 HeaderComponent = VideoTrendingHeaderComponent 18 HeaderComponent = VideoTrendingHeaderComponent
18 titlePage: string 19 titlePage: string
19 defaultSort: VideoSortField = '-trending' 20 defaultSort: VideoSortField = '-trending'
20 21
21 useUserVideoPreferences = true 22 useUserVideoPreferences = true
22 23
24 private algorithmChangeSub: Subscription
25
23 constructor ( 26 constructor (
24 protected router: Router, 27 protected router: Router,
25 protected serverService: ServerService, 28 protected serverService: ServerService,
@@ -35,6 +38,8 @@ export class VideoMostViewedComponent extends AbstractVideoList implements OnIni
35 ) { 38 ) {
36 super() 39 super()
37 40
41 this.defaultSort = this.parseAlgorithm(RedirectService.DEFAULT_TRENDING_ALGORITHM)
42
38 this.headerComponentInjector = this.getInjector() 43 this.headerComponentInjector = this.getInjector()
39 } 44 }
40 45
@@ -43,23 +48,19 @@ export class VideoMostViewedComponent extends AbstractVideoList implements OnIni
43 48
44 this.generateSyndicationList() 49 this.generateSyndicationList()
45 50
46 this.serverService.getConfig().subscribe( 51 this.algorithmChangeSub = this.route.queryParams.subscribe(
47 config => { 52 queryParams => {
48 const trendingDays = config.trending.videos.intervalDays 53 const algorithm = queryParams['alg'] || RedirectService.DEFAULT_TRENDING_ALGORITHM
49
50 if (trendingDays === 1) {
51 this.titleTooltip = $localize`Trending videos are those totalizing the greatest number of views during the last 24 hours`
52 } else {
53 this.titleTooltip = $localize`Trending videos are those totalizing the greatest number of views during the last ${trendingDays} days`
54 }
55 54
56 this.headerComponentInjector = this.getInjector() 55 this.sort = this.parseAlgorithm(algorithm)
57 this.setHeader() 56 this.reloadVideos()
58 }) 57 }
58 )
59 } 59 }
60 60
61 ngOnDestroy () { 61 ngOnDestroy () {
62 super.ngOnDestroy() 62 super.ngOnDestroy()
63 if (this.algorithmChangeSub) this.algorithmChangeSub.unsubscribe()
63 } 64 }
64 65
65 getVideosObservable (page: number) { 66 getVideosObservable (page: number) {
@@ -96,4 +97,15 @@ export class VideoMostViewedComponent extends AbstractVideoList implements OnIni
96 }] 97 }]
97 }) 98 })
98 } 99 }
100
101 private parseAlgorithm (algorithm: string): VideoSortField {
102 switch (algorithm) {
103 case 'most-viewed':
104 return '-trending'
105 case 'most-liked':
106 return '-likes'
107 default:
108 return '-' + algorithm as VideoSortField
109 }
110 }
99} 111}
diff --git a/client/src/app/+videos/videos-routing.module.ts b/client/src/app/+videos/videos-routing.module.ts
index 973935af8..16e3b9bb2 100644
--- a/client/src/app/+videos/videos-routing.module.ts
+++ b/client/src/app/+videos/videos-routing.module.ts
@@ -1,11 +1,9 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { LoginGuard, TrendingGuard } from '@app/core' 3import { LoginGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core' 4import { MetaGuard } from '@ngx-meta/core'
5import { VideoTrendingComponent } from './video-list'
5import { VideoOverviewComponent } from './video-list/overview/video-overview.component' 6import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
6import { VideoHotComponent } from './video-list/trending/video-hot.component'
7import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component'
8import { VideoMostViewedComponent } from './video-list/trending/video-most-viewed.component'
9import { VideoLocalComponent } from './video-list/video-local.component' 7import { VideoLocalComponent } from './video-list/video-local.component'
10import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component' 8import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
11import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component' 9import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
@@ -28,46 +26,16 @@ const videosRoutes: Routes = [
28 }, 26 },
29 { 27 {
30 path: 'trending', 28 path: 'trending',
31 canActivate: [ TrendingGuard ] 29 component: VideoTrendingComponent,
32 },
33 {
34 path: 'hot',
35 component: VideoHotComponent,
36 data: { 30 data: {
37 meta: { 31 meta: {
38 title: $localize`Hot videos` 32 title: $localize`Trending videos`
39 },
40 reuse: {
41 enabled: true,
42 key: 'hot-videos-list'
43 }
44 }
45 },
46 {
47 path: 'most-viewed',
48 component: VideoMostViewedComponent,
49 data: {
50 meta: {
51 title: $localize`Most viewed videos`
52 },
53 reuse: {
54 enabled: true,
55 key: 'most-viewed-videos-list'
56 } 33 }
57 } 34 }
58 }, 35 },
59 { 36 {
60 path: 'most-liked', 37 path: 'most-liked',
61 component: VideoMostLikedComponent, 38 redirectTo: 'trending?alg=most-liked'
62 data: {
63 meta: {
64 title: $localize`Most liked videos`
65 },
66 reuse: {
67 enabled: true,
68 key: 'most-liked-videos-list'
69 }
70 }
71 }, 39 },
72 { 40 {
73 path: 'recently-added', 41 path: 'recently-added',
diff --git a/client/src/app/+videos/videos.module.ts b/client/src/app/+videos/videos.module.ts
index ae9c680eb..61d012d63 100644
--- a/client/src/app/+videos/videos.module.ts
+++ b/client/src/app/+videos/videos.module.ts
@@ -1,16 +1,12 @@
1import { CommonModule } from '@angular/common'
2import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
3import { SharedFormModule } from '@app/shared/shared-forms' 2import { SharedFormModule } from '@app/shared/shared-forms'
4import { SharedGlobalIconModule } from '@app/shared/shared-icons' 3import { SharedGlobalIconModule } from '@app/shared/shared-icons'
5import { SharedMainModule } from '@app/shared/shared-main' 4import { SharedMainModule } from '@app/shared/shared-main'
6import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription' 5import { SharedUserSubscriptionModule } from '@app/shared/shared-user-subscription'
7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' 6import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
8import { OverviewService } from './video-list' 7import { OverviewService, VideoTrendingComponent } from './video-list'
9import { VideoOverviewComponent } from './video-list/overview/video-overview.component' 8import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
10import { VideoTrendingHeaderComponent } from './video-list/trending/video-trending-header.component' 9import { VideoTrendingHeaderComponent } from './video-list/trending/video-trending-header.component'
11import { VideoHotComponent } from './video-list/trending/video-hot.component'
12import { VideoMostViewedComponent } from './video-list/trending/video-most-viewed.component'
13import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component'
14import { VideoLocalComponent } from './video-list/video-local.component' 10import { VideoLocalComponent } from './video-list/video-local.component'
15import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component' 11import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
16import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component' 12import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
@@ -32,9 +28,7 @@ import { VideosComponent } from './videos.component'
32 VideosComponent, 28 VideosComponent,
33 29
34 VideoTrendingHeaderComponent, 30 VideoTrendingHeaderComponent,
35 VideoMostViewedComponent, 31 VideoTrendingComponent,
36 VideoHotComponent,
37 VideoMostLikedComponent,
38 VideoRecentlyAddedComponent, 32 VideoRecentlyAddedComponent,
39 VideoLocalComponent, 33 VideoLocalComponent,
40 VideoUserSubscriptionsComponent, 34 VideoUserSubscriptionsComponent,
diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html
index 77d876f8e..108b127be 100644
--- a/client/src/app/app.component.html
+++ b/client/src/app/app.component.html
@@ -8,7 +8,7 @@
8 <div class="top-left-block"> 8 <div class="top-left-block">
9 <span class="icon icon-menu" role="button" [title]="getToggleTitle()" (click)="menu.toggleMenu()"></span> 9 <span class="icon icon-menu" role="button" [title]="getToggleTitle()" (click)="menu.toggleMenu()"></span>
10 10
11 <a class="peertube-title" [routerLink]="defaultRoute"> 11 <a class="peertube-title c-hand" (click)="goToDefaultRoute()">
12 <span class="icon icon-logo"></span> 12 <span class="icon icon-logo"></span>
13 <span class="instance-name">{{ instanceName }}</span> 13 <span class="instance-name">{{ instanceName }}</span>
14 </a> 14 </a>
diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts
index ca4b69899..66d871b4a 100644
--- a/client/src/app/app.component.ts
+++ b/client/src/app/app.component.ts
@@ -66,8 +66,8 @@ export class AppComponent implements OnInit, AfterViewInit {
66 return this.serverConfig.instance.name 66 return this.serverConfig.instance.name
67 } 67 }
68 68
69 get defaultRoute () { 69 goToDefaultRoute () {
70 return RedirectService.DEFAULT_ROUTE 70 return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE)
71 } 71 }
72 72
73 ngOnInit () { 73 ngOnInit () {
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index 32dfc8f36..2392a234c 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -15,7 +15,7 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
15import { Notifier } from './notification' 15import { Notifier } from './notification'
16import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer' 16import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
17import { RestExtractor, RestService } from './rest' 17import { RestExtractor, RestService } from './rest'
18import { LoginGuard, RedirectService, UserRightGuard, UnloggedGuard, TrendingGuard } from './routing' 18import { LoginGuard, RedirectService, UserRightGuard, UnloggedGuard } from './routing'
19import { CanDeactivateGuard } from './routing/can-deactivate-guard.service' 19import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
20import { ServerConfigResolver } from './routing/server-config-resolver.service' 20import { ServerConfigResolver } from './routing/server-config-resolver.service'
21import { ScopedTokensService } from './scoped-tokens' 21import { ScopedTokensService } from './scoped-tokens'
@@ -56,7 +56,6 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra
56 LoginGuard, 56 LoginGuard,
57 UserRightGuard, 57 UserRightGuard,
58 UnloggedGuard, 58 UnloggedGuard,
59 TrendingGuard,
60 59
61 PluginService, 60 PluginService,
62 HooksService, 61 HooksService,
diff --git a/client/src/app/core/routing/index.ts b/client/src/app/core/routing/index.ts
index b3985d870..239c27caf 100644
--- a/client/src/app/core/routing/index.ts
+++ b/client/src/app/core/routing/index.ts
@@ -8,4 +8,3 @@ export * from './redirect.service'
8export * from './server-config-resolver.service' 8export * from './server-config-resolver.service'
9export * from './unlogged-guard.service' 9export * from './unlogged-guard.service'
10export * from './user-right-guard.service' 10export * from './user-right-guard.service'
11export * from './trending-guard.service'
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts
index 76e28e461..6d26fb504 100644
--- a/client/src/app/core/routing/redirect.service.ts
+++ b/client/src/app/core/routing/redirect.service.ts
@@ -7,14 +7,13 @@ export 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 9 static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
10 static INIT_DEFAULT_TRENDING_ROUTE = '/videos/most-viewed' 10 static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed'
11 static DEFAULT_TRENDING_ROUTE = RedirectService.INIT_DEFAULT_TRENDING_ROUTE 11 static DEFAULT_TRENDING_ALGORITHM = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
12 12
13 private previousUrl: string 13 private previousUrl: string
14 private currentUrl: string 14 private currentUrl: string
15 15
16 private redirectingToHomepage = false 16 private redirectingToHomepage = false
17 private redirectingToTrending = false
18 17
19 constructor ( 18 constructor (
20 private router: Router, 19 private router: Router,
@@ -22,27 +21,25 @@ export class RedirectService {
22 ) { 21 ) {
23 // 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
24 const tmpConfig = this.serverService.getTmpConfig() 23 const tmpConfig = this.serverService.getTmpConfig()
25 if (tmpConfig && tmpConfig.instance) { 24 if (tmpConfig?.instance?.defaultClientRoute) {
26 if (tmpConfig.instance.defaultClientRoute) { 25 RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
27 RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute 26 }
28 } 27 if (tmpConfig?.trending?.videos?.algorithms?.default) {
29 if (tmpConfig.instance.defaultTrendingRoute) { 28 RedirectService.DEFAULT_TRENDING_ALGORITHM = tmpConfig.trending.videos.algorithms.default
30 RedirectService.DEFAULT_TRENDING_ROUTE = tmpConfig.instance.defaultTrendingRoute
31 }
32 } 29 }
33 30
34 // Load default route 31 // Load default route
35 this.serverService.getConfig() 32 this.serverService.getConfig()
36 .subscribe(config => { 33 .subscribe(config => {
37 const defaultRouteConfig = config.instance.defaultClientRoute 34 const defaultRouteConfig = config.instance.defaultClientRoute
38 const defaultTrendingConfig = config.instance.defaultTrendingRoute 35 const defaultTrendingConfig = config.trending.videos.algorithms.default
39 36
40 if (defaultRouteConfig) { 37 if (defaultRouteConfig) {
41 RedirectService.DEFAULT_ROUTE = defaultRouteConfig 38 RedirectService.DEFAULT_ROUTE = defaultRouteConfig
42 } 39 }
43 40
44 if (defaultTrendingConfig) { 41 if (defaultTrendingConfig) {
45 RedirectService.DEFAULT_TRENDING_ROUTE = defaultTrendingConfig 42 RedirectService.DEFAULT_TRENDING_ALGORITHM = defaultTrendingConfig
46 } 43 }
47 }) 44 })
48 45
@@ -70,15 +67,6 @@ export class RedirectService {
70 return this.redirectToHomepage() 67 return this.redirectToHomepage()
71 } 68 }
72 69
73 redirectToTrending () {
74 if (this.redirectingToTrending) return
75
76 this.redirectingToTrending = true
77
78 this.router.navigate([ RedirectService.DEFAULT_TRENDING_ROUTE ])
79 .then(() => this.redirectingToTrending = false)
80 }
81
82 redirectToHomepage (skipLocationChange = false) { 70 redirectToHomepage (skipLocationChange = false) {
83 if (this.redirectingToHomepage) return 71 if (this.redirectingToHomepage) return
84 72
@@ -86,7 +74,7 @@ export class RedirectService {
86 74
87 console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE) 75 console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE)
88 76
89 this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { skipLocationChange }) 77 this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
90 .then(() => this.redirectingToHomepage = false) 78 .then(() => this.redirectingToHomepage = false)
91 .catch(() => { 79 .catch(() => {
92 this.redirectingToHomepage = false 80 this.redirectingToHomepage = false
@@ -98,7 +86,7 @@ export class RedirectService {
98 ) 86 )
99 87
100 RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE 88 RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
101 return this.router.navigate([ RedirectService.DEFAULT_ROUTE ], { skipLocationChange }) 89 return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
102 }) 90 })
103 91
104 } 92 }
diff --git a/client/src/app/core/routing/trending-guard.service.ts b/client/src/app/core/routing/trending-guard.service.ts
deleted file mode 100644
index 7db7fe994..000000000
--- a/client/src/app/core/routing/trending-guard.service.ts
+++ /dev/null
@@ -1,14 +0,0 @@
1import { Injectable } from '@angular/core'
2import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'
3import { RedirectService } from './redirect.service'
4
5@Injectable()
6export class TrendingGuard implements CanActivate {
7
8 constructor (private redirectService: RedirectService) {}
9
10 canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
11 this.redirectService.redirectToTrending()
12 return false
13 }
14}
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index 5f13190b4..a38883eee 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -39,12 +39,6 @@ export class ServerService {
39 isNSFW: false, 39 isNSFW: false,
40 defaultNSFWPolicy: 'do_not_list' as 'do_not_list', 40 defaultNSFWPolicy: 'do_not_list' as 'do_not_list',
41 defaultClientRoute: '', 41 defaultClientRoute: '',
42 defaultTrendingRoute: '',
43 pages: {
44 hot: {
45 enabled: true
46 }
47 },
48 customizations: { 42 customizations: {
49 javascript: '', 43 javascript: '',
50 css: '' 44 css: ''
@@ -131,7 +125,11 @@ export class ServerService {
131 }, 125 },
132 trending: { 126 trending: {
133 videos: { 127 videos: {
134 intervalDays: 0 128 intervalDays: 0,
129 algorithms: {
130 enabled: [ 'hot', 'most-viewed', 'most-liked' ],
131 default: 'most-viewed'
132 }
135 } 133 }
136 }, 134 },
137 autoBlacklist: { 135 autoBlacklist: {
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index fc57b970b..9aa397edd 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -127,14 +127,10 @@
127 <ng-container i18n>Discover</ng-container> 127 <ng-container i18n>Discover</ng-container>
128 </a> 128 </a>
129 129
130 <a routerLink="/videos/trending" routerLinkActive="active" [ngClass]="{ 'active': hot.isActive || mostViewed.isActive || mostLiked.isActive }"> 130 <a routerLink="/videos/trending" routerLinkActive="active">
131 <my-global-icon iconName="trending" aria-hidden="true"></my-global-icon> 131 <my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
132 <ng-container i18n>Trending</ng-container> 132 <ng-container i18n>Trending</ng-container>
133 </a> 133 </a>
134 <a routerLink="/videos/hot" routerLinkActive #hot="routerLinkActive" hidden></a>
135 <a routerLink="/videos/most-viewed" routerLinkActive #mostViewed="routerLinkActive" hidden></a>
136 <a routerLink="/videos/most-liked" routerLinkActive #mostLiked="routerLinkActive" hidden></a>
137
138 134
139 <a routerLink="/videos/recently-added" routerLinkActive="active"> 135 <a routerLink="/videos/recently-added" routerLinkActive="active">
140 <my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon> 136 <my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon>
diff --git a/client/src/app/shared/shared-video-miniature/video-list-header.component.ts b/client/src/app/shared/shared-video-miniature/video-list-header.component.ts
index 67bbf7d7a..08a961be1 100644
--- a/client/src/app/shared/shared-video-miniature/video-list-header.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-list-header.component.ts
@@ -1,16 +1,22 @@
1import { Component, Inject, ViewEncapsulation } from '@angular/core' 1import { Component, Inject, ViewEncapsulation } from '@angular/core'
2 2
3export interface GenericHeaderData {
4 titlePage: string
5 titleTooltip?: string
6}
7
3export abstract class GenericHeaderComponent { 8export abstract class GenericHeaderComponent {
4 constructor (@Inject('data') public data: any) {} 9 constructor (@Inject('data') public data: GenericHeaderData) {}
5} 10}
6 11
7@Component({ 12@Component({
8 selector: 'my-video-list-header', 13 selector: 'my-video-list-header',
14 // tslint:disable-next-line:use-component-view-encapsulation
9 encapsulation: ViewEncapsulation.None, 15 encapsulation: ViewEncapsulation.None,
10 templateUrl: './video-list-header.component.html' 16 templateUrl: './video-list-header.component.html'
11}) 17})
12export class VideoListHeaderComponent extends GenericHeaderComponent { 18export class VideoListHeaderComponent extends GenericHeaderComponent {
13 constructor (@Inject('data') public data: any) { 19 constructor (@Inject('data') public data: GenericHeaderData) {
14 super(data) 20 super(data)
15 } 21 }
16} 22}