This commit eliminates automatic inbox initialization on login, preventing
unwanted network requests and heavy I/O operations. Inbox sync now only
activates when users explicitly enable it.
## Problem
When logging in, the app would automatically:
- Initialize gift wrap service immediately
- Auto-enable inbox sync without user consent
- Load encrypted content from Dexie
- Wait up to 1 second for cache readiness
- Fetch inbox relay lists from network
- Subscribe to gift wrap events
- Open persistent relay connections
This caused:
- App hangs during login (network/IO blocking)
- Unwanted network activity before user opts in
- Poor performance on initial load
- Unnecessary resource consumption when DMs not needed
## Solution
### 1. On-Demand Initialization (useAccountSync.ts)
**Before**: Auto-init and auto-enable on every login
**After**: Watch settings$ and only init when user enables
```typescript
// Only initialize when user explicitly enables inbox sync
const settingsSub = giftWrapService.settings$.subscribe((settings) => {
if (settings.enabled && giftWrapService.userPubkey !== pubkey) {
giftWrapService.init(pubkey, signer);
}
});
```
### 2. Early Exit for Disabled State (gift-wrap.ts)
**Before**: Always loaded cache and relays, then checked enabled flag
**After**: Check enabled FIRST, exit early if disabled
```typescript
async init(pubkey: string, signer: ISigner | null) {
// Set basic properties
this.userPubkey = pubkey;
this.signer = signer;
// Early exit if disabled (prevents expensive operations)
if (!this.settings$.value.enabled) {
return;
}
// Only do expensive operations when enabled
await getStoredEncryptedContentIds();
await this.waitForCacheReady();
this.loadInboxRelays();
// ...
}
```
### 3. Updated Documentation
- Clarified on-demand initialization flow
- Updated lifecycle documentation with performance notes
- Changed "auto-enable" section to "on-demand" section
## Performance Impact
**Login Performance**:
- ✅ No automatic Dexie reads
- ✅ No cache waiting (up to 1s saved)
- ✅ No network requests for inbox relays
- ✅ No relay subscriptions until needed
- ✅ Instant login when DMs not needed
**User Control**:
- Users explicitly opt-in via InboxViewer toggle
- Clear UI feedback about enabling inbox sync
- No surprise network activity
**When Enabled**:
- Full functionality identical to before
- All optimizations from previous commits preserved
## Testing
- ✅ All 864 tests pass
- ✅ Build succeeds with no errors
- ✅ Verified on-demand initialization flow
- ✅ Confirmed no auto-init on login
## Files Changed
- `src/hooks/useAccountSync.ts` - Watch settings, init only when enabled
- `src/services/gift-wrap.ts` - Early exit if disabled, expose userPubkey
- `src/components/InboxViewer.tsx` - Updated comments
- `docs/gift-wrap-architecture.md` - Updated flow and lifecycle docs
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit refines the NIP-17 encrypted messaging implementation with
better relay detection, cleaner UI, and comprehensive documentation.
## Core Fixes
### 1. Fix Missing User Inbox Relays (nip-17-adapter.ts)
- **Problem**: When creating conversations, user's own inbox relays were
only checked from cache, not actively fetched if cache was empty
- **Result**: Send failures with "missing relays" even when relays were
fetched and connected elsewhere
- **Solution**: Actively fetch user's inbox relays if cache is empty,
with fallback to cached value for performance
- **Impact**: Reliable relay detection for sending messages
### 2. Improve View-Only Detection (ChatViewer.tsx)
- **Problem**: Used stale `unreachableParticipants` metadata set at
conversation creation time, causing false warnings even after relay
lists loaded
- **Solution**: Added dynamic `canSendMessage` useMemo that checks
current state of `participantInboxRelays` in real-time
- **Impact**: Send button correctly enables/disables as relay lists load
### 3. Cleaner UI (ChatViewer.tsx)
- Removed large yellow warning banners about view-only mode
- Removed "Sending disabled - waiting for relay lists" text in composer
- Send button now simply disables when relay lists are missing
- **Impact**: Less intrusive, cleaner messaging interface
## Additional Improvements
### Cache Readiness Check (gift-wrap.ts)
- Added `waitForCacheReady()` to prevent race condition on page reload
- Waits up to 1s for encrypted content cache to be accessible before
processing conversations
- **Impact**: Fixes "inbox appears empty" issue on page reload
### Simplified Event Caching (nip-17-adapter.ts)
- Removed redundant `syntheticEventCache` WeakMap
- Uses eventStore as single source of truth with O(1) lookup
- **Impact**: Reduced complexity, eventStore already handles deduplication
### Removed Self-Chat Workaround (nip-17-adapter.ts)
- Deleted 70-line custom gift wrap construction for self-chat
- Applesauce's `SendWrappedMessage` works fine for self-chat
- **Impact**: Cleaner code, better maintainability
### Debug Logging System (dm-debug.ts - NEW)
- Added dedicated DM debug logging utilities
- Enable with: `localStorage.setItem('grimoire:debug:dms', 'true')`
- Levels: dmDebug (verbose), dmInfo (important), dmWarn (warnings)
- **Impact**: Better troubleshooting for NIP-17 issues
### Comprehensive Documentation (docs/gift-wrap-architecture.md - NEW)
- 450+ line architecture document
- Component diagrams, data flow, cache strategy
- Security considerations, performance optimizations
- Debugging guide and testing strategy
- **Impact**: Complete reference for gift wrap implementation
## Testing
- ✅ All tests pass (864 tests)
- ✅ Build succeeds with no errors
- ✅ Lint passes (only pre-existing warnings)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Remove invalid actionOpts parameter from GiftWrapBlueprint.create()
Gift wrap blueprints don't accept meta tag options; those belong on
the rumor itself which already receives actionOpts correctly.
- Fix SubscriptionResponse type handling in gift-wrap.ts
Use type guard to safely check for event objects in subscription
responses instead of accessing properties that may not exist.
Fixes TypeScript compilation errors preventing production builds.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add --json flag to open command for passing raw unsigned events
- Create DMRumorRenderer for kind 14 (NIP-17 DM rumors)
- Hide copy/open options for unsigned events in all menus
- Clean up EventDetailViewer header for unsigned events (no text shown)
- Auto-detect unsigned events in ChatMessageContextMenu
Unsigned events (like NIP-17 rumors) now properly render and display
across the app without showing misleading copy/open options that won't
work for events without signatures.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
CRITICAL FIX: Changed from pool.request() to pool.subscription() to keep
the relay connection open after EOSE. This was the root cause of why
received messages didn't appear automatically.
**Root Cause:**
1. pool.request() fetches historical events and CLOSES after EOSE
2. After EOSE, relay subscription is terminated
3. New gift wraps sent after that aren't received from relay
4. EventStore timeline subscription only fires when events are added
5. Result: Only locally-sent messages appeared (added to EventStore),
but remotely-received messages were invisible until manual sync
**Solution:**
Use pool.subscription() which keeps the WebSocket connection OPEN after
EOSE, allowing real-time message delivery.
**Changes:**
src/services/gift-wrap.ts:
* Changed pool.request() to pool.subscription()
* Added detailed logging for EVENT and EOSE responses
* Properly stores relay subscription in subscriptions array for cleanup
* Connection stays open indefinitely for real-time updates
**Expected Behavior:**
After EOSE:
- ✅ Relay connection stays open (WebSocket active)
- ✅ New gift wraps received in real-time from relay
- ✅ EventStore.add() called automatically (via eventStore option)
- ✅ Timeline subscription fires
- ✅ Message appears in UI within 500ms
Console logs should show:
1. 'Opening subscription to X relays for real-time gift wraps'
2. '✓ EOSE from wss://relay... (subscription stays open)'
3. '📨 Received gift wrap xxxxxx from relay' (when message arrives)
4. '📬 Timeline subscription fired with X gift wraps'
5. '💬 Updated conversations: X conversations, X total rumors'
**Testing:**
1. Login and open self-chat
2. Send message from another client/device
3. Message should appear automatically within 500ms
4. No manual sync needed
5. Works for both self-chat and regular DMs
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
CRITICAL FIX: Gift wrap service was only initialized when opening the
inbox viewer, meaning users wouldn't receive DMs unless they had the
inbox window open. This caused the issue where sent messages appeared
but received messages didn't.
**Root Cause:**
1. Gift wrap service init only happened in InboxViewer component
2. Default settings had enabled=false
3. Subscription only started if enabled=true
4. Result: No subscription → no incoming messages
**Solution:**
Moved gift wrap service initialization to useAccountSync hook, which runs
globally whenever a user is logged in. This ensures DM subscriptions are
always active.
**Changes:**
1. src/hooks/useAccountSync.ts:
* Added gift wrap service initialization when account changes
* Auto-enables inbox sync on first login (enabled=true)
* Properly cleans up on logout/account change
* Runs globally in AppShell, not just in inbox viewer
2. src/components/InboxViewer.tsx:
* Removed redundant service initialization
* Added comment explaining global init
* Kept signer update effect for account switching
**Expected Behavior:**
- ✅ User logs in → gift wrap subscription starts automatically
- ✅ DMs received in real-time even when inbox viewer closed
- ✅ Both sent and received messages appear live in chat
- ✅ Works with self-chat and group chats
- ✅ Proper cleanup on logout
**Testing:**
1. Login to account
2. Console should show: '[useAccountSync] Initializing gift wrap service'
3. Open chat with someone (don't open inbox viewer)
4. Have them send you a message
5. Message should appear automatically within 500ms
6. Send a message back
7. Both messages visible without manual sync
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Replaced action-based self-chat with direct implementation using
publishEventToRelays. This eliminates the dependency on patched
node_modules and provides a more robust solution.
**Why This Change:**
The previous approach relied on patching applesauce-actions in node_modules
to fix the relay hint bug. This patch would be lost on every npm install,
making it fragile and non-reproducible.
**New Self-Chat Flow:**
1. Create rumor using WrappedMessageBlueprint (kind 14)
2. Wrap rumor using GiftWrapBlueprint (kind 1059)
3. Persist encrypted content to Dexie BEFORE publishing
4. Publish directly to inbox relays using publishEventToRelays
5. Add to EventStore for immediate local availability
**Benefits:**
- ✅ No dependency on node_modules patches
- ✅ Guaranteed to work across npm installs
- ✅ Direct control over relay selection
- ✅ Encrypted content persisted before publish (no race conditions)
- ✅ Clean separation: self-chat vs group chat logic
**Non-Self-Chat:**
Still uses SendWrappedMessage action (with patched applesauce for now).
Future work: Extend custom implementation to all message types.
**Testing:**
Self-chat messages should now:
- Appear live within 500ms (no manual sync)
- Persist across page reloads
- Match subscribed relay URLs exactly
- Include proper console logging at each step
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added detailed logging throughout gift-wrap service to trace:
- Timeline subscription firing
- Gift wrap detection (symbol vs persisted)
- Conversation updates
- Rumor extraction
Also fixed updateConversations to check persistedIds in addition to
in-memory symbol, ensuring gift wraps are processed correctly even
after persistence.
This will help identify why messages don't appear live and require
manual sync.
This commit fixes two critical issues preventing live message delivery
in self-chat and other NIP-17 conversations:
**Issue 1: applesauce-actions relay hint bug (PATCHED IN NODE_MODULES)**
- Root cause: Action used gift wrap's random ephemeral pubkey to look up
inbox relays, but the map was keyed by real recipient pubkeys
- Result: inboxRelays.get(giftWrap.pubkey) returned undefined
- publishEvent received no relay hints and failed with "No relays found"
- Messages never published
**Issue 2: Relay URL normalization mismatch**
- Root cause: Inbox relays from kind 10050 not normalized (kept raw format)
- Result: URLs with/without trailing slashes treated as different relays
Example: wss://relay.com/ vs wss://relay.com
- Published to: wss://frens.nostr1.com/ (with slash)
- Subscribed to: wss://frens.nostr1.com (without slash)
- Messages sent but subscription never received them (different relay!)
- User had to click "Sync" to manually fetch from all relay variations
**Changes:**
1. src/services/gift-wrap.ts:
* Added normalizeRelayURL import
* Normalize all inbox relay URLs when loading from kind 10050 event
* Ensures consistent URL format for subscription matching
2. src/services/hub.ts:
* Added normalizeRelayURL import
* Normalize relay hints before publishing
* Ensures sent messages go to same normalized relay as subscription
3. node_modules/applesauce-actions/dist/actions/wrapped-messages.js:
* PATCHED: Track recipient pubkey alongside gift wrap
* Use recipientPubkey (real user) instead of giftWrap.pubkey (ephemeral)
* Ensures correct inbox relay lookup
* NOTE: This is a temporary patch until fix is merged upstream
* TODO: Remove patch after applesauce-actions >5.0.2 is released
**Expected Behavior After Fix:**
1. User sends self-chat message
2. Action looks up inbox relays using recipient pubkey (FIXED)
3. publishEvent receives relay hints: ['wss://relay.com/'] (WORKING)
4. Relay hints normalized: ['wss://relay.com/'] (NEW)
5. Gift wrap published to normalized relays (WORKING)
6. Subscription listening on normalized relays receives message (FIXED)
7. Message appears live in UI without manual sync (~200-500ms latency)
**Testing:**
Self-chat test:
- Send message to self
- Console should show:
* [Publish] Using provided relay hints
* [Publish] Persisted encrypted content for gift wrap
* Match: true (relay alignment)
- Message should appear within 500ms
- Reload page - message persists
- Send another message - both visible, no duplicates
**Note on node_modules patch:**
The applesauce-actions patch will be lost on npm install. Options:
1. Use patch-package to persist the patch
2. Wait for upstream fix and update to applesauce-actions >5.0.2
3. Create local fork until upstream merge
For now, if you run npm install, you'll need to reapply the patch.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit implements a production-ready solution for NIP-17 gift wrap
sending and persistence, fixing the issue where self-chat messages
would appear optimistically but disappear on page reload.
**Root Causes Identified:**
1. **publishEvent ignored relay hints from actions**
- ActionRunner calls publishMethod(event, relays) with two parameters
- Our publishEvent only accepted one parameter (event)
- Gift wrap actions passed inbox relays as second parameter → ignored
- Gift wraps published to wrong relays or failed with no relay list
2. **Encrypted content not persisted during send**
- Gift wraps created with EncryptedContentSymbol (in-memory only)
- When received back from relay, new instance had no symbol
- isGiftWrapUnlocked() returned false → marked as "pending"
- Messages didn't appear in UI until manually decrypted
3. **Optimistic updates created duplicate risk**
- Synthetic rumor created with timestamp T1, ID calculated
- Real rumor created with timestamp T2, different ID
- Potential for duplicates in UI on relay echo
**Changes:**
- src/services/hub.ts:
* Modified publishEvent signature to accept optional relayHints parameter
* Use relay hints when provided, fallback to outbox relay lookup
* Added encrypted content persistence to Dexie for kind 1059 events
* Persists decrypted content using EncryptedContentSymbol during publish
* Fixed TypeScript null handling for getOutboxRelays return type
* Added detailed console logging for relay routing debugging
- src/lib/chat/adapters/nip-17-adapter.ts:
* Removed optimistic update code (lines 651-725)
* Removed synthetic rumor creation and ID calculation
* Removed immediate decryptedRumors$ update on send
* Simplified sendMessage flow to rely on natural relay echo (~200-500ms)
* Kept relay alignment debug logging for self-chat diagnostics
- src/services/gift-wrap.ts:
* Added refreshPersistedIds() method for debugging
* Allows manual reload of persisted IDs from Dexie if needed
**Expected Behavior After Fix:**
1. User sends self-chat message
2. SendWrappedMessage queries own inbox relays (kind 10050)
3. publishEvent receives relay hints, uses them for publishing
4. Gift wrap encrypted content persisted to Dexie during publish
5. Gift wrap sent to inbox relays (same relays we're subscribed to)
6. Gift wrap received back from relay (~200ms)
7. persistedIds check recognizes it as already unlocked
8. Message appears in UI and persists across reloads
**Testing Required:**
Manual testing checklist (see plan in /claudedocs/):
- Self-chat: send message, verify appears and persists on reload
- 1-on-1 chat: verify messages persist across reloads
- Group chat: verify multi-recipient messages work
- Reply functionality: verify reply threads persist
- Error cases: verify clear error messages for missing inbox relays
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added detailed logging to diagnose why self-chat messages disappear on
reload. Logs show:
- Own inbox relays (where SendWrappedMessage will send to)
- Subscribed relays (where gift wrap service is receiving from)
- Whether they match
This will help identify if there's a relay mismatch causing gift wraps
to be sent but not received.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Previously, self-chat relied on giftWrapService.inboxRelays$.value being
populated, but if empty, self-chat would show 0 relays and fail to send
messages. This fix actively fetches own inbox relays using
fetchInboxRelays(activePubkey) for self-chat, ensuring inbox relays are
always populated and self-chat works correctly.
Changes:
- Detect self-chat and actively fetch own inbox relays
- For non-self-chat, use cached value first (optimization)
- Store fetched relays in userInboxRelays variable for metadata
- Add logging for self-chat relay fetch status
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
NIP-17 inbox relays are now properly cached and displayed just like regular relay lists:
**Relay List Cache Integration**:
- Extended CachedRelayList schema with `inbox?: string[]` field for kind 10050
- relay-list-cache now subscribes to both kind 10002 and kind 10050 events
- Parses and caches inbox relays from "relay" tags in kind 10050 events
- Merges inbox relays with existing cached entries (preserves read/write)
**NIP-17 Adapter Improvements**:
- Checks cache first before fetching inbox relays from network
- Fetched kind 10050 events auto-added to EventStore → triggers cache
- Logs show "Using cached" vs "Fetched and cached" for visibility
- Inbox relays now persist across sessions via Dexie
**Benefits**:
- Inbox relays display immediately from cache (no network delay)
- Reduced network requests - fetch once, use everywhere
- RelaysDropdown shows per-participant inbox relays automatically
- Inbox relays sync whenever chatting with participants
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
InboxViewer conversations weren't loading automatically on page reload because
updateConversations() was only called:
1. When gift wraps change from pending to unlocked (not already decrypted)
2. After sync fetches from relays (requires manual refresh)
Now updateConversations() is called immediately after loading stored gift wraps
from Dexie, so conversations appear instantly without needing to click refresh.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Two major improvements to NIP-17 sending:
1. **Aggressive relay resolution** - Try much harder to find inbox relays:
- Query ALL write + read relays (not just 3)
- Query ALL aggregator relays for maximum coverage
- Increased timeout from 5s to 10s
- Better logging with ✅/❌ status indicators
- This should dramatically improve inbox relay discovery success rate
2. **Optimistic message display** - Messages appear instantly:
- Build rumor locally before sending
- Calculate rumor ID using same algorithm as Nostr events
- Add rumor + synthetic gift wrap to decryptedRumors$ immediately
- UI updates instantly without waiting for relay round-trip
- Real gift wrap replaces synthetic when received (deduplication)
- Fixes self-chat messages not appearing
3. **Self-chat fix**:
- Explicitly pass [activePubkey] for self-chat instead of []
- Ensures SendWrappedMessage action sends to self
This provides immediate feedback to users and much better inbox relay resolution.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements the full sendMessage() method for NIP-17 DMs using high-level
applesauce actions (SendWrappedMessage, ReplyToWrappedMessage).
Key features:
- Validates active account and signer before sending
- Blocks sends if any participant lacks inbox relays (safety first)
- Supports reply functionality with parent rumor lookup
- Uses ActionRunner to execute gift wrap actions
- Publishes to own inbox relays for cross-device sync
- Comprehensive error handling with clear user-facing messages
Implementation details:
- ~100 lines replacing stub method in nip-17-adapter.ts
- Uses gift wrap service's natural flow for optimistic updates
- No manual EventStore.add() - relies on receive → decrypt → display pipeline
- Typical UI update latency: 100-500ms
Error cases handled:
- No active account or signer
- Missing inbox relays for participants
- Unreachable participants (no kind 10050 events)
- Reply parent not found in decrypted rumors cache
- Action execution failures
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fix reply preview to support NIP-17 synthetic events via adapter.loadReplyMessage()
- Update DM title styling to use regular text color with lock icon in header
- Add copy button support for NIP-17 chat identifiers (nprofile/npub)
- Ensure header structure matches NIP-29 (consistent control placement)
- Fix comma spacing in participant names (items-baseline, non-breaking spaces)
- Add conditional tooltip with participant details for NIP-17 chats
- Improve inbox relay resolution (longer timeouts, more relays, better logging)
- Move NIP badge to right-side controls for consistency
- Add lock icon before member count for encrypted conversations
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The e-tags in NIP-17 messages reference the innermost event (rumor) IDs,
not the gift wrap IDs. Updated ReplyPreview to properly handle this:
- Add local state fallback for when eventStore doesn't track synthetic events
- Use adapter's loadReplyMessage() return value directly
- syntheticEventCache now reliably provides rumor events for reply previews
This ensures reply previews work correctly even if eventStore doesn't
properly index events with empty signatures.
Major improvements:
- Fix message ordering to be chronological (oldest first)
- Add syntheticEventCache for reliable reply resolution
- Simplify code structure with pure functions
- Sort messages properly for chat display
Key changes:
- createSyntheticEvent() caches and adds events to eventStore
- lookupEvent() checks cache first, then eventStore
- loadMessages() now sorts by timestamp ascending
- fetchInboxRelays() simplified with proper error handling
- Add unreachableParticipants metadata for send validation
The synthetic event cache ensures reply previews work even if
eventStore doesn't persist events with empty signatures.
- Hide "load older messages" button for NIP-17 (loads all at once)
- Show loading indicator while waiting for message decryption
- Remove upload button for NIP-17 (encrypted uploads not supported)
- Fix inbox click to pass proper ProtocolIdentifier with hex pubkeys
- Fetch inbox relays for all participants (not just current user)
- Use participant's outbox relays + aggregators for inbox relay lookup
- Fix NIP-10 e-tag reply resolution with proper marker handling
(prioritizes "reply" marker, falls back to last unmarked e-tag)
- Fix garbled messages by creating synthetic events from rumors
- Add rumor events to EventStore so ReplyPreview can find them
- Fix decrypt toast showing success even on failure
- Show profile names in chat title using DmTitle component
- Support $me alias for saved messages (chat $me)
- Make inbox conversations more compact, remove icon
- Show "Saved Messages" for self-conversation in inbox
- Wire up inbox conversation click to open chat window
- Show per-participant inbox relays in RelaysDropdown
- Add participantInboxRelays metadata type for NIP-17
- Create NIP-17 chat adapter supporting npub, nprofile, hex pubkey, NIP-05
- Support self-messages ("Saved Messages"), groups, and 1-on-1 conversations
- Add gift wrap event persistence to Dexie (version 17)
- Load stored gift wraps into EventStore on startup
- Export Rumor type from gift-wrap service for adapter use
- Wire up NIP-17 adapter to chat parser and ChatViewer
- Add kind 14 to CHAT_KINDS for consistent message filtering
- Update tests to reflect NIP-17 support for npub identifiers
Chat command formats now supported:
- chat npub1.../nprofile1.../hex pubkey (single recipient)
- chat user@example.com (NIP-05 resolution)
- chat npub1...,npub2... (group conversation)
The issue was that on page reload:
1. EventStore starts empty
2. loadInboxRelays() only subscribed to EventStore, not fetching from relays
3. startSync() with no inbox relays passed empty array to subscribeToGiftWraps()
4. subscribeToGiftWraps() only fetched from relays if relays.length > 0
Fixes:
- Add fetchInboxRelayList() to actively fetch kind 10050 from user's
outbox relays (or aggregator relays as fallback)
- Use AGGREGATOR_RELAYS as fallback when no inbox relays configured
- When inbox relays are discovered, restart sync with the new relays
- Always request gift wraps from relays (inbox or aggregator fallback)
- Add console logging for debugging relay operations
Fixes several issues with the inbox feature:
1. Load persisted decrypted content on init:
- Load stored encrypted content IDs from Dexie on service init
- Subscribe to eventStore.update$ to detect cache restoration
- Automatically update conversations when restored content is available
2. Mark already-decrypted gift wraps correctly:
- Check both in-memory unlock state AND persisted IDs
- Prevents showing decrypted messages as "pending" after reload
3. Hide manual decrypt UI when auto-decrypt is enabled:
- Only show "Decrypt All" button when auto-decrypt is off
- Show "Auto-decrypting..." status when auto-decrypt is on
4. Show pending count in user menu:
- Add pendingCount$ observable to gift wrap service
- Display badge on "Private Messages" menu item when there are
undecrypted messages and auto-decrypt is disabled
5. Expose full rumor for future kind support:
- Add decryptedRumors$ observable with all decrypted rumors
- Full rumor event (id, pubkey, kind, tags, content) is preserved
- Enables future support for any kind sent via gift wrap
* Add download button to Zapstore app renderers
Add a download button for the latest release to both the feed and detail
renderers for kind 32267 (Zapstore App Metadata). The feed renderer shows
a compact version button, while the detail renderer shows a prominent
download button in the header. Both fetch the latest release and link to
its file metadata event (kind 1063) for download.
* Add proper relay hints for fetching Zapstore releases
Use useLiveTimeline instead of eventStore.timeline() to actually fetch
release events from relays. Relay selection includes:
- Seen relays (where the app event was received from)
- Publisher's outbox relays (NIP-65)
- Aggregator relays as fallback
This ensures releases are properly fetched rather than just read from
the local event store cache.
* Add relay hints when opening file events for download
Pass relay hints from the release event's seen relays when opening
file metadata events (kind 1063) for download. This ensures the
event loader knows where to fetch the file event from.
Also adds relay hints to the ReleaseItem component for both opening
the release detail and the download file.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
* fix: hide zap reply previews for missing or non-chat events
Only show reply previews for zaps when:
1. The replied-to event exists in the event store
2. The replied-to event is a chat kind (9, 9321, or 1311)
This prevents showing loading states or reply previews for
zaps replying to events we don't have or non-chat events.
* refactor: extract CHAT_KINDS constant and include zap receipts
- Create CHAT_KINDS constant in types/chat.ts with all chat event kinds
- Include kind 9735 (zap receipts) as a chat kind
- Update ChatViewer to use the new constant for validation
- Improves maintainability and makes it easier to update chat kinds
Chat kinds now include:
- 9: NIP-29 group chat messages
- 9321: NIP-61 nutzaps
- 1311: NIP-53 live chat messages
- 9735: NIP-57 zap receipts
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add NIP-43 relay access metadata support
Add feed and detail rendering for kind 13534 (Relay Members) events,
and enable all NIP-43 kind constants for relay access management.
- Add RelayMembersRenderer with feed and detail views
- Enable kind constants: 13534, 28934, 28935, 28936
- Use Shield icon to represent relay access control
- Extract members from NIP-43's "member" tags (not standard "p" tags)
* feat: add renderers for NIP-43 Add/Remove User events
- Change kind 13534 icon from Shield to Users for consistency
- Add feed and detail renderers for kind 8000 (Add User)
- Add feed and detail renderers for kind 8001 (Remove User)
- Both show the affected pubkey using PubkeyListFull component
* fix: show username in Add/Remove User feed renderers
Display the actual username (via UserName component) in kind 8000/8001
feed views instead of just generic text.
* refactor: simplify NIP-43 renderers to follow codebase patterns
- Use PubkeyListPreview in RelayMembersRenderer feed view (shows actual
users instead of just count, matching FollowSetRenderer pattern)
- Remove redundant icon props from detail renderers (PubkeyListFull
has sensible defaults)
- Simplify variable naming and reduce comments
- No functional changes, just cleaner code
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add copy chat ID button to chat header
Add a button next to the chat title that copies the chat identifier
to clipboard. The identifier can be used with the `chat` command to
reopen the same conversation.
- For NIP-29 groups: copies relay'group-id format
- For NIP-53 live activities: copies naddr encoding
The button shows a check icon for feedback when copied.
* refactor: simplify copy chat ID button styling
- Use CopyCheck icon instead of Check for consistency with CodeCopyButton
- Remove tooltip to reduce UI noise
- Keep hover state styling (muted to foreground)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
* fix: improve chat UX with better scroll and symmetric padding
- Add alignToBottom prop to Virtuoso for better last message visibility
- Add small footer to prevent last message from being hidden under scroll
- Make composer padding symmetric (py-1 instead of py-1 pb-0)
* fix: restore pb-0 on composer
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add pointer-events-none to the message preview div in GroupListViewer
to ensure clicks always pass through to the parent group item's onClick
handler. This prevents issues where clicks on the preview text (UserName
or RichText components) might not trigger group selection.
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
Add right-click/long-press context menu to chat messages with event interactions:
- Reply to message
- Copy message text
- Open event detail (opens in new window)
- Copy event ID (nevent/naddr with relay hints)
- View raw JSON
Implements the same EventMenu pattern used in feed renderers, providing
consistent UX across the app. No visible UI elements added - menu appears
only on right-click or long-press (mobile).
Components added:
- src/components/ui/context-menu.tsx - Radix UI context menu primitives
- src/components/chat/ChatMessageContextMenu.tsx - Chat-specific context menu
- Integrated into MessageItem in ChatViewer.tsx
Dependencies:
- Added @radix-ui/react-context-menu
Co-authored-by: Claude <noreply@anthropic.com>
* feat: Add pop-out command route for standalone window rendering
Add a new /run route that allows windows to be opened in separate
browser windows/tabs without affecting the main workspace layout.
Changes:
- Add RunCommandPage component for /run?cmd=<command> route
- Add Pop Out button to WindowToolbar (ExternalLink icon)
- Parse command from URL query parameter and render result
- Construct minimal WindowInstance for rendering
- Display command string in header with clean minimal UI
This enables users to pop out any window into a separate browser
context while maintaining the main workspace layout, useful for
multi-monitor setups or keeping reference windows visible.
* refactor: Remove header from pop-out command page
Simplify RunCommandPage to only show the window renderer without
any additional UI chrome. This provides a cleaner, more focused
experience for popped-out windows.
* refactor: Move pop-out action to window menu dropdown
Move the pop-out button from a standalone icon to the three-dot
menu dropdown to reduce toolbar clutter. The menu now always
appears since pop-out is always available.
* feat: Add AppShell header to pop-out command page
Wrap RunCommandPage with AppShell (hideBottomBar) to show the header
with user menu and command launcher, matching the behavior of NIP-19
preview pages.
When a command is launched from the /run page, it navigates to the
main dashboard (/) where the window system exists.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: Display blossom servers in user menu with caching
Implements caching and display of user's blossom server lists (kind 10063) in the user menu dropdown.
**Key Changes:**
1. **Database Schema (db.ts)**:
- Added `CachedBlossomServerList` interface
- Added `blossomServers` table to Dexie v15
- Indexed by pubkey and updatedAt for efficient querying
2. **Caching Service (blossom-server-cache.ts)**:
- Dual-layer caching: LRU memory cache (100 entries) + Dexie persistent cache
- 24-hour TTL to reduce network requests
- Auto-caches kind:10063 events from EventStore
- Mirrors relay-list-cache pattern for consistency
3. **Sync Hooks**:
- `useBlossomServerCacheSync`: Subscribes to EventStore for auto-caching kind:10063 events
- `useAccountSync`: Extended to fetch and watch active account's blossom servers
4. **State Management**:
- Added `blossomServers` field to `activeAccount` in GrimoireState
- Created `setActiveAccountBlossomServers` pure function and callback
- Reactive updates when user's server list changes
5. **User Menu UI (user-menu.tsx)**:
- Added blossom servers section with HardDrive icon
- Shows server count badge (e.g., "3 servers")
- Clickable links open servers in new tabs
- Displays only when user has configured servers
**Architecture:**
- Follows existing relay list caching pattern for consistency
- Reactive: UI auto-updates when kind:10063 events arrive
- Incremental sync: Fetches on login, caches for 24h
- Performance: Memory cache for <1ms lookups, Dexie for persistence
**Testing:**
- Build: ✓ No TypeScript errors
- Tests: ✓ All 838 tests passing
* feat: Open blossom server file lists directly from menus
**User Menu & Profile Viewer Improvements:**
1. **Enhanced Click Behavior**:
- Clicking a blossom server now opens the file list for that server
- Shows blobs uploaded by the user (user menu) or profile owner (profile viewer)
- Pre-selects the clicked server in the dropdown
2. **UX Improvements**:
- Removed server count from user menu label (cleaner UI)
- Added `cursor-crosshair` to blossom server items (consistent with other clickable items)
- Removed external link icon (not opening external URL anymore)
3. **Technical Changes**:
- Updated `ListBlobsView` to accept optional `serverUrl` prop for pre-selection
- User menu: Opens `blossom list` with `serverUrl` for active user
- Profile viewer: Opens `blossom list` with both `pubkey` and `serverUrl`
**Flow:**
- User menu → Click server → Opens files for active user on that server
- Profile viewer → Click server → Opens files for viewed user on that server
* fix: Properly fetch blossom servers for any profile view
**Problem:**
Blossom servers were only visible for the logged-in user's profile,
not for other users' profiles being viewed.
**Solution:**
Enhanced ProfileViewer blossom server fetching with multi-layer approach:
1. **Cache-first loading**: Check blossomServerCache for instant display
2. **EventStore check**: Use existing cached event if available
3. **Reactive subscription**: Subscribe to EventStore for real-time updates
4. **Network fetch**: Use addressLoader to fetch latest from relays
5. **Auto-caching**: Update cache when new events arrive
**Benefits:**
- Blossom servers now display for ANY user's profile
- Instant display from cache (< 1ms)
- Reactive updates when data changes
- Proper cache hydration for future visits
- Consistent with relay list fetching pattern
**Technical:**
- Imported and integrated blossomServerCache service
- Added cache check before network fetch
- Separated EventStore subscription from network fetch
- Added cache updates on event arrival
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Add controlled state for tooltip open/close
- Enable tap/click to toggle tooltip on mobile devices
- Tooltip now shows conversation metadata on tap (icon, description, protocol type, status, host)
- Maintains existing hover behavior on desktop
- Improves mobile UX by making metadata accessible via tap
Co-authored-by: Claude <noreply@anthropic.com>
* Fix chat composer placeholder and text alignment
- Adjusted .ProseMirror min-height from 2rem to 1.25rem to match container
- Added flexbox layout to .ProseMirror for proper vertical centering
- Removed float:left and height:0 from placeholder causing misalignment
- Moved padding from editor props to wrapper div
- Updated EditorContent to use flex items-center for alignment
Resolves vertical alignment issues in chat composer input field.
* Fix cursor placement in chat composer placeholder
- Changed from flexbox to line-height for vertical centering
- Removed flex from .ProseMirror to fix cursor positioning
- Set line-height: 1.25rem to match min-height for proper alignment
- Removed flex items-center from EditorContent className
This ensures the cursor appears at the correct position when focusing
the input field, rather than after the placeholder text.
* Fix cursor placement on mobile devices
- Made placeholder absolutely positioned to prevent it from affecting cursor
- Added position: relative to .ProseMirror container
- This ensures cursor appears at the start of input on mobile browsers
The absolute positioning removes the placeholder from the normal layout flow,
preventing mobile browsers from placing the cursor after the pseudo-element.
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Replace NIP-05 preference with npub in ShareSpellbookDialog
- Replace NIP-05 preference with npub in SpellbookRenderer preview
- Remove unused useProfile imports from both components
- Update comment to reflect npub-only URL format
This ensures more reliable URL sharing since npub is always available
while NIP-05 may not be set or verified. The routing system still
supports both formats for backwards compatibility.
Co-authored-by: Claude <noreply@anthropic.com>
* Fix newline rendering in chat messages
The Text component was not properly rendering newlines in chat messages.
The previous implementation had buggy logic that only rendered <br /> for
empty lines and used an inconsistent mix of spans and divs for non-empty
lines, which didn't create proper line breaks between consecutive text lines.
Changes:
- Render each line in a span with <br /> between consecutive lines
- Remove unused useMemo import and fix React hooks violation
- Simplify logic for better maintainability
This ensures that multi-line messages in chat (and other text content)
display correctly with proper line breaks.
Fixes rendering of newlines in NIP-29 groups and NIP-53 live chat.
* Preserve newlines when sending chat messages
The MentionEditor's serializeContent function was not handling hardBreak
nodes created by Shift+Enter. This caused newlines within messages to be
lost during serialization, even though the editor displayed them correctly.
Changes:
- Add hardBreak node handling in serializeContent
- Preserve newlines (\n) from Shift+Enter keypresses
- Ensure multi-line messages are sent with proper line breaks
With this fix and the previous Text.tsx fix, newlines are now properly:
1. Captured when typing (Shift+Enter creates hardBreak)
2. Preserved when sending (hardBreak serialized as \n)
3. Rendered when displaying (Text component renders \n as <br />)
* Make Enter insert newline on mobile devices
On mobile devices, pressing Enter now inserts a newline (hardBreak) instead
of submitting the message. This provides better UX since mobile keyboards
don't have easy access to Shift+Enter for multiline input.
Behavior:
- Desktop: Enter submits, Shift+Enter inserts newline (unchanged)
- Mobile: Enter inserts newline, Cmd/Ctrl+Enter submits
- Mobile detection: Uses touch support API (ontouchstart or maxTouchPoints)
Users can still submit messages on mobile using:
1. The Send button (primary method)
2. Ctrl+Enter keyboard shortcut (if available)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add comprehensive NIP-51 list rendering support
Implement rich renderers for all major NIP-51 list types with both
feed and detail views. Creates reusable list item components for
consistent UI across different list kinds.
New list renderers:
- Kind 10000: Mute List (pubkeys, hashtags, words, threads)
- Kind 10001: Pin List (pinned events/addresses)
- Kind 10003: Bookmark List (events, addresses, URLs)
- Kind 10004: Community List (community references)
- Kind 10005: Channel List (public chat channels)
- Kind 10015: Interest List (hashtags + interest sets)
- Kind 10020: Media Follow List (media creators)
- Kind 10030: User Emoji List (custom emojis)
- Kind 10101: Good Wiki Authors
- Kind 10102: Good Wiki Relays
- Kind 30000: Follow Sets (categorized follows)
- Kind 30003: Bookmark Sets (categorized bookmarks)
- Kind 30004: Article Curation Sets
- Kind 30005: Video Curation Sets
- Kind 30006: Picture Curation Sets
- Kind 30007: Kind Mute Sets
- Kind 30015: Interest Sets
- Kind 39089: Starter Packs
- Kind 39092: Media Starter Packs
Reusable components in src/components/nostr/lists/:
- PubkeyListPreview/Full: Display pubkey lists with counts
- HashtagListPreview/Full: Display hashtag pills
- EventRefList: Display event/address references
- WordList: Display muted words
- UrlList: Display URL bookmarks
* Improve NIP-51 list rendering with clickable titles and consistent styling
- Add ClickableEventTitle to all list feed renderers for detail view navigation
- Change colored icons to text-muted-foreground for consistent muted appearance
- Update HashtagListPreview to use Label component with dotted border styling
- Update EventRefList detail view to embed events using EmbeddedEvent component
* Use Label component for muted words with destructive styling
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 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>
- Focus the message input when clicking reply button for immediate typing
- Use RichText component in reply preview to render mentions and emojis
Co-authored-by: Claude <noreply@anthropic.com>
- Add "Move to tab" submenu in window toolbar's more actions dropdown
- Shows nested submenu with available workspace tabs (number + label)
- After moving, automatically switches to the target workspace
- Only displays the more actions menu when multiple workspaces exist
- Preserves existing REQ-specific "Save as spell" action
Co-authored-by: Claude <noreply@anthropic.com>