diff --git a/.cursor/rules/highlights-nip-and-docs.mdc b/.cursor/rules/highlights-nip-and-docs.mdc new file mode 100644 index 00000000..3dca9096 --- /dev/null +++ b/.cursor/rules/highlights-nip-and-docs.mdc @@ -0,0 +1,3 @@ +--- +alwaysApply: true +--- diff --git a/dist/index.html b/dist/index.html index d944c05f..8f71a20a 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5,7 +5,7 @@ Boris - Nostr Bookmarks - + diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index 6f1cbb13..c4384bb2 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -8,7 +8,7 @@ import { Bookmark } from '../types/bookmarks' import { Highlight } from '../types/highlights' import { BookmarkList } from './BookmarkList' import { fetchBookmarks } from '../services/bookmarkService' -import { fetchHighlights } from '../services/highlightService' +import { fetchHighlights, fetchHighlightsForArticle } from '../services/highlightService' import ContentPanel from './ContentPanel' import { HighlightsPanel } from './HighlightsPanel' import { fetchReadableContent, ReadableContent } from '../services/readerService' @@ -68,10 +68,15 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { url: `nostr:${naddr}` }) - // Fetch highlights for this article (using the article author's pubkey) + // Fetch highlights for this article using its address coordinate + // Extract the d-tag identifier from the article event + const dTag = article.event.tags.find(t => t[0] === 'd')?.[1] || '' + const articleCoordinate = `${article.event.kind}:${article.author}:${dTag}` + try { setHighlightsLoading(true) - const fetchedHighlights = await fetchHighlights(relayPool, article.author) + const fetchedHighlights = await fetchHighlightsForArticle(relayPool, articleCoordinate) + console.log(`📌 Found ${fetchedHighlights.length} highlights for article ${articleCoordinate}`) setHighlights(fetchedHighlights) } catch (err) { console.error('Failed to fetch highlights:', err) diff --git a/src/services/highlightService.ts b/src/services/highlightService.ts index c3bcc8d6..b745118d 100644 --- a/src/services/highlightService.ts +++ b/src/services/highlightService.ts @@ -29,6 +29,85 @@ function dedupeHighlights(events: NostrEvent[]): NostrEvent[] { return Array.from(byId.values()) } +/** + * Fetches highlights for a specific article by its address coordinate + * @param relayPool - The relay pool to query + * @param articleCoordinate - The article's address in format "kind:pubkey:identifier" (e.g., "30023:abc...def:my-article") + */ +export const fetchHighlightsForArticle = async ( + relayPool: RelayPool, + articleCoordinate: string +): Promise => { + try { + // Use well-known relays for highlights even if user isn't logged in + const highlightRelays = [ + 'wss://relay.damus.io', + 'wss://nos.lol', + 'wss://relay.nostr.band', + 'wss://relay.snort.social', + 'wss://purplepag.es' + ] + + console.log('🔍 Fetching highlights (kind 9802) for article:', articleCoordinate) + console.log('🔍 From relays:', highlightRelays) + + // Query for highlights that reference this article via the 'a' tag + const rawEvents = await lastValueFrom( + relayPool + .req(highlightRelays, { kinds: [9802], '#a': [articleCoordinate] }) + .pipe(completeOnEose(), takeUntil(timer(10000)), toArray()) + ) + + console.log('📊 Raw highlight events fetched:', rawEvents.length) + + // Deduplicate events by ID + const uniqueEvents = dedupeHighlights(rawEvents) + console.log('📊 Unique highlight events after deduplication:', uniqueEvents.length) + + const highlights: Highlight[] = uniqueEvents.map((event: NostrEvent) => { + // Use applesauce helpers to extract highlight data + const highlightText = getHighlightText(event) + const context = getHighlightContext(event) + const comment = getHighlightComment(event) + const sourceEventPointer = getHighlightSourceEventPointer(event) + const sourceAddressPointer = getHighlightSourceAddressPointer(event) + const sourceUrl = getHighlightSourceUrl(event) + const attributions = getHighlightAttributions(event) + + // Get author from attributions + const author = attributions.find(a => a.role === 'author')?.pubkey + + // Get event reference (prefer event pointer, fallback to address pointer) + const eventReference = sourceEventPointer?.id || + (sourceAddressPointer ? `${sourceAddressPointer.kind}:${sourceAddressPointer.pubkey}:${sourceAddressPointer.identifier}` : undefined) + + return { + id: event.id, + pubkey: event.pubkey, + created_at: event.created_at, + content: highlightText, + tags: event.tags, + eventReference, + urlReference: sourceUrl, + author, + context, + comment + } + }) + + // Sort by creation time (newest first) + return highlights.sort((a, b) => b.created_at - a.created_at) + } catch (error) { + console.error('Failed to fetch highlights for article:', error) + return [] + } +} + +/** + * Fetches highlights created by a specific user + * @param relayPool - The relay pool to query + * @param pubkey - The user's public key + */ export const fetchHighlights = async ( relayPool: RelayPool, pubkey: string @@ -36,7 +115,7 @@ export const fetchHighlights = async ( try { const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) - console.log('🔍 Fetching highlights (kind 9802) from relays:', relayUrls) + console.log('🔍 Fetching highlights (kind 9802) by author:', pubkey) const rawEvents = await lastValueFrom( relayPool @@ -84,7 +163,7 @@ export const fetchHighlights = async ( // Sort by creation time (newest first) return highlights.sort((a, b) => b.created_at - a.created_at) } catch (error) { - console.error('Failed to fetch highlights:', error) + console.error('Failed to fetch highlights by author:', error) return [] } }