mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 03:45:21 +00:00
Fix delivery acks and read receipts (#186)
* delivery ack and read receipt * handle deliveryack * fix uppercase id * fix read receipts too
This commit is contained in:
@@ -66,7 +66,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Wire up PacketProcessor reference for recursive handling in MessageHandler
|
||||
messageHandler.packetProcessor = packetProcessor
|
||||
sendPeriodicBroadcastAnnounce()
|
||||
startPeriodicDebugLogging()
|
||||
// startPeriodicDebugLogging()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,13 +89,13 @@ class BluetoothMeshService(private val context: Context) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send broadcast announcement every 10 seconds
|
||||
* Send broadcast announcement every 30 seconds
|
||||
*/
|
||||
private fun sendPeriodicBroadcastAnnounce() {
|
||||
serviceScope.launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
delay(10000) // 10 seconds
|
||||
delay(30000) // 30 seconds
|
||||
sendBroadcastAnnounce()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error in periodic broadcast announce: ${e.message}")
|
||||
@@ -356,9 +356,9 @@ class BluetoothMeshService(private val context: Context) {
|
||||
return fragmentManager.handleFragment(packet)
|
||||
}
|
||||
|
||||
override fun handleDeliveryAck(routed: RoutedPacket) {
|
||||
serviceScope.launch { messageHandler.handleDeliveryAck(routed) }
|
||||
}
|
||||
// override fun handleDeliveryAck(routed: RoutedPacket) {
|
||||
// serviceScope.launch { messageHandler.handleDeliveryAck(routed) }
|
||||
// }
|
||||
|
||||
override fun handleReadReceipt(routed: RoutedPacket) {
|
||||
serviceScope.launch { messageHandler.handleReadReceipt(routed) }
|
||||
@@ -552,7 +552,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
originalMessageID = message.id,
|
||||
recipientID = myPeerID,
|
||||
recipientNickname = nickname,
|
||||
hopCount = 0u // Will be calculated during relay
|
||||
hopCount = 0u.toUByte() // Will be calculated during relay
|
||||
)
|
||||
|
||||
try {
|
||||
@@ -586,6 +586,47 @@ class BluetoothMeshService(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send read receipt for a received private message
|
||||
*/
|
||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||
serviceScope.launch {
|
||||
try {
|
||||
Log.d(TAG, "Sending read receipt for message $messageID to $recipientPeerID")
|
||||
|
||||
// Create the read receipt
|
||||
val receipt = ReadReceipt(
|
||||
originalMessageID = messageID,
|
||||
readerID = myPeerID,
|
||||
readerNickname = readerNickname
|
||||
)
|
||||
|
||||
// Encode the receipt
|
||||
val receiptData = receipt.encode()
|
||||
|
||||
// Create inner read receipt packet
|
||||
val innerPacket = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.READ_RECEIPT.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = receiptData,
|
||||
signature = null,
|
||||
ttl = 3u
|
||||
)
|
||||
|
||||
// Encrypt the entire inner packet and send as NOISE_ENCRYPTED
|
||||
encryptAndBroadcastNoisePacket(innerPacket, recipientPeerID)
|
||||
|
||||
Log.d(TAG, "Sent read receipt for message $messageID to $recipientPeerID")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send read receipt for message $messageID: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a BitchatPacket and broadcast it as a NOISE_ENCRYPTED message
|
||||
* This is the correct protocol implementation - encrypt the entire packet, not just the payload
|
||||
@@ -644,14 +685,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
payload = nickname.toByteArray()
|
||||
)
|
||||
|
||||
// Send multiple times for reliability
|
||||
delay(Random.nextLong(0, 500))
|
||||
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
||||
|
||||
delay(500 + Random.nextLong(0, 500))
|
||||
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
||||
|
||||
delay(1000 + Random.nextLong(0, 500))
|
||||
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,19 +58,12 @@ class MessageHandler(private val myPeerID: String) {
|
||||
|
||||
// Check if this is a delivery ACK with the new format
|
||||
if (typeMarker == MessageType.DELIVERY_ACK.value) {
|
||||
// Extract the ACK JSON data (skip the type marker)
|
||||
val ackData = decryptedData.sliceArray(1 until decryptedData.size)
|
||||
|
||||
// Decode the delivery ACK
|
||||
val ack = DeliveryAck.decode(ackData)
|
||||
if (ack != null) {
|
||||
delegate?.onDeliveryAckReceived(ack)
|
||||
Log.d(TAG, "Processed delivery ACK from $peerID")
|
||||
return
|
||||
}
|
||||
handleDeliveryAck(decryptedData)
|
||||
Log.d(TAG, "Processed delivery ACK from $peerID")
|
||||
}
|
||||
|
||||
// Check for read receipt with type marker
|
||||
// NOTE: THIS DOESN'T WORK WITH IOS, IT SENDS AN INNER PACKET INSTEAD
|
||||
if (typeMarker == MessageType.READ_RECEIPT.value) {
|
||||
val receiptData = decryptedData.sliceArray(1 until decryptedData.size)
|
||||
val receipt = ReadReceipt.decode(receiptData)
|
||||
@@ -268,7 +261,7 @@ class MessageHandler(private val myPeerID: String) {
|
||||
delegate?.onMessageReceived(message)
|
||||
|
||||
// Send delivery ACK
|
||||
// delegate?.sendDeliveryAck(message, peerID)
|
||||
delegate?.sendDeliveryAck(message, peerID)
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
@@ -302,23 +295,13 @@ class MessageHandler(private val myPeerID: String) {
|
||||
/**
|
||||
* Handle delivery acknowledgment
|
||||
*/
|
||||
suspend fun handleDeliveryAck(routed: RoutedPacket) {
|
||||
val packet = routed.packet
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) {
|
||||
try {
|
||||
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
|
||||
if (decryptedData != null) {
|
||||
val ack = DeliveryAck.decode(decryptedData)
|
||||
if (ack != null) {
|
||||
delegate?.onDeliveryAckReceived(ack)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to decrypt delivery ACK: ${e.message}")
|
||||
}
|
||||
suspend fun handleDeliveryAck(decryptedData: ByteArray) {
|
||||
val ackData = decryptedData.sliceArray(1 until decryptedData.size)
|
||||
val ack = DeliveryAck.decode(ackData)
|
||||
if (ack != null) {
|
||||
delegate?.onDeliveryAckReceived(ack)
|
||||
}
|
||||
// Delivery ACK relay is now handled by centralized PacketRelayManager
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,20 +310,11 @@ class MessageHandler(private val myPeerID: String) {
|
||||
suspend fun handleReadReceipt(routed: RoutedPacket) {
|
||||
val packet = routed.packet
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) {
|
||||
try {
|
||||
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
|
||||
if (decryptedData != null) {
|
||||
val receipt = ReadReceipt.decode(decryptedData)
|
||||
if (receipt != null) {
|
||||
delegate?.onReadReceiptReceived(receipt)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to decrypt read receipt: ${e.message}")
|
||||
}
|
||||
val receipt = ReadReceipt.decode(routed.packet.payload)
|
||||
if (receipt != null) {
|
||||
delegate?.onReadReceiptReceived(receipt)
|
||||
}
|
||||
// Read receipt relay is now handled by centralized PacketRelayManager
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -139,7 +139,7 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
MessageType.NOISE_HANDSHAKE_INIT -> handleNoiseHandshake(routed, 1)
|
||||
MessageType.NOISE_HANDSHAKE_RESP -> handleNoiseHandshake(routed, 2)
|
||||
MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(routed)
|
||||
MessageType.DELIVERY_ACK -> handleDeliveryAck(routed)
|
||||
//MessageType.DELIVERY_ACK -> handleDeliveryAck(routed) // custom packet type...
|
||||
MessageType.READ_RECEIPT -> handleReadReceipt(routed)
|
||||
else -> {
|
||||
validPacket = false
|
||||
@@ -234,11 +234,11 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
/**
|
||||
* Handle delivery acknowledgment
|
||||
*/
|
||||
private suspend fun handleDeliveryAck(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing delivery ACK from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleDeliveryAck(routed)
|
||||
}
|
||||
// private suspend fun handleDeliveryAck(routed: RoutedPacket) {
|
||||
// val peerID = routed.peerID ?: "unknown"
|
||||
// Log.d(TAG, "Processing delivery ACK from ${formatPeerForLog(peerID)}")
|
||||
// delegate?.handleDeliveryAck(routed)
|
||||
// }
|
||||
|
||||
/**
|
||||
* Handle read receipt
|
||||
@@ -313,7 +313,7 @@ interface PacketProcessorDelegate {
|
||||
fun handleMessage(routed: RoutedPacket)
|
||||
fun handleLeave(routed: RoutedPacket)
|
||||
fun handleFragment(packet: BitchatPacket): BitchatPacket?
|
||||
fun handleDeliveryAck(routed: RoutedPacket)
|
||||
// fun handleDeliveryAck(routed: RoutedPacket)
|
||||
fun handleReadReceipt(routed: RoutedPacket)
|
||||
|
||||
// Communication
|
||||
|
||||
@@ -46,7 +46,7 @@ sealed class DeliveryStatus : Parcelable {
|
||||
*/
|
||||
@Parcelize
|
||||
data class BitchatMessage(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val id: String = UUID.randomUUID().toString().uppercase(),
|
||||
val sender: String,
|
||||
val content: String,
|
||||
val timestamp: Date,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.google.gson.GsonBuilder
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import com.bitchat.android.util.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Delivery acknowledgment structure - exact same as iOS version
|
||||
* Uses binary encoding for efficient protocol communication
|
||||
*/
|
||||
@Parcelize
|
||||
data class DeliveryAck(
|
||||
@@ -15,29 +16,91 @@ data class DeliveryAck(
|
||||
val recipientID: String,
|
||||
val recipientNickname: String,
|
||||
val timestamp: Date = Date(),
|
||||
val hopCount: UInt
|
||||
val hopCount: UByte
|
||||
) : Parcelable {
|
||||
|
||||
private val gson = GsonBuilder()
|
||||
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
||||
.create()
|
||||
// Primary constructor for creating new acks
|
||||
constructor(originalMessageID: String, recipientID: String, recipientNickname: String, hopCount: UByte) : this(
|
||||
originalMessageID = originalMessageID,
|
||||
ackID = UUID.randomUUID().toString(),
|
||||
recipientID = recipientID,
|
||||
recipientNickname = recipientNickname,
|
||||
timestamp = Date(),
|
||||
hopCount = hopCount
|
||||
)
|
||||
|
||||
fun encode(): ByteArray? {
|
||||
return try {
|
||||
gson.toJson(this).toByteArray(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
/**
|
||||
* Encode to binary data matching iOS toBinaryData implementation
|
||||
*/
|
||||
fun encode(): ByteArray {
|
||||
val builder = BinaryDataBuilder()
|
||||
|
||||
// Append original message UUID
|
||||
builder.appendUUID(originalMessageID)
|
||||
|
||||
// Append ack ID UUID
|
||||
builder.appendUUID(ackID)
|
||||
|
||||
// Append recipient ID as 8-byte hex string
|
||||
val recipientData = ByteArray(8) { 0 }
|
||||
var tempID = recipientID
|
||||
var index = 0
|
||||
|
||||
while (tempID.length >= 2 && index < 8) {
|
||||
val hexByte = tempID.substring(0, 2)
|
||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
||||
if (byte != null) {
|
||||
recipientData[index] = byte
|
||||
}
|
||||
tempID = tempID.substring(2)
|
||||
index++
|
||||
}
|
||||
|
||||
builder.buffer.addAll(recipientData.toList())
|
||||
|
||||
// Append hop count (UInt8)
|
||||
builder.appendUInt8(hopCount)
|
||||
|
||||
// Append timestamp
|
||||
builder.appendDate(timestamp)
|
||||
|
||||
// Append recipient nickname as string
|
||||
builder.appendString(recipientNickname)
|
||||
|
||||
return builder.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Decode from binary data matching iOS fromBinaryData implementation
|
||||
*/
|
||||
fun decode(data: ByteArray): DeliveryAck? {
|
||||
return try {
|
||||
val json = String(data, Charsets.UTF_8)
|
||||
com.google.gson.Gson().fromJson(json, DeliveryAck::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
// Create defensive copy
|
||||
val dataCopy = data.copyOf()
|
||||
|
||||
// Minimum size: 2 UUIDs (32) + recipientID (8) + hopCount (1) + timestamp (8) + min nickname
|
||||
if (dataCopy.size < 50) return null
|
||||
|
||||
val offset = intArrayOf(0)
|
||||
|
||||
val originalMessageID = dataCopy.readUUID(offset) ?: return null
|
||||
val ackID = dataCopy.readUUID(offset) ?: return null
|
||||
|
||||
val recipientIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
|
||||
val recipientID = recipientIDData.hexEncodedString()
|
||||
|
||||
val hopCount = dataCopy.readUInt8(offset) ?: return null
|
||||
val timestamp = dataCopy.readDate(offset) ?: return null
|
||||
val recipientNickname = dataCopy.readString(offset) ?: return null
|
||||
|
||||
return DeliveryAck(
|
||||
originalMessageID = originalMessageID,
|
||||
ackID = ackID,
|
||||
recipientID = recipientID,
|
||||
recipientNickname = recipientNickname,
|
||||
timestamp = timestamp,
|
||||
hopCount = hopCount
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package com.bitchat.android.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import com.bitchat.android.util.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Read receipt structure - exact same as iOS version
|
||||
* Uses binary encoding for efficient protocol communication
|
||||
*/
|
||||
@Parcelize
|
||||
data class ReadReceipt(
|
||||
@@ -16,22 +18,82 @@ data class ReadReceipt(
|
||||
val timestamp: Date = Date()
|
||||
) : Parcelable {
|
||||
|
||||
fun encode(): ByteArray? {
|
||||
return try {
|
||||
com.google.gson.Gson().toJson(this).toByteArray(Charsets.UTF_8)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
// Primary constructor for creating new read receipts
|
||||
constructor(originalMessageID: String, readerID: String, readerNickname: String) : this(
|
||||
originalMessageID = originalMessageID,
|
||||
receiptID = UUID.randomUUID().toString(),
|
||||
readerID = readerID,
|
||||
readerNickname = readerNickname,
|
||||
timestamp = Date()
|
||||
)
|
||||
|
||||
/**
|
||||
* Encode to binary data matching iOS toBinaryData implementation
|
||||
*/
|
||||
fun encode(): ByteArray {
|
||||
val builder = BinaryDataBuilder()
|
||||
|
||||
// Append original message UUID
|
||||
builder.appendUUID(originalMessageID)
|
||||
|
||||
// Append receipt ID UUID
|
||||
builder.appendUUID(receiptID)
|
||||
|
||||
// Append reader ID as 8-byte hex string
|
||||
val readerData = ByteArray(8) { 0 }
|
||||
var tempID = readerID
|
||||
var index = 0
|
||||
|
||||
while (tempID.length >= 2 && index < 8) {
|
||||
val hexByte = tempID.substring(0, 2)
|
||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
||||
if (byte != null) {
|
||||
readerData[index] = byte
|
||||
}
|
||||
tempID = tempID.substring(2)
|
||||
index++
|
||||
}
|
||||
|
||||
builder.buffer.addAll(readerData.toList())
|
||||
|
||||
// Append timestamp
|
||||
builder.appendDate(timestamp)
|
||||
|
||||
// Append reader nickname as string
|
||||
builder.appendString(readerNickname)
|
||||
|
||||
return builder.toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Decode from binary data matching iOS fromBinaryData implementation
|
||||
*/
|
||||
fun decode(data: ByteArray): ReadReceipt? {
|
||||
return try {
|
||||
val json = String(data, Charsets.UTF_8)
|
||||
com.google.gson.Gson().fromJson(json, ReadReceipt::class.java)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
// Create defensive copy
|
||||
val dataCopy = data.copyOf()
|
||||
|
||||
// Minimum size: 2 UUIDs (32) + readerID (8) + timestamp (8) + min nickname
|
||||
if (dataCopy.size < 49) return null
|
||||
|
||||
val offset = intArrayOf(0)
|
||||
|
||||
val originalMessageID = dataCopy.readUUID(offset) ?: return null
|
||||
val receiptID = dataCopy.readUUID(offset) ?: return null
|
||||
|
||||
val readerIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
|
||||
val readerID = readerIDData.hexEncodedString()
|
||||
|
||||
val timestamp = dataCopy.readDate(offset) ?: return null
|
||||
val readerNickname = dataCopy.readString(offset) ?: return null
|
||||
|
||||
return ReadReceipt(
|
||||
originalMessageID = originalMessageID,
|
||||
receiptID = receiptID,
|
||||
readerID = readerID,
|
||||
readerNickname = readerNickname,
|
||||
timestamp = timestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.*
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Message types - exact same as iOS version with Noise Protocol support
|
||||
@@ -262,6 +262,7 @@ object BinaryProtocol {
|
||||
return paddedData
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("BinaryProtocol", "Error encoding packet type ${packet.type}: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ class PrivateChatManager(
|
||||
// Use centralized fingerprint management - NO LOCAL STORAGE
|
||||
private val fingerprintManager = PeerFingerprintManager.getInstance()
|
||||
|
||||
// Track received private messages that need read receipts
|
||||
private val unreadReceivedMessages = mutableMapOf<String, MutableList<BitchatMessage>>()
|
||||
|
||||
// MARK: - Private Chat Lifecycle
|
||||
|
||||
fun startPrivateChat(peerID: String, meshService: Any): Boolean {
|
||||
@@ -46,6 +49,9 @@ class PrivateChatManager(
|
||||
// Initialize chat if needed
|
||||
messageManager.initializePrivateChat(peerID)
|
||||
|
||||
// Send read receipts for all unread messages from this peer
|
||||
sendReadReceiptsForPeer(peerID, meshService)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -242,16 +248,56 @@ class PrivateChatManager(
|
||||
fun handleIncomingPrivateMessage(message: BitchatMessage) {
|
||||
message.senderPeerID?.let { senderPeerID ->
|
||||
if (!isPeerBlocked(senderPeerID)) {
|
||||
// Add to private messages
|
||||
messageManager.addPrivateMessage(senderPeerID, message)
|
||||
|
||||
// Track as unread for read receipt purposes
|
||||
val unreadList = unreadReceivedMessages.getOrPut(senderPeerID) { mutableListOf() }
|
||||
unreadList.add(message)
|
||||
|
||||
Log.d(TAG, "Added received message ${message.id} from $senderPeerID to unread list (${unreadList.size} unread)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send read receipts for all unread messages from a specific peer
|
||||
* Called when the user focuses on a private chat
|
||||
*/
|
||||
fun sendReadReceiptsForPeer(peerID: String, meshService: Any) {
|
||||
val unreadList = unreadReceivedMessages[peerID]
|
||||
if (unreadList.isNullOrEmpty()) {
|
||||
Log.d(TAG, "No unread messages to send read receipts for peer $peerID")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Sending read receipts for ${unreadList.size} unread messages from $peerID")
|
||||
|
||||
// Send read receipt for each unread message
|
||||
unreadList.forEach { message ->
|
||||
try {
|
||||
val method = meshService::class.java.getDeclaredMethod("sendReadReceipt", String::class.java, String::class.java, String::class.java)
|
||||
val myNickname = state.getNicknameValue() ?: "unknown"
|
||||
method.invoke(meshService, message.id, peerID, myNickname)
|
||||
Log.d(TAG, "Sent read receipt for message ${message.id} to $peerID")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to send read receipt for message ${message.id}: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the unread list since we've sent read receipts
|
||||
unreadReceivedMessages.remove(peerID)
|
||||
}
|
||||
|
||||
fun cleanupDisconnectedPeer(peerID: String) {
|
||||
// End private chat if peer disconnected
|
||||
if (state.getSelectedPrivateChatPeerValue() == peerID) {
|
||||
endPrivateChat()
|
||||
}
|
||||
|
||||
// Clean up unread messages for disconnected peer
|
||||
unreadReceivedMessages.remove(peerID)
|
||||
Log.d(TAG, "Cleaned up unread messages for disconnected peer $peerID")
|
||||
}
|
||||
|
||||
// MARK: - Utility Functions
|
||||
@@ -284,6 +330,9 @@ class PrivateChatManager(
|
||||
state.setSelectedPrivateChatPeer(null)
|
||||
state.setUnreadPrivateMessages(emptySet())
|
||||
|
||||
// Clear unread messages tracking
|
||||
unreadReceivedMessages.clear()
|
||||
|
||||
// Clear fingerprints via centralized manager (only if needed for emergency clear)
|
||||
// Note: This will be handled by the parent PeerManager.clearAllPeers()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user