From d20cc684c3a6f931c4f4ed7c2db38d9a29d3d5d3 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 21 Oct 2025 23:50:12 +0200 Subject: [PATCH 01/43] feat: ensure kind:1 events display their text content in bookmarks bar - Update hydrateItems to parse content for all events with text - Previously, kind:1 events without URLs would appear empty in the bookmarks list - Now any kind:1 event will display its text content appropriately - Improves handling of short-form text notes in bookmarks --- src/services/bookmarkHelpers.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index d8803a42..3795d5a7 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -184,6 +184,9 @@ export function hydrateItems( } } + // Ensure all events with content get parsed content for proper rendering + const parsedContent = content ? (getParsedContent(content) as ParsedContent) : undefined + return { ...item, pubkey: ev.pubkey || item.pubkey, @@ -191,7 +194,7 @@ export function hydrateItems( created_at: ev.created_at || item.created_at, kind: ev.kind || item.kind, tags: ev.tags || item.tags, - parsedContent: ev.content ? (getParsedContent(content) as ParsedContent) : item.parsedContent + parsedContent: parsedContent || item.parsedContent } }) .filter(item => { From 96451e61733bf2b7c8c0a95f91579738bfee1cfc Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 21 Oct 2025 23:52:39 +0200 Subject: [PATCH 02/43] debug: add logging to track kind:1 event hydration - Log when kind:1 events are fetched by EventLoader - Log when kind:1 events are hydrated with content - Helps diagnose why text content isn't displaying for bookmarked notes --- src/services/bookmarkController.ts | 10 ++++++++++ src/services/bookmarkHelpers.ts | 9 +++++++++ 2 files changed, 19 insertions(+) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index 3a5c16c5..ddf409b0 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -146,6 +146,16 @@ class BookmarkController { idToEvent.set(event.id, event) + // Debug logging for kind:1 events + if (event.kind === 1) { + console.log('📝 Fetched kind:1 event:', { + id: event.id.slice(0, 8), + content: event.content?.slice(0, 50), + contentLength: event.content?.length, + created_at: event.created_at + }) + } + // Also index by coordinate for addressable events if (event.kind && event.kind >= 30000 && event.kind < 40000) { const dTag = event.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index 3795d5a7..8c7919bb 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -184,6 +184,15 @@ export function hydrateItems( } } + // Debug logging for kind:1 events + if (ev.kind === 1 && content) { + console.log('💧 Hydrated kind:1 with content:', { + id: item.id.slice(0, 8), + content: content.slice(0, 50), + contentLength: content.length + }) + } + // Ensure all events with content get parsed content for proper rendering const parsedContent = content ? (getParsedContent(content) as ParsedContent) : undefined From 2791c69ebe0d452c0153b6d1ac485aef80a7651b Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 21 Oct 2025 23:54:15 +0200 Subject: [PATCH 03/43] debug: add logging to CompactView to diagnose missing content rendering - Log when kind:1 without URLs is being rendered - Check if bookmark.content is actually present at render time - Help diagnose why text isn't displaying even though it's hydrated --- src/components/BookmarkViews/CompactView.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index 6be3d76f..e7f7fad1 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -30,6 +30,17 @@ export const CompactView: React.FC = ({ const isWebBookmark = bookmark.kind === 39701 const isClickable = hasUrls || isArticle || isWebBookmark + // Debug logging for kind:1 without URLs + if (bookmark.kind === 1 && !hasUrls) { + console.log('🎨 CompactView rendering kind:1 without URLs:', { + id: bookmark.id.slice(0, 8), + content: bookmark.content?.slice(0, 50), + contentLength: bookmark.content?.length, + hasUrls, + displayText: (isArticle && articleSummary ? articleSummary : bookmark.content)?.slice(0, 50) + }) + } + // Calculate progress color (matching BlogPostCard logic) let progressColor = '#6366f1' // Default blue (reading) if (readingProgress && readingProgress >= 0.95) { From e08ce0e477e70fb8c11174f7800931a8a80e1634 Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 21 Oct 2025 23:55:10 +0200 Subject: [PATCH 04/43] debug: add BookmarkList logging to track kind:1 filtering - Log how many kind:1 bookmarks make it past the hasContent filter - Show sample content to verify hydration is reaching the list - Help identify where bookmarks are being filtered out --- src/components/BookmarkList.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index b4bdff72..3a8bb1d7 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -145,6 +145,19 @@ export const BookmarkList: React.FC = ({ .filter(hasContent) .filter(b => !settings?.hideBookmarksWithoutCreationDate || hasCreationDate(b)) + // Debug: log kind:1 events in allIndividualBookmarks + const kind1Bookmarks = allIndividualBookmarks.filter(b => b.kind === 1) + if (kind1Bookmarks.length > 0) { + console.log('📊 BookmarkList kind:1 events after filtering:', { + total: kind1Bookmarks.length, + samples: kind1Bookmarks.slice(0, 3).map(b => ({ + id: b.id.slice(0, 8), + content: b.content?.slice(0, 30), + hasUrls: extractUrlsFromContent(b.content).length > 0 + })) + }) + } + // Apply filter const filteredBookmarks = filterBookmarksByType(allIndividualBookmarks, selectedFilter) From 51e48804fe48ebdbc7aa1d488456481aae32bb5d Mon Sep 17 00:00:00 2001 From: Gigi Date: Tue, 21 Oct 2025 23:58:16 +0200 Subject: [PATCH 05/43] debug: remove console logging for kind:1 hydration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed 📝, 💧, 🎨 and 📊 debug logs - These were added for troubleshooting but are no longer needed - Kind:1 content hydration and rendering is working correctly --- src/components/BookmarkList.tsx | 13 ------------- src/components/BookmarkViews/CompactView.tsx | 20 +++----------------- src/services/bookmarkController.ts | 10 ---------- src/services/bookmarkHelpers.ts | 9 --------- 4 files changed, 3 insertions(+), 49 deletions(-) diff --git a/src/components/BookmarkList.tsx b/src/components/BookmarkList.tsx index 3a8bb1d7..b4bdff72 100644 --- a/src/components/BookmarkList.tsx +++ b/src/components/BookmarkList.tsx @@ -145,19 +145,6 @@ export const BookmarkList: React.FC = ({ .filter(hasContent) .filter(b => !settings?.hideBookmarksWithoutCreationDate || hasCreationDate(b)) - // Debug: log kind:1 events in allIndividualBookmarks - const kind1Bookmarks = allIndividualBookmarks.filter(b => b.kind === 1) - if (kind1Bookmarks.length > 0) { - console.log('📊 BookmarkList kind:1 events after filtering:', { - total: kind1Bookmarks.length, - samples: kind1Bookmarks.slice(0, 3).map(b => ({ - id: b.id.slice(0, 8), - content: b.content?.slice(0, 30), - hasUrls: extractUrlsFromContent(b.content).length > 0 - })) - }) - } - // Apply filter const filteredBookmarks = filterBookmarksByType(allIndividualBookmarks, selectedFilter) diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index e7f7fad1..ac760adb 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -30,18 +30,9 @@ export const CompactView: React.FC = ({ const isWebBookmark = bookmark.kind === 39701 const isClickable = hasUrls || isArticle || isWebBookmark - // Debug logging for kind:1 without URLs - if (bookmark.kind === 1 && !hasUrls) { - console.log('🎨 CompactView rendering kind:1 without URLs:', { - id: bookmark.id.slice(0, 8), - content: bookmark.content?.slice(0, 50), - contentLength: bookmark.content?.length, - hasUrls, - displayText: (isArticle && articleSummary ? articleSummary : bookmark.content)?.slice(0, 50) - }) - } - - // Calculate progress color (matching BlogPostCard logic) + const displayText = isArticle && articleSummary ? articleSummary : bookmark.content + + // Calculate progress color let progressColor = '#6366f1' // Default blue (reading) if (readingProgress && readingProgress >= 0.95) { progressColor = '#10b981' // Green (completed) @@ -59,11 +50,6 @@ export const CompactView: React.FC = ({ } } - // For articles, prefer summary; for others, use content - const displayText = isArticle && articleSummary - ? articleSummary - : bookmark.content - return (
= 30000 && event.kind < 40000) { const dTag = event.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index 8c7919bb..3795d5a7 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -184,15 +184,6 @@ export function hydrateItems( } } - // Debug logging for kind:1 events - if (ev.kind === 1 && content) { - console.log('💧 Hydrated kind:1 with content:', { - id: item.id.slice(0, 8), - content: content.slice(0, 50), - contentLength: content.length - }) - } - // Ensure all events with content get parsed content for proper rendering const parsedContent = content ? (getParsedContent(content) as ParsedContent) : undefined From a081b263336ac02899761f361c62fc07e3d67f40 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:02:11 +0200 Subject: [PATCH 06/43] feat: show event IDs for empty bookmarks and add debug logging - Display event ID (first 12 chars) when bookmark content is missing - Shows ID in dimmed code font as fallback for empty items - Add debug console logging to identify which bookmarks are empty - Helps diagnose hydration issues and identify events that aren't loading --- src/components/BookmarkViews/CompactView.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index ac760adb..01da1bd8 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -32,6 +32,19 @@ export const CompactView: React.FC = ({ const displayText = isArticle && articleSummary ? articleSummary : bookmark.content + // Debug empty bookmarks + if (!displayText && bookmark.kind === 1) { + console.log('📌 Empty kind:1 bookmark:', { + id: bookmark.id.slice(0, 12), + content: bookmark.content, + contentLength: bookmark.content?.length, + contentType: typeof bookmark.content, + parsedContent: !!bookmark.parsedContent, + created_at: bookmark.created_at, + sourceKind: (bookmark as any).sourceKind + }) + } + // Calculate progress color let progressColor = '#6366f1' // Default blue (reading) if (readingProgress && readingProgress >= 0.95) { @@ -61,10 +74,14 @@ export const CompactView: React.FC = ({ - {displayText && ( + {displayText ? (
60 ? '…' : '')} className="" />
+ ) : ( +
+ {bookmark.id.slice(0, 12)}... +
)} {formatDateCompact(bookmark.created_at)} {/* CTA removed */} From 312adea9f92438295c565b1c9a29ae5824724c56 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:03:14 +0200 Subject: [PATCH 07/43] debug: add hydration logging to diagnose empty bookmarks - Log when kind:1 events are fetched from relays - Log when bookmarks are emitted with hydration status - Track how many events are in the idToEvent map - Check if event IDs match between bookmarks and fetched events --- src/services/bookmarkController.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index 3a5c16c5..5a63b10d 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -146,6 +146,14 @@ class BookmarkController { idToEvent.set(event.id, event) + if (event.kind === 1 && event.content) { + console.log('✅ Fetched kind:1 with content:', { + id: event.id.slice(0, 12), + content: event.content.slice(0, 30), + totalInMap: idToEvent.size + }) + } + // Also index by coordinate for addressable events if (event.kind && event.kind >= 30000 && event.kind < 40000) { const dTag = event.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' @@ -263,6 +271,21 @@ class BookmarkController { ...hydrateItems(publicItemsAll, idToEvent), ...hydrateItems(privateItemsAll, idToEvent) ]) + + // Debug: log what we have + const kind1Items = allBookmarks.filter(b => b.kind === 1) + if (kind1Items.length > 0) { + console.log('🔄 Emitting bookmarks with hydration:', { + totalKind1: kind1Items.length, + withContent: kind1Items.filter(b => b.content).length, + mapSize: idToEvent.size, + sample: kind1Items.slice(0, 2).map(b => ({ + id: b.id.slice(0, 12), + content: b.content?.slice(0, 30), + inMap: idToEvent.has(b.id) + })) + }) + } const enriched = allBookmarks.map(b => ({ ...b, From 004367bab622f33f33dba75b8d4634146b4f8dc5 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:05:04 +0200 Subject: [PATCH 08/43] debug: log the actual Bookmark object being emitted to component - Show what's actually in individualBookmarks when emitted - Check if content is present in the emitted object vs what component receives - Identify if the issue is in hydration or state propagation --- src/services/bookmarkController.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index 5a63b10d..3e522341 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -311,6 +311,20 @@ class BookmarkController { encryptedContent: undefined } + // Debug: log the actual content being emitted + const kind1InEmit = sortedBookmarks.filter(b => b.kind === 1) + if (kind1InEmit.length > 0) { + console.log('📤 Emitting Bookmark object with individualBookmarks:', { + totalKind1: kind1InEmit.length, + withContent: kind1InEmit.filter(b => b.content && b.content.length > 0).length, + samples: kind1InEmit.slice(0, 2).map(b => ({ + id: b.id.slice(0, 12), + content: b.content?.slice(0, 20), + contentLength: b.content?.length + })) + }) + } + this.bookmarksListeners.forEach(cb => cb([bookmark])) } From 0f5d42465dfc32374ccff4c16013da0426ee897b Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:08:47 +0200 Subject: [PATCH 09/43] debug: add detailed logging to hydrateItems - Log which kind:1 items are being processed - Show how many match events in the idToEvent map - Compare sample IDs from items vs map keys - Identify ID mismatch issue between bookmarks and fetched events --- src/services/bookmarkHelpers.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index 3795d5a7..ba2797f2 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -170,6 +170,20 @@ export function hydrateItems( items: IndividualBookmark[], idToEvent: Map ): IndividualBookmark[] { + // Debug: log what we're trying to hydrate + const kind1Items = items.filter(b => b.kind === 1) + if (kind1Items.length > 0 && idToEvent.size > 0) { + const found = kind1Items.filter(b => idToEvent.has(b.id)) + console.log('💧 hydrateItems processing:', { + totalKind1Items: kind1Items.length, + mapSize: idToEvent.size, + found: found.length, + notFound: kind1Items.length - found.length, + sampleItemIds: kind1Items.slice(0, 2).map(b => b.id.slice(0, 12)), + sampleMapKeys: Array.from(idToEvent.keys()).slice(0, 2).map(id => id.slice(0, 12)) + }) + } + return items .map(item => { const ev = idToEvent.get(item.id) From f02bc21faf74600c00f38ccd3047ee1bf243e83f Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:10:13 +0200 Subject: [PATCH 10/43] debug: simplify hydration logging for easier diagnosis - Show how many items were matched in the map - If zero matches, show actual IDs from both sides - Makes it easy to see ID mismatch issues --- src/services/bookmarkHelpers.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index ba2797f2..c72f5fcd 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -174,14 +174,11 @@ export function hydrateItems( const kind1Items = items.filter(b => b.kind === 1) if (kind1Items.length > 0 && idToEvent.size > 0) { const found = kind1Items.filter(b => idToEvent.has(b.id)) - console.log('💧 hydrateItems processing:', { - totalKind1Items: kind1Items.length, - mapSize: idToEvent.size, - found: found.length, - notFound: kind1Items.length - found.length, - sampleItemIds: kind1Items.slice(0, 2).map(b => b.id.slice(0, 12)), - sampleMapKeys: Array.from(idToEvent.keys()).slice(0, 2).map(id => id.slice(0, 12)) - }) + console.log(`💧 hydrateItems: ${found.length}/${kind1Items.length} kind:1 items found in map (map has ${idToEvent.size} total events)`) + if (found.length === 0 && kind1Items.length > 0) { + console.log('❌ NO MATCHES! Sample item IDs:', kind1Items.slice(0, 3).map(b => b.id)) + console.log('❌ Map keys:', Array.from(idToEvent.keys()).slice(0, 3)) + } } return items From 42d71438453d9f0718c60e1dbc26765441e83831 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:11:06 +0200 Subject: [PATCH 11/43] debug: add logging for event ID requests - Log how many note IDs and coordinates we're requesting - Log how many unique event IDs are passed to EventLoader - Track if all bookmarks are being requested for hydration --- src/services/bookmarkController.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index 3e522341..21127bb0 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -135,6 +135,8 @@ class BookmarkController { return } + console.log(`📡 hydrateByIds: requesting ${unique.length} events from EventLoader`) + // Convert IDs to EventPointers const pointers: EventPointer[] = unique.map(id => ({ id })) @@ -265,6 +267,8 @@ class BookmarkController { } }) + console.log(`📋 Requesting hydration for: ${noteIds.length} note IDs, ${coordinates.length} coordinates`) + // Helper to build and emit bookmarks const emitBookmarks = (idToEvent: Map) => { const allBookmarks = dedupeBookmarksById([ From 30ed5fb4362554e9558a8a59b3f0ec0154d2f076 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:12:34 +0200 Subject: [PATCH 12/43] fix: batch event hydration with concurrency limit - Replace merge(...map(eventLoader)) with mergeMap concurrency: 5 - Prevents overwhelming relays with 96+ simultaneous requests - EventLoader now properly throttles to 5 concurrent requests at a time - Fixes issue where only ~7 out of 96 events were being fetched --- src/services/bookmarkController.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index 21127bb0..17a799b0 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -3,7 +3,8 @@ import { Helpers, EventStore } from 'applesauce-core' import { createEventLoader, createAddressLoader } from 'applesauce-loaders/loaders' import { NostrEvent } from 'nostr-tools' import { EventPointer } from 'nostr-tools/nip19' -import { merge } from 'rxjs' +import { merge, from } from 'rxjs' +import { mergeMap } from 'rxjs/operators' import { queryEvents } from './dataFetch' import { KINDS } from '../config/kinds' import { RELAYS } from '../config/relays' @@ -140,8 +141,11 @@ class BookmarkController { // Convert IDs to EventPointers const pointers: EventPointer[] = unique.map(id => ({ id })) - // Use EventLoader - it auto-batches and streams results - merge(...pointers.map(this.eventLoader)).subscribe({ + // Use mergeMap with concurrency limit instead of merge to properly batch requests + // This prevents overwhelming relays with 96+ simultaneous requests + from(pointers).pipe( + mergeMap(pointer => this.eventLoader!(pointer), { concurrency: 5 }) + ).subscribe({ next: (event) => { // Check if hydration was cancelled if (this.hydrationGeneration !== generation) return @@ -193,8 +197,10 @@ class BookmarkController { identifier: c.identifier })) - // Use AddressLoader - it auto-batches and streams results - merge(...pointers.map(this.addressLoader)).subscribe({ + // Use mergeMap with concurrency limit instead of merge to properly batch requests + from(pointers).pipe( + mergeMap(pointer => this.addressLoader!(pointer), { concurrency: 5 }) + ).subscribe({ next: (event) => { // Check if hydration was cancelled if (this.hydrationGeneration !== generation) return From 96ef227f79c7a3cb118b23f1048b9f88f0f16de3 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:13:38 +0200 Subject: [PATCH 13/43] debug: log all fetched events to identify ID mismatch - Show sample of note IDs being requested - Log every event fetched with kind and content length - Helps diagnose why kind:1 events aren't in the hydration map --- src/services/bookmarkController.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index 17a799b0..71634f92 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -152,6 +152,14 @@ class BookmarkController { idToEvent.set(event.id, event) + // Log all fetched events to see what we're getting + console.log('📥 Fetched event:', { + id: event.id.slice(0, 12), + kind: event.kind, + contentLen: event.content?.length || 0, + totalInMap: idToEvent.size + }) + if (event.kind === 1 && event.content) { console.log('✅ Fetched kind:1 with content:', { id: event.id.slice(0, 12), @@ -275,6 +283,10 @@ class BookmarkController { console.log(`📋 Requesting hydration for: ${noteIds.length} note IDs, ${coordinates.length} coordinates`) + // Show sample of what we're requesting + const sampleIds = noteIds.slice(0, 3) + console.log(`📋 Sample note IDs to request:`, sampleIds) + // Helper to build and emit bookmarks const emitBookmarks = (idToEvent: Map) => { const allBookmarks = dedupeBookmarksById([ From b8242312b553089305ff3f42230e89a0d396648c Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:15:27 +0200 Subject: [PATCH 14/43] fix: deduplicate bookmarks before requesting hydration - Collect all items, then dedupe before separating IDs/coordinates - Prevents requesting hydration for 410 duplicate items - Only requests ~96 unique event IDs instead - Events are still hydrated for both public and private lists - Dedupe after combining hydrated results --- src/services/bookmarkController.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index 71634f92..e0c66222 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -269,11 +269,14 @@ class BookmarkController { const allItems = [...publicItemsAll, ...privateItemsAll] + // Dedupe BEFORE hydration to avoid requesting duplicate events + const deduped = dedupeBookmarksById(allItems) + // Separate hex IDs from coordinates const noteIds: string[] = [] const coordinates: string[] = [] - allItems.forEach(i => { + deduped.forEach(i => { if (/^[0-9a-f]{64}$/i.test(i.id)) { noteIds.push(i.id) } else if (i.id.includes(':')) { @@ -289,10 +292,12 @@ class BookmarkController { // Helper to build and emit bookmarks const emitBookmarks = (idToEvent: Map) => { - const allBookmarks = dedupeBookmarksById([ + // Now hydrate the ORIGINAL items (which may have duplicates), using the deduplicated results + // This preserves the original public/private split while still getting all the content + const allBookmarks = [ ...hydrateItems(publicItemsAll, idToEvent), ...hydrateItems(privateItemsAll, idToEvent) - ]) + ] // Debug: log what we have const kind1Items = allBookmarks.filter(b => b.kind === 1) From d2c1a16ca63b02d5d2b3fcbc91072b213ebf7c9d Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:17:03 +0200 Subject: [PATCH 15/43] chore: remove verbose debug logging from hydration - Clean up console output after diagnosing ID mismatch issue - Keep error logging for when matches aren't found - Deduplication before hydration now working --- src/services/bookmarkController.ts | 51 ------------------------------ src/services/bookmarkHelpers.ts | 2 -- 2 files changed, 53 deletions(-) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index e0c66222..9606783d 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -136,8 +136,6 @@ class BookmarkController { return } - console.log(`📡 hydrateByIds: requesting ${unique.length} events from EventLoader`) - // Convert IDs to EventPointers const pointers: EventPointer[] = unique.map(id => ({ id })) @@ -152,22 +150,6 @@ class BookmarkController { idToEvent.set(event.id, event) - // Log all fetched events to see what we're getting - console.log('📥 Fetched event:', { - id: event.id.slice(0, 12), - kind: event.kind, - contentLen: event.content?.length || 0, - totalInMap: idToEvent.size - }) - - if (event.kind === 1 && event.content) { - console.log('✅ Fetched kind:1 with content:', { - id: event.id.slice(0, 12), - content: event.content.slice(0, 30), - totalInMap: idToEvent.size - }) - } - // Also index by coordinate for addressable events if (event.kind && event.kind >= 30000 && event.kind < 40000) { const dTag = event.tags?.find((t: string[]) => t[0] === 'd')?.[1] || '' @@ -286,10 +268,6 @@ class BookmarkController { console.log(`📋 Requesting hydration for: ${noteIds.length} note IDs, ${coordinates.length} coordinates`) - // Show sample of what we're requesting - const sampleIds = noteIds.slice(0, 3) - console.log(`📋 Sample note IDs to request:`, sampleIds) - // Helper to build and emit bookmarks const emitBookmarks = (idToEvent: Map) => { // Now hydrate the ORIGINAL items (which may have duplicates), using the deduplicated results @@ -299,21 +277,6 @@ class BookmarkController { ...hydrateItems(privateItemsAll, idToEvent) ] - // Debug: log what we have - const kind1Items = allBookmarks.filter(b => b.kind === 1) - if (kind1Items.length > 0) { - console.log('🔄 Emitting bookmarks with hydration:', { - totalKind1: kind1Items.length, - withContent: kind1Items.filter(b => b.content).length, - mapSize: idToEvent.size, - sample: kind1Items.slice(0, 2).map(b => ({ - id: b.id.slice(0, 12), - content: b.content?.slice(0, 30), - inMap: idToEvent.has(b.id) - })) - }) - } - const enriched = allBookmarks.map(b => ({ ...b, tags: b.tags || [], @@ -338,20 +301,6 @@ class BookmarkController { encryptedContent: undefined } - // Debug: log the actual content being emitted - const kind1InEmit = sortedBookmarks.filter(b => b.kind === 1) - if (kind1InEmit.length > 0) { - console.log('📤 Emitting Bookmark object with individualBookmarks:', { - totalKind1: kind1InEmit.length, - withContent: kind1InEmit.filter(b => b.content && b.content.length > 0).length, - samples: kind1InEmit.slice(0, 2).map(b => ({ - id: b.id.slice(0, 12), - content: b.content?.slice(0, 20), - contentLength: b.content?.length - })) - }) - } - this.bookmarksListeners.forEach(cb => cb([bookmark])) } diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index c72f5fcd..10b1b2de 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -170,11 +170,9 @@ export function hydrateItems( items: IndividualBookmark[], idToEvent: Map ): IndividualBookmark[] { - // Debug: log what we're trying to hydrate const kind1Items = items.filter(b => b.kind === 1) if (kind1Items.length > 0 && idToEvent.size > 0) { const found = kind1Items.filter(b => idToEvent.has(b.id)) - console.log(`💧 hydrateItems: ${found.length}/${kind1Items.length} kind:1 items found in map (map has ${idToEvent.size} total events)`) if (found.length === 0 && kind1Items.length > 0) { console.log('❌ NO MATCHES! Sample item IDs:', kind1Items.slice(0, 3).map(b => b.id)) console.log('❌ Map keys:', Array.from(idToEvent.keys()).slice(0, 3)) From 5bd568680584fd5d714f3372a2efd805cdefcdcb Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:19:20 +0200 Subject: [PATCH 16/43] feat: add /e/:eventId route to display individual notes - New EventViewer component to display kind:1 notes and other events - Shows event ID, creation time, and content with RichContent rendering - Add /e/:eventId route in App.tsx - Update CompactView to navigate to /e/:eventId when clicking kind:1 bookmarks - Mobile-optimized styling with back button and full viewport display - Fallback for missing events with error message --- src/App.tsx | 10 ++ src/components/BookmarkViews/CompactView.tsx | 13 ++- src/components/EventViewer.css | 101 ++++++++++++++++ src/components/EventViewer.tsx | 114 +++++++++++++++++++ 4 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 src/components/EventViewer.css create mode 100644 src/components/EventViewer.tsx diff --git a/src/App.tsx b/src/App.tsx index fe26e955..bb58ef17 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,7 @@ import { readingProgressController } from './services/readingProgressController' import { nostrverseHighlightsController } from './services/nostrverseHighlightsController' import { nostrverseWritingsController } from './services/nostrverseWritingsController' import { archiveController } from './services/archiveController' +import EventViewer from './components/EventViewer' const DEFAULT_ARTICLE = import.meta.env.VITE_DEFAULT_ARTICLE_NADDR || 'naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew' @@ -348,6 +349,15 @@ function AppRoutes({ /> } /> + + } + /> = ({ contentTypeIcon, readingProgress }) => { + const navigate = useNavigate() const isArticle = bookmark.kind === 30023 const isWebBookmark = bookmark.kind === 39701 - const isClickable = hasUrls || isArticle || isWebBookmark + const isNote = bookmark.kind === 1 + const isClickable = hasUrls || isArticle || isWebBookmark || isNote const displayText = isArticle && articleSummary ? articleSummary : bookmark.content @@ -54,12 +57,12 @@ export const CompactView: React.FC = ({ } const handleCompactClick = () => { - if (!onSelectUrl) return - if (isArticle) { - onSelectUrl('', { id: bookmark.id, kind: bookmark.kind, tags: bookmark.tags, pubkey: bookmark.pubkey }) + onSelectUrl?.('', { id: bookmark.id, kind: bookmark.kind, tags: bookmark.tags, pubkey: bookmark.pubkey }) } else if (hasUrls) { - onSelectUrl(extractedUrls[0]) + onSelectUrl?.(extractedUrls[0]) + } else if (isNote) { + navigate(`/e/${bookmark.id}`) } } diff --git a/src/components/EventViewer.css b/src/components/EventViewer.css new file mode 100644 index 00000000..3207b13d --- /dev/null +++ b/src/components/EventViewer.css @@ -0,0 +1,101 @@ +.event-viewer { + display: flex; + flex-direction: column; + height: 100vh; + background: var(--color-bg); + color: var(--color-text); +} + +.event-viewer-header { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + border-bottom: 1px solid var(--color-border); + background: var(--color-bg-secondary, var(--color-bg)); +} + +.event-viewer-header h1 { + margin: 0; + font-size: 1.25rem; + flex: 1; +} + +.back-button { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border: none; + background: var(--color-border); + color: var(--color-text); + border-radius: 0.5rem; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.2s; +} + +.back-button:hover { + background: var(--color-text-muted); +} + +.event-viewer-content { + flex: 1; + overflow-y: auto; + padding: 1.5rem; + max-width: 100%; +} + +.event-meta { + display: flex; + gap: 1rem; + margin-bottom: 1.5rem; + font-size: 0.875rem; + color: var(--color-text-muted); + flex-wrap: wrap; +} + +.event-id { + font-family: monospace; +} + +.event-id code { + background: var(--color-border); + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; +} + +.event-time { + white-space: nowrap; +} + +.event-text { + font-size: 1rem; + line-height: 1.6; + word-wrap: break-word; + white-space: pre-wrap; +} + +/* Mobile optimization */ +@media (max-width: 640px) { + .event-viewer { + height: 100%; + } + + .event-viewer-header { + padding: 0.75rem; + } + + .event-viewer-header h1 { + font-size: 1.1rem; + } + + .event-viewer-content { + padding: 1rem; + } + + .event-meta { + gap: 0.5rem; + } +} diff --git a/src/components/EventViewer.tsx b/src/components/EventViewer.tsx new file mode 100644 index 00000000..2591abf0 --- /dev/null +++ b/src/components/EventViewer.tsx @@ -0,0 +1,114 @@ +import { useParams, useNavigate } from 'react-router-dom' +import { useState, useEffect } from 'react' +import { RelayPool } from 'applesauce-relay' +import { EventStore } from 'applesauce-core' +import { Hooks } from 'applesauce-react' +import { createEventLoader } from 'applesauce-loaders/loaders' +import { NostrEvent } from 'nostr-tools' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faArrowLeft } from '@fortawesome/free-solid-svg-icons' +import RichContent from './RichContent' +import './EventViewer.css' + +interface EventViewerProps { + relayPool: RelayPool + eventStore: EventStore | null +} + +export default function EventViewer({ relayPool, eventStore }: EventViewerProps) { + const { eventId } = useParams<{ eventId: string }>() + const navigate = useNavigate() + const [event, setEvent] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + useEffect(() => { + if (!eventId || !relayPool || !eventStore) return + + setLoading(true) + setError(null) + + // Try to get from event store first + const cachedEvent = eventStore.getEvent(eventId) + if (cachedEvent) { + setEvent(cachedEvent) + setLoading(false) + return + } + + // Otherwise fetch from relays + const eventLoader = createEventLoader(relayPool, { + eventStore, + cacheRequest: true + }) + + const subscription = eventLoader({ id: eventId }).subscribe({ + next: (fetchedEvent) => { + setEvent(fetchedEvent) + setLoading(false) + }, + error: (err) => { + console.error('Error fetching event:', err) + setError('Failed to load event') + setLoading(false) + }, + complete: () => { + setLoading(false) + } + }) + + return () => subscription.unsubscribe() + }, [eventId, relayPool, eventStore]) + + if (loading) { + return ( +
+
+ +

Loading event...

+
+
+ ) + } + + if (error || !event) { + return ( +
+
+ +

{error || 'Event not found'}

+
+
+ ) + } + + return ( +
+
+ +

Note

+
+ +
+
+ + {eventId?.slice(0, 16)}... + + + {new Date(event.created_at * 1000).toLocaleString()} + +
+ +
+ +
+
+
+ ) +} From 145ff138b0972664b46e8773c52f0ec092061a9f Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:22:04 +0200 Subject: [PATCH 17/43] feat: integrate event viewer into three-pane layout for /e/:eventId - Create useEventLoader hook to fetch and display individual events - Events display in middle pane with metadata (ID, timestamp, kind) - Integrates with existing Bookmarks three-pane layout - Remove standalone EventViewer component - Route /e/:eventId now uses Bookmarks component - Metadata displayed above event content for context --- src/App.tsx | 8 ++- src/components/Bookmarks.tsx | 14 ++++ src/components/EventViewer.css | 101 ----------------------------- src/components/EventViewer.tsx | 113 --------------------------------- src/hooks/useEventLoader.ts | 79 +++++++++++++++++++++++ 5 files changed, 98 insertions(+), 217 deletions(-) delete mode 100644 src/components/EventViewer.css create mode 100644 src/hooks/useEventLoader.ts diff --git a/src/App.tsx b/src/App.tsx index bb58ef17..e3ebf655 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,7 +32,6 @@ import { readingProgressController } from './services/readingProgressController' import { nostrverseHighlightsController } from './services/nostrverseHighlightsController' import { nostrverseWritingsController } from './services/nostrverseWritingsController' import { archiveController } from './services/archiveController' -import EventViewer from './components/EventViewer' const DEFAULT_ARTICLE = import.meta.env.VITE_DEFAULT_ARTICLE_NADDR || 'naddr1qvzqqqr4gupzqmjxss3dld622uu8q25gywum9qtg4w4cv4064jmg20xsac2aam5nqqxnzd3cxqmrzv3exgmr2wfesgsmew' @@ -352,9 +351,12 @@ function AppRoutes({ } /> diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index 1c2efa13..789e2ee5 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -13,6 +13,7 @@ import { useHighlightCreation } from '../hooks/useHighlightCreation' import { useBookmarksUI } from '../hooks/useBookmarksUI' import { useRelayStatus } from '../hooks/useRelayStatus' import { useOfflineSync } from '../hooks/useOfflineSync' +import { useEventLoader } from '../hooks/useEventLoader' import { Bookmark } from '../types/bookmarks' import ThreePaneLayout from './ThreePaneLayout' import Explore from './Explore' @@ -55,6 +56,8 @@ const Bookmarks: React.FC = ({ const showMe = location.pathname.startsWith('/me') const showProfile = location.pathname.startsWith('/p/') const showSupport = location.pathname === '/support' + const showEvent = location.pathname.startsWith('/e/') + const eventId = showEvent ? location.pathname.slice(3) : undefined // Extract tab from explore routes const exploreTab = location.pathname === '/explore/writings' ? 'writings' : 'highlights' @@ -255,6 +258,17 @@ const Bookmarks: React.FC = ({ setCurrentArticleEventId }) + // Load event if /e/:eventId route is used + useEventLoader({ + eventId, + relayPool, + eventStore, + setSelectedUrl, + setReaderContent, + setReaderLoading, + setIsCollapsed + }) + // Classify highlights with levels based on user context const classifiedHighlights = useMemo(() => { return classifyHighlights(highlights, activeAccount?.pubkey, followedPubkeys) diff --git a/src/components/EventViewer.css b/src/components/EventViewer.css deleted file mode 100644 index 3207b13d..00000000 --- a/src/components/EventViewer.css +++ /dev/null @@ -1,101 +0,0 @@ -.event-viewer { - display: flex; - flex-direction: column; - height: 100vh; - background: var(--color-bg); - color: var(--color-text); -} - -.event-viewer-header { - display: flex; - align-items: center; - gap: 1rem; - padding: 1rem; - border-bottom: 1px solid var(--color-border); - background: var(--color-bg-secondary, var(--color-bg)); -} - -.event-viewer-header h1 { - margin: 0; - font-size: 1.25rem; - flex: 1; -} - -.back-button { - display: flex; - align-items: center; - justify-content: center; - width: 2.5rem; - height: 2.5rem; - border: none; - background: var(--color-border); - color: var(--color-text); - border-radius: 0.5rem; - cursor: pointer; - font-size: 1rem; - transition: background-color 0.2s; -} - -.back-button:hover { - background: var(--color-text-muted); -} - -.event-viewer-content { - flex: 1; - overflow-y: auto; - padding: 1.5rem; - max-width: 100%; -} - -.event-meta { - display: flex; - gap: 1rem; - margin-bottom: 1.5rem; - font-size: 0.875rem; - color: var(--color-text-muted); - flex-wrap: wrap; -} - -.event-id { - font-family: monospace; -} - -.event-id code { - background: var(--color-border); - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; -} - -.event-time { - white-space: nowrap; -} - -.event-text { - font-size: 1rem; - line-height: 1.6; - word-wrap: break-word; - white-space: pre-wrap; -} - -/* Mobile optimization */ -@media (max-width: 640px) { - .event-viewer { - height: 100%; - } - - .event-viewer-header { - padding: 0.75rem; - } - - .event-viewer-header h1 { - font-size: 1.1rem; - } - - .event-viewer-content { - padding: 1rem; - } - - .event-meta { - gap: 0.5rem; - } -} diff --git a/src/components/EventViewer.tsx b/src/components/EventViewer.tsx index 2591abf0..8b137891 100644 --- a/src/components/EventViewer.tsx +++ b/src/components/EventViewer.tsx @@ -1,114 +1 @@ -import { useParams, useNavigate } from 'react-router-dom' -import { useState, useEffect } from 'react' -import { RelayPool } from 'applesauce-relay' -import { EventStore } from 'applesauce-core' -import { Hooks } from 'applesauce-react' -import { createEventLoader } from 'applesauce-loaders/loaders' -import { NostrEvent } from 'nostr-tools' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faArrowLeft } from '@fortawesome/free-solid-svg-icons' -import RichContent from './RichContent' -import './EventViewer.css' -interface EventViewerProps { - relayPool: RelayPool - eventStore: EventStore | null -} - -export default function EventViewer({ relayPool, eventStore }: EventViewerProps) { - const { eventId } = useParams<{ eventId: string }>() - const navigate = useNavigate() - const [event, setEvent] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - - useEffect(() => { - if (!eventId || !relayPool || !eventStore) return - - setLoading(true) - setError(null) - - // Try to get from event store first - const cachedEvent = eventStore.getEvent(eventId) - if (cachedEvent) { - setEvent(cachedEvent) - setLoading(false) - return - } - - // Otherwise fetch from relays - const eventLoader = createEventLoader(relayPool, { - eventStore, - cacheRequest: true - }) - - const subscription = eventLoader({ id: eventId }).subscribe({ - next: (fetchedEvent) => { - setEvent(fetchedEvent) - setLoading(false) - }, - error: (err) => { - console.error('Error fetching event:', err) - setError('Failed to load event') - setLoading(false) - }, - complete: () => { - setLoading(false) - } - }) - - return () => subscription.unsubscribe() - }, [eventId, relayPool, eventStore]) - - if (loading) { - return ( -
-
- -

Loading event...

-
-
- ) - } - - if (error || !event) { - return ( -
-
- -

{error || 'Event not found'}

-
-
- ) - } - - return ( -
-
- -

Note

-
- -
-
- - {eventId?.slice(0, 16)}... - - - {new Date(event.created_at * 1000).toLocaleString()} - -
- -
- -
-
-
- ) -} diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts new file mode 100644 index 00000000..1d8e31ec --- /dev/null +++ b/src/hooks/useEventLoader.ts @@ -0,0 +1,79 @@ +import { useEffect } from 'react' +import { RelayPool } from 'applesauce-relay' +import { EventStore } from 'applesauce-core' +import { createEventLoader } from 'applesauce-loaders/loaders' +import { NostrEvent } from 'nostr-tools' + +interface UseEventLoaderProps { + eventId?: string + relayPool?: RelayPool | null + eventStore?: EventStore | null + setSelectedUrl: (url: string) => void + setReaderContent: (content: string) => void + setReaderLoading: (loading: boolean) => void + setIsCollapsed: (collapsed: boolean) => void +} + +export function useEventLoader({ + eventId, + relayPool, + eventStore, + setSelectedUrl, + setReaderContent, + setReaderLoading, + setIsCollapsed +}: UseEventLoaderProps) { + useEffect(() => { + if (!eventId) return + + setReaderLoading(true) + setSelectedUrl('') + setIsCollapsed(false) + + // Try to get from event store first + if (eventStore) { + const cachedEvent = eventStore.getEvent(eventId) + if (cachedEvent) { + displayEvent(cachedEvent) + setReaderLoading(false) + return + } + } + + // Otherwise fetch from relays + if (!relayPool) { + setReaderLoading(false) + return + } + + const eventLoader = createEventLoader(relayPool, { + eventStore, + cacheRequest: true + }) + + const subscription = eventLoader({ id: eventId }).subscribe({ + next: (event) => { + displayEvent(event) + setReaderLoading(false) + }, + error: (err) => { + console.error('Error fetching event:', err) + setReaderContent(`Error loading event: ${err instanceof Error ? err.message : 'Unknown error'}`) + setReaderLoading(false) + } + }) + + return () => subscription.unsubscribe() + }, [eventId, relayPool, eventStore]) + + function displayEvent(event: NostrEvent) { + // Format event for display with metadata + const meta = `
+
Event ID: ${event.id.slice(0, 16)}...
+
Posted: ${new Date(event.created_at * 1000).toLocaleString()}
+
Kind: ${event.kind}
+
` + + setReaderContent(meta + event.content) + } +} From 5551cc3a55438422636c0a687b2e4764debe29bf Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:23:01 +0200 Subject: [PATCH 18/43] feat: add relay.nostr.band as hardcoded relay - Create HARDCODED_RELAYS constant with relay.nostr.band - Always include hardcoded relays in relay pool - Update computeRelaySet calls to use HARDCODED_RELAYS - Ensures we can fetch events even if user has no relay list - relay.nostr.band is a public searchable relay that indexes all events --- src/App.tsx | 6 +++--- src/services/relayManager.ts | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e3ebf655..6b73a555 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,7 +21,7 @@ import { useOnlineStatus } from './hooks/useOnlineStatus' import { RELAYS } from './config/relays' import { SkeletonThemeProvider } from './components/Skeletons' import { loadUserRelayList, loadBlockedRelays, computeRelaySet } from './services/relayListService' -import { applyRelaySetToPool, getActiveRelayUrls, ALWAYS_LOCAL_RELAYS } from './services/relayManager' +import { applyRelaySetToPool, getActiveRelayUrls, ALWAYS_LOCAL_RELAYS, HARDCODED_RELAYS } from './services/relayManager' import { Bookmark } from './types/bookmarks' import { bookmarkController } from './services/bookmarkController' import { contactsController } from './services/contactsController' @@ -627,7 +627,7 @@ function App() { loadUserRelayList(pool, pubkey, { onUpdate: (userRelays) => { const interimRelays = computeRelaySet({ - hardcoded: [], + hardcoded: HARDCODED_RELAYS, bunker: bunkerRelays, userList: userRelays, blocked: [], @@ -641,7 +641,7 @@ function App() { const blockedRelays = await blockedPromise.catch(() => []) const finalRelays = computeRelaySet({ - hardcoded: userRelayList.length > 0 ? [] : RELAYS, + hardcoded: userRelayList.length > 0 ? HARDCODED_RELAYS : RELAYS, bunker: bunkerRelays, userList: userRelayList, blocked: blockedRelays, diff --git a/src/services/relayManager.ts b/src/services/relayManager.ts index 48d89d23..877cbdbe 100644 --- a/src/services/relayManager.ts +++ b/src/services/relayManager.ts @@ -9,6 +9,13 @@ export const ALWAYS_LOCAL_RELAYS = [ 'ws://localhost:4869' ] +/** + * Hardcoded relays that are always included + */ +export const HARDCODED_RELAYS = [ + 'wss://relay.nostr.band' +] + /** * Gets active relay URLs from the relay pool */ From a5bdde68fc4607c9a1470a24a3fa63fb50fa4493 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:27:45 +0200 Subject: [PATCH 19/43] fix: resolve all linter and type check errors - Fix mergeMap concurrency syntax (pass as second parameter, not object) - Fix type casting in CompactView debug logging - Update useEventLoader to use ReadableContent type - Fix eventStore type compatibility in useEventLoader - All linter and TypeScript checks now pass --- src/components/BookmarkViews/CompactView.tsx | 7 +-- src/hooks/useEventLoader.ts | 48 ++++++++++++-------- src/services/bookmarkController.ts | 6 +-- 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index e2d1fb84..218e6baa 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -37,15 +37,16 @@ export const CompactView: React.FC = ({ // Debug empty bookmarks if (!displayText && bookmark.kind === 1) { - console.log('📌 Empty kind:1 bookmark:', { + const debugInfo: Record = { id: bookmark.id.slice(0, 12), content: bookmark.content, contentLength: bookmark.content?.length, contentType: typeof bookmark.content, parsedContent: !!bookmark.parsedContent, created_at: bookmark.created_at, - sourceKind: (bookmark as any).sourceKind - }) + sourceKind: (bookmark as unknown as Record).sourceKind + } + console.log('📌 Empty kind:1 bookmark:', debugInfo) } // Calculate progress color diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 1d8e31ec..24effa2f 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -1,15 +1,16 @@ -import { useEffect } from 'react' +import { useEffect, useCallback } from 'react' import { RelayPool } from 'applesauce-relay' -import { EventStore } from 'applesauce-core' +import { IEventStore } from 'applesauce-core' import { createEventLoader } from 'applesauce-loaders/loaders' import { NostrEvent } from 'nostr-tools' +import { ReadableContent } from '../services/readerService' interface UseEventLoaderProps { eventId?: string relayPool?: RelayPool | null - eventStore?: EventStore | null + eventStore?: IEventStore | null setSelectedUrl: (url: string) => void - setReaderContent: (content: string) => void + setReaderContent: (content: ReadableContent | undefined) => void setReaderLoading: (loading: boolean) => void setIsCollapsed: (collapsed: boolean) => void } @@ -23,6 +24,22 @@ export function useEventLoader({ setReaderLoading, setIsCollapsed }: UseEventLoaderProps) { + const displayEvent = useCallback((event: NostrEvent) => { + // Format event HTML for display with metadata + const metaHtml = `
+
Event ID: ${event.id.slice(0, 16)}...
+
Posted: ${new Date(event.created_at * 1000).toLocaleString()}
+
Kind: ${event.kind}
+
` + + const content: ReadableContent = { + url: '', + html: metaHtml + event.content, + title: `Note (${event.kind})` + } + setReaderContent(content) + }, [setReaderContent]) + useEffect(() => { if (!eventId) return @@ -47,8 +64,7 @@ export function useEventLoader({ } const eventLoader = createEventLoader(relayPool, { - eventStore, - cacheRequest: true + eventStore: eventStore ?? undefined }) const subscription = eventLoader({ id: eventId }).subscribe({ @@ -58,22 +74,16 @@ export function useEventLoader({ }, error: (err) => { console.error('Error fetching event:', err) - setReaderContent(`Error loading event: ${err instanceof Error ? err.message : 'Unknown error'}`) + const errorContent: ReadableContent = { + url: '', + html: `Error loading event: ${err instanceof Error ? err.message : 'Unknown error'}`, + title: 'Error' + } + setReaderContent(errorContent) setReaderLoading(false) } }) return () => subscription.unsubscribe() - }, [eventId, relayPool, eventStore]) - - function displayEvent(event: NostrEvent) { - // Format event for display with metadata - const meta = `
-
Event ID: ${event.id.slice(0, 16)}...
-
Posted: ${new Date(event.created_at * 1000).toLocaleString()}
-
Kind: ${event.kind}
-
` - - setReaderContent(meta + event.content) - } + }, [eventId, relayPool, eventStore, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent]) } diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index 9606783d..1aff549b 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -3,7 +3,7 @@ import { Helpers, EventStore } from 'applesauce-core' import { createEventLoader, createAddressLoader } from 'applesauce-loaders/loaders' import { NostrEvent } from 'nostr-tools' import { EventPointer } from 'nostr-tools/nip19' -import { merge, from } from 'rxjs' +import { from } from 'rxjs' import { mergeMap } from 'rxjs/operators' import { queryEvents } from './dataFetch' import { KINDS } from '../config/kinds' @@ -142,7 +142,7 @@ class BookmarkController { // Use mergeMap with concurrency limit instead of merge to properly batch requests // This prevents overwhelming relays with 96+ simultaneous requests from(pointers).pipe( - mergeMap(pointer => this.eventLoader!(pointer), { concurrency: 5 }) + mergeMap(pointer => this.eventLoader!(pointer), 5) ).subscribe({ next: (event) => { // Check if hydration was cancelled @@ -189,7 +189,7 @@ class BookmarkController { // Use mergeMap with concurrency limit instead of merge to properly batch requests from(pointers).pipe( - mergeMap(pointer => this.addressLoader!(pointer), { concurrency: 5 }) + mergeMap(pointer => this.addressLoader!(pointer), 5) ).subscribe({ next: (event) => { // Check if hydration was cancelled From e83d4dbcdb95910c50bcecb92156c33129937f74 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:28:29 +0200 Subject: [PATCH 20/43] feat: render notes like articles with markdown processing - Change useEventLoader to set markdown instead of html - Notes now get proper markdown processing and rendering similar to articles - Use markdown comments for event metadata instead of HTML - This enables proper styling and markdown features for note display --- src/hooks/useEventLoader.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 24effa2f..0f6bbdee 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -25,16 +25,12 @@ export function useEventLoader({ setIsCollapsed }: UseEventLoaderProps) { const displayEvent = useCallback((event: NostrEvent) => { - // Format event HTML for display with metadata - const metaHtml = `
-
Event ID: ${event.id.slice(0, 16)}...
-
Posted: ${new Date(event.created_at * 1000).toLocaleString()}
-
Kind: ${event.kind}
-
` + // Format event metadata as markdown comments for display + const metaMarkdown = `` const content: ReadableContent = { - url: '', - html: metaHtml + event.content, + url: `nostr:${event.id}`, + markdown: metaMarkdown + '\n\n' + event.content, title: `Note (${event.kind})` } setReaderContent(content) @@ -76,7 +72,7 @@ export function useEventLoader({ console.error('Error fetching event:', err) const errorContent: ReadableContent = { url: '', - html: `Error loading event: ${err instanceof Error ? err.message : 'Unknown error'}`, + markdown: `Error loading event: ${err instanceof Error ? err.message : 'Unknown error'}`, title: 'Error' } setReaderContent(errorContent) From cce7507e5033e38878266470110e233aac8cbb12 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:30:54 +0200 Subject: [PATCH 21/43] fix: properly extract eventId from route params - Add eventId to useParams instead of manually parsing pathname - useParams automatically extracts eventId from /e/:eventId route - Add debug logging to track event loading - This fixes the issue where eventId wasn't being passed to useEventLoader --- src/components/Bookmarks.tsx | 10 +++++++--- src/hooks/useEventLoader.ts | 7 ++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index 789e2ee5..5738658f 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -39,7 +39,7 @@ const Bookmarks: React.FC = ({ bookmarksLoading, onRefreshBookmarks }) => { - const { naddr, npub } = useParams<{ naddr?: string; npub?: string }>() + const { naddr, npub, eventId: eventIdParam } = useParams<{ naddr?: string; npub?: string; eventId?: string }>() const location = useLocation() const navigate = useNavigate() const previousLocationRef = useRef() @@ -56,8 +56,12 @@ const Bookmarks: React.FC = ({ const showMe = location.pathname.startsWith('/me') const showProfile = location.pathname.startsWith('/p/') const showSupport = location.pathname === '/support' - const showEvent = location.pathname.startsWith('/e/') - const eventId = showEvent ? location.pathname.slice(3) : undefined + const eventId = eventIdParam + + // Debug event loading + if (eventId) { + console.log('📍 Bookmarks: Event route detected. eventId:', eventId) + } // Extract tab from explore routes const exploreTab = location.pathname === '/explore/writings' ? 'writings' : 'highlights' diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 0f6bbdee..a4b3c322 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -39,6 +39,7 @@ export function useEventLoader({ useEffect(() => { if (!eventId) return + console.log('🔍 useEventLoader: Loading event:', eventId) setReaderLoading(true) setSelectedUrl('') setIsCollapsed(false) @@ -47,6 +48,7 @@ export function useEventLoader({ if (eventStore) { const cachedEvent = eventStore.getEvent(eventId) if (cachedEvent) { + console.log('✅ useEventLoader: Found cached event:', cachedEvent) displayEvent(cachedEvent) setReaderLoading(false) return @@ -55,21 +57,24 @@ export function useEventLoader({ // Otherwise fetch from relays if (!relayPool) { + console.log('❌ useEventLoader: No relay pool available') setReaderLoading(false) return } + console.log('📡 useEventLoader: Fetching from relays...') const eventLoader = createEventLoader(relayPool, { eventStore: eventStore ?? undefined }) const subscription = eventLoader({ id: eventId }).subscribe({ next: (event) => { + console.log('✅ useEventLoader: Fetched event from relays:', event) displayEvent(event) setReaderLoading(false) }, error: (err) => { - console.error('Error fetching event:', err) + console.error('❌ useEventLoader: Error fetching event:', err) const errorContent: ReadableContent = { url: '', markdown: `Error loading event: ${err instanceof Error ? err.message : 'Unknown error'}`, From 167d5f2041c4e58f125437f5d79689851efeb0ac Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:35:33 +0200 Subject: [PATCH 22/43] fix: clear reader content when loading event and set proper selectedUrl - Clear readerContent at start of loading to ensure old content doesn't persist - Set selectedUrl to nostr:eventId to match pattern used in other loaders - This ensures consistent behavior across all content loaders --- src/hooks/useEventLoader.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index a4b3c322..b97d6018 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -41,7 +41,8 @@ export function useEventLoader({ console.log('🔍 useEventLoader: Loading event:', eventId) setReaderLoading(true) - setSelectedUrl('') + setReaderContent(undefined) + setSelectedUrl(`nostr:${eventId}`) setIsCollapsed(false) // Try to get from event store first From 18c78c19bed089c2f014dd93d010fd2ec981ddf0 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:36:55 +0200 Subject: [PATCH 23/43] fix: render events as plain text html instead of markdown - kind:1 notes are plain text, not markdown - Changed from markdown to html rendering - HTML-escape content to prevent injection - Preserve whitespace and newlines for plain text display - Display event metadata in styled HTML header --- src/hooks/useEventLoader.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index b97d6018..4223c60e 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -25,12 +25,23 @@ export function useEventLoader({ setIsCollapsed }: UseEventLoaderProps) { const displayEvent = useCallback((event: NostrEvent) => { - // Format event metadata as markdown comments for display - const metaMarkdown = `` + // Format event metadata as HTML header + const metaHtml = `
+
Event ID: ${event.id.slice(0, 16)}...
+
Posted: ${new Date(event.created_at * 1000).toLocaleString()}
+
Kind: ${event.kind}
+
` + + // Escape HTML in content and convert newlines to breaks for plain text display + const escapedContent = event.content + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
') const content: ReadableContent = { url: `nostr:${event.id}`, - markdown: metaMarkdown + '\n\n' + event.content, + html: metaHtml + `
${escapedContent}
`, title: `Note (${event.kind})` } setReaderContent(content) @@ -78,7 +89,7 @@ export function useEventLoader({ console.error('❌ useEventLoader: Error fetching event:', err) const errorContent: ReadableContent = { url: '', - markdown: `Error loading event: ${err instanceof Error ? err.message : 'Unknown error'}`, + html: `
Error loading event: ${err instanceof Error ? err.message : 'Unknown error'}
`, title: 'Error' } setReaderContent(errorContent) From 225458696004b647b85b7852622cf10dcbcf8ead Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:38:42 +0200 Subject: [PATCH 24/43] perf: check eventStore before setting loading state for instant cached event display - Synchronously check eventStore first before setting loading state - If event is cached, display it immediately without loading spinner - Only set loading state if event not found in cache - Provides instant display of events that are already hydrated - Improves perceived performance when navigating to bookmarked events --- src/hooks/useEventLoader.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 4223c60e..f25c4d88 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -51,22 +51,26 @@ export function useEventLoader({ if (!eventId) return console.log('🔍 useEventLoader: Loading event:', eventId) + + // Try to get from event store first - do this synchronously before setting loading state + if (eventStore) { + const cachedEvent = eventStore.getEvent(eventId) + if (cachedEvent) { + console.log('✅ useEventLoader: Found cached event (instant load):', cachedEvent) + displayEvent(cachedEvent) + setReaderLoading(false) + setIsCollapsed(false) + setSelectedUrl(`nostr:${eventId}`) + return + } + } + + // Event not in cache, now set loading state and fetch from relays setReaderLoading(true) setReaderContent(undefined) setSelectedUrl(`nostr:${eventId}`) setIsCollapsed(false) - // Try to get from event store first - if (eventStore) { - const cachedEvent = eventStore.getEvent(eventId) - if (cachedEvent) { - console.log('✅ useEventLoader: Found cached event:', cachedEvent) - displayEvent(cachedEvent) - setReaderLoading(false) - return - } - } - // Otherwise fetch from relays if (!relayPool) { console.log('❌ useEventLoader: No relay pool available') From 3200bdf378a8ff87a0add04b895c1a5076c7d102 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:42:25 +0200 Subject: [PATCH 25/43] fix: add hydrated bookmark events to global eventStore - bookmarkController now accepts eventStore in start() options - All hydrated events (both by ID and by coordinates) are added to the external eventStore - This makes hydrated bookmark events available to useEventLoader and other hooks - Fixes issue where /e/ path couldn't find events because they weren't in the global eventStore - Now instant loading works for all bookmarked events --- src/App.tsx | 2 +- src/hooks/useEventLoader.ts | 2 ++ src/services/bookmarkController.ts | 17 ++++++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6b73a555..992708f9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -95,7 +95,7 @@ function AppRoutes({ // Load bookmarks if (bookmarks.length === 0 && !bookmarksLoading) { - bookmarkController.start({ relayPool, activeAccount, accountManager }) + bookmarkController.start({ relayPool, activeAccount, accountManager, eventStore: eventStore || undefined }) } // Load contacts diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index f25c4d88..caae1fd9 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -25,6 +25,7 @@ export function useEventLoader({ setIsCollapsed }: UseEventLoaderProps) { const displayEvent = useCallback((event: NostrEvent) => { + console.log('🎨 displayEvent: Creating ReadableContent from event') // Format event metadata as HTML header const metaHtml = `
Event ID: ${event.id.slice(0, 16)}...
@@ -44,6 +45,7 @@ export function useEventLoader({ html: metaHtml + `
${escapedContent}
`, title: `Note (${event.kind})` } + console.log('🎨 displayEvent: Setting readerContent with html:', { title: content.title, htmlLength: content.html?.length }) setReaderContent(content) }, [setReaderContent]) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index 1aff549b..062c7b3d 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -70,6 +70,7 @@ class BookmarkController { private eventStore = new EventStore() private eventLoader: ReturnType | null = null private addressLoader: ReturnType | null = null + private externalEventStore: EventStore | null = null onRawEvent(cb: RawEventCallback): () => void { this.rawEventListeners.push(cb) @@ -157,6 +158,11 @@ class BookmarkController { idToEvent.set(coordinate, event) } + // Add to external event store if available + if (this.externalEventStore) { + this.externalEventStore.add(event) + } + onProgress() }, error: () => { @@ -200,6 +206,11 @@ class BookmarkController { idToEvent.set(coordinate, event) idToEvent.set(event.id, event) + // Add to external event store if available + if (this.externalEventStore) { + this.externalEventStore.add(event) + } + onProgress() }, error: () => { @@ -337,8 +348,12 @@ class BookmarkController { relayPool: RelayPool activeAccount: unknown accountManager: { getActive: () => unknown } + eventStore?: EventStore }): Promise { - const { relayPool, activeAccount, accountManager } = options + const { relayPool, activeAccount, accountManager, eventStore } = options + + // Store the external event store reference for adding hydrated events + this.externalEventStore = eventStore || null if (!activeAccount || typeof (activeAccount as { pubkey?: string }).pubkey !== 'string') { return From dc9a49e8955605ebd6cf78297fee1a5a1d908d5d Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:46:44 +0200 Subject: [PATCH 26/43] chore: remove debug logging from event loader and compact view - Remove debug logs from useEventLoader hook - Remove debug logs from Bookmarks component - Remove empty kind:1 bookmark debug logging from CompactView - Clean console output now that features are working correctly --- src/components/BookmarkViews/CompactView.tsx | 14 -------------- src/components/Bookmarks.tsx | 5 ----- src/hooks/useEventLoader.ts | 9 --------- 3 files changed, 28 deletions(-) diff --git a/src/components/BookmarkViews/CompactView.tsx b/src/components/BookmarkViews/CompactView.tsx index 218e6baa..30edc84a 100644 --- a/src/components/BookmarkViews/CompactView.tsx +++ b/src/components/BookmarkViews/CompactView.tsx @@ -35,20 +35,6 @@ export const CompactView: React.FC = ({ const displayText = isArticle && articleSummary ? articleSummary : bookmark.content - // Debug empty bookmarks - if (!displayText && bookmark.kind === 1) { - const debugInfo: Record = { - id: bookmark.id.slice(0, 12), - content: bookmark.content, - contentLength: bookmark.content?.length, - contentType: typeof bookmark.content, - parsedContent: !!bookmark.parsedContent, - created_at: bookmark.created_at, - sourceKind: (bookmark as unknown as Record).sourceKind - } - console.log('📌 Empty kind:1 bookmark:', debugInfo) - } - // Calculate progress color let progressColor = '#6366f1' // Default blue (reading) if (readingProgress && readingProgress >= 0.95) { diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index 5738658f..2c2a9281 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -58,11 +58,6 @@ const Bookmarks: React.FC = ({ const showSupport = location.pathname === '/support' const eventId = eventIdParam - // Debug event loading - if (eventId) { - console.log('📍 Bookmarks: Event route detected. eventId:', eventId) - } - // Extract tab from explore routes const exploreTab = location.pathname === '/explore/writings' ? 'writings' : 'highlights' diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index caae1fd9..6ee697b7 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -25,7 +25,6 @@ export function useEventLoader({ setIsCollapsed }: UseEventLoaderProps) { const displayEvent = useCallback((event: NostrEvent) => { - console.log('🎨 displayEvent: Creating ReadableContent from event') // Format event metadata as HTML header const metaHtml = `
Event ID: ${event.id.slice(0, 16)}...
@@ -45,20 +44,16 @@ export function useEventLoader({ html: metaHtml + `
${escapedContent}
`, title: `Note (${event.kind})` } - console.log('🎨 displayEvent: Setting readerContent with html:', { title: content.title, htmlLength: content.html?.length }) setReaderContent(content) }, [setReaderContent]) useEffect(() => { if (!eventId) return - console.log('🔍 useEventLoader: Loading event:', eventId) - // Try to get from event store first - do this synchronously before setting loading state if (eventStore) { const cachedEvent = eventStore.getEvent(eventId) if (cachedEvent) { - console.log('✅ useEventLoader: Found cached event (instant load):', cachedEvent) displayEvent(cachedEvent) setReaderLoading(false) setIsCollapsed(false) @@ -75,24 +70,20 @@ export function useEventLoader({ // Otherwise fetch from relays if (!relayPool) { - console.log('❌ useEventLoader: No relay pool available') setReaderLoading(false) return } - console.log('📡 useEventLoader: Fetching from relays...') const eventLoader = createEventLoader(relayPool, { eventStore: eventStore ?? undefined }) const subscription = eventLoader({ id: eventId }).subscribe({ next: (event) => { - console.log('✅ useEventLoader: Fetched event from relays:', event) displayEvent(event) setReaderLoading(false) }, error: (err) => { - console.error('❌ useEventLoader: Error fetching event:', err) const errorContent: ReadableContent = { url: '', html: `
Error loading event: ${err instanceof Error ? err.message : 'Unknown error'}
`, From 43e0dd76c4ebad3ad7dd3b9e98b5034780d2e9f0 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:48:43 +0200 Subject: [PATCH 27/43] fix: don't show user highlights when viewing events on /e/ path - Set selectedUrl and ReadableContent url to empty string for events - This prevents ThreePaneLayout from displaying user highlights for event views - Events should only show event-specific content, not global user highlights - Fixes issue where 422 highlights were always shown for all notes --- src/hooks/useEventLoader.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 6ee697b7..76dc3097 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -40,7 +40,7 @@ export function useEventLoader({ .replace(/\n/g, '
') const content: ReadableContent = { - url: `nostr:${event.id}`, + url: '', // Empty URL to prevent highlight display html: metaHtml + `
${escapedContent}
`, title: `Note (${event.kind})` } @@ -57,7 +57,7 @@ export function useEventLoader({ displayEvent(cachedEvent) setReaderLoading(false) setIsCollapsed(false) - setSelectedUrl(`nostr:${eventId}`) + setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights return } } @@ -65,7 +65,7 @@ export function useEventLoader({ // Event not in cache, now set loading state and fetch from relays setReaderLoading(true) setReaderContent(undefined) - setSelectedUrl(`nostr:${eventId}`) + setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights setIsCollapsed(false) // Otherwise fetch from relays From b9d5e501f4ac055b8bdad6a8ed459ce885ff92f9 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:49:50 +0200 Subject: [PATCH 28/43] improve: better error messages when direct event loading fails - Show error if relayPool is not available when loading direct URL - Improved error message wording to be clearer - These messages will help diagnose direct /e/ path loading issues --- src/hooks/useEventLoader.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 76dc3097..34a1f474 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -70,6 +70,12 @@ export function useEventLoader({ // Otherwise fetch from relays if (!relayPool) { + const errorContent: ReadableContent = { + url: '', + html: `
No relay pool available to fetch event
`, + title: 'Error' + } + setReaderContent(errorContent) setReaderLoading(false) return } @@ -86,7 +92,7 @@ export function useEventLoader({ error: (err) => { const errorContent: ReadableContent = { url: '', - html: `
Error loading event: ${err instanceof Error ? err.message : 'Unknown error'}
`, + html: `
Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}
`, title: 'Error' } setReaderContent(errorContent) From 479d9314bdf0292a6ecb2d9a881eac3d768dcea9 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:50:14 +0200 Subject: [PATCH 29/43] fix: make event loading non-blocking and wait for relay pool - Don't show error if relayPool isn't available yet - Instead, keep loading state and wait for relayPool to become available - Effect will re-run automatically when relayPool is set - Enables smooth loading when navigating directly to /e/ URLs on page load - Fetching happens in background without blocking user --- src/hooks/useEventLoader.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 34a1f474..575da1db 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -68,15 +68,10 @@ export function useEventLoader({ setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights setIsCollapsed(false) - // Otherwise fetch from relays + // If no relay pool yet, wait for it or show a placeholder if (!relayPool) { - const errorContent: ReadableContent = { - url: '', - html: `
No relay pool available to fetch event
`, - title: 'Error' - } - setReaderContent(errorContent) - setReaderLoading(false) + // Show loading state until relayPool becomes available + // The effect will re-run once relayPool is set return } From c04ba0c7878461698eaef68256e357b519ea8c68 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:52:15 +0200 Subject: [PATCH 30/43] feat: add centralized eventManager for event fetching - Create eventManager singleton for fetching and caching events - Handles deduplication of concurrent requests for same event - Waits for relay pool to become available before fetching - Provides both async/await and callback-based APIs - Update useEventLoader to use eventManager instead of direct loader - Simplifies event fetching logic and enables better reuse across app --- src/hooks/useEventLoader.ts | 44 ++++-------- src/services/eventManager.ts | 136 +++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 32 deletions(-) create mode 100644 src/services/eventManager.ts diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 575da1db..54a6354e 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -1,9 +1,9 @@ import { useEffect, useCallback } from 'react' import { RelayPool } from 'applesauce-relay' import { IEventStore } from 'applesauce-core' -import { createEventLoader } from 'applesauce-loaders/loaders' import { NostrEvent } from 'nostr-tools' import { ReadableContent } from '../services/readerService' +import { eventManager } from '../services/eventManager' interface UseEventLoaderProps { eventId?: string @@ -47,44 +47,26 @@ export function useEventLoader({ setReaderContent(content) }, [setReaderContent]) + // Initialize event manager with services + useEffect(() => { + eventManager.setServices(eventStore || null, relayPool || null) + }, [eventStore, relayPool]) + useEffect(() => { if (!eventId) return - // Try to get from event store first - do this synchronously before setting loading state - if (eventStore) { - const cachedEvent = eventStore.getEvent(eventId) - if (cachedEvent) { - displayEvent(cachedEvent) - setReaderLoading(false) - setIsCollapsed(false) - setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights - return - } - } - - // Event not in cache, now set loading state and fetch from relays setReaderLoading(true) setReaderContent(undefined) setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights setIsCollapsed(false) - // If no relay pool yet, wait for it or show a placeholder - if (!relayPool) { - // Show loading state until relayPool becomes available - // The effect will re-run once relayPool is set - return - } - - const eventLoader = createEventLoader(relayPool, { - eventStore: eventStore ?? undefined - }) - - const subscription = eventLoader({ id: eventId }).subscribe({ - next: (event) => { + // Fetch event using the event manager + eventManager.fetchEvent(eventId).then( + (event) => { displayEvent(event) setReaderLoading(false) }, - error: (err) => { + (err) => { const errorContent: ReadableContent = { url: '', html: `
Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}
`, @@ -93,8 +75,6 @@ export function useEventLoader({ setReaderContent(errorContent) setReaderLoading(false) } - }) - - return () => subscription.unsubscribe() - }, [eventId, relayPool, eventStore, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent]) + ) + }, [eventId, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent]) } diff --git a/src/services/eventManager.ts b/src/services/eventManager.ts new file mode 100644 index 00000000..e798ea4a --- /dev/null +++ b/src/services/eventManager.ts @@ -0,0 +1,136 @@ +import { RelayPool } from 'applesauce-relay' +import { IEventStore } from 'applesauce-core' +import { createEventLoader } from 'applesauce-loaders/loaders' +import { NostrEvent } from 'nostr-tools' +import { BehaviorSubject, Observable } from 'rxjs' + +type EventCallback = (event: NostrEvent) => void +type ErrorCallback = (error: Error) => void + +/** + * Centralized event manager for fetching and caching events + * Handles deduplication of requests and provides a single source of truth + */ +class EventManager { + private eventStore: IEventStore | null = null + private relayPool: RelayPool | null = null + private eventLoader: ReturnType | null = null + + // Track pending requests to avoid duplicates + private pendingRequests = new Map>() + + // Event stream for real-time updates + private eventSubject = new BehaviorSubject(null) + + /** + * Initialize the event manager with event store and relay pool + */ + setServices(eventStore: IEventStore | null, relayPool: RelayPool | null): void { + this.eventStore = eventStore + this.relayPool = relayPool + + if (relayPool && this.eventLoader === null) { + this.eventLoader = createEventLoader(relayPool, { + eventStore: eventStore || undefined + }) + } + } + + /** + * Fetch an event by ID, with automatic deduplication and caching + */ + async fetchEvent(eventId: string): Promise { + // Check cache first + if (this.eventStore) { + const cached = this.eventStore.getEvent(eventId) + if (cached) { + return cached + } + } + + // Return a promise that will be resolved when the event is fetched + return new Promise((resolve, reject) => { + this.fetchEventAsync(eventId, resolve, reject) + }) + } + + /** + * Subscribe to event fetching with callbacks + */ + private fetchEventAsync( + eventId: string, + onSuccess: EventCallback, + onError: ErrorCallback + ): void { + // Check if we're already fetching this event + if (this.pendingRequests.has(eventId)) { + // Add to existing request queue + this.pendingRequests.get(eventId)!.push({ onSuccess, onError }) + return + } + + // Start a new fetch request + this.pendingRequests.set(eventId, [{ onSuccess, onError }]) + + // If no relay pool yet, wait for it + if (!this.relayPool || !this.eventLoader) { + // Will retry when services are set + setTimeout(() => { + // Retry if still no pool + if (!this.relayPool) { + this.retryPendingRequest(eventId) + } + }, 1000) + return + } + + const subscription = this.eventLoader({ id: eventId }).subscribe({ + next: (event: NostrEvent) => { + // Call all pending callbacks + const callbacks = this.pendingRequests.get(eventId) || [] + this.pendingRequests.delete(eventId) + + callbacks.forEach(cb => cb.onSuccess(event)) + + // Emit to stream + this.eventSubject.next(event) + + subscription.unsubscribe() + }, + error: (err: unknown) => { + // Call all pending callbacks with error + const callbacks = this.pendingRequests.get(eventId) || [] + this.pendingRequests.delete(eventId) + + const error = err instanceof Error ? err : new Error(String(err)) + callbacks.forEach(cb => cb.onError(error)) + + subscription.unsubscribe() + } + }) + } + + /** + * Retry pending requests after delay (useful when relay pool becomes available) + */ + private retryPendingRequest(eventId: string): void { + const callbacks = this.pendingRequests.get(eventId) + if (!callbacks) return + + // Re-trigger the fetch + this.pendingRequests.delete(eventId) + if (callbacks.length > 0) { + this.fetchEventAsync(eventId, callbacks[0].onSuccess, callbacks[0].onError) + } + } + + /** + * Get the event stream for reactive updates + */ + getEventStream(): Observable { + return this.eventSubject.asObservable() + } +} + +// Singleton instance +export const eventManager = new EventManager() From 160dca628dbcde5565056277bb33cec79a408495 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:54:33 +0200 Subject: [PATCH 31/43] fix: simplify eventManager and restore working event fetching - Revert eventManager to simpler role: initialization and service coordination - Restore original working fetching logic in useEventLoader - eventManager now provides: getCachedEvent, getEventLoader, setServices - Fixes broken bookmark hydration and direct event loading - Uses eventManager for cache checking but direct subscription for fetching --- src/hooks/useEventLoader.ts | 36 +++++++++-- src/services/eventManager.ts | 118 ++++++++--------------------------- 2 files changed, 56 insertions(+), 98 deletions(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 54a6354e..63823743 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -55,18 +55,40 @@ export function useEventLoader({ useEffect(() => { if (!eventId) return + // Try to get from event store first (check cache synchronously) + const cachedEvent = eventManager.getCachedEvent(eventId) + if (cachedEvent) { + displayEvent(cachedEvent) + setReaderLoading(false) + setIsCollapsed(false) + setSelectedUrl('') + return + } + + // Event not in cache, set loading state and fetch from relays setReaderLoading(true) setReaderContent(undefined) setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights setIsCollapsed(false) - // Fetch event using the event manager - eventManager.fetchEvent(eventId).then( - (event) => { + // If no relay pool yet, wait for it (will re-run when relayPool changes) + if (!relayPool) { + return + } + + // Fetch from relays using event manager's loader + const eventLoader = eventManager.getEventLoader() + if (!eventLoader) { + setReaderLoading(false) + return + } + + const subscription = eventLoader({ id: eventId }).subscribe({ + next: (event) => { displayEvent(event) setReaderLoading(false) }, - (err) => { + error: (err) => { const errorContent: ReadableContent = { url: '', html: `
Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}
`, @@ -75,6 +97,8 @@ export function useEventLoader({ setReaderContent(errorContent) setReaderLoading(false) } - ) - }, [eventId, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent]) + }) + + return () => subscription.unsubscribe() + }, [eventId, relayPool, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent]) } diff --git a/src/services/eventManager.ts b/src/services/eventManager.ts index e798ea4a..a15993ca 100644 --- a/src/services/eventManager.ts +++ b/src/services/eventManager.ts @@ -2,26 +2,17 @@ import { RelayPool } from 'applesauce-relay' import { IEventStore } from 'applesauce-core' import { createEventLoader } from 'applesauce-loaders/loaders' import { NostrEvent } from 'nostr-tools' -import { BehaviorSubject, Observable } from 'rxjs' - -type EventCallback = (event: NostrEvent) => void -type ErrorCallback = (error: Error) => void +import { Observable } from 'rxjs' /** - * Centralized event manager for fetching and caching events - * Handles deduplication of requests and provides a single source of truth + * Centralized event manager for event fetching coordination + * Manages initialization and provides utilities for event loading */ class EventManager { private eventStore: IEventStore | null = null private relayPool: RelayPool | null = null private eventLoader: ReturnType | null = null - // Track pending requests to avoid duplicates - private pendingRequests = new Map>() - - // Event stream for real-time updates - private eventSubject = new BehaviorSubject(null) - /** * Initialize the event manager with event store and relay pool */ @@ -29,7 +20,8 @@ class EventManager { this.eventStore = eventStore this.relayPool = relayPool - if (relayPool && this.eventLoader === null) { + // Recreate loader when services change + if (relayPool) { this.eventLoader = createEventLoader(relayPool, { eventStore: eventStore || undefined }) @@ -37,98 +29,40 @@ class EventManager { } /** - * Fetch an event by ID, with automatic deduplication and caching + * Get the event loader for fetching events */ - async fetchEvent(eventId: string): Promise { - // Check cache first - if (this.eventStore) { - const cached = this.eventStore.getEvent(eventId) - if (cached) { - return cached - } - } - - // Return a promise that will be resolved when the event is fetched - return new Promise((resolve, reject) => { - this.fetchEventAsync(eventId, resolve, reject) - }) + getEventLoader(): ReturnType | null { + return this.eventLoader } /** - * Subscribe to event fetching with callbacks + * Get the event store */ - private fetchEventAsync( - eventId: string, - onSuccess: EventCallback, - onError: ErrorCallback - ): void { - // Check if we're already fetching this event - if (this.pendingRequests.has(eventId)) { - // Add to existing request queue - this.pendingRequests.get(eventId)!.push({ onSuccess, onError }) - return - } - - // Start a new fetch request - this.pendingRequests.set(eventId, [{ onSuccess, onError }]) - - // If no relay pool yet, wait for it - if (!this.relayPool || !this.eventLoader) { - // Will retry when services are set - setTimeout(() => { - // Retry if still no pool - if (!this.relayPool) { - this.retryPendingRequest(eventId) - } - }, 1000) - return - } - - const subscription = this.eventLoader({ id: eventId }).subscribe({ - next: (event: NostrEvent) => { - // Call all pending callbacks - const callbacks = this.pendingRequests.get(eventId) || [] - this.pendingRequests.delete(eventId) - - callbacks.forEach(cb => cb.onSuccess(event)) - - // Emit to stream - this.eventSubject.next(event) - - subscription.unsubscribe() - }, - error: (err: unknown) => { - // Call all pending callbacks with error - const callbacks = this.pendingRequests.get(eventId) || [] - this.pendingRequests.delete(eventId) - - const error = err instanceof Error ? err : new Error(String(err)) - callbacks.forEach(cb => cb.onError(error)) - - subscription.unsubscribe() - } - }) + getEventStore(): IEventStore | null { + return this.eventStore } /** - * Retry pending requests after delay (useful when relay pool becomes available) + * Get the relay pool */ - private retryPendingRequest(eventId: string): void { - const callbacks = this.pendingRequests.get(eventId) - if (!callbacks) return - - // Re-trigger the fetch - this.pendingRequests.delete(eventId) - if (callbacks.length > 0) { - this.fetchEventAsync(eventId, callbacks[0].onSuccess, callbacks[0].onError) - } + getRelayPool(): RelayPool | null { + return this.relayPool } /** - * Get the event stream for reactive updates + * Check if event exists in store and return it if available */ - getEventStream(): Observable { - return this.eventSubject.asObservable() + getCachedEvent(eventId: string): NostrEvent | null { + if (!this.eventStore) return null + return this.eventStore.getEvent(eventId) || null + } + + /** + * Fetch event by ID, returning an observable + */ + fetchEvent(eventId: string): Observable | null { + if (!this.eventLoader) return null + return this.eventLoader({ id: eventId }) } } From 1929b5089279848fc8e69f5ebdd05302febba295 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:55:20 +0200 Subject: [PATCH 32/43] fix: properly implement eventManager with promise-based API - Fix eventManager to handle async fetching with proper promise resolution - Track pending requests and deduplicate concurrent requests for same event - Auto-retry when relay pool becomes available - Resolve all pending callbacks when event arrives - Update useEventLoader to use eventManager.fetchEvent - Simplify useEventLoader with just one effect for fetching - Handles both instant cache hits and deferred relay fetching --- src/hooks/useEventLoader.ts | 60 ++++++++----------- src/services/eventManager.ts | 109 +++++++++++++++++++++++++---------- 2 files changed, 103 insertions(+), 66 deletions(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 63823743..946ab813 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -55,50 +55,36 @@ export function useEventLoader({ useEffect(() => { if (!eventId) return - // Try to get from event store first (check cache synchronously) - const cachedEvent = eventManager.getCachedEvent(eventId) - if (cachedEvent) { - displayEvent(cachedEvent) - setReaderLoading(false) - setIsCollapsed(false) - setSelectedUrl('') - return - } - - // Event not in cache, set loading state and fetch from relays setReaderLoading(true) setReaderContent(undefined) setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights setIsCollapsed(false) - // If no relay pool yet, wait for it (will re-run when relayPool changes) - if (!relayPool) { - return - } + // Fetch using event manager (handles cache, deduplication, and retry) + let cancelled = false - // Fetch from relays using event manager's loader - const eventLoader = eventManager.getEventLoader() - if (!eventLoader) { - setReaderLoading(false) - return - } - - const subscription = eventLoader({ id: eventId }).subscribe({ - next: (event) => { - displayEvent(event) - setReaderLoading(false) - }, - error: (err) => { - const errorContent: ReadableContent = { - url: '', - html: `
Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}
`, - title: 'Error' + eventManager.fetchEvent(eventId).then( + (event) => { + if (!cancelled) { + displayEvent(event) + setReaderLoading(false) + } + }, + (err) => { + if (!cancelled) { + const errorContent: ReadableContent = { + url: '', + html: `
Failed to load event: ${err instanceof Error ? err.message : 'Unknown error'}
`, + title: 'Error' + } + setReaderContent(errorContent) + setReaderLoading(false) } - setReaderContent(errorContent) - setReaderLoading(false) } - }) + ) - return () => subscription.unsubscribe() - }, [eventId, relayPool, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent]) + return () => { + cancelled = true + } + }, [eventId, displayEvent, setReaderLoading, setSelectedUrl, setIsCollapsed, setReaderContent]) } diff --git a/src/services/eventManager.ts b/src/services/eventManager.ts index a15993ca..a3a3a9b8 100644 --- a/src/services/eventManager.ts +++ b/src/services/eventManager.ts @@ -2,17 +2,24 @@ import { RelayPool } from 'applesauce-relay' import { IEventStore } from 'applesauce-core' import { createEventLoader } from 'applesauce-loaders/loaders' import { NostrEvent } from 'nostr-tools' -import { Observable } from 'rxjs' + +type PendingRequest = { + resolve: (event: NostrEvent) => void + reject: (error: Error) => void +} /** - * Centralized event manager for event fetching coordination - * Manages initialization and provides utilities for event loading + * Centralized event manager for event fetching and caching + * Handles deduplication of concurrent requests and coordinate with relay pool */ class EventManager { private eventStore: IEventStore | null = null private relayPool: RelayPool | null = null private eventLoader: ReturnType | null = null + // Track pending requests to deduplicate and resolve all at once + private pendingRequests = new Map() + /** * Initialize the event manager with event store and relay pool */ @@ -25,32 +32,14 @@ class EventManager { this.eventLoader = createEventLoader(relayPool, { eventStore: eventStore || undefined }) + + // Retry any pending requests now that we have a loader + this.retryAllPending() } } /** - * Get the event loader for fetching events - */ - getEventLoader(): ReturnType | null { - return this.eventLoader - } - - /** - * Get the event store - */ - getEventStore(): IEventStore | null { - return this.eventStore - } - - /** - * Get the relay pool - */ - getRelayPool(): RelayPool | null { - return this.relayPool - } - - /** - * Check if event exists in store and return it if available + * Get cached event from event store */ getCachedEvent(eventId: string): NostrEvent | null { if (!this.eventStore) return null @@ -58,11 +47,73 @@ class EventManager { } /** - * Fetch event by ID, returning an observable + * Fetch an event by ID, returning a promise + * Automatically deduplicates concurrent requests for the same event */ - fetchEvent(eventId: string): Observable | null { - if (!this.eventLoader) return null - return this.eventLoader({ id: eventId }) + fetchEvent(eventId: string): Promise { + // Check cache first + const cached = this.getCachedEvent(eventId) + if (cached) { + return Promise.resolve(cached) + } + + return new Promise((resolve, reject) => { + // Check if we're already fetching this event + if (this.pendingRequests.has(eventId)) { + // Add to existing request queue + this.pendingRequests.get(eventId)!.push({ resolve, reject }) + return + } + + // Start a new fetch request + this.pendingRequests.set(eventId, [{ resolve, reject }]) + this.fetchFromRelay(eventId) + }) + } + + /** + * Actually fetch the event from relay + */ + private fetchFromRelay(eventId: string): void { + // If no loader yet, schedule retry + if (!this.relayPool || !this.eventLoader) { + setTimeout(() => { + if (this.eventLoader && this.pendingRequests.has(eventId)) { + this.fetchFromRelay(eventId) + } + }, 500) + return + } + + const subscription = this.eventLoader({ id: eventId }).subscribe({ + next: (event: NostrEvent) => { + // Resolve all pending requests + const requests = this.pendingRequests.get(eventId) || [] + this.pendingRequests.delete(eventId) + + requests.forEach(req => req.resolve(event)) + subscription.unsubscribe() + }, + error: (err: unknown) => { + // Reject all pending requests + const requests = this.pendingRequests.get(eventId) || [] + this.pendingRequests.delete(eventId) + + const error = err instanceof Error ? err : new Error(String(err)) + requests.forEach(req => req.reject(error)) + subscription.unsubscribe() + } + }) + } + + /** + * Retry all pending requests after relay pool becomes available + */ + private retryAllPending(): void { + const pendingIds = Array.from(this.pendingRequests.keys()) + pendingIds.forEach(eventId => { + this.fetchFromRelay(eventId) + }) } } From ab8665815bc17d8a835f26d4704d801e6efd25f8 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:56:40 +0200 Subject: [PATCH 33/43] chore: remove debug logging from bookmarkHelpers - Remove 'NO MATCHES' debug logs from hydrateItems - Console is now clean, hydration is working properly --- src/services/bookmarkHelpers.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index 10b1b2de..3795d5a7 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -170,15 +170,6 @@ export function hydrateItems( items: IndividualBookmark[], idToEvent: Map ): IndividualBookmark[] { - const kind1Items = items.filter(b => b.kind === 1) - if (kind1Items.length > 0 && idToEvent.size > 0) { - const found = kind1Items.filter(b => idToEvent.has(b.id)) - if (found.length === 0 && kind1Items.length > 0) { - console.log('❌ NO MATCHES! Sample item IDs:', kind1Items.slice(0, 3).map(b => b.id)) - console.log('❌ Map keys:', Array.from(idToEvent.keys()).slice(0, 3)) - } - } - return items .map(item => { const ev = idToEvent.get(item.id) From c70e6bc2aa5e0a9d76e05d7c65e0884755bc0cae Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:57:47 +0200 Subject: [PATCH 34/43] debug: log hydration progress to track content population - Add logging to see how many hydrated items have content - This will help diagnose why bookmarks are showing IDs instead of content --- src/services/bookmarkHelpers.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index 3795d5a7..ae0d9ccb 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -170,7 +170,7 @@ export function hydrateItems( items: IndividualBookmark[], idToEvent: Map ): IndividualBookmark[] { - return items + const hydrated = items .map(item => { const ev = idToEvent.get(item.id) if (!ev) return item @@ -202,6 +202,14 @@ export function hydrateItems( const isBookmarkListEvent = item.kind === 10003 || item.kind === 30003 || item.kind === 30001 return !isBookmarkListEvent }) + + // Debug: log how many items have content + const withContent = hydrated.filter(i => i.content && i.content.length > 0) + if (withContent.length > 0 || hydrated.length > 0) { + console.log(`📝 Hydrated ${withContent.length}/${hydrated.length} items have content`) + } + + return hydrated } // Note: event decryption/collection lives in `bookmarkProcessing.ts` From 60e9ede9cfd2acaf18f258fae69f0e5df2f2de67 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 00:59:06 +0200 Subject: [PATCH 35/43] debug: add more detail to hydration logging --- src/services/bookmarkHelpers.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index ae0d9ccb..2379f72c 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -205,8 +205,9 @@ export function hydrateItems( // Debug: log how many items have content const withContent = hydrated.filter(i => i.content && i.content.length > 0) - if (withContent.length > 0 || hydrated.length > 0) { - console.log(`📝 Hydrated ${withContent.length}/${hydrated.length} items have content`) + const notFound = items.filter(i => !idToEvent.has(i.id)) + if (hydrated.length > 0) { + console.log(`📝 Hydrated ${withContent.length}/${hydrated.length} have content, ${notFound.length}/${items.length} not found in map`) } return hydrated From 934768ebf233f49a87bb6579c7fc38369e0a9034 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 01:01:04 +0200 Subject: [PATCH 36/43] chore: remove debug logging from hydration --- src/services/bookmarkHelpers.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/services/bookmarkHelpers.ts b/src/services/bookmarkHelpers.ts index 2379f72c..3795d5a7 100644 --- a/src/services/bookmarkHelpers.ts +++ b/src/services/bookmarkHelpers.ts @@ -170,7 +170,7 @@ export function hydrateItems( items: IndividualBookmark[], idToEvent: Map ): IndividualBookmark[] { - const hydrated = items + return items .map(item => { const ev = idToEvent.get(item.id) if (!ev) return item @@ -202,15 +202,6 @@ export function hydrateItems( const isBookmarkListEvent = item.kind === 10003 || item.kind === 30003 || item.kind === 30001 return !isBookmarkListEvent }) - - // Debug: log how many items have content - const withContent = hydrated.filter(i => i.content && i.content.length > 0) - const notFound = items.filter(i => !idToEvent.has(i.id)) - if (hydrated.length > 0) { - console.log(`📝 Hydrated ${withContent.length}/${hydrated.length} have content, ${notFound.length}/${items.length} not found in map`) - } - - return hydrated } // Note: event decryption/collection lives in `bookmarkProcessing.ts` From 347e23ff6f82d44be53e72031d5e8c668e4b54ba Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 01:01:23 +0200 Subject: [PATCH 37/43] fix: only request hydration for items without content - Only fetch events for bookmarks that don't have content yet - Bookmarks with existing content (web bookmarks, etc.) don't need fetching - This reduces unnecessary fetches and focuses on what's needed - Should show much better content in bookmarks list --- src/services/bookmarkController.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index 062c7b3d..cbeaf395 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -261,19 +261,21 @@ class BookmarkController { }) const allItems = [...publicItemsAll, ...privateItemsAll] - - // Dedupe BEFORE hydration to avoid requesting duplicate events const deduped = dedupeBookmarksById(allItems) - // Separate hex IDs from coordinates + // Separate hex IDs from coordinates for fetching const noteIds: string[] = [] const coordinates: string[] = [] + // Request hydration for all items that don't have content yet deduped.forEach(i => { - if (/^[0-9a-f]{64}$/i.test(i.id)) { - noteIds.push(i.id) - } else if (i.id.includes(':')) { - coordinates.push(i.id) + // If item has no content, we need to fetch it + if (!i.content || i.content.length === 0) { + if (/^[0-9a-f]{64}$/i.test(i.id)) { + noteIds.push(i.id) + } else if (i.id.includes(':')) { + coordinates.push(i.id) + } } }) From 6def58f128e2eb0518890b3eebf0d4962f048d82 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 01:04:23 +0200 Subject: [PATCH 38/43] fix(bookmarks): show eventStore content as fallback for bookmarks without hydrated content - Enrich bookmarks with content from externalEventStore when hydration hasn't populated yet - Keeps sidebar from showing only event IDs while background hydration continues --- src/services/bookmarkController.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/bookmarkController.ts b/src/services/bookmarkController.ts index cbeaf395..aa6e6d08 100644 --- a/src/services/bookmarkController.ts +++ b/src/services/bookmarkController.ts @@ -293,7 +293,10 @@ class BookmarkController { const enriched = allBookmarks.map(b => ({ ...b, tags: b.tags || [], - content: b.content || '' + // Prefer hydrated content; fallback to any cached event content in external store + content: b.content && b.content.length > 0 + ? b.content + : (this.externalEventStore?.getEvent(b.id)?.content || '') })) const sortedBookmarks = enriched From 164e941a1faec48488cb9945521e2c6186e5f333 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 01:09:36 +0200 Subject: [PATCH 39/43] fix(events): make direct event loading robust - Add completion and timeout handling to eventManager.fetchEvent - Resolve/reject all pending promises correctly - Prevent silent completes when event not found - Improves /e/:eventId reliability on cold loads --- src/services/eventManager.ts | 47 ++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/services/eventManager.ts b/src/services/eventManager.ts index a3a3a9b8..92e0d33d 100644 --- a/src/services/eventManager.ts +++ b/src/services/eventManager.ts @@ -20,6 +20,9 @@ class EventManager { // Track pending requests to deduplicate and resolve all at once private pendingRequests = new Map() + // Safety timeout for event fetches (ms) + private fetchTimeoutMs = 12000 + /** * Initialize the event manager with event store and relay pool */ @@ -71,6 +74,18 @@ class EventManager { }) } + private resolvePending(eventId: string, event: NostrEvent): void { + const requests = this.pendingRequests.get(eventId) || [] + this.pendingRequests.delete(eventId) + requests.forEach(req => req.resolve(event)) + } + + private rejectPending(eventId: string, error: Error): void { + const requests = this.pendingRequests.get(eventId) || [] + this.pendingRequests.delete(eventId) + requests.forEach(req => req.reject(error)) + } + /** * Actually fetch the event from relay */ @@ -85,25 +100,37 @@ class EventManager { return } + let delivered = false const subscription = this.eventLoader({ id: eventId }).subscribe({ next: (event: NostrEvent) => { - // Resolve all pending requests - const requests = this.pendingRequests.get(eventId) || [] - this.pendingRequests.delete(eventId) - - requests.forEach(req => req.resolve(event)) + delivered = true + clearTimeout(timeoutId) + this.resolvePending(eventId, event) subscription.unsubscribe() }, error: (err: unknown) => { - // Reject all pending requests - const requests = this.pendingRequests.get(eventId) || [] - this.pendingRequests.delete(eventId) - + clearTimeout(timeoutId) const error = err instanceof Error ? err : new Error(String(err)) - requests.forEach(req => req.reject(error)) + this.rejectPending(eventId, error) + subscription.unsubscribe() + }, + complete: () => { + // Completed without next - consider not found + if (!delivered) { + clearTimeout(timeoutId) + this.rejectPending(eventId, new Error('Event not found')) + } subscription.unsubscribe() } }) + + // Safety timeout + const timeoutId = setTimeout(() => { + if (!delivered) { + this.rejectPending(eventId, new Error('Timed out fetching event')) + subscription.unsubscribe() + } + }, this.fetchTimeoutMs) } /** From d03726801d9cd0636304c40353b33695a57b1c7f Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 01:16:30 +0200 Subject: [PATCH 40/43] feat(/e/): title 'Note by @author' with background profile fetch - Immediate fallback title using short pubkey - Fetch kind:0 profile in background; update title when available - Keeps UI responsive while improving attribution --- src/hooks/useEventLoader.ts | 45 +++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 946ab813..4cf8adfc 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -4,6 +4,7 @@ import { IEventStore } from 'applesauce-core' import { NostrEvent } from 'nostr-tools' import { ReadableContent } from '../services/readerService' import { eventManager } from '../services/eventManager' +import { fetchProfiles } from '../services/profileService' interface UseEventLoaderProps { eventId?: string @@ -39,13 +40,43 @@ export function useEventLoader({ .replace(/>/g, '>') .replace(/\n/g, '
') - const content: ReadableContent = { - url: '', // Empty URL to prevent highlight display - html: metaHtml + `
${escapedContent}
`, - title: `Note (${event.kind})` + // Initial title + let title = `Note (${event.kind})` + if (event.kind === 1) { + title = `Note by @${event.pubkey.slice(0, 8)}...` } - setReaderContent(content) - }, [setReaderContent]) + + // Emit immediately + const baseContent: ReadableContent = { + url: '', + html: metaHtml + `
${escapedContent}
`, + title + } + setReaderContent(baseContent) + + // Background: resolve author profile for kind:1 and update title + if (event.kind === 1 && relayPool && eventStore) { + (async () => { + try { + const profiles = await fetchProfiles(relayPool, eventStore as unknown as IEventStore, [event.pubkey]) + if (!profiles || profiles.length === 0) return + const latest = profiles.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))[0] + let resolved = '' + try { + const obj = JSON.parse(latest.content || '{}') as { name?: string; display_name?: string; nip05?: string } + resolved = obj.display_name || obj.name || obj.nip05 || '' + } catch { + // ignore + } + if (resolved) { + setReaderContent({ ...baseContent, title: `Note by @${resolved}` }) + } + } catch { + // ignore profile failures; keep fallback title + } + })() + } + }, [setReaderContent, relayPool, eventStore]) // Initialize event manager with services useEffect(() => { @@ -57,7 +88,7 @@ export function useEventLoader({ setReaderLoading(true) setReaderContent(undefined) - setSelectedUrl('') // Don't set nostr: URL to avoid showing highlights + setSelectedUrl(`nostr-event:${eventId}`) // sentinel: truthy selection, not treated as article setIsCollapsed(false) // Fetch using event manager (handles cache, deduplication, and retry) From f09973c858f623e6af22c90f2c4ae62eb9e36ca7 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 01:18:14 +0200 Subject: [PATCH 41/43] feat(/e/): display publication date in top-right like articles - Remove inline metadata HTML from note content - Pass event.created_at as published timestamp via ReadableContent - ReaderHeader now displays date in top-right corner --- src/hooks/useEventLoader.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 4cf8adfc..8958c346 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -26,13 +26,6 @@ export function useEventLoader({ setIsCollapsed }: UseEventLoaderProps) { const displayEvent = useCallback((event: NostrEvent) => { - // Format event metadata as HTML header - const metaHtml = `
-
Event ID: ${event.id.slice(0, 16)}...
-
Posted: ${new Date(event.created_at * 1000).toLocaleString()}
-
Kind: ${event.kind}
-
` - // Escape HTML in content and convert newlines to breaks for plain text display const escapedContent = event.content .replace(/&/g, '&') @@ -49,8 +42,9 @@ export function useEventLoader({ // Emit immediately const baseContent: ReadableContent = { url: '', - html: metaHtml + `
${escapedContent}
`, - title + html: `
${escapedContent}
`, + title, + published: event.created_at } setReaderContent(baseContent) From bb668239156da151614c54ffd5ece2556e5b99c6 Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 01:18:51 +0200 Subject: [PATCH 42/43] fix(/e/): Search button opens note via /e/ path not search portal - For kind:1 notes, open directly via /e/{eventId} - For articles (kind:30023), continue using search portal - Removes nostr-event: prefix in URLs --- src/components/ContentPanel.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index 26e33c7b..e77a6917 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -485,7 +485,12 @@ const ContentPanel: React.FC = ({ } const handleOpenSearch = () => { - if (articleLinks) { + // For regular notes (kind:1), open via /e/ path + if (currentArticle?.kind === 1) { + const borisUrl = `${window.location.origin}/e/${currentArticle.id}` + window.open(borisUrl, '_blank', 'noopener,noreferrer') + } else if (articleLinks) { + // For articles, use search portal window.open(getSearchUrl(articleLinks.naddr), '_blank', 'noopener,noreferrer') } setShowArticleMenu(false) From 366e10b23ab5b431b51b2173ae71084f23c2bfbe Mon Sep 17 00:00:00 2001 From: Gigi Date: Wed, 22 Oct 2025 01:19:09 +0200 Subject: [PATCH 43/43] feat(/e/): check eventStore first for author profile - Try to load author profile from eventStore cache first - Only fetch from relays if not found in cache - Instant title update if profile already loaded --- src/hooks/useEventLoader.ts | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/hooks/useEventLoader.ts b/src/hooks/useEventLoader.ts index 8958c346..c2cea86e 100644 --- a/src/hooks/useEventLoader.ts +++ b/src/hooks/useEventLoader.ts @@ -49,19 +49,36 @@ export function useEventLoader({ setReaderContent(baseContent) // Background: resolve author profile for kind:1 and update title - if (event.kind === 1 && relayPool && eventStore) { + if (event.kind === 1 && eventStore) { (async () => { try { - const profiles = await fetchProfiles(relayPool, eventStore as unknown as IEventStore, [event.pubkey]) - if (!profiles || profiles.length === 0) return - const latest = profiles.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))[0] let resolved = '' - try { - const obj = JSON.parse(latest.content || '{}') as { name?: string; display_name?: string; nip05?: string } - resolved = obj.display_name || obj.name || obj.nip05 || '' - } catch { - // ignore + + // First, try to get from event store cache + const storedProfile = eventStore.getEvent(event.pubkey + ':0') + if (storedProfile) { + try { + const obj = JSON.parse(storedProfile.content || '{}') as { name?: string; display_name?: string; nip05?: string } + resolved = obj.display_name || obj.name || obj.nip05 || '' + } catch { + // ignore parse errors + } } + + // If not found in event store, fetch from relays + if (!resolved && relayPool) { + const profiles = await fetchProfiles(relayPool, eventStore as unknown as IEventStore, [event.pubkey]) + if (profiles && profiles.length > 0) { + const latest = profiles.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))[0] + try { + const obj = JSON.parse(latest.content || '{}') as { name?: string; display_name?: string; nip05?: string } + resolved = obj.display_name || obj.name || obj.nip05 || '' + } catch { + // ignore parse errors + } + } + } + if (resolved) { setReaderContent({ ...baseContent, title: `Note by @${resolved}` }) }