From f26f423c14be1e987340096f40c72b02908176a1 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 14 Jan 2026 05:33:30 +0700 Subject: [PATCH] update DM queue thread-safety and responsive UI indicators --- .../android/mesh/BluetoothMeshService.kt | 9 +++- .../bitchat/android/services/MessageRouter.kt | 52 +++++++++++-------- .../com/bitchat/android/ui/ChatViewModel.kt | 4 ++ .../bitchat/android/ui/MeshDelegateHandler.kt | 9 ++++ 4 files changed, 51 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 6adb55f0..5252b606 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -188,6 +188,12 @@ class BluetoothMeshService(private val context: Context) { // SecurityManager delegate for key exchange notifications securityManager.delegate = object : SecurityManagerDelegate { override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) { + // Immediate notification for UI updates + delegate?.onSessionEstablished(peerID) + + // Flush router outbox immediately + com.bitchat.android.services.MessageRouter.tryGetInstance()?.onSessionEstablished(peerID) + // Send announcement and cached messages after key exchange serviceScope.launch { Log.d(TAG, "Key exchange completed with $peerID; sending follow-ups") @@ -1418,5 +1424,6 @@ interface BluetoothMeshDelegate { fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? fun getNickname(): String? fun isFavorite(peerID: String): Boolean - // registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager + fun onSessionEstablished(peerID: String) } + diff --git a/app/src/main/java/com/bitchat/android/services/MessageRouter.kt b/app/src/main/java/com/bitchat/android/services/MessageRouter.kt index a6bac09a..595d0bf9 100644 --- a/app/src/main/java/com/bitchat/android/services/MessageRouter.kt +++ b/app/src/main/java/com/bitchat/android/services/MessageRouter.kt @@ -6,6 +6,9 @@ import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.model.ReadReceipt import com.bitchat.android.nostr.NostrTransport +import java.util.concurrent.ConcurrentHashMap +import java.util.Collections + /** * Routes messages between BLE mesh and Nostr transports, matching iOS behavior. */ @@ -39,7 +42,8 @@ class MessageRouter private constructor( } // Outbox: peerID -> queued (content, nickname, messageID) - private val outbox = mutableMapOf>>() + // Thread-safe map with synchronized lists + private val outbox = ConcurrentHashMap>>() // Listener for favorites changes to flush outbox when npub mapping appears/changes private val favoriteListener = object: com.bitchat.android.favorites.FavoritesChangeListener { @@ -80,7 +84,7 @@ class MessageRouter private constructor( nostr.sendPrivateMessage(content, toPeerID, recipientNickname, messageID) } else { Log.d(TAG, "Queued PM for ${toPeerID} (no mesh, no Nostr mapping) msg_id=${messageID.take(8)}…") - val q = outbox.getOrPut(toPeerID) { mutableListOf() } + val q = outbox.getOrPut(toPeerID) { Collections.synchronizedList(mutableListOf()) } q.add(Triple(content, recipientNickname, messageID)) Log.d(TAG, "Initiating noise handshake after queueing PM for ${toPeerID.take(8)}…") mesh.initiateNoiseHandshake(toPeerID) @@ -126,30 +130,34 @@ class MessageRouter private constructor( // Flush any queued messages for a specific peerID fun flushOutboxFor(peerID: String) { val queued = outbox[peerID] ?: return - if (queued.isEmpty()) return - Log.d(TAG, "Flushing outbox for ${peerID.take(8)}… count=${queued.size}") - val iterator = queued.iterator() - while (iterator.hasNext()) { - val (content, nickname, messageID) = iterator.next() - var hasMesh = mesh.getPeerInfo(peerID)?.isConnected == true && mesh.hasEstablishedSession(peerID) - // If this is a noiseHex key, see if there is a connected mesh peer for this identity - if (!hasMesh && peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) { - val meshPeer = resolveMeshPeerForNoiseHex(peerID) - if (meshPeer != null && mesh.getPeerInfo(meshPeer)?.isConnected == true && mesh.hasEstablishedSession(meshPeer)) { - mesh.sendPrivateMessage(content, meshPeer, nickname, messageID) + + synchronized(queued) { + if (queued.isEmpty()) return + Log.d(TAG, "Flushing outbox for ${peerID.take(8)}… count=${queued.size}") + val iterator = queued.iterator() + while (iterator.hasNext()) { + val (content, nickname, messageID) = iterator.next() + var hasMesh = mesh.getPeerInfo(peerID)?.isConnected == true && mesh.hasEstablishedSession(peerID) + // If this is a noiseHex key, see if there is a connected mesh peer for this identity + if (!hasMesh && peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) { + val meshPeer = resolveMeshPeerForNoiseHex(peerID) + if (meshPeer != null && mesh.getPeerInfo(meshPeer)?.isConnected == true && mesh.hasEstablishedSession(meshPeer)) { + mesh.sendPrivateMessage(content, meshPeer, nickname, messageID) + iterator.remove() + continue + } + } + val canNostr = canSendViaNostr(peerID) + if (hasMesh) { + mesh.sendPrivateMessage(content, peerID, nickname, messageID) + iterator.remove() + } else if (canNostr) { + nostr.sendPrivateMessage(content, peerID, nickname, messageID) iterator.remove() - continue } } - val canNostr = canSendViaNostr(peerID) - if (hasMesh) { - mesh.sendPrivateMessage(content, peerID, nickname, messageID) - iterator.remove() - } else if (canNostr) { - nostr.sendPrivateMessage(content, peerID, nickname, messageID) - iterator.remove() - } } + // Cleanup empty lists (safe-ish with ConcurrentHashMap, worst case we create a new one later) if (queued.isEmpty()) { outbox.remove(peerID) } diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 07a5eea9..59545158 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -879,6 +879,10 @@ class ChatViewModel( override fun isFavorite(peerID: String): Boolean { return meshDelegateHandler.isFavorite(peerID) } + + override fun onSessionEstablished(peerID: String) { + meshDelegateHandler.onSessionEstablished(peerID) + } // registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager diff --git a/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt b/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt index d27719c7..c8d5ea76 100644 --- a/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt +++ b/app/src/main/java/com/bitchat/android/ui/MeshDelegateHandler.kt @@ -234,6 +234,15 @@ class MeshDelegateHandler( override fun isFavorite(peerID: String): Boolean { return privateChatManager.isFavorite(peerID) } + + override fun onSessionEstablished(peerID: String) { + coroutineScope.launch { + val sessionStates = state.getPeerSessionStatesValue().toMutableMap() + sessionStates[peerID] = "established" + state.setPeerSessionStates(sessionStates) + android.util.Log.d("MeshDelegateHandler", "UI: Updated session state to 'established' for $peerID") + } + } /** * Check for mentions in mesh messages and trigger notifications