import React, { useState, useEffect } from 'react' import { Hooks } from 'applesauce-react' import { useEventModel } from 'applesauce-react/hooks' import { Models } from 'applesauce-core' import { RelayPool } from 'applesauce-relay' import { completeOnEose } from 'applesauce-relay' import { getParsedContent } from 'applesauce-content/text' import { NostrEvent, Filter } from 'nostr-tools' import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs' interface ParsedNode { type: string value?: string url?: string encoded?: string children?: ParsedNode[] } interface ParsedContent { type: string children: ParsedNode[] } interface Bookmark { id: string title: string url: string content: string created_at: number tags: string[][] bookmarkCount?: number eventReferences?: string[] articleReferences?: string[] urlReferences?: string[] parsedContent?: ParsedContent individualBookmarks?: IndividualBookmark[] } interface IndividualBookmark { id: string content: string created_at: number pubkey: string kind: number tags: string[][] parsedContent?: ParsedContent author?: string type: 'event' | 'article' } interface BookmarksProps { relayPool: RelayPool | null onLogout: () => void } const Bookmarks: React.FC = ({ relayPool, onLogout }) => { const [bookmarks, setBookmarks] = useState([]) const [loading, setLoading] = useState(true) const activeAccount = Hooks.useActiveAccount() // Use ProfileModel to get user profile information const profile = useEventModel(Models.ProfileModel, activeAccount ? [activeAccount.pubkey] : null) useEffect(() => { console.log('Bookmarks useEffect triggered') console.log('relayPool:', !!relayPool) console.log('activeAccount:', !!activeAccount) if (relayPool && activeAccount) { console.log('Starting to fetch bookmarks...') fetchBookmarks() } else { console.log('Not fetching bookmarks - missing dependencies') } }, [relayPool, activeAccount?.pubkey]) // Only depend on pubkey, not the entire activeAccount object const fetchBookmarks = async () => { if (!relayPool || !activeAccount || loading) return try { setLoading(true) console.log('Fetching bookmark list for pubkey:', activeAccount.pubkey) // Get relay URLs from the pool const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url) // Step 1: Fetch the bookmark list event (kind 10003) const bookmarkListFilter: Filter = { kinds: [10003], authors: [activeAccount.pubkey], limit: 1 // Just get the most recent bookmark list } console.log('Fetching bookmark list with filter:', bookmarkListFilter) const bookmarkListEvents = await lastValueFrom( relayPool.req(relayUrls, bookmarkListFilter).pipe( completeOnEose(), takeUntil(timer(10000)), toArray(), ) ) console.log('Found bookmark list events:', bookmarkListEvents.length) if (bookmarkListEvents.length === 0) { console.log('No bookmark list found') setBookmarks([]) setLoading(false) return } // Step 2: Extract event IDs from the bookmark list const bookmarkListEvent = bookmarkListEvents[0] const eventTags = bookmarkListEvent.tags.filter(tag => tag[0] === 'e') const eventIds = eventTags.map(tag => tag[1]) console.log('Found event IDs in bookmark list:', eventIds.length, eventIds) if (eventIds.length === 0) { console.log('No event references found in bookmark list') setBookmarks([]) setLoading(false) return } // Step 3: Fetch each individual event console.log('Fetching individual events...') const individualBookmarks: IndividualBookmark[] = [] for (const eventId of eventIds) { try { console.log('Fetching event:', eventId) const eventFilter: Filter = { ids: [eventId] } const events = await lastValueFrom( relayPool.req(relayUrls, eventFilter).pipe( completeOnEose(), takeUntil(timer(5000)), toArray(), ) ) if (events.length > 0) { const event = events[0] const parsedContent = event.content ? getParsedContent(event.content) as ParsedContent : undefined individualBookmarks.push({ id: event.id, content: event.content, created_at: event.created_at, pubkey: event.pubkey, kind: event.kind, tags: event.tags, parsedContent: parsedContent, type: 'event' }) console.log('Successfully fetched event:', event.id) } else { console.log('Event not found:', eventId) } } catch (error) { console.error('Error fetching event:', eventId, error) } } console.log('Fetched individual bookmarks:', individualBookmarks.length) // Create a single bookmark entry with all individual bookmarks const bookmark: Bookmark = { id: bookmarkListEvent.id, title: bookmarkListEvent.content || `Bookmark List (${individualBookmarks.length} items)`, url: '', content: bookmarkListEvent.content, created_at: bookmarkListEvent.created_at, tags: bookmarkListEvent.tags, bookmarkCount: individualBookmarks.length, eventReferences: eventIds, individualBookmarks: individualBookmarks } setBookmarks([bookmark]) setLoading(false) } catch (error) { console.error('Failed to fetch bookmarks:', error) setLoading(false) } } const formatDate = (timestamp: number) => { return new Date(timestamp * 1000).toLocaleDateString() } // Component to render parsed content using applesauce-content const renderParsedContent = (parsedContent: ParsedContent) => { if (!parsedContent || !parsedContent.children) { return null } const renderNode = (node: ParsedNode, index: number): React.ReactNode => { if (node.type === 'text') { return {node.value} } if (node.type === 'mention') { return ( {node.encoded} ) } if (node.type === 'link') { return ( {node.url} ) } if (node.children) { return ( {node.children.map((child: ParsedNode, childIndex: number) => renderNode(child, childIndex) )} ) } return null } return (
{parsedContent.children.map((node: ParsedNode, index: number) => renderNode(node, index) )}
) } // Component to render individual bookmarks const renderIndividualBookmark = (bookmark: IndividualBookmark, index: number) => { return (
{bookmark.type} {bookmark.id.slice(0, 8)}...{bookmark.id.slice(-8)} {formatDate(bookmark.created_at)}
{bookmark.parsedContent ? (
{renderParsedContent(bookmark.parsedContent)}
) : bookmark.content && (

{bookmark.content}

)}
Kind: {bookmark.kind} Author: {bookmark.pubkey.slice(0, 8)}...{bookmark.pubkey.slice(-8)}
) } const formatUserDisplay = () => { if (!activeAccount) return 'Unknown User' // Use profile data from ProfileModel if available if (profile?.name) { return profile.name } if (profile?.display_name) { return profile.display_name } if (profile?.nip05) { return profile.nip05 } // Fallback to formatted public key return `${activeAccount.pubkey.slice(0, 8)}...${activeAccount.pubkey.slice(-8)}` } if (loading) { return (

Your Bookmarks

{activeAccount && (

Logged in as: {formatUserDisplay()}

)}
Loading bookmarks...
) } return (

Your Bookmarks ({bookmarks.length})

{activeAccount && (

Logged in as: {formatUserDisplay()}

)}
{bookmarks.length === 0 ? (

No bookmarks found.

Add bookmarks using your nostr client to see them here.

) : (
{bookmarks.map((bookmark, index) => (

{bookmark.title}

{bookmark.bookmarkCount && (

{bookmark.bookmarkCount} bookmarks in this list

)} {bookmark.urlReferences && bookmark.urlReferences.length > 0 && (

URLs:

{bookmark.urlReferences.map((url, index) => ( {url} ))}
)} {bookmark.individualBookmarks && bookmark.individualBookmarks.length > 0 && (

Individual Bookmarks ({bookmark.individualBookmarks.length}):

{bookmark.individualBookmarks.map((individualBookmark, index) => renderIndividualBookmark(individualBookmark, index) )}
)} {bookmark.eventReferences && bookmark.eventReferences.length > 0 && bookmark.individualBookmarks?.length === 0 && (

Event References ({bookmark.eventReferences.length}):

{bookmark.eventReferences.slice(0, 3).map((eventId, index) => ( {eventId.slice(0, 8)}...{eventId.slice(-8)} ))} {bookmark.eventReferences.length > 3 && ( ... and {bookmark.eventReferences.length - 3} more )}
)} {bookmark.parsedContent ? (
{renderParsedContent(bookmark.parsedContent)}
) : bookmark.content && (

{bookmark.content}

)}
Created: {formatDate(bookmark.created_at)}
))}
)}
) } export default Bookmarks