From 4c76cc141fbfd6d0ec36e1c50eeef501ec04c107 Mon Sep 17 00:00:00 2001 From: Victor Chang Date: Wed, 1 Apr 2026 07:25:50 +0000 Subject: [PATCH] Better lazy loading, and improve consistency between pages --- .../pages/drift_activities.page.dart | 25 ++++++++++++++++--- .../src/repositories/activity.repository.ts | 1 + server/src/services/activity.service.ts | 2 +- .../asset-viewer/ActivityViewer.svelte | 15 +++++++++++ 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index 8a294116ec..b59eb07bac 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -1,5 +1,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; @@ -34,14 +35,32 @@ class DriftActivitiesPage extends HookConsumerWidget { scrollToBottom(); } + void loadMoreIfNeeded() { + if (activityNotifier.hasMore && !activityNotifier.isLoadingMore) { + activityNotifier.loadMore(); + } + } + + void checkIfViewportNotFilled() { + SchedulerBinding.instance.addPostFrameCallback((_) { + if (listViewScrollController.hasClients && + listViewScrollController.position.maxScrollExtent <= 0) { + loadMoreIfNeeded(); + } + }); + } + + // Auto-load more pages if content doesn't fill the viewport + ref.listen(albumActivityProvider(album.id, assetId), (_, __) { + checkIfViewportNotFilled(); + }); + useEffect(() { void onScroll() { // In a reversed ListView, scrolling toward older items means reaching maxScrollExtent if (listViewScrollController.position.pixels >= listViewScrollController.position.maxScrollExtent - 200) { - if (activityNotifier.hasMore && !activityNotifier.isLoadingMore) { - activityNotifier.loadMore(); - } + loadMoreIfNeeded(); } } listViewScrollController.addListener(onScroll); diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts index 840369b579..2fa7de8f6a 100644 --- a/server/src/repositories/activity.repository.ts +++ b/server/src/repositories/activity.repository.ts @@ -48,6 +48,7 @@ export class ActivityRepository { .$if(!!before, (qb) => qb.where('activity.createdAt', '<', before!)) .$if(!!at, (qb) => qb.where('activity.createdAt', '=', at!)) .orderBy('activity.createdAt', take !== undefined ? 'desc' : 'asc') + .orderBy('activity.id', take !== undefined ? 'desc' : 'asc') .$if(take !== undefined, (qb) => qb.limit(take!)) .execute(); } diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index b00d93d64a..4969e97394 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -40,7 +40,7 @@ export class ActivityService extends BaseService { // Fetch all activities at exactly that timestamp and prepend any not already loaded. if (results.length > 0) { const boundaryTime = results[0].createdAt; - const loadedIds = new Set(results.filter((a) => +new Date(a.createdAt) === +new Date(boundaryTime)).map((a) => a.id)); + const loadedIds = new Set(results.filter((a) => a.createdAt.getTime() === boundaryTime.getTime()).map((a) => a.id)); const extras = await this.activityRepository.search({ ...searchOptions, at: boundaryTime }); const newExtras = extras.map(mapActivity).filter((a) => !loadedIds.has(a.id)); return [...newExtras, ...results]; diff --git a/web/src/lib/components/asset-viewer/ActivityViewer.svelte b/web/src/lib/components/asset-viewer/ActivityViewer.svelte index f58715b7e1..5d78f7e2d5 100644 --- a/web/src/lib/components/asset-viewer/ActivityViewer.svelte +++ b/web/src/lib/components/asset-viewer/ActivityViewer.svelte @@ -136,6 +136,21 @@ } }; + // Auto-load more pages if content doesn't fill the scroll container + $effect(() => { + // Track reactive dependencies + void activityManager.activities.length; + void activityManager.isLoadingMore; + + if (!scrollContainer || !activityManager.hasMore || activityManager.isLoadingMore) return; + // After rendering, check if there's no scrollable overflow + tick().then(() => { + if (scrollContainer && scrollContainer.scrollHeight <= scrollContainer.clientHeight) { + void loadMoreAndPreserveScroll(); + } + }); + }); + const onsubmit = async (event: Event) => { event.preventDefault(); await handleSendComment();