A "like" on a group message was built as a plain NIP-25 reaction — `e`
(message) + `p` (author) + `k` — with no `h` tag. The recipient's only
notification query that reaches the group's host relay,
filterGroupNotificationsToPubkey, is scoped `#p`=them AND `#h`=their
groups (kind 7 is already in GroupNotificationKinds), so a like with no
`h` tag is never matched there. It would only surface if NIP-65 routing
happened to drop it on one of the recipient's inbox relays — never for a
host-relay-only group — so likes on group messages effectively never
notified.
Copy the target's `h` tag onto public reactions to group-scoped events,
mirroring how kind-9 replies carry it. ReactionEvent.build gains an
`initializer` (the API GroupScope's KDoc already documented); ReactionAction
applies the group `h` tag for both the tracked and fire-and-forget paths.
The like now lands on the host relay in-group and the existing kind-7
`#p`+`#h` query picks it up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxpT7J4xt37EF5yJDw1htK
A reply to my message inside a joined NIP-29 relay group is a kind-9
ChatEvent that p-tags me (see ChannelNewMessageViewModel). It is already
fetched at startup by filterGroupNotificationsToPubkey (scoped `#p`=me +
`#h`=my groups on the group's host relay), but NotificationFeedFilter's
`acceptableEvent` checks `kind in NOTIFICATION_KINDS` before the p-tag
gate, and kind 9 (ChatEvent) was missing from that set — so those replies
were dropped before ever reaching the p-tag check and never surfaced.
Add ChatEvent.KIND to NOTIFICATION_KINDS. The existing gates handle the
rest: my own messages are excluded (author != me), the reply p-tags me
(isTaggedUser), and its `q`-tag to my message makes it pass
tagsAnEventByUser in Selected mode too. Pin the behaviour with a tripwire
test in NotificationKindsContractTest.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VxpT7J4xt37EF5yJDw1htK
The relay rows on the Find Groups (browse) screen now use the 55dp
LargeRelayIconModifier, matching the avatar size of the Messages list,
instead of the 13dp default.
When a relay's NIP-11 document resolves with a supported_nips list that
lacks "29" and no self key, the row shows a 'May not support groups
(NIP-29)' warning, and the relay's (empty) group directory screen says
the relay doesn't advertise NIP-29 instead of the generic empty text.
The check lives in looksLikeNonNip29Relay next to the other NIP-29
NIP-11 helpers; a null supported_nips (doc still loading or fetch
failed) never warns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsPtTBsJ3sJkjodN1Knbx6
In the Messages list, a NIP-29 group whose kind-39000 metadata has no
(or a blank) picture now shows its host relay's NIP-11 icon instead of
jumping straight to the robohash fallback. loadRelayInfo fetches the
NIP-11 document on demand if it isn't cached yet, and the robohash
still kicks in when the relay has no icon either.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RsPtTBsJ3sJkjodN1Knbx6
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
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
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
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
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
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
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
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
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
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
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
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
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
- 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
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
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
- 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
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
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
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
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
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
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
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
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
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
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