From 8d4904d120765fb2923263d38d5f0260fa087e9c Mon Sep 17 00:00:00 2001 From: idubnori Date: Fri, 7 Nov 2025 09:30:17 +0900 Subject: [PATCH 1/3] feat: add activity deep link support in DeepLinkService --- mobile/lib/services/deep_link.service.dart | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mobile/lib/services/deep_link.service.dart b/mobile/lib/services/deep_link.service.dart index d67362aac2078..6ede7f6830017 100644 --- a/mobile/lib/services/deep_link.service.dart +++ b/mobile/lib/services/deep_link.service.dart @@ -77,6 +77,7 @@ class DeepLinkService { "memory" => await _buildMemoryDeepLink(queryParams['id'] ?? ''), "asset" => await _buildAssetDeepLink(queryParams['id'] ?? '', ref), "album" => await _buildAlbumDeepLink(queryParams['id'] ?? ''), + "activity" => await _buildActivityDeepLink(queryParams['albumId'] ?? ''), _ => null, }; @@ -185,4 +186,18 @@ class DeepLinkService { return AlbumViewerRoute(albumId: album.id); } } + + Future _buildActivityDeepLink(String albumId) async { + if (Store.isBetaTimelineEnabled == false) { + return null; + } + + final album = await _betaRemoteAlbumService.get(albumId); + + if (album == null || album.isActivityEnabled == false) { + return null; + } + + return DriftActivitiesRoute(album: album); + } } From 0b1914be9ad03c545fc5a32ee3aecd0181e4dca5 Mon Sep 17 00:00:00 2001 From: idubnori Date: Fri, 7 Nov 2025 09:30:40 +0900 Subject: [PATCH 2/3] test: add unit tests for DeepLinkService handling of activity deep links --- .../test/services/deep_link.service_test.dart | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 mobile/test/services/deep_link.service_test.dart diff --git a/mobile/test/services/deep_link.service_test.dart b/mobile/test/services/deep_link.service_test.dart new file mode 100644 index 0000000000000..475da9688f6c8 --- /dev/null +++ b/mobile/test/services/deep_link.service_test.dart @@ -0,0 +1,166 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/asset.service.dart' as beta_asset_service; +import 'package:immich_mobile/domain/services/memory.service.dart'; +import 'package:immich_mobile/domain/services/remote_album.service.dart'; +import 'package:immich_mobile/domain/services/timeline.service.dart'; +import 'package:immich_mobile/providers/album/current_album.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/services/album.service.dart'; +import 'package:immich_mobile/services/asset.service.dart'; +import 'package:immich_mobile/services/deep_link.service.dart'; +import 'package:immich_mobile/services/memory.service.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../utils/action_button_utils_test.dart'; + +class MockMemoryService extends Mock implements MemoryService {} + +class MockAssetService extends Mock implements AssetService {} + +class MockAlbumService extends Mock implements AlbumService {} + +class MockCurrentAsset extends Mock implements CurrentAsset {} + +class MockCurrentAlbum extends Mock implements CurrentAlbum {} + +class MockTimelineFactory extends Mock implements TimelineFactory {} + +class MockBetaAssetService extends Mock implements beta_asset_service.AssetService {} + +class MockRemoteAlbumService extends Mock implements RemoteAlbumService {} + +class MockDriftMemoryService extends Mock implements DriftMemoryService {} + +class MockPlatformDeepLink extends Mock implements PlatformDeepLink { + final Uri _uri; + + MockPlatformDeepLink(this._uri); + + @override + Uri get uri => _uri; +} + +class MockWidgetRef extends Mock implements WidgetRef {} + +void main() { + late DeepLinkService sut; + late MockMemoryService mockMemoryService; + late MockAssetService mockAssetService; + late MockAlbumService mockAlbumService; + late MockCurrentAsset mockCurrentAsset; + late MockCurrentAlbum mockCurrentAlbum; + late MockTimelineFactory mockTimelineFactory; + late MockBetaAssetService mockBetaAssetService; + late MockRemoteAlbumService mockRemoteAlbumService; + late MockDriftMemoryService mockDriftMemoryService; + late ProviderContainer container; + + setUp(() { + mockMemoryService = MockMemoryService(); + mockAssetService = MockAssetService(); + mockAlbumService = MockAlbumService(); + mockCurrentAsset = MockCurrentAsset(); + mockCurrentAlbum = MockCurrentAlbum(); + mockTimelineFactory = MockTimelineFactory(); + mockBetaAssetService = MockBetaAssetService(); + mockRemoteAlbumService = MockRemoteAlbumService(); + mockDriftMemoryService = MockDriftMemoryService(); + + sut = DeepLinkService( + mockMemoryService, + mockAssetService, + mockAlbumService, + mockCurrentAsset, + mockCurrentAlbum, + mockTimelineFactory, + mockBetaAssetService, + mockRemoteAlbumService, + mockDriftMemoryService, + ); + + container = ProviderContainer(); + }); + + tearDown(() { + container.dispose(); + }); + + group('handleScheme - activity', () { + test('should return DeepLink with DriftActivitiesRoute when album exists and isActivityEnabled is true', () async { + final albumId = 'test-album-id'; + final album = createRemoteAlbum(id: albumId, name: 'Test Album', isActivityEnabled: true); + final link = MockPlatformDeepLink(Uri.parse('immich://activity?albumId=$albumId')); + final ref = MockWidgetRef(); + + when(() => mockRemoteAlbumService.get(albumId)).thenAnswer((_) async => album); + + final result = await sut.handleScheme(link, ref, false); + + expect(result, isNotNull); + expect(result, isA()); + // DeepLink is a list-like structure, we can check it's not none or defaultPath + expect(result, isNot(equals(DeepLink.none))); + expect(result, isNot(equals(DeepLink.defaultPath))); + verify(() => mockRemoteAlbumService.get(albumId)).called(1); + }); + + test('should return DeepLink.none when album does not exist', () async { + final albumId = 'non-existent-album-id'; + final link = MockPlatformDeepLink(Uri.parse('immich://activity?albumId=$albumId')); + final ref = MockWidgetRef(); + + when(() => mockRemoteAlbumService.get(albumId)).thenAnswer((_) async => null); + + final result = await sut.handleScheme(link, ref, false); + + expect(result, equals(DeepLink.none)); + verify(() => mockRemoteAlbumService.get(albumId)).called(1); + }); + + test('should return DeepLink.none when album exists but isActivityEnabled is false', () async { + final albumId = 'test-album-id'; + final album = createRemoteAlbum(id: albumId, name: 'Test Album', isActivityEnabled: false); + final link = MockPlatformDeepLink(Uri.parse('immich://activity?albumId=$albumId')); + final ref = MockWidgetRef(); + + when(() => mockRemoteAlbumService.get(albumId)).thenAnswer((_) async => album); + + final result = await sut.handleScheme(link, ref, false); + + expect(result, equals(DeepLink.none)); + verify(() => mockRemoteAlbumService.get(albumId)).called(1); + }); + + test('should return DeepLink.defaultPath when album does not exist and isColdStart is true', () async { + final albumId = 'non-existent-album-id'; + final link = MockPlatformDeepLink(Uri.parse('immich://activity?albumId=$albumId')); + final ref = MockWidgetRef(); + + when(() => mockRemoteAlbumService.get(albumId)).thenAnswer((_) async => null); + + final result = await sut.handleScheme(link, ref, true); + + expect(result, equals(DeepLink.defaultPath)); + verify(() => mockRemoteAlbumService.get(albumId)).called(1); + }); + + test( + 'should return DeepLink.defaultPath when album exists but isActivityEnabled is false and isColdStart is true', + () async { + final albumId = 'test-album-id'; + final album = createRemoteAlbum(id: albumId, name: 'Test Album', isActivityEnabled: false); + final link = MockPlatformDeepLink(Uri.parse('immich://activity?albumId=$albumId')); + final ref = MockWidgetRef(); + + when(() => mockRemoteAlbumService.get(albumId)).thenAnswer((_) async => album); + + final result = await sut.handleScheme(link, ref, true); + + expect(result, equals(DeepLink.defaultPath)); + verify(() => mockRemoteAlbumService.get(albumId)).called(1); + }, + ); + }); +} From 2ededf2e403443c4de2eaf4c3a4995b2eceb3ce2 Mon Sep 17 00:00:00 2001 From: idubnori Date: Mon, 10 Nov 2025 00:58:07 +0900 Subject: [PATCH 3/3] Revert "test: add unit tests for DeepLinkService handling of activity deep links" This reverts commit 0b1914be9ad03c545fc5a32ee3aecd0181e4dca5. --- .../test/services/deep_link.service_test.dart | 166 ------------------ 1 file changed, 166 deletions(-) delete mode 100644 mobile/test/services/deep_link.service_test.dart diff --git a/mobile/test/services/deep_link.service_test.dart b/mobile/test/services/deep_link.service_test.dart deleted file mode 100644 index 475da9688f6c8..0000000000000 --- a/mobile/test/services/deep_link.service_test.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/asset.service.dart' as beta_asset_service; -import 'package:immich_mobile/domain/services/memory.service.dart'; -import 'package:immich_mobile/domain/services/remote_album.service.dart'; -import 'package:immich_mobile/domain/services/timeline.service.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset.service.dart'; -import 'package:immich_mobile/services/deep_link.service.dart'; -import 'package:immich_mobile/services/memory.service.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../utils/action_button_utils_test.dart'; - -class MockMemoryService extends Mock implements MemoryService {} - -class MockAssetService extends Mock implements AssetService {} - -class MockAlbumService extends Mock implements AlbumService {} - -class MockCurrentAsset extends Mock implements CurrentAsset {} - -class MockCurrentAlbum extends Mock implements CurrentAlbum {} - -class MockTimelineFactory extends Mock implements TimelineFactory {} - -class MockBetaAssetService extends Mock implements beta_asset_service.AssetService {} - -class MockRemoteAlbumService extends Mock implements RemoteAlbumService {} - -class MockDriftMemoryService extends Mock implements DriftMemoryService {} - -class MockPlatformDeepLink extends Mock implements PlatformDeepLink { - final Uri _uri; - - MockPlatformDeepLink(this._uri); - - @override - Uri get uri => _uri; -} - -class MockWidgetRef extends Mock implements WidgetRef {} - -void main() { - late DeepLinkService sut; - late MockMemoryService mockMemoryService; - late MockAssetService mockAssetService; - late MockAlbumService mockAlbumService; - late MockCurrentAsset mockCurrentAsset; - late MockCurrentAlbum mockCurrentAlbum; - late MockTimelineFactory mockTimelineFactory; - late MockBetaAssetService mockBetaAssetService; - late MockRemoteAlbumService mockRemoteAlbumService; - late MockDriftMemoryService mockDriftMemoryService; - late ProviderContainer container; - - setUp(() { - mockMemoryService = MockMemoryService(); - mockAssetService = MockAssetService(); - mockAlbumService = MockAlbumService(); - mockCurrentAsset = MockCurrentAsset(); - mockCurrentAlbum = MockCurrentAlbum(); - mockTimelineFactory = MockTimelineFactory(); - mockBetaAssetService = MockBetaAssetService(); - mockRemoteAlbumService = MockRemoteAlbumService(); - mockDriftMemoryService = MockDriftMemoryService(); - - sut = DeepLinkService( - mockMemoryService, - mockAssetService, - mockAlbumService, - mockCurrentAsset, - mockCurrentAlbum, - mockTimelineFactory, - mockBetaAssetService, - mockRemoteAlbumService, - mockDriftMemoryService, - ); - - container = ProviderContainer(); - }); - - tearDown(() { - container.dispose(); - }); - - group('handleScheme - activity', () { - test('should return DeepLink with DriftActivitiesRoute when album exists and isActivityEnabled is true', () async { - final albumId = 'test-album-id'; - final album = createRemoteAlbum(id: albumId, name: 'Test Album', isActivityEnabled: true); - final link = MockPlatformDeepLink(Uri.parse('immich://activity?albumId=$albumId')); - final ref = MockWidgetRef(); - - when(() => mockRemoteAlbumService.get(albumId)).thenAnswer((_) async => album); - - final result = await sut.handleScheme(link, ref, false); - - expect(result, isNotNull); - expect(result, isA()); - // DeepLink is a list-like structure, we can check it's not none or defaultPath - expect(result, isNot(equals(DeepLink.none))); - expect(result, isNot(equals(DeepLink.defaultPath))); - verify(() => mockRemoteAlbumService.get(albumId)).called(1); - }); - - test('should return DeepLink.none when album does not exist', () async { - final albumId = 'non-existent-album-id'; - final link = MockPlatformDeepLink(Uri.parse('immich://activity?albumId=$albumId')); - final ref = MockWidgetRef(); - - when(() => mockRemoteAlbumService.get(albumId)).thenAnswer((_) async => null); - - final result = await sut.handleScheme(link, ref, false); - - expect(result, equals(DeepLink.none)); - verify(() => mockRemoteAlbumService.get(albumId)).called(1); - }); - - test('should return DeepLink.none when album exists but isActivityEnabled is false', () async { - final albumId = 'test-album-id'; - final album = createRemoteAlbum(id: albumId, name: 'Test Album', isActivityEnabled: false); - final link = MockPlatformDeepLink(Uri.parse('immich://activity?albumId=$albumId')); - final ref = MockWidgetRef(); - - when(() => mockRemoteAlbumService.get(albumId)).thenAnswer((_) async => album); - - final result = await sut.handleScheme(link, ref, false); - - expect(result, equals(DeepLink.none)); - verify(() => mockRemoteAlbumService.get(albumId)).called(1); - }); - - test('should return DeepLink.defaultPath when album does not exist and isColdStart is true', () async { - final albumId = 'non-existent-album-id'; - final link = MockPlatformDeepLink(Uri.parse('immich://activity?albumId=$albumId')); - final ref = MockWidgetRef(); - - when(() => mockRemoteAlbumService.get(albumId)).thenAnswer((_) async => null); - - final result = await sut.handleScheme(link, ref, true); - - expect(result, equals(DeepLink.defaultPath)); - verify(() => mockRemoteAlbumService.get(albumId)).called(1); - }); - - test( - 'should return DeepLink.defaultPath when album exists but isActivityEnabled is false and isColdStart is true', - () async { - final albumId = 'test-album-id'; - final album = createRemoteAlbum(id: albumId, name: 'Test Album', isActivityEnabled: false); - final link = MockPlatformDeepLink(Uri.parse('immich://activity?albumId=$albumId')); - final ref = MockWidgetRef(); - - when(() => mockRemoteAlbumService.get(albumId)).thenAnswer((_) async => album); - - final result = await sut.handleScheme(link, ref, true); - - expect(result, equals(DeepLink.defaultPath)); - verify(() => mockRemoteAlbumService.get(albumId)).called(1); - }, - ); - }); -}