Commit Graph

432 Commits

Author SHA1 Message Date
Alejandro Gómez
0818219ace fix: typo 2026-02-27 12:08:19 +01:00
Alejandro Gómez
b59afbaee3 docs: first draft of NIP 2026-02-27 12:07:02 +01:00
Alejandro Gómez
36d321649d ui: show hashtags as labels, not metadata 2026-02-26 22:51:53 +01:00
Alejandro Gómez
aef0eb820c fix: embed trakcs in playlist detail view 2026-02-26 22:47:38 +01:00
Alejandro Gómez
66a422d3f8 feat: add track and playlist rendering 2026-02-26 17:19:38 +01:00
Alejandro Gómez
41a1009166 fix: avoid unnecesary re-renders 2026-02-26 16:23:47 +01:00
Alejandro
d630a72409 Add relay list management UI and settings integration (#254)
* feat(settings): add relay lists management section

- Fetch additional relay list kinds (10006, 10007, 10050) on login
- Add "Relays" tab to Settings with accordion UI for each relay list kind
- Support NIP-65 relay list (kind 10002) with read/write markers
- Support blocked relays (10006), search relays (10007), DM relays (10050)
- Add/remove relays with URL sanitization and normalization
- Explicit save button publishes only modified lists as replaceable events

https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54

* docs: add plan for honoring blocked & search relay lists

Detailed implementation plan for:
- Kind 10006: filter blocked relays from all connection paths
- Kind 10007: use search relays for NIP-50 queries

https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54

* refactor(settings): extract relay list logic into tested lib, fix UX issues

Extract parseRelayEntries, buildRelayListTags, sanitizeRelayInput, and
comparison/mode helpers into src/lib/relay-list-utils.ts with 52 tests
covering roundtrips, normalization, edge cases, and mode conversions.

UX fixes:
- Replace RelayLink (navigates away on click) with static RelaySettingsRow
- Remove redundant inbox/outbox icons (mode dropdown is sufficient)
- Always-visible delete button instead of hover-only opacity
- Per-accordion dirty indicator (CircleDot icon) for modified lists
- Discard button to reset all changes
- Read-only account explanation text
- Human-friendly descriptions (no NIP references or kind numbers)
- Separator between relay list and add input
- Larger relay icons and text for readability

https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54

* feat(settings): use KindBadge and NIPBadge in relay list accordions

Replace plain text kind names with KindBadge (full variant showing icon,
name, and kind number) and add NIPBadge next to each list description.
This gives power users the protocol context they expect.

Also document KindBadge and NIPBadge as shared components in CLAUDE.md.

https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54

* feat(settings): add favorite relays list (kind 10012) to relay settings

Add kind 10012 (Favorite Relays / Relay Feeds) to the settings UI and
account sync fetching. Uses "relay" tags like other NIP-51 lists.

https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54

* fix(kinds): use semantic icons for blocked and search relay lists

- Kind 10006 (Blocked Relays): Radio → ShieldBan
- Kind 10007 (Search Relays): Radio → Search

These icons propagate to KindBadge, settings accordions, and event
renderers via getKindInfo(). Generic relay kinds (10002, 30002, etc.)
keep the Radio icon.

https://claude.ai/code/session_01JHirYU56sKDKYhRx6aCQ54

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 13:11:27 +01:00
Alejandro
96d3d5bab6 Refactor assertion subject rendering to use QuotedEvent component (#255)
* Use QuotedEvent for event/address assertion subjects

Replace opaque hex IDs and kind:pubkey:d-tag strings with inline
QuotedEvent rendering for event assertions (30383) and address
assertions (30384). Feed renderer uses depth=2 (collapsible) and
detail renderer uses depth=1 (inline expanded).

https://claude.ai/code/session_01L4Kwg3Ad4C2S1cvkvwACH1

* Update package-lock.json

https://claude.ai/code/session_01L4Kwg3Ad4C2S1cvkvwACH1

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 09:28:42 +01:00
Alejandro
dd6b30b82e Add NIP-85 Trusted Assertions support with renderers (#252)
* feat: add NIP-85 Trusted Assertions feed & detail renderers

Add support for NIP-85 Trusted Assertion events (kinds 30382-30385) and
the Trusted Provider Declaration (kind 10040) with kind constants,
helper library, and feed + detail renderers.

- Add kind entries for 10040, 30382, 30383, 30384, 30385 to EVENT_KINDS
- Create src/lib/nip85-helpers.ts with cached helpers for parsing
  assertion data (user, event, address, external) and provider lists
- Create shared TrustedAssertionRenderer for all 4 assertion kinds with
  rank bar, subject display, and compact metrics preview
- Create TrustedAssertionDetailRenderer with full metrics table,
  rank visualization, topics, and raw tag fallback
- Create TrustedProviderListRenderer/DetailRenderer for kind 10040
  with provider table and encrypted entries indicator
- Register all renderers in kinds/index.tsx

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB

* refactor: extract NIP-73 helpers and shared ExternalIdentifierDisplay

Move getExternalIdentifierIcon() and getExternalIdentifierLabel() from
nip22-helpers.ts into a new nip73-helpers.ts since they are NIP-73
utilities, not NIP-22 specific. Add inferExternalIdentifierType() and
getExternalIdentifierHref() helpers.

Create shared ExternalIdentifierDisplay components (inline + block
variants) that use proper NIP-73 type-specific icons (Globe for web,
BookOpen for ISBN, Podcast for podcasts, Film for ISAN, etc.) instead
of a generic ExternalLink icon.

- Kind 1111 renderer now uses ExternalIdentifierInline for root scope
- Kind 30385 assertion renderer uses ExternalIdentifierInline (feed)
  and ExternalIdentifierBlock (detail) for NIP-73 subjects
- nip22-helpers.ts re-exports from nip73-helpers for compatibility

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB

* fix: UI polish for NIP-85 renderers

Address review feedback across all NIP-85 components:

Icon: Replace BarChart3 with ShieldCheck for assertion kind constants
(30382-30385) — communicates "verified trust data" vs generic analytics.

Feed renderer: Subject is now the visual anchor (ClickableEventTitle)
with kind label as a small outline Badge above it. Rank bar widened to
w-32, color-coded green/yellow/red by score threshold. Fix "Zaps Recd"
abbreviation to "Zaps In".

Detail renderer: Metrics grouped into Activity, Zaps, Moderation
sections with uppercase section headers. Kind 30384 addresses now
show "Kind X by <UserName> / d-tag" instead of raw hex. ExternalMetrics
type badges now show NIP-73 icons + friendly labels (getExternalTypeLabel)
instead of raw k-tag values like "podcast:item:guid".

Provider list: Feed uses compact Badge for kind:tag + count summary.
Detail uses stacked cards instead of 3-column table for narrow panels.

ExternalIdentifierBlock: Linked blocks now show dotted underline on
label + ExternalLink icon for clear click affordance.

Add getExternalTypeLabel() to nip73-helpers for k-value display names.

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB

* fix: use Progress component for rank bars, consistent sats formatting

Replace custom rank bar divs with the existing Progress UI component
(with new indicatorClassName prop for color-coded fills).

Make sat amount displays consistent with zap receipt renderer pattern:
value and "sats" unit are rendered as separate elements — numeric value
in font-medium, unit in smaller muted text — matching how Kind9735
displays amounts across the app.

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB

* fix: use RelayLink for relay URLs, remove redundant kindTag, add relay hints

Provider list renderers now use RelayLink instead of raw relay URL
strings — shows favicon, insecure ws:// warning, opens relay detail
on click.

Remove kindTag display from provider cards — it's an internal
protocol detail redundant in the UI context.

Pass relay hints from provider entries to UserName so profiles can
be fetched from the relay the provider actually publishes to.

Add UserName relayHints prop (forwarded to useProfile).

Add RelayLink and UserName to Shared Components section in CLAUDE.md
so they're consistently used across the codebase.

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB

* docs: expand shared components section in CLAUDE.md

Add BaseEventContainer, ClickableEventTitle, RichText, QuotedEvent,
and CustomEmoji to the shared components reference. These are the
core building blocks used across all kind renderers — documenting
them prevents re-implementation and ensures consistent patterns.

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB

* docs: trim shared components list in CLAUDE.md

Remove BaseEventContainer and QuotedEvent — these are internal
patterns that kind renderer authors already know from context,
not general-purpose components that get misused or forgotten.

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB

* feat: show metric type labels in trusted assertion feed view

Add Label components to the assertion feed renderer so you can see at a
glance which metrics an assertion carries (Followers, Posts, Zaps, etc.)
instead of just numeric values. Also swap Badge → Label for the kind
indicator for visual consistency.

Replace hardcoded green/yellow/red rank colors with theme variables
(success/warning/destructive) in both feed and detail renderers so the
rank bar works correctly across all themes.

Add Label to CLAUDE.md shared components list (22 imports across the
codebase).

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB

* feat: show provider kind tag in trusted provider list renderers

Add Label with formatKindTag() to both feed and detail views so each
provider row shows what it provides (e.g. "User Assertion: Rank").
Also swap Badge → Label for consistency with the assertion renderers.

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB

* fix: stabilize relayHints in useProfile to prevent fetch abort loop

relayHints was used directly in the useEffect dependency array, so
callers passing a new array literal (e.g. [p.relay]) on every render
caused the effect to re-run each cycle — aborting the previous network
fetch before it could complete. The IndexedDB fast-path masked this in
the feed view (profiles already cached), but the detail view showed
raw pubkeys because profiles were never fetched from the network.

Wrap relayHints in a JSON.stringify-based useMemo (same pattern as
useStableArray) so the effect only re-runs when the actual relay
values change.

https://claude.ai/code/session_01XjwLaShFSVPR5gNA7iUjuB

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 09:14:14 +01:00
Alejandro
6c2adc01e0 refactor: extract relay auth manager into standalone package (#249)
* refactor: extract relay auth manager into standalone package

Decouple NIP-42 relay authentication from Grimoire internals into a
generic, framework-agnostic package at packages/relay-auth-manager/.

The new package uses dependency injection for pool, signer, and storage
(localStorage-like interface), making it reusable in any applesauce-based
app. Fixes the bug where auth prompts appeared even when the signer
couldn't sign events - now only emits pending challenges when a signer
is available.

Key changes:
- New package with RelayAuthManager class, pure auth state machine,
  and comprehensive test suite (103 tests)
- Grimoire's relay-state-manager now delegates all auth logic to the
  package, retaining only connection/notice tracking
- Auth preferences moved from Dexie to localStorage via pluggable storage
- Reactive signer lifecycle: auto-auth re-evaluates when signer appears
- Pool relay lifecycle via add$/remove$ observables (no polling)

https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd

* fix: prevent auth prompts when signer is unavailable

Three bugs fixed:

1. Race condition in emitState(): states$ was emitted before
   pendingChallenges$, so relay-state-manager's states$ subscriber
   would read stale pendingChallenges$.value. Now pendingChallenges$
   is emitted first for consistent reads.

2. relay-state-manager only subscribed to states$, missing
   pendingChallenges$ changes. Now subscribes to both.

3. canAccountSign used constructor.name which is fragile under
   minification. Now uses account.type !== "readonly" (stable
   property from applesauce-accounts).

Added 3 regression tests verifying pendingChallenges$.value
consistency when observed from states$ subscribers.

https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd

* fix: migrate auth preferences from Dexie to localStorage

One-time migration preserves existing user preferences: reads from
Dexie relayAuthPreferences table, writes to localStorage, injects
into the running manager, then clears the Dexie table. Skips if
localStorage already has data (idempotent).

Removes the DexieAuthStorage adapter — sync/async impedance mismatch
made it fragile. localStorage is the right fit for the sync
getItem/setItem interface.

https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd

* docs: add README for relay-auth-manager package

Covers constructor options, observables, methods, auth lifecycle,
preferences, storage interface, state machine, and dependency
interfaces.

https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd

* fix: address 3 bugs in relay-auth-manager

1. State machine: authenticated + CHALLENGE_RECEIVED with "never"
   preference now correctly auto-rejects instead of entering a dead
   challenge_received state that nobody acts on.

2. Auto-auth error handlers now guard with `status === "authenticating"`
   before overwriting state, preventing a late-resolving promise from
   clobbering a valid state transition (e.g. disconnect → "none").

3. API no longer leaks mutable internal references. getRelayState(),
   getAllStates(), getAllPreferences(), and states$ emissions all return
   shallow copies. Previously, consumers holding a reference to a
   previous emission would see it silently mutate.

All three fixes include regression tests (110 tests, up from 103).

https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd

* feat: make relay-auth-manager production-ready

- authenticate() now waits for authenticated$ confirmation via
  firstValueFrom(race(...)) — resolves only when relay confirms,
  rejects on disconnect. Uses Promise.all to avoid unhandled rejections.
- Add retry() for relays stuck in "failed" state.
- Add removePreference() to prevent unbounded preference growth.
- Add normalizeUrl option for custom URL normalization — preferences and
  session rejections now consistently normalized via normalizeForKey().
- Wire Grimoire's normalizeRelayURL as the normalizer in relay-auth.ts.
- Update README with new methods, normalizeUrl option, retry lifecycle.
- 123 package tests passing.

https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd

* chore: prepare relay-auth-manager for npm publishing

- Point exports/main/types to dist/ for correct npm resolution
- Add tsconfig.build.json for emitting ESM JS + declarations
- Add build/clean/prepublishOnly scripts
- Add LICENSE, description, keywords, author, files field
- Add Vite resolve alias + tsconfig paths for workspace dev
  (resolves source directly, no pre-build needed for dev)
- Fix TypeScript strict errors in test file
- Clean npm pack output: dist/, README.md, LICENSE only

https://claude.ai/code/session_01XqrjeQVtJKw9uC1XAw6rqd

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-20 08:52:50 +01:00
Alejandro
c8fb1b005b feat(kind-11): add dedicated ThreadRenderer with optional title (#250)
Kind 11 events are thread roots (NIP-7D) with an optional title tag.
Previously they reused the Kind1Renderer (short text note). Now they
get a dedicated ThreadRenderer that shows the title as a clickable
heading (via ClickableEventTitle) when present, followed by the
content rendered with RichText.

https://claude.ai/code/session_01SQLhrJfeQHceRAS1MjYHrN

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-15 13:23:33 +01:00
Alejandro
62ce435043 feat(kind-1111): show root scope and parent context per NIP-22 (#245)
* feat(kind-1111): show root scope and parent context per NIP-22

The Kind 1111 renderer previously only showed the immediate parent reply,
missing the root scope that defines what a comment thread is about. Now
shows both the root item (blog post, file, URL, podcast, etc.) and the
parent reply (when replying to another comment), giving full threading
context.

- Add NIP-22 helper library using applesauce pointer helpers
  (getEventPointerFromETag, getAddressPointerFromATag,
  getProfilePointerFromPTag) for tag parsing
- Support external identifiers (I-tags) with NIP-73 type-specific icons
- Unified ScopeRow component for consistent inline display
- Only show parent reply line for nested comments (not top-level)

https://claude.ai/code/session_01Dxedi6VWdZG8nFpba211oR

* fix(kind-1111): unify scope row styling, fix redundant icons and link style

- Replace separate NostrEventCard/ExternalScopeCard with children-based
  ScopeRow so root and reply rows look identical
- Remove redundant icons: root events show only KindBadge, replies show
  only Reply arrow — no double icons
- External URLs now use the standard link style (text-accent + dotted
  underline on hover) matching UrlList, with ExternalLink icon
- Children are direct flex items for proper gap spacing

https://claude.ai/code/session_01Dxedi6VWdZG8nFpba211oR

* fix(kind-1111): match icon sizes and make external URLs clickable

- Set KindBadge iconClassname to size-3 so it matches Reply and
  ExternalLink icons (was size-4 by default)
- Use the I-tag value itself as href when it's a URL and no hint is
  provided — most web I-tags only have the URL in value, not in hint

https://claude.ai/code/session_01Dxedi6VWdZG8nFpba211oR

* fix(kind-1111): use muted style for external link rows

Match the muted-foreground + dotted underline style used by other
scope rows instead of accent color.

https://claude.ai/code/session_01Dxedi6VWdZG8nFpba211oR

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-12 22:43:09 +01:00
Alejandro
7167d64d9c fix(status): show Open by default instead of infinite loading spinner (#244)
StatusIndicator was stuck showing a loading spinner indefinitely when no
status events existed, because the useTimeline observable never completes
for live Nostr subscriptions. Default to showing "Open" immediately and
update reactively when status events arrive.

https://claude.ai/code/session_01UAjKegpi8uWyLdjgRpNeEJ

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-12 22:16:18 +01:00
Fernando López Guevara
140a4b207a feat(req-viewer): add compact/list view toggle (#236) 2026-02-04 15:05:26 +01:00
Alejandro
53d156ba04 Limit message batch in NIP-53 chat adapter (#239)
Apply the same EOSE-gating pattern from NIP-29 adapter to prevent
partial renders during initial message load. The loadMessages
observable now only emits after EOSE is received from relays.

https://claude.ai/code/session_01NFuxEMNBiFW2RA2gHffvMh

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-02 13:53:14 +01:00
Alejandro
4be8c6e819 Optimize initial chat scroll to avoid slow animation on load (#238)
* fix: disable smooth scroll on initial chat load

The chat viewer was using smooth scrolling for all followOutput events,
including when first loading the message list. This caused a slow,
animated scroll towards the latest messages on initial load.

Fix by tracking whether initial render is complete and only enabling
smooth scrolling for subsequent followOutput events (new incoming
messages). The ref is reset when switching between conversations.

https://claude.ai/code/session_0174r1Ddh5e2RuhHf2ZrFiNc

* fix: use instant scroll instead of disabling scroll on initial load

The previous fix returned `false` which disabled scrolling entirely,
causing incorrect scroll position. Changed to return "auto" which
scrolls instantly (no animation) while still positioning correctly.

https://claude.ai/code/session_0174r1Ddh5e2RuhHf2ZrFiNc

* fix: buffer NIP-29 messages until EOSE to prevent partial renders

The loadMessages observable was emitting on every event as they streamed
in before EOSE, causing multiple re-renders with partial message lists.
This resulted in incorrect scroll positions during initial chat load.

Now uses combineLatest with a BehaviorSubject to track EOSE state and
only emits the message list after the initial batch is fully loaded.
New messages after EOSE continue to trigger immediate updates.

https://claude.ai/code/session_0174r1Ddh5e2RuhHf2ZrFiNc

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-02 11:42:13 +01:00
Alejandro
38a6dddedb Fix LNURL comment handling to respect server capabilities (#237)
* fix(zap): only send comment to LNURL callback if server accepts it

The comment in the zap request event (kind 9734 content) is always
included as that's part of the Nostr protocol. However, the LNURL
callback `comment` parameter should only be sent if the server's
`commentAllowed` field is > 0.

Previously, the comment was always sent to the LNURL callback
regardless of the server's comment support, which could cause issues
with servers that don't accept comments.

https://claude.ai/code/session_01KTcAyKHVaqg4QKKXQxUzum

* fix(zap): silently skip comment in LNURL callback if too long

Instead of throwing an error when the comment exceeds the server's
commentAllowed limit, simply don't include it in the LNURL callback.
The comment is still preserved in the zap request event (kind 9734).

https://claude.ai/code/session_01KTcAyKHVaqg4QKKXQxUzum

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-02 11:04:59 +01:00
Fernando López Guevara
a7395055af fix(time-format): avoid displaying "0m ago"; show weeks instead (#235) 2026-01-31 00:13:15 +01:00
Alejandro
0a5c80bc9c fix: pass mediaType and blobUrl props to BlossomViewer (#234)
The --type flag for blossom blob command was being parsed correctly but
the mediaType and blobUrl props were not forwarded from WindowRenderer
to BlossomViewer, causing the preview to always show "preview not available".

https://claude.ai/code/session_018edX9kUcEudJzwuFCGh1xf

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-30 17:41:05 +01:00
Alejandro
eca21d3191 feat: load media setting (#230)
* feat: add custom media renderer support to RichText

Add renderMedia prop to RichText component that allows overriding
how images, videos, and audio are rendered across the entire
RichText subtree (including Link and Gallery components).

The custom renderer receives:
- url: the media URL
- type: "image" | "video" | "audio"
- imeta: optional NIP-92 metadata (dimensions, blurhash, alt, etc.)

This enables chat-specific media rendering with compact thumbnails,
lightboxes, or any custom presentation.

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* feat: add ChatMediaRenderer for compact inline media display

Custom media renderer for chat that shows:
- Media type icon (image/video/audio)
- Filename (extracted from URL or imeta alt)
- File size (from imeta if available)
- Blossom link button (opens blob viewer for blossom URLs)

Usage:
  <RichText event={message} renderMedia={ChatMediaRenderer} />

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* feat: use ChatMediaRenderer in chat messages

- Updated ChatMediaRenderer to use getHashFromURL from blossom-client-sdk
- Integrated ChatMediaRenderer into ChatViewer for message content

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* style: make ChatMediaRenderer inline with subtle dotted border

Use a subtle dotted bottom border instead of background/padding
to better integrate with surrounding text flow.

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* feat: improve ChatMediaRenderer UX

- Full dotted border with rounded corners (was bottom-only)
- Increased gap between elements
- Images/videos open in MediaDialog zoom viewer on click
- Audio still opens in new tab
- Better extension extraction for display

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* feat: improve blossom integration in ChatMediaRenderer

- Use HardDrive icon instead of Flower2 (consistent with rest of app)
- Add commandString for Edit functionality in window toolbar
- Pass blobUrl with extension to BlossomViewer for correct media preview
- Extended BlossomViewer.BlobDetailView to accept blobUrl prop

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* refactor: simplify ChatMediaRenderer styling

- All icons now size-3
- Use dotted underline only (cleaner inline appearance)
- All media types open in new tab on click
- Removed MediaDialog dependency

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* fix: inherit custom media renderer in nested RichText components

Previously, nested RichText components (like those in QuotedEvent/KindRenderer)
would always reset the media renderer context to null because the provider
used `renderMedia ?? null`. This caused embedded events in chat to use
the default media renderer instead of ChatMediaRenderer.

Now RichText inherits the parent media renderer when no explicit renderMedia
prop is passed, allowing custom renderers to work throughout the entire
rich text subtree including quoted events.

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* feat: improve ChatMediaRenderer with expandable inline media

- More compact representation: "icon truncated-hash blossom"
- Click filename to expand and show actual media inline (image/video/audio)
- Tooltip shows full filename and file size on hover
- Audio support with inline audio player when expanded
- Fixed BlossomViewer to detect media type from URL extension when
  mimeType is not available from blob descriptor

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* feat: improve ChatMediaRenderer with better UX

- Click to expand shows MediaEmbed (not collapsible once expanded)
- Tooltip only shows when imeta is present, displays: alt, mime type,
  dimensions, size, duration
- More compact representation: [icon] truncated-hash [blossom]
- Pass mediaType to BlossomViewer for correct preview rendering
- BlossomViewer now accepts mediaType hint for preview type detection

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* fix: blossom blob command now parses --type flag

- Added mediaType and blobUrl fields to BlossomCommandResult
- Blob subcommand now parses --type image|video|audio flag
- Fixed server URL overflow in blob detail view with truncation

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

* refactor: add global loadMedia setting with CompactMediaRenderer

- Add `loadMedia` setting to Appearance section (enabled by default)
- Create CompactMediaRenderer in src/components/nostr/ (renamed from ChatMediaRenderer)
- Link.tsx and Gallery.tsx now check the setting and use CompactMediaRenderer when disabled
- Tooltip format improved: "Field <value>" with field name and value side by side
  - Shows: Hash, Size, Dimensions, Type, Duration, Alt
- Remove renderMedia prop from ChatViewer (now automatic based on setting)
- Delete old ChatMediaRenderer.tsx

This makes media rendering a site-wide setting rather than chat-specific.

https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-30 17:34:33 +01:00
Alejandro
a75dd2b3fb Remove compact mode settings and simplify settings architecture (#232)
* refactor: remove unused compactModeKinds from app state

The compactModeKinds feature was partially implemented but never used:
- Settings dialog allowed configuration but the value was never consumed
- REQ viewer uses --view flag for compact mode, not this state value

Removed:
- compactModeKinds from GrimoireState type and initial state
- setCompactModeKinds logic function and state hook callback
- Compact Events section from SettingsDialog (now shows placeholder)
- compactModeKinds from AppearanceSettings in settings service
- Migration now strips compactModeKinds if present instead of adding it

https://claude.ai/code/session_01DiWUxiS5BAzU9mrKvCUuMW

* chore: delete unused SettingsDialog component

The SettingsDialog was imported but never used - the Settings menu
item opens a window via addWindow("settings", ...) instead.

https://claude.ai/code/session_01DiWUxiS5BAzU9mrKvCUuMW

* refactor: simplify settings service to only used settings

Removed unused settings that were defined but never consumed:
- RelaySettings (fallbackRelays, discoveryRelays, outbox*, etc.)
- PrivacySettings (shareReadReceipts, blurWalletBalances, etc.)
- DatabaseSettings (maxEventsCached, autoCleanupDays, etc.)
- NotificationSettings (enabled, notifyOnMention, etc.)
- DeveloperSettings (debugMode, showEventIds, logLevel, etc.)
- Most of AppearanceSettings (theme, fontSizeMultiplier, etc.)
- Most of PostSettings (defaultRelayMode, customPostRelays)

Only kept settings that are actually used:
- post.includeClientTag
- appearance.showClientTags

Also simplified useSettings hook to match.

https://claude.ai/code/session_01DiWUxiS5BAzU9mrKvCUuMW

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-30 14:23:30 +01:00
Alejandro
8f2f055566 feat: repository tree visualization (#31)
* docs: add plan for repository tree visualization feature

Comprehensive plan covering:
- git-natural-api library analysis and API documentation
- useGitTree/useGitBlob hooks for fetching git data
- FileTreeView component using Radix Collapsible
- Shiki migration with on-demand language loading
- Multi-server fallback for redundant clone URLs
- Dexie caching for offline access

* docs: add comprehensive Shiki migration plan

Detailed plan for migrating from Prism.js to Shiki with lazy loading:
- Analysis of all 8 components using SyntaxHighlight
- Shiki service with singleton highlighter and on-demand language loading
- Custom Grimoire dark theme matching current Prism styles
- Language alias mapping for 50+ extensions
- React hook for async highlighting with loading states
- CSS theme preserving minimalistic dark aesthetics
- Migration steps with rollback plan

* feat: migrate syntax highlighting from Prism.js to Shiki

Replace Prism.js with Shiki for syntax highlighting with several key improvements:

- Lazy loading: Languages loaded on-demand via dynamic imports instead of bundling all upfront
- Broader language support: 200+ TextMate grammars vs 11 statically imported
- Singleton highlighter: Core languages (JS, TS, JSON, diff, bash) preloaded, others loaded on first use

New files:
- src/lib/shiki.ts: Shiki service with highlightCode(), normalizeLanguage(), language aliases
- src/hooks/useHighlightedCode.ts: React hook for async highlighting with loading states
- src/styles/shiki-theme.css: Grimoire dark theme matching previous minimalistic style

Updated components:
- SyntaxHighlight: Now uses Shiki with graceful loading/error states
- CodeSnippetRenderer/DetailRenderer: Simplified, removed manual language mapping
- MarkdownContent: Removed type casts, any language now supported

Removed:
- prismjs and @types/prismjs dependencies
- src/styles/prism-theme.css

* feat: add repository file tree visualization

Add file tree explorer to the Repository detail renderer (kind 30617)
using @fiatjaf/git-natural-api for fetching git trees via HTTP.

New files:
- src/lib/git-types.ts: TypeScript types for DirectoryTree, SelectedFile, etc.
- src/hooks/useGitTree.ts: Hook to fetch git repository tree from clone URLs
  - Tries multiple clone URLs in sequence
  - Uses getDirectoryTreeAt with filter capability when available
  - Falls back to shallowCloneRepositoryAt otherwise
- src/hooks/useGitBlob.ts: Hook to fetch individual file content by hash
  - Detects binary files
  - Returns both raw Uint8Array and decoded text
- src/components/ui/FileTreeView.tsx: Recursive tree view component
  - Collapsible directories with chevron icons
  - File icons based on extension (code, json, text, image, etc.)
  - Alphabetical sorting with directories first
- src/components/nostr/kinds/RepositoryFilesSection.tsx: Main integration
  - Side-by-side tree and file preview layout
  - Syntax-highlighted file content using existing SyntaxHighlight
  - Binary file detection with appropriate UI
  - Loading/error states

Modified:
- RepositoryDetailRenderer.tsx: Added RepositoryFilesSection below relays

Dependencies:
- Added @fiatjaf/git-natural-api from JSR

* fix: improve repository tree visualization UX

- Collapse directories by default in file tree
- Hide files section on tree loading error
- Add code-like skeleton loader with file header
- Fix syntax highlight size jump between loading/loaded states
- Replace purple accent with grayscale theme
- Preload Rust and Markdown languages for reliable highlighting

* refactor: improve git tree and syntax highlighting

- Remove shallow clone fallback from useGitTree, only use no-blobs fetch
  Servers without filter capability are skipped instead of downloading blobs
- Add light theme support for Shiki syntax highlighting
  Theme is automatically selected based on current color scheme

* fix: improve dark theme contrast for syntax highlighting

* fix: address code review issues

- useGitTree: use useStableArray for cloneUrls to fix dependency tracking
- useGitTree/useGitBlob: add isMounted checks to prevent state updates after unmount
- RepositoryFilesSection: remove unnecessary useMemo for language
- FileTreeView: use path instead of hash for React keys
- shiki: track failed languages to avoid repeated console warnings

* fix: improve dark theme contrast for syntax highlighting

- Add CSS variables for syntax highlighting instead of hardcoded colors
- Add --syntax-constant and --syntax-tag variables to light and dark themes
- Use high contrast colors for dark mode (bright green strings, purple keywords)
- Simplify Shiki transformer to output CSS classes instead of inline styles
- Remove unused parameters from transformer callback

* fix: restore syntax highlighting colors

Revert the CSS class-based approach which was failing to classify tokens.
Instead, keep Shiki's inline styles from the theme and only remove
backgrounds to let CSS handle those. The theme colors now provide
syntax highlighting directly.

* feat: add copy button and CSS variable-based syntax highlighting

- Add copy button next to file name in file viewer header (icon-only)
- Use Shiki's createCssVariablesTheme for proper theme integration
- Map Shiki CSS variables to our theme system variables
- Syntax highlighting now works correctly across all themes (light/dark)

* refactor: create IconCopyButton component and use CopyCheck consistently

- Add IconCopyButton component for reusable icon-only copy buttons
- Refactor RepositoryFilesSection to use IconCopyButton
- Replace Check with CopyCheck in ChatMessageContextMenu
- Replace Check with CopyCheck in BaseEventRenderer
- Use text-success instead of text-green-500 for consistency

* fix: add HTML, CSS, TOML to core languages and expand token mappings

- Add html, css, toml to CORE_LANGUAGES for eager loading
- Add variableDefaults to cssVarsTheme for proper initialization
- Expand shiki-theme.css with more token type mappings:
  - HTML/XML: tag, attribute, attr-value
  - CSS: selector, property
  - Additional: variable, operator, number, boolean, regex, etc.

* fix: improve diff line spacing with flex layout

- Use flex-col with gap-0 on code element for tight line packing
- Reduce line-height from 1.5 to 1.4 for tighter spacing
- Add .line display:block with min-height for consistent sizing
- Simplify diff background styling (remove negative margin hack)

* fix: improve code block line spacing and wrap long lines

- Increase line-height from 1.4 to 1.5 for better readability
- Use pre-wrap instead of pre to allow long line wrapping
- Add overflow-wrap: break-word to break long URLs/strings

* chore: remove planning docs

* chore: update @fiatjaf/git-natural-api to 0.2.3

* fix: make code blocks horizontally scrollable with full-width diff backgrounds

- Use white-space: pre for horizontal scrolling instead of wrapping
- Add width: fit-content and min-width: 100% to code element
- Ensure diff line backgrounds extend full width when scrolling

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-30 12:35:59 +01:00
Alejandro
121fbb7654 feat: add "+ new tab" option to move window menu (#229)
* feat: add "+ new tab" option to move window menu

Adds a "+ New tab" option at the top of the "Move to tab" submenu in the
window toolbar. This allows users to create a new tab and move the current
window to it in a single action, streamlining the workflow.

Changes:
- Add moveWindowToNewWorkspace function in logic.ts
- Expose moveWindowToNewWorkspace through useGrimoire hook
- Always show "Move to tab" menu (not just when multiple tabs exist)
- Add "+ New tab" option with Plus icon at top of submenu

https://claude.ai/code/session_01Hy2vYBooPyrF7ZJCodcCav

* refactor: simplify new tab menu item copy

- Change menu text from "+ New tab" to "New" (icon already shows +)
- Simplify toast to just "Moved to new tab"

https://claude.ai/code/session_01Hy2vYBooPyrF7ZJCodcCav

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 21:08:34 +01:00
Alejandro
5d35832f85 feat: enhance /review command with deep code review capabilities (#228)
- Accept PR number, branch name, or review uncommitted changes
- Reference project skills (React, Applesauce, Nostr) for context
- Prioritized review criteria: correctness, protocol compliance,
  applesauce patterns, React best practices, simplicity, consistency
- Structured output format with file:line references
- Senior reviewer persona focused on maintainability

https://claude.ai/code/session_012e61BRqaF8LwUAFJuz8FaP

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 18:59:57 +01:00
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