feat: implement individual bookmark fetching and display

- Add IndividualBookmark interface for individual bookmark events
- Implement fetchIndividualBookmarks function to fetch events by e and a tags
- Update parseBookmarkEvent to be async and fetch individual bookmarks
- Add renderIndividualBookmark component for displaying individual bookmarks
- Update UI to show individual bookmarks in a grid layout
- Add CSS styles for individual bookmarks with dark/light mode support
- Support both event references (e tags) and article references (a tags)
- Use applesauce content parsing for proper content rendering
This commit is contained in:
Gigi
2025-10-02 09:05:32 +02:00
parent e2690e7177
commit bf79bbceb8
2 changed files with 252 additions and 6 deletions

View File

@@ -33,6 +33,19 @@ interface Bookmark {
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 {
@@ -110,7 +123,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
const bookmarkList: Bookmark[] = []
for (const event of uniqueEvents) {
console.log('Processing bookmark event:', event)
const bookmarkData = parseBookmarkEvent(event)
const bookmarkData = await parseBookmarkEvent(event)
if (bookmarkData) {
bookmarkList.push(bookmarkData)
console.log('Parsed bookmark:', bookmarkData)
@@ -127,7 +140,85 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
}
}
const parseBookmarkEvent = (event: NostrEvent): Bookmark | null => {
const fetchIndividualBookmarks = async (eventIds: string[], articleIds: string[]): Promise<IndividualBookmark[]> => {
if (!relayPool || (eventIds.length === 0 && articleIds.length === 0)) {
return []
}
try {
const allIds = [...eventIds, ...articleIds]
console.log('Fetching individual bookmarks for IDs:', allIds.length)
// Create filters for both event IDs and article IDs
const eventFilters: Filter[] = []
if (eventIds.length > 0) {
eventFilters.push({
ids: eventIds
})
}
if (articleIds.length > 0) {
// For article IDs, we need to parse the kind and pubkey from the 'a' tag
const articleFilters = articleIds.map(articleId => {
const [kind, pubkey, identifier] = articleId.split(':')
return {
kinds: [parseInt(kind)],
authors: [pubkey],
'#d': [identifier]
}
})
eventFilters.push(...articleFilters)
}
const allEvents: NostrEvent[] = []
// Fetch events for each filter
for (const filter of eventFilters) {
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
const events = await lastValueFrom(
relayPool.req(relayUrls, filter).pipe(
completeOnEose(),
takeUntil(timer(10000)),
toArray(),
)
)
allEvents.push(...events)
}
// Deduplicate events
const uniqueEvents = allEvents.reduce((acc, event) => {
if (!acc.find(e => e.id === event.id)) {
acc.push(event)
}
return acc
}, [] as NostrEvent[])
console.log('Fetched individual bookmarks:', uniqueEvents.length)
// Convert to IndividualBookmark format
return uniqueEvents.map(event => {
const parsedContent = event.content ? getParsedContent(event.content) as ParsedContent : undefined
const isArticle = articleIds.includes(event.id) || event.tags.some(tag => tag[0] === 'a')
return {
id: event.id,
content: event.content,
created_at: event.created_at,
pubkey: event.pubkey,
kind: event.kind,
tags: event.tags,
parsedContent: parsedContent,
type: isArticle ? 'article' : 'event'
}
})
} catch (error) {
console.error('Error fetching individual bookmarks:', error)
return []
}
}
const parseBookmarkEvent = async (event: NostrEvent): Promise<Bookmark | null> => {
try {
// According to NIP-51, bookmark lists (kind 10003) contain:
// - "e" tags for event references (the actual bookmarks)
@@ -144,6 +235,11 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
// Get the title from content or use a default
const title = event.content || `Bookmark List (${eventTags.length + articleTags.length + urlTags.length} items)`
// Fetch individual bookmarks
const eventIds = eventTags.map(tag => tag[1])
const articleIds = articleTags.map(tag => tag[1])
const individualBookmarks = await fetchIndividualBookmarks(eventIds, articleIds)
return {
id: event.id,
title: title,
@@ -154,9 +250,10 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
parsedContent: parsedContent,
// Add metadata about the bookmark list
bookmarkCount: eventTags.length + articleTags.length + urlTags.length,
eventReferences: eventTags.map(tag => tag[1]),
articleReferences: articleTags.map(tag => tag[1]),
urlReferences: urlTags.map(tag => tag[1])
eventReferences: eventIds,
articleReferences: articleIds,
urlReferences: urlTags.map(tag => tag[1]),
individualBookmarks: individualBookmarks
}
} catch (error) {
console.error('Error parsing bookmark event:', error)
@@ -229,6 +326,34 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
)
}
// 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'
@@ -305,7 +430,17 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
))}
</div>
)}
{bookmark.eventReferences && bookmark.eventReferences.length > 0 && (
{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">

View File

@@ -288,6 +288,90 @@ body {
margin-top: 0.5rem;
}
/* Individual Bookmarks Styles */
.individual-bookmarks {
margin: 1rem 0;
}
.individual-bookmarks h4 {
margin: 0 0 1rem 0;
font-size: 1rem;
color: #fff;
}
.bookmarks-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.individual-bookmark {
background: #2a2a2a;
padding: 1rem;
border-radius: 6px;
border: 1px solid #444;
transition: border-color 0.2s;
}
.individual-bookmark:hover {
border-color: #646cff;
}
.bookmark-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
flex-wrap: wrap;
gap: 0.5rem;
}
.bookmark-type {
background: #646cff;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
text-transform: uppercase;
}
.bookmark-id {
font-family: monospace;
font-size: 0.8rem;
color: #888;
background: #1a1a1a;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.bookmark-date {
font-size: 0.8rem;
color: #666;
}
.individual-bookmark .bookmark-content {
margin: 0.75rem 0;
color: #ccc;
line-height: 1.5;
}
.individual-bookmark .bookmark-meta {
display: flex;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.8rem;
color: #888;
margin-top: 0.75rem;
}
.individual-bookmark .bookmark-meta span {
background: #1a1a1a;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: monospace;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
@@ -315,4 +399,31 @@ body {
.user-info {
color: #666;
}
.individual-bookmark {
background: #f5f5f5;
border-color: #ddd;
}
.individual-bookmark:hover {
border-color: #646cff;
}
.individual-bookmarks h4 {
color: #213547;
}
.individual-bookmark .bookmark-content {
color: #666;
}
.bookmark-id {
background: #e9ecef;
color: #666;
}
.individual-bookmark .bookmark-meta span {
background: #e9ecef;
color: #666;
}
}