Commit Graph

408 Commits

Author SHA1 Message Date
Alejandro
f3cc7779e3 feat: make NWC connections more robust and wallet state reactive (#227)
* fix(nwc): improve connection reliability and add health tracking

- Add connection status observable (disconnected/connecting/connected/error)
- Validate wallet connection on restore using support$ observable with 10s timeout
- Add notification subscription error recovery with exponential backoff (5 retries)
- Add retry logic for balance refresh (3 retries with backoff)
- Use library's support$ observable for wallet capabilities (cached by applesauce)
- Replace manual getInfo() calls with reactive support$ subscription
- Add visual connection status indicator in WalletViewer header
- Add reconnect button when connection is in error state
- Store network info in cached connection for display

https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ

* feat(wallet): add copy NWC connection string button to header

Adds a copy button (Copy/Check icons) in the wallet header before the
refresh button that copies the NWC connection string to clipboard for
easy sharing or backup.

https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ

* fix(wallet): use CopyCheck icon and fix transaction loading

- Change Check to CopyCheck icon for copy NWC string button
- Add walletMethods computed value that combines support$ observable
  with cached info fallback from initial connection
- Fix transaction history not loading because support$ waits for
  kind 13194 events which many NWC wallets don't publish
- The cached info from getInfo() RPC call is now used as fallback

https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ

* refactor(nwc): simplify with derived state from observables

Production-ready refactor of NWC implementation:

nwc.ts:
- Add wallet$ observable for reactive wallet instance access
- Remove redundant subscribeToSupport() - only needed for validation
- Cleaner code organization with clear sections

useWallet.ts:
- All state derived from observables (no useState for wallet)
- Move walletMethods computation to hook (reusable)
- isConnected derived from connectionStatus
- Simplified from 240 to 170 lines

WalletViewer.tsx:
- Use walletMethods from hook instead of local useMemo
- Simpler connection state tracking via connectionStatus
- Remove redundant wallet variable from destructuring
- No color change on copy NWC string (per feedback)

https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ

* refactor(wallet): use useCopy hook for clipboard operations

Replace manual clipboard state management with useCopy hook:
- copyInvoice/invoiceCopied for generated invoice
- copyRawTx/rawTxCopied for transaction JSON
- copyNwc/nwcCopied for NWC connection string

Benefits:
- Cleaner code (removed manual setTimeout calls)
- Automatic timeout cleanup on unmount
- Consistent copy behavior across all clipboard operations

https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ

* feat(wallet): implement lazy-loaded transactions observable

- Add shared wallet types (Transaction, TransactionsState) in src/types/wallet.ts
- Add transactionsState$ observable to NWC service for shared tx state
- Implement loadTransactions, loadMoreTransactions, and retryLoadTransactions
- Auto-refresh transactions on payment notifications
- Simplify WalletViewer to use observable state instead of local state
- Remove manual transaction loading logic from component

https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ

* fix(wallet): update balance observable on initial connect

- Call refreshBalance() in createWalletFromURI to fetch initial balance
- Update balance$ directly when ConnectWalletDialog gets balance
- Fixes issue where WalletViewer showed "-" after connecting while
  user menu showed correct balance (different data sources)

https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ

* refactor(wallet): use single data source for balance across UI

Remove fallback to nwcConnection.balance in user-menu - now both
WalletViewer and user-menu use balance$ observable as the single
source of truth for wallet balance.

https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ

* fix(wallet): address code review issues and simplify user menu

- Fix memory leak: track retry timeout and clear on disconnect
- Add explicit WalletSupport type for support observable
- Add comments explaining balance refresh error handling behavior
- Add comment about restoreWallet not being awaited intentionally
- User menu now uses connectionStatus observable (shows connecting/error states)
- Remove wallet name display from user menu (simplifies UI)
- Remove unused walletServiceProfile hook and getWalletName function

https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ

* refactor(wallet): extract WalletConnectionStatus component

- Create reusable WalletConnectionStatus component for connection indicator
- Remove rounded borders from indicator (now square)
- Export getConnectionStatusColor helper for custom usage
- Use component in both user-menu and WalletViewer
- Supports size (sm/md), showLabel, and className props

https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 18:02:40 +01:00
Alejandro
9b883f1173 feat: show [gallery] placeholder when images disabled in galleries (#226)
When images are turned off in the RichText options, galleries now show
a single [gallery] placeholder instead of multiple [image] placeholders.
This provides a cleaner UI for users who have disabled image loading.

https://claude.ai/code/session_01LAhYP5iRQskJC4XWi6MHvf

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 13:26:24 +01:00
Alejandro
f9733878e2 Hide scrollbars completely by setting width and height to 0 (#225)
* fix: hide tabbar scrollbar by setting width/height to 0

The no-scrollbar utility's display:none wasn't fully overriding the
global scrollbar styles that set width/height to 8px. Adding explicit
width:0 and height:0 ensures the scrollbar is completely hidden in
WebKit browsers.

https://claude.ai/code/session_018RiPf74GNf2oWcoYNRoZyx

* fix: use !important to override global scrollbar styles

The global * selector's scrollbar-width: thin was overriding the
no-scrollbar utility due to CSS cascade order. Adding !important
ensures the utility always wins.

https://claude.ai/code/session_018RiPf74GNf2oWcoYNRoZyx

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 13:02:17 +01:00
Fernando López Guevara
0b3c657705 feat(req-viewer): added today on since/until, added window-title (#221) 2026-01-29 10:20:47 +01:00
Alejandro
fdc7b1499f fix: build proper q-tag with relay hint and author pubkey for replies (#224)
* fix: build proper q-tag with relay hint and author pubkey for replies

When sending replies in NIP-29 and NIP-C7 adapters, now build the full
q-tag format per NIP-C7 spec: ["q", eventId, relayUrl, pubkey]

Previously only the event ID was included, making it harder for clients
to fetch the referenced event. Now:
- NIP-29: includes group relay URL and author pubkey
- NIP-C7: includes seen relay hint and author pubkey

https://claude.ai/code/session_01Jy51Ayk57fzaFuuFFm1j1K

* chore: remove unused NIP-C7 adapter

The NIP-C7 adapter was already disabled/commented out everywhere.
Removing the file to reduce dead code.

https://claude.ai/code/session_01Jy51Ayk57fzaFuuFFm1j1K

* chore: remove NIP-C7 references from docs and code

- Remove nip-c7 from ChatProtocol type
- Remove commented NIP-C7 adapter imports and switch cases
- Update comments to reference NIP-29 instead of NIP-C7
- Update kind 9 renderer docs to reference NIP-29
- Clean up chat-parser docs and error messages

https://claude.ai/code/session_01Jy51Ayk57fzaFuuFFm1j1K

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-28 20:29:11 +01:00
Alejandro Gómez
f987ec7705 fix: increase menu button size on mobile 2026-01-28 12:28:37 +01:00
Alejandro Gómez
ee19cdb8fe fix: use dynamic viewport sizes 2026-01-28 12:28:12 +01:00
Alejandro
2ad3f90174 Improve mobile UX with larger touch targets (#223)
- Add useIsMobile hook for viewport detection
- TabBar: larger height (48px), disable reorder on mobile, hide workspace numbers
- Header buttons: larger touch targets for SpellbookDropdown, UserMenu, LayoutControls
- Window toolbar: larger buttons (40px) on mobile
- Mosaic dividers: wider (12px) on mobile for easier dragging
- CommandLauncher: larger items, footer text, hide kbd hints on mobile
- Editor suggestions: responsive widths, min-height 44px touch targets
- EventFooter: larger kind/relay buttons on mobile
- CodeCopyButton: larger padding and icon on mobile
- MembersDropdown: larger trigger and list items on mobile

All changes use mobile-first Tailwind (base styles for mobile, md: for desktop)
to meet Apple HIG 44px minimum touch target recommendation.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 12:19:31 +01:00
Alejandro
a13b990e31 Fix tooltip contrast in editor suggestion lists (#222)
Use text-popover-foreground instead of text-muted-foreground for
suggestion list components to ensure proper contrast with bg-popover
background across all themes.

https://claude.ai/code/session_0145zLfku7idq3WEqGzvTdvu

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-28 11:35:58 +01:00
Alejandro
3282581636 Add follow mode (-f) to REQ command (#220)
* feat: add -f (follow) option to req command

Add tail -f style auto-refresh behavior to the req command. When enabled,
new events are automatically displayed instead of being buffered behind
a "X new events" button.
2026-01-27 13:47:19 +01:00
Alejandro
d69cc1fec6 Migrate from Tailwind CSS v3 to v4 (#219)
- Replace JS config (tailwind.config.js) with CSS-first @theme directive
- Add @tailwindcss/vite plugin for improved Vite integration
- Update src/index.css with v4 syntax (@import, @theme, @utility)
- Convert @layer utilities to @utility syntax
- Fix hardcoded scrollbar colors in command-launcher.css
- Add Tailwind v4 skill document (.claude/skills/tailwind-v4.md)
- Update CLAUDE.md with Tailwind v4 quick reference

https://claude.ai/code/session_01T6RenqDof8br6Nt9aKcjvq

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 11:24:35 +01:00
Alejandro
34bad20ce9 Fix logo SVG centering and regenerate icons (#218)
The SVG path extended slightly beyond the viewBox (bezier control points
reached x=121.464 while viewBox ended at x=121), while the left edge was
flush at x=0. This caused uneven spacing.

Changed viewBox from "0 0 121 160" to "-0.5 0 122 160" to add equal
0.5px margins on both sides, properly centering the logo content.

https://claude.ai/code/session_019PGCQHRovoNE81udohhU3R

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 11:07:43 +01:00
Alejandro
a28ffc1ec3 fix: filter invalid relay URLs from event tags (#217)
* fix: filter invalid relay URLs from event tags

Add validation to prevent invalid URLs from being used as relay hints.
The issue occurred when "r" tags containing non-relay URLs (like
https://(strangelove@basspistol.org/) were being extracted and used
as relay connection targets.

Changes:
- Add isValidRelayURL() helper to validate relay URLs (must have ws://
  or wss:// protocol and valid URL structure)
- Update extractRelayContext() in loaders.ts to filter r-tags, e-tag
  relay hints, and a-tag relay hints using the new validator
- Add comprehensive tests for isValidRelayURL()

https://claude.ai/code/session_01Ca2fKD2r4wHKRD8rcRohj9

* refactor: use applesauce isSafeRelayURL for relay URL validation

Refactor relay URL validation to use applesauce's isSafeRelayURL helper
which provides a fast regex-based check for valid websocket URLs.

Changes:
- Update isValidRelayURL in relay-url.ts to use isSafeRelayURL as fast
  path, with URL constructor fallback for IP addresses
- Re-export isSafeRelayURL from relay-url.ts for convenience
- Update loaders.ts to use isSafeRelayURL directly from applesauce
- Add relay URL validation to:
  - nostr-utils.ts: getEventPointerFromQTag (q-tag relay hints)
  - zapstore-helpers.ts: getAppReferences (a-tag relay hints)
  - nip89-helpers.ts: getHandlerReferences (a-tag relay hints)
  - PublicChatsRenderer.tsx: extractGroups (group relay URLs)

This ensures consistent validation across all relay URL extraction points
using applesauce's battle-tested validation.

https://claude.ai/code/session_01Ca2fKD2r4wHKRD8rcRohj9

* chore: remove unused isSafeRelayURL re-export

The re-export was added but all consumers import directly from
applesauce-core/helpers/relays instead.

https://claude.ai/code/session_01Ca2fKD2r4wHKRD8rcRohj9

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 10:59:26 +01:00
Alejandro
3f3ebcf5f6 Fix duplicate client tag in spell encoding (#215)
The client tag was being added twice to spells:
1. Unconditionally in encodeSpell() in spell-conversion.ts
2. Conditionally in publish-spell.ts based on user settings

Removed the unconditional addition in encodeSpell() so the client tag
is only added once by publish-spell.ts when the user setting is enabled.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-26 12:44:42 +01:00
Alejandro
569388c135 Add PWA icon generation and Grimoire logo component (#214)
* feat: add Grimoire logo SVG and generate PWA assets

- Add logo.svg with the official Grimoire logo and gradient
- Create GrimoireLogo React component for use in the app
- Add scripts/generate-pwa-icons.mjs to generate all PWA icons from SVG
- Regenerate all favicon and PWA icons from the new logo
- Update mobile welcome screen to show the logo instead of text

* feat: use transparent backgrounds for PWA icons and add theme gradient option

- Update generate-pwa-icons.mjs to output PNGs with transparent backgrounds
- Add gradient prop to GrimoireLogo component ("original" or "theme")
- Theme gradient matches text-grimoire-gradient CSS (yellow -> orange -> purple -> cyan)
- Mobile welcome screen now uses theme gradient to match ASCII art

* feat: use original gradient for mobile welcome logo

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-26 11:46:17 +01:00
Alejandro
d1ccd930ff Fix RichText prop in Kind9802Renderer to pass event object (#213)
* fix: pass source event to RichText in highlight feed preview

The source event preview in HighlightRenderer was only passing the
content string to RichText, which meant custom emoji tags from the
source event weren't processed. Now passes the source event with
the preview content to enable proper emoji and tag-based rendering.

* refactor: use CSS truncation for highlight source preview

- Pass sourceEvent directly for notes instead of extracting content
- Only create synthetic event with title for articles
- CSS line-clamp-1 and overflow-hidden handle truncation
- Media and event embeds remain disabled

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-24 10:17:46 +01:00
Alejandro
e008d76021 feat: NIP-34 status events (#209)
* feat(nip34): Add NIP-34 issue status renderers and locale-aware formatting

- Add IssueStatusRenderer for feed view (kinds 1630-1633: Open/Resolved/Closed/Draft)
- Add IssueStatusDetailRenderer for detail view with status badge and embedded issue
- Update IssueRenderer/IssueDetailRenderer to fetch and display current issue status
- Status validation respects issue author, repo owner, and maintainers
- Add status helper functions to nip34-helpers.ts (getStatusType, findCurrentStatus, etc.)
- Use parseReplaceableAddress from applesauce-core for coordinate parsing
- Expand formatTimestamp utility with 'long' and 'datetime' styles
- Fix locale-aware date formatting across all detail renderers
- Update CLAUDE.md with useLocale hook and formatTimestamp documentation

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* refactor(nip34): Use theme semantic colors for issue status

Replace hardcoded colors with theme semantic colors:
- Resolved/merged: accent (positive)
- Closed: destructive (negative)
- Draft: muted
- Open: neutral foreground

Also fixes import placement in nip34-helpers.ts.

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* fix(nip34): Use repository relays instead of AGGREGATOR_RELAYS

Status events for issues are now fetched from the relays configured
in the repository definition, not from hardcoded aggregator relays.

This respects the relay hints provided by repository maintainers for
better decentralization and reliability.

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* perf(nip34): Add memoization caching to helper functions

Use getOrComputeCachedValue from applesauce-core to cache computed
values on event objects. This prevents redundant computation when
helpers are called multiple times for the same event.

Also added documentation in CLAUDE.md about best practices for
writing helper libraries that compute data from Nostr events.

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* fix(nip34): Add relay fallback chain for status event fetching

Status events now use a fallback chain for relay selection:
1. Repository configured relays (from "relays" tag)
2. Repo author's outbox relays (from kind:10002)
3. AGGREGATOR_RELAYS as final fallback

This ensures status events can be fetched even when repository
doesn't have relays configured.

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* feat(nip34): Add status rendering to Patch and PR renderers

- PatchRenderer and PatchDetailRenderer now show merge/closed/draft status
- PullRequestRenderer and PullRequestDetailRenderer now show merge/closed/draft status
- Status events fetched from repository relays with author outbox fallback
- For patches and PRs, kind 1631 displays as "merged" instead of "resolved"
- Fixed destructive color contrast in dark theme (30.6% -> 50% lightness)

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* refactor(nip34): Extract StatusIndicator component, improve UI layout

- Create reusable StatusIndicator component for issues/patches/PRs
- Move status icon next to status text in feed renderers (not title)
- Place status badge below title in detail renderers
- Fix dark theme destructive color contrast (0 90% 65%)
- Remove duplicate getStatusIcon/getStatusColorClass functions

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* fix(nip34): Make status badge width fit content

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* fix(theme): Improve destructive color contrast on dark theme

Increase lightness from 65% to 70% for better readability.

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* fix(theme): Use lighter coral red for destructive on dark theme

Changed to 0 75% 75% (~#E89090) for better contrast against #020817 background.

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* docs: Fix applesauce helper documentation in CLAUDE.md

- Fix parseCoordinate -> parseReplaceableAddress (correct function name)
- Clarify getTagValue (applesauce) vs getTagValues (custom Grimoire)
- Add getOrComputeCachedValue to helpers list
- Improve code example with proper imports and patterns

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* fix(nip34): Render status event content as rich text

Use MarkdownContent component for status event content in
Issue, Patch, and PR detail renderers.

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* fix(nip34): Smaller status indicators, improve issue feed layout

- Use shared StatusIndicator in IssueStatusRenderer (smaller size)
- Render status event content as markdown
- Put status on its own line between title and repo in IssueRenderer

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

* fix(nip34): Use warning color for closed status instead of destructive

- Change closed status from red (destructive) to orange (warning)
- Improve dark theme status colors contrast (warning: 38 92% 60%)
- Less aggressive visual for closed issues/patches/PRs

https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-23 15:31:20 +01:00
Alejandro Gómez
85ab0a1587 fix: add a-tag relays to event resolution 2026-01-23 14:58:21 +01:00
Alejandro
9b36120dfe Add NIP-56 Report (Kind 1984) renderer and helpers (#210)
* Add NIP-56 Report renderer (kind 1984)

- Add nip56-helpers.ts with report parsing and type definitions
- Add ReportRenderer for displaying report events in feeds
- Support profile, event, and blob report targets
- Display report type with color-coded badges and icons
- Show embedded reported event when available

https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ

* refactor(nip56): apply applesauce caching and neutral styling

- Use getOrComputeCachedValue for report parsing (applesauce pattern)
- Rename parseReport to getReportInfo for consistency
- Use muted/neutral colors for all report type icons and badges
- Use QuotedEvent component for embedding reported events
- Remove unnecessary useMemo (helper caches internally)

https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ

* refactor(nip56): use collapsed quote and cleaner copy

- Use "Reported <username> for <reason>" format
- Remove redundant "Event by:" line for event reports
- Use depth=2 for QuotedEvent to show collapsed by default
- Content may be disturbing, user can expand on demand

https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ

* feat(nip56): hide preview and clickable header

- Add hidePreview prop to QuotedEvent for sensitive content
- Hide text preview when collapsed, show "Click to reveal content"
- Make report header clickable to open report detail
- UserName stops propagation so clicking username opens profile

https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ

* style(nip56): use dotted underline hover, reduce spacing

- Remove background highlight on hover
- Use underline dotted with cursor crosshair (consistent with app)
- Reduce gap between header and quoted event

https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ

* refactor(nip56): use RichText for comment and applesauce helpers

- Render report comments using RichText like kind 1 notes
- Use getTagValue/getTagValues helpers instead of direct tag access
- Add explanatory comments where direct tag access is still required

https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-23 13:53:57 +01:00
Alejandro
7838b0ab98 Add NIP-88 Poll support with renderers and helpers (#207)
* feat(nip88): add poll event renderers

Implement NIP-88 poll events support with:
- PollRenderer: Feed view for kind 1068 polls showing question, options, type
- PollDetailRenderer: Detail view with live vote counts and percentages
- PollResponseRenderer: Feed view for kind 1018 showing voted options
- nip88-helpers: Utilities for parsing poll data and counting votes

* fix(nip88): use RelayLink component and differentiate poll type icons

- Use ListCheck icon for single choice, ListChecks for multi choice
- Replace relay text spans with clickable RelayLink components
- Shorten "Vote on these relays" to "Relays"

* refactor(nip88): production readiness improvements

- Add symbol-based caching to all helper functions using getOrComputeCachedValue
- Use QuotedEvent component for embedded polls in PollResponseRenderer
- Simplify PollResponseRenderer by leveraging QuotedEvent's loading states
- Add clear documentation about what can/cannot be cached

* fix(nip88): restore option label resolution in PollResponseRenderer

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 22:39:31 +01:00
Alejandro
459159faca feat(kinds): add search box to KINDS command (#206)
Add a search input to KindsViewer matching the style from NipsViewer.
Users can now filter kinds by number, name, or description. Supports
autofocus on mount, clear button, and Escape to clear.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 22:10:30 +01:00
Alejandro
4e8a8a0e90 feat(chat): make input editor expandable up to 3 lines (#204)
The chat input now grows as you type, from 1 line up to approximately
3 lines before showing a scrollbar. This improves UX when composing
longer messages by providing better visibility of the content.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 20:53:01 +01:00
Alejandro Gómez
0ebe1ec3da ai: agents file 2026-01-22 18:38:26 +01:00
Alejandro Gómez
32d584090f fix: unused import 2026-01-22 18:36:06 +01:00
Alejandro
f551604866 Add profile fallback for pubkey-based NIP-29 group IDs (#203)
* feat: add profile metadata fallback for NIP-29 groups

When a NIP-29 group ID is a valid pubkey and the relay doesn't support
NIP-29 (no kind 39000 metadata), fall back to using the pubkey's profile
metadata (kind 0) for group name, description, and icon.

This allows users to create simple group chats using their pubkey as the
group identifier on relays that don't have full NIP-29 support.

Changes:
- Add isValidPubkey() helper to validate 64-char hex strings
- Modify resolveConversation() to fetch profile when metadata is missing
- Add comprehensive tests for pubkey validation and parsing
- Prefer NIP-29 metadata over profile fallback when available

Tests: 20 NIP-29 adapter tests passing, 1037 total tests passing
Build: Successful

* refactor: extract group metadata resolution into shared helper

Refactored profile fallback logic into a reusable helper that both
NIP-29 adapter and GroupListViewer can use. This fixes the issue where
GroupListViewer wasn't benefiting from the profile metadata fallback.

Changes:
- Created shared `group-metadata-helpers.ts` with:
  - `isValidPubkey()` - validates 64-character hex strings
  - `resolveGroupMetadata()` - unified metadata resolution with fallback
  - `ResolvedGroupMetadata` type with source tracking
- Updated NIP-29 adapter to use shared helper (DRY)
- Updated GroupListViewer to resolve metadata with profile fallback
  - Added resolvedMetadata to GroupInfo interface
  - Added useEffect to resolve metadata for all groups
  - Updated GroupListItem to use resolved metadata

Priority:
1. NIP-29 metadata (kind 39000) if available
2. Profile metadata (kind 0) if groupId is a valid pubkey
3. Fallback to groupId as name

Tests: All 1037 tests passing
Build: Successful

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 18:29:56 +01:00
Alejandro
7cf75c648c feat(nip-66): add relay discovery and monitor announcement renderers (#172)
* feat(nip-66): add relay discovery and monitor announcement renderers

Implements NIP-66 support to display relay health metrics and monitoring
information. Users can now view relay performance data (RTT, network type,
supported NIPs) and monitor announcements to make informed decisions about
relay selection and reliability.

Includes 58 comprehensive tests for all helper functions and event parsing.

* refactor(nip-66): improve UI with Label, NIPBadge, and clickable titles

Enhance NIP-66 renderers with better UI components:
- Use NIPBadge component for clickable NIP numbers
- Replace section headers with Label component for consistency
- Add ClickableEventTitle to monitor announcements
- Improve requirement icons with CheckCircle/XCircle for clarity
- Add proper icons throughout for better visual hierarchy

* refactor(nip-66): use Hammer icon for PoW requirements

Replace Zap (lightning bolt) icon with Hammer icon for proof-of-work
indicators to better represent the mining/work metaphor. Updates both
feed and detail renderers for relay discovery events.

* refactor(nip-66): improve feed UI with clickable titles and simplified layout

- Add ClickableEventTitle to relay discovery feed items for opening detail view
- Remove "Monitoring" label from relay monitor feed items for cleaner layout
- Remove unused imports (RelayLink, Label, Activity) from feed renderers
- Maintain existing Label and NIPBadge usage in detail renderers

* refactor(nip-66): add Label component for check types in monitor feed

Add "Check Types" label to relay monitor feed renderer for better
visual hierarchy and consistency with detail renderer.

* refactor(nip-66): remove Check Types label from monitor feed

Remove label title to simplify monitor feed layout - check type badges
are displayed directly without a header for cleaner appearance.

* refactor(nip-66): use Label component for individual check types in monitor feed

Replace Badge components with Label components for check types to match
the design system and provide better visual consistency.

* refactor(nip-66): rename components to human-readable names

Rename NIP-66 component exports to match established naming convention:
- Kind10166Renderer → MonitorAnnouncementRenderer
- Kind10166DetailRenderer → MonitorAnnouncementDetailRenderer
- Kind30166Renderer → RelayDiscoveryRenderer
- Kind30166DetailRenderer → RelayDiscoveryDetailRenderer

This follows the pattern used elsewhere (e.g., LiveActivityRenderer vs Kind30311Renderer) to make code more readable without memorizing kind numbers.

* refactor(nip-66): extract relay kinds display into reusable component

Create RelayKindsDisplay component to show accepted/rejected kinds in a
consistent format across detail views. Used in RelayDiscoveryDetailRenderer
to reduce code duplication and improve maintainability.

* refactor(nip-66): extract supported NIPs display into reusable component

Create RelaySupportedNips component to show relay-supported NIPs in a
consistent format. Used in RelayDiscoveryDetailRenderer to reduce code
duplication and improve maintainability.

* refactor(nip-66): add icon to supported NIPs component for consistent styling

Add FileText icon to RelaySupportedNips Label to match the visual
hierarchy pattern used in other relay detail sections (Performance
Metrics, Characteristics, Requirements, etc.).

* refactor(nip-66): use nicer NIP rendering from RelayViewer in shared component

Update RelaySupportedNips component to match RelayViewer's nicer styling:
- Show NIP names alongside numbers (showName=true) for better readability
- Use gap-2 for better spacing
- Use h3 title styling instead of Label with icon
- Make component reusable with optional title customization
- Use in both RelayViewer and RelayDiscoveryDetailRenderer

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 17:54:16 +01:00
Alejandro
8e4442ae34 feat: improve embedded event resolution with relay hints (#200)
* fix: parse NIP-29 q-tags with relay hints for embedded event resolution

The issue was that q-tag relay hints were being discarded when parsing
chat messages. When a message quoted another event, only the event ID
was extracted from the q-tag, ignoring the relay URL hint at tag[2].

Changes:
- Add getEventPointerFromQTag and getQuotePointer helpers to nostr-utils
- Update Message.replyTo type from string to EventPointer | AddressPointer
- Update all chat adapters (NIP-29, NIP-10, NIP-53, NIP-C7) to parse
  full pointers with relay hints
- Update loadReplyMessage signature to accept EventPointer with relays
- Update ReplyPreview to pass full pointers to event loading
- Update ChatView and ChatViewer to use new pointer-based q-tag parsing

This ensures embedded events (quotes/replies) are fetched using the
relay hint from the q-tag when the event is not found in the local store.

* fix: pass full EventPointer with relay hints through embed chain

The previous commit fixed q-tag parsing in chat adapters, but the
RichText/Mention chain was also discarding relay hints from nevent.

EventEmbed was only extracting `pointer.id` and passing it as a string
to QuotedEvent, losing the relay hints. Same issue in EmbeddedEvent.

Changes:
- Update QuotedEvent props: eventPointer instead of eventId string
- Update EmbeddedEvent props: eventPointer instead of eventId string
- Update EventEmbed to pass full pointer to QuotedEvent
- Update MarkdownContent to pass full pointers for note/nevent
- Update EventRefList to pass full pointer instead of just ID
- Update HighlightDetailRenderer to pass full eventPointer
- Update RepostRenderer to extract e-tag as EventPointer with hints

Now nevent mentions in content like nostr:nevent1... correctly use
the relay hints from the bech32 encoding when fetching the event.

* fix: use EventPointer for repost replyTo in NIP-10 adapter

Apply the same relay hints fix to the repostToMessage function that was
added in a recent main commit.

* refactor: use applesauce helpers for relay operations in chat adapters

- Use mergeRelaySets for relay deduplication and normalization
- Use getOutboxes helper instead of manual relay list parsing
- Simplify getThreadRelays in NIP-10 adapter
- Simplify loadReplyMessage in NIP-10, NIP-29, and NIP-53 adapters

* refactor: use mergeRelaySets in relay-selection service

- Simplify selectRelaysForInteraction using mergeRelaySets for priority-based relay combination
- Use mergeRelaySets for unique relay extraction in selectRelaysForFilter
- Reduces manual Set operations and gains automatic normalization

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 17:39:58 +01:00
Alejandro
7c6014378b Add ID filtering to REQ command (#202)
* feat: add -i/--id flag for direct event ID filtering in REQ command

Add a new -i/--id flag for direct event lookup via filter.ids, and clarify
-e flag behavior for tag-based filtering (#e/#a tags).

Changes:
- Add -i/--id flag: accepts note1, nevent, or hex event IDs for direct lookup
- Clarify -e flag: now always routes to #e/#a tags (including nevent)
- Update man page with new flag documentation and examples
- Add comprehensive tests for the new -i/--id flag

This aligns with nak's behavior where -i is for direct ID filtering and
-e is for tag-based event references.

* feat: support raw coordinate format (kind:pubkey:d) in -e flag

The -e flag now accepts raw a-tag coordinates like `30023:pubkey:article`
in addition to naddr bech32 encoding. Both route to #a tag filtering.

Examples:
- req -e 30023:abc123...:my-article    # Raw coordinate
- req -e naddr1...                      # Bech32 encoded (same effect)

* feat: add event ID previews in REQ viewer query dropdown

When using -i/--id flag for direct event lookup, the query dropdown now
shows clickable event ID previews (truncated hex).

Click any ID to open the event detail view.

Works in both accordion (complex queries) and simple card views.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 16:14:55 +01:00
Alejandro
93ffd365f5 feat: add repost system messages with grouping in chat (#194)
* feat: add repost system messages with grouping in chat

- Add kind 6 and 16 (reposts) to NIP-10 adapter filters
- Convert simple reposts (no content) to system messages
- Implement consecutive system message grouping
  - Groups consecutive system messages with same action
  - Format: "alice, bob and 3 others reposted"
  - Works for all system messages (reposts, join, leave, etc.)
- Update scroll-to-message to handle grouped messages
- Ignore reposts with content (quotes) to keep it simple

System message grouping UX:
- 1 person: "alice reposted"
- 2 people: "alice and bob reposted"
- 3 people: "alice, bob and charlie reposted"
- 4+ people: "alice, bob and 3 others reposted"

* feat: show all reposts regardless of content

Remove content filtering for reposts - now all kind 6 and 16 events
are shown as system messages. Quote reposts (with content) will just
display as 'reposted' without showing the quote text, keeping the
chat interface clean and consistent.

* refactor: extract and test system message grouping logic

Extract grouping logic to separate module:
- Created src/lib/chat/group-system-messages.ts with:
  - groupSystemMessages(): Groups consecutive system messages
  - isGroupedSystemMessage(): Type guard with validation
  - GroupedSystemMessage interface
- Updated ChatViewer to import from new module
- Improved type guard with additional validation:
  - Check array lengths match (authors.length === messageIds.length)
  - Ensure arrays are not empty
  - Validate all field types

Added comprehensive test coverage (26 tests):
- Basic grouping behavior
- Edge cases (empty arrays, single messages)
- Mixed message types (user, zap, system)
- Timestamp preservation
- Large group handling (100+ items)
- Type guard validation

All tests pass (1006 total), build succeeds.
Production ready.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 13:39:15 +01:00
Alejandro
b3aaabfd5c fix: extract inner zap request for zap-to-zap emoji rendering in compact view (#199)
When displaying a zap that targets another zap (kind 9735), the compact
preview was passing the inner zap receipt directly to RichText. Since a
zap receipt's content is empty (the user's message with emoji tags is in
the embedded zap request), custom emojis weren't rendering.

Now when the zapped event is a zap receipt, we extract its zap request
using getZapRequest() and use that for the preview, matching how the
full renderer handles this via recursive KindRenderer.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 13:34:27 +01:00
Alejandro
b83b26ea9a Fix nostr entity matching in paste handler (#197)
* fix: only match nostr entities at word boundaries in paste handler

Updates the paste handler regex to only match nostr bech32 entities
(npub, note, nevent, naddr, nprofile) when surrounded by whitespace or
at string boundaries. This prevents URLs containing nostr entities
(e.g., https://njump.me/npub1...) from being incorrectly converted
to mentions.

Uses a capture group (^|\s) instead of lookbehind assertion for
Safari compatibility (lookbehind only supported in Safari 16.4+).

* fix: disable pointer events on links in editor

Adds pointer-events: none to anchor tags within the ProseMirror editor
to prevent clicking on pasted URLs from navigating away. This allows
users to edit the text containing links rather than accidentally
triggering navigation.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 13:13:40 +01:00
Alejandro
f329e9c766 fix: guard all RichEditor imperative methods against unmounted view (#196)
Add isEditorReady() helper to check if editor.view.dom is mounted
before accessing editor commands. This prevents the TipTap error
"The editor view is not available" that occurred when PostViewer's
draft loading called editor methods before the view was fully mounted.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 12:30:16 +01:00
Alejandro
3ce77ef97c Remove "Publishing..." text from POST view (#195)
Removed the "Publishing..." text from the Publish button during posting,
keeping only the loading spinner icon for a cleaner UI.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-22 12:10:18 +01:00
Alejandro
7b7b24d41a feat: add client tag support to all event creation (#191)
* feat: add client tag support to all event creation

Implements a global settings system to control whether the Grimoire client tag
should be included in all published events. This allows users to opt-in or
opt-out of identifying their client in published events.

Changes:
- Created global settings service (src/services/settings.ts) with reactive
  BehaviorSubject for app-wide configuration
- Created useSettings hook (src/hooks/useSettings.ts) for React components
- Migrated PostViewer from local settings to global settings system
- Added client tag support to:
  - Post publishing (PostViewer.tsx)
  - Spell publishing (publish-spell.ts)
  - Event deletion (delete-event.ts)
  - NIP-29 chat messages, reactions, join/leave, and group bookmarks
    (nip-29-adapter.ts)
  - Zap requests (create-zap-request.ts)

The client tag setting defaults to enabled (true) for backward compatibility.
Users can toggle this in the post composer settings dropdown.

All event creation locations now check settingsManager.getSetting("includeClientTag")
before adding the GRIMOIRE_CLIENT_TAG to event tags.

* refactor: exclude client tags from NIP-29 and zap requests

Remove client tag support from NIP-29 adapter events and zap requests
as these events may be rejected by servers with large tags or have
specific size constraints.

Changes:
- Removed client tag from NIP-29 chat messages (kind 9)
- Removed client tag from NIP-29 reactions (kind 7)
- Removed client tag from NIP-29 join/leave requests (kind 9021, 9022)
- Removed client tag from NIP-29 group bookmarks (kind 10009)
- Removed client tag from zap requests (kind 9734)

Client tags remain enabled for:
- Regular posts (kind 1)
- Spell publishing (kind 777)
- Event deletion (kind 5)

This ensures maximum compatibility with relay servers and LNURL endpoints
while still providing client identification for standard events.

* feat: implement comprehensive namespaced settings system

Redesigned the settings system with proper namespacing, type safety, validation,
and migration support. This provides a solid foundation for all app configuration.

Settings Structure:
- post: Post composition settings (client tag, relay selection)
- appearance: UI/theme settings (theme, compact mode, font size, animations)
- relay: Relay configuration (fallback, discovery, outbox, timeouts)
- privacy: Privacy settings (read receipts, content warnings, link warnings)
- database: Caching settings (max events, cleanup, IndexedDB options)
- notifications: Browser notifications preferences
- developer: Debug and experimental features

Key Features:
- Fully typed with TypeScript interfaces for each namespace
- Automatic validation with fallback to defaults for invalid data
- Migration system from old flat structure to namespaced structure
- Backwards compatible with old "grimoire-settings" localStorage key
- Import/export functionality for settings backup/restore
- Reactive updates via RxJS BehaviorSubject
- Section-level and individual setting updates
- Reset individual sections or all settings

Changes:
- Created comprehensive AppSettings interface with 7 namespaced sections
- Implemented SettingsManager class with reactive updates and persistence
- Updated useSettings hook to support namespaced API
- Updated PostViewer, publish-spell, and delete-event to use new API
  (settingsManager.getSetting("post", "includeClientTag"))
- Added extensive inline documentation for all settings

Migration:
- Automatically migrates old includeClientTag setting to post.includeClientTag
- Moves data from "grimoire-settings" to "grimoire-settings-v2" key
- Validates all loaded settings and fills in defaults for missing values

This foundation will support future settings UI with tabbed interface.

* feat: add comprehensive settings UI with Post and Appearance sections

Created a minimal MVP settings system accessible via command palette and user menu.
Settings are organized in a clean tabbed interface with two initial sections.

UI Features:
- SettingsViewer component with sidebar navigation
- Post section: Toggle to include Grimoire client tag in published events
- Appearance section:
  - Theme selector (light/dark/system)
  - Toggle to show/hide client tags in event UI ("via Grimoire" etc)

Integration:
- Added "settings" command to command palette
- Added "Settings" option to user menu (before Support Grimoire)
- Registered "settings" as new AppId in window system

Display Logic:
- BaseEventRenderer now honors settings.appearance.showClientTags
- When disabled, "via Grimoire" and other client tags are hidden from events
- Setting applies instantly across all event renderers

Technical Details:
- SettingsViewer uses existing UI components (Checkbox, Button, Label)
- Leverages useSettings hook for reactive updates
- Settings persist to localStorage via settingsManager
- Simple button group for theme selection instead of dropdown
- Clean two-column layout with icons for each section

This provides a solid foundation for adding more settings sections later
(relay config, privacy, database, notifications, developer options).

* refactor: redesign settings UI with shadcn Tabs and cleaner layout

Simplified the settings UI to be more minimal and follow Grimoire patterns:

Changes:
- Use shadcn Tabs component instead of custom sidebar navigation
- Remove non-working theme selector (no theme system implemented yet)
- Use regular <label> elements with htmlFor instead of custom Label component
- Shorter, cleaner copy throughout
- Properly remove unused theme-related imports from user menu
- Follow Grimoire UI patterns (similar to SettingsDialog)

Settings sections:
- Post: Include client tag setting
- Appearance: Show client tags setting

The UI is now cleaner, uses proper form semantics, and matches the rest
of Grimoire's design system.

* feat: add working theme selector and improve settings UI

Added fully functional theme selector and improved the settings UI with
better components and cleaner copy.

Changes:
- Created Switch component (shadcn/radix-ui) for boolean settings
- Added working theme selector that integrates with existing theme system
- Uses useTheme hook to display available themes (Dark, Light, Plan9)
- Reordered sections: Appearance first, Post second
- Reordered settings: Theme first in Appearance section
- Replaced Checkbox with Switch for better UX on boolean toggles
- Simplified copy: "Add Grimoire tag to published events" instead of listing kinds
- Simplified copy: "Display client identifiers in events" instead of "via Grimoire" mention
- Better layout: Label/description on left, Switch on right

Settings now use proper form components:
- Switch for boolean toggles (include client tag, show client tags)
- Button group for theme selection
- Clean justify-between layout for settings rows

The theme selector works immediately - clicking Dark/Light/Plan9 applies
the theme instantly via the existing ThemeProvider context.

* refactor: improve settings UI with Select, icons, and better spacing

Enhanced the settings UI with shadcn Select component, tab icons, and
improved typography and spacing.

Changes:
- Created Select component (shadcn/radix-ui) for dropdowns
- Added icons to tabs: Palette for Appearance, FileEdit for Post
- Replaced button group theme selector with Select dropdown (w-48)
- Added gap-4 between labels and switches for better spacing
- Increased setting names from text-sm to text-base (bigger)
- Reduced help text from text-sm to text-xs (smaller)
- Added gap-3 between "Theme" label and Select
- Added font-medium to "Theme" label for consistency

Layout improvements:
- Theme selector now uses Select with 192px width
- All setting rows have gap-4 between content and controls
- Consistent text hierarchy: font-medium for labels, text-xs for descriptions
- Tab triggers have gap-2 between icon and text

The Select component is properly integrated with the theme system and
displays Dark, Light, and Plan9 options with checkmark indicators.

* refactor: use horizontal layout for theme selector

Aligned theme selector with other settings by using horizontal layout:
- Label and description on left, Select on right
- Added gap-4 for consistent spacing
- Changed label from text-sm to text-base font-medium
- Added helpful description: 'Choose your color scheme'
- Added id='theme' for accessibility

Now all settings follow the same visual pattern.

* refactor: reduce theme selector width for compact display

Changed theme selector from w-48 (192px) to w-32 (128px) since
theme names are short (Dark, Light, Plan9). More compact and
better proportioned for the content.

* fix: pass 'settings' command string when opening from menu

Changed the command string from 'Settings' to 'settings' when opening
the settings window from the user menu. This ensures clicking edit on
the settings window shows the correct command: 'settings'

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 23:11:25 +01:00
Alejandro
53f29b9b63 fix: improve tooltip contrast across all themes (#192)
* fix: improve tooltip contrast across all themes

CRITICAL FIX: Plan 9 theme had a catastrophic contrast failure where
tooltip text (HSL 60, 100%, 96%) was nearly identical to the background
(HSL 60, 100%, 94%), creating a ~1:1 contrast ratio that made tooltips
completely unreadable.

Changes:
- Added dedicated `tooltip` and `tooltipForeground` colors to theme system
- Updated all three built-in themes (dark, light, plan9) with proper colors
- Modified tooltip component to use new colors instead of primary/primary-foreground
- Added rounded corners and shadow to tooltips for better visual separation

Theme-specific tooltip colors:
- Dark theme: Dark blue-gray background (#217.2 32.6% 17.5%) with light text
- Light theme: Very dark background (#222.2 47.4% 11.2%) with light text
- Plan 9 theme: Dark blue (#220 50% 25%) with pale yellow text

All tooltip colors now meet WCAG AA standards (4.5:1+ contrast ratio) and
are clearly visible against their respective theme backgrounds.

Files modified:
- src/lib/themes/types.ts: Added tooltip color types
- src/lib/themes/builtin/*.ts: Added tooltip colors to all themes
- src/lib/themes/apply.ts: Apply tooltip CSS variables on theme change
- src/index.css: Added tooltip CSS variables for light/dark themes
- tailwind.config.js: Exposed tooltip colors as Tailwind utilities
- src/components/ui/tooltip.tsx: Use new tooltip colors with improved styling

* fix: increase dark mode tooltip lightness for better visibility

Dark mode tooltips were too dark (17.5% lightness) against the very dark
background (4.9% lightness), making the tooltip box hard to distinguish.

Changes:
- Increased dark mode tooltip lightness from 17.5% to 30%
- This provides ~6:1 contrast ratio between tooltip and background
- Tooltip box now clearly visible against dark background
- Text contrast remains excellent (light text on medium-dark background)

The tooltip now stands out properly while maintaining high text readability.

* fix: improve ChatViewer group tooltip contrast in dark mode

The group description tooltip in ChatViewer had poor contrast due to
using `text-primary-foreground` color classes that conflicted with
the new tooltip background colors.

Issues fixed:
1. Description text using `text-primary-foreground/90` - replaced with `opacity-90`
2. Protocol button using `bg-primary-foreground/20` with `text-primary-foreground`
   (light-on-light, ~1.5:1 contrast) - now uses `bg-tooltip-foreground/20`
3. All other text using `text-primary-foreground` variants - replaced with `opacity-*`

This allows the text to inherit the correct `text-tooltip-foreground` color
from the TooltipContent component, ensuring proper contrast against the
`bg-tooltip` background in all themes.

Files modified:
- src/components/ChatViewer.tsx: Updated tooltip text color classes

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 22:59:01 +01:00
Alejandro
2e8ef0e5db fix: add iOS PWA notch support with safe area insets (#190)
- Update viewport meta tag to include viewport-fit=cover
- Add apple-mobile-web-app-capable and status-bar-style meta tags
- Add CSS safe area insets to body element for proper notch handling
- Ensures content extends into safe areas on iOS devices while
  respecting notches and rounded corners

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 21:54:28 +01:00
Alejandro
b5b474da3a Add React option to generic event menu (#189)
* feat: add React option to event menu for emoji reactions

Add emoji reaction capability to the generic event menu (dropdown and
right-click context menu). When logged in with a signing account, users
can now react to any event with unicode or custom NIP-30 emoji.

- Add React menu item after Chat in EventMenu and EventContextMenu
- Integrate EmojiPickerDialog from chat components
- Use ReactionBlueprint from applesauce-common for NIP-25 reactions
- Publish reactions to user's outbox relays via publishEvent()
- Hidden when user cannot sign (read-only or not logged in)

* feat: use NIP-65 relay selection for reactions

Add interaction relay selection utility following the NIP-65 outbox model:
- Author's outbox (write) relays: where we publish our events
- Target's inbox (read) relays: so the target sees the interaction

This ensures reactions reach the intended recipient according to their
relay preferences, similar to how zap relay selection works.

New file: src/lib/interaction-relay-selection.ts
- selectInteractionRelays() for full result with sources
- getInteractionRelays() convenience wrapper

* refactor: consolidate interaction relay selection into relay-selection service

Move selectRelaysForInteraction() into the existing relay-selection.ts
service to avoid fragmentation. The service already has the infrastructure
for relay list caching, health filtering, and fallback logic.

- Add selectRelaysForInteraction() to src/services/relay-selection.ts
- Update BaseEventRenderer to import from consolidated location
- Remove separate src/lib/interaction-relay-selection.ts file

* fix: use semantic author for reaction relay selection

Use getSemanticAuthor() to determine the target pubkey for relay
selection. This ensures reactions to zaps notify the zapper (not the
lightning service) and reactions to streams notify the host.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 20:09:09 +01:00
Alejandro
074c3c0b7f Fix relay list flickering during post publish (#187)
* fix: prevent relay list flicker during post publishing

Fixed the relay list flickering issue that occurred while publishing posts:
- Changed overflow-y-auto to overflow-y-scroll to keep scrollbar visible
- Added fixed width (w-6) to status indicator container to prevent layout shifts
- Status indicators no longer cause content reflow when changing states

* fix: show icon for all relay states to prevent layout shifts

Show a Circle icon for pending/unpublished relays so there's always
an icon present. This keeps the status indicator container at a
consistent width and prevents flickering when publishing.

- Added Circle icon import
- Show Circle icon for "pending" state
- Reverted to overflow-y-auto (no longer needed with fixed container)
- Status indicator always has fixed width with centered icon

* refactor: use muted color for pending and publishing relay states

Changed publishing spinner from blue to muted to match pending state,
creating a more consistent visual hierarchy where only success (green)
and error (red) states use color.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 17:22:07 +01:00
Alejandro
66618fb150 fix: prevent TipTap editor crash when view is not ready (#188)
The POST command would sometimes crash with "editor view is not available"
because code was accessing editor.view.dom before the editor was fully
mounted. This fix:

- Adds defensive checks for editor.view?.dom in RichEditor's useEffect
  that attaches keyboard listeners
- Makes setContent method check editor view is ready before setting content
- Fixes PostViewer draft loading to use retry logic instead of fixed timeout
- Removes relayStates from dependency array to prevent effect re-runs
- Adds ref to track if draft was already loaded

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 17:15:41 +01:00
Alejandro
c955bf8eb0 feat: add readable names for wiki list kinds (10101, 10102) (#186)
Add EVENT_KINDS entries for kind 10101 (Wiki Authors) and kind 10102
(Wiki Relays) so getKindName() returns human-readable names instead
of fallback "Kind 10101" / "Kind 10102".

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 16:59:53 +01:00
Alejandro
adf8a62954 feat: add hashtag support to rich editor (#185)
* feat: add automatic hashtag extraction and t tags in POST command

Extract hashtags from post content and automatically add them as t tags to published events.

Changes:
- Add hashtag extraction logic to RichEditor.serializeContent() using Unicode-aware regex
- Update SerializedContent interface to include hashtags field
- Update RichEditor props and callbacks to pass hashtags through the pipeline
- Add t tags for each hashtag in PostViewer.handlePublish()

Hashtags are deduplicated and stored in lowercase (following Nostr convention).
Example: #bitcoin #nostr #Bitcoin → ["t", "bitcoin"], ["t", "nostr"]

* refactor: use NoteBlueprint for automatic hashtag/mention extraction

Replace manual hashtag and mention extraction with applesauce's NoteBlueprint,
which automatically extracts hashtags, mentions, and event quotes from text content.

Changes:
- Simplify SerializedContent interface by removing manually extracted fields
- Remove hashtag extraction regex and mention/eventRef tracking from editors
- Replace manual event building with factory.create(NoteBlueprint, ...)
- Use q tags for event quotes (NIP-18) instead of e tags

Benefits:
- ~70 lines of code removed
- Leverage battle-tested applesauce extraction logic
- Automatic benefits from future applesauce improvements
- Correct semantic tags (q for quotes, p for mentions, t for hashtags)

What still works:
- Custom emoji tags (NIP-30)
- Blob attachments/imeta tags (NIP-92)
- Address references (naddr - not yet in applesauce)
- Client tag

All tests pass (980/980).

* refactor: use NoteReplyBlueprint in NIP-10 adapter

Replace manual NIP-10 tag building with NoteReplyBlueprint, which automatically
handles root/reply markers, p-tag copying, and all the threading logic.

Changes:
- Simplify sendMessage from ~95 lines to ~40 lines
- Remove manual e-tag building with root/reply markers
- Remove manual p-tag deduplication logic
- Use factory.create(NoteReplyBlueprint, parentEvent, content, options)
- Automatically get hashtags, mentions, and event quotes via setShortTextContent

Benefits:
- ~55 lines of complex threading logic removed
- Leverage battle-tested applesauce NIP-10 implementation
- Automatic root detection from parent's existing tags
- Cleaner, more maintainable code

All tests pass (980/980).

* refactor: use GroupMessageBlueprint and ReactionBlueprint in chat adapters

Replace manual event building with applesauce blueprints in all chat adapters.

Changes:
- NIP-29: Use GroupMessageBlueprint for kind 9 messages
  * Auto-handles h-tag, hashtags, mentions, emojis
  * Manually add q-tag for replies (NIP-29 specific)
  * ~15 lines removed

- All adapters (NIP-10, NIP-29, NIP-53, NIP-C7): Use ReactionBlueprint for kind 7 reactions
  * Auto-handles e-tag, k-tag, p-tag, custom emoji support
  * Protocol-specific tags (h-tag, a-tag) added manually
  * ~60 lines removed across 4 adapters

Benefits:
- ~75 lines of code removed total
- Leverage battle-tested applesauce blueprints
- Automatic hashtag, mention, and quote extraction
- Cleaner, more maintainable code

All tests pass (980/980).

* fix: add required previous field to GroupMessageBlueprint options

GroupMessageBlueprintOptions requires a 'previous' field for message threading.
Added empty array for now since we don't support threading yet.

* docs: add comprehensive blueprint documentation to applesauce skills

Added detailed documentation for:
- NoteBlueprint (automatic hashtag/mention/quote extraction)
- NoteReplyBlueprint (NIP-10 threading)
- ReactionBlueprint (kind 7 reactions)
- GroupMessageBlueprint (NIP-29 groups)
- DeleteBlueprint (NIP-09 deletion)
- EventFactory usage patterns
- Before/after examples showing code reduction
- Best practices for using blueprints

This documents the refactoring work done throughout the codebase.

* fix: use single newline separator in TipTap getText() calls

TipTap's getText() uses double newlines (\n\n) by default to separate
block nodes like paragraphs, which was causing extra blank lines in
posted content.

Changed to getText({ blockSeparator: '\n' }) in both RichEditor and
MentionEditor to use single newlines between paragraphs.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 16:20:00 +01:00
Alejandro
b1fb569250 feat: user profile search for resolving usernames when pasting (#184)
* fix: show display names when pasting npub/nprofile in editors

Previously, pasting npub/nprofile strings would only check the EventStore's
in-memory cache for profiles. If a profile was cached in IndexedDB but not
yet loaded into the EventStore, it would show a hex preview instead.

This adds a ProfileCache service that:
- Loads profiles from IndexedDB on startup for instant access
- Subscribes to EventStore for new profiles as they arrive
- Provides synchronous lookups for the paste handler

Also uses consistent fallback format (XXXX:YYYY) when no profile is found.

* refactor: make ProfileSearchService a singleton for shared profile lookups

Instead of creating a separate ProfileCache service, refactored
ProfileSearchService to be a singleton that:
- Auto-initializes on module load
- Loads profiles from IndexedDB for instant startup
- Subscribes to EventStore for new profiles

This allows both the paste handler and mention autocomplete to share
the same profile cache, eliminating duplicate data and subscriptions.

Removed the now-unnecessary profile-cache.ts.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 13:38:59 +01:00
Alejandro
94982ca7f4 feat(post): add POST command with rich text editor and relay selection (#180)
* feat(post): add POST command with rich text editor and relay selection

Phase 3: Create POST command for publishing kind 1 notes

Features:
- RichEditor component with @mentions, :emoji: autocomplete
- Image/video upload via Blossom with drag-and-drop
- Relay selection UI with write relays pre-selected by default
- Per-relay publish status tracking (loading/success/error)
- Submit with Ctrl/Cmd+Enter keyboard shortcut
- Multi-line editing with rich previews for attachments
- NIP-30 emoji tags and NIP-92 imeta tags for attachments

Implementation:
- Add 'post' to AppId type
- Add POST command to man pages
- Create PostViewer component using RichEditor
- Wire PostViewer into WindowRenderer
- Publish events using EventFactory and RelayPool
- Track per-relay status with visual indicators

Usage:
- Run 'post' command to open the composer
- Type content with @mentions and :emoji:
- Upload media via button or drag-and-drop
- Select/deselect relays before publishing
- Press Publish button or Ctrl/Cmd+Enter to post
- View per-relay publish status in real-time

* feat(editor): add nostr bech32 paste handler with inline previews

Implements paste handling for nostr: URIs (npub, note, nevent, naddr, nprofile)
that transforms them into rich inline preview chips in the chat composer.

Changes:
- Add NostrEventPreview TipTap node type for displaying bech32 previews
- Add NostrPasteHandler extension to detect and transform pasted bech32 strings
- Update serializeContent to convert previews back to nostr: URIs on submit
- Add CSS styling for preview chips with hover effects
- Support all major bech32 types: npub, note, nevent, naddr, nprofile

Features:
- Automatic detection of nostr: URIs in pasted text (with or without prefix)
- Visual preview chips with type icon, label, and truncated ID
- Maintains nostr: URI format in final message content (NIP-27 compatible)
- Simple implementation without event fetching (fast, no loading states)
- Styled with primary theme colors for consistency

Technical details:
- Uses ProseMirror Plugin API for paste interception
- Inline atomic nodes for previews (similar to mentions and blob attachments)
- Regex pattern matches all valid bech32 formats
- Proper error handling for invalid bech32 strings
- Extensible foundation for future rich metadata fetching

* refactor(editor): improve nostr preview display with kind icons

Updates the nostr event preview chips to show more meaningful information:

Changes:
- Add getKindIcon() helper function with 15+ kind mappings
- Display format: kind-icon + author/id (e.g., "📄 a1b2c3d4")
- For naddr: show kind icon + author pubkey (first 8 chars)
- For nevent: show kind icon + author if available, else event ID
- For note: show 📝 + event ID (first 8 chars)
- For npub/nprofile: show 👤 + pubkey (first 8 chars)
- Removed verbose type labels (NPUB, NEVENT, etc.)
- Increased label max-width to 120px for better readability

Kind icon mappings:
- 👤 Profile (kind 0)
- 📝 Note (kind 1)
- 👥 Contacts (kind 3)
- 🔁 Repost (kind 6)
- ❤️ Reaction (kind 7)
-  Zap (kind 9735)
- 📄 Long-form (kind 30023)
- 🎙️ Live event (kind 30311)
- 📦 File metadata (kind 1063)
- 📌 Addressable events (30000-39999)
- 🔄 Replaceable events (10000-19999)

Result: More compact, visually intuitive previews that immediately
convey the content type and author without requiring event fetching.

* refactor(editor): simplify nostr preview display and reuse mentions

Major simplification of the nostr bech32 preview display:

Profile handling (npub/nprofile):
- Now creates regular @mention nodes instead of custom preview nodes
- Reuses existing mention infrastructure, styling, and UserName component
- Displays as "@username" with existing mention chip styling
- Serializes to nostr:npub1... on submit (same as manual @mentions)

Event/Address display (note/nevent/naddr):
- Removed emoji icons for cleaner, more minimal appearance
- Display format: "event abc12345" for note/nevent
- Display format: "address article-slug" for naddr (shows d identifier)
- Falls back to short pubkey if naddr has no d identifier
- Simple text-only chips with type + identifier

Benefits:
- Less visual noise (no emojis)
- Consistent mention styling for all profiles
- Profile mentions can now be clicked/hovered like manual mentions
- Smaller code footprint (removed complex icon mapping logic)
- Better UX: profiles look and behave like regular mentions

Technical changes:
- Paste handler creates mention nodes for npub/nprofile
- NostrEventPreview only handles note/nevent/naddr now
- Removed npub/nprofile from serialization (handled by mention serializer)
- Updated type definitions to reflect reduced scope

* feat(editor): add drag-and-drop and paste file upload support

Adds comprehensive file upload support via drag-and-drop and paste
directly into the chat composer editor.

Changes to MentionEditor:
- Add FileDropHandler TipTap extension to intercept file drops/pastes
- Filter for valid media types (image/video/audio)
- Add onFileDrop callback prop to communicate with parent
- Handle both drag-and-drop and clipboard paste events

Changes to useBlossomUpload hook:
- Update open() method to accept optional File[] parameter
- Add initialFiles state to track pre-selected files
- Clear initialFiles when dialog closes
- Pass initialFiles to BlossomUploadDialog component

Changes to BlossomUploadDialog:
- Add initialFiles prop for pre-selecting files
- Auto-select first file when initialFiles provided
- Generate preview URL for images/video on initialization
- Seamless UX: dropped files immediately appear in dialog

Changes to ChatViewer:
- Wire up onFileDrop callback to open upload dialog
- Pass dropped files to upload dialog via open(files)
- Fix onClick handler to properly call openUpload()

User experience:
- Drag image/video/audio file onto chat composer → upload dialog opens
- Paste image from clipboard → upload dialog opens
- File automatically selected and previewed
- Click upload to complete (same as manual file selection)

Technical details:
- Uses ProseMirror Plugin API for drop/paste interception
- File type validation: /^(image|video|audio)\//
- Bonus: Also works with clipboard paste (Ctrl+V)
- Clean state management with automatic cleanup

* fix(editor): add dragover handler to enable file drag-and-drop

The drag-and-drop wasn't working because we need to handle dragover
events and call preventDefault() to signal that drops are allowed.

Changes:
- Add handleDOMEvents with dragover handler to FileDropHandler
- Check for Files type in dataTransfer
- Set dropEffect to 'copy' for visual feedback
- Call preventDefault() to allow drops

Without this, the browser blocks all drops by default and handleDrop
never fires. This is a standard requirement for HTML5 drag-and-drop API.

* feat(editor): show usernames when pasting npub/nprofile mentions

When pasting npub or nprofile URIs, the editor now looks up the profile
from the event store cache and uses the display name if available.

Changes:
- Add getDisplayNameForPubkey helper function
- Import eventStore and profile helpers
- Synchronously check if profile is cached via .value property
- Use getDisplayName from nostr-utils for consistent formatting
- Falls back to short pubkey (first 8 chars) if profile not cached

Result:
- Paste npub1abc... → shows "@alice" if profile is cached
- Paste npub1abc... → shows "@abc12345" if profile not cached
- Consistent with typed @mentions behavior
- No async delays or loading states needed

* refactor(editor): remove drag-and-drop, keep paste file support

Removes drag-and-drop file handling while keeping clipboard paste functionality.
Drag-and-drop will be implemented differently later.

Changes:
- Rename FileDropHandler → FilePasteHandler
- Remove handleDrop and dragover event handlers
- Keep handlePaste for clipboard file support
- Rename onFileDrop prop → onFilePaste for clarity
- Update ChatViewer to use onFilePaste callback
- Update all references and comments

Result:
- Paste images from clipboard (Ctrl+V) still works
- Drag-and-drop removed completely
- Clearer naming reflects actual functionality

* refactor(editor): extract shared extensions and create React node views

Phase 1 of RichEditor variant implementation:

- Extract NostrPasteHandler extension for reuse across editors
- Extract FilePasteHandler extension for clipboard file handling
- Create NostrEventPreviewRich React node view using DetailKindRenderer
- Create BlobAttachmentRich React node view with full-size media previews
- Create rich TipTap node extensions using ReactNodeViewRenderer
- Update MentionEditor to use shared extensions

These shared components will be used by the upcoming RichEditor
variant for long-form content composition with full previews.

* feat(editor): add RichEditor component for long-form content

Phase 2 of RichEditor variant implementation:

- Create RichEditor component based on MentionEditor
- No slash commands (removed SlashCommand extension)
- Multi-line support with Enter for newlines
- Ctrl/Cmd+Enter to submit
- Full-size image/video previews using BlobAttachmentRich
- Full event rendering using NostrEventPreviewRich
- Configurable min/max height (defaults: 200px-600px)
- Retains @mentions and :emoji: autocomplete
- Reuses shared extensions (NostrPasteHandler, FilePasteHandler)
- Targets kind 1 notes composition

Key differences from MentionEditor:
- Block-level rich previews instead of inline badges
- Multi-line editing without Enter-to-submit
- Resizable with overflow-y: auto
- No command palette functionality

* refactor(post): improve PostViewer layout and editor previews

UI improvements:
- Remove flex-1 from editor, use fixed min/max height
- Move upload button next to publish button (icon-only)
- Place relay selector below action buttons (not fixed)
- Limit image/video previews to max-h-96
- Add pointer-events-none to event previews to prevent accidental interaction
- Use EventDetailSkeleton for loading event previews

Layout changes:
- Single scrollable container with space-y-4
- Editor: 150-400px height range
- Action buttons in single row (upload icon + publish button)
- Relay list with max-h-64 scroll area
- Better spacing and visual hierarchy

* feat(post): add draft persistence and improve relay UI

Draft persistence:
- Save draft content to localStorage every 2 seconds
- Load draft on component mount (per-user with pubkey)
- Clear draft after successful publish
- Prevent data loss on tab change or page reload

UI improvements:
- Reset button now icon-only (RotateCcw) with tooltip
- Remove borders from relay list items
- Use RelayLink component for relay rendering
- Show relay icons and secure/insecure indicators
- Cleaner, more compact relay list appearance
- Increased spacing between relay items (space-y-1)

Storage key format: grimoire-post-draft-{pubkey}

* feat(post): enhance draft storage with full JSON state persistence

Draft persistence improvements:
- Store complete editor state as JSON (preserves blobs, emojis, mentions, formatting)
- Save selected relays array in draft
- Restore full editor content using setContent() method
- Maintain draft per-user with pubkey-based key

RichEditor enhancements:
- Add getJSON() method to export editor state
- Add setContent() method to restore from JSON
- Enables lossless draft save/restore

UI improvements:
- Change "Relays" label to plain text without Label component
- Keep unselected relays visible during/after publish
- Only update status for selected relays during publish

Draft storage format:
{
  editorState: {...},      // Full TipTap JSON state
  selectedRelays: [...],   // Array of selected relay URLs
  timestamp: 1234567890
}

* feat(post): add nostr tag extraction, retry failed relays, and disable empty publish

Nostr tag extraction:
- Extract p tags from @mentions (pubkey references)
- Extract e tags from note/nevent references (event ID references)
- Extract a tags from naddr references (address/parameterized replaceable)
- Update SerializedContent interface to include mentions, eventRefs, addressRefs
- Serialize editor content walks all node types to extract references
- Build complete tag array for kind 1 events with proper NIP compliance

Retry failed relays:
- Add retryRelay() function to republish to specific failed relay
- Make error icon clickable with hover state
- Show "Click to retry" in tooltip
- Rebuild event and attempt publish again
- Update status indicators in real-time

Disable publish when empty:
- Track isEditorEmpty state
- Update every 2 seconds along with draft save
- Disable publish button when editor isEmpty()
- Prevents publishing empty notes

Tag generation order:
1. p tags (mentions)
2. e tags (event references)
3. a tags (address references)
4. emoji tags (NIP-30)
5. imeta tags (NIP-92 blob attachments)

This ensures proper Nostr event structure with all referenced pubkeys, events, and addresses tagged.

* feat(post): add client tag and remove reset button

Client tag:
- Add ["client", "grimoire"] tag to all published events
- Added to both handlePublish and retryRelay functions
- Identifies posts as coming from Grimoire client

UI cleanup:
- Remove reset button from relay selection UI
- Relay selection persists across sessions via draft storage
- Simplify header to just show relay count
- Remove unused RotateCcw icon import

Tag generation order (final):
1. p tags (mentions)
2. e tags (event references)
3. a tags (address references)
4. client tag (grimoire)
5. emoji tags (NIP-30)
6. imeta tags (NIP-92)

* fix(post): trim content before publishing

Content trimming:
- Trim content in handlePublish() before building event
- Trim content in retryRelay() before building event
- Removes leading and trailing whitespace from published notes
- Prevents accidental whitespace-only or padded messages
- Applied via content.trim() before factory.build()

* fix(post): reuse signed event for retries instead of recreating

Event reuse for retries:
- Add lastPublishedEvent state to store the signed event
- Store signed event after creation in handlePublish()
- Simplify retryRelay() to republish the same signed event
- Remove duplicate event creation and signing in retryRelay()
- Remove duplicate client tag addition in retryRelay()

Benefits:
- Same event ID and signature across all relays
- More efficient - no need to re-sign
- Consistent event across retries
- Single source of truth for published event

Flow:
1. User publishes - event created, signed, stored
2. Some relays fail - event remains in state
3. User retries - same event republished to failed relay
4. New post overwrites lastPublishedEvent with new event

* feat(post): add published event preview and reset button

After successful publish, replace editor and action buttons with:
- Kind1Renderer preview of the published event
- Reset button to compose another post
- Relay list remains visible with publish status indicators

Users can click "Compose Another Post" to reset the form and
return to the editor for the next post.

* feat(post): improve layout and relay handling

- Fix double scrollbar issue by using flexbox layout instead of overflow-auto
- Disable relay checkboxes during preview mode (can't change after publish)
- Use aggregator relays as fallback when no write relays configured
- Remove conditional error message since fallback relays always available
- Improve layout responsiveness with flex-1 and flex-shrink-0 classes

Aggregator relays (nos.lol, relay.snort.social, relay.primal.net, relay.damus.io)
ensure users can always publish even without configured write relays.

* fix(editor): use feed renderer and skeleton for event previews

When pasting event URLs in the editor, use KindRenderer (feed) and
EventCardSkeleton instead of DetailKindRenderer and EventDetailSkeleton.

Event previews in the editor are inline/compact like feed items, so they
should use the feed renderer rather than the detail view renderer.

* fix(post): remove flex-1 and improve scrolling layout

- Change layout from flex-col with flex-1 to overflow-y-auto on outer container
- Remove flex-1 from all child elements to prevent layout issues with complex posts
- Hide scrollbar in RichEditor for cleaner appearance
- Single scrollbar on container prevents double-scrollbar issues
- Editor now truly full-width without extra padding

This creates a more predictable layout that avoids overlaps and weird
behavior when posts contain lots of content (images, videos, embedded events).

* feat(post): add max-width constraint to content

Add max-w-2xl (768px) with mx-auto to center and constrain post content
width, similar to ZapWindow. Prevents content from becoming too wide on
large screens while maintaining good readability.

* fix(editor): add remove button and fix cursor positioning for event embeds

- Add X button on hover to NostrEventPreviewRich (same pattern as BlobAttachmentRich)
- Fix cursor positioning after pasting nevent/naddr by tracking node sizes correctly
- Import TextSelection from prosemirror-state to properly set cursor position
- Change from `tr.insert(from + index, node)` to `tr.insert(insertPos, node)` with cumulative position tracking

Previously, cursor would stay behind the event embed node after pasting.
Now it correctly positions after the inserted content.

* feat(post): add discard button, settings dropdown, and improve button layout

- Add Discard button to clear editor state and draft
- Add Settings dropdown with client tag toggle (enabled by default)
- Limit publish button width to w-32 and push to right with spacer
- Conditionally include client tag based on setting
- Improve action button layout with better spacing

* feat(post): add relay connectivity status, input widget, and improve error handling

- Show relay connectivity status (Server/ServerOff icons) next to each relay in list
- Add input widget to add custom relays during post composition
  - Validates relay URLs (supports with/without wss:// prefix)
  - Normalizes URLs automatically (defaults to wss://)
  - Shows Plus button enabled only when input is valid
  - Press Enter or click Plus to add relay
- Improve signing failure handling: draft is only cleared after successful publish
- New relay input widget disabled during preview mode
- Connect to relay pool state using use$() hook for reactive connectivity status

Technical details:
- Import Server, ServerOff, Plus icons and Input component
- Add newRelayInput state for relay URL input
- Use pool.relays$ observable to get real-time connection state
- Create isValidRelayInput() validator with URL pattern matching
- Create handleAddRelay() to normalize and add relays to list
- Update relay list rendering to show connectivity icons
- Add input + button widget after relay list

* feat(post): add auth status icons and window-specific draft persistence

- Show auth status icon next to connectivity icon for each relay
  - Uses getAuthIcon() utility from relay-status-utils
  - Displays shield icons for authenticated/failed/rejected states
  - Includes tooltip with auth status label
- Make draft persistence window-specific using window ID
  - Draft key format: `grimoire-post-draft-{pubkey}-{windowId}`
  - Each post window maintains its own independent draft
  - Falls back to pubkey-only key when windowId not available
- Remove toast notification when adding relays (less noisy UX)
  - Still shows error toast if relay URL is invalid
- Pass windowId prop from WindowRenderer to PostViewer

Technical details:
- Import getAuthIcon from @/lib/relay-status-utils
- Import useRelayState hook to get relay auth status
- Add PostViewerProps interface with optional windowId
- Update all draft key computations to include windowId
- Update dependency arrays for useEffect/useCallback with windowId
- Get relay state via getRelay(url) for auth icon display

* feat(post): add global persistent settings and event JSON preview

Settings (persist in localStorage, global across all post windows):
- "Include client tag" - toggle whether to add client tag to events (default: true)
- "Show event JSON" - display copyable JSON preview of event (default: false)

Features:
- Settings stored in localStorage at "grimoire-post-settings"
- Settings persist across sessions and windows
- Event JSON preview shows unsigned event initially, updates to signed version after signing
- Preview displays "(Signed)" or "(Unsigned)" label
- Uses CopyableJsonViewer component for syntax highlighting and copy button
- Preview limited to max-h-64 with scrolling

Draft persistence improvements:
- Added relays now saved in draft state under "addedRelays" field
- On draft load, custom relays are restored to relay list
- Relays identified as "added" if not in user's write relay list

Technical details:
- Added PostSettings interface with includeClientTag and showEventJson fields
- Settings loaded on mount from localStorage, saved on change via useEffect
- updateSetting callback handles individual setting updates
- previewEvent state holds current event (unsigned or signed)
- setPreviewEvent called before and after signing in handlePublish
- Draft format: { editorState, selectedRelays, addedRelays, timestamp }
- Draft loading checks addedRelays and restores non-duplicate relays

* feat(post): add live draft JSON preview with 2-second updates

This commit enhances the POST command with a live JSON preview feature:

- Added generateDraftEventJson() that builds unsigned draft event from editor content
- JSON preview updates every 2 seconds when "Show event JSON" setting is enabled
- Extracts all tags from editor: mentions (p), events (e), addresses (a), emojis, blobs (imeta)
- Displays as "Event JSON (Draft - Unsigned)" to distinguish from signed events
- Uses CopyableJsonViewer component for syntax highlighting and copying

The live preview helps users understand event structure before publishing.

* feat(post): improve JSON preview responsiveness and layout

This commit addresses UX feedback on the JSON preview feature:

- Reduced update interval from 2000ms to 200ms for much more responsive UI
- Moved JSON preview below relay list for better visual flow
- Increased max-height from 256px to 400px for better visibility
- Separated JSON update logic into dedicated effect for cleaner code
- JSON preview now feels instant when typing (10x faster updates)

The JSON preview is now much more pleasant to use and doesn't feel
laggy when editing content.

* refactor(post): replace intervals with debounced onChange handlers

This commit improves the code quality and UX of the POST command:

**Debounced onChange Pattern:**
- Added onChange prop to RichEditor that fires on every content change
- Replaced interval-based updates with proper debounced handlers
- Draft saves debounced to 2000ms (unchanged behavior, cleaner code)
- JSON updates debounced to 200ms (same responsiveness, event-driven)
- Much cleaner React pattern - no weird intervals

**JSON Preview Improvements:**
- Removed "Event JSON" title header for cleaner look
- Shows signed event JSON when published and setting is enabled
- Shows draft (unsigned) JSON when composing
- Always scrollable up to 400px height

**Technical Improvements:**
- Added onUpdate handler to TipTap editor config
- Proper cleanup of debounce timeouts on unmount
- Type-safe timeout refs using ReturnType<typeof setTimeout>

* refactor(post): remove JSON preview and ensure proper content serialization

**Removed JSON preview feature:**
- Removed showEventJson setting and all related UI
- Removed draftEventJson state and generation logic
- Removed "Show event JSON" checkbox from settings
- Simplified PostViewer code by ~100 lines
- Cleaner, production-ready codebase

**Ensured proper content serialization:**
- Added renderText() to Mention extension to serialize as nostr:npub URIs
- Added renderText() to BlobAttachmentRichNode to serialize URLs
- NostrEventPreviewRichNode already had renderText() for nostr:note/nevent/naddr
- Editor now properly converts all rich content to plain text:
  - @mentions → nostr:npub...
  - Event references → nostr:note/nevent...
  - Address references → nostr:naddr...
  - Blob attachments → URLs
  - Custom emojis → :shortcode:

**Result:**
- Cleaner, simplified code ready for production
- All editor elements properly serialize to content string
- JSON preview can be re-added later with better implementation

* perf(post): reduce draft save debounce from 2s to 500ms

Makes the draft saving feel more responsive without hammering localStorage
on every keystroke.

* fix(post): preserve content when signing fails or is rejected

Critical bug fix: Separate signing from publishing to handle failures correctly.

**The Problem:**
- When user rejected signing (or signing failed), we treated it as a publishing failure
- Set all relay states to "error" even though we never tried to publish
- Destroyed the user's post content

**The Solution:**
- Split signing and publishing into separate try-catch blocks
- If signing fails: show toast, reset isPublishing, and RETURN EARLY
- User keeps their content and can try again
- Only update relay states if we actually attempted to publish

**User Experience:**
- User rejects signing → toast appears, post is preserved, can edit or try again
- Publishing fails → relay states show errors, can retry individual relays
- No more losing your carefully crafted post to a rejected signature!

* refactor(post): use Promise.allSettled for relay publishing

Use Promise.allSettled instead of Promise.all to wait for all relay
publish attempts to complete, regardless of individual success/failure.

More defensive and semantically correct since we're handling errors
individually per relay and want all attempts to finish.

* feat(post): use GRIMOIRE_CLIENT_TAG constant with NIP-89 app address

Update PostViewer to use the new GRIMOIRE_CLIENT_TAG constant from main
which includes the NIP-89 app definition address (31990) as the third
element.

Before: ["client", "grimoire"]
After: ["client", "grimoire", "31990:..."]

This allows Nostr clients to display a clickable link to Grimoire's
app definition event.

* fix(post): preserve content when publishing fails to all relays

Critical UX improvement: Only clear draft and show preview if at least
one relay succeeded.

**The Problem:**
- When publishing failed to ALL relays, we still cleared the draft
- User lost their content with no way to retry
- Frustrating experience when all relays are down

**The Solution:**
- Track success/failure by returning result from each publish promise
- Count successful publishes from Promise.allSettled results
- Only clear draft and show preview if successCount > 0
- Keep editor visible with content if all relays fail

**User Experience:**
- All relays succeed → Clear draft, show preview, success toast
- Some relays succeed → Clear draft, show preview, warning toast
- All relays fail → Keep editor with content, error toast, can retry

**Toast Messages:**
- All succeeded: "Published to all X relays"
- Partial success: "Published to X of Y relays"
- Total failure: "Failed to publish to any relay. Please check..."

* fix(post): preserve content when signing or publishing

Critical fix: Don't clear editor content until publish succeeds.

**The Problem:**
- RichEditor automatically cleared content on submit
- Content was wiped immediately when clicking "Publish"
- User lost their content before knowing if publish succeeded

**The Solution:**
- Remove automatic `clearContent()` from RichEditor's handleSubmit
- Let PostViewer control when to clear the editor
- Only clear editor after successful publish (successCount > 0)

**User Experience Now:**
- Click "Publish" → content stays visible while publishing
- Publishing succeeds → content clears, show preview
- Publishing fails → content stays, user can edit and retry
- User rejects signing → content stays, can try again

No more losing your post to a rejected signature or failed publish!

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 13:03:30 +01:00
Alejandro
d18cdc31d5 feat: client tag (#183)
* feat: show client tag in event header

Display "via <client>" after the timestamp in BaseEventContainer
when the event has a client tag. Uses compact 10px font with
reduced opacity to minimize visual noise.

* feat: make client tag link to NIP-89 app definition

When the client tag has a third element with a valid 31990 address
(NIP-89 app handler), make the client name clickable to open the
app definition event.

* fix: use parseAddressPointer from nip89-helpers instead of non-existent parseCoordinate

* feat: add NIP-89 app address to client tag

- Add GRIMOIRE_APP_ADDRESS and GRIMOIRE_CLIENT_TAG constants
- Update all client tag usages to include the 31990 app definition address
- Update tests to verify the app address is included
- Update spell.ts documentation

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 12:23:41 +01:00
Alejandro
d20a92e854 fix: support zapping addressable events (naddr) (#182)
ZapWindow now correctly handles naddr pointers by:
- Using useNostrEvent hook which supports both EventPointer and AddressPointer
- Falling back to addressPointer.pubkey when event hasn't loaded yet

Previously, zapping an naddr would fail because the component only
tried to load events via eventPointer.id, ignoring addressPointer.

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 10:15:11 +01:00
Alejandro
45e68bbdad fix: use addressPointer for zapping addressable events (#181)
When zapping addressable events (like spellbook kind 30777), the zapEvent
function was passing an AddressPointer as eventPointer. This caused an error
because ZapWindow expects eventPointer to have an `id` property, while
AddressPointer has `kind`, `pubkey`, `identifier`.

Fixed by:
- Always passing eventPointer with the event's id for the e-tag
- Additionally passing addressPointer for addressable events for the a-tag

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 09:58:37 +01:00
Alejandro
cbc1fc272a feat(user-menu): make relays and blossom servers collapsible dropdowns (#177)
* feat(user-menu): make relays and blossom servers collapsible dropdowns

Convert the relays and blossom servers sections in the user menu from
always-expanded lists to collapsible dropdown submenus. Each section
now shows a count next to the title and expands on click to show the
full list. This saves vertical space in the menu when users have many
relays or servers configured.

* refactor(user-menu): reorganize menu items for better UX

Reorder menu sections by logical grouping:
- Identity (user profile) at top
- Account config (wallet, relays, blossom) grouped together
- App preferences (theme) in consistent location
- Promotional (support grimoire) separated
- Session actions (login/logout) at bottom

This provides a more intuitive flow and consistent placement
regardless of login state.

* refactor(user-menu): revert dropdowns, add login/logout icons

- Revert relays and blossom servers back to flat lists (not dropdowns)
- Move login to first option for logged out users
- Add LogIn/LogOut icons from lucide-react
- Keep the reorganized menu structure

* style(user-menu): clean up icons and font consistency

- Make theme icon muted like other icons
- Remove HardDrive icons from blossom servers section
- Make blossom label consistent with relays (plain text)
- Remove unused HardDrive import

* fix(user-menu): close menu when clicking relays, blossom, or donate

Wrap RelayLink items in DropdownMenuItem with asChild to properly
trigger menu close on click. Convert Support Grimoire section from
a raw div to DropdownMenuItem so it also closes the menu.

* fix(user-menu): properly close menu when clicking relay items

Use pointer-events-none on RelayLink to make it purely presentational,
and handle the click on DropdownMenuItem instead. This ensures the
menu closes properly when clicking a relay.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-20 19:14:25 +01:00
Alejandro
13081938ff Fix long filename truncation in upload dialog (#176)
* fix: truncate long filenames in blossom upload dialog

- Add line-clamp-1 and proper width constraints to prevent long filenames
  from breaking layout
- Update default fallback server to blossom.band

* fix: use max-w-xs for proper filename truncation

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-20 17:39:51 +01:00
Alejandro
13fec0345f feat: zap goals nip-75 (#174)
* feat: add NIP-75 Zap Goal rendering

Add feed and detail view rendering for kind 9041 (Zap Goals):

- GoalRenderer: Shows clickable title, description, and progress bar
  with target/raised amounts
- GoalDetailRenderer: Adds sorted contributor breakdown with
  individual contribution totals
- nip75-helpers: Helper functions for extracting goal metadata
  (amount, relays, deadline, beneficiaries)

Both views fetch and tally zaps from the goal's specified relays.

* fix: improve NIP-75 goal rendering

- Remove icons from goal renderers
- Remove "sats" suffixes from amounts
- Use muted text instead of destructive color for closed goals
- Content is the title, summary tag is the description
- Only show description if summary tag exists

* feat: add zap button to goal renderers

Show a 'Zap this Goal' button in both feed and detail views
when the goal is still open (not past its closed_at deadline).

* style: unify progress indicator styles

- Update user menu and welcome page progress indicators to match
  goal renderer style: bar on top, progress/total below with percentage
- Remove "sats" suffix from progress displays
- Make goal zap button primary variant and full-width

* fix: polish goal renderers for release

- Remove limit parameter from useTimeline (use whatever relays send)
- Add more spacing between "Support Grimoire" header and progress bar

* refactor: extract useGoalProgress hook for NIP-75 goals

- Create useGoalProgress hook with shared goal logic
- Handle relay selection: goal relays → user inbox → aggregators
- Calculate progress, contributors, and all metadata in one place
- Simplify both GoalRenderer and GoalDetailRenderer

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-20 17:28:57 +01:00
fiatjaf_
b28d5f7892 remove unnecessary toast on popup. (#175) 2026-01-20 17:24:00 +01:00