Offer the review when a key change blocks a call
ios#73. Android already named the reason: a call signal refused with IdentityChangedException hung up and said "Their security code changed". But it then told the person to go and find the conversation themselves, and the banner that compares and accepts lives in the conversation header, which a call started from the Calls tab or answered from a notification never passes through. So the failure described the cure without offering it. The blocked peer is now carried on the phase rather than only named in a sentence, and the failure draws a "Review their security code" button that opens that conversation and clears the failure. Compare and accept stay behind the existing banner. A second accept button, phrased on a call screen in a hurry, is the thing the issue's wording requirement exists to prevent. 4 new tests. 1217 tests pass, counted from the XML. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
1e0bf0ba1f
commit
3d9f18f922
@@ -734,6 +734,18 @@ internal fun ColumnScope.SignedIn(
|
||||
)
|
||||
}
|
||||
|
||||
// A call refused because the peer's keys changed offers the review here,
|
||||
// rather than telling somebody to go and find the conversation (ios#73).
|
||||
// Compare and accept both live behind this, in the banner the chat already
|
||||
// draws, so the wording it is judged by is unchanged.
|
||||
if (phase.callKeyReview != null) {
|
||||
KitSecondaryButton(
|
||||
text = "Review their security code",
|
||||
onClick = { model.reviewBlockedCallKeys() },
|
||||
tag = "review-call-keys",
|
||||
)
|
||||
}
|
||||
|
||||
if (addingContact) {
|
||||
AddContactForm(model, phase, initial = scannedPrefill) {
|
||||
addingContact = false
|
||||
|
||||
@@ -124,6 +124,11 @@ class MessengerViewModel(
|
||||
val lookupMiss: String? = null,
|
||||
/** The call in progress, if any (§8.10). */
|
||||
val call: CallState? = null,
|
||||
/**
|
||||
* The peer whose key change blocked a call, so the failure can
|
||||
* offer the review instead of describing it (ios#73).
|
||||
*/
|
||||
val callKeyReview: Pair<Long, String>? = null,
|
||||
/** Shown once, before the first call ever placed on this device. */
|
||||
val callPrivacyPrompt: Long? = null,
|
||||
/**
|
||||
@@ -383,6 +388,17 @@ class MessengerViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the conversation with the peer whose key change blocked a call, and
|
||||
* clear the failure (ios#73).
|
||||
*/
|
||||
fun reviewBlockedCallKeys() {
|
||||
val current = _phase.value as? Phase.SignedIn ?: return
|
||||
val (peerId, peerName) = current.callKeyReview ?: return
|
||||
_phase.value = current.copy(error = null, callKeyReview = null)
|
||||
openChat(peerId, peerName)
|
||||
}
|
||||
|
||||
fun signIn(
|
||||
handle: String,
|
||||
passphrase: String,
|
||||
@@ -2272,10 +2288,10 @@ class MessengerViewModel(
|
||||
it.copy(
|
||||
call = null,
|
||||
error =
|
||||
"Their security code changed, so the call " +
|
||||
"was not placed. Open the conversation with " +
|
||||
"${nameFor(it, peerId)} to review it, then " +
|
||||
"Their security code changed, so the call was " +
|
||||
"not placed. Review the change before you " +
|
||||
"call again.",
|
||||
callKeyReview = peerId to nameFor(it, peerId),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package io.flamenet.messenger.app
|
||||
|
||||
import java.io.File
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* A call refused because the peer's keys changed offers the review (ios#73).
|
||||
*/
|
||||
class CallKeyReviewTest {
|
||||
private fun source(path: String): String {
|
||||
val file = File("src/main/kotlin/io/flamenet/messenger/app/$path")
|
||||
assertTrue("missing $path", file.exists())
|
||||
return file.readText()
|
||||
}
|
||||
|
||||
/** The blocked peer is carried, not just named in a sentence. */
|
||||
@Test
|
||||
fun theBlockedPeerIsCarried() {
|
||||
val vm = source("MessengerViewModel.kt")
|
||||
assertTrue(
|
||||
"the failure must carry the peer",
|
||||
vm.contains("val callKeyReview: Pair<Long, String>? = null"),
|
||||
)
|
||||
assertTrue(
|
||||
"and be set where the trust gate refuses the signal",
|
||||
vm.contains("callKeyReview = peerId to nameFor(it, peerId)"),
|
||||
)
|
||||
}
|
||||
|
||||
/** Tapping it opens that conversation and clears the failure. */
|
||||
@Test
|
||||
fun reviewOpensTheConversation() {
|
||||
val vm = source("MessengerViewModel.kt")
|
||||
assertTrue(vm.contains("fun reviewBlockedCallKeys()"))
|
||||
assertTrue("must open the chat", vm.contains("openChat(peerId, peerName)"))
|
||||
assertTrue(
|
||||
"and must not leave the failure on screen",
|
||||
vm.contains("copy(error = null, callKeyReview = null)"),
|
||||
)
|
||||
}
|
||||
|
||||
/** The button is on the screen the failure appears on. */
|
||||
@Test
|
||||
fun theButtonIsRendered() {
|
||||
val main = source("MainActivity.kt")
|
||||
assertTrue(main.contains("phase.callKeyReview != null"))
|
||||
assertTrue(main.contains("model.reviewBlockedCallKeys()"))
|
||||
assertTrue(main.contains("\"review-call-keys\""))
|
||||
}
|
||||
|
||||
/**
|
||||
* The reason is still named. A generic "call failed" was the original
|
||||
* complaint, and the fix must not regress into one.
|
||||
*/
|
||||
@Test
|
||||
fun theReasonIsStillNamed() {
|
||||
val vm = source("MessengerViewModel.kt")
|
||||
assertTrue(vm.contains("Their security code changed"))
|
||||
// The quoted form only: the KDoc on reviewBlockedCallKeys describes
|
||||
// opening a conversation, and that is not copy anybody reads.
|
||||
assertFalse(
|
||||
"must not tell somebody to go and find the conversation themselves",
|
||||
vm.contains("\"Open the conversation with "),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user