update DM queue thread-safety and responsive UI indicators

This commit is contained in:
callebtc
2026-01-14 05:33:30 +07:00
parent b0aef8726c
commit f26f423c14
4 changed files with 51 additions and 23 deletions
@@ -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)
}
@@ -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<String, MutableList<Triple<String, String, String>>>()
// Thread-safe map with synchronized lists
private val outbox = ConcurrentHashMap<String, MutableList<Triple<String, String, String>>>()
// 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)
}
@@ -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
@@ -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