Commit Graph

265 Commits

Author SHA1 Message Date
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 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
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
Alejandro
a4eff14620 fix: don't show relay auth dialog when user is not logged in (#32)
Previously, the auth dialog would appear whenever a relay sent an AUTH
challenge, even when there was no active user session. This was confusing
for users who weren't logged in.

Now the shouldPromptAuth() method checks if there's an active account
before returning true, ensuring auth dialogs only appear for logged-in users.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-04 18:46:15 +01:00
Alejandro
38faf16723 fix: remove hardcoded text-sm from media placeholder in Link.tsx (#30)
The MediaPlaceholder component in Link.tsx had text-sm hardcoded,
causing it to appear larger than surrounding text in compact contexts.
Now all placeholder components (Link, Gallery, Mention) consistently
inherit font size from their parent.

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-28 17:26:22 +01:00
Alejandro
7241b3fb5a feat: add copy button for NIP markdown (#26)
* feat: add copy button for NIP markdown

- Add copy button to WindowToolbar for regular NIPs (appId: "nip")
  - Button appears in window toolbar next to edit button
  - Uses Copy/CopyCheck icons from lucide-react
  - Fetches NIP content via useNip hook
  - Shows toast notification on successful copy

- Add copy button to CommunityNIPDetailRenderer for community NIPs (kind 30817)
  - Button appears in header next to title
  - Copies event.content (markdown) to clipboard
  - Uses same Copy/CopyCheck icon pattern
  - Shows toast notification on successful copy

Both implementations use the existing useCopy hook for state management
and maintain consistent styling with other toolbar buttons.

* refactor: use Button component and remove misleading shortcuts

- Replace native button elements with Button component from shadcn/ui
  - Use variant="ghost" and size="icon" for consistent styling
  - Apply h-8 w-8 classes for uniform button sizing
  - Remove manual className styling in favor of component variants

- Remove misleading keyboard shortcut hints from button titles
  - Changed "Edit command (Cmd+E)" to "Edit command"
  - Changed "Close window (Cmd+W)" to "Close window"
  - These shortcuts don't actually work, so they were misleading

- Clean up imports and formatting
  - Format lucide-react imports across multiple lines
  - Add Button component import
  - Run prettier for consistent code style

* refactor: use link variant and remove size class overrides

- Change all Button components from variant="ghost" to variant="link"
- Remove className="h-8 w-8" overrides to use default Button sizing
- Maintains size="icon" for proper icon button behavior
- Applies to WindowToolbar and CommunityNIPDetailRenderer

* style: add muted color to window toolbar icon buttons

- Add className="text-muted-foreground" to all Button components in WindowToolbar
- Improves visual contrast for toolbar buttons
- Applies to Edit, Copy NIP, More actions, and Close window buttons

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-23 00:00:52 +01:00
Alejandro
e5a715aefe fix: memoize contact list pointer to fix $contacts alias (#25)
The $contacts alias in REQ command was broken due to an object reference issue.
The contact list pointer object was being created inline on every render, causing
useNostrEvent to treat it as a new pointer each time and disrupting the subscription.

Now properly memoize the pointer object so it only changes when accountPubkey or
needsAccount actually changes, ensuring stable subscriptions and proper contact
list retrieval.

Fixes the $contacts alias resolution in REQ commands.

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 23:07:04 +01:00
Alejandro
75500fa298 feat: add clickable example commands to welcome screen (#23)
* feat: add clickable example commands to welcome screen

Add 4 example commands to the welcome splash screen to make the client
more friendly on cold start and to new users:
- nip 29: View relay-based groups spec
- profile verbiricha@habla.news: Explore a Nostr profile
- req -k 1 -l 20: Query recent notes
- nips: Browse all NIPs

Commands are clickable and execute directly, opening the appropriate
window without requiring users to type or use the command palette.

Changes:
- Update GrimoireWelcome component with EXAMPLE_COMMANDS constant
- Add onExecuteCommand prop to handle direct command execution
- Update WorkspaceView to pass handleExecuteCommand callback
- Use parseAndExecuteCommand from command-parser for execution

* fix: lint

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-22 22:41:10 +01:00
Alejandro Gómez
5a97caffb1 nit: casing 2025-12-22 22:38:52 +01:00
Alejandro Gómez
abd0cc9750 Merge branch 'pr/feat--add-NIP-34-user-grasp-list-rendering(68a54c52)' 2025-12-22 22:32:56 +01:00
DanConwayDev
6853a2f7c9 feat: add NIP-34 user grasp list rendering
because you wanted to know what it feels like:
nostr:nevent1qvzqqqqqqypzqla9dawkjc4trc7dgf88trpsq2uxvhmmpkxua607nc5g6a634sv5qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309a6xsetxdaex2um59ehx7um5wgcjucm0d5hsqgxwfrfg9kvh99tj6adq5v2spwm9vhnp76zljuseqcpz4nd7w9d6pg000a03
2025-12-22 20:58:58 +00:00
Alejandro
7994b8aaca Merge pull request #24 from purrgrammer/claude/add-mit-license-wIFjZ
Add MIT license to the project
2025-12-22 21:41:31 +01:00
Claude
389be2df8f Add MIT license to the project
- Create LICENSE file with MIT license text
- Update package.json with license field
- Copyright holder: Alejandro Gómez
2025-12-22 20:40:16 +00:00
Alejandro
363840b756 Merge pull request #20 from purrgrammer/claude/improve-reqviewer-state-machine-cBkEO
docs: add comprehensive ReqViewer state machine analysis and improvement plan
2025-12-22 19:51:21 +01:00
Alejandro Gómez
9f0c383ff8 fix: add icon back 2025-12-22 19:46:12 +01:00
Alejandro Gómez
c4bc3ab445 ui: improve relay tooltip, update docs 2025-12-22 19:43:00 +01:00
Alejandro Gómez
3f1c66ec01 ui: adjustments 2025-12-22 19:33:53 +01:00
Claude
b9756b119b refactor: simplify relay list UI with compact status indicators and rich tooltips
**Compact Relay Item Display:**
- Removed left-side inbox/outbox count indicators (were causing misalignment)
- Replaced "EOSE" text with checkmark icon (✓)
- Event count shown as [N] badge (only if > 0)
- Auth icon now always visible (even for unauthenticated relays)
- Clean right-side layout: [count] [✓] [auth] [wifi]

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

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

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

All 639 tests passing.
2025-12-22 18:23:12 +00:00
Claude
af8cf427d6 fix: implement per-relay EOSE detection by subscribing to relays individually
**CRITICAL FIX for EOSE detection:**

**The Problem:**
- Used pool.subscription(relays, filters) which creates a RelayGroup
- RelayGroup tracks per-relay EOSE internally but only emits ONE "EOSE" when ALL relays finish
- This caused:
  1. EOSE indicators taking forever to appear (waiting for slowest relay)
  2. REQ stuck in LOADING state when fast relays finish but slow relays never do
  3. No way to show per-relay EOSE status accurately

**The Solution:**
Subscribe to each relay individually using pool.relay(url).subscription():
- Each relay subscription emits its own EOSE immediately when that relay finishes
- We track per-relay EOSE in relayStates map with accurate timing
- Overall EOSE is derived when ALL relays reach terminal state (eose/error/disconnected)
- EOSE indicators now appear immediately as each relay finishes

**Technical Details:**
- Changed from: pool.subscription(relays, filters)
- Changed to: relays.map(url => pool.relay(url).subscription(filters))
- Added eoseReceivedRef to track overall EOSE in closures
- Mark specific relay as EOSE when that relay emits "EOSE"
- Calculate overall EOSE when all relays in terminal states
- Use url from subscription context (more reliable than event._relay)

**Benefits:**
 Instant per-relay EOSE indicators (no waiting for slowest relay)
 Accurate relay state tracking (each relay independent)
 REQ transitions to LIVE/CLOSED as soon as all relays finish
 Better user feedback (see which relays are done vs still loading)

All 639 tests passing.
2025-12-22 18:10:52 +00:00
Claude
b95ce25955 refactor: normalize hardcoded relay URLs and reorganize relay dropdown UI
**Normalize All Relay URLs:**
- Added trailing slashes to AGGREGATOR_RELAYS constants
- Ensures consistency with RelayStateManager's normalization
- Fixes fallback relay connection state tracking issue
- All hardcoded relay URLs now match normalized keys in relayStates

**Reorganize Relay Item UI:**
- Removed type indicator icons (LinkIcon/Sparkles/Inbox) from individual relay items
- Strategy type is already shown in header, no need to repeat per-item
- Moved inbox/outbox indicators from right side to left side of relay URL
- Left side now shows: inbox count (Mail icon) and/or outbox count (Send icon)
- Right side shows: event count, EOSE indicator, auth status, connection status
- Cleaner, more semantic layout with better visual hierarchy

**Why This Matters:**
The relay URL normalization fix ensures that fallback relays (AGGREGATOR_RELAYS)
now show accurate connection state in the UI. Previously, the non-normalized
URLs couldn't match keys in relayStates, making them appear disconnected even
when connected. This was the root cause of the "fallback relays not tracking"
issue.

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

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

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

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

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

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

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

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

Tests: 639/639 passing (added 5 new edge case tests)
2025-12-22 17:38:31 +00:00
Alejandro
21e972600c Merge pull request #22 from purrgrammer/claude/fix-command-overflow-BOH8k
fix: prevent command text overflow in spell dialogs
2025-12-22 18:10:36 +01:00
Claude
e904fcbaf1 fix: prevent command text overflow in spell dialogs
Replace `break-all` with `break-words overflow-x-auto` to improve
text wrapping behavior in command preview areas. This prevents
words from breaking awkwardly in the middle while still allowing
long commands to be displayed properly.

Changes:
- CreateSpellDialog.tsx: Fixed command preview overflow
- SpellDialog.tsx: Fixed command display overflow
- SpellRenderer.tsx: Fixed detail view command overflow

The new approach breaks at word boundaries when possible and
provides horizontal scrolling for exceptionally long commands.
2025-12-22 17:00:24 +00:00
Claude
70651ae29f fix: improve relay state tracking and add relay type indicators
State Tracking Fixes:
- Sync connection state for ALL relays in query, not just initialized ones
- Defensively initialize missing relay states during sync
- Handle events from unknown relays (defensive initialization)
- Add debug console logs to track state transitions

Relay Type Indicators:
- Explicit relays: Blue link icon (relays specified directly)
- Outbox relays: Purple sparkles (NIP-65 selected)
- Fallback relays: Gray inbox icon (fallback when outbox incomplete)
- Each type has tooltip explaining source

This should fix:
- "0/4 relays but events coming in" bug
- "Stuck in LOADING" when events are arriving
- Missing visibility for relay source types

Tests: 634/634 passing
2025-12-22 16:36:56 +00:00
Claude
c9bf2fe599 fix: show ALL queried relays in dropdown (outbox + fallback + explicit)
Previously only showed relays from NIP-65 reasoning array, missing fallback
relays. Now always iterates over finalRelays (actual queried relays) and
looks up NIP-65 info if available.

Fixes:
- Fallback relays now visible in dropdown
- Relays show connection/subscription status regardless of source
- NIP-65 info (inbox/outbox counts) shown when available
- Works for outbox, fallback, and explicit relay configurations

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

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

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

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

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

Tests: 27/27 passing including edge case scenarios
2025-12-22 16:18:15 +00:00
Claude
bebb4ed834 docs: add comprehensive ReqViewer state machine analysis and improvement plan
Analysis document:
- Identified critical bug in applesauce-relay catchError handling
- Documented 7 edge cases causing "LIVE with 0 relays" issue
- Root cause: relay disconnections treated as EOSE messages
- Detailed Nostr protocol semantics and applesauce behavior

Implementation plan:
- Hybrid approach: RelayStateManager + event metadata tracking
- New state types: ReqRelayState, ReqOverallState
- Enhanced hook: useReqTimelineEnhanced with per-relay tracking
- 3-phase rollout: infrastructure → UI → testing
- Comprehensive state machine with 8 query states, 8 relay states

This provides the foundation for production-quality REQ status tracking
that accurately handles disconnections, timeouts, and partial failures.
2025-12-22 15:59:00 +00:00
Alejandro
0d8f8b7807 Merge pull request #17 from purrgrammer/claude/applesauce-helpers-investigation-EWmaW
docs: add applesauce helpers investigation and refactoring plan
2025-12-22 15:19:41 +01:00
Claude
c40c4ae1f9 fix: TypeScript error in useStableFilters 2025-12-22 14:05:53 +00:00
Claude
912794f4e0 chore: lint fix 2025-12-22 14:04:46 +00:00
Claude
33539c291b refactor: use applesauce helpers for pointer parsing and filter comparison
Phase 2 & 3 of applesauce helpers refactoring plan.

**Phase 2: Replace manual pointer parsing**
- ReactionRenderer.tsx: replaced manual coordinate parsing with parseCoordinate helper
- Benefits: more robust, handles edge cases, consistent with applesauce patterns

**Phase 3: Improve filter comparison**
- useStable.ts: replaced JSON.stringify with isFilterEqual for filter comparison
- Benefits: handles undefined values correctly, supports NIP-ND AND operator
- Implementation uses ref pattern to maintain stable reference when filters are equal

All tests pass (607 tests).
2025-12-22 14:04:09 +00:00