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 a4ad711..c15b1e9 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt @@ -1614,9 +1614,20 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) { if (current != null) { // A story arriving must appear without the user pulling // to refresh; it is filed by `receive()` and read back here. - val threads = withContext(Dispatchers.IO) { - runCatching { current.receive() }.getOrDefault(emptySet()) + val received = withContext(Dispatchers.IO) { + runCatching { current.receive() } } + // 🔴 410 from the poll: this device's registration is gone + // — revoked from another phone, or expired by the sweep. + // Wipe and sign out with an honest sentence, rather than + // hammering a mailbox that no longer exists while sitting + // on ciphertext the account's owner asked to be gone. + val failure = received.exceptionOrNull() + if (failure is RelayException.Http && failure.status == 410) { + signOutRevoked() + return@launch + } + val threads = received.getOrDefault(emptySet()) // 🔴 **Retry the outbox on every tick.** A message to // somebody with no registered device waits here rather than // being lost or dumped on the sender to remember — see @@ -1705,6 +1716,22 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) { const val CALL_SETUP_POLL_MS = 1_000L } + /// The relay said this device was removed. Same teardown as a sign-out — + /// which on Android is already a full vault destroy — plus the sentence + /// that says what happened and what signing in again means. + private fun signOutRevoked() = viewModelScope.launch { + polling?.cancel() + polling = null + session = null + withContext(Dispatchers.IO) { runCatching { KeystoreVault.destroy(getApplication()) } } + identity = null + vault = KeystoreVault.open(getApplication()) + _phase.value = Phase.SignedOut( + error = "This device was removed from the account, so its local " + + "messages were cleared. Signing in again registers it as a " + + "new device.") + } + fun signOut() = viewModelScope.launch { polling?.cancel() polling = null