- New noteActionSections: the full note-action inventory (follow, copy,
share, edit, broadcast, timestamp, pin, label, bookmarks, playlists,
emoji packs, mute, delete/report) lives once, with all gating, and is
rendered by BOTH the 3-dot NoteDropDownMenu (as M3 rows) and the chat
long-press sheet (as icon tiles) - the two surfaces can no longer
drift. The menu also gains the delete confirmation dialog and the
privacy-safe private-rumor delete the sheet got in the audit fixes.
- payViaIntentOrManualSplit: the shared zap-payment tail (wallet intent
vs manual split screen), replacing three verbatim copies in
ReactionsRow and one in the sheet.
- Chat bubble shapes move to commons ChatTheme.kt as the single source
of truth (18dp geometry incl. grouped variants); Desktop now inherits
the modernized corners, and the stale 15dp duplicates are gone.
- Deleted the unreferenced RenderCreateChannelNote /
RenderChangeChannelMetadataNote card renderers (~360 lines).
- ChatSystemMessage renders one Surface with a conditional clickable
instead of two duplicated branches.
- ChatEngagementDetailSheet rows share one EngagementRow scaffold;
delivery tick selection deduped into DeliveryLadderTick /
DeliveryStatusTick.
- Hardcoded chat font sizes replaced with Font12SP / named constants.
Not merged on purpose: ChipReactionGlyph vs MultiSetCompose's gallery
glyph dispatch - the gallery variant handles interactive secret emoji
and alignment modifiers, so unification would change behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
Correctness (from the branch audit):
- ChatDeliveryTracker.destroy() unregisters its persistent OK listener;
wired into AccountCacheState.removeAccount/clear so account switches
no longer leak listeners on the shared client.
- Deletion from the long-press sheet asks for confirmation again
(respecting the hide-dialog opt-out), and own NIP-17 DM messages get a
privacy-safe delete via the gift-wrapped deletion (deletePrivately)
instead of a nonsensical self-report tile.
- Jumbo-emoji bubble transparency now updates when async decryption
lands, so encrypted emoji-only DMs no longer latch an opaque bubble
around 50sp emoji.
- Group positions recompute when a neighbor's event/author loads
(watchChatGroupPosition observes the three notes' metadata flows).
- Delivery ticks: PoW-mined wraps register with the tracker (rumor id
threaded through mineWrapsInBackground); the double-check and k/n
count describe the other participants only (self-copy shown in the
detail dialog, where it belongs); relay lists resolve once per wrap
instead of twice.
- Zap cards keep the sats amount visible against long display names
(weight(fill=false) on the username).
- Bubble colors re-derive when the theme or accent changes in place
(keyed remember; this predates the branch but was re-exposed by it).
- Keycap emoji (1️⃣) count as jumbo; incomplete flag pairs and bare
keycap bases make a message non-jumbo instead of miscounting.
Performance:
- ChatDeliveryTracker holds one small StateFlow per tracked message, so
a relay OK updates a single message instead of copying a whole map and
waking every collector.
- Swipe-to-reply tracks the drag in plain float state (no coroutine per
pointer frame); one cancellable job settles the spring-back.
- JumboEmoji uses the KMP-safe codePointAtKmp/codePointCharCount helpers
(unblocks a future move to commonMain).
- Documented the deliberate per-message reaction/zap observer parity
with the main feeds in ChatReactionChips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
Rows in the engagement detail sheet now navigate to the zap receipt's
or reaction event's own thread view - where replies and reactions TO
the zap/reaction are visible - instead of the author's profile; the
avatar keeps the one-tap profile shortcut and rows fall back to the
profile while a zap receipt is still unknown. The zap pill cards in
live chats become tappable to the same thread view.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
- Swipe-to-reply: drag a bubble toward the screen center (right for
received, left for own); a reply icon fades/scales in behind it and a
haptic tick marks the commit threshold - releasing past it quote-
replies. Disabled for drafts and inner quotes.
- Send/arrival motion: chat feed items use animateItem so a new message
fades in and neighbors slide to make room instead of the list
snapping. Skipped in performance mode.
- Press feedback: bubbles scale down slightly while pressed, with
haptics on long-press (action sheet) and double-tap (like).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
Four features building on the redesign's foundations:
- Who-reacted/who-zapped sheet: long-press a reaction chip (or tap the
sats chip) to open a sheet listing every zapper with amount + comment
and every reactor with the emoji they sent; rows open the profile.
- Delivery detail: tapping the delivery tick opens a dialog showing
per-recipient acceptance for DMs or per-relay acceptance for rooms,
with a re-broadcast action for stuck messages.
- Per-user name colors: group-chat author names get a stable
pubkey-derived hue, tuned separately for light and dark themes.
- Jumbo emoji: messages of 1-3 emoji render as large bare emoji with a
transparent bubble (scroll-to-highlight still tints them); ZWJ
sequences, skin tones, and flags count as single emoji.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
The 'More options' handoff is gone: every action from NoteDropDownMenu
now lives directly in the sheet as icon tiles grouped by similarity into
rows - message (reply, edit, copy text/id/json, share, broadcast),
author (follow, follow sets, copy pubkey), organize (timestamp, pin,
label, bookmarks), and moderation (mute thread, delete/report). The
sheet scrolls; share and on-chain zap swap the sheet for their own
surface, report/label/edit dialogs stack over it, matching the menu's
behavior. Private-rumor and ownership gating mirror the menu exactly.
Playlist/emoji-pack curation specials are omitted - those kinds never
render in chat feeds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
Instead of a single zap icon that opens the amount popup, the sheet now
embeds the rail-aware amount chip grid itself (same chips as the popup:
lightning/cashu/on-chain per amount, reload-mint, preset editor).
Lightning and cashu zaps fire immediately and close the sheet; on-chain
amounts swap the sheet for the OnchainZapSendDialog it hosts.
Extracted from ReactionsRow for reuse: ZapAmountChoiceGrid (the chip
grid without the popup card) and observeZapRailCapability (the async
rail availability computation).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
StreamSystemCard's default corner shape moves from 8dp to 18dp so the
full-width raid and clip cards share the rounding of the redesigned
bubbles, zap pills, and system messages. The zap renderer drops its now
redundant shape override.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
Zap messages in live streams (and other chat feeds) now render as a
content-hugging, centered pill with the 18dp rounding of the redesigned
bubbles and system messages, instead of an edge-to-edge 8dp card. The
bitcoin-orange accent, big-zap emphasis, and inline comment rendering
are unchanged.
StreamSystemCard gains opt-in fillWidth/shape parameters (defaults
preserve the raid and clip cards, which carry media previews and want
the full width).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
NIP-28 channel create (kind 40) and metadata update (kind 41) events
narrate the room rather than talk in it. They now render as a centered,
muted system pill - 'X created the channel Y' / 'X updated the channel
profile' - that taps through to the channel, instead of a full channel
profile card inside a regular user bubble (chat design best practice:
system messages should be visually distinct from user messages).
The old card renderers (RenderCreateChannelNote / RenderChannelData)
stay in the tree but are no longer referenced by the chat feed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
Ticks are sourced from relay OK acceptances, rendered next to the
timestamp on the last message of each own-author run:
- clock: published but no relay accepted yet
- single check: accepted by at least one relay
- double check (green): DMs - every participant's gift wrap accepted by
that participant's relays; rooms - all target room relays accepted
- group DMs additionally show a delivered k/n participant count
New ChatDeliveryTracker captures the recipient -> wrap -> target-relays
mapping inside Account.broadcastPrivately (the only place it exists,
before wraps are aliased onto a single note) and room targets in
signAndSendPrivatelyOrBroadcast, then attributes relay OKs back per
recipient via a persistent listener. Messages from before a restart fall
back to the note's seen-on relays.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
A small time label now sits in the bubble's bottom-end corner of the last
message of a group (and singles), so temporal context is always visible
without repeating it on every bubble. Hidden while the tap-to-expand
detail row is open, which already includes the time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
- Grouped bubbles: middle messages now use sharp corners on every edge so
full rounding only appears at the top and bottom of an author run.
- Reaction chips overlap the bubble: the chip row's vertical center rides
the bubble's bottom border instead of floating detached below it.
- Add the full zap flow (amount popup, custom amounts, progress) to the
long-press bottom sheet's quick-reaction row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
The PoW publishing-queue branch split broadcastPrivately into a
NIP17Factory.Result overload delegating to a List<GiftWrapEvent> one,
while the DM-unread branch appended markDmRoomAsRead(signedEvents.msg)
to the old single body. The merge left that call inside the wraps
overload, where signedEvents does not exist, breaking compilation.
Move the read-marker call to the Result overload, and mark the room as
read on the PoW early-return path too (the rumor is signed inline there
and the wraps only publish after mining), so PoW-enabled accounts keep
the unread-clearing behavior from #1286/#1287.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1hu9zpyiuJo6yyoJs1vdw
- Group consecutive messages by the same author (within 10 min, same day,
no subject divider) with connected bubble shapes, tighter spacing, and
the author line only on the first message of a run.
- Render received reactions and zaps as messenger-style chips under the
bubble: one chip per emoji with count (highlighted when the logged-in
user reacted; tap toggles the reaction) plus a sats total chip.
- Replace the old long-press quick-action popup in chats with a modern
bottom sheet: quick-reaction row, reply/copy rows, and a handoff to the
full note options menu (the 3-dot menu) which previously had no home
in chats.
- Add double-tap-to-like on chat bubbles.
- Stronger bubble colors: accent-following chatBubbleMe and a
higher-contrast chatBubbleThem so mine/theirs/background separate
clearly in light and dark themes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0129yvP2hmVeDFfuKKy94tqX
The mining banner's indeterminate bar becomes determinate: each mining job
gets its own bar that fills over the device's estimated duration for its
difficulty (elapsed / (2^bits / benchmarked hash rate)), with an
"≈ 10 minutes left" label next to the elapsed time. The nonce search is
memoryless, so past the mean the bar honestly falls back to the
indeterminate sweep with "any moment now" instead of parking at 100%.
Queued-only banners keep the shared activity sweep.
The mining notification mirrors this: single-job progress fills toward the
estimate with the time-left text, refreshed every 30 s while mining (the
queue only emits on state changes, not clock ticks).
The composer difficulty menu now prices each option on this device —
"24 bits · ≈ 45 seconds" — using the same cached benchmark, and the duration
formatting is shared (formatApproxDuration/formatTimeLeft) with the settings
estimate instead of being private to the settings screen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
The per-post PoW button on the New Post screen now sits before the
Subject/Title toggle instead of last in the options row.
Every PoW surface (composer button, note chip, mining banner, settings tile,
relay-info minimum-PoW row) switches from the Bolt icon to Manufacturing
(gear, U+E726): Bolt is the app's zap/lightning glyph everywhere else, so a
Bolt PoW badge next to the zap-split/zapraiser/invoice buttons read as a
payment feature. Subset font regenerated for the new codepoint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Shows a button in the relay information header that opens the relay's
NIP-29 group list (RelayGroupServer route) when the relay's NIP-11
document advertises support for NIP-29. Also converts the header button
row to a FlowRow and marks button labels single-line so labels no
longer break mid-word (e.g. "Member/s") when the row runs out of
width.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012w9KridAbRpesbX2ytdjn4
GiftWrapEvent.create no longer takes powDifficulty/powIsActive — it takes a
single templateConversion hook ((template, ephemeralPubKey) -> template,
default identity) that runs on the finished wrap template right before the
ephemeral key signs it. The hook receives the ephemeral pubkey because the
NIP-01 id a nonce commits to includes it and the key never leaves create().
NIP17Factory forwards the same hook through wrapSeal/createWraps and the
create*NIP17 entry points, so quartz's NIP-59/NIP-17 code no longer imports
the NIP-13 miner at all; Account builds the mining closure at the call site.
Any future pre-sign wrap adjustment flows through the same seam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Durability & correctness:
- Keep the job entry + disk checkpoint alive until the publish continuation
completes (was: dropped at mining-complete); failed publishes keep their
checkpoint for restart retry and surface through a failures SharedFlow
- Move draft deletion into the publish continuations in every composer so a
cancelled or process-killed mining job can't destroy the only copy of a post
- Persist gift-wrap mining: split NIP17Factory into createSeals (signer
interaction, runs inline) + wrapSeal (pure CPU, runs on the queue), new
REPLAY_WRAPS records restore pending DM wraps after process death
- Clamp synced NIP-78 difficulty (PoWPolicy.MAX_DIFFICULTY), require a sane
target in PoWMiner, bound PoWRankEvaluator against short ids
- Reaction double-tap while mining now toggles (dedupeKey + cancelByKey)
instead of publishing duplicates
- Restore checkpoints for every loaded account, not just the active one
- logOff purges the account's checkpoints and cancels its queued jobs
- Private notes: composer chip now gates on the gift-wrap kind and the
per-post override reaches sendPrivateNote
Coverage:
- Public/live chat (kinds 42 + 1311) and voice replies (1244 + kind-1 audio
replies) now route through the mining gate
Perf:
- FGS start() dedupes with a running flag; PendingIntents built once
- Banner 1 Hz clock only ticks while a job shows elapsed time
- PoWEstimator benchmark is single-flight behind a Mutex
UX / l10n:
- Post-mining failures toast with retry information
- Count strings converted to <plurals>; elapsed time via DateUtils; settings
estimate uses localized units; shared powKindLabelRes replaces three
duplicated kind→label maps; dead pow_chip_* strings removed
- CLI: pow mine lowercases the pubkey before mining (uppercase hex mined an
id that never matches the signed event) and validates via quartz Hex
New queue tests: checkpoint lifetime, failure reporting, cancelByKey toggle,
per-owner cancellation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
Addresses the issues found reviewing the #1286/#1287 fix:
- Centralize the room read-marker route in privateChatLastReadRoute()
and the authorship rule in chatMessageMarksRoomAsRead(); all writers
and readers now share one implementation.
- Exempt notes-to-self rooms from the authorship rule — there the
user's own messages are the content still to be seen.
- Mark own chat messages as read at the ingestion choke point
(EventProcessor), covering every send path (including the play-flavor
App Functions assistant) and own messages arriving from other devices
at the persisted-marker level, not just visually.
- Advance the send-path marker to the newest known room message, so a
quick-reply to a skew-ahead peer also clears the indicators.
- Revert the authorship guard on public-chat/ephemeral/marmot rows:
in multi-party channels 'I posted last' does not imply the earlier
backlog was seen, and no marker advances there to back the guard.
- Restore the new-items bubble for rooms whose newest item is an
unsent draft — a draft still needs the user's attention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqN8cfqNdo1MAvLN4C9krm
Non-chat audit follow-up: RepostEvent, GenericRepostEvent, and
CommunityPostApprovalEvent consumes never called note.addRelay, so those
notes carried an empty relay list forever.
- CommunityPostApprovalEvent is badge-rendered directly in community feeds
(BadgeBox has no repost-style indirection for it), so its relay icon row
was always empty.
- Repost badges borrow the boosted note's relays in BadgeBox, but the
repost note's own list feeds relayHintUrl()/relayUrls() for nevent hints
when citing or sharing the repost — those fell back to author outbox
guesses.
Add the standard attribution block (author.addRelayBeingUsed +
note.addRelay) before the duplicate check, matching consumeRegularEvent,
so both first arrivals and re-deliveries from other relays are recorded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEiq1NMK3q12KGQ2yYhPEp
Audit follow-up to the gift-wrap relay-icon fix: Marmot group chat rows
render the decrypted inner note (kind 9/7) via the standard chat feed with
RelayBadgesHorizontal, but nothing ever populated that note's relay list —
OK acceptances and relay deliveries all landed on the kind-445 envelope,
which has no link to its inner event.
- GroupEvent now implements HasInnerEvent (same @Transient @Volatile
innerEventId pattern as GiftWrapEvent/SealedRumorEvent), so
LocalCache.addRelayToNoteAndInners drills 445 -> inner for both OK
confirmations and duplicate EVENT deliveries. RouteMaker is unaffected:
it gates on the concrete wrap types before casting to HasInnerEvent.
- GroupEventHandler sets innerEventId at decrypt time and copies the
envelope's accumulated relays down to the inner note (looked up by
event.id, not the eventNote/publicNote params, which belong to the
triggering event when replayed from retryPendingFor).
- sendMarmotGroupMessage sets innerEventId on the freshly built envelope
before consuming/publishing, so acceptances for sent group messages
reach the rendered note as soon as the inner note is indexed.
Also audited the remaining chat-rendered types: NIP-04 PrivateDmEvent,
ChatMessageEvent rumors, NIP-C7 ChatEvent, ChannelMessage/Ephemeral/
LiveActivities messages all route through consumeRegularEvent (duplicates
covered by the shared helper), and NIP-37 drafts render the wrap note
itself with markAsSeen covering the version/addressable pair.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEiq1NMK3q12KGQ2yYhPEp
Retry rounds were the dominant tail cost of deep crawls (rounds 8-10 of a
hop-8 run burned ~80 min for +6.6k lists): every 256-user batch with
attempt>0 or no-outbox users fanned the SAME ~40 backbone+fallback relays,
queueing thousands of small REQs against their 16-permit gates — pure
hot-relay head-of-line blocking.
Restructured:
- routeByOutbox now routes ONLY own-signal relays (write relays; hints when
no outbox) and flags no-outbox/retry users into a communal set.
- New Phase C communalSweep asks the shared relays ONCE per round for all
flagged users in author-chunked units — same (relay, user) coverage,
~8x fewer REQs — and records answered-empty pairs so each round shrinks.
- Attempt accounting moves to Phase C, after the round's full coverage
(own relays + shared set) has run.
- shardedSweep now also records answered-empty (user, relay) pairs, so the
communal sweep never re-asks a pair the shard pass already proved empty.
- Fixpoint recovery dedup: recoverStragglersFromAggregators skips users a
prior pass already asked the aggregators for (each repeat pass cost ~4 min
of store scans + REQs at 470k stragglers for zero new answers).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013zEYRGKF943RgLaHTViJaB
- `amy notes post TEXT --pow BITS [--pow-timeout SECS]` mines the note
pre-signature via quartz's PoWMiner (blocking — the CLI process is the
job), exits 124 on timeout with nothing published, and adds additive
--json keys: pow, pow_target, pow_millis.
- `amy pow check EVENT-JSON|-` reports actual_bits, committed_target,
has_commitment and effective_pow (capped at the commitment per
NIP-13's anti-lucky-spam rule) plus id+sig validity.
- `amy pow mine --target N [--pubkey HEX] [--timeout SECS] TEMPLATE|-`
mines an unsigned template for any pubkey — NIP-13's delegated PoW:
ids don't commit to signatures, so a headless box can mine for a
phone and hand the template back for signing.
- `amy pow bench` prints the machine's hash rate and expected seconds
at 16/20/24/28 bits (commons PoWEstimator).
- cli/tests/pow/pow-headless.sh: relay-free harness covering bench,
mine (commitment shape + 124 timeout), and the delegated round trip
(mine → sign via `amy event` → pow check ≥ target). 6/6 passing.
No logic added to cli/ — thin assembly over quartz nip13Pow + commons
PoWEstimator per the CLI architecture rules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADb3dez9jPk6QqyQ1rTx4V
The give-up toast fired per-relay, but Nostr publishes each event to several
relays (NIP-65 outbox), so one relay rejecting an event that reached the others
produced a misleading "couldn't deliver" popup. It also named no event and fired
mid-scroll on reconnect-driven re-pumps, so it read as random noise. Remove the
toast and its subscription/strings.
The low-level quartz onEventGaveUp signal stays (tested, no UI consumer) as a
primitive for a future per-message send-status indicator, which is the right
surface for delivery failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Two bugs found in a pre-merge audit:
- quartz PoolEventOutboxState: auth-required NAKs only spared the `responses`
budget, but `tries` (grown by every send/re-pump and NOT auth-aware) still
accumulated across reconnects, so a slow/flapping AUTH handshake could exhaust
Tries.isDone() and drop the event — with a spurious onEventGaveUp — before AUTH
landed. Now an auth-required NAK resets the relay's retry budget (it responded,
so it's up and just wants auth). Regression test added.
- RelayAuthPromptBus used a replay=0 SharedFlow, so a challenge that resolved to
ASK before RelayAuthPromptHost subscribed (cold start / account switch) was
dropped and the auth coroutine stalled the full timeout then DISMISSed. Add
replay so late subscribers recover pending prompts (the host already filters
resolved ones). Regression test added.
Also record the as-built design (Always/Never/Custom + toggles, venues, give-up
toast, known deny-relay-outbox limitation) in the plan doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Adopt the same componentized UI as the Security Filters screen for blocks 2 and
3 (block 1, the Always/Never/Custom mode selector, stays as the policy cards):
- The Custom toggles are now a SettingsSection card of SettingsSwitchTiles
(leading colored icon box, title + description, inset dividers) — reusing the
shared settings components instead of bespoke rows.
- The per-relay list is now a grouped rounded card (surfaceContainerLow) with
the same header style. Kept lazy: each row is its own LazyColumn item that
clips the card's top/bottom corners on the first/last row so contiguous rows
read as one card, preserving the earlier perf fix.
- Section headers unified to the SettingsSection primary-colored style.
Toggle icons: Dns (my relays), Download (read follows), Mail (message follows),
Public (message strangers).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
The per-relay list was a non-lazy Column + forEach, so every relay row composed
on first frame — each building a NIP-11 relay icon (robohash generation is
CPU-heavy) plus up to three avatars. With many authenticated relays that is a
lot of synchronous main-thread work, making the screen slow to open. Move the
whole screen to a LazyColumn so only the visible rows (and their icon/avatar
loads) compose.
Also render the per-relay divider only between rows (keyed itemsIndexed, skip
index 0) so there's no full-width divider trailing after the last row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Drop the ad-hoc cards/thin-divider layout and adopt the same structure as the
other settings screens: small primary-colored SectionHeaders, 4dp dividers
between sections, and plain padded rows instead of surfaceVariant blocks.
- The Custom toggles are now standard settings switch rows (24dp inset, 16sp
title / 13sp description) under a "What to log in to" header, not a card.
- The per-relay list is now icon-led rows separated by dividers (like the app's
other relay lists) instead of chunky cards; tapping a row still opens the
relay's NIP-11 info.
- Section labels reworded: "When to authenticate" and "What to log in to".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Restructure the global auth control into a top-level mode — Always authenticate,
Never authenticate, or Custom — where Custom reveals independent per-situation
toggles instead of the confusing single sub-toggle:
- My relays and venues (own relays + joined/subscribed/favorited venues) — on
- Read posts from people I follow — on
- Message people I follow (DMs, replies, notifications) — on
- Message anyone / strangers — off by default (you're asked each time instead)
RelayAuthPolicy is now {ALWAYS, NEVER, CUSTOM}. The resolver takes a
RelayAuthCustomToggles plus split serves-facts (followed-read, followed-write,
stranger-write, own-relay, venue) and, under CUSTOM, allows if any enabled
category matches — else falls through to a prompt. There is deliberately no
"read strangers' posts" category, so that always prompts.
Account settings replace the single delivery flag with four persisted booleans
(default policy CUSTOM; no migration, unreleased). The contextual DM/notification
prompt button now switches to CUSTOM and enables both message toggles. Resolver
tests rewritten per-toggle, including that reading a stranger is never
auto-allowed. Old IF_IN_MY_LIST / TRUSTED_FOLLOWS policies and their strings are
removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Restructure the two TRUSTED_FOLLOWS controls so they read as independent ideas
instead of a confusing read/write sub-toggle:
- The "My relays and people I follow" policy now trusts a followed user as any
counterparty — reading their posts AND reaching them (DM/notification) — plus
your own relays and joined venues. Reading your follows is no longer gated.
- The sub-toggle is repurposed to "Also log in to deliver my messages": trust a
relay to send DMs, replies or notifications to anyone you're talking to, even
people you don't follow.
Resolver: replace servesFollowed{Write,Read}Counterparty with a single
servesFollowedCounterparty, add servesWriteCounterparty (an inbox of anyone
you're messaging), and gate the latter behind the new
messageDeliveryTrustEnabled input. Rename the account setting
relayAuthTrustFollowsForReads -> relayAuthTrustMessageDelivery (+ pref key;
no migration needed, unreleased). The contextual prompt button moves from
read-post prompts to DM/notification prompts and now enables message delivery.
Resolver tests updated for the new inputs, including that the delivery toggle
is write-only and never auto-allows reading a stranger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Restore the "Per-relay overrides" heading and rebuild each relay card using the
same conventions as the other relay screens: the relay's NIP-11 icon (robohash
fallback), the shortened displayUrl instead of the raw URL, last-used as a
subtitle under the name, and the whole card tappable to open the relay's
NIP-11 info screen (Route.RelayInfo). The Allow/Deny chip and Forget stay on
the trailing edge; the served-people facepile sits on its own row below.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a
Fold the separate "Why you're logged into these relays" section into the
per-relay list so each relay is one card: URL, an Allow/Deny chip, a facepile
of the people it serves (3 avatars + "+N"), and when it was last used.
Collapse the two removal actions (the override "X" and the rationale "Forget")
into a single Forget that clears both the stored decision and the recorded
reason, so the relay drops off the list — this removes the earlier ambiguity
about what each button did. The Allow/Deny chip now also shows for relays
allowed by policy (no explicit override), so they can be blocked from here too.
Also clarify the confusing read-trust sub-toggle copy so it reads as the
read-side extension of the write-only follows policy ("On its own, the option
above only logs in to send messages or notifications… turn this on to also log
in when downloading their posts"). Drop the three now-unused strings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZjmYpgHP4pf79Sav5QT8a