Commit Graph
17954 Commits
Author SHA1 Message Date
Vitor PamplonaandGitHub ddbee398ca Merge pull request #3785 from vitorpamplona/fix/buzz-channel-row-background
fix(buzz): drop the grey slab behind the community Channels section
2026-07-28 18:48:08 -04:00
Vitor PamplonaandClaude Opus 5 15a389b155 fix(buzz): separate community rows with a hairline
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>
2026-07-28 18:45:13 -04:00
Vitor PamplonaandClaude Opus 5 4c5fe75f0a fix(buzz): drop the grey slab behind the community Channels section
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>
2026-07-28 18:35:05 -04:00
Vitor PamplonaandGitHub 0373032339 Merge pull request #3784 from vitorpamplona/fix/concord-home-open-target
fix(concord): open a community by tapping its name, not just its avatar
2026-07-28 18:19:24 -04:00
Vitor PamplonaandClaude Opus 5 fa1a32e91c fix(concord): open a community by tapping its name, not just its avatar
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>
2026-07-28 18:09:55 -04:00
Vitor PamplonaandGitHub ae6f56283b Merge pull request #3782 from vitorpamplona/feat/buzz-messages-toggle
fix(buzz): make the Messages toggle read the list it claims to change
2026-07-28 17:31:21 -04:00
Vitor PamplonaandGitHub c9e79e31f8 Merge pull request #3783 from vitorpamplona/claude/highlight-excess-spaces-0678he
Trim highlight context to bounded window, collapse whitespace
2026-07-28 17:25:17 -04:00
Claude de86e54cf8 fix: trim edge blank lines and skip full scan in highlight windowing
Audit follow-ups on the highlight context window:

- Blank lines sitting at the very start/end of a `context` tag were passed
  through untouched when a side was short enough not to be trimmed, so a
  highlight whose context began or ended with blank lines still rendered
  empty space above/below the quote. Trim the outer edges of the windowed
  passage (re-basing the marked range accordingly).
- `locate` enumerated every occurrence of the quote — a full context scan
  plus a list allocation — even in the common no-prefix case where only the
  first match is needed. Short-circuit to a single indexOf there.
- Clamp the returned marked range to the trimmed text length so it can never
  point past the end (e.g. a quote ending in trimmed whitespace).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApuEseGcFjUFqYoLhCuR91
2026-07-28 21:11:37 +00:00
Vitor PamplonaandClaude Opus 5 39f69de5e6 fix(concord): leave room for the FAB in the community channel list
Same defect the relay-group and Buzz DM lists had: the Scaffold's padding
carries the top/bottom bars but deliberately not the FAB (a FAB overlays
content by design), so the last channel row's manager overflow menu sat
underneath it and couldn't be tapped.

As contentPadding rather than a modifier, so rows scroll *through* the
strip instead of the viewport shrinking and leaving the FAB over dead
space. Gated on canManageChannels because that is what renders the FAB —
a plain member has nothing to clear, and no reason to lose the space.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:08:40 -04:00
Claude 89aea073ed fix: bound the context shown around a highlight to a window
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
2026-07-28 21:03:56 +00:00
Vitor PamplonaandGitHub 4f65058e87 Merge pull request #3777 from vitorpamplona/claude/lazycolumn-duplicate-key-ahgh60
Fix NoteListMatchingFilter to prevent duplicate entries under concurrent updates
2026-07-28 17:00:37 -04:00
Vitor PamplonaandGitHub a17e23d796 Merge pull request #3781 from vitorpamplona/claude/posts-malformed-r-tags-piu299
Fix bogus URL references from scheme-less prose tokens
2026-07-28 16:56:13 -04:00
Claude 0851768a8b fix: only tag explicit http(s) URLs as r references
Posts were publishing a flood of bogus `r` tags — the v1.13.0 release note
shipped 11 junk references such as `https://.deb/`, `https://window.nostr/`,
`https://kind:30166/` and `https://crowdin.pretended462/`.

The reference extractor `findURLs` fed the raw, scheme-less UrlDetector output
straight into `r` tags. That detector is deliberately eager: to let the
rich-text renderer linkify a bare `example.com`, it also reports every
`word.word`, `word/word` or `word:port` token with no real-TLD whitelist.
Prose is full of those (`.deb`, `.rpm`, `[database].backend`,
`nostr-wallet-connect/nwc`, `~2.5x`, `@mentions`), so each one became a
reference on the published note. The rich-text side already guards against
this via UrlParser (TLD validation + scheme separation); the tag path never
got the same guard.

Require an explicit http/https scheme and a valid TLD before a detected URL
becomes a reference. Rendering is untouched (separate parser), so bare domains
still show as links — they just no longer pollute the tags.

Adds regression coverage over the exact fragments from the v1.13.0 note plus
checks that real, explicitly-schemed links are still extracted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L8KXx8UQ6mgyxjBSWW6sQV
2026-07-28 20:51:35 +00:00
Vitor PamplonaandClaude Opus 5 618ef66e75 fix(buzz): make the Messages toggle read the list it claims to change
"Remove from Messages" was hardcoded on every surface, so it never showed
an "Add to Messages" counterpart for a channel that was already off the
list, and the Buzz workspace rows read a session-local snapshot of the
kind-10009 list that was seeded once and only ever grew — a channel taken
off Messages still rendered as a disabled "Added", leaving no way back.

Removal itself always worked (verified on-device: the kind-10009
republished and the row left Messages); what was missing was any read of
that list on the way back.

- RelayGroupListState: expose liveRelayGroupIds, the joined groups as
  normalized GroupIds, so the UI can ask whether a channel is on Messages
  without string-matching a raw relay url another client may not have
  normalized the way we do.
- RelayGroupTopBar / BuzzImportRow: one Add/Remove toggle driven by that
  flow. Remove no longer pops back — you stay a member reading the
  channel, and staying is what makes the entry flip so the action is
  visibly undoable. Leave still pops.
- BuzzRelayImportViewModel: track "added" against the live list instead of
  a one-shot seed, and add remove(); add() now also clears the dismissal
  so a relay's kind-44100 re-announcement isn't filtered back out.
- AccountViewModel: addRelayGroupToMessages() as the counterpart to
  removeRelayGroupFromMessages(); acceptChannelInvite() delegates to it.

Buzz DMs had the same one-way shape for a different reason: hiding is a
relay-side per-viewer flag (kind-41012 -> the kind-30622 snapshot), and
rebuildRows dropped hidden DMs on the floor, so a hidden conversation was
gone for good. There is no unhide command — re-opening is the unhide, a
kind-41010 with the same participants resolving to the same canonical
channel. Hidden DMs are now projected into their own list behind a
collapsible "Hidden (N)" header, faded but still openable, each offering
"Add to Messages". Also added to the community view's inline DM rows,
which had no menu at all and are where DMs actually live — the full inbox
sits behind a "see all" row that only appears above six DMs, so in a small
workspace the hidden section would have been unreachable.

Both list screens now leave bottom room for the FAB, which the Scaffold's
padding deliberately doesn't account for; the last row's overflow menu was
sitting underneath it.

Adds SimpleGroupListEventTest covering the removal path, including that a
renamed channel still matches (removal keys on group id + relay only).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:50:49 -04:00
Claude 4d24e2b09c fix: collapse scraped whitespace when reconstructing highlight context
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
2026-07-28 20:47:53 +00:00
Claude 58004fa744 fix: dedup EventListMatchingFilter (observeEvents) and harden emission contract
EventListMatchingFilter had the same mutable-sort-key defect as
NoteListMatchingFilter: it stored Notes in a ConcurrentSkipListSet ordered by
the live created_at, so a newer replaceable version (which mutates the shared
AddressableNote in place) stranded its node and let the same instance be
inserted twice — the emitted event list then carried the same event twice. It
hadn't surfaced as a crash only because its consumers (app recommendations,
relay groups, room reactions) happen to dedup downstream.

Apply the same capture-key + idHex-dedup + per-key compute design, but preserve
EventListMatchingFilter's update-reflecting semantics: an addressable update
re-emits (the snapshot reads the refreshed event live off the note) rather than
being ignored. It keeps the entry's captured position instead of re-sorting —
re-sorting via remove+add let two entries with different captured keys for the
same note transiently coexist and both read the same live event, duplicating it.

Also harden both filters' emission: a ConcurrentSkipListSet iterator is weakly
consistent, so under concurrent add/remove churn a single traversal can
momentarily surface a key twice. snapshot() now dedups by idHex so the emitted
list — the LazyColumn's source of keys — is always unique, regardless of
transient internal states. Corrected the over-claimed "can never hold two"
docstrings accordingly.

Adds EventListMatchingFilterTest mirroring the note tests: update-reflection,
version-note re-emit, sorted order, remove-after-mutation, and two concurrency
stress tests (with/without limit) that failed before this fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ah1aCniyjnzc27x4pwq2Df
2026-07-28 20:42:13 +00:00
Vitor PamplonaandGitHub 311b3ec7f0 Merge pull request #3779 from vitorpamplona/claude/buzz-delete-channel-n24icj
Add delete group/channel functionality (kind-9008)
2026-07-28 16:37:17 -04:00
Claude 83ec9b490b feat: allow admins to delete a Buzz channel / relay group
Wire up the existing kind-9008 DeleteGroupEvent, which was parsed on
receipt but never sent. Admins/owners can now delete a whole channel
(Buzz) or group (NIP-29) from the channel overflow menu.

- Account.deleteRelayGroup: sends the kind-9008 delete-group to the
  channel's relay(s) and drops it from the local list.
- AccountViewModel.deleteRelayGroup: signer-dispatched delegate.
- RelayGroupTopBar: admin-only "Delete channel" / "Delete group" item
  (error color), gated on RelayGroupMembership.ADMIN — the same
  authorization as Edit — behind a confirmation dialog that pops back
  off the deleted channel's screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bfd77tUsZ8h6ywcgwY7VVB
2026-07-28 20:17:43 +00:00
Vitor PamplonaandGitHub af04267f9a Merge pull request #3778 from vitorpamplona/claude/back-arrow-responsiveness-sz40m9
Fix back arrow visibility during predictive back-swipe animations
2026-07-28 16:09:35 -04:00
Claude 88ee46df05 fix: keep back arrow visible until the exiting screen finishes leaving
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
2026-07-28 20:03:43 +00:00
Vitor PamplonaandGitHub ab5a59a106 Merge pull request #3776 from vitorpamplona/claude/amethyst-lint-translation-hvu5pz
Remove unused buzz_dm_hide string resource
2026-07-28 15:55:03 -04:00
Claude c49e2d3390 fix: remove orphaned buzz_dm_hide translations
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
2026-07-28 19:45:48 +00:00
Vitor PamplonaandGitHub f3db2c73dc Merge pull request #3773 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-28 15:41:40 -04:00
vitorpamplonaandgithub-actions[bot] 456684edb1 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-28 19:22:32 +00:00
Vitor PamplonaandGitHub 31582320f2 Merge pull request #3775 from vitorpamplona/claude/resource-consumption-trigger-709n5v
Relax resource usage alert thresholds
2026-07-28 15:19:30 -04:00
Claude 5cb84e16dc test: cover the version-note guard path in observeNotes dedup
Add coverage for a real consumeBaseReplaceable call the suite was missing:
observers are also notified with the "version" note (getOrCreateNote(event.id),
a regular Note carrying the AddressableEvent), which the addressable-list guard
must drop while still listing the AddressableNote for the same event.

Confirmed the concurrency the stress tests exercise is real, not theoretical:
relay events are verified+consumed inline on per-relay socket dispatchers, so
distinct relays drive new()/remove() on the same note instance concurrently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ah1aCniyjnzc27x4pwq2Df
2026-07-28 19:17:57 +00:00
Vitor PamplonaandGitHub 68664bfac1 Merge pull request #3774 from vitorpamplona/claude/buzz-community-channel-style-l2l63v
Simplify ConcordAuthorFacepile to use ClickableUserPicture
2026-07-28 15:10:44 -04:00
Claude d9d02110dd fix(resource-usage): raise the background mobile data trigger to 500 MB
Bump BG_MOBILE_BYTES_PER_DAY from 100 MB to 500 MB/day so the report
prompt only flags genuinely excessive background cellular traffic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R2efhyiQHKFoNjQo6WnGRH
2026-07-28 19:10:43 +00:00
Claude 8a41129dfc fix(resource-usage): double the report-prompt trigger thresholds
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
2026-07-28 18:57:30 +00:00
Claude baf5f37532 feat: render Buzz channel recent-poster facepile with the standard avatar
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
2026-07-28 18:54:23 +00:00
Vitor PamplonaandGitHub 3842ab14bb Merge pull request #3772 from vitorpamplona/claude/concord-buzz-messages-visibility-ye6ocz
Add long-press menu to relay group & Concord channels in Messages
2026-07-28 14:51:15 -04:00
Claude 49e4234f99 feat: align leave vs remove-from-messages actions across chat types
"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
2026-07-28 18:40:16 +00:00
Vitor PamplonaandGitHub 48eea9a350 Merge pull request #3771 from vitorpamplona/claude/top-nav-font-sizes-vg4l9q
Remove unnecessary FontWeight.Bold styling from Text components
2026-07-28 14:34:12 -04:00
davotoula 7718e3ab19 fix: extract duplicated string literals 2026-07-28 20:29:21 +02:00
Claude 47be9909da fix: normalize bold top nav bar titles to default weight
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
2026-07-28 18:27:16 +00:00
Claude 636331487a fix: make observeNotes dedup lock-free and race-safe under concurrent consume
Audit follow-up. The previous fix used two independent concurrent structures
(ConcurrentSkipListSet + ConcurrentHashMap) coordinated with putIfAbsent, but
observer callbacks fire from multiple consume threads at once (relay ingest +
UI-side justConsume). A new()/remove() interleaving for the same idHex could
desync the two structures — remove() clears byId and no-ops on the sorted set
before new() has added the entry — leaving an orphan that a later new()
duplicates, reintroducing the duplicate-key crash.

Keep it lock-free (this observer is used everywhere and needs the throughput):
every write to the sorted index for a given idHex now happens inside that key's
ConcurrentHashMap.compute critical section, so the sorted set and membership map
move together. ConcurrentHashMap stripes per key, so same-idHex ops serialize
while different keys stay fully parallel. Invariant: an entry is added to the
sorted set only while its key is absent from byId, and every path that frees a
key removes its sorted entry first, so the set can never hold two entries for
one idHex.

Adds concurrency stress tests (with and without a relay limit) that fan out 8
threads hammering new/remove while created_at churns; both fail against the
non-atomic version and pass here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ah1aCniyjnzc27x4pwq2Df
2026-07-28 18:24:37 +00:00
David KasparandGitHub 153d28b420 Merge pull request #3770 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-28 20:17:44 +02:00
davotoulaandgithub-actions[bot] 8ce93f3700 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-28 18:15:15 +00:00
davotoula 14db81177f update cs,pt,de,sv 2026-07-28 20:06:18 +02:00
Claude 65e30c0acd fix: keep observeNotes list sorted while deduping addressables by idHex
Address review feedback: keep the incrementally-maintained, created_at-sorted
structure (a feed must stay sorted like a relay) instead of re-sorting a hash
map on every emission.

The root cause is unchanged: there is one Note instance per id/address
(LocalCache owns creation), but a note's sort key is mutable — a newer
replaceable event swaps the event on the SAME AddressableNote instance,
changing created_at in place. A sorted set ordered on that live value corrupts:
the moved node leaves the add()/remove() search path, so the same instance is
inserted twice and the emitted list carries a duplicate idHex, crashing the
App Recommendations LazyColumn (keyed on idHex).

Fix: snapshot the sort key into an immutable Entry when the note first enters,
order a ConcurrentSkipListSet on that snapshot (never read live again), and
index entries by the stable idHex (ConcurrentHashMap + putIfAbsent) so
membership stays unique and removal is reliable regardless of later created_at
changes. Ordering and "new versions do not update the list" are preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ah1aCniyjnzc27x4pwq2Df
2026-07-28 18:03:26 +00:00
Claude d9ae9cbd37 fix: normalize top nav bar title font sizes
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
2026-07-28 17:42:37 +00:00
David KasparandGitHub a585fe3664 Merge pull request #3764 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-28 19:41:02 +02:00
Vitor PamplonaandGitHub b085c8cc37 Merge pull request #3767 from vitorpamplona/claude/corcord-read-only-communities-gu1x4k
Implement CORD-02 §9 community dissolution seal
2026-07-28 13:40:28 -04:00
vitorpamplonaandgithub-actions[bot] 8145bedd15 chore: sync Crowdin translations and seed translator npub placeholders 2026-07-28 17:36:46 +00:00
Vitor PamplonaandGitHub 6666637f9c Merge pull request #3766 from vitorpamplona/claude/lazygrid-duplicate-key-v4k7sj
Fix duplicate app entries in Discover Apps grid
2026-07-28 13:34:03 -04:00
Vitor PamplonaandGitHub 6086e48859 Merge pull request #3765 from vitorpamplona/claude/keyboard-ime-stuck-state-et3yfu
Fix keyboard state tracking and back handler races
2026-07-28 13:33:14 -04:00
Claude eeefacca9e fix(ime): union nav-bar and IME insets on the two remaining bare-Scaffold forms
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
2026-07-28 17:25:34 +00:00
Claude 0b0abeebbb fix: prevent duplicate LazyColumn key from observeNotes on addressable updates
NoteListMatchingFilter (backing LocalCache.observeNotes) stored notes in a
ConcurrentSkipListSet ordered by CreatedAtIdHexComparator. AddressableNotes are
mutable: when a newer replaceable event arrives, LocalCache swaps the event on
the SAME note instance (consumeBaseReplaceable -> loadEvent), changing its
createdAt in place, then re-notifies observers. A sorted set cannot survive a
member's sort key mutating underneath it — the moved node is no longer found on
the add() search path, so the same note gets inserted a second time and the
emitted list carries a duplicate idHex.

The App Recommendations screen keys its LazyColumn on note.idHex (an
AddressableNote's address, e.g. 31990:<pubkey>:nostr-dvm-labeler), so the
duplicate crashed with IllegalArgumentException: "Key ... was already used".

Dedupe by the immutable idHex instead of a createdAt-ordered set; ordering is
computed fresh on each emission. Adds a regression test reproducing the
multi-item corruption path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ah1aCniyjnzc27x4pwq2Df
2026-07-28 17:23:50 +00:00
Claude a569095e19 feat(concord): honor CORD-02 §9 read-only seal on dissolved communities
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
2026-07-28 17:22:24 +00:00
Claude 5174c8e5a3 fix: dedupe Discover apps by coordinate to avoid duplicate LazyGrid keys
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
2026-07-28 17:09:31 +00:00