Simplified to only show unencrypted bookmarks:
- Skip encrypted events entirely (no decrypt for now)
- This eliminates all parse errors
Added comprehensive logging:
- Controller: log when building, how many items, how many listeners, when emitting
- App: log when subscribing, when receiving bookmarks, when loading state changes
This will help identify where the disconnect is between controller and sidebar.
Filter events in buildAndEmitBookmarks to avoid parse errors:
- Unencrypted events: always included
- Encrypted events: only included if already decrypted
Progressive flow:
- Unencrypted event arrives → build bookmarks immediately
- Encrypted event arrives → wait for decrypt → then build bookmarks
- Each build only processes ready events (no parse errors)
Sidebar now populates with unencrypted bookmarks immediately, encrypted ones appear after decrypt.
Changed bookmark controller to emit updates progressively:
- Unencrypted events: immediate buildAndEmitBookmarks call
- Encrypted events: buildAndEmitBookmarks after decrypt completes
- Each update emits new bookmark list to subscribers
Removed coalescing/scheduling logic (scheduleBookmarkUpdate):
- Direct callback pattern is simpler and more predictable
- Updates happen exactly when events are ready
Progressive sidebar population now works correctly without parse errors.
Changed onEvent callback from async to synchronous:
- Removed await inside onEvent that was blocking observable
- Decryption now fires in background using .then()/.catch()
- Allows queryEvents to complete (EOSE) and trigger final bookmark build
This matches the working Debug pattern and allows bookmarks to appear in sidebar.
Deleted bookmarkService.ts and bookmarkStream.ts:
- All functionality now consolidated in bookmarkController.ts
- No more duplication of streaming/decrypt logic
- Single source of truth for bookmark loading
Made bookmarksLoading prop optional in MeProps since it's not currently used.
Reserved for future use when we want to show centralized loading state.
All linting and type checks now pass.
Updated Me.tsx to receive bookmarks from centralized App state:
- Added bookmarks and bookmarksLoading to MeProps
- Removed local bookmarks state
- Removed bookmark caching (now handled at App level)
Updated Bookmarks.tsx to pass bookmarks props to Me component:
- Both 'me' and 'profile' views receive centralized bookmarks
All bookmark data now flows from App.tsx -> Bookmarks.tsx -> Me.tsx with no duplicate fetching or local state.
Created bookmarkStream.ts with shared helpers:
- getEventKey: deduplication logic
- hasEncryptedContent: encryption detection
- loadBookmarksStream: streaming with non-blocking decryption
Refactored bookmarkService.ts to use shared helpers:
- Uses loadBookmarksStream for consistent behavior with Debug page
- Maintains progressive loading via callbacks
- Added accountManager parameter to fetchBookmarks
Updated App.tsx to pass accountManager to fetchBookmarks:
- Progressive loading indicators via onProgressUpdate callback
All bookmark loading now uses the same battle-tested streaming logic as Debug page.
Removed duplicate bookmark loading logic from Debug page:
- Debug 'Load Bookmarks' button now calls centralized onRefreshBookmarks
- Removed redundant state (bookmarkEvents, bookmarkStats, decryptedEvents)
- Removed unused helper functions (getKindName, getEventSize, etc.)
- Cleaned up imports (Helpers, queryEvents, collectBookmarksFromEvents)
- Simplified UI to show timing only, bookmarks visible in sidebar
Now there's truly ONE place for bookmark loading (bookmarkService.ts),
called from App.tsx and used throughout the app. Debug page's button
is now the same as clicking refresh in the bookmark sidebar.
Fixed type error in Debug.tsx:
- Changed highlightVisibility from string to proper HighlightVisibility object
- Used 'support' prop instead of invalid 'children' prop for ThreePaneLayout
- Set showSupport={true} to properly render debug content
All linting and type checks now pass.
Added ThreePaneLayout to Debug page so bookmarks are visible:
- Debug page now has same layout as other pages
- Shows bookmarks sidebar on the left
- Debug content in the main pane
- Can compare centralized app bookmarks with Debug bookmarks side-by-side
This makes it easy to verify that centralized bookmark loading
works the same as the Debug page implementation.
Fixed critical issue where async operations in onEvent callback
were blocking the queryEvents observable from completing:
Changes:
1. Removed async/await from onEvent callback
- Now just collects events synchronously
- No blocking operations in the stream
2. Moved auto-decryption to after query completes
- Batch process encrypted events after EOSE
- Sequential decryption (cleaner, more predictable)
3. Simplified useEffect triggers in App.tsx
- Removed duplicate mount + account change effects
- Single effect handles both cases
Result: Query now completes properly, bookmarks load and display.
Added comprehensive console logs to diagnose bookmark loading issue:
- [app] prefix for all bookmark-related logs
- Log account pubkey being used
- Log each event as it arrives
- Log auto-decrypt attempts
- Log final processing steps
- Log when no bookmarks found
This will help identify where the bookmark loading is failing.
Implemented centralized bookmark loading system:
- Bookmarks loaded in App.tsx with streaming + auto-decrypt pattern
- Load triggers: login, app mount, manual refresh only
- No redundant fetching on route changes
Changes:
1. bookmarkService.ts: Refactored fetchBookmarks for streaming
- Events stream with onEvent callback
- Auto-decrypt encrypted content (NIP-04/NIP-44) as events arrive
- Progressive UI updates during loading
2. App.tsx: Added centralized bookmark state
- bookmarks and bookmarksLoading state in AppRoutes
- loadBookmarks function with streaming support
- Load on mount if account exists (app reopen)
- Load when activeAccount changes (login)
- handleRefreshBookmarks for manual refresh
- Pass props to all Bookmarks components
3. Bookmarks.tsx: Accept bookmarks as props
- Receive bookmarks, bookmarksLoading, onRefreshBookmarks
- Pass onRefreshBookmarks to useBookmarksData
4. useBookmarksData.ts: Simplified to accept bookmarks as props
- Removed bookmark fetching logic
- Removed handleFetchBookmarks function
- Accept onRefreshBookmarks callback
- Use onRefreshBookmarks in handleRefreshAll
5. Me.tsx: Removed fallback bookmark loading
- Removed fetchBookmarks import and calls
- Use bookmarks directly from props (centralized source)
Benefits:
- Single source of truth for bookmarks
- No duplicate fetching across components
- Streaming + auto-decrypt for better UX
- Simpler, more maintainable code
- DRY principle: one place for bookmark loading
Simplified bookmark loading by chaining loading and decryption:
- Events with encrypted content are automatically decrypted as they arrive
- Removed separate "Decrypt" button - now automatic
- Removed individual decrypt buttons - happens automatically
- Removed handleDecryptSingleEvent and related state
- Cleaner UI with just "Load Bookmarks" and "Clear" buttons
Benefits:
- Simpler, more intuitive UX
- DRY - single flow instead of 2-step process
- Shows decryption results inline as events stream in
- Uses same collectBookmarksFromEvents for consistency
Each event with encrypted content (NIP-04 or NIP-44) is decrypted
immediately in the onEvent callback, with results displayed inline.
Documented kind:30001 bookmark format used by Amethyst:
- Public bookmarks in event tags
- Private bookmarks in encrypted content (NIP-04 or NIP-44)
Explained why explicit NIP-04 detection (?iv= check) is required:
- Helpers.hasHiddenContent() only detects NIP-44
- Without NIP-04 detection, private bookmarks never get decrypted
Added example event structure and implementation notes for both
display logic and decryption logic.
Added explicit NIP-04 detection in bookmarkProcessing.ts:
- Check for ?iv= in content (NIP-04 format)
- Previously only checked Helpers.hasHiddenContent() (NIP-44 only)
- Now decrypts both NIP-04 and NIP-44 encrypted bookmarks
This fixes individual bookmark decryption returning 0 private items
despite having encrypted content.
Changed all debug console logs to use [bunker] prefix with emojis:
- 🔵 Individual decrypt clicked
- 🔓 Decrypting event (with details)
- ✅ Event decrypted (with results)
- ⚠️ Warnings (no account, 0 private items)
- ❌ Errors
Now users can filter console by 'bunker' to see all relevant logs.
Added extensive debug logging to help diagnose decryption issues:
- Event details (kind, content length, encryption type)
- Signer information (type, availability)
- Warning when 0 private items found despite encrypted content
This will help identify why decryption might be failing silently.
Added explicit detection for NIP-04 encrypted content format:
- NIP-04: base64 content with ?iv= suffix
- NIP-44: detected by Helpers.hasHiddenContent()
- Encrypted tags: detected by Helpers.hasHiddenTags()
Created hasEncryptedContent() helper that checks all three cases.
Now properly shows padlock emoji and decrypt button for events with
NIP-04 encrypted content (like the example with ?iv=5KzDXv09...).
Fixed mismatch between padlock display and decrypt button visibility:
- Both now use Helpers.hasHiddenContent() and Helpers.hasHiddenTags()
- Previously padlock showed for ANY content, button only for encrypted
- Now both correctly detect actual encrypted content
This ensures decrypt buttons appear whenever padlocks are shown.
Added per-event decryption on debug page:
- Small 'decrypt' button appears on events with encrypted content
- Shows spinner while decrypting individual event
- Displays decryption results (public/private counts) inline
- Button disappears after successful decryption
Uses Helpers.hasHiddenContent() and Helpers.hasHiddenTags() to detect
which events need decryption.
Allows testing individual event decryption without batch operation.
Updated debug page to display the actual account type:
- Browser Extension (type: 'extension')
- Bunker Connection (type: 'nostr-connect')
- Account Connection (fallback)
Changes:
- Section title now reflects active account type
- Connection status message updated accordingly
- No longer always shows 'Bunker Connection' regardless of type
Makes it clear to users which authentication method they're using.
Set accounts.disableQueue = true on AccountManager during initialization:
- Applies to all accounts automatically
- No need for temporary queue toggling in individual operations
- Makes all bunker requests instant (no internal queueing)
Removed temporary queue disabling from bookmarkProcessing.ts since
it's now globally disabled.
Updated Amber.md to document the global approach.
This eliminates the root cause of decrypt hangs - requests no longer
wait in an internal queue for previous requests to complete.
Reduced timeouts to trust EOSE from fast relays:
- Local: 800ms (down from 1200ms)
- Remote: 2000ms (down from 6000ms)
The query completes when relays send EOSE, not when timeout expires.
Fast relays send EOSE in <1 second, so total time should be much less
than the previous 6-second wait.
Result: Debug page bookmark loading now completes in ~1-2 seconds instead of always 6 seconds.
Implemented live streaming of bookmark events as they arrive from relays:
- Events appear immediately as relays respond
- Live deduplication of replaceable events (30003, 30001, 10003)
- Keep newest version when duplicates found
- Web bookmarks (39701) not deduplicated (each unique)
Benefits:
- Much more responsive UI - see events immediately
- Better user experience with progress visibility
- Deduplication happens in real-time
Uses queryEvents onEvent callback to process events as they stream in.
Removed all withTimeout wrappers - now matches debug page behavior:
- Direct decrypt calls with no artificial timeouts
- Let operations fail naturally and quickly
- Bunker responds instantly (success or rejection)
No timeouts needed because:
1. Account queue is disabled (requests sent immediately)
2. Only decrypting truly encrypted content (no wasted attempts)
3. Bunker either succeeds quickly or fails quickly
This makes bookmark decryption instant, just like the debug page
encryption/decryption tests.
Use applesauce Helpers.hasHiddenContent() instead of checking for
any content. This properly detects encrypted content and avoids
sending unnecessary decrypt requests to Amber for events that just
have plain text content.
Before: (evt.content && evt.content.length > 0)
After: Helpers.hasHiddenContent(evt)
Result:
- Only events with encrypted content sent to Amber
- Reduces unnecessary decrypt requests
- Faster bookmark loading
Added findings about applesauce-accounts queue issue:
- Queue MUST be disabled for batch decrypt operations
- Default queueing blocks all requests until first completes
- This was the primary cause of hangs and timeouts
- Updated performance improvements section with all optimizations
- Updated conclusion with key learnings
Ref: https://hzrd149.github.io/applesauce/typedoc/classes/applesauce-accounts.BaseAccount.html#disablequeue
The applesauce BaseAccount queues requests by default, waiting for
each to complete before sending the next. This caused decrypt requests
to timeout before ever reaching Amber/bunker.
Solution:
- Set disableQueue=true before batch operations
- All decrypt requests sent immediately
- Restore original queue state after completion
This should fix the hanging/timeout issue where Amber never saw
the decrypt requests because they were stuck in the account's queue.
Ref: https://hzrd149.github.io/applesauce/typedoc/classes/applesauce-accounts.BaseAccount.html#disablequeue
Removed mapWithConcurrency hack:
- Simpler code with plain for loop
- More predictable behavior
- Better for bunker signers (network round-trips)
- Each decrypt happens in order, no race conditions
Sequential processing is cleaner and works better with remote signers.
Changes:
- Use 5-second timeout instead of 30 seconds
- Detect encryption method from content format
- NIP-04 has '?iv=' in content, NIP-44 doesn't
- Try the likely method first to avoid unnecessary timeouts
- Falls back to other method if first fails
This should:
- Prevent hanging forever (5s timeout)
- Be much faster than 30s when wrong method is tried
- Usually decrypt instantly when right method is used first
The withTimeout wrapper was causing decrypt operations to wait
30 seconds before failing, even when bunker rejection was instant.
Now uses the same direct approach as the debug page encryption tests:
- No artificial timeouts
- Fast natural failures
- Should reduce decrypt time from 30s+ to near-instant
Fixes slow bookmark loading when bunker doesn't support nip44.
- Track load and decrypt operation durations
- Display live timing with spinner during operations
- Show completed timing in milliseconds
- Uses same Stat component as encryption timing
- Show kind names (Simple List, Replaceable List, etc)
- Display data size in human-readable format (B, KB, MB)
- Show count of public bookmarks per event
- Indicate presence of encrypted content
- Show d-tag and title for better identification
- Add withTimeout and mapWithConcurrency helpers in utils/async.ts
- Refactor collectBookmarksFromEvents to decrypt with 6-way concurrency
- Public bookmarks collected immediately, private decrypted in parallel
- Each decrypt wrapped with 30s timeout safety net
- Document non-blocking publish and concurrent decrypt in Amber.md