Move watch later logic in miniature
authorChocobozzz <me@florianbigard.com>
Fri, 3 Jan 2020 14:01:17 +0000 (15:01 +0100)
committerChocobozzz <me@florianbigard.com>
Fri, 3 Jan 2020 14:01:17 +0000 (15:01 +0100)
client/src/app/core/auth/auth-user.model.ts
client/src/app/shared/video-playlist/video-playlist.service.ts
client/src/app/shared/video/video-miniature.component.html
client/src/app/shared/video/video-miniature.component.ts
client/src/app/shared/video/video-thumbnail.component.html
client/src/app/shared/video/video-thumbnail.component.scss
client/src/app/shared/video/video-thumbnail.component.ts
client/src/app/videos/+video-edit/video-add-components/video-upload.component.ts

index 55a5a6ddeb4ca8f9204f41328312b5fd86c73fd1..0843743c9ef7abf0d19d0503dcaecf7e308c21b6 100644 (file)
@@ -1,11 +1,10 @@
 import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage'
 import { UserRight } from '../../../../../shared/models/users/user-right.enum'
-import { User as ServerUserModel } from '../../../../../shared/models/users/user.model'
+import { MyUser as ServerMyUserModel, MyUserSpecialPlaylist } from '../../../../../shared/models/users/user.model'
 // Do not use the barrel (dependency loop)
 import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role'
 import { User } from '../../shared/users/user.model'
 import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
-import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
 
 export type TokenOptions = {
   accessToken: string
@@ -67,7 +66,7 @@ class Tokens {
   }
 }
 
-export class AuthUser extends User {
+export class AuthUser extends User implements ServerMyUserModel {
   private static KEYS = {
     ID: 'id',
     ROLE: 'role',
@@ -80,7 +79,7 @@ export class AuthUser extends User {
   }
 
   tokens: Tokens
-  specialPlaylists: Partial<VideoPlaylist>[]
+  specialPlaylists: MyUserSpecialPlaylist[]
 
   static load () {
     const usernameLocalStorage = peertubeLocalStorage.getItem(this.KEYS.USERNAME)
@@ -115,9 +114,11 @@ export class AuthUser extends User {
     Tokens.flush()
   }
 
-  constructor (userHash: Partial<ServerUserModel>, hashTokens: TokenOptions) {
+  constructor (userHash: Partial<ServerMyUserModel>, hashTokens: TokenOptions) {
     super(userHash)
+
     this.tokens = new Tokens(hashTokens)
+    this.specialPlaylists = userHash.specialPlaylists
   }
 
   getAccessToken () {
@@ -141,7 +142,7 @@ export class AuthUser extends User {
     return hasUserRight(this.role, right)
   }
 
-  canManage (user: ServerUserModel) {
+  canManage (user: ServerMyUserModel) {
     const myRole = this.role
 
     if (myRole === UserRole.ADMINISTRATOR) return true
index fc3b77b2aaa9f10e05cbb96e1ade1e6c5eca454c..d78fdc09f062e8aea90ef292cff3441adeabbe32 100644 (file)
@@ -1,4 +1,4 @@
-import { bufferTime, catchError, filter, first, map, share, switchMap } from 'rxjs/operators'
+import { bufferTime, catchError, distinctUntilChanged, filter, first, map, share, switchMap } from 'rxjs/operators'
 import { Injectable } from '@angular/core'
 import { Observable, ReplaySubject, Subject } from 'rxjs'
 import { RestExtractor } from '../rest/rest-extractor.service'
@@ -30,7 +30,6 @@ export class VideoPlaylistService {
   // Use a replay subject because we "next" a value before subscribing
   private videoExistsInPlaylistSubject: Subject<number> = new ReplaySubject(1)
   private readonly videoExistsInPlaylistObservable: Observable<VideoExistInPlaylist>
-  private cachedWatchLaterPlaylists: VideoPlaylist[]
 
   constructor (
     private authHttp: HttpClient,
@@ -39,6 +38,7 @@ export class VideoPlaylistService {
     private restService: RestService
   ) {
     this.videoExistsInPlaylistObservable = this.videoExistsInPlaylistSubject.pipe(
+      distinctUntilChanged(),
       bufferTime(500),
       filter(videoIds => videoIds.length !== 0),
       switchMap(videoIds => this.doVideosExistInPlaylist(videoIds)),
@@ -224,7 +224,7 @@ export class VideoPlaylistService {
     let params = new HttpParams()
     params = this.restService.addObjectParams(params, { videoIds })
 
-    return this.authHttp.get<VideoExistInPlaylist>(url, { params })
+    return this.authHttp.get<VideoExistInPlaylist>(url, { params, headers: { ignoreLoadingBar: '' } })
                .pipe(catchError(err => this.restExtractor.handleError(err)))
   }
 }
index c6fd570b74d0b8af7ec3a0f2233ed8159a8b68ab..036825e6131700583c8831c725841ac02dd14fa9 100644 (file)
@@ -1,5 +1,8 @@
 <div class="video-miniature" [ngClass]="{ 'display-as-row': displayAsRow }" (mouseenter)="loadActions()">
-  <my-video-thumbnail #thumbnail [video]="video" [nsfw]="isVideoBlur"></my-video-thumbnail>
+  <my-video-thumbnail
+    [video]="video" [nsfw]="isVideoBlur"
+    [displayWatchLaterPlaylist]="isWatchLaterPlaylistDisplayed()" [inWatchLaterPlaylist]="inWatchLaterPlaylist" (watchLaterClick)="onWatchLaterClick($event)"
+  ></my-video-thumbnail>
 
   <div class="video-bottom">
     <div class="video-miniature-information">
index ba65d33b694d951d00e40e7c49f1fe184d8de344..a603f87e5b59b5ff451ec606c8723954ac2774da 100644 (file)
@@ -1,12 +1,24 @@
-import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, LOCALE_ID, OnInit, Output, ViewChild } from '@angular/core'
+import {
+  ChangeDetectionStrategy,
+  ChangeDetectorRef,
+  Component,
+  EventEmitter,
+  Inject,
+  Input,
+  LOCALE_ID,
+  OnInit,
+  Output
+} from '@angular/core'
 import { User } from '../users'
 import { Video } from './video.model'
-import { ServerService } from '@app/core'
-import { ServerConfig, VideoPrivacy, VideoState } from '../../../../../shared'
+import { AuthService, ServerService } from '@app/core'
+import { ServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '../../../../../shared'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { VideoActionsDisplayType } from '@app/shared/video/video-actions-dropdown.component'
 import { ScreenService } from '@app/shared/misc/screen.service'
-import { VideoThumbnailComponent } from './video-thumbnail.component'
+import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
+import { forkJoin } from 'rxjs'
+import { first } from 'rxjs/operators'
 
 export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto'
 export type MiniatureDisplayOptions = {
@@ -47,8 +59,6 @@ export class VideoMiniatureComponent implements OnInit {
   @Output() videoUnblacklisted = new EventEmitter()
   @Output() videoRemoved = new EventEmitter()
 
-  @ViewChild('thumbnail', { static: true }) thumbnail: VideoThumbnailComponent
-
   videoActionsDisplayOptions: VideoActionsDisplayType = {
     playlist: true,
     download: false,
@@ -60,14 +70,28 @@ export class VideoMiniatureComponent implements OnInit {
   showActions = false
   serverConfig: ServerConfig
 
+  addToWatchLaterText: string
+  addedToWatchLaterText: string
+  inWatchLaterPlaylist: boolean
+
+  watchLaterPlaylist: {
+    id: number
+    playlistElementId?: number
+  }
+
   private ownerDisplayTypeChosen: 'account' | 'videoChannel'
 
   constructor (
     private screenService: ScreenService,
     private serverService: ServerService,
     private i18n: I18n,
+    private authService: AuthService,
+    private videoPlaylistService: VideoPlaylistService,
+    private cd: ChangeDetectorRef,
     @Inject(LOCALE_ID) private localeId: string
-  ) { }
+  ) {
+
+  }
 
   get isVideoBlur () {
     return this.video.isVideoNSFWForUser(this.user, this.serverConfig)
@@ -131,7 +155,8 @@ export class VideoMiniatureComponent implements OnInit {
 
   loadActions () {
     if (this.displayVideoActions) this.showActions = true
-    this.thumbnail.load()
+
+    this.loadWatchLater()
   }
 
   onVideoBlacklisted () {
@@ -146,6 +171,38 @@ export class VideoMiniatureComponent implements OnInit {
     this.videoRemoved.emit()
   }
 
+  isUserLoggedIn () {
+    return this.authService.isLoggedIn()
+  }
+
+  onWatchLaterClick (currentState: boolean) {
+    if (currentState === true) this.removeFromWatchLater()
+    else this.addToWatchLater()
+
+    this.inWatchLaterPlaylist = !currentState
+  }
+
+  addToWatchLater () {
+    const body = { videoId: this.video.id }
+
+    this.videoPlaylistService.addVideoInPlaylist(this.watchLaterPlaylist.id, body).subscribe(
+      res => {
+        this.watchLaterPlaylist.playlistElementId = res.videoPlaylistElement.id
+      }
+    )
+  }
+
+  removeFromWatchLater () {
+    this.videoPlaylistService.removeVideoFromPlaylist(this.watchLaterPlaylist.id, this.watchLaterPlaylist.playlistElementId)
+        .subscribe(
+          _ => { /* empty */ }
+        )
+  }
+
+  isWatchLaterPlaylistDisplayed () {
+    return this.inWatchLaterPlaylist !== undefined
+  }
+
   private setUpBy () {
     if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') {
       this.ownerDisplayTypeChosen = this.ownerDisplayType
@@ -163,4 +220,29 @@ export class VideoMiniatureComponent implements OnInit {
       this.ownerDisplayTypeChosen = 'videoChannel'
     }
   }
+
+  private loadWatchLater () {
+    if (!this.isUserLoggedIn()) return
+
+    forkJoin([
+      this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id),
+      this.authService.userInformationLoaded.pipe(first())
+    ]).subscribe(
+      ([ existResult ]) => {
+        const watchLaterPlaylist = this.authService.getUser().specialPlaylists.find(p => p.type === VideoPlaylistType.WATCH_LATER)
+        const existsInWatchLater = existResult[ this.video.id ].find(r => r.playlistId === watchLaterPlaylist.id)
+        this.inWatchLaterPlaylist = false
+
+        this.watchLaterPlaylist = {
+          id: watchLaterPlaylist.id
+        }
+
+        if (existsInWatchLater) {
+          this.inWatchLaterPlaylist = true
+          this.watchLaterPlaylist.playlistElementId = existsInWatchLater.playlistElementId
+        }
+
+        this.cd.markForCheck()
+      })
+  }
 }
index 9679dfefb9e5d0921a7efd5774ee0e41d271452a..c30a435570116c81ed51cb78e92c9da9cef21ebc 100644 (file)
@@ -1,18 +1,18 @@
 <a
   [routerLink]="getVideoRouterLink()" [queryParams]="queryParams" [attr.title]="video.name"
   class="video-thumbnail"
-  (mouseenter)="load()" (focus)="load()"
 >
-  <img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
+  <img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" loading="lazy" />
 
-  <div *ngIf="isUserLoggedIn()" class="video-thumbnail-actions-overlay">
-    <ng-container *ngIf="addedToWatchLater !== true">
-      <div class="video-thumbnail-watch-later-overlay" placement="left" [ngbTooltip]="addToWatchLaterText" container="body" (click)="addToWatchLater();$event.stopPropagation();false">
+  <div *ngIf="displayWatchLaterPlaylist" class="video-thumbnail-actions-overlay">
+    <ng-container *ngIf="inWatchLaterPlaylist !== true">
+      <div class="video-thumbnail-watch-later-overlay" placement="left" [ngbTooltip]="addToWatchLaterText" container="body" (click)="onWatchLaterClick($event)">
         <my-global-icon iconName="clock" [attr.aria-label]="addToWatchLaterText" role="button"></my-global-icon>
       </div>
     </ng-container>
-    <ng-container *ngIf="addedToWatchLater === true">
-      <div class="video-thumbnail-watch-later-overlay" placement="left" [ngbTooltip]="addedToWatchLaterText" container="body" (click)="removeFromWatchLater();$event.stopPropagation();false">
+
+    <ng-container *ngIf="inWatchLaterPlaylist === true">
+      <div class="video-thumbnail-watch-later-overlay" placement="left" [ngbTooltip]="addedToWatchLaterText" container="body" (click)="onWatchLaterClick($event)">
         <my-global-icon iconName="tick" [attr.aria-label]="addedToWatchLaterText" role="button"></my-global-icon>
       </div>
     </ng-container>
index ad221d6ed64a5c4ecf7641b7747578c0348b4af9..573a64987002c04af5f41e30ba408b369777c503 100644 (file)
     bottom: 5px;
   }
 
-  &:focus,
-  &:hover {
-    .video-thumbnail-actions-overlay {
-      opacity: 1;
-    }
-  }
-
   .video-thumbnail-actions-overlay {
     position: absolute;
     display: flex;
index 6f9292d52fd00355dc4783d6580415e58e9bc372..2420ec715a5390947f3a21c4ffd3984364179b78 100644 (file)
@@ -1,9 +1,7 @@
-import { Component, Input, OnInit, ChangeDetectorRef } from '@angular/core'
+import { Component, EventEmitter, Input, Output } from '@angular/core'
 import { Video } from './video.model'
 import { ScreenService } from '@app/shared/misc/screen.service'
-import { AuthService, ThemeService } from '@app/core'
-import { VideoPlaylistService } from '../video-playlist/video-playlist.service'
-import { VideoPlaylistElementCreate } from '../../../../../shared'
+import { I18n } from '@ngx-translate/i18n-polyfill'
 
 @Component({
   selector: 'my-video-thumbnail',
@@ -16,45 +14,20 @@ export class VideoThumbnailComponent {
   @Input() routerLink: any[]
   @Input() queryParams: any[]
 
-  addToWatchLaterText = 'Add to watch later'
-  addedToWatchLaterText = 'Added to watch later'
-  addedToWatchLater: boolean
+  @Input() displayWatchLaterPlaylist: boolean
+  @Input() inWatchLaterPlaylist: boolean
 
-  watchLaterPlaylist: any
+  @Output() watchLaterClick = new EventEmitter<boolean>()
+
+  addToWatchLaterText: string
+  addedToWatchLaterText: string
 
   constructor (
     private screenService: ScreenService,
-    private authService: AuthService,
-    private videoPlaylistService: VideoPlaylistService,
-    private cd: ChangeDetectorRef
-  ) {}
-
-  load () {
-    if (this.addedToWatchLater !== undefined) return
-    if (!this.isUserLoggedIn()) return
-
-    this.videoPlaylistService.doesVideoExistInPlaylist(this.video.id)
-      .subscribe(
-        existResult => {
-          for (const playlist of this.authService.getUser().specialPlaylists) {
-            const existingPlaylist = existResult[ this.video.id ].find(p => p.playlistId === playlist.id)
-            this.addedToWatchLater = !!existingPlaylist
-
-            if (existingPlaylist) {
-              this.watchLaterPlaylist = {
-                playlistId: existingPlaylist.playlistId,
-                playlistElementId: existingPlaylist.playlistElementId
-              }
-            } else {
-              this.watchLaterPlaylist = {
-                playlistId: playlist.id
-              }
-            }
-
-            this.cd.markForCheck()
-          }
-        }
-      )
+    private i18n: I18n
+  ) {
+    this.addToWatchLaterText = this.i18n('Add to watch later')
+    this.addedToWatchLaterText = this.i18n('Remove from watch later')
   }
 
   getImageUrl () {
@@ -81,36 +54,10 @@ export class VideoThumbnailComponent {
     return [ '/videos/watch', this.video.uuid ]
   }
 
-  isUserLoggedIn () {
-    return this.authService.isLoggedIn()
-  }
-
-  addToWatchLater () {
-    if (this.addedToWatchLater === undefined) return
-    this.addedToWatchLater = true
-
-    this.videoPlaylistService.addVideoInPlaylist(
-      this.watchLaterPlaylist.playlistId,
-      { videoId: this.video.id } as VideoPlaylistElementCreate
-    ).subscribe(
-      res => {
-        this.addedToWatchLater = true
-        this.watchLaterPlaylist.playlistElementId = res.videoPlaylistElement.id
-      }
-    )
-  }
-
-  removeFromWatchLater () {
-    if (this.addedToWatchLater === undefined) return
-    this.addedToWatchLater = false
+  onWatchLaterClick (event: Event) {
+    this.watchLaterClick.emit(this.inWatchLaterPlaylist)
 
-    this.videoPlaylistService.removeVideoFromPlaylist(
-      this.watchLaterPlaylist.playlistId,
-      this.watchLaterPlaylist.playlistElementId
-    ).subscribe(
-      _ => {
-        this.addedToWatchLater = false
-      }
-    )
+    event.stopPropagation()
+    return false
   }
 }
index 28e10e5628c271d7567696b76c87dffcab94bd69..aa3a8599542db20857761df7b36a4fdc486021f0 100644 (file)
@@ -14,7 +14,6 @@ import { CanComponentDeactivate } from '@app/shared/guards/can-deactivate-guard.
 import { FormValidatorService, UserService } from '@app/shared'
 import { VideoCaptionService } from '@app/shared/video-caption'
 import { scrollToTop } from '@app/shared/misc/utils'
-import { ServerConfig } from '@shared/models'
 
 @Component({
   selector: 'my-video-upload',