Compare commits

...

2 Commits

Author SHA1 Message Date
Santo Shakil b6c63df1c9 fix(mobile): refresh person thumbnail when the featured photo changes 2026-06-27 14:55:15 +06:00
Santo Shakil 6e1143e799 fix(mobile): hide video thumbnail when video is ready (#29012) 2026-06-26 22:20:40 -05:00
9 changed files with 57 additions and 8 deletions
@@ -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(),
);
},
@@ -94,7 +94,9 @@ class _DriftPeopleCollectionPageState extends ConsumerState<DriftPeopleCollectio
child: CircleAvatar(
key: ValueKey(person.id),
maxRadius: isTablet ? 100 / 2 : 96 / 2,
backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)),
backgroundImage: RemoteImageProvider(
url: getFaceThumbnailUrl(person.id, updatedAt: person.updatedAt),
),
),
),
),
@@ -121,7 +121,9 @@ class _Avatar extends StatelessWidget {
elevation: 3,
child: CircleAvatar(
maxRadius: imageSize / 2,
backgroundImage: RemoteImageProvider(url: getFaceThumbnailUrl(person.id)),
backgroundImage: RemoteImageProvider(
url: getFaceThumbnailUrl(person.id, updatedAt: person.updatedAt),
),
),
),
),
@@ -260,7 +260,7 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
return IgnorePointer(
child: Stack(
children: [
Center(child: widget.image),
if (!_isVideoReady || widget.asset.isMotionPhoto || isCasting) Center(child: widget.image),
if (!isCasting) ...[
Visibility.maintain(
visible: _isVideoReady,
@@ -32,5 +32,6 @@ class PersonApiRepository extends ApiRepository {
isHidden: dto.isHidden,
name: dto.name,
thumbnailPath: dto.thumbnailPath,
updatedAt: dto.updatedAt.orElse(null),
);
}
+3 -2
View File
@@ -20,6 +20,7 @@ String getPlaybackUrlForRemoteId(final String id) {
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?';
}
String getFaceThumbnailUrl(final String personId) {
return '${Store.get(StoreKey.serverEndpoint)}/people/$personId/thumbnail';
String getFaceThumbnailUrl(final String personId, {DateTime? updatedAt}) {
final url = '${Store.get(StoreKey.serverEndpoint)}/people/$personId/thumbnail';
return updatedAt != null ? '$url?c=${updatedAt.millisecondsSinceEpoch}' : url;
}
@@ -230,7 +230,9 @@ class _ExpandedBackgroundState extends ConsumerState<_ExpandedBackground> 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),
),
),
),
),
@@ -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),
),
),
),
),
@@ -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));
});
});
}