diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index 612bad09..614d5ba4 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -16,11 +16,24 @@ export interface BookmarkData { tags?: string[][] } +export interface AddressPointer { + kind: number + pubkey: string + identifier: string + relays?: string[] +} + +export interface EventPointer { + id: string + relays?: string[] + author?: string +} + export interface ApplesauceBookmarks { - notes?: BookmarkData[] - articles?: BookmarkData[] - hashtags?: BookmarkData[] - urls?: BookmarkData[] + notes?: EventPointer[] + articles?: AddressPointer[] + hashtags?: string[] + urls?: string[] } export interface AccountWithExtension { @@ -55,25 +68,83 @@ export const processApplesauceBookmarks = ( if (typeof bookmarks === 'object' && bookmarks !== null && !Array.isArray(bookmarks)) { const applesauceBookmarks = bookmarks as ApplesauceBookmarks - const allItems: BookmarkData[] = [] - if (applesauceBookmarks.notes) allItems.push(...applesauceBookmarks.notes) - if (applesauceBookmarks.articles) allItems.push(...applesauceBookmarks.articles) - if (applesauceBookmarks.hashtags) allItems.push(...applesauceBookmarks.hashtags) - if (applesauceBookmarks.urls) allItems.push(...applesauceBookmarks.urls) + const allItems: IndividualBookmark[] = [] + + // Process notes (EventPointer[]) + if (applesauceBookmarks.notes) { + applesauceBookmarks.notes.forEach((note: EventPointer) => { + allItems.push({ + id: note.id, + content: '', + created_at: Math.floor(Date.now() / 1000), + pubkey: note.author || activeAccount.pubkey, + kind: 1, // Short note kind + tags: [], + parsedContent: undefined, + type: 'event' as const, + isPrivate, + added_at: Math.floor(Date.now() / 1000) + }) + }) + } + + // Process articles (AddressPointer[]) + if (applesauceBookmarks.articles) { + applesauceBookmarks.articles.forEach((article: AddressPointer) => { + // Convert AddressPointer to coordinate format: kind:pubkey:identifier + const coordinate = `${article.kind}:${article.pubkey}:${article.identifier || ''}` + allItems.push({ + id: coordinate, + content: '', + created_at: Math.floor(Date.now() / 1000), + pubkey: article.pubkey, + kind: article.kind, // Usually 30023 for long-form articles + tags: [], + parsedContent: undefined, + type: 'event' as const, + isPrivate, + added_at: Math.floor(Date.now() / 1000) + }) + }) + } + + // Process hashtags (string[]) + if (applesauceBookmarks.hashtags) { + applesauceBookmarks.hashtags.forEach((hashtag: string) => { + allItems.push({ + id: `hashtag-${hashtag}`, + content: `#${hashtag}`, + created_at: Math.floor(Date.now() / 1000), + pubkey: activeAccount.pubkey, + kind: 1, + tags: [['t', hashtag]], + parsedContent: undefined, + type: 'event' as const, + isPrivate, + added_at: Math.floor(Date.now() / 1000) + }) + }) + } + + // Process URLs (string[]) + if (applesauceBookmarks.urls) { + applesauceBookmarks.urls.forEach((url: string) => { + allItems.push({ + id: `url-${url}`, + content: url, + created_at: Math.floor(Date.now() / 1000), + pubkey: activeAccount.pubkey, + kind: 1, + tags: [['r', url]], + parsedContent: undefined, + type: 'event' as const, + isPrivate, + added_at: Math.floor(Date.now() / 1000) + }) + }) + } + return allItems - .filter((bookmark: BookmarkData) => bookmark.id) // Skip bookmarks without valid IDs - .map((bookmark: BookmarkData) => ({ - id: bookmark.id!, - content: bookmark.content || '', - created_at: bookmark.created_at || Math.floor(Date.now() / 1000), - pubkey: activeAccount.pubkey, - kind: bookmark.kind || 30001, - tags: bookmark.tags || [], - parsedContent: bookmark.content ? (getParsedContent(bookmark.content) as ParsedContent) : undefined, - type: 'event' as const, - isPrivate, - added_at: bookmark.created_at || Math.floor(Date.now() / 1000) - })) } const bookmarkArray = Array.isArray(bookmarks) ? bookmarks : [bookmarks] diff --git a/src/services/bookmarkService.ts b/src/services/bookmarkService.ts index 612c36ea..1e5c2f46 100644 --- a/src/services/bookmarkService.ts +++ b/src/services/bookmarkService.ts @@ -114,20 +114,87 @@ export const fetchBookmarks = async ( ) const allItems = [...publicItemsAll, ...privateItemsAll] - const noteIds = Array.from(new Set(allItems.map(i => i.id).filter(isHexId))) + + // Separate hex IDs (regular events) from coordinates (addressable events) + const noteIds: string[] = [] + const coordinates: string[] = [] + + allItems.forEach(i => { + if (isHexId(i.id)) { + noteIds.push(i.id) + } else if (i.id.includes(':')) { + // Coordinate format: kind:pubkey:identifier + coordinates.push(i.id) + } + }) + let idToEvent: Map = new Map() + + // Fetch regular events by ID if (noteIds.length > 0) { try { const events = await queryEvents( relayPool, - { ids: noteIds }, + { ids: Array.from(new Set(noteIds)) }, { localTimeoutMs: 800, remoteTimeoutMs: 2500 } ) - idToEvent = new Map(events.map((e: NostrEvent) => [e.id, e])) + events.forEach((e: NostrEvent) => { + idToEvent.set(e.id, e) + // Also store by coordinate if it's an addressable event + if (e.kind && e.kind >= 30000 && e.kind < 40000) { + const dTag = e.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' + const coordinate = `${e.kind}:${e.pubkey}:${dTag}` + idToEvent.set(coordinate, e) + } + }) } catch (error) { - console.warn('Failed to fetch events for hydration:', error) + console.warn('Failed to fetch events by ID:', error) } } + + // Fetch addressable events by coordinates + if (coordinates.length > 0) { + try { + // Group by kind for more efficient querying + const byKind = new Map>() + + coordinates.forEach(coord => { + const parts = coord.split(':') + const kind = parseInt(parts[0]) + const pubkey = parts[1] + const identifier = parts[2] || '' + + if (!byKind.has(kind)) { + byKind.set(kind, []) + } + byKind.get(kind)!.push({ pubkey, identifier }) + }) + + // Query each kind group + for (const [kind, items] of byKind.entries()) { + const authors = Array.from(new Set(items.map(i => i.pubkey))) + const identifiers = Array.from(new Set(items.map(i => i.identifier))) + + const events = await queryEvents( + relayPool, + { kinds: [kind], authors, '#d': identifiers }, + { localTimeoutMs: 800, remoteTimeoutMs: 2500 } + ) + + events.forEach((e: NostrEvent) => { + const dTag = e.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' + const coordinate = `${e.kind}:${e.pubkey}:${dTag}` + idToEvent.set(coordinate, e) + // Also store by event ID + idToEvent.set(e.id, e) + }) + } + } catch (error) { + console.warn('Failed to fetch addressable events:', error) + } + } + + console.log(`📦 Hydration: fetched ${idToEvent.size} events for ${allItems.length} bookmarks (${noteIds.length} notes, ${coordinates.length} articles)`) const allBookmarks = dedupeBookmarksById([ ...hydrateItems(publicItemsAll, idToEvent), ...hydrateItems(privateItemsAll, idToEvent)