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
created_at: number
tags: string[][]
bookmarkCount?: number
eventReferences?: string[]
articleReferences?: string[]
urlReferences?: string[]
}
interface BookmarksProps {
@@ -87,37 +91,31 @@ const Bookmarks: React.FC<BookmarksProps> = ({ addressLoader, onLogout }) => {
const parseBookmarkEvent = (event: NostrEvent): Bookmark | null => {
try {
// Parse the event content as JSON (bookmark list)
const content = JSON.parse(event.content || '{}')
// According to NIP-51, bookmark lists (kind 10003) contain:
// - "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 {
id: event.id,
title: content.name || 'Untitled Bookmark List',
url: '',
title: title,
url: '', // Bookmark lists don't have a single URL
content: event.content,
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) {
console.error('Error parsing bookmark event:', error)
return null
@@ -189,21 +187,41 @@ const Bookmarks: React.FC<BookmarksProps> = ({ addressLoader, onLogout }) => {
{bookmarks.map((bookmark) => (
<div key={bookmark.id} className="bookmark-item">
<h3>{bookmark.title}</h3>
{bookmark.url && (
<a
href={bookmark.url}
target="_blank"
rel="noopener noreferrer"
className="bookmark-url"
>
{bookmark.url}
{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.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 && (
<p className="bookmark-content">{bookmark.content}</p>
)}
<div className="bookmark-meta">
<span>Added: {formatDate(bookmark.created_at)}</span>
<span>Created: {formatDate(bookmark.created_at)}</span>
</div>
</div>
))}

View File

@@ -122,6 +122,65 @@ body {
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 {
background: #dc3545;
color: white;