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