Handle overview pagination in client
authorChocobozzz <me@florianbigard.com>
Wed, 11 Mar 2020 15:41:38 +0000 (16:41 +0100)
committerChocobozzz <me@florianbigard.com>
Wed, 11 Mar 2020 15:45:09 +0000 (16:45 +0100)
client/src/app/core/server/server.service.ts
client/src/app/menu/language-chooser.component.ts
client/src/app/menu/menu.component.ts
client/src/app/shared/overview/overview.service.ts
client/src/app/videos/video-list/video-overview.component.html
client/src/app/videos/video-list/video-overview.component.ts

index e015d0e1430d9e7f9727dd5c70f595804602e1f9..da7832b3261e8cf5765e66f3d81994f24697e723 100644 (file)
@@ -263,17 +263,19 @@ export class ServerService {
                               .pipe(map(data => ({ data, translations })))
                  }),
                  map(({ data, translations }) => {
-                   const hashToPopulate: VideoConstant<T>[] = []
-
-                   Object.keys(data)
-                         .forEach(dataKey => {
-                           const label = data[ dataKey ]
-
-                           hashToPopulate.push({
-                             id: (attributeName === 'languages' ? dataKey : parseInt(dataKey, 10)) as T,
-                             label: peertubeTranslate(label, translations)
-                           })
-                         })
+                   const hashToPopulate: VideoConstant<T>[] = Object.keys(data)
+                                                                    .map(dataKey => {
+                                                                      const label = data[ dataKey ]
+
+                                                                      const id = attributeName === 'languages'
+                                                                        ? dataKey as T
+                                                                        : parseInt(dataKey, 10) as T
+
+                                                                      return {
+                                                                        id,
+                                                                        label: peertubeTranslate(label, translations)
+                                                                      }
+                                                                    })
 
                    if (sort === true) sortBy(hashToPopulate, 'label')
 
index fb74cdf19f2ff75219da0ea3ed55702440bc90a2..9bc934ad400f440f35dcf9b5ddbd9e5f802abe10 100644 (file)
@@ -36,6 +36,7 @@ export class LanguageChooserComponent {
   getCurrentLanguage () {
     const english = 'English'
     const locale = isOnDevLocale() ? getDevLocale() : getCompleteLocale(this.localeId)
+
     if (locale) return I18N_LOCALES[locale] || english
     return english
   }
index ce209457ca8d06e609da3e0fe622171ac93d52bb..37702e975d0c5a94a9ba86bca34a6152abbf24b3 100644 (file)
@@ -23,8 +23,10 @@ export class MenuComponent implements OnInit {
 
   userHasAdminAccess = false
   helpVisible = false
-  languages: VideoConstant<string>[] = []
 
+  videoLanguages: string[] = []
+
+  private languages: VideoConstant<string>[] = []
   private serverConfig: ServerConfig
   private routesPerRight: { [ role in UserRight ]?: string } = {
     [UserRight.MANAGE_USERS]: '/admin/users',
@@ -71,30 +73,32 @@ export class MenuComponent implements OnInit {
       }
     )
 
-    this.hotkeysService.cheatSheetToggle.subscribe(isOpen => this.helpVisible = isOpen)
+    this.hotkeysService.cheatSheetToggle
+        .subscribe(isOpen => this.helpVisible = isOpen)
+
+    this.serverService.getVideoLanguages()
+        .subscribe(languages => {
+          this.languages = languages
 
-    this.serverService.getVideoLanguages().subscribe(languages => this.languages = languages)
+          this.authService.userInformationLoaded
+              .subscribe(() => this.buildUserLanguages())
+        })
   }
 
   get language () {
     return this.languageChooserModal.getCurrentLanguage()
   }
 
-  get videoLanguages (): string[] {
-    if (!this.user) return
-    if (!this.user.videoLanguages) return [this.i18n('any language')]
-    return this.user.videoLanguages
-      .map(locale => this.langForLocale(locale))
-      .map(value => value === undefined ? '?' : value)
-  }
-
   get nsfwPolicy () {
     if (!this.user) return
+
     switch (this.user.nsfwPolicy) {
       case 'do_not_list':
         return this.i18n('hide')
+
       case 'blur':
         return this.i18n('blur')
+
       case 'display':
         return this.i18n('display')
     }
@@ -156,13 +160,29 @@ export class MenuComponent implements OnInit {
   toggleUseP2P () {
     if (!this.user) return
     this.user.webTorrentEnabled = !this.user.webTorrentEnabled
-    this.userService.updateMyProfile({
-      webTorrentEnabled: this.user.webTorrentEnabled
-    }).subscribe(() => this.authService.refreshUserInformation())
+
+    this.userService.updateMyProfile({ webTorrentEnabled: this.user.webTorrentEnabled })
+        .subscribe(() => this.authService.refreshUserInformation())
   }
 
   langForLocale (localeId: string) {
-    return this.languages.find(lang => lang.id = localeId).label
+    return this.languages.find(lang => lang.id === localeId).label
+  }
+
+  private buildUserLanguages () {
+    if (!this.user) {
+      this.videoLanguages = []
+      return
+    }
+
+    if (!this.user.videoLanguages) {
+      this.videoLanguages = [ this.i18n('any language') ]
+      return
+    }
+
+    this.videoLanguages = this.user.videoLanguages
+                              .map(locale => this.langForLocale(locale))
+                              .map(value => value === undefined ? '?' : value)
   }
 
   private computeIsUserHasAdminAccess () {
index 79cb781f7c30d2f07b50134d9e3346d96d19b8f9..6d8af8052003daff68eaba12713d390d886df13d 100644 (file)
@@ -1,5 +1,5 @@
 import { catchError, map, switchMap, tap } from 'rxjs/operators'
-import { HttpClient } from '@angular/common/http'
+import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { forkJoin, Observable, of } from 'rxjs'
 import { VideosOverview as VideosOverviewServer, peertubeTranslate } from '../../../../../shared/models'
@@ -21,9 +21,12 @@ export class OverviewService {
     private serverService: ServerService
   ) {}
 
-  getVideosOverview (): Observable<VideosOverview> {
+  getVideosOverview (page: number): Observable<VideosOverview> {
+    let params = new HttpParams()
+    params = params.append('page', page + '')
+
     return this.authHttp
-               .get<VideosOverviewServer>(OverviewService.BASE_OVERVIEW_URL + 'videos')
+               .get<VideosOverviewServer>(OverviewService.BASE_OVERVIEW_URL + 'videos', { params })
                .pipe(
                  switchMap(serverVideosOverview => this.updateVideosOverview(serverVideosOverview)),
                  catchError(err => this.restExtractor.handleError(err))
index 5fe1f5c80a7ba4999e44d5af14c7ac5b0b41db77..84999cfb2723df6e6bf9cf981c86d76cdb66d1b8 100644 (file)
@@ -2,35 +2,44 @@
 
   <div class="no-results" i18n *ngIf="notResults">No results.</div>
 
-  <div class="section" *ngFor="let object of overview.categories">
-    <div class="section-title">
-      <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a>
-    </div>
+  <div
+    myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" [dataObservable]="onDataSubject.asObservable()"
+  >
+    <ng-container *ngFor="let overview of overviews">
 
-    <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false">
-    </my-video-miniature>
-  </div>
+      <div class="section" *ngFor="let object of overview.categories">
+        <div class="section-title">
+          <a routerLink="/search" [queryParams]="{ categoryOneOf: [ object.category.id ] }">{{ object.category.label }}</a>
+        </div>
 
-  <div class="section" *ngFor="let object of overview.tags">
-    <div class="section-title">
-      <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a>
-    </div>
+        <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false">
+        </my-video-miniature>
+      </div>
 
-    <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false">
-    </my-video-miniature>
-  </div>
+      <div class="section" *ngFor="let object of overview.tags">
+        <div class="section-title">
+          <a routerLink="/search" [queryParams]="{ tagsOneOf: [ object.tag ] }">#{{ object.tag }}</a>
+        </div>
+
+        <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false">
+        </my-video-miniature>
+      </div>
+
+      <div class="section channel" *ngFor="let object of overview.channels">
+        <div class="section-title">
+          <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]">
+            <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" />
+
+            <div>{{ object.channel.displayName }}</div>
+          </a>
+        </div>
 
-  <div class="section channel" *ngFor="let object of overview.channels">
-    <div class="section-title">
-      <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]">
-        <img [src]="buildVideoChannelAvatarUrl(object)" alt="Avatar" />
+        <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false">
+        </my-video-miniature>
+      </div>
 
-        <div>{{ object.channel.displayName }}</div>
-      </a>
-    </div>
+    </ng-container>
 
-    <my-video-miniature *ngFor="let video of buildVideos(object.videos)" [video]="video" [user]="user" [displayVideoActions]="false">
-    </my-video-miniature>
   </div>
 
 </div>
index 4fee92d54a1e9f7997ca8ac55b42fac2e5a609c4..101073949c24a66b409919d65bfb8007b7b36a65 100644 (file)
@@ -5,6 +5,7 @@ import { VideosOverview } from '@app/shared/overview/videos-overview.model'
 import { OverviewService } from '@app/shared/overview'
 import { Video } from '@app/shared/video/video.model'
 import { ScreenService } from '@app/shared/misc/screen.service'
+import { Subject } from 'rxjs'
 
 @Component({
   selector: 'my-video-overview',
@@ -12,13 +13,17 @@ import { ScreenService } from '@app/shared/misc/screen.service'
   styleUrls: [ './video-overview.component.scss' ]
 })
 export class VideoOverviewComponent implements OnInit {
-  overview: VideosOverview = {
-    categories: [],
-    channels: [],
-    tags: []
-  }
+  onDataSubject = new Subject<any>()
+
+  overviews: VideosOverview[] = []
   notResults = false
 
+  private loaded = false
+  private currentPage = 1
+  private maxPage = 20
+  private lastWasEmpty = false
+  private isLoading = false
+
   constructor (
     private i18n: I18n,
     private notifier: Notifier,
@@ -32,20 +37,7 @@ export class VideoOverviewComponent implements OnInit {
   }
 
   ngOnInit () {
-    this.overviewService.getVideosOverview()
-        .subscribe(
-          overview => {
-            this.overview = overview
-
-            if (
-              this.overview.categories.length === 0 &&
-              this.overview.channels.length === 0 &&
-              this.overview.tags.length === 0
-            ) this.notResults = true
-          },
-
-          err => this.notifier.error(err.message)
-        )
+    this.loadMoreResults()
   }
 
   buildVideoChannelBy (object: { videos: Video[] }) {
@@ -61,4 +53,41 @@ export class VideoOverviewComponent implements OnInit {
 
     return videos.slice(0, numberOfVideos * 2)
   }
+
+  onNearOfBottom () {
+    if (this.currentPage >= this.maxPage) return
+    if (this.lastWasEmpty) return
+    if (this.isLoading) return
+
+    this.currentPage++
+    this.loadMoreResults()
+  }
+
+  private loadMoreResults () {
+    this.isLoading = true
+
+    this.overviewService.getVideosOverview(this.currentPage)
+        .subscribe(
+          overview => {
+            this.isLoading = false
+
+            if (overview.tags.length === 0 && overview.channels.length === 0 && overview.categories.length === 0) {
+              this.lastWasEmpty = true
+              if (this.loaded === false) this.notResults = true
+
+              return
+            }
+
+            this.loaded = true
+            this.onDataSubject.next(overview)
+
+            this.overviews.push(overview)
+          },
+
+          err => {
+            this.notifier.error(err.message)
+            this.isLoading = false
+          }
+        )
+  }
 }