diff --git a/dist/index.html b/dist/index.html
index f10b8c3b..971caa06 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -5,8 +5,8 @@
Boris - Nostr Bookmarks
-
-
+
+
diff --git a/src/App.tsx b/src/App.tsx
index aa5c0308..ac727625 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -43,6 +43,15 @@ function AppRoutes({
/>
}
/>
+
+ }
+ />
} />
)
diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx
index 08475f10..780cf0b0 100644
--- a/src/components/Bookmarks.tsx
+++ b/src/components/Bookmarks.tsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useMemo } from 'react'
-import { useParams } from 'react-router-dom'
+import { useParams, useLocation } from 'react-router-dom'
import { Hooks } from 'applesauce-react'
import { useEventStore } from 'applesauce-react/hooks'
import { RelayPool } from 'applesauce-relay'
@@ -16,6 +16,7 @@ import Settings from './Settings'
import Toast from './Toast'
import { useSettings } from '../hooks/useSettings'
import { useArticleLoader } from '../hooks/useArticleLoader'
+import { useExternalUrlLoader } from '../hooks/useExternalUrlLoader'
import { loadContent, BookmarkReference } from '../utils/contentLoader'
import { HighlightVisibility } from './HighlightsPanel'
import { HighlightButton, HighlightButtonRef } from './HighlightButton'
@@ -31,6 +32,13 @@ interface BookmarksProps {
const Bookmarks: React.FC = ({ relayPool, onLogout }) => {
const { naddr } = useParams<{ naddr?: string }>()
+ const location = useLocation()
+
+ // Extract external URL from /r/* route
+ const externalUrl = location.pathname.startsWith('/r/')
+ ? location.pathname.slice(3) // Remove '/r/' prefix
+ : undefined
+
const [bookmarks, setBookmarks] = useState([])
const [bookmarksLoading, setBookmarksLoading] = useState(true)
const [highlights, setHighlights] = useState([])
@@ -66,7 +74,7 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => {
accountManager
})
- // Load article if naddr is in URL
+ // Load nostr-native article if naddr is in URL
useArticleLoader({
naddr,
relayPool,
@@ -80,6 +88,20 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => {
setCurrentArticleEventId,
setCurrentArticle
})
+
+ // Load external URL if /r/* route is used
+ useExternalUrlLoader({
+ url: externalUrl,
+ relayPool,
+ setSelectedUrl,
+ setReaderContent,
+ setReaderLoading,
+ setIsCollapsed,
+ setHighlights,
+ setHighlightsLoading,
+ setCurrentArticleCoordinate,
+ setCurrentArticleEventId
+ })
// Load initial data on login
useEffect(() => {
diff --git a/src/hooks/useExternalUrlLoader.ts b/src/hooks/useExternalUrlLoader.ts
new file mode 100644
index 00000000..4eccac86
--- /dev/null
+++ b/src/hooks/useExternalUrlLoader.ts
@@ -0,0 +1,85 @@
+import { useEffect } from 'react'
+import { RelayPool } from 'applesauce-relay'
+import { fetchReadableContent, ReadableContent } from '../services/readerService'
+import { fetchHighlightsForUrl } from '../services/highlightService'
+import { Highlight } from '../types/highlights'
+
+interface UseExternalUrlLoaderProps {
+ url: string | undefined
+ relayPool: RelayPool | null
+ setSelectedUrl: (url: string) => void
+ setReaderContent: (content: ReadableContent | undefined) => void
+ setReaderLoading: (loading: boolean) => void
+ setIsCollapsed: (collapsed: boolean) => void
+ setHighlights: (highlights: Highlight[]) => void
+ setHighlightsLoading: (loading: boolean) => void
+ setCurrentArticleCoordinate: (coord: string | undefined) => void
+ setCurrentArticleEventId: (id: string | undefined) => void
+}
+
+export function useExternalUrlLoader({
+ url,
+ relayPool,
+ setSelectedUrl,
+ setReaderContent,
+ setReaderLoading,
+ setIsCollapsed,
+ setHighlights,
+ setHighlightsLoading,
+ setCurrentArticleCoordinate,
+ setCurrentArticleEventId
+}: UseExternalUrlLoaderProps) {
+ useEffect(() => {
+ if (!relayPool || !url) return
+
+ const loadExternalUrl = async () => {
+ setReaderLoading(true)
+ setReaderContent(undefined)
+ setSelectedUrl(url)
+ setIsCollapsed(true)
+ // Clear article-specific state
+ setCurrentArticleCoordinate(undefined)
+ setCurrentArticleEventId(undefined)
+
+ try {
+ const content = await fetchReadableContent(url)
+ setReaderContent(content)
+
+ console.log('🌐 External URL loaded:', content.title)
+
+ // Set reader loading to false immediately after content is ready
+ setReaderLoading(false)
+
+ // Fetch highlights for this URL asynchronously
+ try {
+ setHighlightsLoading(true)
+ setHighlights([])
+
+ // Check if fetchHighlightsForUrl exists, otherwise skip
+ if (typeof fetchHighlightsForUrl === 'function') {
+ const highlightsList = await fetchHighlightsForUrl(relayPool, url)
+ setHighlights(highlightsList.sort((a, b) => b.created_at - a.created_at))
+ console.log(`📌 Found ${highlightsList.length} highlights for URL`)
+ } else {
+ console.log('📌 Highlight fetching for URLs not yet implemented')
+ }
+ } catch (err) {
+ console.error('Failed to fetch highlights:', err)
+ } finally {
+ setHighlightsLoading(false)
+ }
+ } catch (err) {
+ console.error('Failed to load external URL:', err)
+ setReaderContent({
+ title: 'Error Loading Content',
+ html: `Failed to load content: ${err instanceof Error ? err.message : 'Unknown error'}
`,
+ url
+ })
+ setReaderLoading(false)
+ }
+ }
+
+ loadExternalUrl()
+ }, [url, relayPool])
+}
+
diff --git a/src/services/highlightService.ts b/src/services/highlightService.ts
index f743281a..9585178b 100644
--- a/src/services/highlightService.ts
+++ b/src/services/highlightService.ts
@@ -175,6 +175,70 @@ export const fetchHighlightsForArticle = async (
}
}
+/**
+ * Fetches highlights for a specific URL
+ * @param relayPool - The relay pool to query
+ * @param url - The external URL to find highlights for
+ */
+export const fetchHighlightsForUrl = async (
+ relayPool: RelayPool,
+ url: string
+): Promise => {
+ try {
+ console.log('🔍 Fetching highlights (kind 9802) for URL:', url)
+
+ const seenIds = new Set()
+ const rawEvents = await lastValueFrom(
+ relayPool
+ .req(RELAYS, { kinds: [9802], '#r': [url] })
+ .pipe(
+ onlyEvents(),
+ tap((event: NostrEvent) => {
+ seenIds.add(event.id)
+ }),
+ completeOnEose(),
+ takeUntil(timer(10000)),
+ toArray()
+ )
+ )
+
+ console.log('📊 Highlights for URL:', rawEvents.length)
+
+ const uniqueEvents = dedupeHighlights(rawEvents)
+ const highlights: Highlight[] = uniqueEvents.map((event: NostrEvent) => {
+ 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)
+
+ const author = attributions.find(a => a.role === 'author')?.pubkey
+ 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
+ }
+ })
+
+ return highlights.sort((a, b) => b.created_at - a.created_at)
+ } catch (error) {
+ console.error('Failed to fetch highlights for URL:', error)
+ return []
+ }
+}
+
/**
* Fetches highlights created by a specific user
* @param relayPool - The relay pool to query