Commit Graph

109 Commits

Author SHA1 Message Date
Alejandro
c52e783fce fix(nip-29): fetch admin and member lists in parallel and correctly tag admins (#169)
* fix(nip-29): fetch admin and member lists in parallel and correctly tag admins

This commit improves NIP-29 group member fetching with two key changes:

1. **Parallel fetching**: Admins (kind 39001) and members (kind 39002) are now
   fetched in parallel using separate subscriptions, improving performance

2. **Correct admin role tagging**: Users in kind 39001 events now correctly
   default to "admin" role instead of "member" when no explicit role tag is
   provided, as per NIP-29 spec

Changes:
- Split participantsFilter into separate adminsFilter and membersFilter
- Use Promise.all to fetch both kinds in parallel
- Updated normalizeRole helper to accept a defaultRole parameter
- Process kind 39001 with "admin" default, kind 39002 with "member" default
- Added clearer logging for admin/member event counts

Related: src/lib/chat/adapters/nip-29-adapter.ts:192-320

* refactor(nip-29): simplify parallel fetch using pool.request

Use pool.request() with both filters in a single call instead of manual
subscription management. This is cleaner and more idiomatic:
- Auto-closes on EOSE (no manual unsubscribe needed)
- Fetches both kinds (39001 and 39002) in parallel with one request
- Reduces code complexity significantly

* fix(nip-29): use limit 1 for replaceable participant events

Kinds 39001 and 39002 are replaceable events with d-tag, so there should
only be one valid event of each kind per group. Changed limit from 5 to 1.

* fix(chat): change "Sign in to send messages" to "Sign in to post"

Simplified the login prompt text in chat interface for clarity.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-20 11:41:51 +01:00
Alejandro
97b3842692 fix: convert NIP-10 chat zap amounts to sats (#161)
* fix: convert NIP-10 chat zap amounts to sats

The NIP-10 adapter was storing zap amounts in millisats instead of sats,
which was inconsistent with:
- The MessageMetadata type definition (zapAmount documented as "Amount in sats")
- Other chat adapters like NIP-53 which correctly convert to sats
- UI expectations for displaying zap amounts

This fix adds the millisat-to-sat conversion (Math.floor(amount / 1000))
matching the implementation in the NIP-53 adapter.

Without this fix, zap amounts would display 1000x larger than intended
(e.g., a 1000 sat zap would show as 1,000,000).

* chore: update TypeScript build info

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 20:14:50 +01:00
Alejandro
8f008ddd39 feat: nip-10 chat interface (#153)
* docs: add comprehensive NIP-10 thread chat design documentation

Add detailed design documents for implementing NIP-10 thread chat feature:

- nip10-thread-chat-design.md: Full architecture, data structures, adapter
  implementation plan, relay selection strategy, UI requirements, and 7-phase
  implementation checklist
- nip10-thread-chat-examples.md: Complete code examples showing identifier
  parsing, conversation resolution, message loading, reply sending with proper
  NIP-10 tags, and ChatViewer integration
- nip10-thread-chat-summary.md: Quick reference with visual comparisons,
  architecture diagrams, protocol comparison table, data flow, and FAQ

The feature will enable "chat nevent1..." to display kind 1 threaded
conversations as chat interfaces, with the root event prominently displayed
at the top and all replies shown as chat messages below.

Key design decisions:
- Use e-tags with NIP-10 markers (root/reply) instead of q-tags
- Merge multiple relay sources (seen, hints, outbox) for coverage
- Display root event centered with full feed renderer
- Reuse existing ChatViewer infrastructure via adapter pattern
- Support both nevent (with relay hints) and note (ID only) formats

* feat: implement NIP-10 thread chat support

Add complete NIP-10 thread chat implementation enabling "chat nevent..." to
display kind 1 threaded conversations as chat interfaces.

**Type Definitions**:
- Add ThreadIdentifier for nevent/note event pointers
- Add "nip-10" to ChatProtocol type
- Extend ConversationMetadata with thread-specific fields (rootEventId,
  providedEventId, threadDepth, relays)

**NIP-10 Adapter** (src/lib/chat/adapters/nip-10-adapter.ts):
- parseIdentifier: Decode nevent/note format, reject non-kind-1 events
- resolveConversation: Fetch provided event, find root via NIP-10 refs,
  determine conversation relays (merge hints, outbox, fallbacks)
- loadMessages: Subscribe to kind 1 replies, kind 7 reactions, kind 9735 zaps
- sendMessage: Build proper NIP-10 tags (root/reply markers), add p-tags for
  all participants
- sendReaction: Send kind 7 with proper event/author references
- Smart relay selection: Merges seen relays, nevent hints, author outbox,
  user outbox (limit 7 relays for performance)

**ChatViewer Updates**:
- Detect NIP-10 threads (protocol === "nip-10")
- Fetch and display root event at top (centered with KindRenderer)
- Show visual separator ("Replies") between root and messages
- Update empty state message for threads ("No replies yet...")
- Enhanced header: Show "Author • Preview" for thread chats
- Update getAdapter to handle "nip-10" protocol

**Chat Parser**:
- Add Nip10Adapter to priority list (before other adapters to catch
  nevent/note)
- Update error message with nevent/note format examples
- Update adapter priority documentation

**Component Enhancements**:
- ReplyPreview: Show "thread root" when replying to root event (NIP-10)
- RelaysDropdown: Support conversation.metadata.relays for thread relay
  breakdown
- ChatMessageContextMenu: Add "Zap" option to context menu (opens ZapWindow)

**Features**:
- Root event displayed with full feed renderer (can interact: like, zap, etc.)
- All replies shown as chat messages with proper threading
- Reply/React/Zap options on all messages
- Relay dropdown shows breakdown of thread relays
- Participants dropdown shows all thread participants
- @ mention autocomplete works for participants
- Proper NIP-10 tag structure for nested replies
- Smart relay selection for maximum reach

**Usage**:
  chat nevent1qqsxyz...  # Thread with relay hints
  chat note1abc...       # Thread with event ID only

Root event is centered at top, all replies below as chat messages. Sending
replies creates kind 1 events with proper NIP-10 root/reply markers and
p-tags for all participants.

* fix: remove unused imports and mark intentionally unused parameter

- Remove unused getTagValues import
- Prefix providedEvent parameter with underscore to indicate intentional non-use (reserved for future relay hint extraction)

* feat: hide reply preview when directly replying to root in NIP-10 threads

In NIP-10 thread chats, the root event is already displayed prominently at
the top, so showing a reply preview when replying directly to the root is
redundant.

Changes:
- Skip reply preview for regular messages when replying to thread root
- Skip reply preview for zap messages when zapping the thread root
- Root event remains visible at top as reference point

This reduces visual clutter and makes the chat feel cleaner when replies
are directed at the already-visible root post.

* refactor: simplify NIP-10 thread chat implementation

Major simplifications based on user feedback:
- Render root event as regular message instead of special feed renderer
- Remove special-case logic for hiding reply preview when replying to root
- Include root event in messages list using RxJS combineLatest
- Change root author role from "admin" to "op" (original poster)
- Fix NIP-10 protocol link handler in chat header
- Add "Chat" option to event context menu for kind 1 events
- Fix unused parameter warnings in nip-10-adapter

This refactoring reduces complexity and makes NIP-10 threads behave
more consistently with other chat protocols.

* feat: add zap support for NIP-10 thread chat messages

Implement getZapConfig method to enable zapping messages in NIP-10 threads:
- Returns message author as recipient
- Includes event pointer for the message being zapped
- Uses conversation metadata relays for zap receipt publishing
- Integrates with new zap infrastructure from main branch

* fix: pass protocol and identifier directly to chat window

Fix 'undefined protocol' error when opening chat from event menu.
Instead of passing raw nevent as args, directly pass the parsed
protocol and identifier props that ChatViewer expects.

* debug: add logging to diagnose chat protocol issue

Add console logging to:
- chat command argParser to see input args and parsed result
- ChatViewer to see received props

This will help diagnose why 'undefined protocol' error appears
when opening chat via command or event menu.

* refactor: improve NIP-10 thread chat UX and relay/participant handling

UI Improvements:
- Remove "thread root" marker from reply previews - treat all replies uniformly
- Hide "load older messages" for NIP-10 threads (all replies loaded reactively)
- Display "Thread" with note icon instead of "Group" for NIP-10 conversations
- Remove debug logging from chat command parser and ChatViewer

Participant Management:
- Derive participants dynamically from messages (like live-chat does)
- Root author (OP) always listed first with "op" role
- All unique message authors included in member list
- Updates in real-time as new people reply

Relay Management:
- Expand relay collection to include participant outbox relays
- Fetch relays from root author, provided event author, and p-tagged participants
- Check up to 5 participants for relay diversity
- Increase max relay limit from 7 to 10 for better coverage
- Add logging for relay collection debugging

This makes NIP-10 threads feel more like proper chat conversations with
accurate participant lists and better relay coverage across the thread.

* refactor: remove debug logging from NIP-10 adapter

Remove informational console.log statements to reduce noise in production.
Keep only console.warn and console.error for actual problems.

This makes the adapter production-ready while maintaining useful error
and warning messages for debugging issues.

* fix: resolve lint errors from logging cleanup

- Fix empty block statement by simplifying next handler
- Prefix unused parameters with underscore (_response, _err)
- All lint checks now pass with 0 errors

* refactor: use AGGREGATOR_RELAYS constant and remove design docs

Replace hardcoded relay URLs with AGGREGATOR_RELAYS constant:
- Import AGGREGATOR_RELAYS from @/services/loaders
- Use constant for fallback relays in getThreadRelays()
- Use constant for default relays in getDefaultRelays()

Remove design documentation files (no longer needed):
- docs/nip10-thread-chat-design.md
- docs/nip10-thread-chat-examples.md
- docs/nip10-thread-chat-summary.md

This improves maintainability by centralizing relay configuration
and reduces repository clutter.

* refactor: remove relay.nostr.band and update AGGREGATOR_RELAYS

relay.nostr.band is no longer operational, so remove it from the codebase:

AGGREGATOR_RELAYS changes:
- Removed: wss://relay.nostr.band/
- Removed: wss://purplepag.es/
- Added: wss://relay.snort.social/
- Added: wss://relay.damus.io/
- New list: nos.lol, relay.snort.social, relay.primal.net, relay.damus.io

Updated code:
- src/services/loaders.ts: Updated AGGREGATOR_RELAYS constant
- src/lib/chat/adapters/nip-53-adapter.ts: Use AGGREGATOR_RELAYS instead of hardcoded relays

Updated tests:
- All test files updated to expect new relay URLs
- Replaced relay.nostr.band references with relay.snort.social
- Replaced purplepag.es references with relay.snort.social
- Fixed URL formats to include trailing slashes for normalization

All 980 tests passing ✓

* fix: change grimRelays from let to const in supporters.ts

Fix lint error from rebase - grimRelays is never reassigned so it should
use const instead of let.

* style: reduce padding on sign-in message to match composer

Change from px-3 py-2 to px-2 py-1 to match the horizontal and vertical
padding of the logged-in message composer (px-2 py-1), ensuring
consistent height between logged-in and logged-out states.

* feat: make sign-in message clickable to open login dialog

Add clickable 'Sign in' link to the logged-out message composer:
- Import LoginDialog component
- Add showLogin state management
- Make 'Sign in' text an underlined button that opens the login dialog
- Add LoginDialog component with controlled state

This provides a better UX by allowing users to quickly sign in
directly from the chat interface.

---------

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

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

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

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

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

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

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

* feat: migrate donation tracking to IndexedDB with monthly calculations

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

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

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

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

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

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

* fix: properly await async zap processing in subscription

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

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

* perf: optimize Dexie queries for donation tracking

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

**Optimizations:**

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

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

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

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

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

* refactor: singleton supporters service with optimized Dexie queries

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

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

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

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

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

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

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

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

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

* fix: fetch Grimoire relay list before subscribing to zaps

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

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

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

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

Fixes startup zap loading issue.

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

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

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

**Layout:**

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

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

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

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

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

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

Fixes several issues with zap loading on cold start:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* chore: merge main and remove debug logging for production

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

Includes comprehensive tests for relay selection logic.

---------

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

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

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

* feat: add custom tags and relays to zap command

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

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

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

* fix: include event pointer when zapping chat messages

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

* feat: add zap command reconstruction for Edit feature

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

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

* fix: separate eventPointer and addressPointer for proper zap tagging

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

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

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

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

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

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

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

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

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

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

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

* fix: pass all zap props from WindowRenderer to ZapWindow

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

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

* refactor: let createZapRequest collect relays from both authors

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

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

* fix: deduplicate explicit relays in createZapRequest

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

---------

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

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

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

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

* fix: load event in DynamicWindowTitle to derive zap recipient

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

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

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

---------

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

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

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

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

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

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

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

* feat: add LNURL address caching for instant zap UI

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

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

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

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

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

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

Verification: All 950 tests pass, build succeeds

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

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

This creates visual consistency between the amount and comment fields.

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

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

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

---------

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

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

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

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

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

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

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

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

Core Implementation:

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

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

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

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

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

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

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

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

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

* fix: resolve TypeScript errors in ZapWindow

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

* fix: resolve all TypeScript build errors in zap implementation

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

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

* fix: correct useProfile usage and ProfileContent handling in ZapWindow

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

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

* feat: refine ZapWindow UI and add dynamic window title

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

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

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

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

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

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

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

All tests passing (939 passed)
Build successful

* feat: compact ZapWindow UI and improve debugging

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

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

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

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

All tests passing (939 passed)
Build successful

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

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

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

All tests passing (939 passed)
Build successful

* feat: auto-close ZapWindow after successful wallet payment

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

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

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

All tests passing (939 passed)
Build successful

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

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

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

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

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

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

All tests passing (939 passed)
Build successful

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

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

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

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

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

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

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

* fix: remove duplicate zapTitle condition in DynamicWindowTitle

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

* refactor: use applesauce caching pattern and RichText for zaps

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

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

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

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

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

* revert: restore BATCH_SIZE to 20 transactions

* refactor: improve zap transaction list item UI

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

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

* fix: improve spacing and truncation in zap transaction items

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

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

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

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

* feat: parse zap requests from invoice description as fallback

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

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

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

---------

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

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

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

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

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

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

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

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

* chore: update TypeScript build info

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

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

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

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

* refactor: integrate Grimoire member styling into UserName component

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

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

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

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

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

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

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

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

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

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

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

* refactor: show profile username with theme-aware gradient

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

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

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

* chore: simplify and fix tests for production readiness

Removes redundant test file and updates comments for accuracy.

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

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

* chore: remove grimoire-members test file

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

* fix: show profile NIP-05 for Grimoire members

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-18 12:54:50 +01:00
Alejandro
14c73c07a3 feat: Add NIP-58 Badge Definition renderers (#123)
* feat: Add NIP-58 Badge Definition renderers

Implement feed and detail renderers for kind 30009 Badge Definition events.

- Add nip58-helpers.ts with badge metadata extraction functions
- Create BadgeDefinitionRenderer for compact feed view
- Create BadgeDefinitionDetailRenderer with award statistics
- Register both renderers in kinds registry

Badge definitions display:
- Badge image or Award icon fallback
- Badge name, description, and identifier
- In detail view: issuer, award count, recipients, image variants
- Automatically queries for badge awards (kind 8) to show stats

Follows existing renderer patterns (ZapstoreApp, EmojiSet) with
reactive queries using useLiveTimeline and cached helpers.

* refactor: Simplify badge renderers

Simplify NIP-58 badge renderers based on feedback:
- Rename "badge definition" to "badge" in comments and docs
- Remove image and ID from feed view (show only name + description)
- Remove award statistics fetching/display from detail view
- Remove badge address section from detail view

Feed view now shows minimal info (name, description) while detail
view focuses on badge metadata and image variants without external queries.

* feat: Add NIP-58 Badge Award renderers (kind 8)

Implement feed and detail renderers for Badge Award events that:
- Fetch and display badge metadata (thumbnail, name)
- Show recipient count in feed view ("Awarded to n people")
- Display full recipient list with usernames in detail view
- Link badge thumbnail and name to the badge event (kind 30009)
- Support award comments in detail view

Technical details:
- Parse badge address from "a" tag (30009:pubkey:identifier format)
- Fetch badge event reactively using eventStore.replaceable
- Use AddressPointer from nostr-tools/nip19 for type safety
- Register kind 8 in both feed and detail renderer registries

Also updates all "badge definition" terminology to just "badge"
in code comments for consistency with user-facing strings.

* refactor: Make badge award feed view more compact

Change BadgeAwardRenderer to display inline compact format:
- Small thumbnail (size-6) inline with text
- Badge name linked to badge event
- "awarded to n people" linked to award event (clickable)
- All elements in single line with flex-wrap for overflow

Layout: [thumbnail] badge-name awarded to n people

* feat: Show username when badge awarded to single person

Update BadgeAwardRenderer to display the recipient's username
when only 1 person is awarded the badge, instead of "1 person".

- Single recipient: "awarded to @username"
- Multiple recipients: "awarded to n people"

Uses UserName component for proper profile name resolution.

* feat: Add NIP-58 Profile Badges renderer (kind 30008)

- Add ProfileBadgesRenderer for feed view showing first 4 badges with count
- Add ProfileBadgesDetailRenderer for detail view showing all badges in grid
- Add getProfileBadgePairs helper to extract badge pairs from events
- Adjust BadgeAwardRenderer icon size from 6 to 5 and spacing to gap-1.5
- Register kind 30008 in both feed and detail renderer registries

Completes NIP-58 implementation with all three event types:
- Kind 8: Badge Awards
- Kind 30009: Badge Definitions
- Kind 30008: Profile Badges (this commit)

* refactor: Improve Profile Badges UX

Feed view:
- Show all badge thumbnails (removed 4-badge limit)
- Entire feed item is clickable to open detail view
- Badge count displayed inline

Detail view:
- Change from grid to vertical list layout
- Show one badge per row with horizontal layout
- Display: awarded by author, badge image, name, and description
- Better readability for badge information

* refactor: Improve Profile Badges layout and hierarchy

Feed view:
- Badge count now appears as clickable title
- Thumbnails displayed below title in separate row
- Better visual hierarchy and clearer affordance

Detail view:
- Increase badge images from size-16 to size-24
- Remove "Awarded by" label, show issuer directly
- Cleaner, more prominent badge presentation

* feat: Add Badge Awards (kind 8) to chat as system messages

Implemented NIP-58 badge award rendering in chat adapters:

Chat types (src/types/chat.ts):
- Add kind 8 to CHAT_KINDS array
- Add badgeAddress and awardedPubkeys to MessageMetadata

NIP-29 adapter (src/lib/chat/adapters/nip-29-adapter.ts):
- Include kind 8 in message filters
- Convert badge awards to system messages
- Extract badge metadata (address, recipients)

ChatViewer (src/components/ChatViewer.tsx):
- Add BadgeAwardSystemMessage component
- Parse badge address and fetch badge definition
- Render: "* username awarded 🏅 badge-name to username(s)"
- Show badge icon/image inline with badge name

Badge awards now appear as system messages showing issuer, badge
icon, badge name, and recipients in a clean horizontal layout.

* feat: Add Badge Awards (kind 8) to NIP-53 live chat

Extended badge award system messages to NIP-53 live streaming chats:

NIP-53 adapter (src/lib/chat/adapters/nip-53-adapter.ts):
- Import getAwardedPubkeys and getTagValues helpers
- Add kind 8 to message filters (loadMessages and loadMoreMessages)
- Add badge award handler in eventToMessage
- Convert to system messages with badge metadata

Badge awards from stream hosts now appear in live chat as system
messages, showing issuer, badge icon, and recipients in real-time.

* Revert "feat: Add Badge Awards to chat"

This reverts commits:
- 1686a94 feat: Add Badge Awards (kind 8) to NIP-53 live chat
- 909359f feat: Add Badge Awards (kind 8) to chat as system messages

Badge award rendering in chat will be implemented later.
This keeps the PR focused on Profile Badges (kind 30008) implementation.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-17 20:30:32 +01:00
KoalaSat
14d5255bce feat: Add P2P orders renderer and detail (#116) 2026-01-16 23:06:07 +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
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
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
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
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
c4687da3ef Add multi-room group chat support (#70)
* feat: add multi-room group chat interface (GroupListViewer)

Add a Discord/Slack-style multi-room chat interface for NIP-29 groups:

- New GroupListViewer component with split layout:
  - Left panel: List of groups from kind 10009, sorted by recency
  - Right panel: Full chat view for selected group
- Loads group metadata (kind 39000) for icons and names
- Tracks latest messages (kind 9) for activity-based sorting
2026-01-13 10:30:10 +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
76dd1e801d fix: reconstruct chat command for edit button (#62)
Add chat case to reconstructCommand to properly reconstruct the chat
command string when clicking the edit button in the window toolbar.

- NIP-29 groups: reconstruct as `chat relay'group-id`
- NIP-53 live activities: reconstruct as `chat naddr1...`

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 17:07: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
ef1032964c Merge branch 'chat' 2026-01-12 10:27:50 +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
938800c350 feat(RichText): extend parser with custom options and NIP reference support (#50)
* feat(RichText): extend parser with custom options and NIP reference support

- Add nipReferences transformer to parse NIP-xx patterns (e.g., NIP-01, nip-19)
- Create Nip component that renders NIP references as clickable links opening Grimoire's NIP viewer
- Expose parserOptions prop for customizing transformers, maxLength, and cacheKey
- Add expand/collapse functionality for truncated content with "Show more" button
- Include NIP transformer in default transformer pipeline
- Add comprehensive tests for NIP pattern matching and normalization

* feat(nip-transformer): add support for hex NIPs like NIP-C7

- Extend pattern to match hex NIP identifiers (NIP-C7, NIP-C0, NIP-A0)
- Normalize hex NIPs to uppercase (nip-c7 -> C7)
- Add tests for hex NIP parsing and normalization

* refactor(RichText): remove maxLength for now, export transformer types

* style(Nip): add icon, dotted underline, muted text for NIP links

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-11 22:21:13 +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
Alejandro
5233c57a1c feat: Add Zapstore app and app curation set renderers (#49)
* feat: Add Zapstore app and app curation set renderers

Add support for rendering Zapstore app-related Nostr events:
- Kind 32267 (App Metadata): Display app details, icon, platforms, screenshots
- Kind 30267 (App Curation Set): Display curated app collections

New files:
- src/lib/zapstore-helpers.ts: Helper functions for extracting app metadata
- src/lib/zapstore-helpers.test.ts: Comprehensive test coverage (43 tests)
- src/components/nostr/kinds/ZapstoreAppRenderer.tsx: Feed view for apps
- src/components/nostr/kinds/ZapstoreAppDetailRenderer.tsx: Detail view for apps
- src/components/nostr/kinds/ZapstoreAppSetRenderer.tsx: Feed view for collections
- src/components/nostr/kinds/ZapstoreAppSetDetailRenderer.tsx: Detail view for collections

Modified:
- src/components/nostr/kinds/index.tsx: Register new renderers in kind registry

All tests pass (726 total), build succeeds, no lint errors.

* feat: Add Zapstore release renderer (kind 30063)

Add support for rendering Zapstore app release events (kind 30063):
- Kind 30063 (Release): Connects apps (32267) to file artifacts (1063)

New files:
- src/components/nostr/kinds/ZapstoreReleaseRenderer.tsx: Feed view for releases
- src/components/nostr/kinds/ZapstoreReleaseDetailRenderer.tsx: Detail view with embedded file metadata

Modified:
- src/lib/zapstore-helpers.ts: Add release helper functions
  - getReleaseIdentifier(): Extract release ID (package@version)
  - getReleaseVersion(): Parse version from identifier
  - getReleaseFileEventId(): Get file metadata event pointer
  - getReleaseAppPointer(): Get app metadata pointer
- src/lib/zapstore-helpers.test.ts: Add 18 new tests for release helpers (61 total)
- src/components/nostr/kinds/index.tsx: Register kind 30063 renderers

Complete Zapstore app ecosystem now supported:
- Kind 32267: App metadata (name, icon, description)
- Kind 30267: App curation sets (collections)
- Kind 30063: App releases (version tracking)
- Kind 1063: File metadata (downloads)

All tests pass (744 total), build succeeds.

* refactor: Simplify Zapstore app renderers with platform icons

Improve Zapstore app rendering for cleaner, more intuitive display:

Changes:
- Add detectPlatforms() helper to normalize architecture tags (e.g., "android-arm64-v8a" → "android")
- Replace verbose platform badges with clean platform icons (Android, iOS, Web, macOS, Windows, Linux)
- Remove screenshots from feed view (keep in detail view only)
- Remove repository links and license badges from feed view
- Update detail view to show "Available On" with icon+label platform items

Feed view now shows:
- App icon
- App name
- Summary (2 lines max)
- Platform icons (just icons, no text)

Detail view now shows:
- App icon, name, summary
- Publisher, Package ID, License, Repository (metadata grid)
- Available On (platforms with icons and labels)
- Screenshots gallery (unchanged)

All tests pass (744 total), build succeeds.

* fix: Polish Zapstore renderers with platform labels and clean layout

Address feedback to improve Zapstore renderer UX:

Changes:
1. App feed (ZapstoreAppRenderer):
   - Add platform text labels next to icons (e.g., "Android", "iOS", "Web")
   - Now shows icon + label for better clarity

2. Release feed (ZapstoreReleaseRenderer):
   - Remove big package icon from feed view
   - Cleaner, more compact layout with just app name, version badge, and action links

3. Registry comments:
   - Update to human-friendly names:
     * "Zapstore App" (instead of "App Metadata (Zapstore)")
     * "Zapstore App Collection" (instead of "App Curation Set (Zapstore)")
     * "Zapstore App Release" (instead of "App Release (Zapstore)")

All tests pass (744 total), build succeeds.

* refactor: Update Zapstore app set renderers with improved UX

- ZapstoreAppSetRenderer: Show ALL apps with compact spacing (gap-0.5) like relay lists, removed 5-app limit
- ZapstoreAppSetDetailRenderer: Replace raw platform tags with normalized platform icons using detectPlatforms()
- Both renderers now provide cleaner, more consistent UI following Grimoire patterns

* refactor: Add human-friendly names and simplify Zapstore renderers

- kinds.ts: Add kind 32267 (App), update 30063 to "App Release", update 30267 to "App Collection"
- Extract PlatformIcon to shared component (zapstore/PlatformIcon.tsx)
- Update all renderer comments to use human-friendly terminology
- Remove unnecessary comments throughout Zapstore renderers
- Simplify code without changing functionality

* feat: Add releases section to app detail view

- Query for all releases (kind 30063) that reference the app
- Display releases sorted by version (newest first)
- Each release shows version badge and download link
- Clicking release opens full release detail view
- Clicking download opens file metadata view

* fix: Force screenshots as images and filter releases by author

- Add type="image" to MediaEmbed for screenshots to fix "unsupported media type" errors
- Filter releases to only show those from the same author (pubkey) as the app
- Prevents releases from other apps or authors from appearing in the app detail view

* fix: Remove author filter from releases query

The a tag already uniquely identifies the app (32267:pubkey:identifier).
Releases may be published by different authors (maintainers, packagers)
than the app author, so we should show all releases that reference
the app via the a tag, regardless of who published them.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-11 21:36:47 +01:00
Alejandro
84b5ac88aa Fix emoji rendering for dashed emoji codes (#48)
* Fix custom emoji shortcode matching to include dashes

The regex pattern for matching NIP-30 custom emoji shortcodes was missing
the dash character, causing emojis like :work-out: to fail matching.
Updated both ReactionRenderer and ReactionCompactPreview to use
[a-zA-Z0-9_-] instead of [a-zA-Z0-9_].

* Refactor: extract emoji shortcode regex to constant

Move the emoji shortcode matching pattern to EMOJI_SHORTCODE_REGEX
constant in emoji-helpers.ts for reuse across reaction renderers.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-09 11:03:43 +01:00
Alejandro
a1fe411161 NIP-30 custom emoji kinds (#47)
* feat(emoji): implement NIP-30 custom emoji support

- Fix shortcode regex to be NIP-30 spec compliant (alphanumeric + underscore only)
- Add feed and detail renderers for kind 30030 Emoji Sets
- Create shared CustomEmoji component for consistent emoji rendering
- Add getEmojiTags helper with applesauce-style symbol caching

* fix(emoji): remove colons from emoji set title display

* style(emoji): enhance detail renderer title with icon

* style(emoji): simplify detail renderer layout

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-08 16:55:21 +01:00
Alejandro
35f55b8063 feat(calendar): add renderers for NIP-52 calendar events (kinds 31922 & 31923) (#43)
* feat(calendar): add renderers for NIP-52 calendar events (kinds 31922 & 31923)

- Add CalendarDays and CalendarClock icons for date/time event kinds
- Create calendar-event.ts helper with parsing and formatting functions
- Add feed renderers showing status badge, title, date/time, location, participant count
- Add detail renderers with full description, participant list with names, tags, and links
- Register renderers in kinds/index.tsx for both feed and detail views
- Use locale-aware date/time formatting throughout

* refactor(calendar): improve feed renderer layout

- Rename both kinds to "Calendar Event" for consistency
- Move date/time info below title
- Place time on left, status badge on right with justify-between
- Remove timezone indicator from feed view (keep in detail)

* refactor(calendar): apply feed layout to detail views

- Move title above date/time in detail views
- Use justify-between for time left, status badge right
- Use Label component for hashtags (no # prefix, consistent with feed)
- Use Label component for participant roles (subtle dotted border style)

* refactor(calendar): separate location/tags rows and tone down time font

- Separate location/participants from hashtags onto different rows in feed
- Reduce time font size: text-xs in feed, text-sm in detail (was text-sm/text-lg)
- Remove font-medium from time display

* style(calendar): tone down status badges to match date/time styling

Replace bold background-colored badges with subtle text-colored badges:
- Use text-blue-500, text-green-500, text-muted-foreground instead of backgrounds
- Lowercase labels ("upcoming", "now", "past")
- Consistent sizing with date/time text

* refactor(calendar): extract shared components and add caching

- Extract CalendarStatusBadge to shared component with variant/size props
- Add symbol-based caching to parseDateCalendarEvent and parseTimeCalendarEvent
  using applesauce's getOrComputeCachedValue for performance
- Use 'd' tag (identifier) as title fallback instead of "Untitled Event"
- Remove duplicate CalendarStatusBadge implementations from all 4 renderers
- Remove unused imports (cn, CalendarDays, CalendarClock, Clock, CheckCircle)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-07 21:20:04 +01:00
Alejandro
b2b398b9fb docs: add applesauce v5 upgrade plan (#39)
* docs: add applesauce v5 upgrade plan

Comprehensive migration plan covering:
- Package updates (add applesauce-common, update to v5)
- EventFactory import migration (applesauce-factory → applesauce-core)
- Unified event loader setup
- ActionHub → ActionRunner migration
- useObservableMemo → use$ hook migration
- New features: casting system, encrypted content caching
- Documentation and skills updates needed

* feat: upgrade applesauce libraries to v5

Major upgrade from applesauce v4 to v5 with breaking changes:

Package updates:
- applesauce-core: ^4.0.0 → ^5.0.0
- applesauce-actions: ^4.0.0 → ^5.0.0
- applesauce-loaders: ^4.0.0 → ^5.0.0
- applesauce-react: ^4.0.0 → ^5.0.0
- applesauce-relay: ^4.0.0 → ^5.0.0
- applesauce-signers: ^4.0.0 → ^5.0.0
- applesauce-accounts: ^4.0.0 → ^5.0.0
- Added new applesauce-common: ^5.0.0 package

API migrations:
- EventFactory: applesauce-factory → applesauce-core/event-factory
- ActionHub → ActionRunner with async function pattern (not generators)
- useObservableMemo → use$ hook across all components
- Helper imports: article, highlight, threading, zap, comment, lists
  moved from applesauce-core to applesauce-common
- parseCoordinate → parseReplaceableAddress
- Subscription options: retries → reconnect
- getEventPointerFromETag now returns null instead of throwing

New features:
- Unified event loader via createEventLoaderForStore
- Updated loaders.ts to use v5 unified loader pattern

Documentation:
- Updated CLAUDE.md with v5 patterns and migration notes
- Updated applesauce-core skill for v5 changes
- Created new applesauce-common skill

Test fixes:
- Updated publish-spellbook.test.ts for v5 ActionRunner pattern
- Updated publish-spell.test.ts with eventStore mock
- Updated relay-selection.test.ts with valid test events
- Updated loaders.test.ts with valid 64-char hex event IDs
- Added createEventLoaderForStore mock

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 14:54:21 +01:00
Alejandro
d14c2f3028 feat: implement NIP-89 app definitions and recommendations with rich rendering (#36)
* feat: implement NIP-89 app definitions and recommendations with rich rendering

Add comprehensive support for NIP-89 Application Handlers (kind 31990) and
Handler Recommendations (kind 31989) with rich, interactive visualizations.

Core Implementation:
- nip89-helpers.ts: Utility functions for extracting NIP-89 event metadata
  - App name, description, image from kind 31990 content JSON
  - Supported kinds from k tags
  - Platform URLs (web, ios, android) from platform tags
  - Handler references from kind 31989 a tags
  - URL template substitution for <bech32> placeholders

Feed Renderers:
- ApplicationHandlerRenderer (31990): Shows app name, supported kinds as
  clickable KindBadges (max 8 in feed), and platform badges
- HandlerRecommendationRenderer (31989): Shows recommended kind and handler
  list (max 3 in feed) with platform indicators

Detail Renderers:
- ApplicationHandlerDetailRenderer (31990): Comprehensive view with app info,
  all supported kinds in grid layout (clickable), platform URLs with copy
  buttons, and metadata JSON viewer
- HandlerRecommendationDetailRenderer (31989): Full view with platform
  filtering tabs, expanded handler cards showing app details, and raw
  reference data

Features:
- Clickable KindBadges throughout for quick navigation
- Platform-aware filtering and display
- Fetches referenced kind 31990 events reactively
- Copy buttons for URL templates
- Platform icons (web, ios, android)
- Follows existing Grimoire patterns (SpellRenderer for kinds display,
  CodeSnippetDetailRenderer for metadata sections)

Testing:
- Comprehensive test suite for nip89-helpers (50+ test cases)
- Tests cover all helper functions with edge cases
- Follows existing test patterns from codebase

Registry:
- Added both kinds (31989, 31990) to kindRenderers and detailRenderers
- Automatically expands supported kinds count in KindsViewer

* fix: remove unused imports and parameters in NIP-89 renderers

* fix: correct AddressPointer import and apply prettier formatting

- Change AddressPointer import from applesauce-core/helpers to nostr-tools/nip19
  to match codebase conventions
- Auto-fix prettier formatting for nip89 files

* fix: add defensive type checks to prevent React error 31

- Add type guards in nip89-helpers to ensure string types
- Check metadata object structure before accessing properties
- Add fallbacks for undefined address.identifier values
- Prevents accidentally rendering objects as React children

* fix: stringify contentJson for CopyableJsonViewer and support 'about' field

- Fix React error 31: CopyableJsonViewer expects string, not object
- Add JSON.stringify() with pretty printing for metadata display
- Support both 'description' and 'about' fields in content JSON (common in kind 0)
- Add tests for 'about' field handling

* refactor: simplify NIP-89 detail renderers

Remove unnecessary metadata displays:
- Remove app image from ApplicationHandlerDetailRenderer
- Remove Event ID and Created timestamp from both detail renderers
- Remove Raw Metadata section from ApplicationHandlerDetailRenderer
- Remove Raw References section from HandlerRecommendationDetailRenderer
- Clean up unused imports (getAppImage, CopyableJsonViewer, useMemo, formatAddressPointer)

Keeps the UI focused on the essential information: app name, description,
supported kinds, and platform URLs.

* feat: add website display and filter non-platform tags

NIP-89 renderer improvements:
- Add getAppWebsite() helper to extract website from content JSON
- Display website URL in both feed and detail renderers with external link
- Filter out non-platform tags (r, t, client, alt, e, p, a) to prevent garbage display
- Remove relay hint display from HandlerRecommendationDetailRenderer
- Clean up unused relayHint parameter

Fixes the 'r r' tag appearing as a platform by properly excluding
common non-platform tags when detecting platform URLs.

* refactor: create reusable ExternalLink component for consistent styling

Create ExternalLink component following patterns from HighlightRenderer and
BookmarkRenderer with:
- Two variants: 'muted' (default, text-muted-foreground with underline)
  and 'default' (text-primary with hover:underline)
- Three sizes: xs, sm, base
- Configurable icon display
- Consistent truncate behavior for long URLs
- Stop propagation on click

Apply to NIP-89 renderers:
- ApplicationHandlerRenderer: uses muted variant (feed view)
- ApplicationHandlerDetailRenderer: uses default variant (detail view)

This ensures consistent link styling across the entire application
and makes it easy to maintain a unified design language.

* refactor: consolidate JSON parsing into cached getAppMetadata helper

Performance optimization:
- Create getAppMetadata() helper that parses content JSON once and caches
  the result using Symbol.for('nip89-metadata') as cache key
- All metadata helpers (getAppName, getAppDescription, getAppWebsite) now
  use the cached metadata instead of parsing JSON multiple times
- Prevents redundant JSON.parse() calls when multiple helpers are used

Code cleanup - removed unused functions:
- getAppImage() - no longer used after removing image display
- getHandlersByPlatform() - filtering done in component state
- substituteTemplate() - not needed in current implementation
- hasPlaceholder() - utility never used
- formatAddressPointer() - not needed anymore

Updated tests:
- Replace getAppImage tests with getAppWebsite tests
- Remove tests for deleted utility functions
- All remaining tests pass

This consolidation improves performance by ensuring JSON.parse() is called
at most once per event, regardless of how many metadata fields are accessed.

* feat: use app name in window titles for NIP-89 app events

Add special handling for kind 31990 (Application Handler) events in
getEventDisplayTitle to use the app name from content JSON instead of
generic kind name. Falls back to identifier if app name not available.

This gives NIP-89 app handler events nice readable window titles.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 11:42:14 +01:00
Alejandro Gómez
c4bc3ab445 ui: improve relay tooltip, update docs 2025-12-22 19:43:00 +01:00
Claude
b9756b119b refactor: simplify relay list UI with compact status indicators and rich tooltips
**Compact Relay Item Display:**
- Removed left-side inbox/outbox count indicators (were causing misalignment)
- Replaced "EOSE" text with checkmark icon (✓)
- Event count shown as [N] badge (only if > 0)
- Auth icon now always visible (even for unauthenticated relays)
- Clean right-side layout: [count] [✓] [auth] [wifi]

**Always-Visible Auth Status:**
- Modified getAuthIcon() to always return an icon (never null)
- Unauthenticated relays show subtle shield icon (muted-foreground/40)
- Provides at-a-glance view of auth status for all relays
- Label: "No Authentication Required" for clarity

**Rich Hover Tooltips:**
- Comprehensive tooltip shows all relay details on hover
- Displays: connection status, auth status, subscription state, event count
- Shows inbox/outbox counts when available (moved from inline display)
- Formatted as structured table for easy scanning
- Positioned on left side to avoid blocking content

**Benefits:**
 Perfect alignment (no variable-width counts on left)
 Cleaner, more scannable visual design
 All information still accessible via hover
 Consistent icon count (always 2-4 icons per relay)
 Easy to spot EOSE status at a glance (green checkmark)

All 639 tests passing.
2025-12-22 18:23:12 +00:00
Claude
b5e1cffc91 fix: add EOSE indicator, mute all icons, and fix relay URL normalization bug
Critical fixes for ReqViewer relay state accuracy:

1. **URL Normalization Fix** (fixes mismatch with CONN):
   - Added normalizeRelayURL to normalize all relay URLs in finalRelays
   - RelayStateManager normalizes URLs (adds trailing slash, lowercase) but
     finalRelays did not, causing lookup failures in relayStates
   - Now normalizedRelays is used for all state lookups and passed to
     useReqTimelineEnhanced to ensure consistency
   - This fixes the bug where ReqViewer showed different connected relay
     counts than CONN viewer

2. **EOSE Indicator**:
   - Added back EOSE indicator to relay dropdown (was removed in UI redesign)
   - Shows subtle "EOSE" text when relay has sent End of Stored Events
   - Includes tooltip explaining "End of stored events received"

3. **Muted Icons** (per user request for subtlety):
   - Type indicators: blue-500/purple-500 → muted-foreground/60
   - Strategy header icons: all → muted-foreground/60
   - Section headers: green-500 → muted-foreground
   - Connection icons: green-500/yellow-500/red-500 → /70 opacity variants
   - Auth icons: same color reduction for consistency
   - Maintains semantic meaning while reducing visual noise

All 639 tests passing.
2025-12-22 17:53:24 +00:00
Claude
ce3a4a7322 fix: handle all relays disconnecting before EOSE (stuck in LOADING bug)
Critical Edge Case Fix:
Previously, when all relays disconnected before sending EOSE, the state
remained stuck in LOADING because overallEoseReceived stayed false.

Solution: Check if all relays are in terminal states
- Terminal states: eose, error, or disconnected
- If all terminal AND no overall EOSE yet, derive state from events:
  * No events → FAILED
  * Has events, all disconnected, streaming → OFFLINE
  * Has events, all disconnected, non-streaming → CLOSED
  * Some active, some terminal → PARTIAL

New Test Coverage (5 tests):
1. All relays disconnect before EOSE, no events → FAILED
2. All relays disconnect before EOSE, with events (streaming) → OFFLINE
3. All relays disconnect before EOSE, with events (non-streaming) → CLOSED
4. Some EOSE, others disconnect before EOSE → PARTIAL
5. Mix of EOSE and errors, all terminal → PARTIAL

This fixes the user-reported issue where disconnected relays show LOADING
instead of transitioning to appropriate terminal state.

Tests: 639/639 passing (added 5 new edge case tests)
2025-12-22 17:38:31 +00:00
Claude
1bb2727930 fix: remove unused variables and apply prettier formatting
- Remove unused connectedCount and relayStatesForReq variables
- Fix prettier formatting in ReqViewer.tsx
- All tests passing (634/634)
- Build successful
2025-12-22 16:25:42 +00:00
Claude
c60abe6df4 feat: implement production-grade REQ state machine with per-relay tracking
Core Infrastructure:
- Add ReqRelayState and ReqOverallState types for granular state tracking
- Implement deriveOverallState() state machine with 8 query states
- Create useReqTimelineEnhanced hook combining RelayStateManager + event tracking
- Add comprehensive unit tests (27 tests, all passing)

State Machine Logic:
- DISCOVERING: NIP-65 relay selection in progress
- CONNECTING: Waiting for first relay connection
- LOADING: Initial events loading
- LIVE: Streaming with active relays (only when actually connected!)
- PARTIAL: Some relays ok, some failed/disconnected
- OFFLINE: All relays disconnected after being live
- CLOSED: Query completed, all relays closed
- FAILED: All relays failed to connect

UI Updates:
- Single-word status indicators with detailed tooltips
- Condensed relay status into NIP-65 section (no duplicate lists)
- Per-relay subscription state badges (RECEIVING, EOSE, ERROR, OFFLINE)
- Event counts per relay
- Connection + Auth status integrated into single dropdown

Fixes Critical Bug:
- Solves "LIVE with 0 relays" issue (Scenario 5 from analysis)
- Distinguishes real EOSE from relay disconnections
- Accurate status for all 7 edge cases documented in analysis

Technical Approach:
- Hybrid: RelayStateManager for connections + event._relay for tracking
- Works around applesauce-relay catchError bug without forking
- No duplicate subscriptions
- Production-quality error handling

Tests: 27/27 passing including edge case scenarios
2025-12-22 16:18:15 +00:00