diff --git a/mobile/lib/presentation/pages/drift_library.page.dart b/mobile/lib/presentation/pages/drift_library.page.dart index 673df089d5..e93a58be7d 100644 --- a/mobile/lib/presentation/pages/drift_library.page.dart +++ b/mobile/lib/presentation/pages/drift_library.page.dart @@ -180,7 +180,11 @@ class _PeopleCollectionCard extends ConsumerWidget { mainAxisSpacing: 8, physics: const NeverScrollableScrollPhysics(), children: people.take(4).map((person) { - return CircleAvatar(backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id))); + return CircleAvatar( + backgroundImage: RemoteImageProvider( + url: getFaceThumbnailUrl(person.id, updatedAt: person.updatedAt), + ), + ); }).toList(), ); }, diff --git a/mobile/lib/presentation/pages/drift_people_collection.page.dart b/mobile/lib/presentation/pages/drift_people_collection.page.dart index 26aa2e62ab..0afe723dc6 100644 --- a/mobile/lib/presentation/pages/drift_people_collection.page.dart +++ b/mobile/lib/presentation/pages/drift_people_collection.page.dart @@ -94,7 +94,9 @@ class _DriftPeopleCollectionPageState extends ConsumerState with S elevation: 3, child: CircleAvatar( maxRadius: 84 / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(widget.person.id)), + backgroundImage: RemoteImageProvider( + url: getFaceThumbnailUrl(widget.person.id, updatedAt: widget.person.updatedAt), + ), ), ), ), diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index 24b625e95e..a9382ec3ae 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -80,7 +80,9 @@ class PeoplePicker extends HookConsumerWidget { child: CircleAvatar( key: ValueKey(person.id), maxRadius: imageSize / 2, - backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)), + backgroundImage: RemoteImageProvider( + url: getFaceThumbnailUrl(person.id, updatedAt: person.updatedAt), + ), ), ), ), diff --git a/mobile/test/utils/image_url_builder_test.dart b/mobile/test/utils/image_url_builder_test.dart new file mode 100644 index 0000000000..1845d38eeb --- /dev/null +++ b/mobile/test/utils/image_url_builder_test.dart @@ -0,0 +1,35 @@ +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; + +void main() { + const endpoint = 'http://localhost:3000'; + + setUpAll(() async { + final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); + await StoreService.init(storeRepository: DriftStoreRepository(db), listenUpdates: false); + await StoreService.I.put(StoreKey.serverEndpoint, endpoint); + }); + + group('getFaceThumbnailUrl', () { + test('omits the cache buster when updatedAt is null', () { + expect(getFaceThumbnailUrl('person-1'), '$endpoint/people/person-1/thumbnail'); + }); + + test('appends the updatedAt cache buster so a changed featured photo busts the cache (#27434)', () { + final url = getFaceThumbnailUrl('person-1', updatedAt: DateTime.fromMillisecondsSinceEpoch(1717000000000)); + expect(url, '$endpoint/people/person-1/thumbnail?c=1717000000000'); + }); + + test('a newer updatedAt yields a different url so the image cache key changes', () { + final before = getFaceThumbnailUrl('person-1', updatedAt: DateTime.fromMillisecondsSinceEpoch(1)); + final after = getFaceThumbnailUrl('person-1', updatedAt: DateTime.fromMillisecondsSinceEpoch(2)); + expect(before, isNot(after)); + }); + }); +}