- Move add bookmark button from web section header to filter bar
- Position button on the right side of filter bar
- Remove conditional rendering (always show button)
- Add bookmark-filters-wrapper styling for proper layout
- Add object-fit: contain to prevent image squishing
- Make max-height conditional: none when full-width enabled, 70vh otherwise
- Apply fix to both desktop and mobile image styles
- Change from --image-max-width CSS variable to --image-width
- When enabled, sets images to width: 100% (enlarging small images)
- Always constrains with max-width: 100% to prevent overflow
- Update mobile responsive styles to respect the setting
Removed all debug logging that was added for troubleshooting the
link processing issue. The functionality remains intact, including
the parser-based markdown link detection and HTTP URL protection.
Added check to detect if markdown has already been processed by looking
for our internal routes (/a/naddr1... or /p/npub1...) in markdown links.
If found, skip re-processing to prevent nested markdown link issues.
This addresses timing issues where markdown might be processed multiple
times, causing nostr URIs that were already converted to links to be
processed again, creating nested/duplicated markdown link structures.
Enhanced protection to also skip nostr URIs that are part of HTTP/HTTPS
URL patterns, not just markdown link URLs. This addresses timing issues
where the source markdown may contain plain URLs with nostr identifiers
before they're formatted as markdown links.
The detection checks if a nostr URI appears after 'https://' or 'http://'
and is part of a valid URL continuation to avoid false positives.
Added extensive debug logs to track:
- Input markdown preview and existing link count
- Each markdown link found with context and content
- Warnings when link URLs contain nostr URIs (should be protected)
- Detailed position information for each nostr URI match
- Whether matches are correctly identified as inside/outside link URLs
- Detection of nested markdown links in result (indicates bug)
This will help diagnose the timing issue where processing sometimes
works and sometimes doesn't.
Replace regex-based markdown link detection with a character-by-character
parser that correctly handles URLs containing brackets and parentheses.
The parser tracks parenthesis depth and escaped characters to correctly
find the end of markdown link URLs, even when they contain special
characters like brackets or nested parentheses.
This should fix the issue where nostr identifiers inside markdown link
URLs were still being processed, causing nested/duplicated markdown links.
Added debug logs prefixed with [nostrUriResolver] to track:
- When markdown processing starts
- All markdown links found and their URL ranges
- All nostr URI matches and their positions
- Whether each nostr URI is skipped or replaced
- Final processing results
This will help diagnose why nostr identifiers are still being
processed inside markdown link URLs.
Prevents nested markdown link issues when nostr identifiers appear in URLs.
The replaceNostrUrisInMarkdown functions now skip nostr URIs that are
already inside markdown link syntax [text](url) to avoid creating
malformed nested links.
Fix scope issue where cachedResponse wasn't accessible in catch block.
Now if fetch fails, we first check if we have a cached response and
return it. If no cache exists, we let the error propagate so the
browser can handle it gracefully.
Add proper error handling to prevent uncaught promise rejections when
image fetches fail. If a fetch fails, try to return cached response,
or gracefully handle the error instead of letting it propagate as an
uncaught promise rejection.
Remove image preloading from BlogPostCard and profileService to prevent
trying to fetch hundreds of images simultaneously. Images are already
lazy-loaded and will be cached by Service Worker when they come into view.
Only preload images when specifically needed (e.g., when loading an article
from cache, or the logged-in user's profile image in SidebarHeader).
This fixes thousands of ERR_INSUFFICIENT_RESOURCES errors when loading
the explore page with many blog posts.
Remove all console.log statements from Service Worker registration
and ReaderHeader image loading code, keeping only console.error and
console.warn for actual error handling.
Move useEffect hook before the conditional early return to satisfy
React's rules of hooks. All hooks must be called before any
conditional returns to prevent 'Rendered fewer hooks than expected'
errors.
Remove all console.log, console.warn, and console.error statements
that were added for debugging in article cache, service worker,
and image caching code.
Add a refresh button to the highlights panel header, positioned to the
left of the eye icon. The button refreshes highlights for the current
article and shows a spinning animation while loading.
Skip image preload in useArticleLoader when preview data is available,
since the image should already be cached from BlogPostCard. This prevents
unnecessary network requests when navigating from explore.
Preload article cover images when BlogPostCard is rendered to ensure
they're cached by Service Worker before navigating to the article.
This prevents re-fetching images that are already displayed in explore.
Add 500ms save suppression when article changes to prevent
accidentally saving 0% reading position during navigation.
This works together with existing safeguards (tracking disabled,
document height check, throttling) to ensure reading progress
is only saved during actual reading.
Reset scroll position to top immediately when articleIdentifier changes
to prevent showing wrong scroll position from previous article. Also
reset hasAttemptedRestoreRef when article changes to ensure proper
scroll restoration for new articles.
Remove all debug console.log statements that were added during
article loading and caching implementation, keeping only error
and warning logs for actual error handling.
- Move cache/EventStore checks before relayPool check in useArticleLoader
to fix race condition where articles wouldn't load on direct navigation
- Add relayPool to dependency array so effect re-runs when it becomes available
- Populate localStorage cache when articles are loaded in explore view
- Extract cacheArticleEvent() helper to eliminate code duplication
- Enhance saveToCache() with settings parameter and better error handling
With injectManifest strategy, the Service Worker needs to be built, so it's
not available in dev mode. To enable testing image caching in dev, we now:
1. Created public/sw-dev.js - a simplified SW that only handles image caching
2. Updated registration to use sw-dev.js in dev mode, sw.js in production
3. Dev SW uses simple cache-first strategy for images
This allows testing image caching in development without needing a build.
With devOptions.enabled: true, vite-plugin-pwa should serve the SW
in dev mode. Now we:
1. Attempt registration in both dev and prod
2. In dev mode, check if SW file exists and has correct MIME type first
3. Only register if file is actually available (not HTML fallback)
4. Handle errors gracefully with informative warnings
This allows testing image caching in dev mode when vite-plugin-pwa
is properly serving the Service Worker file.
With injectManifest strategy, the Service Worker file is only generated
during build, so it's not available in development mode. This causes
MIME type errors when trying to register a non-existent file.
Now we:
1. Only register Service Worker in production builds
2. Skip registration gracefully in dev mode with informative log
3. Image caching will work in production but not in dev (expected)
This eliminates the 'unsupported MIME type' errors in development.
1. Check for existing registrations first to avoid duplicate registrations
2. In dev mode, check if SW file exists before attempting registration
3. Handle registration errors gracefully - don't crash if SW unavailable in dev
4. Use getRegistrations() instead of getRegistration() for better coverage
5. Add more detailed error logging for debugging
This prevents the 'Failed to register ServiceWorker' errors when the
SW file isn't available in development mode.
Service Worker was only registered in production, but vite-plugin-pwa
has devOptions.enabled=true, so SW should work in dev too. Now we:
1. Register SW in both dev and prod modes
2. Use correct SW path for dev (/dev-sw.js?dev-sw) vs prod (/sw.js)
3. Add comprehensive debug logs for registration and activation
4. Log Service Worker state changes for debugging
Service Workers don't require PWA installation - they work in regular
browsers. This enables image caching in development mode.
Add debug logs prefixed with [image-preload], [image-cache], [sw-image-cache],
and [reader-header] to track:
- When images are preloaded
- Service Worker availability and controller status
- Image fetch success/failure
- Service Worker intercepting and caching image requests
- Image loading in ReaderHeader component
- Cache hits/misses in Service Worker
This will help debug why images aren't available offline.
When loading articles from localStorage cache, images aren't automatically
cached by the Service Worker because they're not fetched until the <img> tag
renders. If the user goes offline before that, images won't be available.
Now we:
1. Added preloadImage() function to explicitly fetch images via Image() and fetch()
2. Preload images when loading from localStorage cache
3. Preload images when receiving first event from relays
This ensures images are cached by Service Worker before going offline,
making them available on refresh when offline.
1. Fix cache name mismatch: imageCacheService now uses 'boris-images'
to match the Service Worker cache name
2. Remove cross-origin restriction: Cache ALL images, not just
cross-origin ones. This ensures article images from any source
are cached by the Service Worker
3. Update comments to clarify Service Worker caching behavior
Images should now be properly cached when loaded via <img> tags.
Simplify the finalization cache save - we already save on first event,
so only save in finalization if first event wasn't emitted. This
avoids TypeScript narrowing issues and duplicate cache saves.
Move cache save to happen immediately when first event is received
via onEvent callback, instead of waiting for queryEvents to complete.
This ensures articles are cached even if queryEvents hangs or never
resolves.
Also deduplicate cache saves - only save again in finalization if
it's a different/newer event than the first one.
We were loading articles from relays but never saving them to cache,
which meant every refresh would query relays again. Now we:
1. Save to cache immediately after successfully loading from relays
2. Export saveToCache function for reuse
3. Add debug logs to track cache saves
This ensures articles are cached after first load, enabling instant
loading on subsequent visits/refreshes.
Add detailed debug logs prefixed with [article-loader] and [article-cache]
to track:
- Cache checks (hit/miss/expired)
- EventStore checks
- Relay queries and event streaming
- UI state updates
- Request lifecycle and abort conditions
This will help debug why articles are still loading from relays on refresh.
Move localStorage cache check outside async function to execute
immediately before any loading state is set. This prevents loading
skeletons from appearing when cached content is available.
Previously, cache was checked inside async function, allowing a render
cycle where loading=true was shown before cache could load content.
Previously, articles always loaded from relays on browser refresh because:
- EventStore is in-memory only and doesn't persist
- localStorage cache was only checked as last resort after relay queries failed
Now we check the localStorage cache immediately after EventStore,
before querying relays. This allows instant article loading from cache
on refresh without unnecessary relay queries.
- Remove unused relayNames variable from HighlightItem.tsx
- Remove unused failedRelays variable from highlightCreationService.ts
- All linting and type checks now pass
- Add localStorage persistence for highlightMetadataCache Map
- Add localStorage persistence for offlineCreatedEvents Set
- Load both caches from localStorage on module initialization
- Save to localStorage whenever caches are updated
- Update metadata cache during sync operations (isSyncing changes)
- Ensures airplane icon displays correctly after page reloads
- Gracefully handles localStorage errors and corrupted data
- Remove debug logs from highlight creation, publishing, and UI rendering
- Keep only essential error logging
- Improves performance by reducing console spam
- Flight mode detection still works via fallback mechanisms
- Check isLocalOnly first before checking publishedRelays length
- Show airplane icon if isLocalOnly is true, even if publishedRelays is empty
- This ensures flight mode highlights show airplane icon via offline sync fallback
- Add debug logs to track cache storage and retrieval
- Fixes issue where airplane icon doesn't show when creating highlights offline
- Add isEventOfflineCreated and isLocalRelay to imports
- Remove require() calls that don't work in ES modules
- Fixes ReferenceError: require is not defined
- Add isEventOfflineCreated function to check offline sync service
- Use offline sync service as fallback if isLocalOnly is undefined
- Also check if publishedRelays only contains local relays
- This provides multiple fallback mechanisms to detect flight mode highlights
- Should finally fix the airplane icon not showing
- Create highlightMetadataCache to store isLocalOnly and publishedRelays
- Properties stored as __highlightProps are lost during EventStore serialization
- Cache persists across storage/retrieval cycles
- eventToHighlight now checks cache first before __highlightProps
- This should finally fix the airplane icon not showing for flight mode highlights
- Add debug log to see what highlight data is available when rendering
- Check if publishedRelays or seenOnRelays are being used
- This will help identify why tooltip shows all relays instead of just published ones
- Only publish to currently connected relays instead of all configured relays
- This prevents waiting for disconnected/offline relays to timeout
- Improves performance significantly in flight mode
- Handle case when no relays are connected
- Remove console.log that was spamming console with EVENT-TO-HIGHLIGHT logs
- This function is called frequently during renders, causing performance issues
- Keep the function logic but remove the logging
- Set publishedRelays, isLocalOnly, and isSyncing directly on highlight object
- This bypasses the __highlightProps mechanism which wasn't working
- Add logging to verify properties are set correctly
- This should finally fix the airplane icon not showing in flight mode
- Add [EVENT-TO-HIGHLIGHT] logs to see if __highlightProps are being preserved
- Add [HIGHLIGHT-CREATION] logs before calling eventToHighlight
- This will help identify why isLocalOnly and publishedRelays are undefined in final highlight
- Move eventStore.add() call to AFTER updating __highlightProps with final values
- This ensures highlights loaded from EventStore have correct isLocalOnly and publishedRelays
- Reduce UI logging spam by only logging when values are meaningful
- This should fix the airplane icon not showing and reduce excessive re-renders
- Log entire highlight object to see what properties are actually present
- Log publishedRelayCount and actualPublishedRelaysInUI for clarity
- Add conditionResult to show what icon should be displayed
- This will help identify why airplane icon isn't showing despite isLocalOnly being true
- Add [HIGHLIGHT-CREATION] logs to track highlight creation flow
- Log when createHighlight function is called
- Log highlight properties after creation to verify isLocalOnly is set
- This will help debug why [HIGHLIGHT-PUBLISH] logs are missing
- Attach custom properties to event as __highlightProps before storing
- Update eventToHighlight to preserve these properties when converting
- This ensures highlights loaded from EventStore maintain flight mode status
- Fixes issue where isLocalOnly was undefined in UI component
- The airplane icon should now show correctly for flight mode highlights
- Add detailed logs with [HIGHLIGHT-PUBLISH] prefix for publication process
- Add detailed logs with [HIGHLIGHT-UI] prefix for UI rendering
- Log relay responses, success/failure analysis, and flight mode reasoning
- Log icon decision making process in UI component
- This will help debug why airplane icon isn't showing in flight mode
- Use pool.publish() which returns individual relay responses
- Track which relays actually accepted the event (response.ok === true)
- Set isLocalOnly = true only when only local relays accepted the event
- This provides accurate flight mode detection based on actual publishing success
- Debug logging shows all relay responses for troubleshooting
- Always publish to all relays but track which ones are actually connected
- isLocalOnly = true when only local relays are connected (flight mode)
- Store event in EventStore first for immediate UI display
- Track actually connected relays instead of guessing based on connection status
- This should fix the airplane icon not showing in flight mode
- Move connection status check before publishEvent call
- Set isLocalOnly based on actual connection state at creation time
- This ensures airplane icon shows correctly in flight mode
- Previous logic was checking after publishing, which was too late
- Add console.log to debug why isLocalOnly is not being set correctly
- Fix logic: isLocalOnly should be true when only local relays are connected
- Previous logic was checking expectedSuccessRelays instead of actual connections
- This will help identify why airplane icon doesn't show in flight mode
- isLocalOnly is more accurate - covers both offline and online-but-local-only scenarios
- Update tooltip to 'Local relays only - will sync when remote relays available'
- Better semantic meaning: highlights only published to local relays
- Covers cases where user is online but only connected to local relays
- Remove isLocalOnly field from Highlight type and creation logic
- Use only isOfflineCreated flag for flight mode highlights
- Simplify UI logic to check only isOfflineCreated
- Both flags were set to the same value, making them redundant
- Cleaner, more maintainable code with single source of truth
- Remove eventStore from useArticleLoader and useExternalUrlLoader dependencies
- Remove setter functions from dependencies as they shouldn't change
- Only keep naddr/url and previewData/cachedUrlHighlights as dependencies
- This prevents content loaders from re-running when going offline
- Fixes the core issue where loading skeleton appears immediately on offline detection
- Move EventStore check before setReaderLoading(true) call
- Only show loading skeleton if content not found in store and no preview data
- This prevents loading skeleton from appearing when cached content is available
- Fixes the core issue where offline mode shows loading skeleton with cached content
- Remove relayPool from useEffect dependencies in useArticleLoader and useExternalUrlLoader
- This prevents content reloading when relay status changes (going offline/online)
- Content loaders now only re-run when the actual content identifier changes
- Fixes issue where loading skeleton appears when going offline with cached content
- Return early from useArticleLoader when content is found in EventStore
- This prevents loading skeleton from showing when going offline with cached content
- Improves offline experience by using locally cached article content
- Change 'document' case to 'article' to match valid UrlType
- Fix TypeScript compilation error for invalid UrlType comparison
- Maintain proper type safety while preserving icon functionality
- All linting and type checks now passing
- Add minHeight property to ReadingProgressBar container and inner div
- Ensure empty progress bar maintains same 3px height as filled progress bar
- Fix visual consistency between empty and filled reading progress states
- Maintain proper visual separator thickness in large card view
- Add conditional rendering for ReadingProgressBar in CompactView
- Only display progress bar when readingProgress > 0
- Remove empty progress bar separator from compact list view
- Maintain clean, minimal compact view without unnecessary visual elements
- Keep progress bar functionality for cards with actual reading progress
- Reduce ReadingProgressBar margins from 0.25rem to 0.125rem (75% reduction)
- Reduce bookmark footer padding-top from 0.25rem to 0.125rem (75% reduction)
- Reduce reading progress separator margin from 0.25rem to 0.125rem (75% reduction)
- Update responsive breakpoints for ultra-compact spacing
- Achieve minimal gap between progress bar and date/author footer
- Create ultra-tight vertical layout with almost no wasted space
- Reduce card content gap from 0.5rem to 0.25rem (50% reduction)
- Reduce bookmark title margin-bottom from 0.5rem to 0.25rem (50% reduction)
- Reduce bookmark footer padding-top from 0.5rem to 0.25rem (50% reduction)
- Reduce reading progress separator margin from 0.5rem to 0.25rem (50% reduction)
- Update ReadingProgressBar component margins to 0.25rem
- Update responsive breakpoints for consistent compact spacing
- Achieve much tighter vertical layout with minimal wasted space
- Create ReadingProgressBar component with configurable props
- Remove duplicated progress color logic from all view components
- Replace inline progress bar code with reusable component
- Maintain consistent behavior across CardView, CompactView, and LargeView
- Reduce code duplication and improve maintainability
- Update CompactView to always show progress bar for all bookmark types
- Update LargeView to always show progress bar for all bookmark types
- Remove conditional logic that only showed progress for articles
- Ensure consistent visual separator across CardView, CompactView, and LargeView
- Maintain empty state display (1px border line) when no progress available
- Remove isArticle condition from reading progress bar display
- Show progress bar for videos, links, and articles
- Maintain consistent visual separator for all bookmark types
- Ensure reading progress tracking works across all content types
- Reduce reading progress separator margin from 0.75rem to 0.5rem
- Reduce card content gap from 0.75rem to 0.5rem
- Reduce bookmark title margin-bottom from 0.75rem to 0.5rem
- Reduce bookmark footer padding-top from 0.75rem to 0.5rem
- Update responsive breakpoints for consistent compact spacing
- Make cards more compact and visually tighter
- Change progress bar background from transparent to border color when no progress
- Reading progress separator now always shows 1px line for articles
- Maintains visual consistency between articles with and without reading progress
- Ensures proper visual separation between content and footer
- Remove contentTypeIcon from CardViewProps interface
- Remove type icon display from bookmark footer
- Replace contentTypeIcon with faLink in thumbnail placeholder
- Simplify card interface by removing content type indicator
- Clean up unused icon-related code
- Remove expanded state and shouldTruncate logic
- Remove chevron icons and expand/collapse buttons
- Simplify content display to show full content without truncation
- Remove unused faChevronDown and faChevronUp imports
- Streamline card interface for cleaner, simpler design
- Change bookmark-type display to inline-flex for inline layout
- Add flex-wrap: nowrap to prevent wrapping
- Set timestamp elements to display: inline with white-space: nowrap
- Reduce gap between timestamp and icon to 0.5rem for tighter spacing
- Ensure both elements stay on the same horizontal line
- Set type icon color to var(--color-text-secondary) to match author text
- Icon now has same muted color as author link instead of bright white
- Creates better visual consistency in footer metadata
- Move timestamp from header to footer positioned next to type icon
- Create bookmark-footer-right container for timestamp and type icon
- Hide empty bookmark-header since timestamp is now in footer
- Update footer layout: author (left), timestamp + type icon (right)
- Maintain proper spacing and alignment for all elements
- Remove color: var(--color-primary) from global .bookmark-type rule
- Icon was still blue due to global CSS rule overriding footer-specific rule
- Now uses default text color for subtle appearance
- Remove var(--color-primary) color from bookmark type icon
- Use default text color for more subtle icon appearance
- Maintain font size and spacing for consistent layout
- Extract title from tags for all bookmark types, not just articles
- Display titles for regular bookmarks that have title tags
- Support both article titles and bookmark titles in card display
- Maintain existing article title functionality
- Improve title coverage across all bookmark types
- Remove type icon from header and move to footer
- Position author name on left, type icon on right in footer
- Update header to right-align date only
- Add flex layout to footer for proper spacing
- Maintain consistent styling and responsive design
- Add articleTitle prop to CardView component interface
- Display article titles for kind:30023 articles in card layout
- Style titles with proper typography and responsive design
- Position titles between header and URLs for optimal hierarchy
- Add line clamping for long titles (2 lines max)
- Update BookmarkItem to pass articleTitle to CardView
- Remove unused imageLoading and imageError state variables
- Clean up CardView component to pass ESLint checks
- Maintain all existing functionality while fixing linting issues
- Move bookmark type icon to top-left corner as overlay
- Add bookmark-type-overlay with absolute positioning
- Style icon with background, border, and shadow for visibility
- Update responsive design for smaller screens
- Remove icon from bookmark header to avoid duplication
- Ensure icon is always visible and accessible
- Move reading progress bar outside of text content area
- Position progress bar between content and author name
- Update CSS to remove card-content scoping for full-width display
- Maintain 1px thickness and smooth transitions
- Ensure progress bar spans entire card width for better visual separation
- Move thumbnail to be next to text content instead of blocking author position
- Create card-content-header with thumbnail + text-content flex layout
- Position author name in bottom-left corner of card footer
- Update responsive design for new layout structure
- Maintain thumbnail functionality while fixing author positioning
- Show reading progress bar for all article cards, even without progress
- Change progress bar thickness from 4px to 1px for subtle separation
- Remove fallback separator since progress bar is always shown
- Empty progress bars show as transparent fill with border background
- Maintain consistent visual separation across all article cards
- Remove separate border separator from bookmark footer
- Enhance reading progress bar styling as primary separator
- Add subtle separator for cards without reading progress
- Improve visual hierarchy with progress-based separation
- Maintain consistent spacing and visual flow
- Add card-view class for better visual hierarchy
- Implement hero image display with fallback placeholder
- Add responsive design for mobile and tablet screens
- Improve content truncation with line clamping
- Enhance URL display with better styling
- Add hover effects and smooth transitions
- Optimize card layout for better readability
- Rename fetch import to fetchOpenGraph to avoid global variable conflict
- Replace any types with proper Record<string, unknown> types
- Add proper type guards for OpenGraph data extraction
- Remove unused CACHE_TTL variable
- Fix TypeScript null assignment error in opengraphEnhancer
- All linting rules and type checks now pass
- Add debug logs to track OpenGraph data fetching
- Add debug logs to track description extraction logic
- Help identify why description field is not being populated
- Web bookmarks (kind:39701) store description in content field, not summary tag
- Update linksFromBookmarks.ts to check content field for web bookmarks
- Maintain backward compatibility with regular bookmarks using summary tag
- Fixes description display for web bookmarks in Links tab
- Add opengraphEnhancer service using fetch-opengraph library
- Enhance ReadItems with proper titles, descriptions, and cover images
- Update deriveLinksFromBookmarks to use async OpenGraph enhancement
- Add caching and batching to avoid overwhelming external services
- Improve bookmark card display with rich metadata from OpenGraph tags
- Add useDocumentTitle hook to manage document title dynamically
- Update useArticleLoader to set title when articles load
- Update useExternalUrlLoader to set title for external URLs/videos
- Update useEventLoader to set title for events
- Reset title to default when navigating away from content
- Browser title now shows article/video title instead of always 'Boris'
- Add detailed logging to track highlight loading process
- Implement fallback timeout mechanism to retry highlight loading after 2 seconds
- Add backup effect that triggers when article coordinate changes
- Ensure highlights are loaded reliably after article content is fully loaded
- Add console logging to help debug highlight loading issues
- Extract article coordinate from nostr: URLs using nip19.decode
- Filter highlights by eventReference matching the article coordinate
- Fix issue where unrelated highlights were showing in sidebar
- Apply same filtering logic to both useFilteredHighlights and filterHighlightsByUrl
- Preserve highlights that belong to the current article when switching articles
- Only clear highlights that don't match the current article coordinate or event ID
- Improve user experience by maintaining relevant highlights during navigation
- Add missing eventStore parameter to fetchHighlightsForArticle call
- Clear highlights immediately when starting to load new article
- Fix infinite loading spinners when articles have zero highlights
- Ensure highlights are properly stored and persisted
Remove duplicate ContentSkeleton components that were showing simultaneously.
Now uses a single skeleton for both loading and no-content states.
This follows DRY principles and prevents multiple skeletons from appearing
at the same time in the article view.
Replace the small spinner used for markdown content loading with a proper
ContentSkeleton for better visual consistency and user experience.
This ensures all content loading states use skeleton loaders instead of
spinners where appropriate.
Replace the confusing 'No readable content found for this URL' message that
appears during loading states with a skeleton loader for better UX.
This prevents users from seeing error messages while content is still loading.
Add coordination logic to ensure only one content loader (article/external/event)
runs at a time. This prevents state conflicts that caused 'No readable content found'
errors and stale content from previous articles appearing.
The existing instant-load + background-refresh flow is preserved.
- Move home button from right side to left side in sidebar header
- Add sidebar-header-left container for left-aligned elements
- Update CSS to support new layout with flex positioning
- Home button now appears next to profile button when logged in
- Extract first 100 characters of note content as video title
- Truncate with ellipsis if content is longer than 100 characters
- Fallback to YouTube metadata title or original title if no note content
- Improves user experience by showing meaningful titles for direct videos from Nostr notes
- Add noteContent prop to VideoView component for displaying note text
- Update VideoView to prioritize note content over metadata when available
- Detect direct video URLs from Nostr notes (nostr.build, nostr.video domains)
- Pass bookmark information through URL selection in bookmark components
- Show placeholder message for direct videos from Nostr notes
- Maintains backward compatibility with existing video metadata extraction
- Add YouTube thumbnail extraction using existing getYouTubeThumbnail utility
- Add Vimeo thumbnail support using vumbnail.com service
- Update VideoView to use video thumbnails as cover images in ReaderHeader
- Update Vimeo API to include thumbnail_url in response
- Fallback to original image prop if no video thumbnail available
- Supports both YouTube and Vimeo video thumbnails
- Add actual YouTube title and description fetching via web scraping
- Fix syntax error in video-meta.ts (missing opening brace)
- Complete Vimeo metadata implementation
- Both APIs now properly extract title and description from video pages
- Caption extraction remains functional for supported videos
- Create VideoView component with dedicated video player, metadata, and menu
- Remove video-specific logic from ContentPanel for better separation of concerns
- Update ThreePaneLayout to conditionally render VideoView vs ContentPanel
- Maintain all existing video features: YouTube metadata, transcripts, mark as watched
- Improve code organization and maintainability
- Add CSS media query to hide .tab-label on screens ≤768px
- Adjust tab padding and gap for mobile to show only icons
- Saves space on mobile for highlights, bookmarks, reads, links, writings tabs
- Affects Me, Profile, and Explore components
- Update HTML title and meta tags in index.html
- Update PWA manifest in public/manifest.webmanifest
- Update Vite PWA manifest in vite.config.ts
- Update article OG title fallback in api/article-og.ts
- Reflects the core functionality: reading, highlighting, and exploring content
- Add mobile sidebar close logic to handleMenuItemClick for profile menu items
- Add mobile sidebar close logic to Home, Settings, and Explore buttons
- Fixes issue where mobile bookmarks bar didn't close when navigating to My Reads, My Highlights, etc.
- Remove 'ver' field from ReadingProgressContent interface
- Remove ver: '1' from saveReadingPosition function
- Update PROGRESS_CACHE_KEY to remove v1 suffix
- Increase friends highlights limit from 200 to 1000
- Add 1000 limit to nostrverse highlights on initial load
- Ensures users see more highlights from friends and nostrverse
- Incremental syncs continue to use 'since' filter without limit
- Initialize highlightsMap with existing highlights on incremental sync
- Merge new highlights with existing ones instead of replacing entire list
- Keep existing highlights on error instead of clearing them
- Fixes issue where nostrverse highlights would disappear on page refresh
- Load explore scope visibility from localStorage on mount
- Save user's scope toggles to localStorage when changed
- Only reset to settings defaults if no saved preference exists
- Ensures user's scope selection persists across page refreshes
Web bookmarks store their URL in the 'd' tag, not in content.
The getBookmarkReadingProgress function was only extracting URLs from
content, which meant reading progress indicators weren't showing for
web bookmarks. Now it properly extracts URLs from the 'd' tag for
kind:39701 bookmarks.
Wrap relay.close() in try-catch to gracefully handle cases where WebSocket
connections are closed before they're fully established. This can occur when
relay sets change rapidly during app initialization (e.g., when loading user
relay lists).
Replaced aria-hidden with inert attribute on mobile sidebar and highlights panes. The inert attribute both hides from assistive technology AND prevents focus, eliminating the accessibility warning about focused elements being hidden.
- Add dedupeBookmarksById to flatten operation in Me.tsx
- Same article can appear in multiple bookmark lists/sets
- Use coordinate-based deduplication (kind:pubkey:identifier) for articles
- Prevents duplicate display when article is in multiple bookmark sources
- Extract article title from tags in BookmarkItem
- Update CompactView to display title as main text for articles
- Remove unused articleSummary prop from CompactView to keep code DRY
- Follows NIP-23 article metadata structure
- Add check to filter out nostr-event: URLs from reading position tracking
- nostr-event: is an internal sentinel, not a valid Nostr URI per NIP-21
- Prevents these sentinel URLs from being saved to reading position data
- Add special handling for nostr-event: URLs in getReadItemUrl
- Add special handling for nostr-event: URLs in handleSelectUrl
- Prevent nostr-event URLs from being incorrectly routed to /a/ path (which expects naddr)
- Route nostr-event URLs to /e/ path for proper event loading
- Fixes 'String must be lowercase or uppercase' error when loading base64-encoded nostr-event URLs
- Fix scroll position reset when toggling highlights panel on mobile
by using a ref to store the position and requestAnimationFrame
to restore it after the DOM updates
- Fix infinite loop in useReadingPosition caused by callbacks in
dependency array by storing them in refs instead
When opening/closing the highlights sidebar on mobile, the body gets
position:fixed to prevent background scrolling. This was causing the
scroll position to reset to the top.
Now we save the scroll position before locking, apply it as a negative
top value to maintain visual position, and restore it when unlocking.
- Update CompactView to navigate to /a/:naddr for kind:30023 articles
- Update BookmarkItem handleReadNow to navigate to /a/:naddr for articles
- Fixes issue where clicking bookmarked articles showed 'Select a bookmark' message
- Remove unused lastFetchTime parameter from BookmarkList
- Remove unused loading and onRefresh parameters from HighlightsPanelHeader
- Update HighlightsPanel to not pass removed props
- All linting and type checking now passing
Move the collapse highlights panel button from right to left side of the header, making it symmetrical to the bookmarks collapse button. Desktop only (hidden on mobile).
Move the grouped/chronological toggle button from right/center to the left side, positioned next to the orange heart support button in both BookmarkList and Me components for better UX consistency.
Add dropdown menu next to profile picture in bookmarks sidebar with:
- My Highlights
- My Bookmarks
- My Reads
- My Links
- My Writings
- Separator
- Logout
Includes click-outside-to-close functionality and smooth animations.
- Remove incremental loading (since filter) from highlightsController
- Fetch ALL highlights without limits for complete results
- Remove unused timestamp tracking methods and constant
- Ensures /my/highlights shows all highlights consistently
- Matches the fix applied to writingsController
- Remove incremental loading (since filter) from writingsController
- Fetch ALL writings without limits for complete results
- Remove duplicate background fetch from Me.tsx and Profile.tsx
- Use writingsController.start() in Profile to populate event store
- Keep code DRY by having single source of truth in controller
- Follows controller pattern: stream, dedupe, store, emit
Add background fetch effect to Me component to populate event store with
all writings without limits, matching the behavior of Profile component.
This ensures all writings are displayed on /my/writings page.
Updated handleLinkClick in PWASettings to check if URL is an internal
route (starts with /) and navigate directly, otherwise wrap external
URLs with /r/ path. This fixes the third relay education link to open
the nostr article correctly.
- Added autoScrollToReadingPosition setting (enabled by default)
- Users can now disable auto-scroll while keeping position sync enabled
- Setting appears in Layout & Behavior section of settings
- Auto-scroll only happens when both syncReadingPosition and
autoScrollToReadingPosition are enabled
The save timer was being cleared every time the effect unmounted (when
tracking toggled on/off), preventing saves from ever completing.
Now the save timer persists across tracking toggles and will fire even
if tracking is temporarily disabled. This fixes the core issue where
saves were scheduled but never executed.
Added logic to properly disable tracking when isTextContent becomes false.
This prevents the tracking state from flipping and ensures saves work
consistently.
Now tracking is only enabled once content is stable and stays enabled
until the article changes or content becomes unsuitable.
Fixed maximum update depth error by using refs for html/markdown content
instead of including them in useCallback dependencies. This prevents
handleSavePosition from being recreated on every content change, which
was causing scheduleSave to recreate, triggering infinite effect loops.
Now:
- handleSavePosition is stable across renders
- scheduleSave is stable
- Effect doesn't re-run infinitely
- Saves work properly with 3s throttle
Changes:
- Removed log spam during suppression (was logging on every scroll event)
- Reduced suppression time from 2000ms to 1500ms for smooth scroll
(500ms render delay + 1000ms smooth scroll animation)
The suppression still works but is now silent to avoid console spam.
After smooth scroll completes, saves will resume normally.
Removed unnecessary refs and logic that are no longer needed with
the simple 3s throttle:
- Removed lastSavedPosition (not used for any logic)
- Removed hasSavedOnce (not used)
- Removed lastSavedAtRef (not used)
- Removed saveNow() function (no longer needed after removing save-on-unmount)
- Simplified to just lastSaved100Ref to prevent duplicate 100% saves
The hook is now much simpler and easier to understand.
Simplified throttle logic to just save every 3 seconds during scrolling,
regardless of how much the position changed. This ensures all position
updates are captured reliably.
The 5% check was causing issues and unnecessary complexity. Now:
- First scroll schedules a save in 3s
- Continued scrolling updates pending position
- Timer fires and saves latest position
- Next scroll schedules another save in 3s
Simple and reliable.
Previous fix didn't work because after a save, the 5% check would
prevent scheduling a new timer during slow scrolling.
Changes:
- Always update pendingPositionRef (line 62)
- Schedule timer if significant change OR 3s has passed since last save
- Check 5% delta again when timer fires before actually saving
This ensures continuous slow scrolling triggers saves every 3s.
Changed from debounce (which resets timer on every scroll) to throttle
(which saves at regular 3s intervals). This ensures position is saved
during continuous slow scrolling.
Key changes:
- Don't reset timer if one is already pending
- Track latest position in pendingPositionRef
- Save the latest position when timer fires, not the position from when scheduled
This prevents the issue where slow continuous scrolling would never
trigger a save because the debounce timer kept resetting.
Pass highlightId and openHighlights in navigation state when clicking
highlights from the highlights list. This triggers the scroll behavior
in Bookmarks.tsx that was already implemented but not being used.
The useHighlightInteractions hook automatically scrolls to the selected
highlight once the article loads and the highlight mark is found in the DOM.
The root cause was scheduleSave being in the scroll effect's dependency array.
Even though scheduleSave had an empty dependency array, React still saw it as
a dependency and re-ran the effect constantly, causing unmount/remount loops
and triggering flush-on-unmount repeatedly.
Solution: Store scheduleSave in a ref (scheduleSaveRef) and call it via the ref
in the scroll handler. This removes scheduleSave from the effect dependencies
while still allowing the scroll handler to access the latest version.
This fixes the "Maximum update depth exceeded" error and stops the spam saves.
The issue was that scheduleSave and saveNow had syncEnabled/onSave in their
dependency arrays, causing them to be recreated when those props changed.
This triggered the scroll effect to unmount/remount repeatedly during smooth
scroll animations, flushing saves on each unmount.
Solution: Use refs (syncEnabledRef, onSaveRef) for all callback dependencies,
making scheduleSave and saveNow stable with empty dependency arrays. This
prevents effect re-runs and stops the save spam.
Now the scroll effect only runs once per article load, not on every render.
Previously, if user navigated away within the 3-second debounce window,
the pending save would be canceled and reading progress would be lost.
Now flushes any pending save on unmount if:
- There's a pending save timer active
- Position has changed by at least 5% since last save
- Not currently in suppression window (e.g., during restore)
This ensures reading progress is always saved even when navigating away
quickly, while still avoiding the 0% save issue from back navigation
(which doesn't trigger scroll events that would set up a pending save).
Uses refs to stabilize cleanup function and avoid effect re-runs.
Removed save-on-unmount behavior that was causing 0% position saves when
using mobile back gesture. The browser scrolls to top during navigation,
triggering a position update to 0% before unmount, which then gets saved.
The auto-save with 3-second debounce already captures position during
normal reading, so saving on unmount is unnecessary and error-prone.
Fixes issue where back gesture on mobile would overwrite reading progress.
Instead of fetching reading position from scratch using collectReadingPositionsOnce,
now uses the position already loaded by readingProgressController and displayed on cards.
Benefits:
- Faster restore (no network wait)
- Simpler code (no stabilization window needed)
- Data consistency (same data shown on card and used for restore)
- Reduced relay queries
Removed:
- isTrackingEnabled state and delays
- Complex composite keys
- Verbose debug logging
- isTrackingEnabledRef checks
Back to simple:
- isTextContent = basic check (loading, content exists, not video)
- Restore once per articleIdentifier
- Save on unmount
- Suppression during restore window
Much simpler, closer to original working version.
Split tracking enable logic into two effects:
1. Reset to false when article changes (selectedUrl)
2. Enable after 500ms if isTextContent is true AND not already enabled
Prevents isTextContent flipping from resetting the timer repeatedly, which was preventing isTrackingEnabled from ever becoming true.
Previously, if restore was skipped due to missing dependencies (content not loaded), it would never retry even after content loaded. Now resets the attempt tracker whenever articleIdentifier changes, allowing retry when dependencies become available.
- Check content length before enabling tracking (uses existing 1000 char minimum)
- Wait 500ms after content loads before enabling tracking (ensures stability)
- Prevents tracking on short notes and during page load transitions
- isTextContent now uses useMemo with comprehensive checks
saveNow() was bypassing suppression, causing 0% to overwrite saved positions during restore. Now checks suppressUntilRef before saving, just like the debounced auto-save.
- Suppress saves for 1700ms when restore starts (covers collection + render time)
- If no position found or delta too small, clear suppression immediately
- If restore happens, extend suppression for 1.5s after scroll
- Prevents 0% from overwriting saved 22% position during page load
Track whether we've already attempted restore for each article using a ref. Prevents the effect from restarting multiple times as html/markdown/loading state changes during initial page load, which was stopping the stabilization timer before it could complete.
Removed the check that prevented saving 0% positions. Now tracks when articles are opened, even if not read yet. Useful for engagement metrics and history.
- Use ref for suppressSavesFor to prevent restore effect from restarting on every position change
- Skip saving positions at 0% (meaningless start position)
- Restore effect now only restarts when article actually changes, not on every scroll
The unmount effect had saveNow in its dependency array. Since saveNow is a useCallback that depends on position, it was recreated on every scroll event, triggering the effect cleanup and calling saveNow() repeatedly (every ~14ms).
Now using a ref to store the latest saveNow callback, so the cleanup only runs when selectedUrl changes (i.e., when actually navigating away).
Replace complex interval logic with simple 2-second debounce. Every scroll event resets the timer, so saves only happen after 2s of no scrolling (or when reaching 100%). Much less aggressive than the previous 15s minimum interval.
Add detailed console logs to trace:
- Position collection and stabilization
- Save scheduling, suppression, and execution
- Restore calculations and decisions
- Scroll deltas and thresholds
Logs use [reading-position] prefix with emoji indicators for easy filtering and visual scanning.
Replace continuous restore with stabilized one-shot collector. Suppress saves for 1.5s after restore, skip tiny deltas (<48px or <5%), and use instant scroll (behavior: auto) to eliminate jumpy view behavior from conflicting relay updates.
Add collectReadingPositionsOnce() that buffers position updates for ~700ms, then emits the best one (newest timestamp, tie-break by highest progress). Prevents jumpy scrolling from conflicting relay updates.
Add suppressSavesFor(ms) API to temporarily block auto-saves after programmatic scrolls, preventing feedback loops where restore triggers save which triggers restore.
- Added third relay education article link in PWA settings
- Links to /a/naddr1qvzqqqr4gupzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqq9hyetvv9uj6um9w36hq9mgjg8
- Updated punctuation to use commas for better readability (here, here, and here)
- Changed timestamp links in CardView and LargeView to use internal routes
- Articles (kind:30023) open in /a/{naddr}
- Notes (kind:1) open in /e/{eventId}
- External URLs open in /r/{encodedUrl}
- Removed unused eventNevent prop and neventEncode import
- Timestamp now uses Link component for client-side navigation
- Refactored VideoEmbedProcessor to process HTML and extract URLs in single pass
- Previously processedHtml and videoUrls were computed separately, causing index mismatches
- Now both are computed together ensuring placeholders match collected URLs
- Added check to skip empty HTML parts to prevent rendering stray characters
- Web bookmarks (kind:39701) are replaceable events and should be deduplicated by d-tag
- Update dedupeNip51Events to include kind:39701 in d-tag deduplication logic
- Use coordinate format (kind:pubkey:d-tag) for web bookmark IDs instead of event IDs
- Ensures same URL bookmarked multiple times only appears once
- Keeps newest version when duplicates exist
- Add mobile media query to profile-avatar-button for consistent sizing
- Use --min-touch-target (44px) on mobile to match IconButton components
- Ensures consistent touch target size across all sidebar buttons
- Change explore icon from fa-newspaper to fa-person-hiking in SidebarHeader and Explore components
- Switch positions of settings and explore buttons in sidebar navigation
- Remove all console.log statements from bookmarkController and bookmarkProcessing
- Update CHANGELOG.md with v0.10.11 changes
Run all coordinate queries in parallel with Promise.all instead of
sequential awaits. This prevents each query from triggering a rebuild
that causes another hydration cycle, which was creating infinite loops.
The issue was that awaiting each query sequentially would:
1. Fetch articles for author A
2. Call onProgress, rebuild bookmarks
3. Trigger new hydration because coordinates changed
4. Repeat indefinitely
Now all queries start at once and stream results as they arrive,
matching the original loader behavior.
Separate fetching of articles with empty vs non-empty d-tags to work
around relay filter handling issues. For empty d-tags, fetch all events
of that kind/author and filter client-side.
Replace EventLoader and AddressLoader with queryEvents for bookmark
hydration to properly prioritize local relays. The applesauce loaders
were not using local-first fetching strategy, causing bookmarked events
to not be hydrated from local relay cache.
- Remove createEventLoader and createAddressLoader usage
- Replace with queryEvents which handles local-first fetching
- Properly streams events from local relays before remote relays
- Follows the controller pattern used by other services (writings, etc)
This fixes the issue where bookmarks would only show event IDs instead
of full content, while blog posts (kind:30023) worked correctly.
- Remove console.log from bookmark hydration
- Remove console.log from relay initialization
- Remove all console.debug calls from TTS hook and controls
- Remove debug logging from RouteDebug component
- Fix useCallback dependency warning in speak function
- 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
- Remove inline metadata HTML from note content
- Pass event.created_at as published timestamp via ReadableContent
- ReaderHeader now displays date in top-right corner
- Immediate fallback title using short pubkey
- Fetch kind:0 profile in background; update title when available
- Keeps UI responsive while improving attribution
- 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
- Enrich bookmarks with content from externalEventStore when hydration hasn't populated yet
- Keeps sidebar from showing only event IDs while background hydration continues
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- Clean up console output after diagnosing ID mismatch issue
- Keep error logging for when matches aren't found
- Deduplication before hydration now working
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- Removed 📝, 💧, 🎨 and 📊 debug logs
- These were added for troubleshooting but are no longer needed
- Kind:1 content hydration and rendering is working correctly
- 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
- 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
- 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
- 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
- Add eventStore parameter to fetchBlogPostsFromAuthors
- Store events as they stream in, not just at the end
- Update all callers to pass eventStore parameter
- This fixes issue where profile pages don't show all writings
- Add share_target to manifest.webmanifest with POST method
- Implement service worker handler for POST /share-target requests
- Create ShareTargetHandler component to process and save shared URLs
- Add /share-target route in App.tsx
- Auto-saves shared URLs as web bookmarks (NIP-B0)
- Handles Android case where url param is omitted from share data
- Set .reader-video to width: 100%, max-width: 100%, aspect-ratio: 16/9
- Remove negative margins and viewport-based sizing
- Add overflow: hidden to contain player within reader bounds
- Fixes video bleeding to the right on smaller screens
- Initialize settings with renderVideoLinksAsEmbeds: true
- Merge default when loading and watching settings events
- Ensures video links are embedded by default
- Replace entire <video>...</video> and <img> tags with placeholders
- Extract URLs in same order to align with placeholders
- Also replace bare file URLs and platform-classified video URLs
- Ensures no broken tags remain; uses ReactPlayer for rendering
- Changed approach to find ALL img tags first, then check if they contain video URLs
- Properly escapes regex special characters in img tags before replacement
- Fixes issue where img tags with video src attributes were not being replaced
- Handles edge cases like React-added attributes (node=[object Object])
- Now correctly converts markdown video images to embedded players
- Updated VideoEmbedProcessor regex patterns to use lookahead assertions
- This prevents capturing HTML attribute syntax like quotes and angle brackets
- Fixes text artifact appearing in UI when processing video URLs in HTML content
- Modified ContentPanel to disable VideoEmbedProcessor when isExternalVideo is true
- This prevents both ContentPanel and VideoEmbedProcessor from rendering ReactPlayer for the same video URL
- Fixes issue where video players were showing twice
- Set fullWidthImages default to true
- Set renderVideoLinksAsEmbeds default to true
- Users now get enhanced media experience out of the box
- Can still be disabled in settings if preferred
- Remove unused faExpand import from ReadingDisplaySettings
- Fix TypeScript type errors in VideoEmbedProcessor
- Add explicit string[] type annotations for regex match results
- All linting and type checking now passes
- Create new MediaDisplaySettings component for media-related settings
- Move full-width images and video embed settings from Reading & Display
- Add MediaDisplaySettings to main Settings component
- Improve settings organization and user experience
- Keep media settings logically grouped together
- Add renderVideoLinksAsEmbeds setting to UserSettings interface
- Add checkbox control in ReadingDisplaySettings component
- Create VideoEmbedProcessor component to handle video link embedding
- Integrate VideoEmbedProcessor into ContentPanel for article rendering
- Support .mp4, .webm, .ogg, .mov, .avi, .mkv, .m4v video formats
- Use ReactPlayer for embedded video playback
- Default to false (render as links)
- When enabled, video links are rendered as embedded players
- Add fullWidthImages setting to UserSettings interface
- Add checkbox control in ReadingDisplaySettings component
- Implement CSS custom property --image-max-width
- Set property in useSettings hook based on user preference
- Default to false (constrained width)
- When enabled, images use max-width: none for full-width display
- Remove blocking full-screen spinner on support page
- Show page content immediately with skeleton placeholders
- Load supporters data in background without blocking UI
- Fetch profiles asynchronously to avoid blocking
- Add SupporterSkeleton component with proper animations
- Significantly improve perceived loading performance
- Add sorting to Profile component's cachedWritings
- Sort by publication date (or created_at as fallback) with newest first
- Ensures consistent sorting across all writings displays
- Uses useMemo for performance optimization
- Update highlight to include the period: 'your own highlights.'
- Ensures complete phrase is highlighted for better visual consistency
- Maintains proper sentence structure in the highlighted text
- Add highlight styling to 'Connect your npub' text in login screen
- Now both 'Connect your npub' and 'your own highlights' are highlighted
- Uses same login-highlight class for consistent styling
- Improves visual emphasis on key action phrases
- Update CSS to center images in reader content
- Change margin from '0.75rem 0' to '0.75rem auto' for horizontal centering
- Applies to both HTML and Markdown content in article view
- Improves visual presentation of images in articles
- Change login-highlight text color from var(--color-text) to #000000
- Ensures proper contrast against bright yellow highlight background in dark mode
- Fixes readability issue where light gray text was hard to read on yellow background
- Added user relay list integration (NIP-65) and blocked relays (NIP-51)
- Improved relay list loading performance with streaming callbacks
- Enhanced relay URL handling and normalization
- Fixed all linting issues and TypeScript type safety
- Added relay list debug capabilities
- Cleaned up temporary test relays and debug output
- Import NostrEvent type from nostr-tools
- Replace any[] with NostrEvent[] for events array
- Replace Map<string, any> with Map<string, NostrEvent> for eventsMap
- Resolves ESLint warnings about explicit any usage
- loadUserRelayList accepts onUpdate callback to stream first user relay list
- App.tsx applies interim relay set on first event, keeps alive, then recomputes with blocked relays
- Keeps startup non-blocking and matches Debug page behavior
- Add onEvent streaming callback to relayListService queryEvents call
- Process events as they arrive instead of waiting for all relays to respond
- Deduplicate events by id and keep most recent version
- Remove artificial delay since streaming provides immediate results
- Should resolve hanging issue where debug works but app query hangs
- Remove temporary relay additions that were added for debugging
- Restore clean hardcoded relay list now that dynamic relay integration is working
- The non-blocking relay loading implementation handles user relay lists properly
- Start with hardcoded relays immediately when user logs in
- Load user relay list and blocked relays in background Promise
- Apply user relay preferences when they become available
- Remove blocking await that was preventing immediate relay setup
- Update keep-alive subscription and address loader when user relays load
- Continue with initial relay set if user relay loading fails
- Add state variables for relay list loading (isLoadingRelayList, relayListEvents, timing)
- Add handleLoadRelayList function to query kind 10002 events
- Add handleClearRelayList function to clear loaded data
- Add UI section with Load/Clear buttons and event display
- Show relay URLs and permissions for each relay list event
- Add loadRelayList to live timing type definition
Remove bunker-related debug logs and keep-alive subscription warnings.
Keep only relay-related logs ([relay-init] and [relayManager]) for debugging
relay loading and management.
When logged in:
- If user has relay list (kind:10002): use ONLY user relays + bunker + localhost
- If user has NO relay list: fall back to hardcoded RELAYS
This ensures the relay list changes when you log in based on your NIP-65 relay list.
Added debug logging to show user relay list, blocked relays, and final relay set.
applesauce-relay adds trailing slashes to relay URLs without paths,
but our RELAYS config doesn't include them. This caused applyRelaySetToPool
to think they were different URLs and remove all relays except the proxy.
Now we normalize URLs before comparison to match the pool's format.
- 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
- Replace useMountedState custom hook with inline useRef approach
- Set mountedRef.current = true at start of each effect run
- Ensures proper reset when navigating between articles
- Simpler and more reliable than custom hook approach
- isMounted is a stable function from useMountedState and shouldn't be in deps
- Including it was preventing effects from running correctly
- Fixes issue where articles wouldn't load (stuck on spinner)
- 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 useMountedState hook to track component mount status
- Refactor useArticleLoader to use shared hook
- Refactor useExternalUrlLoader to use shared hook
- Remove duplicated isMounted pattern across both loaders
- Cleaner, more DRY code with same functionality
- Add isMounted flag to track component lifecycle in useArticleLoader
- Add isMounted flag to track component lifecycle in useExternalUrlLoader
- Remove setter functions from useEffect dependencies to prevent re-triggers
- Add cleanup functions to cancel pending state updates on unmount
- Check isMounted before all state updates in async operations
- Fixes issue where spinner would spin forever when loading articles
- 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
- Bookmark list events (kind:10003, 30003, 30001) are containers, not content
- Add filter in hydrateItems to exclude these kinds after hydration
- Add debug logging to track which items are being filtered
- Prevents bookmark list events from showing as individual bookmarks in UI
- Log parentCreatedAt value when processApplesauceBookmarks is called
- Log each bookmark event with its kind and created_at timestamp
- Log count and timestamp for notes, articles, and URLs being processed
- Prefixed with [BOOKMARK_TS] for easy console filtering
- Add parentCreatedAt parameter to processApplesauceBookmarks function
- Replace all Math.floor(Date.now() / 1000) placeholders with parentCreatedAt || 0
- Update all call sites in bookmarkProcessing.ts to pass evt.created_at
- Individual bookmarks now inherit timestamp from their bookmark list event
- Bookmarks without valid parent timestamp will show as 0 (epoch) and be filtered by hideBookmarksWithoutCreationDate setting
- Eliminates 'now' placeholder timestamps in bookmark sidebar
- Enhanced hasCreationDate() to better detect unhydrated bookmark references
- Web bookmarks (kind 39701) always have real timestamps, always shown
- Filter out bookmarks with no content (failed hydration)
- Filter out URL-only bookmarks with minimal tags and synthetic IDs
- These are created during NIP-51 processing and show 'now' if not hydrated
- Fixes issue where placeholder timestamps would pass filter after time elapsed
- 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
Temporarily skip loading mark-as-read reactions to unblock the reads feature.
Focus on getting reading progress working first.
TODO: Debug why queryEvents hangs when querying kind:7 and kind:17 reactions.
The Promise never resolves even though we're not using timeouts.
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 comprehensive logging to see:
- When reactions queries start and complete
- How many kind:17 and kind:7 events are returned
- What reactions have MARK_AS_READ_EMOJI content
- Event ID to naddr mapping progress
- Final count of markedAsReadIds
This will help identify why markedAsReadIds is empty.
Non-blocking, background loading pattern:
- Subscribe to eventStore timeline immediately (returns right away)
- Mark as loaded immediately
- Fire-and-forget background queries for reading progress from relays
- Fire-and-forget background queries for mark-as-read reactions
- All updates stream via eventStore subscription
No timeouts. No blocking awaits. Updates arrive progressively as relays
respond, UI shows data as soon as eventStore delivers it.
Added logs at each step:
- Setting up timeline subscription
- Timeline subscription ready
- Querying reading progress events
- Got reading progress events count
- Generation changed abort
This will show exactly which step is blocking.
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 bug: start() was setting lastLoadedPubkey at the beginning, so if
start() got called twice (which it was), the second call would see
isLoadedFor(pubkey) return true and skip the entire loading process,
including fetching mark-as-read reactions.
Fix: Only set lastLoadedPubkey AFTER all fetching is complete. This
ensures that concurrent start() calls don't skip the loading.
This allows kind:7 and kind:17 mark-as-read reactions to be fetched
and tracked properly.
Added initial logs to show:
- When start() is called
- Whether already loaded (and skipped)
This helps confirm the controller is even being initialized.
Added:
- getMarkedAsReadIds() method to expose markedAsReadIds for debugging
- Final state logging showing all progressMap keys and markedAsReadIds
- Comprehensive logging throughout kind:7/kind:17 processing
This will help identify why markedAsRead articles aren't showing in /me/reads/completed.
Check console logs to see:
1. All progressMap entries (nadrs)
2. All markedAsReadIds entries
3. Step-by-step kind:7 and kind:17 processing
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.
Added detailed logging throughout the kind:7 and kind:17 reaction
processing to understand:
- What reactions are being fetched
- Which ones have MARK_AS_READ_EMOJI
- Event ID extraction
- Article lookups
- Event ID to naddr mapping
- Final markedAsReadIds set
Check browser console when loading /me/reads to see the full flow.
Restored kind:7 reaction handling with proper implementation:
1. Fetch kind:7 reactions with MARK_AS_READ_EMOJI
2. Extract event IDs from #e tags
3. Fetch the referenced articles (kind:30023)
4. Build mapping of event IDs to nadrs
5. Add marked articles to markedAsReadIds using their nadrs
Now both kind:7 (Nostr articles) and kind:17 (URLs) mark-as-read
reactions are properly tracked and will appear in /me/reads/completed.
Added nip19 import for naddr encoding.
Fixed several issues:
1. Clear markedAsReadIds on reset() so it doesn't persist across logouts
2. Skip kind:7 reactions (events) as they require complex event ID to naddr mapping
3. Only process kind:17 reactions (URLs) which directly use URLs as identifiers
4. Correctly extract URL from #r tag instead of using emoji content
Now kind:17 mark-as-read reactions for external URLs are properly tracked.
These articles will appear in /me/reads/completed.
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.
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.
- 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
- Move left offset from outer container padding to inner progress fill margin
- Background bar now spans full width while progress fill starts at text position
- Creates cleaner visual alignment without distorting the bar appearance
- Add explicit CSS rule to remove border from compact bookmarks in .bookmarks-list
- Override the border styling from me.css that was applying to all .individual-bookmark elements
- Ensure compact cards remain borderless and transparent
- Reduce padding from 0.5rem to 0.25rem vertically
- Reduce compact row height from 28px to 24px
- Reduce gap between compact cards from 0.5rem to 0.25rem
- Creates a tighter, more space-efficient list layout
- Add reading progress state and subscription to BookmarkList component
- Create helper function to get reading progress for both articles (using naddr) and web bookmarks (using URL)
- Update CompactView to display reading progress indicator for all bookmark types
- Progress indicator now shows for any bookmark with reading data, not just articles
- Add bookmarks to useEffect dependencies that load tab data
- Reads tab now updates when bookmarks are loaded/updated
- Fixes 'No articles ready yet' disappearing when switching tabs
- Ensures reads are always derived from current bookmark state
- Re-renders Reads tab whenever bookmarks change
- Enrich reads and links arrays with reading progress from readingProgressMap
- Use item.id to lookup progress for articles
- Use item.url to lookup progress for links
- Now 'started' and 'reading' filters show correct articles
- Filters respond in real-time as reading progress updates from controller
- Add readingProgressController.start() to App.tsx
- Follows same pattern as highlightsController and writingsController
- Checks isLoadedFor(pubkey) to prevent duplicate loading
- Automatically fetches reading progress when user logs in
- Loads progress from cache first, then streams from relays
- Reading progress now available immediately for filters and indicators
- Convert coordinate-format eventReferences (30023:pubkey:identifier) to naddr
- ReadItems use naddr format for IDs, but highlights store coordinates
- Properly match highlights to articles by normalizing both formats
- Fixes 'highlighted' filter showing no results
- Handles conversion errors gracefully by falling back to original format
- Move highlighted filter before completed in button order
- Reading filters now appear in logical order:
All → Unopened → Started → Reading → Highlighted → Completed
- Replace require() call with ES6 import for READING_PROGRESS constant
- Fixes linter error: 'require' is not defined (no-undef)
- All linter checks now pass with no warnings or errors
- Rename 'Amethyst Lists' to 'My Lists'
- Rename 'Amethyst Private' to 'Private Lists'
- Clearer and more intuitive names without referencing the Amethyst client
- Applied in both Me.tsx and BookmarkList.tsx
These sections contain kind:30001 bookmarks (replaceable list events).
- Add readingProgress prop to BookmarkItem component
- Display reading progress in CompactView with 2px indicator
- Display reading progress in CardView with 3px indicator
- Progress color matches main app: blue (reading), green (completed), neutral (started)
- Add getBookmarkReadingProgress helper in Me.tsx
- Show progress only for kind:30023 articles with progress > 0
- Reading progress now visible across all bookmark view modes
- Add 'highlighted' filter type to ReadingProgressFilterType
- New filter button with yellow highlighter icon
- Filter shows only articles that have highlights
- Highlights filter checks both eventReference and urlReference tags
- Color-coded: green for completed, yellow for highlighted, blue for others
- Applies to reads and links tabs in /me page
- Add MIN_CONTENT_LENGTH constant (1000 chars ≈ 150 words) to config/kinds
- Create shouldTrackReadingProgress helper to validate content length
- Strip HTML tags when calculating character count
- Only save reading progress for articles meeting the threshold
- Log when content is too short to track
This prevents noisy tracking of very short articles or excerpts.
- 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
- Add completionHoldMs (default 2000ms) to useReadingPosition
- Start hold timer when position hits 100%; cancel if user scrolls up
- Fallback to threshold completion when configured
- Clears timers on unmount/disable
- Treat undefined as enabled in ContentPanel (only false disables)
- Keeps DEFAULT_SETTINGS at true; ensures consistent behavior even for users without the new setting persisted yet
- Added autoMarkAsReadOnCompletion to default settings (disabled by default)
- Added toggle in Layout & Behavior section
- Existing ContentPanel logic already hooks into this to trigger animation & mark-as-read
- 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
- Changed syncReadingPosition default from false to true in Settings.tsx
- Users can still disable it in settings if they prefer
- This ensures reading progress tracking works out of the box
- Log when scheduleSave returns early (syncEnabled false, no onSave callback)
- Log when position is too low (<5%)
- Log when change is not significant enough (<1%)
- Log ContentPanel sync status (enabled, settings, requirements)
- This will help diagnose why no events are being created
- 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.reset() to handleLogout in App.tsx
- Ensures reading progress data is cleared when user logs out
- Consistent with other controllers (bookmarks, contacts, highlights)
- 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
- Add reading progress loading and display in Explore component
- Add reading progress loading and display in Profile component
- Add reading progress loading and display in Me writings tab
- Reading progress now shows as colored progress bar in all blog post cards
- Progress colors: gray (started 0-10%), blue (reading 10-95%), green (completed 95%+)
- 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 recommendation to clean URLs from tracking parameters
- Add URL Handling subsection with best practices
- Ensure same article from different sources maps to same progress
- Inspired by NIP-84 (Highlights) URL handling guidelines
- 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
- Create Profile.tsx for viewing other users (highlights + writings only)
- Profile uses useStoreTimeline for instant cache-first display
- Background fetches populate event store non-blocking
- Extract toBlogPostPreview helper for reuse
- Simplify Me.tsx to only handle own profile (/me routes)
- Remove isOwnProfile branching and cached data logic from Me
- Update Bookmarks.tsx to render Profile for /p/ routes
- Keep code DRY and files under 210 lines
- Add handlers for loading my writings, friends writings, and nostrverse writings
- Display writings with title, summary, author, and d-tag
- Show timing metrics (total load time and first event time)
- Use writingsController for own writings to test controller functionality
- Remove cachedHighlights, cachedWritings, myHighlights from useEffect deps
- These are derived from eventStore and caused infinite refetch loop
- Content is still seeded from cache but doesn't trigger re-fetches
- Allow exploring nostrverse writings and highlights without account
- Default to nostrverse visibility when logged out
- Update visibility settings when login state changes
- Add useStoreTimeline hook for reactive EventStore queries
- Add dedupe helpers for highlights and writings
- Explore: seed highlights and writings from store instantly
- Article sidebar: seed article-specific highlights from store
- External URLs: seed URL-specific highlights from store
- Profile pages: seed other-profile highlights and writings from store
- Remove debug logging
- All data loads from cache first, then updates with fresh data
- Follows DRY principles with single reusable hook
- Use eventStore.timeline() to query cached highlights
- Seed Explore page with cached highlights immediately
- Provides instant display of nostrverse highlights from store
- Fresh data still fetched in background and merged
- Follows applesauce pattern with useObservableMemo
- Add key prop based on activeTab to wrapper div
- Forces complete unmount/remount of content when switching tabs
- Prevents DOM element reuse that was causing blog posts to bleed into highlights tab
- Use consistent deduplication key (author:d-tag) for replaceable events
- Prevents duplicate blog posts when same article has multiple event IDs
- Streaming updates now properly replace older versions with newer ones
- Fixes issue where same blog post card appeared multiple times
- 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
- Pass eventStore to fetchHighlightsForArticle in useBookmarksData
- Pass eventStore to fetchHighlightsForUrl in useExternalUrlLoader
- All fetched highlights now persist in the centralized event store
- Enables offline access and consistent state management
- Subscribe to highlightsController for user's own highlights
- Subscribe to contactsController for followed pubkeys
- Merge controller highlights with article-specific highlights
- Remove duplicate fetching logic for contacts and own highlights
- Maintain article-specific highlight fetching for context-aware display
The subscription pattern only fires on *changes*, not initial state.
When Me component mounts, we need to immediately get the current
highlights from the controller, not wait for a change event.
Before:
- Subscribe to controller
- Wait for controller to emit (only happens on changes)
- Meanwhile, myHighlights stays []
After:
- Get initial state immediately: highlightsController.getHighlights()
- Then subscribe to future updates
- myHighlights is populated right away
This ensures highlights are always available when navigating to
/me/highlights, even if the controller hasn't emitted any new events.
The real issue: loadHighlightsTab was calling setHighlights(myHighlights)
before the controller subscription had populated myHighlights, resulting
in setting highlights to an empty array.
Solution: For own profile, let the sync effect handle setting highlights.
The controller subscription + sync effect is the single source of truth.
Only fetch highlights manually when viewing other users' profiles.
Flow for own profile:
1. Controller subscription populates myHighlights
2. Sync effect (useEffect) updates local highlights state
3. No manual setting needed in loadHighlightsTab
This ensures highlights are always synced from the controller, never
from a stale/empty initial value.
Fix issue where "No highlights yet" message would show briefly when
navigating to /me/highlights even when user has many highlights.
Root cause:
- Sync effect only ran when myHighlights.length > 0
- Local highlights state could be empty during navigation
- "No highlights yet" condition didn't check myHighlightsLoading
Changes:
- Remove length check from sync effect (always sync myHighlights)
- Add myHighlightsLoading check to "No highlights yet" condition
- Now shows skeleton or content, never false empty state
The controller always has the highlights loaded, so we should always
sync them to local state regardless of length.
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.
Remove background color from .highlight-level-toggles bar in /explore page.
The visibility filter buttons (nostrverse, friends, mine) now have no
background, making the UI cleaner.
Remove unnecessary prop drilling of myHighlights/myHighlightsLoading.
Components now subscribe directly to highlightsController (DRY principle).
Changes:
- Explore: Subscribe to controller directly, no props needed
- Me: Subscribe to controller directly, no props needed
- Bookmarks: Remove myHighlights props (no longer passes through)
- App: Remove highlights state, controller manages it internally
Benefits:
- ✅ Simpler code (no prop drilling through 3 layers)
- ✅ More DRY (single source of truth in controller)
- ✅ Consistent with applesauce patterns (like useActiveAccount)
- ✅ Less boilerplate (removed ~30 lines of prop passing)
- ✅ Controller encapsulates all state management
Pattern: Components import and subscribe to controller directly,
just like they use Hooks.useActiveAccount() or other applesauce hooks.
- Pass myHighlightsLoading state from controller through App → Bookmarks → Explore/Me
- Update Explore showSkeletons logic to include myHighlightsLoading
- Update Me showSkeletons logic to include myHighlightsLoading for own profile
- Sync myHighlights to Me component via useEffect for real-time updates
- Remove highlightsController import from Me (now uses props)
Benefits:
- Better UX with skeleton placeholders instead of empty/spinner states
- Consistent loading experience across Explore and Me pages
- Clear visual feedback when highlights are loading from controller
- Smooth transition from skeleton to actual content
- Pass myHighlights from controller through App.tsx → Bookmarks → Explore
- Merge controller highlights with friends/nostrverse highlights
- Seed Explore with myHighlights immediately (no re-fetch needed)
- Eliminate redundant fetching of user's own highlights
- Improve performance and consistency across the app
Benefits:
- User's highlights appear instantly in /explore (already loaded)
- No duplicate fetching of same data
- DRY principle - single source of truth for user highlights
- Better offline support (highlights from controller are in event store)
- 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
- Combine both auto-load effects into single useEffect
- Load bookmarks and contacts together when account is ready
- Keep code DRY - same pattern, same timing, same place
- Both use their respective controllers
- Both check loading state before triggering
- Comment out contacts state and subscriptions
- Comment out auto-load effect
- Allows manual testing of contact loading in Debug page
- Remember to re-enable after testing
- Remove redundant contact loading check
- Directly use contacts from centralized controller
- App.tsx already auto-loads contacts on login
- Clearer message indicating cached contacts are being used
- Faster execution since no contact loading needed
- Add local loading state for button (friendsButtonLoading)
- Clear friends list before loading to show streaming
- Set final result after controller completes
- Add error handling and logging
- Remove unused global friendsLoading subscription
- Button now properly shows loading state and results
- 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
- Start fetching highlights immediately when partial contacts arrive
- Track seen authors to avoid duplicate queries
- Fire-and-forget pattern for partial fetches (like bookmark loading)
- Only await final batch for remaining authors
- Highlights stream in progressively as contacts are discovered
- Matches the non-blocking pattern used in Explore.tsx and bookmark loading
- Use direct queryEvents with kind:9802 filter instead of service wrapper
- Add streaming with onEvent callback for immediate UI updates
- Track first event timing for performance analysis
- Remove unused fetchNostrverseHighlights import
- Add three quick-load buttons: Load My Highlights, Load Friends Highlights, Load Nostrverse Highlights
- Add Web of Trust section with Load Friends button to display followed npubs
- Stream highlights with dedupe and timing metrics
- Display friends count and scrollable list of npubs
- All buttons respect loading states and account requirements
- Show highlight button when readerContent exists (both nostr articles and external URLs)
- Hide highlight button when browsing app pages like explore, settings, etc.
- Ensures highlighting is available for all readable content but not for navigation pages
- Only display the floating highlight button when currentArticle exists or selectedUrl is a nostr article
- Prevents highlight button from showing on external URLs, videos, or other content types
- Improves UX by showing highlight functionality only where it's relevant
- Track and display time to first bookmark event arrival
- Mirror highlight loading metrics for consistency
- Shows how quickly local/fast relays respond
- Renamed 'load' stat to 'total' for clarity
- Clear first event timing on reset
- Author mode now defaults to current user's pubkey if not specified
- Changed default mode from 'article' to 'author' for better UX
- Updated placeholder to show logged-in user's pubkey
- Updated description to clarify default behavior
- Makes 'Load Highlights' button immediately useful without input
- Add query mode selector (Article/#a, URL/#r, Author)
- Stream highlight events as they arrive with onEvent callback
- Track timing metrics: total load time and time-to-first-event
- Display highlight summaries with content, tags, and metadata
- Support EOSE-based completion via queryEvents helper
- Mirror bookmark loading section UX for consistency
- 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
- Only show heart/support button when logged out
- Hide refresh, grouping, and view mode buttons when not logged in
- Cleaner, simpler footer for logged out state
- Add text-align: left to login-error
- Change align-items to flex-start for better multi-line text alignment
- Icon now aligns to top instead of center
- Show Amber and Aegis links when bunker URI format is invalid
- Consistent helpful messaging across all bunker errors
- Helps users even when they don't have the right format
- Show helpful message when bunker connection fails
- Suggest Amber (Android) and Aegis (iOS) signers with links
- Links: Amber GitHub and Aegis TestFlight
- Similar pattern to extension error message
- Add check for 'Signer extension missing' error
- Add case-insensitive check for 'extension missing'
- Ensure nos2x link is shown when no extension is found
- Add primary color and underline to links in error messages
- Increase font weight to 600 for better visibility
- Add hover state with color transition
- nos2x link now clearly stands out as clickable
- Show specific message when no extension is found
- Show message when authentication is cancelled/denied
- Display actual error message for other failures
- Remove generic 'Login failed' message
- Update error message to mention 'like nos2x'
- Add clickable link to nos2x Chrome Web Store
- Change error type to support React nodes for richer messages
- Import and use FontAwesomeIcon component from @fortawesome/react-fontawesome
- Add puzzle piece icon (faPuzzlePiece) for Extension button
- Add shield icon (faShieldHalved) for Bunker button
- Add info circle icon (faCircleInfo) for error messages
- Update CSS to properly style SVG icons with correct sizing
- Remove redundant login button from sidebar header
- Hide profile avatar when no active account
- Users can now only login through the main login screen
- Logout button only shown when logged in
- Clean up unused imports (useState, Accounts, faRightToBracket)
- Style 'your own highlights' text with user's mine highlight color
- Uses --highlight-color-mine CSS variable from settings
- Adds subtle padding and border-radius for clean highlight effect
- Add welcoming title 'Welcome to Boris'
- Update description to highlight key features (bookmarks, long-form articles, highlights)
- Change button labels to 'Login with Extension' and 'Login with Bunker'
- Add FontAwesome icons to login buttons
- Create dedicated login.css with modern, mobile-first styling
- Improve bunker input UI with better spacing and visual hierarchy
- Use softer error styling with amber/warning colors instead of harsh red
- Add smooth transitions and hover effects
- Ensure mobile-optimized touch targets
Added centralized auto-loading effect that handles all scenarios:
- User logs in (activeAccount becomes available)
- Page loads with existing session
- User logs out and back in (bookmarks cleared by reset)
Watches activeAccount and relayPool, triggers when both ready and no
bookmarks loaded yet. Handles all login methods (extension, bunker) via
single reactive effect.
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.
Fixed issue where 489 kind:30001 bookmarks were not appearing in groups
because they had setName: undefined instead of setName: 'bookmark'.
Changes:
- Removed setName === 'bookmark' requirement from amethystPublic/Private filters
- Now all kind:30001 bookmarks are grouped correctly regardless of setName
- Removed debug logging that was added to diagnose the issue
Before: Only 40 bookmarks shown (26 NIP-51 + 7 standalone + 7 web)
After: All 522 bookmarks shown (26 NIP-51 + 489 Amethyst + 7 web)
Added console log to show the distribution of setName values for all
kind:30001 bookmarks. This will help diagnose why 489 Amethyst bookmarks
aren't appearing in the amethystPublic/amethystPrivate groups.
Expected to see setName='bookmark' but need to verify what values are
actually present in the data.
Added console logs in groupIndividualBookmarks to show:
- Distribution of sourceKind values across all bookmarks
- Sample items with their sourceKind, isPrivate, setName, and id
- Count of items in each group after filtering
This will help identify why grouped view shows only ~40 bookmarks
while flat view shows 500+.
Updated documentation to explicitly state that:
- Amethyst bookmarks are stored in a SINGLE kind:30001 event with d-tag 'bookmark'
- This one event contains BOTH public (in tags) and private (in encrypted content) bookmarks
- When processed, it produces separate items with different isPrivate flags
- Example: 76 public + 416 private = 492 total bookmarks from one event
Added sections on:
- Event structure with d-tag requirement
- Processing flow showing how items are tagged
- UI grouping logic with setName check
- Why both public and private come from the same event
Removed temporary console.log statements added for debugging. The issue has been identified and fixed - bookmarks were being filtered out by hasContent() when they only had IDs.
Root cause: hasContent() was filtering out bookmarks that didn't have content text yet. When we skip event fetching for large collections (>100 IDs), bookmarks only have IDs as placeholders, causing 511/522 bookmarks to be filtered out.
Solution: Updated hasContent() to return true if bookmark has either:
- Valid content (original behavior)
- OR a valid ID (placeholder until events are fetched)
This allows all 522 bookmarks to appear in the sidebar immediately, showing IDs/URLs as placeholders until full event data loads.
Removed debug logging from bookmarkUtils as it served its purpose.
Added console logs to groupIndividualBookmarks to see:
- Total items being grouped
- Count per group (nip51Public, nip51Private, amethystPublic, amethystPrivate, standaloneWeb)
- Sample of first 3 items with their sourceKind and isPrivate properties
This will help diagnose why 532 bookmarks are emitted but not appearing in sidebar.
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:
- Updated groupIndividualBookmarks to group by source kind (10003, 30001, 39701) instead of content type
- Added toggle button in bookmark footer to switch between grouped and flat views
- Default mode is 'grouped by source' showing: My Bookmarks, Private Bookmarks, Amethyst Lists, Web Bookmarks
- Flat mode shows single 'All Bookmarks (X)' section sorted chronologically
- Preference persists to localStorage
- Implemented in both BookmarkList.tsx and Me.tsx
Files modified:
- src/utils/bookmarkUtils.tsx - New grouping logic
- src/components/BookmarkList.tsx - Added state, toggle button, conditional sections
- src/components/Me.tsx - Added state, toggle button, conditional sections
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
Added logging at every step of buildAndEmitBookmarks:
- After collectBookmarksFromEvents returns
- Before/after fetching events by ID
- Before/after fetching addressable events
- Before/after hydration and dedup
- Before/after enrichment and sorting
- Before creating final Bookmark object
This will show exactly where the process is hanging.
Changed all console logs to use [bookmark] prefix:
- Controller: all logs now use [bookmark] instead of [controller]
- App: all bookmark-related logs use [bookmark] instead of [app]
This allows filtering console with 'bookmark' to see only relevant logs for bookmark loading/debugging.
Added logging at each step:
- Before calling collectBookmarksFromEvents
- After collectBookmarksFromEvents returns
- Detailed error info if it fails (message + stack)
This will show us exactly where the silent failure is happening.
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
- Pass getDefaultBunkerPermissions() to connect() to ensure decrypt perms
- Keeps existing reconnection safeguards and logging
- Aims to make Amber accept decrypt requests after restore
- After opening subscription, call connect() once per session if remote is present
- Helps Amber authorize decrypt ops; safe-guarded and logged
- Keep isConnected=true for subsequent requireConnection() paths
- Bunker (NIP-46) signers don't reliably support async decrypt operations
- Skip attempting to decrypt private bookmarks when using bunker
- Users can still see all public bookmarks
- Use extension signer for access to encrypted private bookmarks
- Prevents 15+ second hangs waiting for decrypt responses that won't come
- Small 100ms delay after opening signer subscription
- Ensures the subscription is ready to receive decrypt responses
- May fix timeout issues with bunker decrypt operations
- Give bunker operations more time to respond
- Will help determine if this is a timing issue or a fundamental limitation
- Still logging timeout errors for visibility
- Previous commit had wrong message, code wasn't actually changed
- Now properly add relays to pool before creating NostrConnectSigner
- Ensures publishMethod/subscriptionMethod have full relay list available
- Bunker relays must be in pool when signer sets up publishMethod/subscriptionMethod
- Previously added after signer recreation, leaving pool incomplete
- This should fix decrypt operations that rely on publishMethod being set up correctly
- Same fix pattern as we used for signing
- Check if recreated NostrConnectSigner has methods needed for decrypt operations
- This will help identify if the issue is missing publishMethod for sending decrypt requests
- Or missing subscriptionMethod for receiving responses
- Log nip04/nip44 decrypt errors instead of silently ignoring
- Will help identify why bookmark decryption is timing out with bunker
- Timeout errors will now be visible in console
- Wrap nip04/nip44 decrypt calls with 5 second timeout
- Prevents UI from hanging if decrypt request doesn't receive response
- Allows graceful degradation instead of infinite wait
- With bunker, decrypt responses may not arrive if perms/relay issues
- EventFactory expects an EventSigner interface with signEvent method
- account.signer is the actual NostrConnectSigner instance
- Add debug logging to trace signer type
- This should fix signing hanging when using bunker
- Restored signers from JSON don't have pool context
- Recreate signer with pool passed explicitly to fix subscriptionMethod binding
- This ensures signing requests are properly sent/received through the pool
- Fixes hanging on signing after page reload
- Only add bunker relays that aren't already in the pool
- Prevents duplicate subscriptions that could cause signing hangs
- Improves stability when account is reconnected
- Remove reconnectBunkerSigner function, inline logic into App.tsx for better control
- Clean up try-catch wrapper in highlightCreationService, signing now works reliably
- Remove extra logging from signing process (already has [bunker] prefix logs)
- Simplify nostrConnect.ts to just export permissions helper
- Update api/article-og.ts to use local relay config instead of import
- All bunker signing tests now passing ✅
- Without this, requireConnection() tries to connect() again
- That breaks the entire signing flow
- Mark signer as connected after opening subscription
- The global decrypt queue in bookmarkProcessing was getting stuck
- Caused all NIP-46 operations to hang indefinitely
- Decrypt already has per-call timeouts; queue was unnecessary
- Highlights should now sign immediately without waiting for bookmarks
- Revert logging wrappers around subscription/publish
- Use pool.subscription.bind(pool) and pool.publish.bind(pool)
- Avoid any side effects interfering with signer requests
- Remove connect(undefined, permissions) on restore
- Let requireConnection() trigger connect per op
- Keeps highlights signing working as before while we debug decrypt
- Wrap subscriptionMethod/publishMethod to log relays, filters, responses
- Helps confirm decrypt/sign requests are actually sent and on which relays
- Continue using applesauce-recommended binding pattern
- Call signer.connect() instead of forcing isConnected
- Add [bunker] logs for connect lifecycle
- Should unblock nip44/nip04 decrypt calls that were timing out
- Wrap nip44/nip04 decrypt and unlockHiddenTags in timeouts
- Fallback nip44->nip04 if nip44 hangs/fails
- Add detailed [bunker] logs for each stage
- Keeps UI responsive while debugging bunker responses
- Remove bunkerFixVersion migration logic
- Simplify account loading to match applesauce examples
- Simplify reconnectBunkerSigner (no waiting, no complex logging)
- Direct nip04/nip44 exposure from signer (like ExtensionAccount)
- Clean up bookmark service account checking
- Keep debug logs for now until verified working
- Old bunker accounts were created before proper method binding
- Add version check to clear nostr-connect accounts once
- Preserves extension accounts
- Users will need to reconnect bunker (one-time migration)
- Was setting NostrConnectSigner.pool (wrong approach)
- Should set subscriptionMethod and publishMethod directly
- Follows the pattern from applesauce/packages/examples/src/examples/signers/bunker.tsx
- This is the correct way to wire up the signer with the relay pool
- Getters were returning new objects each time
- Code was getting reference then calling decrypt on it
- Now assign wrapped objects directly as properties
- This ensures our logging wrappers are actually used
- Log when decrypt/encrypt methods are called
- Log when they complete or fail
- Show pubkey and ciphertext/plaintext lengths
- This will tell us if decrypt is hanging in the signer or never returning
- Add detailed logging for signer subscription opening
- Enable debug logs for NostrConnectSigner via localStorage
- This will show if requests are being sent and responses received
- Helps diagnose why decrypt requests hang indefinitely
- Decryption was hanging because relay connections weren't established
- NostrConnectSigner sends requests via relays but pool wasn't connected
- Now wait for at least one bunker relay to be connected (5s timeout)
- Prevents decrypt/sign requests from being sent to unconnected relays
- Adds detailed logging for connection status
- NostrConnectSigner has nip04/nip44 but not exposed at account level
- ExtensionAccount exposes these via getters, NostrConnectAccount didn't
- Add properties dynamically during reconnection for compatibility
- Enables private bookmark decryption with bunker accounts
- NostrConnectSigner uses its own relay list for signing requests
- Pool must be connected to bunker relays to send/receive requests
- Add bunker relays to pool when reconnecting after page load
- This fixes signing hanging indefinitely
- NostrConnectAccount.fromJSON needs NostrConnectSigner.pool to be set
- Move pool creation and setup before accounts.fromJSON()
- This fixes 'Missing subscriptionMethod' error on page reload
- Now bunker accounts can be properly restored from localStorage
- Log raw accounts JSON from localStorage
- Log parsed account count and types
- Log active ID lookup and restoration steps
- This will help diagnose why accounts aren't persisting across refresh
- Move NostrConnectSigner.pool assignment before active account subscription
- Move pool.group(RELAYS) before subscription
- This ensures pool is ready when bunker signer tries to send requests
- The subscription can fire immediately, so pool must be configured first
- Add log to confirm pool assignment
- After page reload, signer is restored with isConnected=false
- When signing, requireConnection() would call connect() again without permissions
- Now we set isConnected=true after open() to prevent re-connection
- The bunker remembers permissions from initial connection
- This ensures signing works after page refresh
- fromBunkerURI() already calls connect() with permissions during login
- Calling connect() again breaks the connection state
- Just call open() to ensure subscription is active
- This matches the pattern in applesauce examples which don't reconnect
- Log final signer status including relays for debugging
- Track reconnected accounts to avoid double-connecting
- Log signer status after open() and connect() to verify state
- This should prevent the double reconnection issue
- Will help diagnose if connection is being lost immediately
- Add detailed logs for active account changes and bunker detection
- Log signer status (listening, isConnected, hasRemote)
- Log each step of reconnection process
- Add signing attempt logs in highlightCreationService
- This will help diagnose where the signing process hangs
- Import Accounts from 'applesauce-accounts' instead of 'applesauce-accounts/accounts'
- Fixes TypeScript error TS2305
- All linter and type checks now pass
- Create centralized getDefaultBunkerPermissions() in nostrConnect service
- Update LoginOptions to use centralized permissions
- Add bunker reconnection logic in App.tsx on active account change
- Reconnect bunker signer with open() and connect() when restored from localStorage
- Surface permission errors to users via toast in useHighlightCreation
- Ensures highlights, reactions, settings, and bookmarks work after page reload with bunker
- Add explicit signing permissions for event kinds: 5, 7, 17, 9802, 30078, 39701, 0
- Add encryption/decryption permissions: nip04_encrypt/decrypt, nip44_encrypt/decrypt
- Improve error messages when bunker permissions are missing or denied
- Add debug logging hint for bunker permission issues in write service
- This ensures highlights, reactions, settings, reading positions, and web bookmarks all work with bunker
- Wire NostrConnectSigner to RelayPool in App.tsx
- Create LoginOptions component with Extension and Bunker login flows
- Show LoginOptions in BookmarkList when user is logged out
- Add applesauce-accounts and applesauce-signers to vite optimizeDeps
- Support NIP-46 bunker:// URI authentication alongside extension login
Use named parameter syntax in Vercel rewrite and add <base href="/"> tag to ensure assets load correctly from root when serving index.html through the API.
Instead of redirecting, serve the static index.html file directly. The Vercel rewrite preserves the /a/{naddr} URL, allowing client-side SPA routing to work correctly.
Browsers get 302 redirect to / where the SPA handles routing client-side with the original /a/{naddr} URL preserved. Crawlers/bots get the full HTML with OG meta tags.
- Remove unused readingPositions and markedAsReadIds from useBookmarksData
- Remove eventStore parameter from useBookmarksData call
- Add reads and links fields to MeCache interface
Resolved conflicts by keeping feature branch changes:
- Kept /me/reads and /me/links routes (not /me/archive)
- Kept ReadingProgressFilters component and readingProgressUtils
- Kept readsService, linksService, and readingDataProcessor
- Restored files that were renamed/deleted in master
- Remove unused state variables (readsMap, linksMap) by using only setters
- Move VALID_FILTERS constant outside component to fix exhaustive-deps warning
- Remove unused isReading variable in ReadingProgressIndicator
- Remove unused extractUrlFromBookmark function and IndividualBookmark import
- Fix type errors in linksFromBookmarks by extracting metadata from tags instead of non-existent properties
- Replace closure over tempMap with setState callback pattern
- Ensures we always work with latest state when merging progress
- Prevents stale closure issues that block state updates
- Apply same fix to both reads and links tabs
- Fixes reading progress not updating in UI
- Convert bookmark coordinates to naddr format in deriveReadsFromBookmarks
- Reading positions store progress with naddr as ID
- Using naddr format enables proper merging of reading progress data
- Simplify getReadItemUrl to use item.id directly (already naddr)
- Fixes reading progress not showing in /me/reads tab
- Extract metadata from tags same way BookmarkItem does (DRY)
- Add image tag extraction for article images
- Add summary tag extraction for article summaries
- Add published_at tag extraction for publish dates
- Images and summaries now display in /me/reads tab
- IndividualBookmark doesn't have separate title/event fields
- After hydration, article titles are stored in content field
- Simplified extraction logic to just use bookmark.content
- Use getArticleTitle, getArticleSummary, getArticleImage, getArticlePublished from Helpers
- Extract metadata from bookmark.event when available
- Fallback to bookmark fields if event not hydrated
- Fixes 'Untitled' articles in Reads tab
- Add deriveReadsFromBookmarks helper to convert 30023 bookmarks to ReadItems
- Add deriveLinksFromBookmarks helper for web bookmarks (39701) and URLs
- Update loadReadsTab to show bookmarked articles immediately, enrich in background
- Update loadLinksTab to show bookmarked links immediately, enrich in background
- Background enrichment merges reading progress only for displayed items
- Preserve existing pull-to-refresh and empty state logic
- Create src/config/kinds.ts with named Nostr kind constants
- Add streaming support to fetchAllReads and fetchLinks with onItem callbacks
- Update all services to use KINDS constants instead of magic numbers
- Add mergeReadItem utility for DRY state management
- Add fallbackTitleFromUrl for external links without titles
- Relax validation to allow external items without titles
- Update Me.tsx to use streaming with Map-based state for reads/links
- Fix refresh to merge new data instead of clearing state
- Fix empty states for Reads and Links tabs (no more infinite skeletons)
- Services updated: readsService, linksService, libraryService, bookmarkService, exploreService, highlights/fetchByAuthor
Removed empty state messages like "No articles in your reads" and
"No links yet" - now just show loading skeletons until data arrives.
This is simpler and prevents showing empty states while data is still
being fetched in the background.
Users will only see:
- Skeletons when no data (loading or truly empty)
- "No articles/links match this filter" when filtered out
- Actual content when data is available
The bug was that showSkeletons checked if ANY tab had data, so if you
had highlights or bookmarks, it would never show skeletons for reads/links
even while they were still loading.
Fix: Each tab now checks its own loading state (loading && tabData.length === 0)
instead of using the shared showSkeletons variable.
This makes the logic simple and clear:
1. If loading AND no data → show skeletons
2. If not loading AND no data → show empty state
3. If has data but filtered out → show no match message
4. Otherwise → show content
- Create readingProgressUtils.ts with filterByReadingProgress function
- Create readingDataProcessor.ts with shared processing functions:
- processReadingPositions
- processMarkedAsRead
- filterValidItems
- sortByReadingActivity
- Refactor readsService.ts to use shared utilities
- Refactor linksService.ts to use shared utilities
- Eliminate 100+ lines of duplicated code
- Simplify Me.tsx filter logic to 2 lines
Benefits:
- Single source of truth for reading progress filtering
- Easier to maintain and modify
- Less code duplication across services
- More testable with isolated utility functions
- Reads: Only Nostr-native articles (kind:30023)
- Links: Only external URLs with reading progress
- Create linksService.ts for fetching external URL links
- Update readsService to filter only Nostr articles
- Add Links tab between Reads and Writings with same filtering
- Add /me/links route
- Update meCache to include links field
- Both tabs support reading progress filters
- Lazy loading for both tabs
This provides clear separation between native Nostr content and external web links.
- Exclude Nostr articles without event data (can't fetch title)
- Exclude external URLs without proper titles
- Prevents cluttering Reads with items that have no meaningful title
- Only shows items we can properly identify and display
- External URLs with 0% progress are now filtered out
- External URLs only appear if readingProgress > 0 OR marked as read
- Nostr articles still show even at 0% (bookmarked articles)
- Keeps Reads tab focused on actual reading activity for external links
- Replace spinner in highlights tab with 'No highlights yet' message
- Replace spinner in reading-list tab with 'No bookmarks yet' message
- Only show these messages when loading is complete and arrays are empty
- Remove unused faSpinner import
- Consistent with skeleton placeholder pattern used elsewhere
- Add loadedTabs state to track which tabs have been loaded
- Create tab-specific loading functions (loadHighlightsTab, loadWritingsTab, loadReadingListTab, loadReadsTab)
- Only load data for active tab on mount and tab switches
- Show cached data immediately, refresh in background when revisiting tabs
- Update pull-to-refresh to only reload the active tab
- Show loading skeletons only on first load of each tab
- Works for both /me (own profile) and /p/ (other profiles)
This reduces initial load time from 30+ seconds to 2-5 seconds by only fetching data for the active tab.
- Remove separate loadingReads state
- Keep single loading state true until ALL data is loaded
- Matches existing pattern used in other tabs
- Keeps code DRY and simple
- Exclude items without readingTimestamp or markedAt from reads
- Prevents 'Just Now' items from appearing in the reads list
- Only show reads with valid activity timestamps
- Add separate loadingReads state to track reads fetching
- Show skeletons during the entire reads loading period
- Set loading=false after public data (highlights/writings) completes
- Prevents showing 'No articles match this filter' while reads are being fetched
- Create new readsService to aggregate all read content from multiple sources
- Include bookmarked articles, reading progress tracked articles, and manually marked-as-read items
- Update Me component to use new reads service
- Update routes from /me/archive to /me/reads
- Update meCache to use ReadItem[] instead of BlogPostPreview[]
- Update filter logic to use actual reading progress data
- Support both Nostr-native articles and external URLs in reads
- Fetch and display article metadata from multiple sources
- Sort by most recent reading activity
- Match marked-as-read event IDs to bookmark coordinate IDs
- Use eventStore to lookup events and build coordinates from them
- Add both event ID and coordinate format to markedAsReadIds set
- This fixes filtering of bookmarked articles by reading progress
- Apply same fix to both Bookmarks and Explore components
- Icon spins 360° with bounce effect (scale up during spin)
- Button background changes to vibrant green gradient (#10b981)
- Green pulsing box-shadow effect on activation
- Button scales up slightly on click for emphasis
- Holds green state for 1.5 seconds
- Smoothly fades to gray after animation
- Final state is gray button to indicate marked status
- Uses cubic-bezier easing for modern, smooth feel
- Total animation duration: 2.5 seconds
- Prevents interaction during animation
- Update 'to-read' filter range from 0-5% to 0-10%
- Update 'reading' filter to start at 10% instead of 5%
- Adjust filter comments to reflect new ranges
- Fetch marked-as-read articles in useBookmarksData and Explore
- Pass markedAsReadIds through component chain (Bookmarks -> ThreePaneLayout -> BookmarkList)
- Display 100% progress for marked articles in all views (Archive, Bookmarks, Explore)
- Update filter logic to treat marked articles as completed
- Marked articles show green 100% progress bar
- Marked articles only appear in 'completed' or 'all' filters
- Remove reading position tracking from Me.tsx (not needed when all are marked)
- Clean up unused imports and variables
- Add autoMarkAsReadAt100 setting (default: false)
- Add checkbox in Layout & Behavior settings
- Automatically mark article as read after 2 seconds at 100% progress
- Trigger same animation as manual mark as read button
- Move isNostrArticle computation earlier for useCallback deps
- Move handleMarkAsRead to useCallback for use in auto-mark effect
- Remove 'marked' filter type from ReadingProgressFilterType
- Update ReadingProgressFilters component to show only 4 filters
- Keep checkmark icon for unified 'Completed' filter
- Completed filter now shows both:
- Articles with 95%+ reading progress
- Articles manually marked as read (no position data or 0%)
- Remove unused faBooks icon import
- Update filter logic in BookmarkList and Me components
- Changed spinner to empty state message only when not loading
- During refresh, keeps showing cached content or skeletons
- Archive: shows 'No articles in your archive' only when done loading
- Writings: shows 'No articles written yet' only when done loading
- Prevents jarring transition from skeletons to spinner during refresh
- More accurate naming: filters are based on reading progress/position
- Renamed component: ArchiveFilters -> ReadingProgressFilters
- Renamed type: ArchiveFilterType -> ReadingProgressFilterType
- Renamed variables: archiveFilter -> readingProgressFilter
- Renamed CSS class: archive-filters-wrapper -> reading-progress-filters-wrapper
- Updated all imports and references in BookmarkList and Me components
- Updated comments to reflect reading progress filtering
- Add reading position loading to Explore component
- Add reading position loading to useBookmarksData hook
- Display progress bars in Explore tab blog posts
- Display progress bars in Bookmarks large preview view
- Progress shown as colored bar (green for completed, orange for in-progress)
- Only shown for kind:30023 articles with saved reading positions
- Requires syncReadingPosition setting to be enabled
- Filter now shows articles with 0-5% reading progress
- Excludes manually marked as read articles (those without position data)
- Updates comment to reflect new logic
- Change 'All' icon to asterisk (*) to match Bookmarks filter
- Change 'Marked as Read' icon to faBooks (custom icon)
- Maintains consistent iconography across filter types
- Add 5px threshold to detect when scrolled to bottom
- Set position to exactly 1.0 (100%) when within 5px of bottom
- Remove upper limit on saving positions (now saves 100% completion)
- Always save when reaching 100% completion (important milestone)
- Don't restore position for completed articles (100%), start from top
- Better handling of edge cases in position detection
- Matches ReadingProgressIndicator calculation logic
- Use same CSS classes (filter-btn) as BookmarkFilters
- Show icons only, no text labels for consistency
- Add title and aria-label for accessibility
- Keep code DRY by following established pattern
- Create ArchiveFilters component with 5 filter options
- All: Show all archived articles
- To Read: Articles with 0% progress (not started)
- Reading: Articles with progress between 0-95%
- Completed: Articles with 95%+ reading progress
- Marked: Manually marked as read (no position data)
- Filter logic based on reading position data
- Show empty state when no articles match filter
- Matches BookmarkFilters styling and UX pattern
- Move progress indicator between summary and meta sections
- Replace the border-top dividing line with progress bar
- Show 3px progress bar when reading position exists
- Show 1px gray divider when no progress (maintains original look)
- Remove absolute positioning from bottom of card
- Remove border-top from meta section to avoid double lines
- Add detailed console logs with emoji prefixes for easy filtering
- Log save/load operations in readingPositionService
- Log position restore in ContentPanel with requirements check
- Log Archive tab position loading with article details
- All logs prefixed with component/service name for clarity
- Log shows position percentages, identifiers, and timestamps
- Helps debug why positions may not be showing or syncing
- Display reading position as a horizontal progress bar at bottom of blog post cards
- Use blue (#6366f1) for progress <95%, green (#10b981) for >=95% complete
- Load reading positions for all articles in Archive tab
- Progress bar fills from left to right showing how much has been read
- Only shown when reading progress exists and is >0%
- Smooth transition animations on progress updates
- HighlightItem now handles navigation internally
- Remove duplicate navigation logic from Explore component
- Simplifies code and ensures consistent behavior across all highlight displays
- Click on highlights in /me/highlights or /p/:npub pages to open referenced article
- Parse eventReference to detect kind:30023 articles and navigate to /a/{naddr}
- Fall back to urlReference for external URLs, navigate to /r/{url}
- Maintain backward compatibility with existing onHighlightClick prop
- Show pointer cursor when highlight has navigable reference
- Create readingPositionService.ts for save/load operations
- Add syncReadingPosition setting (opt-in via Settings > Layout & Behavior)
- Enhance useReadingPosition hook with auto-save (debounced 5s) and immediate save on navigation
- Integrate position restore in ContentPanel with smooth scroll to saved position
- Support both Nostr articles (naddr) and external URLs
- Reading positions stored privately to user's relays
- Auto-save excludes first 5% and last 5% of content to avoid noise
- Position automatically restored when returning to article
- Wrap button in sticky positioned container with height: 0
- Button now floats and stays visible while scrolling
- Remains within reader pane boundaries on desktop
- Uses flexbox to align button to the right side
- Move HighlightButton from fixed viewport positioning to absolute positioning within main pane
- Add position: relative to .pane.main for both desktop and mobile layouts
- Button now stays within the article/reader view instead of floating outside on desktop
- Maintains proper z-index and responsive behavior
- Remove all padding from filter buttons
- Reduce top padding from 0.75rem to 0.25rem
- Reduce bottom margin from 0.5rem to 0.25rem
- Much tighter, more compact layout
- Reduce padding on bookmark filters from 1rem to 0.5rem
- Reduce top padding of tab content when filters are present
- Tighten spacing for more compact layout
- Add filter buttons to reading-list tab in Me component
- Apply same filtering logic as main bookmarks sidebar
- Center-align filters and remove border for cleaner look
- Show empty state message when no bookmarks match filter
- Make buttons smaller (32px) and more compact
- Remove borders for cleaner look
- Active state uses primary color without background
- Match icon styling used on bookmark cards
- Add BookmarkFilters component with icon-based filter buttons
- Create bookmarkTypeClassifier utility for content type classification
- Filter bookmarks by article, video, note, or web types
- Apply filters across all bookmark lists (private, public, web, sets)
- Style filter buttons to match existing UI design
- Private bookmarks are now grouped in 'Private Bookmarks' section
- No need for redundant lock icon on each individual bookmark
- Cleaner UI with less visual clutter
- Removed faUserLock import and conditional rendering from all three views
- Only non-encrypted legacy bookmarks (kind:30001) now appear in Legacy section
- Encrypted legacy bookmarks are grouped with other private bookmarks
- Improves organization by grouping by privacy level rather than source
- Capitalize all bookmark section labels for consistency
- Change 'Old Bookmarks (Legacy)' to 'Legacy Bookmarks' for cleaner look
- Updated labels in both BookmarkList and Me components
The issue was that useBookmarksData was fetching general highlights
whenever there was no naddr, which included external URL routes (/r/*).
This caused the URL-specific highlights loaded by useExternalUrlLoader
to be overwritten after a couple seconds.
Now we skip fetching general highlights when viewing external URLs,
letting useExternalUrlLoader manage those highlights instead.
- Improve error handling in fetchHighlightsForUrl to prevent silent failures
- Remove redundant setHighlights call that was overwriting streamed highlights
- Add logging to help diagnose highlight fetching issues
- Isolate rebroadcast errors so they don't break highlight display
Replace popup modal with inline confirmation UI:
- When delete is clicked, show red trash icon with 'Confirm?' text
- Clicking red trash icon again confirms deletion
- Confirmation appears to left of three-dot menu
- Click outside or reopen menu cancels confirmation
- Remove ConfirmDialog component dependency
Final order:
1. Highlight Style
2. Paragraph Alignment
3. Default Highlight Visibility
4. Reading Font + Font Size
5. My Highlights color
6. Friends Highlights color
7. Nostrverse Highlights color
8. Show highlights checkbox
9. Preview
Add min-width: 220px to inline setting labels to create consistent
'tab stops' so buttons align vertically regardless of label length.
Remove constraint on mobile where settings stack vertically.
Final order:
1. Reading Font + Font Size
2. My Highlights color
3. Friends Highlights color
4. Nostrverse Highlights color
5. Highlight Style
6. Paragraph Alignment
7. Default Highlight Visibility
8. Show highlights checkbox
9. Preview
- Highlight Style (first)
- Paragraph Alignment (second)
- Reading Font + Font Size (third)
Better logical grouping with text styling before font selection
- Reduce verbose warning text to simple 'This will delete your highlight'
- Add proper CSS styling for confirm dialog with backdrop blur
- Center-aligned text and circular icon with color-coded background
- Modern button styling with proper hover states
- Full-width buttons in action row
- Theme-aware colors using CSS variables
- Add paragraphAlignment setting (left/justify) to UserSettings interface
- Add UI control with icon buttons in ReadingDisplaySettings
- Apply alignment via CSS variable to reader content and preview
- Default to left-aligned to maintain current behavior
- Keep headings always left-aligned for better readability
- Added matching padding to bookmark-section-action button
- Button now has same vertical padding as section title (1.5rem top, 0.75rem bottom)
- Also handles first section case with reduced padding (0.5rem top)
- Removed unnecessary marginBottom from flex container
- Removed add bookmark button from sidebar header
- Added small CompactButton style button next to 'Web bookmarks' heading
- Button only shows when user is logged in and web bookmarks section exists
- Moved bookmark creation logic from SidebarHeader to BookmarkList
- Cleaned up unused imports in SidebarHeader
Replaced 'No X yet. Pull to refresh!' messages with spinning loaders for:
- No highlights yet (Me & Explore)
- No bookmarks yet (Me)
- No read articles yet (Me)
- No articles written yet (Me)
- No blog posts yet (Explore)
This provides better UX by showing an active loading state instead of
static empty state messages.
- Changed idToEvent from let to const (prefer-const)
- Fixed TypeScript type narrowing issue by using direct regex test instead of isHexId type guard
- Removed unused isHexId import
All lint and type checks now pass for src directory.
The issue was that Primal bookmarks long-form articles using 'a' tags
(AddressPointer format: kind:pubkey:identifier) but our code was only
expecting EventPointer objects with 'id' properties.
Changes:
- Updated ApplesauceBookmarks interface to match actual applesauce types
- Added AddressPointer and EventPointer interfaces
- Rewrote processApplesauceBookmarks to handle all bookmark types:
* notes (EventPointer) - regular notes
* articles (AddressPointer) - long-form content (kind:30023)
* hashtags (string[])
* urls (string[])
- Updated bookmark hydration to query addressable events by coordinates
- Added logging to show hydration stats
This should fix the issue where Primal's Reads bookmarks weren't showing up.
Added logging to show:
- e and a tag counts for all events
- which events survived deduplication
- specific check for Primal reads list (kind:10003 with d='reads')
Previously, the dedupeNip51Events function was only looking for kind:10003
and kind:30001 when finding the default bookmark list. This excluded
kind:30003 events without a 'd' tag, which is what Primal uses for
bookmarks. Now kind:30003 is properly included in the filter.
- Remove text-shadow from CSS for .publish-date-topright
- Remove shadowColor from useAdaptiveTextColor hook
- Only apply adaptive text color, no shadows or backgrounds
- Cleaner appearance with color-based readability only
- Increase shadow opacity from 0.5 to 0.8 for better readability
- Revert semi-transparent background approach per user feedback
- Keep debugging logs to diagnose color detection
- Remove incorrect await on synchronous getColor method
- Add console logging to debug color detection
- This should fix black-on-black readability issues
- Install fast-average-color library for image color detection
- Create useAdaptiveTextColor hook to analyze top-right image corner
- Update ReaderHeader to dynamically adjust date text/shadow colors
- Ensures publication date is readable on both light and dark backgrounds
- Add setName, setTitle, setDescription, and setImage fields to IndividualBookmark type
- Extract d tag and metadata from kind 30003 events in bookmark processing
- Create helper functions to group bookmarks by set and extract set metadata
- Display bookmark sets as separate sections in BookmarkList UI
- Maintain existing content-type categorization alongside bookmark sets
- Bookmark button now visible at top (only hides on scroll down)
- Highlights button hides both at top AND on scroll down
- Separated visibility logic into showBookmarkButton and showHighlightsButton
- Relay status indicator follows bookmark button behavior
- Highlights button now hidden when at the very top of the page
- Tracks scroll position and hides button when scrollY <= 10px
- Bookmark button remains visible as always
- Highlights button appears when scrolling up from below
- Improves UX by reducing visual clutter at page top
- Blog post cards now show subtle gradient background when no image
- Reader view displays gradient placeholder with newspaper icon
- Large view bookmarks use gradient backgrounds
- Gradients use theme colors (--color-bg-elevated, --color-bg-subtle)
- Placeholder icons have reduced opacity for subtlety
- Adapts automatically to light/dark themes
- Profile pages now accept both npub and nprofile identifiers (NIP-19)
- Extract pubkey from nprofile.data.pubkey when decoding
- Maintains backward compatibility with existing npub links
- Users can now share profiles with relay metadata included
- Add section-title class to heading to match other settings sections
- Replace gradient button with standard zap-preset-btn styling
- Remove inline styles and JavaScript hover effects
- Consistent with app's design system and theme colors
- Bookmark button now visible on all pages except settings
- Only hides when scrolling down while reading an article
- Fixes issue where button was hidden on /p/ (profile) and other pages
- Highlights button only shows when viewing article content
- Prevents users from getting stuck without navigation options
- Wrapped toggleSidebar in useCallback to prevent function recreation on every render
- Updated route change effect to only close sidebar on actual pathname changes, not state changes
- Fixes issue where bookmarks sidebar wouldn't stay open on mobile PWA
- Add level-based colored borders to blog post cards (mine/friends/nostrverse)
- Updated BlogPostCard to accept and apply level prop
- Modified Explore.tsx to classify blog posts by relationship level
- Added CSS styling using settings colors for visual distinction
- Highlights already had this feature, now writings have it too
- Remove settings parameter from useImageCache and useCacheImageOnLoad hooks as it was never used
- Update all call sites in CardView, CompactView, LargeView, and ReaderHeader
- Remove settings prop from BookmarkItem and its child view components
- Remove settings prop from BookmarkList component
- Update ThreePaneLayout to not pass settings to BookmarkList
This change cascades through the component tree to clean up unused props that were
introduced when we refactored the image caching to use Service Worker instead of
local storage.
- Replace @typescript-eslint/no-explicit-any with proper Filter type from nostr-tools/filter in dataFetch.ts and helpers.ts
- Replace @typescript-eslint/no-explicit-any with IAccount and AccountManager types from applesauce-accounts in hooks
- Replace @typescript-eslint/no-explicit-any with unknown type casts in App.tsx for keep-alive subscription
- Fix react-hooks/exhaustive-deps warnings by including all dependencies in useEffect hooks
- Remove unused _settings parameters in useImageCache.ts that were causing no-unused-vars warnings
Instead of suppressing the warning, use functional setState updates to
check current state without creating dependencies. This allows the effect
to check if blogPosts/highlights are empty without adding them as
dependencies, which would cause infinite re-fetch loops.
The pattern prev.length === 0 ? cached : prev ensures we only seed from
cache on initial load, not on every refresh.
- Remove unused imports (useRef, faExclamationCircle, getProfileUrl, Observable, UserSettings)
- Remove unused error state and setError calls in Explore and Me components
- Remove unused 'events' variable from exploreService and nostrverseService
- Remove unused '_relays' parameter from saveSettings
- Remove unused '_settings' parameter from publishEvent
- Update all callers of publishEvent and saveSettings to match new signatures
- Add eslint-disable comment for intentional dependency omission in Explore
- Update BookmarkList to use new pull-to-refresh library and RefreshIndicator
- All type checks and linting now pass
- Replace old usePullToRefresh hook with use-pull-to-refresh library
- Update to use RefreshIndicator component
- Remove ref-based implementation in favor of simpler library API
- Remove full-screen error messages in Explore and Me
- Show skeletons while loading if no data cached
- Display empty states with 'Pull to refresh!' message
- Allow users to pull-to-refresh to retry on errors
- Keep content visible as data streams in progressively
- Reduced from w-12 h-12 md:w-16 md:h-16 to w-10 h-10 md:w-12 md:h-12
- Removed ring border for regular supporters (keep yellow ring for whales only)
- Simplified styling logic
- Lower supporter threshold from 2100 to 2 sats
- Lower whale threshold from 69420 to 21 sats
- Add TODO comment to restore production values
- For testing support page with smaller zaps
- Add exclamation mark to 'Thank You!' heading for warmth
- Simplify description text (remove redundant thank you)
- Rename 'Mega Supporters' to 'Absolute Legends' for more fun tone
- Add thank-you.svg illustration asset
- Use CSS variables for background, text, and border colors
- Add min-h-screen wrapper with proper background color
- Replace hardcoded zinc colors with theme-aware variables
- Ensure text is readable in both light and dark themes
- Stream via onEvent; dedupe replaceables; emit immediately.
- Parallel local/remote queries; complete on EOSE.
- Finalize and persist since after completion.
- Guard with generations to cancel stale runs.
- UI flips off loading on first streamed result.
We always include and prefer local relays for reads; optionally rebroadcast fetched content to local relays (depending on setting); and tolerate local‑only mode for writes (queueing for later).
Since we are streaming results, we should NEVER use timeouts for fetching data. We should always rely on EOSE.
In short: Local-first hydration, background network fetch, reactive updates, and replaceable lookups provide instant UI with eventual consistency. Use local relays as local data store for everything we fetch from remote relays.
@@ -3,4 +3,8 @@ description: anything related to UI/UX
alwaysApply: false
---
# Mobile-First UI/UX
This is a mobile-first application. All UI elements should be designed with that in mind. The application should work well on small screens, including older smartphones. The UX should be immaculate on mobile, even when in flight mode. (We use local caches and local relays, so that app works offline too.)
Let's not show too many error messages, and more importantly: let's not make them red. Nothing is ever this tragic.
- Client: Boris (web) using `applesauce` stack (`NostrConnectSigner`, `RelayPool`).
- Bunker: Amber (mobile).
- We restored a `nostr-connect` account from localStorage and re-wired the signer to the app `RelayPool` before use.
## What we changed client-side
- **Signer wiring**
- Bound `NostrConnectSigner.subscriptionMethod/publishMethod` to the app `RelayPool` at startup.
- After deserialization, recreated the signer with pool context and merged its relays with app `RELAYS` (includes local relays).
- Opened the signer subscription and performed a guarded `connect()` with default permissions including `nip04_encrypt/decrypt` and `nip44_encrypt/decrypt`.
- **Account queue disabling (CRITICAL)**
-`applesauce-accounts``BaseAccount` queues requests by default - each request waits for the previous one to complete before being sent.
- This caused batch decrypt operations to hang: first request would timeout waiting for user interaction, blocking all subsequent requests in the queue.
- **Solution**: Set `accounts.disableQueue = true` globally on the `AccountManager` in `App.tsx` during initialization. This applies to all accounts.
- Without this, Amber never sees decrypt requests because they're stuck in the account's internal queue.
- Switched to roundtrip probes: `encrypt(self, ... )` then `decrypt(self, cipher)` for both nip-44 and nip-04.
- Increased probe timeout from 3s → 10s; increased bookmark decrypt timeout from 15s → 30s.
- **Logging**
- Added logs for publish/subscribe and parsed the NIP-46 request content length.
- Confirmed NIP‑46 request events are kind `24133` with a single `p` tag (expected). The method is inside the encrypted content, so it prints as `method: undefined` (expected).
## Evidence from logs (client)
```
[bunker] ✅ Wired NostrConnectSigner to RelayPool publish/subscription
[bunker] 🔗 Signer relays merged with app RELAYS: (19) [...]
[bunker] subscribe via signer: { relays: [...], filters: [...] }
bookmarkProcessing.ts: ❌ nip44.decrypt failed: Decrypt timeout after 30000ms
bookmarkProcessing.ts: ❌ nip04.decrypt failed: Decrypt timeout after 30000ms
```
Notes:
- Final signer status shows `listening: true`, `isConnected: true`, and requests are published to 19 relays (includes Amber’s).
## Evidence from Amber (device)
- Activity screen shows multiple entries for: “Encrypt data using nip 4” and “Encrypt data using nip 44” with green checkmarks.
- No entries for “Decrypt data using nip 4” or “Decrypt data using nip 44”.
## Interpretation
- Transport and publish paths are working: Boris is publishing NIP‑46 requests (kind 24133) and Amber receives them (ENCRYPT activity visible).
- The persistent failure is specific to DECRYPT handling: Amber does not show any DECRYPT activity and Boris receives no decrypt responses within 10–30s windows.
- Client-side wiring is likely correct (subscription open, permissions requested, relays merged). The remaining issue appears provider-side in Amber’s NIP‑46 decrypt handling or permission gating.
## Repro steps (quick)
1) Revoke Boris in Amber.
2) Reconnect with a fresh bunker URI; approve signing and both encrypt/decrypt scopes for nip‑04 and nip‑44.
3) Keep Amber unlocked and foregrounded.
4) Reload Boris; observe:
- Logs showing `publish via signer` for kind 24133.
- In Amber, activity should include “Decrypt data using nip 4/44”.
If DECRYPT entries still don’t appear:
- This points to Amber’s NIP‑46 provider not executing/authorizing `nip04_decrypt`/`nip44_decrypt` methods, or not publishing responses.
## Suggestions for Amber-side debugging
- Verify permission gating allows `nip04_decrypt` and `nip44_decrypt` (not just encrypt).
- Confirm the provider recognizes NIP‑46 methods `nip04_decrypt` and `nip44_decrypt` in the decrypted payload and routes them to decrypt routines.
- Ensure the response event is published back to the same relays and correctly addressed to the client (`p` tag set and content encrypted back to client pubkey).
- Add activity logging for “Decrypt …” attempts and failures to surface denial/exception states.
## Performance improvements (post-debugging)
### Non-blocking publish wiring
- **Problem**: Awaiting `pool.publish()` completion blocks until all relay sends finish (can take 30s+ with timeouts).
- **Solution**: Wrapped `NostrConnectSigner.publishMethod` at app startup to fire-and-forget publish Observable/Promise; responses still arrive via signer subscription.
- **Result**: Encrypt/decrypt operations complete in <2sasseenin`/debug`page(NIP-44:~900msenc,~700msdec;NIP-04:~1senc,~2sdec).
This NIP defines kind `39802`, a parameterized replaceable event for tracking reading progress across articles and web content.
## Table of Contents
* [Format](#format)
* [Tags](#tags)
* [Content](#content)
* [Examples](#examples)
## Format
Reading progress events use NIP-33 parameterized replaceable semantics. The `d` tag serves as the unique identifier per author and target content.
### Tags
Events SHOULD tag the source of the reading progress, whether nostr-native or not. `a` tags should be used for nostr events and `r` tags for URLs.
When tagging a URL, clients generating these events SHOULD do a best effort of cleaning the URL from trackers or obvious non-useful information from the query string.
-`d` (required): Unique identifier for the target content
- For Nostr articles: `30023:<pubkey>:<identifier>` (matching the article's coordinate)
- For external URLs: `url:<base64url-encoded-url>`
-`a` (optional but recommended for Nostr articles): Article coordinate `30023:<pubkey>:<identifier>`
-`r` (optional but recommended for URLs): Raw URL of the external content
### Content
The content is a JSON object with the following fields:
-`progress` (required): Number between 0 and 1 representing reading progress (0 = not started, 1 = completed)
-`loc` (optional): Number representing a location marker (e.g., pixel scroll position, page number, etc.)
-`ts` (optional): Unix timestamp (seconds) when the progress was recorded
-`ver` (optional): Schema version string
The latest event by `created_at` per (`pubkey`, `d`) pair is authoritative (NIP-33 semantics).
Clients SHOULD implement rate limiting to avoid excessive relay traffic (debounce writes, only save significant changes).
d="m 29.707386,18.657657 c 0.366641,-2.450824 -1.499389,-3.768325 -4.05094,-4.647244 l 0.827682,-3.319949 -2.020864,-0.503633 -0.805812,3.232462 C 23.126191,13.286911 22.580541,13.16201 22.038338,13.038257 L 22.849909,9.7844991 20.830193,9.2808662 20.001937,12.599667 c -0.439747,-0.100152 -0.87143,-0.199148 -1.290455,-0.303328 l 0.0023,-0.0104 -2.786965,-0.695882 -0.537594,2.158431 c 0,0 1.49939,0.343622 1.467733,0.364918 0.818479,0.204332 0.966398,0.745954 0.941649,1.17534 l -0.942797,3.782139 c 0.05642,0.01436 0.129503,0.03508 0.210083,0.06736 -0.06736,-0.01669 -0.13929,-0.03516 -0.213537,-0.05293 l -1.321536,5.29823 c -0.100152,0.248646 -0.353983,0.621627 -0.926112,0.480032 0.02018,0.02934 -1.468882,-0.366648 -1.468882,-0.366648 l -1.003261,2.313258 2.629833,0.65558 c 0.489237,0.122604 0.968703,0.250959 1.44068,0.371832 l -0.83632,3.357938 2.018559,0.503633 0.828264,-3.322254 c 0.551408,0.14965 1.086697,0.287791 1.610477,0.417869 l -0.825385,3.306717 2.020864,0.503632 0.83632,-3.351612 c 3.446006,0.652134 6.037269,0.3891 7.127993,-2.727673 0.878911,-2.509534 -0.04377,-3.957121 -1.856825,-4.901074 1.320388,-0.304485 2.314988,-1.173035 2.580335,-2.967123 z m -4.61731,6.474711 c -0.624507,2.509534 -4.849846,1.152888 -6.219732,0.812719 l 1.109723,-4.448662 c 1.369878,0.341891 5.762717,1.018775 5.110009,3.635943 z m 0.62508,-6.510969 c -0.569824,2.28275 -4.086632,1.122955 -5.227429,0.838617 l 1.006118,-4.034821 c 1.140796,0.284338 4.814736,0.815024 4.221311,3.196204 z"
d="m 29.707386,18.657657 c 0.366641,-2.450824 -1.499389,-3.768325 -4.05094,-4.647244 l 0.827682,-3.319949 -2.020864,-0.503633 -0.805812,3.232462 C 23.126191,13.286911 22.580541,13.16201 22.038338,13.038257 l 0.811571,-3.253758 -2.019716,-0.503633 -0.828256,3.318801 c -0.439747,-0.100152 -0.87143,-0.199148 -1.290455,-0.303328 l 0.0023,-0.0104 -2.786965,-0.695882 -0.537594,2.158431 c 0,0 1.49939,0.343622 1.467733,0.364918 0.818479,0.204332 0.966398,0.745954 0.941649,1.17534 l -0.942797,3.782139 c 0.05642,0.01436 0.129503,0.03508 0.210083,0.06736 -0.06736,-0.01669 -0.13929,-0.03516 -0.213537,-0.05293 l -1.321536,5.29823 c -0.100152,0.248646 -0.353983,0.621627 -0.926112,0.480032 0.02018,0.02934 -1.468882,-0.366648 -1.468882,-0.366648 l -1.003261,2.313258 2.629833,0.65558 c 0.489237,0.122604 0.968703,0.250959 1.44068,0.371832 l -0.83632,3.357938 2.018559,0.503633 0.828264,-3.322254 c 0.551408,0.14965 1.086697,0.287791 1.610477,0.417869 l -0.825385,3.306717 2.020864,0.503632 0.83632,-3.351612 c 3.446006,0.652134 6.037269,0.3891 7.127993,-2.727673 0.878911,-2.509534 -0.04377,-3.957121 -1.856825,-4.901074 1.320388,-0.304485 2.314988,-1.173035 2.580335,-2.967123 z m -4.61731,6.474711 c -0.624507,2.509534 -4.849846,1.152888 -6.219732,0.812719 l 1.109723,-4.448662 c 1.369878,0.341891 5.762717,1.018775 5.110009,3.635943 z m 0.62508,-6.510969 c -0.569824,2.28275 -4.086632,1.122955 -5.227429,0.838617 l 1.006118,-4.034821 c 1.140796,0.284338 4.814736,0.815024 4.221311,3.196204 z"
d="m 29.707386,18.657657 c 0.366641,-2.450824 -1.499389,-3.768325 -4.05094,-4.647244 l 0.827682,-3.319949 -2.020864,-0.503633 -0.805812,3.232462 C 23.126191,13.286911 22.580541,13.16201 22.038338,13.038257 L 22.849909,9.784499 20.830193,9.2808661 20.001937,12.599667 c -0.439747,-0.100152 -0.87143,-0.199148 -1.290455,-0.303328 l 0.0023,-0.0104 -2.786965,-0.695882 -0.537594,2.158431 c 0,0 1.49939,0.343622 1.467733,0.364918 0.818479,0.204332 0.966398,0.745954 0.941649,1.17534 l -0.942797,3.782139 c 0.05642,0.01436 0.129503,0.03508 0.210083,0.06736 -0.06736,-0.01669 -0.13929,-0.03516 -0.213537,-0.05293 l -1.321536,5.29823 c -0.100152,0.248646 -0.353983,0.621627 -0.926112,0.480032 0.02018,0.02934 -1.468882,-0.366648 -1.468882,-0.366648 l -1.003261,2.313258 2.629833,0.65558 c 0.489237,0.122604 0.968703,0.250959 1.44068,0.371832 l -0.83632,3.357938 2.018559,0.503633 0.828264,-3.322254 c 0.551408,0.14965 1.086697,0.287791 1.610477,0.417869 l -0.825385,3.306717 2.020864,0.503632 0.83632,-3.351612 c 3.446006,0.652134 6.037269,0.3891 7.127993,-2.727673 0.878911,-2.509534 -0.04377,-3.957121 -1.856825,-4.901074 1.320388,-0.304485 2.314988,-1.173035 2.580335,-2.967123 z m -4.61731,6.474711 c -0.624507,2.509534 -4.849846,1.152888 -6.219732,0.812719 l 1.109723,-4.448662 c 1.369878,0.341891 5.762717,1.018775 5.110009,3.635943 z m 0.62508,-6.510969 c -0.569824,2.28275 -4.086632,1.122955 -5.227429,0.838617 l 1.006118,-4.034821 c 1.140796,0.284338 4.814736,0.815024 4.221311,3.196204 z"
d="m 29.707386,18.657657 c 0.366641,-2.450824 -1.499389,-3.768325 -4.05094,-4.647244 l 0.827682,-3.319949 -2.020864,-0.503633 -0.805812,3.232462 C 23.126191,13.286911 22.580541,13.16201 22.038338,13.038257 L 22.849909,9.784499 20.830193,9.2808661 20.001937,12.599667 c -0.439747,-0.100152 -0.87143,-0.199148 -1.290455,-0.303328 l 0.0023,-0.0104 -2.786965,-0.695882 -0.537594,2.158431 c 0,0 1.49939,0.343622 1.467733,0.364918 0.818479,0.204332 0.966398,0.745954 0.941649,1.17534 l -0.942797,3.782139 c 0.05642,0.01436 0.129503,0.03508 0.210083,0.06736 -0.06736,-0.01669 -0.13929,-0.03516 -0.213537,-0.05293 l -1.321536,5.29823 c -0.100152,0.248646 -0.353983,0.621627 -0.926112,0.480032 0.02018,0.02934 -1.468882,-0.366648 -1.468882,-0.366648 l -1.003261,2.313258 2.629833,0.65558 c 0.489237,0.122604 0.968703,0.250959 1.44068,0.371832 l -0.83632,3.357938 2.018559,0.503633 0.828264,-3.322254 c 0.551408,0.14965 1.086697,0.287791 1.610477,0.417869 l -0.825385,3.306717 2.020864,0.503632 0.83632,-3.351612 c 3.446006,0.652134 6.037269,0.3891 7.127993,-2.727673 0.878911,-2.509534 -0.04377,-3.957121 -1.856825,-4.901074 1.320388,-0.304485 2.314988,-1.173035 2.580335,-2.967123 z m -4.61731,6.474711 c -0.624507,2.509534 -4.849846,1.152888 -6.219732,0.812719 l 1.109723,-4.448662 c 1.369878,0.341891 5.762717,1.018775 5.110009,3.635943 z m 0.62508,-6.510969 c -0.569824,2.28275 -4.086632,1.122955 -5.227429,0.838617 l 1.006118,-4.034821 c 1.140796,0.284338 4.814736,0.815024 4.221311,3.196204 z"
d="m 29.707386,18.657657 c 0.366641,-2.450824 -1.499389,-3.768325 -4.05094,-4.647244 l 0.827682,-3.319949 -2.020864,-0.503633 -0.805812,3.232462 C 23.126191,13.286911 22.580541,13.16201 22.038338,13.038257 l 0.811571,-3.253758 -2.019716,-0.503633 -0.828256,3.318801 c -0.439747,-0.100152 -0.87143,-0.199148 -1.290455,-0.303328 l 0.0023,-0.0104 -2.786965,-0.695882 -0.537594,2.158431 c 0,0 1.49939,0.343622 1.467733,0.364918 0.818479,0.204332 0.966398,0.745954 0.941649,1.17534 l -0.942797,3.782139 c 0.05642,0.01436 0.129503,0.03508 0.210083,0.06736 -0.06736,-0.01669 -0.13929,-0.03516 -0.213537,-0.05293 l -1.321536,5.29823 c -0.100152,0.248646 -0.353983,0.621627 -0.926112,0.480032 0.02018,0.02934 -1.468882,-0.366648 -1.468882,-0.366648 l -1.003261,2.313258 2.629833,0.65558 c 0.489237,0.122604 0.968703,0.250959 1.44068,0.371832 l -0.83632,3.357938 2.018559,0.503633 0.828264,-3.322254 c 0.551408,0.14965 1.086697,0.287791 1.610477,0.417869 l -0.825385,3.306717 2.020864,0.503632 0.83632,-3.351612 c 3.446006,0.652134 6.037269,0.3891 7.127993,-2.727673 0.878911,-2.509534 -0.04377,-3.957121 -1.856825,-4.901074 1.320388,-0.304485 2.314988,-1.173035 2.580335,-2.967123 z m -4.61731,6.474711 c -0.624507,2.509534 -4.849846,1.152888 -6.219732,0.812719 l 1.109723,-4.448662 c 1.369878,0.341891 5.762717,1.018775 5.110009,3.635943 z m 0.62508,-6.510969 c -0.569824,2.28275 -4.086632,1.122955 -5.227429,0.838617 l 1.006118,-4.034821 c 1.140796,0.284338 4.814736,0.815024 4.221311,3.196204 z"
constEXAMPLE_TEXT="Boris aims to be a calm reader app with clean typography, beautiful design, and a focus on readability. Boris does not and will never have ads, trackers, paywalls, subscriptions, or any other distractions."
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.