Use commons showAmount() for the Zaps tab total and each zapper row instead of
raw sats, matching Amethyst Android and other Nostr clients.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The filter (your own posts that mention this profile) matches Android's
UserProfileMutualFeedFilter, but Android's user-facing string is 'Yours'
(<string name="mutual">Yours</string>). The internal filter name stays
DesktopMutualFeedFilter.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1. Header actions were direct children of a SpaceBetween Row, so the overflow
(⋮) menu floated to the center. Group Edit/Follow/⋮ in their own Row and put
the overflow last (after the primary Follow action), per convention. Extract
the Follow button into FollowButtonRegion for the reorder.
2. Followers/Following rows now subscribe to kind-0 metadata (batched, capped)
and observe each user's metadata flow so names + avatars render instead of
raw npubs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Wire real PrivateZapCache(signer) into DesktopIAccount (was a null stub)
- Subscribe to kind 9735 receipts p-tagging the profile; aggregate sats per
zapper (private zaps decrypt only on your own profile, else fall back to the
public zap-request pubkey)
- New ProfileZapRow; tab header shows the total sats
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Message opens the Messages column + 1:1 room via a DesktopDmRoute bridge
(observed by both deck and single-pane layouts)
- Add to list: pack picker from LocalFollowPacksState, appends via
FollowListEvent.add + broadcast
- Share copies a nostr: profile link to the clipboard
- Actions live in the existing writeable/other-profile overflow menu, so
self/read-only are already excluded
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Scrollable tab row; Followers/Following render UserSearchCard from live
contact-list subscriptions; Relays render a new RelayRowCard from kind 10002;
Bookmarks (kind 10003 → DesktopBookmarkFeedFilter) and Mutual (new
DesktopMutualFeedFilter) render FeedNoteCard
- Respects the shared mute/block hidden-set via DesktopFeedViewModel
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Render kind-0 banner image in the profile card header
- Render bio as rich text (mentions/hashtags/links) via DesktopRichText
- Add CLINK offer (noffer) field to Edit Profile + shared EditProfileFields
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address composer feedback:
- The annotation field is now the short-note composer's rich MessageField
(@-mention + custom-emoji autocomplete, inline previews). On publish it becomes
the highlight's `comment` tag, with the mentions (`p`), emoji, URLs (`r`),
hashtags and quotes it references emitted as their own tags — via
NewMessageTagger + a new tag-builder initializer on HighlightEvent.build().
- A nostr source now renders as a reply-style preview card (NoteCompose, quoted
style) instead of showing a URL field; the URL field appears only for web
sources. The source note is resolved from the a/e coordinate carried in the
route.
Verified with :quartz:jvmTest and :amethyst:compileFdroidDebugKotlin.
- Add a "Highlight" action to the note action menu (3-dot menu + chat long-press
sheet). It opens the NIP-84 composer pre-tagged with the note as source — an
`a` reference for an addressable article, else an `e` reference, plus the
author `p` — with the passage left for the user to type/paste. Prose kinds
only (text notes, long-form), and never a private rumor (a public highlight
would leak an e-tag of the unsigned rumor). This is the in-app entry point for
"post with the source as an event".
- Remove the live preview from the New Highlight screen per review.
- Add two real-world regression tests for the shared-highlight parser: Chrome
"copy link to highlight" with a prefix/suffix fragment (incl. an encoded
hyphen inside the prefix) and a long start-only fragment with encoded commas.
Verified with :quartz:jvmTest and :amethyst:compileFdroidDebugKotlin.
Redesign the New Highlight composer as a pull-quote you craft: a rounded tonal
hero card with a quotation-mark watermark, a highlighter-amber accent bar, and
the passage in large type, plus a live highlighter-pen preview (reusing the feed's
HighlightedQuote) and icon-led source/note fields.
Also extend HighlightEvent.create()/build() to emit a/e/p tags, so the builder
now covers every NIP-84 source — a web page (r), a nostr article (a), a nostr
note (e), each with optional author attribution (p) and context — not just web
highlights. Route.NewHighlight and the composer ViewModel carry the nostr source
(address/event/author) and context through so a future in-app "highlight this
passage" action can open the composer for a nostr article/note.
Covered by a new builder test for the a/e/p tags; verified with :quartz:jvmTest
and :amethyst:compileFdroidDebugKotlin.
`trimUrlEnd` strips a wrapping `)` off the URL token, but the `(` it opened
stayed at the tail of the passage, so sharing
`See this quote (https://example.com/article)` published a highlight whose
content read `See this quote (`.
Trim an unbalanced bracket left at the end of the passage after the URL token
is removed. Only an unbalanced one at the very end goes, so `He said (see
below) https://…` keeps its matched pair, and `.../Mercury_(planet)` — where
nothing was trimmed off the URL — is untouched on both sides.
The existing paren tests only asserted `result.url`, which is why this slipped
through; the new cases assert `result.quote` too, including a regression guard
for the slug-paren direction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a browsable Highlights feed (NIP-84 kind:9802) modeled file-for-file on
the Git Repositories feed pattern, since highlights are announcement-like
regular events with the same follow-list top-nav filtering.
New feed stack under ui/screen/loggedIn/highlights:
- dal/HighlightsFeedFilter — AdditiveFeedFilter scanning LocalCache.notes for
kind 9802 (regular events, so notes rather than addressables), gated by the
selected top-nav follow list via FilterByListParams.
- datasource/ assembler + compose subscription + PerUserAndFollowList sub-
assembler + makeHighlightsFilter dispatcher, with per-relay subassemblies for
follows / authors / muted / global / hashtag / geohash. Communities aren't a
highlight concept, so those sets fall through and aren't offered.
- HighlightsScreen (DisappearingScaffold + shared RenderFeedContentState; the
existing RenderHighlight card handles rendering) with a HighlightsTopBar
FeedFilterSpinner and a NewHighlightButton FAB into Route.NewHighlight.
Wiring into the shared plumbing mirrors gitRepositories exactly: Route.Highlights
+ AppNavigation, NavBarItem enum/catalog/drawer lists (FormatQuote icon, already
in the subset font), AccountFeedContentStates (feed state + update/delete/trim),
RelaySubscriptionsCoordinator, TopNavFilterState.highlightsRoutes,
AccountSettings.defaultHighlightsFollowList + changer, Account live follow-list
flows, LocalPreferences persistence, ScrollStateKeys, and the bottom-bar
preloader. Verified with :amethyst:compileFdroidDebugKotlin.
The URL trailing-punctuation trim now leaves a closing bracket in place when
it is balanced within the token, so a shared Wikipedia link like
`.../wiki/Mercury_(planet)` keeps its `)` while a wrapping `(https://…)` still
loses the paren the surrounding prose added.
Adds a "New Highlight" share target and composer so a user can select text in
a browser, hit Share → Amethyst, and publish it as a kind:9802 highlight.
- Manifest: a text/plain-only ShareAsHighlightAlias activity-alias, matched at
runtime by ShareIntentRouting.isShareAsHighlight (mirrors the Send-as-DM
alias pattern).
- AppNavigation: both the launch-intent and warm onNewIntent parse blocks now
branch on the highlight alias, run the shared text through
SharedHighlightParser, and open Route.NewHighlight pre-split into
quote/url/prefix/suffix.
- NewHighlightScreen + NewHighlightPostViewModel: a trimmed composer (passage,
source URL, optional note — no polls/zaps/media/scheduling) that publishes via
account.signAndComputeBroadcast(HighlightEvent.build(...)).
Also adds HighlightEvent.build(), the unsigned EventTemplate counterpart of
create(), for the sign-and-broadcast pipeline. Verified with :quartz:jvmTest
and :amethyst:compileFdroidDebugKotlin.
Adds a shared-highlight parsing layer under nip84Highlights/parse that turns
the free-form text a browser hands Amethyst on "Share selection" into the
pieces of a kind:9802 highlight:
- SharedHighlightParser normalises selection-only, selection+URL, URL-only and
"link to highlight" (#:~:text= fragment) shares into a SharedHighlight.
- TextFragmentParser decodes/strips WICG text-fragment directives (prefix,
start, end, suffix), leaving literal '+' verbatim.
- UrlTrackerCleaner strips utm_*/fbclid/etc. from the source URL per NIP-84's
"clean the URL from trackers" guidance, preserving path and fragment.
Also adds a HighlightEvent.create() builder overload that assembles the r,
textquoteselector, context and comment tags from parsed data, centralising
what the desktop publish action does by hand.
Covered by commonTest suites for each piece plus a builder round-trip.
A kind:9802 highlight that mentions users before its author rendered the
wrong name and a stray "published by …" caption.
- HighlightEvent.author() took the first p tag regardless of role, so a
highlight with leading "mention" p tags was attributed to a mention
instead of the "author"-marked one. Prefer the NIP-84 author marker,
falling back to the first p tag for highlights (including Amethyst's own)
that omit the marker.
- DisplayEntryForNote used the source note's title/subject/alt as a caption.
For a kind-1 note there is no title/subject, so it fell to the NIP-31 alt
tag — which clients like Jumble fill with a generic "This event was
published by https://jumble.imwald.eu." line. Drop alt from the lookup and
name the source by its event kind instead, kept clickable to the note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJh3QYonEa9hw6mJcnrmve
The NIP-84 highlight comment was passed to TranslatableRichTextViewer with
an empty tag list, so custom emoji (:shortcode:) never resolved against the
event's `emoji` tags and rendered as raw text.
Thread the highlight event's tags through to the comment's rich-text viewer
so the emoji map is built from them, matching how regular text notes render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FyyodHWxSHeiY6pndPxGSb
blossomHashOrNull runs on every media URL the feed loads. Quartz's unrolled
Hex.isHex64 is the fast path for validating a 32-byte hex id (~30% faster
than isHex, far faster than a regex match). It only checks the first 64
chars and doesn't verify length, so keep an explicit length == 64 guard to
reject longer segments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao9w26c2gAm4gjJdhgvLyp
The retry-on-401 interceptor re-probed anonymously on every blob, so each
image from an auth-gated host (e.g. a Buzz community feed, where nearly all
media is on one host) paid a wasted 401 round trip before the signed retry.
Remember hosts that answered 401 and attach the cached token up front on
their subsequent blobs — one round trip instead of two in steady state. The
first blob per host still costs the probe; the learned set falls back to an
anonymous request when no signer is available so a logged-out user never
loops.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao9w26c2gAm4gjJdhgvLyp
Merges nostr proposal ed443f87 into main:
- Cache the Keyring behind double-checked locking so the OS backend is
opened once per SecureKeyStorage rather than per save/get/delete. Cold
start touches storage at least twice (metadata AES key, then the active
account nsec), which surfaced as two keychain unlock prompts.
- Add a KeyringHandle seam so the test suite can substitute an in-memory
backend instead of touching the real OS keychain, plus tests pinning the
single-open invariant, including a 16-thread concurrent first-touch race.
Sharing one handle across Dispatchers.IO threads is safe on all three
backends: the macOS path resolves to pt.davidafsilva.apple.OSXKeychain,
itself a static singleton with no mutable instance state, so every
Keyring.create() already shared it; the Freedesktop and Windows backends
hold a single final collaborator assigned in their constructor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merges nostr proposal 22a8247d into main:
- Wrap MainContent in key(account.pubKeyHex) so the account-scoped subtree
is torn down on an account switch. Deck layout and workspace state are
remembered outside this key and survive.
Fixes a stale-identity leak, not just staleness: NotificationsScreen holds
its accumulated items in an unkeyed `remember { EventCollectionState(...) }`,
so account A's notifications stayed on screen under account B even though
the subscription below re-keyed correctly. ReadsScreen has the same unkeyed
collection, and DeckColumnContainer's ColumnNavigationState is keyed on
column.id only, so each column's screen stack also survived the switch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A private/closed NIP-29 group serves its content (kind-9 chat, kind-11
threads, …) only over a NIP-42-authenticated connection. Amethyst's
group-content reads are all `#h`-scoped with all authors, so they never
name the user's pubkey, and a group host relay (e.g. wss://chat.wisp.talk)
is not in any of the NIP-65/DM/search/… lists that feed
`account.trustedRelays`. The per-account first-party AUTH gate therefore
returned false and Amethyst never AUTHed, so the relay refused the content
with `auth-required` — the group's public 39000 metadata still loaded, so
the group appeared but showed no messages.
Treat a NIP-29 relay group the user explicitly joined (their kind-10009
list) as a first-party reason to authenticate with its host relay,
mirroring the existing `BuzzWorkspaces.isJoined` carve-out. Reads of a
group the user has not joined (e.g. browsing a relay's public directory)
still do not AUTH.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018puT2YnevbLHYG2PdCLAeh
Buzz's private media relay (*.communities.buzz.xyz) gates blob downloads
behind BUD-01 read auth, returning `401 {"error":"authentication failed"}`
to anonymous GETs. Amethyst loaded every media URL anonymously through
Coil/OkHttp, so those images (and their thumbnails) never decoded.
Quartz already had BlossomAuthorizationEvent.createGetAuth but nothing in
the app ever called it. Wire it into the media HTTP client:
- BlossomReadAuthInterceptor: on a 401 for a GET whose URL last segment is
a Blossom sha256 filename (covers `<hash>.png` and `<hash>.thumb.jpg`),
retry once with a signed `Authorization: Nostr <event>` header. Narrowly
gated so unrelated 401s never trigger a second request or any signing.
- BlossomReadAuthTokenProvider: bridges the suspend signer synchronously
(runBlocking + timeout so a slow remote/external signer can't pin the
OkHttp thread) and caches one server-scoped token per host, which also
covers derived blobs like thumbnails.
- BlossomAuth.createGetAuth exposes the read-auth builder to the app layer.
Public Blossom/NIP-96 hosts stay zero-overhead: they answer 200, so no
token is ever signed for them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ao9w26c2gAm4gjJdhgvLyp
Add a "Content in the feed" section to the Home settings screen with a
switch per event-kind group (text notes, reposts, comments, articles,
polls, voice, live activities, chess, ...). Disabling a group both drops
its kinds from the always-on home relay filters (the assembler) and hides
them from the New Threads / Conversations / Everything tabs (the DAL).
- HomeFeedType: single source of truth mapping each toggleable group to
its Nostr kinds, with stable codes + encode/decode for persistence
(mirrors the existing ChatFeedType pattern for Messages).
- AccountSettings.enabledHomeFeedTypes (+ setHomeFeedTypeEnabled),
persisted per-account as the set of disabled codes so new groups
default on.
- Assembler: HomeOutboxEventsEoseManager strips disabled kinds from every
home relay filter at the single choke point, and re-arms on toggle.
- DAL: HomeNewThreadFeedFilter / HomeConversationsFeedFilter reject
disabled kinds; AccountFeedContentStates rebuilds the home feeds when a
toggle flips.
Also extracts the inbox / relay-auth / feed-type preference reads out of
LocalPreferences' account-load lambda into a helper: that lambda was
already at the JVM per-method bytecode limit and the new field tipped it
over ("Method too large").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L576BhgU3c638PkGHL8YUS
Add a CONTINENT(1) level to GeohashChannelLevel so the coarsest selectable
geohash channel is a single character (~5000 km, one of 32 cells for the
globe), previously floored at REGION(2). Every precision picker iterates
GeohashChannelLevel.ordered, so the map picker, Teleport, "Near me" list and
New-geohash-chat all pick it up automatically; GeohashChatsScreen now labels a
1-char cell "Continent" instead of showing no level.
Adds the "Continent" label and "~5000 km" chip subtitle, and extends the
GeohashChannelLevel test coverage. REGION..BUILDING (2-8 chars) still mirror
the Bitchat channel levels; CONTINENT is an Amethyst extension beyond them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vtvo2iNSnG3M21QwbFPznU
Both nostr proposals merged just now (82e72369, bdb4b03e) referenced types
by fully-qualified name inside function bodies, which CLAUDE.md's Kotlin
style rule forbids. Merged them as authored rather than rewriting someone
else's patch mid-merge; this is the follow-up.
- NamecoinNameResolver: import kotlinx.coroutines.CancellationException.
Both catch sites were inline-qualified (one added by the proposal, one
already there), and the sibling resolvers in this same package
(Nip05Client, UserHexResolver) already import it.
- desktop Main.kt: import the five notification symbols in the block the
proposal touched — the two Preferences* factories and the three
Local*Notification* CompositionLocals. Each occurs exactly once, so no
fully-qualified stragglers are left behind for those names.
Deliberately scoped to that block: Main.kt has ~130 other inline
fully-qualified names, and sweeping them belongs in its own change, not
tacked onto a style follow-up.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merges nostr proposal bdb4b03e into main:
- Catch NamecoinLookupException in performLookup and return null, restoring
resolve()'s documented "null on any failure" contract. nameShowWithFallback
throws NameNotFound / NameExpired / ServersUnreachable, which escaped
resolve() and reached Nip05State.checkAndUpdate as a hard error.
- CancellationException is rethrown first so coroutine cancellation is not
swallowed.
- resolveDetailed() is unaffected: it goes through performLookupDetailed(),
which still distinguishes each failure reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merges nostr proposal 82e72369 into main:
- Provide LocalNotificationSettings / LocalNotificationReadState at the app
root. Both CompositionLocals were already consumed by NotificationsScreen
and NotificationSettingsScreen but never provided, so each screen fell back
to its own PreferencesNotificationSettings(); disk prefs stayed in sync but
the in-memory StateFlow updates never crossed instances.
- Flip the master toggle when the OS permission is granted, so the "Enable OS
notifications" button matches its label end to end.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removing the per-row Cards also removed the only thing separating adjacent
rows. Restore that with the 0.25dp outlineVariant hairline the vanilla
NIP-29 branch of this same screen and the Concord server list already use.
Drawn before each row except the first, so a section's own header keeps
providing the separation at its boundaries — no stray line under the last
channel or above "Direct Messages". Applied to every row list on the screen
(chat channels, forums, inline DMs, hidden DMs) rather than just the
Channels section, so the screen stays one consistent surface; the vanilla
branch now shares the same helper instead of inlining it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Each channel row was its own filled Material3 Card. They stack with no gaps,
so their container colour merged into one continuous grey block behind the
whole Channels (and Forums) section — reading as a box drawn around the
channels that the Direct Messages rows immediately below, which are plain
rows, didn't have. The two halves of the same screen looked like different
surfaces.
Render the row as a plain Box on the screen background instead, keeping the
click on the container so the whole row still opens the channel. Matches
BuzzDmInlineRow directly below it and the Concord server list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`AccountManager.switchAccount()` correctly emits a new
`AccountState.LoggedIn` and the `remember(account, ...)` blocks around
`iAccount` / `accountRelays` in Main.kt already rebuild those. But the
Composables *inside* MainContent — feed screens, notification inbox,
profile screen, messages list — all held their own `remember { ... }`
state (LazyListState scroll position, expanded rows, per-column filter
tabs, in-flight metadata observers, view-models keyed on nothing) that
did NOT include the account as a key. So after a profile switch the
user still saw account A's feed items, notification unread state, and
follow-status overlays rendered under account B's identity, until they
navigated away and back to force the column to recompose from scratch.
Wrap the `MainContent(...)` call in `key(account.pubKeyHex) { ... }`.
This is the idiomatic Compose pattern for "identity changed — tear
down the entire subtree and rebuild it fresh": every child's
`remember { ... }` block re-runs, every `LaunchedEffect` re-enters,
every subscription restarts. Outer state (`deckState`, `workspaceManager`,
`singlePaneState`, `pinnedNavBarState`) lives above this call site so
the user's column layout / workspace / nav backstack are preserved —
only the account-owned content resets.
Concretely, after this change:
- Home Feed on account A → switch to account B → column now shows
account B's home feed, following account B's follow-list.
- Notifications tab on A → switch to B → tab reloads with B's unread
cursor, B's notification-kind toggles, B's mute/block enforcement.
- Direct Messages column stays on the Messages screen, but the
chatroom list is B's, DM subscriptions restart against B's kind:10050
inbox relays, giftwrap decryption uses B's signer.
- Profile screen viewing @alice: still viewing @alice, but the follow
button / mute button reflect B's follow/mute state instead of A's.
No new tests: verifying this needs a Compose UI test harness Amethyst
Desktop doesn't ship yet. The scoped-teardown behaviour of `key()` is
Compose runtime contract, not app-level state to pin down.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Two independent gaps meant the desktop notifications flow silently
did nothing after the user clicked the settings button:
1. `LocalNotificationSettings` and `LocalNotificationReadState` were
declared but never `.provides()`'d anywhere. Both
`NotificationSettingsScreen` and `NotificationsScreen` fell back to
a fresh `PreferencesNotificationSettings()` / `PreferencesNotification-
ReadState()` instance each time — the java.util.prefs backing store
kept the values consistent across cold restarts, but each Composable
held its own `MutableStateFlow`, so a `setEnabled(true)` in Settings
never fired the collectors in the inbox banner or in the auto-
dispatcher. The user could toggle the master switch back and forth
and nothing observable happened until the app was restarted. The
auto-dispatcher in Main.kt was already using its own hoisted
`notifSettings` instance too — just never handed down through the
composition — so it never even saw the toggle.
2. The "Enable OS notifications" button (only shown on macOS when
permission == NotRequested) called `dispatcher.requestPermission()`
and updated `permissionState`, but it never touched `settings.enabled`.
So the OS prompt appeared, user clicked Allow, the UI cheerfully
said "Permission granted" — and toasts still didn't fire because the
master switch was still off (defaults to false, first-launch UX
choice). The button label promised the whole flow; the code did
half of it.
Fix:
- Hoist `notifSettings` (already existed for the auto-dispatcher) into
the app-level `CompositionLocalProvider` as
`LocalNotificationSettings provides notifSettings`, so every consumer
reads/writes the same instance. Same-instance sharing means StateFlow
emissions actually propagate.
- Add a per-account `PreferencesNotificationReadState`, keyed on
`loggedIn?.pubKeyHex` via `remember(pubKeyHex)`, provided through
`LocalNotificationReadState`. This also fixes the "Mark all as read"
button in the inbox, which was `enabled = false` because the
composition-local was always null.
- In `NotificationSettingsScreen`, if `requestPermission()` returns
Granted and the master switch is currently off, call
`settings.setEnabled(true)` alongside setting the status message. The
master switch UI observes `settings.enabled` and recomposes
automatically, so clicking one button now does the whole handshake.
Behaviour on other platforms is unchanged: Windows / Linux start in
`PermissionState.NotApplicable`, so the "Enable OS notifications" branch
never renders there — they see the "Send a test toast" button directly.
The auto-enable is guarded by `!enabled`, so users who deliberately
disabled the master switch and then re-granted permission (e.g. after
denying in System Settings) don't get overridden if the switch was
still on.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
On the Concord home list the row's `clickable` expands/collapses, and the
only way to open a community was a separate `clickable` on the 40dp
avatar — nothing marked it as its own target, and tapping the name (the
thing most people reach for) silently expanded the row instead.
Give the name/subtitle column the same `onOpen` the avatar already has, so
the identity block — icon and title together — opens the community while
the rest of the row and the chevron keep expanding it. Tapping a title to
open the thing it names is the convention users arrive with; the disclosure
control stays the disclosure control.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>