* feat: add Educational Resource (kind 30142) with AMB metadata support
- Add feed and detail renderers for AMB Educational Resource events
- Add amb-helpers library with cached helper functions and tests
- Handle broken thumbnail images with BookOpen placeholder fallback
- Surface primary resource URL prominently in both renderers (bookmark-style)
- Register kind 30142 with GraduationCap icon in kind registry
- Link NIP-AMB badge to community NIP event (kind 30817)
* fix: address PR #260 review comments for Educational Resource renderers
- Rename Kind30142*Renderer to EducationalResource*Renderer (human-friendly names)
- Localize language names with Intl.DisplayNames via shared locale-utils
- Use ExternalLink component for license and reference URLs
- Localize ISO dates with formatISODate, fixing UTC timezone shift bug
- Remove # prefix from keyword labels in both feed and detail renderers
- Remove image/thumbnail from feed renderer
- Extract getBrowserLanguage to shared locale-utils, reuse in amb-helpers
* fix: mock getBrowserLanguage in tests for Node < 21 compat
Tests were directly mutating navigator.language which doesn't exist
as a global in Node < 21, causing ReferenceError in CI.
* feat: centralize publish flow with RxJS-based PublishService
Create a unified PublishService that:
- Provides consistent relay selection (outbox + state + hints + fallbacks)
- Emits RxJS observables for per-relay status updates
- Handles EventStore integration automatically
- Supports both fire-and-forget and observable-based publishing
Refactor all publish locations to use the centralized service:
- hub.ts: Use PublishService for ActionRunner publish
- delete-event.ts: Use PublishService (fixes missing eventStore.add)
- publish-spell.ts: Use PublishService with relay hint support
- PostViewer.tsx: Use publishWithUpdates() for per-relay UI tracking
This lays the groundwork for the event log feature by providing
observable hooks into all publish operations.
* feat: add LOG command for relay event introspection
Add an ephemeral event log system that tracks relay operations:
- EventLogService (src/services/event-log.ts):
- Subscribes to PublishService for PUBLISH events with per-relay status
- Monitors relay pool for CONNECT/DISCONNECT events
- Tracks AUTH challenges and results
- Captures NOTICE messages from relays
- Uses RxJS BehaviorSubject for reactive updates
- Circular buffer with configurable max entries (default 500)
- useEventLog hook (src/hooks/useEventLog.ts):
- React hook for filtering and accessing log entries
- Filter by type, relay, or limit
- Retry failed relays directly from the hook
- EventLogViewer component (src/components/EventLogViewer.tsx):
- Tab-based filtering (All/Publish/Connect/Auth/Notice)
- Expandable PUBLISH entries showing per-relay status
- Click to retry failed relays
- Auto-scroll to new entries (pause on scroll)
- Clear log button
- LOG command accessible via Cmd+K palette
* fix: prevent duplicate log entries and check relay OK response
- EventLogService: Check for existing entry before creating new one
when handling publish events (prevents duplicates from start/complete)
- PublishService: Check response.ok from pool.publish() to detect
relay rejections instead of assuming success on resolve
- Update test mock to return proper publish response format
* feat: keep relay selection in call site, compact logs
* chore: cleanup
* fix: make Timestamp component locale-aware via formatTimestamp
Timestamp was hardcoded to "es" locale. Now uses formatTimestamp()
from useLocale.ts for consistent locale-aware time formatting.
Added Timestamp to CLAUDE.md shared components documentation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: improve event-log reliability, add ERROR type and per-relay timing
Service improvements:
- Fix notice$ duplicate logging with per-relay dedup tracking
- Remove dead Array.isArray code path (notice$ emits strings)
- Increase relay poll interval from 1s to 5s
- Clean publishIdToEntryId map on terminal state, not just overflow
- Immutable entry updates (spread instead of in-place mutation)
- Extract NewEntry<T>/AddEntryInput helper types for clean addEntry signature
- Clear lastNoticePerRelay on log clear
New capabilities:
- ERROR log type: subscribes to relay.error$ for connection failure reasons
- RelayStatusEntry with updatedAt timestamp for per-relay response timing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: improve EventLogViewer with virtualization, timing, and error display
- Virtualize log list with react-virtuoso for 500-entry buffer performance
- Add ErrorEntry renderer for new ERROR log type (AlertTriangle icon)
- Show per-relay response time (e.g. "142ms", "2.3s") in publish details
- Make all entry types expandable (connect/disconnect now have details)
- Show absolute timestamp in all expanded detail views
- Group ERROR events under Connect tab filter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent duplicate PUBLISH log entries from completion event
PublishService emits publish$ twice: once at start, once on completion.
The eager publishIdToEntryId cleanup in handleStatusUpdate fired before
the completion emission, causing handlePublishEvent to create a second
entry. Removed eager cleanup — overflow eviction is sufficient.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Removes ~45 lines of identical relay resolution boilerplate duplicated
across 6 renderers (Issue, Patch, PR - feed and detail views).
The hook encapsulates the 3-tier fallback: repo relays → owner outbox →
aggregators, and also returns the repository event needed for
getValidStatusAuthors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(editor): replace DOM manipulation with React node views and floating-ui
- Convert all inline node views (emoji, blob attachment, event preview) from
imperative document.createElement() to React components via ReactNodeViewRenderer
- Replace tippy.js with @floating-ui/react-dom for suggestion popup positioning
- Create useSuggestionRenderer hook bridging Tiptap suggestion callbacks to React state
- Extract shared EmojiMention, SubmitShortcut, and inline node extensions to separate files
- Extract types (EmojiTag, BlobAttachment, SerializedContent) to editor/types.ts
- Extract serialization logic to editor/utils/serialize.ts
- Remove redundant DOM keydown listener from RichEditor (handled by SubmitShortcut extension)
- Remove tippy.js dependency (-1045 lines net, RichEditor 632→297, MentionEditor 1038→354)
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* fix(editor): fix suggestion popover positioning, scrolling, and profile click behavior
- Replace UserName component in ProfileSuggestionList with plain text display
so clicking a suggestion autocompletes instead of opening their profile
(UserName has an onClick that calls addWindow and stopPropagation)
- Add react-virtuoso to ProfileSuggestionList for efficient lazy rendering
of up to 20 search results with fixed item height scrolling
- Add profile avatars with lazy loading and initial-letter fallback
- Fix SuggestionPopover positioning with autoUpdate for scroll/resize tracking
- Add size middleware to constrain popover max-height to available viewport space
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* refactor(editor): convert emoji suggestion from grid to scrollable list with Virtuoso
Replace the 8-column grid layout with a vertical list matching the profile
suggestion style — each row shows the emoji preview alongside its :shortcode:
name. Uses react-virtuoso with fixedItemHeight for lazy rendering and smooth
keyboard-driven scrolling through large emoji sets.
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* fix(editor): set mentionSuggestionChar to ':' for emoji nodes
When backspacing over a mention-based node, Tiptap inserts the node's
mentionSuggestionChar attribute as undo text. The EmojiMention extension
inherits Mention's default of '@', so deleting an emoji left '@' instead
of ':'. Fix by explicitly setting mentionSuggestionChar: ':' in the emoji
command's attrs for both RichEditor and MentionEditor.
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* test(editor): add comprehensive test suite for custom TipTap extensions
Tests all 8 custom extensions using headless TipTap Editor instances in
jsdom environment (TipTap has no official testing package):
- EmojiMention: schema, renderText (unicode vs custom), mentionSuggestionChar
attribute handling, backspace behavior regression test
- BlobAttachmentRichNode/InlineNode: schema (block vs inline), attributes,
renderText URL serialization, parseHTML selectors
- NostrEventPreviewRichNode/InlineNode: schema, renderText encoding for
note/nevent/naddr back to nostr: URIs
- SubmitShortcut: Mod-Enter always submits, Enter behavior with
enterSubmits flag
- FilePasteHandler: media type filtering (image/video/audio), non-media
rejection, mixed paste filtering, edge cases (no files, no callback)
- NostrPasteHandler: bech32 regex matching (npub/note/nevent/naddr/nprofile),
nostr: prefix handling, URL exclusion, node creation (mention vs preview),
surrounding text preservation, multiple entities
- Serialization: formatBlobSize, serializeRichContent (emoji tag extraction,
blob dedup, address refs), serializeInlineContent (mention→nostr: URI,
emoji→shortcode, blob→URL, event preview encoding)
90 new tests total.
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* fix(editor): paste handler and serialization bugs found via adversarial testing
NostrPasteHandler fixes:
- Punctuation after bech32 now matches (npub1..., npub1...! etc.)
Changed trailing lookahead from (?=$|\s) to (?=$|\s|[.,!?;:)\]}>])
- Fixed double-space between entities — unconditional " " after every
entity caused doubled spaces. Now only adds trailing space when entity
is at the very end of pasted text (for cursor positioning).
- Tightened regex character class from [\w] to [a-z0-9] to match actual
bech32 charset (rejects uppercase, underscore)
- Wrapped dispatch in try/catch to handle block-node-at-inline-position
errors gracefully (falls back to default paste)
Serialization fix:
- serializeRichContent now guards blob collection with `url && sha256`
matching the defensive checks already in serializeInlineContent.
Previously null sha256 would corrupt the dedup Set and null url would
produce invalid BlobAttachment entries.
Added 22 new edge case tests:
- Paste handler: punctuation boundaries, double-space regression,
malformed bech32 fallback, uppercase rejection, error resilience
- Serialization: empty editor, null sha256/url blobs, invalid pubkey
fallback, missing mention attrs, inline dedup, multi-paragraph
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* fix(editor): raise suggestion search limits for profiles and emojis
Both suggestion dropdowns use Virtuoso for virtualized rendering, so
they can handle large result sets without performance issues. The
previous limits (20 profiles, 24 emojis) were too restrictive — users
with many custom emojis sharing a substring or large contact lists
couldn't scroll to find the right match.
Raised both limits to 200 to allow thorough browsing while still
bounding the result set.
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* refactor(chat): rework emoji picker to scrollable list with search
Replace the fixed 1-row grid (8 emojis) with a scrollable virtualized
list matching the editor's EmojiSuggestionList look & feel:
- Search box at top with magnifying glass icon
- Virtuoso-backed scrollable list (8 visible items, unlimited results)
- Each row shows emoji icon + :shortcode: label
- Keyboard navigation: arrow keys to select, Enter to confirm
- Mouse hover highlights, click selects
- Frequently used emojis still shown first when no search query
- Narrower dialog (max-w-xs) for a compact picker feel
https://claude.ai/code/session_01CzeTFzSETs9wSPH2feDE6u
* fix: add address field to EmojiTag in editor types, fix GroupMessageOptions
- Add optional `address` field to EmojiTag in editor/types.ts to match
NIP-30 changes from main (30030 emoji set address)
- Extend GroupMessageOptions with MetaTagOptions to fix type error in
GroupMessageBlueprint's setMetaTags call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(editor): restore address attr, fix serialization, UserName, no-scroll
- Restore `address` attribute in shared EmojiMention extension (emoji.ts)
that was dropped during refactor — required for NIP-30 emoji set tracking
- Extract `address` from emoji nodes in both serializeRichContent and
serializeInlineContent so it makes it into published events
- Fix MentionEditorProps.onSubmit signature: use EmojiTag[] (not the narrower
inline type) so address field flows through to callers
- Restore UserName component in ProfileSuggestionList for proper display
with Grimoire member badges and supporter flame
- Remove scrollbar when all items fit: set overflow:hidden on Virtuoso when
items.length <= MAX_VISIBLE (profile list, emoji list, emoji picker dialog)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
NIP-30 allows an optional 4th tag parameter specifying the source emoji
set address (e.g. "30030:pubkey:identifier"). This threads that address
through the full emoji pipeline so it appears in posts, replies, reactions,
and zap requests.
- Add local blueprints.ts with patched NoteBlueprint, NoteReplyBlueprint,
GroupMessageBlueprint, and ReactionBlueprint that emit the 4th param;
marked TODO to revert once applesauce-common supports it upstream
- Add address? to EmojiWithAddress, EmojiTag, EmojiSearchResult, and
EmojiTag in create-zap-request
- Store address in EmojiSearchService.addEmojiSet (30030:pubkey:identifier)
- Thread address through both editor serializers (MentionEditor, RichEditor)
and the emoji node TipTap attributes
- Fix EmojiPickerDialog to pass address when calling onEmojiSelect and
when re-indexing context emojis
- Update SendMessageOptions.emojiTags and sendReaction customEmoji param
to use EmojiTag throughout the adapter chain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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>
* 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>
* 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>
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>
* 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>
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>
* 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>
* 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>
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>
* 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>
* 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>
* 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>
* 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>
* 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>
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>
* 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>