From 12b7cd066b8824ecf7b17d2e835c0f8ec2ce5fb9 Mon Sep 17 00:00:00 2001 From: Santo Shakil Date: Wed, 17 Jun 2026 00:46:52 +0600 Subject: [PATCH] fix(mobile): show memories with no showAt/hideAt in the timeline lane (#29158) --- .../repositories/memory.repository.dart | 4 +- .../repositories/memory_repository_test.dart | 108 ++++++++++++++++++ mobile/test/medium/repository_context.dart | 36 ++++++ 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 mobile/test/medium/repositories/memory_repository_test.dart diff --git a/mobile/lib/infrastructure/repositories/memory.repository.dart b/mobile/lib/infrastructure/repositories/memory.repository.dart index 0dcf7200cc..37cccbf2d4 100644 --- a/mobile/lib/infrastructure/repositories/memory.repository.dart +++ b/mobile/lib/infrastructure/repositories/memory.repository.dart @@ -25,8 +25,8 @@ class DriftMemoryRepository extends DriftDatabaseRepository { ]) ..where(_db.memoryEntity.ownerId.equals(ownerId)) ..where(_db.memoryEntity.deletedAt.isNull()) - ..where(_db.memoryEntity.showAt.isSmallerOrEqualValue(localUtc)) - ..where(_db.memoryEntity.hideAt.isBiggerOrEqualValue(localUtc)) + ..where(_db.memoryEntity.showAt.isNull() | _db.memoryEntity.showAt.isSmallerOrEqualValue(localUtc)) + ..where(_db.memoryEntity.hideAt.isNull() | _db.memoryEntity.hideAt.isBiggerOrEqualValue(localUtc)) ..orderBy([OrderingTerm.desc(_db.memoryEntity.memoryAt), OrderingTerm.asc(_db.remoteAssetEntity.createdAt)]); final rows = await query.get(); diff --git a/mobile/test/medium/repositories/memory_repository_test.dart b/mobile/test/medium/repositories/memory_repository_test.dart new file mode 100644 index 0000000000..e57098f3bd --- /dev/null +++ b/mobile/test/medium/repositories/memory_repository_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart'; + +import '../repository_context.dart'; + +void main() { + late MediumRepositoryContext ctx; + late DriftMemoryRepository sut; + + setUp(() { + ctx = MediumRepositoryContext(); + sut = DriftMemoryRepository(ctx.db); + }); + + tearDown(() async { + await ctx.dispose(); + }); + + group('getAll', () { + // #24745: memories created via the API have no showAt/hideAt. before the fix + // the window filter (show_at <= now AND hide_at >= now) drops them because a + // NULL comparison is never true. + test('includes a memory with null showAt/hideAt', () async { + final user = await ctx.newUser(); + final asset = await ctx.newRemoteAsset(ownerId: user.id); + final memory = await ctx.newMemory(ownerId: user.id); + await ctx.newMemoryAsset(memoryId: memory.id, assetId: asset.id); + + final result = await sut.getAll(user.id); + + expect(result, hasLength(1)); + expect(result.first.id, memory.id); + expect(result.first.assets.single.id, asset.id); + }); + + test('includes a memory whose window covers today', () async { + final user = await ctx.newUser(); + final asset = await ctx.newRemoteAsset(ownerId: user.id); + final now = DateTime.now().toUtc(); + final memory = await ctx.newMemory( + ownerId: user.id, + showAt: now.subtract(const Duration(days: 10)), + hideAt: now.add(const Duration(days: 10)), + ); + await ctx.newMemoryAsset(memoryId: memory.id, assetId: asset.id); + + final result = await sut.getAll(user.id); + + expect(result.map((m) => m.id), [memory.id]); + }); + + test('excludes a memory whose hideAt is in the past', () async { + final user = await ctx.newUser(); + final asset = await ctx.newRemoteAsset(ownerId: user.id); + final now = DateTime.now().toUtc(); + final memory = await ctx.newMemory( + ownerId: user.id, + showAt: now.subtract(const Duration(days: 20)), + hideAt: now.subtract(const Duration(days: 10)), + ); + await ctx.newMemoryAsset(memoryId: memory.id, assetId: asset.id); + + final result = await sut.getAll(user.id); + + expect(result, isEmpty); + }); + + test('excludes a memory whose showAt is in the future', () async { + final user = await ctx.newUser(); + final asset = await ctx.newRemoteAsset(ownerId: user.id); + final now = DateTime.now().toUtc(); + final memory = await ctx.newMemory( + ownerId: user.id, + showAt: now.add(const Duration(days: 10)), + hideAt: now.add(const Duration(days: 20)), + ); + await ctx.newMemoryAsset(memoryId: memory.id, assetId: asset.id); + + final result = await sut.getAll(user.id); + + expect(result, isEmpty); + }); + + test('includes a memory with showAt in the past and null hideAt', () async { + final user = await ctx.newUser(); + final asset = await ctx.newRemoteAsset(ownerId: user.id); + final now = DateTime.now().toUtc(); + final memory = await ctx.newMemory(ownerId: user.id, showAt: now.subtract(const Duration(days: 10))); + await ctx.newMemoryAsset(memoryId: memory.id, assetId: asset.id); + + final result = await sut.getAll(user.id); + + expect(result.map((m) => m.id), [memory.id]); + }); + + test('excludes a memory with null showAt and hideAt in the past', () async { + final user = await ctx.newUser(); + final asset = await ctx.newRemoteAsset(ownerId: user.id); + final now = DateTime.now().toUtc(); + final memory = await ctx.newMemory(ownerId: user.id, hideAt: now.subtract(const Duration(days: 10))); + await ctx.newMemoryAsset(memoryId: memory.id, assetId: asset.id); + + final result = await sut.getAll(user.id); + + expect(result, isEmpty); + }); + }); +} diff --git a/mobile/test/medium/repository_context.dart b/mobile/test/medium/repository_context.dart index 436a58aaf8..00390d455f 100644 --- a/mobile/test/medium/repository_context.dart +++ b/mobile/test/medium/repository_context.dart @@ -3,11 +3,14 @@ import 'package:drift/native.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; @@ -309,4 +312,37 @@ class MediumRepositoryContext { Future newLocalAlbumAsset({required String albumId, required String assetId}) => db .into(db.localAlbumAssetEntity) .insert(LocalAlbumAssetEntityCompanion(albumId: .new(albumId), assetId: .new(assetId))); + + Future newMemory({ + String? id, + String? ownerId, + MemoryTypeEnum? type, + int? year, + DateTime? memoryAt, + DateTime? showAt, + DateTime? hideAt, + DateTime? deletedAt, + bool? isSaved, + }) async { + id ??= TestUtils.uuid(); + return db + .into(db.memoryEntity) + .insertReturning( + MemoryEntityCompanion( + id: .new(id), + ownerId: .new(TestUtils.uuid(ownerId)), + type: .new(type ?? MemoryTypeEnum.onThisDay), + data: .new(MemoryData(year: year ?? 2020).toJson()), + isSaved: .new(isSaved ?? false), + memoryAt: .new(TestUtils.date(memoryAt)), + showAt: .new(showAt), + hideAt: .new(hideAt), + deletedAt: .new(deletedAt), + ), + ); + } + + Future newMemoryAsset({required String memoryId, required String assetId}) => db + .into(db.memoryAssetEntity) + .insert(MemoryAssetEntityCompanion(memoryId: .new(memoryId), assetId: .new(assetId))); }