Commit Graph
16890 Commits
Author SHA1 Message Date
Claude b6a238b71a fix(concord): mute the community-name pill now that the logo is the avatar
The Concord community chip (Messages header + Notifications) no longer needs the
strong secondaryContainer highlight — the community's logo is now the row avatar,
so the name reads as faint tappable metadata (same wash as the note-header
markers). The NIP-29 relay-host chip (RelayNameChip) keeps its highlight; a relay
group has no avatar of its own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 23:50:27 +00:00
Claude c482af0578 feat(concord): custom-emoji autocomplete in the channel composer
Wire the shared NIP-30 custom-emoji picker into the Concord composer like the
@-mention flow: typing `:shortcode:` opens ShowEmojiSuggestionList (backed by
EmojiSuggestionState(account.emoji)); WatchAndLoadMyEmojiList loads the user's
packs. On send, account.emoji.findEmojiTags(text) attaches the NIP-30 emoji tags
to the kind-9 rumor (plain message + inline reply), so recipients render the
custom image inline via the shared chat renderer. Image uploads in messages,
icon and banner were already wired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 23:36:50 +00:00
Claude 7cd2be1c74 feat(concord): channel management + community banner & relay editing
Two ways to update a Concord community/its channels that were missing:

Channels (net-new): ConcordModeration.defineChannel writes a ChannelEntity
control edition (create/rename/delete via version chaining); Account gains
createConcordChannel/renameConcordChannel/deleteConcordChannel. The channel-list
screen gets a create FAB and a per-row rename/delete menu, all gated on
MANAGE_CHANNELS (the same predicate the fold enforces).

Community metadata: the edit screen now edits the banner (encrypted ImagePointer
upload via the shared banner hero, reusing ConcordImageUploader) and the relay
set (add/remove chips + RelayUrlEditField). Also fixes editConcordMetadata
silently dropping the banner on every save (it now round-trips it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 23:36:50 +00:00
Vitor PamplonaandClaude Opus 4.8 d84555a27b fix(concord): real role names + full member roster (CORD-02 §5 / CORD-04)
Two roster gaps against the reference client (Armada):

1. Moderators showed as "Admin" and role definitions were often empty. The
   displayed roles came from ConcordCommunityState.roles, which was built from the
   RAW structural fold heads — so a rogue higher-version edition on a role's
   coordinate (e.g. marking the Admin role deleted) corrupted or emptied the roster,
   and even when present the UI collapsed every role-holder to a single "Admin"
   badge. Now state.roles comes from the authority-gated resolver
   (AuthorityResolver.roles(), exposed alongside rolesFor()), and ConcordMembersScreen
   renders each member's actual most-privileged role name (Admin / Moderator / custom).

2. Member count was a fraction of the real one (e.g. 10 vs ~44). CORD-02 §5: "an
   author seen publishing is observably present, auto-included even if their Join
   never arrived." The roster only counted Guestbook joiners + the privileged roster,
   omitting the bulk of members who never post a Join. ConcordCommunitySession now
   tracks observedAuthors from every decrypted channel message and folds them into
   allMembers() and the roster.

Also: amy's `concord roles/grant/ban/...` now register the control-plane stream key
before draining (like `channels`/`read`/`send` already do), so the mod verbs aren't
served an empty fold on NIP-42-gated relays — used to ground-truth the resolved roles.

Verified via amy against live Soapbox: `concord roles` now returns Admin (pos 1) and
Moderator (pos 2) instead of []. quartz + commons concord suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:13:47 -04:00
Claude af8a95c44c feat(concord): add an Open channel action to the minichat thread
A Concord minichat pins its root and backfills it from the channel history, but
when the root is still loading (or you aren't a member yet) there was no way out.
Add an Open channel action to the top bar for Concord threads, navigating to the
full channel (Route.Concord) where the whole timeline loads and membership lives
— the correct affordance to click through to the chat room itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 23:11:42 +00:00
Claude e8b7ad8f8b fix(concord): tapping a minichat reply opens its thread, not just the channel
routeFor sent every Concord-gathered note to the channel screen, so tapping a
kind-1111 minichat reply (e.g. from Notifications or the thread view) landed in
the channel instead of the reply's thread. Route a Concord CommentEvent through
minichatRouteFor (its thread); a top-level message / inline reply / reaction
still opens the channel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 22:59:09 +00:00
Claude 86e29ed25a fix(concord): surface Concord replies/reactions on the Notifications tab
Review of the notification path (datasource + dal + display) for Concord replies
and likes that p-tag the user:

- Datasource OK: ConcordChannelPreload (mounted always-on in LoggedInPage) keeps
  every joined community's control + channel planes subscribed app-wide, so the
  wrapped reply/reaction wraps arrive regardless of tab. (The account-level
  #p=self notification sub can't see them — the outer wrap p-tag is ephemeral.)
- Projection OK: channelRumors filters only by channel/epoch binding, so kind-7
  reactions and kind-1111 replies both reach LocalCache via consumeConcordRumor.
- DAL had two gaps, now fixed:
  1. The follow-scope gate dropped notifications from community members who
     aren't in my follows (they usually aren't) unless in Global mode. Concord
     notes now bypass it like Chess/DM/Marmot — the explicit p-tag already scopes
     to genuine replies/reactions/mentions, so general chatter never leaks.
  2. Concord's default reply mode is an INLINE reply (kind-9 ChatEvent), which
     wasn't in NOTIFICATION_KINDS at all, so inline replies and @-mentions never
     notified. Added kind-9 (only notifies when it p-tags me). Minichat replies
     (kind-1111) and likes (kind-7) were already displayable.

Also let Concord notes skip the per-kind relevance heuristic (the p-tag is the
relevance signal), so a reaction still notifies when my target message hasn't
loaded yet to resolve replyTo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 22:46:52 +00:00
Vitor PamplonaandClaude Opus 4.8 83c985819f fix(concord): render the decrypted community icon (local file:// avatar)
A Concord community icon is CORD-02 §6 encrypted media: rememberConcordImageModel
fetches the ciphertext, AES-256-GCM-decrypts it, and caches the plaintext at a
local file:// path, which it hands to the avatar as the model. But
RobohashFallbackAsyncImage always wrapped the model in ProfilePictureUrl, and the
thumbnail-cache fetcher behind it (ProfilePictureFetcher) delegates a cache-miss
to Coil's http-only NetworkFetcher — so the file:// load failed and the row fell
back to the robohash, even though the icon had decrypted and cached correctly.

Only route remote http(s) pictures through the thumbnail cache; hand local/content
URIs straight to Coil's native fetchers, which load them directly. Verified
on-device: the Soapbox community icon now renders on the Messages rows and the
community header instead of a robohash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:10:21 -04:00
Vitor PamplonaandClaude Opus 4.8 92f422be88 fix(concord): authority-gate the control-plane fold (CORD-04 §1)
The fold selected each entity's head by STRUCTURAL fold only and never dropped
editions from unauthorized signers, so a spoofed edition that structurally
supersedes a legit one won — on the live Soapbox community this surfaced a decoy
metadata edition ("invalid ... clients should be ignoring this", no icon) over
the real owner-authorized one, so no community icon/banner ever rendered.

Two stacked wire/authority bugs, both fixed:

- RoleEntity.scope was typed String, but the reference client (Armada) writes it
  as an object ({"kind":"server"} / {"kind":"channel","channel_id":...}) per
  CORD-04 §2. The type mismatch failed the whole RoleEntity decode, dropping the
  role — and with it every grant depending on it — so no admin ever resolved.
  Added RoleScope and retyped the field (no writer set it, so no migration).

- AuthorityResolver.resolve now folds each role/grant/banlist CHAIN through
  authorized editions only, via an owner-rooted fixpoint over the full edition
  set (not the post-hoc structural heads). A rogue higher-version grant from an
  unprivileged key is dropped instead of superseding the owner's grant, so the
  legit authority stands. ConcordCommunityState.fold then gates metadata by
  MANAGE_METADATA, channels by MANAGE_CHANNELS, the banlist by BAN, and the
  dissolution tombstone to the owner alone.

Verified live end-to-end: `amy concord channels Soapbox` now folds the real
metadata (name "Soapbox Community", the blossom icon + banner pointers) and all
9 channels; fetching + AES-256-GCM-decrypting the icon pointer yields a
hash-verified PNG. Regression tests cover the object-scoped role and the rogue
superseding grant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Vitor PamplonaandClaude Opus 4.8 122403baf4 feat(cli): AUTH as the Concord plane stream key so amy can read planes
Concord relays gate a plane's kind-1059 wraps behind NIP-42 and serve them
only to a connection authenticated AS the plane's derived stream key — the
member is neither the wrap's author (the stream key) nor its recipient (a
throwaway ephemeral key), so an account AUTH is refused and every plane REQ
came back empty (no channels, no messages), even though the community folded
its name.

Mirror the app's per-plane AUTH (60475c10c0) in the CLI:
- Context.registerConcordStreamKeys(relays, secrets) records the derived
  control/channel stream secrets, scoped to the community's relays.
- The RelayAuthenticator provider now signs one kind-22242 per registered
  stream key alongside the account AUTH — locally, from the raw derived key
  (NostrSignerSync), never the account, so no user identity is exposed.
- The concord channels/read/send verbs register their control + channel
  stream keys before draining/publishing.
- drain() gains pendingOnAuthRequired: an auth-required CLOSED keeps the relay
  pending instead of terminal, so the post-auth subscription re-fire delivers
  the events rather than the one-shot drain returning empty. The concord verbs
  opt in; all other drains are unchanged.

Verified end-to-end against the live Soapbox community (relay.ditto.pub /
relay.dreamith.to): `amy concord channels` now folds the name + 9 channels
consistently and `amy concord read` returns messages. `channels` also emits
the folded icon/banner/description pointers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Vitor PamplonaandClaude Opus 4.8 a4d9087187 fix(concord): include the 32-zero id in the invite bundle key (CORD-05)
CORD-05 Appendix A.1 says the HKDF `id` is always present — 32 bytes,
all-zero for a label with no meaningful id — and A.6 lists
`concord/invite-key` with `id = 0…0`. inviteBundleKey built its info with
no id at all, so it derived a different bundle key than the reference
client (Armada) and NIP-44 decryption of an Armada-minted invite bundle
failed with "Invalid Mac" — the join aborted with "no valid bundle for
this link". amy round-tripped with itself (same wrong key both sides), so
its own tests passed and the divergence went unnoticed.

Verified by decrypting a real Armada invite bundle: the no-id key fails the
MAC; the zero-id key yields a valid CommunityInvite JSON. Pass ByteArray(32)
like the sibling banlist/dissolved derivations already do.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Vitor PamplonaandClaude Opus 4.8 8060dc0bb1 fix(concord): fold refounded community control plane for fresh joiners
After a Refounding (CORD-06 §3), compaction re-wraps each entity's head
verbatim, so a head edited past genesis still carries an `ep`/prev citing
an edition in the prior epoch that a fresh joiner never fetches.
EditionFold.foldEntity required a genesis edition (prevHash == null) and
returned null otherwise, so a fresh login folded the whole Control Plane to
nothing: no community icon, no name, no edited channels (while Armada,
which implements the CORD-04 §1 fresh-joiner rule, showed them all).

Anchor at the lowest-version edition when no genesis is present and walk up
from there. Safe: Amethyst always re-folds the whole buffer from scratch
(no persistent floor), so it is structurally always a fresh joiner;
authority is validated on top by AuthorityResolver, so an unrooted forgery
is still dropped; and a genuine mid-chain gap still stops at the intact
prefix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:12:49 -04:00
Claude 6f63b33119 feat(concord): show the community pill on Concord notifications
Extract the Messages row's highlighted community chip into a shared
ConcordCommunityPill (secondaryContainer, group icon, 20-char cap) and render it
in NoteCompose's header markers for any Concord-gathered note, so a Concord
message surfaced in Notifications names its community and taps through to the
channel list — the same chip, wherever the message appears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 20:17:15 +00:00
Claude 24c2cfedc6 fix(concord): reply from Notifications/feed goes through the channel plane
routeReplyTo special-cased Marmot groups but had no Concord case, so replying to
a Concord message outside a chat screen (e.g. from Notifications) fell through to
a public kind-1111 comment — leaking the private rumor id onto public relays and
producing a reply that doesn't bind to the channel and no member would see.
Route a Concord-gathered note's reply into its minichat (its thread root for a
kind-1111, the message itself for a kind-9), whose composer sends the wrapped
kind-1111 on the channel plane. (Reactions already route through
reactToConcordMessage; zaps are forced PRIVATE via isPrivateRumor since a Concord
rumor is unsigned.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 20:17:13 +00:00
Claude 64e0fa2a91 fix(concord): restore the Messages server/community chip's highlight
PR #3559 muted the shared HeaderPill (correct for NoteCompose's PoW/OTS/location
markers), but that also flattened the Messages tab's server/community chip, which
is a first-class navigation entry point and should stand out. Give RelayNameChip
back its own secondaryContainer highlight instead of the muted pill, and hard-cap
the community name at 20 chars so a long title can't crowd the row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 20:08:20 +00:00
Claude e734d6400c feat(concord): send & receive encrypted image messages (Armada-compatible)
Concord channel messages can now carry images, wire-identical to Soapbox
Armada's `encryptAttachments`: a normal channel-bound kind-9 whose ciphertext
URL is appended to the content and annotated by a NIP-92 `imeta` tag with
`encryption-algorithm aes-gcm`, hex `decryption-key`/`decryption-nonce`, and the
plaintext `ox` hash (no `x`). The blob is AES-256-GCM ciphertext on Blossom, so
the media host and relays only ever see encrypted bytes — the community's E2E
guarantee holds.

Reuses the NIP-17 encrypted-media stack end to end: quartz's imeta tag vocab
and IMetaTagBuilder to build/parse the tag (ChannelChat.imageMessage /
encryptedImageImeta / encryptedImagesOf), the shared UploadOrchestrator
encrypted upload + ChatFileUploadDialog picker on the send side, and the OkHttp
EncryptedBlobInterceptor keyCache on the receive side — registering each
attachment's cipher (keyed by URL) lets the normal feed renderer display the
decrypted image with no shared-render changes. Encryption is mandatory
(no toggle, and a missing cipher fails closed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzJ2Cwo8tg4oZq43oRa3ig
2026-07-14 19:44:56 +00:00
Claude e231b0cf5b Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-14 15:25:40 +00:00
Vitor PamplonaandGitHub 8fc841e50e Merge pull request #3559 from vitorpamplona/claude/notecompose-pow-pill-style-vr6hbt
Unify note-header markers into a pill / quiet-mark design system
2026-07-14 11:19:34 -04:00
Vitor PamplonaandGitHub 596cb24de4 Merge pull request #3553 from vitorpamplona/claude/large-screen-layout-v5otrb
Adaptive large-screen layout: nav rail, permanent drawer, notification panel, reading-column cap
2026-07-14 11:19:10 -04:00
Claude 14d28dddad Merge remote-tracking branch 'origin/main' into claude/notecompose-pow-pill-style-vr6hbt
# Conflicts:
#	commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf
2026-07-14 15:00:16 +00:00
Vitor PamplonaandGitHub 01c4ee1870 Merge pull request #3558 from vitorpamplona/claude/community-approval-parse-error-vneyon
Improve NIP-72 approval event parsing robustness
2026-07-14 10:56:19 -04:00
Vitor PamplonaandGitHub ee49c8a987 Merge pull request #3557 from vitorpamplona/claude/antispam-filter-null-pointer-hkx70i
Fix null pointer in AntiSpamFilter when LRU cache evicts duplicates
2026-07-14 10:55:29 -04:00
Vitor PamplonaandGitHub a915a03c32 Merge pull request #3556 from vitorpamplona/claude/left-drawer-banner-crop-bkxw9f
Fix profile banner image scaling from FillWidth to Crop
2026-07-14 10:54:18 -04:00
Claude adb26df838 feat: tier-scaled navigation transitions for large screens
Full-width slide-from-end pushes read as disconnected on large screens:
the click comes from the docked drawer on the left and a whole 600dp+
column flies across the pane from the right. Keep one navigation
grammar (drill-in from end, modal from bottom, tab switches fade) and
scale the motion per tier: phones keep the existing full-width slides,
large screens get shared-axis moves — a 1/10-pane nudge plus fade — and
the screen behind a push fades with its slight scale so both aren't
visible mid-transition.

Transition specs run outside composition and can't read
LocalScreenLayout, so AppNavigation mirrors the tier into
NavTransitionTier for the spec lambdas, all of which live in
NavigationEffects' shared builders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHSoAVvYioihaJeSumX8J7
2026-07-14 14:51:14 +00:00
Claude cc03651460 fix: pad custom top bars by systemBars so desktop caption bars are respected
The Search and Browser top bars are plain layouts (not Material3
TopAppBars) padded with statusBarsPadding only, and DisappearingScaffold
used the same for its no-top-bar fallback. In a desktop-style window
(Waydroid/DeX freeform) the window's caption/title bar is a separate
inset that statusBars does not include — Material's own top bars pad by
systemBars and were fine, but these three drew underneath the title bar.
Pad by WindowInsets.systemBars top instead, which covers both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHSoAVvYioihaJeSumX8J7
2026-07-14 14:51:14 +00:00
Claude bced238db4 fix: give the notification panel a Surface so its text follows the theme
The panel's Column sat in a bare Row with no Surface above it, so
LocalContentColor fell back to Color.Black and the header label was
invisible on the dark theme — the same trap DisappearingScaffold
documents for its own root. The Surface provides the container color
and onBackground content color for everything inside the panel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHSoAVvYioihaJeSumX8J7
2026-07-14 14:51:14 +00:00
Claude 6b6f0fd2ae feat: cap every screen to the reading-column width on wide panes
Capping only the feeds' contentPadding left top bars, list screens,
bookmarks and settings stretched across the whole center pane. Move the
cap up a level: every NavHost destination is wrapped in
CappedScreenContent (600dp, centered) through the shared route builders
in NavigationEffects, so each screen's entire surface — top bar, tabs,
content — shares one reading column, on all ~200 destinations at once.

Opt-outs at registration: Route.Message keeps the full pane for its
two-pane list/conversation split, and Browser/WebApp/NostrApp stay
full-pane so the warm EmbeddedTabLayer surfaces keep lining up.

This supersedes the LocalFeedSidePadding-based capping on Android: the
shell no longer provides side padding (CenterPane is a plain Box again)
and the now-dead overrides in MessagesTwoPane and NotificationSidePanel
are removed. The commons local stays, documented as the padding-based
alternative for hosts like a desktop reading column where gutters
should still scroll.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHSoAVvYioihaJeSumX8J7
2026-07-14 14:51:13 +00:00
Claude c72024578e fix: harden the large-screen shell against runtime window-size changes
Fixes from an adversarially verified audit of the large-screen commit.
The two most serious bugs shared a root cause: the shell was correct at
any fixed size but mishandled the size CHANGING mid-session, which
foldables and multi-window make routine (MainActivity handles those
configChanges without recreation).

Bug fixes:
- Hoist the shell content into movableContentOf so crossing a layout
  tier (fold/unfold, rotate, resize) MOVES the NavHost subtree between
  shells instead of disposing it — screen state (drafts, pager tabs,
  expanded states, warm embedded tabs) now survives.
- DisappearingScaffold snaps bars back to visible when hiding gets
  disabled, so chrome scrolled away before a resize is no longer
  stranded off-screen with no reset path.
- ProfileScreen keeps WindowInsets.navigationBars instead of zeroing
  all content insets; with the bottom bar gone (large screens, and
  pushed entries on phones) content no longer underlaps the system bar.
- New TabReselectCoordinator: AppBottomBar registers each screen's
  re-tap handler even when the bar renders nothing, and the rail routes
  selected-item taps through it — restoring tap-current-tab-scrolls-to-
  top on the rail tier with the screens' existing logic.
- NotificationSidePanel now reuses the screen's SingleNotificationsBody
  (parameterized by scroll-state key), which restores WatchScrollToTop —
  previously the panel stranded scrolltoTopPending=true on the shared
  feed state, suppressing later send-to-top requests — and the inbox-
  relay warning header; it also honors split notifications by showing
  the Following feed when that setting is on.
- Entering the permanent-drawer tier snaps a stale Open drawerState to
  Closed, so returning to a modal tier no longer pops the drawer
  uninvited.
- MessagesTwoPane keys its TwoPane strategy on the width size class so
  the split fraction updates when the pane crosses 840dp in place.
- The drawer status editor calls onDone() after send/delete, so it can
  collapse back to the read-only bar in the docked drawer (and no
  longer waits for a drawer close in the modal one).
- The landscape auto-close drawer effect's inverted condition
  (close-only-when-already-closed, a pre-existing no-op) now closes an
  open drawer as intended.

Structure and performance:
- INav.isDrawerDocked models docked-ness explicitly: Nav.openDrawer()
  no-ops while docked, and consumers stop inferring from a DrawerState
  that never transitions.
- zonedDrawerSwipeIfModal wraps the edge-swipe modifier with the docked
  check so call sites can't forget it; TopBarNavigationIcon centralizes
  the back-arrow/avatar-or-nothing leading slot.
- The rail reuses AppBottomBar's entry icons (NotifiableIcon,
  FavoriteEntryIcon, rememberFavoriteIconModel) instead of duplicating
  them.
- MessagesScreen derives its pane size class via
  WindowSizeClass.calculateFromSize instead of restating the 600/840
  breakpoints.
- rememberFeedContentPadding folds the scaffold, baseline, and side
  paddings into one remember slot; the shell quantizes the feed side
  padding to 8dp steps so continuous resizes don't invalidate every
  feed per pixel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHSoAVvYioihaJeSumX8J7
2026-07-14 14:51:13 +00:00
Claude 8a34ae692f feat: adapt the app shell to large screens
Three layout tiers driven by the window width size class, published once
through LocalScreenLayout (ScreenLayout.kt):

- Compact (phones): unchanged — bottom bar + modal drawer.
- Medium (portrait tablets, unfolded foldables): the bottom bar is replaced
  by a left NavigationRail built from the same user-configured
  BottomBarEntry list (customization, pinned favorites and new-item dots
  carry over); the drawer stays modal behind the rail's avatar button.
- Expanded (landscape tablets, desktop windows): the drawer is permanently
  docked on the left (no ModalNavigationDrawer), and on windows >= 1200dp a
  docked notification panel renders the notifications card feed on the
  right, sharing last-read marking with the full screen. The panel hides
  while the Notifications screen itself is open.

Large screens also pin the chrome: DisappearingScaffold stops hiding the
top/bottom bars on scroll (and stops toggling the OS status bar), and
AppBottomBar renders nothing everywhere.

Feed content width is capped at 600dp inside wide center panes:
the shell measures the center pane and provides
(paneWidth - 600dp) / 2 via LocalFeedSidePadding (commons), which
rememberFeedContentPadding merges into every feed's contentPadding — the
scroll surface stays full-width so pull-to-refresh and edge scrolling keep
working. Panes that manage their own width (Messages two-pane, the
notification panel) override it back to 0.

Screen sweep: Messages now picks single/two-pane from its actual pane
width instead of the window size class; the Home/Messages pagers only
attach the drawer edge-swipe when a modal drawer exists; top-bar avatar
drawer-openers hide on large screens; FABs keep their bottom spacing
without the bar; the status editor in the drawer no longer cancels editing
when the drawer is permanent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHSoAVvYioihaJeSumX8J7
2026-07-14 14:50:29 +00:00
Vitor PamplonaandGitHub d1fb2ed42c Merge pull request #3551 from vitorpamplona/claude/mobile-battery-optimization-s7vw4a
Battery: background-usage fixes, 122-relay ping study, and an on-device resource-usage ledger
2026-07-14 10:48:54 -04:00
Vitor PamplonaandGitHub 4fcae15bcb Merge pull request #3554 from vitorpamplona/claude/flatpak-desktop-release-ci-wfbcre
Add Flatpak bundle support to release CI + Flathub submission
2026-07-14 10:48:19 -04:00
Claude 103a3dfc23 fix: skip event parse of community approval contents that are not JSON objects
Clients in the wild publish kind-4550 approvals whose content is plain
text (e.g. braille ASCII art) or a custom non-event JSON wrapper instead
of the NIP-72 stringified approved event. containedPost() fed any
non-blank content straight into Event.fromJson, producing a
JsonParseException and a noisy logcat warning for every such approval.

Only attempt the parse when the trimmed content starts with '{', so
plain-text contents are skipped silently while genuinely malformed
event-looking JSON still logs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXiy3Ajmr3vdrV83jZsUx6
2026-07-14 14:41:43 +00:00
Claude 621cdda2e8 fix: crop drawer banner instead of letterboxing short images
The left drawer banner used ContentScale.FillWidth inside a fixed
120dp-tall box, so banners wider than the box's aspect ratio left
empty bars above and below the image. ContentScale.Crop fills the
full banner height and crops the excess width instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BhQ1FCyoKCD1UeBT5mWy18
2026-07-14 14:33:35 +00:00
Claude 079417ba60 feat: add submission-ready Flathub manifest variant
New self-contained desktopApp/packaging/flatpak/flathub/ directory whose
contents are copied verbatim into the flathub per-app repo at submission
time (Flathub has no separate accounts — submission is a GitHub PR):

- manifest builds from the published GH Release tarball (pinned to
  v1.12.6 url + sha256 from the release asset digest) instead of the
  local type:dir tree CI uses — Flathub build servers must fetch all
  sources themselves
- x-checker-data (json type, is-main-source) on the archive source so
  flatpak-external-data-checker auto-PRs url/sha256 bumps and metainfo
  <release> entries on every new GitHub Release
- own metainfo copy carrying the permanent <releases> history Flathub
  requires (the CI variant keeps injecting its entry at build time);
  screenshots remain the documented submission blocker
- flathub.json restricting builds to x86_64 (no aarch64 tarball exists
  and jpackage cannot cross-compile one)

Verified locally: built the flathub manifest end-to-end from the real
v1.12.6 tarball (sha256 enforced by flatpak-builder), installed it, and
booted the actual app inside the sandbox under Xvfb — UI rendered and
the embedded Tor daemon spawned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYfNxhPZC3WRrn82Dm2Cb4
2026-07-14 14:32:47 +00:00
Claude e684cb02a3 feat(ui): tap explainers for the PoW, private-rumor and expiration markers
Following the OTS pill's pattern, tapping now explains what the marker
means: PoW describes the mining difficulty as an anti-spam stamp
(pow_info_description), the private-rumor lock explains the unsigned
private delivery and its deniability (private_rumor_info_*), and the
expiration pill shows the request to delete plus the exact expiry
date/time (expiration_info_description).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5
2026-07-14 14:31:23 +00:00
Claude 079822ed9d fix: NPE in AntiSpamFilter.isSpam when the first duplicate was evicted from the LRU cache
The spam check can fire via the spamMessages record alone, after the
original event id/address has been evicted from the 2000-entry
recentEventIds/recentAddressables LruCache. In that case existingEvent /
existingAddress is null and building the njump link threw a
NullPointerException, aborting event consumption in LocalCache.

Fall back to the current event's link when the original is no longer
cached, and use setOfNotNull in logOffender so a null cache entry can't
sneak into the Spammer sets through the platform-type hole.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UGiCujAHWMbmGT89X77S4K
2026-07-14 14:30:40 +00:00
Claude 500de62841 fix(ui): explain pending OTS on tap, render the stamp glyph and pill icons larger
Tapping the pending OTS pill now shows a toast explaining that the
attestation is waiting to be stamped into the Bitcoin blockchain (new
ots_info_pending_description). The OpenTimestamps glyph gets a
near-full-em content box — its fine outline read much lighter than
Material's solid shapes at the standard 80..880 bounds — and HeaderPill
icons go from 11dp to 13dp.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5
2026-07-14 14:15:56 +00:00
Claude 7e78204336 feat: ship a Flatpak bundle of the desktop app on release CI
The linux-portable release leg now wraps the createReleaseDistributable
tree it already builds into a single-file Flatpak bundle and attaches it
to the GH Release as amethyst-desktop-<version>-linux-x64.flatpak.

Packaging fixes to the existing (previously unwired) Flathub manifest:
- add the missing 512x512 icon (copy of desktopApp icon.png) and install
  it under hicolor/512x512 instead of the never-present 256px path
- drop the openjdk module + sdk-extension: the jpackage tree bundles its
  own trimmed JRE, so /app/jre was pure bloat
- grant --socket=x11 instead of wayland/fallback-x11: Compose Desktop
  renders via AWT/skiko (X11-only on Linux, XWayland on Wayland), so
  fallback-x11 left the app socketless on Wayland sessions

CI wiring:
- install flatpak tooling + the freedesktop runtime/sdk (version greped
  from the manifest so the pin can't drift), retried like other fetches
- inject the AppStream <release> entry for the tagged version at build
  time (the checked-in metainfo deliberately carries none)
- flatpak-builder with --disable-rofiles-fuse (GH runners) and a
  --state-dir under desktopApp/build so the repo tree stays clean
- collect via the existing asset-name.sh contract (new flatpak ext)

Verified end-to-end locally: built the bundle from the manifest with a
stubbed jpackage tree, installed it, and ran the exported command inside
the sandbox (freedesktop 24.08, args forwarded through the wrapper).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYfNxhPZC3WRrn82Dm2Cb4
2026-07-14 14:06:37 +00:00
Claude 568aa9b005 fix(ui): use middle ellipsis in HeaderPill labels
Truncated pill labels (capped city names, relay hosts) keep their
distinguishing endings, matching the codebase convention for URLs and
relay links.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5
2026-07-14 13:49:08 +00:00
Claude 811d9cf802 feat(ui): icon-only fork mark, pencil edit mark, tighter expiration and location pills
- The fork marker drops "Forked from <name>" for a bare fork-right icon
  (new MaterialSymbols.ForkRight, U+EBAC); tapping still opens the
  original version. Font regenerated via subset.sh, which also re-baked
  the custom OpenTimestamps glyph, proving the custom-glyph pipeline
  survives regeneration.
- The edited mark becomes a pencil: bare pencil for the latest edit,
  pencil + "#2"/"original" only while cycling versions on tap.
- The expiration pill clamps beyond one year to "1y+" instead of
  switching to a full date.
- The location pill caps at 110dp and ellipsizes, so unbounded city
  names cannot squeeze the author's name out of the row.
- Remove the now-unused existed_since string from all 35 locale files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5
2026-07-14 13:46:12 +00:00
Claude 170bc7c121 fix(ui): restore full-size quiet marks, tighten the dotted timestamp, drop Boosted
Quiet marks go back to the row's regular text size in bold with 16dp
icons; the hashtag/community soft links lose their 12sp override too
(the smaller tier read as too small). A new TimeAgoStyle.DottedTight
renders "• 5m" without the leading space for rows whose spacedBy
already provides the gap, removing the double space before the
timestamp. The OTS pending pill shrinks to the stamp icon plus an
ellipsis (the words move to the content description). The Boosted mark
is removed entirely — from the Android header, the commons component,
and the desktop feed — since the repost context is already visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5
2026-07-14 13:30:38 +00:00
Claude 41ef1d89b3 feat(ui): add the OpenTimestamps logo as a custom glyph and use it in the OTS pill
Bake the official OpenTimestamps stamp logo (traced from
opentimestamps/logo vector.svg; monochrome outline, tinted at render
time like every other glyph) into the Material Symbols subset font at
U+F8F0. New tools/material-symbols-subset/add_custom_glyphs.py converts
the traced SVGs in custom/ into TrueType glyphs and is invoked by
subset.sh after pyftsubset, so font regenerations keep them.

With the logo identifying the pill, drop the verbose "OTS:" prefix:
the pill now reads icon + "2y" (or icon + "Pending", new
R.string.pending).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5
2026-07-14 12:59:30 +00:00
Vitor Pamplona efae1dec22 Adjustments 2026-07-14 08:21:44 -04:00
Claude d29ee4e6a6 Merge remote-tracking branch 'origin/main' into claude/concord-quartz-amethyst-plan-0oy779 2026-07-14 04:27:53 +00:00
Claude 86da29fa78 fix(ui): mute the header pill wash, restore dotted timestamps, tighten menu gap
- HeaderPill's secondary-container background pulled too much attention;
  it now uses a faint onSurface wash (7%) with placeholderText content,
  so pills read as tappable metadata without competing with the note.
- Note-header timestamps go back to the original dotted format at the
  default size; the TimeAgo style/fontSize params are reverted.
- The timestamp + more-options pair renders unspaced again (the dot and
  the button's icon inset provide the separation), fixing the oversized
  gap the row-level spacedBy introduced before the 3-dot menu.
- The header preview now consumes its fabricated events as
  already-verified (they cannot pass id/sig checks, which left every row
  bare), gives the repost a parseable inner event, and fetches the draft
  through its AddressableNote (draft wraps are addressable events).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgEpNtwnetPhETXQSdq4b5
2026-07-14 01:26:33 +00:00
Vitor PamplonaandGitHub d043f4d143 Merge pull request #3549 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-07-13 20:42:21 -04:00
vitorpamplonaandgithub-actions[bot] a335c0387c chore: sync Crowdin translations and seed translator npub placeholders 2026-07-14 00:39:07 +00:00
Vitor PamplonaandGitHub 7c82c9cc17 Merge pull request #3550 from vitorpamplona/claude/1059-empty-content-filter-nge9qp
Decouple Android notification kinds from Desktop subscription list
2026-07-13 20:36:57 -04:00
Claude 7a30c18590 refactor: decouple Android NOTIFICATION_KINDS from desktop's subscription list
Android never uses NotificationKinds.SUBSCRIPTION_KINDS for relay
subscriptions — every kind in it already arrives via Android's own
datasources (FilterNotificationsToPubkey, the chat datasources, the
wallet assembler) or via local unwrapping (14/15). Spreading it into
NOTIFICATION_KINDS only coupled Android's display gate to a list whose
job is desktop's relay-filter/toast allow-list, which is exactly how
the kind-1059 wrap leaked into the Android notifications tab.

Restore NOTIFICATION_KINDS as an explicit flat set (identical content
to the previous commit's subtraction) and add
NotificationKindsContractTest as the drift tripwire: envelope kinds
(1059/21059) must never render on the Android tab, and every kind
desktop notifies on must be either displayable on Android or a known
envelope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011A6iSrJJHUoz686eDnCQMV
2026-07-14 00:19:01 +00:00
Vitor PamplonaandGitHub 107777369f Merge pull request #3548 from vitorpamplona/claude/user-nicknames-contactcard-mglbz8
Nickname users via NIP-85 contact cards (encrypted petname + private note, custom emojis)
2026-07-13 20:16:09 -04:00