Commit Graph

342 Commits

Author SHA1 Message Date
Alejandro Gómez
f7d00f1dfd fix: Resolve TypeScript errors in NIP-17 adapter and gift-wrap service
- Remove invalid actionOpts parameter from GiftWrapBlueprint.create()
  Gift wrap blueprints don't accept meta tag options; those belong on
  the rumor itself which already receives actionOpts correctly.

- Fix SubscriptionResponse type handling in gift-wrap.ts
  Use type guard to safely check for event objects in subscription
  responses instead of accessing properties that may not exist.

Fixes TypeScript compilation errors preventing production builds.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 16:07:07 +01:00
Alejandro Gómez
28e8d30ac9 fix: Update error message to include --json flag option
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 16:02:27 +01:00
Alejandro Gómez
345b760cec feat: Add unsigned event support and kind 14 DM renderer
- Add --json flag to open command for passing raw unsigned events
- Create DMRumorRenderer for kind 14 (NIP-17 DM rumors)
- Hide copy/open options for unsigned events in all menus
- Clean up EventDetailViewer header for unsigned events (no text shown)
- Auto-detect unsigned events in ChatMessageContextMenu

Unsigned events (like NIP-17 rumors) now properly render and display
across the app without showing misleading copy/open options that won't
work for events without signatures.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 15:55:16 +01:00
Alejandro Gómez
5bf618ff01 fix: disable pointer events 2026-01-16 15:24:16 +01:00
Alejandro Gómez
9776c71a99 ui: tweaks 2026-01-16 15:23:25 +01:00
Alejandro Gómez
b6d03a1644 fix: Use persistent subscription instead of request for real-time DMs
CRITICAL FIX: Changed from pool.request() to pool.subscription() to keep
the relay connection open after EOSE. This was the root cause of why
received messages didn't appear automatically.

**Root Cause:**

1. pool.request() fetches historical events and CLOSES after EOSE
2. After EOSE, relay subscription is terminated
3. New gift wraps sent after that aren't received from relay
4. EventStore timeline subscription only fires when events are added
5. Result: Only locally-sent messages appeared (added to EventStore),
   but remotely-received messages were invisible until manual sync

**Solution:**

Use pool.subscription() which keeps the WebSocket connection OPEN after
EOSE, allowing real-time message delivery.

**Changes:**

src/services/gift-wrap.ts:
* Changed pool.request() to pool.subscription()
* Added detailed logging for EVENT and EOSE responses
* Properly stores relay subscription in subscriptions array for cleanup
* Connection stays open indefinitely for real-time updates

**Expected Behavior:**

After EOSE:
-  Relay connection stays open (WebSocket active)
-  New gift wraps received in real-time from relay
-  EventStore.add() called automatically (via eventStore option)
-  Timeline subscription fires
-  Message appears in UI within 500ms

Console logs should show:
1. 'Opening subscription to X relays for real-time gift wraps'
2. '✓ EOSE from wss://relay... (subscription stays open)'
3. '📨 Received gift wrap xxxxxx from relay' (when message arrives)
4. '📬 Timeline subscription fired with X gift wraps'
5. '💬 Updated conversations: X conversations, X total rumors'

**Testing:**

1. Login and open self-chat
2. Send message from another client/device
3. Message should appear automatically within 500ms
4. No manual sync needed
5. Works for both self-chat and regular DMs

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 14:34:25 +01:00
Alejandro Gómez
cb98fc129b fix: Auto-initialize gift wrap subscriptions on login
CRITICAL FIX: Gift wrap service was only initialized when opening the
inbox viewer, meaning users wouldn't receive DMs unless they had the
inbox window open. This caused the issue where sent messages appeared
but received messages didn't.

**Root Cause:**

1. Gift wrap service init only happened in InboxViewer component
2. Default settings had enabled=false
3. Subscription only started if enabled=true
4. Result: No subscription → no incoming messages

**Solution:**

Moved gift wrap service initialization to useAccountSync hook, which runs
globally whenever a user is logged in. This ensures DM subscriptions are
always active.

**Changes:**

1. src/hooks/useAccountSync.ts:
   * Added gift wrap service initialization when account changes
   * Auto-enables inbox sync on first login (enabled=true)
   * Properly cleans up on logout/account change
   * Runs globally in AppShell, not just in inbox viewer

2. src/components/InboxViewer.tsx:
   * Removed redundant service initialization
   * Added comment explaining global init
   * Kept signer update effect for account switching

**Expected Behavior:**

-  User logs in → gift wrap subscription starts automatically
-  DMs received in real-time even when inbox viewer closed
-  Both sent and received messages appear live in chat
-  Works with self-chat and group chats
-  Proper cleanup on logout

**Testing:**

1. Login to account
2. Console should show: '[useAccountSync] Initializing gift wrap service'
3. Open chat with someone (don't open inbox viewer)
4. Have them send you a message
5. Message should appear automatically within 500ms
6. Send a message back
7. Both messages visible without manual sync

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 14:31:41 +01:00
Alejandro Gómez
024be8d7ea feat: Custom self-chat implementation bypassing applesauce-actions
Replaced action-based self-chat with direct implementation using
publishEventToRelays. This eliminates the dependency on patched
node_modules and provides a more robust solution.

**Why This Change:**

The previous approach relied on patching applesauce-actions in node_modules
to fix the relay hint bug. This patch would be lost on every npm install,
making it fragile and non-reproducible.

**New Self-Chat Flow:**

1. Create rumor using WrappedMessageBlueprint (kind 14)
2. Wrap rumor using GiftWrapBlueprint (kind 1059)
3. Persist encrypted content to Dexie BEFORE publishing
4. Publish directly to inbox relays using publishEventToRelays
5. Add to EventStore for immediate local availability

**Benefits:**

-  No dependency on node_modules patches
-  Guaranteed to work across npm installs
-  Direct control over relay selection
-  Encrypted content persisted before publish (no race conditions)
-  Clean separation: self-chat vs group chat logic

**Non-Self-Chat:**

Still uses SendWrappedMessage action (with patched applesauce for now).
Future work: Extend custom implementation to all message types.

**Testing:**

Self-chat messages should now:
- Appear live within 500ms (no manual sync)
- Persist across page reloads
- Match subscribed relay URLs exactly
- Include proper console logging at each step

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 14:25:52 +01:00
Alejandro Gómez
b6dad2429c debug: Add comprehensive logging to diagnose live message delivery
Added detailed logging throughout gift-wrap service to trace:
- Timeline subscription firing
- Gift wrap detection (symbol vs persisted)
- Conversation updates
- Rumor extraction

Also fixed updateConversations to check persistedIds in addition to
in-memory symbol, ensuring gift wraps are processed correctly even
after persistence.

This will help identify why messages don't appear live and require
manual sync.
2026-01-16 14:02:40 +01:00
Alejandro Gómez
ae46088333 fix: Relay normalization and live message delivery for NIP-17
This commit fixes two critical issues preventing live message delivery
in self-chat and other NIP-17 conversations:

**Issue 1: applesauce-actions relay hint bug (PATCHED IN NODE_MODULES)**
- Root cause: Action used gift wrap's random ephemeral pubkey to look up
  inbox relays, but the map was keyed by real recipient pubkeys
- Result: inboxRelays.get(giftWrap.pubkey) returned undefined
- publishEvent received no relay hints and failed with "No relays found"
- Messages never published

**Issue 2: Relay URL normalization mismatch**
- Root cause: Inbox relays from kind 10050 not normalized (kept raw format)
- Result: URLs with/without trailing slashes treated as different relays
  Example: wss://relay.com/ vs wss://relay.com
- Published to: wss://frens.nostr1.com/ (with slash)
- Subscribed to: wss://frens.nostr1.com (without slash)
- Messages sent but subscription never received them (different relay!)
- User had to click "Sync" to manually fetch from all relay variations

**Changes:**

1. src/services/gift-wrap.ts:
   * Added normalizeRelayURL import
   * Normalize all inbox relay URLs when loading from kind 10050 event
   * Ensures consistent URL format for subscription matching

2. src/services/hub.ts:
   * Added normalizeRelayURL import
   * Normalize relay hints before publishing
   * Ensures sent messages go to same normalized relay as subscription

3. node_modules/applesauce-actions/dist/actions/wrapped-messages.js:
   * PATCHED: Track recipient pubkey alongside gift wrap
   * Use recipientPubkey (real user) instead of giftWrap.pubkey (ephemeral)
   * Ensures correct inbox relay lookup
   * NOTE: This is a temporary patch until fix is merged upstream
   * TODO: Remove patch after applesauce-actions >5.0.2 is released

**Expected Behavior After Fix:**

1. User sends self-chat message
2. Action looks up inbox relays using recipient pubkey (FIXED)
3. publishEvent receives relay hints: ['wss://relay.com/'] (WORKING)
4. Relay hints normalized: ['wss://relay.com/'] (NEW)
5. Gift wrap published to normalized relays (WORKING)
6. Subscription listening on normalized relays receives message (FIXED)
7. Message appears live in UI without manual sync (~200-500ms latency)

**Testing:**

Self-chat test:
- Send message to self
- Console should show:
  * [Publish] Using provided relay hints
  * [Publish] Persisted encrypted content for gift wrap
  * Match: true (relay alignment)
- Message should appear within 500ms
- Reload page - message persists
- Send another message - both visible, no duplicates

**Note on node_modules patch:**

The applesauce-actions patch will be lost on npm install. Options:
1. Use patch-package to persist the patch
2. Wait for upstream fix and update to applesauce-actions >5.0.2
3. Create local fork until upstream merge

For now, if you run npm install, you'll need to reapply the patch.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 13:58:15 +01:00
Alejandro Gómez
119f737d3d fix: Complete NIP-17 gift wrap persistence for self-chat
This commit implements a production-ready solution for NIP-17 gift wrap
sending and persistence, fixing the issue where self-chat messages
would appear optimistically but disappear on page reload.

**Root Causes Identified:**

1. **publishEvent ignored relay hints from actions**
   - ActionRunner calls publishMethod(event, relays) with two parameters
   - Our publishEvent only accepted one parameter (event)
   - Gift wrap actions passed inbox relays as second parameter → ignored
   - Gift wraps published to wrong relays or failed with no relay list

2. **Encrypted content not persisted during send**
   - Gift wraps created with EncryptedContentSymbol (in-memory only)
   - When received back from relay, new instance had no symbol
   - isGiftWrapUnlocked() returned false → marked as "pending"
   - Messages didn't appear in UI until manually decrypted

3. **Optimistic updates created duplicate risk**
   - Synthetic rumor created with timestamp T1, ID calculated
   - Real rumor created with timestamp T2, different ID
   - Potential for duplicates in UI on relay echo

**Changes:**

- src/services/hub.ts:
  * Modified publishEvent signature to accept optional relayHints parameter
  * Use relay hints when provided, fallback to outbox relay lookup
  * Added encrypted content persistence to Dexie for kind 1059 events
  * Persists decrypted content using EncryptedContentSymbol during publish
  * Fixed TypeScript null handling for getOutboxRelays return type
  * Added detailed console logging for relay routing debugging

- src/lib/chat/adapters/nip-17-adapter.ts:
  * Removed optimistic update code (lines 651-725)
  * Removed synthetic rumor creation and ID calculation
  * Removed immediate decryptedRumors$ update on send
  * Simplified sendMessage flow to rely on natural relay echo (~200-500ms)
  * Kept relay alignment debug logging for self-chat diagnostics

- src/services/gift-wrap.ts:
  * Added refreshPersistedIds() method for debugging
  * Allows manual reload of persisted IDs from Dexie if needed

**Expected Behavior After Fix:**

1. User sends self-chat message
2. SendWrappedMessage queries own inbox relays (kind 10050)
3. publishEvent receives relay hints, uses them for publishing
4. Gift wrap encrypted content persisted to Dexie during publish
5. Gift wrap sent to inbox relays (same relays we're subscribed to)
6. Gift wrap received back from relay (~200ms)
7. persistedIds check recognizes it as already unlocked
8. Message appears in UI and persists across reloads

**Testing Required:**

Manual testing checklist (see plan in /claudedocs/):
- Self-chat: send message, verify appears and persists on reload
- 1-on-1 chat: verify messages persist across reloads
- Group chat: verify multi-recipient messages work
- Reply functionality: verify reply threads persist
- Error cases: verify clear error messages for missing inbox relays

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 13:38:34 +01:00
Alejandro Gómez
195f9046ef debug: Add relay alignment diagnostics for NIP-17 self-chat
Added detailed logging to diagnose why self-chat messages disappear on
reload. Logs show:
- Own inbox relays (where SendWrappedMessage will send to)
- Subscribed relays (where gift wrap service is receiving from)
- Whether they match

This will help identify if there's a relay mismatch causing gift wraps
to be sent but not received.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 13:22:59 +01:00
Alejandro Gómez
c890d67d6f fix: Actively fetch own inbox relays for self-chat in NIP-17
Previously, self-chat relied on giftWrapService.inboxRelays$.value being
populated, but if empty, self-chat would show 0 relays and fail to send
messages. This fix actively fetches own inbox relays using
fetchInboxRelays(activePubkey) for self-chat, ensuring inbox relays are
always populated and self-chat works correctly.

Changes:
- Detect self-chat and actively fetch own inbox relays
- For non-self-chat, use cached value first (optimization)
- Store fetched relays in userInboxRelays variable for metadata
- Add logging for self-chat relay fetch status

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 13:19:16 +01:00
Alejandro Gómez
0293d2dde6 feat: Integrate NIP-17 inbox relays (kind 10050) into relay list cache
NIP-17 inbox relays are now properly cached and displayed just like regular relay lists:

**Relay List Cache Integration**:
- Extended CachedRelayList schema with `inbox?: string[]` field for kind 10050
- relay-list-cache now subscribes to both kind 10002 and kind 10050 events
- Parses and caches inbox relays from "relay" tags in kind 10050 events
- Merges inbox relays with existing cached entries (preserves read/write)

**NIP-17 Adapter Improvements**:
- Checks cache first before fetching inbox relays from network
- Fetched kind 10050 events auto-added to EventStore → triggers cache
- Logs show "Using cached" vs "Fetched and cached" for visibility
- Inbox relays now persist across sessions via Dexie

**Benefits**:
- Inbox relays display immediately from cache (no network delay)
- Reduced network requests - fetch once, use everywhere
- RelaysDropdown shows per-participant inbox relays automatically
- Inbox relays sync whenever chatting with participants

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 13:11:15 +01:00
Alejandro Gómez
a7e1746cf9 fix: Rebuild conversations on startup from stored gift wraps
InboxViewer conversations weren't loading automatically on page reload because
updateConversations() was only called:
1. When gift wraps change from pending to unlocked (not already decrypted)
2. After sync fetches from relays (requires manual refresh)

Now updateConversations() is called immediately after loading stored gift wraps
from Dexie, so conversations appear instantly without needing to click refresh.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 13:06:08 +01:00
Alejandro Gómez
34847bc7ab feat: Aggressive relay resolution and optimistic message display for NIP-17
Two major improvements to NIP-17 sending:

1. **Aggressive relay resolution** - Try much harder to find inbox relays:
   - Query ALL write + read relays (not just 3)
   - Query ALL aggregator relays for maximum coverage
   - Increased timeout from 5s to 10s
   - Better logging with / status indicators
   - This should dramatically improve inbox relay discovery success rate

2. **Optimistic message display** - Messages appear instantly:
   - Build rumor locally before sending
   - Calculate rumor ID using same algorithm as Nostr events
   - Add rumor + synthetic gift wrap to decryptedRumors$ immediately
   - UI updates instantly without waiting for relay round-trip
   - Real gift wrap replaces synthetic when received (deduplication)
   - Fixes self-chat messages not appearing

3. **Self-chat fix**:
   - Explicitly pass [activePubkey] for self-chat instead of []
   - Ensures SendWrappedMessage action sends to self

This provides immediate feedback to users and much better inbox relay resolution.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 13:04:14 +01:00
Alejandro Gómez
d27015c986 feat: Implement NIP-17 encrypted message sending
Implements the full sendMessage() method for NIP-17 DMs using high-level
applesauce actions (SendWrappedMessage, ReplyToWrappedMessage).

Key features:
- Validates active account and signer before sending
- Blocks sends if any participant lacks inbox relays (safety first)
- Supports reply functionality with parent rumor lookup
- Uses ActionRunner to execute gift wrap actions
- Publishes to own inbox relays for cross-device sync
- Comprehensive error handling with clear user-facing messages

Implementation details:
- ~100 lines replacing stub method in nip-17-adapter.ts
- Uses gift wrap service's natural flow for optimistic updates
- No manual EventStore.add() - relies on receive → decrypt → display pipeline
- Typical UI update latency: 100-500ms

Error cases handled:
- No active account or signer
- Missing inbox relays for participants
- Unreachable participants (no kind 10050 events)
- Reply parent not found in decrypted rumors cache
- Action execution failures

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 12:54:55 +01:00
Alejandro Gómez
b8b6e2d0be feat: Improve NIP-17 UI/UX and relay resolution
- Fix reply preview to support NIP-17 synthetic events via adapter.loadReplyMessage()
- Update DM title styling to use regular text color with lock icon in header
- Add copy button support for NIP-17 chat identifiers (nprofile/npub)
- Ensure header structure matches NIP-29 (consistent control placement)
- Fix comma spacing in participant names (items-baseline, non-breaking spaces)
- Add conditional tooltip with participant details for NIP-17 chats
- Improve inbox relay resolution (longer timeouts, more relays, better logging)
- Move NIP badge to right-side controls for consistency
- Add lock icon before member count for encrypted conversations

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 12:53:24 +01:00
Claude
3db40fb140 fix: Show accurate deduplicated relay count in dropdown 2026-01-16 11:02:58 +00:00
Claude
e057760b55 fix: Fix e-tag reply resolution for NIP-17 rumor IDs
The e-tags in NIP-17 messages reference the innermost event (rumor) IDs,
not the gift wrap IDs. Updated ReplyPreview to properly handle this:

- Add local state fallback for when eventStore doesn't track synthetic events
- Use adapter's loadReplyMessage() return value directly
- syntheticEventCache now reliably provides rumor events for reply previews

This ensures reply previews work correctly even if eventStore doesn't
properly index events with empty signatures.
2026-01-16 11:00:11 +00:00
Claude
8849d9561e refactor: Clean up NIP-17 adapter for production readiness
Major improvements:
- Fix message ordering to be chronological (oldest first)
- Add syntheticEventCache for reliable reply resolution
- Simplify code structure with pure functions
- Sort messages properly for chat display

Key changes:
- createSyntheticEvent() caches and adds events to eventStore
- lookupEvent() checks cache first, then eventStore
- loadMessages() now sorts by timestamp ascending
- fetchInboxRelays() simplified with proper error handling
- Add unreachableParticipants metadata for send validation

The synthetic event cache ensures reply previews work even if
eventStore doesn't persist events with empty signatures.
2026-01-16 10:56:21 +00:00
Claude
29ae487e2a fix: Improve NIP-17 chat UX and fix e-tag reply resolution
- Hide "load older messages" button for NIP-17 (loads all at once)
- Show loading indicator while waiting for message decryption
- Remove upload button for NIP-17 (encrypted uploads not supported)
- Fix inbox click to pass proper ProtocolIdentifier with hex pubkeys
- Fetch inbox relays for all participants (not just current user)
- Use participant's outbox relays + aggregators for inbox relay lookup
- Fix NIP-10 e-tag reply resolution with proper marker handling
  (prioritizes "reply" marker, falls back to last unmarked e-tag)
2026-01-16 10:47:13 +00:00
Claude
9332dcc35a fix: Improve NIP-17 chat UI and fix decrypt handling
- Fix garbled messages by creating synthetic events from rumors
- Add rumor events to EventStore so ReplyPreview can find them
- Fix decrypt toast showing success even on failure
- Show profile names in chat title using DmTitle component
- Support $me alias for saved messages (chat $me)
- Make inbox conversations more compact, remove icon
- Show "Saved Messages" for self-conversation in inbox
- Wire up inbox conversation click to open chat window
- Show per-participant inbox relays in RelaysDropdown
- Add participantInboxRelays metadata type for NIP-17
2026-01-16 10:19:29 +00:00
Claude
6dca82d658 feat: Add read-only NIP-17 chat adapter for private DMs
- Create NIP-17 chat adapter supporting npub, nprofile, hex pubkey, NIP-05
- Support self-messages ("Saved Messages"), groups, and 1-on-1 conversations
- Add gift wrap event persistence to Dexie (version 17)
- Load stored gift wraps into EventStore on startup
- Export Rumor type from gift-wrap service for adapter use
- Wire up NIP-17 adapter to chat parser and ChatViewer
- Add kind 14 to CHAT_KINDS for consistent message filtering
- Update tests to reflect NIP-17 support for npub identifiers

Chat command formats now supported:
- chat npub1.../nprofile1.../hex pubkey (single recipient)
- chat user@example.com (NIP-05 resolution)
- chat npub1...,npub2... (group conversation)
2026-01-16 10:00:00 +00:00
Claude
e04d691f1f fix: Properly load gift wraps and conversations on page reload
The issue was that on page reload:
1. EventStore starts empty
2. loadInboxRelays() only subscribed to EventStore, not fetching from relays
3. startSync() with no inbox relays passed empty array to subscribeToGiftWraps()
4. subscribeToGiftWraps() only fetched from relays if relays.length > 0

Fixes:
- Add fetchInboxRelayList() to actively fetch kind 10050 from user's
  outbox relays (or aggregator relays as fallback)
- Use AGGREGATOR_RELAYS as fallback when no inbox relays configured
- When inbox relays are discovered, restart sync with the new relays
- Always request gift wraps from relays (inbox or aggregator fallback)
- Add console logging for debugging relay operations
2026-01-16 09:42:27 +00:00
Claude
35e1f9fe1a fix: Improve inbox gift wrap handling and UI
Fixes several issues with the inbox feature:

1. Load persisted decrypted content on init:
   - Load stored encrypted content IDs from Dexie on service init
   - Subscribe to eventStore.update$ to detect cache restoration
   - Automatically update conversations when restored content is available

2. Mark already-decrypted gift wraps correctly:
   - Check both in-memory unlock state AND persisted IDs
   - Prevents showing decrypted messages as "pending" after reload

3. Hide manual decrypt UI when auto-decrypt is enabled:
   - Only show "Decrypt All" button when auto-decrypt is off
   - Show "Auto-decrypting..." status when auto-decrypt is on

4. Show pending count in user menu:
   - Add pendingCount$ observable to gift wrap service
   - Display badge on "Private Messages" menu item when there are
     undecrypted messages and auto-decrypt is disabled

5. Expose full rumor for future kind support:
   - Add decryptedRumors$ observable with all decrypted rumors
   - Full rumor event (id, pubkey, kind, tags, content) is preserved
   - Enables future support for any kind sent via gift wrap
2026-01-16 09:34:47 +00:00
Claude
325ffa5aa8 feat: Add inbox for NIP-17 private direct messages
Implements private message inbox with gift wrap (NIP-59) support:

- Add GiftWrapService for managing encrypted messages (NIP-17/NIP-59)
  - Track decrypt states (pending/decrypting/success/error)
  - Load inbox relays from kind 10050 (DM relay list)
  - Auto-decrypt option with settings persistence
  - Group messages into conversations

- Add InboxViewer component with:
  - Enable/disable private messages toggle
  - Auto-decrypt toggle
  - Inbox relay display
  - Decrypt status badges (pending/success/error)
  - Conversation list with last message preview
  - Click to decrypt individual messages

- Add encrypted content persistence using Dexie (version 16)
- Add "inbox" command to man pages
- Add "Private Messages" menu item to user menu
2026-01-16 09:19:56 +00:00
Alejandro
d172d67584 Add download button to Zapstore app renderers (#108)
* Add download button to Zapstore app renderers

Add a download button for the latest release to both the feed and detail
renderers for kind 32267 (Zapstore App Metadata). The feed renderer shows
a compact version button, while the detail renderer shows a prominent
download button in the header. Both fetch the latest release and link to
its file metadata event (kind 1063) for download.

* Add proper relay hints for fetching Zapstore releases

Use useLiveTimeline instead of eventStore.timeline() to actually fetch
release events from relays. Relay selection includes:
- Seen relays (where the app event was received from)
- Publisher's outbox relays (NIP-65)
- Aggregator relays as fallback

This ensures releases are properly fetched rather than just read from
the local event store cache.

* Add relay hints when opening file events for download

Pass relay hints from the release event's seen relays when opening
file metadata events (kind 1063) for download. This ensures the
event loader knows where to fetch the file event from.

Also adds relay hints to the ReleaseItem component for both opening
the release detail and the download file.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 16:30:19 +01:00
Alejandro
7a293bb41b feat: COUNT (#105)
* Add COUNT command for NIP-45 event counting

Implements the COUNT verb from NIP-45, allowing users to count events
on relays without fetching them. Features:

- New `count` command requiring at least one relay
- Filter-only flags (excludes --limit, --close-on-eose, --view)
- Single relay shows centered count result
- Multiple relays show per-relay breakdown
- Handles approximate counts, errors, and unsupported relays
- Supports $me/$contacts aliases and NIP-05 resolution

Examples:
  count relay.damus.io -k 3 -p npub1...
  count nos.lol relay.damus.io -k 1 -a fiatjaf.com

* Fix $me and $contacts alias resolution in CountViewer

- Fetch contact list (kind 3) using useNostrEvent hook
- Extract contacts from p tags to resolve $contacts alias
- Add "Account Required" message when aliases used without active account
- Match ReqViewer pattern for consistent alias resolution

* Refactor: extract FilterSummaryBadges for compact headers

- Create shared FilterSummaryBadges component (nostr/FilterSummaryBadges.tsx)
- Simplify CountViewer header to single compact line
- Use FilterSummaryBadges in both ReqViewer and CountViewer
- Remove verbose collapsible filter section from CountViewer

* Remove 'events' suffix from count result

* Update count synopsis to show relays can appear anywhere

* Refactor CountViewer to use applesauce-relay pool

Replace manual WebSocket connections with the relay pool's
count() method for NIP-45 COUNT requests. This provides:
- Proper connection reuse via the existing relay pool
- Automatic reconnection handling
- Better integration with the rest of the app

Remove the approximate property since applesauce-relay's
CountResponse type doesn't expose it yet.

* Simplify CountViewer with one-shot requests and compact UI

- Use per-relay count requests with firstValueFrom and timeout
  instead of pool.count() observable that may not complete
- Replace Collapsible-based header with icon-only DropdownMenus
  matching ReqViewer's compact style
- Add raw JSON filter view with syntax highlighting and copy button
- Show relay count and filter in dropdowns instead of expanded sections
- Requests complete after timeout (10s) instead of spinning indefinitely

* Add NIP-45 support detection via NIP-11 relay info

- Check relay's supported_nips via NIP-11 before sending COUNT request
- Return early with "unsupported" status if relay explicitly doesn't support NIP-45
- Differentiate UI between unsupported (yellow Ban icon) and error (red AlertCircle)
- Provide clearer error messages based on whether NIP-11 info was available
- Uses cached relay info when available to avoid redundant requests

* Improve CountViewer header with human-readable filter summary

- Show kinds as badges, authors ("by"), mentions ("@"), hashtags on left
- Move relay status into relay dropdown with per-relay results
- Dropdown shows count per relay, status icons, and error tooltips
- Header now shows "2/3" style relay count trigger with loading state

* Reorder CountViewer header controls and remove redundant mention prefix

- Change control order to: refresh, relays, filter (was: filter, relays, refresh)
- Remove redundant "@" prefix from mentions since UserName with isMention already shows @

* Increase COUNT timeout to 30s and improve window title

- Extend per-relay timeout from 10s to 30s for more reliable results
- Update count window title to show human-readable kind names instead of
  command-line format (e.g., "count: Short Note by abc123..." instead of
  "count -k 1 -a npub...")

* Add spell support for COUNT commands

- Extend spell system to support both REQ and COUNT commands
- Add detectCommandType() to identify command type from string
- Update encodeSpell to use ["cmd", "COUNT"] tag for count commands
- Update decodeSpell to handle COUNT spells
- Update reconstructCommand to accept cmdType parameter
- Add "Save as spell" option to COUNT windows in WindowToolbar
- Update SpellDialog to handle both REQ and COUNT commands

* Add dynamic window title for COUNT with human-readable filter summary

- Add profile fetching for COUNT authors and tagged users
- Add countTitle useMemo with human-readable kind names, authors, mentions, hashtags, and search
- Use same formatting helpers as REQ titles (getKindName, formatProfileNames, etc.)
- Add countTitle to title priority chain after reqTitle
- Title now shows "Short Note • @alice • #bitcoin" instead of "COUNT"

* Update count command documentation for production

- Add note about automatic NIP-11/NIP-45 support detection
- Mention spell saving capability in description

* Add automatic relay selection with NIP-45 filtering for COUNT

- Make relays optional in count-parser (no longer throws if none specified)
- Add useOutboxRelays for automatic relay selection based on filter criteria
- Filter selected relays by NIP-45 support via NIP-11 before querying
- Show "Selecting relays..." and "Filtering by NIP-45..." loading states
- Fall back to aggregator relays if no NIP-45 relays found
- Update man page: relays now optional, new examples showing auto-selection

* Revert automatic relay selection for COUNT command

Simplify COUNT by requiring explicit relay specification:
- Restore relay requirement validation in count-parser.ts
- Remove useOutboxRelays and NIP-45 auto-filtering from CountViewer
- Update man page documentation to reflect required relays
- Keep NIP-45 support detection for better error messages

This keeps the feature simpler for now; automatic relay selection
can be added later when the UX is better understood.

* Reduce padding and sizing in COUNT single result view

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 16:13:50 +01:00
Alejandro
dade9a79a6 Fix chat zap preview display logic (#106)
* fix: hide zap reply previews for missing or non-chat events

Only show reply previews for zaps when:
1. The replied-to event exists in the event store
2. The replied-to event is a chat kind (9, 9321, or 1311)

This prevents showing loading states or reply previews for
zaps replying to events we don't have or non-chat events.

* refactor: extract CHAT_KINDS constant and include zap receipts

- Create CHAT_KINDS constant in types/chat.ts with all chat event kinds
- Include kind 9735 (zap receipts) as a chat kind
- Update ChatViewer to use the new constant for validation
- Improves maintainability and makes it easier to update chat kinds

Chat kinds now include:
- 9: NIP-29 group chat messages
- 9321: NIP-61 nutzaps
- 1311: NIP-53 live chat messages
- 9735: NIP-57 zap receipts

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 13:08:33 +01:00
Alejandro
571e7a0d14 Implement NIP-43 relay access metadata (#104)
* feat: add NIP-43 relay access metadata support

Add feed and detail rendering for kind 13534 (Relay Members) events,
and enable all NIP-43 kind constants for relay access management.

- Add RelayMembersRenderer with feed and detail views
- Enable kind constants: 13534, 28934, 28935, 28936
- Use Shield icon to represent relay access control
- Extract members from NIP-43's "member" tags (not standard "p" tags)

* feat: add renderers for NIP-43 Add/Remove User events

- Change kind 13534 icon from Shield to Users for consistency
- Add feed and detail renderers for kind 8000 (Add User)
- Add feed and detail renderers for kind 8001 (Remove User)
- Both show the affected pubkey using PubkeyListFull component

* fix: show username in Add/Remove User feed renderers

Display the actual username (via UserName component) in kind 8000/8001
feed views instead of just generic text.

* refactor: simplify NIP-43 renderers to follow codebase patterns

- Use PubkeyListPreview in RelayMembersRenderer feed view (shows actual
  users instead of just count, matching FollowSetRenderer pattern)
- Remove redundant icon props from detail renderers (PubkeyListFull
  has sensible defaults)
- Simplify variable naming and reduce comments
- No functional changes, just cleaner code

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 12:26:37 +01:00
Alejandro
1ce784561a Add copy chat ID button to header (#103)
* feat: add copy chat ID button to chat header

Add a button next to the chat title that copies the chat identifier
to clipboard. The identifier can be used with the `chat` command to
reopen the same conversation.

- For NIP-29 groups: copies relay'group-id format
- For NIP-53 live activities: copies naddr encoding

The button shows a check icon for feedback when copied.

* refactor: simplify copy chat ID button styling

- Use CopyCheck icon instead of Check for consistency with CodeCopyButton
- Remove tooltip to reduce UI noise
- Keep hover state styling (muted to foreground)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 11:43:01 +01:00
Alejandro
72eca99c2e Add custom parsing for relay links (#102)
* feat: Add relay link parsing and rendering to rich text

Implements custom parsing for relay URLs (wss:// and ws://) in text content,
automatically converting them to clickable links that open the relay viewer.

Changes:
- Add relay-transformer.ts with pattern matching for relay URLs
- Create Relay.tsx component for rendering relay links inline
- Register relay transformer in RichText component pipeline
- Add comprehensive test suite (26 tests) covering all URL formats

Supported formats:
- wss:// and ws:// protocols
- Domains, subdomains, and IP addresses
- Custom ports, paths, query parameters
- Multiple relay URLs in single message

All tests passing (864/864). No breaking changes.

* refactor: Update relay link styling to match other inline links

- Use muted/underline styling consistent with NIP references
- Remove icons and show only relay name (formatted)
- Display full URL in tooltip
- Match text size with surrounding content
- Simplify component by not using RelayLink wrapper

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-15 11:33:27 +01:00
Alejandro
64c181dd87 Improve chat UX with scroll and padding fixes (#99)
* fix: improve chat UX with better scroll and symmetric padding

- Add alignToBottom prop to Virtuoso for better last message visibility
- Add small footer to prevent last message from being hidden under scroll
- Make composer padding symmetric (py-1 instead of py-1 pb-0)

* fix: restore pb-0 on composer

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 21:25:03 +01:00
Alejandro
6541e06b62 feat: disable pointer events on group message preview (#98)
Add pointer-events-none to the message preview div in GroupListViewer
to ensure clicks always pass through to the parent group item's onClick
handler. This prevents issues where clicks on the preview text (UserName
or RichText components) might not trigger group selection.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 21:04:09 +01:00
Alejandro
f464c68bde feat: theme selector (#95)
* feat: Add reusable theme system with Plan 9 proof of concept

Implement a comprehensive theme system that:
- Defines typed Theme interface with colors, syntax highlighting, scrollbar, and gradient variables
- Creates ThemeProvider with React context for runtime theme switching
- Persists theme selection to localStorage
- Includes 3 built-in themes: dark (default), light, and plan9

Theme structure supports:
- Core UI colors (background, foreground, primary, secondary, accent, etc.)
- Status colors (success, warning, info) replacing hardcoded Tailwind colors
- Syntax highlighting variables for code blocks
- Diff highlighting colors (inserted, deleted, meta)
- Scrollbar styling variables
- Gradient colors for branding

Technical changes:
- Update CSS to use new theme variables throughout
- Update prism-theme.css to use syntax variables instead of hardcoded values
- Remove chart colors (unused)
- Add success/warning/info to tailwind.config.js
- Wire up ThemeProvider in main.tsx

For Nostr publishing (future):
- d tag: "grimoire-theme"
- name tag: theme display name

* feat: Add theme selector to user menu, remove configurable border radius

- Remove border radius from theme configuration (borders are always square)
- Add theme selector dropdown to user menu (available to all users)
- Theme selector shows active theme indicator
- Theme selection persists via localStorage

* fix: Improve theme contrast and persistence

- Fix theme persistence: properly check localStorage before using default
- Plan9: make blue subtler (reduce saturation), darken gradient colors
  for better contrast on pale yellow background
- Light theme: improve contrast with darker muted foreground and borders
- Change theme selector from flat list to dropdown submenu

* fix: Replace Plan9 yellow accent with purple, add zap/live theme colors

- Replace Plan9's bright yellow accent with purple (good contrast on pale yellow)
- Add zap and live colors to theme system (used by ZapReceiptRenderer, StatusBadge)
- Make light theme gradient orange darker for better contrast
- Update ZapReceiptRenderer to use theme zap color instead of hardcoded yellow-500
- Update StatusBadge to use theme live color instead of hardcoded red-600
- Add CSS variables and Tailwind utilities for zap/live colors

* fix: Make gradient orange darker, theme status colors

- Make gradient orange darker in light and plan9 themes for better contrast
- Make req viewer status colors themeable:
  - loading/connecting → text-warning
  - live/receiving → text-success
  - error/failed → text-destructive
  - eose → text-info
- Update relay status icons to use theme colors
- Update tests to expect theme color classes

* fix: Use themeable zap color for active user names

- Replace hardcoded text-orange-400 with text-zap in UserName component
- Replace hardcoded text-orange-400 with text-zap in SpellRenderer ($me placeholder)
- Now uses dark amber/gold with proper contrast on light/plan9 themes

* feat: Add highlight theme color for active user display

Add dedicated 'highlight' color to theme system for displaying the
logged-in user's name, replacing the use of 'zap' color which felt
semantically incorrect. The highlight color is optimized for contrast
on each theme's background.

- Add highlight to ThemeColors interface and apply.ts
- Add --highlight CSS variable to index.css (light and dark)
- Add highlight to tailwind.config.js
- Configure appropriate highlight values for dark, light, and plan9 themes
- Update UserName.tsx to use text-highlight for active account
- Update SpellRenderer.tsx MePlaceholder to use text-highlight

* fix: Restore original orange-400 highlight color for dark theme

Update dark theme highlight to match original text-orange-400 color
(27 96% 61%) for backward compatibility with existing appearance.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 19:24:37 +01:00
Alejandro
5fa2a1c9b8 feat: Add context menu for chat message interactions (#94)
Add right-click/long-press context menu to chat messages with event interactions:
- Reply to message
- Copy message text
- Open event detail (opens in new window)
- Copy event ID (nevent/naddr with relay hints)
- View raw JSON

Implements the same EventMenu pattern used in feed renderers, providing
consistent UX across the app. No visible UI elements added - menu appears
only on right-click or long-press (mobile).

Components added:
- src/components/ui/context-menu.tsx - Radix UI context menu primitives
- src/components/chat/ChatMessageContextMenu.tsx - Chat-specific context menu
- Integrated into MessageItem in ChatViewer.tsx

Dependencies:
- Added @radix-ui/react-context-menu

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 18:09:19 +01:00
Alejandro
59fdfc5611 Add route for command results in popup window (#93)
* feat: Add pop-out command route for standalone window rendering

Add a new /run route that allows windows to be opened in separate
browser windows/tabs without affecting the main workspace layout.

Changes:
- Add RunCommandPage component for /run?cmd=<command> route
- Add Pop Out button to WindowToolbar (ExternalLink icon)
- Parse command from URL query parameter and render result
- Construct minimal WindowInstance for rendering
- Display command string in header with clean minimal UI

This enables users to pop out any window into a separate browser
context while maintaining the main workspace layout, useful for
multi-monitor setups or keeping reference windows visible.

* refactor: Remove header from pop-out command page

Simplify RunCommandPage to only show the window renderer without
any additional UI chrome. This provides a cleaner, more focused
experience for popped-out windows.

* refactor: Move pop-out action to window menu dropdown

Move the pop-out button from a standalone icon to the three-dot
menu dropdown to reduce toolbar clutter. The menu now always
appears since pop-out is always available.

* feat: Add AppShell header to pop-out command page

Wrap RunCommandPage with AppShell (hideBottomBar) to show the header
with user menu and command launcher, matching the behavior of NIP-19
preview pages.

When a command is launched from the /run page, it navigates to the
main dashboard (/) where the window system exists.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 17:30:56 +01:00
Alejandro
16764e1aca Display user's blossom servers in menu (#90)
* feat: Display blossom servers in user menu with caching

Implements caching and display of user's blossom server lists (kind 10063) in the user menu dropdown.

**Key Changes:**

1. **Database Schema (db.ts)**:
   - Added `CachedBlossomServerList` interface
   - Added `blossomServers` table to Dexie v15
   - Indexed by pubkey and updatedAt for efficient querying

2. **Caching Service (blossom-server-cache.ts)**:
   - Dual-layer caching: LRU memory cache (100 entries) + Dexie persistent cache
   - 24-hour TTL to reduce network requests
   - Auto-caches kind:10063 events from EventStore
   - Mirrors relay-list-cache pattern for consistency

3. **Sync Hooks**:
   - `useBlossomServerCacheSync`: Subscribes to EventStore for auto-caching kind:10063 events
   - `useAccountSync`: Extended to fetch and watch active account's blossom servers

4. **State Management**:
   - Added `blossomServers` field to `activeAccount` in GrimoireState
   - Created `setActiveAccountBlossomServers` pure function and callback
   - Reactive updates when user's server list changes

5. **User Menu UI (user-menu.tsx)**:
   - Added blossom servers section with HardDrive icon
   - Shows server count badge (e.g., "3 servers")
   - Clickable links open servers in new tabs
   - Displays only when user has configured servers

**Architecture:**
- Follows existing relay list caching pattern for consistency
- Reactive: UI auto-updates when kind:10063 events arrive
- Incremental sync: Fetches on login, caches for 24h
- Performance: Memory cache for <1ms lookups, Dexie for persistence

**Testing:**
- Build: ✓ No TypeScript errors
- Tests: ✓ All 838 tests passing

* feat: Open blossom server file lists directly from menus

**User Menu & Profile Viewer Improvements:**

1. **Enhanced Click Behavior**:
   - Clicking a blossom server now opens the file list for that server
   - Shows blobs uploaded by the user (user menu) or profile owner (profile viewer)
   - Pre-selects the clicked server in the dropdown

2. **UX Improvements**:
   - Removed server count from user menu label (cleaner UI)
   - Added `cursor-crosshair` to blossom server items (consistent with other clickable items)
   - Removed external link icon (not opening external URL anymore)

3. **Technical Changes**:
   - Updated `ListBlobsView` to accept optional `serverUrl` prop for pre-selection
   - User menu: Opens `blossom list` with `serverUrl` for active user
   - Profile viewer: Opens `blossom list` with both `pubkey` and `serverUrl`

**Flow:**
- User menu → Click server → Opens files for active user on that server
- Profile viewer → Click server → Opens files for viewed user on that server

* fix: Properly fetch blossom servers for any profile view

**Problem:**
Blossom servers were only visible for the logged-in user's profile,
not for other users' profiles being viewed.

**Solution:**
Enhanced ProfileViewer blossom server fetching with multi-layer approach:

1. **Cache-first loading**: Check blossomServerCache for instant display
2. **EventStore check**: Use existing cached event if available
3. **Reactive subscription**: Subscribe to EventStore for real-time updates
4. **Network fetch**: Use addressLoader to fetch latest from relays
5. **Auto-caching**: Update cache when new events arrive

**Benefits:**
- Blossom servers now display for ANY user's profile
- Instant display from cache (< 1ms)
- Reactive updates when data changes
- Proper cache hydration for future visits
- Consistent with relay list fetching pattern

**Technical:**
- Imported and integrated blossomServerCache service
- Added cache check before network fetch
- Separated EventStore subscription from network fetch
- Added cache updates on event arrival

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 14:52:16 +01:00
Alejandro
998944fdf7 feat: Add mobile tap support for chat name tooltip (#82)
- Add controlled state for tooltip open/close
- Enable tap/click to toggle tooltip on mobile devices
- Tooltip now shows conversation metadata on tap (icon, description, protocol type, status, host)
- Maintains existing hover behavior on desktop
- Improves mobile UX by making metadata accessible via tap

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 09:49:15 +01:00
Alejandro
3117aea34f Update profile example domain name (#86)
* Update profile example from verbiricha@habla.news to fiatjaf.com

* Update remaining verbiricha@habla.news examples to fiatjaf.com in man.ts

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 09:47:56 +01:00
Alejandro
7036fb5539 Fix chat composer placeholder and text alignment (#84)
* Fix chat composer placeholder and text alignment

- Adjusted .ProseMirror min-height from 2rem to 1.25rem to match container
- Added flexbox layout to .ProseMirror for proper vertical centering
- Removed float:left and height:0 from placeholder causing misalignment
- Moved padding from editor props to wrapper div
- Updated EditorContent to use flex items-center for alignment

Resolves vertical alignment issues in chat composer input field.

* Fix cursor placement in chat composer placeholder

- Changed from flexbox to line-height for vertical centering
- Removed flex from .ProseMirror to fix cursor positioning
- Set line-height: 1.25rem to match min-height for proper alignment
- Removed flex items-center from EditorContent className

This ensures the cursor appears at the correct position when focusing
the input field, rather than after the placeholder text.

* Fix cursor placement on mobile devices

- Made placeholder absolutely positioned to prevent it from affecting cursor
- Added position: relative to .ProseMirror container
- This ensures cursor appears at the start of input on mobile browsers

The absolute positioning removes the placeholder from the normal layout flow,
preventing mobile browsers from placing the cursor after the pseudo-element.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 22:07:04 +01:00
Alejandro
20aeac2bc2 Use npub instead of NIP-05 in sharing URLs (#83)
- Replace NIP-05 preference with npub in ShareSpellbookDialog
- Replace NIP-05 preference with npub in SpellbookRenderer preview
- Remove unused useProfile imports from both components
- Update comment to reflect npub-only URL format

This ensures more reliable URL sharing since npub is always available
while NIP-05 may not be set or verified. The routing system still
supports both formats for backwards compatibility.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 21:41:59 +01:00
Alejandro
1356afe9ea Fix newline rendering in chat messages (#80)
* Fix newline rendering in chat messages

The Text component was not properly rendering newlines in chat messages.
The previous implementation had buggy logic that only rendered <br /> for
empty lines and used an inconsistent mix of spans and divs for non-empty
lines, which didn't create proper line breaks between consecutive text lines.

Changes:
- Render each line in a span with <br /> between consecutive lines
- Remove unused useMemo import and fix React hooks violation
- Simplify logic for better maintainability

This ensures that multi-line messages in chat (and other text content)
display correctly with proper line breaks.

Fixes rendering of newlines in NIP-29 groups and NIP-53 live chat.

* Preserve newlines when sending chat messages

The MentionEditor's serializeContent function was not handling hardBreak
nodes created by Shift+Enter. This caused newlines within messages to be
lost during serialization, even though the editor displayed them correctly.

Changes:
- Add hardBreak node handling in serializeContent
- Preserve newlines (\n) from Shift+Enter keypresses
- Ensure multi-line messages are sent with proper line breaks

With this fix and the previous Text.tsx fix, newlines are now properly:
1. Captured when typing (Shift+Enter creates hardBreak)
2. Preserved when sending (hardBreak serialized as \n)
3. Rendered when displaying (Text component renders \n as <br />)

* Make Enter insert newline on mobile devices

On mobile devices, pressing Enter now inserts a newline (hardBreak) instead
of submitting the message. This provides better UX since mobile keyboards
don't have easy access to Shift+Enter for multiline input.

Behavior:
- Desktop: Enter submits, Shift+Enter inserts newline (unchanged)
- Mobile: Enter inserts newline, Cmd/Ctrl+Enter submits
- Mobile detection: Uses touch support API (ontouchstart or maxTouchPoints)

Users can still submit messages on mobile using:
1. The Send button (primary method)
2. Ctrl+Enter keyboard shortcut (if available)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 20:50:46 +01:00
Alejandro
4078ea372a Review NIP-51 list types and rendering support (#77)
* Add comprehensive NIP-51 list rendering support

Implement rich renderers for all major NIP-51 list types with both
feed and detail views. Creates reusable list item components for
consistent UI across different list kinds.

New list renderers:
- Kind 10000: Mute List (pubkeys, hashtags, words, threads)
- Kind 10001: Pin List (pinned events/addresses)
- Kind 10003: Bookmark List (events, addresses, URLs)
- Kind 10004: Community List (community references)
- Kind 10005: Channel List (public chat channels)
- Kind 10015: Interest List (hashtags + interest sets)
- Kind 10020: Media Follow List (media creators)
- Kind 10030: User Emoji List (custom emojis)
- Kind 10101: Good Wiki Authors
- Kind 10102: Good Wiki Relays
- Kind 30000: Follow Sets (categorized follows)
- Kind 30003: Bookmark Sets (categorized bookmarks)
- Kind 30004: Article Curation Sets
- Kind 30005: Video Curation Sets
- Kind 30006: Picture Curation Sets
- Kind 30007: Kind Mute Sets
- Kind 30015: Interest Sets
- Kind 39089: Starter Packs
- Kind 39092: Media Starter Packs

Reusable components in src/components/nostr/lists/:
- PubkeyListPreview/Full: Display pubkey lists with counts
- HashtagListPreview/Full: Display hashtag pills
- EventRefList: Display event/address references
- WordList: Display muted words
- UrlList: Display URL bookmarks

* Improve NIP-51 list rendering with clickable titles and consistent styling

- Add ClickableEventTitle to all list feed renderers for detail view navigation
- Change colored icons to text-muted-foreground for consistent muted appearance
- Update HashtagListPreview to use Label component with dotted border styling
- Update EventRefList detail view to embed events using EmbeddedEvent component

* Use Label component for muted words with destructive styling

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 17:23:57 +01:00
Alejandro
9ef1fefd3d feat: BLOSSOM (#75)
* Add Blossom blob storage integration

- Add blossom-client-sdk dependency for blob storage operations
- Create blossom.ts service with upload, list, check, mirror, delete primitives
- Add kind 10063 server list fetching and parsing
- Create blossom-parser.ts for command argument parsing with subcommands
- Add BLOSSOM command to man.ts with subcommands:
  - servers: Show configured Blossom servers
  - check: Check server health
  - upload: Upload files to user's servers
  - list: List blobs for a user
  - mirror: Mirror blobs between servers
  - delete: Delete blobs from servers
- Create BlossomViewer component with views for each subcommand
- Wire up BlossomViewer in WindowRenderer
- Add Blossom servers dropdown to ProfileViewer header
- Upload primitives can be called programmatically for use in other components

* Enhance Blossom viewer with server selection and blob details

- Add server selection checkboxes to upload view for choosing target servers
- Add BlobDetailView with media preview (image/video/audio) and metadata display
- Add 'blob' subcommand to view individual blob details
- Remove unused 'check' subcommand

* Add Blossom upload dialog with chat integration

- Create BlossomUploadDialog component with file picker, server selection, and preview
- Create useBlossomUpload hook for easy integration in any component
- Add insertText method to MentionEditor for programmatic text insertion
- Integrate upload button (paperclip icon) in chat composer
- Supports image, video, and audio uploads with drag-and-drop

* Add rich blob attachments with imeta tags for chat

- Add BlobAttachment TipTap extension with inline preview (thumbnail for images, icons for video/audio)
- Store full blob metadata (sha256, url, mimeType, size, server) in editor nodes
- Convert blob nodes to URLs in content with NIP-92 imeta tags when sending
- Add insertBlob method to MentionEditor for programmatic blob insertion
- Update NIP-29 and NIP-53 adapters to include imeta tags with blob metadata
- Pass blob attachments through entire send flow (editor -> ChatViewer -> adapter)

* Add fallback public Blossom servers for users without server list

- Add well-known public servers as fallbacks (blossom.primal.net, nostr.download, files.v0l.io)
- Use fallbacks when user has no kind 10063 server list configured
- Show "Public Servers" label with Globe icon when using fallbacks
- Inform user that no server list was found
- Select first fallback server by default (vs all user servers)

* Fix: Don't show fallback servers when not logged in

Blossom uploads require signed auth events, so users must be logged in.
The 'Account required' message is already shown in this case.

* Remove files.v0l.io from fallback servers

* Add rich renderer for kind 10063 Blossom server list

- Create BlossomServerListRenderer.tsx with feed and detail views
- Show user's configured Blossom servers with clickable links
- Clicking a server opens the Blossom window with server info
- Register renderers for kind 10063 (BUD-03)
- Fix lint error by renaming useFallbackServers to applyFallbackServers

* Add individual server view and NIP-05 support for blossom commands

- Add 'server' subcommand to view info about a specific Blossom server
- Update BlossomServerListRenderer to open server view on click
- Make blossom parser async to support NIP-05 resolution in 'list' command
- Add kind 10063 (Blossom Server List) to EVENT_KINDS constants with BUD-03 reference
- Update command examples with NIP-05 identifier support

* Add comprehensive tests for blossom-parser

- 34 test cases covering all subcommands (servers, server, upload, list, blob, mirror, delete)
- Tests for NIP-05 resolution, npub/nprofile decoding, $me alias
- Tests for error handling and input validation
- Tests for case insensitivity and command aliases (ls, view, rm)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 17:16:31 +01:00
Alejandro
ed6f2fd856 Improve chat reply UX with auto-focus and rich text preview (#76)
- Focus the message input when clicking reply button for immediate typing
- Use RichText component in reply preview to render mentions and emojis

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 16:26:28 +01:00
Alejandro
fd31e707fc Add window action to move windows between tabs (#74)
- Add "Move to tab" submenu in window toolbar's more actions dropdown
- Shows nested submenu with available workspace tabs (number + label)
- After moving, automatically switches to the target workspace
- Only displays the more actions menu when multiple workspaces exist
- Preserves existing REQ-specific "Save as spell" action

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 13:28:02 +01:00
Alejandro
797510107b Fix slash command autocomplete and add bookmark commands (#73)
* fix: slash autocomplete only at input start + add bookmark commands

- Fix slash command autocomplete to only trigger when / is at the
  beginning of input text (position 1 in TipTap), not in the middle
  of messages
- Add /bookmark command to add NIP-29 group to user's kind 10009 list
- Add /unbookmark command to remove group from user's kind 10009 list

* fix: normalize relay URLs when checking group list bookmarks

Use normalizeRelayURL for comparing relay URLs in bookmark/unbookmark
commands to handle differences in trailing slashes, casing, and protocol
prefixes between stored tags and conversation metadata.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 12:39:22 +01:00
Alejandro
280a395187 Make grouplist viewer mobile friendly with sidebar (#72)
* feat: make GroupListViewer mobile-friendly with Sheet sidebar

- Add Sheet component for mobile drawer behavior
- Add Separator component for UI dividers
- Add Sidebar component with mobile/desktop variants
- Update GroupListViewer to show sheet-based sidebar on mobile (<768px)
  and resizable sidebar on desktop
- Mobile view includes toggle button in header and auto-closes on selection

* refactor: integrate sidebar toggle into ChatViewer header

- Add headerPrefix prop to ChatViewer for custom header content
- Pass sidebar toggle button via headerPrefix on mobile
- Remove duplicate mobile header from GroupListViewer
- Reduces vertical space usage by reusing ChatViewer's existing header

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 11:52:52 +01:00