* feat: enhance login options with read-only and nsec support
- Add read-only login mode supporting:
- npub (bech32 public key)
- nprofile (bech32 profile with relay hints)
- hex public key
- NIP-05 addresses (user@domain.com)
- Add private key (nsec) login with security warning
- Supports nsec1... format
- Supports 64-char hex private key
- Shows prominent security warning about localStorage storage
- Reorganize user menu to show login before theme option
- Use ReadonlyAccount from applesauce-accounts for read-only mode
- Use PrivateKeyAccount from applesauce-accounts for nsec login
- Update LoginDialog with 4 tabs: Extension, Read-Only, Private Key, Remote
- All account types properly registered via registerCommonAccountTypes()
Technical notes:
- ReadonlySigner throws errors on sign/encrypt operations
- Existing components naturally handle accounts without signing capability
- Hub/ActionRunner already syncs with account signers automatically
* feat: add generate identity button to login dialog
- Add "Generate Identity" button above login tabs
- Uses Wand2 icon from lucide-react
- Creates new key pair using PrivateKeyAccount.generateNew()
- Automatically stores nsec in localStorage and sets as active account
- Provides quick onboarding for new users without external wallet setup
* feat: add useAccount hook for signing capability detection
Created a centralized hook to check account signing capabilities and
refactored components to distinguish between signing and read-only operations.
New hook (src/hooks/useAccount.ts):
- Returns account, pubkey, canSign, signer, isLoggedIn
- Detects ReadonlyAccount vs signing accounts
- Provides clear API for checking signing capability
Refactored components:
- ChatViewer: Use canSign for message composer, replying, actions
- Show "Sign in to send messages" for read-only accounts
- Disable message input for accounts without signing
- SpellDialog: Use canSign for publishing spells
- Show clear warning for read-only accounts
- Updated error messages to mention read-only limitation
- useEmojiSearch: Use pubkey for loading custom emoji lists
- Works correctly with both signing and read-only accounts
Benefits:
- Clear separation between read (pubkey) and write (canSign, signer) operations
- Read-only accounts can browse, view profiles, load data
- Signing operations properly disabled for read-only accounts
- Consistent pattern across the codebase for account checks
- Better UX with specific messages about account capabilities
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: Display blossom servers in user menu with caching
Implements caching and display of user's blossom server lists (kind 10063) in the user menu dropdown.
**Key Changes:**
1. **Database Schema (db.ts)**:
- Added `CachedBlossomServerList` interface
- Added `blossomServers` table to Dexie v15
- Indexed by pubkey and updatedAt for efficient querying
2. **Caching Service (blossom-server-cache.ts)**:
- Dual-layer caching: LRU memory cache (100 entries) + Dexie persistent cache
- 24-hour TTL to reduce network requests
- Auto-caches kind:10063 events from EventStore
- Mirrors relay-list-cache pattern for consistency
3. **Sync Hooks**:
- `useBlossomServerCacheSync`: Subscribes to EventStore for auto-caching kind:10063 events
- `useAccountSync`: Extended to fetch and watch active account's blossom servers
4. **State Management**:
- Added `blossomServers` field to `activeAccount` in GrimoireState
- Created `setActiveAccountBlossomServers` pure function and callback
- Reactive updates when user's server list changes
5. **User Menu UI (user-menu.tsx)**:
- Added blossom servers section with HardDrive icon
- Shows server count badge (e.g., "3 servers")
- Clickable links open servers in new tabs
- Displays only when user has configured servers
**Architecture:**
- Follows existing relay list caching pattern for consistency
- Reactive: UI auto-updates when kind:10063 events arrive
- Incremental sync: Fetches on login, caches for 24h
- Performance: Memory cache for <1ms lookups, Dexie for persistence
**Testing:**
- Build: ✓ No TypeScript errors
- Tests: ✓ All 838 tests passing
* feat: Open blossom server file lists directly from menus
**User Menu & Profile Viewer Improvements:**
1. **Enhanced Click Behavior**:
- Clicking a blossom server now opens the file list for that server
- Shows blobs uploaded by the user (user menu) or profile owner (profile viewer)
- Pre-selects the clicked server in the dropdown
2. **UX Improvements**:
- Removed server count from user menu label (cleaner UI)
- Added `cursor-crosshair` to blossom server items (consistent with other clickable items)
- Removed external link icon (not opening external URL anymore)
3. **Technical Changes**:
- Updated `ListBlobsView` to accept optional `serverUrl` prop for pre-selection
- User menu: Opens `blossom list` with `serverUrl` for active user
- Profile viewer: Opens `blossom list` with both `pubkey` and `serverUrl`
**Flow:**
- User menu → Click server → Opens files for active user on that server
- Profile viewer → Click server → Opens files for viewed user on that server
* fix: Properly fetch blossom servers for any profile view
**Problem:**
Blossom servers were only visible for the logged-in user's profile,
not for other users' profiles being viewed.
**Solution:**
Enhanced ProfileViewer blossom server fetching with multi-layer approach:
1. **Cache-first loading**: Check blossomServerCache for instant display
2. **EventStore check**: Use existing cached event if available
3. **Reactive subscription**: Subscribe to EventStore for real-time updates
4. **Network fetch**: Use addressLoader to fetch latest from relays
5. **Auto-caching**: Update cache when new events arrive
**Benefits:**
- Blossom servers now display for ANY user's profile
- Instant display from cache (< 1ms)
- Reactive updates when data changes
- Proper cache hydration for future visits
- Consistent with relay list fetching pattern
**Technical:**
- Imported and integrated blossomServerCache service
- Added cache check before network fetch
- Separated EventStore subscription from network fetch
- Added cache updates on event arrival
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add Blossom blob storage integration
- Add blossom-client-sdk dependency for blob storage operations
- Create blossom.ts service with upload, list, check, mirror, delete primitives
- Add kind 10063 server list fetching and parsing
- Create blossom-parser.ts for command argument parsing with subcommands
- Add BLOSSOM command to man.ts with subcommands:
- servers: Show configured Blossom servers
- check: Check server health
- upload: Upload files to user's servers
- list: List blobs for a user
- mirror: Mirror blobs between servers
- delete: Delete blobs from servers
- Create BlossomViewer component with views for each subcommand
- Wire up BlossomViewer in WindowRenderer
- Add Blossom servers dropdown to ProfileViewer header
- Upload primitives can be called programmatically for use in other components
* Enhance Blossom viewer with server selection and blob details
- Add server selection checkboxes to upload view for choosing target servers
- Add BlobDetailView with media preview (image/video/audio) and metadata display
- Add 'blob' subcommand to view individual blob details
- Remove unused 'check' subcommand
* Add Blossom upload dialog with chat integration
- Create BlossomUploadDialog component with file picker, server selection, and preview
- Create useBlossomUpload hook for easy integration in any component
- Add insertText method to MentionEditor for programmatic text insertion
- Integrate upload button (paperclip icon) in chat composer
- Supports image, video, and audio uploads with drag-and-drop
* Add rich blob attachments with imeta tags for chat
- Add BlobAttachment TipTap extension with inline preview (thumbnail for images, icons for video/audio)
- Store full blob metadata (sha256, url, mimeType, size, server) in editor nodes
- Convert blob nodes to URLs in content with NIP-92 imeta tags when sending
- Add insertBlob method to MentionEditor for programmatic blob insertion
- Update NIP-29 and NIP-53 adapters to include imeta tags with blob metadata
- Pass blob attachments through entire send flow (editor -> ChatViewer -> adapter)
* Add fallback public Blossom servers for users without server list
- Add well-known public servers as fallbacks (blossom.primal.net, nostr.download, files.v0l.io)
- Use fallbacks when user has no kind 10063 server list configured
- Show "Public Servers" label with Globe icon when using fallbacks
- Inform user that no server list was found
- Select first fallback server by default (vs all user servers)
* Fix: Don't show fallback servers when not logged in
Blossom uploads require signed auth events, so users must be logged in.
The 'Account required' message is already shown in this case.
* Remove files.v0l.io from fallback servers
* Add rich renderer for kind 10063 Blossom server list
- Create BlossomServerListRenderer.tsx with feed and detail views
- Show user's configured Blossom servers with clickable links
- Clicking a server opens the Blossom window with server info
- Register renderers for kind 10063 (BUD-03)
- Fix lint error by renaming useFallbackServers to applyFallbackServers
* Add individual server view and NIP-05 support for blossom commands
- Add 'server' subcommand to view info about a specific Blossom server
- Update BlossomServerListRenderer to open server view on click
- Make blossom parser async to support NIP-05 resolution in 'list' command
- Add kind 10063 (Blossom Server List) to EVENT_KINDS constants with BUD-03 reference
- Update command examples with NIP-05 identifier support
* Add comprehensive tests for blossom-parser
- 34 test cases covering all subcommands (servers, server, upload, list, blob, mirror, delete)
- Tests for NIP-05 resolution, npub/nprofile decoding, $me alias
- Tests for error handling and input validation
- Tests for case insensitivity and command aliases (ls, view, rm)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add NIP-30 emoji autocompletion to editor
Implement emoji autocomplete triggered by `:` in the MentionEditor:
- EmojiSearchService: flexsearch-based indexing for emoji shortcodes
- useEmojiSearch hook: loads Unicode emojis + user's custom emoji (kind 10030/30030)
- EmojiSuggestionList: grid-based suggestion UI with keyboard nav
- Update MentionEditor with second Mention extension for emoji
- Serialize emoji as `:shortcode:` format with NIP-30 emoji tags
- Update chat adapters to include emoji tags in messages
Sources:
- Unicode: ~300 common emojis with shortcodes
- Custom: user's emoji list (kind 10030) and referenced sets (kind 30030)
- Context: emoji tags from events being replied to
* feat: add rich emoji preview in editor
Emoji inserted via the autocomplete now display as actual images/characters
instead of :shortcode: text:
- Custom emoji: renders as inline <img> with proper sizing
- Unicode emoji: renders as text with emoji font sizing
- Both show :shortcode: on hover via title attribute
CSS styles ensure proper vertical alignment with surrounding text.
* fix: store emoji url and source attributes in node schema
The TipTap Mention extension only defines `id` and `label` by default.
Added `addAttributes()` to EmojiMention extension to also store `url`
and `source` attributes, fixing emoji tags not being included in sent
messages.
* fix: improve emoji node rendering in editor
- Remove redundant renderLabel (nodeView handles display)
- Add renderText for proper clipboard behavior
- Make nodeView more robust with null checks
- Add fallback to shortcode if image fails to load
- Unicode emoji shows character, custom shows image
* fix: serialize unicode emoji as actual characters, not shortcodes
When sending messages:
- Unicode emoji (😄, 🔥) → outputs 😄, 🔥 (the actual character)
- Custom emoji (:pepe:) → outputs :pepe: with emoji tag for rendering
---------
Co-authored-by: Claude <noreply@anthropic.com>
Implements rich text editing with profile mentions, NIP-29 system messages,
day markers, and naddr support for a more complete chat experience.
Editor Features:
- TipTap-based rich text editor with @mention autocomplete
- FlexSearch-powered profile search (case-insensitive)
- Converts mentions to nostr:npub URIs on submission
- Keyboard navigation (Arrow keys, Enter, Escape)
- Fixed Enter key and Send button submission
NIP-29 Chat Improvements:
- System messages for join/leave events (kinds 9000, 9001, 9021, 9022)
- Styled system messages aligned left with muted text
- Shows "joined" instead of "was added" for consistency
- Accepts kind 39000 naddr (group metadata addresses)
- Day markers between messages from different days
- Day markers use locale-aware formatting (short month, no year)
Components:
- src/components/editor/MentionEditor.tsx - TipTap editor with mention support
- src/components/editor/ProfileSuggestionList.tsx - Autocomplete dropdown
- src/services/profile-search.ts - FlexSearch service for profile indexing
- src/hooks/useProfileSearch.ts - React hook for profile search
Dependencies:
- @tiptap/react, @tiptap/starter-kit, @tiptap/extension-mention
- @tiptap/extension-placeholder, @tiptap/suggestion
- flexsearch@0.7.43, tippy.js@6.3.7
Tests:
- Added 6 new tests for naddr parsing in NIP-29 adapter
- All 710 tests passing
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: add preview routes for Nostr identifiers (npub, nevent, note, naddr)
This commit adds dedicated preview routes for Nostr identifiers at the root level:
- /npub... - Shows a single profile view for npub identifiers
- /nevent... - Shows a single event detail view for nevent identifiers
- /note... - Shows a single event detail view for note identifiers
- /naddr... - Redirects spellbooks (kind 30777) to /:actor/:identifier route
Key changes:
- Created PreviewProfilePage component for npub identifiers
- Created PreviewEventPage component for nevent/note identifiers
- Created PreviewAddressPage component for naddr redirects
- Added hideBottomBar prop to AppShell to hide tabs in preview mode
- Added routes to root.tsx for all identifier types
Preview pages don't show bottom tabs and don't affect user's workspace layout.
* chore: update package-lock.json
* refactor: create reusable useNip19Decode hook and improve preview pages
This commit makes the preview pages production-ready by:
1. Created useNip19Decode hook (src/hooks/useNip19Decode.ts):
- Reusable hook for decoding NIP-19 identifiers (npub, note, nevent, naddr, nprofile)
- Type-safe with discriminated union for decoded entities
- Comprehensive error handling with retry functionality
- Loading states and error messages
- Well-documented with JSDoc comments and usage examples
2. Comprehensive test coverage (src/hooks/useNip19Decode.test.ts):
- 11 tests covering all entity types (npub, note, nevent, naddr)
- Tests for error handling (missing identifier, invalid format, corrupted bech32)
- Tests for retry functionality and state changes
- Uses jsdom environment for React hook testing
- All tests passing ✓
3. Refactored preview pages to use the hook:
- PreviewProfilePage: Simplified from 80 to 81 lines with cleaner logic
- PreviewEventPage: Improved type safety and error handling
- PreviewAddressPage: Better separation of concerns
- All pages now have consistent error handling and retry functionality
- Better user experience with improved error messages
4. Dependencies added:
- @testing-library/react for React hook testing
- @testing-library/dom for DOM testing utilities
- jsdom and happy-dom for browser environment simulation in tests
Benefits:
- Code deduplication: Preview pages share decoding logic
- Type safety: Discriminated union prevents type errors
- Testability: Hook can be tested independently
- Maintainability: Single source of truth for NIP-19 decoding
- User experience: Consistent error handling and retry across all preview pages
- Production-ready: Comprehensive tests and error handling
* refactor: simplify useNip19Decode to synchronous with memoization
NIP-19 decoding is synchronous - removed unnecessary async complexity:
Hook changes (src/hooks/useNip19Decode.ts):
- Removed loading states (isLoading, setIsLoading)
- Removed retry functionality (unnecessary for sync operations)
- Now uses useMemo for efficient memoization
- Returns { decoded, error } instead of { decoded, isLoading, error, retry }
- Same string always yields same result (memoized)
- Went from ~120 lines to ~115 lines, but much simpler
Preview page changes:
- Removed loading spinners and states
- Removed retry buttons
- Simplified error handling
- Cleaner, more readable code
- PreviewProfilePage: 55 lines (down from 81)
- PreviewEventPage: 83 lines (down from 105)
- PreviewAddressPage: 83 lines (down from 117)
Test changes (src/hooks/useNip19Decode.test.ts):
- Removed waitFor and async/await (not needed for sync)
- Tests run faster (39ms vs 77ms - 49% improvement)
- Added memoization tests to verify caching works
- Simplified from 11 async tests to 11 sync tests
- All 11 tests passing ✓
Benefits:
- Simpler mental model: decode happens instantly
- Better performance: no state updates, just memoization
- Easier to test: synchronous tests are simpler
- More correct: matches the actual synchronous nature of nip19.decode()
- Less code: removed ~150 lines of unnecessary complexity
* feat: show detail view for all addressable events in naddr preview
Previously, PreviewAddressPage only handled spellbooks (kind 30777) and
showed errors for other addressable events. Now:
- Spellbooks (kind 30777): Redirect to /:actor/:identifier (existing behavior)
- All other addressable events: Show in EventDetailViewer
This enables previewing any addressable event (long-form articles, live
events, community posts, etc.) via naddr links.
Changes:
- Import EventDetailViewer
- Removed error state for non-spellbook kinds
- Show EventDetailViewer with AddressPointer for all other kinds
- Simplified from 83 lines to 77 lines
* fix: correct route patterns for NIP-19 identifier previews
The previous route patterns (/npub:identifier) conflicted with the catch-all
/:actor/:identifier route and didn't properly match NIP-19 identifiers.
Fixed by:
1. Using wildcard routes with correct prefixes:
- /npub1* (not /npub:identifier)
- /nevent1* (not /nevent:identifier)
- /note1* (not /note:identifier)
- /naddr1* (not /naddr:identifier)
2. Updated preview components to use params['*'] for wildcard capture:
- Reconstruct full identifier as prefix + captured part
- e.g., npub1 + params['*'] = npub107jk7htfv...
This ensures routes properly match before the catch-all /:actor/:identifier
route and correctly capture the full bech32-encoded identifier.
Test URL: /npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg
* style: apply prettier formatting
* fix: use loader-based routing for NIP-19 identifiers in React Router v7
Previous attempts using wildcard routes didn't work properly in React Router v7.
Solution:
- Single /:identifier route with a loader that validates NIP-19 prefixes
- Loader throws 404 if identifier doesn't start with npub1/note1/nevent1/naddr1
- Created Nip19PreviewRouter component that routes to correct preview page
- Routes are properly ordered: /:identifier before /:actor/:identifier catch-all
This ensures /npub107jk... routes to profile preview, not spellbook route.
Benefits:
- Simpler routing configuration (1 route vs 4 duplicate routes)
- Proper validation via loader
- Clean separation of concerns with router component
- Works correctly in React Router v7
---------
Co-authored-by: Claude <noreply@anthropic.com>
**CRITICAL FIX for EOSE detection:**
**The Problem:**
- Used pool.subscription(relays, filters) which creates a RelayGroup
- RelayGroup tracks per-relay EOSE internally but only emits ONE "EOSE" when ALL relays finish
- This caused:
1. EOSE indicators taking forever to appear (waiting for slowest relay)
2. REQ stuck in LOADING state when fast relays finish but slow relays never do
3. No way to show per-relay EOSE status accurately
**The Solution:**
Subscribe to each relay individually using pool.relay(url).subscription():
- Each relay subscription emits its own EOSE immediately when that relay finishes
- We track per-relay EOSE in relayStates map with accurate timing
- Overall EOSE is derived when ALL relays reach terminal state (eose/error/disconnected)
- EOSE indicators now appear immediately as each relay finishes
**Technical Details:**
- Changed from: pool.subscription(relays, filters)
- Changed to: relays.map(url => pool.relay(url).subscription(filters))
- Added eoseReceivedRef to track overall EOSE in closures
- Mark specific relay as EOSE when that relay emits "EOSE"
- Calculate overall EOSE when all relays in terminal states
- Use url from subscription context (more reliable than event._relay)
**Benefits:**
✅ Instant per-relay EOSE indicators (no waiting for slowest relay)
✅ Accurate relay state tracking (each relay independent)
✅ REQ transitions to LIVE/CLOSED as soon as all relays finish
✅ Better user feedback (see which relays are done vs still loading)
All 639 tests passing.
State Tracking Fixes:
- Sync connection state for ALL relays in query, not just initialized ones
- Defensively initialize missing relay states during sync
- Handle events from unknown relays (defensive initialization)
- Add debug console logs to track state transitions
Relay Type Indicators:
- Explicit relays: Blue link icon (relays specified directly)
- Outbox relays: Purple sparkles (NIP-65 selected)
- Fallback relays: Gray inbox icon (fallback when outbox incomplete)
- Each type has tooltip explaining source
This should fix:
- "0/4 relays but events coming in" bug
- "Stuck in LOADING" when events are arriving
- Missing visibility for relay source types
Tests: 634/634 passing
Core Infrastructure:
- Add ReqRelayState and ReqOverallState types for granular state tracking
- Implement deriveOverallState() state machine with 8 query states
- Create useReqTimelineEnhanced hook combining RelayStateManager + event tracking
- Add comprehensive unit tests (27 tests, all passing)
State Machine Logic:
- DISCOVERING: NIP-65 relay selection in progress
- CONNECTING: Waiting for first relay connection
- LOADING: Initial events loading
- LIVE: Streaming with active relays (only when actually connected!)
- PARTIAL: Some relays ok, some failed/disconnected
- OFFLINE: All relays disconnected after being live
- CLOSED: Query completed, all relays closed
- FAILED: All relays failed to connect
UI Updates:
- Single-word status indicators with detailed tooltips
- Condensed relay status into NIP-65 section (no duplicate lists)
- Per-relay subscription state badges (RECEIVING, EOSE, ERROR, OFFLINE)
- Event counts per relay
- Connection + Auth status integrated into single dropdown
Fixes Critical Bug:
- Solves "LIVE with 0 relays" issue (Scenario 5 from analysis)
- Distinguishes real EOSE from relay disconnections
- Accurate status for all 7 edge cases documented in analysis
Technical Approach:
- Hybrid: RelayStateManager for connections + event._relay for tracking
- Works around applesauce-relay catchError bug without forking
- No duplicate subscriptions
- Production-quality error handling
Tests: 27/27 passing including edge case scenarios
- Replace mounted boolean flag with AbortController pattern
- Check abort signal before initiating database writes
- Proper cleanup on unmount/pubkey change
This prevents stale data from being written to IndexedDB when:
- Component unmounts during async operations
- Pubkey changes while a fetch is in progress
- Create src/hooks/useStable.ts with:
- useStableValue<T>() - stabilizes any value using JSON.stringify
- useStableArray<T>() - stabilizes string arrays (uses JSON.stringify
for safety, handles arrays with commas in elements)
- useStableFilters<T>() - specialized for Nostr filters
- Update timeline hooks to use stabilization:
- useTimeline.ts - use useStableFilters for filter dependencies
- useReqTimeline.ts - use useStableValue for filter dependencies
- useLiveTimeline.ts - use useStableArray for relay dependencies
Prevents unnecessary re-renders and subscription restarts when
filter/relay objects are recreated with the same content.
- Display option flags on separate lines with indented descriptions to prevent overflow
- Parse and separate example commands from their descriptions
- Highlight commands in accent color with muted descriptions below
- Increase spacing between items for better readability