Groups verified, and stop calling the user a number

Groups now driven end to end on Android against relay.flamenet.io: created
"Testers" with a member, it appears in the list, it SURVIVES a full `pm clear`
and re-sign-in (restored from the relay), it opens, and a message sends into
it. This was one of the original asks and had never been exercised.

Two things it turned up:

🔴 **The relay's 401 also blamed the passphrase.** The AccountsClient fix in
the previous commit was correct and incomplete: `describe()` mapped
`RelayException.Unauthorized` to "That handle or passphrase isn't right" too,
and that is the path that actually fires when a relay token ages out. Same
lie, same consequence — a user changing a password that was never wrong. It
now says the session expired, which is what happened.

⚠️ **You are never in your own buddy list**, so every "find the name among the
buddies" fallback rendered the signed-in user as `user 31`. The group row read
"user 31, fnreviewbeta". `Phase.SignedIn` now carries its own `userId` and both
fallbacks say "You". Verified live: the row reads "You, fnreviewbeta".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-26 07:30:19 -04:00
co-authored by Claude Opus 5
parent cda64d8c2e
commit 5c8060b690
2 changed files with 25 additions and 3 deletions
@@ -364,8 +364,12 @@ fun ColumnScope.GroupsSection(
fontWeight = if (thread in phase.unread) FontWeight.Bold else null,
)
Text(
// "You" first: you are a member of your own group and are
// not in your own buddy list, so this used to read
// "user 31, fnreviewbeta".
group.memberIds.joinToString(", ") { id ->
phase.buddies.firstOrNull { it.userId == id }
if (id == phase.userId) "You"
else phase.buddies.firstOrNull { it.userId == id }
?.let { b -> b.displayName.ifBlank { b.handle } }
?: "user $id"
},
@@ -38,6 +38,15 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
data class SignedIn(
val handle: String,
/**
* This account's own id.
*
* ⚠️ Needed because **you are never in your own buddy list**, so
* every "look the name up among the buddies" fallback rendered you
* as `user 31` — visible on the group row, which listed its members
* as "user 31, fnreviewbeta".
*/
val userId: Long,
val deviceId: String,
val relay: String,
val status: String,
@@ -214,6 +223,7 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
)
stored to Phase.SignedIn(
handle = credential.username,
userId = credential.userId,
deviceId = stored.deviceId,
relay = SiteConfig(credential.relayBaseUrl).host,
status = "Signed in on this device.",
@@ -326,6 +336,7 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
return Phase.SignedIn(
handle = account.username,
userId = account.userId,
deviceId = device.deviceId,
relay = config.host,
status = "Registered with ${preKeys.size} one-time prekeys published.",
@@ -776,7 +787,8 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
getApplication<Application>().getSharedPreferences("calls", android.content.Context.MODE_PRIVATE)
private fun nameFor(phase: Phase.SignedIn, userId: Long): String =
phase.buddies.firstOrNull { it.userId == userId }
if (userId == phase.userId) "You"
else phase.buddies.firstOrNull { it.userId == userId }
?.let { it.displayName.ifBlank { it.handle } } ?: "user $userId"
/**
@@ -1171,7 +1183,13 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
is AccountsClient.Failure.HandleTaken -> "That handle is already taken."
is AccountsClient.Failure.RateLimited -> "Too many attempts. Wait a few minutes."
is AccountsClient.Failure.Transport -> failure.message.orEmpty()
is RelayException.Unauthorized -> "That handle or passphrase isn't right."
// ⚠️ NOT "that passphrase is wrong". The relay rejects a TOKEN, which
// the user never typed and cannot check. Telling them their passphrase
// is wrong when their session merely aged out sends them off to change
// a password that was fine — observed on a send ten hours after a
// successful sign-in.
is RelayException.Unauthorized ->
"Your session expired. Sign out and back in to keep going."
is RelayException.NotFound -> "This server doesn't look like a Flamenet relay."
is RelayException.RateLimited -> "Too many attempts. Wait a few minutes."
is Vault.Corrupt -> "This phone's stored keys could not be read."