fix(mobile): update trash sync review markers instead of skipping already-reviewed rows

This commit is contained in:
Peter Ombodi 2026-08-10 17:24:25 +03:00
parent 0a699d0d0e
commit 6da802d22e
3 changed files with 25 additions and 12 deletions

View file

@ -135,15 +135,6 @@ class DriftTrashSyncRepository extends DriftDatabaseRepository {
Future<void> _recordReviewAssets(Expression<bool> contentExists, {Expression<DateTime>? remoteDeletedAt}) async {
final pending = Constant(TrashSyncStatus.pending.index);
final selectedAssetsQuery = _selectedAssetsQuery();
final reviewDecisionsQuery = _db.selectOnly(_db.trashSyncEntity)
..addColumns([_db.trashSyncEntity.assetId])
..where(
_db.trashSyncEntity.assetId.equalsExp(_db.localAssetEntity.id) &
_db.trashSyncEntity.status.isIn([
TrashSyncStatus.reviewRejected.index,
TrashSyncStatus.reviewApproved.index,
]),
);
final nonPendingMarkerQuery = _db.selectOnly(_db.trashSyncEntity)
..addColumns([_db.trashSyncEntity.assetId])
..where(
@ -156,7 +147,6 @@ class DriftTrashSyncRepository extends DriftDatabaseRepository {
_db.localAssetEntity.checksum.isNotNull() &
contentExists &
existsQuery(selectedAssetsQuery) &
notExistsQuery(reviewDecisionsQuery) &
notExistsQuery(nonPendingMarkerQuery),
);
if (remoteDeletedAt != null) {
@ -184,7 +174,15 @@ class DriftTrashSyncRepository extends DriftDatabaseRepository {
_db.trashSyncEntity.assetUpdatedAt: _db.localAssetEntity.updatedAt,
if (remoteDeletedAt != null) _db.trashSyncEntity.remoteDeletedAt: remoteDeletedAt,
},
mode: .insertOrReplace,
onConflict: DoUpdate.withExcluded(
(old, excluded) => remoteDeletedAt != null
? TrashSyncEntityCompanion.custom(
status: excluded.status,
assetUpdatedAt: excluded.assetUpdatedAt,
remoteDeletedAt: excluded.remoteDeletedAt,
)
: TrashSyncEntityCompanion.custom(status: excluded.status, assetUpdatedAt: excluded.assetUpdatedAt),
),
);
}

View file

@ -63,7 +63,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
ActionIconButtonWidget(action: FavoriteAction(assets: assetForAction)),
ViewerKebabMenu(originalTheme: originalTheme),
ImmichColorOverride(color: null, child: ViewerKebabMenu(originalTheme: originalTheme)),
];
final lockedViewActions = <Widget>[ViewerKebabMenu(originalTheme: originalTheme)];

View file

@ -195,6 +195,21 @@ void main() {
expect(rows.single.assetUpdatedAt, existingUpdatedAt);
});
test('preserves remoteDeletedAt when a hard-delete pass matches an already pending review marker', () async {
final existingDeletedAt = DateTime(2026, 1, 1);
final asset = await backedUpAsset(ownerId: userId, remoteDeletedAt: existingDeletedAt);
await sut.recordSoftDeleteReviewAssets();
// Simulates the same checksum also being recorded as hard-deleted (e.g. reused content),
// without the remote row being removed first, so the pending marker stays matchable by both passes.
await sut.recordHardDeletedChecksums([asset.remoteId]);
await sut.recordHardDeletedReviewAssets();
final rows = await ctx.db.select(ctx.db.trashSyncEntity).get();
expect(rows.single.status, TrashSyncStatus.pending);
expect(rows.single.remoteDeletedAt, existingDeletedAt.toUtc());
});
test('rejecting an asset without a pending marker does not create a review decision', () async {
final asset = await backedUpAsset(ownerId: userId, remoteDeletedAt: DateTime(2026, 1, 1));