fix: properly parse NIP-51 bookmark lists (kind 10003) according to specification

- Parse bookmark lists correctly according to NIP-51 specification
- Handle 'e' tags for event references (the actual bookmarks)
- Handle 'a' tags for article references
- Handle 'r' tags for URL references
- Display bookmark count and organize references by type
- Show event IDs in a readable format with truncation
- Add proper CSS styling for bookmark list display
- Update Bookmark interface to include metadata fields

This fixes the issue where kind:10003 bookmark lists weren't being
displayed properly. Now follows the NIP-51 specification exactly
as shown in the example event structure.
This commit is contained in:
Gigi
2025-10-02 08:31:19 +02:00
parent 9eeda77132
commit b6721f685b
2 changed files with 115 additions and 38 deletions

View File

@@ -11,6 +11,10 @@ interface Bookmark {
content: string content: string
created_at: number created_at: number
tags: string[][] tags: string[][]
bookmarkCount?: number
eventReferences?: string[]
articleReferences?: string[]
urlReferences?: string[]
} }
interface BookmarksProps { interface BookmarksProps {
@@ -87,37 +91,31 @@ const Bookmarks: React.FC<BookmarksProps> = ({ addressLoader, onLogout }) => {
const parseBookmarkEvent = (event: NostrEvent): Bookmark | null => { const parseBookmarkEvent = (event: NostrEvent): Bookmark | null => {
try { try {
// Parse the event content as JSON (bookmark list) // According to NIP-51, bookmark lists (kind 10003) contain:
const content = JSON.parse(event.content || '{}') // - "e" tags for event references (the actual bookmarks)
// - "a" tags for article references
// - "r" tags for URL references
const eventTags = event.tags.filter((tag: string[]) => tag[0] === 'e')
const articleTags = event.tags.filter((tag: string[]) => tag[0] === 'a')
const urlTags = event.tags.filter((tag: string[]) => tag[0] === 'r')
// Get the title from content or use a default
const title = event.content || `Bookmark List (${eventTags.length + articleTags.length + urlTags.length} items)`
if (content.bookmarks && Array.isArray(content.bookmarks)) {
// Handle bookmark list format
return { return {
id: event.id, id: event.id,
title: content.name || 'Untitled Bookmark List', title: title,
url: '', url: '', // Bookmark lists don't have a single URL
content: event.content, content: event.content,
created_at: event.created_at, created_at: event.created_at,
tags: event.tags tags: event.tags,
// 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])
} }
}
// Handle individual bookmark entries
const urlTag = event.tags.find((tag: string[]) => tag[0] === 'r' && tag[1])
const titleTag = event.tags.find((tag: string[]) => tag[0] === 'title' && tag[1])
if (urlTag) {
return {
id: event.id,
title: titleTag?.[1] || 'Untitled',
url: urlTag[1],
content: event.content,
created_at: event.created_at,
tags: event.tags
}
}
return null
} catch (error) { } catch (error) {
console.error('Error parsing bookmark event:', error) console.error('Error parsing bookmark event:', error)
return null return null
@@ -189,21 +187,41 @@ const Bookmarks: React.FC<BookmarksProps> = ({ addressLoader, onLogout }) => {
{bookmarks.map((bookmark) => ( {bookmarks.map((bookmark) => (
<div key={bookmark.id} className="bookmark-item"> <div key={bookmark.id} className="bookmark-item">
<h3>{bookmark.title}</h3> <h3>{bookmark.title}</h3>
{bookmark.url && ( {bookmark.bookmarkCount && (
<a <p className="bookmark-count">
href={bookmark.url} {bookmark.bookmarkCount} bookmarks in this list
target="_blank" </p>
rel="noopener noreferrer" )}
className="bookmark-url" {bookmark.urlReferences && bookmark.urlReferences.length > 0 && (
> <div className="bookmark-urls">
{bookmark.url} <h4>URLs:</h4>
{bookmark.urlReferences.map((url, index) => (
<a key={index} href={url} target="_blank" rel="noopener noreferrer" className="bookmark-url">
{url}
</a> </a>
))}
</div>
)}
{bookmark.eventReferences && bookmark.eventReferences.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.content && ( {bookmark.content && (
<p className="bookmark-content">{bookmark.content}</p> <p className="bookmark-content">{bookmark.content}</p>
)} )}
<div className="bookmark-meta"> <div className="bookmark-meta">
<span>Added: {formatDate(bookmark.created_at)}</span> <span>Created: {formatDate(bookmark.created_at)}</span>
</div> </div>
</div> </div>
))} ))}

View File

@@ -122,6 +122,65 @@ body {
font-family: monospace; font-family: monospace;
} }
.bookmark-count {
color: #666;
font-size: 0.9rem;
margin: 0.5rem 0;
}
.bookmark-urls {
margin: 1rem 0;
}
.bookmark-urls h4 {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: #666;
}
.bookmark-url {
display: block;
margin: 0.25rem 0;
color: #007bff;
text-decoration: none;
word-break: break-all;
}
.bookmark-url:hover {
text-decoration: underline;
}
.bookmark-events {
margin: 1rem 0;
}
.bookmark-events h4 {
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
color: #666;
}
.event-ids {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.event-id {
background: #f5f5f5;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.8rem;
color: #666;
}
.more-events {
color: #999;
font-style: italic;
font-size: 0.8rem;
}
.logout-button { .logout-button {
background: #dc3545; background: #dc3545;
color: white; color: white;