Commit Graph
16756 Commits
Author SHA1 Message Date
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
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 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 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
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
Claude d2f02fbd32 style: double the nickname card's top margin, halve its bottom margin
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 23:52:13 +00:00
Claude b2456f2051 revert: remove the 'What's using your battery' insights section
The relay-focused recommendations were built on a wrong premise: with the
outbox model, the number of open connections and the reconnect churn are
driven by the relays the user's FOLLOWS publish to, not by the user's own
relay list — so 'Edit relays' steered users at a lever that doesn't control
the number. Rather than ship a recommendations engine whose headline advice
is misleading, the section, the UsageInsights rules, their tests, and their
strings are removed. The measured data itself (rates tiles, cost cards,
subsystem bars, screen time) stays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
2026-07-13 23:42:56 +00:00
Claude 19bbfb3be4 fix: keep kind:1059 gift wraps out of the Android notifications tab
The desktop notifications redesign (ecedc4af) extracted the shared
NotificationKinds.SUBSCRIPTION_KINDS list from Android's
NotificationFeedFilter and rewired NOTIFICATION_KINDS to spread it. The
shared list includes GiftWrapEvent.KIND (1059) because it doubles as the
relay subscription filter (you must ask relays for wraps to receive
NIP-17 DMs) and because Desktop renders the wrap itself as a DM inbox
row. Android's display list never had 1059 before that commit, so the
delegation silently started rendering wrap envelopes in the
Notifications tab — with created_at randomized up to 2 days back per
NIP-59, producing misordered, undecryptable rows instead of routing DMs
to the chat screens.

Subtract GiftWrapEvent.KIND from the Android display set, restoring the
pre-extraction behavior (1059 was the only kind the delegation added).
SUBSCRIPTION_KINDS keeps 1059, so the desktop relay subscription, the
desktop OS-toast allow-list, and the desktop inbox DM rows are all
unaffected. Android push is likewise untouched: NotificationDispatcher
already excludes 1059/21059/13 and notifies on the unwrapped inner
event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011A6iSrJJHUoz686eDnCQMV
2026-07-13 23:37:27 +00:00
Claude b76b37744e feat: nickname card on the user profile, above the real display name
The profile header no longer replaces the big display name with the petname.
Instead, when the account nicknamed the user (or kept a private note about
them), an outlined card renders above it — petname, divider, private summary —
with the standard Lock private marker in its top-right corner, since both
fields live NIP-44 encrypted in the account's contact card. Tapping the card
opens the shared nickname editor. The profile's own display name stays fully
visible underneath. Feeds, chats and mentions keep rendering the petname
instead of the display name.

To carry the summary into the UI, the commons PetName holder generalizes to
Nickname(petName?, summary?, tags), built when either field exists — so a
note-only card (no petname) now shows on the profile too, while the name
override everywhere else keys strictly off petName.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 23:33:05 +00:00
Claude ef3be70741 fix: reject kind:1059 gift wraps with empty content before caching
A gift wrap with an empty content string carries no NIP-44 ciphertext,
so it can never be unwrapped. Filtering it at LocalCache's ingestion
choke point (justConsumeInnerInner) rejects it before the Schnorr
signature check and before it takes a permanent cache slot, and keeps
GiftWrapEventHandler from retrying a doomed unwrap on every batch.

Locally stripped wraps (copyNoContent, used to drop the ciphertext
after a successful unwrap) are assigned directly to note.event and
never pass through justConsume, so they are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011A6iSrJJHUoz686eDnCQMV
2026-07-13 23:02:21 +00:00
Claude a6da2ee69d fix: record contact-card EOSEs from the d-tag so card syncs are incremental
UserCardsSubAssembler.newEose read the filter's p-tags to stamp each target
user's card EOSE, but kind:30382 filters address targets in the d-tag — so
no per-user EOSE was ever recorded, groupByRelayPresence always classified
users as never-checked, and every filter update re-downloaded the visible
users' cards with since = null. Read the d-tag instead, and use DTag.TAG_NAME
on both the filter builder and the EOSE reader so the two keys cannot drift
apart again. Pre-existing on main; surfaced while verifying that card syncs
are incremental from the account's outbox relays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 21:52:29 +00:00
Claude e8ecf429a2 refactor: move nickname policy, emoji autocomplete, filters and dialog to commons
Generalizes the remaining platform-bound pieces of the nickname feature so
the desktop app can adopt it:

- ContactCardsState.displayNameFlow/cachedDisplayName own the NIP-81 render
  policy (petname over profile name over npub); the Android observeUserName
  is now a thin wrapper around it
- EmojiSuggestionState.autocompleteInto completes the word under the cursor
  and closes the list — replaces the insert+reset pair copy-pasted in the 8
  composer ViewModels and the nickname dialog
- the kind:30382 filter builders move to commons relayClient/assemblers
  (cards about targets from trusted accounts, and the account's own cards by
  author), shared by the user watcher and the login subscription
- ShowEmojiSuggestionList moves to commons nip30CustomEmojis/ui using coil
  and commons string resources
- EditNicknameDialog moves to commons nip85TrustedAssertions/ui: it takes the
  ContactCardsState and an onSave callback, so each front end only wires its
  own publish path and menu entry; dialog strings move to commons resources

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 21:20:36 +00:00
Vitor PamplonaandGitHub beaae90eb5 Merge pull request #3547 from vitorpamplona/claude/build-oom-investigation-ryd9u6
fix(ci): serialize :amethyst Kotlin compilations on CI to stop daemon OOMs
2026-07-13 16:50:39 -04:00
Claude 9d176edd1c fix: make usage insights accurate — impact ranking, honest attribution, missing rules
Accuracy fixes:
- Insights are ranked by an estimated-impact score (rough hours of extra
  radio/CPU activity, ranking only, never displayed) before the cut to
  three — declaration order no longer decides which consumer gets dropped.
- The notification insight states the measurement ('held connections open
  for N relay-hours in the background') instead of an unverified 'largest
  background cost' superlative, and only fires when the service's uptime
  covers at least half the background window — a service that ran 20
  minutes can no longer be blamed for 30 hours of background connections.
- When the measured drain split shows foreground use dominating (>=3x the
  background share with enough signal), an informational note leads the
  list saying most battery went to screen-on use — so nobody flips a
  setting expecting savings it cannot deliver.

Completeness — new rules, each with a deep link:
- PoW mining time -> compose settings (default difficulty), scored as
  full-tilt CPU.
- Reconnect churn -> relay list (a flaky relay burns a handshake plus a
  radio wake-up per retry) — distinct diagnosis from 'too many relays'.
- Push-processing wakelock time / process-start churn -> notification
  settings.

Count strings converted to plurals per res/CLAUDE.md. Insight test suite
extended to 15 cases covering the attribution gate, score ordering, the
honesty note, and each new rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
2026-07-13 20:23:16 +00:00
Claude 8a98ed3d15 feat: actionable usage insights, rates, cellular-first bars, Tor card, and per-screen time
The screen presented evidence and left the user to be the analyst; now it
draws the conclusions and links each one to the setting that acts on it:

- Insights: a UsageInsights rules layer (unit-tested, thresholds informed by
  the ping study and normalized per observed day) renders up to three
  plain-language recommendations, each deep-linking to notification
  settings, media settings, the relay list, or privacy options.
- Rates: battery %/hour in app (from the measured drain sampler), data/hour
  in app, and average simultaneous relay connections — totals aren't
  judgeable, rates are, and avg relays is directly actionable.
- Subsystem bars rank by CELLULAR bytes when any exist (with totals as
  secondary context) — Wi-Fi bytes are nearly free, so ranking by totals
  pointed users at the wrong feature.
- Built-in Tor gets the same cost card as the always-on service: uptime,
  bootstraps, and a link to privacy options.
- Per-screen time (screen.<Name>.ms): a ScreenTimeIntegrator fed by the nav
  controller's destination listener, gated on foreground. PRIVACY: only the
  route's base name is recorded — screenNameOf strips every navigation
  argument before the value leaves the navigation layer, so the ledger says
  'Profile', never whose profile. Shown as proportion bars and included in
  the report's summary table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
2026-07-13 18:07:59 +00:00
Claude 899ada61e4 fix(ci): apply the :amethyst Kotlin compile limiter only on CI
The daemon OOM needs a cache-cold compile of several :amethyst variants in
one invocation, which only CI's combined test/lint/assembleBenchmark run
produces; local builds are incremental and usually build a single variant,
and have never shown the problem. Gate the maxParallelUsages=1 service on
the CI env var (always set on GitHub Actions) so local multi-variant builds
keep their parallelism.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ifbUGCADckUb3zaox9wBo
2026-07-13 18:00:28 +00:00
Claude 53e8f8df09 feat: always-on service cost card with a link to notification settings
The uptime row told users the service ran, not what it cost — and the
setting to turn it off lives three screens away. The resource screen now
shows a dedicated card (only while the service has run this week) that
frames the cost in decision terms: running time and restarts, the relay
connections it held while the app was closed, and the measured background
battery drain — with a button straight to notification settings where the
delivery method can be changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
2026-07-13 16:30:07 +00:00
Claude b2914fbc4b feat: count always-on notification service starts
Uptime alone can't distinguish a stable 24h service from one the watchdog
restarts every hour — each restart is a process wake plus a full round of
relay reconnects. Reported alongside uptime in the usage report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
2026-07-13 16:21:20 +00:00
Claude bd4d80160e fix: resolve remaining audit findings in worker gates, ledger accuracy, and alert flow
- ScheduledPostWorkGate now treats PUBLISHING as pending work: claiming the
  last PENDING post no longer cancels the periodic worker mid-publish (which
  stranded the post in PUBLISHING with no recovery). ScheduledPostStore also
  releases stale PUBLISHING claims at initial load — a claim found on disk
  belongs to a dead process and is reset to PENDING for retry.
- CalendarReminderWorker decides its self-cancel on a FRESH cache snapshot,
  closing the race where an RSVP accepted mid-run was lost forever (the
  observer's KEEP schedule no-ops while the chain exists). A second observer
  on kind-31922/31923 re-arms the chain when the organizer reschedules the
  target event — the existing RSVP never re-fires observeNewEvents, so the
  moved appointment previously produced no reminder. The rescan is throttled
  (conflate + 30s) and gated on the feature being enabled.
- UsageCountingInterceptor skips loopback hosts: local Blossom cache hits
  (127.0.0.1) never touch the radio and were inflating mobile rx bytes,
  radio-burst estimates, and potentially the background-data alert.
- The usage alert's 7-day rate limit is now consumed when the user acts on
  the dialog (any button or dismissal) instead of before the first frame, so
  a config change or process death no longer burns the prompt budget on a
  dialog nobody saw.
- DEV_REPORT_PUBKEY is defined once in service/crashreports and imported by
  both the crash and resource-usage send paths.
- ResourceUsageScreen remembers its summaries keyed on the loaded data — the
  2s memory ticker was recomputing the full multi-day counter walk on every
  recomposition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
2026-07-13 16:14:41 +00:00
Claude ed04c373b8 fix(ci): serialize :amethyst Kotlin compilations to stop daemon OOMs
CI runs die with java.lang.OutOfMemoryError (GC overhead limit exceeded /
Java heap space) inside the shared Kotlin compile daemon when two variant
compilations of :amethyst run at the same time — e.g. compilePlayDebugKotlin
and compilePlayBenchmarkKotlin in the test-and-build-android job, which
demands four :amethyst variants (unit tests, lint, assembleBenchmark) in one
invocation. KotlinCompile submits its work asynchronously via the Worker
API, so the project lock is released while compilation is still running in
the daemon and Gradle happily starts the next variant's compile task in
parallel; two full :amethyst codegen passes no longer fit the daemon's
-Xmx8g heap (see runs 29260711359 and 29248908870; removing Kover in #3539
helped but was not sufficient).

Register a no-op shared build service with maxParallelUsages = 1 and make
every :amethyst KotlinCompile task use it. The scheduler then runs this
module's Kotlin compilations one at a time — a task holding the service is
not complete until its async worker finishes — while all other work (other
modules' compilation, JVM tests, lint analysis, packaging) keeps running in
parallel. Verified with Worker-API probe tasks: unconstrained same-project
tasks start simultaneously; with usesService they run strictly one after
the other.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ifbUGCADckUb3zaox9wBo
2026-07-13 15:56:51 +00:00
Claude aa34f9cb0e feat: track PoW, Tor, calls, decrypts, remote signs, and battery drain in the resource ledger
New counters for the app's remaining heavy battery consumers, all
transition-based so tracking never adds work to the activities measured:

- pow.ms/sessions: NIP-13 mining time from the PoW queue's isMining flow —
  the largest attributable CPU burner, previously an unattributed cpu.ms
  mystery
- tor.ms/starts: in-app Arti uptime from the raw TorService status (not
  TorManager.status, whose WhileSubscribed upstream would keep Tor's
  control flow alive if the ledger subscribed to it)
- service.alwayson.ms, call.ms/sessions, nests.ms/sessions: foreground
  service lifecycles for the always-on relay service, calls, and NIP-53
  audio rooms
- crypto.decrypt/encrypt.count+us and sign.local/nip46/nip55.count via a
  MeteringNostrSigner decorator wrapped inside NostrSignerWithClientTag at
  account load; crypto durations only metered for local-key signers so
  IPC/relay waits never poison the CPU numbers
- location.ms: GPS listening segments around LocationFlow collection
- battery.drain.fg/bg: measured percent-while-discharging sampled at each
  flush — the ground truth to correlate the other counters against

Ledger infrastructure hardening the new counters build on:

- all duration math now uses SystemClock.elapsedRealtime; wall-clock jumps
  (NTP, timezone) no longer fabricate or delete accounted time
- pre-flush-hook adds are drained in place and no longer re-arm the
  debounced flush, killing a self-perpetuating 30s wake-and-write loop
- drain is now AtomicLong.getAndSet(0) instead of remove+sum, closing a
  lost-update race that undercounted the hottest counters
- shared TimeSegmentIntegrator base extracted; relay/foreground integrators
  and the new SessionTimeIntegrator all use it
- @Volatile on LocalCache.verifyMeter (matching the onchainBackend
  precedent)

New counters surface in the usage screen (zero rows hidden), the NIP-17
report tables, and the per-day dump. 9 new unit tests including a
regression test for the flush loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
2026-07-13 15:56:03 +00:00
Vitor PamplonaandGitHub b6560ab3f4 Merge pull request #3546 from vitorpamplona/claude/compose-chip-notification-toggle-6nmy6h
Replace remove-from-notify with mute/unmute bell toggle
2026-07-13 11:23:01 -04:00
Claude b69304c4a8 fix: import EmojiSuggestionState from commons in ShowEmojiSuggestionList
ShowEmojiSuggestionList sat in the same package as EmojiSuggestionState, so
it referenced the class without an import — moving the class to commons broke
the Android compile. Caught by an honest verification run: earlier gradle
invocations were targeting a nonexistent :amethyst:compileDebugKotlin task
(the app builds flavored compilePlayDebugKotlin/compileFdroidDebugKotlin)
with the failure masked behind a pipe, so the last three commits had never
actually been compiled.

Verified at this commit: quartz nip85 tests (13/13), :commons:compileKotlinJvm,
:cli:compileKotlin, :amethyst:compilePlayDebugKotlin, :desktopApp:compileKotlin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 15:12:15 +00:00
Claude ab4b0b984d refactor: audit nickname code — dedupe emoji helpers into commons
Review pass over the nickname/contact-card feature for reuse, simplicity and
performance:

- EmojiSuggestionState moves to commons next to EmojiPackState and now takes
  the EmojiPackState directly instead of the whole Android Account, so the
  desktop app can reuse the :shortcode: autocomplete
- the findEmoji helper that was copy-pasted in 8 composer ViewModels is gone;
  everyone calls the shared EmojiPackState.findEmojiTags, which now resolves
  codes through a map lookup instead of a linear scan per code
- ContactCardsState owns the emoji resolution for nickname saves (Account
  just publishes), takes EmojiPackState, and drops its unused scope param
- new synchronous cachedPetName path (decryption-cache read, no crypto) seeds
  observeUserName/observeUserPetName initial values, removing the flash of
  the real name before the flow's first emission
- nickname dialog prefill no longer clobbers text typed while a slow external
  signer decrypts the existing card

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 12:40:38 +00:00
Claude 184f0bfde7 refactor: align ContactCardEvent with the nip88Polls class structure
Replicates the PollEvent layout: all tag parsing moves to TagArray extensions
in TagArrayExt.kt with the event accessors delegating to them, builder
extensions use addUnique for single-instance tags, and construction goes
through template-returning builders instead of methods that sign internally.

- build() returns an EventTemplate<ContactCardEvent> (the signer is only used
  to NIP-44 encrypt the private tags, matching TrustProviderListEvent);
  create() remains as the signer.sign(build(...)) convenience and now takes
  the emoji list directly
- updatePetNameAndSummary() returns an unsigned EventTemplate; callers sign
  it (ContactCardsState and tests updated)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 12:28:23 +00:00
Vitor PamplonaandGitHub 9d79d3bc7f Merge pull request #3545 from vitorpamplona/claude/amethyst-video-repeat-limit-bq2ut7
Add AutoReplayLimiter to cap video auto-loops in feed
2026-07-13 08:11:28 -04:00
Claude b282ac32af feat: turn Notify chips into bell mute-toggles listing all thread members
The composer's Notify row previously showed only the parent's p tags and
its author, each with an x that removed the user for good. It now lists
every thread member (authors along the reply chain plus the parent's
mentions), and each chip carries a bell instead: tapping it mutes the
notification for that user but keeps the chip — faded and with a
bell-off icon so the state is obvious — making it one tap to add them
back. Muted members are dropped from the outgoing event's p tags (and
from a private note's receivers), and drafts round-trip the muted state
by re-deriving thread members whose p tag the draft dropped.

The NIP-22 comment composer gets the same chip semantics; there the
mute is honored for the optional zap-sender tag, while the structural
root/reply scope tags stay as NIP-22 requires.

Adds the notifications_off glyph to the Material Symbols subset font.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013eK8mGPcuKYXNyhKVJ8Wg7
2026-07-13 08:48:02 +00:00
Claude 5761f37d0f feat: pause looping videos after 5 automatic plays
Feed videos run under REPEAT_MODE_ONE and looped forever. AutoReplayLimiter
counts each MEDIA_ITEM_TRANSITION_REASON_REPEAT as one completed play and
pauses the player when the 5th play finishes, leaving the video on its first
frame with the play button showing. Pressing play (or the feed mutex resuming
a video scrolled back into view) grants a fresh 5-play allowance, and pooled
players reset the count when they switch to a different media item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9RXiM42yqznwTVjw8tLZv
2026-07-13 01:58:10 +00:00
Claude 903f3d9dcd feat(ledger): catch-all HTTP metering, radio-burst estimator, media play time
Closes the gaps the "should we track the non-websocket client?" question
exposed, and adds the battery-estimation layer bytes alone can't provide:

- Counting moves from per-role wrappers to a single interceptor on the
  shared base client (OkHttpClientFactory), with role attribution via
  request tags (role wrappers now only tag, inserted outside the counter).
  Anything reaching the shared clients untagged lands in a new "other"
  subsystem instead of escaping the ledger — which the napplet broker did,
  and which Coil's image traffic did too: setImageLoader called
  getHttpClient directly, bypassing the metered okHttpClientForImage.
  The app's largest HTTP consumer was invisible; now it's tagged "image".

- Battery-relevant shape of the traffic, not just its size: per-subsystem
  request counts and active-transfer time, plus a radio-burst estimator
  (new HTTP activity after >10s of silence ~= one cellular radio
  ramp+tail). Scattered small requests cost far more energy than one
  continuous download of the same byte count; bursts/day makes that
  pattern visible and reportable.

- Media playback time (ExoPlayer isPlaying segments via a listener in
  ExoPlayerBuilder — every player in the app): decoder+screen+streaming
  at once, turning "video used 800 MB" into "800 MB over 2h" (normal)
  vs "over 10 min" (a bug).

All surfaced in the usage screen, the NIP-17 report, and UsageSummary;
20 ledger tests total (burst gap logic, play-time segments, and the
existing suites) all passing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016RJ8EAsdkHx5WHHU2eQJ1P
2026-07-13 01:54:38 +00:00
Claude ebeecd1ee7 feat: custom emojis in contact card petnames
Nicknames can now use NIP-30 custom emojis: typing : in the nickname dialog
autocompletes from the account's emoji packs, and the emoji mappings for any
shortcode used are embedded in the card's NIP-44 encrypted content next to the
petname — so even the emoji set stays private. Renderers resolve the petname's
shortcodes against the card's decrypted tags instead of the profile's metadata
tags.

- quartz: updatePetNameAndSummary replaces the private emoji tag set wholesale
  and keeps it out of the public tags; round-trip test added
- commons: PetName(name, tags) holder with content equality, decryption cache
  returns the merged decrypted tag list, EmojiPackState.findEmojiTags resolves
  :codes: against the selected packs
- amethyst: Account embeds resolved emoji tags on save; all petname render
  sites pass the card tags to the WithEmoji composables; nickname dialog gets
  the : emoji autocomplete via EmojiSuggestionState

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 01:39:59 +00:00
Vitor PamplonaandGitHub a2e858d7b4 Merge pull request #3544 from vitorpamplona/claude/pr-note-button-sizing-732r64
Clean up Git card styling and fix duplicated subject in content
2026-07-12 21:28:11 -04:00
Claude 8ee90907ca feat: nickname users via NIP-85 contact cards signed by the account key
Adds petnames/nicknames per https://github.com/nostr-protocol/nips/pull/761,
reusing the kind:30382 contact card already used for WoT scores — a card only
acts as a nickname when signed by the account's main key. Petname and summary
are always stored in the card's NIP-44 encrypted content.

quartz:
- ContactCardEvent.updatePetNameAndSummary edits both fields in the encrypted
  private tags, strips stray public copies, preserves every other tag
- TagArray.petName()/summary() parsers for decrypted tag lists + tests

commons:
- ContactCardDecryptionCache: LRU NIP-44 decrypt cache for own cards
- ContactCardsState: account-scoped access to the account's own cards,
  petname flow per target user, create/update entry point

amethyst:
- Account.updateContactCardPetName publishes through the extended outbox
  relays (NIP-65 write + private outbox + local + broadcast) and the login
  subscription now downloads the account's own kind:30382s from its relays
- petname renders instead of the display name in usernames, profile header,
  chats and @mentions (observeUserPetName)
- Edit-nickname action + dialog on the profile actions menu

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
2026-07-13 01:26:27 +00:00
Vitor PamplonaandGitHub 82fa652c19 Merge pull request #3543 from vitorpamplona/fix/inbox-nostr-wine-over-auth
fix(relay-auth): stop paid relays billing accounts that don't use them (inbox.nostr.wine over-AUTH)
2026-07-12 21:05:35 -04:00