From 45eff1c663ae8098418e2c2d209dedeb80c6ff27 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:43:30 +0100 Subject: [PATCH] fix(web): prevent unrelated assets from appearing in tag view (#26816) --- .../timeline-manager/timeline-manager.svelte.spec.ts | 11 +++++++++++ .../timeline-manager/timeline-manager.svelte.ts | 1 + web/src/lib/managers/timeline-manager/types.ts | 1 + web/src/lib/utils/timeline-util.ts | 1 + web/src/test-data/factories/asset-factory.ts | 1 + 5 files changed, 15 insertions(+) diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts index 8e31f28138..8addc173c4 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts @@ -286,6 +286,17 @@ describe('TimelineManager', () => { expect(timelineManager.assetCount).toEqual(1); }); + it('ignores new assets that do not match the tag filter', async () => { + await timelineManager.updateOptions({ tagId: 'tag-1' }); + + const matching = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build({ tags: ['tag-1'] })); + const unrelated = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build({ tags: ['tag-2'] })); + + timelineManager.upsertAssets([matching, unrelated]); + + expect(await getAssets(timelineManager)).toEqual([matching]); + }); + // disabled due to the wasm Justified Layout import it('ignores trashed assets when isTrashed is true', async () => { const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build({ isTrashed: false })); diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index 019290a5c9..38c593bd00 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -596,6 +596,7 @@ export class TimelineManager extends VirtualScrollManager { isMismatched(this.#options.visibility, asset.visibility) || isMismatched(this.#options.isFavorite, asset.isFavorite) || isMismatched(this.#options.isTrashed, asset.isTrashed) || + (this.#options.tagId && asset.tags && !asset.tags.includes(this.#options.tagId)) || (this.#options.assetFilter !== undefined && !this.#options.assetFilter.has(asset.id)) ); } diff --git a/web/src/lib/managers/timeline-manager/types.ts b/web/src/lib/managers/timeline-manager/types.ts index d528bfbdff..c7b0d226d2 100644 --- a/web/src/lib/managers/timeline-manager/types.ts +++ b/web/src/lib/managers/timeline-manager/types.ts @@ -18,6 +18,7 @@ export type Direction = 'earlier' | 'later'; export type TimelineAsset = { id: string; ownerId: string; + tags?: string[]; ratio: number; thumbhash: string | null; localDateTime: TimelineDateTime; diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index deccdd7d6e..d7dc5d6aa4 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -170,6 +170,7 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): return { id: assetResponse.id, ownerId: assetResponse.ownerId, + tags: assetResponse.tags?.map((tag) => tag.id), ratio, thumbhash: assetResponse.thumbhash, localDateTime, diff --git a/web/src/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts index 00dd588243..f3d2e80747 100644 --- a/web/src/test-data/factories/asset-factory.ts +++ b/web/src/test-data/factories/asset-factory.ts @@ -37,6 +37,7 @@ export const timelineAssetFactory = Sync.makeFactory({ id: Sync.each(() => faker.string.uuid()), ratio: Sync.each((i) => 0.2 + ((i * 0.618_034) % 3.8)), // deterministic random float between 0.2 and 4.0 ownerId: Sync.each(() => faker.string.uuid()), + tags: [], thumbhash: Sync.each(() => faker.string.alphanumeric(28)), localDateTime: Sync.each(() => fromISODateTimeUTCToObject(faker.date.past().toISOString())), fileCreatedAt: Sync.each(() => fromISODateTimeUTCToObject(faker.date.past().toISOString())),