Commit Graph

9 Commits

Author SHA1 Message Date
Claude
67440c55d6 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.
2026-01-19 15:20:13 +00: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
97f18de358 feat: message reactions (#110)
* Add subtle inline reactions to chat messages

Implements NIP-25 reaction display for chat messages with per-message lazy loading:

- Created MessageReactions component that independently loads kind 7 reactions
  for each message using EventStore timeline queries
- Displays reactions as tiny inline badges in bottom-right corner (doesn't affect
  message height)
- Aggregates reactions by emoji with deduplication by pubkey
- Supports both unicode emoji and NIP-30 custom emoji with images
- Shows reaction count next to each emoji
- Integrated into both regular user messages and zap messages in ChatViewer
- Reactions load reactively - new reactions appear automatically via EventStore
  observables

No "+" button for adding reactions yet - this is display-only for now.
Works with NIP-29 groups and will work with any chat protocol that uses
kind 7 reactions with e-tags.

* Fix reaction loading to use protocol-specific relay hints

Previously MessageReactions was only querying EventStore without actually
fetching reactions from relays. Now it properly:

- Starts a relay subscription per message to fetch kind 7 reactions
- Uses protocol-specific relay hints via getConversationRelays() helper:
  * NIP-29 groups: Single relay from conversation.metadata.relayUrl
  * NIP-53 live chats: Multiple relays from conversation.metadata.liveActivity.relays
- Memoizes relay array in MessageItem to prevent unnecessary re-subscriptions
- Cleans up subscriptions when message unmounts or changes

This ensures reactions are actually fetched and displayed correctly across
different chat protocols.

* Remove unused NostrEvent import

* Move reactions inline after timestamp with subtler styling

Reactions now appear directly after the timestamp in the message header:
- Removed absolute positioning and background color
- Increased spacing between emoji and count (gap-1 instead of gap-0.5)
- Simple inline display with no border or background
- Appears in natural reading flow: "Alice  10:30 AM  ❤️ 3  👍 1"
- Removed relative positioning from message container (no longer needed)

This makes reactions much more subtle and integrated into the message UI.

* Add detailed tooltips to reactions showing who reacted

Each reaction badge now shows a tooltip with:
- Emoji and count on first line
- Comma-separated list of display names who reacted

Implementation:
- Split into ReactionBadge component per reaction
- Loads profiles for all reactor pubkeys using eventStore.profiles()
- Uses getDisplayName() helper for human-readable names
- Tooltip format: "❤️ 3\nAlice, Bob, Carol"

This makes it easy to see exactly who reacted with each emoji.

* Simplify reaction tooltips to show truncated pubkeys

Changed tooltip implementation from loading profiles (which wasn't working
with EventStore API) to showing truncated pubkeys for simplicity and performance:

- Removed profile loading logic (eventStore.profiles() doesn't exist)
- Tooltips now show: "❤️ 3\nabcd1234..., efgh5678..."
- Truncated to first 8 chars for readability
- No external API calls needed, purely computed from reaction data
- Can be enhanced later to load profiles if needed

Build verified: TypeScript compilation passes, all tests pass.
This is production-ready code.

* Add emoji reaction picker to chat messages

Implements complete reaction functionality with searchable emoji picker:

**UI Enhancements:**
- Reactions display horizontally with hidden scrollbar (hide-scrollbar CSS utility)
- Messages with many reactions scroll smoothly without visible scrollbar
- Inline positioning after timestamp for clean, integrated look

**Emoji Picker Dialog:**
- Real-time search using FlexSearch (EmojiSearchService)
- Quick reaction bar with common emojis (❤️ 👍 🔥 😂 🎉 👀 🤔 💯)
- Frequently used section based on localStorage history
- Support for both unicode and NIP-30 custom emoji
- Grid layout with 48-emoji results
- Auto-focus search input for keyboard-first UX

**Protocol Implementation:**
- Added sendReaction() method to ChatProtocolAdapter base class
- NIP-29 groups: kind 7 with e-tag + h-tag (group context)
- NIP-53 live chats: kind 7 with e-tag + a-tag (activity context)
- NIP-C7 DMs: kind 7 with e-tag + p-tag (partner context)
- All reactions include k-tag for reacted event kind
- NIP-30 custom emoji support via emoji tags

**Context Menu Integration:**
- Added "React" action to ChatMessageContextMenu with Smile icon
- Opens emoji picker dialog on click
- Passes conversation and adapter for protocol-specific reactions
- Only shows when conversation and adapter are available

**Frequently Used Tracking:**
- Stores reaction history in localStorage (grimoire:reaction-history)
- Displays top 8 most-used reactions when no search query
- Increments count on each reaction sent

**Tooltips:**
- Show emoji + count + truncated pubkeys
- Format: "❤️ 3\nabcd1234..., efgh5678..."
- Future enhancement: load profiles for display names

Ready for testing! Users can now right-click messages → React → search/pick emoji.

* Load user's custom emoji list in reaction picker

The emoji picker now loads custom emoji from:
- User's emoji list (kind 10030) - personal custom emoji
- Context emojis from conversation messages
- Unicode emojis (default set)

EmojiSearchService prioritizes:
1. User emoji (highest priority)
2. Context emoji (from conversation)
3. Emoji sets (if any)
4. Unicode emoji (default)

When searching or browsing, users will see their custom emoji alongside
standard unicode emoji, making it easy to use personal/community emoji
in reactions.

* Fix reaction container to prevent message width expansion

Wrapped reactions in a scrollable container with proper constraints:
- inline-flex: displays inline with message metadata
- max-w-full: prevents expanding beyond message width
- overflow-x-auto: enables horizontal scrolling for many reactions
- hide-scrollbar: hides scrollbar for clean appearance

Messages with many reactions now stay within their width and scroll
horizontally without a visible scrollbar.

* Highlight reaction counts when active user has reacted

Changes:
- Reaction counts now show in highlight color (text-highlight) when the
  active user has reacted with that emoji
- Added font-semibold to make user's reactions more prominent
- Checks if activeAccount.pubkey is in reaction.pubkeys array
- Provides clear visual feedback showing which reactions you've made

This makes it easy to see at a glance which reactions are yours in a
conversation with many reactions.

* Refine reaction badge styling: increase spacing and remove bold

- Increase gap between reaction badges from gap-1 to gap-2
- Remove font-semibold from highlighted reaction counts
- Keep text-highlight color for active user's reactions
- Results in more subtle, polished appearance

* Refactor emoji picker to use useEmojiSearch hook

- Replace manual emoji service management with useEmojiSearch hook
- Use same emoji loading approach as chat autocomplete
- Subscribe to EventStore observables for reactive updates
- Load user's emoji list (kind 10030) and emoji sets (kind 30030)
- Fix custom emoji search not working
- Remove redundant async loading logic

* Fix emoji picker UI issues

- Remove quick reaction bar (❤️ 👍 🔥 etc.)
- Fix custom emoji in "Recently used" section - now renders images instead of shortcodes
- Increase grid spacing from gap-2 to gap-3 to reduce crowding
- Add helpers to properly lookup and render custom emoji from service

* Improve reaction badge sizing and spacing

- Increase custom emoji size from size-3 (12px) to size-3.5 (14px)
- Increase gap between emoji and count from gap-1 to gap-1.5
- Add object-contain to custom emoji images for proper aspect ratio
- Add leading-none to unicode emoji for consistent vertical alignment
- Results in better visual balance between custom and unicode emoji

* Fix custom emoji shrinking in reaction badges

- Add flex-shrink-0 to custom emoji images to prevent compression
- Add flex-shrink-0 to unicode emoji spans for consistency
- Ensures both custom and unicode emoji maintain their size-3.5 dimensions

* Improve emoji picker UX with fixed layout

- Always show exactly 2 rows (16 emoji) to prevent height jumping
- Merge recently used with search results into unified grid
- When no search: show recently used first, then fill with other emoji
- When searching: show top 16 results
- Remove separate "Recently used" section for cleaner layout
- Add aspect-square to buttons for consistent sizing
- Add object-contain to custom emoji for proper aspect ratio
- Replace scrollable area with fixed-height grid

* Refine emoji picker to show single row with fixed height

- Show only 1 row (8 emoji) instead of 2 rows for more compact UI
- Add min-h-[3.5rem] to prevent height changes
- Ensure custom emoji (w-6 h-6) matches unicode emoji (text-2xl) size
- Add leading-none to unicode emoji for better vertical alignment
- Empty state "No emojis found" maintains same grid height
- Consistent sizing between custom and unicode emoji across the picker

* Fix emoji sizing in picker to match unicode and custom emoji

- Reduce unicode emoji from text-2xl (24px) to text-xl (20px)
- Reduce custom emoji from w-6 h-6 (24px) to size-5 (20px)
- Both now render at same 20px size for visual consistency
- Fixes custom emoji appearing too large compared to unicode emoji

* ui: dialog tweaks

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-16 18:47:16 +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
20eb19bdbb fix: improve chat architecture robustness and error handling (#66)
* fix: improve chat architecture robustness and error handling

- Fix scroll-to-message index mismatch (was searching in wrong array)
- Fix subscription memory leaks by tracking and cleaning up subscriptions
- Add error handling for conversation resolution with retry UI
- Add error handling for send message with toast notifications
- Fix array mutation bugs in NIP-53 relay handling
- Add type guards for LiveActivityMetadata
- Fix RelaysDropdown O(n²) performance issue
- Add loading state for send button

* refactor: add stronger types and optimize message sorting

- Add discriminated union types for ProtocolIdentifier (GroupIdentifier,
  LiveActivityIdentifier, DMIdentifier, NIP05Identifier, ChannelIdentifier)
- Optimize message sorting using reverse() instead of full sort (O(n) vs O(n log n))
- Add type narrowing in adapter resolveConversation methods
- Remove unused Observable import from ChatViewer

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 21:12:22 +01:00
Alejandro
5bc89386ea Add NIP-53 live event chat adapter (#56)
* feat: add NIP-53 live activity chat adapter

Add support for joining live stream chat via naddr (kind 30311):
- Create Nip53Adapter with parseIdentifier, resolveConversation, loadMessages, sendMessage
- Show live activity status badge (LIVE/UPCOMING/ENDED) in chat header
- Display host name and stream metadata from the live activity event
- Support kind 1311 live chat messages with a-tag references
- Use relays from activity's relays tag or naddr relay hints
- Add tests for adapter identifier parsing and chat-parser integration

* ui: derive live chat participants from messages, icon-only status badge

- Derive participants list from unique pubkeys in chat messages for NIP-53
- Move status badge after title with hideLabel for compact icon-only display

* feat: show zaps in NIP-53 live chat with gradient border

- Fetch kind 9735 zaps with #a tag matching the live activity
- Combine zaps and chat messages in the timeline, sorted by timestamp
- Display zap messages with gradient border (yellow → orange → purple → cyan)
- Show zapper, amount, recipient, and optional comment
- Add "zap" message type with zapAmount and zapRecipient metadata

* fix: use RichText for zap comments and remove arrow in chat

- Use RichText with zap request event for zap comments (renders emoji tags)
- Remove the arrow (→) between zapper and recipient in zap messages

* refactor: simplify zap message rendering in chat

- Put timestamp right next to recipient (removed ml-auto)
- Use RichText with content prop and event for emoji resolution
- Inline simple expressions, remove unnecessary variables
- Follow codebase patterns from ZapCompactPreview

* docs: update chat command to include NIP-53 live activity

- Update synopsis to use generic <identifier>
- Add NIP-53 live activity chat to description
- Update option description to cover both protocols
- Add naddr example for live activity chat
- Add 'live' to seeAlso references

* fix: use host outbox relays for NIP-53 live chat events

Combine activity relays, naddr hints, and host's outbox relays when
subscribing to chat messages and zaps. This ensures events are fetched
from all relevant sources where they may be published.

* ui: show host first in members list, all relays in dropdown

- derivedParticipants now puts host first with 'host' role
- Other participants from messages follow as 'member'
- RelaysDropdown shows all NIP-53 liveActivity.relays

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 13:05:09 +01:00
Alejandro Gómez
a3eec2e3c9 feat: disable inline media and event embeds in chat replies
Pass options to RichText component in ReplyPreview to disable:
- All media types (images, videos, audio) via showMedia: false
- Event embeds (note/nevent/naddr mentions) via showEventEmbeds: false

Chat reply previews now only show text content for cleaner, more
focused message context display.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 10:59:39 +01:00
Alejandro Gómez
fc2e680afd feat: add reply functionality and require active account for composer
Reply Functionality:
- Added Reply button to each message (visible on hover)
- Button appears in message header next to timestamp
- Uses Reply icon from lucide-react
- Clicking reply sets the replyTo state with message ID
- Reply preview shows in composer when replying

Active Account Requirements:
- Check for active account using accountManager.active$
- Only show composer if user has active account
- Only enable reply buttons if user has active account
- Show "Sign in to send messages" message when no active account
- Prevent sending messages without active account

UI Improvements:
- Reply button uses opacity transition on hover (0 → 100)
- Positioned with ml-auto to align right in header
- Reply button only visible on group hover for clean UI
- Consistent styling with muted-foreground color scheme

Benefits:
- Users can reply to specific messages inline
- Clear indication when authentication is required
- Prevents errors from attempting to send without account
- Professional chat UX with hover interactions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 23:12:07 +01:00
Alejandro Gómez
6d01ee33ef feat: implement unified chat system with NIP-C7 and NIP-29 support
Core Architecture:
- Protocol adapter pattern for chat implementations
- Base adapter interface with protocol-specific implementations
- Auto-detection of protocol from identifier format
- Reactive message loading via EventStore observables

Protocol Implementations:
- NIP-C7 adapter: Simple chat (kind 9) with npub/nprofile support
- NIP-29 adapter: Relay-based groups with member roles and moderation
- Protocol-aware reply message loading with relay hints
- Proper NIP-29 members/admins fetching using #d tags

UI Components:
- ChatViewer: Main chat interface with virtualized message timeline
- ChatMessage: Message rendering with reply preview
- ReplyPreview: Auto-loading replied-to messages from relays
- MembersDropdown: Virtualized member list with role labels
- RelaysDropdown: Connection status for chat relays
- ChatComposer: Message input with send functionality

Command System:
- chat command with identifier parsing and auto-detection
- Support for npub, nprofile, NIP-05, and relay'group-id formats
- Integration with window system and dynamic titles

NIP-29 Specific:
- Fetch kind:39000 (metadata), kind:39001 (admins), kind:39002 (members)
- Extract roles from p tags: ["p", "<pubkey>", "<role1>", "<role2>"]
- Role normalization (admin, moderator, host, member)
- Single group relay connection management

Testing:
- Comprehensive chat parser tests
- Protocol adapter test structure
- All tests passing (704 tests)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 21:38:23 +01:00