mirror of
https://github.com/dergigi/boris.git
synced 2026-01-02 14:34:25 +01:00
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:
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
85
src/hooks/useExternalUrlLoader.ts
Normal file
85
src/hooks/useExternalUrlLoader.ts
Normal 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])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user