From 05160eb5f1aea9534d1d293b4ca978524b327bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Thu, 3 Sep 2026 13:51:15 -0600 Subject: [PATCH] Prevent activity duplicates when loading more items --- app/services/activity.js | 21 ++++++++-- tests/unit/services/activity-test.js | 59 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/app/services/activity.js b/app/services/activity.js index 5e7b50f..663ed88 100644 --- a/app/services/activity.js +++ b/app/services/activity.js @@ -184,11 +184,24 @@ export default class ActivityService extends Service { const newEntries = groupSocialPhotos(filtered); - for (const entry of newEntries) { + const seenPhotoIds = new Set(); + for (const entry of this._socialItems) { + for (const photo of entry.photos) { + seenPhotoIds.add(photo.eventId); + } + } + + const dedupedEntries = newEntries.filter( + (entry) => !entry.photos.every((photo) => seenPhotoIds.has(photo.eventId)) + ); + + if (dedupedEntries.length === 0) return false; + + for (const entry of dedupedEntries) { this._resolveSender(entry); } - for (const entry of newEntries) { + for (const entry of dedupedEntries) { if (entry.osmId) { const bookmarkName = this.placeNameResolver.resolveBookmark( entry.osmId @@ -200,11 +213,11 @@ export default class ActivityService extends Service { } } - this._socialItems = [...this._socialItems, ...newEntries]; + this._socialItems = [...this._socialItems, ...dedupedEntries]; this._mergeItems(); void this.placeNameResolver - .resolveInBackground(newEntries) + .resolveInBackground(dedupedEntries) .then(() => this._mergeItems()); return true; diff --git a/tests/unit/services/activity-test.js b/tests/unit/services/activity-test.js index cf4e40d..ff6d99c 100644 --- a/tests/unit/services/activity-test.js +++ b/tests/unit/services/activity-test.js @@ -787,6 +787,65 @@ module('Unit | Service | activity', function (hooks) { assert.false(called, 'no fetch when items is empty'); }); + test('loadMore does not append duplicate photo group', async function (assert) { + const service = this.owner.lookup('service:activity'); + service._userPubkey = USER_PUBKEY; + service._sourceMode = 'explore'; + service.sourceMode = 'explore'; + service.nostrData._contactPubkeys = new Set([SENDER_PUBKEY]); + + const STRANGER_PUBKEY = 'c'.repeat(64); + service.nostrData.isTrustedEvent = (event) => + event.pubkey === STRANGER_PUBKEY; + + const NOW = Math.floor(Date.now() / 1000); + service._since = NOW; + + const photo1 = makePhotoEvent({ + id: 'd1'.padEnd(64, '0'), + author: STRANGER_PUBKEY, + placeIdentifier: 'osm:node:100', + created_at: NOW - 100, + url: 'https://x.com/p1.jpg', + }); + const photo2 = makePhotoEvent({ + id: 'd2'.padEnd(64, '0'), + author: STRANGER_PUBKEY, + placeIdentifier: 'osm:node:100', + created_at: NOW - 200, + url: 'https://x.com/p2.jpg', + }); + const photo3 = makePhotoEvent({ + id: 'd3'.padEnd(64, '0'), + author: STRANGER_PUBKEY, + placeIdentifier: 'osm:node:100', + created_at: NOW - 300, + url: 'https://x.com/p3.jpg', + }); + + service._updateSocialItems([photo1, photo2, photo3]); + assert.strictEqual( + service.items.length, + 1, + 'one grouped entry with 3 photos' + ); + assert.strictEqual(service.items[0].photos.length, 3); + + service.nostrData.fetchActivityPhotos = async () => ({ + cacheEvents: [photo1, photo2, photo3], + networkEvents: Promise.resolve([]), + }); + + await service.loadMore(); + + assert.strictEqual(service.items.length, 1, 'duplicate group not appended'); + assert.strictEqual( + service.items[0].photos.length, + 3, + 'photo count unchanged' + ); + }); + test('stop resets isLoadingMore and isLoading', function (assert) { const service = this.owner.lookup('service:activity'); service._isLoadingMore = true; -- 2.50.1