Merge branch 'fix/unsend-tells-the-truth' into 'main'
"Unsend" did not unsend (android#18) See merge request paulhitt/flamenet-messenger-android!65
This commit was merged in pull request #92.
This commit is contained in:
@@ -1953,7 +1953,16 @@ fun ColumnScope.ChatScreen(
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(if (line.mine) "Unsend" else "Delete for me") },
|
||||
// 🔴 **Said "Unsend" on your own messages and did
|
||||
// nothing of the kind (android#18).** `deleteMessage`
|
||||
// reaches `vault.deleteLine` and stops there — no
|
||||
// envelope leaves the phone, and the recipient's copy
|
||||
// stays exactly where it is. Somebody who sent to the
|
||||
// wrong person tapped it, watched the bubble vanish,
|
||||
// and had no way to find out. "Delete for me" was
|
||||
// already the other half of this ternary, and it was
|
||||
// always the accurate half.
|
||||
text = { Text("Delete for me") },
|
||||
onClick = { model.deleteMessage(line); menuOpen = false },
|
||||
modifier = Modifier.testTag("delete-message"),
|
||||
)
|
||||
@@ -3950,8 +3959,8 @@ internal fun memberLabel(
|
||||
* rather not. The cursor sits at the end for the same reason.
|
||||
*
|
||||
* ⚠️ Saving nothing is refused rather than treated as a delete. Emptying the
|
||||
* box is the shape of a slip, and "Unsend" is right there in the same menu for
|
||||
* anybody who meant it.
|
||||
* box is the shape of a slip, and "Delete for me" is right there in the same
|
||||
* menu for anybody who meant it.
|
||||
*/
|
||||
@Composable
|
||||
private fun EditMessageSheet(
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package io.flamenet.messenger.app
|
||||
|
||||
import java.io.File
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* A destructive button must not claim more than it does (android#18).
|
||||
*
|
||||
* 🔴 **"Unsend" did not unsend.** The message menu offered it on your own
|
||||
* messages and called `deleteMessage`, which reaches `vault.deleteLine` and
|
||||
* stops there. No envelope leaves the phone; the recipient's copy stays exactly
|
||||
* where it is. Somebody who sent to the wrong person tapped it, watched the
|
||||
* bubble vanish, and had no way to find out.
|
||||
*
|
||||
* ⚠️ **Worse than an overclaiming page**, which is why this is a test and not
|
||||
* a one-line fix left to stay fixed. A page that overclaims can be read
|
||||
* sceptically. A button that reports success cannot: the feedback IS the claim,
|
||||
* and it arrives at the exact moment the person is most relieved to believe it.
|
||||
*
|
||||
* A source scan, and coarse on purpose. It cannot prove every label is honest.
|
||||
* It proves this particular word cannot come back on a local-only action
|
||||
* without somebody deleting a test that explains why — and it is a plain JVM
|
||||
* test, so it costs no emulator.
|
||||
*/
|
||||
class DestructiveLabelsTest {
|
||||
|
||||
private fun uiSources(): List<File> {
|
||||
// From `app/build/classes/...` back to the module root, whatever the
|
||||
// working directory happens to be. Resolving against a relative path
|
||||
// is how this kind of test silently scans nothing.
|
||||
var dir = File(System.getProperty("user.dir"))
|
||||
while (!File(dir, "app/src/main/kotlin").isDirectory && dir.parentFile != null) {
|
||||
dir = dir.parentFile
|
||||
}
|
||||
val root = File(dir, "app/src/main/kotlin")
|
||||
return root.walkTopDown().filter { it.extension == "kt" }.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ **Comments are stripped, and that is required rather than tidy.** The
|
||||
* comment explaining this bug necessarily quotes the word it is about, and
|
||||
* a guard that flags its own explanation is a guard somebody deletes.
|
||||
*/
|
||||
private fun codeLines(body: String): List<String> =
|
||||
body.lines()
|
||||
.filterNot { it.trimStart().startsWith("//") || it.trimStart().startsWith("*") }
|
||||
.map { it.substringBefore("//") }
|
||||
|
||||
/**
|
||||
* 🔴 **The word promises the recipient's copy is gone.** Nothing in this app
|
||||
* can do that yet: it would take a signed control envelope naming the `mid`,
|
||||
* which the recipient acts on — and even then it is a *request*, so whatever
|
||||
* the button eventually says must not promise more than "we asked them to
|
||||
* remove it."
|
||||
*
|
||||
* When that lands, this test is the thing to update, deliberately, in the
|
||||
* same change.
|
||||
*/
|
||||
@Test
|
||||
fun `no button says Unsend`() {
|
||||
val files = uiSources()
|
||||
assertTrue(files.isNotEmpty(), "scanned no sources — the walk found nothing")
|
||||
for (file in files) {
|
||||
codeLines(file.readText()).forEachIndexed { i, line ->
|
||||
assertTrue(
|
||||
!line.contains("\"Unsend\""),
|
||||
"${file.name} line ~${i + 1} offers a button labelled \"Unsend\", but " +
|
||||
"nothing in this app removes a message from the recipient's device. " +
|
||||
"See android#18. If a real retract now exists, update this test in " +
|
||||
"the same change and say which envelope carries it.",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ The walk is the part most likely to rot. A scan that finds no files
|
||||
* passes every assertion in it, so this pins that it is actually reading
|
||||
* the screen the bug was on.
|
||||
*/
|
||||
@Test
|
||||
fun `the scan actually reaches the chat screen`() {
|
||||
assertTrue(
|
||||
uiSources().any { it.name == "ChatScreens.kt" },
|
||||
"the source walk did not reach ChatScreens.kt — this guard is scanning nothing",
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user