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 []
}
}