Commit Graph

295 Commits

Author SHA1 Message Date
Alejandro
fd31e707fc Add window action to move windows between tabs (#74)
- Add "Move to tab" submenu in window toolbar's more actions dropdown
- Shows nested submenu with available workspace tabs (number + label)
- After moving, automatically switches to the target workspace
- Only displays the more actions menu when multiple workspaces exist
- Preserves existing REQ-specific "Save as spell" action

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 13:28:02 +01:00
Alejandro
797510107b Fix slash command autocomplete and add bookmark commands (#73)
* fix: slash autocomplete only at input start + add bookmark commands

- Fix slash command autocomplete to only trigger when / is at the
  beginning of input text (position 1 in TipTap), not in the middle
  of messages
- Add /bookmark command to add NIP-29 group to user's kind 10009 list
- Add /unbookmark command to remove group from user's kind 10009 list

* fix: normalize relay URLs when checking group list bookmarks

Use normalizeRelayURL for comparing relay URLs in bookmark/unbookmark
commands to handle differences in trailing slashes, casing, and protocol
prefixes between stored tags and conversation metadata.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 12:39:22 +01:00
Alejandro
280a395187 Make grouplist viewer mobile friendly with sidebar (#72)
* feat: make GroupListViewer mobile-friendly with Sheet sidebar

- Add Sheet component for mobile drawer behavior
- Add Separator component for UI dividers
- Add Sidebar component with mobile/desktop variants
- Update GroupListViewer to show sheet-based sidebar on mobile (<768px)
  and resizable sidebar on desktop
- Mobile view includes toggle button in header and auto-closes on selection

* refactor: integrate sidebar toggle into ChatViewer header

- Add headerPrefix prop to ChatViewer for custom header content
- Pass sidebar toggle button via headerPrefix on mobile
- Remove duplicate mobile header from GroupListViewer
- Reduces vertical space usage by reusing ChatViewer's existing header

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 11:52:52 +01:00
Alejandro
4ee385ea6c Add actions support to chat adapter protocol (#67)
* feat: add protocol-specific actions to chat adapters

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

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

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

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

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

* feat: add autocomplete for slash commands

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

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

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

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

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

* feat: execute slash commands immediately on autocomplete selection

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-13 11:49:32 +01:00
Alejandro Gómez
9657ec635f fix: support other user groups 2026-01-13 10:50:17 +01:00
Alejandro
c4687da3ef Add multi-room group chat support (#70)
* feat: add multi-room group chat interface (GroupListViewer)

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

- New GroupListViewer component with split layout:
  - Left panel: List of groups from kind 10009, sorted by recency
  - Right panel: Full chat view for selected group
- Loads group metadata (kind 39000) for icons and names
- Tracks latest messages (kind 9) for activity-based sorting
2026-01-13 10:30:10 +01:00
Alejandro Gómez
5de60f17d3 ui: load older styles 2026-01-13 09:37:34 +01:00
Alejandro
cf1f384691 Make chat composer more compact (#68)
* ui: make chat composer more compact

- Reduce input and button height from 2.5rem to 1.75rem (h-7)
- Reduce padding from px-3 py-1.5 to px-2 py-1
- Use text-sm for consistent sizing with chat messages
- Make Send button smaller with text-xs and smaller icon
- Tighten gap between input and button

* ui: remove bottom padding from chat composer

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 22:21:03 +01:00
Alejandro
20eb19bdbb fix: improve chat architecture robustness and error handling (#66)
* fix: improve chat architecture robustness and error handling

- Fix scroll-to-message index mismatch (was searching in wrong array)
- Fix subscription memory leaks by tracking and cleaning up subscriptions
- Add error handling for conversation resolution with retry UI
- Add error handling for send message with toast notifications
- Fix array mutation bugs in NIP-53 relay handling
- Add type guards for LiveActivityMetadata
- Fix RelaysDropdown O(n²) performance issue
- Add loading state for send button

* refactor: add stronger types and optimize message sorting

- Add discriminated union types for ProtocolIdentifier (GroupIdentifier,
  LiveActivityIdentifier, DMIdentifier, NIP05Identifier, ChannelIdentifier)
- Optimize message sorting using reverse() instead of full sort (O(n) vs O(n log n))
- Add type narrowing in adapter resolveConversation methods
- Remove unused Observable import from ChatViewer

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 21:12:22 +01:00
Alejandro
159d35e347 feat: add rich text rendering for live chat messages (kind 1311) (#64)
- Created LiveChatMessageRenderer component for NIP-53 live chat messages
- Displays messages with RichText component for full formatting support
- Links to parent live activity event (kind 30311) with clickable header
- Shows activity title or fallback to "Live chat with [host]"
- Registered kind 1311 in renderer registry
- Exported both human-readable name (LiveChatMessageRenderer) and kind alias (Kind1311Renderer)

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 19:41:20 +01:00
Alejandro Gómez
7d460352e3 ui: zap margin 2026-01-12 18:11:54 +01:00
Alejandro
73de36a544 Render NIP-29 group metadata as links (#63)
* feat: add GroupMetadataRenderer for NIP-29 group metadata (kind 39000)

Render kind 39000 events with group name, picture, description, and
an "Open Chat" link that opens the NIP-29 group in the chat viewer.

* feat: add kind names for NIP-29 group events and simplify renderer

- Add kind 39000 (Group), 39001 (Group Admins), 39002 (Group Members)
  to EVENT_KINDS so kind badge displays proper names
- Simplify GroupMetadataRenderer to show only title, description, and
  Open Chat CTA (remove group picture)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 17:46:41 +01:00
Alejandro
76dd1e801d fix: reconstruct chat command for edit button (#62)
Add chat case to reconstructCommand to properly reconstruct the chat
command string when clicking the edit button in the window toolbar.

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

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 17:07:22 +01:00
Alejandro
b24810074d feat: add NIP-61 nutzap support to NIP-29 groups (#59)
* feat: add NIP-61 nutzap support to NIP-29 groups

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

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

* fix: improve zap/nutzap rendering in chat

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

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

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

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

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

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

* feat: add load older messages support to chat adapters

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 15:26:41 +01:00
Alejandro Gómez
e50fcca386 ui: chat tooltip, better zaps 2026-01-12 13:41:40 +01:00
Alejandro Gómez
6beda51ad7 ui: don't show chat description 2026-01-12 13:31:45 +01:00
Alejandro
5bc89386ea Add NIP-53 live event chat adapter (#56)
* feat: add NIP-53 live activity chat adapter

Add support for joining live stream chat via naddr (kind 30311):
- Create Nip53Adapter with parseIdentifier, resolveConversation, loadMessages, sendMessage
- Show live activity status badge (LIVE/UPCOMING/ENDED) in chat header
- Display host name and stream metadata from the live activity event
- Support kind 1311 live chat messages with a-tag references
- Use relays from activity's relays tag or naddr relay hints
- Add tests for adapter identifier parsing and chat-parser integration

* ui: derive live chat participants from messages, icon-only status badge

- Derive participants list from unique pubkeys in chat messages for NIP-53
- Move status badge after title with hideLabel for compact icon-only display

* feat: show zaps in NIP-53 live chat with gradient border

- Fetch kind 9735 zaps with #a tag matching the live activity
- Combine zaps and chat messages in the timeline, sorted by timestamp
- Display zap messages with gradient border (yellow → orange → purple → cyan)
- Show zapper, amount, recipient, and optional comment
- Add "zap" message type with zapAmount and zapRecipient metadata

* fix: use RichText for zap comments and remove arrow in chat

- Use RichText with zap request event for zap comments (renders emoji tags)
- Remove the arrow (→) between zapper and recipient in zap messages

* refactor: simplify zap message rendering in chat

- Put timestamp right next to recipient (removed ml-auto)
- Use RichText with content prop and event for emoji resolution
- Inline simple expressions, remove unnecessary variables
- Follow codebase patterns from ZapCompactPreview

* docs: update chat command to include NIP-53 live activity

- Update synopsis to use generic <identifier>
- Add NIP-53 live activity chat to description
- Update option description to cover both protocols
- Add naddr example for live activity chat
- Add 'live' to seeAlso references

* fix: use host outbox relays for NIP-53 live chat events

Combine activity relays, naddr hints, and host's outbox relays when
subscribing to chat messages and zaps. This ensures events are fetched
from all relevant sources where they may be published.

* ui: show host first in members list, all relays in dropdown

- derivedParticipants now puts host first with 'host' role
- Other participants from messages follow as 'member'
- RelaysDropdown shows all NIP-53 liveActivity.relays

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 13:05:09 +01:00
Alejandro
59e79959a6 Simplify NIP link styling in rich text (#57)
Remove icon and inline-flex layout from NIP links so they align
consistently with surrounding text and have the same size.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 12:20:45 +01:00
Alejandro
035fd829d5 Add Ctrl+Enter shortcut to send messages (#55)
* fix: allow Ctrl/Cmd+Enter to submit messages when suggestion popup is open

The suggestion list components were intercepting all Enter key presses,
including Ctrl+Enter and Cmd+Enter, preventing message submission when
the autocomplete popup was visible. Now these modifier combinations
pass through to the editor's submit handler.

* fix: make Ctrl/Cmd+Enter work reliably and prevent text overflow

- Add TipTap Extension with addKeyboardShortcuts for Mod-Enter and Enter
  which has higher priority than the suggestion plugin, ensuring
  Ctrl/Cmd+Enter always submits even when autocomplete is open
- Use ref to access handleSubmit from extension without recreating it
- Add whitespace-nowrap to prevent text wrapping in single-line input
- Add overflow-hidden to container and overflow-x-auto to editor content
  to handle long text gracefully with horizontal scrolling

* fix: handle Ctrl/Cmd+Enter directly in suggestion onKeyDown handlers

The TipTap suggestion plugin intercepts key events at the plugin level,
which runs before extension keyboard shortcuts. Even returning false
from the suggestion's onKeyDown didn't properly propagate events.

Now the suggestion handlers directly handle Ctrl/Cmd+Enter by:
1. Capturing the editor reference from onStart (where it's available)
2. Checking for Ctrl/Cmd+Enter in onKeyDown
3. Closing the popup and calling handleSubmitRef.current()

Also moved handleSubmitRef to the top of the component so it can be
accessed from within the suggestion config closures.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 12:02:47 +01:00
Alejandro Gómez
385f599b67 ui: show 'left' in chat 2026-01-12 11:43:14 +01:00
Alejandro Gómez
a76d0fabba fix: don't shrink chat title 2026-01-12 11:43:05 +01:00
Alejandro
2bad592a3a feat: emoji autocompletion (#54)
* feat: add NIP-30 emoji autocompletion to editor

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

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

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

* feat: add rich emoji preview in editor

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

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

CSS styles ensure proper vertical alignment with surrounding text.

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

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

* fix: improve emoji node rendering in editor

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

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 11:30:52 +01:00
Alejandro
ae3af2d63c feat: add NIP-46 remote signer login support (#53)
* feat: add NIP-46 remote signer login support

Add a login dialog with two authentication options:
- Extension login (NIP-07): Connect via browser extensions like nos2x, Alby
- Nostr Connect (NIP-46): Login via QR code scan or bunker:// URL

The dialog allows users to generate a nostrconnect:// QR code that can be
scanned with a signer app, or paste a bunker:// URL for direct connection.

* fix: improve NIP-46 login experience

- Remove redundant signer.open() call after fromBunkerURI (it already connects)
- Increase QR code margin from 2 to 4 for better scanning
- Increase QR code width to 280px

* fix: establish WebSocket connection before showing QR code

Fixes NIP-46 QR code login by opening the relay connections BEFORE
displaying the QR code. This ensures the client is listening when
the remote signer responds.

Also adds console logging for debugging NIP-46 connection flow.

* fix: set NostrConnectSigner pool before loading accounts

Fixes crash on reload when a NIP-46 account is saved. The pool must
be configured globally before accounts are restored from localStorage,
otherwise NostrConnectSigner throws "Missing subscriptionMethod".

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 11:28:44 +01:00
Alejandro Gómez
a3eec2e3c9 feat: disable inline media and event embeds in chat replies
Pass options to RichText component in ReplyPreview to disable:
- All media types (images, videos, audio) via showMedia: false
- Event embeds (note/nevent/naddr mentions) via showEventEmbeds: false

Chat reply previews now only show text content for cleaner, more
focused message context display.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 10:59:39 +01:00
Alejandro
bff51857d6 feat: message ux improvements (#52)
* feat(chat): add Ctrl+Enter shortcut to send messages

Allows users to send messages using Ctrl+Enter (or Cmd+Enter on Mac)
in addition to the existing Enter key shortcut.

* fix(chat): disable autofocus on message editor

Prevents keyboard from popping up automatically on mobile devices.

---------

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 10:26:38 +01:00
Alejandro
a7682c757b Fix custom emoji rendering in compact view (#51)
Custom emoji were not displaying in compact event previews because
DefaultCompactPreview was passing only content string to RichText,
which strips emoji tag metadata needed for :shortcode: -> URL mapping.

Now passes full event object to preserve emoji tags when rendering
content, while still using plain text rendering for specific titles.

Fixes emoji display in compact rows and reply previews.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-12 09:07:56 +01:00
Alejandro Gómez
fc2e680afd feat: add reply functionality and require active account for composer
Reply Functionality:
- Added Reply button to each message (visible on hover)
- Button appears in message header next to timestamp
- Uses Reply icon from lucide-react
- Clicking reply sets the replyTo state with message ID
- Reply preview shows in composer when replying

Active Account Requirements:
- Check for active account using accountManager.active$
- Only show composer if user has active account
- Only enable reply buttons if user has active account
- Show "Sign in to send messages" message when no active account
- Prevent sending messages without active account

UI Improvements:
- Reply button uses opacity transition on hover (0 → 100)
- Positioned with ml-auto to align right in header
- Reply button only visible on group hover for clean UI
- Consistent styling with muted-foreground color scheme

Benefits:
- Users can reply to specific messages inline
- Clear indication when authentication is required
- Prevents errors from attempting to send without account
- Professional chat UX with hover interactions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 23:12:07 +01:00
Alejandro
938800c350 feat(RichText): extend parser with custom options and NIP reference support (#50)
* feat(RichText): extend parser with custom options and NIP reference support

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

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

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

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-11 22:21:13 +01:00
Alejandro Gómez
cfd897f96c feat: wait for EOSE before rendering messages to prevent scroll jumping
Message Loading Improvements:
- Use RxJS Subject to track EOSE (End Of Stored Events)
- Use skipUntil operator to delay timeline emissions until EOSE received
- Prevents scroll position jumping during initial message load
- Messages still update reactively after initial EOSE

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 22:12:53 +01:00
Alejandro Gómez
8f262e1189 feat: batch-load group metadata and handle unmanaged "_" groups
Improvements to kind 10009 (Public Chats) renderer:

Batch Loading:
- PublicChatsRenderer now batch-loads all group metadata (kind 39000) in a single subscription
- Much more efficient than individual subscriptions per group
- Filters out "_" groups from metadata fetch (they don't have metadata)
- Creates a metadata map to pass to each GroupLink

Special "_" Group Handling:
- "_" represents the unmanaged relay top-level group
- Displays the relay name instead of group ID
- Example: "pyramid.fiatjaf.com" instead of "_"

GroupLink Updates:
- Accepts optional metadata prop (pre-loaded from parent)
- No longer fetches metadata individually (more efficient)
- Extracts group name and icon from provided metadata
- Falls back to group ID if metadata not available

Performance:
- Single subscription for all groups vs N subscriptions
- Reduces relay traffic and improves rendering speed

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 22:10:22 +01:00
Alejandro Gómez
6b6605ded0 fix: pass proper ProtocolIdentifier object when opening group chats
GroupLink was passing a string identifier instead of a properly structured
ProtocolIdentifier object, causing chat window to fail opening.

Fixed to pass:
{
  protocol: "nip-29",
  identifier: {
    type: "group",
    value: groupId,
    relays: [relayUrl]
  }
}

This matches the expected ChatViewer props interface and allows the
NIP-29 adapter to properly resolve the conversation with the group relay.

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 22:04:41 +01:00
Alejandro Gómez
6d01ee33ef feat: implement unified chat system with NIP-C7 and NIP-29 support
Core Architecture:
- Protocol adapter pattern for chat implementations
- Base adapter interface with protocol-specific implementations
- Auto-detection of protocol from identifier format
- Reactive message loading via EventStore observables

Protocol Implementations:
- NIP-C7 adapter: Simple chat (kind 9) with npub/nprofile support
- NIP-29 adapter: Relay-based groups with member roles and moderation
- Protocol-aware reply message loading with relay hints
- Proper NIP-29 members/admins fetching using #d tags

UI Components:
- ChatViewer: Main chat interface with virtualized message timeline
- ChatMessage: Message rendering with reply preview
- ReplyPreview: Auto-loading replied-to messages from relays
- MembersDropdown: Virtualized member list with role labels
- RelaysDropdown: Connection status for chat relays
- ChatComposer: Message input with send functionality

Command System:
- chat command with identifier parsing and auto-detection
- Support for npub, nprofile, NIP-05, and relay'group-id formats
- Integration with window system and dynamic titles

NIP-29 Specific:
- Fetch kind:39000 (metadata), kind:39001 (admins), kind:39002 (members)
- Extract roles from p tags: ["p", "<pubkey>", "<role1>", "<role2>"]
- Role normalization (admin, moderator, host, member)
- Single group relay connection management

Testing:
- Comprehensive chat parser tests
- Protocol adapter test structure
- All tests passing (704 tests)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-11 21:38:23 +01:00
Alejandro
5233c57a1c feat: Add Zapstore app and app curation set renderers (#49)
* feat: Add Zapstore app and app curation set renderers

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

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

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

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

* feat: Add Zapstore release renderer (kind 30063)

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

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

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

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

All tests pass (744 total), build succeeds.

* refactor: Simplify Zapstore app renderers with platform icons

Improve Zapstore app rendering for cleaner, more intuitive display:

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

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

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

All tests pass (744 total), build succeeds.

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

Address feedback to improve Zapstore renderer UX:

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

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

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

All tests pass (744 total), build succeeds.

* refactor: Update Zapstore app set renderers with improved UX

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

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

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

* feat: Add releases section to app detail view

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

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

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

* fix: Remove author filter from releases query

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

---------

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

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

* Refactor: extract emoji shortcode regex to constant

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

---------

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

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

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

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

* style(emoji): simplify detail renderer layout

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-08 16:55:21 +01:00
Alejandro
ce6ec47b81 feat(EventMenu): add relay hints to Copy ID feature (#44)
Enhance the Copy ID feature in the event menu to include relay hints
from where the event has been seen. This improves event discoverability
by providing clients with relay suggestions when decoding nevent/naddr.

Changes:
- Import getSeenRelays from applesauce-core/helpers/relays
- Update copyEventId to retrieve seen relays for the event
- Pass relay hints to both neventEncode and naddrEncode
- Follows the same pattern as EventDetailViewer

Benefits:
- Better event sharing with relay context
- Helps other clients find events more easily
- Improves NIP-19 identifier quality

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

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

* refactor(calendar): improve feed renderer layout

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-07 21:20:04 +01:00
Alejandro
1d61d095a8 Fix command launcher in event preview (#42)
* fix(CommandLauncher): redirect to dashboard when executing commands from preview routes

NIP-19 preview routes (/nevent..., /npub..., etc.) don't render the
window system, so commands executed there appeared to do nothing.
Now detects preview routes and navigates to the dashboard before
creating the window, so the command actually takes effect.

* fix(test): add WebSocket polyfill for Node.js test environment

nostr-tools relay code requires WebSocket which isn't available in
Node.js by default. Adding the ws package polyfill prevents
"ReferenceError: WebSocket is not defined" errors in tests.

* fix(test): add @types/ws and fix type cast for WebSocket polyfill

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-07 11:09:59 +01:00
Alejandro
3e84feb450 feat(RichText): render gallery as 3-column grid for compact display (#41)
- Add new 'grid' preset to MediaEmbed with square aspect ratio and object-cover
- Update Gallery component to use CSS grid (3 cols) for images/videos
- Separate audio items into stacked layout below the grid
- Grid adapts to container width for varied event card sizes

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-07 10:40:21 +01:00
Alejandro
6707e2d7ba chore(deps): upgrade applesauce-react to ^5.0.1 (#40)
Update applesauce-react from ^5.0.0 to ^5.0.1 to get latest fixes.
React 19 (^19.2.1) is already in use.

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

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

* feat: upgrade applesauce libraries to v5

Major upgrade from applesauce v4 to v5 with breaking changes:

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

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

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

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 14:54:21 +01:00
Alejandro
c223deb4b9 feat: enhance Claude Code setup with slash commands, hooks, and verification (#38)
Add slash commands for common workflows:
- /commit-push-pr: Streamlined PR creation
- /verify: Full verification suite (lint + test + build)
- /test: Run tests with results summary
- /lint-fix: Auto-fix lint and formatting
- /review: Code review for quality and Nostr patterns

Update settings.json:
- Expand permissions for common safe bash commands
- Add PostToolUse hook for auto-formatting with Prettier

Update CLAUDE.md:
- Add Verification Requirements section
- Document available slash commands
- Emphasize running /verify before PRs

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 12:45:49 +01:00
Alejandro
f22cfc930f Add concise README to repository (#37)
* Add README.md with project overview and getting started guide

* Remove inaccurate offline support claim from README

---------

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

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

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

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

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

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

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

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

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

* fix: correct AddressPointer import and apply prettier formatting

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

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

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

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

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

* refactor: simplify NIP-89 detail renderers

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

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

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

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

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

* refactor: create reusable ExternalLink component for consistent styling

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

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

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

* refactor: consolidate JSON parsing into cached getAppMetadata helper

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

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

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

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

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

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-05 11:42:14 +01:00
Alejandro
a5040b8ab6 feat: render highlight comments with RichText component (#34)
* feat: render highlight comments with RichText component

Enable support for custom emoji, mentions, hashtags, and other rich text features in highlight comments by using the RichText component instead of plain text rendering.

Changes:
- HighlightRenderer: Use RichText for comment rendering with media/embeds disabled
- HighlightDetailRenderer: Add RichText import and use it for comment rendering

* chore: update package-lock.json

* fix: pass event to RichText for custom emoji tag support

Create synthetic events that preserve emoji tags from the original
highlight event while using the comment as content. This ensures
custom emoji in comments render correctly.

Changes:
- HighlightRenderer: Create commentEvent with emoji tags preserved
- HighlightDetailRenderer: Create commentEvent with emoji tags preserved

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-04 20:17:14 +01:00
Alejandro
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>
2026-01-04 20:12:48 +01:00