- Replace `remember { side effect }` with lazy `entry.ensure(context)` in
the row's onClick; the system per-channel page only needs the channel
to exist at open time, not before, and the ensure call is idempotent.
- Replace the `refreshKey++` invalidation trick with a real
`Map<channelId, ChannelStatus>` state holder updated inside
`LifecycleResumeEffect`; downstream reads are direct map lookups.
- Replace `Triple`-with-destructuring in `ChannelStatusBadge` with three
direct `when` branches calling `StatusChip` — no tuples, no
temporaries.
- Drop the unused `notification_settings_open_system` string.
Verify video/x-m4v maps to video/mp4 (the failing case), normalization
is case-insensitive, and supported image/video/audio types pass through
unchanged. Required making normalizeMimeTypeForMediaStore internal.
Reorganize Notification settings into three sections that reflect what
each control actually does:
- Delivery: how notifications reach the device — push provider (fdroid)
and the always-on relay service live together here.
- In-app display: how the notifications screen renders incoming activity —
currently the Split-by-Follows toggle.
- Categories: one row per user-facing Android NotificationChannel (DMs,
Mentions, Replies, Reactions, Zaps, Chess, Scheduled posts, Calls),
showing the current importance (On / Silent / Off) and opening the
system per-channel settings page on tap. Foreground-service channels
are intentionally omitted — disabling them breaks the service contract.
The screen ensures every listed channel exists on first open and
re-reads importance via LifecycleResumeEffect so the badge reflects
changes made in system settings.
API surface bumped for the channel registry:
- CallNotifier.CALL_CHANNEL_ID is now public.
- ScheduledPostNotifier.ensureChannel is now public.
Replaces the previous narrow try/catch fix. The earlier patch silently
swallowed SignerExceptions in NewMediaModel / EditPostViewModel — no
toast, no error dialog — so the user would tap Post, see the dialog
close, and never learn that the signer prompt timed out or was
rejected. It also bypassed the project's standard signer-error
pipeline.
This version routes the sign+publish phase through
AccountViewModel.launchSigner, which is the same path every other
signing entry point uses:
- ReadOnly / SignerNotFound / UnauthorizedDecryption / IllegalState →
toastManager surfaces a localized alert.
- TimedOut / ManuallyUnauthorized / etc. → logged silently (no crash),
matching the rest of the app's behavior when a user dismisses the
external signer.
NewMediaModel.upload now takes an AccountViewModel parameter; the three
inner viewModelScope.launch(Dispatchers.IO) blocks become
accountViewModel.launchSigner { ... }. The joinAll wait is preserved
(launchSigner returns Job).
EditPostViewModel.uploadUnsafe routes its single launch the same way
and wraps the body in try/finally so mediaUploadTracker.finishUpload()
still runs if a signer throws mid-flow.
launchSigner is changed from Unit to Job (= viewModelScope.launch ...)
so callers can join — non-breaking for the ~190 existing callsites that
ignore the return value.
- Promote the duplicate `SwitchTile` from SecurityFiltersScreen and
NotificationSettingsScreen into a shared `SettingsSwitchTile` in
SettingsSectionCard so all settings switches share one implementation.
- Render BatteryOptimizationBanner outside the in-app section card
instead of nesting a Card inside a Card and splitting the section's
divider away from the row it explains.
- Refresh the battery-optimization exemption on LifecycleResumeEffect
so the banner disappears after the user returns from the system
settings page; drop the racy post-button re-read.
- Demote `HasPushNotificationProvider` to a plain `hasPushNotificationProvider`
since it returns a per-flavor constant and reads no Compose state.
#2946 fixed the ClassCastException with a bespoke AtomicReference<Map>
+ CAS copy-on-write helper. Quartz already has a concurrent-map
abstraction for exactly this purpose — LargeCache — with platform-tuned
actuals (ConcurrentSkipListMap on jvmAndroid, CacheMap on Apple, custom
on Linux). Swap to it.
Removes the bespoke putAuthStatus/removeAuthStatus helpers, the
ExperimentalAtomicApi opt-in, and the AtomicReference imports.
The RelayAuthenticatorConcurrencyTest from #2946 still passes against
the new implementation.
Reuses the NamecoinResolutionRow composable already shipping for the
onchain-zap recipient field, promoting it from
ui/screen/loggedIn/wallet/ to a generic ui/components/namecoin/
location so it can be mounted anywhere a .bit-shaped search input may
race the local-cache prefix search.
In the global search bar, typing a bare ".bit" host (e.g.
"testls.bit") used to surface a cached sibling profile like
"m@testls.bit" first (LocalCache.findUsersStartingWith hits the
prefix) and only several seconds later be corrected by the slower
on-chain ElectrumX resolution from
SearchBarViewModel.directNip05Resolver. No in-flight indicator and no
feedback on hard failures (timeout, malformed record, etc.).
Changes:
- git-rename NamecoinResolutionRow.kt and its test from
ui/screen/loggedIn/wallet/ to ui/components/namecoin/, updating
the package declaration only.
- Add an optional `modifier: Modifier = Modifier` parameter to the
composable (standard Compose convention) and wrap the spinner /
result / error rows in a Column taking the caller-provided
modifier. No visual change in OnchainZapSendDialog.
- Update OnchainZapSendDialog import to the new package location.
- Mount NamecoinResolutionRow in SearchScreen.SearchBar between
SearchTextField and SearchFilterRow, with horizontal padding to
match the rest of the bar. onUserResolved navigates to the user
and clears the field, matching the bech32 auto-resolve path in
SearchBarViewModel.directRouteResolver.
State is held in the shared
commons.NamecoinResolveState (no new state class introduced) and
diagnostic wording comes from the existing mapOutcomeToResolveState
helper, so every Namecoin surface continues to produce the same
message for the same outcome.
The local-cache suggestion dropdown can momentarily show a stale match
when the user types a .bit identifier. For example, after resolving
"m@testls.bit" earlier in the session, typing the bare host
"testls.bit" causes findUsersStartingWith() to return the cached
m@... profile first; the correct _@testls.bit profile only appears a
few seconds later when ElectrumX resolution finishes. There is no
visual hint that a Namecoin lookup is in flight, and on hard failures
(timeout, malformed record, no nostr field, etc.) the user gets no
feedback at all.
This adds a dedicated NamecoinResolutionRow composable mounted between
the recipient field and the local-cache dropdown:
- "Resolving <name> on Namecoin…" spinner while the on-chain
lookup is in flight (after a 300 ms debounce that matches the
dropdown's own debounce).
- On success, a tappable row showing the resolved profile with a
distinct "Namecoin" badge (MaterialSymbols.Link), so the user
can pick the on-chain-verified profile unambiguously.
- On failure, a single explanatory error line covering all
NamecoinResolveOutcome variants the resolver already produces:
NameNotFound, NoNostrField, MalformedRecord (with the underlying
parser error verbatim), ServersUnreachable, InvalidIdentifier and
Timeout.
State is held in the shared NamecoinResolveState sealed class already
used by NamecoinNameService and the desktop SearchScreen — no new
state model is introduced. A small mapOutcomeToResolveState() helper
mirrors the same wording desktop ships, so all Namecoin surfaces
produce the same diagnostic string for the same outcome.
The row owns its own LaunchedEffect keyed on the typed query, so it
cancels in-flight lookups whenever the user keeps typing, and it
unmounts cleanly once a recipient is selected. It uses the existing
Amethyst.instance.namecoinResolver instance, so no DI plumbing or new
network calls beyond what was already wired up for .bit resolution.
The dropdown is left untouched: local-cache results are still valid
hits (just not necessarily the *intended* on-chain identity), so they
remain available below the new row.
Pure helpers (looksLikeNamecoinIdentifier + the outcome-to-state
mapper) are covered by unit tests in NamecoinResolutionRowTest.
The Overview tab is a plain vertical-scroll Column inside DisappearingScaffold,
which lays content under the top app bar + tab row and bottom nav. The Issues
and Patches tabs go through RefresheableFeedView and already consume
LocalDisappearingScaffoldPadding internally, but the Overview tab did not, so
its top and bottom were hidden behind the surrounding UI.
OkHttp dispatches WebSocket callbacks on one thread per relay socket,
so RelayAuthenticator's plain LinkedHashMap was mutated concurrently
from many threads during connection storms. When a bucket crossed
HashMap's TREEIFY_THRESHOLD the racing treeify corrupted internal
state and threw ClassCastException: LinkedHashMap$Entry cannot be
cast to HashMap$TreeNode from onDisconnected.
MacOsVlcDiscoverer.setPluginPath() called LibC.INSTANCE.setenv() via
JNA, but macOS 13+ uses versioned symbols (setenv$3b99ba0d) that JNA
can't resolve, breaking all video playback without system VLC.
Changes:
- Replace LibC.setenv with direct JNA Function.getFunction("c","setenv")
call that bypasses the problematic interface binding
- Store discoveredPluginPath for --plugin-path factory arg fallback
- Add -Dvlc.plugin.path JVM property as ultimate fallback
- Delete stale VLC plugin cache on macOS before factory creation
- Pass --plugin-path to audio factory too when env var fails
- Add jdk.unsupported module to jlink (VLCJ ByteBufferFactory needs
sun.misc.Unsafe for video frame buffer allocation)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Currently the only profile fields with an explicit copy affordance are
npub and nprofile (small ContentCopy IconButtons next to the keys).
The other identifier-shaped fields rendered by DrawAdditionalInfo and
DisplayLNAddress (NIP-05 Nostr Address, Website URL, lud16 Lightning
Address, NIP-39 external identities, NIP-A3 payment targets) all have
a tap action that *uses* the value (opens the URL, expands the zap
sheet, etc.) but no way to actually copy the value itself short of
selecting the underlying "View source" or opening the link and
copying from the browser. Closes that gap with the platform-native
long-press gesture.
Adds a small reusable LongPressCopyText composable in
ui/components/util/ that:
- renders plain Text styled in primary colour (visual match for the
previous ClickableTextPrimary),
- wires onClick through Modifier.combinedClickable (so existing tap
behaviour is preserved),
- adds onLongClick to copy the supplied raw value to the system
Clipboard and surface a Toast.makeText "Copied to clipboard"
confirmation,
- announces the long-press action to TalkBack via onLongClickLabel.
The composable deliberately uses a plain Text + outer
combinedClickable rather than an AnnotatedString with an inline
LinkAnnotation.Clickable: an annotation-level click can't see a
parent combinedClickable's long-press, because the parent's tap area
sits above the annotation's hit-test region and would consume the
tap before the annotation could fire it. Plain Text + outer modifier
keeps both gestures behaving correctly.
Wired into:
- DisplayNip05ProfileStatus (NIP-05 Nostr Address) — tap opens the
domain in the browser, long-press copies the full
user@domain value.
- The website row in DrawAdditionalInfo — tap opens the URL,
long-press copies the *raw* website value as stored in the
profile (the displayed form is stripped of the scheme + trailing
slash for readability; the copy preserves the original).
- The external-identity rows (Twitter, Mastodon, Telegram, GitHub)
— tap opens the proof URL, long-press copies the identity handle.
- DisplayLNAddress lud16 — tap toggles the zap sheet (unchanged),
long-press copies the lud16 address.
- PaymentTargetRow authority — tap opens the payto: URI, long-press
copies the authority.
The existing inline ContentCopy IconButtons on npub and nprofile are
left in place; the long-press gesture is purely additive for the
other rows.
Two notable side-effects from the ClickableTextPrimary -> Text +
combinedClickable rewrite:
- Touch ripple feedback now appears on tap (combinedClickable's
default indication). Previously the inline LinkAnnotation rendered
no ripple. This is consistent with how other clickable rows in the
app behave and provides better tap feedback.
- The NIP-05 row no longer renders the click region as a styled
inline span; the entire text behaves as one tap+long-press target.
Visually identical for short addresses; for any address long
enough to wrap, the click region now matches the visible bounds
rather than the per-glyph bounds.
Adds a new copied_to_clipboard string. Picks up the existing
copy_to_clipboard string for the TalkBack long-press hint.
The Send-onchain-zap Recipient field already supported typing a NIP-05
or bare .bit name and tapping the resolved suggestion. If a user typed
a fully-qualified name (e.g. "m@testls.bit" or "testls.bit") and hit
Send without first tapping the suggestion, the eager resolver only ran
decodePublicKeyAsHexOrNull(), which returns null for non-npub inputs,
so the button stayed disabled with no inline feedback.
Mirror the dropdown's existing NIP-05 / .bit resolution into the dialog
state via UserSuggestionState.nip05ResolutionFlow, and use the resolved
User's pubkey as one more fallback in resolvedRecipient. The flow is
already debounced (300ms) and shared with the suggestion list, so this
adds zero extra network traffic and zero new code paths -- just enables
Send 300ms after a valid name resolves.
Behaviour:
- Typing "m@testls.bit" + waiting 300ms -> Send enables.
- Typing "testls.bit" + waiting 300ms -> Send enables (same path the
dropdown uses for bare .bit names; resolves as _@testls.bit).
- Typing an npub or hex pubkey -> unchanged (already worked).
- Typing an unrecognised string -> Send stays disabled, unchanged.
- Tapping the suggestion before Send -> unchanged (selectedUser wins).
No behaviour change anywhere outside the onchain zap dialog.
When the initial chatroom list picked a ChannelMetadataEvent or
ChannelCreateEvent as the representative note for a public channel
(because no ChannelMessageEvent had been seen yet), the arrival of a
later ChannelMessageEvent caused updateListWith to append a second
entry for the same channel — its match check only recognized old
notes whose event was a ChannelMessageEvent. Two notes for the same
channelId then produced the same PublicChannelLazyKey in the LazyColumn
and crashed with IllegalArgumentException: Key was already used.
Extract the channel id from any of the three public-chat event types
when matching the existing entry so the new message replaces the
placeholder instead of duplicating it.
Android's MediaStore rejects video/x-m4v (returned by MimeTypeMap for .m4v
URLs) with IllegalArgumentException. M4V is an MP4 container variant, so
map it to video/mp4 before the ContentResolver.insert call.
When a post is filtered out by the max-hashtag-per-post setting, the
"Show Anyway" card previously displayed the generic "Post was muted or
reported by" text with no avatars below it, which was confusing.
Plumb a hasExcessiveHashtags flag and the configured limit through
NoteComposeReportState and BlockReportChecker so HiddenNote can render a
hashtag-specific message ("This post has more than N hashtags"). When
both reports and the hashtag limit apply, show both reasons.
Primal-style clients emit "dim 317.0x498.0" in NIP-92 imeta tags. DimensionTag.parse
called Int.parseInt on each component, threw NumberFormatException, and returned
null. With dim==null and no cached aspect ratio, GifVideoView / UrlImageView built
the container without an aspectRatio modifier, so the inline image collapsed to
zero height and the post body looked empty until Coil delivered the bitmap.
Parse each component as Double then truncate to Int. Adds DimensionTagTest covering
integer, float, truncation, 0x0 and malformed inputs.
https://claude.ai/code/session_01W1crao6Hwip8k5ByoLxVrc