Reads don't actually need bookmarks to load. Reading progress (kind:39802)
is independent and stands on its own. Bookmarks are just optional enrichment.
Changed:
- readsController.start() no longer takes bookmarks parameter
- Pass empty array to fetchAllReads instead
- Load reads immediately in App.tsx like highlights/writings
- No more circular dependency on bookmarks loading first
This is simpler and loads reading progress faster.
The onItem callback was filtering to only 'article' type items,
which excluded external URLs from reading progress. Now all items
(articles and external URLs) are emitted to readsController.
This fixes the empty reads list issue where reading progress exists
but wasn't being displayed.
- New src/services/readsController.ts manages all reading activity centrally
- Streams reading items as they arrive (progress, marks as read, bookmarks)
- Supports subscriptions via onReads() and onLoading() callbacks
- Tracks loading state and last synced timestamp per user
- Generation-based cancellation for logout/pubkey changes
- Deduplicates by article ID and sorts by reading activity
- Updated Me.tsx loadReadsTab to use readsController instead of calling fetchAllReads
- Provides same reactive, non-blocking UX as highlightsController
- Add hideBookmarksWithoutCreationDate to UserSettings
- New checkbox in Layout & Behavior settings
- Bookmarks without valid creation dates shown as 'Now'
- Setting disabled by default to maintain current behavior
- Remove low-position guard; allow 0% saves
- One-time initial save even without significant change
- Always allow immediate save regardless of position
- Fix linter empty-catch warnings in readingProgressController
- Seed controller state from cache on start for instant display after refresh
- Persist updated progress map after processing events
- Keeps progress visible even without immediate relay responses
- Subscribe to timeline for immediate local events and reactive updates
- Clean up timeline subscription on reset/start to avoid leaks
- Keep relay sync for background augmentation
- Should populate progress map even without relay roundtrip
- Add proper types (Filter, NostrEvent) to readingProgressController
- Add eslint-disable comment for position dependency in useReadingPosition
(position is derived from scroll and including it would cause infinite re-renders)
- All lint warnings resolved
- TypeScript type checks pass
- If currentProgressMap is empty, do a full sync (no 'since' filter)
- This ensures first load gets all events, not just recent ones
- Incremental sync only happens when we already have data
- This was the bug: lastSynced was preventing initial load of events
- Timeline subscription is async and emits empty array first
- queryEvents already checks local store then relays
- Simpler and actually works correctly
- This is how all other controllers work (highlights, bookmarks, etc.)
- Capture events from timeline before unsubscribing
- Add log to show when timeline emits
- Add log after unsubscribe to show what we got
- This will help debug why processEvents isn't being called
- Subscribe to timeline to get initial cached events
- Unsubscribe immediately after reading initial value
- This works with IEventStore interface correctly
- Query local event store immediately for instant display
- Then augment with relay data in background
- This matches how bookmarks work: local-first, then sync
- Events saved locally now appear immediately without waiting for relay propagation
- Show all map keys when looking up reading progress
- Show d-tag generation from naddr in save flow
- This will help identify if naddr encoding/decoding is causing mismatch
- Add logs in useReadingPosition: scroll position calculation (throttled to 5% changes)
- Add logs for scheduling and triggering auto-save
- Add detailed logs in ContentPanel handleSavePosition
- Add logs in saveReadingPosition: event creation, signing, publishing
- Add logs in publishEvent: event store addition, relay status, publishing
- All logs prefixed with [progress] for easy filtering
- Shows complete flow from scroll → calculate → save → create event → publish to relays
- Add logs in readingProgressController: processing events, emitting to listeners
- Add logs in Explore component: receiving updates, looking up progress
- Add logs in BlogPostCard: rendering with progress
- Add detailed logs in processReadingProgress: event parsing, naddr conversion
- All logs prefixed with [progress] for easy filtering
- EventStore doesn't have a list() method
- Follow same pattern as highlightsController and just fetch from relays
- Fixes TypeError: eventStore.list is not a function
- Add readingProgressController following the same pattern as highlightsController and writingsController
- Controller manages reading progress (kind:39802) centrally with subscriptions
- Remove duplicated reading progress loading logic from Explore, Profile, and Me components
- Components now subscribe to controller updates instead of loading data individually
- Supports incremental sync and force reload
- Improves efficiency and maintainability
- Remove 'client' tag from NIP-85 specification
- Remove 'client' tag from code implementation
- Align with Nostr principles of client-agnostic data
- Follow NIP-84 pattern which doesn't include client tags
Events should be client-agnostic and not include branding/tracking.
- Rename NIP-39802.md to NIP-85.md
- Update all references from NIP-39802 to NIP-85 in code comments
- Add Table of Contents to NIP document
- Update kinds.ts to reference NIP-85 and NIP-84 (highlights)
- Maintain kind number 39802 for the event type
NIP-85 is the specification number, 39802 is the event kind number.
- Add autoMarkAsReadOnCompletion setting (opt-in, default: false)
- Implement auto-mark as read when reaching 95%+ completion
- Add validation for progress bounds (0-1) per NIP-39802 spec
- Align completion threshold to 95% to match filter behavior
- Skip invalid progress events with warning log
Improvements ensure consistency between completion detection and
filtering, while adding safety validation per the NIP spec.
- Create READING_PROGRESS_MIGRATION.md with detailed migration phases
- Document test scenarios inline in readingPositionService and readingDataProcessor
- Outline timeline for dual-write, prefer-new, and deprecation phases
- Add rollback plan and settings API documentation
- Include comparison table of legacy vs new event formats
- Add kind 39802 (ReadingProgress) as dedicated parameterized replaceable event
- Create NIP-39802 specification document in public/md/
- Implement dual-write: publish both kind 39802 and legacy kind 30078
- Implement dual-read: prefer kind 39802, fall back to kind 30078
- Add migration flags to settings (useReadingProgressKind, writeLegacyReadingPosition)
- Update readingPositionService with new d-tag generation and tag helpers
- Add processReadingProgress() for kind 39802 events in readingDataProcessor
- Update readsService and linksService to query and process both kinds
- Use event.created_at as authoritative timestamp per NIP-39802 spec
- ContentPanel respects migration flags from settings
- Maintain backward compatibility during migration phase
- Remove all console.log statements with [bookmark] prefix from App.tsx
- Remove all console.log statements with [bookmark] prefix from bookmarkController.ts
- Replace verbose error logging with simple error messages
- Keep code clean and reduce console clutter
- Make limit parameter configurable in fetchBlogPostsFromAuthors
- Default limit is 100 for Explore page (multiple authors)
- Pass null limit for Profile pages to fetch all writings
- Fixes issue where only 1 writing was shown instead of all
- Add eventStore parameter to fetchNostrverseBlogPosts
- Add eventStore parameter to fetchNostrverseHighlights
- Pass eventStore from Explore component to nostrverse fetchers
- Store all nostrverse blog posts and highlights in event store
- Enables offline access to nostrverse content
Add user setting to control default visibility scope in /explore page.
Changes:
- Add defaultExploreScopeNostrverse/Friends/Mine to UserSettings type
- Add "Default Explore Scope" setting in ReadingDisplaySettings UI
- Update Explore component to use defaultExploreScope settings
- Set default to friends-only (nostrverse: false, friends: true, mine: false)
Users can now configure which content types (nostrverse/friends/mine)
are visible by default when visiting the explore page, separate from
the highlight visibility settings.
- Create highlightsController with subscription API and event store integration
- Auto-load user highlights on app start (alongside bookmarks and contacts)
- Store highlight events in applesauce event store for offline support
- Update Me.tsx to use controller for own profile highlights
- Add optional eventStore parameter to all highlight fetch functions
- Pass eventStore through Debug component for persistent storage
- Implement incremental sync with localStorage-based lastSyncedAt tracking
- Add generation-based cancellation for in-flight requests
- Reset highlights on logout
Closes #highlights-controller
- Create contactsController similar to bookmarkController
- Manage friends/contacts list in one place across the app
- Auto-load contacts on login, cache results per pubkey
- Stream partial contacts as they arrive
- Update App.tsx to subscribe to contacts controller
- Update Debug.tsx to use centralized contacts instead of fetching directly
- Reset contacts on logout
- Contacts won't reload unnecessarily (cached by pubkey)
- Debug 'Load Friends' button forces reload to show streaming behavior
- Add in-memory cache with 60s TTL for article/url/author queries
- Check cache before network fetch to reduce redundant queries
- Support force flag to bypass cache when needed
- Stream cached results through onHighlight callback for consistency
The loaders were initialized without extraRelays, so they had no relays
to fetch from. Added RELAYS config as extraRelays option for both loaders.
This ensures the loaders know where to query for events when hydrating
bookmarks in the background.
Removed excessive per-event logging from EventLoader and AddressLoader
subscriptions. Keep only essential logs:
- Initial hydration count
- Error logging
This reduces console noise while maintaining visibility into hydration
progress and errors.
Added extensive logging to track queryEvents lifecycle:
- Log when queryEvents is called
- Log each event as it's received via onEvent callback
- Log when batch completes with event count
- Log errors if batch fails
This will help identify where the hydration is hanging - whether:
- queryEvents never returns
- No events are received
- Some batches fail silently
No functional changes, only diagnostic logging.
Problem: With 400+ bookmarked events, trying to fetch all referenced events at once caused queryEvents to hang/timeout, making bookmarks appear to not load even though they were emitted.
Solution:
- Added MAX_IDS_TO_FETCH limit (100 IDs)
- Added MAX_COORDS_TO_FETCH limit (100 coordinates)
- If counts exceed limits, skip fetching and show bookmarks with IDs only
- Bookmarks still appear immediately with placeholder data (IDs)
- For smaller collections, metadata still loads in background
This fixes the hanging issue for users with large bookmark collections - all 532 bookmarks will now appear instantly in the sidebar (showing IDs), without waiting for potentially slow/hanging queryEvents calls.
Root cause: When decryption completed, we were only storing counts, not the actual decrypted bookmark items. When buildAndEmitBookmarks ran, it would try to decrypt again or skip encrypted events entirely.
Changes:
- Renamed decryptedEvents to decryptedResults and changed type to store actual IndividualBookmark arrays
- Store full decrypted results (publicItems, privateItems, metadata) when decryption completes
- In buildAndEmitBookmarks, separate unencrypted and decrypted events
- Process only unencrypted events with collectBookmarksFromEvents
- Merge in stored decrypted results for encrypted events
- Updated filter to check decryptedResults map for encrypted events
This fixes the missing Amethyst bookmarks issue - all 416 private items should now appear in the sidebar after decryption completes.
Changes:
- Emit bookmarks IMMEDIATELY with placeholders (IDs only)
- Fetch referenced events in background (non-blocking)
- Re-emit progressively as events load:
1. First emit: IDs only (instant)
2. Second emit: after fetching events by ID
3. Third emit: after fetching addressable events
This solves the hanging issue by:
- Never blocking the initial display
- Making all event fetching happen in background Promises
- Updating the UI progressively as metadata loads
Sidebar will show bookmarks instantly with IDs, then titles/content will populate as events arrive.
Root cause: queryEvents() hangs when fetching referenced events by ID
Temporary fix: Skip event fetching entirely, show bookmark items without full metadata
The logs showed:
- [bookmark] 🔧 Fetching events by ID...
- (never completes, hangs indefinitely)
This blocked buildAndEmitBookmarks from completing and emitting to the sidebar.
TODO: Investigate why queryEvents with { ids: [...] } doesn't complete/timeout