mirror of
https://github.com/dergigi/boris.git
synced 2026-02-16 12:34:41 +01:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c34e8d806 | ||
|
|
934043f858 | ||
|
|
774ce0f1bf | ||
|
|
6481dd1bed | ||
|
|
d7b5b4f9b4 | ||
|
|
bed0f3d508 | ||
|
|
44954a6c15 | ||
|
|
a43e742183 | ||
|
|
a97808b23e | ||
|
|
bf79bbceb8 | ||
|
|
e2690e7177 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "markr",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"description": "A minimal nostr client for bookmark management",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 { Filter } from 'nostr-tools'
|
||||
import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs'
|
||||
|
||||
interface ParsedNode {
|
||||
@@ -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 {
|
||||
@@ -58,101 +71,135 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
} else {
|
||||
console.log('Not fetching bookmarks - missing dependencies')
|
||||
}
|
||||
}, [relayPool, activeAccount])
|
||||
}, [relayPool, activeAccount?.pubkey]) // Only depend on pubkey, not the entire activeAccount object
|
||||
|
||||
const fetchBookmarks = async () => {
|
||||
if (!relayPool || !activeAccount) return
|
||||
console.log('🔍 fetchBookmarks called, loading:', loading)
|
||||
if (!relayPool || !activeAccount) {
|
||||
console.log('🔍 fetchBookmarks early return - relayPool:', !!relayPool, 'activeAccount:', !!activeAccount)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
console.log('Fetching bookmarks for pubkey:', activeAccount.pubkey)
|
||||
console.log('Starting bookmark fetch for:', activeAccount.pubkey.slice(0, 8) + '...')
|
||||
console.log('🚀 NEW VERSION: Fetching bookmark list for pubkey:', activeAccount.pubkey)
|
||||
|
||||
// Use applesauce relay pool to fetch bookmark events (kind 10003)
|
||||
// This follows the proper applesauce pattern from the documentation
|
||||
|
||||
// Create a filter for bookmark events (kind 10003) for the specific pubkey
|
||||
const filter: Filter = {
|
||||
kinds: [10003],
|
||||
authors: [activeAccount.pubkey]
|
||||
}
|
||||
// Set a timeout to ensure loading state gets reset
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.log('⏰ Timeout reached, resetting loading state')
|
||||
setLoading(false)
|
||||
}, 15000) // 15 second timeout
|
||||
|
||||
// Get relay URLs from the pool
|
||||
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
|
||||
console.log('Querying relay pool with filter:', filter)
|
||||
console.log('Using relays:', relayUrls)
|
||||
|
||||
// Use the proper applesauce pattern with req() method
|
||||
const events = await lastValueFrom(
|
||||
relayPool.req(relayUrls, filter).pipe(
|
||||
// Complete when EOSE is received
|
||||
// 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(),
|
||||
// Timeout after 10 seconds
|
||||
takeUntil(timer(10000)),
|
||||
// Collect all events into an array
|
||||
toArray(),
|
||||
)
|
||||
)
|
||||
|
||||
console.log('Received events:', events.length)
|
||||
console.log('Found bookmark list events:', bookmarkListEvents.length)
|
||||
|
||||
// Parse the events into bookmarks
|
||||
const bookmarkList: Bookmark[] = []
|
||||
for (const event of events) {
|
||||
console.log('Processing bookmark event:', event)
|
||||
const bookmarkData = parseBookmarkEvent(event)
|
||||
if (bookmarkData) {
|
||||
bookmarkList.push(bookmarkData)
|
||||
console.log('Parsed bookmark:', bookmarkData)
|
||||
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('Bookmark fetch complete. Found:', bookmarkList.length, 'bookmarks')
|
||||
setBookmarks(bookmarkList)
|
||||
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])
|
||||
clearTimeout(timeoutId)
|
||||
setLoading(false)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch bookmarks:', error)
|
||||
clearTimeout(timeoutId)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const parseBookmarkEvent = (event: NostrEvent): Bookmark | null => {
|
||||
try {
|
||||
// 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')
|
||||
|
||||
// Use applesauce-content to parse the content properly
|
||||
const parsedContent = event.content ? getParsedContent(event.content) as ParsedContent : undefined
|
||||
|
||||
// Get the title from content or use a default
|
||||
const title = event.content || `Bookmark List (${eventTags.length + articleTags.length + urlTags.length} items)`
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
title: title,
|
||||
url: '', // Bookmark lists don't have a single URL
|
||||
content: event.content,
|
||||
created_at: event.created_at,
|
||||
tags: event.tags,
|
||||
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])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing bookmark event:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
return new Date(timestamp * 1000).toLocaleDateString()
|
||||
@@ -219,6 +266,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'
|
||||
|
||||
@@ -277,8 +352,8 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="bookmarks-list">
|
||||
{bookmarks.map((bookmark) => (
|
||||
<div key={bookmark.id} className="bookmark-item">
|
||||
{bookmarks.map((bookmark, index) => (
|
||||
<div key={`${bookmark.id}-${index}`} className="bookmark-item">
|
||||
<h3>{bookmark.title}</h3>
|
||||
{bookmark.bookmarkCount && (
|
||||
<p className="bookmark-count">
|
||||
@@ -295,7 +370,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">
|
||||
|
||||
111
src/index.css
111
src/index.css
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user