diff --git a/mobile/lib/services/view_intent_asset_resolver.service.dart b/mobile/lib/services/view_intent_asset_resolver.service.dart index e44839c2d2..f9d5ed2183 100644 --- a/mobile/lib/services/view_intent_asset_resolver.service.dart +++ b/mobile/lib/services/view_intent_asset_resolver.service.dart @@ -55,7 +55,11 @@ class ViewIntentAssetResolver { ({LocalAsset? asset, String? checksum}) resolvedLocal = (asset: null, checksum: null); if (localAssetId != null) { resolvedLocal = await _resolveLocalAsset(localAssetId); - final remoteAsset = await _resolveRemoteAsset(localAssetId, resolvedLocal.asset?.remoteId); + final remoteAsset = await _resolveRemoteAsset( + localAssetId, + remoteAssetId: resolvedLocal.asset?.remoteId, + checksum: resolvedLocal.checksum, + ); if (remoteAsset != null) { return ViewIntentResolvedAsset(asset: remoteAsset, timelineService: _timelineFor(remoteAsset)); } @@ -111,21 +115,45 @@ class ViewIntentAssetResolver { } } - Future _resolveRemoteAsset(String localAssetId, String? remoteAssetId) async { - if (remoteAssetId == null) { - return null; + Future _resolveRemoteAsset( + String localAssetId, { + required String? remoteAssetId, + required String? checksum, + }) async { + RemoteAsset? remoteAsset; + if (remoteAssetId != null) { + remoteAsset = await _assetService.getRemoteAsset(remoteAssetId); + if (remoteAsset != null) { + _logger.fine('resolve matched remote asset by id: $remoteAssetId, asset=$remoteAsset'); + } + } + + if (remoteAsset == null && checksum != null) { + final candidates = await _assetService.getAllRemoteAssetDebugByChecksum(checksum); + if (candidates.isNotEmpty) { + remoteAsset = ([...candidates]..sort(_compareRemoteAssetCandidates)).first; + _logger.fine('resolve matched remote asset by checksum: $checksum, asset=$remoteAsset'); + } } - final remoteAsset = await _assetService.getRemoteAsset(remoteAssetId); if (remoteAsset == null) { return null; } - final asset = remoteAsset.copyWith(localId: localAssetId); - _logger.fine('resolve matched remote asset by id: $remoteAssetId, asset=$asset'); return asset; } + static int _compareRemoteAssetCandidates(RemoteAsset first, RemoteAsset second) { + if (first.isTrashed != second.isTrashed) { + return first.isTrashed ? 1 : -1; + } + + final firstDate = first.uploadedAt ?? first.createdAt; + final secondDate = second.uploadedAt ?? second.createdAt; + final date = secondDate.compareTo(firstDate); + return date != 0 ? date : first.id.compareTo(second.id); + } + LocalAsset _toTransientAsset(ViewIntentPayload attachment, String? checksum) { final now = DateTime.now(); // A FileBackedAsset could model the path more explicitly, but would require broader changes to the asset hierarchy. diff --git a/mobile/test/services/view_intent_asset_resolver_test.dart b/mobile/test/services/view_intent_asset_resolver_test.dart index 544692fb13..8c8369776a 100644 --- a/mobile/test/services/view_intent_asset_resolver_test.dart +++ b/mobile/test/services/view_intent_asset_resolver_test.dart @@ -44,6 +44,7 @@ void main() { when(() => mockLocalAssetRepository.get(any())).thenAnswer((_) async => null); when(() => assetService.getRemoteAsset(any())).thenAnswer((_) async => null); + when(() => assetService.getAllRemoteAssetDebugByChecksum(any())).thenAnswer((_) async => const []); when(() => nativeSyncApi.hashAssets(any())).thenAnswer((_) async => const []); when(() => mockLocalAssetRepository.updateHashes(any())).thenAnswer((_) async {}); @@ -142,6 +143,22 @@ void main() { expect(result.viewIntentFilePath, '/tmp/incoming.jpg'); }); + test('returns cached remote asset when local Drift row is absent but checksum matches', () async { + final remoteAsset = _remoteAsset(id: 'remote-1', checksum: 'checksum-1'); + when( + () => nativeSyncApi.hashAssets(['local-1']), + ).thenAnswer((_) async => [HashResult(assetId: 'local-1', hash: 'checksum-1')]); + when(() => assetService.getAllRemoteAssetDebugByChecksum('checksum-1')).thenAnswer((_) async => [remoteAsset]); + + final result = await _resolve(container, _payload(localAssetId: 'local-1')); + + expect(result.asset, isA()); + expect((result.asset as RemoteAsset).id, 'remote-1'); + expect((result.asset as RemoteAsset).localId, 'local-1'); + expect(result.timelineService.origin, TimelineOrigin.deepLink); + verify(() => assetService.getAllRemoteAssetDebugByChecksum('checksum-1')).called(1); + }); + test('returns transient asset for path-only attachment', () async { final result = await _resolve( container,