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
This commit is contained in:
Yohanes Kirana
2026-09-03 12:23:29 -04:00
committed by GitHub
parent 8e04e6e2c6
commit 040ae6bc0e
3 changed files with 100 additions and 2 deletions
@@ -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);
@@ -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;
@@ -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;
}