A kind:9802 highlight can carry a huge `context` tag — a quote pulled from
the middle of a long article may ship several paragraphs of surrounding
text. Rendered whole, that fills the feed card with paragraphs around a
one-sentence highlight.
Trim the context in `HighlightQuote.of` to at most ~160 characters on each
side of the marked quote, snapping the cut to a whole-word boundary and
marking it with an ellipsis. The quote itself is always kept in full and
the marked range is re-based onto the trimmed text, so the in-context
marker still lands exactly on the highlighted passage. Short contexts are
left untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApuEseGcFjUFqYoLhCuR91
A kind:9802 highlight with no `context` tag falls back to reconstructing
the surrounding passage from the W3C `textquoteselector` prefix/suffix.
Those fragments are scraped from the source web page, so they carry the
page's block-boundary whitespace (runs of newlines/spaces between DOM
nodes). Glued in verbatim as `prefix + content + suffix`, they render as a
stack of blank lines above the marked quote.
Collapse each whitespace run in the prefix/suffix to a single space (and
trim the outer edges) in `HighlightEvent.contextOrReconstructed()`. The
highlight's own `content` is left verbatim so its offsets inside the
reconstructed context stay exact for the in-context marker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApuEseGcFjUFqYoLhCuR91
canPop() read the globally-current back-stack entry via
currentBackStackEntryAsState(). A pop commits the instant it is accepted
— most visibly during a predictive back-swipe, whose exit animation is
long and finger-driven — so controller.currentBackStackEntry flips to the
destination while the screen being dismissed is still on screen, sliding
out and still composing its top bar. Evaluating canPop against the
incoming destination there dropped the back arrow (and re-showed the
bottom bar) before the outgoing screen had finished leaving, which is
exactly the flicker seen when back-swiping to Home or a bottom-nav root.
Evaluate poppability against the screen's own NavBackStackEntry — the one
the NavHost provides to each destination via LocalViewModelStoreOwner —
which is intrinsic to that screen and never changes for the life of its
composition. The arrow now stays put until the screen itself is gone.
Callers outside a NavHost destination (shell chrome, drawer) see the
account-scoped owner instead of an entry and fall back to the previous
globally-current behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011matx1mGJ6ZyS5dS77EQUo
The `buzz_dm_hide` string key exists only in translation files but has no
entry in the default `values/strings.xml` and is not referenced anywhere in
code. This triggered the lint ExtraTranslation error and failed the
`:amethyst:lintFdroidBenchmark` build. Removed the orphaned key from all 7
locale files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ksnCb8x45HnL7nFPzxh4B
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