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.
Bump collectUntilEose timeout from 3000ms to 8000ms across all suggestion
queries (active now, news, profile fetches) to handle slow connections.
Add relay.wisp.talk and pyramid.fiatjaf.com to ACTIVE_RELAYS so more
users appear in the 'Active right now' onboarding section.
Expands group support beyond open/unrestricted creation:
- Create dialog exposes the 4 NIP-29 config flags (private, closed,
restricted, hidden) as plain-English toggles; the follow-up kind:9002
is only sent when the user actually customizes posture, with a 1.5s
delay after the 9007 so the relay can mark us admin first.
- Group Detail shows per-flag icon + label + description rows plus a
header badge chip (defaults to "Public" when fully open), so every
room's posture is legible at a glance.
- Admin-only invite code section generates kind:9009 events and
surfaces copy-code / copy-invite-link actions; Join dialog parses
`relay'groupId?code=...` and plumbs the code through kind:9021.
- Per-member "Assign role" action sends kind:9000 with free-form role
tags; admins list parses role strings without breaking call sites.
- Join flow waits on the relay's OK response before adding the room
locally — rejections surface as a dialog instead of a broken
chat screen with no messages.
- Joined chat relays now get tier-2 AUTH prompts (prompt-and-remember)
instead of silently discarding challenges; after AUTH completes we
re-fire group REQs and clear stale "auth-required" banners.
- Raw 39000/39001/39002 events logged so relay-side behavior can be
inspected directly from logcat.
- Default group relay switched from groups.0xchat.com to
chat.wisp.talk.
Known: chat.wisp.talk omits non-admin pubkeys from kind:39002 on
closed groups, so the local member list stays sparse until the relay
behavior is fixed. To be addressed server-side.
Reduces false positives against real users that still show up with
the v2.2 model, at the cost of letting through some borderline
spam. Applied to both reply notifications and thread rendering.
Replaces the v0.9 logistic-regression reply-spam classifier with
the v2.2 gradient-boosted tree model (500 trees, 63 leaves). Ships
model.txt + calibration.npz and adds a pure-Kotlin LightGBM tree
walker. Feature extraction is unchanged; tree-walk output matches
LightGBM's C++ predict bit-exactly against the published parity
fixtures.
New default feed type that combines three streams into one sorted list:
- Main author subscription (extended network if cached, follow feed otherwise)
- Top-reactions and top-replies trending notes (today timeframe)
- Notes for all followed hashtags, batched 10 at a time against the search
relay over a 24h window
Supplementary fetches are one-shot per feed-generation and self-close on
EOSE + drain; events flow through EventRepository.addEvent() for dedup and
in-order insertion alongside the main feed. Pagination paginates only the
author subscription; trending and hashtag batches are initial-load only.
Users expect pressing back from the home screen to minimize the app.
The BackHandler was swallowing the press on FEED; disable it there
so Android's default behavior handles it.
ExoPlayer in STATE_ENDED stays at the end position, so calling play()
on the overlay tap had no effect after the video finished once. Seek
back to 0 before playing if the player has ended.
Every upload path (profile picture, compose attachment, DM attachment)
now runs through a shared preprocessing pipeline:
- image/gif -> transcoded to H.264 MP4 via MediaCodec + MediaMuxer
through an EGL input surface (typical 5-20x size reduction)
- other image/* -> decoded with ImageDecoder (handles HEIC on API 28+),
EXIF-rotated when needed, proportionally scaled, JPEG re-encoded
- videos and other types -> passed through unchanged
Profile pictures cap at 400px wide / JPEG quality 75. Compose and DM
uploads cap at 1920px / quality 82. imeta dim and Kind 15 size/dim
tags are recomputed from post-pipeline bytes so clients see accurate
metadata. Gallery-mode video/image mixing check treats GIFs as video
since they become MP4s.
Extends the content parser's extension and MIME sets so HLS streams and
Apple HEIF images route to the existing video/image renderers instead of
falling through to plain links. HLS plays via the media3-exoplayer-hls
module that's already on the classpath; HEIC decodes via Coil3 ->
ImageDecoder on API 28+.
Two onboarding polish fixes:
- Topic selections were racing: createInterestSet launched a coroutine
to publish an empty kind 30015, then a for-loop called followHashtag
for each tag. Each followHashtag read the repo synchronously before
the prior coroutines landed, so each call produced its own event
with just one tag, and the later events with equal created_at did
not overwrite the first one in the repo. Add a bulk followHashtags
that takes a Set and publishes a single event with a proper title.
- First-post bottom bar now matches ComposeScreen: during the send
countdown, an Undo button cancels and a 'Post now (Ns)' button
publishes immediately, instead of a single button that only canceled.
The nonAppRoutes set is used to decide whether the post-process-death
redirect-to-LOADING effect should fire. With ONBOARDING_TOPICS and
ONBOARDING_FIRST_POST missing from the set, that effect treated them
as regular app screens and kicked users from the topic/first-post
screens to LOADING mid-flow.
New flow after profile: People suggestions → Topics → First post → Feed.
- Topic screen fetches a curated 'trending' set and an 'all' set from
feeds.nostrarchives.com (kind 30015) so users can pick hashtags via
autocomplete or tap popular chips. Selections are saved through the
existing InterestRepository / followHashtag path (kind 30015 interest
set named 'Interests').
- First-post screen pre-fills #introductions and reuses the existing
ComposeViewModel.publish flow. The feed subscription kicks off on
screen entry so the feed is ready by the time the user finishes.
- Skip handler on the People screen now calls finishOnboarding with an
empty selection, so skippers still get a kind 3 event that at least
self-follows — their own posts will show up in their feed.
On fresh signup the feed subscription fires before author NIP-65 relay
lists have fully propagated, so outbox routing defaults to pinned relays
that may not carry the followed authors' posts. The existing late-
arrival safety net only covered the opposite case (follows arriving
after initial subscribe). Add a symmetric retry: if the initial subscribe
had a non-empty follow list but the feed is still empty after 5s, resub
once so routing uses the now-known per-author relays.
Opt-in display mode that renders zap and wallet amounts in a fiat currency
(default USD) instead of satoshis. Rates are fetched once at startup from
CoinGecko and cached in SharedPreferences as a fallback.
- FiatPreferences + ExchangeRateRepository for the toggle, currency
selection, and cached rates across 26 fiat currencies
- AmountFormatter centralizes sat→fiat display across feed zap pills,
wallet balance / transactions / invoice confirm + receive screens,
zap dialog, DM bubbles, notifications stats, and lightning invoice cards
- Fiat Mode section lives inside the Interface settings screen
- Fiat Mode forces the bolt (voltage) icon everywhere so the ₿ symbol is
never shown while fiat amounts are displayed
- Receive screen accepts fiat amounts with a decimal key and a dedicated
"Generate invoice" button; converts fiat→sats before creating the invoice
- Zap dialog preset chips keep their sat values internally and relabel as
fiat; "Send Zap" copy flips to "Send Money"
- Wallet balance-unit selector (SATS / BITCOIN / LIGHTNING) is hidden in
Fiat Mode since it only applies to sat rendering
- New npub signups default to Fiat Mode + USD; nsec / npub / signer logins
never touch the preference, so returning users keep their choice
SubcomposeAsyncImage (SubcomposeLayout) crashes when rendered inside
chat bubble columns that use IntrinsicSize.Max for shrink-wrap sizing.
Switch to AsyncImage with loading state overlay instead.
The heightIn constraint used for "show more" truncation propagated
down to video/image players, causing them to resize and shift position
when toggling show more/less. Posts with media now render at full
height; only text-heavy posts get the collapsible behavior.
When enabled in Settings → Safety → Filters, events from pubkeys
outside the user's qualified social graph (self + follows + extended
network) are silently dropped at the EventRepository and
NotificationRepository ingestion layer. This covers notes, reactions,
zaps, and reposts.
Exempt kinds: profiles (0), contact lists (3), relay lists (10002),
DM-related kinds (4, 1059, 13, 14), and DM relay lists (10050).
The filter fails open when the social graph is not computed or stale.
Toggling the filter triggers an immediate feed rebuild.
Ports the nspam v0.9 logistic regression model (~937KB) to pure Kotlin
for on-device classification of likely-bot authors. The model runs
MurmurHash3 feature hashing over character and word n-grams plus
structural note features — no ML runtime needed.
Spam filtering is applied only to replies from non-followed authors in
threads and notifications. Suspected spam replies appear at the bottom
of threads behind a toggle. Users can mark authors as "not spam" to
permanently safelist them. The classifier improves over time by
rescoring authors when more of their notes become available.
Adds a new Filters tab to Safety settings with toggles for the spam
filter (on by default) and Web of Trust filter (off, gated by social
graph — implementation in follow-up PR).