feat: add routing support for external URLs

- Add /r/* route in App.tsx for external URL content
- Create useExternalUrlLoader hook to load external web content
- Add fetchHighlightsForUrl service to fetch highlights by URL using 'r' tag
- Update Bookmarks component to handle both nostr-native (naddr) and external URLs
- Support two URL patterns: /a/naddr... for nostr content, /r/https://... for external URLs
This commit is contained in:
Gigi
2025-10-06 19:22:18 +01:00
parent 89bd9f631a
commit 107d6757bd
5 changed files with 184 additions and 4 deletions

4
dist/index.html vendored
View File

@@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Boris - Nostr Bookmarks</title>
<script type="module" crossorigin src="/assets/index--wClm1wz.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bj-Uhit8.css">
<script type="module" crossorigin src="/assets/index-CqyXD-qH.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bqz-n1DY.css">
</head>
<body>
<div id="root"></div>

View File

@@ -43,6 +43,15 @@ function AppRoutes({
/>
}
/>
<Route
path="/r/*"
element={
<Bookmarks
relayPool={relayPool}
onLogout={handleLogout}
/>
}
/>
<Route path="/" element={<Navigate to={`/a/${DEFAULT_ARTICLE}`} replace />} />
</Routes>
)

View File

@@ -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<BookmarksProps> = ({ 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<Bookmark[]>([])
const [bookmarksLoading, setBookmarksLoading] = useState(true)
const [highlights, setHighlights] = useState<Highlight[]>([])
@@ -66,7 +74,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ 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<BookmarksProps> = ({ 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(() => {

View File

@@ -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: `<p>Failed to load content: ${err instanceof Error ? err.message : 'Unknown error'}</p>`,
url
})
setReaderLoading(false)
}
}
loadExternalUrl()
}, [url, relayPool])
}

View File

@@ -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<Highlight[]> => {
try {
console.log('🔍 Fetching highlights (kind 9802) for URL:', url)
const seenIds = new Set<string>()
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