aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-02-13 14:11:05 +0100
committerChocobozzz <me@florianbigard.com>2018-02-13 14:20:46 +0100
commit0cd4344f3cf529b15308fcf3eb7d7eb07726df56 (patch)
tree276f6e8cfe72d583114d82fd5db981550a395488
parent29c6b829446a6fb29dffc6b7b638079ce60f3771 (diff)
downloadPeerTube-0cd4344f3cf529b15308fcf3eb7d7eb07726df56.tar.gz
PeerTube-0cd4344f3cf529b15308fcf3eb7d7eb07726df56.tar.zst
PeerTube-0cd4344f3cf529b15308fcf3eb7d7eb07726df56.zip
Rewrite infinite scroll
-rw-r--r--client/package.json1
-rw-r--r--client/src/app/account/account-videos/account-videos.component.html60
-rw-r--r--client/src/app/account/account-videos/account-videos.component.scss7
-rw-r--r--client/src/app/account/account-videos/account-videos.component.ts31
-rw-r--r--client/src/app/shared/misc/utils.ts7
-rw-r--r--client/src/app/shared/shared.module.ts8
-rw-r--r--client/src/app/shared/video/abstract-video-list.html22
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts96
-rw-r--r--client/src/app/shared/video/infinite-scroller.directive.ts77
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.html7
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.ts7
-rw-r--r--client/src/app/videos/video-list/video-recently-added.component.ts7
-rw-r--r--client/src/app/videos/video-list/video-search.component.ts12
-rw-r--r--client/src/app/videos/video-list/video-trending.component.ts6
-rw-r--r--client/yarn.lock4
15 files changed, 239 insertions, 113 deletions
diff --git a/client/package.json b/client/package.json
index 4cb0b002c..fe8bdd1f9 100644
--- a/client/package.json
+++ b/client/package.json
@@ -61,7 +61,6 @@
61 "ngx-bootstrap": "2.0.2", 61 "ngx-bootstrap": "2.0.2",
62 "ngx-chips": "1.6.3", 62 "ngx-chips": "1.6.3",
63 "ngx-clipboard": "9.0.1", 63 "ngx-clipboard": "9.0.1",
64 "ngx-infinite-scroll": "0.7.2",
65 "ngx-pipes": "^2.0.5", 64 "ngx-pipes": "^2.0.5",
66 "node-sass": "^4.1.1", 65 "node-sass": "^4.1.1",
67 "npm-font-source-sans-pro": "^1.0.2", 66 "npm-font-source-sans-pro": "^1.0.2",
diff --git a/client/src/app/account/account-videos/account-videos.component.html b/client/src/app/account/account-videos/account-videos.component.html
index 0755158b9..3d8c656ee 100644
--- a/client/src/app/account/account-videos/account-videos.component.html
+++ b/client/src/app/account/account-videos/account-videos.component.html
@@ -1,44 +1,44 @@
1<div *ngIf="pagination.totalItems === 0">No results.</div> 1<div *ngIf="pagination.totalItems === 0">No results.</div>
2 2
3<div 3<div
4 class="videos" 4 myInfiniteScroller
5 infiniteScroll 5 [pageHeight]="pageHeight"
6 [infiniteScrollDistance]="0.5" 6 (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
7 [infiniteScrollUpDistance]="1.5" 7 class="videos" #videoElement
8 (scrolled)="onNearOfBottom()"
9 (scrolledUp)="onNearOfTop()"
10> 8>
11 <div class="video" *ngFor="let video of videos; let i = index"> 9 <div *ngFor="let videos of videoPages; let i = index" class="videos-page">
12 <div class="checkbox-container"> 10 <div class="video" *ngFor="let video of videos; let j = index">
13 <input [id]="'video-check-' + i" type="checkbox" [(ngModel)]="checkedVideos[video.id]" /> 11 <div class="checkbox-container">
14 <label [for]="'video-check-' + i"></label> 12 <input [id]="'video-check-' + video.id" type="checkbox" [(ngModel)]="checkedVideos[video.id]" />
15 </div> 13 <label [for]="'video-check-' + video.id"></label>
14 </div>
16 15
17 <my-video-thumbnail [video]="video"></my-video-thumbnail> 16 <my-video-thumbnail [video]="video"></my-video-thumbnail>
18 17
19 <div class="video-info"> 18 <div class="video-info">
20 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> 19 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
21 <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> 20 <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
22 </div> 21 </div>
23 22
24 <!-- Display only once --> 23 <!-- Display only once -->
25 <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0"> 24 <div class="action-selection-mode" *ngIf="isInSelectionMode() === true && i === 0 && j === 0">
26 <div class="action-selection-mode-child"> 25 <div class="action-selection-mode-child">
27 <span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()"> 26 <span class="action-button action-button-cancel-selection" (click)="abortSelectionMode()">
28 Cancel 27 Cancel
29 </span> 28 </span>
30 29
31 <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()"> 30 <span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
32 <span class="icon icon-delete-white"></span> 31 <span class="icon icon-delete-white"></span>
33 Delete 32 Delete
34 </span> 33 </span>
34 </div>
35 </div> 35 </div>
36 </div>
37 36
38 <div class="video-buttons" *ngIf="isInSelectionMode() === false"> 37 <div class="video-buttons" *ngIf="isInSelectionMode() === false">
39 <my-delete-button (click)="deleteVideo(video)"></my-delete-button> 38 <my-delete-button (click)="deleteVideo(video)"></my-delete-button>
40 39
41 <my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button> 40 <my-edit-button [routerLink]="[ '/videos', 'edit', video.uuid ]"></my-edit-button>
41 </div>
42 </div> 42 </div>
43 </div> 43 </div>
44</div> 44</div>
diff --git a/client/src/app/account/account-videos/account-videos.component.scss b/client/src/app/account/account-videos/account-videos.component.scss
index 707bd66ad..449cc6af4 100644
--- a/client/src/app/account/account-videos/account-videos.component.scss
+++ b/client/src/app/account/account-videos/account-videos.component.scss
@@ -45,16 +45,13 @@
45 display: flex; 45 display: flex;
46 min-height: 130px; 46 min-height: 130px;
47 padding-bottom: 20px; 47 padding-bottom: 20px;
48 margin-bottom: 20px;
49 border-bottom: 1px solid #C6C6C6;
48 50
49 &:first-child { 51 &:first-child {
50 margin-top: 47px; 52 margin-top: 47px;
51 } 53 }
52 54
53 &:not(:last-child) {
54 margin-bottom: 20px;
55 border-bottom: 1px solid #C6C6C6;
56 }
57
58 .checkbox-container { 55 .checkbox-container {
59 display: flex; 56 display: flex;
60 align-items: center; 57 align-items: center;
diff --git a/client/src/app/account/account-videos/account-videos.component.ts b/client/src/app/account/account-videos/account-videos.component.ts
index bce135557..e9d044dbf 100644
--- a/client/src/app/account/account-videos/account-videos.component.ts
+++ b/client/src/app/account/account-videos/account-videos.component.ts
@@ -1,5 +1,7 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils'
4import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
3import { NotificationsService } from 'angular2-notifications' 5import { NotificationsService } from 'angular2-notifications'
4import 'rxjs/add/observable/from' 6import 'rxjs/add/observable/from'
5import 'rxjs/add/operator/concatAll' 7import 'rxjs/add/operator/concatAll'
@@ -19,7 +21,9 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
19 titlePage = 'My videos' 21 titlePage = 'My videos'
20 currentRoute = '/account/videos' 22 currentRoute = '/account/videos'
21 checkedVideos: { [ id: number ]: boolean } = {} 23 checkedVideos: { [ id: number ]: boolean } = {}
22 pagination = { 24 videoHeight = 155
25 videoWidth = -1
26 pagination: ComponentPagination = {
23 currentPage: 1, 27 currentPage: 1,
24 itemsPerPage: 10, 28 itemsPerPage: 10,
25 totalItems: null 29 totalItems: null
@@ -46,8 +50,10 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
46 return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true) 50 return Object.keys(this.checkedVideos).some(k => this.checkedVideos[k] === true)
47 } 51 }
48 52
49 getVideosObservable () { 53 getVideosObservable (page: number) {
50 return this.videoService.getMyVideos(this.pagination, this.sort) 54 const newPagination = immutableAssign(this.pagination, { currentPage: page })
55
56 return this.videoService.getMyVideos(newPagination, this.sort)
51 } 57 }
52 58
53 deleteSelectedVideos () { 59 deleteSelectedVideos () {
@@ -71,9 +77,12 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
71 Observable.from(observables) 77 Observable.from(observables)
72 .concatAll() 78 .concatAll()
73 .subscribe( 79 .subscribe(
74 res => this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`), 80 res => {
81 this.notificationsService.success('Success', `${toDeleteVideosIds.length} videos deleted.`)
82 this.buildVideoPages()
83 },
75 84
76 err => this.notificationsService.error('Error', err.message) 85 err => this.notificationsService.error('Error', err.message)
77 ) 86 )
78 } 87 }
79 ) 88 )
@@ -89,6 +98,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
89 status => { 98 status => {
90 this.notificationsService.success('Success', `Video ${video.name} deleted.`) 99 this.notificationsService.success('Success', `Video ${video.name} deleted.`)
91 this.spliceVideosById(video.id) 100 this.spliceVideosById(video.id)
101 this.buildVideoPages()
92 }, 102 },
93 103
94 error => this.notificationsService.error('Error', error.message) 104 error => this.notificationsService.error('Error', error.message)
@@ -98,7 +108,14 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit
98 } 108 }
99 109
100 private spliceVideosById (id: number) { 110 private spliceVideosById (id: number) {
101 const index = this.videos.findIndex(v => v.id === id) 111 for (const key of Object.keys(this.loadedPages)) {
102 this.videos.splice(index, 1) 112 const videos = this.loadedPages[key]
113 const index = videos.findIndex(v => v.id === id)
114
115 if (index !== -1) {
116 videos.splice(index, 1)
117 return
118 }
119 }
103 } 120 }
104} 121}
diff --git a/client/src/app/shared/misc/utils.ts b/client/src/app/shared/misc/utils.ts
index 6620ac973..e6a697098 100644
--- a/client/src/app/shared/misc/utils.ts
+++ b/client/src/app/shared/misc/utils.ts
@@ -55,6 +55,10 @@ function dateToHuman (date: string) {
55 return datePipe.transform(date, 'medium') 55 return datePipe.transform(date, 'medium')
56} 56}
57 57
58function immutableAssign <A, B> (target: A, source: B) {
59 return Object.assign({}, target, source)
60}
61
58function isInSmallView () { 62function isInSmallView () {
59 return window.innerWidth < 600 63 return window.innerWidth < 600
60} 64}
@@ -70,5 +74,6 @@ export {
70 getAbsoluteAPIUrl, 74 getAbsoluteAPIUrl,
71 dateToHuman, 75 dateToHuman,
72 isInSmallView, 76 isInSmallView,
73 isInMobileView 77 isInMobileView,
78 immutableAssign
74} 79}
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index d8f98bdf6..330a0ba84 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -4,13 +4,13 @@ import { NgModule } from '@angular/core'
4import { FormsModule, ReactiveFormsModule } from '@angular/forms' 4import { FormsModule, ReactiveFormsModule } from '@angular/forms'
5import { RouterModule } from '@angular/router' 5import { RouterModule } from '@angular/router'
6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component' 6import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
7import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
7import { MarkdownService } from '@app/videos/shared' 8import { MarkdownService } from '@app/videos/shared'
8import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' 9import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
9 10
10import { BsDropdownModule } from 'ngx-bootstrap/dropdown' 11import { BsDropdownModule } from 'ngx-bootstrap/dropdown'
11import { ModalModule } from 'ngx-bootstrap/modal' 12import { ModalModule } from 'ngx-bootstrap/modal'
12import { TabsModule } from 'ngx-bootstrap/tabs' 13import { TabsModule } from 'ngx-bootstrap/tabs'
13import { InfiniteScrollModule } from 'ngx-infinite-scroll'
14import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' 14import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
15import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared' 15import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
16 16
@@ -42,7 +42,6 @@ import { VideoService } from './video/video.service'
42 ModalModule.forRoot(), 42 ModalModule.forRoot(),
43 43
44 PrimeSharedModule, 44 PrimeSharedModule,
45 InfiniteScrollModule,
46 NgPipesModule, 45 NgPipesModule,
47 TabsModule.forRoot() 46 TabsModule.forRoot()
48 ], 47 ],
@@ -55,7 +54,8 @@ import { VideoService } from './video/video.service'
55 EditButtonComponent, 54 EditButtonComponent,
56 NumberFormatterPipe, 55 NumberFormatterPipe,
57 FromNowPipe, 56 FromNowPipe,
58 MarkdownTextareaComponent 57 MarkdownTextareaComponent,
58 InfiniteScrollerDirective
59 ], 59 ],
60 60
61 exports: [ 61 exports: [
@@ -70,7 +70,6 @@ import { VideoService } from './video/video.service'
70 BsDropdownModule, 70 BsDropdownModule,
71 ModalModule, 71 ModalModule,
72 PrimeSharedModule, 72 PrimeSharedModule,
73 InfiniteScrollModule,
74 BytesPipe, 73 BytesPipe,
75 KeysPipe, 74 KeysPipe,
76 75
@@ -80,6 +79,7 @@ import { VideoService } from './video/video.service'
80 DeleteButtonComponent, 79 DeleteButtonComponent,
81 EditButtonComponent, 80 EditButtonComponent,
82 MarkdownTextareaComponent, 81 MarkdownTextareaComponent,
82 InfiniteScrollerDirective,
83 83
84 NumberFormatterPipe, 84 NumberFormatterPipe,
85 FromNowPipe 85 FromNowPipe
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html
index 60a2563b3..fb7f86852 100644
--- a/client/src/app/shared/video/abstract-video-list.html
+++ b/client/src/app/shared/video/abstract-video-list.html
@@ -6,17 +6,17 @@
6 <div *ngIf="pagination.totalItems === 0">No results.</div> 6 <div *ngIf="pagination.totalItems === 0">No results.</div>
7 7
8 <div 8 <div
9 class="videos" 9 myInfiniteScroller
10 infiniteScroll 10 [pageHeight]="pageHeight"
11 [infiniteScrollUpDistance]="1.5" 11 (nearOfTop)="onNearOfTop()" (nearOfBottom)="onNearOfBottom()" (pageChanged)="onPageChanged($event)"
12 [infiniteScrollDistance]="0.5" 12 class="videos" #videoElement
13 (scrolled)="onNearOfBottom()"
14 (scrolledUp)="onNearOfTop()"
15 > 13 >
16 <my-video-miniature 14 <div *ngFor="let videos of videoPages" class="videos-page">
17 class="ng-animate" 15 <my-video-miniature
18 *ngFor="let video of videos" [video]="video" [user]="user" 16 class="ng-animate"
19 > 17 *ngFor="let video of videos" [video]="video" [user]="user"
20 </my-video-miniature> 18 >
19 </my-video-miniature>
20 </div>
21 </div> 21 </div>
22</div> 22</div>
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
index a25fc532c..034d0d879 100644
--- a/client/src/app/shared/video/abstract-video-list.ts
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -1,6 +1,7 @@
1import { OnInit } from '@angular/core' 1import { ElementRef, OnInit, ViewChild, ViewChildren } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { isInMobileView, isInSmallView } from '@app/shared/misc/utils' 3import { isInMobileView } from '@app/shared/misc/utils'
4import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
4import { NotificationsService } from 'angular2-notifications' 5import { NotificationsService } from 'angular2-notifications'
5import { Observable } from 'rxjs/Observable' 6import { Observable } from 'rxjs/Observable'
6import { AuthService } from '../../core/auth' 7import { AuthService } from '../../core/auth'
@@ -9,30 +10,35 @@ import { SortField } from './sort-field.type'
9import { Video } from './video.model' 10import { Video } from './video.model'
10 11
11export abstract class AbstractVideoList implements OnInit { 12export abstract class AbstractVideoList implements OnInit {
13 private static LINES_PER_PAGE = 3
14
15 @ViewChild('videoElement') videosElement: ElementRef
16 @ViewChild(InfiniteScrollerDirective) infiniteScroller: InfiniteScrollerDirective
17
12 pagination: ComponentPagination = { 18 pagination: ComponentPagination = {
13 currentPage: 1, 19 currentPage: 1,
14 itemsPerPage: 25, 20 itemsPerPage: 10,
15 totalItems: null 21 totalItems: null
16 } 22 }
17 sort: SortField = '-createdAt' 23 sort: SortField = '-createdAt'
18 defaultSort: SortField = '-createdAt' 24 defaultSort: SortField = '-createdAt'
19 videos: Video[] = []
20 loadOnInit = true 25 loadOnInit = true
26 pageHeight: number
27 videoWidth = 215
28 videoHeight = 230
29 videoPages: Video[][]
21 30
22 protected abstract notificationsService: NotificationsService 31 protected abstract notificationsService: NotificationsService
23 protected abstract authService: AuthService 32 protected abstract authService: AuthService
24 protected abstract router: Router 33 protected abstract router: Router
25 protected abstract route: ActivatedRoute 34 protected abstract route: ActivatedRoute
26
27 protected abstract currentRoute: string 35 protected abstract currentRoute: string
28
29 abstract titlePage: string 36 abstract titlePage: string
30 37
31 protected otherParams = {} 38 protected loadedPages: { [ id: number ]: Video[] } = {}
32 39 protected otherRouteParams = {}
33 private loadedPages: { [ id: number ]: boolean } = {}
34 40
35 abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}> 41 abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}>
36 42
37 get user () { 43 get user () {
38 return this.authService.getUser() 44 return this.authService.getUser()
@@ -45,15 +51,26 @@ export abstract class AbstractVideoList implements OnInit {
45 51
46 if (isInMobileView()) { 52 if (isInMobileView()) {
47 this.pagination.itemsPerPage = 5 53 this.pagination.itemsPerPage = 5
54 this.videoWidth = -1
55 }
56
57 if (this.videoWidth !== -1) {
58 const videosWidth = this.videosElement.nativeElement.offsetWidth
59 this.pagination.itemsPerPage = Math.floor(videosWidth / this.videoWidth) * AbstractVideoList.LINES_PER_PAGE
60 }
61
62 // Video takes all the width
63 if (this.videoWidth === -1) {
64 this.pageHeight = this.pagination.itemsPerPage * this.videoHeight
65 } else {
66 this.pageHeight = this.videoHeight * AbstractVideoList.LINES_PER_PAGE
48 } 67 }
49 68
50 if (this.loadOnInit === true) this.loadMoreVideos('after') 69 if (this.loadOnInit === true) this.loadMoreVideos(this.pagination.currentPage)
51 } 70 }
52 71
53 onNearOfTop () { 72 onNearOfTop () {
54 if (this.pagination.currentPage > 1) { 73 this.previousPage()
55 this.previousPage()
56 }
57 } 74 }
58 75
59 onNearOfBottom () { 76 onNearOfBottom () {
@@ -62,16 +79,20 @@ export abstract class AbstractVideoList implements OnInit {
62 } 79 }
63 } 80 }
64 81
82 onPageChanged (page: number) {
83 this.pagination.currentPage = page
84 this.setNewRouteParams()
85 }
86
65 reloadVideos () { 87 reloadVideos () {
66 this.videos = []
67 this.loadedPages = {} 88 this.loadedPages = {}
68 this.loadMoreVideos('before') 89 this.loadMoreVideos(this.pagination.currentPage)
69 } 90 }
70 91
71 loadMoreVideos (where: 'before' | 'after') { 92 loadMoreVideos (page: number) {
72 if (this.loadedPages[this.pagination.currentPage] === true) return 93 if (this.loadedPages[page] !== undefined) return
73 94
74 const observable = this.getVideosObservable() 95 const observable = this.getVideosObservable(page)
75 96
76 observable.subscribe( 97 observable.subscribe(
77 ({ videos, totalVideos }) => { 98 ({ videos, totalVideos }) => {
@@ -82,13 +103,14 @@ export abstract class AbstractVideoList implements OnInit {
82 return this.reloadVideos() 103 return this.reloadVideos()
83 } 104 }
84 105
85 this.loadedPages[this.pagination.currentPage] = true 106 this.loadedPages[page] = videos
107 this.buildVideoPages()
86 this.pagination.totalItems = totalVideos 108 this.pagination.totalItems = totalVideos
87 109
88 if (where === 'before') { 110 // Initialize infinite scroller now we loaded the first page
89 this.videos = videos.concat(this.videos) 111 if (Object.keys(this.loadedPages).length === 1) {
90 } else { 112 // Wait elements creation
91 this.videos = this.videos.concat(videos) 113 setTimeout(() => this.infiniteScroller.initialize(), 500)
92 } 114 }
93 }, 115 },
94 error => this.notificationsService.error('Error', error.message) 116 error => this.notificationsService.error('Error', error.message)
@@ -107,17 +129,15 @@ export abstract class AbstractVideoList implements OnInit {
107 } 129 }
108 130
109 protected previousPage () { 131 protected previousPage () {
110 this.pagination.currentPage-- 132 const min = this.minPageLoaded()
111 133
112 this.setNewRouteParams() 134 if (min > 1) {
113 this.loadMoreVideos('before') 135 this.loadMoreVideos(min - 1)
136 }
114 } 137 }
115 138
116 protected nextPage () { 139 protected nextPage () {
117 this.pagination.currentPage++ 140 this.loadMoreVideos(this.maxPageLoaded() + 1)
118
119 this.setNewRouteParams()
120 this.loadMoreVideos('after')
121 } 141 }
122 142
123 protected buildRouteParams () { 143 protected buildRouteParams () {
@@ -127,7 +147,7 @@ export abstract class AbstractVideoList implements OnInit {
127 page: this.pagination.currentPage 147 page: this.pagination.currentPage
128 } 148 }
129 149
130 return Object.assign(params, this.otherParams) 150 return Object.assign(params, this.otherRouteParams)
131 } 151 }
132 152
133 protected loadRouteParams (routeParams: { [ key: string ]: any }) { 153 protected loadRouteParams (routeParams: { [ key: string ]: any }) {
@@ -144,4 +164,16 @@ export abstract class AbstractVideoList implements OnInit {
144 const routeParams = this.buildRouteParams() 164 const routeParams = this.buildRouteParams()
145 this.router.navigate([ this.currentRoute, routeParams ]) 165 this.router.navigate([ this.currentRoute, routeParams ])
146 } 166 }
167
168 protected buildVideoPages () {
169 this.videoPages = Object.values(this.loadedPages)
170 }
171
172 private minPageLoaded () {
173 return Math.min(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
174 }
175
176 private maxPageLoaded () {
177 return Math.max(...Object.keys(this.loadedPages).map(e => parseInt(e, 10)))
178 }
147} 179}
diff --git a/client/src/app/shared/video/infinite-scroller.directive.ts b/client/src/app/shared/video/infinite-scroller.directive.ts
new file mode 100644
index 000000000..43e014cbd
--- /dev/null
+++ b/client/src/app/shared/video/infinite-scroller.directive.ts
@@ -0,0 +1,77 @@
1import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import 'rxjs/add/operator/distinct'
3import 'rxjs/add/operator/startWith'
4import { fromEvent } from 'rxjs/observable/fromEvent'
5
6@Directive({
7 selector: '[myInfiniteScroller]'
8})
9export class InfiniteScrollerDirective implements OnInit {
10 private static PAGE_VIEW_TOP_MARGIN = 500
11
12 @Input() containerHeight: number
13 @Input() pageHeight: number
14 @Input() percentLimit = 70
15 @Input() autoLoading = false
16
17 @Output() nearOfBottom = new EventEmitter<void>()
18 @Output() nearOfTop = new EventEmitter<void>()
19 @Output() pageChanged = new EventEmitter<number>()
20
21 private decimalLimit = 0
22 private lastCurrentBottom = -1
23 private lastCurrentTop = 0
24
25 constructor () {
26 this.decimalLimit = this.percentLimit / 100
27 }
28
29 ngOnInit () {
30 if (this.autoLoading === true) return this.initialize()
31 }
32
33 initialize () {
34 const scrollObservable = fromEvent(window, 'scroll')
35 .startWith(true)
36 .map(() => ({ current: window.scrollY, maximumScroll: document.body.clientHeight - window.innerHeight }))
37
38 // Scroll Down
39 scrollObservable
40 // Check we scroll down
41 .filter(({ current }) => {
42 const res = this.lastCurrentBottom < current
43
44 this.lastCurrentBottom = current
45 return res
46 })
47 .filter(({ current, maximumScroll }) => maximumScroll <= 0 || (current / maximumScroll) > this.decimalLimit)
48 .debounceTime(200)
49 .distinct()
50 .subscribe(() => this.nearOfBottom.emit())
51
52 // Scroll up
53 scrollObservable
54 // Check we scroll up
55 .filter(({ current }) => {
56 const res = this.lastCurrentTop > current
57
58 this.lastCurrentTop = current
59 return res
60 })
61 .filter(({ current, maximumScroll }) => {
62 return current !== 0 && (1 - (current / maximumScroll)) > this.decimalLimit
63 })
64 .debounceTime(200)
65 .distinct()
66 .subscribe(() => this.nearOfTop.emit())
67
68 // Page change
69 scrollObservable
70 .debounceTime(500)
71 .distinct()
72 .map(({ current }) => Math.max(1, Math.round((current + InfiniteScrollerDirective.PAGE_VIEW_TOP_MARGIN) / this.pageHeight)))
73 .distinctUntilChanged()
74 .subscribe(res => this.pageChanged.emit(res))
75 }
76
77}
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html
index 80b200931..7f2e96e93 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.html
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html
@@ -15,10 +15,9 @@
15 15
16 <div 16 <div
17 class="comment-threads" 17 class="comment-threads"
18 infiniteScroll 18 myInfiniteScroller
19 [infiniteScrollUpDistance]="1.5" 19 [autoLoading]="true"
20 [infiniteScrollDistance]="0.5" 20 (nearOfBottom)="onNearOfBottom()"
21 (scrolled)="onNearOfBottom()"
22 > 21 >
23 <div *ngFor="let comment of comments"> 22 <div *ngFor="let comment of comments">
24 <my-video-comment 23 <my-video-comment
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
index 6025256de..7ca3bafb5 100644
--- a/client/src/app/videos/+video-watch/comment/video-comments.component.ts
+++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts
@@ -160,11 +160,8 @@ export class VideoCommentsComponent implements OnChanges {
160 this.threadComments = {} 160 this.threadComments = {}
161 this.threadLoading = {} 161 this.threadLoading = {}
162 this.inReplyToCommentId = undefined 162 this.inReplyToCommentId = undefined
163 this.componentPagination = { 163 this.componentPagination.currentPage = 1
164 currentPage: 1, 164 this.componentPagination.totalItems = null
165 itemsPerPage: 10,
166 totalItems: null
167 }
168 165
169 this.loadMoreComments() 166 this.loadMoreComments()
170 } 167 }
diff --git a/client/src/app/videos/video-list/video-recently-added.component.ts b/client/src/app/videos/video-list/video-recently-added.component.ts
index 3020b8c30..f150e38da 100644
--- a/client/src/app/videos/video-list/video-recently-added.component.ts
+++ b/client/src/app/videos/video-list/video-recently-added.component.ts
@@ -1,5 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils'
3import { NotificationsService } from 'angular2-notifications' 4import { NotificationsService } from 'angular2-notifications'
4import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
5import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -28,7 +29,9 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
28 super.ngOnInit() 29 super.ngOnInit()
29 } 30 }
30 31
31 getVideosObservable () { 32 getVideosObservable (page: number) {
32 return this.videoService.getVideos(this.pagination, this.sort) 33 const newPagination = immutableAssign(this.pagination, { currentPage: page })
34
35 return this.videoService.getVideos(newPagination, this.sort)
33 } 36 }
34} 37}
diff --git a/client/src/app/videos/video-list/video-search.component.ts b/client/src/app/videos/video-list/video-search.component.ts
index 67726afdc..241b97bc7 100644
--- a/client/src/app/videos/video-list/video-search.component.ts
+++ b/client/src/app/videos/video-list/video-search.component.ts
@@ -1,5 +1,6 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils'
3import { NotificationsService } from 'angular2-notifications' 4import { NotificationsService } from 'angular2-notifications'
4import { Subscription } from 'rxjs/Subscription' 5import { Subscription } from 'rxjs/Subscription'
5import { AuthService } from '../../core/auth' 6import { AuthService } from '../../core/auth'
@@ -16,7 +17,7 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
16 currentRoute = '/videos/search' 17 currentRoute = '/videos/search'
17 loadOnInit = false 18 loadOnInit = false
18 19
19 protected otherParams = { 20 protected otherRouteParams = {
20 search: '' 21 search: ''
21 } 22 }
22 private subActivatedRoute: Subscription 23 private subActivatedRoute: Subscription
@@ -35,9 +36,9 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
35 this.subActivatedRoute = this.route.queryParams.subscribe( 36 this.subActivatedRoute = this.route.queryParams.subscribe(
36 queryParams => { 37 queryParams => {
37 const querySearch = queryParams['search'] 38 const querySearch = queryParams['search']
38 if (!querySearch || this.otherParams.search === querySearch) return 39 if (!querySearch || this.otherRouteParams.search === querySearch) return
39 40
40 this.otherParams.search = querySearch 41 this.otherRouteParams.search = querySearch
41 this.reloadVideos() 42 this.reloadVideos()
42 }, 43 },
43 44
@@ -51,7 +52,8 @@ export class VideoSearchComponent extends AbstractVideoList implements OnInit, O
51 } 52 }
52 } 53 }
53 54
54 getVideosObservable () { 55 getVideosObservable (page: number) {
55 return this.videoService.searchVideos(this.otherParams.search, this.pagination, this.sort) 56 const newPagination = immutableAssign(this.pagination, { currentPage: page })
57 return this.videoService.searchVideos(this.otherRouteParams.search, newPagination, this.sort)
56 } 58 }
57} 59}
diff --git a/client/src/app/videos/video-list/video-trending.component.ts b/client/src/app/videos/video-list/video-trending.component.ts
index fc48086d6..a42457273 100644
--- a/client/src/app/videos/video-list/video-trending.component.ts
+++ b/client/src/app/videos/video-list/video-trending.component.ts
@@ -1,5 +1,6 @@
1import { Component, OnInit } from '@angular/core' 1import { Component, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { immutableAssign } from '@app/shared/misc/utils'
3import { NotificationsService } from 'angular2-notifications' 4import { NotificationsService } from 'angular2-notifications'
4import { AuthService } from '../../core/auth' 5import { AuthService } from '../../core/auth'
5import { AbstractVideoList } from '../../shared/video/abstract-video-list' 6import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@@ -28,7 +29,8 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit
28 super.ngOnInit() 29 super.ngOnInit()
29 } 30 }
30 31
31 getVideosObservable () { 32 getVideosObservable (page: number) {
32 return this.videoService.getVideos(this.pagination, this.sort) 33 const newPagination = immutableAssign(this.pagination, { currentPage: page })
34 return this.videoService.getVideos(newPagination, this.sort)
33 } 35 }
34} 36}
diff --git a/client/yarn.lock b/client/yarn.lock
index cd4492150..dd0a6bb9c 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -4526,10 +4526,6 @@ ngx-clipboard@9.0.1:
4526 dependencies: 4526 dependencies:
4527 ngx-window-token "0.0.4" 4527 ngx-window-token "0.0.4"
4528 4528
4529ngx-infinite-scroll@0.7.2:
4530 version "0.7.2"
4531 resolved "https://registry.yarnpkg.com/ngx-infinite-scroll/-/ngx-infinite-scroll-0.7.2.tgz#c1f0e7fba4731a55f15557dc6fce2721fd562420"
4532
4533ngx-pipes@^2.0.5: 4529ngx-pipes@^2.0.5:
4534 version "2.1.0" 4530 version "2.1.0"
4535 resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.1.0.tgz#969cbc78f1c7512b12cc050f441c2528fb3a05a0" 4531 resolved "https://registry.yarnpkg.com/ngx-pipes/-/ngx-pipes-2.1.0.tgz#969cbc78f1c7512b12cc050f441c2528fb3a05a0"