diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index bb1877cbd..ad0940b77 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/settings.provider.dart'; import 'package:immich_mobile/providers/permission.provider.dart'; @@ -115,6 +116,7 @@ class AppLifeCycleNotifier extends StateNotifier { _safeRun(backgroundManager.syncLocal(full: CurrentPlatform.isAndroid ? true : false), "syncLocal"), _safeRun(backgroundManager.syncRemote().then((success) => syncSuccess = success), "syncRemote"), ]); + _ref.invalidate(driftMemoryFutureProvider); if (syncSuccess) { await Future.wait([ _safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) { diff --git a/mobile/lib/providers/infrastructure/memory.provider.dart b/mobile/lib/providers/infrastructure/memory.provider.dart index 91495bb5e..3dd32454b 100644 --- a/mobile/lib/providers/infrastructure/memory.provider.dart +++ b/mobile/lib/providers/infrastructure/memory.provider.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/services/memory.service.dart'; @@ -19,6 +21,11 @@ final driftMemoryFutureProvider = FutureProvider.autoDispose>( return const []; } + final now = DateTime.now(); + final nextMidnight = DateTime(now.year, now.month, now.day + 1); + final timer = Timer(nextMidnight.difference(now) + const Duration(seconds: 5), ref.invalidateSelf); + ref.onDispose(timer.cancel); + final service = ref.watch(driftMemoryServiceProvider); return service.getMemoryLane(userId); }); diff --git a/mobile/test/providers/infrastructure/memory_provider_test.dart b/mobile/test/providers/infrastructure/memory_provider_test.dart new file mode 100644 index 000000000..e588153fc --- /dev/null +++ b/mobile/test/providers/infrastructure/memory_provider_test.dart @@ -0,0 +1,96 @@ +import 'package:fake_async/fake_async.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/user.model.dart'; +import 'package:immich_mobile/domain/services/memory.service.dart'; +import 'package:immich_mobile/domain/services/user.service.dart'; +import 'package:immich_mobile/providers/infrastructure/memory.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockDriftMemoryService extends Mock implements DriftMemoryService {} + +class MockUserService extends Mock implements UserService {} + +void main() { + late MockDriftMemoryService memoryService; + late MockUserService userService; + + UserDto user({bool memoryEnabled = true}) => UserDto( + id: 'user-1', + email: 'user@test.dev', + name: 'user', + memoryEnabled: memoryEnabled, + profileChangedAt: DateTime(2026), + ); + + ProviderContainer makeContainer() { + final container = ProviderContainer( + overrides: [ + driftMemoryServiceProvider.overrideWithValue(memoryService), + currentUserProvider.overrideWith((ref) => CurrentUserProvider(userService)), + ], + ); + addTearDown(container.dispose); + return container; + } + + setUp(() { + memoryService = MockDriftMemoryService(); + userService = MockUserService(); + + when(() => memoryService.getMemoryLane('user-1')).thenAnswer((_) async => []); + when(() => userService.tryGetMyUser()).thenReturn(user()); + when(() => userService.watchMyUser()).thenAnswer((_) => const Stream.empty()); + }); + + group('driftMemoryFutureProvider', () { + test('re-queries after local midnight', () { + fakeAsync((async) { + final container = makeContainer(); + container.listen(driftMemoryFutureProvider, (_, __) {}); + async.flushMicrotasks(); + + verify(() => memoryService.getMemoryLane('user-1')).called(1); + + async.elapse(const Duration(seconds: 4)); + async.flushMicrotasks(); + verifyNever(() => memoryService.getMemoryLane('user-1')); + + async.elapse(const Duration(hours: 25)); + async.flushMicrotasks(); + verify(() => memoryService.getMemoryLane('user-1')).called(greaterThanOrEqualTo(1)); + }); + }); + + test('cancels the midnight timer when disposed', () { + fakeAsync((async) { + final container = makeContainer(); + final subscription = container.listen(driftMemoryFutureProvider, (_, __) {}); + async.flushMicrotasks(); + verify(() => memoryService.getMemoryLane('user-1')).called(1); + + subscription.close(); + async.elapse(const Duration(hours: 25)); + async.flushMicrotasks(); + + verifyNever(() => memoryService.getMemoryLane('user-1')); + }); + }); + + test('does not query or arm the timer when memories are disabled', () { + when(() => userService.tryGetMyUser()).thenReturn(user(memoryEnabled: false)); + + fakeAsync((async) { + final container = makeContainer(); + container.listen(driftMemoryFutureProvider, (_, __) {}); + async.flushMicrotasks(); + + async.elapse(const Duration(hours: 25)); + async.flushMicrotasks(); + + verifyNever(() => memoryService.getMemoryLane(any())); + }); + }); + }); +}