From 933ec9cf0fa1a5181f3f4ed40a0ca5e4dc92e764 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 7b9adce5d0bc58e045e00fe44e426f3160c6ac18 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 | 46 +++++++++++++------ 2 files changed, 39 insertions(+), 13 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 14db74ed9fe..0ffded05cac 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/common/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) } From 52aba7a1cb9347f9f12c2043b0e5e75563790ba4 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 | 3 +-- 1 file changed, 1 insertion(+), 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 a3f25fcc0f7..5da6f0904ab 100644 --- a/client/src/app/shared/standalone-notifications/user-notifications.component.ts +++ b/client/src/app/shared/standalone-notifications/user-notifications.component.ts @@ -80,9 +80,8 @@ export class UserNotificationsComponent implements OnInit { onNearOfBottom () { if (this.infiniteScroll === false) return - this.componentPagination.currentPage++ - if (hasMoreItems(this.componentPagination)) { + this.componentPagination.currentPage++ this.loadNotifications() } } From a91fd0cb35fc214b6ac25392617113dfd59bd719 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 | 21 ++++-- .../my-follows/my-followers.component.html | 13 +++- .../my-follows/my-followers.component.ts | 17 +++-- .../my-subscriptions.component.html | 13 +++- .../my-follows/my-subscriptions.component.ts | 22 ++++-- .../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 | 18 +++-- client/src/app/+search/search.component.html | 11 ++- client/src/app/+search/search.component.ts | 52 +++++++++---- .../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 | 14 +++- .../video-watch-playlist.component.scss | 4 + .../video-watch-playlist.component.ts | 22 +++++- .../overview/video-overview.component.html | 10 ++- .../overview/video-overview.component.ts | 22 +++--- .../common/infinite-scroller.component.html | 7 ++ .../common/infinite-scroller.component.scss | 4 + ...tive.ts => infinite-scroller.component.ts} | 73 ++++++++++++++----- .../videos-list.component.html | 21 +++--- .../videos-list.component.ts | 47 ++++-------- .../videos-selection.component.html | 13 +++- .../videos-selection.component.ts | 22 +++--- .../user-notifications.component.html | 11 ++- .../user-notifications.component.ts | 19 +++-- 38 files changed, 505 insertions(+), 260 deletions(-) create mode 100644 client/src/app/shared/shared-main/common/infinite-scroller.component.html create mode 100644 client/src/app/shared/shared-main/common/infinite-scroller.component.scss rename client/src/app/shared/shared-main/common/{infinite-scroller.directive.ts => infinite-scroller.component.ts} (59%) 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..621888732ab 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 f31382ff945..93560cd831f 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/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/common/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/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..6537a6ae12f 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 6bea42fcebe..440d9ea1ac4 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/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../../shared/shared-main/common/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 96aca5b5a7e..316378f249e 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 edd4baf46d9..c8d16d9407c 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 @@ -6,14 +6,14 @@ import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginServ import { AlertComponent } from '@app/shared/shared-main/common/alert.component' import { PeerTubePluginIndex, PluginType, PluginType_Type } from '@peertube/peertube-models' import { logger } from '@root-helpers/logger' -import { Subject } from 'rxjs' -import { debounceTime, distinctUntilChanged } from 'rxjs/operators' +import { ReplaySubject, Subject } from 'rxjs' +import { debounceTime, distinct, distinctUntilChanged, filter } from 'rxjs/operators' import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component' import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component' import { EditButtonComponent } from '../../../shared/shared-main/buttons/edit-button.component' import { AutofocusDirective } from '../../../shared/shared-main/common/autofocus.directive' -import { InfiniteScrollerDirective } from '../../../shared/shared-main/common/infinite-scroller.directive' import { PluginCardComponent } from '../shared/plugin-card.component' +import { InfiniteScrollerComponent } from '../../../shared/shared-main/common/infinite-scroller.component' import { PluginNavigationComponent } from '../shared/plugin-navigation.component' @Component({ @@ -26,7 +26,7 @@ import { PluginNavigationComponent } from '../shared/plugin-navigation.component NgIf, GlobalIconComponent, AutofocusDirective, - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgFor, PluginCardComponent, EditButtonComponent, @@ -43,17 +43,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, @@ -75,10 +75,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() @@ -95,14 +100,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) @@ -110,10 +108,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 => { @@ -125,6 +124,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 d4d1c093130..d77657fe1c1 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 @@

My channels

-
+ 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 998b910ee23..b8405e4f71d 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/common/defer-loading.directive' -import { InfiniteScrollerDirective } from '../../shared/shared-main/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/common/infinite-scroller.component' import { NumberFormatterPipe } from '../../shared/shared-main/common/number-formatter.pipe' import { DeleteButtonComponent } from '../../shared/shared-main/buttons/delete-button.component' import { EditButtonComponent } from '../../shared/shared-main/buttons/edit-button.component' @@ -31,7 +31,7 @@ type CustomChartData = (ChartData & { startDate: string, total: number }) RouterLink, ChannelsSetupMessageComponent, AdvancedInputFilterComponent, - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgFor, ActorAvatarComponent, EditButtonComponent, @@ -43,6 +43,8 @@ type CustomChartData = (ChartData & { startDate: string, total: number }) }) export class MyVideoChannelsComponent { videoChannels: VideoChannel[] = [] + hasMoreResults = true + isLoading = true videoChannelsChartData: CustomChartData[] @@ -76,10 +78,9 @@ export class MyVideoChannelsComponent { this.search = search this.pagination.currentPage = 1 - this.videoChannels = [] this.pagesDone.clear() - this.loadMoreVideoChannels() + this.onPageChange() } async deleteVideoChannel (videoChannel: VideoChannel) { @@ -111,6 +112,10 @@ export class MyVideoChannelsComponent { }) } + onPageChange () { + this.loadMoreVideoChannels(true) + } + onNearOfBottom () { if (!hasMoreItems(this.pagination)) return @@ -119,9 +124,10 @@ export class MyVideoChannelsComponent { 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( @@ -136,6 +142,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 205c2473c88..b7b876e2edd 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 @@

My followers

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

My followers

- + 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 4c1d4741e8f..83fe1a4ab13 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,21 +4,22 @@ 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/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/common/infinite-scroller.component' import { formatICU } from '@app/helpers' @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, @@ -26,7 +27,6 @@ export class MyFollowersComponent implements OnInit { totalItems: null } - onDataSubject = new Subject() search: string inputFilters: AdvancedInputFilter[] @@ -58,6 +58,10 @@ export class MyFollowersComponent implements OnInit { ] } + onPageChange () { + this.loadFollowers(false) + } + onNearOfBottom () { // Last page if (this.pagination.totalItems <= (this.pagination.currentPage * this.pagination.itemsPerPage)) return @@ -83,6 +87,8 @@ export class MyFollowersComponent implements OnInit { } private loadFollowers (more = true) { + this.isLoading = true + this.userSubscriptionService.listFollowers({ pagination: this.pagination, nameWithHost: this.getUsername(), @@ -93,8 +99,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 547eb8c734a..57f39938aed 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 @@

My subscriptions

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

My subscriptions

-
+ 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 4136c19c483..7913af220a3 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/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/common/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' @@ -20,7 +19,7 @@ import { formatICU } from '@app/helpers' GlobalIconComponent, NgIf, AdvancedInputFilterComponent, - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgFor, ActorAvatarComponent, RouterLink, @@ -29,15 +28,15 @@ import { formatICU } from '@app/helpers' }) export class MySubscriptionsComponent { videoChannels: VideoChannel[] = [] + hasMoreResults = true + isLoading = true pagination: ComponentPagination = { currentPage: 1, itemsPerPage: 10, totalItems: null } - onDataSubject = new Subject() - search: string constructor ( @@ -45,9 +44,15 @@ export class MySubscriptionsComponent { 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() @@ -66,6 +71,7 @@ export class MySubscriptionsComponent { } private loadSubscriptions (more = true) { + this.isLoading = true this.userSubscriptionService.listSubscriptions({ pagination: this.pagination, search: this.search }) .subscribe({ next: res => { @@ -73,8 +79,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..7bfc63f1a69 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 c6f8760a304..4d08e3561d0 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/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/common/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 fd326e03ce9..f34cb6ec679 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 @@ -9,7 +9,7 @@

My playlists

-
+
My playlists

- + 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 57e04d27388..e1538dbd46e 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/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/common/infinite-scroller.component' import { RouterLink } from '@angular/router' import { AdvancedInputFilterComponent } from '../../shared/shared-forms/advanced-input-filter.component' import { ChannelsSetupMessageComponent } from '../../shared/shared-main/channel/channels-setup-message.component' @@ -26,7 +25,7 @@ import { formatICU } from '@app/helpers' ChannelsSetupMessageComponent, AdvancedInputFilterComponent, RouterLink, - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgFor, VideoPlaylistMiniatureComponent, DeleteButtonComponent, @@ -35,6 +34,8 @@ import { formatICU } from '@app/helpers' }) export class MyVideoPlaylistsComponent { videoPlaylists: VideoPlaylist[] = [] + hasMoreResults = true + isLoading = true pagination: ComponentPagination = { currentPage: 1, @@ -42,8 +43,6 @@ export class MyVideoPlaylistsComponent { totalItems: null } - onDataSubject = new Subject() - search: string constructor ( @@ -77,6 +76,10 @@ 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 @@ -98,6 +101,8 @@ export class MyVideoPlaylistsComponent { } private loadVideoPlaylists (reset = false) { + this.isLoading = true + this.authService.userInformationLoaded .pipe(mergeMap(() => { const user = this.authService.getUser() @@ -108,8 +113,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 7c05fd1853a..675c0de86a1 100644 --- a/client/src/app/+search/search.component.html +++ b/client/src/app/+search/search.component.html @@ -1,4 +1,11 @@ -
+
@@ -79,4 +86,4 @@
-
+ diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts index ad1ef1f3864..71436d95bf1 100644 --- a/client/src/app/+search/search.component.ts +++ b/client/src/app/+search/search.component.ts @@ -11,11 +11,11 @@ import { AdvancedSearch } from '@app/shared/shared-search/advanced-search.model' import { SearchService } from '@app/shared/shared-search/search.service' import { VideoPlaylist } from '@app/shared/shared-video-playlist/video-playlist.model' import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap' -import { HTMLServerConfig, SearchTargetType } from '@peertube/peertube-models' -import { forkJoin, Subject, Subscription } from 'rxjs' +import { HTMLServerConfig, ResultList, SearchTargetType } from '@peertube/peertube-models' +import { forkJoin, Subscription } from 'rxjs' import { LinkType } from 'src/types/link.type' import { ActorAvatarComponent } from '../shared/shared-actor-image/actor-avatar.component' -import { InfiniteScrollerDirective } from '../shared/shared-main/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../shared/shared-main/common/infinite-scroller.component' import { NumberFormatterPipe } from '../shared/shared-main/common/number-formatter.pipe' import { SubscribeButtonComponent } from '../shared/shared-user-subscription/subscribe-button.component' import { MiniatureDisplayOptions, VideoMiniatureComponent } from '../shared/shared-video-miniature/video-miniature.component' @@ -28,7 +28,7 @@ import { SearchFiltersComponent } from './search-filters.component' templateUrl: './search.component.html', standalone: true, imports: [ - InfiniteScrollerDirective, + InfiniteScrollerComponent, NgIf, NgbCollapse, SearchFiltersComponent, @@ -52,6 +52,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 @@ -71,14 +73,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 @@ -123,8 +120,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) @@ -175,9 +170,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 => { @@ -200,6 +193,11 @@ export class SearchComponent implements OnInit, OnDestroy { }) } + onPageChange () { + this.results = [] + this.search() + } + onNearOfBottom () { // Last page if (!this.hasMoreResults || this.isSearching) return @@ -280,7 +278,7 @@ export class SearchComponent implements OnInit, OnDestroy { this.pagination.currentPage = 1 this.pagination.totalItems = null - this.results = [] + this.onPageChange() } private updateTitle () { @@ -303,7 +301,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 } @@ -366,6 +364,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 8c4d62d7d7b..0dfc9e37155 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/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../shared/shared-main/common/infinite-scroller.component' import { NgIf, NgFor } from '@angular/common' import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model' import { VideoChannelService } from '@app/shared/shared-main/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..ab7a5e301ce 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 2eeda38bf0f..6c4cd8aa85c 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/common/infinite-scroller.directive' +import { Subscription } from 'rxjs' +import { InfiniteScrollerComponent } from '../../../../shared/shared-main/common/infinite-scroller.component' import { FeedComponent } from '../../../../shared/shared-main/feeds/feed.component' import { LoaderComponent } from '../../../../shared/shared-main/common/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..84141ee8699 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 @@ -
@@ -44,4 +50,4 @@ [touchScreenEditButton]="true" >
-
+ 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 f0fc359ca60..4525a533cfb 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/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../../../shared/shared-main/common/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..35fc6da2b50 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 f5bf7dba05e..0f6c4d7b255 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/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../../../shared/shared-main/common/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/common/infinite-scroller.component.html b/client/src/app/shared/shared-main/common/infinite-scroller.component.html new file mode 100644 index 00000000000..8a00e78a105 --- /dev/null +++ b/client/src/app/shared/shared-main/common/infinite-scroller.component.html @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/client/src/app/shared/shared-main/common/infinite-scroller.component.scss b/client/src/app/shared/shared-main/common/infinite-scroller.component.scss new file mode 100644 index 00000000000..61deddbe92f --- /dev/null +++ b/client/src/app/shared/shared-main/common/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/common/infinite-scroller.directive.ts b/client/src/app/shared/shared-main/common/infinite-scroller.component.ts similarity index 59% rename from client/src/app/shared/shared-main/common/infinite-scroller.directive.ts rename to client/src/app/shared/shared-main/common/infinite-scroller.component.ts index 2ad446e9287..75dbfd1e640 100644 --- a/client/src/app/shared/shared-main/common/infinite-scroller.directive.ts +++ b/client/src/app/shared/shared-main/common/infinite-scroller.component.ts @@ -1,16 +1,26 @@ -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 @@ -18,37 +28,61 @@ export class InfiniteScrollerDirective implements OnInit, OnDestroy, AfterViewCh @Output() nearOfBottom = 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 +112,13 @@ 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) } 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..951d887df33 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 @@

- - -
- - 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 0ffded05cac..6f3eeff95b8 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, 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/common/infinite-scroller.directive' +import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' +import { InfiniteScrollerComponent } from '../shared-main/common/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 { @@ -98,11 +97,12 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { @Input() displayOptions: MiniatureDisplayOptions - @Input({ transform: booleanAttribute }) disabled: boolean + @Input({ transform: booleanAttribute }) disabled = false @Output() filtersChanged = new EventEmitter() @Output() videosLoaded = new EventEmitter() + hasMoreResults = true videos: Video[] = [] highlightedLives: Video[] = [] @@ -119,8 +119,6 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { itemsPerPage: 25 } - lastQueryLength: number - private defaultDisplayOptions: MiniatureDisplayOptions = { date: true, views: true, @@ -138,6 +136,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { private groupedDateLabels: { [id in GroupDate]: string } private groupedDates: { [id: number]: GroupDate } = {} + private lastQueryLength: number + private videoRequests = new Subject<{ reset: boolean obsVideos: Observable, 'data'>> @@ -153,32 +153,13 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { private route: ActivatedRoute, private screenService: ScreenService, private peertubeRouter: PeerTubeRouterService, - private serverService: ServerService, - public router: Router + private serverService: ServerService ) { } 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 +192,6 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { this.loadUserSettings(user) } - this.scheduleOnFiltersChanged(false) - this.subscribeToAnonymousUpdate() this.subscribeToSearchChange() }) @@ -272,6 +251,10 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { return video.id } + onPageChange () { + this.loadMoreVideos(true) + } + onNearOfBottom () { if (this.disabled) return @@ -312,7 +295,6 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { } reloadVideos () { - this.pagination.currentPage = +this.route.snapshot.queryParams.page || 1 this.loadMoreVideos(true) } @@ -504,6 +486,7 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { .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..f0000a41131 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 e9be233f5e6..9614abf1bac 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/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../shared-main/common/infinite-scroller.component' import { NgIf, NgFor, NgTemplateOutlet } from '@angular/common' import { Video } from '../shared-main/video/video.model' import { PeerTubeTemplateDirective } from '../shared-main/common/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,6 +106,10 @@ export class VideosSelectionComponent implements AfterContentInit { return video.id } + onPageChange () { + this.loadMoreVideos(true) + } + onNearOfBottom () { if (this.disabled) return @@ -121,10 +123,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 +136,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 +150,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..2c71105a8a0 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 5da6f0904ab..4fa9f2acd44 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/date/from-now.pipe' -import { InfiniteScrollerDirective } from '../shared-main/common/infinite-scroller.directive' +import { InfiniteScrollerComponent } from '../shared-main/common/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, @@ -44,8 +44,6 @@ export class UserNotificationsComponent implements OnInit { totalItems: null } - this.loadNotifications() - if (this.markAllAsReadSubject) { this.markAllAsReadSubject.subscribe(() => this.markAllAsRead()) } @@ -61,22 +59,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 10662889f3c869e8665b32607f1d79944873bdb9 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 | 39 ++++++++++-- packages/tests/src/client/og-twitter-tags.ts | 21 ++++++- server/core/controllers/client.ts | 14 +++++ server/core/lib/html/client-html.ts | 16 +++++ server/core/lib/html/shared/actor-html.ts | 20 +++++- server/core/lib/html/shared/videos-html.ts | 65 ++++++++++++++++++++ server/core/models/account/account.ts | 4 +- server/core/models/video/video-channel.ts | 4 +- 8 files changed, 172 insertions(+), 11 deletions(-) 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 f7eda2753d7..06a99f78bbe 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' @@ -25,6 +25,8 @@ describe('Test index HTML generation', function () { shortDescription: string } + const getTitleWithSuffix = (title: string, config: ServerConfig) => `${title} - ${config.instance.name}` + before(async function () { this.timeout(120000); @@ -49,7 +51,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, instanceConfig.name, instanceConfig.shortDescription, '', config) + checkIndexTags(res.text, getTitleWithSuffix('Trending', config), instanceConfig.shortDescription, '', config) }) it('Should update the customized configuration and have the correct index html tags', async function () { @@ -73,20 +75,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) @@ -114,6 +121,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(``) @@ -123,6 +142,18 @@ 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') + + expect(res.text).to.contain(``) + }) }) describe('Indexation tags', function () { diff --git a/packages/tests/src/client/og-twitter-tags.ts b/packages/tests/src/client/og-twitter-tags.ts index f0fc8943f09..5d9b9270430 100644 --- a/packages/tests/src/client/og-twitter-tags.ts +++ b/packages/tests/src/client/og-twitter-tags.ts @@ -62,6 +62,20 @@ describe('Test Open Graph and Twitter cards HTML tags', function () { expect(text).to.contain(``)) + expect(text).to.contain(``) + expect(text).to.contain('') + expect(text).to.contain(`