From ab8049e308fbecea653b16523d83534ad76609ad Mon Sep 17 00:00:00 2001 From: priyanshuANDcoad Date: Fri, 24 Jul 2026 01:15:53 +0530 Subject: [PATCH 1/3] fix(mobile): ignore trashed assets in album date range and sorting queries --- .../repositories/remote_album.repository.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index b9cd41c960..b62fb0b1bf 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -222,7 +222,11 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) ..addColumns([_db.remoteAssetEntity.createdAt.min(), _db.remoteAssetEntity.createdAt.max()]) ..join([ - innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)), + innerJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) & + _db.remoteAssetEntity.deletedAt.isNull(), + ), ]); return query.map((row) { @@ -272,7 +276,11 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { Future> getAssets(String albumId) { final query = _db.remoteAlbumAssetEntity.select().join([ - innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)), + innerJoin( + _db.remoteAssetEntity, + _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) & + _db.remoteAssetEntity.deletedAt.isNull(), + ), ])..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)); return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); @@ -425,6 +433,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { ON raae.album_id = ids.value INNER JOIN remote_asset_entity rae ON rae.id = raae.asset_id + WHERE rae.deleted_at IS NULL GROUP BY raae.album_id ORDER BY asset_date ASC ''', From 82c55e5c5fca9f6091714f3babd4c5067b852b74 Mon Sep 17 00:00:00 2001 From: priyanshuANDcoad Date: Sat, 8 Aug 2026 00:28:35 +0530 Subject: [PATCH 2/3] fix(mobile): apply visibility filters, local time date range, and test improvements --- .../repositories/remote_album.repository.dart | 40 ++++-- .../remote_album_repository_test.dart | 131 ++++++++++++++++++ 2 files changed, 157 insertions(+), 14 deletions(-) diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart index b62fb0b1bf..6a1a272d35 100644 --- a/mobile/lib/infrastructure/repositories/remote_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -34,7 +34,8 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { leftOuterJoin( _db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) & - _db.remoteAssetEntity.deletedAt.isNull(), + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility.isIn([AssetVisibility.archive.index, AssetVisibility.timeline.index]), useColumns: false, ), leftOuterJoin( @@ -94,7 +95,11 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { leftOuterJoin( _db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) & - _db.remoteAssetEntity.deletedAt.isNull(), + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility.isIn([ + AssetVisibility.archive.index, + AssetVisibility.timeline.index, + ]), useColumns: false, ), leftOuterJoin( @@ -218,20 +223,22 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { } Stream<(DateTime, DateTime)> watchDateRange(String albumId) { + final dateExp = coalesce([_db.remoteAssetEntity.localDateTime, _db.remoteAssetEntity.createdAt]); final query = _db.remoteAlbumAssetEntity.selectOnly() ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) - ..addColumns([_db.remoteAssetEntity.createdAt.min(), _db.remoteAssetEntity.createdAt.max()]) + ..addColumns([dateExp.min(), dateExp.max()]) ..join([ innerJoin( _db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) & - _db.remoteAssetEntity.deletedAt.isNull(), + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility.isIn([AssetVisibility.archive.index, AssetVisibility.timeline.index]), ), ]); return query.map((row) { - final minDate = row.read(_db.remoteAssetEntity.createdAt.min()); - final maxDate = row.read(_db.remoteAssetEntity.createdAt.max()); + final minDate = row.read(dateExp.min()); + final maxDate = row.read(dateExp.max()); return (minDate ?? DateTime.now(), maxDate ?? DateTime.now()); }).watchSingle(); } @@ -276,11 +283,7 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { Future> getAssets(String albumId) { final query = _db.remoteAlbumAssetEntity.select().join([ - innerJoin( - _db.remoteAssetEntity, - _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) & - _db.remoteAssetEntity.deletedAt.isNull(), - ), + innerJoin(_db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId)), ])..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)); return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); @@ -427,17 +430,22 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { ''' SELECT raae.album_id, - $sqlAgg(rae.local_date_time) AS asset_date + $sqlAgg(COALESCE(rae.local_date_time, rae.created_at)) AS asset_date FROM json_each(?) ids INNER JOIN remote_album_asset_entity raae ON raae.album_id = ids.value INNER JOIN remote_asset_entity rae ON rae.id = raae.asset_id WHERE rae.deleted_at IS NULL + AND rae.visibility IN (?, ?) GROUP BY raae.album_id ORDER BY asset_date ASC ''', - variables: [Variable(jsonIds)], + variables: [ + Variable(jsonIds), + Variable(AssetVisibility.timeline.index), + Variable(AssetVisibility.archive.index), + ], readsFrom: {_db.remoteAlbumAssetEntity, _db.remoteAssetEntity}, ) .get(); @@ -539,7 +547,11 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository { leftOuterJoin( _db.remoteAssetEntity, _db.remoteAssetEntity.id.equalsExp(_db.remoteAlbumAssetEntity.assetId) & - _db.remoteAssetEntity.deletedAt.isNull(), + _db.remoteAssetEntity.deletedAt.isNull() & + _db.remoteAssetEntity.visibility.isIn([ + AssetVisibility.archive.index, + AssetVisibility.timeline.index, + ]), useColumns: false, ), leftOuterJoin( diff --git a/mobile/test/medium/repositories/remote_album_repository_test.dart b/mobile/test/medium/repositories/remote_album_repository_test.dart index 5a82ff315f..2ae84039e0 100644 --- a/mobile/test/medium/repositories/remote_album_repository_test.dart +++ b/mobile/test/medium/repositories/remote_album_repository_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/constants/enums.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import '../repository_context.dart'; @@ -86,6 +87,24 @@ void main() { expect(albums.first.id, album.id); expect(albums.first.assetCount, 0); }); + + test('excludes non-default visibility assets (hidden/locked) from assetCount', () async { + final user = await ctx.newUser(); + final album = await ctx.newRemoteAlbum(ownerId: user.id); + final active = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.timeline); + final archived = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.archive); + final hidden = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.hidden); + final locked = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.locked); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active.id); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: archived.id); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: hidden.id); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: locked.id); + + final albums = await sut.getAll(); + + expect(albums, hasLength(1)); + expect(albums.first.assetCount, 2); + }); }); group('get', () { @@ -101,6 +120,20 @@ void main() { expect(result?.id, album.id); expect(result?.assetCount, 0); }); + + test('excludes non-default visibility assets (hidden/locked) from assetCount', () async { + final user = await ctx.newUser(); + final album = await ctx.newRemoteAlbum(ownerId: user.id); + final active = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.timeline); + final hidden = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.hidden); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active.id); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: hidden.id); + + final result = await sut.get(album.id); + + expect(result, isNotNull); + expect(result?.assetCount, 1); + }); }); group('getAlbumsContainingAsset', () { @@ -130,6 +163,58 @@ void main() { expect(albums, hasLength(1)); expect(albums.first.assetCount, 0); }); + + test('excludes non-default visibility assets from assetCount', () async { + final user = await ctx.newUser(); + final album = await ctx.newRemoteAlbum(ownerId: user.id); + final active = await ctx.newRemoteAsset(ownerId: user.id); + final hidden = await ctx.newRemoteAsset(ownerId: user.id, visibility: AssetVisibility.hidden); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active.id); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: hidden.id); + + final albums = await sut.getAlbumsContainingAsset(active.id); + + expect(albums, hasLength(1)); + expect(albums.first.assetCount, 1); + }); + }); + + group('watchDateRange', () { + test('excludes trashed and hidden/locked assets from date range calculation', () async { + final user = await ctx.newUser(); + final album = await ctx.newRemoteAlbum(ownerId: user.id); + + final trashed = await ctx.newRemoteAsset( + ownerId: user.id, + createdAt: DateTime(2023, 1, 1), + deletedAt: DateTime(2025, 1, 1), + ); + final active1 = await ctx.newRemoteAsset( + ownerId: user.id, + createdAt: DateTime(2024, 5, 1), + visibility: AssetVisibility.timeline, + ); + final active2 = await ctx.newRemoteAsset( + ownerId: user.id, + createdAt: DateTime(2024, 10, 1), + visibility: AssetVisibility.archive, + ); + final hidden = await ctx.newRemoteAsset( + ownerId: user.id, + createdAt: DateTime(2024, 12, 1), + visibility: AssetVisibility.hidden, + ); + + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: trashed.id); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active1.id); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: active2.id); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: hidden.id); + + final range = await sut.watchDateRange(album.id).first; + + expect(range.$1.toUtc(), active1.createdAt.toUtc()); + expect(range.$2.toUtc(), active2.createdAt.toUtc()); + }); }); group('getSortedAlbumIds', () { @@ -321,5 +406,51 @@ void main() { // album2 (Jan 1) should come before album1 (Jan 25) expect(resultEnd, [album2.id, album1.id]); }); + + test('ignores trashed assets when sorting albums by date', () async { + // Album 1: one active asset at Jan 10, one trashed asset at Jan 1 (older) + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final trashedEarly = await ctx.newRemoteAsset( + ownerId: userId, + createdAt: DateTime(2024, 1, 1), + deletedAt: DateTime(2025, 1, 1), + ); + final active = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + await ctx.newRemoteAlbumAsset(albumId: album1.id, assetId: trashedEarly.id); + await ctx.newRemoteAlbumAsset(albumId: album1.id, assetId: active.id); + + // Album 2: one active asset at Jan 5 (earlier than album1's active asset) + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final active2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 5)); + await ctx.newRemoteAlbumAsset(albumId: album2.id, assetId: active2.id); + + final result = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.start); + + // album2 (Jan 5) should come before album1 (Jan 10, ignoring trashed Jan 1) + expect(result, [album2.id, album1.id]); + }); + + test('ignores hidden and locked assets when sorting albums by date', () async { + // Album 1: active asset at Jan 20, hidden asset at Jan 1 (should be ignored) + final album1 = await ctx.newRemoteAlbum(ownerId: userId); + final hiddenEarly = await ctx.newRemoteAsset( + ownerId: userId, + createdAt: DateTime(2024, 1, 1), + visibility: AssetVisibility.hidden, + ); + final activeAlbum1 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 20)); + await ctx.newRemoteAlbumAsset(albumId: album1.id, assetId: hiddenEarly.id); + await ctx.newRemoteAlbumAsset(albumId: album1.id, assetId: activeAlbum1.id); + + // Album 2: one active asset at Jan 10 (earlier than album1's visible start) + final album2 = await ctx.newRemoteAlbum(ownerId: userId); + final activeAlbum2 = await ctx.newRemoteAsset(ownerId: userId, createdAt: DateTime(2024, 1, 10)); + await ctx.newRemoteAlbumAsset(albumId: album2.id, assetId: activeAlbum2.id); + + final result = await sut.getSortedAlbumIds([album1.id, album2.id], aggregation: AssetDateAggregation.start); + + // album2 (Jan 10) before album1 (Jan 20, ignoring hidden Jan 1) + expect(result, [album2.id, album1.id]); + }); }); } From aa8c9531f3af24ae3887a3832c84250392216348 Mon Sep 17 00:00:00 2001 From: priyanshuANDcoad Date: Sat, 8 Aug 2026 01:02:46 +0530 Subject: [PATCH 3/3] test(mobile): verify watchDateRange localDateTime prioritization and visibility filtering --- .../remote_album_repository_test.dart | 37 ++++++++++++++++-- mobile/test/medium/repository_context.dart | 39 ++++++++++--------- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/mobile/test/medium/repositories/remote_album_repository_test.dart b/mobile/test/medium/repositories/remote_album_repository_test.dart index 2ae84039e0..8ed3c75d14 100644 --- a/mobile/test/medium/repositories/remote_album_repository_test.dart +++ b/mobile/test/medium/repositories/remote_album_repository_test.dart @@ -191,12 +191,14 @@ void main() { ); final active1 = await ctx.newRemoteAsset( ownerId: user.id, - createdAt: DateTime(2024, 5, 1), + createdAt: DateTime(2024, 5, 10), + localDateTime: DateTime(2024, 5, 1), visibility: AssetVisibility.timeline, ); final active2 = await ctx.newRemoteAsset( ownerId: user.id, - createdAt: DateTime(2024, 10, 1), + createdAt: DateTime(2024, 10, 10), + localDateTime: DateTime(2024, 10, 1), visibility: AssetVisibility.archive, ); final hidden = await ctx.newRemoteAsset( @@ -212,8 +214,35 @@ void main() { final range = await sut.watchDateRange(album.id).first; - expect(range.$1.toUtc(), active1.createdAt.toUtc()); - expect(range.$2.toUtc(), active2.createdAt.toUtc()); + expect(range.$1.toUtc(), active1.localDateTime!.toUtc()); + expect(range.$2.toUtc(), active2.localDateTime!.toUtc()); + }); + + test('prioritizes localDateTime over createdAt for date range calculation', () async { + final user = await ctx.newUser(); + final album = await ctx.newRemoteAlbum(ownerId: user.id); + + final asset1 = await ctx.newRemoteAsset( + ownerId: user.id, + createdAt: DateTime(2024, 5, 10), + localDateTime: DateTime(2024, 5, 1), + visibility: AssetVisibility.timeline, + ); + final asset2 = await ctx.newRemoteAsset( + ownerId: user.id, + createdAt: DateTime(2024, 10, 10), + localDateTime: DateTime(2024, 10, 1), + visibility: AssetVisibility.archive, + ); + + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: asset1.id); + await ctx.newRemoteAlbumAsset(albumId: album.id, assetId: asset2.id); + + final range = await sut.watchDateRange(album.id).first; + + // Dates should match localDateTime, not createdAt + expect(range.$1.toUtc(), DateTime(2024, 5, 1).toUtc()); + expect(range.$2.toUtc(), DateTime(2024, 10, 1).toUtc()); }); }); diff --git a/mobile/test/medium/repository_context.dart b/mobile/test/medium/repository_context.dart index ed06774e82..747de312c2 100644 --- a/mobile/test/medium/repository_context.dart +++ b/mobile/test/medium/repository_context.dart @@ -102,6 +102,7 @@ class MediumRepositoryContext { String? stackId, String? thumbHash, String? libraryId, + DateTime? localDateTime, }) async { id ??= TestUtils.uuid(); createdAt ??= TestUtils.date(); @@ -109,25 +110,25 @@ class MediumRepositoryContext { .into(db.remoteAssetEntity) .insertReturning( RemoteAssetEntityCompanion( - id: .new(id), - name: .new('remote_$id.jpg'), - checksum: .new(TestUtils.uuid(checksum)), - type: .new(type ?? .image), - createdAt: .new(createdAt), - updatedAt: .new(TestUtils.date(updatedAt)), - ownerId: .new(TestUtils.uuid(ownerId)), - visibility: .new(visibility ?? .timeline), - deletedAt: .new(deletedAt), - durationMs: .new(durationMs ?? 0), - width: .new(width ?? TestUtils.randInt(1000)), - height: .new(height ?? TestUtils.randInt(1000)), - isFavorite: .new(isFavorite ?? false), - isEdited: .new(isEdited ?? false), - livePhotoVideoId: .new(livePhotoVideoId), - stackId: .new(stackId), - localDateTime: .new(createdAt.toLocal()), - thumbHash: .new(TestUtils.uuid(thumbHash)), - libraryId: .new(TestUtils.uuid(libraryId)), + id: Value(id), + name: Value('remote_$id.jpg'), + checksum: Value(TestUtils.uuid(checksum)), + type: Value(type ?? AssetType.image), + createdAt: Value(createdAt), + updatedAt: Value(TestUtils.date(updatedAt)), + ownerId: Value(TestUtils.uuid(ownerId)), + visibility: Value(visibility ?? AssetVisibility.timeline), + deletedAt: Value(deletedAt), + durationMs: Value(durationMs ?? 0), + width: Value(width ?? TestUtils.randInt(1000)), + height: Value(height ?? TestUtils.randInt(1000)), + isFavorite: Value(isFavorite ?? false), + isEdited: Value(isEdited ?? false), + livePhotoVideoId: Value(livePhotoVideoId), + stackId: Value(stackId), + localDateTime: Value(localDateTime ?? createdAt.toLocal()), + thumbHash: Value(TestUtils.uuid(thumbHash)), + libraryId: Value(TestUtils.uuid(libraryId)), ), ); }