Add wispSwitchColors() helper in Theme.kt that sets unchecked thumb,
track, and border colors so off-state toggles are distinguishable from
the background across all color schemes. Applied to all Switch instances
app-wide (10 files). Consolidates duplicate inline colors from
NotificationsScreen into the shared helper.
Adds configurable undo countdown for publishing:
- "Undo countdown" toggle (default on)
- Duration picker (5/10/15/20/30s, default 10)
- "Include replies" toggle (default off — replies send immediately)
ComposeViewModel reads settings to skip the countdown when disabled
or for replies unless opted in. Countdown duration is now configurable
instead of hardcoded to 10s.
Dedicated to Séimí Mac Síomón and Corndalorian.
Ports barrydeen/wisp-ios#34 to Android.
Adds a Scan tab alongside Nostr/Lightning in the drawer's QR sheet that
opens the camera and decodes Nostr entities (npub, note, nprofile, nevent,
naddr) — with or without the nostr: prefix — and navigates directly to
the matching profile, thread, or article.
Extracts the camera scanner from WalletScreen into a reusable QrScanner
component, and centralizes NostrUriData→route mapping as toRoute() in
Navigation.kt so the deep-link handler and scanner share it.
Add quoteDepth parameter to RichContent, QuotedNote, PostCard, and
GalleryCard. At depth >= 1 (already inside a quote), force compact
preview rendering (avatar + name + snippet) instead of full PostCard
with action bar. Prevents recursive nesting from crushing layouts.
Show coin stack icon in fiat mode for notifications (filter header,
per-zap icon, DM zap, summary stat), reaction details zap row, and
embedded invoices. Add maxLines=1 to all ActionBar count/amount text
to prevent vertical wrapping in tight layouts.
Replace the bolt/bitcoin icon with a coin stack icon on note zap
displays and the action bar zap button when fiat mode is active.
Also swap the drawer wallet menu item to the wallet icon in fiat mode.
Fixes corrupted Box import in WispDrawerContent.
Disable legacy jniLibs packaging so native libraries are stored
uncompressed and page-aligned in the APK. Bump secp256k1-kmp to 0.19.0
and kmp-tor-resource to 408.16.4 since the prior versions shipped
ELF binaries with 4 KB LOAD alignment. All 13 .so files in the release
APK now report 2**14 alignment.
Replace items(...) + notifications.indexOf(item) with itemsIndexed(...)
in NotificationsScreen. The indexOf call ran during composition for
every visible item, causing a linear scan per item and quadratic total
work as the list grew. Reported as a UI freeze when tapping the
notifications tab on slower devices (GrapheneOS).
Quoted notes nested inside another note were using the default unicode
emoji fallback instead of the user's sorted-by-frequency picker with
their custom emojis. Plumb resolvedEmojis, unicodeEmojis, and the
onOpenEmojiLibrary callback through NoteActions so nested PostCard /
GalleryCard renders in RichContent.QuotedNote pick up the same emoji
state as their parent.
Replaces the basic inline audio widget with a global mini-player that persists
across feed scrolling and tab navigation. Playback continues with system
notification / lock-screen transport controls via MediaSessionService.
The dock has two states: a collapsed row showing the author's avatar and name
with rewind-15 / play-pause / forward-15; swipe up to expand for a scrub slider,
speed cycling, and close. Single ExoPlayer is owned by AudioPlayerController
and shared with the inline tap-to-play widget, which now reflects global
playback state.
Two repos called clear() during resetForAccountSwitch() were doing
`prefs.edit().clear().apply()` on their SharedPreferences files while
those files were still pointing at the outgoing account:
- CustomEmojiRepository wipes its own per-pubkey file, erasing the
"most used" emoji frequency map. No relay mirror, so the data was
lost for good.
- BlossomRepository shares `wisp_prefs_{pubkey}` with KeyRepository, so
its full-file wipe also took out local_relay, relays, dm_relays, etc.
Most of those keys came back via replaceable-event refetches, but
local_relay has no network source and stayed null.
Drop the disk wipe from both clear() methods. The subsequent
reload(newPubkey) already repoints prefs to the incoming account's file.
Mentions used to be stored in the text field as raw nostr:nprofile URIs
rendered as @Name via an OutputTransformation. The visual/underlying
offset mismatch made the cursor jump, the IME desync on rapid typing,
forced a trailing space after every mention (so '@name's' was impossible),
and turned a single backspace into a silent delete of the whole mention.
Store '@Name' directly in the text field and track mention ranges
out-of-band in the ViewModel; splice nostr:nprofile URIs back in at
publish time. Editing is now plain text — identity offset mapping, no
forced space, and breaking a mention drops only that mention, not the
surrounding text.
Remove the preConnectEphemeral optimization from the reconnect path. It
created a race where the ephemeral was mid-connect when subscribeTrendingFeed
fired its REQ, leaving the subscription stuck on Connecting. The direct
metric-switch path (no preConnect) worked fine, so match it.
Relays sometimes keep serving hashtag sets (and other addressable
events) after the user publishes a NIP-09 kind 5 deletion, so sets
reappeared on app reload — especially after a fresh install or on a
second device where the local DeletedEventsRepository was empty.
Self-data subscription now also requests our own kind 5 events, and
EventRouter routes them through EventRepository (marking deletion
coords/ids) and sweeps any matching interest sets out of
InterestRepository so a late-arriving deletion still wins.
When publishing a kind-1 note (root or reply) that mentions pubkeys via
nostr: references, also deliver the event to each mentioned pubkey's
read (inbox) relays per NIP-65. Inbox URLs are deduplicated across
targets and against our own write relays, so shared inboxes only
receive the event once.
Group joins and admin actions (create group / invite / edit metadata / put user /
remove user / leave / delete) previously fired their events as soon as the signer
returned, racing the relay's NIP-42 AUTH challenge. On an auth-gated relay the
unauthenticated event hits first and the relay either responds "auth-required:"
or silently downgrades the request — a 9021 with a valid invite code returns
"restricted: group is closed, you need an invite code", a 9007 can fail silently,
a 9009 then fires against a ghost group and gets "blocked: group doesn't exist",
and the admin never sees any of it because the publishes were fire-and-forget.
attemptJoin now waits up to 5s for authCompleted before sending the 9021, and
still retries once on an auth-required rejection as a safety net. A new
publishAdminEvent helper wraps the admin kinds with the same auth-wait, awaits
the relay's OK, and emits an AdminError on rejection or timeout. createGroup
additionally rolls back its local placeholder if the relay rejects 9007 so we
don't keep trying to issue invites for a group the relay doesn't know about.
A new adminErrors SharedFlow on GroupListViewModel is collected by DmListScreen
and rendered as an AlertDialog showing the step (e.g. "createGroup/9007") and
the relay's message, so silent admin failures become visible.
The social graph (ExtendedNetworkRepository + SocialGraphDb) was wiped
on every account switch because clear() deleted both the SharedPreferences
cache and SQLite tables, and SocialGraphDb used a single shared DB file.
Now SocialGraphDb uses per-account files with a one-time legacy migration,
and clear() no longer touches persistent state (only in-memory). Notification
events from account A were leaking into account B because the ObjectBox
seeding coroutine fired after the switch and NotificationRepository had no
reload(). Adds reload() to NotificationRepository/SafetyPreferences
with a stale-pubkey rejection guard and proper job cancellation.