From 6cce8efed80af2a7e212eae3975264fcf0e23a30 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Sat, 7 Nov 2026 13:07:08 +0600 Subject: [PATCH] fix(mobile): handle links to locked and trashed photos (#33034) * fix(mobile): handle links to locked and trashed photos * use a real timeline service in the deep link test (cherry picked from commit 87ef895041e160aef4f61f2d7af1ad3f8e38827d) --- mobile/lib/services/deep_link.service.dart | 4 +-- .../test/services/deep_link_service_test.dart | 28 +++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index 76aaf8bc6..6db90679d 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -108,7 +108,7 @@ class DeepLinkService { Future _buildAssetDeepLink(String assetId, WidgetRef ref, {String? albumId}) async { final asset = await _betaAssetService.getRemoteAsset(assetId); - if (asset == null) { + if (asset == null || asset.isLocked) { return null; } @@ -117,7 +117,7 @@ class DeepLinkService { AssetViewer.setAsset(ref, asset); return AssetViewerRoute( initialIndex: 0, - timelineService: _betaTimelineFactory.fromAssets([asset], TimelineOrigin.deepLink), + timelineService: _betaTimelineFactory.fromAssets([asset], asset.isTrashed ? .trash : .deepLink), currentAlbum: album, ); } diff --git a/mobile/test/services/deep_link_service_test.dart b/mobile/test/services/deep_link_service_test.dart index 09ea2a19d..e3cac477f 100644 --- a/mobile/test/services/deep_link_service_test.dart +++ b/mobile/test/services/deep_link_service_test.dart @@ -23,7 +23,6 @@ class MockRemoteAlbumService extends Mock implements RemoteAlbumService {} class MockMemoryService extends Mock implements MemoryService {} - class MockPlatformDeepLink extends Mock implements PlatformDeepLink {} class MockWidgetRef extends Mock implements WidgetRef {} @@ -88,6 +87,8 @@ void main() { late List createdTimelineServices; late DeepLinkService sut; + setUpAll(() => registerFallbackValue(TimelineOrigin.main)); + setUp(() { timelineFactory = MockTimelineFactory(); assetService = MockAssetService(); @@ -96,12 +97,12 @@ void main() { ref = MockWidgetRef(); createdTimelineServices = []; - when(() => timelineFactory.fromAssets(any(), TimelineOrigin.deepLink)).thenAnswer((invocation) { + when(() => timelineFactory.fromAssets(any(), any())).thenAnswer((invocation) { final assets = List.from(invocation.positionalArguments[0] as List); final timelineService = TimelineService(( assetSource: (index, count) async => assets.skip(index).take(count).toList(), bucketSource: () => Stream.value([Bucket(assetCount: assets.length)]), - origin: TimelineOrigin.deepLink, + origin: invocation.positionalArguments[1] as TimelineOrigin, )); createdTimelineServices.add(timelineService); return timelineService; @@ -179,4 +180,25 @@ void main() { verifyNever(() => memoryService.getMemoryLane(any())); verify(() => memoryService.get(_memoryId)).called(1); }); + + test('locked photo link opens nothing', () async { + when( + () => assetService.getRemoteAsset(_assetId), + ).thenAnswer((_) async => _asset.copyWith(visibility: AssetVisibility.locked)); + + final route = await sut.handleMyImmichApp(link('/photos/$_assetId'), ref); + + expect(route, isNull); + }); + + test('trashed photo link opens the viewer on a trash timeline', () async { + when( + () => assetService.getRemoteAsset(_assetId), + ).thenAnswer((_) async => _asset.copyWith(deletedAt: DateTime(2026, 6, 13))); + + final route = await sut.handleMyImmichApp(link('/photos/$_assetId'), ref); + + expect(route, isA()); + expect((route!.args! as AssetViewerRouteArgs).timelineService.origin, TimelineOrigin.trash); + }); }