Restore videos list components
authorChocobozzz <me@florianbigard.com>
Thu, 21 Mar 2019 15:49:46 +0000 (16:49 +0100)
committerChocobozzz <me@florianbigard.com>
Tue, 2 Apr 2019 09:45:02 +0000 (11:45 +0200)
25 files changed:
client/src/app/+accounts/account-videos/account-videos.component.ts
client/src/app/+accounts/accounts-routing.module.ts
client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.html
client/src/app/+admin/moderation/video-auto-blacklist-list/video-auto-blacklist-list.component.ts
client/src/app/+my-account/my-account-history/my-account-history.component.html
client/src/app/+my-account/my-account-history/my-account-history.component.ts
client/src/app/+my-account/my-account-routing.module.ts
client/src/app/+my-account/my-account-videos/my-account-videos.component.html
client/src/app/+my-account/my-account-videos/my-account-videos.component.ts
client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
client/src/app/+video-channels/video-channels-routing.module.ts
client/src/app/app-routing.module.ts
client/src/app/app.component.ts
client/src/app/core/routing/custom-reuse-strategy.ts [new file with mode: 0644]
client/src/app/core/routing/disable-for-reuse-hook.ts [new file with mode: 0644]
client/src/app/shared/video/abstract-video-list.html
client/src/app/shared/video/abstract-video-list.ts
client/src/app/shared/video/infinite-scroller.directive.ts
client/src/app/videos/video-list/video-local.component.ts
client/src/app/videos/video-list/video-recently-added.component.ts
client/src/app/videos/video-list/video-trending.component.ts
client/src/app/videos/video-list/video-user-subscriptions.component.ts
client/src/app/videos/videos-routing.module.ts
server/models/video/video-comment.ts
server/models/video/video-share.ts

index 13b634a0137f52190fac56ed9de54f8107a6bceb..7535eef087e4d471b89e9909dc77d8563725d743 100644 (file)
@@ -1,6 +1,5 @@
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
 import { immutableAssign } from '@app/shared/misc/utils'
 import { AuthService } from '../../core/auth'
 import { ConfirmService } from '../../core/confirm'
@@ -12,7 +11,7 @@ import { tap } from 'rxjs/operators'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { Subscription } from 'rxjs'
 import { ScreenService } from '@app/shared/misc/screen.service'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
 
 @Component({
   selector: 'my-account-videos',
@@ -25,7 +24,6 @@ import { Notifier } from '@app/core'
 export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
   titlePage: string
   marginContent = false // Disable margin
-  currentRoute = '/accounts/videos'
   loadOnInit = false
 
   private account: Account
@@ -33,13 +31,13 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
 
   constructor (
     protected router: Router,
+    protected serverService: ServerService,
     protected route: ActivatedRoute,
     protected authService: AuthService,
     protected notifier: Notifier,
     protected confirmService: ConfirmService,
-    protected location: Location,
     protected screenService: ScreenService,
-    protected i18n: I18n,
+    private i18n: I18n,
     private accountService: AccountService,
     private videoService: VideoService
   ) {
@@ -55,7 +53,6 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
     this.accountSub = this.accountService.accountLoaded
       .subscribe(account => {
         this.account = account
-        this.currentRoute = '/accounts/' + this.account.nameWithHost + '/videos'
 
         this.reloadVideos()
         this.generateSyndicationList()
index ffe606b4372aa5b7d7c56a80e4f0c71c00c92b31..531d763c4ba5ac1ebdafc4b6758b1367b482df1a 100644 (file)
@@ -23,6 +23,10 @@ const accountsRoutes: Routes = [
         data: {
           meta: {
             title: 'Account videos'
+          },
+          reuse: {
+            enabled: true,
+            key: 'account-videos-list'
           }
         }
       },
index fe579ffd7c540270fdd34e08e00693fb6d746ce9..961ac51d3f5d2c565b8337b9292efb3d87e53827 100644 (file)
@@ -1,49 +1,42 @@
 <div i18n *ngIf="pagination.totalItems === 0">No results.</div>
-<div
-  myInfiniteScroller
-  [pageHeight]="pageHeight"
-  (nearOfTop)="onNearOfTop()"
-  (nearOfBottom)="onNearOfBottom()"
-  (pageChanged)="onPageChanged($event)"
-  class="videos" #videosElement
->
-  <div *ngFor="let videos of videoPages; let i = index" class="videos-page">
-    <div class="video" *ngFor="let video of videos; let j = index">
-      <div class="checkbox-container">
-        <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
-      </div>
-      <my-video-thumbnail [video]="video"></my-video-thumbnail>
 
-      <div class="video-info">
-        <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
-        <div>{{ video.account.displayName }}</div>
-        <div>{{ video.publishedAt | myFromNow }}</div>
-        <div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div>
-        <div><span i18n>Sensitve: </span><span> {{ video.nsfw }}</span></div>
-      </div>
+<div myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" class="videos">
+  <div class="video" *ngFor="let video of videos; let i = index">
+    <div class="checkbox-container">
+      <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
+    </div>
 
-      <!-- Display only once -->
-      <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
-        <div class="action-selection-mode-child">
-          <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
-            Cancel
-          </span>
+    <my-video-thumbnail [video]="video"></my-video-thumbnail>
 
-          <span class="action-button action-button-unblacklist-selection" (click)="removeSelectedVideosFromBlacklist()">
-            <my-global-icon iconName="tick"></my-global-icon>
-            <ng-container i18n>Unblacklist</ng-container>
-          </span>
-        </div>
-      </div>
+    <div class="video-info">
+      <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
+      <div>{{ video.account.displayName }}</div>
+      <div>{{ video.publishedAt | myFromNow }}</div>
+      <div><span i18n>Privacy: </span><span>{{ video.privacy.label }}</span></div>
+      <div><span i18n>Sensitive: </span><span> {{ video.nsfw }}</span></div>
+    </div>
+
+    <!-- Display only once -->
+    <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
+      <div class="action-selection-mode-child">
+        <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
+          Cancel
+        </span>
 
-      <div class="video-buttons" *ngIf="isInSelectionMode() === false">
-        <my-button
-          i18n-label
-          label="Unblacklist"
-          icon="tick"
-          (click)="removeVideoFromBlacklist(video)"
-        ></my-button>
+        <span class="action-button action-button-unblacklist-selection" (click)="removeSelectedVideosFromBlacklist()">
+          <my-global-icon iconName="tick"></my-global-icon>
+          <ng-container i18n>Unblacklist</ng-container>
+        </span>
       </div>
     </div>
 
-</div>
\ No newline at end of file
+    <div class="video-buttons" *ngIf="isInSelectionMode() === false">
+      <my-button
+        i18n-label
+        label="Unblacklist"
+        icon="tick"
+        (click)="removeVideoFromBlacklist(video)"
+      ></my-button>
+    </div>
+  </div>
+</div>
index b79f574c9428612000eb52075dbe3c8fa9276521..af68d7e2ed2e52a98ac5ea58e7bde1638c46118a 100644 (file)
@@ -4,7 +4,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { Router, ActivatedRoute } from '@angular/router'
 import { AbstractVideoList } from '@app/shared/video/abstract-video-list'
 import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
-import { Notifier, AuthService } from '@app/core'
+import { Notifier, AuthService, ServerService } from '@app/core'
 import { Video } from '@shared/models'
 import { VideoBlacklistService } from '@app/shared'
 import { immutableAssign } from '@app/shared/misc/utils'
@@ -17,7 +17,6 @@ import { ScreenService } from '@app/shared/misc/screen.service'
 })
 export class VideoAutoBlacklistListComponent extends AbstractVideoList implements OnInit, OnDestroy {
   titlePage: string
-  currentRoute = '/admin/moderation/video-auto-blacklist/list'
   checkedVideos: { [ id: number ]: boolean } = {}
   pagination: ComponentPagination = {
     currentPage: 1,
@@ -25,18 +24,15 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement
     totalItems: null
   }
 
-  protected baseVideoWidth = -1
-  protected baseVideoHeight = 155
-
   constructor (
     protected router: Router,
     protected route: ActivatedRoute,
-    protected i18n: I18n,
     protected notifier: Notifier,
-    protected location: Location,
     protected authService: AuthService,
     protected screenService: ScreenService,
-    private videoBlacklistService: VideoBlacklistService,
+    protected serverService: ServerService,
+    private i18n: I18n,
+    private videoBlacklistService: VideoBlacklistService
   ) {
     super()
 
@@ -96,5 +92,4 @@ export class VideoAutoBlacklistListComponent extends AbstractVideoList implement
       error => this.notifier.error(error.message)
     )
   }
-
 }
index 2349f02f50c33388fdd86dfea22f961212a4c9c3..00ee5fbd1f2ebdb97d29198e90bc3028dcaed208 100644 (file)
 
 <div class="no-history" i18n *ngIf="pagination.totalItems === 0">You don't have videos history yet.</div>
 
-<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" class="videos" #videosElement>
-  <div *ngFor="let videos of videoPages;" class="videos-page">
-    <div class="video" *ngFor="let video of videos">
-      <my-video-thumbnail [video]="video"></my-video-thumbnail>
+<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos">
+  <div class="video" *ngFor="let video of videos">
+    <my-video-thumbnail [video]="video"></my-video-thumbnail>
 
-      <div class="video-info">
-        <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
-        <span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span>
-        <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
-      </div>
+    <div class="video-info">
+      <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
+      <span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span>
+      <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
     </div>
   </div>
 </div>
index 394091bad3cecd92ea7067989ddf638d7a50b223..73340d21aee1133b5a72804fc71f7203c46e7a09 100644 (file)
@@ -1,6 +1,5 @@
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
 import { immutableAssign } from '@app/shared/misc/utils'
 import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
 import { AuthService } from '../../core/auth'
@@ -11,7 +10,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { UserHistoryService } from '@app/shared/users/user-history.service'
 import { UserService } from '@app/shared'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
 
 @Component({
   selector: 'my-account-history',
@@ -20,7 +19,6 @@ import { Notifier } from '@app/core'
 })
 export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy {
   titlePage: string
-  currentRoute = '/my-account/history/videos'
   pagination: ComponentPagination = {
     currentPage: 1,
     itemsPerPage: 5,
@@ -28,16 +26,13 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn
   }
   videosHistoryEnabled: boolean
 
-  protected baseVideoWidth = -1
-  protected baseVideoHeight = 155
-
   constructor (
     protected router: Router,
+    protected serverService: ServerService,
     protected route: ActivatedRoute,
     protected authService: AuthService,
     protected userService: UserService,
     protected notifier: Notifier,
-    protected location: Location,
     protected screenService: ScreenService,
     protected i18n: I18n,
     private confirmService: ConfirmService,
index 07557a029b2bf03407d019bdf25cb97106fb560f..018d6f99627544e93a3d5ece0dbe7804d44d4ccc 100644 (file)
@@ -118,6 +118,10 @@ const myAccountRoutes: Routes = [
         data: {
           meta: {
             title: 'Account videos'
+          },
+          reuse: {
+            enabled: true,
+            key: 'my-account-videos-list'
           }
         }
       },
@@ -172,6 +176,10 @@ const myAccountRoutes: Routes = [
         data: {
           meta: {
             title: 'Videos history'
+          },
+          reuse: {
+            enabled: true,
+            key: 'my-videos-history-list'
           }
         }
       },
index b09e845ace2876b3674894724654906a40042ec6..1f3ac0005edaa0f9b9e69f82b5202f73b21661e6 100644 (file)
@@ -1,54 +1,47 @@
 <div i18n *ngIf="pagination.totalItems === 0">No results.</div>
 
-<div
-  myInfiniteScroller
-  [pageHeight]="pageHeight"
-  (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
-  class="videos" #videosElement
->
-  <div *ngFor="let videos of videoPages; let i = index" class="videos-page">
-    <div class="video" *ngFor="let video of videos; let j = index">
-      <div class="checkbox-container">
-        <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
-      </div>
+<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true" class="videos">
+  <div class="video" *ngFor="let video of videos; let i = index">
+    <div class="checkbox-container">
+      <my-peertube-checkbox [inputName]="'video-check-' + video.id" [(ngModel)]="checkedVideos[video.id]"></my-peertube-checkbox>
+    </div>
 
-      <my-video-thumbnail [video]="video"></my-video-thumbnail>
+    <my-video-thumbnail [video]="video"></my-video-thumbnail>
 
-      <div class="video-info">
-        <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
-        <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
-        <div class="video-info-privacy">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
-        <div *ngIf="video.blacklisted" class="video-info-blacklisted">
-          <span class="blacklisted-label" i18n>Blacklisted</span>
-          <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
-        </div>
+    <div class="video-info">
+      <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
+      <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
+      <div class="video-info-privacy">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
+      <div *ngIf="video.blacklisted" class="video-info-blacklisted">
+        <span class="blacklisted-label" i18n>Blacklisted</span>
+        <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
       </div>
+    </div>
 
-      <!-- Display only once -->
-      <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
-        <div class="action-selection-mode-child">
-          <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
-            Cancel
-          </span>
-
-          <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
-            <my-global-icon iconName="delete"></my-global-icon>
-            <ng-container i18n>Delete</ng-container>
-          </span>
-        </div>
+    <!-- Display only once -->
+    <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0">
+      <div class="action-selection-mode-child">
+        <span i18n class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
+          Cancel
+        </span>
+
+        <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
+          <my-global-icon iconName="delete"></my-global-icon>
+          <ng-container i18n>Delete</ng-container>
+        </span>
       </div>
+    </div>
 
-      <div class="video-buttons" *ngIf="isInSelectionMode() === false">
-        <my-delete-button (click)="deleteVideo(video)"></my-delete-button>
+    <div class="video-buttons" *ngIf="isInSelectionMode() === false">
+      <my-delete-button (click)="deleteVideo(video)"></my-delete-button>
 
-        <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button>
+      <my-edit-button [routerLink]="[ '/videos', 'update', video.uuid ]"></my-edit-button>
 
-        <my-button i18n-label label="Change ownership"
-                   className="action-button-change-ownership"
-                   icon="im-with-her"
-                   (click)="changeOwnership($event, video)"
-        ></my-button>
-      </div>
+      <my-button i18n-label label="Change ownership"
+                 className="action-button-change-ownership"
+                 icon="im-with-her"
+                 (click)="changeOwnership($event, video)"
+      ></my-button>
     </div>
   </div>
 </div>
index 41608f796f69562c1aea5f9ce760f64a688890a8..eb5096a5e9319b7c77b86087977d167e132749ef 100644 (file)
@@ -1,11 +1,10 @@
-import { from as observableFrom, Observable } from 'rxjs'
-import { concatAll, tap } from 'rxjs/operators'
-import { Component, OnDestroy, OnInit, Inject, LOCALE_ID, ViewChild } from '@angular/core'
+import { concat, Observable } from 'rxjs'
+import { tap, toArray } from 'rxjs/operators'
+import { Component, Inject, LOCALE_ID, OnDestroy, OnInit, ViewChild } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
 import { immutableAssign } from '@app/shared/misc/utils'
 import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
 import { AuthService } from '../../core/auth'
 import { ConfirmService } from '../../core/confirm'
 import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -22,8 +21,9 @@ import { VideoChangeOwnershipComponent } from './video-change-ownership/video-ch
   styleUrls: [ './my-account-videos.component.scss' ]
 })
 export class MyAccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
+  @ViewChild('videoChangeOwnershipModal') videoChangeOwnershipModal: VideoChangeOwnershipComponent
+
   titlePage: string
-  currentRoute = '/my-account/videos'
   checkedVideos: { [ id: number ]: boolean } = {}
   pagination: ComponentPagination = {
     currentPage: 1,
@@ -31,19 +31,14 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
     totalItems: null
   }
 
-  protected baseVideoWidth = -1
-  protected baseVideoHeight = 155
-
-  @ViewChild('videoChangeOwnershipModal') videoChangeOwnershipModal: VideoChangeOwnershipComponent
-
   constructor (
     protected router: Router,
+    protected serverService: ServerService,
     protected route: ActivatedRoute,
     protected authService: AuthService,
     protected notifier: Notifier,
-    protected location: Location,
     protected screenService: ScreenService,
-    protected i18n: I18n,
+    private i18n: I18n,
     private confirmService: ConfirmService,
     private videoService: VideoService,
     @Inject(LOCALE_ID) private localeId: string
@@ -93,19 +88,18 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
     const observables: Observable<any>[] = []
     for (const videoId of toDeleteVideosIds) {
       const o = this.videoService.removeVideo(videoId)
-                    .pipe(tap(() => this.spliceVideosById(videoId)))
+                    .pipe(tap(() => this.removeVideoFromArray(videoId)))
 
       observables.push(o)
     }
 
-    observableFrom(observables)
-      .pipe(concatAll())
+    concat(...observables)
+      .pipe(toArray())
       .subscribe(
-        res => {
+        () => {
           this.notifier.success(this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length }))
 
           this.abortSelectionMode()
-          this.reloadVideos()
         },
 
         err => this.notifier.error(err.message)
@@ -156,20 +150,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni
     return ' - ' + suffix
   }
 
-  protected buildVideoHeight () {
-    // In account videos, the video height is fixed
-    return this.baseVideoHeight
-  }
-
-  private spliceVideosById (id: number) {
-    for (const key of Object.keys(this.loadedPages)) {
-      const videos: Video[] = this.loadedPages[ key ]
-      const index = videos.findIndex(v => v.id === id)
-
-      if (index !== -1) {
-        videos.splice(index, 1)
-        return
-      }
-    }
+  private removeVideoFromArray (id: number) {
+    this.videos = this.videos.filter(v => v.id !== id)
   }
 }
index dea378a6e6a638c4e05e86e50318fc46f2d81a8b..8af31000e3e3a5a85c57d33a7bd7d2f8752de561 100644 (file)
@@ -1,6 +1,5 @@
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
 import { immutableAssign } from '@app/shared/misc/utils'
 import { AuthService } from '../../core/auth'
 import { ConfirmService } from '../../core/confirm'
@@ -12,7 +11,7 @@ import { tap } from 'rxjs/operators'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { Subscription } from 'rxjs'
 import { ScreenService } from '@app/shared/misc/screen.service'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
 
 @Component({
   selector: 'my-video-channel-videos',
@@ -25,7 +24,6 @@ import { Notifier } from '@app/core'
 export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
   titlePage: string
   marginContent = false // Disable margin
-  currentRoute = '/video-channels/videos'
   loadOnInit = false
 
   private videoChannel: VideoChannel
@@ -33,13 +31,13 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
 
   constructor (
     protected router: Router,
+    protected serverService: ServerService,
     protected route: ActivatedRoute,
     protected authService: AuthService,
     protected notifier: Notifier,
     protected confirmService: ConfirmService,
-    protected location: Location,
     protected screenService: ScreenService,
-    protected i18n: I18n,
+    private i18n: I18n,
     private videoChannelService: VideoChannelService,
     private videoService: VideoService
   ) {
@@ -55,7 +53,6 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
     this.videoChannelSub = this.videoChannelService.videoChannelLoaded
       .subscribe(videoChannel => {
         this.videoChannel = videoChannel
-        this.currentRoute = '/video-channels/' + this.videoChannel.nameWithHost + '/videos'
 
         this.reloadVideos()
         this.generateSyndicationList()
index cedd07d39a533250e99a75162b47d2f5e03daa99..d4872a0a597656ce25d2e27d9ba8ac677201320f 100644 (file)
@@ -23,6 +23,10 @@ const videoChannelsRoutes: Routes = [
         data: {
           meta: {
             title: 'Video channel videos'
+          },
+          reuse: {
+            enabled: true,
+            key: 'video-channel-videos-list'
           }
         }
       },
index cff37a7d6f187930eb7622881cdac10ce493aa58..db8888dbae2a316faa80fde0d47aad1196513f7c 100644 (file)
@@ -1,8 +1,9 @@
 import { NgModule } from '@angular/core'
-import { RouterModule, Routes } from '@angular/router'
+import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router'
 
 import { PreloadSelectedModulesList } from './core'
 import { AppComponent } from '@app/app.component'
+import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
 
 const routes: Routes = [
   {
@@ -43,12 +44,14 @@ const routes: Routes = [
   imports: [
     RouterModule.forRoot(routes, {
       useHash: Boolean(history.pushState) === false,
+      scrollPositionRestoration: 'disabled',
       preloadingStrategy: PreloadSelectedModulesList,
-      anchorScrolling: 'enabled'
+      anchorScrolling: 'disabled'
     })
   ],
   providers: [
-    PreloadSelectedModulesList
+    PreloadSelectedModulesList,
+    { provide: RouteReuseStrategy, useClass: CustomReuseStrategy }
   ],
   exports: [ RouterModule ]
 })
index c5c5a8f668703dda6121343790cd3e8565b3387e..ad0588b99be1153b4a0b66749fb01d2c6876ade1 100644 (file)
@@ -1,13 +1,14 @@
 import { Component, OnInit } from '@angular/core'
 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
-import { GuardsCheckStart, NavigationEnd, Router } from '@angular/router'
+import { Event, GuardsCheckStart, NavigationEnd, Router, Scroll } from '@angular/router'
 import { AuthService, RedirectService, ServerService, ThemeService } from '@app/core'
 import { is18nPath } from '../../../shared/models/i18n'
 import { ScreenService } from '@app/shared/misc/screen.service'
-import { skip, debounceTime } from 'rxjs/operators'
-import { HotkeysService, Hotkey } from 'angular2-hotkeys'
+import { debounceTime, filter, map, pairwise, skip } from 'rxjs/operators'
+import { Hotkey, HotkeysService } from 'angular2-hotkeys'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { fromEvent } from 'rxjs'
+import { ViewportScroller } from '@angular/common'
 
 @Component({
   selector: 'my-app',
@@ -22,6 +23,7 @@ export class AppComponent implements OnInit {
 
   constructor (
     private i18n: I18n,
+    private viewportScroller: ViewportScroller,
     private router: Router,
     private authService: AuthService,
     private serverService: ServerService,
@@ -52,15 +54,6 @@ export class AppComponent implements OnInit {
   ngOnInit () {
     document.getElementById('incompatible-browser').className += ' browser-ok'
 
-    this.router.events.subscribe(e => {
-      if (e instanceof NavigationEnd) {
-        const pathname = window.location.pathname
-        if (!pathname || pathname === '/' || is18nPath(pathname)) {
-          this.redirectService.redirectToHomepage(true)
-        }
-      }
-    })
-
     this.authService.loadClientCredentials()
 
     if (this.isUserLoggedIn()) {
@@ -81,15 +74,94 @@ export class AppComponent implements OnInit {
       this.isMenuDisplayed = false
     }
 
-    this.router.events.subscribe(
-      e => {
-        // User clicked on a link in the menu, change the page
-        if (e instanceof GuardsCheckStart && this.screenService.isInSmallView()) {
-          this.isMenuDisplayed = false
-        }
+    this.initRouteEvents()
+    this.injectJS()
+    this.injectCSS()
+
+    this.initHotkeys()
+
+    fromEvent(window, 'resize')
+      .pipe(debounceTime(200))
+      .subscribe(() => this.onResize())
+  }
+
+  isUserLoggedIn () {
+    return this.authService.isLoggedIn()
+  }
+
+  toggleMenu () {
+    this.isMenuDisplayed = !this.isMenuDisplayed
+    this.isMenuChangedByUser = true
+  }
+
+  onResize () {
+    this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
+  }
+
+  private initRouteEvents () {
+    let resetScroll = true
+    const eventsObs = this.router.events
+
+    const scrollEvent = eventsObs.pipe(filter((e: Event): e is Scroll => e instanceof Scroll))
+    const navigationEndEvent = eventsObs.pipe(filter((e: Event): e is NavigationEnd => e instanceof NavigationEnd))
+
+    scrollEvent.subscribe(e => {
+      if (e.position) {
+        return this.viewportScroller.scrollToPosition(e.position)
       }
-    )
 
+      if (e.anchor) {
+        return this.viewportScroller.scrollToAnchor(e.anchor)
+      }
+
+      if (resetScroll) {
+        return this.viewportScroller.scrollToPosition([ 0, 0 ])
+      }
+    })
+
+    // When we add the a-state parameter, we don't want to alter the scroll
+    navigationEndEvent.pipe(pairwise())
+                      .subscribe(([ e1, e2 ]) => {
+                        try {
+                          resetScroll = false
+
+                          const previousUrl = new URL(window.location.origin + e1.url)
+                          const nextUrl = new URL(window.location.origin + e2.url)
+
+                          if (previousUrl.pathname !== nextUrl.pathname) {
+                            resetScroll = true
+                            return
+                          }
+
+                          const nextSearchParams = nextUrl.searchParams
+                          nextSearchParams.delete('a-state')
+
+                          const previousSearchParams = previousUrl.searchParams
+
+                          nextSearchParams.sort()
+                          previousSearchParams.sort()
+
+                          if (nextSearchParams.toString() !== previousSearchParams.toString()) {
+                            resetScroll = true
+                          }
+                        } catch (e) {
+                          console.error('Cannot parse URL to check next scroll.', e)
+                          resetScroll = true
+                        }
+                      })
+
+    navigationEndEvent.pipe(
+      map(() => window.location.pathname),
+      filter(pathname => !pathname || pathname === '/' || is18nPath(pathname))
+    ).subscribe(() => this.redirectService.redirectToHomepage(true))
+
+    eventsObs.pipe(
+      filter((e: Event): e is GuardsCheckStart => e instanceof GuardsCheckStart),
+      filter(() => this.screenService.isInSmallView())
+    ).subscribe(() => this.isMenuDisplayed = false) // User clicked on a link in the menu, change the page
+  }
+
+  private injectJS () {
     // Inject JS
     this.serverService.configLoaded
         .subscribe(() => {
@@ -104,7 +176,9 @@ export class AppComponent implements OnInit {
             }
           }
         })
+  }
 
+  private injectCSS () {
     // Inject CSS if modified (admin config settings)
     this.serverService.configLoaded
         .pipe(skip(1)) // We only want to subscribe to reloads, because the CSS is already injected by the server
@@ -120,7 +194,9 @@ export class AppComponent implements OnInit {
             this.customCSS = this.domSanitizer.bypassSecurityTrustHtml(styleTag)
           }
         })
+  }
 
+  private initHotkeys () {
     this.hotkeysService.add([
       new Hotkey(['/', 's'], (event: KeyboardEvent): boolean => {
         document.getElementById('search-video').focus()
@@ -155,22 +231,5 @@ export class AppComponent implements OnInit {
         return false
       }, undefined, this.i18n('Toggle Dark theme'))
     ])
-
-    fromEvent(window, 'resize')
-      .pipe(debounceTime(200))
-      .subscribe(() => this.onResize())
-  }
-
-  isUserLoggedIn () {
-    return this.authService.isLoggedIn()
-  }
-
-  toggleMenu () {
-    this.isMenuDisplayed = !this.isMenuDisplayed
-    this.isMenuChangedByUser = true
-  }
-
-  onResize () {
-    this.isMenuDisplayed = window.innerWidth >= 800 && !this.isMenuChangedByUser
   }
 }
diff --git a/client/src/app/core/routing/custom-reuse-strategy.ts b/client/src/app/core/routing/custom-reuse-strategy.ts
new file mode 100644 (file)
index 0000000..a9f61ac
--- /dev/null
@@ -0,0 +1,81 @@
+import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router'
+
+export class CustomReuseStrategy implements RouteReuseStrategy {
+  storedRouteHandles = new Map<string, DetachedRouteHandle>()
+  recentlyUsed: string
+
+  private readonly MAX_SIZE = 2
+
+  // Decides if the route should be stored
+  shouldDetach (route: ActivatedRouteSnapshot): boolean {
+    return this.isReuseEnabled(route)
+  }
+
+  // Store the information for the route we're destructing
+  store (route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
+    if (!handle) return
+
+    const key = this.generateKey(route)
+    this.recentlyUsed = key
+
+    console.log('Storing component %s to reuse later.', key);
+
+    (handle as any).componentRef.instance.disableForReuse()
+
+    this.storedRouteHandles.set(key, handle)
+
+    this.gb()
+  }
+
+  // Return true if we have a stored route object for the next route
+  shouldAttach (route: ActivatedRouteSnapshot): boolean {
+    const key = this.generateKey(route)
+    return this.isReuseEnabled(route) && this.storedRouteHandles.has(key)
+  }
+
+  // If we returned true in shouldAttach(), now return the actual route data for restoration
+  retrieve (route: ActivatedRouteSnapshot): DetachedRouteHandle {
+    if (!this.isReuseEnabled(route)) return undefined
+
+    const key = this.generateKey(route)
+    this.recentlyUsed = key
+
+    console.log('Reusing component %s.', key)
+
+    const handle = this.storedRouteHandles.get(key)
+    if (!handle) return handle;
+
+    (handle as any).componentRef.instance.enabledForReuse()
+
+    return handle
+  }
+
+  // Reuse the route if we're going to and from the same route
+  shouldReuseRoute (future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
+    return future.routeConfig === curr.routeConfig
+  }
+
+  private gb () {
+    if (this.storedRouteHandles.size >= this.MAX_SIZE) {
+      this.storedRouteHandles.forEach((r, key) => {
+        if (key === this.recentlyUsed) return
+
+        console.log('Removing stored component %s.', key);
+
+        (r as any).componentRef.destroy()
+        this.storedRouteHandles.delete(key)
+      })
+    }
+  }
+
+  private generateKey (route: ActivatedRouteSnapshot) {
+    const reuse = route.data.reuse
+    if (!reuse) return undefined
+
+    return reuse.key + JSON.stringify(route.queryParams)
+  }
+
+  private isReuseEnabled (route: ActivatedRouteSnapshot) {
+    return route.data.reuse && route.data.reuse.enabled && route.queryParams['a-state']
+  }
+}
diff --git a/client/src/app/core/routing/disable-for-reuse-hook.ts b/client/src/app/core/routing/disable-for-reuse-hook.ts
new file mode 100644 (file)
index 0000000..c5eb5c5
--- /dev/null
@@ -0,0 +1,7 @@
+export interface DisableForReuseHook {
+
+  disableForReuse (): void
+
+  enabledForReuse (): void
+
+}
index 1f97bc38937600de09db188725eb50914b7f3412..e134654a35a63ad88863cec4171b6885050f0920 100644 (file)
 
   <div class="no-results" i18n *ngIf="pagination.totalItems === 0">No results.</div>
   <div
-    myInfiniteScroller
-    [pageHeight]="pageHeight" [firstLoadedPage]="firstLoadedPage"
-    (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
-    class="videos" #videosElement
+    myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [autoInit]="true"
+    class="videos"
   >
-    <div *ngFor="let videos of videoPages; trackBy: pageByVideoId" class="videos-page">
-      <my-video-miniature *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"></my-video-miniature>
-    </div>
+    <my-video-miniature *ngFor="let video of videos; trackBy: videoById" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType">
+    </my-video-miniature>
   </div>
 </div>
index 2cd5bc3937b964675c301b54c79365a8282580ed..467f629eae20e18983d2ccdd5e013e4a3a9bbc29 100644 (file)
@@ -1,66 +1,52 @@
 import { debounceTime } from 'rxjs/operators'
-import { ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'
+import { OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
-import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
 import { fromEvent, Observable, Subscription } from 'rxjs'
 import { AuthService } from '../../core/auth'
 import { ComponentPagination } from '../rest/component-pagination.model'
 import { VideoSortField } from './sort-field.type'
 import { Video } from './video.model'
-import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
 import { Syndication } from '@app/shared/video/syndication.model'
-import { Notifier } from '@app/core'
-
-export abstract class AbstractVideoList implements OnInit, OnDestroy {
-  private static LINES_PER_PAGE = 4
-
-  @ViewChild('videosElement') videosElement: ElementRef
-  @ViewChild(InfiniteScrollerDirective) infiniteScroller: InfiniteScrollerDirective
+import { Notifier, ServerService } from '@app/core'
+import { DisableForReuseHook } from '@app/core/routing/disable-for-reuse-hook'
 
+export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableForReuseHook {
   pagination: ComponentPagination = {
     currentPage: 1,
-    itemsPerPage: 10,
+    itemsPerPage: 25,
     totalItems: null
   }
   sort: VideoSortField = '-publishedAt'
+
   categoryOneOf?: number
   defaultSort: VideoSortField = '-publishedAt'
+
   syndicationItems: Syndication[] = []
 
   loadOnInit = true
   marginContent = true
-  pageHeight: number
-  videoWidth: number
-  videoHeight: number
-  videoPages: Video[][] = []
+  videos: Video[] = []
   ownerDisplayType: OwnerDisplayType = 'account'
-  firstLoadedPage: number
   displayModerationBlock = false
   titleTooltip: string
 
-  protected baseVideoWidth = 238
-  protected baseVideoHeight = 225
+  disabled = false
 
   protected abstract notifier: Notifier
   protected abstract authService: AuthService
-  protected abstract router: Router
   protected abstract route: ActivatedRoute
+  protected abstract serverService: ServerService
   protected abstract screenService: ScreenService
-  protected abstract i18n: I18n
-  protected abstract location: Location
-  protected abstract currentRoute: string
+  protected abstract router: Router
   abstract titlePage: string
 
-  protected loadedPages: { [ id: number ]: Video[] } = {}
-  protected loadingPage: { [ id: number ]: boolean } = {}
-  protected otherRouteParams = {}
-
   private resizeSubscription: Subscription
+  private angularState: number
+
+  abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number }>
 
-  abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}>
   abstract generateSyndicationList (): void
 
   get user () {
@@ -77,207 +63,87 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
       .subscribe(() => this.calcPageSizes())
 
     this.calcPageSizes()
-    if (this.loadOnInit === true) this.loadMoreVideos(this.pagination.currentPage)
+    if (this.loadOnInit === true) this.loadMoreVideos()
   }
 
   ngOnDestroy () {
     if (this.resizeSubscription) this.resizeSubscription.unsubscribe()
   }
 
-  pageByVideoId (index: number, page: Video[]) {
-    // Video are unique in all pages
-    return page.length !== 0 ? page[0].id : 0
+  disableForReuse () {
+    this.disabled = true
   }
 
-  videoById (index: number, video: Video) {
-    return video.id
+  enabledForReuse () {
+    this.disabled = false
   }
 
-  onNearOfTop () {
-    this.previousPage()
+  videoById (index: number, video: Video) {
+    return video.id
   }
 
   onNearOfBottom () {
-    if (this.hasMoreVideos()) {
-      this.nextPage()
-    }
-  }
+    if (this.disabled) return
 
-  onPageChanged (page: number) {
-    this.pagination.currentPage = page
-    this.setNewRouteParams()
-  }
+    // Last page
+    if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return
 
-  reloadVideos () {
-    this.loadedPages = {}
-    this.loadMoreVideos(this.pagination.currentPage)
-  }
-
-  loadMoreVideos (page: number, loadOnTop = false) {
-    this.adjustVideoPageHeight()
+    this.pagination.currentPage += 1
 
-    const currentY = window.scrollY
+    this.setScrollRouteParams()
 
-    if (this.loadedPages[page] !== undefined) return
-    if (this.loadingPage[page] === true) return
+    this.loadMoreVideos()
+  }
 
-    this.loadingPage[page] = true
-    const observable = this.getVideosObservable(page)
+  loadMoreVideos () {
+    const observable = this.getVideosObservable(this.pagination.currentPage)
 
     observable.subscribe(
       ({ videos, totalVideos }) => {
-        this.loadingPage[page] = false
-
-        if (this.firstLoadedPage === undefined || this.firstLoadedPage > page) this.firstLoadedPage = page
-
-        // Paging is too high, return to the first one
-        if (this.pagination.currentPage > 1 && totalVideos <= ((this.pagination.currentPage - 1) * this.pagination.itemsPerPage)) {
-          this.pagination.currentPage = 1
-          this.setNewRouteParams()
-          return this.reloadVideos()
-        }
-
-        this.loadedPages[page] = videos
-        this.buildVideoPages()
         this.pagination.totalItems = totalVideos
-
-        // Initialize infinite scroller now we loaded the first page
-        if (Object.keys(this.loadedPages).length === 1) {
-          // Wait elements creation
-          setTimeout(() => {
-            this.infiniteScroller.initialize()
-
-            // At our first load, we did not load the first page
-            // Load the previous page so the user can move on the top (and browser previous pages)
-            if (this.pagination.currentPage > 1) this.loadMoreVideos(this.pagination.currentPage - 1, true)
-          }, 500)
-        }
-
-        // Insert elements on the top but keep the scroll in the previous position
-        if (loadOnTop) setTimeout(() => { window.scrollTo(0, currentY + this.pageHeight) }, 0)
+        this.videos = this.videos.concat(videos)
       },
-      error => {
-        this.loadingPage[page] = false
-        this.notifier.error(error.message)
-      }
-    )
-  }
-
-  toggleModerationDisplay () {
-    throw new Error('toggleModerationDisplay is not implemented')
-  }
 
-  protected hasMoreVideos () {
-    // No results
-    if (this.pagination.totalItems === 0) return false
-
-    // Not loaded yet
-    if (!this.pagination.totalItems) return true
-
-    const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage
-    return maxPage > this.maxPageLoaded()
-  }
-
-  protected previousPage () {
-    const min = this.minPageLoaded()
-
-    if (min > 1) {
-      this.loadMoreVideos(min - 1, true)
-    }
+      error => this.notifier.error(error.message)
+    )
   }
 
-  protected nextPage () {
-    this.loadMoreVideos(this.maxPageLoaded() + 1)
+  reloadVideos () {
+    this.pagination.currentPage = 1
+    this.videos = []
+    this.loadMoreVideos()
   }
 
-  protected buildRouteParams () {
-    // There is always a sort and a current page
-    const params = {
-      sort: this.sort,
-      page: this.pagination.currentPage
-    }
-
-    return Object.assign(params, this.otherRouteParams)
+  toggleModerationDisplay () {
+    throw new Error('toggleModerationDisplay is not implemented')
   }
 
   protected loadRouteParams (routeParams: { [ key: string ]: any }) {
-    this.sort = routeParams['sort'] as VideoSortField || this.defaultSort
-    this.categoryOneOf = routeParams['categoryOneOf']
-    if (routeParams['page'] !== undefined) {
-      this.pagination.currentPage = parseInt(routeParams['page'], 10)
-    } else {
-      this.pagination.currentPage = 1
-    }
-  }
-
-  protected setNewRouteParams () {
-    const paramsObject = this.buildRouteParams()
-
-    const queryParams = Object.keys(paramsObject)
-                              .map(p => p + '=' + paramsObject[p])
-                              .join('&')
-    this.location.replaceState(this.currentRoute, queryParams)
-  }
-
-  protected buildVideoPages () {
-    this.videoPages = Object.values(this.loadedPages)
-  }
-
-  protected adjustVideoPageHeight () {
-    const numberOfPagesLoaded = Object.keys(this.loadedPages).length
-    if (!numberOfPagesLoaded) return
-
-    this.pageHeight = this.videosElement.nativeElement.offsetHeight / numberOfPagesLoaded
-  }
-
-  protected buildVideoHeight () {
-    // Same ratios than base width/height
-    return this.videosElement.nativeElement.offsetWidth * (this.baseVideoHeight / this.baseVideoWidth)
-  }
-
-  private minPageLoaded () {
-    return Math.min(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
-  }
-
-  private maxPageLoaded () {
-    return Math.max(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
+    this.sort = routeParams[ 'sort' ] as VideoSortField || this.defaultSort
+    this.categoryOneOf = routeParams[ 'categoryOneOf' ]
+    this.angularState = routeParams[ 'a-state' ]
   }
 
   private calcPageSizes () {
-    if (this.screenService.isInMobileView() || this.baseVideoWidth === -1) {
+    if (this.screenService.isInMobileView()) {
       this.pagination.itemsPerPage = 5
-
-      // Video takes all the width
-      this.videoWidth = -1
-      this.videoHeight = this.buildVideoHeight()
-      this.pageHeight = this.pagination.itemsPerPage * this.videoHeight
-    } else {
-      this.videoWidth = this.baseVideoWidth
-      this.videoHeight = this.baseVideoHeight
-
-      const videosWidth = this.videosElement.nativeElement.offsetWidth
-      this.pagination.itemsPerPage = Math.floor(videosWidth / this.videoWidth) * AbstractVideoList.LINES_PER_PAGE
-      this.pageHeight = this.videoHeight * AbstractVideoList.LINES_PER_PAGE
     }
+  }
 
-    // Rebuild pages because maybe we modified the number of items per page
-    const videos = [].concat(...this.videoPages)
-    this.loadedPages = {}
+  private setScrollRouteParams () {
+    // Already set
+    if (this.angularState) return
 
-    let i = 1
-    // Don't include the last page if it not complete
-    while (videos.length >= this.pagination.itemsPerPage && i < 10000) { // 10000 -> Hard limit in case of infinite loop
-      this.loadedPages[i] = videos.splice(0, this.pagination.itemsPerPage)
-      i++
-    }
+    this.angularState = 42
 
-    // Re fetch the last page
-    if (videos.length !== 0) {
-      this.loadMoreVideos(i)
-    } else {
-      this.buildVideoPages()
+    const queryParams = {
+      'a-state': this.angularState,
+      categoryOneOf: this.categoryOneOf
     }
 
-    console.log('Rebuilt pages with %s elements per page.', this.pagination.itemsPerPage)
+    let path = this.router.url
+    if (!path || path === '/') path = this.serverService.getConfig().instance.defaultClientRoute
+
+    this.router.navigate([ path ], { queryParams, replaceUrl: true, queryParamsHandling: 'merge' })
   }
 }
index a9e75007cb26ef4fc21de07cf23d711244f3bd80..5f8a1dd6e49606ec65294f8b4ad9b22a02db8a0a 100644 (file)
@@ -6,24 +6,15 @@ import { fromEvent, Subscription } from 'rxjs'
   selector: '[myInfiniteScroller]'
 })
 export class InfiniteScrollerDirective implements OnInit, OnDestroy {
-  @Input() containerHeight: number
-  @Input() pageHeight: number
-  @Input() firstLoadedPage = 1
   @Input() percentLimit = 70
   @Input() autoInit = false
   @Input() onItself = false
 
   @Output() nearOfBottom = new EventEmitter<void>()
-  @Output() nearOfTop = new EventEmitter<void>()
-  @Output() pageChanged = new EventEmitter<number>()
 
   private decimalLimit = 0
   private lastCurrentBottom = -1
-  private lastCurrentTop = 0
   private scrollDownSub: Subscription
-  private scrollUpSub: Subscription
-  private pageChangeSub: Subscription
-  private middleScreen: number
   private container: HTMLElement
 
   constructor (private el: ElementRef) {
@@ -36,8 +27,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
 
   ngOnDestroy () {
     if (this.scrollDownSub) this.scrollDownSub.unsubscribe()
-    if (this.scrollUpSub) this.scrollUpSub.unsubscribe()
-    if (this.pageChangeSub) this.pageChangeSub.unsubscribe()
   }
 
   initialize () {
@@ -45,8 +34,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
       this.container = this.el.nativeElement
     }
 
-    this.middleScreen = window.innerHeight / 2
-
     // Emit the last value
     const throttleOptions = { leading: true, trailing: true }
 
@@ -72,40 +59,6 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy {
         filter(({ current, maximumScroll }) => maximumScroll <= 0 || (current / maximumScroll) > this.decimalLimit)
       )
       .subscribe(() => this.nearOfBottom.emit())
-
-    // Scroll up
-    this.scrollUpSub = scrollObservable
-      .pipe(
-        // Check we scroll up
-        filter(({ current }) => {
-          const res = this.lastCurrentTop > current
-
-          this.lastCurrentTop = current
-          return res
-        }),
-        filter(({ current, maximumScroll }) => {
-          return current !== 0 && (1 - (current / maximumScroll)) > this.decimalLimit
-        })
-      )
-      .subscribe(() => this.nearOfTop.emit())
-
-    // Page change
-    this.pageChangeSub = scrollObservable
-      .pipe(
-        distinct(),
-        map(({ current }) => this.calculateCurrentPage(current)),
-        distinctUntilChanged()
-      )
-      .subscribe(res => this.pageChanged.emit(res))
-  }
-
-  private calculateCurrentPage (current: number) {
-    const scrollY = current + this.middleScreen
-
-    const page = Math.max(1, Math.ceil(scrollY / this.pageHeight))
-
-    // Offset page
-    return page + (this.firstLoadedPage - 1)
   }
 
   private getScrollInfo () {
index c0be4b88564a84066947ba33a5f502030fc7bd71..13d4023c27a3b3595b7673ac59cc26375531d384 100644 (file)
@@ -1,7 +1,6 @@
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { immutableAssign } from '@app/shared/misc/utils'
-import { Location } from '@angular/common'
 import { AuthService } from '../../core/auth'
 import { AbstractVideoList } from '../../shared/video/abstract-video-list'
 import { VideoSortField } from '../../shared/video/sort-field.type'
@@ -10,7 +9,7 @@ import { VideoFilter } from '../../../../../shared/models/videos/video-query.typ
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { UserRight } from '../../../../../shared/models/users'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
 
 @Component({
   selector: 'my-videos-local',
@@ -19,18 +18,17 @@ import { Notifier } from '@app/core'
 })
 export class VideoLocalComponent extends AbstractVideoList implements OnInit, OnDestroy {
   titlePage: string
-  currentRoute = '/videos/local'
   sort = '-publishedAt' as VideoSortField
   filter: VideoFilter = 'local'
 
   constructor (
     protected router: Router,
+    protected serverService: ServerService,
     protected route: ActivatedRoute,
     protected notifier: Notifier,
     protected authService: AuthService,
-    protected location: Location,
-    protected i18n: I18n,
     protected screenService: ScreenService,
+    private i18n: I18n,
     private videoService: VideoService
   ) {
     super()
index f99c8abb648b820ac9c756ba7559b54a000a8559..80cef813ef6e49548bbb0bef0ca46ee8877be972 100644 (file)
@@ -1,6 +1,5 @@
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
 import { immutableAssign } from '@app/shared/misc/utils'
 import { AuthService } from '../../core/auth'
 import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -8,7 +7,7 @@ import { VideoSortField } from '../../shared/video/sort-field.type'
 import { VideoService } from '../../shared/video/video.service'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
 
 @Component({
   selector: 'my-videos-recently-added',
@@ -17,17 +16,16 @@ import { Notifier } from '@app/core'
 })
 export class VideoRecentlyAddedComponent extends AbstractVideoList implements OnInit, OnDestroy {
   titlePage: string
-  currentRoute = '/videos/recently-added'
   sort: VideoSortField = '-publishedAt'
 
   constructor (
-    protected router: Router,
     protected route: ActivatedRoute,
-    protected location: Location,
+    protected serverService: ServerService,
+    protected router: Router,
     protected notifier: Notifier,
     protected authService: AuthService,
-    protected i18n: I18n,
     protected screenService: ScreenService,
+    private i18n: I18n,
     private videoService: VideoService
   ) {
     super()
index a66a0f97c28d6b98d8f364f87b8fdcbb0300b4a4..e2ad95bc4f31098f2b6f15c48253224104acd17a 100644 (file)
@@ -1,6 +1,5 @@
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
-import { Location } from '@angular/common'
 import { immutableAssign } from '@app/shared/misc/utils'
 import { AuthService } from '../../core/auth'
 import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -17,18 +16,16 @@ import { Notifier, ServerService } from '@app/core'
 })
 export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
   titlePage: string
-  currentRoute = '/videos/trending'
   defaultSort: VideoSortField = '-trending'
 
   constructor (
     protected router: Router,
+    protected serverService: ServerService,
     protected route: ActivatedRoute,
     protected notifier: Notifier,
     protected authService: AuthService,
-    protected location: Location,
     protected screenService: ScreenService,
-    private serverService: ServerService,
-    protected i18n: I18n,
+    private i18n: I18n,
     private videoService: VideoService
   ) {
     super()
index bee828e126a23e6bc24aa01237119a839ef698f5..2f0685ccce12f6a7634034aa0fd87d039254c80a 100644 (file)
@@ -1,7 +1,6 @@
 import { Component, OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { immutableAssign } from '@app/shared/misc/utils'
-import { Location } from '@angular/common'
 import { AuthService } from '../../core/auth'
 import { AbstractVideoList } from '../../shared/video/abstract-video-list'
 import { VideoSortField } from '../../shared/video/sort-field.type'
@@ -9,7 +8,7 @@ import { VideoService } from '../../shared/video/video.service'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
 
 @Component({
   selector: 'my-videos-user-subscriptions',
@@ -18,18 +17,17 @@ import { Notifier } from '@app/core'
 })
 export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy {
   titlePage: string
-  currentRoute = '/videos/subscriptions'
   sort = '-publishedAt' as VideoSortField
   ownerDisplayType: OwnerDisplayType = 'auto'
 
   constructor (
     protected router: Router,
+    protected serverService: ServerService,
     protected route: ActivatedRoute,
     protected notifier: Notifier,
     protected authService: AuthService,
-    protected location: Location,
-    protected i18n: I18n,
     protected screenService: ScreenService,
+    private i18n: I18n,
     private videoService: VideoService
   ) {
     super()
index 69a9232ce52eeab2c879bc6c9170bc1fe2014f73..505173a5b1746c4c9ec27e360dcb25b3d1cc5eee 100644 (file)
@@ -29,6 +29,10 @@ const videosRoutes: Routes = [
         data: {
           meta: {
             title: 'Trending videos'
+          },
+          reuse: {
+            enabled: true,
+            key: 'trending-videos-list'
           }
         }
       },
@@ -38,6 +42,10 @@ const videosRoutes: Routes = [
         data: {
           meta: {
             title: 'Recently added videos'
+          },
+          reuse: {
+            enabled: true,
+            key: 'recently-added-videos-list'
           }
         }
       },
@@ -47,6 +55,10 @@ const videosRoutes: Routes = [
         data: {
           meta: {
             title: 'Subscriptions'
+          },
+          reuse: {
+            enabled: true,
+            key: 'subscription-videos-list'
           }
         }
       },
@@ -56,6 +68,10 @@ const videosRoutes: Routes = [
         data: {
           meta: {
             title: 'Local videos'
+          },
+          reuse: {
+            enabled: true,
+            key: 'local-videos-list'
           }
         }
       },
index e733138c1369ece6a74c003321250ad292c898a5..93d84c6fc7145c70cf9c52cd7950c78077298f64 100644 (file)
@@ -1,5 +1,4 @@
 import * as Sequelize from 'sequelize'
-import { Op } from 'sequelize'
 import {
   AllowNull,
   BeforeDestroy,
@@ -458,7 +457,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
     const query = {
       where: {
         updatedAt: {
-          [Op.lt]: beforeUpdatedAt
+          [Sequelize.Op.lt]: beforeUpdatedAt
         },
         videoId
       }
index fb52b35d932e86c0abb32277a72767f15a745d46..39908156447aa672afbdb4faf91f7cd3e357dd13 100644 (file)
@@ -1,5 +1,4 @@
 import * as Sequelize from 'sequelize'
-import { Op } from 'sequelize'
 import * as Bluebird from 'bluebird'
 import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
@@ -206,7 +205,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
     const query = {
       where: {
         updatedAt: {
-          [Op.lt]: beforeUpdatedAt
+          [Sequelize.Op.lt]: beforeUpdatedAt
         },
         videoId
       }