From 040ae6bc0ee4ededc9862b70a64bdeac39ab4e70 Mon Sep 17 00:00:00 2001 From: Yohanes Kirana Date: Thu, 3 Sep 2026 18:23:29 +0200 Subject: [PATCH] fix(web): partner sharing timeline (#31241) * fix(web): prevent websocket assets from leaking into scoped timelines * test: add 1 more test case to make sure the changes didnt break the logic --- .../internal/websocket-support.svelte.ts | 4 +- .../timeline-manager.svelte.spec.ts | 79 +++++++++++++++++++ .../timeline-manager.svelte.ts | 19 +++++ 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/web/src/lib/managers/timeline-manager/internal/websocket-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/websocket-support.svelte.ts index 0a004b1045..bc9b3a843f 100644 --- a/web/src/lib/managers/timeline-manager/internal/websocket-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/websocket-support.svelte.ts @@ -13,10 +13,10 @@ export class WebsocketSupport { #processPendingChanges = throttle(() => { const { add, update, remove } = this.#getPendingChangeBatches(); if (add.length > 0) { - this.#timelineManager.upsertAssets(add); + this.#timelineManager.upsertAssetsFromLiveEvent(add); } if (update.length > 0) { - this.#timelineManager.upsertAssets(update); + this.#timelineManager.upsertAssetsFromLiveEvent(update); } if (remove.length > 0) { this.#timelineManager.removeAssets(remove); 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 f44f8a3046..15b6ffb053 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 @@ -485,6 +485,85 @@ describe('TimelineManager', () => { }); }); + describe('live event asset insertion', () => { + let timelineManager: TimelineManager; + + beforeEach(async () => { + timelineManager = new TimelineManager(); + sdkMock.getTimeBuckets.mockResolvedValue([]); + + await timelineManager.updateViewport({ width: 1588, height: 1000 }); + }); + + afterEach(() => { + timelineManager.destroy(); + }); + + it('does not insert live event assets with a different owner into a user-scoped timeline', async () => { + await timelineManager.updateOptions({ userId: 'partner-id', visibility: AssetVisibility.Timeline }); + + const asset = deriveLocalDateTimeFromFileCreatedAt( + timelineAssetFactory.build({ + ownerId: 'current-user-id', + visibility: AssetVisibility.Timeline, + }), + ); + + timelineManager.upsertAssetsFromLiveEvent([asset]); + + expect(timelineManager.assetCount).toEqual(0); + }); + + it('inserts live event assets for the matching owner into a user-scoped timeline', async () => { + await timelineManager.updateOptions({ userId: 'partner-id', visibility: AssetVisibility.Timeline }); + + const asset = deriveLocalDateTimeFromFileCreatedAt( + timelineAssetFactory.build({ + ownerId: 'partner-id', + visibility: AssetVisibility.Timeline, + }), + ); + + timelineManager.upsertAssetsFromLiveEvent([asset]); + + expect(timelineManager.assetCount).toEqual(1); + }); + + it('does not insert unknown live event assets into album timelines', async () => { + await timelineManager.updateOptions({ albumId: 'album-id' }); + + const asset = deriveLocalDateTimeFromFileCreatedAt(timelineAssetFactory.build()); + + timelineManager.upsertAssetsFromLiveEvent([asset]); + + expect(timelineManager.assetCount).toEqual(0); + }); + + it('updates existing live event assets in scoped timelines', async () => { + await timelineManager.updateOptions({ albumId: 'album-id' }); + + const asset = deriveLocalDateTimeFromFileCreatedAt( + timelineAssetFactory.build({ + isFavorite: false, + }), + ); + + timelineManager.upsertAssets([asset]); + expect(timelineManager.assetCount).toEqual(1); + expect(timelineManager.months[0].getFirstAsset().isFavorite).toEqual(false); + + timelineManager.upsertAssetsFromLiveEvent([ + { + ...asset, + isFavorite: true, + }, + ]); + + expect(timelineManager.assetCount).toEqual(1); + expect(timelineManager.months[0].getFirstAsset().isFavorite).toEqual(true); + }); + }); + describe('removeAssets', () => { let timelineManager: TimelineManager; 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 7016371f8e..dac5644c3a 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -381,6 +381,12 @@ export class TimelineManager extends VirtualScrollManager { this.addAssetsUpsertSegments([...notExcluded]); } + upsertAssetsFromLiveEvent(assets: TimelineAsset[]) { + const notUpdated = this.#updateAssets(assets); + const insertable = notUpdated.filter((asset) => this.canInsertAssetFromLiveEvent(asset)); + this.addAssetsUpsertSegments(insertable); + } + async findTimelineMonthForAsset(asset: AssetDescriptor | AssetResponseDto) { if (!this.isInitialized) { await this.initTask.waitUntilExecution(); @@ -620,6 +626,19 @@ export class TimelineManager extends VirtualScrollManager { ); } + canInsertAssetFromLiveEvent(asset: TimelineAsset) { + if (this.isExcluded(asset)) { + return false; + } + if (this.#options.albumId || this.#options.personId || this.#options.timelineAlbumId) { + return false; + } + if (this.#options.userId && !this.#options.withPartners && asset.ownerId !== this.#options.userId) { + return false; + } + return true; + } + getAssetOrder() { return this.#options.order ?? AssetOrder.Desc; }