diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt b/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt index 7afe9aa..514a4b3 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MainActivity.kt @@ -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 diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt index 525680e..4517ec3 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt @@ -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? = 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), ) } } diff --git a/app/src/test/kotlin/io/flamenet/messenger/app/CallKeyReviewTest.kt b/app/src/test/kotlin/io/flamenet/messenger/app/CallKeyReviewTest.kt new file mode 100644 index 0000000..dc86a4d --- /dev/null +++ b/app/src/test/kotlin/io/flamenet/messenger/app/CallKeyReviewTest.kt @@ -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? = 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 "), + ) + } +}