Finish the swallowed-failure sweep, and stop stories being wiped
Completing the four sites the previous commit named rather than leaving them. 🔴 **Stories were CLOBBERED by a failed fetch, not merely mis-described.** Both `refreshStories` and the poll loop did `runCatching { current.stories() }.getOrDefault(emptyList())` and then wrote that straight into state — so one hiccup emptied the strip the user was looking at, and the poll repeated it on every tick. A poll that cannot reach the relay has learned nothing, and nothing is not "no stories". Both now keep what is on screen and skip that tick. ⚠️ Groups, both platforms. Android rendered NOTHING for an empty list — no guidance and no way to tell "you have no groups" from "the list did not load". It now says which, matching the contacts card. iOS already had copy and it had the same flaw: `refreshGroups` uses `try?`, so a failure left the list empty and the section confidently said "No groups yet. Start one with your buddies." `groupsLoaded` gates that on both sides. That shape is worth naming, because it produced three separate findings today: a swallowed failure, written into state, rendered as a confident empty state. The group DECODE bug hid inside it for months — groups were invisible on iOS and working on Android, and nothing anywhere said a word. Android 194 tests green, iOS 46 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
62b4798881
commit
f6f2fbd283
@@ -352,6 +352,21 @@ fun ColumnScope.GroupsSection(
|
||||
) { Text("Create group") }
|
||||
}
|
||||
|
||||
// ⚠️ An empty list rendered NOTHING here — no guidance, and no way to tell
|
||||
// "you have no groups" from "the list did not load". Say which.
|
||||
if (phase.groups.isEmpty()) {
|
||||
Text(
|
||||
if (phase.groupsLoaded) {
|
||||
"No groups yet. Start one with your buddies."
|
||||
} else {
|
||||
"Couldn't load your groups. Check the connection and sign in again."
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Palette.SecondaryText,
|
||||
modifier = Modifier.testTag("groups-empty"),
|
||||
)
|
||||
}
|
||||
|
||||
phase.groups.forEach { group ->
|
||||
val thread = io.flamenet.e2e.Vault.threadForGroup(group.id)
|
||||
Row(
|
||||
|
||||
@@ -65,6 +65,9 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
* groups invisible on iOS for far longer.
|
||||
*/
|
||||
val buddiesLoaded: Boolean = false,
|
||||
|
||||
/** Same question for the group list. See [buddiesLoaded]. */
|
||||
val groupsLoaded: Boolean = false,
|
||||
val groups: List<io.flamenet.client.RelayClient.Group> = emptyList(),
|
||||
val blocked: Set<Long> = emptySet(),
|
||||
val notifications: List<MessengerApi.Notification> = emptyList(),
|
||||
@@ -236,6 +239,7 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
credential.relayBaseUrl, credential.username, credential.appPassword),
|
||||
)
|
||||
val fetchedBuddies = runCatching { session!!.api.buddies() }
|
||||
val fetchedGroups = runCatching { session!!.relay.listGroups() }
|
||||
stored to Phase.SignedIn(
|
||||
handle = credential.username,
|
||||
userId = credential.userId,
|
||||
@@ -244,7 +248,8 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
status = "Signed in on this device.",
|
||||
buddies = fetchedBuddies.getOrDefault(emptyList()),
|
||||
buddiesLoaded = fetchedBuddies.isSuccess,
|
||||
groups = runCatching { session!!.relay.listGroups() }.getOrDefault(emptyList()),
|
||||
groups = fetchedGroups.getOrDefault(emptyList()),
|
||||
groupsLoaded = fetchedGroups.isSuccess,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -351,6 +356,7 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
)
|
||||
|
||||
val fetchedBuddies = runCatching { session!!.api.buddies() }
|
||||
val fetchedGroups = runCatching { session!!.relay.listGroups() }
|
||||
return Phase.SignedIn(
|
||||
handle = account.username,
|
||||
userId = account.userId,
|
||||
@@ -359,7 +365,8 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
status = "Registered with ${preKeys.size} one-time prekeys published.",
|
||||
buddies = fetchedBuddies.getOrDefault(emptyList()),
|
||||
buddiesLoaded = fetchedBuddies.isSuccess,
|
||||
groups = runCatching { session!!.relay.listGroups() }.getOrDefault(emptyList()),
|
||||
groups = fetchedGroups.getOrDefault(emptyList()),
|
||||
groupsLoaded = fetchedGroups.isSuccess,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -904,7 +911,12 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
|
||||
fun refreshStories() = viewModelScope.launch {
|
||||
val current = session ?: return@launch
|
||||
val live = withContext(Dispatchers.IO) { runCatching { current.stories() }.getOrDefault(emptyList()) }
|
||||
// 🔴 **A failed fetch must not empty the strip.** Defaulting to an
|
||||
// empty list here did not merely look wrong, it CLOBBERED whatever was
|
||||
// on screen — so one hiccup wiped the stories the user was looking at,
|
||||
// and the poll below repeated that on every tick.
|
||||
val fetched = withContext(Dispatchers.IO) { runCatching { current.stories() } }
|
||||
val live = fetched.getOrNull() ?: return@launch
|
||||
update { it.copy(stories = live) }
|
||||
}
|
||||
|
||||
@@ -1166,12 +1178,19 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
// Read back every tick rather than only when a thread was
|
||||
// touched: a story files itself outside the thread map, so
|
||||
// `threads` is empty for a poll that brought one in.
|
||||
val live = withContext(Dispatchers.IO) {
|
||||
runCatching { current.stories() }.getOrDefault(emptyList())
|
||||
// Keep what is on screen when the fetch fails; see
|
||||
// refreshStories. A poll that cannot reach the relay has
|
||||
// learned nothing, and "nothing" is not "no stories".
|
||||
val polled = withContext(Dispatchers.IO) {
|
||||
runCatching { current.stories() }
|
||||
}
|
||||
update { state ->
|
||||
if (state.stories.map { it.id } == live.map { it.id }) state
|
||||
else state.copy(stories = live)
|
||||
// Skip THIS tick's update on failure rather than leaving
|
||||
// the loop — the next poll may well succeed.
|
||||
polled.getOrNull()?.let { live ->
|
||||
update { state ->
|
||||
if (state.stories.map { it.id } == live.map { it.id }) state
|
||||
else state.copy(stories = live)
|
||||
}
|
||||
}
|
||||
if (threads.isNotEmpty()) {
|
||||
update { state ->
|
||||
|
||||
Reference in New Issue
Block a user