Critical UX improvement: Only clear draft and show preview if at least
one relay succeeded.
**The Problem:**
- When publishing failed to ALL relays, we still cleared the draft
- User lost their content with no way to retry
- Frustrating experience when all relays are down
**The Solution:**
- Track success/failure by returning result from each publish promise
- Count successful publishes from Promise.allSettled results
- Only clear draft and show preview if successCount > 0
- Keep editor visible with content if all relays fail
**User Experience:**
- All relays succeed → Clear draft, show preview, success toast
- Some relays succeed → Clear draft, show preview, warning toast
- All relays fail → Keep editor with content, error toast, can retry
**Toast Messages:**
- All succeeded: "Published to all X relays"
- Partial success: "Published to X of Y relays"
- Total failure: "Failed to publish to any relay. Please check..."
Update PostViewer to use the new GRIMOIRE_CLIENT_TAG constant from main
which includes the NIP-89 app definition address (31990) as the third
element.
Before: ["client", "grimoire"]
After: ["client", "grimoire", "31990:..."]
This allows Nostr clients to display a clickable link to Grimoire's
app definition event.
Use Promise.allSettled instead of Promise.all to wait for all relay
publish attempts to complete, regardless of individual success/failure.
More defensive and semantically correct since we're handling errors
individually per relay and want all attempts to finish.
Critical bug fix: Separate signing from publishing to handle failures correctly.
**The Problem:**
- When user rejected signing (or signing failed), we treated it as a publishing failure
- Set all relay states to "error" even though we never tried to publish
- Destroyed the user's post content
**The Solution:**
- Split signing and publishing into separate try-catch blocks
- If signing fails: show toast, reset isPublishing, and RETURN EARLY
- User keeps their content and can try again
- Only update relay states if we actually attempted to publish
**User Experience:**
- User rejects signing → toast appears, post is preserved, can edit or try again
- Publishing fails → relay states show errors, can retry individual relays
- No more losing your carefully crafted post to a rejected signature!
**Removed JSON preview feature:**
- Removed showEventJson setting and all related UI
- Removed draftEventJson state and generation logic
- Removed "Show event JSON" checkbox from settings
- Simplified PostViewer code by ~100 lines
- Cleaner, production-ready codebase
**Ensured proper content serialization:**
- Added renderText() to Mention extension to serialize as nostr:npub URIs
- Added renderText() to BlobAttachmentRichNode to serialize URLs
- NostrEventPreviewRichNode already had renderText() for nostr:note/nevent/naddr
- Editor now properly converts all rich content to plain text:
- @mentions → nostr:npub...
- Event references → nostr:note/nevent...
- Address references → nostr:naddr...
- Blob attachments → URLs
- Custom emojis → :shortcode:
**Result:**
- Cleaner, simplified code ready for production
- All editor elements properly serialize to content string
- JSON preview can be re-added later with better implementation
This commit improves the code quality and UX of the POST command:
**Debounced onChange Pattern:**
- Added onChange prop to RichEditor that fires on every content change
- Replaced interval-based updates with proper debounced handlers
- Draft saves debounced to 2000ms (unchanged behavior, cleaner code)
- JSON updates debounced to 200ms (same responsiveness, event-driven)
- Much cleaner React pattern - no weird intervals
**JSON Preview Improvements:**
- Removed "Event JSON" title header for cleaner look
- Shows signed event JSON when published and setting is enabled
- Shows draft (unsigned) JSON when composing
- Always scrollable up to 400px height
**Technical Improvements:**
- Added onUpdate handler to TipTap editor config
- Proper cleanup of debounce timeouts on unmount
- Type-safe timeout refs using ReturnType<typeof setTimeout>
This commit addresses UX feedback on the JSON preview feature:
- Reduced update interval from 2000ms to 200ms for much more responsive UI
- Moved JSON preview below relay list for better visual flow
- Increased max-height from 256px to 400px for better visibility
- Separated JSON update logic into dedicated effect for cleaner code
- JSON preview now feels instant when typing (10x faster updates)
The JSON preview is now much more pleasant to use and doesn't feel
laggy when editing content.
This commit enhances the POST command with a live JSON preview feature:
- Added generateDraftEventJson() that builds unsigned draft event from editor content
- JSON preview updates every 2 seconds when "Show event JSON" setting is enabled
- Extracts all tags from editor: mentions (p), events (e), addresses (a), emojis, blobs (imeta)
- Displays as "Event JSON (Draft - Unsigned)" to distinguish from signed events
- Uses CopyableJsonViewer component for syntax highlighting and copying
The live preview helps users understand event structure before publishing.
Settings (persist in localStorage, global across all post windows):
- "Include client tag" - toggle whether to add client tag to events (default: true)
- "Show event JSON" - display copyable JSON preview of event (default: false)
Features:
- Settings stored in localStorage at "grimoire-post-settings"
- Settings persist across sessions and windows
- Event JSON preview shows unsigned event initially, updates to signed version after signing
- Preview displays "(Signed)" or "(Unsigned)" label
- Uses CopyableJsonViewer component for syntax highlighting and copy button
- Preview limited to max-h-64 with scrolling
Draft persistence improvements:
- Added relays now saved in draft state under "addedRelays" field
- On draft load, custom relays are restored to relay list
- Relays identified as "added" if not in user's write relay list
Technical details:
- Added PostSettings interface with includeClientTag and showEventJson fields
- Settings loaded on mount from localStorage, saved on change via useEffect
- updateSetting callback handles individual setting updates
- previewEvent state holds current event (unsigned or signed)
- setPreviewEvent called before and after signing in handlePublish
- Draft format: { editorState, selectedRelays, addedRelays, timestamp }
- Draft loading checks addedRelays and restores non-duplicate relays
- Show auth status icon next to connectivity icon for each relay
- Uses getAuthIcon() utility from relay-status-utils
- Displays shield icons for authenticated/failed/rejected states
- Includes tooltip with auth status label
- Make draft persistence window-specific using window ID
- Draft key format: `grimoire-post-draft-{pubkey}-{windowId}`
- Each post window maintains its own independent draft
- Falls back to pubkey-only key when windowId not available
- Remove toast notification when adding relays (less noisy UX)
- Still shows error toast if relay URL is invalid
- Pass windowId prop from WindowRenderer to PostViewer
Technical details:
- Import getAuthIcon from @/lib/relay-status-utils
- Import useRelayState hook to get relay auth status
- Add PostViewerProps interface with optional windowId
- Update all draft key computations to include windowId
- Update dependency arrays for useEffect/useCallback with windowId
- Get relay state via getRelay(url) for auth icon display
- Show relay connectivity status (Server/ServerOff icons) next to each relay in list
- Add input widget to add custom relays during post composition
- Validates relay URLs (supports with/without wss:// prefix)
- Normalizes URLs automatically (defaults to wss://)
- Shows Plus button enabled only when input is valid
- Press Enter or click Plus to add relay
- Improve signing failure handling: draft is only cleared after successful publish
- New relay input widget disabled during preview mode
- Connect to relay pool state using use$() hook for reactive connectivity status
Technical details:
- Import Server, ServerOff, Plus icons and Input component
- Add newRelayInput state for relay URL input
- Use pool.relays$ observable to get real-time connection state
- Create isValidRelayInput() validator with URL pattern matching
- Create handleAddRelay() to normalize and add relays to list
- Update relay list rendering to show connectivity icons
- Add input + button widget after relay list
- Add Discard button to clear editor state and draft
- Add Settings dropdown with client tag toggle (enabled by default)
- Limit publish button width to w-32 and push to right with spacer
- Conditionally include client tag based on setting
- Improve action button layout with better spacing
- Add X button on hover to NostrEventPreviewRich (same pattern as BlobAttachmentRich)
- Fix cursor positioning after pasting nevent/naddr by tracking node sizes correctly
- Import TextSelection from prosemirror-state to properly set cursor position
- Change from `tr.insert(from + index, node)` to `tr.insert(insertPos, node)` with cumulative position tracking
Previously, cursor would stay behind the event embed node after pasting.
Now it correctly positions after the inserted content.
Add max-w-2xl (768px) with mx-auto to center and constrain post content
width, similar to ZapWindow. Prevents content from becoming too wide on
large screens while maintaining good readability.
- Change layout from flex-col with flex-1 to overflow-y-auto on outer container
- Remove flex-1 from all child elements to prevent layout issues with complex posts
- Hide scrollbar in RichEditor for cleaner appearance
- Single scrollbar on container prevents double-scrollbar issues
- Editor now truly full-width without extra padding
This creates a more predictable layout that avoids overlaps and weird
behavior when posts contain lots of content (images, videos, embedded events).
When pasting event URLs in the editor, use KindRenderer (feed) and
EventCardSkeleton instead of DetailKindRenderer and EventDetailSkeleton.
Event previews in the editor are inline/compact like feed items, so they
should use the feed renderer rather than the detail view renderer.
- Fix double scrollbar issue by using flexbox layout instead of overflow-auto
- Disable relay checkboxes during preview mode (can't change after publish)
- Use aggregator relays as fallback when no write relays configured
- Remove conditional error message since fallback relays always available
- Improve layout responsiveness with flex-1 and flex-shrink-0 classes
Aggregator relays (nos.lol, relay.snort.social, relay.primal.net, relay.damus.io)
ensure users can always publish even without configured write relays.
After successful publish, replace editor and action buttons with:
- Kind1Renderer preview of the published event
- Reset button to compose another post
- Relay list remains visible with publish status indicators
Users can click "Compose Another Post" to reset the form and
return to the editor for the next post.
Event reuse for retries:
- Add lastPublishedEvent state to store the signed event
- Store signed event after creation in handlePublish()
- Simplify retryRelay() to republish the same signed event
- Remove duplicate event creation and signing in retryRelay()
- Remove duplicate client tag addition in retryRelay()
Benefits:
- Same event ID and signature across all relays
- More efficient - no need to re-sign
- Consistent event across retries
- Single source of truth for published event
Flow:
1. User publishes - event created, signed, stored
2. Some relays fail - event remains in state
3. User retries - same event republished to failed relay
4. New post overwrites lastPublishedEvent with new event
Content trimming:
- Trim content in handlePublish() before building event
- Trim content in retryRelay() before building event
- Removes leading and trailing whitespace from published notes
- Prevents accidental whitespace-only or padded messages
- Applied via content.trim() before factory.build()
Client tag:
- Add ["client", "grimoire"] tag to all published events
- Added to both handlePublish and retryRelay functions
- Identifies posts as coming from Grimoire client
UI cleanup:
- Remove reset button from relay selection UI
- Relay selection persists across sessions via draft storage
- Simplify header to just show relay count
- Remove unused RotateCcw icon import
Tag generation order (final):
1. p tags (mentions)
2. e tags (event references)
3. a tags (address references)
4. client tag (grimoire)
5. emoji tags (NIP-30)
6. imeta tags (NIP-92)
Nostr tag extraction:
- Extract p tags from @mentions (pubkey references)
- Extract e tags from note/nevent references (event ID references)
- Extract a tags from naddr references (address/parameterized replaceable)
- Update SerializedContent interface to include mentions, eventRefs, addressRefs
- Serialize editor content walks all node types to extract references
- Build complete tag array for kind 1 events with proper NIP compliance
Retry failed relays:
- Add retryRelay() function to republish to specific failed relay
- Make error icon clickable with hover state
- Show "Click to retry" in tooltip
- Rebuild event and attempt publish again
- Update status indicators in real-time
Disable publish when empty:
- Track isEditorEmpty state
- Update every 2 seconds along with draft save
- Disable publish button when editor isEmpty()
- Prevents publishing empty notes
Tag generation order:
1. p tags (mentions)
2. e tags (event references)
3. a tags (address references)
4. emoji tags (NIP-30)
5. imeta tags (NIP-92 blob attachments)
This ensures proper Nostr event structure with all referenced pubkeys, events, and addresses tagged.
Draft persistence improvements:
- Store complete editor state as JSON (preserves blobs, emojis, mentions, formatting)
- Save selected relays array in draft
- Restore full editor content using setContent() method
- Maintain draft per-user with pubkey-based key
RichEditor enhancements:
- Add getJSON() method to export editor state
- Add setContent() method to restore from JSON
- Enables lossless draft save/restore
UI improvements:
- Change "Relays" label to plain text without Label component
- Keep unselected relays visible during/after publish
- Only update status for selected relays during publish
Draft storage format:
{
editorState: {...}, // Full TipTap JSON state
selectedRelays: [...], // Array of selected relay URLs
timestamp: 1234567890
}
Draft persistence:
- Save draft content to localStorage every 2 seconds
- Load draft on component mount (per-user with pubkey)
- Clear draft after successful publish
- Prevent data loss on tab change or page reload
UI improvements:
- Reset button now icon-only (RotateCcw) with tooltip
- Remove borders from relay list items
- Use RelayLink component for relay rendering
- Show relay icons and secure/insecure indicators
- Cleaner, more compact relay list appearance
- Increased spacing between relay items (space-y-1)
Storage key format: grimoire-post-draft-{pubkey}
UI improvements:
- Remove flex-1 from editor, use fixed min/max height
- Move upload button next to publish button (icon-only)
- Place relay selector below action buttons (not fixed)
- Limit image/video previews to max-h-96
- Add pointer-events-none to event previews to prevent accidental interaction
- Use EventDetailSkeleton for loading event previews
Layout changes:
- Single scrollable container with space-y-4
- Editor: 150-400px height range
- Action buttons in single row (upload icon + publish button)
- Relay list with max-h-64 scroll area
- Better spacing and visual hierarchy
Phase 2 of RichEditor variant implementation:
- Create RichEditor component based on MentionEditor
- No slash commands (removed SlashCommand extension)
- Multi-line support with Enter for newlines
- Ctrl/Cmd+Enter to submit
- Full-size image/video previews using BlobAttachmentRich
- Full event rendering using NostrEventPreviewRich
- Configurable min/max height (defaults: 200px-600px)
- Retains @mentions and :emoji: autocomplete
- Reuses shared extensions (NostrPasteHandler, FilePasteHandler)
- Targets kind 1 notes composition
Key differences from MentionEditor:
- Block-level rich previews instead of inline badges
- Multi-line editing without Enter-to-submit
- Resizable with overflow-y: auto
- No command palette functionality
Phase 1 of RichEditor variant implementation:
- Extract NostrPasteHandler extension for reuse across editors
- Extract FilePasteHandler extension for clipboard file handling
- Create NostrEventPreviewRich React node view using DetailKindRenderer
- Create BlobAttachmentRich React node view with full-size media previews
- Create rich TipTap node extensions using ReactNodeViewRenderer
- Update MentionEditor to use shared extensions
These shared components will be used by the upcoming RichEditor
variant for long-form content composition with full previews.
Removes drag-and-drop file handling while keeping clipboard paste functionality.
Drag-and-drop will be implemented differently later.
Changes:
- Rename FileDropHandler → FilePasteHandler
- Remove handleDrop and dragover event handlers
- Keep handlePaste for clipboard file support
- Rename onFileDrop prop → onFilePaste for clarity
- Update ChatViewer to use onFilePaste callback
- Update all references and comments
Result:
- Paste images from clipboard (Ctrl+V) still works
- Drag-and-drop removed completely
- Clearer naming reflects actual functionality
When pasting npub or nprofile URIs, the editor now looks up the profile
from the event store cache and uses the display name if available.
Changes:
- Add getDisplayNameForPubkey helper function
- Import eventStore and profile helpers
- Synchronously check if profile is cached via .value property
- Use getDisplayName from nostr-utils for consistent formatting
- Falls back to short pubkey (first 8 chars) if profile not cached
Result:
- Paste npub1abc... → shows "@alice" if profile is cached
- Paste npub1abc... → shows "@abc12345" if profile not cached
- Consistent with typed @mentions behavior
- No async delays or loading states needed
The drag-and-drop wasn't working because we need to handle dragover
events and call preventDefault() to signal that drops are allowed.
Changes:
- Add handleDOMEvents with dragover handler to FileDropHandler
- Check for Files type in dataTransfer
- Set dropEffect to 'copy' for visual feedback
- Call preventDefault() to allow drops
Without this, the browser blocks all drops by default and handleDrop
never fires. This is a standard requirement for HTML5 drag-and-drop API.
Adds comprehensive file upload support via drag-and-drop and paste
directly into the chat composer editor.
Changes to MentionEditor:
- Add FileDropHandler TipTap extension to intercept file drops/pastes
- Filter for valid media types (image/video/audio)
- Add onFileDrop callback prop to communicate with parent
- Handle both drag-and-drop and clipboard paste events
Changes to useBlossomUpload hook:
- Update open() method to accept optional File[] parameter
- Add initialFiles state to track pre-selected files
- Clear initialFiles when dialog closes
- Pass initialFiles to BlossomUploadDialog component
Changes to BlossomUploadDialog:
- Add initialFiles prop for pre-selecting files
- Auto-select first file when initialFiles provided
- Generate preview URL for images/video on initialization
- Seamless UX: dropped files immediately appear in dialog
Changes to ChatViewer:
- Wire up onFileDrop callback to open upload dialog
- Pass dropped files to upload dialog via open(files)
- Fix onClick handler to properly call openUpload()
User experience:
- Drag image/video/audio file onto chat composer → upload dialog opens
- Paste image from clipboard → upload dialog opens
- File automatically selected and previewed
- Click upload to complete (same as manual file selection)
Technical details:
- Uses ProseMirror Plugin API for drop/paste interception
- File type validation: /^(image|video|audio)\//
- Bonus: Also works with clipboard paste (Ctrl+V)
- Clean state management with automatic cleanup
Major simplification of the nostr bech32 preview display:
Profile handling (npub/nprofile):
- Now creates regular @mention nodes instead of custom preview nodes
- Reuses existing mention infrastructure, styling, and UserName component
- Displays as "@username" with existing mention chip styling
- Serializes to nostr:npub1... on submit (same as manual @mentions)
Event/Address display (note/nevent/naddr):
- Removed emoji icons for cleaner, more minimal appearance
- Display format: "event abc12345" for note/nevent
- Display format: "address article-slug" for naddr (shows d identifier)
- Falls back to short pubkey if naddr has no d identifier
- Simple text-only chips with type + identifier
Benefits:
- Less visual noise (no emojis)
- Consistent mention styling for all profiles
- Profile mentions can now be clicked/hovered like manual mentions
- Smaller code footprint (removed complex icon mapping logic)
- Better UX: profiles look and behave like regular mentions
Technical changes:
- Paste handler creates mention nodes for npub/nprofile
- NostrEventPreview only handles note/nevent/naddr now
- Removed npub/nprofile from serialization (handled by mention serializer)
- Updated type definitions to reflect reduced scope
Implements paste handling for nostr: URIs (npub, note, nevent, naddr, nprofile)
that transforms them into rich inline preview chips in the chat composer.
Changes:
- Add NostrEventPreview TipTap node type for displaying bech32 previews
- Add NostrPasteHandler extension to detect and transform pasted bech32 strings
- Update serializeContent to convert previews back to nostr: URIs on submit
- Add CSS styling for preview chips with hover effects
- Support all major bech32 types: npub, note, nevent, naddr, nprofile
Features:
- Automatic detection of nostr: URIs in pasted text (with or without prefix)
- Visual preview chips with type icon, label, and truncated ID
- Maintains nostr: URI format in final message content (NIP-27 compatible)
- Simple implementation without event fetching (fast, no loading states)
- Styled with primary theme colors for consistency
Technical details:
- Uses ProseMirror Plugin API for paste interception
- Inline atomic nodes for previews (similar to mentions and blob attachments)
- Regex pattern matches all valid bech32 formats
- Proper error handling for invalid bech32 strings
- Extensible foundation for future rich metadata fetching
Phase 3: Create POST command for publishing kind 1 notes
Features:
- RichEditor component with @mentions, :emoji: autocomplete
- Image/video upload via Blossom with drag-and-drop
- Relay selection UI with write relays pre-selected by default
- Per-relay publish status tracking (loading/success/error)
- Submit with Ctrl/Cmd+Enter keyboard shortcut
- Multi-line editing with rich previews for attachments
- NIP-30 emoji tags and NIP-92 imeta tags for attachments
Implementation:
- Add 'post' to AppId type
- Add POST command to man pages
- Create PostViewer component using RichEditor
- Wire PostViewer into WindowRenderer
- Publish events using EventFactory and RelayPool
- Track per-relay status with visual indicators
Usage:
- Run 'post' command to open the composer
- Type content with @mentions and :emoji:
- Upload media via button or drag-and-drop
- Select/deselect relays before publishing
- Press Publish button or Ctrl/Cmd+Enter to post
- View per-relay publish status in real-time
* feat: show client tag in event header
Display "via <client>" after the timestamp in BaseEventContainer
when the event has a client tag. Uses compact 10px font with
reduced opacity to minimize visual noise.
* feat: make client tag link to NIP-89 app definition
When the client tag has a third element with a valid 31990 address
(NIP-89 app handler), make the client name clickable to open the
app definition event.
* fix: use parseAddressPointer from nip89-helpers instead of non-existent parseCoordinate
* feat: add NIP-89 app address to client tag
- Add GRIMOIRE_APP_ADDRESS and GRIMOIRE_CLIENT_TAG constants
- Update all client tag usages to include the 31990 app definition address
- Update tests to verify the app address is included
- Update spell.ts documentation
---------
Co-authored-by: Claude <noreply@anthropic.com>
ZapWindow now correctly handles naddr pointers by:
- Using useNostrEvent hook which supports both EventPointer and AddressPointer
- Falling back to addressPointer.pubkey when event hasn't loaded yet
Previously, zapping an naddr would fail because the component only
tried to load events via eventPointer.id, ignoring addressPointer.
Co-authored-by: Claude <noreply@anthropic.com>
When zapping addressable events (like spellbook kind 30777), the zapEvent
function was passing an AddressPointer as eventPointer. This caused an error
because ZapWindow expects eventPointer to have an `id` property, while
AddressPointer has `kind`, `pubkey`, `identifier`.
Fixed by:
- Always passing eventPointer with the event's id for the e-tag
- Additionally passing addressPointer for addressable events for the a-tag
Co-authored-by: Claude <noreply@anthropic.com>
* feat(user-menu): make relays and blossom servers collapsible dropdowns
Convert the relays and blossom servers sections in the user menu from
always-expanded lists to collapsible dropdown submenus. Each section
now shows a count next to the title and expands on click to show the
full list. This saves vertical space in the menu when users have many
relays or servers configured.
* refactor(user-menu): reorganize menu items for better UX
Reorder menu sections by logical grouping:
- Identity (user profile) at top
- Account config (wallet, relays, blossom) grouped together
- App preferences (theme) in consistent location
- Promotional (support grimoire) separated
- Session actions (login/logout) at bottom
This provides a more intuitive flow and consistent placement
regardless of login state.
* refactor(user-menu): revert dropdowns, add login/logout icons
- Revert relays and blossom servers back to flat lists (not dropdowns)
- Move login to first option for logged out users
- Add LogIn/LogOut icons from lucide-react
- Keep the reorganized menu structure
* style(user-menu): clean up icons and font consistency
- Make theme icon muted like other icons
- Remove HardDrive icons from blossom servers section
- Make blossom label consistent with relays (plain text)
- Remove unused HardDrive import
* fix(user-menu): close menu when clicking relays, blossom, or donate
Wrap RelayLink items in DropdownMenuItem with asChild to properly
trigger menu close on click. Convert Support Grimoire section from
a raw div to DropdownMenuItem so it also closes the menu.
* fix(user-menu): properly close menu when clicking relay items
Use pointer-events-none on RelayLink to make it purely presentational,
and handle the click on DropdownMenuItem instead. This ensures the
menu closes properly when clicking a relay.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: truncate long filenames in blossom upload dialog
- Add line-clamp-1 and proper width constraints to prevent long filenames
from breaking layout
- Update default fallback server to blossom.band
* fix: use max-w-xs for proper filename truncation
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add NIP-75 Zap Goal rendering
Add feed and detail view rendering for kind 9041 (Zap Goals):
- GoalRenderer: Shows clickable title, description, and progress bar
with target/raised amounts
- GoalDetailRenderer: Adds sorted contributor breakdown with
individual contribution totals
- nip75-helpers: Helper functions for extracting goal metadata
(amount, relays, deadline, beneficiaries)
Both views fetch and tally zaps from the goal's specified relays.
* fix: improve NIP-75 goal rendering
- Remove icons from goal renderers
- Remove "sats" suffixes from amounts
- Use muted text instead of destructive color for closed goals
- Content is the title, summary tag is the description
- Only show description if summary tag exists
* feat: add zap button to goal renderers
Show a 'Zap this Goal' button in both feed and detail views
when the goal is still open (not past its closed_at deadline).
* style: unify progress indicator styles
- Update user menu and welcome page progress indicators to match
goal renderer style: bar on top, progress/total below with percentage
- Remove "sats" suffix from progress displays
- Make goal zap button primary variant and full-width
* fix: polish goal renderers for release
- Remove limit parameter from useTimeline (use whatever relays send)
- Add more spacing between "Support Grimoire" header and progress bar
* refactor: extract useGoalProgress hook for NIP-75 goals
- Create useGoalProgress hook with shared goal logic
- Handle relay selection: goal relays → user inbox → aggregators
- Calculate progress, contributors, and all metadata in one place
- Simplify both GoalRenderer and GoalDetailRenderer
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(nip-29): fetch admin and member lists in parallel and correctly tag admins
This commit improves NIP-29 group member fetching with two key changes:
1. **Parallel fetching**: Admins (kind 39001) and members (kind 39002) are now
fetched in parallel using separate subscriptions, improving performance
2. **Correct admin role tagging**: Users in kind 39001 events now correctly
default to "admin" role instead of "member" when no explicit role tag is
provided, as per NIP-29 spec
Changes:
- Split participantsFilter into separate adminsFilter and membersFilter
- Use Promise.all to fetch both kinds in parallel
- Updated normalizeRole helper to accept a defaultRole parameter
- Process kind 39001 with "admin" default, kind 39002 with "member" default
- Added clearer logging for admin/member event counts
Related: src/lib/chat/adapters/nip-29-adapter.ts:192-320
* refactor(nip-29): simplify parallel fetch using pool.request
Use pool.request() with both filters in a single call instead of manual
subscription management. This is cleaner and more idiomatic:
- Auto-closes on EOSE (no manual unsubscribe needed)
- Fetches both kinds (39001 and 39002) in parallel with one request
- Reduces code complexity significantly
* fix(nip-29): use limit 1 for replaceable participant events
Kinds 39001 and 39002 are replaceable events with d-tag, so there should
only be one valid event of each kind per group. Changed limit from 5 to 1.
* fix(chat): change "Sign in to send messages" to "Sign in to post"
Simplified the login prompt text in chat interface for clarity.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* style: make kind label in event menu more subtle
Reduced visual prominence of kind label in generic event menu:
- Smaller text size (text-xs)
- Reduced gap between elements (gap-2 instead of gap-4)
- More muted colors (text-muted-foreground, opacity-60)
- Subtler icon (text-muted-foreground/60)
The label now appears as a small informational element rather than
looking like an interactive dropdown item.
* refactor: remove kind label from menu, add context menu to default renderer
1. Removed kind label from EventMenu dropdown
- Kind is already shown in EventFooter, making it redundant
- Removed DropdownMenuLabel with kind badges
- Removed unused KindBadge import
2. Added EventContextMenu component
- Same functionality as EventMenu but triggered by right-click
- Reuses all the same menu items (Open, Zap, Copy ID, View JSON)
- Supports chat option for kind 1 notes
3. Updated DefaultKindRenderer
- Wrapped content with EventContextMenu
- Generic events now have context menu on right-click
- Updated documentation to indicate right-click access
This provides a cleaner UI by removing redundant information and gives
users a consistent way to interact with all events, including generic
ones that don't have custom renderers.
* fix: add context menu to all events in BaseEventContainer
Moved EventContextMenu from DefaultKindRenderer to BaseEventContainer
so all events (not just generic ones) have context menu support.
Now all events in feeds have both interaction methods:
- Tap/click the menu button (three dots) for dropdown menu
- Right-click or long-press anywhere on the event for context menu
This provides a consistent experience across all event types and
makes the context menu accessible for all custom renderers, not just
the default one.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: remove redundant reply preview for NIP-10 root replies
In NIP-10 thread chats, messages replying directly to the thread root
no longer show a reply preview, since replying to the root is implicit
in the thread chat context.
Reply previews are still shown for replies to other messages within
the thread, maintaining proper conversation threading.
Changes:
- Modified eventToMessage() in nip-10-adapter.ts to only set replyTo
when a message has a NIP-10 "reply" marker (replying to another message)
- Messages with only a "root" marker now have replyTo=undefined
- Prefixed unused rootEventId parameter with underscore to fix lint warning
* fix: hide reply button and menu for NIP-10 root message
In NIP-10 thread chats, the root message now cannot be replied to
directly, as all messages in the thread are implicitly replies to
the root.
Changes:
- Added isRootMessage prop to MessageItem component
- Hide reply button when message is the root message
- Remove reply option from context menu for root message
- Root message check: protocol === "nip-10" && message ID matches rootEventId
* revert: restore reply preview logic in NIP-10 adapter
Reverted changes to eventToMessage() that removed reply previews
for root replies. The UI-level hiding of reply interactions on the
root message is sufficient - reply previews can still be shown for
consistency with other messages.
Changes:
- Restored rootEventId parameter (removed underscore prefix)
- Restored full reply detection logic (reply, root, or fallback)
- Reply previews now shown for all replies including root replies
* chore: remove tsconfig.node.tsbuildinfo from repository
This build artifact is already gitignored (*.tsbuildinfo) but was
previously committed. Removing it from the repository as it should
not be tracked in version control.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: convert NIP-10 chat zap amounts to sats
The NIP-10 adapter was storing zap amounts in millisats instead of sats,
which was inconsistent with:
- The MessageMetadata type definition (zapAmount documented as "Amount in sats")
- Other chat adapters like NIP-53 which correctly convert to sats
- UI expectations for displaying zap amounts
This fix adds the millisat-to-sat conversion (Math.floor(amount / 1000))
matching the implementation in the NIP-53 adapter.
Without this fix, zap amounts would display 1000x larger than intended
(e.g., a 1000 sat zap would show as 1,000,000).
* chore: update TypeScript build info
---------
Co-authored-by: Claude <noreply@anthropic.com>
Changed the supporter badge icon from Zap to Flame in the UserName component for a more distinctive visual indicator of Grimoire supporters.
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add comprehensive NIP-10 thread chat design documentation
Add detailed design documents for implementing NIP-10 thread chat feature:
- nip10-thread-chat-design.md: Full architecture, data structures, adapter
implementation plan, relay selection strategy, UI requirements, and 7-phase
implementation checklist
- nip10-thread-chat-examples.md: Complete code examples showing identifier
parsing, conversation resolution, message loading, reply sending with proper
NIP-10 tags, and ChatViewer integration
- nip10-thread-chat-summary.md: Quick reference with visual comparisons,
architecture diagrams, protocol comparison table, data flow, and FAQ
The feature will enable "chat nevent1..." to display kind 1 threaded
conversations as chat interfaces, with the root event prominently displayed
at the top and all replies shown as chat messages below.
Key design decisions:
- Use e-tags with NIP-10 markers (root/reply) instead of q-tags
- Merge multiple relay sources (seen, hints, outbox) for coverage
- Display root event centered with full feed renderer
- Reuse existing ChatViewer infrastructure via adapter pattern
- Support both nevent (with relay hints) and note (ID only) formats
* feat: implement NIP-10 thread chat support
Add complete NIP-10 thread chat implementation enabling "chat nevent..." to
display kind 1 threaded conversations as chat interfaces.
**Type Definitions**:
- Add ThreadIdentifier for nevent/note event pointers
- Add "nip-10" to ChatProtocol type
- Extend ConversationMetadata with thread-specific fields (rootEventId,
providedEventId, threadDepth, relays)
**NIP-10 Adapter** (src/lib/chat/adapters/nip-10-adapter.ts):
- parseIdentifier: Decode nevent/note format, reject non-kind-1 events
- resolveConversation: Fetch provided event, find root via NIP-10 refs,
determine conversation relays (merge hints, outbox, fallbacks)
- loadMessages: Subscribe to kind 1 replies, kind 7 reactions, kind 9735 zaps
- sendMessage: Build proper NIP-10 tags (root/reply markers), add p-tags for
all participants
- sendReaction: Send kind 7 with proper event/author references
- Smart relay selection: Merges seen relays, nevent hints, author outbox,
user outbox (limit 7 relays for performance)
**ChatViewer Updates**:
- Detect NIP-10 threads (protocol === "nip-10")
- Fetch and display root event at top (centered with KindRenderer)
- Show visual separator ("Replies") between root and messages
- Update empty state message for threads ("No replies yet...")
- Enhanced header: Show "Author • Preview" for thread chats
- Update getAdapter to handle "nip-10" protocol
**Chat Parser**:
- Add Nip10Adapter to priority list (before other adapters to catch
nevent/note)
- Update error message with nevent/note format examples
- Update adapter priority documentation
**Component Enhancements**:
- ReplyPreview: Show "thread root" when replying to root event (NIP-10)
- RelaysDropdown: Support conversation.metadata.relays for thread relay
breakdown
- ChatMessageContextMenu: Add "Zap" option to context menu (opens ZapWindow)
**Features**:
- Root event displayed with full feed renderer (can interact: like, zap, etc.)
- All replies shown as chat messages with proper threading
- Reply/React/Zap options on all messages
- Relay dropdown shows breakdown of thread relays
- Participants dropdown shows all thread participants
- @ mention autocomplete works for participants
- Proper NIP-10 tag structure for nested replies
- Smart relay selection for maximum reach
**Usage**:
chat nevent1qqsxyz... # Thread with relay hints
chat note1abc... # Thread with event ID only
Root event is centered at top, all replies below as chat messages. Sending
replies creates kind 1 events with proper NIP-10 root/reply markers and
p-tags for all participants.
* fix: remove unused imports and mark intentionally unused parameter
- Remove unused getTagValues import
- Prefix providedEvent parameter with underscore to indicate intentional non-use (reserved for future relay hint extraction)
* feat: hide reply preview when directly replying to root in NIP-10 threads
In NIP-10 thread chats, the root event is already displayed prominently at
the top, so showing a reply preview when replying directly to the root is
redundant.
Changes:
- Skip reply preview for regular messages when replying to thread root
- Skip reply preview for zap messages when zapping the thread root
- Root event remains visible at top as reference point
This reduces visual clutter and makes the chat feel cleaner when replies
are directed at the already-visible root post.
* refactor: simplify NIP-10 thread chat implementation
Major simplifications based on user feedback:
- Render root event as regular message instead of special feed renderer
- Remove special-case logic for hiding reply preview when replying to root
- Include root event in messages list using RxJS combineLatest
- Change root author role from "admin" to "op" (original poster)
- Fix NIP-10 protocol link handler in chat header
- Add "Chat" option to event context menu for kind 1 events
- Fix unused parameter warnings in nip-10-adapter
This refactoring reduces complexity and makes NIP-10 threads behave
more consistently with other chat protocols.
* feat: add zap support for NIP-10 thread chat messages
Implement getZapConfig method to enable zapping messages in NIP-10 threads:
- Returns message author as recipient
- Includes event pointer for the message being zapped
- Uses conversation metadata relays for zap receipt publishing
- Integrates with new zap infrastructure from main branch
* fix: pass protocol and identifier directly to chat window
Fix 'undefined protocol' error when opening chat from event menu.
Instead of passing raw nevent as args, directly pass the parsed
protocol and identifier props that ChatViewer expects.
* debug: add logging to diagnose chat protocol issue
Add console logging to:
- chat command argParser to see input args and parsed result
- ChatViewer to see received props
This will help diagnose why 'undefined protocol' error appears
when opening chat via command or event menu.
* refactor: improve NIP-10 thread chat UX and relay/participant handling
UI Improvements:
- Remove "thread root" marker from reply previews - treat all replies uniformly
- Hide "load older messages" for NIP-10 threads (all replies loaded reactively)
- Display "Thread" with note icon instead of "Group" for NIP-10 conversations
- Remove debug logging from chat command parser and ChatViewer
Participant Management:
- Derive participants dynamically from messages (like live-chat does)
- Root author (OP) always listed first with "op" role
- All unique message authors included in member list
- Updates in real-time as new people reply
Relay Management:
- Expand relay collection to include participant outbox relays
- Fetch relays from root author, provided event author, and p-tagged participants
- Check up to 5 participants for relay diversity
- Increase max relay limit from 7 to 10 for better coverage
- Add logging for relay collection debugging
This makes NIP-10 threads feel more like proper chat conversations with
accurate participant lists and better relay coverage across the thread.
* refactor: remove debug logging from NIP-10 adapter
Remove informational console.log statements to reduce noise in production.
Keep only console.warn and console.error for actual problems.
This makes the adapter production-ready while maintaining useful error
and warning messages for debugging issues.
* fix: resolve lint errors from logging cleanup
- Fix empty block statement by simplifying next handler
- Prefix unused parameters with underscore (_response, _err)
- All lint checks now pass with 0 errors
* refactor: use AGGREGATOR_RELAYS constant and remove design docs
Replace hardcoded relay URLs with AGGREGATOR_RELAYS constant:
- Import AGGREGATOR_RELAYS from @/services/loaders
- Use constant for fallback relays in getThreadRelays()
- Use constant for default relays in getDefaultRelays()
Remove design documentation files (no longer needed):
- docs/nip10-thread-chat-design.md
- docs/nip10-thread-chat-examples.md
- docs/nip10-thread-chat-summary.md
This improves maintainability by centralizing relay configuration
and reduces repository clutter.
* refactor: remove relay.nostr.band and update AGGREGATOR_RELAYS
relay.nostr.band is no longer operational, so remove it from the codebase:
AGGREGATOR_RELAYS changes:
- Removed: wss://relay.nostr.band/
- Removed: wss://purplepag.es/
- Added: wss://relay.snort.social/
- Added: wss://relay.damus.io/
- New list: nos.lol, relay.snort.social, relay.primal.net, relay.damus.io
Updated code:
- src/services/loaders.ts: Updated AGGREGATOR_RELAYS constant
- src/lib/chat/adapters/nip-53-adapter.ts: Use AGGREGATOR_RELAYS instead of hardcoded relays
Updated tests:
- All test files updated to expect new relay URLs
- Replaced relay.nostr.band references with relay.snort.social
- Replaced purplepag.es references with relay.snort.social
- Fixed URL formats to include trailing slashes for normalization
All 980 tests passing ✓
* fix: change grimRelays from let to const in supporters.ts
Fix lint error from rebase - grimRelays is never reassigned so it should
use const instead of let.
* style: reduce padding on sign-in message to match composer
Change from px-3 py-2 to px-2 py-1 to match the horizontal and vertical
padding of the logged-in message composer (px-2 py-1), ensuring
consistent height between logged-in and logged-out states.
* feat: make sign-in message clickable to open login dialog
Add clickable 'Sign in' link to the logged-out message composer:
- Import LoginDialog component
- Add showLogin state management
- Make 'Sign in' text an underlined button that opens the login dialog
- Add LoginDialog component with controlled state
This provides a better UX by allowing users to quickly sign in
directly from the chat interface.
---------
Co-authored-by: Claude <noreply@anthropic.com>