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
|
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
|
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 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
|
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
|
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
|
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. */
|
/** Restores sends for which this phone has not yet seen a durable server event. */
|
||||||
internal fun loadPendingMessages(context: Context, key: String): List<QueuedMessage> {
|
internal fun loadPendingMessages(context: Context, key: String): List<QueuedMessage> {
|
||||||
val encoded =
|
val encoded =
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ fun SessionScreen(
|
|||||||
val pendingKey =
|
val pendingKey =
|
||||||
remember(settings, address) { "${settings.host}:${settings.port}/${address.cachePath}" }
|
remember(settings, address) { "${settings.host}:${settings.port}/${address.cachePath}" }
|
||||||
var queued by remember(pendingKey) { mutableStateOf(loadPendingMessages(context, pendingKey)) }
|
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.
|
* Changes the visible outbox and keeps only its not-yet-durable part across a screen reopen.
|
||||||
@@ -1569,6 +1570,12 @@ fun SessionScreen(
|
|||||||
onTakeBack =
|
onTakeBack =
|
||||||
if (waiting.local) null
|
if (waiting.local) null
|
||||||
else ({ takeBack(waiting.id) }),
|
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) {
|
if (settingsOpen) {
|
||||||
// Measured when the dialog opens rather than kept up to date: what the reader is being told
|
// 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
|
// 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.
|
* 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
|
* [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.
|
* 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
|
* 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.
|
* `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,
|
pending: Boolean = false,
|
||||||
refusal: String? = null,
|
refusal: String? = null,
|
||||||
onTakeBack: (() -> Unit)? = null,
|
onTakeBack: (() -> Unit)? = null,
|
||||||
|
onDiscard: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
Box(Modifier.fillMaxWidth()) {
|
Box(Modifier.fillMaxWidth()) {
|
||||||
Card(
|
Card(
|
||||||
@@ -2268,6 +2300,11 @@ private fun UserBubble(
|
|||||||
color = MaterialTheme.colorScheme.error,
|
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)
|
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
|
@Test
|
||||||
fun identical_messages_are_reconciled_one_at_a_time() {
|
fun identical_messages_are_reconciled_one_at_a_time() {
|
||||||
val queued = listOf(local(), local().copy(id = "local-2"))
|
val queued = listOf(local(), local().copy(id = "local-2"))
|
||||||
|
|||||||
Reference in new issue
Block a user