diff --git a/mobile/lib/infrastructure/repositories/trash_sync.repository.dart b/mobile/lib/infrastructure/repositories/trash_sync.repository.dart index 6c57ecc37b..0f91288115 100644 --- a/mobile/lib/infrastructure/repositories/trash_sync.repository.dart +++ b/mobile/lib/infrastructure/repositories/trash_sync.repository.dart @@ -135,15 +135,6 @@ class DriftTrashSyncRepository extends DriftDatabaseRepository { Future _recordReviewAssets(Expression contentExists, {Expression? 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), + ), ); } diff --git a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart index 8d51b8cd2e..5997e15bf0 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart @@ -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 = [ViewerKebabMenu(originalTheme: originalTheme)]; diff --git a/mobile/test/medium/repositories/trash_sync_repository_test.dart b/mobile/test/medium/repositories/trash_sync_repository_test.dart index 31c2642995..102ac88195 100644 --- a/mobile/test/medium/repositories/trash_sync_repository_test.dart +++ b/mobile/test/medium/repositories/trash_sync_repository_test.dart @@ -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));