diff --git a/dist/index.html b/dist/index.html index 745918ac..c9f411ee 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5,7 +5,7 @@ Boris - Nostr Bookmarks - + diff --git a/src/App.tsx b/src/App.tsx index 6bdbadc6..252df5a0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,6 @@ import { RelayPool } from 'applesauce-relay' import { createAddressLoader } from 'applesauce-loaders/loaders' import Login from './components/Login' import Bookmarks from './components/Bookmarks' -import Article from './components/Article' function App() { const [eventStore, setEventStore] = useState(null) @@ -67,7 +66,15 @@ function App() {
- } /> + setIsAuthenticated(false)} + /> + } + /> setIsAuthenticated(true)} /> diff --git a/src/components/Article.tsx b/src/components/Article.tsx deleted file mode 100644 index 97ba6f42..00000000 --- a/src/components/Article.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { useState, useEffect } from 'react' -import { useParams, Link } from 'react-router-dom' -import { nip19 } from 'nostr-tools' -import { AddressPointer } from 'nostr-tools/nip19' -import { NostrEvent } from 'nostr-tools' -import ReactMarkdown from 'react-markdown' -import remarkGfm from 'remark-gfm' -import { remarkNostrMentions } from 'applesauce-content/markdown' -import { - getArticleTitle, - getArticleImage, - getArticlePublished, - getArticleSummary -} from 'applesauce-core/helpers' -import { npubEncode } from 'nostr-tools/nip19' -import { RelayPool, completeOnEose } from 'applesauce-relay' -import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs' - -interface ArticleProps { - relayPool: RelayPool -} - -const Article: React.FC = ({ relayPool }) => { - const { naddr } = useParams<{ naddr: string }>() - const [article, setArticle] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - if (!naddr) return - - const fetchArticle = async () => { - setLoading(true) - setError(null) - - try { - // Decode the naddr - const decoded = nip19.decode(naddr) - - if (decoded.type !== 'naddr') { - throw new Error('Invalid naddr format') - } - - const pointer = decoded.data as AddressPointer - - // Define relays to query - const relays = pointer.relays && pointer.relays.length > 0 - ? pointer.relays - : [ - 'wss://relay.damus.io', - 'wss://nos.lol', - 'wss://relay.nostr.band', - 'wss://relay.primal.net' - ] - - // Fetch the article event - const filter = { - kinds: [pointer.kind], - authors: [pointer.pubkey], - '#d': [pointer.identifier] - } - - // Use applesauce relay pool pattern - const events = await lastValueFrom( - relayPool - .req(relays, filter) - .pipe(completeOnEose(), takeUntil(timer(10000)), toArray()) - ) - - if (events.length > 0) { - // Sort by created_at and take the most recent - events.sort((a, b) => b.created_at - a.created_at) - setArticle(events[0]) - } else { - setError('Article not found') - } - } catch (err) { - console.error('Failed to fetch article:', err) - setError(err instanceof Error ? err.message : 'Failed to load article') - } finally { - setLoading(false) - } - } - - fetchArticle() - }, [naddr, relayPool]) - - if (loading) { - return ( -
-
Loading article...
-
- ) - } - - if (error) { - return ( -
-
Error: {error}
- - Go Home - -
- ) - } - - if (!article) { - return ( -
-
Article not found
- - Go Home - -
- ) - } - - const title = getArticleTitle(article) - const image = getArticleImage(article) - const published = getArticlePublished(article) - const summary = getArticleSummary(article) - - return ( -
-
- - ← Back to Home - - - {image && ( -
- {title} -
- )} - -
-

{title}

- -
- By {npubEncode(article.pubkey).slice(0, 12)}... -
- - {published && ( -
- Published: {new Date(published * 1000).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - })} -
- )} - - {summary && ( -
- {summary} -
- )} - -
- - {article.content} - -
-
-
-
- ) -} - -export default Article diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index b0ef7b57..a3cee56f 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react' +import { useParams } from 'react-router-dom' import { Hooks } from 'applesauce-react' import { useEventStore } from 'applesauce-react/hooks' import { RelayPool } from 'applesauce-relay' @@ -10,6 +11,7 @@ import { fetchHighlights } from '../services/highlightService' import ContentPanel from './ContentPanel' import { HighlightsPanel } from './HighlightsPanel' import { fetchReadableContent, ReadableContent } from '../services/readerService' +import { fetchArticleByNaddr } from '../services/articleService' import Settings from './Settings' import Toast from './Toast' import { useSettings } from '../hooks/useSettings' @@ -21,6 +23,7 @@ interface BookmarksProps { } const Bookmarks: React.FC = ({ relayPool, onLogout }) => { + const { naddr } = useParams<{ naddr?: string }>() const [bookmarks, setBookmarks] = useState([]) const [highlights, setHighlights] = useState([]) const [highlightsLoading, setHighlightsLoading] = useState(true) @@ -44,6 +47,38 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { accountManager }) + // Load article if naddr is in URL + useEffect(() => { + if (!relayPool || !naddr) return + + const loadArticle = async () => { + setReaderLoading(true) + setReaderContent(undefined) + setSelectedUrl(`nostr:${naddr}`) // Use naddr as the URL identifier + setIsCollapsed(true) + + try { + const article = await fetchArticleByNaddr(relayPool, naddr) + setReaderContent({ + title: article.title, + markdown: article.markdown, + url: `nostr:${naddr}` + }) + } catch (err) { + console.error('Failed to load article:', err) + setReaderContent({ + title: 'Error Loading Article', + html: `

Failed to load article: ${err instanceof Error ? err.message : 'Unknown error'}

`, + url: `nostr:${naddr}` + }) + } finally { + setReaderLoading(false) + } + } + + loadArticle() + }, [naddr, relayPool]) + // Load initial data on login useEffect(() => { if (!relayPool || !activeAccount) return diff --git a/src/services/articleService.ts b/src/services/articleService.ts new file mode 100644 index 00000000..b6e71997 --- /dev/null +++ b/src/services/articleService.ts @@ -0,0 +1,102 @@ +import { RelayPool, completeOnEose } from 'applesauce-relay' +import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs' +import { nip19 } from 'nostr-tools' +import { AddressPointer } from 'nostr-tools/nip19' +import { NostrEvent } from 'nostr-tools' +import { + getArticleTitle, + getArticleImage, + getArticlePublished, + getArticleSummary +} from 'applesauce-core/helpers' + +export interface ArticleContent { + title: string + markdown: string + image?: string + published?: number + summary?: string + author: string + event: NostrEvent +} + +/** + * Fetches a Nostr long-form article (NIP-23) by naddr + */ +export async function fetchArticleByNaddr( + relayPool: RelayPool, + naddr: string +): Promise { + try { + // Decode the naddr + const decoded = nip19.decode(naddr) + + if (decoded.type !== 'naddr') { + throw new Error('Invalid naddr format') + } + + const pointer = decoded.data as AddressPointer + + // Define relays to query + const relays = pointer.relays && pointer.relays.length > 0 + ? pointer.relays + : [ + 'wss://relay.damus.io', + 'wss://nos.lol', + 'wss://relay.nostr.band', + 'wss://relay.primal.net' + ] + + // Fetch the article event + const filter = { + kinds: [pointer.kind], + authors: [pointer.pubkey], + '#d': [pointer.identifier] + } + + // Use applesauce relay pool pattern + const events = await lastValueFrom( + relayPool + .req(relays, filter) + .pipe(completeOnEose(), takeUntil(timer(10000)), toArray()) + ) + + if (events.length === 0) { + throw new Error('Article not found') + } + + // Sort by created_at and take the most recent + events.sort((a, b) => b.created_at - a.created_at) + const article = events[0] + + const title = getArticleTitle(article) || 'Untitled Article' + const image = getArticleImage(article) + const published = getArticlePublished(article) + const summary = getArticleSummary(article) + + return { + title, + markdown: article.content, + image, + published, + summary, + author: article.pubkey, + event: article + } + } catch (err) { + console.error('Failed to fetch article:', err) + throw err + } +} + +/** + * Checks if a string is a valid naddr + */ +export function isNaddr(str: string): boolean { + try { + const decoded = nip19.decode(str) + return decoded.type === 'naddr' + } catch { + return false + } +}