From 5bd568680584fd5d714f3372a2efd805cdefcdcb Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:19:20 +0200 Subject: [PATCH] feat: add /e/:eventId route to display individual notes - New EventViewer component to display kind:1 notes and other events - Shows event ID, creation time, and content with RichContent rendering - Add /e/:eventId route in App.tsx - Update CompactView to navigate to /e/:eventId when clicking kind:1 bookmarks - Mobile-optimized styling with back button and full viewport display - Fallback for missing events with error message --- src/App.tsx | 10 ++ src/components/BookmarkViews/CompactView.tsx | 13 ++- src/components/EventViewer.css | 101 ++++++++++++++++ src/components/EventViewer.tsx | 114 +++++++++++++++++++ 4 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 src/components/EventViewer.css create mode 100644 src/components/EventViewer.tsx diff --git a/src/App.tsx b/src/App.tsx index fe26e955..bb58ef17 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ import { readingProgressController } from './services/readingProgressController' import { nostrverseHighlightsController } from './services/nostrverseHighlightsController' import { nostrverseWritingsController } from './services/nostrverseWritingsController' import { archiveController } from './services/archiveController' +import EventViewer from './components/EventViewer' const DEFAULT_ARTICLE = import.meta.env.VITE_DEFAULT_ARTICLE_NADDR || 'naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew' @@ -348,6 +349,15 @@ function AppRoutes({ /> } /> + + } + /> = ({ contentTypeIcon, readingProgress }) => { + const navigate = useNavigate() const isArticle = bookmark.kind === 30023 const isWebBookmark = bookmark.kind === 39701 - const isClickable = hasUrls || isArticle || isWebBookmark + const isNote = bookmark.kind === 1 + const isClickable = hasUrls || isArticle || isWebBookmark || isNote const displayText = isArticle && articleSummary ? articleSummary : bookmark.content @@ -54,12 +57,12 @@ export const CompactView: React.FC = ({ } const handleCompactClick = () => { - if (!onSelectUrl) return - if (isArticle) { - onSelectUrl('', { id: bookmark.id, kind: bookmark.kind, tags: bookmark.tags, pubkey: bookmark.pubkey }) + onSelectUrl?.('', { id: bookmark.id, kind: bookmark.kind, tags: bookmark.tags, pubkey: bookmark.pubkey }) } else if (hasUrls) { - onSelectUrl(extractedUrls[0]) + onSelectUrl?.(extractedUrls[0]) + } else if (isNote) { + navigate(`/e/${bookmark.id}`) } } diff --git a/src/components/EventViewer.css b/src/components/EventViewer.css new file mode 100644 index 00000000..3207b13d --- /dev/null +++ b/src/components/EventViewer.css @@ -0,0 +1,101 @@ +.event-viewer { + display: flex; + flex-direction: column; + height: 100vh; + background: var(--color-bg); + color: var(--color-text); +} + +.event-viewer-header { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + border-bottom: 1px solid var(--color-border); + background: var(--color-bg-secondary, var(--color-bg)); +} + +.event-viewer-header h1 { + margin: 0; + font-size: 1.25rem; + flex: 1; +} + +.back-button { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border: none; + background: var(--color-border); + color: var(--color-text); + border-radius: 0.5rem; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.2s; +} + +.back-button:hover { + background: var(--color-text-muted); +} + +.event-viewer-content { + flex: 1; + overflow-y: auto; + padding: 1.5rem; + max-width: 100%; +} + +.event-meta { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + font-size: 0.875rem; + color: var(--color-text-muted); + flex-wrap: wrap; +} + +.event-id { + font-family: monospace; +} + +.event-id code { + background: var(--color-border); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} + +.event-time { + white-space: nowrap; +} + +.event-text { + font-size: 1rem; + line-height: 1.6; + word-wrap: break-word; + white-space: pre-wrap; +} + +/* Mobile optimization */ +@media (max-width: 640px) { + .event-viewer { + height: 100%; + } + + .event-viewer-header { + padding: 0.75rem; + } + + .event-viewer-header h1 { + font-size: 1.1rem; + } + + .event-viewer-content { + padding: 1rem; + } + + .event-meta { + gap: 0.5rem; + } +} diff --git a/src/components/EventViewer.tsx b/src/components/EventViewer.tsx new file mode 100644 index 00000000..2591abf0 --- /dev/null +++ b/src/components/EventViewer.tsx @@ -0,0 +1,114 @@ +import { useParams, useNavigate } from 'react-router-dom' +import { useState, useEffect } from 'react' +import { RelayPool } from 'applesauce-relay' +import { EventStore } from 'applesauce-core' +import { Hooks } from 'applesauce-react' +import { createEventLoader } from 'applesauce-loaders/loaders' +import { NostrEvent } from 'nostr-tools' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faArrowLeft } from '@fortawesome/free-solid-svg-icons' +import RichContent from './RichContent' +import './EventViewer.css' + +interface EventViewerProps { + relayPool: RelayPool + eventStore: EventStore | null +} + +export default function EventViewer({ relayPool, eventStore }: EventViewerProps) { + const { eventId } = useParams<{ eventId: string }>() + const navigate = useNavigate() + const [event, setEvent] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + if (!eventId || !relayPool || !eventStore) return + + setLoading(true) + setError(null) + + // Try to get from event store first + const cachedEvent = eventStore.getEvent(eventId) + if (cachedEvent) { + setEvent(cachedEvent) + setLoading(false) + return + } + + // Otherwise fetch from relays + const eventLoader = createEventLoader(relayPool, { + eventStore, + cacheRequest: true + }) + + const subscription = eventLoader({ id: eventId }).subscribe({ + next: (fetchedEvent) => { + setEvent(fetchedEvent) + setLoading(false) + }, + error: (err) => { + console.error('Error fetching event:', err) + setError('Failed to load event') + setLoading(false) + }, + complete: () => { + setLoading(false) + } + }) + + return () => subscription.unsubscribe() + }, [eventId, relayPool, eventStore]) + + if (loading) { + return ( +
+
+ +

Loading event...

+
+
+ ) + } + + if (error || !event) { + return ( +
+
+ +

{error || 'Event not found'}

+
+
+ ) + } + + return ( +
+
+ +

Note

+
+ +
+
+ + {eventId?.slice(0, 16)}... + + + {new Date(event.created_at * 1000).toLocaleString()} + +
+ +
+ +
+
+
+ ) +}