From c9258a11a227683521bd2cf54a458cda759db03a Mon Sep 17 00:00:00 2001 From: kontrollanten <6680299+kontrollanten@users.noreply.github.com> Date: Wed, 29 May 2024 11:43:34 +0200 Subject: [PATCH 1/5] chore: fix eslint for vs code --- .eslintrc.json => .eslintrc.cjs | 5 +++-- client/{.eslintrc.json => .eslintrc.cjs} | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) rename .eslintrc.json => .eslintrc.cjs (97%) rename client/{.eslintrc.json => .eslintrc.cjs} (97%) diff --git a/.eslintrc.json b/.eslintrc.cjs similarity index 97% rename from .eslintrc.json rename to .eslintrc.cjs index 17afd1b835c..af0b9c9e44f 100644 --- a/.eslintrc.json +++ b/.eslintrc.cjs @@ -1,4 +1,4 @@ -{ +module.exports = { "extends": "standard-with-typescript", "root": true, "rules": { @@ -146,6 +146,7 @@ "project": [ "./tsconfig.eslint.json" ], - "EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true + "EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true, + "tsconfigRootDir": __dirname } } diff --git a/client/.eslintrc.json b/client/.eslintrc.cjs similarity index 97% rename from client/.eslintrc.json rename to client/.eslintrc.cjs index d00e6e7abbc..5c2e1ae3255 100644 --- a/client/.eslintrc.json +++ b/client/.eslintrc.cjs @@ -1,4 +1,4 @@ -{ +module.exports = { "root": true, "ignorePatterns": [ "projects/**/*", @@ -15,10 +15,11 @@ "tsconfig.eslint.json" ], "EXPERIMENTAL_useSourceOfProjectReferenceRedirect": true, - "createDefaultProgram": false + "createDefaultProgram": false, + "tsconfigRootDir": __dirname, }, "extends": [ - "../.eslintrc.json", + "../.eslintrc.cjs", "plugin:@angular-eslint/recommended", "plugin:@angular-eslint/template/process-inline-templates" ], From 542a14719c01c6b2d4e7df9b9d38544f4d0cfc77 Mon Sep 17 00:00:00 2001 From: kontrollanten <6680299+kontrollanten@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:53:13 +0200 Subject: [PATCH 2/5] feat(client/videos-list): infinite scroll SEO Add a "Load more" button in the bottom to help search engine bots to navigate to the next page. In order to debug this functionality, add ?finiteScroll=true to the URL. --- .../videos-list.component.html | 6 +++ .../videos-list.component.ts | 48 +++++++++++++------ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.html b/client/src/app/shared/shared-video-miniature/videos-list.component.html index e6f735e7c62..5b9bc786286 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.html +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.html @@ -81,4 +81,10 @@

+ +
+ + Load more + +
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts index cbd555a7279..6781537476e 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts @@ -1,6 +1,6 @@ import { NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common' import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, booleanAttribute } from '@angular/core' -import { ActivatedRoute, RouterLink, RouterLinkActive } from '@angular/router' +import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router' import { AuthService, ComponentPaginationLight, @@ -18,7 +18,7 @@ import { ResultList, UserRight, VideoSortField } from '@peertube/peertube-models import { logger } from '@root-helpers/logger' import debug from 'debug' import { Observable, Subject, Subscription, forkJoin, fromEvent, of } from 'rxjs' -import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' +import { concatMap, debounceTime, filter, map, switchMap } from 'rxjs/operators' import { InfiniteScrollerDirective } from '../shared-main/angular/infinite-scroller.directive' import { ButtonComponent } from '../shared-main/buttons/button.component' import { FeedComponent } from '../shared-main/feeds/feed.component' @@ -67,7 +67,8 @@ enum GroupDate { VideoFiltersHeaderComponent, InfiniteScrollerDirective, VideoMiniatureComponent, - GlobalIconComponent + GlobalIconComponent, + RouterLink ] }) export class VideosListComponent implements OnInit, OnChanges, OnDestroy { @@ -97,7 +98,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { @Input() displayOptions: MiniatureDisplayOptions - @Input({ transform: booleanAttribute }) disabled = false + @Input({ transform: booleanAttribute }) disabled: boolean @Output() filtersChanged = new EventEmitter() @Output() videosLoaded = new EventEmitter() @@ -113,6 +114,13 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { userMiniature: User + pagination: ComponentPaginationLight = { + currentPage: 1, + itemsPerPage: 25 + } + + lastQueryLength: number + private defaultDisplayOptions: MiniatureDisplayOptions = { date: true, views: true, @@ -127,16 +135,9 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { private userSub: Subscription private resizeSub: Subscription - private pagination: ComponentPaginationLight = { - currentPage: 1, - itemsPerPage: 25 - } - private groupedDateLabels: { [id in GroupDate]: string } private groupedDates: { [id: number]: GroupDate } = {} - private lastQueryLength: number - private videoRequests = new Subject<{ reset: boolean obsVideos: Observable, 'data'>> @@ -152,13 +153,32 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { private route: ActivatedRoute, private screenService: ScreenService, private peertubeRouter: PeerTubeRouterService, - private serverService: ServerService + private serverService: ServerService, + public router: Router ) { } ngOnInit () { this.subscribeToVideoRequests() + this.disabled = this.disabled || this.route.snapshot.queryParams.finiteScroll === 'true' + + this.router.events + .pipe( + filter(event => event instanceof NavigationEnd) + ) + .subscribe((event: NavigationEnd) => { + const search = event.url.split('?')[1] + const params = new URLSearchParams(search) + const newPage = +params.get('page') || this.pagination.currentPage + + if (newPage === this.pagination.currentPage) { + return + } + + this.pagination.currentPage = newPage + this.loadMoreVideos(true) + }) const hiddenFilters = this.hideScopeFilter ? [ 'scope' ] @@ -292,7 +312,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { } reloadVideos () { - this.pagination.currentPage = 1 + this.pagination.currentPage = +this.route.snapshot.queryParams.page || 1 this.loadMoreVideos(true) } @@ -473,7 +493,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { private subscribeToVideoRequests () { this.videoRequests - .pipe( + .pipe( concatMap(({ reset, obsHighlightedLives, obsVideos }) => { return forkJoin([ obsHighlightedLives, obsVideos ]) .pipe( From 77ea750cc4b5c55e5c3aac970e40c83f39a0bc48 Mon Sep 17 00:00:00 2001 From: kontrollanten <6680299+kontrollanten@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:11:54 +0200 Subject: [PATCH 3/5] fix(client/user-notifications): load last page hasMoreItems has to be called before bumping the current page in order to work accordingly. --- .../standalone-notifications/user-notifications.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/app/shared/standalone-notifications/user-notifications.component.ts b/client/src/app/shared/standalone-notifications/user-notifications.component.ts index 53b996a776b..72723d1a497 100644 --- a/client/src/app/shared/standalone-notifications/user-notifications.component.ts +++ b/client/src/app/shared/standalone-notifications/user-notifications.component.ts @@ -80,11 +80,11 @@ export class UserNotificationsComponent implements OnInit { onNearOfBottom () { if (this.infiniteScroll === false) return - this.componentPagination.currentPage++ - if (hasMoreItems(this.componentPagination)) { this.loadNotifications() } + + this.componentPagination.currentPage++ } markAsRead (notification: UserNotification) { From a85a48f754b347b1551539d22e565d1d2776eb8a Mon Sep 17 00:00:00 2001 From: kontrollanten <6680299+kontrollanten@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:12:51 +0200 Subject: [PATCH 4/5] feat(client/infinite-scroll): SEO friendly closes #6332 --- .../account-video-channels.component.html | 11 ++- .../account-video-channels.component.ts | 62 ++++++++------- .../plugin-list-installed.component.html | 11 ++- .../plugin-list-installed.component.ts | 34 +++++---- .../plugin-search.component.html | 11 ++- .../plugin-search/plugin-search.component.ts | 43 ++++++----- .../my-video-channels.component.html | 13 +++- .../my-video-channels.component.ts | 26 +++++-- .../my-follows/my-followers.component.html | 13 +++- .../my-follows/my-followers.component.ts | 21 ++++-- .../my-subscriptions.component.html | 13 +++- .../my-follows/my-subscriptions.component.ts | 24 +++--- .../my-video-playlist-elements.component.html | 14 +++- .../my-video-playlist-elements.component.scss | 4 +- .../my-video-playlist-elements.component.ts | 23 ++++-- .../my-video-playlists.component.html | 13 +++- .../my-video-playlists.component.ts | 22 ++++-- client/src/app/+search/search.component.html | 11 ++- client/src/app/+search/search.component.ts | 54 ++++++++----- .../video-channel-playlists.component.html | 11 ++- .../video-channel-playlists.component.ts | 24 +++--- .../comment/video-comments.component.html | 11 ++- .../comment/video-comments.component.ts | 23 ++++-- .../player-widget.component.scss | 1 + .../video-watch-playlist.component.html | 16 ++-- .../video-watch-playlist.component.scss | 4 + .../video-watch-playlist.component.ts | 22 +++++- .../overview/video-overview.component.html | 10 ++- .../overview/video-overview.component.ts | 22 +++--- .../angular/infinite-scroller.component.html | 7 ++ .../angular/infinite-scroller.component.scss | 4 + ...tive.ts => infinite-scroller.component.ts} | 75 ++++++++++++++----- .../videos-list.component.html | 21 +++--- .../videos-list.component.ts | 50 +++++-------- .../videos-selection.component.html | 13 +++- .../videos-selection.component.ts | 26 ++++--- .../user-notifications.component.html | 11 ++- .../user-notifications.component.ts | 17 +++-- 38 files changed, 525 insertions(+), 266 deletions(-) create mode 100644 client/src/app/shared/shared-main/angular/infinite-scroller.component.html create mode 100644 client/src/app/shared/shared-main/angular/infinite-scroller.component.scss rename client/src/app/shared/shared-main/angular/{infinite-scroller.directive.ts => infinite-scroller.component.ts} (58%) diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index e3b4997737f..a80ac5e4779 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html @@ -4,7 +4,14 @@

Video channels

This account does not have channels.
-
+
@@ -52,5 +59,5 @@

- + diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts index 34e03d6fd59..31d91e68265 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts @@ -1,6 +1,6 @@ -import { from, Subject, Subscription } from 'rxjs' +import { from, Subject } from 'rxjs' import { concatMap, map, switchMap, tap } from 'rxjs/operators' -import { Component, OnDestroy, OnInit } from '@angular/core' +import { Component, OnInit } from '@angular/core' import { ComponentPagination, hasMoreItems, MarkdownService, User, UserService } from '@app/core' import { SimpleMemoize } from '@app/helpers' import { NSFWPolicyType, VideoSortField } from '@peertube/peertube-models' @@ -8,7 +8,7 @@ import { MiniatureDisplayOptions, VideoMiniatureComponent } from '../../shared/s import { SubscribeButtonComponent } from '../../shared/shared-user-subscription/subscribe-button.component' import { RouterLink } from '@angular/router' import { ActorAvatarComponent } from '../../shared/shared-actor-image/actor-avatar.component' -import { InfiniteScrollerDirective } from '../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/angular/infinite-scroller.component' import { NgIf, NgFor } from '@angular/common' import { AccountService } from '@app/shared/shared-main/account/account.service' import { VideoChannelService } from '@app/shared/shared-main/video-channel/video-channel.service' @@ -22,14 +22,17 @@ import { Video } from '@app/shared/shared-main/video/video.model' templateUrl: './account-video-channels.component.html', styleUrls: [ './account-video-channels.component.scss' ], standalone: true, - imports: [ NgIf, InfiniteScrollerDirective, NgFor, ActorAvatarComponent, RouterLink, SubscribeButtonComponent, VideoMiniatureComponent ] + imports: [ NgIf, InfiniteScrollerComponent, NgFor, ActorAvatarComponent, RouterLink, SubscribeButtonComponent, VideoMiniatureComponent ] }) -export class AccountVideoChannelsComponent implements OnInit, OnDestroy { +export class AccountVideoChannelsComponent implements OnInit { account: Account videoChannels: VideoChannel[] = [] videos: { [id: number]: { total: number, videos: Video[] } } = {} + hasMoreVideoChannels = true + isLoading = true + channelsDescriptionHTML: { [ id: number ]: string } = {} channelPagination: ComponentPagination = { @@ -60,8 +63,6 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { blacklistInfo: false } - private accountSub: Subscription - constructor ( private accountService: AccountService, private videoChannelService: VideoChannelService, @@ -71,15 +72,6 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { ) { } ngOnInit () { - // Parent get the account for us - this.accountSub = this.accountService.accountLoaded - .subscribe(account => { - this.account = account - this.videoChannels = [] - - this.loadMoreChannels() - }) - this.userService.getAnonymousOrLoggedUser() .subscribe(user => { this.userMiniature = user @@ -88,18 +80,22 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { }) } - ngOnDestroy () { - if (this.accountSub) this.accountSub.unsubscribe() - } - - loadMoreChannels () { - const options = { - account: this.account, - componentPagination: this.channelPagination, - sort: '-updatedAt' - } + loadMoreChannels (reset = false) { + let hasDoneReset = false + this.isLoading = true - this.videoChannelService.listAccountVideoChannels(options) + // Parent get the account for us + this.accountService.accountLoaded + .pipe( + tap(account => { + this.account = account + }), + switchMap(() => this.videoChannelService.listAccountVideoChannels({ + account: this.account, + componentPagination: this.channelPagination, + sort: '-updatedAt' + })) + ) .pipe( tap(res => { this.channelPagination.totalItems = res.total @@ -118,13 +114,21 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { }) ) .subscribe(async ({ videoChannel, videos, total }) => { + this.isLoading = false this.channelsDescriptionHTML[videoChannel.id] = await this.markdown.textMarkdownToHTML({ markdown: videoChannel.description, withEmoji: true, withHtml: true }) + if (reset && !hasDoneReset) { + hasDoneReset = true + this.videoChannels = [] + } + this.videoChannels.push(videoChannel) + this.hasMoreVideoChannels = (this.channelPagination.currentPage * this.channelPagination.itemsPerPage) < + this.channelPagination.totalItems this.videos[videoChannel.id] = { videos, total } @@ -150,6 +154,10 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy { return this.channelsDescriptionHTML[videoChannel.id] } + onPageChange () { + this.loadMoreChannels(true) + } + onNearOfBottom () { if (!hasMoreItems(this.channelPagination)) return diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html index e80855a023b..6f16435b5f2 100644 --- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html +++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.html @@ -4,7 +4,14 @@ {{ getNoResultMessage() }} -
+
@@ -27,4 +34,4 @@
-
+ diff --git a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts index 61407a7b3ec..dac3af36174 100644 --- a/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts +++ b/client/src/app/+admin/plugins/plugin-list-installed/plugin-list-installed.component.ts @@ -1,4 +1,4 @@ -import { Subject } from 'rxjs' +import { distinct, filter, ReplaySubject } from 'rxjs' import { Component, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' @@ -10,7 +10,7 @@ import { DeleteButtonComponent } from '../../../shared/shared-main/buttons/delet import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component' import { EditButtonComponent } from '../../../shared/shared-main/buttons/edit-button.component' import { PluginCardComponent } from '../shared/plugin-card.component' -import { InfiniteScrollerDirective } from '../../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../../shared/shared-main/angular/infinite-scroller.component' import { NgIf, NgFor } from '@angular/common' import { PluginNavigationComponent } from '../shared/plugin-navigation.component' @@ -22,7 +22,7 @@ import { PluginNavigationComponent } from '../shared/plugin-navigation.component imports: [ PluginNavigationComponent, NgIf, - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgFor, PluginCardComponent, EditButtonComponent, @@ -39,12 +39,14 @@ export class PluginListInstalledComponent implements OnInit { totalItems: null } sort = 'name' + hasMoreResults = true + isLoading = true plugins: PeerTubePlugin[] = [] updating: { [name: string]: boolean } = {} uninstalling: { [name: string]: boolean } = {} - onDataSubject = new Subject() + private hasInitialized = new ReplaySubject() constructor ( private pluginService: PluginService, @@ -68,31 +70,35 @@ export class PluginListInstalledComponent implements OnInit { this.pluginType = parseInt(query['pluginType'], 10) as PluginType_Type - this.reloadPlugins() + this.hasInitialized.next(true) }) } - reloadPlugins () { - this.pagination.currentPage = 1 - this.plugins = [] - - this.loadMorePlugins() - } - - loadMorePlugins () { + loadMorePlugins (reset = false) { + this.isLoading = true this.pluginApiService.getPlugins(this.pluginType, this.pagination, this.sort) .subscribe({ next: res => { + if (reset) this.plugins = [] this.plugins = this.plugins.concat(res.data) this.pagination.totalItems = res.total + this.hasMoreResults = (this.pagination.itemsPerPage * this.pagination.currentPage) < this.pagination.totalItems - this.onDataSubject.next(res.data) + this.isLoading = false }, error: err => this.notifier.error(err.message) }) } + onPageChange () { + this.hasInitialized.pipe( + distinct(), + filter(val => val) + ) + .subscribe(() => this.loadMorePlugins(true)) + } + onNearOfBottom () { if (!hasMoreItems(this.pagination)) return diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html index 189002eae01..ad3b784f52f 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.html @@ -28,7 +28,14 @@ No results. -
+
@@ -58,4 +65,4 @@ -
+
diff --git a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts index fbb59628b7d..70914a1bedd 100644 --- a/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts +++ b/client/src/app/+admin/plugins/plugin-search/plugin-search.component.ts @@ -1,5 +1,5 @@ -import { Subject } from 'rxjs' -import { debounceTime, distinctUntilChanged } from 'rxjs/operators' +import { ReplaySubject, Subject } from 'rxjs' +import { debounceTime, distinct, distinctUntilChanged, filter } from 'rxjs/operators' import { Component, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service' @@ -9,7 +9,7 @@ import { logger } from '@root-helpers/logger' import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component' import { EditButtonComponent } from '../../../shared/shared-main/buttons/edit-button.component' import { PluginCardComponent } from '../shared/plugin-card.component' -import { InfiniteScrollerDirective } from '../../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../../shared/shared-main/angular/infinite-scroller.component' import { AutofocusDirective } from '../../../shared/shared-main/angular/autofocus.directive' import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component' import { NgIf, NgFor } from '@angular/common' @@ -25,7 +25,7 @@ import { PluginNavigationComponent } from '../shared/plugin-navigation.component NgIf, GlobalIconComponent, AutofocusDirective, - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgFor, PluginCardComponent, EditButtonComponent, @@ -41,17 +41,17 @@ export class PluginSearchComponent implements OnInit { totalItems: null } sort = '-trending' + hasMoreResults = true search = '' - isSearching = false + isSearching = true plugins: PeerTubePluginIndex[] = [] installing: { [name: string]: boolean } = {} pluginInstalled = false - onDataSubject = new Subject() - private searchSubject = new Subject() + private hasInitialized = new ReplaySubject() constructor ( private pluginService: PluginService, @@ -73,10 +73,15 @@ export class PluginSearchComponent implements OnInit { this.route.queryParams.subscribe(query => { if (!query['pluginType']) return + const oldSearch = this.search this.pluginType = parseInt(query['pluginType'], 10) as PluginType_Type this.search = query['search'] || '' + this.hasInitialized.next(true) - this.reloadPlugins() + if (oldSearch !== this.search) { + this.pagination.currentPage = 1 + this.onPageChange() + } }) this.searchSubject.asObservable() @@ -93,14 +98,7 @@ export class PluginSearchComponent implements OnInit { this.searchSubject.next(target.value) } - reloadPlugins () { - this.pagination.currentPage = 1 - this.plugins = [] - - this.loadMorePlugins() - } - - loadMorePlugins () { + loadMorePlugins (reset = false) { this.isSearching = true this.pluginApiService.searchAvailablePlugins(this.pluginType, this.pagination, this.sort, this.search) @@ -108,10 +106,11 @@ export class PluginSearchComponent implements OnInit { next: res => { this.isSearching = false + if (reset) this.plugins = [] + this.plugins = this.plugins.concat(res.data) this.pagination.totalItems = res.total - - this.onDataSubject.next(res.data) + this.hasMoreResults = (this.pagination.itemsPerPage * this.pagination.currentPage) < this.pagination.totalItems }, error: err => { @@ -123,6 +122,14 @@ export class PluginSearchComponent implements OnInit { }) } + onPageChange () { + this.hasInitialized.pipe( + distinct(), + filter(val => val) + ) + .subscribe(() => this.loadMorePlugins(true)) + } + onNearOfBottom () { if (!hasMoreItems(this.pagination)) return diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html index 58a43272e9f..e9c3151ccfe 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html @@ -16,7 +16,7 @@

-

+ diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts index a0060864e2b..ddead5ed070 100644 --- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts +++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.ts @@ -13,7 +13,7 @@ import { ActorAvatarComponent } from '../../shared/shared-actor-image/actor-avat import { AdvancedInputFilterComponent } from '../../shared/shared-forms/advanced-input-filter.component' import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component' import { DeferLoadingDirective } from '../../shared/shared-main/angular/defer-loading.directive' -import { InfiniteScrollerDirective } from '../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/angular/infinite-scroller.component' import { NumberFormatterPipe } from '../../shared/shared-main/angular/number-formatter.pipe' import { DeleteButtonComponent } from '../../shared/shared-main/buttons/delete-button.component' import { EditButtonComponent } from '../../shared/shared-main/buttons/edit-button.component' @@ -29,7 +29,7 @@ import { ChannelsSetupMessageComponent } from '../../shared/shared-main/misc/cha RouterLink, ChannelsSetupMessageComponent, AdvancedInputFilterComponent, - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgFor, ActorAvatarComponent, EditButtonComponent, @@ -41,6 +41,8 @@ import { ChannelsSetupMessageComponent } from '../../shared/shared-main/misc/cha }) export class MyVideoChannelsComponent { videoChannels: VideoChannel[] = [] + hasMoreResults = true + isLoading = true videoChannelsChartData: ChartData[] @@ -74,9 +76,7 @@ export class MyVideoChannelsComponent { this.search = search this.pagination.currentPage = 1 - this.videoChannels = [] - - this.loadMoreVideoChannels() + this.onPageChange() } async deleteVideoChannel (videoChannel: VideoChannel) { @@ -108,17 +108,24 @@ export class MyVideoChannelsComponent { }) } + onPageChange () { + this.loadMoreVideoChannels(true) + } + onNearOfBottom () { - if (!hasMoreItems(this.pagination)) return + if (!hasMoreItems(this.pagination)) { + return + } this.pagination.currentPage += 1 this.loadMoreVideoChannels() } - private loadMoreVideoChannels () { - if (this.pagesDone.has(this.pagination.currentPage)) return + private loadMoreVideoChannels (reset = false) { + if (!reset && this.pagesDone.has(this.pagination.currentPage)) return this.pagesDone.add(this.pagination.currentPage) + this.isLoading = true return this.authService.userInformationLoaded .pipe( @@ -133,6 +140,9 @@ export class MyVideoChannelsComponent { switchMap(options => this.videoChannelService.listAccountVideoChannels(options)) ) .subscribe(res => { + this.isLoading = false + this.hasMoreResults = res.data.length === this.pagination.itemsPerPage + if (reset) this.videoChannels = [] this.videoChannels = this.videoChannels.concat(res.data) this.pagination.totalItems = res.total diff --git a/client/src/app/+my-library/my-follows/my-followers.component.html b/client/src/app/+my-library/my-follows/my-followers.component.html index 743187edb75..4a220140e93 100644 --- a/client/src/app/+my-library/my-follows/my-followers.component.html +++ b/client/src/app/+my-library/my-follows/my-followers.component.html @@ -7,12 +7,19 @@

- +
No follower found.
-
+
@@ -28,4 +35,4 @@

- + diff --git a/client/src/app/+my-library/my-follows/my-followers.component.ts b/client/src/app/+my-library/my-follows/my-followers.component.ts index 7b1a8edc891..8355f6a745c 100644 --- a/client/src/app/+my-library/my-follows/my-followers.component.ts +++ b/client/src/app/+my-library/my-follows/my-followers.component.ts @@ -4,20 +4,21 @@ import { ActivatedRoute } from '@angular/router' import { AuthService, ComponentPagination, Notifier } from '@app/core' import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service' import { ActorFollow } from '@peertube/peertube-models' -import { Subject } from 'rxjs' import { ActorAvatarComponent } from '../../shared/shared-actor-image/actor-avatar.component' import { AdvancedInputFilter, AdvancedInputFilterComponent } from '../../shared/shared-forms/advanced-input-filter.component' import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component' -import { InfiniteScrollerDirective } from '../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/angular/infinite-scroller.component' @Component({ templateUrl: './my-followers.component.html', styleUrls: [ './my-followers.component.scss' ], standalone: true, - imports: [ GlobalIconComponent, NgIf, AdvancedInputFilterComponent, InfiniteScrollerDirective, NgFor, ActorAvatarComponent ] + imports: [ GlobalIconComponent, NgIf, AdvancedInputFilterComponent, InfiniteScrollerComponent, NgFor, ActorAvatarComponent ] }) export class MyFollowersComponent implements OnInit { follows: ActorFollow[] = [] + hasMoreResults = true + isLoading = true pagination: ComponentPagination = { currentPage: 1, @@ -25,7 +26,6 @@ export class MyFollowersComponent implements OnInit { totalItems: null } - onDataSubject = new Subject() search: string inputFilters: AdvancedInputFilter[] @@ -57,9 +57,15 @@ export class MyFollowersComponent implements OnInit { ] } + onPageChange () { + this.loadFollowers(false) + } + onNearOfBottom () { // Last page - if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return + if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) { + return + } this.pagination.currentPage += 1 this.loadFollowers() @@ -75,6 +81,8 @@ export class MyFollowersComponent implements OnInit { } private loadFollowers (more = true) { + this.isLoading = true + this.userSubscriptionService.listFollowers({ pagination: this.pagination, nameWithHost: this.getUsername(), @@ -85,8 +93,9 @@ export class MyFollowersComponent implements OnInit { ? this.follows.concat(res.data) : res.data this.pagination.totalItems = res.total + this.hasMoreResults = (this.pagination.itemsPerPage * this.pagination.currentPage) < this.pagination.totalItems - this.onDataSubject.next(res.data) + this.isLoading = false }, error: err => this.notifier.error(err.message) diff --git a/client/src/app/+my-library/my-follows/my-subscriptions.component.html b/client/src/app/+my-library/my-follows/my-subscriptions.component.html index f928d8caba7..23527187fd6 100644 --- a/client/src/app/+my-library/my-follows/my-subscriptions.component.html +++ b/client/src/app/+my-library/my-follows/my-subscriptions.component.html @@ -7,12 +7,19 @@

- +
You don't have any subscription yet.
-
+
@@ -33,4 +40,4 @@

-
+ diff --git a/client/src/app/+my-library/my-follows/my-subscriptions.component.ts b/client/src/app/+my-library/my-follows/my-subscriptions.component.ts index 0cf70d6f911..fe4ac039d32 100644 --- a/client/src/app/+my-library/my-follows/my-subscriptions.component.ts +++ b/client/src/app/+my-library/my-follows/my-subscriptions.component.ts @@ -1,10 +1,9 @@ -import { Subject } from 'rxjs' import { Component } from '@angular/core' import { ComponentPagination, Notifier } from '@app/core' import { SubscribeButtonComponent } from '../../shared/shared-user-subscription/subscribe-button.component' import { RouterLink } from '@angular/router' import { ActorAvatarComponent } from '../../shared/shared-actor-image/actor-avatar.component' -import { InfiniteScrollerDirective } from '../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/angular/infinite-scroller.component' import { AdvancedInputFilterComponent } from '../../shared/shared-forms/advanced-input-filter.component' import { NgIf, NgFor } from '@angular/common' import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component' @@ -19,7 +18,7 @@ import { UserSubscriptionService } from '@app/shared/shared-user-subscription/us GlobalIconComponent, NgIf, AdvancedInputFilterComponent, - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgFor, ActorAvatarComponent, RouterLink, @@ -28,25 +27,31 @@ import { UserSubscriptionService } from '@app/shared/shared-user-subscription/us }) export class MySubscriptionsComponent { videoChannels: VideoChannel[] = [] + hasMoreResults = true + isLoading = true pagination: ComponentPagination = { currentPage: 1, itemsPerPage: 10, totalItems: null } - onDataSubject = new Subject() - search: string constructor ( private userSubscriptionService: UserSubscriptionService, private notifier: Notifier - ) {} + ) { } + + onPageChange () { + this.loadSubscriptions() + } onNearOfBottom () { // Last page - if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return + if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) { + return + } this.pagination.currentPage += 1 this.loadSubscriptions() @@ -58,6 +63,7 @@ export class MySubscriptionsComponent { } private loadSubscriptions (more = true) { + this.isLoading = true this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.search }) .subscribe({ next: res => { @@ -65,8 +71,8 @@ export class MySubscriptionsComponent { ? this.videoChannels.concat(res.data) : res.data this.pagination.totalItems = res.total - - this.onDataSubject.next(res.data) + this.hasMoreResults = (this.pagination.itemsPerPage * this.pagination.currentPage) < this.pagination.totalItems + this.isLoading = false }, error: err => this.notifier.error(err.message) diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html index df4687ce97a..532bcb92182 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.html @@ -33,9 +33,15 @@ -
-
+ diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss index 295f2015d70..4ac317d1ddf 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.scss @@ -56,8 +56,8 @@ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } -.video:last-child { - border: 0; +.video:not(:has(+ .video)) { + margin-bottom: 20px; } .videos.cdk-drop-list-dragging .video:not(.cdk-drag-placeholder) { diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts index 27b5158d361..f7bae203335 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlist-elements.component.ts @@ -1,11 +1,11 @@ -import { Subject, Subscription } from 'rxjs' +import { Subscription } from 'rxjs' import { CdkDragDrop, CdkDropList, CdkDrag } from '@angular/cdk/drag-drop' import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { ComponentPagination, ConfirmService, HooksService, Notifier, ScreenService } from '@app/core' import { VideoPlaylistType } from '@peertube/peertube-models' import { VideoPlaylistElementMiniatureComponent } from '../../shared/shared-video-playlist/video-playlist-element-miniature.component' -import { InfiniteScrollerDirective } from '../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/angular/infinite-scroller.component' import { ActionDropdownComponent, DropdownAction } from '../../shared/shared-main/buttons/action-dropdown.component' import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component' import { VideoPlaylistMiniatureComponent } from '../../shared/shared-video-playlist/video-playlist-miniature.component' @@ -24,7 +24,7 @@ import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-pl VideoPlaylistMiniatureComponent, GlobalIconComponent, ActionDropdownComponent, - InfiniteScrollerDirective, + InfiniteScrollerComponent, CdkDropList, NgFor, CdkDrag, @@ -35,6 +35,8 @@ import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-pl export class MyVideoPlaylistElementsComponent implements OnInit, OnDestroy { @ViewChild('videoShareModal') videoShareModal: VideoShareComponent + hasMoreResults = true + isLoading = true playlistElements: VideoPlaylistElement[] = [] playlist: VideoPlaylist @@ -46,8 +48,6 @@ export class MyVideoPlaylistElementsComponent implements OnInit, OnDestroy { totalItems: null } - onDataSubject = new Subject() - private videoPlaylistId: string | number private paramsSub: Subscription @@ -122,6 +122,10 @@ export class MyVideoPlaylistElementsComponent implements OnInit, OnDestroy { this.reorderClientPositions(oldFirst) } + onPageChange () { + this.loadElements(true) + } + onNearOfBottom () { // Last page if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return @@ -175,7 +179,9 @@ export class MyVideoPlaylistElementsComponent implements OnInit, OnDestroy { return null } - private loadElements () { + private loadElements (reset = false) { + this.isLoading = true + this.hooks.wrapObsFun( this.videoPlaylistService.getPlaylistVideos.bind(this.videoPlaylistService), { videoPlaylistId: this.videoPlaylistId, componentPagination: this.pagination }, @@ -184,10 +190,13 @@ export class MyVideoPlaylistElementsComponent implements OnInit, OnDestroy { 'filter:api.my-library.video-playlist-elements.list.result' ) .subscribe(({ total, data }) => { + if (reset) this.playlistElements = [] + this.playlistElements = this.playlistElements.concat(data) this.pagination.totalItems = total + this.hasMoreResults = (this.pagination.itemsPerPage * this.pagination.currentPage) < this.pagination.totalItems - this.onDataSubject.next(data) + this.isLoading = false }) } diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html index 79b90807b8f..91c26cb831d 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.html @@ -7,7 +7,7 @@

-
+
- + diff --git a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts index 109cab80052..4a651aef78b 100644 --- a/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts +++ b/client/src/app/+my-library/my-video-playlists/my-video-playlists.component.ts @@ -1,4 +1,3 @@ -import { Subject } from 'rxjs' import { mergeMap } from 'rxjs/operators' import { Component } from '@angular/core' import { AuthService, ComponentPagination, ConfirmService, Notifier } from '@app/core' @@ -6,7 +5,7 @@ import { VideoPlaylistType } from '@peertube/peertube-models' import { EditButtonComponent } from '../../shared/shared-main/buttons/edit-button.component' import { DeleteButtonComponent } from '../../shared/shared-main/buttons/delete-button.component' import { VideoPlaylistMiniatureComponent } from '../../shared/shared-video-playlist/video-playlist-miniature.component' -import { InfiniteScrollerDirective } from '../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/angular/infinite-scroller.component' import { RouterLink } from '@angular/router' import { AdvancedInputFilterComponent } from '../../shared/shared-forms/advanced-input-filter.component' import { ChannelsSetupMessageComponent } from '../../shared/shared-main/misc/channels-setup-message.component' @@ -25,7 +24,7 @@ import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-pl ChannelsSetupMessageComponent, AdvancedInputFilterComponent, RouterLink, - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgFor, VideoPlaylistMiniatureComponent, DeleteButtonComponent, @@ -34,6 +33,8 @@ import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-pl }) export class MyVideoPlaylistsComponent { videoPlaylists: VideoPlaylist[] = [] + hasMoreResults = true + isLoading = true pagination: ComponentPagination = { currentPage: 1, @@ -41,8 +42,6 @@ export class MyVideoPlaylistsComponent { totalItems: null } - onDataSubject = new Subject() - search: string constructor ( @@ -76,9 +75,15 @@ export class MyVideoPlaylistsComponent { return playlist.type.id === VideoPlaylistType.REGULAR } + onPageChange () { + this.loadVideoPlaylists(true) + } + onNearOfBottom () { // Last page - if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return + if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) { + return + } this.pagination.currentPage += 1 this.loadVideoPlaylists() @@ -90,6 +95,8 @@ export class MyVideoPlaylistsComponent { } private loadVideoPlaylists (reset = false) { + this.isLoading = true + this.authService.userInformationLoaded .pipe(mergeMap(() => { const user = this.authService.getUser() @@ -100,8 +107,9 @@ export class MyVideoPlaylistsComponent { this.videoPlaylists = this.videoPlaylists.concat(res.data) this.pagination.totalItems = res.total + this.hasMoreResults = (this.pagination.itemsPerPage * this.pagination.currentPage) < this.pagination.totalItems - this.onDataSubject.next(res.data) + this.isLoading = false }) } } diff --git a/client/src/app/+search/search.component.html b/client/src/app/+search/search.component.html index 7e154e953dc..312ecc03651 100644 --- a/client/src/app/+search/search.component.html +++ b/client/src/app/+search/search.component.html @@ -1,4 +1,11 @@ -
+
@@ -77,4 +84,4 @@
-
+ diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts index ee9324cae0b..d3f0d7faee2 100644 --- a/client/src/app/+search/search.component.ts +++ b/client/src/app/+search/search.component.ts @@ -1,11 +1,11 @@ -import { forkJoin, Subject, Subscription } from 'rxjs' +import { forkJoin, Subscription } from 'rxjs' import { LinkType } from 'src/types/link.type' import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router, RouterLink } from '@angular/router' import { AuthService, HooksService, MetaService, Notifier, ServerService, User, UserService } from '@app/core' import { immutableAssign, SimpleMemoize } from '@app/helpers' import { validateHost } from '@app/shared/form-validators/host-validators' -import { HTMLServerConfig, SearchTargetType } from '@peertube/peertube-models' +import { HTMLServerConfig, ResultList, SearchTargetType } from '@peertube/peertube-models' import { NumberFormatterPipe } from '../shared/shared-main/angular/number-formatter.pipe' import { VideoPlaylistMiniatureComponent } from '../shared/shared-video-playlist/video-playlist-miniature.component' import { MiniatureDisplayOptions, VideoMiniatureComponent } from '../shared/shared-video-miniature/video-miniature.component' @@ -14,7 +14,7 @@ import { ActorAvatarComponent } from '../shared/shared-actor-image/actor-avatar. import { SearchFiltersComponent } from './search-filters.component' import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap' import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common' -import { InfiniteScrollerDirective } from '../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../shared/shared-main/angular/infinite-scroller.component' import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' import { Video } from '@app/shared/shared-main/video/video.model' import { VideoPlaylist } from '@app/shared/shared-video-playlist/video-playlist.model' @@ -27,7 +27,7 @@ import { SearchService } from '@app/shared/shared-search/search.service' templateUrl: './search.component.html', standalone: true, imports: [ - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgIf, NgbCollapse, SearchFiltersComponent, @@ -50,6 +50,8 @@ export class SearchComponent implements OnInit, OnDestroy { currentPage: 1, totalItems: null as number } + hasMoreResults = true + isSearching = false advancedSearch: AdvancedSearch = new AdvancedSearch() isSearchFilterCollapsed = true currentSearch: string @@ -69,14 +71,9 @@ export class SearchComponent implements OnInit, OnDestroy { userMiniature: User - onSearchDataSubject = new Subject() - private subActivatedRoute: Subscription private isInitialLoad = false // set to false to show the search filters on first arrival - private hasMoreResults = true - private isSearching = false - private lastSearchTarget: SearchTargetType private serverConfig: HTMLServerConfig @@ -121,8 +118,6 @@ export class SearchComponent implements OnInit, OnDestroy { // Don't hide filters if we have some of them AND the user just came on the webpage, or we have an error this.isSearchFilterCollapsed = !this.error && (this.isInitialLoad === false || !this.advancedSearch.containsValues()) this.isInitialLoad = false - - this.search() }, error: err => this.notifier.error(err.message) @@ -173,9 +168,7 @@ export class SearchComponent implements OnInit, OnDestroy { this.pagination.totalItems = results.reduce((p, r) => p += r.total, 0) this.lastSearchTarget = this.advancedSearch.searchTarget - this.hasMoreResults = this.results.length < this.pagination.totalItems - - this.onSearchDataSubject.next(results) + this.hasMoreResults = this.calculateHasMoreResults(results) }, error: err => { @@ -198,6 +191,11 @@ export class SearchComponent implements OnInit, OnDestroy { }) } + onPageChange () { + this.results = [] + this.search() + } + onNearOfBottom () { // Last page if (!this.hasMoreResults || this.isSearching) return @@ -271,10 +269,10 @@ export class SearchComponent implements OnInit, OnDestroy { } private resetPagination () { - this.pagination.currentPage = 1 this.pagination.totalItems = null + this.pagination.currentPage = 1 - this.results = [] + this.onPageChange() } private updateTitle () { @@ -297,7 +295,7 @@ export class SearchComponent implements OnInit, OnDestroy { private getVideosObs () { const params = { search: this.currentSearch, - componentPagination: immutableAssign(this.pagination, { itemsPerPage: 10 }), + componentPagination: immutableAssign(this.pagination, { itemsPerPage: this.buildVideosPerPage() }), advancedSearch: this.advancedSearch } @@ -360,6 +358,28 @@ export class SearchComponent implements OnInit, OnDestroy { return undefined } + private calculateHasMoreResults (results: [ResultList, ResultList, ResultList
diff --git a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts index 3afc16a914c..969c460fb7e 100644 --- a/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts +++ b/client/src/app/+video-channels/video-channel-playlists/video-channel-playlists.component.ts @@ -1,8 +1,8 @@ -import { Subject, Subscription } from 'rxjs' +import { Subscription } from 'rxjs' import { AfterViewInit, Component, OnDestroy, OnInit } from '@angular/core' import { ComponentPagination, hasMoreItems, HooksService, ScreenService } from '@app/core' import { VideoPlaylistMiniatureComponent } from '../../shared/shared-video-playlist/video-playlist-miniature.component' -import { InfiniteScrollerDirective } from '../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/angular/infinite-scroller.component' import { NgIf, NgFor } from '@angular/common' import { VideoChannel } from '@app/shared/shared-main/video-channel/video-channel.model' import { VideoChannelService } from '@app/shared/shared-main/video-channel/video-channel.service' @@ -14,7 +14,7 @@ import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-pl templateUrl: './video-channel-playlists.component.html', styleUrls: [ './video-channel-playlists.component.scss' ], standalone: true, - imports: [ NgIf, InfiniteScrollerDirective, NgFor, VideoPlaylistMiniatureComponent ] + imports: [ NgIf, InfiniteScrollerComponent, NgFor, VideoPlaylistMiniatureComponent ] }) export class VideoChannelPlaylistsComponent implements OnInit, AfterViewInit, OnDestroy { videoPlaylists: VideoPlaylist[] = [] @@ -24,8 +24,8 @@ export class VideoChannelPlaylistsComponent implements OnInit, AfterViewInit, On itemsPerPage: 20, totalItems: null } - - onDataSubject = new Subject() + hasMoreResults = true + isLoading = false private videoChannelSub: Subscription private videoChannel: VideoChannel @@ -46,8 +46,6 @@ export class VideoChannelPlaylistsComponent implements OnInit, AfterViewInit, On this.hooks.runAction('action:video-channel-playlists.video-channel.loaded', 'video-channel', { videoChannel }) this.videoPlaylists = [] - this.pagination.currentPage = 1 - this.loadVideoPlaylists() }) } @@ -59,6 +57,10 @@ export class VideoChannelPlaylistsComponent implements OnInit, AfterViewInit, On if (this.videoChannelSub) this.videoChannelSub.unsubscribe() } + onPageChange () { + this.loadVideoPlaylists(true) + } + onNearOfBottom () { if (!hasMoreItems(this.pagination)) return @@ -70,15 +72,19 @@ export class VideoChannelPlaylistsComponent implements OnInit, AfterViewInit, On return this.screenService.isInMobileView() } - private loadVideoPlaylists () { + private loadVideoPlaylists (reset = false) { + this.isLoading = true + this.videoPlaylistService.listChannelPlaylists(this.videoChannel, this.pagination) .subscribe(res => { + if (reset) this.videoPlaylists = [] this.videoPlaylists = this.videoPlaylists.concat(res.data) this.pagination.totalItems = res.total + this.hasMoreResults = this.videoPlaylists.length < this.pagination.totalItems this.hooks.runAction('action:video-channel-playlists.playlists.loaded', 'video-channel', { playlists: this.videoPlaylists }) - this.onDataSubject.next(res.data) + this.isLoading = false }) } } diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html index b2cbd01f63e..c8a104e7002 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.html @@ -28,7 +28,14 @@

No comments.
-
+
-
+ } @else {
Comments are disabled.
} diff --git a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts index e88b1699cf2..67f7373e2e0 100644 --- a/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts +++ b/client/src/app/+videos/+video-watch/shared/comment/video-comments.component.ts @@ -10,8 +10,8 @@ import { VideoComment } from '@app/shared/shared-video-comment/video-comment.mod import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service' import { NgbDropdown, NgbDropdownButtonItem, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap' import { PeerTubeProblemDocument, ServerErrorCode, VideoCommentPolicy } from '@peertube/peertube-models' -import { Subject, Subscription } from 'rxjs' -import { InfiniteScrollerDirective } from '../../../../shared/shared-main/angular/infinite-scroller.directive' +import { Subscription } from 'rxjs' +import { InfiniteScrollerComponent } from '../../../../shared/shared-main/angular/infinite-scroller.component' import { FeedComponent } from '../../../../shared/shared-main/feeds/feed.component' import { LoaderComponent } from '../../../../shared/shared-main/loaders/loader.component' import { VideoCommentAddComponent } from './video-comment-add.component' @@ -31,7 +31,7 @@ import { VideoCommentComponent } from './video-comment.component' NgbDropdownItem, NgIf, VideoCommentAddComponent, - InfiniteScrollerDirective, + InfiniteScrollerComponent, VideoCommentComponent, NgFor, LoaderComponent @@ -56,6 +56,8 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { totalItems: null } totalNotDeletedComments: number + hasMoreResults = true + isLoading = false inReplyToCommentId: number commentReplyRedraftValue: string @@ -68,8 +70,6 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { syndicationItems: Syndication[] = [] - onDataSubject = new Subject() - private sub: Subscription constructor ( @@ -161,13 +161,16 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { 'filter:api.video-watch.video-threads.list.result' ) + this.isLoading = true + obs.subscribe({ next: res => { this.comments = this.comments.concat(res.data) this.componentPagination.totalItems = res.total this.totalNotDeletedComments = res.totalNotDeletedComments + this.hasMoreResults = hasMoreItems(this.componentPagination) - this.onDataSubject.next(res.data) + this.isLoading = false this.hooks.runAction('action:video-watch.video-threads.loaded', 'video-watch', { data: this.componentPagination }) }, @@ -277,6 +280,10 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { return this.authService.isLoggedIn() } + onPageChange (newPage: number) { + this.resetVideo(newPage) + } + onNearOfBottom () { if (hasMoreItems(this.componentPagination)) { this.componentPagination.currentPage++ @@ -291,7 +298,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { comment.account = null } - private resetVideo () { + private resetVideo (page = 1) { if (this.video.commentsPolicy.id === VideoCommentPolicy.DISABLED) return // Reset all our fields @@ -300,7 +307,7 @@ export class VideoCommentsComponent implements OnInit, OnChanges, OnDestroy { this.threadComments = {} this.threadLoading = {} this.inReplyToCommentId = undefined - this.componentPagination.currentPage = 1 + this.componentPagination.currentPage = page this.componentPagination.totalItems = null this.totalNotDeletedComments = null diff --git a/client/src/app/+videos/+video-watch/shared/player-widgets/player-widget.component.scss b/client/src/app/+videos/+video-watch/shared/player-widgets/player-widget.component.scss index 13163d4ee67..cbe5d6a4a79 100644 --- a/client/src/app/+videos/+video-watch/shared/player-widgets/player-widget.component.scss +++ b/client/src/app/+videos/+video-watch/shared/player-widgets/player-widget.component.scss @@ -10,6 +10,7 @@ background-color: pvar(--mainBackgroundColor); overflow-y: auto; border-bottom: 1px solid $separator-border-color; + display: block; .widget-header { background-color: pvar(--submenuBackgroundColor); diff --git a/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.html b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.html index 3cbfa33ad88..7737289c0c8 100644 --- a/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.html +++ b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.html @@ -1,6 +1,12 @@ -
@@ -37,11 +43,11 @@
-
+
-
+ diff --git a/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.scss b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.scss index 24c9fb5c068..dd2faf166c2 100644 --- a/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.scss +++ b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.scss @@ -27,6 +27,10 @@ } } + ::ng-deep .load-more { + margin: 20px 0; + } + my-video-playlist-element-miniature { ::ng-deep { .video { diff --git a/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.ts b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.ts index 85601fd8afe..8038084722a 100644 --- a/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.ts +++ b/client/src/app/+videos/+video-watch/shared/player-widgets/video-watch-playlist.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, Output } from '@angular/core' import { Router } from '@angular/router' -import { AuthService, ComponentPagination, HooksService, Notifier, SessionStorageService, UserService } from '@app/core' +import { AuthService, ComponentPagination, hasMoreItems, HooksService, Notifier, SessionStorageService, UserService } from '@app/core' import { isInViewport } from '@app/helpers' import { getBoolOrDefault } from '@root-helpers/local-storage-utils' import { peertubeSessionStorage } from '@root-helpers/peertube-web-storage' @@ -8,7 +8,7 @@ import { VideoPlaylistPrivacy } from '@peertube/peertube-models' import { VideoPlaylistElementMiniatureComponent } from '../../../../shared/shared-video-playlist/video-playlist-element-miniature.component' import { GlobalIconComponent } from '../../../../shared/shared-icons/global-icon.component' import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap' -import { InfiniteScrollerDirective } from '../../../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../../../shared/shared-main/angular/infinite-scroller.component' import { NgIf, NgClass, NgFor } from '@angular/common' import { VideoPlaylist } from '@app/shared/shared-video-playlist/video-playlist.model' import { VideoPlaylistElement } from '@app/shared/shared-video-playlist/video-playlist-element.model' @@ -19,7 +19,7 @@ import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-pl templateUrl: './video-watch-playlist.component.html', styleUrls: [ './player-widget.component.scss', './video-watch-playlist.component.scss' ], standalone: true, - imports: [ NgIf, InfiniteScrollerDirective, NgClass, NgbTooltip, GlobalIconComponent, NgFor, VideoPlaylistElementMiniatureComponent ] + imports: [ NgIf, InfiniteScrollerComponent, NgClass, NgbTooltip, GlobalIconComponent, NgFor, VideoPlaylistElementMiniatureComponent ] }) export class VideoWatchPlaylistComponent { static SESSION_STORAGE_LOOP_PLAYLIST = 'loop_playlist' @@ -29,6 +29,9 @@ export class VideoWatchPlaylistComponent { @Output() videoFound = new EventEmitter() @Output() noVideoFound = new EventEmitter() + hasMoreResults = true + isLoading = true + playlistElements: VideoPlaylistElement[] = [] playlistPagination: ComponentPagination = { currentPage: 1, @@ -42,7 +45,7 @@ export class VideoWatchPlaylistComponent { loopPlaylist: boolean loopPlaylistSwitchText = '' - noPlaylistVideos = false + noPlaylistVideos = true currentPlaylistPosition: number constructor ( @@ -63,6 +66,14 @@ export class VideoWatchPlaylistComponent { this.setLoopPlaylistSwitchText() } + onPageChange () { + // Prevent triggering upon initial page load + if (this.isLoading) return + + this.playlistElements = [] + this.loadPlaylistElements(this.playlist, false) + } + onPlaylistVideosNearOfBottom (position?: number) { // Last page if (this.playlistPagination.totalItems <= (this.playlistPagination.currentPage * this.playlistPagination.itemsPerPage)) return @@ -103,10 +114,13 @@ export class VideoWatchPlaylistComponent { 'filter:api.video-watch.video-playlist-elements.get.params', 'filter:api.video-watch.video-playlist-elements.get.result' ) + this.isLoading = true obs.subscribe(({ total, data: playlistElements }) => { this.playlistElements = this.playlistElements.concat(playlistElements) this.playlistPagination.totalItems = total + this.hasMoreResults = hasMoreItems(this.playlistPagination) + this.isLoading = false const firstAvailableVideo = this.playlistElements.find(e => !!e.video) if (!firstAvailableVideo) { diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.html b/client/src/app/+videos/video-list/overview/video-overview.component.html index b38516af82e..9a7d070f1ca 100644 --- a/client/src/app/+videos/video-list/overview/video-overview.component.html +++ b/client/src/app/+videos/video-list/overview/video-overview.component.html @@ -3,8 +3,12 @@

Discover

No results.
-
@@ -47,6 +51,6 @@

{{ object.channel.displayName }}

-
+
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.ts b/client/src/app/+videos/video-list/overview/video-overview.component.ts index 3aaf338ba26..ac39dd2b9a9 100644 --- a/client/src/app/+videos/video-list/overview/video-overview.component.ts +++ b/client/src/app/+videos/video-list/overview/video-overview.component.ts @@ -1,4 +1,4 @@ -import { Subject, Subscription, switchMap } from 'rxjs' +import { Subscription, switchMap } from 'rxjs' import { Component, OnDestroy, OnInit } from '@angular/core' import { Notifier, ScreenService, User, UserService } from '@app/core' import { Video } from '@app/shared/shared-main/video/video.model' @@ -7,7 +7,7 @@ import { VideosOverview } from './videos-overview.model' import { ActorAvatarComponent } from '../../../shared/shared-actor-image/actor-avatar.component' import { VideoMiniatureComponent } from '../../../shared/shared-video-miniature/video-miniature.component' import { RouterLink } from '@angular/router' -import { InfiniteScrollerDirective } from '../../../shared/shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../../shared/shared-main/angular/infinite-scroller.component' import { NgIf, NgFor } from '@angular/common' @Component({ @@ -15,21 +15,21 @@ import { NgIf, NgFor } from '@angular/common' templateUrl: './video-overview.component.html', styleUrls: [ './video-overview.component.scss' ], standalone: true, - imports: [ NgIf, InfiniteScrollerDirective, NgFor, RouterLink, VideoMiniatureComponent, ActorAvatarComponent ] + imports: [ NgIf, InfiniteScrollerComponent, NgFor, RouterLink, VideoMiniatureComponent, ActorAvatarComponent ] }) export class VideoOverviewComponent implements OnInit, OnDestroy { - onDataSubject = new Subject() + hasMoreResults = true overviews: VideosOverview[] = [] notResults = false userMiniature: User + currentPage = 1 + isLoading = true private loaded = false - private currentPage = 1 private maxPage = 20 private lastWasEmpty = false - private isLoading = false private userSub: Subscription @@ -74,22 +74,26 @@ export class VideoOverviewComponent implements OnInit, OnDestroy { return videos.slice(0, numberOfVideos * 2) } + onPageChange () { + this.loadMoreResults(true) + } + onNearOfBottom () { if (this.currentPage >= this.maxPage) return if (this.lastWasEmpty) return - if (this.isLoading) return this.currentPage++ this.loadMoreResults() } - private loadMoreResults () { + private loadMoreResults (reset = false) { this.isLoading = true this.overviewService.getVideosOverview(this.currentPage) .subscribe({ next: overview => { this.isLoading = false + this.hasMoreResults = this.currentPage < this.maxPage if (overview.tags.length === 0 && overview.channels.length === 0 && overview.categories.length === 0) { this.lastWasEmpty = true @@ -99,8 +103,8 @@ export class VideoOverviewComponent implements OnInit, OnDestroy { } this.loaded = true - this.onDataSubject.next(overview) + if (reset) this.overviews = [] this.overviews.push(overview) }, diff --git a/client/src/app/shared/shared-main/angular/infinite-scroller.component.html b/client/src/app/shared/shared-main/angular/infinite-scroller.component.html new file mode 100644 index 00000000000..8a00e78a105 --- /dev/null +++ b/client/src/app/shared/shared-main/angular/infinite-scroller.component.html @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/client/src/app/shared/shared-main/angular/infinite-scroller.component.scss b/client/src/app/shared/shared-main/angular/infinite-scroller.component.scss new file mode 100644 index 00000000000..61deddbe92f --- /dev/null +++ b/client/src/app/shared/shared-main/angular/infinite-scroller.component.scss @@ -0,0 +1,4 @@ +.load-more { + grid-column-start: 1; + grid-column-end: -1; +} diff --git a/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts b/client/src/app/shared/shared-main/angular/infinite-scroller.component.ts similarity index 58% rename from client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts rename to client/src/app/shared/shared-main/angular/infinite-scroller.component.ts index 2ad446e9287..56c897c17f9 100644 --- a/client/src/app/shared/shared-main/angular/infinite-scroller.directive.ts +++ b/client/src/app/shared/shared-main/angular/infinite-scroller.component.ts @@ -1,54 +1,89 @@ -import { fromEvent, Observable, Subscription } from 'rxjs' +import { fromEvent, Subscription } from 'rxjs' import { distinctUntilChanged, filter, map, share, startWith, throttleTime } from 'rxjs/operators' -import { AfterViewChecked, Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' +import { AfterViewChecked, Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core' import { PeerTubeRouterService, RouterSetting } from '@app/core' - -@Directive({ - selector: '[myInfiniteScroller]', - standalone: true +import { I18nSelectPipe, NgIf } from '@angular/common' +import { ActivatedRoute, NavigationEnd, Router, RouterLink } from '@angular/router' + +@Component({ + selector: 'my-infinite-scroller', + standalone: true, + templateUrl: './infinite-scroller.component.html', + styleUrl: './infinite-scroller.component.scss', + imports: [ + NgIf, + RouterLink, + I18nSelectPipe + ] }) -export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewChecked { +export class InfiniteScrollerComponent implements OnInit, OnDestroy, AfterViewChecked { + @Input() hasMore: boolean + @Input() isLoading: boolean @Input() percentLimit = 70 @Input() onItself = false - @Input() dataObservable: Observable // Add angular state in query params to reuse the routed component @Input() setAngularState: boolean @Input() parentDisabled = false @Output() nearOfBottom = new EventEmitter() + @Output() pageChange = new EventEmitter() + + @Input() currentPage!: number + @Output() currentPageChange = new EventEmitter() + + private disabled: boolean private decimalLimit = 0 private lastCurrentBottom = -1 private scrollDownSub: Subscription private container: HTMLElement - private checkScroll = false + private routeEventSub: Subscription constructor ( private peertubeRouter: PeerTubeRouterService, - private el: ElementRef + private el: ElementRef, + private route: ActivatedRoute, + private router: Router ) { this.decimalLimit = this.percentLimit / 100 } ngAfterViewChecked () { - if (this.checkScroll) { - this.checkScroll = false - + if (this.hasMore && !this.isLoading) { // Wait HTML update setTimeout(() => { - if (this.hasScroll() === false) this.nearOfBottom.emit() + if (this.hasScroll() === false && !this.disabled) this.nearOfBottom.emit() }) } } ngOnInit () { + this.disabled = !!this.route.snapshot.queryParams.page + + this.changePage(+this.route.snapshot.queryParams['page'] || 1) + + this.routeEventSub = this.router.events + .pipe( + filter(event => event instanceof NavigationEnd) + ) + .subscribe((event: NavigationEnd) => { + const search = event.url.split('?')[1] + const params = new URLSearchParams(search) + const newPage = +params.get('page') || 1 + + if (newPage === this.currentPage) return + + this.changePage(newPage) + }) + this.initialize() } ngOnDestroy () { if (this.scrollDownSub) this.scrollDownSub.unsubscribe() + if (this.routeEventSub) this.routeEventSub.unsubscribe() } initialize () { @@ -78,14 +113,14 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewCh .subscribe(() => { if (this.setAngularState && !this.parentDisabled) this.setScrollRouteParams() - this.nearOfBottom.emit() + if (!this.disabled) this.nearOfBottom.emit() }) + } - if (this.dataObservable) { - this.dataObservable - .pipe(filter(d => d.length !== 0)) - .subscribe(() => this.checkScroll = true) - } + private changePage (newPage: number) { + this.currentPage = newPage + this.currentPageChange.emit(newPage) + this.pageChange.emit(newPage) } private getScrollInfo () { diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.html b/client/src/app/shared/shared-video-miniature/videos-list.component.html index 5b9bc786286..7818ae5db0e 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.html +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.html @@ -40,10 +40,15 @@

No results.

-

@@ -80,11 +85,5 @@

- - - + diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts index 6781537476e..23cdbb78d12 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts @@ -1,6 +1,6 @@ import { NgClass, NgFor, NgIf, NgTemplateOutlet } from '@angular/common' import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, booleanAttribute } from '@angular/core' -import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive } from '@angular/router' +import { ActivatedRoute, Router, RouterLink, RouterLinkActive } from '@angular/router' import { AuthService, ComponentPaginationLight, @@ -18,8 +18,8 @@ import { ResultList, UserRight, VideoSortField } from '@peertube/peertube-models import { logger } from '@root-helpers/logger' import debug from 'debug' import { Observable, Subject, Subscription, forkJoin, fromEvent, of } from 'rxjs' -import { concatMap, debounceTime, filter, map, switchMap } from 'rxjs/operators' -import { InfiniteScrollerDirective } from '../shared-main/angular/infinite-scroller.directive' +import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' +import { InfiniteScrollerComponent } from '../shared-main/angular/infinite-scroller.component' import { ButtonComponent } from '../shared-main/buttons/button.component' import { FeedComponent } from '../shared-main/feeds/feed.component' import { Syndication } from '../shared-main/feeds/syndication.model' @@ -65,10 +65,9 @@ enum GroupDate { NgTemplateOutlet, ButtonComponent, VideoFiltersHeaderComponent, - InfiniteScrollerDirective, + InfiniteScrollerComponent, VideoMiniatureComponent, - GlobalIconComponent, - RouterLink + GlobalIconComponent ] }) export class VideosListComponent implements OnInit, OnChanges, OnDestroy { @@ -103,6 +102,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { @Output() filtersChanged = new EventEmitter() @Output() videosLoaded = new EventEmitter() + hasMoreResults = true videos: Video[] = [] highlightedLives: Video[] = [] @@ -161,24 +161,6 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { ngOnInit () { this.subscribeToVideoRequests() - this.disabled = this.disabled || this.route.snapshot.queryParams.finiteScroll === 'true' - - this.router.events - .pipe( - filter(event => event instanceof NavigationEnd) - ) - .subscribe((event: NavigationEnd) => { - const search = event.url.split('?')[1] - const params = new URLSearchParams(search) - const newPage = +params.get('page') || this.pagination.currentPage - - if (newPage === this.pagination.currentPage) { - return - } - - this.pagination.currentPage = newPage - this.loadMoreVideos(true) - }) const hiddenFilters = this.hideScopeFilter ? [ 'scope' ] @@ -211,8 +193,6 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { this.loadUserSettings(user) } - this.scheduleOnFiltersChanged(false) - this.subscribeToAnonymousUpdate() this.subscribeToSearchChange() }) @@ -272,11 +252,17 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { return video.id } + onPageChange () { + this.loadMoreVideos(true) + } + onNearOfBottom () { if (this.disabled) return // No more results - if (this.lastQueryLength !== undefined && this.lastQueryLength < this.pagination.itemsPerPage) return + if (this.lastQueryLength !== undefined && this.lastQueryLength < this.pagination.itemsPerPage) { + return + } this.pagination.currentPage += 1 @@ -312,7 +298,6 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { } reloadVideos () { - this.pagination.currentPage = +this.route.snapshot.queryParams.page || 1 this.loadMoreVideos(true) } @@ -494,16 +479,17 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { private subscribeToVideoRequests () { this.videoRequests .pipe( - concatMap(({ reset, obsHighlightedLives, obsVideos }) => { - return forkJoin([ obsHighlightedLives, obsVideos ]) + concatMap(({ reset, obsHighlightedLives, obsVideos }) => { + return forkJoin([ obsHighlightedLives, obsVideos ]) .pipe( map(([ resHighlightedLives, resVideos ]) => ({ highlightedLives: resHighlightedLives.data, videos: resVideos.data, reset })) ) - }) - ) + }) + ) .subscribe({ next: ({ videos, highlightedLives, reset }) => { this.hasDoneFirstQuery = true + this.hasMoreResults = videos.length === this.pagination.itemsPerPage this.lastQueryLength = videos.length if (reset) { diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.html b/client/src/app/shared/shared-video-miniature/videos-selection.component.html index 8931158a956..13b9927692d 100644 --- a/client/src/app/shared/shared-video-miniature/videos-selection.component.html +++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.html @@ -1,9 +1,14 @@
{{ noResultMessage }}
-
@@ -32,4 +37,4 @@
-
+ diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts index 4f06b1770e6..b3abdac5a0e 100644 --- a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts @@ -1,4 +1,4 @@ -import { Observable, Subject } from 'rxjs' +import { Observable } from 'rxjs' import { AfterContentInit, Component, ContentChildren, EventEmitter, Input, Output, QueryList, TemplateRef } from '@angular/core' import { ComponentPagination, Notifier, User } from '@app/core' import { logger } from '@root-helpers/logger' @@ -7,7 +7,7 @@ import { ResultList, VideosExistInPlaylists, VideoSortField } from '@peertube/pe import { MiniatureDisplayOptions, VideoMiniatureComponent } from './video-miniature.component' import { FormsModule } from '@angular/forms' import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component' -import { InfiniteScrollerDirective } from '../shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../shared-main/angular/infinite-scroller.component' import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common' import { Video } from '../shared-main/video/video.model' import { PeerTubeTemplateDirective } from '../shared-main/angular/peertube-template.directive' @@ -19,7 +19,7 @@ export type SelectionType = { [ id: number ]: boolean } templateUrl: './videos-selection.component.html', styleUrls: [ './videos-selection.component.scss' ], standalone: true, - imports: [ NgIf, InfiniteScrollerDirective, NgFor, PeertubeCheckboxComponent, FormsModule, VideoMiniatureComponent, NgTemplateOutlet ] + imports: [ NgIf, InfiniteScrollerComponent, NgFor, PeertubeCheckboxComponent, FormsModule, VideoMiniatureComponent, NgTemplateOutlet ] }) export class VideosSelectionComponent implements AfterContentInit { @Input() videosContainedInPlaylists: VideosExistInPlaylists @@ -44,14 +44,14 @@ export class VideosSelectionComponent implements AfterContentInit { _selection: SelectionType = {} + hasMoreResults = true + isLoading = true rowButtonsTemplate: TemplateRef globalButtonsTemplate: TemplateRef videos: Video[] = [] sort: VideoSortField = '-publishedAt' - onDataSubject = new Subject() - hasDoneFirstQuery = false private lastQueryLength: number @@ -88,8 +88,6 @@ export class VideosSelectionComponent implements AfterContentInit { const t = this.templates.find(t => t.name === 'globalButtons') if (t) this.globalButtonsTemplate = t.template } - - this.loadMoreVideos() } getVideosObservable (page: number) { @@ -108,11 +106,17 @@ export class VideosSelectionComponent implements AfterContentInit { return video.id } + onPageChange () { + this.loadMoreVideos(true) + } + onNearOfBottom () { if (this.disabled) return // No more results - if (this.lastQueryLength !== undefined && this.lastQueryLength < this.pagination.itemsPerPage) return + if (this.lastQueryLength !== undefined && this.lastQueryLength < this.pagination.itemsPerPage) { + return + } this.pagination.currentPage += 1 @@ -121,10 +125,12 @@ export class VideosSelectionComponent implements AfterContentInit { loadMoreVideos (reset = false) { if (reset) this.hasDoneFirstQuery = false + this.isLoading = true this.getVideosObservable(this.pagination.currentPage) .subscribe({ next: ({ data }) => { + this.hasMoreResults = data.length === this.pagination.itemsPerPage this.hasDoneFirstQuery = true this.lastQueryLength = data.length @@ -132,7 +138,7 @@ export class VideosSelectionComponent implements AfterContentInit { this.videos = this.videos.concat(data) this.videosModel = this.videos - this.onDataSubject.next(data) + this.isLoading = false }, error: err => { @@ -146,7 +152,7 @@ export class VideosSelectionComponent implements AfterContentInit { reloadVideos () { this.pagination.currentPage = 1 - this.loadMoreVideos(true) + this.onPageChange() } removeVideoFromArray (video: Video) { diff --git a/client/src/app/shared/standalone-notifications/user-notifications.component.html b/client/src/app/shared/standalone-notifications/user-notifications.component.html index c0c8242210f..fb58a68c982 100644 --- a/client/src/app/shared/standalone-notifications/user-notifications.component.html +++ b/client/src/app/shared/standalone-notifications/user-notifications.component.html @@ -1,6 +1,13 @@
You don't have notifications.
-
+
@@ -258,4 +265,4 @@
{{ notification.createdAt | myFromNow }}
-
+ diff --git a/client/src/app/shared/standalone-notifications/user-notifications.component.ts b/client/src/app/shared/standalone-notifications/user-notifications.component.ts index 72723d1a497..8410df53980 100644 --- a/client/src/app/shared/standalone-notifications/user-notifications.component.ts +++ b/client/src/app/shared/standalone-notifications/user-notifications.component.ts @@ -6,7 +6,7 @@ import { CommonModule } from '@angular/common' import { GlobalIconComponent } from '../shared-icons/global-icon.component' import { RouterLink } from '@angular/router' import { FromNowPipe } from '../shared-main/angular/from-now.pipe' -import { InfiniteScrollerDirective } from '../shared-main/angular/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../shared-main/angular/infinite-scroller.component' import { UserNotificationService } from '../shared-main/users/user-notification.service' import { UserNotification } from '../shared-main/users/user-notification.model' @@ -15,7 +15,7 @@ import { UserNotification } from '../shared-main/users/user-notification.model' templateUrl: 'user-notifications.component.html', styleUrls: [ 'user-notifications.component.scss' ], standalone: true, - imports: [ CommonModule, GlobalIconComponent, RouterLink, FromNowPipe, InfiniteScrollerDirective ] + imports: [ CommonModule, GlobalIconComponent, RouterLink, FromNowPipe, InfiniteScrollerComponent ] }) export class UserNotificationsComponent implements OnInit { @Input() ignoreLoadingBar = false @@ -29,8 +29,8 @@ export class UserNotificationsComponent implements OnInit { sortField = 'createdAt' componentPagination: ComponentPagination - - onDataSubject = new Subject() + hasMoreResults = true + isLoading = true constructor ( private userNotificationService: UserNotificationService, @@ -61,22 +61,27 @@ export class UserNotificationsComponent implements OnInit { order: this.sortField === 'createdAt' ? -1 : 1 } } + this.isLoading = true this.userNotificationService.listMyNotifications(options) .subscribe({ next: result => { this.notifications = reset ? result.data : this.notifications.concat(result.data) this.componentPagination.totalItems = result.total + this.hasMoreResults = hasMoreItems(this.componentPagination) this.notificationsLoaded.emit() - - this.onDataSubject.next(result.data) + this.isLoading = false }, error: err => this.notifier.error(err.message) }) } + onPageChange () { + this.loadNotifications(true) + } + onNearOfBottom () { if (this.infiniteScroll === false) return From b620bea43d33d6fd4e9449aebbbeb445b8081c37 Mon Sep 17 00:00:00 2001 From: kontrollanten <6680299+kontrollanten@users.noreply.github.com> Date: Sat, 21 Sep 2024 22:41:32 +0200 Subject: [PATCH 5/5] feat: add canonical tag to pages with pagination --- packages/tests/src/client/index-html.ts | 40 +++++++++- server/core/controllers/client.ts | 14 ++++ server/core/controllers/index.html | 90 ++++++++++++++++++++++ server/core/lib/html/client-html.ts | 16 ++++ server/core/lib/html/shared/actor-html.ts | 20 ++++- server/core/lib/html/shared/video-html.ts | 1 - server/core/lib/html/shared/videos-html.ts | 52 +++++++++++++ server/core/models/account/account.ts | 4 +- server/core/models/video/video-channel.ts | 4 +- 9 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 server/core/controllers/index.html create mode 100644 server/core/lib/html/shared/videos-html.ts diff --git a/packages/tests/src/client/index-html.ts b/packages/tests/src/client/index-html.ts index 5f33955dc2e..30484e209d4 100644 --- a/packages/tests/src/client/index-html.ts +++ b/packages/tests/src/client/index-html.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ import { expect } from 'chai' -import { HttpStatusCode, VideoPlaylistCreateResult } from '@peertube/peertube-models' +import { HttpStatusCode, ServerConfig, VideoPlaylistCreateResult } from '@peertube/peertube-models' import { cleanupTests, makeGetRequest, makeHTMLRequest, PeerTubeServer } from '@peertube/peertube-server-commands' import { checkIndexTags, getWatchPlaylistBasePaths, getWatchVideoBasePaths, prepareClientTests } from '@tests/shared/client.js' @@ -22,6 +22,8 @@ describe('Test index HTML generation', function () { let instanceDescription: string + const getTitleWithSuffix = (title: string, config: ServerConfig) => `${title} - ${config.instance.name}` + before(async function () { this.timeout(120000); @@ -46,7 +48,7 @@ describe('Test index HTML generation', function () { const config = await servers[0].config.getConfig() const res = await makeHTMLRequest(servers[0].url, '/videos/trending') - checkIndexTags(res.text, 'PeerTube', instanceDescription, '', config) + checkIndexTags(res.text, getTitleWithSuffix('Trending', config), instanceDescription, '', config) }) it('Should update the customized configuration and have the correct index html tags', async function () { @@ -70,20 +72,25 @@ describe('Test index HTML generation', function () { const config = await servers[0].config.getConfig() const res = await makeHTMLRequest(servers[0].url, '/videos/trending') - checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config) + checkIndexTags(res.text, getTitleWithSuffix('Trending', config), 'my short description', 'body { background-color: red; }', config) }) it('Should have valid index html updated tags (title, description...)', async function () { const config = await servers[0].config.getConfig() const res = await makeHTMLRequest(servers[0].url, '/videos/trending') - checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', config) + checkIndexTags(res.text, getTitleWithSuffix('Trending', config), 'my short description', 'body { background-color: red; }', config) }) }) describe('Canonical tags', function () { it('Should use the original video URL for the canonical tag', async function () { + const res = await makeHTMLRequest(servers[0].url, '/videos/trending?page=2') + expect(res.text).to.contain(``) + }) + + it('Should use pagination in video URL for the canonical tag', async function () { for (const basePath of getWatchVideoBasePaths()) { for (const id of videoIds) { const res = await makeHTMLRequest(servers[0].url, basePath + id) @@ -111,6 +118,18 @@ describe('Test index HTML generation', function () { accountURLtest(await makeHTMLRequest(servers[0].url, '/@root@' + servers[0].host)) }) + it('Should use pagination in account video channels URL for the canonical tag', async function () { + const res = await makeHTMLRequest(servers[0].url, '/a/root/video-channels?page=2') + + expect(res.text).to.contain(``) + }) + + it('Should use pagination in account videos URL for the canonical tag', async function () { + const res = await makeHTMLRequest(servers[0].url, '/a/root/videos?page=2') + + expect(res.text).to.contain(``) + }) + it('Should use the original channel URL for the canonical tag', async function () { const channelURLtests = res => { expect(res.text).to.contain(``) @@ -120,6 +139,19 @@ describe('Test index HTML generation', function () { channelURLtests(await makeHTMLRequest(servers[0].url, '/c/root_channel@' + servers[0].host)) channelURLtests(await makeHTMLRequest(servers[0].url, '/@root_channel@' + servers[0].host)) }) + + it('Should use pagination in channel videos URL for the canonical tag', async function () { + const res = await makeHTMLRequest(servers[0].url, '/c/root_channel/videos?page=2') + + expect(res.text).to.contain(``) + }) + + it('Should use pagination in channel playlists URL for the canonical tag', async function () { + const res = await makeHTMLRequest(servers[0].url, '/c/root_channel/video-playlists?page=2') + console.log(res.text) + + expect(res.text).to.contain(``) + }) }) describe('Indexation tags', function () { diff --git a/server/core/controllers/client.ts b/server/core/controllers/client.ts index 403b8b141a6..f977106a60f 100644 --- a/server/core/controllers/client.ts +++ b/server/core/controllers/client.ts @@ -11,6 +11,7 @@ import { currentDir, root } from '@peertube/peertube-node-utils' import { STATIC_MAX_AGE } from '../initializers/constants.js' import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/html/client-html.js' import { asyncMiddleware, buildRateLimiter, embedCSP } from '../middlewares/index.js' +import { VideosOrderType } from '../lib/html/shared/videos-html.js' const clientsRouter = express.Router() @@ -29,6 +30,11 @@ clientsRouter.use([ '/w/p/:id', '/videos/watch/playlist/:id' ], asyncMiddleware(generateWatchPlaylistHtmlPage) ) +clientsRouter.get([ '/videos/:type(overview|trending|recently-added|local)', '/' ], + clientsRateLimiter, + asyncMiddleware(generateVideosHtmlPage) +) + clientsRouter.use([ '/w/:id', '/videos/watch/:id' ], clientsRateLimiter, asyncMiddleware(generateWatchHtmlPage) @@ -186,6 +192,14 @@ async function generateVideoPlaylistEmbedHtmlPage (req: express.Request, res: ex return sendHTML(html, res) } +async function generateVideosHtmlPage (req: express.Request, res: express.Response) { + const { type } = req.params as { type: VideosOrderType } + + const html = await ClientHtml.getVideosHTMLPage(type, req, res, req.params.language) + + return sendHTML(html, res, true) +} + async function generateWatchHtmlPage (req: express.Request, res: express.Response) { // Thread link is '/w/:videoId;threadId=:threadId' // So to get the videoId we need to remove the last part diff --git a/server/core/controllers/index.html b/server/core/controllers/index.html new file mode 100644 index 00000000000..ce50f7be83b --- /dev/null +++ b/server/core/controllers/index.html @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/core/lib/html/client-html.ts b/server/core/lib/html/client-html.ts index c5e765ec8eb..c0561a00980 100644 --- a/server/core/lib/html/client-html.ts +++ b/server/core/lib/html/client-html.ts @@ -6,6 +6,8 @@ import { VideoHtml } from './shared/video-html.js' import { PlaylistHtml } from './shared/playlist-html.js' import { ActorHtml } from './shared/actor-html.js' import { PageHtml } from './shared/page-html.js' +import { VideosHtml, VideosOrderType } from './shared/videos-html.js' +import { CONFIG } from '@server/initializers/config.js' class ClientHtml { @@ -19,6 +21,20 @@ class ClientHtml { // --------------------------------------------------------------------------- + static getVideosHTMLPage (type: VideosOrderType, req: express.Request, res: express.Response, paramLang?: string) { + if (type) { + return VideosHtml.getVideosHTML(type, req, res) + } + + const [ , eventualType ] = CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE.split('/videos/') as VideosOrderType[] + + if (eventualType) { + return VideosHtml.getVideosHTML(eventualType, req, res) + } + + return PageHtml.getDefaultHTML(req, res, paramLang) + } + static getWatchHTMLPage (videoIdArg: string, req: express.Request, res: express.Response) { return VideoHtml.getWatchVideoHTML(videoIdArg, req, res) } diff --git a/server/core/lib/html/shared/actor-html.ts b/server/core/lib/html/shared/actor-html.ts index 2c6990afa5b..223537cb70f 100644 --- a/server/core/lib/html/shared/actor-html.ts +++ b/server/core/lib/html/shared/actor-html.ts @@ -55,7 +55,25 @@ export class ActorHtml { let customHTML = TagsHtml.addTitleTag(html, entity.getDisplayName()) customHTML = TagsHtml.addDescriptionTag(customHTML, escapedTruncatedDescription) - const url = entity.getClientUrl() + const eventualPage = req.path.split('/').pop() + let url + + if (entity instanceof AccountModel) { + const page = [ 'video-channels', 'videos' ].includes(eventualPage) + ? eventualPage + : undefined + url = entity.getClientUrl(page as 'video-channels' | 'videos') + } else if (entity instanceof VideoChannelModel) { + const page = [ 'video-playlists', 'videos' ].includes(eventualPage) + ? eventualPage + : undefined + url = entity.getClientUrl(page as 'video-playlists' | 'videos') + } + + if (req.query.page) { + url += `?page=${req.query.page}` + } + const siteName = CONFIG.INSTANCE.NAME const title = entity.getDisplayName() diff --git a/server/core/lib/html/shared/video-html.ts b/server/core/lib/html/shared/video-html.ts index e1b285a9c2a..e050851f7be 100644 --- a/server/core/lib/html/shared/video-html.ts +++ b/server/core/lib/html/shared/video-html.ts @@ -15,7 +15,6 @@ import { PageHtml } from './page-html.js' import { TagsHtml } from './tags-html.js' export class VideoHtml { - static async getWatchVideoHTML (videoIdArg: string, req: express.Request, res: express.Response) { const videoId = toCompleteUUID(videoIdArg) diff --git a/server/core/lib/html/shared/videos-html.ts b/server/core/lib/html/shared/videos-html.ts new file mode 100644 index 00000000000..31e3fd32e4e --- /dev/null +++ b/server/core/lib/html/shared/videos-html.ts @@ -0,0 +1,52 @@ +import { escapeHTML } from '@peertube/peertube-core-utils' +import express from 'express' +import { CONFIG } from '../../../initializers/config.js' +import { WEBSERVER } from '../../../initializers/constants.js' +import { PageHtml } from './page-html.js' +import { TagsHtml } from './tags-html.js' + +export type VideosOrderType = 'local' | 'trending' | 'overview' | 'recently-added' + +export class VideosHtml { + + static async getVideosHTML (type: VideosOrderType, req: express.Request, res: express.Response) { + const html = await PageHtml.getIndexHTML(req, res) + + return this.buildVideosHTML({ + html, + type, + currentPage: req.query.page + }) + } + + // --------------------------------------------------------------------------- + // Private + // --------------------------------------------------------------------------- + + private static buildVideosHTML (options: { + html: string + type: VideosOrderType + currentPage: string + }) { + const { html, currentPage, type } = options + + const title = type === 'recently-added' ? 'Recently added' : type.slice(0, 1).toUpperCase() + type.slice(1) + let customHTML = TagsHtml.addTitleTag(html, title) + customHTML = TagsHtml.addDescriptionTag(customHTML) + + let url = WEBSERVER.URL + '/videos/' + type + + if (currentPage) { + url += `?page=${currentPage}` + } + + return TagsHtml.addTags(customHTML, { + url, + + escapedSiteName: escapeHTML(CONFIG.INSTANCE.NAME), + escapedTitle: title, + + indexationPolicy: 'always' + }, {}) + } +} diff --git a/server/core/models/account/account.ts b/server/core/models/account/account.ts index f86a290623e..d9b42f5e203 100644 --- a/server/core/models/account/account.ts +++ b/server/core/models/account/account.ts @@ -476,8 +476,8 @@ export class AccountModel extends SequelizeModel { } // Avoid error when running this method on MAccount... | MChannel... - getClientUrl (this: MAccountHost | MChannelHost) { - return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + '/video-channels' + getClientUrl (this: MAccountHost | MChannelHost, page: 'video-channels' | 'videos' = 'video-channels') { + return WEBSERVER.URL + '/a/' + this.Actor.getIdentifier() + '/' + page } isBlocked () { diff --git a/server/core/models/video/video-channel.ts b/server/core/models/video/video-channel.ts index 3b8795d07d2..e434d8c43e1 100644 --- a/server/core/models/video/video-channel.ts +++ b/server/core/models/video/video-channel.ts @@ -841,8 +841,8 @@ export class VideoChannelModel extends SequelizeModel { } // Avoid error when running this method on MAccount... | MChannel... - getClientUrl (this: MAccountHost | MChannelHost) { - return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + '/videos' + getClientUrl (this: MAccountHost | MChannelHost, page: 'video-playlists' | 'videos' = 'videos') { + return WEBSERVER.URL + '/c/' + this.Actor.getIdentifier() + '/' + page } getDisplayName () {