Commit Graph

17 Commits

Author SHA1 Message Date
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
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
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
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
4ee385ea6c Add actions support to chat adapter protocol (#67)
* feat: add protocol-specific actions to chat adapters

Extends the chat adapter system with support for slash commands and
protocol-specific actions without parameters.

New features:
- ChatAction type system for defining simple commands
- Base adapter getActions() and executeAction() methods
- NIP-29 /join and /leave slash commands
- Slash command parser for detecting /commands
- ChatViewer integration with toast notifications

Example usage in NIP-29 groups:
  /join  - Request to join the group
  /leave - Leave the group

The action system is extensible and can be enhanced with parameterized
actions (e.g., /kick <user>, /ban <user>) in future iterations.

Tests: All 804 tests passing
Build: Successful
Lint: No errors

* feat: add autocomplete for slash commands

Extends the chat system with autocomplete UI for slash commands, making
actions discoverable and easier to execute.

New features:
- SlashCommandSuggestionList component with keyboard navigation
- Slash command search integrated into MentionEditor
- Autocomplete popup shows command name and description
- Arrow keys for navigation, Enter to select, Escape to close
- Filters available commands based on typed text

When typing "/" in chat, users now see:
  /join   Request to join the group
  /leave  Leave the group

The autocomplete uses the same TipTap suggestion system as @mentions
and :emoji:, providing a consistent UX across all autocomplete features.

Tests: All 804 tests passing
Build: Successful
Lint: 1 warning fixed (unused parameter)

* feat: execute slash commands immediately on autocomplete selection

When selecting a slash command from the autocomplete menu, the action
now executes immediately and clears the input, providing a more
streamlined UX.

Changes:
- Added onCommandExecute prop to MentionEditor
- Modified slash command suggestion to call onCommandExecute on selection
- Clears editor content immediately after selection
- Added handleCommandExecute callback in ChatViewer
- Executes action and shows toast notifications

Before: Type "/" → select "/join" → press Enter → executes
After: Type "/" → select "/join" → executes immediately

This matches the expected behavior for command selection - when you
choose a command from autocomplete, you want to execute it, not just
insert it into the text field.

Tests: All 804 tests passing
Build: Successful
Lint: No new errors

* feat: make chat actions context-aware and filter by membership status

Actions are now intelligently filtered based on the user's membership in
the conversation, providing a cleaner and more intuitive UX.

Changes:
- Added GetActionsOptions type with conversation and activePubkey
- Updated base adapter getActions() to accept optional context
- Modified NIP-29 adapter to filter actions based on membership:
  - /join: only shown when user is NOT a member/admin
  - /leave: only shown when user IS a member
- Updated ChatViewer to pass conversation and user context to searchCommands
- Moved searchCommands callback after conversation is defined

This prevents showing irrelevant commands like "/join" when you're already
a member, or "/leave" when you haven't joined yet. The autocomplete menu
now only displays actions that are actually executable in the current context.

Implementation notes:
- NIP-29 uses getAllActions() fallback when context unavailable
- Membership determined by checking conversation.participants array
- Other protocols return empty array by default (no actions yet)

Tests: All 804 tests passing
Build: Successful
Lint: No new errors

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 11:49:32 +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
b24810074d feat: add NIP-61 nutzap support to NIP-29 groups (#59)
* feat: add NIP-61 nutzap support to NIP-29 groups

Fetch and render nutzap events (kind 9321) in NIP-29 relay groups
using the same visual styling as lightning zaps. Nutzaps are P2PK
locked Cashu token transfers defined in NIP-61.

- Add nutzap filter subscription in loadMessages
- Combine chat and nutzap observables with RxJS combineLatest
- Add nutzapToMessage helper to parse NIP-61 event structure
- Extract amount by summing proof amounts from proof tag JSON
- Add nutzapUnit metadata field for future multi-currency support

* fix: improve zap/nutzap rendering in chat

- Add mb-1 margin bottom to zap messages for spacing
- Show inline reply preview for zaps that target specific messages
- Fix nutzap amount extraction to handle multiple proof tags
- Extract replyTo from e-tag for nutzaps
- Pass nutzap event to RichText for custom emoji rendering

* fix: pass event only to RichText for proper emoji rendering

* refactor: consolidate NIP-29 chat and nutzap into single REQ

- Use single filter with kinds [9, 9000, 9001, 9321] instead of
  separate subscriptions with combineLatest
- Enables proper pagination for "load older" with single page fetches
- Add rounded corners to zap gradient border for consistent rendering

* refactor: consolidate NIP-53 chat and zap into single REQ

- Use single filter with kinds [1311, 9735] instead of separate
  subscriptions with combineLatest
- Enables proper pagination for "load older" with single page fetches
- Filter invalid zaps inline during event mapping

* feat: add load older messages support to chat adapters

- Implement loadMoreMessages in NIP-29 adapter using pool.request
- Implement loadMoreMessages in NIP-53 adapter using pool.request
- Add "Load older messages" button to ChatViewer header
- Use firstValueFrom + toArray to convert Observable to Promise
- Track loading state and hasMore for pagination UI

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 15:26:41 +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
385f599b67 ui: show 'left' in chat 2026-01-12 11:43:14 +01:00
Alejandro
2bad592a3a feat: emoji autocompletion (#54)
* feat: add NIP-30 emoji autocompletion to editor

Implement emoji autocomplete triggered by `:` in the MentionEditor:

- EmojiSearchService: flexsearch-based indexing for emoji shortcodes
- useEmojiSearch hook: loads Unicode emojis + user's custom emoji (kind 10030/30030)
- EmojiSuggestionList: grid-based suggestion UI with keyboard nav
- Update MentionEditor with second Mention extension for emoji
- Serialize emoji as `:shortcode:` format with NIP-30 emoji tags
- Update chat adapters to include emoji tags in messages

Sources:
- Unicode: ~300 common emojis with shortcodes
- Custom: user's emoji list (kind 10030) and referenced sets (kind 30030)
- Context: emoji tags from events being replied to

* feat: add rich emoji preview in editor

Emoji inserted via the autocomplete now display as actual images/characters
instead of :shortcode: text:

- Custom emoji: renders as inline <img> with proper sizing
- Unicode emoji: renders as text with emoji font sizing
- Both show :shortcode: on hover via title attribute

CSS styles ensure proper vertical alignment with surrounding text.

* fix: store emoji url and source attributes in node schema

The TipTap Mention extension only defines `id` and `label` by default.
Added `addAttributes()` to EmojiMention extension to also store `url`
and `source` attributes, fixing emoji tags not being included in sent
messages.

* fix: improve emoji node rendering in editor

- Remove redundant renderLabel (nodeView handles display)
- Add renderText for proper clipboard behavior
- Make nodeView more robust with null checks
- Add fallback to shortcode if image fails to load
- Unicode emoji shows character, custom shows image

* fix: serialize unicode emoji as actual characters, not shortcodes

When sending messages:
- Unicode emoji (😄, 🔥) → outputs 😄, 🔥 (the actual character)
- Custom emoji (:pepe:) → outputs :pepe: with emoji tag for rendering

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 11:30:52 +01:00
Alejandro Gómez
0b20a628e2 feat: add mention editor and NIP-29 chat enhancements
Implements rich text editing with profile mentions, NIP-29 system messages,
day markers, and naddr support for a more complete chat experience.

Editor Features:
- TipTap-based rich text editor with @mention autocomplete
- FlexSearch-powered profile search (case-insensitive)
- Converts mentions to nostr:npub URIs on submission
- Keyboard navigation (Arrow keys, Enter, Escape)
- Fixed Enter key and Send button submission

NIP-29 Chat Improvements:
- System messages for join/leave events (kinds 9000, 9001, 9021, 9022)
- Styled system messages aligned left with muted text
- Shows "joined" instead of "was added" for consistency
- Accepts kind 39000 naddr (group metadata addresses)
- Day markers between messages from different days
- Day markers use locale-aware formatting (short month, no year)

Components:
- src/components/editor/MentionEditor.tsx - TipTap editor with mention support
- src/components/editor/ProfileSuggestionList.tsx - Autocomplete dropdown
- src/services/profile-search.ts - FlexSearch service for profile indexing
- src/hooks/useProfileSearch.ts - React hook for profile search

Dependencies:
- @tiptap/react, @tiptap/starter-kit, @tiptap/extension-mention
- @tiptap/extension-placeholder, @tiptap/suggestion
- flexsearch@0.7.43, tippy.js@6.3.7

Tests:
- Added 6 new tests for naddr parsing in NIP-29 adapter
- All 710 tests passing

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 10:26:38 +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
cfd897f96c feat: wait for EOSE before rendering messages to prevent scroll jumping
Message Loading Improvements:
- Use RxJS Subject to track EOSE (End Of Stored Events)
- Use skipUntil operator to delay timeline emissions until EOSE received
- Prevents scroll position jumping during initial message load
- Messages still update reactively after initial EOSE

NIP-29 Adapter:
- Create eoseSubject to track EOSE state
- Emit from subject when EOSE string received from relay subscription
- Apply skipUntil(eoseSubject) to eventStore.timeline() observable

NIP-C7 Adapter:
- Add relay subscription to track EOSE (was missing)
- Use same EOSE tracking pattern as NIP-29
- Apply skipUntil to prevent premature timeline emissions

Benefits:
- Smooth initial load experience without scroll jumping
- All messages appear together after EOSE
- Maintains reactive updates for new messages
- Consistent behavior across both chat protocols

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 22:15:04 +01:00
Alejandro Gómez
6c1c1bbf04 fix: prevent message horizontal overflow with proper text wrapping
Message Layout Improvements:
- Added proper padding to message items (px-3 py-2)
- Added flex-1 min-w-0 to message content container to allow shrinking
- Added break-words and overflow-hidden to prevent horizontal overflow
- Ensures long URLs and unbreakable text wrap properly

Message Composer Improvements:
- Added border-t and consistent padding (px-3 py-2)
- Added border and rounded-md to textarea for better visual separation
- Added min-w-0 to textarea to prevent overflow in flex layout
- Added flex-shrink-0 to Send button to prevent squishing
- Added mb-2 to reply preview for spacing

These changes ensure:
- Long messages and URLs wrap correctly without horizontal scroll
- Consistent spacing throughout the chat interface
- Proper flex behavior prevents layout breaks
- Professional chat UI appearance

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 22:12:53 +01:00
Alejandro Gómez
64b97e4926 feat: add kind 10009 (Public Chats) renderer with clickable group links
Implements rendering of NIP-51 kind 10009 (Public Chats list) events,
displaying NIP-29 groups similar to how relay lists are rendered.

Components:
- GroupLink: Clickable group component with icon and name
  - Fetches kind 39000 metadata from EventStore for group info
  - Displays group icon (if available) or chat icon fallback
  - Opens chat window on click with relay'group-id identifier

- PublicChatsRenderer: Renderer for kind 10009 events
  - Extracts "group" tags: ["group", "<group-id>", "<relay-url>"]
  - Displays each group as clickable link
  - Similar styling to relay list renderers

Integration:
- Registered kind 10009 renderer in kinds index
- Opens chat with NIP-29 protocol on click

Fixes:
- Fix missing type imports in NIP-29 adapter (Participant, ParticipantRole)
- Fix relayUrl type error in sendMessage (use validated variable)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 22:04:41 +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