mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-18 03:17:04 +02:00
9ebf9b54a262f313d15849cfa5e6f5ebca02e64c
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
26bb0713ac | feat: keep relay selection in call site, compact logs | ||
|
|
9a668bbdac |
feat: centralize publish flow with RxJS-based PublishService
Create a unified PublishService that: - Provides consistent relay selection (outbox + state + hints + fallbacks) - Emits RxJS observables for per-relay status updates - Handles EventStore integration automatically - Supports both fire-and-forget and observable-based publishing Refactor all publish locations to use the centralized service: - hub.ts: Use PublishService for ActionRunner publish - delete-event.ts: Use PublishService (fixes missing eventStore.add) - publish-spell.ts: Use PublishService with relay hint support - PostViewer.tsx: Use publishWithUpdates() for per-relay UI tracking This lays the groundwork for the event log feature by providing observable hooks into all publish operations. |
||
|
|
8c9ecd574c |
refactor(editor): replace DOM manipulation with React node views and floating-ui (#253)
* refactor(editor): replace DOM manipulation with React node views and floating-ui - Convert all inline node views (emoji, blob attachment, event preview) from imperative document.createElement() to React components via ReactNodeViewRenderer - Replace tippy.js with @floating-ui/react-dom for suggestion popup positioning - Create useSuggestionRenderer hook bridging Tiptap suggestion callbacks to React state - Extract shared EmojiMention, SubmitShortcut, and inline node extensions to separate files - Extract types (EmojiTag, BlobAttachment, SerializedContent) to editor/types.ts - Extract serialization logic to editor/utils/serialize.ts - Remove redundant DOM keydown listener from RichEditor (handled by SubmitShortcut extension) - Remove tippy.js dependency (-1045 lines net, RichEditor 632→297, MentionEditor 1038→354) https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u * fix(editor): fix suggestion popover positioning, scrolling, and profile click behavior - Replace UserName component in ProfileSuggestionList with plain text display so clicking a suggestion autocompletes instead of opening their profile (UserName has an onClick that calls addWindow and stopPropagation) - Add react-virtuoso to ProfileSuggestionList for efficient lazy rendering of up to 20 search results with fixed item height scrolling - Add profile avatars with lazy loading and initial-letter fallback - Fix SuggestionPopover positioning with autoUpdate for scroll/resize tracking - Add size middleware to constrain popover max-height to available viewport space https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u * refactor(editor): convert emoji suggestion from grid to scrollable list with Virtuoso Replace the 8-column grid layout with a vertical list matching the profile suggestion style — each row shows the emoji preview alongside its :shortcode: name. Uses react-virtuoso with fixedItemHeight for lazy rendering and smooth keyboard-driven scrolling through large emoji sets. https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u * fix(editor): set mentionSuggestionChar to ':' for emoji nodes When backspacing over a mention-based node, Tiptap inserts the node's mentionSuggestionChar attribute as undo text. The EmojiMention extension inherits Mention's default of '@', so deleting an emoji left '@' instead of ':'. Fix by explicitly setting mentionSuggestionChar: ':' in the emoji command's attrs for both RichEditor and MentionEditor. https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u * test(editor): add comprehensive test suite for custom TipTap extensions Tests all 8 custom extensions using headless TipTap Editor instances in jsdom environment (TipTap has no official testing package): - EmojiMention: schema, renderText (unicode vs custom), mentionSuggestionChar attribute handling, backspace behavior regression test - BlobAttachmentRichNode/InlineNode: schema (block vs inline), attributes, renderText URL serialization, parseHTML selectors - NostrEventPreviewRichNode/InlineNode: schema, renderText encoding for note/nevent/naddr back to nostr: URIs - SubmitShortcut: Mod-Enter always submits, Enter behavior with enterSubmits flag - FilePasteHandler: media type filtering (image/video/audio), non-media rejection, mixed paste filtering, edge cases (no files, no callback) - NostrPasteHandler: bech32 regex matching (npub/note/nevent/naddr/nprofile), nostr: prefix handling, URL exclusion, node creation (mention vs preview), surrounding text preservation, multiple entities - Serialization: formatBlobSize, serializeRichContent (emoji tag extraction, blob dedup, address refs), serializeInlineContent (mention→nostr: URI, emoji→shortcode, blob→URL, event preview encoding) 90 new tests total. https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u * fix(editor): paste handler and serialization bugs found via adversarial testing NostrPasteHandler fixes: - Punctuation after bech32 now matches (npub1..., npub1...! etc.) Changed trailing lookahead from (?=$|\s) to (?=$|\s|[.,!?;:)\]}>]) - Fixed double-space between entities — unconditional " " after every entity caused doubled spaces. Now only adds trailing space when entity is at the very end of pasted text (for cursor positioning). - Tightened regex character class from [\w] to [a-z0-9] to match actual bech32 charset (rejects uppercase, underscore) - Wrapped dispatch in try/catch to handle block-node-at-inline-position errors gracefully (falls back to default paste) Serialization fix: - serializeRichContent now guards blob collection with `url && sha256` matching the defensive checks already in serializeInlineContent. Previously null sha256 would corrupt the dedup Set and null url would produce invalid BlobAttachment entries. Added 22 new edge case tests: - Paste handler: punctuation boundaries, double-space regression, malformed bech32 fallback, uppercase rejection, error resilience - Serialization: empty editor, null sha256/url blobs, invalid pubkey fallback, missing mention attrs, inline dedup, multi-paragraph https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u * fix(editor): raise suggestion search limits for profiles and emojis Both suggestion dropdowns use Virtuoso for virtualized rendering, so they can handle large result sets without performance issues. The previous limits (20 profiles, 24 emojis) were too restrictive — users with many custom emojis sharing a substring or large contact lists couldn't scroll to find the right match. Raised both limits to 200 to allow thorough browsing while still bounding the result set. https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u * refactor(chat): rework emoji picker to scrollable list with search Replace the fixed 1-row grid (8 emojis) with a scrollable virtualized list matching the editor's EmojiSuggestionList look & feel: - Search box at top with magnifying glass icon - Virtuoso-backed scrollable list (8 visible items, unlimited results) - Each row shows emoji icon + :shortcode: label - Keyboard navigation: arrow keys to select, Enter to confirm - Mouse hover highlights, click selects - Frequently used emojis still shown first when no search query - Narrower dialog (max-w-xs) for a compact picker feel https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u * fix: add address field to EmojiTag in editor types, fix GroupMessageOptions - Add optional `address` field to EmojiTag in editor/types.ts to match NIP-30 changes from main (30030 emoji set address) - Extend GroupMessageOptions with MetaTagOptions to fix type error in GroupMessageBlueprint's setMetaTags call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(editor): restore address attr, fix serialization, UserName, no-scroll - Restore `address` attribute in shared EmojiMention extension (emoji.ts) that was dropped during refactor — required for NIP-30 emoji set tracking - Extract `address` from emoji nodes in both serializeRichContent and serializeInlineContent so it makes it into published events - Fix MentionEditorProps.onSubmit signature: use EmojiTag[] (not the narrower inline type) so address field flows through to callers - Restore UserName component in ProfileSuggestionList for proper display with Grimoire member badges and supporter flame - Remove scrollbar when all items fit: set overflow:hidden on Virtuoso when items.length <= MAX_VISIBLE (profile list, emoji list, emoji picker dialog) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
cffb981ad1 |
feat: add emoji set address as 4th param in NIP-30 emoji tags
NIP-30 allows an optional 4th tag parameter specifying the source emoji set address (e.g. "30030:pubkey:identifier"). This threads that address through the full emoji pipeline so it appears in posts, replies, reactions, and zap requests. - Add local blueprints.ts with patched NoteBlueprint, NoteReplyBlueprint, GroupMessageBlueprint, and ReactionBlueprint that emit the 4th param; marked TODO to revert once applesauce-common supports it upstream - Add address? to EmojiWithAddress, EmojiTag, EmojiSearchResult, and EmojiTag in create-zap-request - Store address in EmojiSearchService.addEmojiSet (30030:pubkey:identifier) - Thread address through both editor serializers (MentionEditor, RichEditor) and the emoji node TipTap attributes - Fix EmojiPickerDialog to pass address when calling onEmojiSelect and when re-indexing context emojis - Update SendMessageOptions.emojiTags and sendReaction customEmoji param to use EmojiTag throughout the adapter chain Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
3ce77ef97c |
Remove "Publishing..." text from POST view (#195)
Removed the "Publishing..." text from the Publish button during posting, keeping only the loading spinner icon for a cleaner UI. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
074c3c0b7f |
Fix relay list flickering during post publish (#187)
* fix: prevent relay list flicker during post publishing Fixed the relay list flickering issue that occurred while publishing posts: - Changed overflow-y-auto to overflow-y-scroll to keep scrollbar visible - Added fixed width (w-6) to status indicator container to prevent layout shifts - Status indicators no longer cause content reflow when changing states * fix: show icon for all relay states to prevent layout shifts Show a Circle icon for pending/unpublished relays so there's always an icon present. This keeps the status indicator container at a consistent width and prevents flickering when publishing. - Added Circle icon import - Show Circle icon for "pending" state - Reverted to overflow-y-auto (no longer needed with fixed container) - Status indicator always has fixed width with centered icon * refactor: use muted color for pending and publishing relay states Changed publishing spinner from blue to muted to match pending state, creating a more consistent visual hierarchy where only success (green) and error (red) states use color. --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
66618fb150 |
fix: prevent TipTap editor crash when view is not ready (#188)
The POST command would sometimes crash with "editor view is not available" because code was accessing editor.view.dom before the editor was fully mounted. This fix: - Adds defensive checks for editor.view?.dom in RichEditor's useEffect that attaches keyboard listeners - Makes setContent method check editor view is ready before setting content - Fixes PostViewer draft loading to use retry logic instead of fixed timeout - Removes relayStates from dependency array to prevent effect re-runs - Adds ref to track if draft was already loaded Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
adf8a62954 |
feat: add hashtag support to rich editor (#185)
* feat: add automatic hashtag extraction and t tags in POST command
Extract hashtags from post content and automatically add them as t tags to published events.
Changes:
- Add hashtag extraction logic to RichEditor.serializeContent() using Unicode-aware regex
- Update SerializedContent interface to include hashtags field
- Update RichEditor props and callbacks to pass hashtags through the pipeline
- Add t tags for each hashtag in PostViewer.handlePublish()
Hashtags are deduplicated and stored in lowercase (following Nostr convention).
Example: #bitcoin #nostr #Bitcoin → ["t", "bitcoin"], ["t", "nostr"]
* refactor: use NoteBlueprint for automatic hashtag/mention extraction
Replace manual hashtag and mention extraction with applesauce's NoteBlueprint,
which automatically extracts hashtags, mentions, and event quotes from text content.
Changes:
- Simplify SerializedContent interface by removing manually extracted fields
- Remove hashtag extraction regex and mention/eventRef tracking from editors
- Replace manual event building with factory.create(NoteBlueprint, ...)
- Use q tags for event quotes (NIP-18) instead of e tags
Benefits:
- ~70 lines of code removed
- Leverage battle-tested applesauce extraction logic
- Automatic benefits from future applesauce improvements
- Correct semantic tags (q for quotes, p for mentions, t for hashtags)
What still works:
- Custom emoji tags (NIP-30)
- Blob attachments/imeta tags (NIP-92)
- Address references (naddr - not yet in applesauce)
- Client tag
All tests pass (980/980).
* refactor: use NoteReplyBlueprint in NIP-10 adapter
Replace manual NIP-10 tag building with NoteReplyBlueprint, which automatically
handles root/reply markers, p-tag copying, and all the threading logic.
Changes:
- Simplify sendMessage from ~95 lines to ~40 lines
- Remove manual e-tag building with root/reply markers
- Remove manual p-tag deduplication logic
- Use factory.create(NoteReplyBlueprint, parentEvent, content, options)
- Automatically get hashtags, mentions, and event quotes via setShortTextContent
Benefits:
- ~55 lines of complex threading logic removed
- Leverage battle-tested applesauce NIP-10 implementation
- Automatic root detection from parent's existing tags
- Cleaner, more maintainable code
All tests pass (980/980).
* refactor: use GroupMessageBlueprint and ReactionBlueprint in chat adapters
Replace manual event building with applesauce blueprints in all chat adapters.
Changes:
- NIP-29: Use GroupMessageBlueprint for kind 9 messages
* Auto-handles h-tag, hashtags, mentions, emojis
* Manually add q-tag for replies (NIP-29 specific)
* ~15 lines removed
- All adapters (NIP-10, NIP-29, NIP-53, NIP-C7): Use ReactionBlueprint for kind 7 reactions
* Auto-handles e-tag, k-tag, p-tag, custom emoji support
* Protocol-specific tags (h-tag, a-tag) added manually
* ~60 lines removed across 4 adapters
Benefits:
- ~75 lines of code removed total
- Leverage battle-tested applesauce blueprints
- Automatic hashtag, mention, and quote extraction
- Cleaner, more maintainable code
All tests pass (980/980).
* fix: add required previous field to GroupMessageBlueprint options
GroupMessageBlueprintOptions requires a 'previous' field for message threading.
Added empty array for now since we don't support threading yet.
* docs: add comprehensive blueprint documentation to applesauce skills
Added detailed documentation for:
- NoteBlueprint (automatic hashtag/mention/quote extraction)
- NoteReplyBlueprint (NIP-10 threading)
- ReactionBlueprint (kind 7 reactions)
- GroupMessageBlueprint (NIP-29 groups)
- DeleteBlueprint (NIP-09 deletion)
- EventFactory usage patterns
- Before/after examples showing code reduction
- Best practices for using blueprints
This documents the refactoring work done throughout the codebase.
* fix: use single newline separator in TipTap getText() calls
TipTap's getText() uses double newlines (\n\n) by default to separate
block nodes like paragraphs, which was causing extra blank lines in
posted content.
Changed to getText({ blockSeparator: '\n' }) in both RichEditor and
MentionEditor to use single newlines between paragraphs.
---------
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> |