mirror of
https://github.com/dergigi/boris.git
synced 2025-12-31 21:44:26 +01:00
- Fetch bookmark list event (kind 10003) first - Extract event IDs from e tags - Fetch each individual event by ID - Remove complex async parsing logic - Simplify to direct sequential fetching - Remove unused functions
405 lines
13 KiB
TypeScript
405 lines
13 KiB
TypeScript
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<BookmarksProps> = ({ relayPool, onLogout }) => {
|
|
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
|
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 <span key={index}>{node.value}</span>
|
|
}
|
|
|
|
if (node.type === 'mention') {
|
|
return (
|
|
<a
|
|
key={index}
|
|
href={`nostr:${node.encoded}`}
|
|
className="nostr-mention"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{node.encoded}
|
|
</a>
|
|
)
|
|
}
|
|
|
|
if (node.type === 'link') {
|
|
return (
|
|
<a
|
|
key={index}
|
|
href={node.url}
|
|
className="nostr-link"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
{node.url}
|
|
</a>
|
|
)
|
|
}
|
|
|
|
if (node.children) {
|
|
return (
|
|
<span key={index}>
|
|
{node.children.map((child: ParsedNode, childIndex: number) =>
|
|
renderNode(child, childIndex)
|
|
)}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="parsed-content">
|
|
{parsedContent.children.map((node: ParsedNode, index: number) =>
|
|
renderNode(node, index)
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Component to render individual bookmarks
|
|
const renderIndividualBookmark = (bookmark: IndividualBookmark, index: number) => {
|
|
return (
|
|
<div key={`${bookmark.id}-${index}`} className="individual-bookmark">
|
|
<div className="bookmark-header">
|
|
<span className="bookmark-type">{bookmark.type}</span>
|
|
<span className="bookmark-id">{bookmark.id.slice(0, 8)}...{bookmark.id.slice(-8)}</span>
|
|
<span className="bookmark-date">{formatDate(bookmark.created_at)}</span>
|
|
</div>
|
|
|
|
{bookmark.parsedContent ? (
|
|
<div className="bookmark-content">
|
|
{renderParsedContent(bookmark.parsedContent)}
|
|
</div>
|
|
) : bookmark.content && (
|
|
<div className="bookmark-content">
|
|
<p>{bookmark.content}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="bookmark-meta">
|
|
<span>Kind: {bookmark.kind}</span>
|
|
<span>Author: {bookmark.pubkey.slice(0, 8)}...{bookmark.pubkey.slice(-8)}</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 (
|
|
<div className="bookmarks-container">
|
|
<div className="bookmarks-header">
|
|
<div>
|
|
<h2>Your Bookmarks</h2>
|
|
{activeAccount && (
|
|
<p className="user-info">Logged in as: {formatUserDisplay()}</p>
|
|
)}
|
|
</div>
|
|
<button onClick={onLogout} className="logout-button">
|
|
Logout
|
|
</button>
|
|
</div>
|
|
<div className="loading">Loading bookmarks...</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="bookmarks-container">
|
|
<div className="bookmarks-header">
|
|
<div>
|
|
<h2>Your Bookmarks ({bookmarks.length})</h2>
|
|
{activeAccount && (
|
|
<p className="user-info">Logged in as: {formatUserDisplay()}</p>
|
|
)}
|
|
</div>
|
|
<button onClick={onLogout} className="logout-button">
|
|
Logout
|
|
</button>
|
|
</div>
|
|
|
|
{bookmarks.length === 0 ? (
|
|
<div className="empty-state">
|
|
<p>No bookmarks found.</p>
|
|
<p>Add bookmarks using your nostr client to see them here.</p>
|
|
</div>
|
|
) : (
|
|
<div className="bookmarks-list">
|
|
{bookmarks.map((bookmark, index) => (
|
|
<div key={`${bookmark.id}-${index}`} className="bookmark-item">
|
|
<h3>{bookmark.title}</h3>
|
|
{bookmark.bookmarkCount && (
|
|
<p className="bookmark-count">
|
|
{bookmark.bookmarkCount} bookmarks in this list
|
|
</p>
|
|
)}
|
|
{bookmark.urlReferences && bookmark.urlReferences.length > 0 && (
|
|
<div className="bookmark-urls">
|
|
<h4>URLs:</h4>
|
|
{bookmark.urlReferences.map((url, index) => (
|
|
<a key={index} href={url} target="_blank" rel="noopener noreferrer" className="bookmark-url">
|
|
{url}
|
|
</a>
|
|
))}
|
|
</div>
|
|
)}
|
|
{bookmark.individualBookmarks && bookmark.individualBookmarks.length > 0 && (
|
|
<div className="individual-bookmarks">
|
|
<h4>Individual Bookmarks ({bookmark.individualBookmarks.length}):</h4>
|
|
<div className="bookmarks-grid">
|
|
{bookmark.individualBookmarks.map((individualBookmark, index) =>
|
|
renderIndividualBookmark(individualBookmark, index)
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{bookmark.eventReferences && bookmark.eventReferences.length > 0 && bookmark.individualBookmarks?.length === 0 && (
|
|
<div className="bookmark-events">
|
|
<h4>Event References ({bookmark.eventReferences.length}):</h4>
|
|
<div className="event-ids">
|
|
{bookmark.eventReferences.slice(0, 3).map((eventId, index) => (
|
|
<span key={index} className="event-id">
|
|
{eventId.slice(0, 8)}...{eventId.slice(-8)}
|
|
</span>
|
|
))}
|
|
{bookmark.eventReferences.length > 3 && (
|
|
<span className="more-events">... and {bookmark.eventReferences.length - 3} more</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
{bookmark.parsedContent ? (
|
|
<div className="bookmark-content">
|
|
{renderParsedContent(bookmark.parsedContent)}
|
|
</div>
|
|
) : bookmark.content && (
|
|
<p className="bookmark-content">{bookmark.content}</p>
|
|
)}
|
|
<div className="bookmark-meta">
|
|
<span>Created: {formatDate(bookmark.created_at)}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Bookmarks
|