From a84d4394893475c3ecf59e56b16986dce254d5d8 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 13:28:36 +0200 Subject: [PATCH] fix: properly deduplicate web bookmarks by d-tag - Web bookmarks (kind:39701) are replaceable events and should be deduplicated by d-tag - Update dedupeNip51Events to include kind:39701 in d-tag deduplication logic - Use coordinate format (kind:pubkey:d-tag) for web bookmark IDs instead of event IDs - Ensures same URL bookmarked multiple times only appears once - Keeps newest version when duplicates exist --- src/services/bookmarkEvents.ts | 14 ++++++++------ src/services/bookmarkProcessing.ts | 6 +++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/services/bookmarkEvents.ts b/src/services/bookmarkEvents.ts index a8c8220f..ebf5a47f 100644 --- a/src/services/bookmarkEvents.ts +++ b/src/services/bookmarkEvents.ts @@ -15,28 +15,30 @@ export function dedupeNip51Events(events: NostrEvent[]): NostrEvent[] { } const unique = Array.from(byId.values()) - // Separate web bookmarks (kind:39701) from list-based bookmarks - const webBookmarks = unique.filter(e => e.kind === 39701) - const bookmarkLists = unique .filter(e => e.kind === 10003 || e.kind === 30003 || e.kind === 30001) .sort((a, b) => (b.created_at || 0) - (a.created_at || 0)) const latestBookmarkList = bookmarkLists.find(list => !list.tags?.some((t: string[]) => t[0] === 'd')) + // Deduplicate replaceable events (kind:30003, 30001, 39701) by d-tag const byD = new Map() for (const e of unique) { - if (e.kind === 10003 || e.kind === 30003 || e.kind === 30001) { + if (e.kind === 10003 || e.kind === 30003 || e.kind === 30001 || e.kind === 39701) { const d = (e.tags || []).find((t: string[]) => t[0] === 'd')?.[1] || '' const prev = byD.get(d) if (!prev || (e.created_at || 0) > (prev.created_at || 0)) byD.set(d, e) } } - const setsAndNamedLists = Array.from(byD.values()) + // Separate web bookmarks from bookmark sets/lists + const allReplaceable = Array.from(byD.values()) + const webBookmarks = allReplaceable.filter(e => e.kind === 39701) + const setsAndNamedLists = allReplaceable.filter(e => e.kind !== 39701) + const out: NostrEvent[] = [] if (latestBookmarkList) out.push(latestBookmarkList) out.push(...setsAndNamedLists) - // Add web bookmarks as individual events + // Add deduplicated web bookmarks as individual events out.push(...webBookmarks) return out } diff --git a/src/services/bookmarkProcessing.ts b/src/services/bookmarkProcessing.ts index 774d464a..fdc3d913 100644 --- a/src/services/bookmarkProcessing.ts +++ b/src/services/bookmarkProcessing.ts @@ -133,8 +133,12 @@ export async function collectBookmarksFromEvents( // Handle web bookmarks (kind:39701) as individual bookmarks if (evt.kind === 39701) { + // Use coordinate format for web bookmarks to enable proper deduplication + // Web bookmarks are replaceable events (kind:39701:pubkey:d-tag) + const webBookmarkId = dTag ? `${evt.kind}:${evt.pubkey}:${dTag}` : evt.id + publicItemsAll.push({ - id: evt.id, + id: webBookmarkId, content: evt.content || '', created_at: evt.created_at ?? null, pubkey: evt.pubkey,