Compare commits

...

11 Commits

Author SHA1 Message Date
Gigi
9c34e8d806 chore: bump version to 0.0.2
- Version bump for working bookmark fetching functionality
- Individual bookmark display is now working correctly
2025-10-02 09:16:15 +02:00
Gigi
934043f858 fix: resolve loading state stuck issue
- Remove loading check from early return condition
- Add timeout to ensure loading state gets reset
- Clear timeout when function completes
- This should allow fetchBookmarks to run properly
2025-10-02 09:15:04 +02:00
Gigi
774ce0f1bf debug: add detailed logging to fetchBookmarks function
- Add console logs to track if fetchBookmarks is called
- Log early return conditions
- This will help identify why the function might not be executing
2025-10-02 09:14:24 +02:00
Gigi
6481dd1bed fix: remove unused NostrEvent import
- Fix TypeScript compilation error
- Remove unused import that was causing build issues
2025-10-02 09:13:43 +02:00
Gigi
d7b5b4f9b4 debug: add obvious console log to verify new code is running 2025-10-02 09:13:28 +02:00
Gigi
bed0f3d508 refactor: implement proper bookmark fetching flow
- 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
2025-10-02 09:12:05 +02:00
Gigi
44954a6c15 debug: add test filter to check if any events are found
- Add broader test filter to see if user has any events
- Log event kinds to understand what events exist
- This will help diagnose if the issue is with bookmark events specifically
2025-10-02 09:09:05 +02:00
Gigi
a43e742183 fix: temporarily disable individual bookmark fetching
- Disable individual bookmark fetching to isolate the issue
- Keep basic bookmark list functionality working
- Add TODO to re-enable individual bookmark fetching later
2025-10-02 09:08:51 +02:00
Gigi
a97808b23e debug: add debugging logs to bookmark fetching
- Add console logs to track bookmark fetching progress
- Add early return when no bookmark events found
- Reduce timeout for individual bookmark fetching
- Add debugging for individual bookmark fetching process
2025-10-02 09:08:45 +02:00
Gigi
bf79bbceb8 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
2025-10-02 09:05:32 +02:00
Gigi
e2690e7177 fix: resolve duplicate events and React key warnings
- Add event deduplication by ID to prevent duplicates from multiple relays
- Fix React key warnings by using unique keys with index
- Prevent multiple simultaneous fetchBookmarks calls with loading state check
- Optimize useEffect dependencies to only depend on pubkey
- Add logging for deduplication process
2025-10-02 09:00:23 +02:00
3 changed files with 268 additions and 72 deletions

View File

@@ -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": {

View File

@@ -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">

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;
}
}