aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+videos/+video-watch/recommendations
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+videos/+video-watch/recommendations')
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recent-videos-recommendation.service.ts81
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommendation-info.model.ts4
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommendations.module.ts34
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommendations.service.ts7
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html24
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss31
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts91
-rw-r--r--client/src/app/+videos/+video-watch/recommendations/recommended-videos.store.ts37
8 files changed, 309 insertions, 0 deletions
diff --git a/client/src/app/+videos/+video-watch/recommendations/recent-videos-recommendation.service.ts b/client/src/app/+videos/+video-watch/recommendations/recent-videos-recommendation.service.ts
new file mode 100644
index 000000000..29fa268f4
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/recommendations/recent-videos-recommendation.service.ts
@@ -0,0 +1,81 @@
1import { Observable, of } from 'rxjs'
2import { map, switchMap } from 'rxjs/operators'
3import { Injectable } from '@angular/core'
4import { ServerService, UserService } from '@app/core'
5import { Video, VideoService } from '@app/shared/shared-main'
6import { AdvancedSearch, SearchService } from '@app/shared/shared-search'
7import { ServerConfig } from '@shared/models'
8import { RecommendationInfo } from './recommendation-info.model'
9import { RecommendationService } from './recommendations.service'
10
11/**
12 * Provides "recommendations" by providing the most recently uploaded videos.
13 */
14@Injectable()
15export class RecentVideosRecommendationService implements RecommendationService {
16 readonly pageSize = 5
17
18 private config: ServerConfig
19
20 constructor (
21 private videos: VideoService,
22 private searchService: SearchService,
23 private userService: UserService,
24 private serverService: ServerService
25 ) {
26 this.config = this.serverService.getTmpConfig()
27
28 this.serverService.getConfig()
29 .subscribe(config => this.config = config)
30 }
31
32 getRecommendations (recommendation: RecommendationInfo): Observable<Video[]> {
33 return this.fetchPage(1, recommendation)
34 .pipe(
35 map(videos => {
36 const otherVideos = videos.filter(v => v.uuid !== recommendation.uuid)
37 return otherVideos.slice(0, this.pageSize)
38 })
39 )
40 }
41
42 private fetchPage (page: number, recommendation: RecommendationInfo): Observable<Video[]> {
43 const pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 }
44 const defaultSubscription = this.videos.getVideos({ videoPagination: pagination, sort: '-createdAt' })
45 .pipe(map(v => v.data))
46
47 const tags = recommendation.tags
48 const searchIndexConfig = this.config.search.searchIndex
49 if (
50 !tags || tags.length === 0 ||
51 (searchIndexConfig.enabled === true && searchIndexConfig.disableLocalSearch === true)
52 ) {
53 return defaultSubscription
54 }
55
56 return this.userService.getAnonymousOrLoggedUser()
57 .pipe(
58 map(user => {
59 return {
60 search: '',
61 componentPagination: pagination,
62 advancedSearch: new AdvancedSearch({
63 tagsOneOf: recommendation.tags.join(','),
64 sort: '-createdAt',
65 searchTarget: 'local',
66 nsfw: user.nsfwPolicy
67 ? this.videos.nsfwPolicyToParam(user.nsfwPolicy)
68 : undefined
69 })
70 }
71 }),
72 switchMap(params => this.searchService.searchVideos(params)),
73 map(v => v.data),
74 switchMap(videos => {
75 if (videos.length <= 1) return defaultSubscription
76
77 return of(videos)
78 })
79 )
80 }
81}
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommendation-info.model.ts b/client/src/app/+videos/+video-watch/recommendations/recommendation-info.model.ts
new file mode 100644
index 000000000..0233563bb
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/recommendations/recommendation-info.model.ts
@@ -0,0 +1,4 @@
1export interface RecommendationInfo {
2 uuid: string
3 tags?: string[]
4}
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommendations.module.ts b/client/src/app/+videos/+video-watch/recommendations/recommendations.module.ts
new file mode 100644
index 000000000..259afb196
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/recommendations/recommendations.module.ts
@@ -0,0 +1,34 @@
1import { InputSwitchModule } from 'primeng/inputswitch'
2import { CommonModule } from '@angular/common'
3import { NgModule } from '@angular/core'
4import { SharedMainModule } from '@app/shared/shared-main'
5import { SharedSearchModule } from '@app/shared/shared-search'
6import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
7import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
8import { RecentVideosRecommendationService } from './recent-videos-recommendation.service'
9import { RecommendedVideosComponent } from './recommended-videos.component'
10import { RecommendedVideosStore } from './recommended-videos.store'
11
12@NgModule({
13 imports: [
14 CommonModule,
15 InputSwitchModule,
16
17 SharedMainModule,
18 SharedSearchModule,
19 SharedVideoPlaylistModule,
20 SharedVideoMiniatureModule
21 ],
22 declarations: [
23 RecommendedVideosComponent
24 ],
25 exports: [
26 RecommendedVideosComponent
27 ],
28 providers: [
29 RecommendedVideosStore,
30 RecentVideosRecommendationService
31 ]
32})
33export class RecommendationsModule {
34}
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommendations.service.ts b/client/src/app/+videos/+video-watch/recommendations/recommendations.service.ts
new file mode 100644
index 000000000..1d79d35f6
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/recommendations/recommendations.service.ts
@@ -0,0 +1,7 @@
1import { Observable } from 'rxjs'
2import { Video } from '@app/shared/shared-main'
3import { RecommendationInfo } from './recommendation-info.model'
4
5export interface RecommendationService {
6 getRecommendations (recommendation: RecommendationInfo): Observable<Video[]>
7}
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html
new file mode 100644
index 000000000..0467cabf5
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.html
@@ -0,0 +1,24 @@
1<div class="other-videos">
2 <ng-container *ngIf="hasVideos$ | async">
3 <div class="title-page-container">
4 <h2 i18n class="title-page title-page-single">
5 Other videos
6 </h2>
7 <div *ngIf="!playlist" class="title-page-autoplay"
8 [ngbTooltip]="autoPlayNextVideoTooltip" placement="bottom-right auto"
9 >
10 <span i18n>AUTOPLAY</span>
11 <p-inputSwitch class="small" [(ngModel)]="autoPlayNextVideo" (ngModelChange)="switchAutoPlayNextVideo()"></p-inputSwitch>
12 </div>
13 </div>
14
15 <ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count">
16 <my-video-miniature
17 [displayOptions]="displayOptions" [video]="video" [user]="userMiniature"
18 (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()">
19 </my-video-miniature>
20
21 <hr *ngIf="!playlist && i == 0 && length > 1" />
22 </ng-container>
23 </ng-container>
24</div>
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss
new file mode 100644
index 000000000..b278c9654
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.scss
@@ -0,0 +1,31 @@
1.title-page-container {
2 display: flex;
3 justify-content: space-between;
4 align-items: baseline;
5 margin-bottom: 25px;
6 flex-wrap: wrap-reverse;
7
8 .title-page.active, .title-page.title-page-single {
9 margin-bottom: unset;
10 margin-right: .5rem !important;
11 }
12}
13
14.title-page-autoplay {
15 display: flex;
16 width: max-content;
17 height: max-content;
18 align-items: center;
19 margin-left: auto;
20
21 span {
22 margin-right: 0.3rem;
23 text-transform: uppercase;
24 font-size: 85%;
25 font-weight: 600;
26 }
27}
28
29hr {
30 margin-top: 0;
31}
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts
new file mode 100644
index 000000000..016975341
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.component.ts
@@ -0,0 +1,91 @@
1import { Observable } from 'rxjs'
2import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'
3import { AuthService, Notifier, SessionStorageService, User, UserService } from '@app/core'
4import { Video } from '@app/shared/shared-main'
5import { MiniatureDisplayOptions } from '@app/shared/shared-video-miniature'
6import { VideoPlaylist } from '@app/shared/shared-video-playlist'
7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { RecommendationInfo } from './recommendation-info.model'
9import { RecommendedVideosStore } from './recommended-videos.store'
10
11@Component({
12 selector: 'my-recommended-videos',
13 templateUrl: './recommended-videos.component.html',
14 styleUrls: [ './recommended-videos.component.scss' ]
15})
16export class RecommendedVideosComponent implements OnInit, OnChanges {
17 @Input() inputRecommendation: RecommendationInfo
18 @Input() playlist: VideoPlaylist
19 @Output() gotRecommendations = new EventEmitter<Video[]>()
20
21 autoPlayNextVideo: boolean
22 autoPlayNextVideoTooltip: string
23
24 displayOptions: MiniatureDisplayOptions = {
25 date: true,
26 views: true,
27 by: true,
28 avatar: true
29 }
30
31 userMiniature: User
32
33 readonly hasVideos$: Observable<boolean>
34 readonly videos$: Observable<Video[]>
35
36 constructor (
37 private userService: UserService,
38 private authService: AuthService,
39 private notifier: Notifier,
40 private i18n: I18n,
41 private store: RecommendedVideosStore,
42 private sessionStorageService: SessionStorageService
43 ) {
44 this.videos$ = this.store.recommendations$
45 this.hasVideos$ = this.store.hasRecommendations$
46 this.videos$.subscribe(videos => this.gotRecommendations.emit(videos))
47
48 if (this.authService.isLoggedIn()) {
49 this.autoPlayNextVideo = this.authService.getUser().autoPlayNextVideo
50 } else {
51 this.autoPlayNextVideo = this.sessionStorageService.getItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' || false
52 this.sessionStorageService.watch([User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO]).subscribe(
53 () => this.autoPlayNextVideo = this.sessionStorageService.getItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
54 )
55 }
56
57 this.autoPlayNextVideoTooltip = this.i18n('When active, the next video is automatically played after the current one.')
58 }
59
60 ngOnInit () {
61 this.userService.getAnonymousOrLoggedUser()
62 .subscribe(user => this.userMiniature = user)
63 }
64
65 ngOnChanges () {
66 if (this.inputRecommendation) {
67 this.store.requestNewRecommendations(this.inputRecommendation)
68 }
69 }
70
71 onVideoRemoved () {
72 this.store.requestNewRecommendations(this.inputRecommendation)
73 }
74
75 switchAutoPlayNextVideo () {
76 this.sessionStorageService.setItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, this.autoPlayNextVideo.toString())
77
78 if (this.authService.isLoggedIn()) {
79 const details = {
80 autoPlayNextVideo: this.autoPlayNextVideo
81 }
82
83 this.userService.updateMyProfile(details).subscribe(
84 () => {
85 this.authService.refreshUserInformation()
86 },
87 err => this.notifier.error(err.message)
88 )
89 }
90 }
91}
diff --git a/client/src/app/+videos/+video-watch/recommendations/recommended-videos.store.ts b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.store.ts
new file mode 100644
index 000000000..8c3fb6480
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/recommendations/recommended-videos.store.ts
@@ -0,0 +1,37 @@
1import { Observable, ReplaySubject } from 'rxjs'
2import { map, shareReplay, switchMap, take } from 'rxjs/operators'
3import { Inject, Injectable } from '@angular/core'
4import { Video } from '@app/shared/shared-main'
5import { RecentVideosRecommendationService } from './recent-videos-recommendation.service'
6import { RecommendationInfo } from './recommendation-info.model'
7import { RecommendationService } from './recommendations.service'
8
9/**
10 * This store is intended to provide data for the RecommendedVideosComponent.
11 */
12@Injectable()
13export class RecommendedVideosStore {
14 public readonly recommendations$: Observable<Video[]>
15 public readonly hasRecommendations$: Observable<boolean>
16 private readonly requestsForLoad$$ = new ReplaySubject<RecommendationInfo>(1)
17
18 constructor (
19 @Inject(RecentVideosRecommendationService) private recommendations: RecommendationService
20 ) {
21 this.recommendations$ = this.requestsForLoad$$.pipe(
22 switchMap(requestedRecommendation => {
23 return this.recommendations.getRecommendations(requestedRecommendation)
24 .pipe(take(1))
25 }),
26 shareReplay()
27 )
28
29 this.hasRecommendations$ = this.recommendations$.pipe(
30 map(otherVideos => otherVideos.length > 0)
31 )
32 }
33
34 requestNewRecommendations (recommend: RecommendationInfo) {
35 this.requestsForLoad$$.next(recommend)
36 }
37}