Commit Graph

373 Commits

Author SHA1 Message Date
Alejandro
2e8ef0e5db fix: add iOS PWA notch support with safe area insets (#190)
- Update viewport meta tag to include viewport-fit=cover
- Add apple-mobile-web-app-capable and status-bar-style meta tags
- Add CSS safe area insets to body element for proper notch handling
- Ensures content extends into safe areas on iOS devices while
  respecting notches and rounded corners

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 21:54:28 +01:00
Alejandro
b5b474da3a Add React option to generic event menu (#189)
* feat: add React option to event menu for emoji reactions

Add emoji reaction capability to the generic event menu (dropdown and
right-click context menu). When logged in with a signing account, users
can now react to any event with unicode or custom NIP-30 emoji.

- Add React menu item after Chat in EventMenu and EventContextMenu
- Integrate EmojiPickerDialog from chat components
- Use ReactionBlueprint from applesauce-common for NIP-25 reactions
- Publish reactions to user's outbox relays via publishEvent()
- Hidden when user cannot sign (read-only or not logged in)

* feat: use NIP-65 relay selection for reactions

Add interaction relay selection utility following the NIP-65 outbox model:
- Author's outbox (write) relays: where we publish our events
- Target's inbox (read) relays: so the target sees the interaction

This ensures reactions reach the intended recipient according to their
relay preferences, similar to how zap relay selection works.

New file: src/lib/interaction-relay-selection.ts
- selectInteractionRelays() for full result with sources
- getInteractionRelays() convenience wrapper

* refactor: consolidate interaction relay selection into relay-selection service

Move selectRelaysForInteraction() into the existing relay-selection.ts
service to avoid fragmentation. The service already has the infrastructure
for relay list caching, health filtering, and fallback logic.

- Add selectRelaysForInteraction() to src/services/relay-selection.ts
- Update BaseEventRenderer to import from consolidated location
- Remove separate src/lib/interaction-relay-selection.ts file

* fix: use semantic author for reaction relay selection

Use getSemanticAuthor() to determine the target pubkey for relay
selection. This ensures reactions to zaps notify the zapper (not the
lightning service) and reactions to streams notify the host.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 20:09:09 +01:00
Alejandro
074c3c0b7f Fix relay list flickering during post publish (#187)
* fix: prevent relay list flicker during post publishing

Fixed the relay list flickering issue that occurred while publishing posts:
- Changed overflow-y-auto to overflow-y-scroll to keep scrollbar visible
- Added fixed width (w-6) to status indicator container to prevent layout shifts
- Status indicators no longer cause content reflow when changing states

* fix: show icon for all relay states to prevent layout shifts

Show a Circle icon for pending/unpublished relays so there's always
an icon present. This keeps the status indicator container at a
consistent width and prevents flickering when publishing.

- Added Circle icon import
- Show Circle icon for "pending" state
- Reverted to overflow-y-auto (no longer needed with fixed container)
- Status indicator always has fixed width with centered icon

* refactor: use muted color for pending and publishing relay states

Changed publishing spinner from blue to muted to match pending state,
creating a more consistent visual hierarchy where only success (green)
and error (red) states use color.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 17:22:07 +01:00
Alejandro
66618fb150 fix: prevent TipTap editor crash when view is not ready (#188)
The POST command would sometimes crash with "editor view is not available"
because code was accessing editor.view.dom before the editor was fully
mounted. This fix:

- Adds defensive checks for editor.view?.dom in RichEditor's useEffect
  that attaches keyboard listeners
- Makes setContent method check editor view is ready before setting content
- Fixes PostViewer draft loading to use retry logic instead of fixed timeout
- Removes relayStates from dependency array to prevent effect re-runs
- Adds ref to track if draft was already loaded

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 17:15:41 +01:00
Alejandro
c955bf8eb0 feat: add readable names for wiki list kinds (10101, 10102) (#186)
Add EVENT_KINDS entries for kind 10101 (Wiki Authors) and kind 10102
(Wiki Relays) so getKindName() returns human-readable names instead
of fallback "Kind 10101" / "Kind 10102".

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 16:59:53 +01:00
Alejandro
adf8a62954 feat: add hashtag support to rich editor (#185)
* feat: add automatic hashtag extraction and t tags in POST command

Extract hashtags from post content and automatically add them as t tags to published events.

Changes:
- Add hashtag extraction logic to RichEditor.serializeContent() using Unicode-aware regex
- Update SerializedContent interface to include hashtags field
- Update RichEditor props and callbacks to pass hashtags through the pipeline
- Add t tags for each hashtag in PostViewer.handlePublish()

Hashtags are deduplicated and stored in lowercase (following Nostr convention).
Example: #bitcoin #nostr #Bitcoin → ["t", "bitcoin"], ["t", "nostr"]

* refactor: use NoteBlueprint for automatic hashtag/mention extraction

Replace manual hashtag and mention extraction with applesauce's NoteBlueprint,
which automatically extracts hashtags, mentions, and event quotes from text content.

Changes:
- Simplify SerializedContent interface by removing manually extracted fields
- Remove hashtag extraction regex and mention/eventRef tracking from editors
- Replace manual event building with factory.create(NoteBlueprint, ...)
- Use q tags for event quotes (NIP-18) instead of e tags

Benefits:
- ~70 lines of code removed
- Leverage battle-tested applesauce extraction logic
- Automatic benefits from future applesauce improvements
- Correct semantic tags (q for quotes, p for mentions, t for hashtags)

What still works:
- Custom emoji tags (NIP-30)
- Blob attachments/imeta tags (NIP-92)
- Address references (naddr - not yet in applesauce)
- Client tag

All tests pass (980/980).

* refactor: use NoteReplyBlueprint in NIP-10 adapter

Replace manual NIP-10 tag building with NoteReplyBlueprint, which automatically
handles root/reply markers, p-tag copying, and all the threading logic.

Changes:
- Simplify sendMessage from ~95 lines to ~40 lines
- Remove manual e-tag building with root/reply markers
- Remove manual p-tag deduplication logic
- Use factory.create(NoteReplyBlueprint, parentEvent, content, options)
- Automatically get hashtags, mentions, and event quotes via setShortTextContent

Benefits:
- ~55 lines of complex threading logic removed
- Leverage battle-tested applesauce NIP-10 implementation
- Automatic root detection from parent's existing tags
- Cleaner, more maintainable code

All tests pass (980/980).

* refactor: use GroupMessageBlueprint and ReactionBlueprint in chat adapters

Replace manual event building with applesauce blueprints in all chat adapters.

Changes:
- NIP-29: Use GroupMessageBlueprint for kind 9 messages
  * Auto-handles h-tag, hashtags, mentions, emojis
  * Manually add q-tag for replies (NIP-29 specific)
  * ~15 lines removed

- All adapters (NIP-10, NIP-29, NIP-53, NIP-C7): Use ReactionBlueprint for kind 7 reactions
  * Auto-handles e-tag, k-tag, p-tag, custom emoji support
  * Protocol-specific tags (h-tag, a-tag) added manually
  * ~60 lines removed across 4 adapters

Benefits:
- ~75 lines of code removed total
- Leverage battle-tested applesauce blueprints
- Automatic hashtag, mention, and quote extraction
- Cleaner, more maintainable code

All tests pass (980/980).

* fix: add required previous field to GroupMessageBlueprint options

GroupMessageBlueprintOptions requires a 'previous' field for message threading.
Added empty array for now since we don't support threading yet.

* docs: add comprehensive blueprint documentation to applesauce skills

Added detailed documentation for:
- NoteBlueprint (automatic hashtag/mention/quote extraction)
- NoteReplyBlueprint (NIP-10 threading)
- ReactionBlueprint (kind 7 reactions)
- GroupMessageBlueprint (NIP-29 groups)
- DeleteBlueprint (NIP-09 deletion)
- EventFactory usage patterns
- Before/after examples showing code reduction
- Best practices for using blueprints

This documents the refactoring work done throughout the codebase.

* fix: use single newline separator in TipTap getText() calls

TipTap's getText() uses double newlines (\n\n) by default to separate
block nodes like paragraphs, which was causing extra blank lines in
posted content.

Changed to getText({ blockSeparator: '\n' }) in both RichEditor and
MentionEditor to use single newlines between paragraphs.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 16:20:00 +01:00
Alejandro
b1fb569250 feat: user profile search for resolving usernames when pasting (#184)
* fix: show display names when pasting npub/nprofile in editors

Previously, pasting npub/nprofile strings would only check the EventStore's
in-memory cache for profiles. If a profile was cached in IndexedDB but not
yet loaded into the EventStore, it would show a hex preview instead.

This adds a ProfileCache service that:
- Loads profiles from IndexedDB on startup for instant access
- Subscribes to EventStore for new profiles as they arrive
- Provides synchronous lookups for the paste handler

Also uses consistent fallback format (XXXX:YYYY) when no profile is found.

* refactor: make ProfileSearchService a singleton for shared profile lookups

Instead of creating a separate ProfileCache service, refactored
ProfileSearchService to be a singleton that:
- Auto-initializes on module load
- Loads profiles from IndexedDB for instant startup
- Subscribes to EventStore for new profiles

This allows both the paste handler and mention autocomplete to share
the same profile cache, eliminating duplicate data and subscriptions.

Removed the now-unnecessary profile-cache.ts.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 13:38:59 +01:00
Alejandro
94982ca7f4 feat(post): add POST command with rich text editor and relay selection (#180)
* feat(post): add POST command with rich text editor and relay selection

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(editor): add nostr bech32 paste handler with inline previews

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

* refactor(editor): improve nostr preview display with kind icons

Updates the nostr event preview chips to show more meaningful information:

Changes:
- Add getKindIcon() helper function with 15+ kind mappings
- Display format: kind-icon + author/id (e.g., "📄 a1b2c3d4")
- For naddr: show kind icon + author pubkey (first 8 chars)
- For nevent: show kind icon + author if available, else event ID
- For note: show 📝 + event ID (first 8 chars)
- For npub/nprofile: show 👤 + pubkey (first 8 chars)
- Removed verbose type labels (NPUB, NEVENT, etc.)
- Increased label max-width to 120px for better readability

Kind icon mappings:
- 👤 Profile (kind 0)
- 📝 Note (kind 1)
- 👥 Contacts (kind 3)
- 🔁 Repost (kind 6)
- ❤️ Reaction (kind 7)
-  Zap (kind 9735)
- 📄 Long-form (kind 30023)
- 🎙️ Live event (kind 30311)
- 📦 File metadata (kind 1063)
- 📌 Addressable events (30000-39999)
- 🔄 Replaceable events (10000-19999)

Result: More compact, visually intuitive previews that immediately
convey the content type and author without requiring event fetching.

* refactor(editor): simplify nostr preview display and reuse mentions

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

* feat(editor): add drag-and-drop and paste file upload support

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

* fix(editor): add dragover handler to enable file drag-and-drop

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.

* feat(editor): show usernames when pasting npub/nprofile mentions

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

* refactor(editor): remove drag-and-drop, keep paste file support

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

* refactor(editor): extract shared extensions and create React node views

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.

* feat(editor): add RichEditor component for long-form content

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

* refactor(post): improve PostViewer layout and editor previews

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

* feat(post): add draft persistence and improve relay UI

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}

* feat(post): enhance draft storage with full JSON state persistence

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
}

* feat(post): add nostr tag extraction, retry failed relays, and disable empty publish

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.

* feat(post): add client tag and remove reset button

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)

* fix(post): trim content before publishing

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()

* fix(post): reuse signed event for retries instead of recreating

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

* feat(post): add published event preview and reset button

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.

* feat(post): improve layout and relay handling

- 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.

* fix(editor): use feed renderer and skeleton for event previews

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(post): remove flex-1 and improve scrolling layout

- 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).

* feat(post): add max-width constraint to 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.

* fix(editor): add remove button and fix cursor positioning for event embeds

- 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.

* feat(post): add discard button, settings dropdown, and improve button layout

- 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

* feat(post): add relay connectivity status, input widget, and improve error handling

- 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

* feat(post): add auth status icons and window-specific draft persistence

- 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

* feat(post): add global persistent settings and event JSON preview

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

* feat(post): add live draft JSON preview with 2-second updates

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.

* feat(post): improve JSON preview responsiveness and layout

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.

* refactor(post): replace intervals with debounced onChange handlers

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>

* refactor(post): remove JSON preview and ensure proper content serialization

**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

* perf(post): reduce draft save debounce from 2s to 500ms

Makes the draft saving feel more responsive without hammering localStorage
on every keystroke.

* fix(post): preserve content when signing fails or is rejected

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!

* refactor(post): use Promise.allSettled for relay publishing

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.

* feat(post): use GRIMOIRE_CLIENT_TAG constant with NIP-89 app address

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.

* fix(post): preserve content when publishing fails to all relays

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..."

* fix(post): preserve content when signing or publishing

Critical fix: Don't clear editor content until publish succeeds.

**The Problem:**
- RichEditor automatically cleared content on submit
- Content was wiped immediately when clicking "Publish"
- User lost their content before knowing if publish succeeded

**The Solution:**
- Remove automatic `clearContent()` from RichEditor's handleSubmit
- Let PostViewer control when to clear the editor
- Only clear editor after successful publish (successCount > 0)

**User Experience Now:**
- Click "Publish" → content stays visible while publishing
- Publishing succeeds → content clears, show preview
- Publishing fails → content stays, user can edit and retry
- User rejects signing → content stays, can try again

No more losing your post to a rejected signature or failed publish!

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 13:03:30 +01:00
Alejandro
d18cdc31d5 feat: client tag (#183)
* 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>
2026-01-21 12:23:41 +01:00
Alejandro
d20a92e854 fix: support zapping addressable events (naddr) (#182)
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>
2026-01-21 10:15:11 +01:00
Alejandro
45e68bbdad fix: use addressPointer for zapping addressable events (#181)
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>
2026-01-21 09:58:37 +01:00
Alejandro
cbc1fc272a feat(user-menu): make relays and blossom servers collapsible dropdowns (#177)
* 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>
2026-01-20 19:14:25 +01:00
Alejandro
13081938ff Fix long filename truncation in upload dialog (#176)
* 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>
2026-01-20 17:39:51 +01:00
Alejandro
13fec0345f feat: zap goals nip-75 (#174)
* 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>
2026-01-20 17:28:57 +01:00
fiatjaf_
b28d5f7892 remove unnecessary toast on popup. (#175) 2026-01-20 17:24:00 +01:00
Alejandro
c52e783fce fix(nip-29): fetch admin and member lists in parallel and correctly tag admins (#169)
* 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>
2026-01-20 11:41:51 +01:00
Alejandro
c2f6f1bcd2 style: make kind label in event menu more subtle (#168)
* 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>
2026-01-20 10:10:18 +01:00
Alejandro
83b3b0e416 fix: remove redundant reply preview for NIP-10 root replies (#163)
* 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>
2026-01-19 21:43:22 +01:00
Alejandro
97b3842692 fix: convert NIP-10 chat zap amounts to sats (#161)
* 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>
2026-01-19 20:14:50 +01:00
Alejandro
ca58a4e7c3 feat: replace zap icon with flame icon for supporter badges (#160)
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>
2026-01-19 20:13:46 +01:00
Alejandro
8f008ddd39 feat: nip-10 chat interface (#153)
* 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>
2026-01-19 16:47:12 +01:00
Alejandro
14f19a4517 Remove lint job from QA workflow (#157)
The lint CI check was more annoying than helpful.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 16:40:44 +01:00
Alejandro
5475f9b518 Add spacing between wallet viewer icon buttons (#156)
* style: increase spacing between wallet header icon buttons

Changed gap-2 to gap-3 for better visual separation between the info,
refresh, and disconnect icon buttons in the wallet viewer header.

* style: remove "Waiting for payment..." text from receive dialog

The QR code overlay spinner already indicates payment checking status.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 16:25:26 +01:00
Alejandro
2ce81f3ad8 fix: truncate invoice descriptions in confirm send dialog (#155)
Add proper truncation for long invoice descriptions in the wallet's
confirm send dialog. The description label is now flex-shrink-0 to
prevent it from shrinking, and a title attribute shows the full
description on hover.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 16:24:53 +01:00
Alejandro
9e11fb590f Add donate call to action feature (#150)
* feat: add donate CTA and supporter recognition system

Adds donation call-to-action and visual recognition for Grimoire supporters who zap the project.

**UserMenu Changes:**
- Add "Support Grimoire " button that opens ZapWindow with preset donation address
- Add monthly goal tracker with progress bar (currently showing placeholder values)
- Integrate Lightning address (grimoire@coinos.io) and donation pubkey from members list

**Supporter Tracking System:**
- Create supporters service to monitor kind 9735 (zap receipt) events
- Track users who zap Grimoire donation address
- Cache supporter info (pubkey, total sats, zap count, last zap timestamp) in localStorage
- Reactive updates via RxJS BehaviorSubject
- Initialize tracking on app startup

**Visual Flair for Supporters:**
- Add useIsSupporter hook for checking supporter status
- Style supporter usernames with yellow/gold color
- Add filled  zap icon badge next to supporter names
- Only applies to non-Grimoire members (members keep their existing gradient badges)

**Implementation Details:**
- Constants: GRIMOIRE_DONATE_PUBKEY and GRIMOIRE_LIGHTNING_ADDRESS in grimoire-members.ts
- Service automatically processes zap receipts and persists supporter data
- Monthly goal tracker uses placeholder values (42k/500k sats, 8.4% progress)
- Future: Make goal dynamic by calculating from actual zap receipts

Related to zap feature implementation in #141, #144, #145

* feat: migrate donation tracking to IndexedDB with monthly calculations

Replaces in-memory Map + localStorage with proper Dexie database storage for accurate monthly donation tracking.

**Database Changes (Version 17):**
- Add `grimoireZaps` table to store individual zap receipts
- Schema: eventId (PK), senderPubkey, amountSats, timestamp, comment
- Indexes on senderPubkey and timestamp for efficient queries

**Supporters Service:**
- Store each zap receipt as separate DB record with full metadata
- Track individual zap timestamps (not just latest per user)
- Cache total and monthly donations for synchronous access
- Refresh cache on new zaps for reactive UI updates

**Monthly Calculations:**
- `getMonthlyDonations()` - Last 30 days (rolling window)
- `getCurrentMonthDonations()` - Current calendar month
- Both use indexed DB queries for efficiency
- Cached values updated on each new zap

**UserMenu:**
- Set monthly goal to 210M sats (2.1 BTC)
- Dynamic progress calculation from actual zap data
- Reactive updates when new donations arrive
- Number formatting: 1M+ → "2.1M", 1k+ → "42k"

**Benefits:**
- Accurate historical tracking with timestamps
- Efficient monthly queries using DB indexes
- No data loss on localStorage quota issues
- Foundation for supporter leaderboards and analytics

* fix: properly await async zap processing in subscription

Changes processZapReceipt calls from forEach to Promise.all to ensure async DB operations complete before processing next batch of events.

Prevents race conditions where zaps might not be properly stored in DB if multiple events arrive simultaneously.

* perf: optimize Dexie queries for donation tracking

Replaces inefficient toArray() + reduce patterns with direct Dexie iteration APIs for better memory efficiency and performance.

**Optimizations:**

1. **Supporter Count** - Use `uniqueKeys()` instead of loading all records
   - Before: Load all → create Set → get size
   - After: `orderBy('senderPubkey').uniqueKeys().length`
   - ~90% memory reduction for large datasets

2. **Aggregation Queries** - Use `.each()` iterator pattern
   - `getTotalDonationsAsync()` - Stream records, accumulate sum
   - `getMonthlyDonationsAsync()` - Indexed query + iteration
   - `getCurrentMonthDonations()` - Indexed query + iteration
   - `getSupporterInfo()` - Per-pubkey indexed query with iteration
   - `getAllSupporters()` - Stream all, group in Map, sort

3. **Cache Refresh** - Optimized `refreshSupporters()`
   - uniqueKeys for supporter set
   - Direct iteration for total/monthly sums
   - Single indexed query for monthly window

**Monthly Goal:**
- Update from 210M sats to 210k sats (0.0021 BTC)
- More achievable target for initial launch

**Benefits:**
- Lower memory usage (no intermediate arrays)
- Faster queries (direct iteration vs map/reduce)
- Better scalability with growing zap history
- Leverages Dexie's indexed cursors for efficiency

* refactor: singleton supporters service with optimized Dexie queries

Complete refactor of donation tracking to proper singleton pattern with relay-based subscriptions and zero in-memory caching.

**Singleton Service Pattern:**
- Class-based SupportersService matching relay-liveness/accounts patterns
- Single `init()` method initializes subscriptions
- Observable `supporters$` for reactive UI updates
- Proper cleanup with `destroy()` method

**Relay-Based Subscription:**
- Fetch Grimoire's inbox relays via relayListCache
- Subscribe to zaps using `#p` tag filter (NIP-57 recipient tag)
- Use createTimelineLoader with proper relay hints
- Fallback to aggregator relays if no inbox relays found
- Dual subscription: loader + event store timeline for comprehensive coverage

**Optimized Dexie Schema:**
- Add compound index: `[senderPubkey+timestamp]`
- Enables efficient per-user date range queries
- Schema: `&eventId, senderPubkey, timestamp, [senderPubkey+timestamp]`

**Zero In-Memory Caching:**
- Remove cachedTotalDonations and cachedMonthlyDonations
- All queries go directly to IndexedDB
- Use Dexie iteration APIs (`.each()`, `.uniqueKeys()`)
- Compound index queries for monthly aggregations

**Premium Supporter Detection:**
- New threshold: 2.1k sats/month = premium supporter
- `isPremiumSupporter()` uses compound index query
- `getMonthlySupporterInfo()` returns monthly stats per user

**Badge Logic Updates:**
- Premium supporters (2.1k+/month): Zap badge in username color
- Regular supporters: Yellow text + yellow filled zap icon
- useIsSupporter returns `{ isSupporter, isPremiumSupporter }`

**UserMenu Updates:**
- Use async `getMonthlyDonations()` with useState/useEffect
- Subscribe to `supporters$` to trigger monthly recalculation
- Remove synchronous function calls

**Key Benefits:**
- Proper singleton lifecycle management
- Accurate relay selection for zap discovery
- No memory overhead from caching
- Efficient compound index queries
- Scales to thousands of zaps without performance degradation

* fix: fetch Grimoire relay list before subscribing to zaps

Ensures kind 10002 relay list is loaded before subscribing to zap receipts, preventing fallback to aggregators only.

**Problem:**
At startup, relayListCache was empty, so getInboxRelays() returned null and service fell back to aggregator relays only, missing zaps published to Grimoire's actual inbox relays.

**Solution:**
1. Explicitly fetch Grimoire's kind 10002 relay list using addressLoader
2. Wait up to 5 seconds for relay list to load
3. Then get inbox relays from populated cache
4. Subscribe to those relays + aggregators

**Flow:**
init() → subscribeToZapReceipts()
  → fetch kind 10002 (5s timeout)
  → getInboxRelays() from cache
  → subscribe to inbox relays + aggregators
  → keep subscription open for live updates

Fixes startup zap loading issue.

* feat: compact support section with manual refresh and full clickability

Makes the entire support section clickable to open zap dialog and adds manual refresh button for donation stats.

**Changes:**
- Entire support section now clickable (opens zap dialog)
- Add refresh button with spinning animation
- Remove 'Help us build...' tagline for more compact design
- Keep just title, stats, and progress bar
- Manual refresh re-fetches Grimoire relay list and reloads zaps

**Layout:**

Click anywhere to donate, click refresh icon to manually sync zaps.

* refactor: replace BehaviorSubject with Dexie useLiveQuery for reactive supporter tracking

Replace manual BehaviorSubject pattern with Dexie's built-in useLiveQuery hook
for reactive database queries. This simplifies the code and leverages Dexie's
optimized change detection.

Changes:
- Remove BehaviorSubject from SupportersService
- Remove refreshSupporters() method and all calls to it
- Update useIsSupporter hook to use useLiveQuery for supporter pubkeys
- Update GrimoireWelcome to use useLiveQuery for monthly donations
- Update UserMenu to use useLiveQuery for monthly donations
- Remove unused imports (cn, useEffect, useState) and fields (initialized)

Benefits:
- Less code to maintain (no manual observable management)
- Automatic reactivity when DB changes
- Better performance with Dexie's built-in change detection

* fix: improve cold start zap loading and fix subscription memory leak

Fixes several issues with zap loading on cold start:

1. Memory leak fix: Timeline subscription wasn't being stored or cleaned up
   - Now properly add timeline subscription to main subscription for cleanup

2. Better cold start handling:
   - Increase timeout from 5s to 10s for relay list fetching
   - Add 100ms delay after addressLoader to let relayListCache update
   - Add more detailed logging to debug cold start issues

3. Improved logging:
   - Show which relays are being used for subscription
   - Log when processing zap events from eventStore
   - Better error messages with context

These changes should help diagnose why zaps aren't loading on cold start
and prevent memory leaks from unclosed subscriptions.

* feat: add hardcoded relay fallback for instant cold start zap loading

Add hardcoded relays (wss://nos.lol, wss://lightning.red) to immediately
start loading zaps on cold start, without waiting for relay list fetch.

Changes:
- Add GRIMOIRE_ZAP_RELAYS constant with hardcoded reliable relays
- Refactor subscribeToZapReceipts() to start with hardcoded relays immediately
- Move relay list fetching to separate non-blocking method fetchAndMergeRelayList()
- Relay list fetch now happens in parallel with subscription (non-blocking)
- Still fetch and log additional relays from kind 10002 in background

This ensures zaps load immediately on app start rather than waiting for
relay list to be fetched from network.

* fix: remove dead relay.nostr.band and add detailed zap processing logs

Remove relay.nostr.band from all relay lists as it's dead.

Changes:
- Remove from AGGREGATOR_RELAYS in loaders.ts
- Remove from NIP-53 fallback relays
- Update all test files to use alternative relays

Add detailed logging to debug progress bar showing 0:
- Log each zap processing step (validation, recipient check, sender check)
- Log duplicate zaps, invalid zaps, and 0-sat zaps
- Log existing zap count in DB on init
- Add timestamps to successful zap recordings

This will help diagnose why the progress bar shows 0 even though
zaps are being fetched.

* fix: reduce zap query limit from 1000 to 500 to avoid relay rejections

Many relays reject REQ filters with limit > 500. This was likely causing
the zap subscription to fail silently on some relays, resulting in no zaps
being fetched and the progress bar showing 0.

Reduced limit from 1000 to 500 to be compatible with more relays.

* fix: remove lightning.red and aggregator relays, add monthly calculation debug logs

Changes:
- Remove wss://lightning.red from hardcoded relays (only use wss://nos.lol)
- Remove aggregator relays from zap subscription (don't use for fetching zaps)
- Remove AGGREGATOR_RELAYS import

Add debug logging to diagnose progress bar issue:
- GrimoireWelcome: Log count and total of zaps found in last 30 days
- UserMenu: Log count and total of zaps found in last 30 days
- Show cutoff date in GrimoireWelcome log

This will help identify if:
1. Zaps are being stored in DB
2. Zaps are within the 30-day window
3. Monthly calculation is running and finding the zaps

* fix: subscribe to eventStore.insert$ to catch zaps in real-time

The previous approach using eventStore.timeline() was only emitting once
with the initial state (0 events) and not re-emitting when new zaps were
added to the store.

Changed to subscribe to eventStore.insert$ which fires for every new event
added to the store. This catches zaps as they arrive from relays in real-time.

Changes:
- Subscribe to eventStore.insert$ instead of eventStore.timeline()
- Filter events for kind 9735 with #p tag matching Grimoire pubkey
- Process each zap as it's inserted into the store
- Add logging to show when zaps are received from insert$ stream

This should fix the progress bar showing 0 even though zaps are being
fetched from relays.

* fix: also subscribe to eventStore.timeline() to process existing zaps

The insert$ stream only fires for NEW events being added. If events were
already in the eventStore (from cache or previous session), we wouldn't
see them.

Now subscribing to both:
- insert$ - catches new events in real-time as they're added
- timeline() - emits ALL matching events (existing + new)

This ensures we process:
1. Zaps already in the eventStore from previous sessions
2. Zaps that loaded from relays before our subscription was active
3. New zaps arriving in real-time

Added logging to distinguish between new (insert$) and all (timeline) events.

* fix: subscribe directly to loader observable to process zaps as they arrive

Complete rethink of the zap sync strategy. The issue was that we were
subscribing to eventStore.timeline() and eventStore.insert$ which were
not working as expected.

The TimelineLoader returns Observable<NostrEvent> that emits individual
events as they arrive from relays. We should subscribe directly to this.

Changes:
- Remove eventStore.timeline() and eventStore.insert$ subscriptions
- Subscribe directly to loader().subscribe() with next/error/complete handlers
- Process each event in the next() handler as it arrives from relays
- Log each zap received from loader for debugging
- Remove unused eventStore import

This is the simplest and most direct approach - process events exactly
as the loader emits them from the relays.

* chore: merge main and remove debug logging for production

Merged latest changes from main branch and removed all debug logging
added during development to make the code production-ready.

Changes:
- Merged origin/main into branch
- Removed console.log statements from supporters service init/subscribe
- Removed debug logging from processZapReceipt method
- Removed debug logging from fetchAndMergeRelayList method
- Removed debug logging from GrimoireWelcome monthly calculation
- Removed debug logging from UserMenu monthly calculation
- Kept only error logging for production debugging

The donation system is now production-ready with clean, minimal logging.

* fix: remove yellow text color from regular supporters, keep only badge

Regular supporters (who zapped but not 2.1k+/month) should only show the
yellow zap badge next to their name, not have their username colored yellow.

Changes:
- Remove yellow text color (text-yellow-500) from regular supporters
- Regular supporters now: normal username color + yellow zap badge
- Premium supporters still: normal username color + badge in username color
- Updated component documentation to reflect this change

This provides cleaner visual hierarchy where only Grimoire team members
get special username colors (gradient), while supporters are distinguished
by their badge alone.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 16:15:39 +01:00
Alejandro Gómez
1068fae78f style: limit transaction history list width to match wallet UI
Add max-w-md constraint to transaction history list to match
the width of balance display, send/receive buttons, and detail dialog.
Creates a consistent, centered, compact layout throughout the entire
wallet interface.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 14:11:48 +01:00
Alejandro Gómez
7664861825 style: limit transaction detail dialog width to match wallet UI
Add max-w-md constraint to transaction detail dialog to match
the width of the balance display and send/receive buttons.
This creates a consistent, compact layout throughout the wallet UI.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 14:09:59 +01:00
Alejandro Gómez
3eb8cea9e0 fix: use preimage field for Bitcoin txid and make mempool link icon-only
Correct Bitcoin transaction display to use the preimage field which
contains the actual txid (with optional output index like "txid:0").

Changes:
- Extract txid from preimage field, removing output index if present
- Use preimage instead of payment_hash for Bitcoin transactions
- Make mempool.space link icon-only with tooltip
- Icon is larger (size-4) and properly aligned with txid display

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 14:08:14 +01:00
Alejandro Gómez
1dd3841220 fix: correct Bitcoin transaction detection logic
Fix Bitcoin transaction detection to properly distinguish between
Lightning and on-chain transactions by checking the invoice field.

Detection logic:
- Bitcoin: invoice field contains a Bitcoin address (not "ln..." invoice)
- Lightning: invoice field starts with "ln" (lnbc, lntb, etc.)
- Supports all Bitcoin address formats: legacy (1..., 3...), bech32 (bc1...)
- Supports testnet addresses: tb1..., 2..., m/n...

Display logic:
- Bitcoin: Show payment_hash (or preimage) as Transaction ID
- Link to mempool.space for blockchain exploration
- Lightning: Show payment hash and preimage separately

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 13:59:42 +01:00
Alejandro Gómez
a01533b495 feat: add Bitcoin transaction detection and mempool.space explorer links
Detect Bitcoin on-chain transactions and display them differently
from Lightning transactions in the wallet viewer. Bitcoin transactions
show Transaction ID with a link to mempool.space for blockchain
exploration.

Detection:
- Bitcoin txid is 64-character hex string in preimage field
- Uses regex validation to detect on-chain transactions

Display changes:
- Bitcoin: Show "Transaction ID" with mempool.space link
- Lightning: Show "Payment Hash" and "Preimage" as before
- Network-aware: Links to testnet/signet explorers when applicable

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 13:56:31 +01:00
Alejandro Gómez
3adc9bdfc3 feat: add invoice description fallback for wallet transactions
Add fallback to lightning invoice description when transaction
description is not available. Improves transaction list readability
by showing invoice descriptions instead of generic "Payment" labels.

- Add getInvoiceDescription helper with applesauce caching pattern
- Update TransactionLabel to check invoice description
- Update detail dialog to show invoice description as fallback
- Maintains zap detection logic and UI

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-19 13:54:45 +01:00
Alejandro
97dd30f587 Add anonymous zap option with throwaway signer (#154)
* feat: add anonymous zap option

Add "Zap anonymously" checkbox that allows users to send zaps without
revealing their identity. When enabled, creates a throwaway keypair to
sign the zap request instead of using the active account's signer.

This also enables users without a signer account to send zaps by
checking the anonymous option.

* feat: prioritize recipient's inbox relays for zap receipts

Add selectZapRelays utility that properly selects relays for zap receipt
publication with the following priority:
1. Recipient's inbox relays (so they see the zap)
2. Sender's inbox relays (so sender can verify)
3. Fallback aggregator relays

This ensures zap receipts are published where recipients will actually
see them, rather than just the sender's relays.

Includes comprehensive tests for relay selection logic.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 12:33:46 +01:00
Alejandro
3f811ed072 feat: zap action for chat (#151)
* feat: add configurable zap tagging for chat messages

Implements a protocol adapter interface for configuring how zap
requests should be tagged for chat messages. This enables proper
NIP-53 live activity zapping with appropriate a-tag and goal
e-tag support.

Changes:
- Add ZapConfig interface to base-adapter for protocol-specific zap configuration
- Add getZapConfig() method to ChatProtocolAdapter (default: unsupported)
- Implement getZapConfig() in NIP-53 adapter with proper tagging:
  - Always a-tag the live activity (kind 30311)
  - If zapping host with goal, also e-tag the goal event
- Add goal tag parsing to live-activity.ts and types
- Update createZapRequest to accept custom tags parameter
- Add Zap action to ChatMessageContextMenu (shown when supported)
- Update ZapWindow to pass custom tags through to zap request
- NIP-29 groups inherit default (unsupported) behavior

* feat: add custom tags and relays to zap command

Extends the zap command to support custom tags and relay specification,
enabling full translation from chat zap config to zap command.

Changes:
- Add -T/--tag flag to specify custom tags (type, value, optional relay hint)
- Add -r/--relay flag to specify where zap receipt should be published
- Update ZapWindow to accept and pass through relays prop
- Update ChatMessageContextMenu to pass relays from zapConfig
- Update man page with new options and examples
- Add comprehensive tests for zap parser flag handling

Example usage:
  zap npub... -T a 30311:pk:id wss://relay.example.com
  zap npub... -r wss://relay1.com -r wss://relay2.com

* fix: include event pointer when zapping chat messages

Pass the message event as eventPointer when opening ZapWindow from
chat context menu. This enables:
- Event preview in the zap window
- Proper window title showing "Zap [username]"

* feat: add zap command reconstruction for Edit feature

Add zap case to command-reconstructor.ts so that clicking "Edit" on
a zap window title shows a complete command with:
- Recipient as npub
- Event pointer as nevent/naddr
- Custom tags with -T flags
- Relays with -r flags

This enables users to see and modify the full zap configuration.

* fix: separate eventPointer and addressPointer for proper zap tagging

- Refactor createZapRequest to use separate eventPointer (for e-tag)
  and addressPointer (for a-tag) instead of a union type
- Remove duplicate p-tag issue (only tag recipient, not event author)
- Remove duplicate e-tag issue (only one e-tag with relay hint if available)
- Update ZapConfig interface to include addressPointer field
- Update NIP-53 adapter to return addressPointer for live activity context
- Update ChatMessageContextMenu to pass addressPointer from zapConfig
- Update command-reconstructor to properly serialize addressPointer as -T a
- Update ZapWindow to pass addressPointer to createZapRequest

This ensures proper NIP-53 zap tagging: message author gets p-tag,
live activity gets a-tag, and message event gets e-tag (all separate).

* refactor: move eventPointer to ZapConfig for NIP-53 adapter

- Add eventPointer field to ZapConfig interface for message e-tag
- NIP-53 adapter now returns eventPointer from getZapConfig
- ChatMessageContextMenu uses eventPointer from zapConfig directly
- Remove goal logic from NIP-53 zap config (simplify for now)

This gives the adapter full control over zap configuration, including
which event to reference in the e-tag.

* fix: update zap-parser to return separate eventPointer and addressPointer

The ParsedZapCommand interface now properly separates:
- eventPointer: for regular events (nevent, note, hex ID) → e-tag
- addressPointer: for addressable events (naddr) → a-tag

This aligns with ZapWindowProps which expects separate fields,
fixing the issue where addressPointer from naddr was being
passed as eventPointer and ignored.

* feat: improve relay selection for zap requests with e+a tags

When both eventPointer and addressPointer are provided:
- Collect outbox relays from both semantic authors
- Include relay hints from both pointers
- Deduplicate and use combined relay set

Priority order:
1. Explicit params.relays (respects CLI -r flags)
2. Semantic author outbox relays + pointer relay hints
3. Sender read relays (fallback)
4. Aggregator relays (final fallback)

* fix: pass all zap props from WindowRenderer to ZapWindow

WindowRenderer was only passing recipientPubkey and eventPointer,
dropping addressPointer, customTags, and relays. This caused
CLI flags like -T (custom tags) and -r (relays) to be ignored.

Now all parsed zap command props flow through to ZapWindow
and subsequently to createZapRequest.

* refactor: let createZapRequest collect relays from both authors

Remove top-level relays from NIP-53 zapConfig so createZapRequest
can automatically collect outbox relays from both:
- eventPointer.author (message author / zap recipient)
- addressPointer.pubkey (stream host)

The relay hints in the pointers are still included via the
existing logic in createZapRequest.

* fix: deduplicate explicit relays in createZapRequest

Ensure params.relays is deduplicated before use, not just
the automatically collected relays. This handles cases where
CLI -r flags might specify duplicate relay URLs.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 12:16:51 +01:00
Alejandro
599e8b6c60 Fix zap dialog to target correct event author (#152)
* fix: use semantic author for zap targeting

When zapping certain event kinds (zaps, streams), use the semantic
author instead of event.pubkey:
- Zaps (9735): Target the zapper, not the lightning service
- Streams (30311): Target the host, not the event publisher

Changes:
- Extract getSemanticAuthor() to shared utility (src/lib/semantic-author.ts)
- Update BaseEventRenderer to use semantic author when opening zap dialog
- Update ZapWindow to resolve recipient using semantic author
- Refactor DynamicWindowTitle to use shared utility

This ensures that when you zap an event, you're zapping the right person
(the one who semantically "owns" or created the event), not just whoever
signed it.

* fix: load event in DynamicWindowTitle to derive zap recipient

When opening a zap dialog via 'zap naddr1...' or 'zap nevent1...', the
window title was showing "ZAP" instead of "Zap {host name}" because
DynamicWindowTitle only had access to the empty recipientPubkey from
the initial props.

Now DynamicWindowTitle:
- Loads the event from eventPointer if present
- Derives the recipient using getSemanticAuthor() if recipientPubkey is empty
- Falls back to explicit recipientPubkey if provided

This ensures the window title shows the correct recipient name
immediately, matching the behavior in the ZapWindow component itself.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 11:41:55 +01:00
Alejandro
ab64fc75f4 Restrict relay auth to account owner (#149)
* fix: only prompt relay auth for accounts that can sign

- Add canAccountSign() helper to check if account is read-only
- Block auth prompts for read-only accounts in shouldPromptAuth()
- Throw error when authenticateRelay() called with read-only account
- Document all major app hooks in CLAUDE.md for future reference

Read-only accounts cannot sign events, so they should never be prompted
for relay authentication or attempt to authenticate. This prevents
confusing UX where users are asked to sign but cannot.

* refactor: extract canAccountSign helper to useAccount

- Move canAccountSign function from relay-state-manager to useAccount.ts
- Import and reuse the shared helper in relay-state-manager
- Update useAccount hook to use the extracted helper internally
- Follows DRY principle by centralizing account sign capability logic

This keeps the account sign capability detection logic in one place,
making it easier to maintain and ensuring consistency across the app.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 10:19:16 +01:00
Alejandro
55d9c88f50 fix: pass zap request event to RichText for emoji rendering in compact preview (#148)
The compact zap preview was passing only the content string to RichText,
which meant NIP-30 custom emoji tags were not available for rendering.
Now passes the full zap request event so emoji tags are properly parsed.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 10:17:09 +01:00
Alejandro
b000ef8dd3 feat: display zap recipient username in all contexts (#147)
* feat: display zap recipient username in all contexts

- Add recipient username display for profile zaps
- Show recipient after amount in compact preview
- Show recipient with arrow (→) in full zap receipt renderer
- Fixes missing context when zaps don't target specific events

* refactor: remove arrow from zap recipient display

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 09:58:19 +01:00
Alejandro
ed86698c07 fix: pass full zap request event to RichText for proper parsing (#146)
Previously, only the content string was passed to RichText when
rendering zap comments. This prevented proper parsing of mentions,
event references, hashtags, URLs, custom emojis, and other rich
content features.

Now passing the full zapRequest event object to RichText, enabling
all content transformers to work correctly on zap comments.

Changes:
- Remove zapComment variable that extracted just the content string
- Pass zapRequest event directly to RichText component
- Update condition to check zapRequest && zapRequest.content

Fixes rendering in both feed and detail views (detail falls back
to feed renderer for kind 9735).

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 09:36:42 +01:00
Alejandro
f56228f88a fix: remove double URL encoding in zap requests (#145)
The zap request JSON was being encoded twice:
1. Manually via encodeURIComponent in serializeZapRequest()
2. Automatically by URLSearchParams.set() when building callback URL

This caused overly-encoded URLs (e.g., '{' → '%7B' → '%257B').

Fix: Remove manual encoding and let URLSearchParams handle it.
Verified: Tests pass, build succeeds

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 09:08:53 +01:00
Alejandro
dbcbcf6181 feat: improve zap wallet payment flow UX (#144)
* feat: improve zap wallet payment flow UX

Improvements to the zap window to better communicate wallet payment status:

- Add clear "Paying with wallet..." message during NWC payment attempts
- Show QR code immediately on payment timeout or failure
- Improve error messages with actionable guidance
- Always display "Open in External Wallet" option in QR view
- Rename "Retry with Wallet" to "Retry with NWC Wallet" for clarity
- Generate QR code upfront to enable instant display on errors

This provides better feedback when wallet payments fail or timeout,
giving users clear fallback options without confusion.

* feat: add LNURL address caching for instant zap UI

Implements LNURL address caching similar to NIP-05 caching pattern:

**Database Changes** (v16):
- Add `lnurlCache` table with 24-hour TTL
- Stores LNURL-pay response data for Lightning addresses
- Indexed by address and fetchedAt for efficient queries

**New Hook** (`useLnurlCache`):
- Reactive hook using Dexie + useLiveQuery pattern
- Auto-fetches and caches LNURL data on first use
- Returns cached data instantly on subsequent calls
- Re-fetches when cache is stale (>24 hours)

**ZapWindow Optimization**:
- Uses cached LNURL data instead of network calls
- Eliminates 10-second delay on repeat zaps
- Shows zap amounts/limits instantly from cache
- Graceful error handling when cache is warming up

**Testing**:
- 11 comprehensive tests for LNURL validation
- Validates zap support, pubkey format, field requirements
- Tests edge cases (uppercase hex, missing fields, etc.)

**Benefits**:
- Instant zap UI for frequently zapped users
- Reduced load on LNURL servers
- Offline capability (show cached limits/amounts)
- Better UX with sub-100ms response time

Verification: All 950 tests pass, build succeeds

* fix: match comment input styling to amount input in zap window

Update MentionEditor styling to match Input component:
- Change padding from py-2 to py-1
- Add responsive text sizing (text-base md:text-sm)
- Add min-h-9 to match Input height

This creates visual consistency between the amount and comment fields.

* feat: add amount preview above invoice in zap QR view

Display the zap amount prominently above the invoice when showing
the QR code. This provides clear visual confirmation of what the
user is paying before they scan or copy the invoice.

Format:
- Large bold amount with k/m notation (e.g., "420", "2.1k", "100m")
- Smaller "sats" label underneath
- Positioned between QR code and invoice field

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 00:07:05 +01:00
Alejandro
9f6e524ea9 feat: add tap-to-blur privacy feature for wallet balances (#143)
* feat: add tap-to-blur privacy feature for wallet balances

Implement privacy toggle for wallet balances and transaction amounts.
Tapping any balance display toggles a global blur effect across all
wallet UIs. Persisted to localStorage for consistent privacy.

- Add walletBalancesBlurred state to GrimoireState
- Add toggleWalletBalancesBlur pure function in core/logic
- Make big balance in WalletViewer clickable with eye icon indicator
- Apply blur to all transaction amounts in list and detail views
- Add blur to send/receive dialog amounts
- Make balance in user menu wallet info clickable with eye icon
- Apply blur to balance in dropdown menu item

UX matches common financial app pattern: tap balance → blur on/off

* refactor: replace blur with fixed-width placeholders for privacy

Prevent balance size information leakage by using fixed-width
placeholder characters instead of blur effect. A blurred "1000000"
would still reveal it's a large balance vs "100" even when blurred.

Changes:
- Replace blur-sm class with conditional placeholder text
- Use "••••••" for main balances
- Use "••••" for transaction amounts in lists
- Use "•••••• sats" for detailed amounts with unit
- Use "•••• sats" for smaller amounts like fees

Security improvement: No information about balance size is leaked
when privacy mode is enabled. All hidden amounts appear identical.

* refactor: improve privacy UX with stars and clearer send flow

Three UX improvements to the wallet privacy feature:

1. Don't hide amounts in send confirmation dialog
   - Users need to verify invoice amounts before sending
   - Privacy mode now only affects viewing, not sending

2. Replace bullet placeholders (••••) with stars (✦✦✦✦)
   - More visually distinct and recognizable as privacy indicator
   - Unicode BLACK FOUR POINTED STAR (U+2726)
   - Better matches common "redacted" aesthetic

3. Reduce eye icon sizes for subtler presence
   - Main balance: size-6 → size-5
   - Wallet info dialog: size-3.5 → size-3
   - Smaller icons feel less intrusive

Result: Clearer privacy state, safer payment flow, better aesthetics.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 23:22:00 +01:00
Alejandro
72fb47224c feat: add NIP-57 zap command foundation (#141)
* feat: add NIP-57 zap command foundation

Implements the foundational structure for sending Lightning zaps (NIP-57) to
Nostr users and events. This commit adds the command interface, UI components,
and routing logic. The actual LNURL resolution and zap request creation will
be implemented in follow-up commits.

Components Added:
- ZapWindow: Full-featured UI for zapping with amount presets, custom amounts,
  wallet integration, and QR code fallback
- parseZapCommand: Parser supporting multiple formats (npub, nprofile, nevent,
  naddr, NIP-05, $me alias)
- Command registration in man pages with examples
- Window routing and title generation

Features:
- Preset amount buttons (21, 100, 500, 1000, 5000, 10000 sats)
- Custom amount input
- Amount usage tracking (remembers most-used amounts)
- Comment field for zap messages
- Event context rendering (shows zapped event in UI)
- Dual payment methods: NWC wallet or QR code/invoice copy
- Dynamic recipient resolution (from event author if zapping event)

Usage:
  zap fiatjaf.com                  # Zap a user by NIP-05
  zap npub1...                     # Zap a user by npub
  zap nevent1...                   # Zap an event (recipient = author)
  zap npub1... nevent1...          # Zap specific user for specific event

Next Steps:
- Implement LNURL-pay resolution (fetch callback URL and nostrPubkey)
- Create kind 9734 zap request event with applesauce factory
- Implement invoice generation via LNURL callback
- Integrate NWC wallet payment
- Add zap action to event context menus
- Implement zap receipt listening (kind 9735)

* feat: implement full NIP-57 zap flow with LNURL and NWC

Completes the production-ready implementation of Lightning zaps (NIP-57) with
full LNURL-pay resolution, zap request creation, NWC wallet payment, and QR
code fallback.

Core Implementation:

1. **LNURL Resolution** (src/lib/lnurl.ts)
   - Resolve Lightning addresses (lud16) to LNURL-pay endpoints
   - Validate zap support (allowsNostr, nostrPubkey)
   - Fetch invoices from LNURL callbacks with zap requests
   - Amount validation (min/max sendable)
   - Comment length validation

2. **Zap Request Creation** (src/lib/create-zap-request.ts)
   - Build kind 9734 zap request events using applesauce EventFactory
   - Sign with user's active account
   - Include recipient (p tag), amount, relays, optional event context (e/a tags)
   - Serialize to URL-encoded JSON for LNURL callbacks
   - Smart relay selection (user's inbox relays for zap receipts)

3. **ZapWindow Complete Flow** (src/components/ZapWindow.tsx)
   - Resolve recipient's Lightning address from profile (lud16)
   - Create and sign zap request with user credentials
   - Fetch invoice from LNURL callback
   - Pay with NWC wallet OR show QR code
   - QR code generation with qrcode library
   - Success feedback with LNURL success actions
   - Comprehensive error handling and user notifications
   - Toast notifications for each step

4. **Event Menu Integration** (src/components/nostr/kinds/BaseEventRenderer.tsx)
   - Add "Zap" action to event dropdown menu
   - Automatically includes event context (e or a tag)
   - Yellow zap icon () for visual consistency
   - Opens ZapWindow with pre-filled recipient and event

Flow Diagram:
1. User clicks "Zap" on event or runs `zap` command
2. Resolve recipient's lud16 → LNURL-pay endpoint
3. Validate zap support (allowsNostr, nostrPubkey)
4. Create kind 9734 zap request (signed by sender)
5. Send to LNURL callback → get BOLT11 invoice
6. Pay via NWC wallet OR show QR code
7. Zap receipt (kind 9735) published by LNURL service

Features:
-  Full NIP-57 compliance
-  LNURL-pay support with validation
-  Applesauce EventFactory for signing
-  NWC wallet integration
-  QR code fallback for manual payment
-  Event context (zapping specific notes/articles)
-  Amount presets with usage tracking
-  Custom amounts and comments
-  Comprehensive error handling
-  Step-by-step user feedback
-  Event menu integration

Security:
- Uses user's active account signer
- Validates LNURL responses
- Validates amount ranges
- No private key exposure
- HTTPS-only LNURL endpoints

Dependencies:
- qrcode: QR code generation
- applesauce-core: EventFactory for signing
- Existing NWC wallet implementation

Related: #135 (NWC wallet viewer)
Implements: NIP-57 (Lightning Zaps)

* fix: resolve TypeScript errors in ZapWindow

- Remove unused imports (useEffect, isAddressableKind, NostrEvent)
- Fix walletInfo access: fetch from getInfo() hook instead of direct property
- Store wallet info in component state with useEffect
- All TypeScript syntax errors resolved

* fix: resolve all TypeScript build errors in zap implementation

- ZapWindow.tsx: Import useProfile from @/hooks/useProfile instead of applesauce-react/hooks
- ZapWindow.tsx: Remove Tabs import (unused)
- ZapWindow.tsx: Remove htmlFor prop from Label components (not supported)
- create-zap-request.ts: Remove non-existent EventTemplate import
- create-zap-request.ts: Use inferred type for template object
- lnurl.ts: Prefix unused lnurl parameter with underscore

All TypeScript compilation errors resolved. Code ready for production build.

* fix: correct useProfile usage and ProfileContent handling in ZapWindow

- Remove eventStore argument from useProfile (takes pubkey and optional relay hints)
- Fix recipientProfile usage: already ProfileContent, don't call getProfileContent again
- Fix authorProfile: call getProfileContent on NostrEvent, not on ProfileContent
- Fix lud16/lud06 access: use recipientProfile directly
- Fix success toast: use recipientProfile?.name instead of content?.name

All type errors resolved. ProfileContent is returned by useProfile, not NostrEvent.

* feat: refine ZapWindow UI and add dynamic window title

UI Refinements per user request:
- Remove QrCode unused import
- Simplify payment flow to single adaptive button
- Button shows "Log in to Zap" if user can't sign
- Button shows "Pay with Wallet" if NWC available, else "Pay"
- Fix activeAccount usage to use accountManager.active
- Remove unused getProfileContent import
- Remove unused eventAuthorName variable

Dynamic Title:
- Add "Zap [username]" dynamic title in DynamicWindowTitle
- Fetches recipient profile and displays name or fallback
- Shows recipient's display name, name, or truncated pubkey

Build fixes:
- Fix TypeScript errors with unused imports
- Fix activeAccount.signer property access
- All tests passing (939 passed)

* feat: add emoji autocompletion to zap comments and refine UI

Zap Comment Enhancements:
- Replace plain Input with MentionEditor for emoji autocompletion
- Add NIP-30 emoji tag support to zap requests (kind 9734)
- Emoji tags are properly serialized and included in zap events
- Support :emoji: syntax with custom emoji from emoji search

Event Preview Refinements:
- Remove Card wrapper from zapped event preview
- Remove padding and borders for cleaner display
- Event renders directly without container styling

Implementation Details:
- Add EmojiTag interface to create-zap-request.ts
- Update ZapRequestParams to include emojiTags array
- Extract emoji tags from MentionEditor in handleZap
- Pass emoji tags through zap request creation pipeline
- Add useProfileSearch and useEmojiSearch hooks to ZapWindow
- Use MentionEditor ref to get serialized content with emojis

All tests passing (939 passed)
Build successful

* feat: compact ZapWindow UI and improve debugging

UI Improvements:
- Reduce padding from p-6 to p-4 and space-y-6 to space-y-3
- Convert amount grid to single-row flex layout with gap-1.5
- Add formatAmount() helper for shortened numbers (21, 1k, 5k, 10k)
- Move custom amount input inline with preset amounts
- Reduce button size to "sm" for more compact display
- Remove separate label for custom amount
- Make comment field more compact (removed min-height)

Debugging Enhancements:
- Add console.log for recipient profile and lud16/lud06
- Add logging for LNURL resolution steps
- Add logging for zap request creation
- Add logging for invoice fetch from callback
- Add debug logging for emoji search service initialization
- Test emoji search on mount to verify it's working

Number Format:
- 21 → "21"
- 1000 → "1k"
- 5000 → "5k"
- 10000 → "10k"
- Handles decimals: 1500 → "1.5k"

The compact layout makes better use of vertical space and provides
comprehensive debug logging to help troubleshoot LNURL and emoji issues.

All tests passing (939 passed)
Build successful

* fix: open LoginDialog instead of connection window for zap login

Changes:
- Import LoginDialog component into ZapWindow
- Add showLogin state to control LoginDialog visibility
- Update handleLogin to open LoginDialog instead of connection window
- Remove unused useGrimoire import and addWindow destructuring
- Add LoginDialog component to render alongside QR dialog

User Experience:
- "Log in to Zap" button now opens proper login dialog
- Users can log in with extension, readonly, nsec, or NIP-46
- After login, user can proceed with zap flow
- More intuitive than opening relay connection window

All tests passing (939 passed)
Build successful

* feat: auto-close ZapWindow after successful wallet payment

Changes:
- Add onClose callback prop to ZapWindowProps interface
- Pass onClose from WindowRenderer to ZapWindow component
- Call onClose() with 1.5s delay after successful wallet payment
- Allow user to see success toast before window closes

User Experience:
- After zapping with wallet, window automatically closes
- 1.5 second delay allows user to see success message
- Prevents accidental double-zapping
- Cleaner flow - no manual window closing needed

Implementation:
- WindowRenderer passes onClose callback to ZapWindow
- ZapWindow calls onClose after payment success and toasts
- setTimeout(onClose, 1500) provides brief delay for UX
- QR code path unchanged (window stays open for payment)

All tests passing (939 passed)
Build successful

* feat: full-width custom amount and wallet timeout with QR fallback

Custom Amount Input:
- Change custom amount input from w-24 to flex-1
- Now takes full remaining width in flex row
- Better UX on smaller screens and more obvious input field

Wallet Payment Timeout Handling:
- Add 30 second timeout to wallet payments using Promise.race
- On timeout, automatically show QR code as fallback
- Add paymentTimedOut state to track timeout condition
- Toast warning when payment times out

Retry with Wallet Feature:
- Add handleRetryWallet function to retry timed out payment
- Show "Retry with Wallet" button in QR dialog when timed out
- Button only appears if wallet is connected and payment capable
- Retry uses same 30s timeout, shows error if fails again
- Provides loading state with spinner during retry

User Flow:
1. User attempts wallet payment
2. If timeout after 30s, shows QR code automatically
3. User can scan QR to pay manually OR
4. User can click "Retry with Wallet" to try again
5. If retry times out, stays on QR for manual payment

Implementation Details:
- Promise.race between payInvoice and 30s timeout
- Timeout throws Error("TIMEOUT") for easy detection
- QR dialog conditionally shows retry button
- Retry resets state and attempts payment again
- Console logging for debugging timeout issues

All tests passing (939 passed)
Build successful

* feat: production-ready zaps with manual close and profile integration

Payment Flow Improvements:
- Remove auto-close after successful payment
- Change "Zap Sent!" button to "Done" button that requires user click
- User must manually close window by clicking Done after payment
- Retry payment also requires manual close after success

Profile Viewer Integration:
- Add Zap icon next to lightning address in ProfileViewer
- Click zap icon to open ZapWindow for that profile
- Yellow icon with hover effect for visual feedback
- Integrates seamlessly with existing profile UI

Production Cleanup:
- Remove all debug console.log statements
- Keep console.error for production error logging
- Remove unused emojiService variable from useEmojiSearch
- Fix Loader2 className typo (animate-spin)
- Clean code ready for production deployment

User Experience:
1. View profile with lightning address
2. Click yellow zap icon to open zap window
3. Enter amount and optional comment
4. Pay with wallet (or QR code if timeout)
5. See success message
6. Click "Done" to close window (manual control)

Testing:
- All lint checks pass (no errors, only warnings)
- TypeScript build successful
- All 939 tests passing
- Production-ready code

Code Quality:
- No debug logging in production
- Proper error handling maintained
- Clean, maintainable code
- Follows project conventions

* fix: remove duplicate zapTitle condition in DynamicWindowTitle

Removed duplicate zapTitle if-else branch at line 870 that was causing
lint error. The first zapTitle condition at line 803 handles all cases,
making the second occurrence unreachable.

* feat: improve zap UX with inline QR and faster imports

- Move imports to top level instead of dynamic imports for faster resolution
- Show QR code inline in ZapWindow instead of separate dialog
- Show recipient name and address when not zapping an event
- Make Lightning address clickable in ProfileViewer with icon on left
- Use recipientName consistently throughout zap flow

This significantly reduces the "Resolving Lightning address..." delay and
provides a cleaner, more integrated UX for viewing and paying invoices.

* feat: optimize zap UX with better error handling and UI improvements

LNURL improvements:
- Add 10s timeouts to Lightning address resolution and invoice fetching
- Better error messages with more context (response status, error text)
- Handle AbortError for timeout scenarios

UI improvements:
- Bigger amount buttons (default size instead of sm)
- Custom amount on separate line for better layout
- Disable all zap UI when recipient has no Lightning address
- Show clear warning when Lightning address is missing
- Only show comment editor when Lightning address is available

Toast cleanup:
- Remove chatty info toasts ("Resolving...", "Creating...", "Fetching...")
- Only show errors and success messages
- Cleaner, less noisy UX

This addresses common issues with LNURL requests timing out and makes
the UI more responsive and informative when zaps cannot be sent.

* feat: full-width custom amount and wallet timeout with QR fallback

QR code improvements:
- Add profile picture overlay in center of QR code (25% size, circular)
- Remove redundant "Copy Invoice" button (keep icon button only)
- Show "Open in Wallet" as full-width button

UI improvements:
- Use UserName component everywhere (clickable, styled, shows Grimoire members)
- Custom amount now full-width on separate line
- Better visual hierarchy

Default amounts updated:
- Changed from [21, 100, 500, 1000, 5000, 10000]
- To [21, 420, 2100, 42000]
- More aligned with common zap amounts

The profile picture overlay helps users identify who they're zapping
while maintaining QR code scannability. UserName component provides
consistent styling and clickable profile links.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 23:19:11 +01:00
Alejandro
e6e663c3d8 feat: add separate gradients for Grimoire members based on login state (#138)
* feat: add separate gradients for Grimoire members based on login state

Updates the UserName component to display different gradient colors for
Grimoire members depending on whether they are the logged-in user:
- Orange-yellow gradient for logged-in Grimoire members (current user)
- Purple-blue gradient for other Grimoire members (not logged in)

This visual distinction makes it easier to identify your own username
versus other Grimoire members in feeds and conversations.

* refine: make blue tone more subtle in Grimoire member gradient

Changes the non-logged-in Grimoire member gradient from cyan-500 to
blue-400 for a softer, more subtle blue tone on the bottom-right
diagonal of the gradient.

* refine: reposition blue to match yellow placement in gradients

Rearranges the purple-blue gradient to start with blue-400 (matching
the top-left position of yellow-500 in the orange-yellow gradient) and
transition through purple-500 to purple-600.

Gradient order:
- Logged-in: yellow → orange → orange
- Other members: blue → purple → purple

* refine: adjust purple tones to match accent color

Updates the gradient to use purple-400 and purple-500 (instead of
purple-500 and purple-600) to better match the accent purple (HSL 270
100% 70%) used for regular usernames in dark mode.

Final gradient: blue-400 → purple-400 → purple-500

* refine: improve gradients to match theme colors and work across themes

Updates both Grimoire member gradients to better align with their
non-member equivalents while ensuring visibility in both light and dark themes:

**Logged-in gradient** (orange-dominant to match highlight color):
- Changed: yellow-500 → orange-500 → orange-600
- To: orange-400 → orange-500 → amber-600
- Matches highlight (HSL 25 90% 35% light, 27 96% 61% dark)

**Non-logged-in gradient** (purple-dominant to match accent color):
- Changed: blue-400 → purple-400 → purple-500
- To: violet-500 → purple-500 → fuchsia-600
- Matches accent (HSL 270 100% 70% dark)
- More saturated to stand out in light theme

Both gradients now maintain their primary colors while being more
visible and consistent across light and dark themes.

* feat: add 4-color gradients with bright highlights for classy look

Enhances both Grimoire member gradients with a subtle bright color at
the top for a more stylish, classy appearance:

**Logged-in gradient:**
- yellow-300 → orange-400 → orange-500 → amber-600
- Bright yellow shimmer at top, deepening to amber

**Non-logged-in gradient:**
- violet-400 → violet-500 → purple-500 → fuchsia-600
- Lighter violet glow at top, transitioning to purple-pink

The 4-color gradients add depth and sophistication while maintaining
the theme color alignment (orange for highlight, purple for accent).

* refine: simplify to elegant 2-color gradients and add badge check icon

Simplifies Grimoire member styling for a more elegant appearance:

**Gradient changes:**
- Direction: bg-gradient-to-br → bg-gradient-to-tr (diagonal from top-left)
- Logged-in: Simplified to orange-400 → amber-600 (warm elegant gradient)
- Non-logged-in: Simplified to violet-500 → fuchsia-600 (cool elegant gradient)

**Badge check icon:**
- Added BadgeCheck icon after member names
- Icon scales automatically with username size using w-[1em] h-[1em]
- Provides visual verification of Grimoire membership

The simpler 2-color gradients with top-right direction create a more
refined, classy look while the badge check adds instant recognition.

* fix: make BadgeCheck icon visible by separating from gradient text

The badge icon was invisible because it inherited the text-transparent
class from the parent span. Fixed by:

- Moving gradient styling to only the displayName span
- Giving BadgeCheck its own explicit color (orange-500 for logged-in,
  violet-500 for others)
- Icon now properly visible and color-coordinated with the gradient

* feat: add subtle gradient to BadgeCheck icon matching username

The badge icon now has its own subtle gradient that continues from
the username gradient's ending color:

- Logged-in: amber-600 → amber-400 (darker to brighter amber)
- Others: fuchsia-600 → fuchsia-400 (darker to brighter fuchsia)
- Both use bg-gradient-to-tr direction to match username

This creates a cohesive visual flow where the gradient subtly
continues from the username into the verification badge.

* fix: use solid colors for BadgeCheck icon (gradients don't work on SVG)

The bg-clip-text technique only works on actual text elements, not SVG
paths. Reverted to solid colors that complement the gradient themes:

- Logged-in: text-amber-500 (matches the amber gradient tones)
- Others: text-fuchsia-500 (matches the fuchsia gradient tones)

The icon is now visible and color-coordinated with the username gradients.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 22:27:24 +01:00
Alejandro
582596df47 feat: display lightning address in NWC wallet info and clean up UI (#142)
- Add lightning address (lud16) display in wallet info dropdown
- Remove redundant "Connected" text from wallet heading
- Keep status indicator icon for cleaner, more compact UI

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 22:25:44 +01:00
Alejandro
c94203852e feat: detect and display zap payments in NWC wallet viewer (#140)
* feat: detect and display zap payments in NWC wallet viewer

Add intelligent zap payment detection and enhanced display in the NWC wallet transaction list and detail views.

Changes:
- Add wallet-utils.ts with zap request parsing (kind 9734 detection)
- Parse zap requests from transaction descriptions with LRU caching (500 entry limit)
- Display username + message preview in transaction list with  indicator
- Show full zap details in transaction detail dialog:
  - Zapper name (clickable UserName component)
  - Full zap message with RichText formatting
  - Zapped post rendered inline using KindRenderer
  - Loading states for event fetching
- Follow React Hooks rules (unconditional hook calls)
- Type-safe implementation with proper pointer handling

Technical notes:
- parseZapRequest() extracts sender, message, and event pointers from JSON-embedded zap requests
- Caching prevents redundant JSON parsing on re-renders
- Supports both event IDs (e tag) and address coordinates (a tag)
- parseAddressCoordinate() handles kind:pubkey:identifier format

* refactor: use applesauce caching pattern and RichText for zaps

Improvements:
- Replace Map-based cache with getOrComputeCachedValue pattern
  - Cache parsed zap requests on transaction objects using Symbol
  - Follows applesauce convention for computed value caching
  - More memory-efficient than global Map cache
- Use RichText component for zap messages in transaction list
  - Supports links, mentions, and other rich formatting
  - CSS truncation instead of JS string manipulation
- Update parseZapRequest to accept transaction object instead of description string
  - Enables proper caching on the transaction object
  - Cleaner API surface

Technical changes:
- Remove getZapMessagePreview() helper (now using CSS truncate)
- Add getOrComputeCachedValue import from applesauce-core/helpers
- Update all parseZapRequest call sites to pass transaction object
- Wrap zap message in RichText component for proper formatting

* feat: enhance zap display with RichText context and scrollable details

Improvements:
- Pass zap request event as context to RichText components
  - Enables proper mention/link resolution in zap messages
  - Supports interactive elements (mentions, hashtags, links)
  - Provides full event context for rendering
- Make transaction detail dialog scrollable
  - Add max-h-[90vh] to DialogContent with flex layout
  - Wrap content in overflow-y-auto container with max-h-[calc(90vh-8rem)]
  - Prevents dialog overflow when displaying large zapped posts
  - Smooth scrolling for long zap message threads
- Reduce transaction page size from 20 to 10
  - Better performance with rich zap rendering
  - Faster initial load and scroll rendering
  - Reduces memory footprint for transaction list

Technical changes:
- Add zapRequestEvent field to ZapRequestInfo interface
- Pass zapRequestEvent to all RichText components rendering zap messages
- Update BATCH_SIZE constant from 20 to 10
- Add flex layout to DialogContent for proper scrolling
- Add pr-2 padding to scrollable container for visual spacing

* revert: restore BATCH_SIZE to 20 transactions

* refactor: improve zap transaction list item UI

Changes:
- Use UserName component for zapper display (applies accent color)
- Remove colon separator between username and message
- Keep username and message on single line with proper truncation
- Remove unused imports (getDisplayName, useProfile)
- Reduce transaction detail dialog max height from 90vh to 70vh
  - More compact display for better UX
  - Prevents excessive white space

UI improvements:
- Zap icon + UserName (accent color) + message all on one line
- UserName component is flex-shrink-0 to prevent squishing
- Message text truncates with CSS overflow
- Cleaner, more compact visual hierarchy

* fix: improve spacing and truncation in zap transaction items

- Increase gap between username and message from gap-1 to gap-2 (0.5rem)
- Add min-w-0 to message span for proper ellipsis truncation in flex
- Remove duplicate truncate class from parent div to prevent conflicts
- Message now properly shows ellipsis (...) when it doesn't fit on one line

* feat: add line-clamp and expandable raw transaction view

Changes:
- Replace truncate with line-clamp-1 on zap message for proper single-line clamping
- Add expandable 'Show Raw Transaction' section in transaction detail dialog
  - Collapsible with ChevronRight/ChevronDown icons
  - Shows JSON.stringify(transaction, null, 2) in scrollable pre block
  - Uses CodeCopyButton component for consistent copy UX
  - Max height 60 (15rem) with overflow-y-auto for long transactions
- Add state management for raw transaction expansion and copy status
- Reset raw transaction state when dialog closes

UI improvements:
- Clean expansion interaction with hover effects
- Properly formatted JSON with 2-space indentation
- Accessible copy button with aria-label
- Auto-collapses when closing the dialog

* feat: parse zap requests from invoice description as fallback

Enhance zap request parsing to check multiple sources:
- First try transaction.description (primary source)
- If not found, decode the Lightning invoice and check its description field
- This handles cases where the zap request is embedded in the invoice

Changes:
- Extract parsing logic into tryParseZapRequestJson() helper
- Add invoice field to parseZapRequest() transaction parameter
- Import light-bolt11-decoder to decode invoices
- Try invoice description as fallback when tx description doesn't contain zap
- Maintain applesauce caching pattern on transaction object

This ensures zap payments are detected and displayed correctly regardless
of where the zap request JSON is stored (tx description vs invoice description).

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 21:52:44 +01:00
Alejandro
3172288ecf feat: use RelayLink component in wallet info dropdown (#139)
Replace plain text relay URL with RelayLink component in WalletViewer's
info dropdown. This provides:
- Clickable relay URLs that open relay detail window
- Relay icon display (if available)
- Insecure relay warnings (ws://)
- Consistent styling with other relay displays

Also removes unused ExternalLink import.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 20:28:46 +01:00
Alejandro
3408872811 feat: add comprehensive NWC wallet viewer with dynamic UI (#135)
* feat: add comprehensive NWC wallet viewer with dynamic UI

Implements a full-featured Lightning wallet interface using Nostr Wallet
Connect (NWC/NIP-47) with method-based UI that adapts to wallet capabilities.

**New Features:**
- WalletViewer component with tabbed interface (Overview, Send, Receive, Transactions)
- Real-time balance display with manual refresh
- Send Lightning payments via BOLT11 invoices
- Generate invoices with QR codes for receiving payments
- Transaction history viewer (when supported by wallet)
- Wallet info and capabilities display
- Enhanced useWallet hook with additional NWC methods

**Enhanced Methods:**
- listTransactions() - View recent payment history
- lookupInvoice() - Check invoice status by payment hash
- payKeysend() - Direct keysend payments to node pubkeys

**UI Features:**
- Dynamic tabs based on wallet capabilities
- QR code generation for invoices
- Copy-to-clipboard for invoices
- Error handling with user-friendly messages
- Loading states for async operations
- Empty states for no wallet connection

**Command:**
- New `wallet` command to open the wallet viewer

**Technical Details:**
- Integrates with existing NWC service singleton
- Uses reactive balance$ observable for auto-updates
- Proper TypeScript types aligned with applesauce-wallet-connect
- Follows Grimoire patterns for window system integration
- Lazy-loaded component for optimal bundle size

All tests passing. Build verified.

* refactor: redesign wallet UI to single-view layout with virtualized transactions

Converts the tabbed wallet interface to a conventional single-view layout
with improved UX and performance optimizations.

**Layout Changes:**
- Removed tabs in favor of single-page layout
- Balance header at top with wallet name and refresh button
- Side-by-side Send/Receive cards for quick access
- Transaction history below with virtualized scrolling
- Disconnect button at bottom of page

**New Features:**
- Connect Wallet button when no wallet is connected (opens dialog in-app)
- Wallet capabilities shown in tooltip on info icon
- Virtualized transaction list using react-virtuoso
- Batched transaction loading (20 per batch)
- Automatic "load more" when scrolling to bottom
- Loading states for initial load and pagination
- "No more transactions" message when exhausted

**Performance Improvements:**
- Virtualized list rendering for smooth scrolling with many transactions
- Only renders visible transactions in viewport
- Lazy loads additional batches on demand
- Reduced initial load to 20 transactions instead of 50

**UX Improvements:**
- More conventional wallet UI pattern
- Send/Receive always visible (no tab switching)
- QR code and invoice appear inline when generated
- Info icon with tooltip for capabilities (cleaner than full card)
- Disconnect option always accessible at bottom

**Technical Details:**
- Fixed transaction loading race condition with separate useEffect
- Proper dependency tracking for loadMoreTransactions callback
- Footer component in Virtuoso for loading/end states
- Responsive grid layout for Send/Receive cards

All tests passing. Build verified.

* refactor: redesign wallet UI with ReqViewer-style header and dialogs

Implements a cleaner, more consistent wallet UI matching Grimoire patterns.

**Header Changes:**
- ReqViewer-style compact header (border-b, px-4, py-2, font-mono, text-xs)
- Left side: Wallet name · Balance with info tooltip
- Right side: Icon-only action buttons (Receive, Send, Refresh)
- All actions use tooltips for labels
- Consistent spacing and typography with rest of app

**Send/Receive as Dialogs:**
- Removed always-visible Send/Receive cards
- Converted to modal dialogs opened from header icons
- Send dialog: Invoice input + optional amount override
- Receive dialog: Two-step flow (generate → show QR + invoice)
- "Generate Another" button to create multiple invoices without closing
- Cleaner main view focused on transaction list

**Transaction List Cleanup:**
- Removed Card wrapper and extra padding
- Direct virtualized list fills viewport
- Removed "Transaction History" heading (redundant)
- Added flex-shrink-0 to prevent transaction items from shrinking
- Improved truncation handling for long descriptions
- Hover effect for better interactivity
- Footer messages use consistent border-b styling

**Disconnect Button:**
- Changed to destructive variant (red appearance)
- Moved to footer with border-t
- Full width for emphasis
- Clearer visual indication of destructive action

**UX Improvements:**
- More screen space for transactions (primary content)
- Consistent icon sizing (size-3 for header actions, size-4 for tx icons)
- Better visual hierarchy with header actions
- Dialog pattern matches other Grimoire components
- Receive dialog stays open to show generated invoice

**Technical Details:**
- Proper dialog state management
- Invoice QR with max-height scrolling for long invoices
- Reset form state when closing dialogs
- Maintained all existing functionality
- Optimized for virtualized scrolling performance

All tests passing. Build verified.

* feat: final wallet UI improvements with prominent balance and enhanced UX

Redesigns wallet viewer with cleaner layout and improved user experience.

**Layout Changes:**
- Big centered balance display (4xl font, prominent)
- Large Send/Receive buttons below balance (side by side)
- Single-line transaction items with better spacing
- Info dropdown next to refresh button in header

**Transaction List:**
- Single-line compact design (description + time + amount)
- No +/- signs on amounts (cleaner look)
- Generic "Payment"/"Received" labels when description missing
- Time displayed in compact format (HH:MM)
- Day markers between days (Today/Yesterday/Jan 15)
- Virtualized scrolling with batched loading

**Info Dropdown:**
- Wallet capabilities shown in dropdown (Info icon + ChevronDown)
- Network information
- Methods displayed as compact badges
- Notifications support

**User Menu Integration:**
- Wallet option always visible (regardless of account status)
- Clicking wallet opens wallet window (not info dialog)
- Balance shown inline when connected
- "Connect Wallet" option when not connected

**Dialog Improvements:**
- Send dialog with confirmation step
- Receive dialog with payment detection
- Auto-close on payment received
- QR code with loading overlay during payment check

**Visual Hierarchy:**
- Header: Wallet name (left) | Info dropdown + Refresh (right)
- Big centered balance with "sats" label
- Prominent action buttons (Send default, Receive outline)
- Clean transaction list with hover states
- Destructive disconnect button in footer

All tests passing  Build verified 

* fix: replace AlertDialog with Dialog for disconnect confirmation

- AlertDialog component doesn't exist in UI library
- Use regular Dialog with custom footer buttons instead
- All 929 tests passing, build successful

* refine: wallet UI improvements based on feedback

- Remove "sats" text from balance display
- Swap send/receive button positions (receive left, send right)
- Remove top border from transaction list
- Remove timestamps from transaction list items
- Add relay link to wallet info dropdown with external link icon
- Change disconnect button to destructive color (always red)
- Fix imports and remove unused formatTime function

* feat: enhance send/receive flows with invoice parsing and auto-confirm

Send flow improvements:
- Parse BOLT11 invoices using light-bolt11-decoder
- Auto-proceed to confirm step when valid invoice is entered
- Show parsed amount and description in confirmation dialog
- Validate invoice before allowing confirmation

Receive flow improvements:
- Fix invoice overflow with proper truncate display
- Use nested div structure for single-line truncation

All changes preserve type safety with proper Section type guards

* feat: add Lightning address support and refine auto-confirm behavior

Send flow enhancements:
- Only auto-proceed to confirm if invoice has an amount (not for zero-amount invoices)
- Add Lightning address (LNURL-pay) support with automatic resolution
- Fetch invoice from Lightning address with amount validation
- Show "Resolving..." loading state when processing Lightning addresses
- Update UI labels and placeholders to indicate Lightning address support
- Require amount field for Lightning address payments

Lightning address flow:
1. Detect @ symbol in input (and not starting with "ln")
2. Validate amount is provided
3. Fetch LNURL-pay endpoint from .well-known/lnurlp/{username}
4. Check min/max sendable amounts
5. Request invoice from callback with specified amount
6. Parse and confirm invoice details

Error handling:
- Invalid Lightning address format
- Failed to fetch Lightning address
- Amount out of range (too small/too large)
- Failed to generate invoice from callback

* fix: UI improvements for wallet viewer and mobile receive flow

Confirmation dialog improvements:
- Remove yellow warning styles (border, background, icon)
- Fix amount calculation: show proper sats conversion with Math.floor
- Clean layout with key-value pairs instead of cluttered text
- Show either invoice amount OR override amount (not both)

Mobile receive flow improvements:
- Large prominent "Copy Invoice" button (h-12, full width)
- Invoice box now tappable to copy
- Better touch targets for mobile users
- Clearer label: "Invoice (tap to view)"

Connection status:
- Show green dot + "Connected" next to wallet name in header
- Clear visual feedback that wallet is online

Auto-launch wallet:
- After successfully connecting wallet, automatically open wallet window
- Added onConnected callback to ConnectWalletDialog
- Wire up callback in user menu to call openWallet

Code cleanup:
- Remove unused AlertTriangle import

* fix: correct amount handling and mobile invoice display

Amount input fixes:
- Changed from millisats to sats for user input
- Updated label: "Amount (sats, optional)"
- Fixed Lightning address resolution: no division, amount is already in sats
- Fixed payInvoice: multiply by 1000 to convert sats to millisats for NWC
- Fixed confirmation dialog: removed incorrect division by 1000

Before: User enters 1000 (meant as sats) → system treats as 1000000 millisats → sends 1000 sats
After: User enters 1000 sats → system converts to 1000000 millisats → sends 1000 sats ✓

Invoice parsing was correct - already converting millisats to sats.
The bug was in the confirmation display and amount submission.

Mobile receive invoice fix:
- Removed nested div with truncate (was causing overflow)
- Changed to break-all + line-clamp-2
- Invoice now wraps properly on mobile (2 lines max)
- Still tappable to copy

* fix: convert sats to millisats when generating invoices

Critical bug fix - we were generating nearly amountless invoices.

Before:
- User enters 1000 sats in UI
- We passed 1000 to makeInvoice (thinking it's millisats)
- Generated invoice for 1000 millisats = 1 sat

After:
- User enters 1000 sats in UI
- We multiply by 1000 → 1,000,000 millisats
- Generated invoice for 1,000,000 millisats = 1000 sats ✓

This matches the send flow where we also convert sats to millisats
before passing to NWC protocol.

* refactor: production-readiness improvements for WalletViewer

Critical fixes:
- Wrap loadWalletInfo and loadInitialTransactions in useCallback to prevent infinite loops
- Fix dependency arrays in useEffect hooks
- Fix early return bug in handleConfirmSend that left sending state stuck
- Add async/await error handling to handleCopyInvoice
- Change throw new Error instead of early return in Lightning address resolution

Performance optimizations:
- Removed duplicate function definitions (moved to useCallback hooks)
- Proper memoization of wallet loading functions

Code quality:
- All functions properly handle errors with try/catch
- Loading states properly reset in finally blocks
- No memory leaks from useEffect dependencies

* fix: prevent infinite transaction loading loop in WalletViewer

Previously, the transaction list would try to load constantly and fail
repeatedly due to a circular dependency in the useEffect hooks. The
listTransactions function from useWallet wasn't wrapped in useCallback,
causing loadInitialTransactions to be recreated on every render, which
triggered the useEffect infinitely.

Changes:
- Add txLoadAttempted and txLoadFailed flags to prevent repeated attempts
- Only attempt to load transactions once on wallet connection
- Fail silently on initial load (no toast spam)
- Show retry button when transaction loading fails
- Reset flags when wallet connects/disconnects or after successful payments
- Make transaction list truly optional - wallet still works if loading fails

This ensures a better UX when wallets don't support list_transactions
or when the method fails for any reason.

* fix: remove circular dependency in loadInitialTransactions

Removed txLoadFailed from the dependency array of loadInitialTransactions
callback, which was causing a circular dependency:
- loadInitialTransactions depended on txLoadFailed
- Function sets txLoadFailed, triggering recreation
- New function reference triggers useEffect again
- Infinite loop

The txLoadAttempted flag in the useEffect is sufficient to prevent
repeated loads. No need to check txLoadFailed inside the callback.

* fix: resolve React error #185 by removing callbacks from useEffect dependencies

React error #185 occurs when state updates happen during render due to
unstable function references in useEffect dependencies. The root cause
was that functions from useWallet (getInfo, listTransactions) aren't
memoized, so they create new references on every render, causing the
useEffects that depend on them to run repeatedly.

Changes:
- Removed loadWalletInfo and loadInitialTransactions callbacks
- Use refs (walletInfoLoadedRef, lastConnectionStateRef) to track state
- Call async functions directly in useEffect without depending on them
- Created reloadTransactions() helper that just resets flags
- Simplified all reload logic to use the helper

This ensures:
- No circular dependencies in useEffect
- Functions only run once when conditions are met
- No state updates during render
- Clean, predictable loading behavior

* fix: properly clear wallet state on disconnect and update copy

Changes:
- Clear all wallet state when disconnecting (transactions, walletInfo, loading flags)
- Previously only cleared state on connect, leaving stale data visible
- Remove "Mutiny" mention from connection dialog copy
- Update to generic "NWC wallet provider" text

Now when you disconnect the wallet with the window open, it properly
clears all data and returns to the "No Wallet Connected" state.

* fix: prevent wallet from auto-reconnecting after disconnect

The bug was in the useWallet hook which automatically restores the wallet
if nwcConnection exists in state but the wallet instance is null. When
disconnecting, it only cleared the wallet instance but left nwcConnection
in state, causing an immediate reconnection.

Fix:
- Call disconnectNWCFromState() to clear nwcConnection from Grimoire state
- Then call disconnect() to clear the wallet service
- This prevents the auto-restore logic from triggering

Now when you disconnect the wallet, it stays disconnected until you
manually reconnect.

* security: add critical production-ready security fixes

Invoice Validation & Expiry Checks:
- Validate BOLT11 invoice format (must start with 'ln')
- Check invoice expiry before displaying/processing
- Validate amount is reasonable (< 21M BTC)
- Surface parse errors to user with toast notifications
- Prevent processing of expired invoices

Lightning Address Security:
- Enforce HTTPS-only for LNURL-pay requests
- Add 5-second timeout to all HTTP requests
- Validate callback URLs use HTTPS
- Proper AbortController cleanup on timeout
- Better error messages for network failures

Rate Limiting:
- Balance refresh: minimum 2 seconds between calls
- Transaction reload: minimum 5 seconds between reloads
- User-friendly warning messages with countdown
- Prevents spam to wallet service providers

Storage Security Warning:
- Add prominent security notice in ConnectWalletDialog
- Warn users about browser storage implications
- Advise to only connect on trusted devices

Capability Detection:
- Hide Send button if wallet doesn't support pay_invoice
- Hide Receive button if wallet doesn't support make_invoice
- Dynamic button rendering based on wallet capabilities
- Prevents errors from unsupported operations

Error Handling:
- WindowErrorBoundary already wraps all windows (verified)
- Proper error propagation with user-friendly messages
- No silent failures on critical operations

These changes significantly improve security and production-readiness
without breaking existing functionality.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 20:05:19 +01:00
Alejandro
1756715e30 feat: add site icon and favicon (#137)
* feat: add site icon and favicon

- Downloaded and cropped Grimoire icon from nostr.build
- Created multiple favicon sizes (16x16, 32x32, 180x180, 512x512)
- Generated traditional .ico format and PNG variants
- Added Apple touch icon for iOS devices
- Updated index.html with proper favicon links

* feat: make Grimoire installable as a PWA

- Created web manifest (site.webmanifest) with app metadata
  - Added app name, description, theme colors
  - Configured standalone display mode for native-like experience
  - Included all required icon sizes (192x192, 512x512)
  - Added keyboard shortcut for command palette
- Generated 192x192 icon for PWA requirements
- Added manifest and theme-color meta tags to index.html
- Implemented service worker (sw.js) for offline functionality
  - Network-first caching strategy for optimal performance
  - Precaches core assets on install
  - Provides offline fallback for navigation requests
- Registered service worker in main.tsx

Users can now install Grimoire as a standalone app on desktop and mobile devices.

* fix: properly configure maskable PWA icons

The previous configuration incorrectly marked regular icons as "maskable",
which would cause them to be cropped when displayed in circular or rounded
shapes on Android devices.

Changes:
- Created dedicated maskable icons with 10% padding (safe zone)
- Maskable icons use dark background (#020817) matching app theme
- Separated "any" and "maskable" purposes in manifest
- Regular icons (192x192, 512x512) use full space with purpose="any"
- Maskable icons (192x192-maskable, 512x512-maskable) have padding with purpose="maskable"

This ensures icons display correctly in all contexts:
- Regular icons for browser tabs, shortcuts, splash screens
- Maskable icons for adaptive icon shapes on Android

* chore: simplify PWA manifest

- Simplify name to just 'Grimoire'
- Add 'nostr' to categories for better discoverability
- Remove shortcuts (not needed for initial launch)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 18:39:07 +01:00
Alejandro
b70eb82fea feat: add @domain alias for NIP-05 domain directory resolution (#136)
Add support for @domain syntax in req and count commands to query all
users from a domain's NIP-05 directory (e.g., @habla.news).

Features:
- Fetches /.well-known/nostr.json from domain
- Extracts all pubkeys from the names object
- Works with -a (authors), -p (#p tags), and -P (#P tags) flags
- Supports mixed usage with npub, hex, NIP-05, $me, $contacts
- 5-minute caching for domain lookups
- UI display in ReqViewer query dropdown

Implementation:
- Added resolveDomainDirectory and resolveDomainDirectoryBatch to nip05.ts
- Updated req-parser and count-parser to detect @domain syntax
- Updated argParsers in man.ts to resolve domains asynchronously
- Updated ReqViewer to display queried domains in dropdown
- Added comprehensive tests for domain resolution

Examples:
- req -k 1 -a @habla.news
- req -k 7 -p @nostr.band
- count relay.damus.io -k 1 -a @getcurrent.io

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 14:49:37 +01:00
Alejandro
4d90aab83c feat: add Grimoire member system with special NIP-05 usernames (#134)
* feat: add Grimoire member system with special NIP-05 usernames

Implements a member verification system for Grimoire project contributors
with custom usernames and visual badges.

Features:
- Member registry with pubkey to username mapping
- _ (underscore) username for ce3cd5ba...
- verbiricha username for 7fa56f5d...
- Special @grimoire.pro NIP-05 style display
- BookOpen icon badge for verified members
- Integration with UserName and NIP-05 components
- Comprehensive test suite for member utilities

The system prioritizes Grimoire member usernames over regular NIP-05
identifiers and adds visual badges throughout the UI for member recognition.

* chore: update TypeScript build info

* feat: configure NIP-05 verification for grimoire.rocks domain

Updates Grimoire member system to use grimoire.rocks domain and adds
proper NIP-05 verification infrastructure.

Changes:
- Update member NIP-05 identifiers from @grimoire.pro to @grimoire.rocks
- Create public/.well-known/nostr.json with member pubkey mappings
- Configure Vercel to serve nostr.json with proper headers:
  - Content-Type: application/json
  - Access-Control-Allow-Origin: * (required for NIP-05)
  - Cache-Control: public, max-age=3600
- Update rewrites to exclude .well-known paths from SPA routing

This enables NIP-05 verification for:
- _@grimoire.rocks → ce3cd5ba...
- verbiricha@grimoire.rocks → 7fa56f5d...

* refactor: integrate Grimoire member styling into UserName component

Simplifies the member system by removing the separate GrimoireUsername
component and handling everything directly in UserName.

Changes:
- UserName now checks isGrimoireMember and displays special styling
- Grimoire members show with yellow-orange gradient (from-yellow-400 to-orange-500)
- Member username displayed as "username@grimoire.rocks"
- Removed GrimoireUsername and GrimoireBadge components
- Updated nip05 to skip display for Grimoire members (UserName handles it)

This consolidates the logic and creates a cleaner, more maintainable
architecture where UserName is the single source of truth for all
username displays.

* feat: update _ member pubkey and show username only with gradient

Updates Grimoire member system to use the correct nprofile for _ and
displays member usernames without the @grimoire.rocks suffix.

Changes:
- Update _ member pubkey to 60dfe8bda... (from nprofile with relay hints)
- UserName component now shows just the username for Grimoire members
- Yellow-orange gradient styling applied to member usernames
- Updated nostr.json with new _ pubkey
- Fixed all tests to use correct pubkeys

Member usernames now display as:
- "_" with gradient (not "_@grimoire.rocks")
- "verbiricha" with gradient (not "verbiricha@grimoire.rocks")

* feat: update _ member pubkey and add diagonal gradient styling

Updates the _ member pubkey and enhances the visual styling with a more
stylish diagonal gradient effect.

Changes:
- Update _ member pubkey to c8fb0d3a... (new pubkey)
- Change gradient from horizontal to diagonal (bg-gradient-to-br)
- Add middle color stop (via-orange-400) for smoother gradient
- Updated nostr.json with new _ pubkey
- Updated all tests with correct pubkey
- Chat system already uses UserName component (verified)

Member usernames now display with a vibrant yellow-to-orange diagonal
gradient that flows from top-left to bottom-right with smooth transitions.

* refactor: show profile username with theme-aware gradient

Updates UserName component to display the user's actual profile name
instead of the Grimoire username, while still applying gradient styling
to Grimoire members. Also improves gradient colors for better visibility
across all themes.

Changes:
- Always use profile name from getDisplayName (not Grimoire username)
- Grimoire members still get gradient styling applied to their profile name
- Updated gradient colors to darker shades for better contrast:
  - from-yellow-500 (was yellow-400)
  - via-orange-500 (was orange-400)
  - to-orange-600 (was orange-500)
- Removed unused getGrimoireUsername import

The darker gradient colors (#eab308 → #f97316 → #ea580c) provide better
readability on both light and dark themes while maintaining the vibrant
yellow-to-orange diagonal effect.

* chore: simplify and fix tests for production readiness

Removes redundant test file and updates comments for accuracy.

Changes:
- Remove src/lib/nip05.test.ts (tested nprofile decoding not used in implementation)
- Update comments in grimoire-members.ts:
  - Change "@grimoire.pro" references to "@grimoire.rocks"
  - Remove outdated "verification badges" mentions
  - Clarify that members get "gradient styling" not badges

The member system is now cleaner and production-ready with:
- Hardcoded member pubkeys in grimoire-members.ts
- NIP-05 verification served via public/.well-known/nostr.json
- Gradient styling applied to member usernames via UserName component
- Proper test coverage via grimoire-members.test.ts

* chore: remove grimoire-members test file

Removes test file as part of simplifying the codebase for production.
The member system is simple enough that it doesn't require dedicated tests.

* fix: show profile NIP-05 for Grimoire members

Removes the check that was hiding NIP-05 for Grimoire members.
Members now display whatever NIP-05 they have in their profile,
just like everyone else, while still getting gradient username styling.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 12:54:50 +01:00