Raise every ResourceUsageAlerts threshold to 2x so the "this app is
consuming too much" report prompt only fires at twice the previous
consumption levels, cutting false positives on heavy-but-normal days:
- background mobile data: 50 MB -> 100 MB / day
- background mobile relay-connection time: 12 h -> 24 h / day
- notification wakelock: 30 min -> 60 min / day
- app process starts: 75 -> 150 / day
- completed relay (re)connections: 5000 -> 10000 / day
Tests reference the constants symbolically, so the alert suite stays green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R2efhyiQHKFoNjQo6WnGRH
The recent-posters facepile in a Buzz community's channel rows drew each
poster with ObserveAndDrawInnerUserPicture — a bare cropped image wrapped
in a surface-coloured ring, overlapped into a deck. That omitted the
following badge and trust-score tag every other user avatar in the app
carries.
Draw each poster with ClickableUserPicture (the regular BaseUserPicture
path) instead, so the facepile shows the following icon (top-right) and
trust-score tag (bottom-centre). Lay the avatars out with a small gap
rather than an overlapping stack so those badges stay readable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013f3JZdoChYEEDtqTArFAxn
"Leave" meant three different things and the actions were scattered across
each channel's own screen, so they were hard to find from Messages. Make the
vocabulary consistent and reachable:
- "Leave" now always means "renounce membership / you're out" — the kind-9022
LeaveRequestEvent for NIP-29/Buzz groups, and the kind-13302 self-list removal
for Concord (its only exit).
- "Remove from Messages" is the single soft action: take it off my list but keep
membership. For a joined relay group this drops the kind-10009 entry without a
9022 (I stay in the roster) and dismisses the invite so a Buzz kind-44100
re-announce can't bounce it back. The Buzz DM "Hide conversation" reuses the
same label.
Surface both on the Messages rows via long-press (previously only reachable
inside each group/community screen):
- Relay-group row: "Remove from Messages" + "Leave".
- Concord row: "Leave" (reuses the existing confirm dialog; a community has no
soft/hard split since the list entry is the whole membership).
Split the relay-group top-bar menu into the same two actions, thread an optional
onLongClick through ChannelName, and consolidate the buzz_dm_hide string into the
shared remove_from_messages string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRifAJ75U4zWbg3V3Y3g8g
The Buzz relay group (community) top bar and its sub-screens (members,
threads, browse, channel list), the Buzz canvas, geohash chat/new/teleport
screens, the single-URL and single-geohash viewers, and the Marmot group
chat all forced FontWeight.Bold (geohash chat also titleMedium) on their
top-bar titles, while every sibling channel header — DM rooms, public
chats, ephemeral chats, live activities — and the shared TopBar wrappers
render the Material3 titleLarge default at normal weight.
Drop the bold (and size) overrides so all top nav bar titles share one
consistent treatment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019N7b8D4vBvafhG9eU7M9iJ
Three top nav bar titles overrode the Material3 titleLarge default with
titleMedium, rendering ~16sp while every other top bar title inherits
~22sp. Drop the titleMedium override so the Calendar event detail and Git
repository (home + sub-screen) titles match the rest of the app.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019N7b8D4vBvafhG9eU7M9iJ
Sweep of every imePadding() in the app for the same nav-bar-over-IME
double-count fixed for the chats. Two more bare Material3 Scaffold screens
applied the scaffold content padding (which carries the nav-bar inset) and
then imePadding() without consuming in between, so the nav bar stacked on top
of the IME while the keyboard was up:
- NewGeohashChatScreen (geohash create form)
- MarmotGroupInfoScreen (has the add-member search field)
Both now consumeWindowInsets(pad) before imePadding(), matching the idiom the
other forms already use. Every other imePadding() call was verified fine:
DisappearingScaffold-based screens are handled by the scaffold's own
reservation, dialogs/bottom sheets carry no nav-bar content padding, and the
rest either apply imePadding() alone at a root or already use the
navigationBarsPadding()/consumeWindowInsets union.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dT2RLbPZzan2hULL2cmc6
A dissolved community (owner-signed kind-3308 tombstone) is sealed
read-only per CORD-02 §9: held keys still open history, but nothing new
is honored. The `dissolved` flag was folded in quartz but ignored by the
write gates, so members — and the CLI — could still post to a dissolved
community.
- commons: ConcordChannel now tracks `dissolved` from the folded state
and `canPost()` returns false when set, so the Android composer (which
gates on it) is hidden. The self-delete carve-out is unaffected — it
runs through the note context menu, not the composer.
- amethyst: show a read-only notice where the composer would be so the
seal is explained rather than silent.
- cli: `amy concord send` folds the community and refuses with a
`dissolved` error before building/publishing; `amy concord channels`
surfaces the `dissolved` flag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HateTjrutJ23wAEttEwHQA
The Browser home "Discover nsites/napplets" sections render each followed
manifest in a LazyVerticalGrid keyed by "ns:"/"np:" + coordinate. The
observed store (NoteListMatchingFilter, backed by a ConcurrentSkipListSet
ordered by created-at/id) can surface the same addressable note twice when a
replaceable manifest gets a new version — mutating the note's sort key inside
the set breaks dedup. Both copies map to the same coordinate, producing a
duplicate grid key and crashing with IllegalArgumentException.
Collapse the mapped list by coordinate so each app appears once and grid keys
stay unique.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P9TomvuP9YnPUDCZuiQae6
While the keyboard was up, the public / relay-group / live-activity /
ephemeral chats — and the Concord channel + minichat views — left an extra
navigation-bar-height gap between the composer and the keyboard. WindowInsets.ime
is measured from the bottom of the screen, so it already spans the nav-bar
band; reserving the nav bar again on top of it double-counts. NIP-17 DMs were
unaffected because their scaffold reserves the nav bar with the consuming
navigationBarsPadding(), which excludes the already-consumed IME inset (a
union), while the bottom-bar chats reserved it as a plain, non-excluding pad.
- DisappearingScaffold: when the bottom-bar slot renders empty it now reserves
max(0, navigationBars - ime) instead of the full nav-bar inset, so the
reservation drops to zero while the keyboard is up (the root imePadding has
already lifted the scaffold above it). Covers every bottom-bar chat at once.
- ConcordChannelScreen / MinichatScreen: use the standard
padding(padding).consumeWindowInsets(padding).imePadding() union instead of
summing a plain padding(padding) with imePadding().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dT2RLbPZzan2hULL2cmc6
Leaving a chat with the keyboard up via a back gesture left a large IME
padding stranded at the bottom — and, because WindowInsets.ime is a single
app-wide holder, on every other screen too — until some later inset pass
happened to rebalance it. It only reproduced on release builds.
Root cause: the chat composers install a BackHandler that flushes the draft
and pops the screen. When that pop runs while the keyboard is still visible,
the predictive-back window animation races the IME's close animation. On an
optimized release build the window animation wins, and the IME
WindowInsetsAnimation is cancelled before its terminal (zero) frame reaches
Compose, so the shared insets holder stays "animating" and every
imePadding() in the app freezes at the keyboard height. Debug and benchmark
builds are slow enough that the IME animation completes first, which is why
they were unaffected.
Fixes:
- New KeyboardAwareBackHandler: while the keyboard is on screen it does not
consume back, so the system dismisses the keyboard first (its animation
completes cleanly); the next back runs the original handler. The top-bar
back arrow remains an always-available exit. Adopted in the DM, public-chat
and new-group-DM composers.
- Rederive keyboardAsState() from WindowInsets.ime instead of the
pre-edge-to-edge getWindowVisibleDisplayFrame/OnGlobalLayout heuristic,
which under enableEdgeToEdge() could itself latch Opened after the keyboard
closed. Now it tracks the same inset that drives imePadding().
- Concord channel chat and the minichat thread view used a bare Material3
Scaffold whose content insets ignore the IME; add imePadding() so the
composer rides above the keyboard while typing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012dT2RLbPZzan2hULL2cmc6
- Rename LocalCache Dao overrides to match supertype parameter names
(getOrCreateUser: pubkey->hex, getOrCreateNote: idHex->hex,
getOrCreateAddressableNote: key->address) so named-argument calls stay safe.
- Replace deprecated MenuAnchorType with ExposedDropdownMenuAnchorType in the
Buzz dropdown composables.
- Remove the buzzChannelType() extension in BuzzChannelMetadata, now shadowed by
the equivalent (stricter) member on GroupMetadataEvent, and drop its unused
imports.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018VBP6HGj9M2kzA4WKAivjC
Move the bottom navigation bar entry out of App Settings and into the
Account Settings section of the All Settings catalog, since its state is
stored per account (AccountNavigationPreferencesInternal.bottomBarItems in
the account-synced settings blob).
While auditing the App Settings section for other per-account entries, two
more were purely per-account and moved alongside it:
- Reactions settings (reaction row actions) writes account-synced
reactionRowItems and now sits next to the existing "Reactions" entry.
- Messages settings writes account-scoped chat feed toggles and view modes
(account.settings.enabledChatFeeds / relayGroupViewMode / concordViewMode).
The remaining App Settings entries stay put: UI preferences, Home tabs,
Profile UI, Calendar reminder, OTS, Namecoin and Resource usage are all
device-global, and Compose/Notification settings are mixed (they hold
genuine app-global preferences alongside a few per-account toggles).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH88gCbbdTfdzp9wrr7buJ
Replacing the markdown pipeline dropped TranslatableRichTextViewer, and
with it the ML Kit auto-translation of the quoted passage. Restore it
without giving up the marker.
A translation rewrites the passage, so the character offsets that locate
the quote inside its context stop pointing at anything. Translate the
quote as well and re-find it in the translated context: ML Kit works
sentence by sentence, so a quote that is one or more whole sentences --
the common case, since people highlight sentences -- comes back identical
whether it is translated alone or inside its paragraph.
untranslated -> context, original span marked
translated + quote found -> translated context, translated span marked
translated + not found -> translated quote alone, fully marked
The fallback is free: HighlightQuote.of already returns the quote alone
when the needle is not in the haystack, so the not-found case needs no
special casing. Marking a guessed span, or claiming the whole paragraph
was highlighted, would both be worse than showing less.
That second translation must not draw its own status bar, so add
rememberTranslation() to both flavors: play reuses the existing
translateAndCache and its cache; fdroid, which ships no translation
service, returns the content unchanged.
Also indent the "Auto-translated from X to Y" line to the quote's own
15dp so it lines up with the text rather than the bar.
Verified on device: an English highlight renders as a fully translated
Portuguese paragraph with the quoted sentence marked inside it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The renderer never drew a highlight. It synthesised a markdown string --
blockquote each line with "> ", wrap the quoted span in "**" -- and handed
it to the rich-text viewer, so a highlight arrived as bold text. That also
meant the quoted article prose was parsed as markdown, so any *, _, # or [
in it was interpreted as formatting rather than shown.
Drop the markdown round-trip and paint the marker behind the glyphs. The
stroke is drawn per visual line from the TextLayoutResult, so it follows
soft wraps and stops at real glyph edges. Per-line rounded rects rather
than SpanStyle(background), which can only ever be a hard full-line-height
rectangle -- that is what buys the rounded pen ends.
Size the stroke from the baseline and font size, not the line box, so
leading and stroke weight stay independent knobs.
Along the way:
- Locate the quote as an index range instead of context.replace(), which
marked every occurrence when a quote repeated. Use the W3C
TextQuoteSelector prefix -- already on the event, previously ignored --
to disambiguate.
- Restore 1.35em leading. The markdown path forced 1.5em via
MarkdownTextStyle; the ambient bodyLarge sets no lineHeight at all, so
rendering plain text inherited the font's intrinsic ~1.2em.
- Indent the source attribution by the quote's own 15.dp so it lines up
with the text rather than the bar, and space the comment, quote and
attribution 8.dp apart -- they were flush at 0.dp.
- Clamp the stroke to the column so it cannot be clipped on full-width
lines.
Light keeps a near-opaque yellow with dark glyphs reading through it. Dark
cannot do that, so it gets a translucent amber that glows rather than
covers. Not derived from the user's accent: a highlighter reads as yellow.
The quoted passage no longer routes through TranslatableRichTextViewer, so
it loses its auto-translate affordance; drawing the marker requires owning
the text layout. The author's own comment above the quote keeps it.
Verified on device in both themes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>