- Wrap AuthorCard in profile-card-with-menu container
- Use CompactButton for ellipsis menu aligned with highlight cards
- Position menu button at bottom-right inside card and open menu upward
- Add menu button with options: Copy Link, Share, Open with njump, Open with Native App
- Position menu next to AuthorCard in profile header
- Add click-outside detection to close menu
- Style menu consistently with other menus in the app
- Prefer onHighlightClick for quote button, quote text, and menu item
- Fall back to navigation when no highlight-click handler is provided
- Ensures reliable scroll-to-quote behavior in the reader
- Add bottom padding to highlights-list to ensure menu has space
- Make menu open upward for last highlight item when space is limited
- Prevents three-dot menu from being clipped by container overflow
- Extract navigateToArticle helper function for reusability
- Make quote button navigate to article and scroll to highlight
- Make quote text (blockquote) clickable to navigate to article
- Add 'Go to quote' menu item in ellipsis menu
- All quote interactions now navigate to article with highlight scroll
- Make author name clickable to navigate to profile view
- Add 'View profile' option in highlight ellipsis menu
- Implement navigateToProfile helper with error handling
- Use existing /p/:npub routing infrastructure
- Add articleCoordinate and eventId to BlogPostCard navigation state
- Update useArticleLoader to check navigation state first before cache/EventStore
- Hydrate article content immediately from eventStore when coming from Explore
- Preserve existing cache/EventStore paths for deep links
- Add background query to check for newer replaceable versions without blocking UI
- Guard updates with requestId to prevent race conditions
This fixes the issue where articles opened from Explore would hang on loading
skeleton when queryEvents never completes. Now articles load instantly by reusing
the full event that Explore already fetched and cached.
- Extract updateKeepAlive and updateAddressLoader helpers in App.tsx for better code reuse
- Improve relay hint selection in HighlightItem with priority: published > seen > configured relays
- Add URL normalization for consistent relay comparison across services
- Unify relay set approach in articleService (hints + configured relays together)
- Improve relay deduplication in relayListService using normalized URLs
- Move normalizeRelayUrl to helpers.ts for shared use
- Update isContentRelay to use normalized URLs for comparison
- Use getFallbackContentRelays for HARDCODED_RELAYS in relayManager
- Create typed RelayRole and RelayConfig interface in relays.ts
- Add centralized RELAY_CONFIGS registry with role annotations
- Add helper getters: getLocalRelays(), getDefaultRelays(), getContentRelays(), getFallbackContentRelays()
- Maintain backwards compatibility with RELAYS and NON_CONTENT_RELAYS constants
- Refactor relayManager to use new registry helpers
- Harden applyRelaySetToPool with consistent normalization and local relay preservation
- Add RelaySetChangeSummary return type for debugging
- Improve articleService to prioritize and filter relay hints from naddr
- Use centralized fallback content relay helpers instead of hard-coded arrays
- Add NON_CONTENT_RELAYS list and isContentRelay helper to classify relays
- Update ContentPanel to filter out non-content relays (e.g., relay.nsec.app) from naddr hints
- Update HighlightItem to prefer publishedRelays/seenOnRelays and filter using isContentRelay
- Ensures relay.nsec.app and other auth/utility relays are never suggested as content hints
- Change OG HTML redirect to use ?_spa=1 query param instead of redirecting to /
- Simplify vercel.json rewrites: serve SPA when _spa=1, otherwise serve OG HTML
- Remove brittle user-agent detection patterns
- Add cleanup effect to strip _spa param from URL after SPA loads
- Fixes refresh redirect regression while maintaining OG preview support
- Add conditional rewrite rules in vercel.json to only serve OG HTML to crawlers
- Add ?og=1 query parameter override for manual testing
- Document ?og=1 testing path in README
- Fixes regression where browser refresh on /a/:naddr redirected to root
- Set --color-link directly in preview inline styles based on current theme
- Preview now shows the correct link color for the active theme
- Link color updates immediately when changed in settings
- Restore linkColorDark and linkColorLight settings
- Single color picker UI updates the appropriate theme's color based on current theme
- Dark theme color picker updates linkColorDark
- Light theme color picker updates linkColorLight
- Separate values applied to --color-link-dark and --color-link-light CSS variables
- Matches the pattern used for --color-primary
- Revert to single linkColor setting (removed linkColorDark/Light)
- Add theme-specific color palettes: LINK_COLORS_DARK and LINK_COLORS_LIGHT
- Color picker shows appropriate palette based on current theme
- Single link color value applied to both dark and light CSS variables
- Dark theme shows lighter colors (sky-400, cyan-400, etc.)
- Light theme shows darker colors (blue-500, indigo-500, etc.)
- Rename CSS variable from --link-color to --color-link
- Add linkColorDark and linkColorLight settings (replacing single linkColor)
- Add --color-link to dark and light theme CSS variables
- Use CSS var() references to automatically switch based on theme
- Update settings UI to show separate color pickers for dark and light themes
- Default: dark=#38bdf8 (sky-400), light=#3b82f6 (blue-500)
- Update all CSS references to use new variable name
- Add linkColor field to UserSettings interface
- Add LINK_COLORS palette with 6 link-appropriate colors
- Update ColorPicker to accept custom color arrays
- Add Link Color setting UI after Reading Font setting
- Apply link color as CSS variable in useSettings hook
- Update reader CSS to use --link-color variable instead of --color-primary
- Add link color preview in settings preview section
- Default to indigo-400 (#818cf8) for better visibility on dimmed displays
- Change primary color from indigo-500 to indigo-400 (#818cf8) in dark mode
- Improves readability on dimmed mobile displays
- Update both theme-dark and theme-system dark mode variants
Add comprehensive test suite for basic markdown syntax features:
- basic-headings.md: All heading levels and setext syntax
- basic-paragraphs-line-breaks.md: Paragraph separation and line breaks
- basic-emphasis.md: Bold and italic formatting
- basic-blockquotes.md: Blockquotes with nested content
- basic-lists.md: Ordered and unordered lists with nesting
- basic-code.md: Inline code and code blocks
- basic-horizontal-rules.md: Horizontal rule variants
- basic-links-and-images.md: Links and images with various syntax
- basic-escaping.md: Character escaping
- basic-index.md: Index of all test files
All files follow the Markdown Guide's Basic Syntax specification.
- Add fetchFirstEvent helper that resolves on first event (not waiting for complete)
- Add fetchAuthorProfile helper for DRY author fetching
- Refactor fetchArticleMetadataViaRelays to:
- Return immediately when first article event arrives (no 7s wait)
- Fetch author profile with 400ms micro-wait (connections already warm)
- Optional hedge: try again with 600ms if first attempt fails
- Fallback to pubkey prefix if profile not found
- Add logging to track article fetch and author resolution source
This dramatically improves first-request latency by returning as soon as
any relay responds, while still attempting to get author name with
minimal additional delay (400-600ms total).
- Add tags field to ArticleMetadata type (extracted from 't' tags)
- Add imageAlt field to ArticleMetadata type (uses title as fallback)
- Extract 't' tags from article events in fetchArticleMetadataViaRelays
- Generate multiple article:tag meta tags in HTML output
- Add og:image:alt meta tag for better accessibility
This improves SEO and social media previews by including
article categorization tags and image descriptions.
Remove Promise.race timeout wrapper and let relay fetch use its natural
timeouts (7s for article, 5s for profile). Remove background refresh
complexity entirely.
Flow is now simple:
1. Check Redis cache
2. If miss, fetch from relays (up to 7s)
3. Cache result
4. Subsequent requests hit cache
First request may take 7-8 seconds, but after that it's cached and fast.
Much simpler and more reliable.
Add more detailed logging to diagnose background refresh issues:
- Log the exact URL being called
- Log whether secret is present
- Better error handling for non-JSON responses
- More detailed error messages
This will help identify if the refresh endpoint is being called
and why we're not seeing logs from it.
Relays can be slow, especially on first connection. Increase timeout
to 5 seconds to give relays more time to respond before falling back
to default metadata.
Add detailed logging to verify background refresh is working:
- Log when background refresh is triggered
- Log the response status and result from refresh endpoint
- Log errors if refresh fetch fails
- Add logging in refresh endpoint to track:
- When refresh starts
- When metadata is found
- When metadata is cached
- Any errors during refresh
This will help diagnose if background refresh is actually
populating the Redis cache after timeouts.
- Remove gateway HTTP fetch entirely
- Fetch directly from relays on cache miss with 3s timeout
- If relay fetch succeeds: cache and return article metadata
- If relay fetch fails/times out: return default OG and trigger background refresh
- Update logging to reflect relay-first strategy
- Keep background refresh endpoint for eventual consistency
This simplifies the code and removes dependency on unreliable gateway,
while ensuring fast responses and eventual correctness via background refresh.
Add comprehensive logging to diagnose why gateway fetch is failing:
- Log the exact URL being fetched
- Log HTTP status on failure
- Log response length on success
- Log which OG tags were found/missing
- Add detailed error information
This will help identify if the issue is:
- Fetch timeout/failure
- Missing OG tags in response
- Regex pattern mismatch
- Support both KV_* and UPSTASH_* env var names for Redis
- Add error handling for Redis get operations
- Add console logging to track cache hits/misses and gateway fetches
- Fix syntax error in background refresh trigger
This will help diagnose why article metadata isn't being returned correctly.
ESM requires explicit file extensions in import paths. Add .js
extensions to all relative imports in API files and services,
even though source files are .ts (they compile to .js).
This fixes ERR_MODULE_NOT_FOUND errors on Vercel.
- Move ogStore, ogHtml, and articleMeta from src/services/ to api/services/
- Update imports in article-og.ts and article-og-refresh.ts
- Update import paths in articleMeta.ts (lib/profile and src/config/relays)
- Remove old files from src/services/
- Clean up ESLint config to only reference api/**/*.ts
This fixes the ERR_MODULE_NOT_FOUND error on Vercel by ensuring
serverless functions can access the service modules.
- Add ogStore service for Redis get/set operations
- Extract shared logic: ogHtml (generateHtml, escapeHtml) and articleMeta (relay/gateway fetching)
- Refactor article-og endpoint to read from Redis, try gateway on miss, trigger background refresh
- Add article-og-refresh endpoint for background relay fetching and caching
- Update vercel.json with refresh function config
- Remove WebSocket dependencies from main OG endpoint for faster crawler responses
All /a/:naddr requests now route to article-og handler, which handles crawler detection internally. This ensures social media crawlers always receive proper OpenGraph meta tags.
Add documentation about the test account used for publishing markdown test documents, including the npub and profile link to Marky Markdown Testerson's writings.
Add documentation about the test account used for publishing markdown test documents, including the npub and profile link to Marky Markdown Testerson's writings.
- Add descriptive text before each table explaining what it tests
- Improve documentation for table test cases
- Help developers understand the purpose of each test scenario
- Add comprehensive table styles with borders, padding, and spacing
- Style table headers with elevated background
- Add subtle row striping for better readability
- Support text alignment (left, center, right)
- Maintain mobile responsiveness with horizontal scrolling
- Use theme CSS variables for consistent theming across light/dark modes
Replace mouseup/touchend handlers with selectionchange event listener
for more reliable mobile text selection detection. This fixes the issue
where the highlight button required an extra tap to become active on
mobile devices.
- Extract selection checking logic into shared checkSelection function
- Use selectionchange event with requestAnimationFrame for immediate detection
- Remove onMouseUp and onTouchEnd props from VideoEmbedProcessor
- Simplify code by eliminating separate mouse/touch event handlers
- perf: collect text nodes once instead of per highlight (O(n×m) -> O(n+m))
- fix: correct normalized index mapping algorithm for whitespace handling
- feat: allow nested mark elements for overlapping highlights
- perf: add caching for highlighted HTML results with TTL and size limits
Replace manual type checking and pubkey extraction with getPubkeyFromDecodeResult helper:
- Update getNostrUriLabel to use helper instead of manual npub/nprofile cases
- Update replaceNostrUrisInMarkdownWithProfileLabels to use helper
- Update addLoadingClassToProfileLinks to use helper
- Simplify NostrMentionLink by removing redundant type checks
- Update Bookmarks.tsx to use helper for profile pubkey extraction
This eliminates duplicate logic and ensures consistent handling of npub/nprofile
across the codebase using applesauce helpers.
- Fix getNpubFallbackDisplay to return names without @ prefix
- Update all call sites to consistently add @ when rendering mentions
- Fix incomplete error handling in getNpubFallbackDisplay catch block
- Add nprofile support to addLoadingClassToProfileLinks
- Extract shared isProfileInCacheOrStore utility to eliminate duplicate loading state checks
- Update ResolvedMention and NostrMentionLink to use shared utility
This ensures consistent @ prefix handling across all profile display contexts
and eliminates code duplication for profile loading state detection.
Fix regression where npubs/nprofiles weren't being replaced with profile names.
The issue was a race condition: loading state was cleared immediately, but labels
were applied asynchronously via RAF, causing the condition check to fail.
Changes:
- Apply profile labels immediately when profiles resolve, instead of batching via RAF
- Update condition check to explicitly handle undefined loading state (isLoading !== true)
- This ensures labels are available in the Map when loading becomes false
- Fix merge logic in useEffect that syncs profileLabels state
- Previously was overwriting newly resolved labels when initialLabels changed
- Now preserves existing labels and only adds missing ones from initialLabels
- This fixes the issue where profileLabels was being reset to 0 after applyPendingUpdates
- Add debug logs to track when useEffect sync runs
- Add debug logs in applyPendingUpdates to see when updates are applied
- Add debug logs in scheduleBatchedUpdate to track RAF scheduling
- Add debug logs when adding to pending updates
- Add debug logs for profileLabelsKey computation to verify state updates
- Will help diagnose why profileLabels stays at size 0 despite profiles resolving
- Separate markdown processing from HTML extraction
- Add useEffect that watches processedMarkdown and extracts HTML
- Use double RAF to ensure ReactMarkdown has finished rendering before extracting
- This fixes the issue where resolved profile names weren't updating in the article view
- Add debug logs to track HTML extraction after processedMarkdown changes
- Add [shimmer-debug] prefixed logs to trace loading state flow
- Log when profiles are marked as loading in useProfileLabels
- Log when loading state is cleared after profile resolution
- Log detailed post-processing steps in addLoadingClassToProfileLinks
- Log markdown replacement decisions in replaceNostrUrisInMarkdownWithProfileLabels
- Log HTML changes and class counts in useMarkdownToHTML
- All logs use [shimmer-debug] prefix for easy filtering
- HTML inside markdown links doesn't render correctly with rehype-raw
- Instead, post-process rendered HTML to find profile links (/p/npub...)
- Decode npub to get pubkey and check loading state
- Add profile-loading class directly to <a> tags
- This ensures the loading shimmer appears on the actual link element
- Check loading state FIRST before checking for resolved labels
- Profiles have fallback labels immediately, which caused early return
- Now loading shimmer will show even when fallback label exists
- This fixes the issue where shimmer never appeared
- Changed useProfileLabels to use pubkey as key for canonical identification
- Updated replaceNostrUrisInMarkdownWithProfileLabels to extract pubkey and use it for lookup
- This fixes the key mismatch issue where different nprofile encodings map to the same pubkey
- Multiple nprofile strings can refer to the same pubkey (different relay hints)
- Using pubkey as key is the Nostr standard way to identify profiles
- Check if encoded value from regex matches Map keys
- Log full comparison when mismatch detected
- Will help identify if regex capture group format differs from Map storage format
- Log the exact encoded value being processed
- Log sample of Map keys for comparison
- Will help identify format mismatch between markdown and Map storage
- Log when replacement function is called with Map sizes
- Log all loading keys in the Map
- Log detailed info for each npub/nprofile found: type, hasLoading, isLoading
- Will help identify if encoded IDs don't match or loading state isn't detected
- Use stable string keys instead of Map objects as dependencies
- Only clear rendered HTML when markdown content actually changes
- Use refs to access latest Map values without triggering re-renders
- Prevents excessive markdown reprocessing on every profile update
- Should significantly reduce screen flickering during profile resolution
- Add logs to useProfileLabels for loading state tracking
- Add logs to markdown processing to track when content is cleared/reprocessed
- Add logs to article loader for refresh behavior
- Add logs to ResolvedMention and NostrMentionLink for loading detection
- Add logs to nostr URI resolver when loading state is shown
- All logs prefixed with meaningful tags for easy filtering
- Extend useProfileLabels to return loading Map alongside labels
- Update markdown replacement to show loading indicator for unresolved profiles
- Add loading state detection to ResolvedMention and NostrMentionLink components
- Add CSS animation for profile-loading class with opacity pulse
- Respect prefers-reduced-motion for accessibility
- Capture refs at effect level and use in cleanup function
- This satisfies react-hooks/exhaustive-deps rule for cleanup functions
- Prevents stale closure issues while keeping code clean
- Sync state when initialLabels changes (e.g., content changes)
- Flush pending batched updates after EOSE completes
- Flush pending updates in cleanup to avoid losing updates
- Better handling of profile data changes vs same profiles
Fixes issue where @npub... placeholders sometimes weren't replaced
until refresh. Now all profile updates are guaranteed to be applied.
- Use requestAnimationFrame to batch rapid profile label updates
- Collect pending updates in a ref instead of updating state immediately
- Apply all pending updates in one render cycle
- Add cleanup to cancel pending RAF on unmount/effect cleanup
This prevents flickering when multiple profiles stream in quickly while
still maintaining progressive updates as profiles arrive.
- Add optional onEvent callback to fetchProfiles (following queryEvents pattern)
- Remove all timeouts - rely entirely on EOSE signals
- Update useProfileLabels to use reactive streaming callback
- Labels update progressively as profiles arrive from relays
- Remove unused timer/takeUntil imports
- Backwards compatible: other callers of fetchProfiles still work
This follows the controller pattern from fetching-data-with-controllers rule:
'Since we are streaming results, we should NEVER use timeouts for fetching
data. We should always rely on EOSE.'
Increase timeout from 6s to 10s to give slow relays (including purplepag.es)
more time to respond with profile metadata. This may help find profiles that
were timing out before.
Add logic to check if purplepag.es is in the active relay pool when fetching
profiles. If not, add it temporarily to ensure we query this relay for
profile metadata. This should help find profiles that might not be available
on other relays.
Also adds debug logging to show which active relays are being queried.
Add comprehensive logs prefixed with [fetch-profiles] to track:
- How many profiles are requested
- Cache lookup results
- Relay query configuration
- Each profile event as it's received
- Summary of fetched vs missing profiles
- Which profiles weren't found on relays
This will help diagnose why only 9/19 profiles are being returned.
Add detailed debug logs prefixed with [profile-labels] and [markdown-replace]
to track the profile resolution flow:
- Profile identifier extraction from content
- Cache lookup and eventStore checks
- Profile fetching from relays
- Label updates when profiles resolve
- Markdown URI replacement with profile labels
This will help diagnose why profile names aren't resolving correctly.
Previously, useProfileLabels would set fallback npub labels immediately for
missing profiles, then skip updating them when profiles were fetched because
the condition checked if the label already existed.
Now we track which profiles were being fetched (pubkeysToFetch) and update
their labels even if they already have fallback labels set, allowing profiles
to resolve progressively from fallback npubs to actual names as they load.
- Add getProfileDisplayName() utility function for consistent profile name resolution
- Update all components to use standardized npub fallback format instead of hex
- Fix useProfileLabels hook to include fallback npub labels when profiles lack names
- Refactor NostrMentionLink to eliminate duplication between npub/nprofile cases
- Remove debug console.log statements from RichContent component
- Update AuthorCard, SidebarHeader, HighlightItem, Support, BlogPostCard, ResolvedMention, and useEventLoader to use new utilities
- Remove all debug console.log/error statements (39+) and ts() helpers
- Eliminate redundant localStorage cache check in useProfileLabels
- Standardize fallback display format using getNpubFallbackDisplay() utility
- Update ResolvedMention to use npub format consistently
- Add LRU eviction strategy: limit to 1000 cached profiles, evict oldest when full
- Track lastAccessed timestamp for each cached profile
- Automatically evict old profiles when quota is exceeded
- Reduce error logging spam: only log quota error once per session
- Silently handle cache errors to match articleService pattern
- Proactively evict before caching when approaching limit
This prevents localStorage quota exceeded errors and ensures
the most recently accessed profiles remain cached.
- Use useMemo to check localStorage cache synchronously during render, before useEffect
- Initialize useState with labels from cache, so first render shows cached profiles immediately
- Add detailed logging for cache operations to debug caching issues
- Fix ESLint warnings about unused variables and dependencies
This eliminates the delay where profiles were only resolved after useEffect ran,
causing profiles to display instantly on page reload when cached.
- Add localStorage caching functions to profileService.ts following articleService.ts pattern
- getCachedProfile: get single cached profile with TTL validation (30 days)
- cacheProfile: save profile to localStorage with error handling
- loadCachedProfiles: batch load multiple profiles from cache
- Modify fetchProfiles() to check localStorage cache first, only fetch missing/expired profiles, and cache fetched profiles
- Update useProfileLabels hook to check localStorage before EventStore, add cached profiles to EventStore for consistency
- Update logging to show cache hits from localStorage
- Benefits: instant profile resolution on page reload, reduced relay queries, offline support for previously-seen profiles
- Add duration tracking for fetchProfiles (shows how long it takes)
- Add total time tracking for entire resolution process
- Reduce log noise by only logging when profileLabels size changes
- Helps identify performance bottlenecks
- Add timestamp helper function (HH:mm:ss.SSS format)
- Update all console.log/error statements to include timestamps
- Helps identify timing bottlenecks in profile resolution
- fetchProfiles returns profiles that we should use immediately
- Check returned array first, then fallback to eventStore lookup
- Fixes issue where profiles were returned but not used for resolution
- Log fetchProfiles return count
- Log profile events found in store vs missing
- Log profiles with names vs without names
- Help diagnose why 0 profiles are being resolved
- Poll eventStore every 200ms for up to 2 seconds after fetchProfiles
- Accumulate resolved labels across checks instead of resetting
- Add detailed logging to diagnose why profiles aren't resolving
- Fixes issue where profiles arrive asynchronously after fetchProfiles completes
- After fetchProfiles completes, re-check eventStore for all profiles
- This ensures profiles are resolved even if fetchProfiles returns partial results
- Fixes issue where only 5 out of 19 profiles were being resolved
- Show @derggg instead of @npub1derggg for truncated npubs
- Update getNostrUriLabel to skip first 5 chars ('npub1')
- Update NostrMentionLink fallback display to match
- Add logs to useProfileLabels hook
- Add logs to useMarkdownToHTML hook
- Add logs to RichContent component
- Add logs to extractNostrUris function
- Add error handling with fallbacks
- Merge related bookmark button changes in 0.10.31
- Consolidate image preloading entries in 0.10.27
- Group flight mode fixes in 0.10.26
- Combine OpenGraph-related changes in 0.10.24
- Consolidate bookmark sorting fixes in 0.10.11
- Merge reading progress bar fixes in 0.10.25
- Reduce file from 2158 to 2108 lines
- Remove nested bullets and verbose explanations
- Condense implementation details to user-facing changes
- Maintain Keep a Changelog format and structure
- 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.
- 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,6 +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).
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.