aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/+videos/+video-watch/shared/recommendations
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/+videos/+video-watch/shared/recommendations')
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/index.ts5
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts79
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/recommendation-info.model.ts4
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/recommendations.module.ts35
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/recommendations.service.ts7
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html26
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss68
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts95
-rw-r--r--client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.store.ts37
9 files changed, 356 insertions, 0 deletions
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/index.ts b/client/src/app/+videos/+video-watch/shared/recommendations/index.ts
new file mode 100644
index 000000000..ffcf84585
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/recommendations/index.ts
@@ -0,0 +1,5 @@
1export * from './recent-videos-recommendation.service'
2export * from './recommendation-info.model'
3export * from './recommendations.module'
4export * from './recommended-videos.component'
5export * from './recommended-videos.store'
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts
new file mode 100644
index 000000000..4654da847
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/recommendations/recent-videos-recommendation.service.ts
@@ -0,0 +1,79 @@
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 { HTMLServerConfig } 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: HTMLServerConfig
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.getHTMLConfig()
27 }
28
29 getRecommendations (recommendation: RecommendationInfo): Observable<Video[]> {
30
31 return this.fetchPage(1, recommendation)
32 .pipe(
33 map(videos => {
34 const otherVideos = videos.filter(v => v.uuid !== recommendation.uuid)
35 return otherVideos.slice(0, this.pageSize)
36 })
37 )
38 }
39
40 private fetchPage (page: number, recommendation: RecommendationInfo): Observable<Video[]> {
41 const pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 }
42 const defaultSubscription = this.videos.getVideos({ videoPagination: pagination, sort: '-createdAt' })
43 .pipe(map(v => v.data))
44
45 const tags = recommendation.tags
46 const searchIndexConfig = this.config.search.searchIndex
47 if (
48 !tags || tags.length === 0 ||
49 (searchIndexConfig.enabled === true && searchIndexConfig.disableLocalSearch === true)
50 ) {
51 return defaultSubscription
52 }
53
54 return this.userService.getAnonymousOrLoggedUser()
55 .pipe(
56 map(user => {
57 return {
58 search: '',
59 componentPagination: pagination,
60 advancedSearch: new AdvancedSearch({
61 tagsOneOf: recommendation.tags.join(','),
62 sort: '-publishedAt',
63 searchTarget: 'local',
64 nsfw: user.nsfwPolicy
65 ? this.videos.nsfwPolicyToParam(user.nsfwPolicy)
66 : undefined
67 })
68 }
69 }),
70 switchMap(params => this.searchService.searchVideos(params)),
71 map(v => v.data),
72 switchMap(videos => {
73 if (videos.length <= 1) return defaultSubscription
74
75 return of(videos)
76 })
77 )
78 }
79}
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommendation-info.model.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommendation-info.model.ts
new file mode 100644
index 000000000..0233563bb
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/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/shared/recommendations/recommendations.module.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommendations.module.ts
new file mode 100644
index 000000000..1417f3e2a
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommendations.module.ts
@@ -0,0 +1,35 @@
1
2import { CommonModule } from '@angular/common'
3import { NgModule } from '@angular/core'
4import { SharedFormModule } from '@app/shared/shared-forms'
5import { SharedMainModule } from '@app/shared/shared-main'
6import { SharedSearchModule } from '@app/shared/shared-search'
7import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature'
8import { SharedVideoPlaylistModule } from '@app/shared/shared-video-playlist'
9import { RecentVideosRecommendationService } from './recent-videos-recommendation.service'
10import { RecommendedVideosComponent } from './recommended-videos.component'
11import { RecommendedVideosStore } from './recommended-videos.store'
12
13@NgModule({
14 imports: [
15 CommonModule,
16
17 SharedMainModule,
18 SharedSearchModule,
19 SharedVideoPlaylistModule,
20 SharedVideoMiniatureModule,
21 SharedFormModule
22 ],
23 declarations: [
24 RecommendedVideosComponent
25 ],
26 exports: [
27 RecommendedVideosComponent
28 ],
29 providers: [
30 RecommendedVideosStore,
31 RecentVideosRecommendationService
32 ]
33})
34export class RecommendationsModule {
35}
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommendations.service.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommendations.service.ts
new file mode 100644
index 000000000..1d79d35f6
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/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/shared/recommendations/recommended-videos.component.html b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html
new file mode 100644
index 000000000..e1040fead
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.html
@@ -0,0 +1,26 @@
1<div class="other-videos" [ngClass]="{ 'display-as-row': displayAsRow }">
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 <my-input-switch class="small" [(ngModel)]="autoPlayNextVideo" (ngModelChange)="switchAutoPlayNextVideo()"></my-input-switch>
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" [displayAsRow]="displayAsRow"
18 (videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()"
19 actorImageSize="32"
20 >
21 </my-video-miniature>
22
23 <hr *ngIf="!playlist && i == 0 && length > 1" />
24 </ng-container>
25 </ng-container>
26</div>
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss
new file mode 100644
index 000000000..84ed25ae8
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.scss
@@ -0,0 +1,68 @@
1@use '_variables' as *;
2@use '_mixins' as *;
3
4.title-page-container {
5 display: flex;
6 justify-content: space-between;
7 align-items: baseline;
8 margin-bottom: 25px;
9 flex-wrap: wrap-reverse;
10
11 .title-page.active,
12 .title-page.title-page-single {
13 @include margin-right(.5rem !important);
14
15 margin-bottom: unset;
16 }
17}
18
19.title-page {
20 margin-top: 0;
21}
22
23.title-page-autoplay {
24 @include margin-left(auto);
25
26 display: flex;
27 width: max-content;
28 height: max-content;
29 align-items: center;
30
31 span {
32 @include margin-right(0.3rem);
33
34 text-transform: uppercase;
35 font-size: 85%;
36 font-weight: 600;
37 }
38}
39
40hr {
41 margin-top: 0;
42}
43
44my-video-miniature {
45 display: block;
46}
47
48.other-videos:not(.display-as-row) my-video-miniature {
49 min-width: $video-thumbnail-medium-width;
50 max-width: $video-thumbnail-medium-width;
51}
52
53.display-as-row {
54 my-video-miniature {
55 margin-bottom: 20px;
56 }
57
58 hr {
59 display: none;
60 }
61
62 @media screen and (max-width: $mobile-view) {
63 my-video-miniature {
64 margin-bottom: 10px;
65 }
66 }
67}
68
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts
new file mode 100644
index 000000000..89b9c01b6
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.component.ts
@@ -0,0 +1,95 @@
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 { UserLocalStorageKeys } from '@root-helpers/users'
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 @Input() displayAsRow: boolean
20
21 @Output() gotRecommendations = new EventEmitter<Video[]>()
22
23 autoPlayNextVideo: boolean
24 autoPlayNextVideoTooltip: string
25
26 displayOptions: MiniatureDisplayOptions = {
27 date: true,
28 views: true,
29 by: true,
30 avatar: true
31 }
32
33 userMiniature: User
34
35 readonly hasVideos$: Observable<boolean>
36 readonly videos$: Observable<Video[]>
37
38 constructor (
39 private userService: UserService,
40 private authService: AuthService,
41 private notifier: Notifier,
42 private store: RecommendedVideosStore,
43 private sessionStorageService: SessionStorageService
44 ) {
45 this.videos$ = this.store.recommendations$
46 this.hasVideos$ = this.store.hasRecommendations$
47 this.videos$.subscribe(videos => this.gotRecommendations.emit(videos))
48
49 if (this.authService.isLoggedIn()) {
50 this.autoPlayNextVideo = this.authService.getUser().autoPlayNextVideo
51 } else {
52 this.autoPlayNextVideo = this.sessionStorageService.getItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
53
54 this.sessionStorageService.watch([UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO]).subscribe(
55 () => {
56 this.autoPlayNextVideo = this.sessionStorageService.getItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
57 }
58 )
59 }
60
61 this.autoPlayNextVideoTooltip = $localize`When active, the next video is automatically played after the current one.`
62 }
63
64 ngOnInit () {
65 this.userService.getAnonymousOrLoggedUser()
66 .subscribe(user => this.userMiniature = user)
67 }
68
69 ngOnChanges () {
70 if (this.inputRecommendation) {
71 this.store.requestNewRecommendations(this.inputRecommendation)
72 }
73 }
74
75 onVideoRemoved () {
76 this.store.requestNewRecommendations(this.inputRecommendation)
77 }
78
79 switchAutoPlayNextVideo () {
80 this.sessionStorageService.setItem(UserLocalStorageKeys.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, this.autoPlayNextVideo.toString())
81
82 if (this.authService.isLoggedIn()) {
83 const details = {
84 autoPlayNextVideo: this.autoPlayNextVideo
85 }
86
87 this.userService.updateMyProfile(details).subscribe(
88 () => {
89 this.authService.refreshUserInformation()
90 },
91 err => this.notifier.error(err.message)
92 )
93 }
94 }
95}
diff --git a/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.store.ts b/client/src/app/+videos/+video-watch/shared/recommendations/recommended-videos.store.ts
new file mode 100644
index 000000000..8c3fb6480
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/shared/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}