mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 15:36:53 +02:00
459159faca22be5c9bc9784f4e495d50ec817b36
44 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7b7b24d41a |
feat: add client tag support to all event creation (#191)
* feat: add client tag support to all event creation
Implements a global settings system to control whether the Grimoire client tag
should be included in all published events. This allows users to opt-in or
opt-out of identifying their client in published events.
Changes:
- Created global settings service (src/services/settings.ts) with reactive
BehaviorSubject for app-wide configuration
- Created useSettings hook (src/hooks/useSettings.ts) for React components
- Migrated PostViewer from local settings to global settings system
- Added client tag support to:
- Post publishing (PostViewer.tsx)
- Spell publishing (publish-spell.ts)
- Event deletion (delete-event.ts)
- NIP-29 chat messages, reactions, join/leave, and group bookmarks
(nip-29-adapter.ts)
- Zap requests (create-zap-request.ts)
The client tag setting defaults to enabled (true) for backward compatibility.
Users can toggle this in the post composer settings dropdown.
All event creation locations now check settingsManager.getSetting("includeClientTag")
before adding the GRIMOIRE_CLIENT_TAG to event tags.
* refactor: exclude client tags from NIP-29 and zap requests
Remove client tag support from NIP-29 adapter events and zap requests
as these events may be rejected by servers with large tags or have
specific size constraints.
Changes:
- Removed client tag from NIP-29 chat messages (kind 9)
- Removed client tag from NIP-29 reactions (kind 7)
- Removed client tag from NIP-29 join/leave requests (kind 9021, 9022)
- Removed client tag from NIP-29 group bookmarks (kind 10009)
- Removed client tag from zap requests (kind 9734)
Client tags remain enabled for:
- Regular posts (kind 1)
- Spell publishing (kind 777)
- Event deletion (kind 5)
This ensures maximum compatibility with relay servers and LNURL endpoints
while still providing client identification for standard events.
* feat: implement comprehensive namespaced settings system
Redesigned the settings system with proper namespacing, type safety, validation,
and migration support. This provides a solid foundation for all app configuration.
Settings Structure:
- post: Post composition settings (client tag, relay selection)
- appearance: UI/theme settings (theme, compact mode, font size, animations)
- relay: Relay configuration (fallback, discovery, outbox, timeouts)
- privacy: Privacy settings (read receipts, content warnings, link warnings)
- database: Caching settings (max events, cleanup, IndexedDB options)
- notifications: Browser notifications preferences
- developer: Debug and experimental features
Key Features:
- Fully typed with TypeScript interfaces for each namespace
- Automatic validation with fallback to defaults for invalid data
- Migration system from old flat structure to namespaced structure
- Backwards compatible with old "grimoire-settings" localStorage key
- Import/export functionality for settings backup/restore
- Reactive updates via RxJS BehaviorSubject
- Section-level and individual setting updates
- Reset individual sections or all settings
Changes:
- Created comprehensive AppSettings interface with 7 namespaced sections
- Implemented SettingsManager class with reactive updates and persistence
- Updated useSettings hook to support namespaced API
- Updated PostViewer, publish-spell, and delete-event to use new API
(settingsManager.getSetting("post", "includeClientTag"))
- Added extensive inline documentation for all settings
Migration:
- Automatically migrates old includeClientTag setting to post.includeClientTag
- Moves data from "grimoire-settings" to "grimoire-settings-v2" key
- Validates all loaded settings and fills in defaults for missing values
This foundation will support future settings UI with tabbed interface.
* feat: add comprehensive settings UI with Post and Appearance sections
Created a minimal MVP settings system accessible via command palette and user menu.
Settings are organized in a clean tabbed interface with two initial sections.
UI Features:
- SettingsViewer component with sidebar navigation
- Post section: Toggle to include Grimoire client tag in published events
- Appearance section:
- Theme selector (light/dark/system)
- Toggle to show/hide client tags in event UI ("via Grimoire" etc)
Integration:
- Added "settings" command to command palette
- Added "Settings" option to user menu (before Support Grimoire)
- Registered "settings" as new AppId in window system
Display Logic:
- BaseEventRenderer now honors settings.appearance.showClientTags
- When disabled, "via Grimoire" and other client tags are hidden from events
- Setting applies instantly across all event renderers
Technical Details:
- SettingsViewer uses existing UI components (Checkbox, Button, Label)
- Leverages useSettings hook for reactive updates
- Settings persist to localStorage via settingsManager
- Simple button group for theme selection instead of dropdown
- Clean two-column layout with icons for each section
This provides a solid foundation for adding more settings sections later
(relay config, privacy, database, notifications, developer options).
* refactor: redesign settings UI with shadcn Tabs and cleaner layout
Simplified the settings UI to be more minimal and follow Grimoire patterns:
Changes:
- Use shadcn Tabs component instead of custom sidebar navigation
- Remove non-working theme selector (no theme system implemented yet)
- Use regular <label> elements with htmlFor instead of custom Label component
- Shorter, cleaner copy throughout
- Properly remove unused theme-related imports from user menu
- Follow Grimoire UI patterns (similar to SettingsDialog)
Settings sections:
- Post: Include client tag setting
- Appearance: Show client tags setting
The UI is now cleaner, uses proper form semantics, and matches the rest
of Grimoire's design system.
* feat: add working theme selector and improve settings UI
Added fully functional theme selector and improved the settings UI with
better components and cleaner copy.
Changes:
- Created Switch component (shadcn/radix-ui) for boolean settings
- Added working theme selector that integrates with existing theme system
- Uses useTheme hook to display available themes (Dark, Light, Plan9)
- Reordered sections: Appearance first, Post second
- Reordered settings: Theme first in Appearance section
- Replaced Checkbox with Switch for better UX on boolean toggles
- Simplified copy: "Add Grimoire tag to published events" instead of listing kinds
- Simplified copy: "Display client identifiers in events" instead of "via Grimoire" mention
- Better layout: Label/description on left, Switch on right
Settings now use proper form components:
- Switch for boolean toggles (include client tag, show client tags)
- Button group for theme selection
- Clean justify-between layout for settings rows
The theme selector works immediately - clicking Dark/Light/Plan9 applies
the theme instantly via the existing ThemeProvider context.
* refactor: improve settings UI with Select, icons, and better spacing
Enhanced the settings UI with shadcn Select component, tab icons, and
improved typography and spacing.
Changes:
- Created Select component (shadcn/radix-ui) for dropdowns
- Added icons to tabs: Palette for Appearance, FileEdit for Post
- Replaced button group theme selector with Select dropdown (w-48)
- Added gap-4 between labels and switches for better spacing
- Increased setting names from text-sm to text-base (bigger)
- Reduced help text from text-sm to text-xs (smaller)
- Added gap-3 between "Theme" label and Select
- Added font-medium to "Theme" label for consistency
Layout improvements:
- Theme selector now uses Select with 192px width
- All setting rows have gap-4 between content and controls
- Consistent text hierarchy: font-medium for labels, text-xs for descriptions
- Tab triggers have gap-2 between icon and text
The Select component is properly integrated with the theme system and
displays Dark, Light, and Plan9 options with checkmark indicators.
* refactor: use horizontal layout for theme selector
Aligned theme selector with other settings by using horizontal layout:
- Label and description on left, Select on right
- Added gap-4 for consistent spacing
- Changed label from text-sm to text-base font-medium
- Added helpful description: 'Choose your color scheme'
- Added id='theme' for accessibility
Now all settings follow the same visual pattern.
* refactor: reduce theme selector width for compact display
Changed theme selector from w-48 (192px) to w-32 (128px) since
theme names are short (Dark, Light, Plan9). More compact and
better proportioned for the content.
* fix: pass 'settings' command string when opening from menu
Changed the command string from 'Settings' to 'settings' when opening
the settings window from the user menu. This ensures clicking edit on
the settings window shows the correct command: 'settings'
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
b1fb569250 |
feat: user profile search for resolving usernames when pasting (#184)
* fix: show display names when pasting npub/nprofile in editors Previously, pasting npub/nprofile strings would only check the EventStore's in-memory cache for profiles. If a profile was cached in IndexedDB but not yet loaded into the EventStore, it would show a hex preview instead. This adds a ProfileCache service that: - Loads profiles from IndexedDB on startup for instant access - Subscribes to EventStore for new profiles as they arrive - Provides synchronous lookups for the paste handler Also uses consistent fallback format (XXXX:YYYY) when no profile is found. * refactor: make ProfileSearchService a singleton for shared profile lookups Instead of creating a separate ProfileCache service, refactored ProfileSearchService to be a singleton that: - Auto-initializes on module load - Loads profiles from IndexedDB for instant startup - Subscribes to EventStore for new profiles This allows both the paste handler and mention autocomplete to share the same profile cache, eliminating duplicate data and subscriptions. Removed the now-unnecessary profile-cache.ts. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
94982ca7f4 |
feat(post): add POST command with rich text editor and relay selection (#180)
* feat(post): add POST command with rich text editor and relay selection Phase 3: Create POST command for publishing kind 1 notes Features: - RichEditor component with @mentions, :emoji: autocomplete - Image/video upload via Blossom with drag-and-drop - Relay selection UI with write relays pre-selected by default - Per-relay publish status tracking (loading/success/error) - Submit with Ctrl/Cmd+Enter keyboard shortcut - Multi-line editing with rich previews for attachments - NIP-30 emoji tags and NIP-92 imeta tags for attachments Implementation: - Add 'post' to AppId type - Add POST command to man pages - Create PostViewer component using RichEditor - Wire PostViewer into WindowRenderer - Publish events using EventFactory and RelayPool - Track per-relay status with visual indicators Usage: - Run 'post' command to open the composer - Type content with @mentions and :emoji: - Upload media via button or drag-and-drop - Select/deselect relays before publishing - Press Publish button or Ctrl/Cmd+Enter to post - View per-relay publish status in real-time * feat(editor): add nostr bech32 paste handler with inline previews Implements paste handling for nostr: URIs (npub, note, nevent, naddr, nprofile) that transforms them into rich inline preview chips in the chat composer. Changes: - Add NostrEventPreview TipTap node type for displaying bech32 previews - Add NostrPasteHandler extension to detect and transform pasted bech32 strings - Update serializeContent to convert previews back to nostr: URIs on submit - Add CSS styling for preview chips with hover effects - Support all major bech32 types: npub, note, nevent, naddr, nprofile Features: - Automatic detection of nostr: URIs in pasted text (with or without prefix) - Visual preview chips with type icon, label, and truncated ID - Maintains nostr: URI format in final message content (NIP-27 compatible) - Simple implementation without event fetching (fast, no loading states) - Styled with primary theme colors for consistency Technical details: - Uses ProseMirror Plugin API for paste interception - Inline atomic nodes for previews (similar to mentions and blob attachments) - Regex pattern matches all valid bech32 formats - Proper error handling for invalid bech32 strings - Extensible foundation for future rich metadata fetching * refactor(editor): improve nostr preview display with kind icons Updates the nostr event preview chips to show more meaningful information: Changes: - Add getKindIcon() helper function with 15+ kind mappings - Display format: kind-icon + author/id (e.g., "📄 a1b2c3d4") - For naddr: show kind icon + author pubkey (first 8 chars) - For nevent: show kind icon + author if available, else event ID - For note: show 📝 + event ID (first 8 chars) - For npub/nprofile: show 👤 + pubkey (first 8 chars) - Removed verbose type labels (NPUB, NEVENT, etc.) - Increased label max-width to 120px for better readability Kind icon mappings: - 👤 Profile (kind 0) - 📝 Note (kind 1) - 👥 Contacts (kind 3) - 🔁 Repost (kind 6) - ❤️ Reaction (kind 7) - ⚡ Zap (kind 9735) - 📄 Long-form (kind 30023) - 🎙️ Live event (kind 30311) - 📦 File metadata (kind 1063) - 📌 Addressable events (30000-39999) - 🔄 Replaceable events (10000-19999) Result: More compact, visually intuitive previews that immediately convey the content type and author without requiring event fetching. * refactor(editor): simplify nostr preview display and reuse mentions Major simplification of the nostr bech32 preview display: Profile handling (npub/nprofile): - Now creates regular @mention nodes instead of custom preview nodes - Reuses existing mention infrastructure, styling, and UserName component - Displays as "@username" with existing mention chip styling - Serializes to nostr:npub1... on submit (same as manual @mentions) Event/Address display (note/nevent/naddr): - Removed emoji icons for cleaner, more minimal appearance - Display format: "event abc12345" for note/nevent - Display format: "address article-slug" for naddr (shows d identifier) - Falls back to short pubkey if naddr has no d identifier - Simple text-only chips with type + identifier Benefits: - Less visual noise (no emojis) - Consistent mention styling for all profiles - Profile mentions can now be clicked/hovered like manual mentions - Smaller code footprint (removed complex icon mapping logic) - Better UX: profiles look and behave like regular mentions Technical changes: - Paste handler creates mention nodes for npub/nprofile - NostrEventPreview only handles note/nevent/naddr now - Removed npub/nprofile from serialization (handled by mention serializer) - Updated type definitions to reflect reduced scope * feat(editor): add drag-and-drop and paste file upload support Adds comprehensive file upload support via drag-and-drop and paste directly into the chat composer editor. Changes to MentionEditor: - Add FileDropHandler TipTap extension to intercept file drops/pastes - Filter for valid media types (image/video/audio) - Add onFileDrop callback prop to communicate with parent - Handle both drag-and-drop and clipboard paste events Changes to useBlossomUpload hook: - Update open() method to accept optional File[] parameter - Add initialFiles state to track pre-selected files - Clear initialFiles when dialog closes - Pass initialFiles to BlossomUploadDialog component Changes to BlossomUploadDialog: - Add initialFiles prop for pre-selecting files - Auto-select first file when initialFiles provided - Generate preview URL for images/video on initialization - Seamless UX: dropped files immediately appear in dialog Changes to ChatViewer: - Wire up onFileDrop callback to open upload dialog - Pass dropped files to upload dialog via open(files) - Fix onClick handler to properly call openUpload() User experience: - Drag image/video/audio file onto chat composer → upload dialog opens - Paste image from clipboard → upload dialog opens - File automatically selected and previewed - Click upload to complete (same as manual file selection) Technical details: - Uses ProseMirror Plugin API for drop/paste interception - File type validation: /^(image|video|audio)\// - Bonus: Also works with clipboard paste (Ctrl+V) - Clean state management with automatic cleanup * fix(editor): add dragover handler to enable file drag-and-drop The drag-and-drop wasn't working because we need to handle dragover events and call preventDefault() to signal that drops are allowed. Changes: - Add handleDOMEvents with dragover handler to FileDropHandler - Check for Files type in dataTransfer - Set dropEffect to 'copy' for visual feedback - Call preventDefault() to allow drops Without this, the browser blocks all drops by default and handleDrop never fires. This is a standard requirement for HTML5 drag-and-drop API. * feat(editor): show usernames when pasting npub/nprofile mentions When pasting npub or nprofile URIs, the editor now looks up the profile from the event store cache and uses the display name if available. Changes: - Add getDisplayNameForPubkey helper function - Import eventStore and profile helpers - Synchronously check if profile is cached via .value property - Use getDisplayName from nostr-utils for consistent formatting - Falls back to short pubkey (first 8 chars) if profile not cached Result: - Paste npub1abc... → shows "@alice" if profile is cached - Paste npub1abc... → shows "@abc12345" if profile not cached - Consistent with typed @mentions behavior - No async delays or loading states needed * refactor(editor): remove drag-and-drop, keep paste file support Removes drag-and-drop file handling while keeping clipboard paste functionality. Drag-and-drop will be implemented differently later. Changes: - Rename FileDropHandler → FilePasteHandler - Remove handleDrop and dragover event handlers - Keep handlePaste for clipboard file support - Rename onFileDrop prop → onFilePaste for clarity - Update ChatViewer to use onFilePaste callback - Update all references and comments Result: - Paste images from clipboard (Ctrl+V) still works - Drag-and-drop removed completely - Clearer naming reflects actual functionality * refactor(editor): extract shared extensions and create React node views Phase 1 of RichEditor variant implementation: - Extract NostrPasteHandler extension for reuse across editors - Extract FilePasteHandler extension for clipboard file handling - Create NostrEventPreviewRich React node view using DetailKindRenderer - Create BlobAttachmentRich React node view with full-size media previews - Create rich TipTap node extensions using ReactNodeViewRenderer - Update MentionEditor to use shared extensions These shared components will be used by the upcoming RichEditor variant for long-form content composition with full previews. * feat(editor): add RichEditor component for long-form content Phase 2 of RichEditor variant implementation: - Create RichEditor component based on MentionEditor - No slash commands (removed SlashCommand extension) - Multi-line support with Enter for newlines - Ctrl/Cmd+Enter to submit - Full-size image/video previews using BlobAttachmentRich - Full event rendering using NostrEventPreviewRich - Configurable min/max height (defaults: 200px-600px) - Retains @mentions and :emoji: autocomplete - Reuses shared extensions (NostrPasteHandler, FilePasteHandler) - Targets kind 1 notes composition Key differences from MentionEditor: - Block-level rich previews instead of inline badges - Multi-line editing without Enter-to-submit - Resizable with overflow-y: auto - No command palette functionality * refactor(post): improve PostViewer layout and editor previews UI improvements: - Remove flex-1 from editor, use fixed min/max height - Move upload button next to publish button (icon-only) - Place relay selector below action buttons (not fixed) - Limit image/video previews to max-h-96 - Add pointer-events-none to event previews to prevent accidental interaction - Use EventDetailSkeleton for loading event previews Layout changes: - Single scrollable container with space-y-4 - Editor: 150-400px height range - Action buttons in single row (upload icon + publish button) - Relay list with max-h-64 scroll area - Better spacing and visual hierarchy * feat(post): add draft persistence and improve relay UI Draft persistence: - Save draft content to localStorage every 2 seconds - Load draft on component mount (per-user with pubkey) - Clear draft after successful publish - Prevent data loss on tab change or page reload UI improvements: - Reset button now icon-only (RotateCcw) with tooltip - Remove borders from relay list items - Use RelayLink component for relay rendering - Show relay icons and secure/insecure indicators - Cleaner, more compact relay list appearance - Increased spacing between relay items (space-y-1) Storage key format: grimoire-post-draft-{pubkey} * feat(post): enhance draft storage with full JSON state persistence Draft persistence improvements: - Store complete editor state as JSON (preserves blobs, emojis, mentions, formatting) - Save selected relays array in draft - Restore full editor content using setContent() method - Maintain draft per-user with pubkey-based key RichEditor enhancements: - Add getJSON() method to export editor state - Add setContent() method to restore from JSON - Enables lossless draft save/restore UI improvements: - Change "Relays" label to plain text without Label component - Keep unselected relays visible during/after publish - Only update status for selected relays during publish Draft storage format: { editorState: {...}, // Full TipTap JSON state selectedRelays: [...], // Array of selected relay URLs timestamp: 1234567890 } * feat(post): add nostr tag extraction, retry failed relays, and disable empty publish Nostr tag extraction: - Extract p tags from @mentions (pubkey references) - Extract e tags from note/nevent references (event ID references) - Extract a tags from naddr references (address/parameterized replaceable) - Update SerializedContent interface to include mentions, eventRefs, addressRefs - Serialize editor content walks all node types to extract references - Build complete tag array for kind 1 events with proper NIP compliance Retry failed relays: - Add retryRelay() function to republish to specific failed relay - Make error icon clickable with hover state - Show "Click to retry" in tooltip - Rebuild event and attempt publish again - Update status indicators in real-time Disable publish when empty: - Track isEditorEmpty state - Update every 2 seconds along with draft save - Disable publish button when editor isEmpty() - Prevents publishing empty notes Tag generation order: 1. p tags (mentions) 2. e tags (event references) 3. a tags (address references) 4. emoji tags (NIP-30) 5. imeta tags (NIP-92 blob attachments) This ensures proper Nostr event structure with all referenced pubkeys, events, and addresses tagged. * feat(post): add client tag and remove reset button Client tag: - Add ["client", "grimoire"] tag to all published events - Added to both handlePublish and retryRelay functions - Identifies posts as coming from Grimoire client UI cleanup: - Remove reset button from relay selection UI - Relay selection persists across sessions via draft storage - Simplify header to just show relay count - Remove unused RotateCcw icon import Tag generation order (final): 1. p tags (mentions) 2. e tags (event references) 3. a tags (address references) 4. client tag (grimoire) 5. emoji tags (NIP-30) 6. imeta tags (NIP-92) * fix(post): trim content before publishing Content trimming: - Trim content in handlePublish() before building event - Trim content in retryRelay() before building event - Removes leading and trailing whitespace from published notes - Prevents accidental whitespace-only or padded messages - Applied via content.trim() before factory.build() * fix(post): reuse signed event for retries instead of recreating Event reuse for retries: - Add lastPublishedEvent state to store the signed event - Store signed event after creation in handlePublish() - Simplify retryRelay() to republish the same signed event - Remove duplicate event creation and signing in retryRelay() - Remove duplicate client tag addition in retryRelay() Benefits: - Same event ID and signature across all relays - More efficient - no need to re-sign - Consistent event across retries - Single source of truth for published event Flow: 1. User publishes - event created, signed, stored 2. Some relays fail - event remains in state 3. User retries - same event republished to failed relay 4. New post overwrites lastPublishedEvent with new event * feat(post): add published event preview and reset button After successful publish, replace editor and action buttons with: - Kind1Renderer preview of the published event - Reset button to compose another post - Relay list remains visible with publish status indicators Users can click "Compose Another Post" to reset the form and return to the editor for the next post. * feat(post): improve layout and relay handling - Fix double scrollbar issue by using flexbox layout instead of overflow-auto - Disable relay checkboxes during preview mode (can't change after publish) - Use aggregator relays as fallback when no write relays configured - Remove conditional error message since fallback relays always available - Improve layout responsiveness with flex-1 and flex-shrink-0 classes Aggregator relays (nos.lol, relay.snort.social, relay.primal.net, relay.damus.io) ensure users can always publish even without configured write relays. * fix(editor): use feed renderer and skeleton for event previews When pasting event URLs in the editor, use KindRenderer (feed) and EventCardSkeleton instead of DetailKindRenderer and EventDetailSkeleton. Event previews in the editor are inline/compact like feed items, so they should use the feed renderer rather than the detail view renderer. * fix(post): remove flex-1 and improve scrolling layout - Change layout from flex-col with flex-1 to overflow-y-auto on outer container - Remove flex-1 from all child elements to prevent layout issues with complex posts - Hide scrollbar in RichEditor for cleaner appearance - Single scrollbar on container prevents double-scrollbar issues - Editor now truly full-width without extra padding This creates a more predictable layout that avoids overlaps and weird behavior when posts contain lots of content (images, videos, embedded events). * feat(post): add max-width constraint to content Add max-w-2xl (768px) with mx-auto to center and constrain post content width, similar to ZapWindow. Prevents content from becoming too wide on large screens while maintaining good readability. * fix(editor): add remove button and fix cursor positioning for event embeds - Add X button on hover to NostrEventPreviewRich (same pattern as BlobAttachmentRich) - Fix cursor positioning after pasting nevent/naddr by tracking node sizes correctly - Import TextSelection from prosemirror-state to properly set cursor position - Change from `tr.insert(from + index, node)` to `tr.insert(insertPos, node)` with cumulative position tracking Previously, cursor would stay behind the event embed node after pasting. Now it correctly positions after the inserted content. * feat(post): add discard button, settings dropdown, and improve button layout - Add Discard button to clear editor state and draft - Add Settings dropdown with client tag toggle (enabled by default) - Limit publish button width to w-32 and push to right with spacer - Conditionally include client tag based on setting - Improve action button layout with better spacing * feat(post): add relay connectivity status, input widget, and improve error handling - Show relay connectivity status (Server/ServerOff icons) next to each relay in list - Add input widget to add custom relays during post composition - Validates relay URLs (supports with/without wss:// prefix) - Normalizes URLs automatically (defaults to wss://) - Shows Plus button enabled only when input is valid - Press Enter or click Plus to add relay - Improve signing failure handling: draft is only cleared after successful publish - New relay input widget disabled during preview mode - Connect to relay pool state using use$() hook for reactive connectivity status Technical details: - Import Server, ServerOff, Plus icons and Input component - Add newRelayInput state for relay URL input - Use pool.relays$ observable to get real-time connection state - Create isValidRelayInput() validator with URL pattern matching - Create handleAddRelay() to normalize and add relays to list - Update relay list rendering to show connectivity icons - Add input + button widget after relay list * feat(post): add auth status icons and window-specific draft persistence - Show auth status icon next to connectivity icon for each relay - Uses getAuthIcon() utility from relay-status-utils - Displays shield icons for authenticated/failed/rejected states - Includes tooltip with auth status label - Make draft persistence window-specific using window ID - Draft key format: `grimoire-post-draft-{pubkey}-{windowId}` - Each post window maintains its own independent draft - Falls back to pubkey-only key when windowId not available - Remove toast notification when adding relays (less noisy UX) - Still shows error toast if relay URL is invalid - Pass windowId prop from WindowRenderer to PostViewer Technical details: - Import getAuthIcon from @/lib/relay-status-utils - Import useRelayState hook to get relay auth status - Add PostViewerProps interface with optional windowId - Update all draft key computations to include windowId - Update dependency arrays for useEffect/useCallback with windowId - Get relay state via getRelay(url) for auth icon display * feat(post): add global persistent settings and event JSON preview Settings (persist in localStorage, global across all post windows): - "Include client tag" - toggle whether to add client tag to events (default: true) - "Show event JSON" - display copyable JSON preview of event (default: false) Features: - Settings stored in localStorage at "grimoire-post-settings" - Settings persist across sessions and windows - Event JSON preview shows unsigned event initially, updates to signed version after signing - Preview displays "(Signed)" or "(Unsigned)" label - Uses CopyableJsonViewer component for syntax highlighting and copy button - Preview limited to max-h-64 with scrolling Draft persistence improvements: - Added relays now saved in draft state under "addedRelays" field - On draft load, custom relays are restored to relay list - Relays identified as "added" if not in user's write relay list Technical details: - Added PostSettings interface with includeClientTag and showEventJson fields - Settings loaded on mount from localStorage, saved on change via useEffect - updateSetting callback handles individual setting updates - previewEvent state holds current event (unsigned or signed) - setPreviewEvent called before and after signing in handlePublish - Draft format: { editorState, selectedRelays, addedRelays, timestamp } - Draft loading checks addedRelays and restores non-duplicate relays * feat(post): add live draft JSON preview with 2-second updates This commit enhances the POST command with a live JSON preview feature: - Added generateDraftEventJson() that builds unsigned draft event from editor content - JSON preview updates every 2 seconds when "Show event JSON" setting is enabled - Extracts all tags from editor: mentions (p), events (e), addresses (a), emojis, blobs (imeta) - Displays as "Event JSON (Draft - Unsigned)" to distinguish from signed events - Uses CopyableJsonViewer component for syntax highlighting and copying The live preview helps users understand event structure before publishing. * feat(post): improve JSON preview responsiveness and layout This commit addresses UX feedback on the JSON preview feature: - Reduced update interval from 2000ms to 200ms for much more responsive UI - Moved JSON preview below relay list for better visual flow - Increased max-height from 256px to 400px for better visibility - Separated JSON update logic into dedicated effect for cleaner code - JSON preview now feels instant when typing (10x faster updates) The JSON preview is now much more pleasant to use and doesn't feel laggy when editing content. * refactor(post): replace intervals with debounced onChange handlers This commit improves the code quality and UX of the POST command: **Debounced onChange Pattern:** - Added onChange prop to RichEditor that fires on every content change - Replaced interval-based updates with proper debounced handlers - Draft saves debounced to 2000ms (unchanged behavior, cleaner code) - JSON updates debounced to 200ms (same responsiveness, event-driven) - Much cleaner React pattern - no weird intervals **JSON Preview Improvements:** - Removed "Event JSON" title header for cleaner look - Shows signed event JSON when published and setting is enabled - Shows draft (unsigned) JSON when composing - Always scrollable up to 400px height **Technical Improvements:** - Added onUpdate handler to TipTap editor config - Proper cleanup of debounce timeouts on unmount - Type-safe timeout refs using ReturnType<typeof setTimeout> * refactor(post): remove JSON preview and ensure proper content serialization **Removed JSON preview feature:** - Removed showEventJson setting and all related UI - Removed draftEventJson state and generation logic - Removed "Show event JSON" checkbox from settings - Simplified PostViewer code by ~100 lines - Cleaner, production-ready codebase **Ensured proper content serialization:** - Added renderText() to Mention extension to serialize as nostr:npub URIs - Added renderText() to BlobAttachmentRichNode to serialize URLs - NostrEventPreviewRichNode already had renderText() for nostr:note/nevent/naddr - Editor now properly converts all rich content to plain text: - @mentions → nostr:npub... - Event references → nostr:note/nevent... - Address references → nostr:naddr... - Blob attachments → URLs - Custom emojis → :shortcode: **Result:** - Cleaner, simplified code ready for production - All editor elements properly serialize to content string - JSON preview can be re-added later with better implementation * perf(post): reduce draft save debounce from 2s to 500ms Makes the draft saving feel more responsive without hammering localStorage on every keystroke. * fix(post): preserve content when signing fails or is rejected Critical bug fix: Separate signing from publishing to handle failures correctly. **The Problem:** - When user rejected signing (or signing failed), we treated it as a publishing failure - Set all relay states to "error" even though we never tried to publish - Destroyed the user's post content **The Solution:** - Split signing and publishing into separate try-catch blocks - If signing fails: show toast, reset isPublishing, and RETURN EARLY - User keeps their content and can try again - Only update relay states if we actually attempted to publish **User Experience:** - User rejects signing → toast appears, post is preserved, can edit or try again - Publishing fails → relay states show errors, can retry individual relays - No more losing your carefully crafted post to a rejected signature! * refactor(post): use Promise.allSettled for relay publishing Use Promise.allSettled instead of Promise.all to wait for all relay publish attempts to complete, regardless of individual success/failure. More defensive and semantically correct since we're handling errors individually per relay and want all attempts to finish. * feat(post): use GRIMOIRE_CLIENT_TAG constant with NIP-89 app address Update PostViewer to use the new GRIMOIRE_CLIENT_TAG constant from main which includes the NIP-89 app definition address (31990) as the third element. Before: ["client", "grimoire"] After: ["client", "grimoire", "31990:..."] This allows Nostr clients to display a clickable link to Grimoire's app definition event. * fix(post): preserve content when publishing fails to all relays Critical UX improvement: Only clear draft and show preview if at least one relay succeeded. **The Problem:** - When publishing failed to ALL relays, we still cleared the draft - User lost their content with no way to retry - Frustrating experience when all relays are down **The Solution:** - Track success/failure by returning result from each publish promise - Count successful publishes from Promise.allSettled results - Only clear draft and show preview if successCount > 0 - Keep editor visible with content if all relays fail **User Experience:** - All relays succeed → Clear draft, show preview, success toast - Some relays succeed → Clear draft, show preview, warning toast - All relays fail → Keep editor with content, error toast, can retry **Toast Messages:** - All succeeded: "Published to all X relays" - Partial success: "Published to X of Y relays" - Total failure: "Failed to publish to any relay. Please check..." * fix(post): preserve content when signing or publishing Critical fix: Don't clear editor content until publish succeeds. **The Problem:** - RichEditor automatically cleared content on submit - Content was wiped immediately when clicking "Publish" - User lost their content before knowing if publish succeeded **The Solution:** - Remove automatic `clearContent()` from RichEditor's handleSubmit - Let PostViewer control when to clear the editor - Only clear editor after successful publish (successCount > 0) **User Experience Now:** - Click "Publish" → content stays visible while publishing - Publishing succeeds → content clears, show preview - Publishing fails → content stays, user can edit and retry - User rejects signing → content stays, can try again No more losing your post to a rejected signature or failed publish! --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
13fec0345f |
feat: zap goals nip-75 (#174)
* feat: add NIP-75 Zap Goal rendering Add feed and detail view rendering for kind 9041 (Zap Goals): - GoalRenderer: Shows clickable title, description, and progress bar with target/raised amounts - GoalDetailRenderer: Adds sorted contributor breakdown with individual contribution totals - nip75-helpers: Helper functions for extracting goal metadata (amount, relays, deadline, beneficiaries) Both views fetch and tally zaps from the goal's specified relays. * fix: improve NIP-75 goal rendering - Remove icons from goal renderers - Remove "sats" suffixes from amounts - Use muted text instead of destructive color for closed goals - Content is the title, summary tag is the description - Only show description if summary tag exists * feat: add zap button to goal renderers Show a 'Zap this Goal' button in both feed and detail views when the goal is still open (not past its closed_at deadline). * style: unify progress indicator styles - Update user menu and welcome page progress indicators to match goal renderer style: bar on top, progress/total below with percentage - Remove "sats" suffix from progress displays - Make goal zap button primary variant and full-width * fix: polish goal renderers for release - Remove limit parameter from useTimeline (use whatever relays send) - Add more spacing between "Support Grimoire" header and progress bar * refactor: extract useGoalProgress hook for NIP-75 goals - Create useGoalProgress hook with shared goal logic - Handle relay selection: goal relays → user inbox → aggregators - Calculate progress, contributors, and all metadata in one place - Simplify both GoalRenderer and GoalDetailRenderer --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
ab64fc75f4 |
Restrict relay auth to account owner (#149)
* fix: only prompt relay auth for accounts that can sign - Add canAccountSign() helper to check if account is read-only - Block auth prompts for read-only accounts in shouldPromptAuth() - Throw error when authenticateRelay() called with read-only account - Document all major app hooks in CLAUDE.md for future reference Read-only accounts cannot sign events, so they should never be prompted for relay authentication or attempt to authenticate. This prevents confusing UX where users are asked to sign but cannot. * refactor: extract canAccountSign helper to useAccount - Move canAccountSign function from relay-state-manager to useAccount.ts - Import and reuse the shared helper in relay-state-manager - Update useAccount hook to use the extracted helper internally - Follows DRY principle by centralizing account sign capability logic This keeps the account sign capability detection logic in one place, making it easier to maintain and ensuring consistency across the app. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
3408872811 |
feat: add comprehensive NWC wallet viewer with dynamic UI (#135)
* feat: add comprehensive NWC wallet viewer with dynamic UI Implements a full-featured Lightning wallet interface using Nostr Wallet Connect (NWC/NIP-47) with method-based UI that adapts to wallet capabilities. **New Features:** - WalletViewer component with tabbed interface (Overview, Send, Receive, Transactions) - Real-time balance display with manual refresh - Send Lightning payments via BOLT11 invoices - Generate invoices with QR codes for receiving payments - Transaction history viewer (when supported by wallet) - Wallet info and capabilities display - Enhanced useWallet hook with additional NWC methods **Enhanced Methods:** - listTransactions() - View recent payment history - lookupInvoice() - Check invoice status by payment hash - payKeysend() - Direct keysend payments to node pubkeys **UI Features:** - Dynamic tabs based on wallet capabilities - QR code generation for invoices - Copy-to-clipboard for invoices - Error handling with user-friendly messages - Loading states for async operations - Empty states for no wallet connection **Command:** - New `wallet` command to open the wallet viewer **Technical Details:** - Integrates with existing NWC service singleton - Uses reactive balance$ observable for auto-updates - Proper TypeScript types aligned with applesauce-wallet-connect - Follows Grimoire patterns for window system integration - Lazy-loaded component for optimal bundle size All tests passing. Build verified. * refactor: redesign wallet UI to single-view layout with virtualized transactions Converts the tabbed wallet interface to a conventional single-view layout with improved UX and performance optimizations. **Layout Changes:** - Removed tabs in favor of single-page layout - Balance header at top with wallet name and refresh button - Side-by-side Send/Receive cards for quick access - Transaction history below with virtualized scrolling - Disconnect button at bottom of page **New Features:** - Connect Wallet button when no wallet is connected (opens dialog in-app) - Wallet capabilities shown in tooltip on info icon - Virtualized transaction list using react-virtuoso - Batched transaction loading (20 per batch) - Automatic "load more" when scrolling to bottom - Loading states for initial load and pagination - "No more transactions" message when exhausted **Performance Improvements:** - Virtualized list rendering for smooth scrolling with many transactions - Only renders visible transactions in viewport - Lazy loads additional batches on demand - Reduced initial load to 20 transactions instead of 50 **UX Improvements:** - More conventional wallet UI pattern - Send/Receive always visible (no tab switching) - QR code and invoice appear inline when generated - Info icon with tooltip for capabilities (cleaner than full card) - Disconnect option always accessible at bottom **Technical Details:** - Fixed transaction loading race condition with separate useEffect - Proper dependency tracking for loadMoreTransactions callback - Footer component in Virtuoso for loading/end states - Responsive grid layout for Send/Receive cards All tests passing. Build verified. * refactor: redesign wallet UI with ReqViewer-style header and dialogs Implements a cleaner, more consistent wallet UI matching Grimoire patterns. **Header Changes:** - ReqViewer-style compact header (border-b, px-4, py-2, font-mono, text-xs) - Left side: Wallet name · Balance with info tooltip - Right side: Icon-only action buttons (Receive, Send, Refresh) - All actions use tooltips for labels - Consistent spacing and typography with rest of app **Send/Receive as Dialogs:** - Removed always-visible Send/Receive cards - Converted to modal dialogs opened from header icons - Send dialog: Invoice input + optional amount override - Receive dialog: Two-step flow (generate → show QR + invoice) - "Generate Another" button to create multiple invoices without closing - Cleaner main view focused on transaction list **Transaction List Cleanup:** - Removed Card wrapper and extra padding - Direct virtualized list fills viewport - Removed "Transaction History" heading (redundant) - Added flex-shrink-0 to prevent transaction items from shrinking - Improved truncation handling for long descriptions - Hover effect for better interactivity - Footer messages use consistent border-b styling **Disconnect Button:** - Changed to destructive variant (red appearance) - Moved to footer with border-t - Full width for emphasis - Clearer visual indication of destructive action **UX Improvements:** - More screen space for transactions (primary content) - Consistent icon sizing (size-3 for header actions, size-4 for tx icons) - Better visual hierarchy with header actions - Dialog pattern matches other Grimoire components - Receive dialog stays open to show generated invoice **Technical Details:** - Proper dialog state management - Invoice QR with max-height scrolling for long invoices - Reset form state when closing dialogs - Maintained all existing functionality - Optimized for virtualized scrolling performance All tests passing. Build verified. * feat: final wallet UI improvements with prominent balance and enhanced UX Redesigns wallet viewer with cleaner layout and improved user experience. **Layout Changes:** - Big centered balance display (4xl font, prominent) - Large Send/Receive buttons below balance (side by side) - Single-line transaction items with better spacing - Info dropdown next to refresh button in header **Transaction List:** - Single-line compact design (description + time + amount) - No +/- signs on amounts (cleaner look) - Generic "Payment"/"Received" labels when description missing - Time displayed in compact format (HH:MM) - Day markers between days (Today/Yesterday/Jan 15) - Virtualized scrolling with batched loading **Info Dropdown:** - Wallet capabilities shown in dropdown (Info icon + ChevronDown) - Network information - Methods displayed as compact badges - Notifications support **User Menu Integration:** - Wallet option always visible (regardless of account status) - Clicking wallet opens wallet window (not info dialog) - Balance shown inline when connected - "Connect Wallet" option when not connected **Dialog Improvements:** - Send dialog with confirmation step - Receive dialog with payment detection - Auto-close on payment received - QR code with loading overlay during payment check **Visual Hierarchy:** - Header: Wallet name (left) | Info dropdown + Refresh (right) - Big centered balance with "sats" label - Prominent action buttons (Send default, Receive outline) - Clean transaction list with hover states - Destructive disconnect button in footer All tests passing ✅ Build verified ✅ * fix: replace AlertDialog with Dialog for disconnect confirmation - AlertDialog component doesn't exist in UI library - Use regular Dialog with custom footer buttons instead - All 929 tests passing, build successful * refine: wallet UI improvements based on feedback - Remove "sats" text from balance display - Swap send/receive button positions (receive left, send right) - Remove top border from transaction list - Remove timestamps from transaction list items - Add relay link to wallet info dropdown with external link icon - Change disconnect button to destructive color (always red) - Fix imports and remove unused formatTime function * feat: enhance send/receive flows with invoice parsing and auto-confirm Send flow improvements: - Parse BOLT11 invoices using light-bolt11-decoder - Auto-proceed to confirm step when valid invoice is entered - Show parsed amount and description in confirmation dialog - Validate invoice before allowing confirmation Receive flow improvements: - Fix invoice overflow with proper truncate display - Use nested div structure for single-line truncation All changes preserve type safety with proper Section type guards * feat: add Lightning address support and refine auto-confirm behavior Send flow enhancements: - Only auto-proceed to confirm if invoice has an amount (not for zero-amount invoices) - Add Lightning address (LNURL-pay) support with automatic resolution - Fetch invoice from Lightning address with amount validation - Show "Resolving..." loading state when processing Lightning addresses - Update UI labels and placeholders to indicate Lightning address support - Require amount field for Lightning address payments Lightning address flow: 1. Detect @ symbol in input (and not starting with "ln") 2. Validate amount is provided 3. Fetch LNURL-pay endpoint from .well-known/lnurlp/{username} 4. Check min/max sendable amounts 5. Request invoice from callback with specified amount 6. Parse and confirm invoice details Error handling: - Invalid Lightning address format - Failed to fetch Lightning address - Amount out of range (too small/too large) - Failed to generate invoice from callback * fix: UI improvements for wallet viewer and mobile receive flow Confirmation dialog improvements: - Remove yellow warning styles (border, background, icon) - Fix amount calculation: show proper sats conversion with Math.floor - Clean layout with key-value pairs instead of cluttered text - Show either invoice amount OR override amount (not both) Mobile receive flow improvements: - Large prominent "Copy Invoice" button (h-12, full width) - Invoice box now tappable to copy - Better touch targets for mobile users - Clearer label: "Invoice (tap to view)" Connection status: - Show green dot + "Connected" next to wallet name in header - Clear visual feedback that wallet is online Auto-launch wallet: - After successfully connecting wallet, automatically open wallet window - Added onConnected callback to ConnectWalletDialog - Wire up callback in user menu to call openWallet Code cleanup: - Remove unused AlertTriangle import * fix: correct amount handling and mobile invoice display Amount input fixes: - Changed from millisats to sats for user input - Updated label: "Amount (sats, optional)" - Fixed Lightning address resolution: no division, amount is already in sats - Fixed payInvoice: multiply by 1000 to convert sats to millisats for NWC - Fixed confirmation dialog: removed incorrect division by 1000 Before: User enters 1000 (meant as sats) → system treats as 1000000 millisats → sends 1000 sats After: User enters 1000 sats → system converts to 1000000 millisats → sends 1000 sats ✓ Invoice parsing was correct - already converting millisats to sats. The bug was in the confirmation display and amount submission. Mobile receive invoice fix: - Removed nested div with truncate (was causing overflow) - Changed to break-all + line-clamp-2 - Invoice now wraps properly on mobile (2 lines max) - Still tappable to copy * fix: convert sats to millisats when generating invoices Critical bug fix - we were generating nearly amountless invoices. Before: - User enters 1000 sats in UI - We passed 1000 to makeInvoice (thinking it's millisats) - Generated invoice for 1000 millisats = 1 sat After: - User enters 1000 sats in UI - We multiply by 1000 → 1,000,000 millisats - Generated invoice for 1,000,000 millisats = 1000 sats ✓ This matches the send flow where we also convert sats to millisats before passing to NWC protocol. * refactor: production-readiness improvements for WalletViewer Critical fixes: - Wrap loadWalletInfo and loadInitialTransactions in useCallback to prevent infinite loops - Fix dependency arrays in useEffect hooks - Fix early return bug in handleConfirmSend that left sending state stuck - Add async/await error handling to handleCopyInvoice - Change throw new Error instead of early return in Lightning address resolution Performance optimizations: - Removed duplicate function definitions (moved to useCallback hooks) - Proper memoization of wallet loading functions Code quality: - All functions properly handle errors with try/catch - Loading states properly reset in finally blocks - No memory leaks from useEffect dependencies * fix: prevent infinite transaction loading loop in WalletViewer Previously, the transaction list would try to load constantly and fail repeatedly due to a circular dependency in the useEffect hooks. The listTransactions function from useWallet wasn't wrapped in useCallback, causing loadInitialTransactions to be recreated on every render, which triggered the useEffect infinitely. Changes: - Add txLoadAttempted and txLoadFailed flags to prevent repeated attempts - Only attempt to load transactions once on wallet connection - Fail silently on initial load (no toast spam) - Show retry button when transaction loading fails - Reset flags when wallet connects/disconnects or after successful payments - Make transaction list truly optional - wallet still works if loading fails This ensures a better UX when wallets don't support list_transactions or when the method fails for any reason. * fix: remove circular dependency in loadInitialTransactions Removed txLoadFailed from the dependency array of loadInitialTransactions callback, which was causing a circular dependency: - loadInitialTransactions depended on txLoadFailed - Function sets txLoadFailed, triggering recreation - New function reference triggers useEffect again - Infinite loop The txLoadAttempted flag in the useEffect is sufficient to prevent repeated loads. No need to check txLoadFailed inside the callback. * fix: resolve React error #185 by removing callbacks from useEffect dependencies React error #185 occurs when state updates happen during render due to unstable function references in useEffect dependencies. The root cause was that functions from useWallet (getInfo, listTransactions) aren't memoized, so they create new references on every render, causing the useEffects that depend on them to run repeatedly. Changes: - Removed loadWalletInfo and loadInitialTransactions callbacks - Use refs (walletInfoLoadedRef, lastConnectionStateRef) to track state - Call async functions directly in useEffect without depending on them - Created reloadTransactions() helper that just resets flags - Simplified all reload logic to use the helper This ensures: - No circular dependencies in useEffect - Functions only run once when conditions are met - No state updates during render - Clean, predictable loading behavior * fix: properly clear wallet state on disconnect and update copy Changes: - Clear all wallet state when disconnecting (transactions, walletInfo, loading flags) - Previously only cleared state on connect, leaving stale data visible - Remove "Mutiny" mention from connection dialog copy - Update to generic "NWC wallet provider" text Now when you disconnect the wallet with the window open, it properly clears all data and returns to the "No Wallet Connected" state. * fix: prevent wallet from auto-reconnecting after disconnect The bug was in the useWallet hook which automatically restores the wallet if nwcConnection exists in state but the wallet instance is null. When disconnecting, it only cleared the wallet instance but left nwcConnection in state, causing an immediate reconnection. Fix: - Call disconnectNWCFromState() to clear nwcConnection from Grimoire state - Then call disconnect() to clear the wallet service - This prevents the auto-restore logic from triggering Now when you disconnect the wallet, it stays disconnected until you manually reconnect. * security: add critical production-ready security fixes Invoice Validation & Expiry Checks: - Validate BOLT11 invoice format (must start with 'ln') - Check invoice expiry before displaying/processing - Validate amount is reasonable (< 21M BTC) - Surface parse errors to user with toast notifications - Prevent processing of expired invoices Lightning Address Security: - Enforce HTTPS-only for LNURL-pay requests - Add 5-second timeout to all HTTP requests - Validate callback URLs use HTTPS - Proper AbortController cleanup on timeout - Better error messages for network failures Rate Limiting: - Balance refresh: minimum 2 seconds between calls - Transaction reload: minimum 5 seconds between reloads - User-friendly warning messages with countdown - Prevents spam to wallet service providers Storage Security Warning: - Add prominent security notice in ConnectWalletDialog - Warn users about browser storage implications - Advise to only connect on trusted devices Capability Detection: - Hide Send button if wallet doesn't support pay_invoice - Hide Receive button if wallet doesn't support make_invoice - Dynamic button rendering based on wallet capabilities - Prevents errors from unsupported operations Error Handling: - WindowErrorBoundary already wraps all windows (verified) - Proper error propagation with user-friendly messages - No silent failures on critical operations These changes significantly improve security and production-readiness without breaking existing functionality. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
7fae344dd9 |
feat: add Nostr Wallet Connect (NWC) integration (#131)
* feat: add Nostr Wallet Connect (NWC) integration
Add NWC (NIP-47) support to connect Lightning wallets:
- Add NWCConnection type and state management
- Implement custom NWC client service for wallet communication
- Create ConnectWalletDialog for entering connection strings
- Add wallet button to user menu showing balance
- Display wallet info (balance, alias) in user menu dropdown
- Support get_info, get_balance, pay_invoice, make_invoice commands
- Persist wallet connection to localStorage
- Install applesauce-wallet (for future NIP-60 support)
Note: Current implementation uses custom NWC client. Can optionally
switch to @getalby/sdk for official Alby SDK support.
* refactor: use applesauce-wallet-connect and move wallet to header
Replace custom NWC client implementation with applesauce-wallet-connect:
- Install applesauce-wallet-connect for official NIP-47 support
- Create nwc.ts service wrapper for WalletConnect singleton
- Update NWCConnection type to match WalletConnectURI interface
- Use service/relays/secret properties instead of custom names
Move wallet display from user menu to header:
- Create standalone WalletButton component
- Add WalletButton to header next to UserMenu
- Remove wallet UI from user menu dropdown
- Show balance in header with yellow zap icon
- Clicking wallet button opens connect dialog
This provides better UX with wallet status visible in header
and uses the official applesauce implementation for reliability.
* feat: improve wallet button UX and add detailed info dialog
UI improvements:
- Remove border and padding from wallet button (use ghost variant)
- Remove "sats" suffix from balance display (show just the number)
- Change click behavior to show detailed wallet info dialog
Wallet info dialog:
- Show balance prominently without suffix
- Display wallet alias if available
- Show lightning address (lud16) if present
- List all supported NWC methods as badges
- Display connected relay URLs
- Add disconnect button with confirmation toast
This provides a cleaner header appearance and better wallet
management with all details accessible in one place.
* refactor: move wallet functionality to user menu
Move wallet connection and info from separate header button into
the user menu dropdown for better organization.
Changes:
- Remove standalone WalletButton component
- Add wallet section to user menu dropdown
- Show "Connect Wallet" option when no wallet is connected
- Display wallet balance and alias when connected
- Clicking wallet info opens detailed dialog with:
- Balance (without suffix)
- Wallet name/alias
- Lightning address (lud16)
- Supported NWC methods
- Connected relay URLs
- Disconnect button
This consolidates all user-related settings (account, relays,
blossom servers, wallet) in one consistent location.
* feat: improve wallet UX with profile-based naming and better layout
UX improvements:
- Use service pubkey username/profile as wallet name instead of alias
- Remove format hint text from connection string input
- Move wallet to single row directly below user name (remove label)
- Use RelayLink component for relay URLs in wallet info dialog
- Show wallet name from service profile with fallback to alias/pubkey
This provides better integration with Nostr profiles and a cleaner,
more compact menu layout. The wallet service provider's identity is
now shown using their actual Nostr profile name.
* feat: add persistent wallet service with auto-updating balance
Implement comprehensive NWC wallet management architecture:
**Service Layer** (`src/services/nwc.ts`):
- Maintain singleton WalletConnect instance across app lifetime
- Poll balance every 30 seconds for automatic updates
- Subscribe to NIP-47 notifications (kind 23197) for real-time updates
- Expose RxJS observable (balance$) for reactive balance changes
- Restore wallet from saved connection on app startup
- Proper cleanup on disconnect
**Hook Layer** (`src/hooks/useWallet.ts`):
- useWallet() hook exposes wallet throughout the app
- Provides methods: payInvoice, makeInvoice, getBalance, etc.
- Auto-syncs balance updates with Jotai state
- Handles wallet restoration on mount
- Type-safe wrapper around WalletConnect API
**UI Updates**:
- Add refresh button to wallet info dialog
- Auto-update balance display when transactions occur
- Proper cleanup on wallet disconnect
**Architecture**:
```
User Action → useWallet() → NWC Service → WalletConnect
↓ ↓
Update State ← balance$ observable ← Polling/Notifications
```
This enables:
- Real-time balance updates when paying/receiving
- Easy wallet access: `const { payInvoice, balance } = useWallet()`
- Persistent connection across page reloads
- Automatic polling as fallback for notifications
* refactor: make NWC fully reactive with notifications and graceful balance handling
Changes:
- Remove polling mechanism in favor of pure reactive notifications$ observable
- Subscribe to wallet.notifications$ for real-time balance updates
- Make balance display conditional (only show if available)
- Fix TypeScript errors (notification.type access, unused variable)
- Remove Jotai callback mechanism for balance updates
- Use use$() directly for reactive balance subscription
- Update comments to reflect reactive architecture (no polling)
The wallet now updates balance automatically when payments are sent/received
via NIP-47 notifications, with no polling overhead.
* feat: improve wallet UX with profile-based naming and better layout
Improvements to NWC wallet UI:
- Add separator between user info and wallet section in menu
- Show wallet icon instead of zap icon for better clarity
- Display connection status indicator (green/red dot) in both menu and dialog
- Make wallet service username clickable in wallet info dialog to open profile
- Use wallet relays as hints when fetching service profile for better resolution
- Enhanced useProfile hook to accept optional relay hints parameter
The wallet now properly resolves service profiles using the NWC relay
and shows visual connection status at a glance.
* fix: remove toast descriptions for better contrast
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
ee2b62f2d6 |
feat: enhance login options with read-only and nsec support (#126)
* feat: enhance login options with read-only and nsec support - Add read-only login mode supporting: - npub (bech32 public key) - nprofile (bech32 profile with relay hints) - hex public key - NIP-05 addresses (user@domain.com) - Add private key (nsec) login with security warning - Supports nsec1... format - Supports 64-char hex private key - Shows prominent security warning about localStorage storage - Reorganize user menu to show login before theme option - Use ReadonlyAccount from applesauce-accounts for read-only mode - Use PrivateKeyAccount from applesauce-accounts for nsec login - Update LoginDialog with 4 tabs: Extension, Read-Only, Private Key, Remote - All account types properly registered via registerCommonAccountTypes() Technical notes: - ReadonlySigner throws errors on sign/encrypt operations - Existing components naturally handle accounts without signing capability - Hub/ActionRunner already syncs with account signers automatically * feat: add generate identity button to login dialog - Add "Generate Identity" button above login tabs - Uses Wand2 icon from lucide-react - Creates new key pair using PrivateKeyAccount.generateNew() - Automatically stores nsec in localStorage and sets as active account - Provides quick onboarding for new users without external wallet setup * feat: add useAccount hook for signing capability detection Created a centralized hook to check account signing capabilities and refactored components to distinguish between signing and read-only operations. New hook (src/hooks/useAccount.ts): - Returns account, pubkey, canSign, signer, isLoggedIn - Detects ReadonlyAccount vs signing accounts - Provides clear API for checking signing capability Refactored components: - ChatViewer: Use canSign for message composer, replying, actions - Show "Sign in to send messages" for read-only accounts - Disable message input for accounts without signing - SpellDialog: Use canSign for publishing spells - Show clear warning for read-only accounts - Updated error messages to mention read-only limitation - useEmojiSearch: Use pubkey for loading custom emoji lists - Works correctly with both signing and read-only accounts Benefits: - Clear separation between read (pubkey) and write (canSign, signer) operations - Read-only accounts can browse, view profiles, load data - Signing operations properly disabled for read-only accounts - Consistent pattern across the codebase for account checks - Better UX with specific messages about account capabilities --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
16764e1aca |
Display user's blossom servers in menu (#90)
* feat: Display blossom servers in user menu with caching Implements caching and display of user's blossom server lists (kind 10063) in the user menu dropdown. **Key Changes:** 1. **Database Schema (db.ts)**: - Added `CachedBlossomServerList` interface - Added `blossomServers` table to Dexie v15 - Indexed by pubkey and updatedAt for efficient querying 2. **Caching Service (blossom-server-cache.ts)**: - Dual-layer caching: LRU memory cache (100 entries) + Dexie persistent cache - 24-hour TTL to reduce network requests - Auto-caches kind:10063 events from EventStore - Mirrors relay-list-cache pattern for consistency 3. **Sync Hooks**: - `useBlossomServerCacheSync`: Subscribes to EventStore for auto-caching kind:10063 events - `useAccountSync`: Extended to fetch and watch active account's blossom servers 4. **State Management**: - Added `blossomServers` field to `activeAccount` in GrimoireState - Created `setActiveAccountBlossomServers` pure function and callback - Reactive updates when user's server list changes 5. **User Menu UI (user-menu.tsx)**: - Added blossom servers section with HardDrive icon - Shows server count badge (e.g., "3 servers") - Clickable links open servers in new tabs - Displays only when user has configured servers **Architecture:** - Follows existing relay list caching pattern for consistency - Reactive: UI auto-updates when kind:10063 events arrive - Incremental sync: Fetches on login, caches for 24h - Performance: Memory cache for <1ms lookups, Dexie for persistence **Testing:** - Build: ✓ No TypeScript errors - Tests: ✓ All 838 tests passing * feat: Open blossom server file lists directly from menus **User Menu & Profile Viewer Improvements:** 1. **Enhanced Click Behavior**: - Clicking a blossom server now opens the file list for that server - Shows blobs uploaded by the user (user menu) or profile owner (profile viewer) - Pre-selects the clicked server in the dropdown 2. **UX Improvements**: - Removed server count from user menu label (cleaner UI) - Added `cursor-crosshair` to blossom server items (consistent with other clickable items) - Removed external link icon (not opening external URL anymore) 3. **Technical Changes**: - Updated `ListBlobsView` to accept optional `serverUrl` prop for pre-selection - User menu: Opens `blossom list` with `serverUrl` for active user - Profile viewer: Opens `blossom list` with both `pubkey` and `serverUrl` **Flow:** - User menu → Click server → Opens files for active user on that server - Profile viewer → Click server → Opens files for viewed user on that server * fix: Properly fetch blossom servers for any profile view **Problem:** Blossom servers were only visible for the logged-in user's profile, not for other users' profiles being viewed. **Solution:** Enhanced ProfileViewer blossom server fetching with multi-layer approach: 1. **Cache-first loading**: Check blossomServerCache for instant display 2. **EventStore check**: Use existing cached event if available 3. **Reactive subscription**: Subscribe to EventStore for real-time updates 4. **Network fetch**: Use addressLoader to fetch latest from relays 5. **Auto-caching**: Update cache when new events arrive **Benefits:** - Blossom servers now display for ANY user's profile - Instant display from cache (< 1ms) - Reactive updates when data changes - Proper cache hydration for future visits - Consistent with relay list fetching pattern **Technical:** - Imported and integrated blossomServerCache service - Added cache check before network fetch - Separated EventStore subscription from network fetch - Added cache updates on event arrival --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
32d394b398 |
feat: add preview routes for Nostr identifiers (npub, nevent, note, naddr) (#33)
* feat: add preview routes for Nostr identifiers (npub, nevent, note, naddr)
This commit adds dedicated preview routes for Nostr identifiers at the root level:
- /npub... - Shows a single profile view for npub identifiers
- /nevent... - Shows a single event detail view for nevent identifiers
- /note... - Shows a single event detail view for note identifiers
- /naddr... - Redirects spellbooks (kind 30777) to /:actor/:identifier route
Key changes:
- Created PreviewProfilePage component for npub identifiers
- Created PreviewEventPage component for nevent/note identifiers
- Created PreviewAddressPage component for naddr redirects
- Added hideBottomBar prop to AppShell to hide tabs in preview mode
- Added routes to root.tsx for all identifier types
Preview pages don't show bottom tabs and don't affect user's workspace layout.
* chore: update package-lock.json
* refactor: create reusable useNip19Decode hook and improve preview pages
This commit makes the preview pages production-ready by:
1. Created useNip19Decode hook (src/hooks/useNip19Decode.ts):
- Reusable hook for decoding NIP-19 identifiers (npub, note, nevent, naddr, nprofile)
- Type-safe with discriminated union for decoded entities
- Comprehensive error handling with retry functionality
- Loading states and error messages
- Well-documented with JSDoc comments and usage examples
2. Comprehensive test coverage (src/hooks/useNip19Decode.test.ts):
- 11 tests covering all entity types (npub, note, nevent, naddr)
- Tests for error handling (missing identifier, invalid format, corrupted bech32)
- Tests for retry functionality and state changes
- Uses jsdom environment for React hook testing
- All tests passing ✓
3. Refactored preview pages to use the hook:
- PreviewProfilePage: Simplified from 80 to 81 lines with cleaner logic
- PreviewEventPage: Improved type safety and error handling
- PreviewAddressPage: Better separation of concerns
- All pages now have consistent error handling and retry functionality
- Better user experience with improved error messages
4. Dependencies added:
- @testing-library/react for React hook testing
- @testing-library/dom for DOM testing utilities
- jsdom and happy-dom for browser environment simulation in tests
Benefits:
- Code deduplication: Preview pages share decoding logic
- Type safety: Discriminated union prevents type errors
- Testability: Hook can be tested independently
- Maintainability: Single source of truth for NIP-19 decoding
- User experience: Consistent error handling and retry across all preview pages
- Production-ready: Comprehensive tests and error handling
* refactor: simplify useNip19Decode to synchronous with memoization
NIP-19 decoding is synchronous - removed unnecessary async complexity:
Hook changes (src/hooks/useNip19Decode.ts):
- Removed loading states (isLoading, setIsLoading)
- Removed retry functionality (unnecessary for sync operations)
- Now uses useMemo for efficient memoization
- Returns { decoded, error } instead of { decoded, isLoading, error, retry }
- Same string always yields same result (memoized)
- Went from ~120 lines to ~115 lines, but much simpler
Preview page changes:
- Removed loading spinners and states
- Removed retry buttons
- Simplified error handling
- Cleaner, more readable code
- PreviewProfilePage: 55 lines (down from 81)
- PreviewEventPage: 83 lines (down from 105)
- PreviewAddressPage: 83 lines (down from 117)
Test changes (src/hooks/useNip19Decode.test.ts):
- Removed waitFor and async/await (not needed for sync)
- Tests run faster (39ms vs 77ms - 49% improvement)
- Added memoization tests to verify caching works
- Simplified from 11 async tests to 11 sync tests
- All 11 tests passing ✓
Benefits:
- Simpler mental model: decode happens instantly
- Better performance: no state updates, just memoization
- Easier to test: synchronous tests are simpler
- More correct: matches the actual synchronous nature of nip19.decode()
- Less code: removed ~150 lines of unnecessary complexity
* feat: show detail view for all addressable events in naddr preview
Previously, PreviewAddressPage only handled spellbooks (kind 30777) and
showed errors for other addressable events. Now:
- Spellbooks (kind 30777): Redirect to /:actor/:identifier (existing behavior)
- All other addressable events: Show in EventDetailViewer
This enables previewing any addressable event (long-form articles, live
events, community posts, etc.) via naddr links.
Changes:
- Import EventDetailViewer
- Removed error state for non-spellbook kinds
- Show EventDetailViewer with AddressPointer for all other kinds
- Simplified from 83 lines to 77 lines
* fix: correct route patterns for NIP-19 identifier previews
The previous route patterns (/npub:identifier) conflicted with the catch-all
/:actor/:identifier route and didn't properly match NIP-19 identifiers.
Fixed by:
1. Using wildcard routes with correct prefixes:
- /npub1* (not /npub:identifier)
- /nevent1* (not /nevent:identifier)
- /note1* (not /note:identifier)
- /naddr1* (not /naddr:identifier)
2. Updated preview components to use params['*'] for wildcard capture:
- Reconstruct full identifier as prefix + captured part
- e.g., npub1 + params['*'] = npub107jk7htfv...
This ensures routes properly match before the catch-all /:actor/:identifier
route and correctly capture the full bech32-encoded identifier.
Test URL: /npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg
* style: apply prettier formatting
* fix: use loader-based routing for NIP-19 identifiers in React Router v7
Previous attempts using wildcard routes didn't work properly in React Router v7.
Solution:
- Single /:identifier route with a loader that validates NIP-19 prefixes
- Loader throws 404 if identifier doesn't start with npub1/note1/nevent1/naddr1
- Created Nip19PreviewRouter component that routes to correct preview page
- Routes are properly ordered: /:identifier before /:actor/:identifier catch-all
This ensures /npub107jk... routes to profile preview, not spellbook route.
Benefits:
- Simpler routing configuration (1 route vs 4 duplicate routes)
- Proper validation via loader
- Clean separation of concerns with router component
- Works correctly in React Router v7
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
af8cf427d6 |
fix: implement per-relay EOSE detection by subscribing to relays individually
**CRITICAL FIX for EOSE detection:** **The Problem:** - Used pool.subscription(relays, filters) which creates a RelayGroup - RelayGroup tracks per-relay EOSE internally but only emits ONE "EOSE" when ALL relays finish - This caused: 1. EOSE indicators taking forever to appear (waiting for slowest relay) 2. REQ stuck in LOADING state when fast relays finish but slow relays never do 3. No way to show per-relay EOSE status accurately **The Solution:** Subscribe to each relay individually using pool.relay(url).subscription(): - Each relay subscription emits its own EOSE immediately when that relay finishes - We track per-relay EOSE in relayStates map with accurate timing - Overall EOSE is derived when ALL relays reach terminal state (eose/error/disconnected) - EOSE indicators now appear immediately as each relay finishes **Technical Details:** - Changed from: pool.subscription(relays, filters) - Changed to: relays.map(url => pool.relay(url).subscription(filters)) - Added eoseReceivedRef to track overall EOSE in closures - Mark specific relay as EOSE when that relay emits "EOSE" - Calculate overall EOSE when all relays in terminal states - Use url from subscription context (more reliable than event._relay) **Benefits:** ✅ Instant per-relay EOSE indicators (no waiting for slowest relay) ✅ Accurate relay state tracking (each relay independent) ✅ REQ transitions to LIVE/CLOSED as soon as all relays finish ✅ Better user feedback (see which relays are done vs still loading) All 639 tests passing. |
||
|
|
70651ae29f |
fix: improve relay state tracking and add relay type indicators
State Tracking Fixes: - Sync connection state for ALL relays in query, not just initialized ones - Defensively initialize missing relay states during sync - Handle events from unknown relays (defensive initialization) - Add debug console logs to track state transitions Relay Type Indicators: - Explicit relays: Blue link icon (relays specified directly) - Outbox relays: Purple sparkles (NIP-65 selected) - Fallback relays: Gray inbox icon (fallback when outbox incomplete) - Each type has tooltip explaining source This should fix: - "0/4 relays but events coming in" bug - "Stuck in LOADING" when events are arriving - Missing visibility for relay source types Tests: 634/634 passing |
||
|
|
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 |
||
|
|
c40c4ae1f9 | fix: TypeScript error in useStableFilters | ||
|
|
912794f4e0 | chore: lint fix | ||
|
|
33539c291b |
refactor: use applesauce helpers for pointer parsing and filter comparison
Phase 2 & 3 of applesauce helpers refactoring plan. **Phase 2: Replace manual pointer parsing** - ReactionRenderer.tsx: replaced manual coordinate parsing with parseCoordinate helper - Benefits: more robust, handles edge cases, consistent with applesauce patterns **Phase 3: Improve filter comparison** - useStable.ts: replaced JSON.stringify with isFilterEqual for filter comparison - Benefits: handles undefined values correctly, supports NIP-ND AND operator - Implementation uses ref pattern to maintain stable reference when filters are equal All tests pass (607 tests). |
||
|
|
32c895e150 | chore: lint fix | ||
|
|
d35345f720 |
Merge pull request #13 from purrgrammer/claude/useprofile-race-fix-EeWQZ
fix: prevent race conditions in useProfile hook |
||
|
|
645e12cddc |
fix: prevent race conditions in useProfile hook
- Replace mounted boolean flag with AbortController pattern - Check abort signal before initiating database writes - Proper cleanup on unmount/pubkey change This prevents stale data from being written to IndexedDB when: - Component unmounts during async operations - Pubkey changes while a fetch is in progress |
||
|
|
bdfc634c54 |
feat: add dependency stabilization hooks
- Create src/hooks/useStable.ts with:
- useStableValue<T>() - stabilizes any value using JSON.stringify
- useStableArray<T>() - stabilizes string arrays (uses JSON.stringify
for safety, handles arrays with commas in elements)
- useStableFilters<T>() - specialized for Nostr filters
- Update timeline hooks to use stabilization:
- useTimeline.ts - use useStableFilters for filter dependencies
- useReqTimeline.ts - use useStableValue for filter dependencies
- useLiveTimeline.ts - use useStableArray for relay dependencies
Prevents unnecessary re-renders and subscription restarts when
filter/relay objects are recreated with the same content.
|
||
|
|
812b719ea0 | feat: debug command, simplify state | ||
|
|
a7dd4635dc | feat: kind schemas and better man pages | ||
|
|
3b06e23686 |
ui: improve man page layout for options and examples
- Display option flags on separate lines with indented descriptions to prevent overflow - Parse and separate example commands from their descriptions - Highlight commands in accent color with muted descriptions below - Increase spacing between items for better readability |
||
|
|
97c89142ae | wip: live video events | ||
|
|
a7059c8e8f | feat: outbox relay selection | ||
|
|
88a6e38ee9 | refactor: use applesauce for profile parsing | ||
|
|
e5c871617e | chore: cleanup, a11y and state migrations | ||
|
|
bef7369de9 | fix: normalize relay URLs | ||
|
|
8e92a8ebfb | feat: filter by arbitrary tag, relay state improvements | ||
|
|
6568daf944 | wip: relay pool view and auth | ||
|
|
16158e7045 | feat: better p and authors titles | ||
|
|
38f5461b54 | chore: setup eslint and prettier with code formatting | ||
|
|
e926272686 | feat: relay command | ||
|
|
929365a813 | refactor: copy hook | ||
|
|
8ffd0fd2cb | feat: timestamps and user locale | ||
|
|
c6e3325720 | wip: render hashtags correctly | ||
|
|
5b00e42ddf | feat: RTL support | ||
|
|
cd41034b2f | 👶 |