fix(mobile): resolve view intents from remote checksum matches

Use cached remote assets when the local Drift record is unavailable,
preferring active and most recently uploaded matches.
This commit is contained in:
Peter Ombodi 2026-08-06 16:49:32 +03:00
parent f90359655b
commit 595a3a7f28
2 changed files with 52 additions and 7 deletions

View file

@ -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<RemoteAsset?> _resolveRemoteAsset(String localAssetId, String? remoteAssetId) async {
if (remoteAssetId == null) {
return null;
Future<RemoteAsset?> _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.

View file

@ -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<RemoteAsset>());
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,