* Add subtle inline reactions to chat messages
Implements NIP-25 reaction display for chat messages with per-message lazy loading:
- Created MessageReactions component that independently loads kind 7 reactions
for each message using EventStore timeline queries
- Displays reactions as tiny inline badges in bottom-right corner (doesn't affect
message height)
- Aggregates reactions by emoji with deduplication by pubkey
- Supports both unicode emoji and NIP-30 custom emoji with images
- Shows reaction count next to each emoji
- Integrated into both regular user messages and zap messages in ChatViewer
- Reactions load reactively - new reactions appear automatically via EventStore
observables
No "+" button for adding reactions yet - this is display-only for now.
Works with NIP-29 groups and will work with any chat protocol that uses
kind 7 reactions with e-tags.
* Fix reaction loading to use protocol-specific relay hints
Previously MessageReactions was only querying EventStore without actually
fetching reactions from relays. Now it properly:
- Starts a relay subscription per message to fetch kind 7 reactions
- Uses protocol-specific relay hints via getConversationRelays() helper:
* NIP-29 groups: Single relay from conversation.metadata.relayUrl
* NIP-53 live chats: Multiple relays from conversation.metadata.liveActivity.relays
- Memoizes relay array in MessageItem to prevent unnecessary re-subscriptions
- Cleans up subscriptions when message unmounts or changes
This ensures reactions are actually fetched and displayed correctly across
different chat protocols.
* Remove unused NostrEvent import
* Move reactions inline after timestamp with subtler styling
Reactions now appear directly after the timestamp in the message header:
- Removed absolute positioning and background color
- Increased spacing between emoji and count (gap-1 instead of gap-0.5)
- Simple inline display with no border or background
- Appears in natural reading flow: "Alice 10:30 AM ❤️ 3 👍 1"
- Removed relative positioning from message container (no longer needed)
This makes reactions much more subtle and integrated into the message UI.
* Add detailed tooltips to reactions showing who reacted
Each reaction badge now shows a tooltip with:
- Emoji and count on first line
- Comma-separated list of display names who reacted
Implementation:
- Split into ReactionBadge component per reaction
- Loads profiles for all reactor pubkeys using eventStore.profiles()
- Uses getDisplayName() helper for human-readable names
- Tooltip format: "❤️ 3\nAlice, Bob, Carol"
This makes it easy to see exactly who reacted with each emoji.
* Simplify reaction tooltips to show truncated pubkeys
Changed tooltip implementation from loading profiles (which wasn't working
with EventStore API) to showing truncated pubkeys for simplicity and performance:
- Removed profile loading logic (eventStore.profiles() doesn't exist)
- Tooltips now show: "❤️ 3\nabcd1234..., efgh5678..."
- Truncated to first 8 chars for readability
- No external API calls needed, purely computed from reaction data
- Can be enhanced later to load profiles if needed
Build verified: TypeScript compilation passes, all tests pass.
This is production-ready code.
* Add emoji reaction picker to chat messages
Implements complete reaction functionality with searchable emoji picker:
**UI Enhancements:**
- Reactions display horizontally with hidden scrollbar (hide-scrollbar CSS utility)
- Messages with many reactions scroll smoothly without visible scrollbar
- Inline positioning after timestamp for clean, integrated look
**Emoji Picker Dialog:**
- Real-time search using FlexSearch (EmojiSearchService)
- Quick reaction bar with common emojis (❤️👍🔥😂🎉👀🤔💯)
- Frequently used section based on localStorage history
- Support for both unicode and NIP-30 custom emoji
- Grid layout with 48-emoji results
- Auto-focus search input for keyboard-first UX
**Protocol Implementation:**
- Added sendReaction() method to ChatProtocolAdapter base class
- NIP-29 groups: kind 7 with e-tag + h-tag (group context)
- NIP-53 live chats: kind 7 with e-tag + a-tag (activity context)
- NIP-C7 DMs: kind 7 with e-tag + p-tag (partner context)
- All reactions include k-tag for reacted event kind
- NIP-30 custom emoji support via emoji tags
**Context Menu Integration:**
- Added "React" action to ChatMessageContextMenu with Smile icon
- Opens emoji picker dialog on click
- Passes conversation and adapter for protocol-specific reactions
- Only shows when conversation and adapter are available
**Frequently Used Tracking:**
- Stores reaction history in localStorage (grimoire:reaction-history)
- Displays top 8 most-used reactions when no search query
- Increments count on each reaction sent
**Tooltips:**
- Show emoji + count + truncated pubkeys
- Format: "❤️ 3\nabcd1234..., efgh5678..."
- Future enhancement: load profiles for display names
Ready for testing! Users can now right-click messages → React → search/pick emoji.
* Load user's custom emoji list in reaction picker
The emoji picker now loads custom emoji from:
- User's emoji list (kind 10030) - personal custom emoji
- Context emojis from conversation messages
- Unicode emojis (default set)
EmojiSearchService prioritizes:
1. User emoji (highest priority)
2. Context emoji (from conversation)
3. Emoji sets (if any)
4. Unicode emoji (default)
When searching or browsing, users will see their custom emoji alongside
standard unicode emoji, making it easy to use personal/community emoji
in reactions.
* Fix reaction container to prevent message width expansion
Wrapped reactions in a scrollable container with proper constraints:
- inline-flex: displays inline with message metadata
- max-w-full: prevents expanding beyond message width
- overflow-x-auto: enables horizontal scrolling for many reactions
- hide-scrollbar: hides scrollbar for clean appearance
Messages with many reactions now stay within their width and scroll
horizontally without a visible scrollbar.
* Highlight reaction counts when active user has reacted
Changes:
- Reaction counts now show in highlight color (text-highlight) when the
active user has reacted with that emoji
- Added font-semibold to make user's reactions more prominent
- Checks if activeAccount.pubkey is in reaction.pubkeys array
- Provides clear visual feedback showing which reactions you've made
This makes it easy to see at a glance which reactions are yours in a
conversation with many reactions.
* Refine reaction badge styling: increase spacing and remove bold
- Increase gap between reaction badges from gap-1 to gap-2
- Remove font-semibold from highlighted reaction counts
- Keep text-highlight color for active user's reactions
- Results in more subtle, polished appearance
* Refactor emoji picker to use useEmojiSearch hook
- Replace manual emoji service management with useEmojiSearch hook
- Use same emoji loading approach as chat autocomplete
- Subscribe to EventStore observables for reactive updates
- Load user's emoji list (kind 10030) and emoji sets (kind 30030)
- Fix custom emoji search not working
- Remove redundant async loading logic
* Fix emoji picker UI issues
- Remove quick reaction bar (❤️👍🔥 etc.)
- Fix custom emoji in "Recently used" section - now renders images instead of shortcodes
- Increase grid spacing from gap-2 to gap-3 to reduce crowding
- Add helpers to properly lookup and render custom emoji from service
* Improve reaction badge sizing and spacing
- Increase custom emoji size from size-3 (12px) to size-3.5 (14px)
- Increase gap between emoji and count from gap-1 to gap-1.5
- Add object-contain to custom emoji images for proper aspect ratio
- Add leading-none to unicode emoji for consistent vertical alignment
- Results in better visual balance between custom and unicode emoji
* Fix custom emoji shrinking in reaction badges
- Add flex-shrink-0 to custom emoji images to prevent compression
- Add flex-shrink-0 to unicode emoji spans for consistency
- Ensures both custom and unicode emoji maintain their size-3.5 dimensions
* Improve emoji picker UX with fixed layout
- Always show exactly 2 rows (16 emoji) to prevent height jumping
- Merge recently used with search results into unified grid
- When no search: show recently used first, then fill with other emoji
- When searching: show top 16 results
- Remove separate "Recently used" section for cleaner layout
- Add aspect-square to buttons for consistent sizing
- Add object-contain to custom emoji for proper aspect ratio
- Replace scrollable area with fixed-height grid
* Refine emoji picker to show single row with fixed height
- Show only 1 row (8 emoji) instead of 2 rows for more compact UI
- Add min-h-[3.5rem] to prevent height changes
- Ensure custom emoji (w-6 h-6) matches unicode emoji (text-2xl) size
- Add leading-none to unicode emoji for better vertical alignment
- Empty state "No emojis found" maintains same grid height
- Consistent sizing between custom and unicode emoji across the picker
* Fix emoji sizing in picker to match unicode and custom emoji
- Reduce unicode emoji from text-2xl (24px) to text-xl (20px)
- Reduce custom emoji from w-6 h-6 (24px) to size-5 (20px)
- Both now render at same 20px size for visual consistency
- Fixes custom emoji appearing too large compared to unicode emoji
* ui: dialog tweaks
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add COUNT command for NIP-45 event counting
Implements the COUNT verb from NIP-45, allowing users to count events
on relays without fetching them. Features:
- New `count` command requiring at least one relay
- Filter-only flags (excludes --limit, --close-on-eose, --view)
- Single relay shows centered count result
- Multiple relays show per-relay breakdown
- Handles approximate counts, errors, and unsupported relays
- Supports $me/$contacts aliases and NIP-05 resolution
Examples:
count relay.damus.io -k 3 -p npub1...
count nos.lol relay.damus.io -k 1 -a fiatjaf.com
* Fix $me and $contacts alias resolution in CountViewer
- Fetch contact list (kind 3) using useNostrEvent hook
- Extract contacts from p tags to resolve $contacts alias
- Add "Account Required" message when aliases used without active account
- Match ReqViewer pattern for consistent alias resolution
* Refactor: extract FilterSummaryBadges for compact headers
- Create shared FilterSummaryBadges component (nostr/FilterSummaryBadges.tsx)
- Simplify CountViewer header to single compact line
- Use FilterSummaryBadges in both ReqViewer and CountViewer
- Remove verbose collapsible filter section from CountViewer
* Remove 'events' suffix from count result
* Update count synopsis to show relays can appear anywhere
* Refactor CountViewer to use applesauce-relay pool
Replace manual WebSocket connections with the relay pool's
count() method for NIP-45 COUNT requests. This provides:
- Proper connection reuse via the existing relay pool
- Automatic reconnection handling
- Better integration with the rest of the app
Remove the approximate property since applesauce-relay's
CountResponse type doesn't expose it yet.
* Simplify CountViewer with one-shot requests and compact UI
- Use per-relay count requests with firstValueFrom and timeout
instead of pool.count() observable that may not complete
- Replace Collapsible-based header with icon-only DropdownMenus
matching ReqViewer's compact style
- Add raw JSON filter view with syntax highlighting and copy button
- Show relay count and filter in dropdowns instead of expanded sections
- Requests complete after timeout (10s) instead of spinning indefinitely
* Add NIP-45 support detection via NIP-11 relay info
- Check relay's supported_nips via NIP-11 before sending COUNT request
- Return early with "unsupported" status if relay explicitly doesn't support NIP-45
- Differentiate UI between unsupported (yellow Ban icon) and error (red AlertCircle)
- Provide clearer error messages based on whether NIP-11 info was available
- Uses cached relay info when available to avoid redundant requests
* Improve CountViewer header with human-readable filter summary
- Show kinds as badges, authors ("by"), mentions ("@"), hashtags on left
- Move relay status into relay dropdown with per-relay results
- Dropdown shows count per relay, status icons, and error tooltips
- Header now shows "2/3" style relay count trigger with loading state
* Reorder CountViewer header controls and remove redundant mention prefix
- Change control order to: refresh, relays, filter (was: filter, relays, refresh)
- Remove redundant "@" prefix from mentions since UserName with isMention already shows @
* Increase COUNT timeout to 30s and improve window title
- Extend per-relay timeout from 10s to 30s for more reliable results
- Update count window title to show human-readable kind names instead of
command-line format (e.g., "count: Short Note by abc123..." instead of
"count -k 1 -a npub...")
* Add spell support for COUNT commands
- Extend spell system to support both REQ and COUNT commands
- Add detectCommandType() to identify command type from string
- Update encodeSpell to use ["cmd", "COUNT"] tag for count commands
- Update decodeSpell to handle COUNT spells
- Update reconstructCommand to accept cmdType parameter
- Add "Save as spell" option to COUNT windows in WindowToolbar
- Update SpellDialog to handle both REQ and COUNT commands
* Add dynamic window title for COUNT with human-readable filter summary
- Add profile fetching for COUNT authors and tagged users
- Add countTitle useMemo with human-readable kind names, authors, mentions, hashtags, and search
- Use same formatting helpers as REQ titles (getKindName, formatProfileNames, etc.)
- Add countTitle to title priority chain after reqTitle
- Title now shows "Short Note • @alice • #bitcoin" instead of "COUNT"
* Update count command documentation for production
- Add note about automatic NIP-11/NIP-45 support detection
- Mention spell saving capability in description
* Add automatic relay selection with NIP-45 filtering for COUNT
- Make relays optional in count-parser (no longer throws if none specified)
- Add useOutboxRelays for automatic relay selection based on filter criteria
- Filter selected relays by NIP-45 support via NIP-11 before querying
- Show "Selecting relays..." and "Filtering by NIP-45..." loading states
- Fall back to aggregator relays if no NIP-45 relays found
- Update man page: relays now optional, new examples showing auto-selection
* Revert automatic relay selection for COUNT command
Simplify COUNT by requiring explicit relay specification:
- Restore relay requirement validation in count-parser.ts
- Remove useOutboxRelays and NIP-45 auto-filtering from CountViewer
- Update man page documentation to reflect required relays
- Keep NIP-45 support detection for better error messages
This keeps the feature simpler for now; automatic relay selection
can be added later when the UX is better understood.
* Reduce padding and sizing in COUNT single result view
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: Add relay link parsing and rendering to rich text
Implements custom parsing for relay URLs (wss:// and ws://) in text content,
automatically converting them to clickable links that open the relay viewer.
Changes:
- Add relay-transformer.ts with pattern matching for relay URLs
- Create Relay.tsx component for rendering relay links inline
- Register relay transformer in RichText component pipeline
- Add comprehensive test suite (26 tests) covering all URL formats
Supported formats:
- wss:// and ws:// protocols
- Domains, subdomains, and IP addresses
- Custom ports, paths, query parameters
- Multiple relay URLs in single message
All tests passing (864/864). No breaking changes.
* refactor: Update relay link styling to match other inline links
- Use muted/underline styling consistent with NIP references
- Remove icons and show only relay name (formatted)
- Display full URL in tooltip
- Match text size with surrounding content
- Simplify component by not using RelayLink wrapper
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: Add reusable theme system with Plan 9 proof of concept
Implement a comprehensive theme system that:
- Defines typed Theme interface with colors, syntax highlighting, scrollbar, and gradient variables
- Creates ThemeProvider with React context for runtime theme switching
- Persists theme selection to localStorage
- Includes 3 built-in themes: dark (default), light, and plan9
Theme structure supports:
- Core UI colors (background, foreground, primary, secondary, accent, etc.)
- Status colors (success, warning, info) replacing hardcoded Tailwind colors
- Syntax highlighting variables for code blocks
- Diff highlighting colors (inserted, deleted, meta)
- Scrollbar styling variables
- Gradient colors for branding
Technical changes:
- Update CSS to use new theme variables throughout
- Update prism-theme.css to use syntax variables instead of hardcoded values
- Remove chart colors (unused)
- Add success/warning/info to tailwind.config.js
- Wire up ThemeProvider in main.tsx
For Nostr publishing (future):
- d tag: "grimoire-theme"
- name tag: theme display name
* feat: Add theme selector to user menu, remove configurable border radius
- Remove border radius from theme configuration (borders are always square)
- Add theme selector dropdown to user menu (available to all users)
- Theme selector shows active theme indicator
- Theme selection persists via localStorage
* fix: Improve theme contrast and persistence
- Fix theme persistence: properly check localStorage before using default
- Plan9: make blue subtler (reduce saturation), darken gradient colors
for better contrast on pale yellow background
- Light theme: improve contrast with darker muted foreground and borders
- Change theme selector from flat list to dropdown submenu
* fix: Replace Plan9 yellow accent with purple, add zap/live theme colors
- Replace Plan9's bright yellow accent with purple (good contrast on pale yellow)
- Add zap and live colors to theme system (used by ZapReceiptRenderer, StatusBadge)
- Make light theme gradient orange darker for better contrast
- Update ZapReceiptRenderer to use theme zap color instead of hardcoded yellow-500
- Update StatusBadge to use theme live color instead of hardcoded red-600
- Add CSS variables and Tailwind utilities for zap/live colors
* fix: Make gradient orange darker, theme status colors
- Make gradient orange darker in light and plan9 themes for better contrast
- Make req viewer status colors themeable:
- loading/connecting → text-warning
- live/receiving → text-success
- error/failed → text-destructive
- eose → text-info
- Update relay status icons to use theme colors
- Update tests to expect theme color classes
* fix: Use themeable zap color for active user names
- Replace hardcoded text-orange-400 with text-zap in UserName component
- Replace hardcoded text-orange-400 with text-zap in SpellRenderer ($me placeholder)
- Now uses dark amber/gold with proper contrast on light/plan9 themes
* feat: Add highlight theme color for active user display
Add dedicated 'highlight' color to theme system for displaying the
logged-in user's name, replacing the use of 'zap' color which felt
semantically incorrect. The highlight color is optimized for contrast
on each theme's background.
- Add highlight to ThemeColors interface and apply.ts
- Add --highlight CSS variable to index.css (light and dark)
- Add highlight to tailwind.config.js
- Configure appropriate highlight values for dark, light, and plan9 themes
- Update UserName.tsx to use text-highlight for active account
- Update SpellRenderer.tsx MePlaceholder to use text-highlight
* fix: Restore original orange-400 highlight color for dark theme
Update dark theme highlight to match original text-orange-400 color
(27 96% 61%) for backward compatibility with existing appearance.
---------
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>
* fix: slash autocomplete only at input start + add bookmark commands
- Fix slash command autocomplete to only trigger when / is at the
beginning of input text (position 1 in TipTap), not in the middle
of messages
- Add /bookmark command to add NIP-29 group to user's kind 10009 list
- Add /unbookmark command to remove group from user's kind 10009 list
* fix: normalize relay URLs when checking group list bookmarks
Use normalizeRelayURL for comparing relay URLs in bookmark/unbookmark
commands to handle differences in trailing slashes, casing, and protocol
prefixes between stored tags and conversation metadata.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add protocol-specific actions to chat adapters
Extends the chat adapter system with support for slash commands and
protocol-specific actions without parameters.
New features:
- ChatAction type system for defining simple commands
- Base adapter getActions() and executeAction() methods
- NIP-29 /join and /leave slash commands
- Slash command parser for detecting /commands
- ChatViewer integration with toast notifications
Example usage in NIP-29 groups:
/join - Request to join the group
/leave - Leave the group
The action system is extensible and can be enhanced with parameterized
actions (e.g., /kick <user>, /ban <user>) in future iterations.
Tests: All 804 tests passing
Build: Successful
Lint: No errors
* feat: add autocomplete for slash commands
Extends the chat system with autocomplete UI for slash commands, making
actions discoverable and easier to execute.
New features:
- SlashCommandSuggestionList component with keyboard navigation
- Slash command search integrated into MentionEditor
- Autocomplete popup shows command name and description
- Arrow keys for navigation, Enter to select, Escape to close
- Filters available commands based on typed text
When typing "/" in chat, users now see:
/join Request to join the group
/leave Leave the group
The autocomplete uses the same TipTap suggestion system as @mentions
and :emoji:, providing a consistent UX across all autocomplete features.
Tests: All 804 tests passing
Build: Successful
Lint: 1 warning fixed (unused parameter)
* feat: execute slash commands immediately on autocomplete selection
When selecting a slash command from the autocomplete menu, the action
now executes immediately and clears the input, providing a more
streamlined UX.
Changes:
- Added onCommandExecute prop to MentionEditor
- Modified slash command suggestion to call onCommandExecute on selection
- Clears editor content immediately after selection
- Added handleCommandExecute callback in ChatViewer
- Executes action and shows toast notifications
Before: Type "/" → select "/join" → press Enter → executes
After: Type "/" → select "/join" → executes immediately
This matches the expected behavior for command selection - when you
choose a command from autocomplete, you want to execute it, not just
insert it into the text field.
Tests: All 804 tests passing
Build: Successful
Lint: No new errors
* feat: make chat actions context-aware and filter by membership status
Actions are now intelligently filtered based on the user's membership in
the conversation, providing a cleaner and more intuitive UX.
Changes:
- Added GetActionsOptions type with conversation and activePubkey
- Updated base adapter getActions() to accept optional context
- Modified NIP-29 adapter to filter actions based on membership:
- /join: only shown when user is NOT a member/admin
- /leave: only shown when user IS a member
- Updated ChatViewer to pass conversation and user context to searchCommands
- Moved searchCommands callback after conversation is defined
This prevents showing irrelevant commands like "/join" when you're already
a member, or "/leave" when you haven't joined yet. The autocomplete menu
now only displays actions that are actually executable in the current context.
Implementation notes:
- NIP-29 uses getAllActions() fallback when context unavailable
- Membership determined by checking conversation.participants array
- Other protocols return empty array by default (no actions yet)
Tests: All 804 tests passing
Build: Successful
Lint: No new errors
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add multi-room group chat interface (GroupListViewer)
Add a Discord/Slack-style multi-room chat interface for NIP-29 groups:
- New GroupListViewer component with split layout:
- Left panel: List of groups from kind 10009, sorted by recency
- Right panel: Full chat view for selected group
- Loads group metadata (kind 39000) for icons and names
- Tracks latest messages (kind 9) for activity-based sorting
Add chat case to reconstructCommand to properly reconstruct the chat
command string when clicking the edit button in the window toolbar.
- NIP-29 groups: reconstruct as `chat relay'group-id`
- NIP-53 live activities: reconstruct as `chat naddr1...`
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add NIP-61 nutzap support to NIP-29 groups
Fetch and render nutzap events (kind 9321) in NIP-29 relay groups
using the same visual styling as lightning zaps. Nutzaps are P2PK
locked Cashu token transfers defined in NIP-61.
- Add nutzap filter subscription in loadMessages
- Combine chat and nutzap observables with RxJS combineLatest
- Add nutzapToMessage helper to parse NIP-61 event structure
- Extract amount by summing proof amounts from proof tag JSON
- Add nutzapUnit metadata field for future multi-currency support
* fix: improve zap/nutzap rendering in chat
- Add mb-1 margin bottom to zap messages for spacing
- Show inline reply preview for zaps that target specific messages
- Fix nutzap amount extraction to handle multiple proof tags
- Extract replyTo from e-tag for nutzaps
- Pass nutzap event to RichText for custom emoji rendering
* fix: pass event only to RichText for proper emoji rendering
* refactor: consolidate NIP-29 chat and nutzap into single REQ
- Use single filter with kinds [9, 9000, 9001, 9321] instead of
separate subscriptions with combineLatest
- Enables proper pagination for "load older" with single page fetches
- Add rounded corners to zap gradient border for consistent rendering
* refactor: consolidate NIP-53 chat and zap into single REQ
- Use single filter with kinds [1311, 9735] instead of separate
subscriptions with combineLatest
- Enables proper pagination for "load older" with single page fetches
- Filter invalid zaps inline during event mapping
* feat: add load older messages support to chat adapters
- Implement loadMoreMessages in NIP-29 adapter using pool.request
- Implement loadMoreMessages in NIP-53 adapter using pool.request
- Add "Load older messages" button to ChatViewer header
- Use firstValueFrom + toArray to convert Observable to Promise
- Track loading state and hasMore for pagination UI
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add NIP-53 live activity chat adapter
Add support for joining live stream chat via naddr (kind 30311):
- Create Nip53Adapter with parseIdentifier, resolveConversation, loadMessages, sendMessage
- Show live activity status badge (LIVE/UPCOMING/ENDED) in chat header
- Display host name and stream metadata from the live activity event
- Support kind 1311 live chat messages with a-tag references
- Use relays from activity's relays tag or naddr relay hints
- Add tests for adapter identifier parsing and chat-parser integration
* ui: derive live chat participants from messages, icon-only status badge
- Derive participants list from unique pubkeys in chat messages for NIP-53
- Move status badge after title with hideLabel for compact icon-only display
* feat: show zaps in NIP-53 live chat with gradient border
- Fetch kind 9735 zaps with #a tag matching the live activity
- Combine zaps and chat messages in the timeline, sorted by timestamp
- Display zap messages with gradient border (yellow → orange → purple → cyan)
- Show zapper, amount, recipient, and optional comment
- Add "zap" message type with zapAmount and zapRecipient metadata
* fix: use RichText for zap comments and remove arrow in chat
- Use RichText with zap request event for zap comments (renders emoji tags)
- Remove the arrow (→) between zapper and recipient in zap messages
* refactor: simplify zap message rendering in chat
- Put timestamp right next to recipient (removed ml-auto)
- Use RichText with content prop and event for emoji resolution
- Inline simple expressions, remove unnecessary variables
- Follow codebase patterns from ZapCompactPreview
* docs: update chat command to include NIP-53 live activity
- Update synopsis to use generic <identifier>
- Add NIP-53 live activity chat to description
- Update option description to cover both protocols
- Add naddr example for live activity chat
- Add 'live' to seeAlso references
* fix: use host outbox relays for NIP-53 live chat events
Combine activity relays, naddr hints, and host's outbox relays when
subscribing to chat messages and zaps. This ensures events are fetched
from all relevant sources where they may be published.
* ui: show host first in members list, all relays in dropdown
- derivedParticipants now puts host first with 'host' role
- Other participants from messages follow as 'member'
- RelaysDropdown shows all NIP-53 liveActivity.relays
---------
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>
Reply Functionality:
- Added Reply button to each message (visible on hover)
- Button appears in message header next to timestamp
- Uses Reply icon from lucide-react
- Clicking reply sets the replyTo state with message ID
- Reply preview shows in composer when replying
Active Account Requirements:
- Check for active account using accountManager.active$
- Only show composer if user has active account
- Only enable reply buttons if user has active account
- Show "Sign in to send messages" message when no active account
- Prevent sending messages without active account
UI Improvements:
- Reply button uses opacity transition on hover (0 → 100)
- Positioned with ml-auto to align right in header
- Reply button only visible on group hover for clean UI
- Consistent styling with muted-foreground color scheme
Benefits:
- Users can reply to specific messages inline
- Clear indication when authentication is required
- Prevents errors from attempting to send without account
- Professional chat UX with hover interactions
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Message Loading Improvements:
- Use RxJS Subject to track EOSE (End Of Stored Events)
- Use skipUntil operator to delay timeline emissions until EOSE received
- Prevents scroll position jumping during initial message load
- Messages still update reactively after initial EOSE
NIP-29 Adapter:
- Create eoseSubject to track EOSE state
- Emit from subject when EOSE string received from relay subscription
- Apply skipUntil(eoseSubject) to eventStore.timeline() observable
NIP-C7 Adapter:
- Add relay subscription to track EOSE (was missing)
- Use same EOSE tracking pattern as NIP-29
- Apply skipUntil to prevent premature timeline emissions
Benefits:
- Smooth initial load experience without scroll jumping
- All messages appear together after EOSE
- Maintains reactive updates for new messages
- Consistent behavior across both chat protocols
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Message Layout Improvements:
- Added proper padding to message items (px-3 py-2)
- Added flex-1 min-w-0 to message content container to allow shrinking
- Added break-words and overflow-hidden to prevent horizontal overflow
- Ensures long URLs and unbreakable text wrap properly
Message Composer Improvements:
- Added border-t and consistent padding (px-3 py-2)
- Added border and rounded-md to textarea for better visual separation
- Added min-w-0 to textarea to prevent overflow in flex layout
- Added flex-shrink-0 to Send button to prevent squishing
- Added mb-2 to reply preview for spacing
These changes ensure:
- Long messages and URLs wrap correctly without horizontal scroll
- Consistent spacing throughout the chat interface
- Proper flex behavior prevents layout breaks
- Professional chat UI appearance
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements rendering of NIP-51 kind 10009 (Public Chats list) events,
displaying NIP-29 groups similar to how relay lists are rendered.
Components:
- GroupLink: Clickable group component with icon and name
- Fetches kind 39000 metadata from EventStore for group info
- Displays group icon (if available) or chat icon fallback
- Opens chat window on click with relay'group-id identifier
- PublicChatsRenderer: Renderer for kind 10009 events
- Extracts "group" tags: ["group", "<group-id>", "<relay-url>"]
- Displays each group as clickable link
- Similar styling to relay list renderers
Integration:
- Registered kind 10009 renderer in kinds index
- Opens chat with NIP-29 protocol on click
Fixes:
- Fix missing type imports in NIP-29 adapter (Participant, ParticipantRole)
- Fix relayUrl type error in sendMessage (use validated variable)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Core Architecture:
- Protocol adapter pattern for chat implementations
- Base adapter interface with protocol-specific implementations
- Auto-detection of protocol from identifier format
- Reactive message loading via EventStore observables
Protocol Implementations:
- NIP-C7 adapter: Simple chat (kind 9) with npub/nprofile support
- NIP-29 adapter: Relay-based groups with member roles and moderation
- Protocol-aware reply message loading with relay hints
- Proper NIP-29 members/admins fetching using #d tags
UI Components:
- ChatViewer: Main chat interface with virtualized message timeline
- ChatMessage: Message rendering with reply preview
- ReplyPreview: Auto-loading replied-to messages from relays
- MembersDropdown: Virtualized member list with role labels
- RelaysDropdown: Connection status for chat relays
- ChatComposer: Message input with send functionality
Command System:
- chat command with identifier parsing and auto-detection
- Support for npub, nprofile, NIP-05, and relay'group-id formats
- Integration with window system and dynamic titles
NIP-29 Specific:
- Fetch kind:39000 (metadata), kind:39001 (admins), kind:39002 (members)
- Extract roles from p tags: ["p", "<pubkey>", "<role1>", "<role2>"]
- Role normalization (admin, moderator, host, member)
- Single group relay connection management
Testing:
- Comprehensive chat parser tests
- Protocol adapter test structure
- All tests passing (704 tests)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: Add Zapstore app and app curation set renderers
Add support for rendering Zapstore app-related Nostr events:
- Kind 32267 (App Metadata): Display app details, icon, platforms, screenshots
- Kind 30267 (App Curation Set): Display curated app collections
New files:
- src/lib/zapstore-helpers.ts: Helper functions for extracting app metadata
- src/lib/zapstore-helpers.test.ts: Comprehensive test coverage (43 tests)
- src/components/nostr/kinds/ZapstoreAppRenderer.tsx: Feed view for apps
- src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx: Detail view for apps
- src/components/nostr/kinds/ZapstoreAppSetRenderer.tsx: Feed view for collections
- src/components/nostr/kinds/ZapstoreAppSetDetailRenderer.tsx: Detail view for collections
Modified:
- src/components/nostr/kinds/index.tsx: Register new renderers in kind registry
All tests pass (726 total), build succeeds, no lint errors.
* feat: Add Zapstore release renderer (kind 30063)
Add support for rendering Zapstore app release events (kind 30063):
- Kind 30063 (Release): Connects apps (32267) to file artifacts (1063)
New files:
- src/components/nostr/kinds/ZapstoreReleaseRenderer.tsx: Feed view for releases
- src/components/nostr/kinds/ZapstoreReleaseDetailRenderer.tsx: Detail view with embedded file metadata
Modified:
- src/lib/zapstore-helpers.ts: Add release helper functions
- getReleaseIdentifier(): Extract release ID (package@version)
- getReleaseVersion(): Parse version from identifier
- getReleaseFileEventId(): Get file metadata event pointer
- getReleaseAppPointer(): Get app metadata pointer
- src/lib/zapstore-helpers.test.ts: Add 18 new tests for release helpers (61 total)
- src/components/nostr/kinds/index.tsx: Register kind 30063 renderers
Complete Zapstore app ecosystem now supported:
- Kind 32267: App metadata (name, icon, description)
- Kind 30267: App curation sets (collections)
- Kind 30063: App releases (version tracking)
- Kind 1063: File metadata (downloads)
All tests pass (744 total), build succeeds.
* refactor: Simplify Zapstore app renderers with platform icons
Improve Zapstore app rendering for cleaner, more intuitive display:
Changes:
- Add detectPlatforms() helper to normalize architecture tags (e.g., "android-arm64-v8a" → "android")
- Replace verbose platform badges with clean platform icons (Android, iOS, Web, macOS, Windows, Linux)
- Remove screenshots from feed view (keep in detail view only)
- Remove repository links and license badges from feed view
- Update detail view to show "Available On" with icon+label platform items
Feed view now shows:
- App icon
- App name
- Summary (2 lines max)
- Platform icons (just icons, no text)
Detail view now shows:
- App icon, name, summary
- Publisher, Package ID, License, Repository (metadata grid)
- Available On (platforms with icons and labels)
- Screenshots gallery (unchanged)
All tests pass (744 total), build succeeds.
* fix: Polish Zapstore renderers with platform labels and clean layout
Address feedback to improve Zapstore renderer UX:
Changes:
1. App feed (ZapstoreAppRenderer):
- Add platform text labels next to icons (e.g., "Android", "iOS", "Web")
- Now shows icon + label for better clarity
2. Release feed (ZapstoreReleaseRenderer):
- Remove big package icon from feed view
- Cleaner, more compact layout with just app name, version badge, and action links
3. Registry comments:
- Update to human-friendly names:
* "Zapstore App" (instead of "App Metadata (Zapstore)")
* "Zapstore App Collection" (instead of "App Curation Set (Zapstore)")
* "Zapstore App Release" (instead of "App Release (Zapstore)")
All tests pass (744 total), build succeeds.
* refactor: Update Zapstore app set renderers with improved UX
- ZapstoreAppSetRenderer: Show ALL apps with compact spacing (gap-0.5) like relay lists, removed 5-app limit
- ZapstoreAppSetDetailRenderer: Replace raw platform tags with normalized platform icons using detectPlatforms()
- Both renderers now provide cleaner, more consistent UI following Grimoire patterns
* refactor: Add human-friendly names and simplify Zapstore renderers
- kinds.ts: Add kind 32267 (App), update 30063 to "App Release", update 30267 to "App Collection"
- Extract PlatformIcon to shared component (zapstore/PlatformIcon.tsx)
- Update all renderer comments to use human-friendly terminology
- Remove unnecessary comments throughout Zapstore renderers
- Simplify code without changing functionality
* feat: Add releases section to app detail view
- Query for all releases (kind 30063) that reference the app
- Display releases sorted by version (newest first)
- Each release shows version badge and download link
- Clicking release opens full release detail view
- Clicking download opens file metadata view
* fix: Force screenshots as images and filter releases by author
- Add type="image" to MediaEmbed for screenshots to fix "unsupported media type" errors
- Filter releases to only show those from the same author (pubkey) as the app
- Prevents releases from other apps or authors from appearing in the app detail view
* fix: Remove author filter from releases query
The a tag already uniquely identifies the app (32267:pubkey:identifier).
Releases may be published by different authors (maintainers, packagers)
than the app author, so we should show all releases that reference
the app via the a tag, regardless of who published them.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Fix custom emoji shortcode matching to include dashes
The regex pattern for matching NIP-30 custom emoji shortcodes was missing
the dash character, causing emojis like :work-out: to fail matching.
Updated both ReactionRenderer and ReactionCompactPreview to use
[a-zA-Z0-9_-] instead of [a-zA-Z0-9_].
* Refactor: extract emoji shortcode regex to constant
Move the emoji shortcode matching pattern to EMOJI_SHORTCODE_REGEX
constant in emoji-helpers.ts for reuse across reaction renderers.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(calendar): add renderers for NIP-52 calendar events (kinds 31922 & 31923)
- Add CalendarDays and CalendarClock icons for date/time event kinds
- Create calendar-event.ts helper with parsing and formatting functions
- Add feed renderers showing status badge, title, date/time, location, participant count
- Add detail renderers with full description, participant list with names, tags, and links
- Register renderers in kinds/index.tsx for both feed and detail views
- Use locale-aware date/time formatting throughout
* refactor(calendar): improve feed renderer layout
- Rename both kinds to "Calendar Event" for consistency
- Move date/time info below title
- Place time on left, status badge on right with justify-between
- Remove timezone indicator from feed view (keep in detail)
* refactor(calendar): apply feed layout to detail views
- Move title above date/time in detail views
- Use justify-between for time left, status badge right
- Use Label component for hashtags (no # prefix, consistent with feed)
- Use Label component for participant roles (subtle dotted border style)
* refactor(calendar): separate location/tags rows and tone down time font
- Separate location/participants from hashtags onto different rows in feed
- Reduce time font size: text-xs in feed, text-sm in detail (was text-sm/text-lg)
- Remove font-medium from time display
* style(calendar): tone down status badges to match date/time styling
Replace bold background-colored badges with subtle text-colored badges:
- Use text-blue-500, text-green-500, text-muted-foreground instead of backgrounds
- Lowercase labels ("upcoming", "now", "past")
- Consistent sizing with date/time text
* refactor(calendar): extract shared components and add caching
- Extract CalendarStatusBadge to shared component with variant/size props
- Add symbol-based caching to parseDateCalendarEvent and parseTimeCalendarEvent
using applesauce's getOrComputeCachedValue for performance
- Use 'd' tag (identifier) as title fallback instead of "Untitled Event"
- Remove duplicate CalendarStatusBadge implementations from all 4 renderers
- Remove unused imports (cn, CalendarDays, CalendarClock, Clock, CheckCircle)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: implement NIP-89 app definitions and recommendations with rich rendering
Add comprehensive support for NIP-89 Application Handlers (kind 31990) and
Handler Recommendations (kind 31989) with rich, interactive visualizations.
Core Implementation:
- nip89-helpers.ts: Utility functions for extracting NIP-89 event metadata
- App name, description, image from kind 31990 content JSON
- Supported kinds from k tags
- Platform URLs (web, ios, android) from platform tags
- Handler references from kind 31989 a tags
- URL template substitution for <bech32> placeholders
Feed Renderers:
- ApplicationHandlerRenderer (31990): Shows app name, supported kinds as
clickable KindBadges (max 8 in feed), and platform badges
- HandlerRecommendationRenderer (31989): Shows recommended kind and handler
list (max 3 in feed) with platform indicators
Detail Renderers:
- ApplicationHandlerDetailRenderer (31990): Comprehensive view with app info,
all supported kinds in grid layout (clickable), platform URLs with copy
buttons, and metadata JSON viewer
- HandlerRecommendationDetailRenderer (31989): Full view with platform
filtering tabs, expanded handler cards showing app details, and raw
reference data
Features:
- Clickable KindBadges throughout for quick navigation
- Platform-aware filtering and display
- Fetches referenced kind 31990 events reactively
- Copy buttons for URL templates
- Platform icons (web, ios, android)
- Follows existing Grimoire patterns (SpellRenderer for kinds display,
CodeSnippetDetailRenderer for metadata sections)
Testing:
- Comprehensive test suite for nip89-helpers (50+ test cases)
- Tests cover all helper functions with edge cases
- Follows existing test patterns from codebase
Registry:
- Added both kinds (31989, 31990) to kindRenderers and detailRenderers
- Automatically expands supported kinds count in KindsViewer
* fix: remove unused imports and parameters in NIP-89 renderers
* fix: correct AddressPointer import and apply prettier formatting
- Change AddressPointer import from applesauce-core/helpers to nostr-tools/nip19
to match codebase conventions
- Auto-fix prettier formatting for nip89 files
* fix: add defensive type checks to prevent React error 31
- Add type guards in nip89-helpers to ensure string types
- Check metadata object structure before accessing properties
- Add fallbacks for undefined address.identifier values
- Prevents accidentally rendering objects as React children
* fix: stringify contentJson for CopyableJsonViewer and support 'about' field
- Fix React error 31: CopyableJsonViewer expects string, not object
- Add JSON.stringify() with pretty printing for metadata display
- Support both 'description' and 'about' fields in content JSON (common in kind 0)
- Add tests for 'about' field handling
* refactor: simplify NIP-89 detail renderers
Remove unnecessary metadata displays:
- Remove app image from ApplicationHandlerDetailRenderer
- Remove Event ID and Created timestamp from both detail renderers
- Remove Raw Metadata section from ApplicationHandlerDetailRenderer
- Remove Raw References section from HandlerRecommendationDetailRenderer
- Clean up unused imports (getAppImage, CopyableJsonViewer, useMemo, formatAddressPointer)
Keeps the UI focused on the essential information: app name, description,
supported kinds, and platform URLs.
* feat: add website display and filter non-platform tags
NIP-89 renderer improvements:
- Add getAppWebsite() helper to extract website from content JSON
- Display website URL in both feed and detail renderers with external link
- Filter out non-platform tags (r, t, client, alt, e, p, a) to prevent garbage display
- Remove relay hint display from HandlerRecommendationDetailRenderer
- Clean up unused relayHint parameter
Fixes the 'r r' tag appearing as a platform by properly excluding
common non-platform tags when detecting platform URLs.
* refactor: create reusable ExternalLink component for consistent styling
Create ExternalLink component following patterns from HighlightRenderer and
BookmarkRenderer with:
- Two variants: 'muted' (default, text-muted-foreground with underline)
and 'default' (text-primary with hover:underline)
- Three sizes: xs, sm, base
- Configurable icon display
- Consistent truncate behavior for long URLs
- Stop propagation on click
Apply to NIP-89 renderers:
- ApplicationHandlerRenderer: uses muted variant (feed view)
- ApplicationHandlerDetailRenderer: uses default variant (detail view)
This ensures consistent link styling across the entire application
and makes it easy to maintain a unified design language.
* refactor: consolidate JSON parsing into cached getAppMetadata helper
Performance optimization:
- Create getAppMetadata() helper that parses content JSON once and caches
the result using Symbol.for('nip89-metadata') as cache key
- All metadata helpers (getAppName, getAppDescription, getAppWebsite) now
use the cached metadata instead of parsing JSON multiple times
- Prevents redundant JSON.parse() calls when multiple helpers are used
Code cleanup - removed unused functions:
- getAppImage() - no longer used after removing image display
- getHandlersByPlatform() - filtering done in component state
- substituteTemplate() - not needed in current implementation
- hasPlaceholder() - utility never used
- formatAddressPointer() - not needed anymore
Updated tests:
- Replace getAppImage tests with getAppWebsite tests
- Remove tests for deleted utility functions
- All remaining tests pass
This consolidation improves performance by ensuring JSON.parse() is called
at most once per event, regardless of how many metadata fields are accessed.
* feat: use app name in window titles for NIP-89 app events
Add special handling for kind 31990 (Application Handler) events in
getEventDisplayTitle to use the app name from content JSON instead of
generic kind name. Falls back to identifier if app name not available.
This gives NIP-89 app handler events nice readable window titles.
---------
Co-authored-by: Claude <noreply@anthropic.com>
**Compact Relay Item Display:**
- Removed left-side inbox/outbox count indicators (were causing misalignment)
- Replaced "EOSE" text with checkmark icon (✓)
- Event count shown as [N] badge (only if > 0)
- Auth icon now always visible (even for unauthenticated relays)
- Clean right-side layout: [count] [✓] [auth] [wifi]
**Always-Visible Auth Status:**
- Modified getAuthIcon() to always return an icon (never null)
- Unauthenticated relays show subtle shield icon (muted-foreground/40)
- Provides at-a-glance view of auth status for all relays
- Label: "No Authentication Required" for clarity
**Rich Hover Tooltips:**
- Comprehensive tooltip shows all relay details on hover
- Displays: connection status, auth status, subscription state, event count
- Shows inbox/outbox counts when available (moved from inline display)
- Formatted as structured table for easy scanning
- Positioned on left side to avoid blocking content
**Benefits:**
✅ Perfect alignment (no variable-width counts on left)
✅ Cleaner, more scannable visual design
✅ All information still accessible via hover
✅ Consistent icon count (always 2-4 icons per relay)
✅ Easy to spot EOSE status at a glance (green checkmark)
All 639 tests passing.
Critical fixes for ReqViewer relay state accuracy:
1. **URL Normalization Fix** (fixes mismatch with CONN):
- Added normalizeRelayURL to normalize all relay URLs in finalRelays
- RelayStateManager normalizes URLs (adds trailing slash, lowercase) but
finalRelays did not, causing lookup failures in relayStates
- Now normalizedRelays is used for all state lookups and passed to
useReqTimelineEnhanced to ensure consistency
- This fixes the bug where ReqViewer showed different connected relay
counts than CONN viewer
2. **EOSE Indicator**:
- Added back EOSE indicator to relay dropdown (was removed in UI redesign)
- Shows subtle "EOSE" text when relay has sent End of Stored Events
- Includes tooltip explaining "End of stored events received"
3. **Muted Icons** (per user request for subtlety):
- Type indicators: blue-500/purple-500 → muted-foreground/60
- Strategy header icons: all → muted-foreground/60
- Section headers: green-500 → muted-foreground
- Connection icons: green-500/yellow-500/red-500 → /70 opacity variants
- Auth icons: same color reduction for consistency
- Maintains semantic meaning while reducing visual noise
All 639 tests passing.
Critical Edge Case Fix:
Previously, when all relays disconnected before sending EOSE, the state
remained stuck in LOADING because overallEoseReceived stayed false.
Solution: Check if all relays are in terminal states
- Terminal states: eose, error, or disconnected
- If all terminal AND no overall EOSE yet, derive state from events:
* No events → FAILED
* Has events, all disconnected, streaming → OFFLINE
* Has events, all disconnected, non-streaming → CLOSED
* Some active, some terminal → PARTIAL
New Test Coverage (5 tests):
1. All relays disconnect before EOSE, no events → FAILED
2. All relays disconnect before EOSE, with events (streaming) → OFFLINE
3. All relays disconnect before EOSE, with events (non-streaming) → CLOSED
4. Some EOSE, others disconnect before EOSE → PARTIAL
5. Mix of EOSE and errors, all terminal → PARTIAL
This fixes the user-reported issue where disconnected relays show LOADING
instead of transitioning to appropriate terminal state.
Tests: 639/639 passing (added 5 new edge case tests)
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
- Create src/lib/nostr-kinds.ts with:
- Re-exports from nostr-tools/kinds (isRegularKind, isReplaceableKind, etc.)
- New isParameterizedReplaceableKind() function
- New isAddressableKind() for determining naddr vs nevent encoding
- NIP-01 boundary constants with clarifying comments
- getKindCategory() for display purposes
- Update KindRenderer.tsx to use shared utilities:
- Replace inline range checks with helper functions
- Fix "Regular Lists" -> "Replaceable Events" naming
- Simplify redundant condition (isReplaceableKind includes kinds 0, 3)
- Update BaseEventRenderer.tsx to use isAddressableKind()
- Add comprehensive tests for all utilities
- Add -v, --view <list|compact> flag to req command
- Remove UI toggle controls from ReqViewer header
- View mode is now set via command flag instead of runtime toggle
- Update man page with new flag documentation and example
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Redesign SpellbookDropdown with clear status indicators (ownership, storage)
- Add SpellbookStatus component showing you/other and local/published/network
- Enhance activeSpellbook type with source, localId, isPublished fields
- Fix PublishSpellbook action to properly yield events (caller handles side-effects)
- Add k tags extraction from REQ windows for kind-based filtering/discovery
- Update terminology from "Layout" to "Spellbook" consistently
- Add comprehensive tests for k tags and source tracking
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Improve the preview route UX with better loading states, error handling, and metadata display. Add version comparison logic and conflict resolution dialog for handling local vs network spellbook conflicts.
Changes:
- Enhanced preview route in Home.tsx:
- Add loading state with spinner while resolving actor
- Add NIP-05 resolution timeout (10 seconds)
- Display error banner for resolution failures
- Show author name and creation date in preview banner
- Add copy link button to share spellbook easily
- Improve banner layout with metadata
- Add compareSpellbookVersions() in spellbook-manager.ts:
- Detects conflicts between local and network versions
- Compares timestamps, workspace counts, window counts
- Identifies newer version and content differences
- Returns structured comparison data
- Create ConflictResolutionDialog component:
- Side-by-side comparison of local vs network versions
- Shows metadata: timestamps, counts, author, publish status
- Clear explanation of resolution choices
- Accessible UI with proper button hierarchy
TypeScript compilation successful ✅🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add activeSpellbook to GrimoireState
- Display active spellbook title in header with clear button
- Add 'Update Layout' and 'Save as new' to SpellbookDropdown
- Highlight active spellbook in dropdown list
- Add 'Manage Spells' link to dropdown
- Refine dropdown styles (muted hover, no accent color)
Implement renderers for NIP-34 repository state announcements:
- Add helper functions in nip34-helpers.ts to parse HEAD refs, branches, and tags
- Create RepositoryStateRenderer for compact feed view showing push notifications
- Create RepositoryStateDetailRenderer for detailed view with all refs
- Register both renderers in the kind registry
Feed view shows: "pushed <commit> to <branch> in <repo>"
Detail view shows: HEAD info, all branches, all tags with copyable commit hashes