Let failed messages be discarded
This commit is contained in:
1 parent
3d1b1e304d
commit
33b130b6bb
4 files changed
+55
-1
No files matched your search
@@ -454,7 +454,9 @@ response are stored per server and session on the phone, so leaving and reopenin
|
||||
eat the only copy. Once the server accepts the request, the bubble remains in memory until the
|
||||
provider event but the phone stops storing it: ownership has crossed to the server, whose transcript
|
||||
and driver state survive the screen. Accepted queued messages remain the server transcript's fact
|
||||
and are replayed from it on every device.
|
||||
and are replayed from it on every device. A failed local bubble can be discarded after an explicit
|
||||
confirmation that this removes the phone's only copy; otherwise the persistence that protects it
|
||||
would also leave it on screen forever with no way out.
|
||||
|
||||
Reconciliation uses the first local message with the same text and attachments because the current
|
||||
message route has no caller-supplied id. Identical sends are therefore consumed in wire order. A
|
||||
|
||||
@@ -65,6 +65,11 @@ internal fun markPendingAccepted(queued: List<QueuedMessage>, id: String): List<
|
||||
if (message.local && message.id == id) message.copy(serverAccepted = true) else message
|
||||
}
|
||||
|
||||
internal fun discardPendingMessage(
|
||||
queued: List<QueuedMessage>,
|
||||
id: String,
|
||||
): List<QueuedMessage> = queued.filterNot { it.local && it.id == id }
|
||||
|
||||
/** Restores sends for which this phone has not yet seen a durable server event. */
|
||||
internal fun loadPendingMessages(context: Context, key: String): List<QueuedMessage> {
|
||||
val encoded =
|
||||
|
||||
@@ -373,6 +373,7 @@ fun SessionScreen(
|
||||
val pendingKey =
|
||||
remember(settings, address) { "${settings.host}:${settings.port}/${address.cachePath}" }
|
||||
var queued by remember(pendingKey) { mutableStateOf(loadPendingMessages(context, pendingKey)) }
|
||||
var discardingMessage by remember(pendingKey) { mutableStateOf<QueuedMessage?>(null) }
|
||||
|
||||
/**
|
||||
* Changes the visible outbox and keeps only its not-yet-durable part across a screen reopen.
|
||||
@@ -1569,6 +1570,12 @@ fun SessionScreen(
|
||||
onTakeBack =
|
||||
if (waiting.local) null
|
||||
else ({ takeBack(waiting.id) }),
|
||||
onDiscard =
|
||||
if (waiting.local && waiting.refusal != null) {
|
||||
{ discardingMessage = waiting }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2084,6 +2091,29 @@ fun SessionScreen(
|
||||
},
|
||||
)
|
||||
}
|
||||
discardingMessage?.let { message ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { discardingMessage = null },
|
||||
title = { Text("Discard unsent message?") },
|
||||
text = {
|
||||
Text(
|
||||
"The server did not confirm this message. Discarding it removes this phone's " +
|
||||
"only copy and cannot be undone."
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
replaceQueued(discardPendingMessage(queued, message.id))
|
||||
discardingMessage = null
|
||||
}
|
||||
) {
|
||||
Text("Discard", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { discardingMessage = null }) { Text("Keep") } },
|
||||
)
|
||||
}
|
||||
if (settingsOpen) {
|
||||
// Measured when the dialog opens rather than kept up to date: what the reader is being told
|
||||
// is what pressing the button now would discard, and null until the walk of the directory
|
||||
@@ -2202,6 +2232,7 @@ private fun UserChunkRow(
|
||||
* before the session reads it. A local one is not, because its request may still be in flight.
|
||||
* [refusal] is either that take-back refusal or the send's network failure. It is drawn here rather
|
||||
* than with the screen's other errors because this is the message the failure belongs to.
|
||||
* [onDiscard] is confirmed by the screen because a failed local bubble is the phone's only copy.
|
||||
*
|
||||
* A settled message longer than [USER_SPLIT_CHARS] is drawn as [UserChunkRow] slices instead -- one
|
||||
* `Text` holding a pasted log is a hundred-thousand-pixel layout in the frame the row scrolls into.
|
||||
@@ -2216,6 +2247,7 @@ private fun UserBubble(
|
||||
pending: Boolean = false,
|
||||
refusal: String? = null,
|
||||
onTakeBack: (() -> Unit)? = null,
|
||||
onDiscard: (() -> Unit)? = null,
|
||||
) {
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
Card(
|
||||
@@ -2268,6 +2300,11 @@ private fun UserBubble(
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
onDiscard?.let {
|
||||
TextButton(onClick = it, modifier = Modifier.align(Alignment.End)) {
|
||||
Text("Discard", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,16 @@ class PendingMessagesTest {
|
||||
assertTrue(queued.single().serverAccepted)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun discarding_a_failed_send_removes_only_that_local_copy() {
|
||||
val server = QueuedMessage("server-1", "already accepted", emptyList())
|
||||
val queued = listOf(local(), local("keep this one").copy(id = "local-2"), server)
|
||||
|
||||
val discarded = discardPendingMessage(queued, "local-1")
|
||||
|
||||
assertEquals(listOf("local-2", "server-1"), discarded.map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun identical_messages_are_reconciled_one_at_a_time() {
|
||||
val queued = listOf(local(), local().copy(id = "local-2"))
|
||||
|
||||
Reference in new issue
Block a user