fix(mobile): refresh memories on resume and day change (#28983)

* fix(mobile): refresh memories on resume and day change

* fix(mobile): invalidate memories once after the resume sync
This commit is contained in:
Santo Shakil 2026-06-18 21:16:20 +06:00 committed by GitHub
parent 769c4015d3
commit 793487e52c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 105 additions and 0 deletions

View file

@ -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<AppLifeCycleEnum> {
_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((_) {

View file

@ -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<List<DriftMemory>>(
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);
});

View file

@ -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()));
});
});
});
}