Match private conversation confirmations before opening the requested peer

This commit is contained in:
evgeny committed 2026-10-29 13:35:41 +01:00
1 parent 69ae8b57cb
commit 321a2c3ed7
3 files changed
+75 -20

No files matched your search

@@ -330,6 +330,7 @@ class MainActivity : AppCompatActivity() {
// После смерти процесса ViewModel уже новый; сохранённый экран может потерять свою беседу или звонок.
LaunchedEffect(screen) {
if (screen != Screen.MemberList && screen != Screen.Dm) vm.cancelDmStart()
inputScreen = screen.name
LogManager.addLog("INFO", "MAIN", "UI screen=$screen")
val unavailable = when (screen) {
@@ -382,7 +383,8 @@ class MainActivity : AppCompatActivity() {
vm.dmOpen.collect { convId ->
if (convId != null) {
vm.consumeDmOpen()
screen = Screen.Dm
if (screen == Screen.MemberList && vm.currentDm.value?.convId == convId) screen = Screen.Dm
else LogManager.addLog("DEBUG", "MAIN", "Ignoring stale DM navigation conv=$convId screen=$screen")
}
}
}
@@ -462,8 +464,14 @@ class MainActivity : AppCompatActivity() {
isStuck = stuck,
cpuLoad = cpuLoad,
onCpuLoadVisible = vm::setCpuLoadVisible,
onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat },
onSelectDm = { dm -> vm.openDm(dm.convId, dm.peerNodeId, dm.peerName, dm.generation); screen = Screen.Dm },
onSelectChannel = { ch ->
vm.selectChannel(ch)
if (vm.currentChannel.value?.id == ch.id) screen = Screen.Chat
},
onSelectDm = { dm ->
vm.openDm(dm.convId, dm.peerNodeId, dm.peerName, dm.generation)
if (vm.currentDm.value?.convId == dm.convId) screen = Screen.Dm
},
onCreateChannel = { name -> vm.createChannel(name, "channel_1") },
onJoinChannel = { showJoinDialog = true },
onInviteClick = showInvite,
@@ -0,0 +1,25 @@
package com.utun.chat.data
/** Одна ожидаемая навигация: ID принятой worker команды фиксируется до DM_CONV_UPDATED.
* Чужое событие сохраняет ожидание. Новый запрос/отмена лишает старый timeout права менять состояние. */
class PendingDmOpen {
private data class Request(val token: Long, val conversation: String, val peer: Long, val name: String)
private var sequence = 0L
private var request: Request? = null
val pending: Boolean get() = request != null
fun begin(conversation: String, peer: Long, name: String): Long {
require(conversation.toULongOrNull()?.let { it != 0UL } == true && peer != 0L)
val token = ++sequence
request = Request(token, conversation, peer, name)
return token
}
fun isPending(token: Long): Boolean = request?.token == token
fun cancel() { request = null }
fun complete(conversation: String, generation: Long): DmConversation? {
val current = request ?: return null
if (current.conversation != conversation || generation <= 0) return null
request = null
return DmConversation(convId = conversation, peerNodeId = current.peer, peerName = current.name, generation = generation)
}
}
@@ -349,8 +349,15 @@ class ChatViewModel : ViewModel() {
private val _dmOpen = MutableStateFlow<String?>(null)
val dmOpen: StateFlow<String?> = _dmOpen
/* peer-данные начатой беседы — для автооткрытия по событию DM_CONV_UPDATED. */
private var pendingStartPeer: Pair<Long, String>? = null
private val pendingDmOpen = PendingDmOpen()
private var dmStartTimeout: kotlinx.coroutines.Job? = null
private val _dmStartFailed = MutableStateFlow(false)
val dmStartFailed: StateFlow<Boolean> = _dmStartFailed
fun dismissDmStartError() { _dmStartFailed.value = false }
fun cancelDmStart() {
pendingDmOpen.cancel(); dmStartTimeout?.cancel(); dmStartTimeout = null
}
val audioRecorder = AudioRecorderManager()
@@ -505,6 +512,8 @@ class ChatViewModel : ViewModel() {
private fun handleEvent(type: Int, data: ByteArray?) {
if (type == 13) {
if (pendingDmOpen.pending) { _dmStartFailed.value = true; LogManager.addLog("WARN", "PM", "Conversation start interrupted by core stop") }
cancelDmStart()
cpuLoadPending = false; _cpuLoad.value = 0
dmPresenceRefreshJob?.cancel()
_dmCallAvailable.value = false
@@ -895,16 +904,11 @@ class ChatViewModel : ViewModel() {
refreshDmMessages()
}
}
val pend = pendingStartPeer
if (pend != null) {
pendingStartPeer = null
_currentDm.value = DmConversation(convId = convId, peerNodeId = pend.first, peerName = pend.second,
generation = generation)
_dmCallAvailable.value = false
refreshDmCallRoute()
refreshDmMessages()
_dmOpen.value = convId
LogManager.addLog("INFO", "VM", "DM auto-open conv=$convId")
pendingDmOpen.complete(convId, generation)?.let { started ->
dmStartTimeout?.cancel(); dmStartTimeout = null
openDm(started.convId, started.peerNodeId, started.peerName, started.generation)
if (_currentDm.value?.convId == convId) _dmOpen.value = convId
LogManager.addLog("INFO", "VM", "DM start confirmed conv=$convId peer=${started.peerNodeId}")
}
}
32 -> { /* DM_MSG_RECEIVED: [conv_id_len:1][conv_id:var][author_node_id:8] */
@@ -1149,7 +1153,10 @@ class ChatViewModel : ViewModel() {
_inviteCandidates.value = emptyList()
_inviteLink.value = null
_inviteLinkPending.value = true
NativeLib.requestInviteCandidates(channelId)
if (!NativeLib.requestInviteCandidates(channelId)) {
_inviteLinkPending.value = false
LogManager.addLog("ERROR", "INVITE", "Candidate request rejected ch=$channelId")
}
LogManager.addLog("INFO", "VM", "requestInvite ch=$channelId")
}
@@ -1157,7 +1164,10 @@ class ChatViewModel : ViewModel() {
_inviteLink.value = null
_inviteLinkPending.value = true
inviteChannelId = channelId
NativeLib.requestInviteLink(channelId, targetNodeId, ++inviteRequestId)
if (!NativeLib.requestInviteLink(channelId, targetNodeId, ++inviteRequestId)) {
_inviteLinkPending.value = false
LogManager.addLog("ERROR", "INVITE", "Link request rejected ch=$channelId target=$targetNodeId")
}
LogManager.addLog("INFO", "VM", "requestInviteLink ch=$channelId node=0x%016x".format(targetNodeId))
}
@@ -1331,14 +1341,25 @@ class ChatViewModel : ViewModel() {
fun startDm(channelId: String, nodeId: Long, name: String) {
val r = repo ?: return
if (nodeId == 0L || nodeId == NativeLib.getMyNodeId()) return
LogManager.addLog("INFO", "VM", "startDm ch=$channelId peer=0x%016x name=$name".format(nodeId))
pendingStartPeer = nodeId to name
r.dmStart(nodeId, channelId, name)
cancelDmStart(); _dmStartFailed.value = false
try {
val conversation = checkNotNull(r.dmStart(nodeId, channelId, name)) { "Conversation start rejected" }
val token = pendingDmOpen.begin(conversation, nodeId, name)
LogManager.addLog("INFO", "PM", "Conversation start accepted channel=$channelId peer=$nodeId conv=$conversation")
dmStartTimeout = viewModelScope.launch {
delay(10_000)
if (pendingDmOpen.isPending(token)) {
pendingDmOpen.cancel(); _dmStartFailed.value = true
LogManager.addLog("ERROR", "PM", "Conversation start confirmation timed out conv=$conversation peer=$nodeId")
}
}
} catch (e: Exception) { _dmStartFailed.value = true; LogManager.addLog("ERROR", "PM", "Conversation start failed peer=$nodeId: $e") }
}
fun consumeDmOpen() { _dmOpen.value = null }
fun openDm(convId: String, peerNodeId: Long, peerName: String, generation: Long) {
cancelDmStart()
if (repo == null) return
_editing.value = null
val dm = DmConversation(convId = convId, peerNodeId = peerNodeId, peerName = peerName, generation = generation)
@@ -1902,6 +1923,7 @@ class ChatViewModel : ViewModel() {
}
fun clearState() {
cancelDmStart(); _dmStartFailed.value = false; _dmOpen.value = null
++channelRefreshRevision; ++messageRefreshRevision; ++jumpRevision
memberDetailJob?.cancel(); memberDetailTarget = null; _selectedMember.value = null
dismissPmInvite(); _linkJoin.value = null; _joinedChannel.value = null