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:
Paul Hitt
2026-09-09 08:06:22 -04:00
co-authored by Claude Opus 5
parent 1e0bf0ba1f
commit 3d9f18f922
3 changed files with 99 additions and 3 deletions
@@ -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),
)
}
}