- Add relayListService to load kind:10002 (user relay list) and kind:10006 (blocked relays)
- Add relayManager to compute active relay set and dynamically manage pool membership
- Update App.tsx to fetch and apply user relays on login, reset on logout
- Replace all hardcoded RELAYS usages with dynamic getActiveRelayUrls() across services and components
- Always preserve localhost relays (ws://localhost:10547, ws://localhost:4869) regardless of user blocks
- Merge bunker relays, user relays, and hardcoded relays while excluding blocked relays
- Update keep-alive subscription and address loaders to use dynamic relay set
- Modified files: App.tsx, relayListService.ts (new), relayManager.ts (new), readsService.ts, readingProgressController.ts, archiveController.ts, libraryService.ts, reactionService.ts, writeService.ts, HighlightItem.tsx, ContentPanel.tsx, BookmarkList.tsx, Profile.tsx
- Move useEventModel hook call to top level (Rules of Hooks)
- Extract pubkey before calling the hook
- Profile resolution now works correctly for npub and nprofile mentions
- Fixes issue where profiles weren't being fetched and displayed
- Create RichContent component to handle ALL nostr URI types
- Support npub, nprofile, note, nevent, naddr with profile resolution
- Handle both 'nostr:npub1...' and plain 'npub1...' formats
- Replace all ContentWithResolvedProfiles usages in CardView, LargeView, and CompactView
- Now all bookmark content properly displays resolved nostr mentions
- Create NostrMentionLink component to fetch and display user names
- Replace truncated pubkey display with resolved profile names
- Fetch profiles in background non-blocking way using useEventModel
- Falls back to truncated pubkey if profile not available
- Import hasCreationDate utility function in Me.tsx
- Add UserSettings to MeProps interface
- Pass settings prop from Bookmarks to Me component
- Filter out bookmarks without creation dates when setting is enabled
- This ensures bookmarks showing 'Now' are hidden by default
- Create readsController service with background article fetching
- Implement progressive hydration pattern similar to bookmarkController
- Use AddressLoader for efficient batched article event retrieval
- Update Me.tsx to use readsController instead of direct readingProgressController
- Articles now show titles, summaries, images as data arrives from relays
- Fixes issue where reads showed 'Untitled' for all articles
- Keep event store integration for caching article events
- Maintain DRY principle by centralizing reads data fetching
Implemented event listener pattern in readingProgressController:
- Added onMarkedAsReadChanged() method for subscribers
- Added emitMarkedAsReadChanged() to notify when marked IDs update
- Call emitMarkedAsReadChanged() after loading reactions
In Me.tsx:
- Subscribe to onMarkedAsReadChanged() in new useEffect
- When fired, rebuild reads list with new marked-as-read items
- Include marked-only items (no progress event)
Now when reactions finish loading in background, /me/reads/completed
will update automatically with newly marked articles.
Added isLoading flag to block multiple start() calls from running in parallel.
The repeated start() calls were all waiting on queryEvents() calls,
creating a thundering herd that prevented any from completing.
Now only one start() runs at a time, and concurrent calls are skipped
with a console log.
- Reads (/me/reads/completed): fetch kind:7 📚 reactions and map #e -> 30023 naddr; include as completed reads
- Links (/me/links/completed): fetch kind:17 📚 reactions and use #r URL; include as completed links
- Keep progress-based items from readingProgressController, but explicitly add marked-only items per tab
This matches the debug page behavior and splits articles vs links cleanly.
If an article or URL is marked as read (📚) but has no reading
progress event yet, include it in the reads list so the 'completed'
filter surfaces it.
Uses readingProgressController.getMarkedAsReadIds() to synthesize
ReadItems for marked-only entries.
The eventReference can be either:
1. Raw event ID (hex string) - from event pointers
2. Coordinate string (kind:pubkey:identifier) - from address pointers
3. Already-encoded naddr - from some sources
Raw event IDs cannot be converted to nadrs without additional context
(we don't have the kind, pubkey, or identifier), so skip title fetching
for them to avoid bech32 decoding errors.
Fixes console errors:
- 'Invalid checksum in <hex>'
- 'Unknown letter: "b". Allowed: qpzry9x8gf2tvdw0s3jn54khce6mua7l'
These errors occurred when trying to decode raw hex event IDs as bech32.
Extended readingProgressController to also fetch and track mark-as-read
reactions (kind:7 and kind:17 with MARK_AS_READ_EMOJI) alongside reading
progress events.
Changes:
- Added markedAsReadIds Set to controller
- Query mark-as-read reactions in parallel with reading progress
- Added isMarkedAsRead() method to check if article is marked as read
- Updated Me.tsx to include markedAsRead status in ReadItems
Now /me/reads/completed shows:
- Articles with >= 95% reading progress
- Articles marked as read with the 📚 emoji
Removed the complex readsController wrapper. Now /me/reads simply:
1. Uses readingProgressController (already loaded in App.tsx)
2. Converts progress map to ReadItems
3. Subscribes to progress updates
This is much simpler and DRY - no need for a separate controller.
Reading progress is already deduped and managed centrally.
Same approach as debug page - just use the data source directly.
- Import readsController in App.tsx
- Start readsController in the central useEffect when user logs in
- Pass bookmarks to readsController.start() for article lookups
- Simplify Me.tsx loadReadsTab to just mark tab as loaded
- Subscription to readsController in Me.tsx still streams updates to UI
This means:
- Reads load in the background automatically
- Data is available even before clicking the Reads tab
- Consistent with how bookmarks, highlights, and writings are loaded
- Non-blocking - readsController streams updates progressively
The loadReadsTab async function was trying to return cleanup functions,
which doesn't work in React. Moved the subscription logic to a separate
useEffect hook with empty dependency array so:
- Subscriptions are set up once on mount
- Cleanup happens properly on unmount
- readsController updates flow through to UI correctly
This fixes the empty reads list issue.
- 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
Changed loadReadsTab to not await fetchAllReads. Instead:
- Start with empty state immediately
- Use onItem callback to stream updates as they're fetched
- Reading data flows in as it arrives (reading progress, marks as read, etc)
- UI doesn't block waiting for all article data to be fetched
Same pattern as debug page - provides responsive UI with progressive loading.
Changed loadReadsTab to use fetchAllReads directly instead of deriveReadsFromBookmarks.
Now /me/reads shows ALL articles with any reading activity:
- Articles with reading progress (kind:39802)
- Articles marked as read (kind:7, kind:17 reactions)
- Articles with highlights
- Bookmarked articles
Previously only showed bookmarked articles and tried to enrich with reading data.
Now the reading data (progress, marks as read) is the primary source.
Shows counts of articles in each reading progress category:
- Unopened (0%)
- Started (0% < progress ≤ 10%)
- Reading (10% < progress ≤ 94%) - highlighted in green
- Completed (≥ 95%)
This helps understand why /me/reads/reading shows fewer articles than
the total reading progress events - most articles fall into other categories.
- Load raw events from queryEvents for transparency
- Load deduplicated results from readingProgressController in parallel
- Display raw events first, then deduplicated results below for comparison
- Helps debugging by showing all events plus the final processed state
- Replace raw queryEvents with readingProgressController.start() for reading progress
- Controller already handles deduplication by article (d-tag) and keeps most recent
- Display deduplicated progress map below raw events for easy comparison
- Add progress percentage and visual progress bar for each article
- Add styling with blue background to distinguish deduplicated results
- Add state variables for reading progress events and mark-as-read reactions
- Implement handler to load all reading progress events (kind:39802) for logged-in user
- Implement handler to load all mark-as-read reactions (kind:7, kind:17) with MARK_AS_READ_EMOJI filter
- Add two new sections to debug page with buttons and results display
- Display event details including author, creation time, and relevant tags
- Include timing metrics for load operations
- Add left margin of 1.75rem to progress bar to start where text begins
- Prevents progress bar from looking like a separator
- Creates visual association between progress indicator and the specific bookmark item
- Add padding-left to progress bar container to offset it to title position
- Remove margin from inner fill
- Progress bar now visually starts where the title starts, not at the icon