mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-15 06:56:30 +00:00
* wifi aware wip * wifi aware wip * starting to work * werk * dms work * wip * fix(wifi-aware): use bindSocket and scoped IPv6 instead of bindProcessToNetwork * Merge branch 'upstream/main' into fix/wifi-aware-socket-binding * Fix Wi-Fi Aware connectivity and UI integration post-merge - Replace bindProcessToNetwork with bindSocket for VPN compatibility. - Implement Scoped IPv6 address resolution (aware0) for mesh routing. - Bridge Wi-Fi Aware incoming messages to AppStateStore for UI visibility. - Fix syntax errors and variable name conflicts in Debug UI. * Enhance Wi-Fi Aware robustness and debug UI display - Clean up transport resources (sockets, server sockets, network callbacks) immediately on peer disconnection. - Implement resolveScopedAddress to show scoped IPv6 (e.g., %aware0) in Debug UI. - Fix Map type mismatch warning in ChatViewModel bridge. - Filter self-ID from peer cleanup tables to prevent recursive self-removal. * Share GossipSyncManager across transports to prevent redundant message synchronization - Registered BluetoothMeshService's GossipSyncManager as a singleton in MeshServiceHolder. - Modified WifiAwareMeshService to use the shared GossipSyncManager if available. - Added background cleanup for peer mappings on socket disconnection. - Fixed Kotlin type mismatch during nickname map merging. * Restore VPN acquisition logic and improve peer cleanup - Revert removal of NET_CAPABILITY_NOT_VPN to allow hardware handle acquisition while VPN is active. - Refactor handlePeerDisconnection to more reliably cleanup initial and routed IDs. - Switch cleanup logging to debug level to reduce log noise. * Fix wifi aware socket binding 2 (#536) * Background persistence (#505) * persistence step 1 * fix build * messages in the background work, notifications not yet * app state store * DM icon shows up * notification launches when app is closed! * keep ui updated * lifecycle fixes * extensive logging, maybe revert later * send nickname in announcement * quit in notification * setting in about sheet * fix quit bitchat * lifecycle fixes * power mode based on background state * stats for both direciotns * fix graph persistence * better counting * count per device * only compute when debug sheet is open? untested * fix read receipts * fix read receipts fully * fix unread badge if messages have been read in focus * foreground promotion fix * fix app kill in notification * adjust to new tor * nice * about sheet design * bump version 1.6.0 (#524) * Automated update of relay data - Sun Dec 14 06:06:53 UTC 2025 * bump targetSdk (#526) * Automated update of relay data - Sun Dec 21 06:06:56 UTC 2025 * Automated update of relay data - Sun Dec 28 06:07:12 UTC 2025 * Prevent quit notification from reappearing (#530) * shutdown sequence * Prevent quit notification from reappearing * Restrict force-finish broadcast * Cancel quit shutdown on relaunch * fix(wifi-aware): use bindSocket and scoped IPv6 instead of bindProcessToNetwork * Merge branch 'upstream/main' into fix/wifi-aware-socket-binding * Fix Wi-Fi Aware connectivity and UI integration post-merge - Replace bindProcessToNetwork with bindSocket for VPN compatibility. - Implement Scoped IPv6 address resolution (aware0) for mesh routing. - Bridge Wi-Fi Aware incoming messages to AppStateStore for UI visibility. - Fix syntax errors and variable name conflicts in Debug UI. * Enhance Wi-Fi Aware robustness and debug UI display - Clean up transport resources (sockets, server sockets, network callbacks) immediately on peer disconnection. - Implement resolveScopedAddress to show scoped IPv6 (e.g., %aware0) in Debug UI. - Fix Map type mismatch warning in ChatViewModel bridge. - Filter self-ID from peer cleanup tables to prevent recursive self-removal. * Share GossipSyncManager across transports to prevent redundant message synchronization - Registered BluetoothMeshService's GossipSyncManager as a singleton in MeshServiceHolder. - Modified WifiAwareMeshService to use the shared GossipSyncManager if available. - Added background cleanup for peer mappings on socket disconnection. - Fixed Kotlin type mismatch during nickname map merging. * Restore VPN acquisition logic and improve peer cleanup - Revert removal of NET_CAPABILITY_NOT_VPN to allow hardware handle acquisition while VPN is active. - Refactor handlePeerDisconnection to more reliably cleanup initial and routed IDs. - Switch cleanup logging to debug level to reduce log noise. --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: aidenvalue <> * Fix wifi aware routing (#534) * Background persistence (#505) * persistence step 1 * fix build * messages in the background work, notifications not yet * app state store * DM icon shows up * notification launches when app is closed! * keep ui updated * lifecycle fixes * extensive logging, maybe revert later * send nickname in announcement * quit in notification * setting in about sheet * fix quit bitchat * lifecycle fixes * power mode based on background state * stats for both direciotns * fix graph persistence * better counting * count per device * only compute when debug sheet is open? untested * fix read receipts * fix read receipts fully * fix unread badge if messages have been read in focus * foreground promotion fix * fix app kill in notification * adjust to new tor * nice * about sheet design * bump version 1.6.0 (#524) * Automated update of relay data - Sun Dec 14 06:06:53 UTC 2025 * bump targetSdk (#526) * Automated update of relay data - Sun Dec 21 06:06:56 UTC 2025 * Automated update of relay data - Sun Dec 28 06:07:12 UTC 2025 * Prevent quit notification from reappearing (#530) * shutdown sequence * Prevent quit notification from reappearing * Restrict force-finish broadcast * Cancel quit shutdown on relaunch * fix(wifi-aware): use bindSocket and scoped IPv6 instead of bindProcessToNetwork * Merge branch 'upstream/main' into fix/wifi-aware-socket-binding * Fix Wi-Fi Aware connectivity and UI integration post-merge - Replace bindProcessToNetwork with bindSocket for VPN compatibility. - Implement Scoped IPv6 address resolution (aware0) for mesh routing. - Bridge Wi-Fi Aware incoming messages to AppStateStore for UI visibility. - Fix syntax errors and variable name conflicts in Debug UI. * Enhance Wi-Fi Aware robustness and debug UI display - Clean up transport resources (sockets, server sockets, network callbacks) immediately on peer disconnection. - Implement resolveScopedAddress to show scoped IPv6 (e.g., %aware0) in Debug UI. - Fix Map type mismatch warning in ChatViewModel bridge. - Filter self-ID from peer cleanup tables to prevent recursive self-removal. * Share GossipSyncManager across transports to prevent redundant message synchronization - Registered BluetoothMeshService's GossipSyncManager as a singleton in MeshServiceHolder. - Modified WifiAwareMeshService to use the shared GossipSyncManager if available. - Added background cleanup for peer mappings on socket disconnection. - Fixed Kotlin type mismatch during nickname map merging. * Restore VPN acquisition logic and improve peer cleanup - Revert removal of NET_CAPABILITY_NOT_VPN to allow hardware handle acquisition while VPN is active. - Refactor handlePeerDisconnection to more reliably cleanup initial and routed IDs. - Switch cleanup logging to debug level to reduce log noise. * fix wifi aware routing --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: aidenvalue <> * unify connection tracker for ble and wifi (#537) * unify connection tracker for ble and wifi * cleanup nicely * Wifi aware skip bluetooth (#538) * unify connection tracker for ble and wifi * bluetooth optional * refactor mesh core * tests * share gossip * refresh peer list more often * patches: wifi aware mesh refactor core (#549) * Refactor WifiAware memory management and lifecycle handling * Fix Wi-Fi Aware reconnection by ensuring callback unregistration and session watchdog * fix(wifiaware): allow WifiAwareController to restart service if session drops * Enhance WiFi Aware logging with network request timeouts and detailed callback status * Ensure explicit release of WiFi Aware network callbacks on failure or disconnection --------- Co-authored-by: aidenvalue <> * fix: wifi aware mesh private dms (#561) * fix(wifi-aware): restore peer check and remove aggressive session restarts * synchronized access to sockets. * clear old peer socket `onClientConnected` if one was found. * address second round of review * fix: Restart publish session on termination in WifiAwareMeshService * fix: cleanup connection tracker resources when handling peer disconnection without active socket --------- Co-authored-by: aidenvalu3 <> * fix(mesh): address memory leaks and background persistence issues in Wi-Fi Aware - Move message persistence and background notifications for Wi-Fi Aware into the service layer via MeshCore hooks. - Fix memory leak in MainActivity by properly detaching WifiAwareMeshDelegate on pause. - Ensure BluetoothMeshService is initialized before WifiAwareMeshService to guarantee shared GossipSyncManager instance. - Deduplicate persistence logic from MainActivity delegate. * wifi aware working * more robust * fragments work * new files * wifi improvements * handshake works * improve * wifi mesh more robust * fix review * check for support * wifi aware fixes --------- Co-authored-by: aidenvalue <> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: aidenvalu3 <erendentman@gmail.com> Co-authored-by: a1denvalu3 <43107113+a1denvalu3@users.noreply.github.com> Co-authored-by: CC <cc@ggg.local>
338 lines
16 KiB
Kotlin
338 lines
16 KiB
Kotlin
package com.bitchat.android.ui
|
|
|
|
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
|
import com.bitchat.android.ui.NotificationTextUtils
|
|
import com.bitchat.android.mesh.MeshService
|
|
import com.bitchat.android.model.BitchatMessage
|
|
import com.bitchat.android.model.DeliveryStatus
|
|
import kotlinx.coroutines.CoroutineScope
|
|
import kotlinx.coroutines.launch
|
|
import java.util.Date
|
|
|
|
/**
|
|
* Handles all BluetoothMeshDelegate callbacks and routes them to appropriate managers
|
|
*/
|
|
class MeshDelegateHandler(
|
|
private val state: ChatState,
|
|
private val messageManager: MessageManager,
|
|
private val channelManager: ChannelManager,
|
|
private val privateChatManager: PrivateChatManager,
|
|
private val notificationManager: NotificationManager,
|
|
private val coroutineScope: CoroutineScope,
|
|
private val onHapticFeedback: () -> Unit,
|
|
private val getMyPeerID: () -> String,
|
|
private val getMeshService: () -> MeshService
|
|
) : BluetoothMeshDelegate {
|
|
|
|
override fun didReceiveMessage(message: BitchatMessage) {
|
|
coroutineScope.launch {
|
|
// FIXED: Deduplicate messages from dual connection paths
|
|
val messageKey = messageManager.generateMessageKey(message)
|
|
if (messageManager.isMessageProcessed(messageKey)) {
|
|
return@launch // Duplicate message, ignore
|
|
}
|
|
messageManager.markMessageProcessed(messageKey)
|
|
|
|
// Check if sender is blocked
|
|
message.senderPeerID?.let { senderPeerID ->
|
|
if (privateChatManager.isPeerBlocked(senderPeerID)) {
|
|
return@launch
|
|
}
|
|
}
|
|
|
|
// Trigger haptic feedback
|
|
onHapticFeedback()
|
|
|
|
if (message.isPrivate) {
|
|
// Private message
|
|
privateChatManager.handleIncomingPrivateMessage(message)
|
|
|
|
// Reactive read receipts: if chat is focused, send immediately for this message
|
|
message.senderPeerID?.let { senderPeerID ->
|
|
sendReadReceiptIfFocused(message)
|
|
}
|
|
|
|
// Show notification with enhanced information - now includes senderPeerID
|
|
message.senderPeerID?.let { senderPeerID ->
|
|
// Use nickname if available, fall back to sender or senderPeerID
|
|
val senderNickname = message.sender.takeIf { it != senderPeerID } ?: senderPeerID
|
|
val preview = NotificationTextUtils.buildPrivateMessagePreview(message)
|
|
notificationManager.showPrivateMessageNotification(
|
|
senderPeerID = senderPeerID,
|
|
senderNickname = senderNickname,
|
|
messageContent = preview
|
|
)
|
|
}
|
|
} else if (message.channel != null) {
|
|
// Channel message: AppStateStore is the source of truth for list; only manage unread
|
|
if (state.getJoinedChannelsValue().contains(message.channel)) {
|
|
val channel = message.channel
|
|
val viewingClassic = state.getCurrentChannelValue() == channel
|
|
val viewingGeohash = try {
|
|
if (channel.startsWith("geo:")) {
|
|
val geo = channel.removePrefix("geo:")
|
|
val selected = state.selectedLocationChannel.value
|
|
selected is com.bitchat.android.geohash.ChannelID.Location && selected.channel.geohash.equals(geo, ignoreCase = true)
|
|
} else false
|
|
} catch (_: Exception) { false }
|
|
if (!viewingClassic && !viewingGeohash) {
|
|
val currentUnread = state.getUnreadChannelMessagesValue().toMutableMap()
|
|
currentUnread[channel] = (currentUnread[channel] ?: 0) + 1
|
|
state.setUnreadChannelMessages(currentUnread)
|
|
}
|
|
}
|
|
} else {
|
|
// Public mesh message: AppStateStore is the source of truth; avoid double-adding to UI state
|
|
// Still run mention detection/notifications
|
|
checkAndTriggerMeshMentionNotification(message)
|
|
}
|
|
|
|
// Periodic cleanup
|
|
if (messageManager.isMessageProcessed("cleanup_check_${System.currentTimeMillis()/30000}")) {
|
|
messageManager.cleanupDeduplicationCaches()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun didUpdatePeerList(peers: List<String>) {
|
|
coroutineScope.launch {
|
|
processPeerUpdate(peers.distinct())
|
|
}
|
|
}
|
|
|
|
private suspend fun processPeerUpdate(mergedPeers: List<String>) {
|
|
state.setConnectedPeers(mergedPeers)
|
|
state.setIsConnected(mergedPeers.isNotEmpty())
|
|
notificationManager.showActiveUserNotification(mergedPeers)
|
|
|
|
// Flush router outbox for any peers that just connected (and their noiseHex aliases)
|
|
runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance()?.onPeersUpdated(mergedPeers) }
|
|
|
|
// Clean up channel members who disconnected
|
|
channelManager.cleanupDisconnectedMembers(mergedPeers, getMyPeerID())
|
|
|
|
// Handle chat view migration based on current selection and new peer list
|
|
state.getSelectedPrivateChatPeerValue()?.let { currentPeer ->
|
|
val isNostrAlias = currentPeer.startsWith("nostr_")
|
|
val isNoiseHex = currentPeer.length == 64 && currentPeer.matches(Regex("^[0-9a-fA-F]+$"))
|
|
val isMeshEphemeral = currentPeer.length == 16 && currentPeer.matches(Regex("^[0-9a-fA-F]+$"))
|
|
|
|
if (isNostrAlias || isNoiseHex) {
|
|
// Reverse case: Nostr/offline chat is open, and peer may have come online on mesh.
|
|
// Resolve canonical target (prefer connected mesh peer if available)
|
|
val canonical = com.bitchat.android.services.ConversationAliasResolver.resolveCanonicalPeerID(
|
|
selectedPeerID = currentPeer,
|
|
connectedPeers = mergedPeers,
|
|
meshNoiseKeyForPeer = { pid -> getPeerInfo(pid)?.noisePublicKey },
|
|
meshHasPeer = { pid -> mergedPeers.contains(pid) },
|
|
nostrPubHexForAlias = { alias ->
|
|
// Use GeohashAliasRegistry for geohash aliases, but for mesh favorites, derive from favorites mapping
|
|
if (com.bitchat.android.nostr.GeohashAliasRegistry.contains(alias)) {
|
|
com.bitchat.android.nostr.GeohashAliasRegistry.get(alias)
|
|
} else {
|
|
// Best-effort: derive pub hex from favorites mapping for mesh nostr_ aliases
|
|
val prefix = alias.removePrefix("nostr_")
|
|
val favs = try { com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites() } catch (_: Exception) { emptyList() }
|
|
favs.firstNotNullOfOrNull { rel ->
|
|
rel.peerNostrPublicKey?.let { s ->
|
|
runCatching { com.bitchat.android.nostr.Bech32.decode(s) }.getOrNull()?.let { dec ->
|
|
if (dec.first == "npub") dec.second.joinToString("") { b -> "%02x".format(b) } else null
|
|
}
|
|
}
|
|
}?.takeIf { it.startsWith(prefix, ignoreCase = true) }
|
|
}
|
|
},
|
|
findNoiseKeyForNostr = { key -> com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNoiseKey(key) }
|
|
)
|
|
if (canonical != currentPeer) {
|
|
// Merge conversations and switch selection to the live mesh peer (or noiseHex)
|
|
com.bitchat.android.services.ConversationAliasResolver.unifyChatsIntoPeer(state, canonical, listOf(currentPeer))
|
|
state.setSelectedPrivateChatPeer(canonical)
|
|
}
|
|
} else if (isMeshEphemeral && !mergedPeers.contains(currentPeer)) {
|
|
// Forward case: Mesh chat lost connection. If mutual favorite exists, migrate to Nostr (noiseHex)
|
|
val favoriteRel = try {
|
|
val info = getPeerInfo(currentPeer)
|
|
val noiseKey = info?.noisePublicKey
|
|
if (noiseKey != null) {
|
|
com.bitchat.android.favorites.FavoritesPersistenceService.shared.getFavoriteStatus(noiseKey)
|
|
} else null
|
|
} catch (_: Exception) { null }
|
|
|
|
if (favoriteRel?.isMutual == true) {
|
|
val noiseHex = favoriteRel.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
|
|
if (noiseHex != currentPeer) {
|
|
com.bitchat.android.services.ConversationAliasResolver.unifyChatsIntoPeer(
|
|
state = state,
|
|
targetPeerID = noiseHex,
|
|
keysToMerge = listOf(currentPeer)
|
|
)
|
|
state.setSelectedPrivateChatPeer(noiseHex)
|
|
}
|
|
} else {
|
|
privateChatManager.cleanupDisconnectedPeer(currentPeer)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Global unification: for each connected peer, merge any offline/stable conversations
|
|
// (noiseHex or nostr_<pub16>) into the connected peer's chat so there is only one chat per identity.
|
|
mergedPeers.forEach { pid ->
|
|
try {
|
|
val info = getPeerInfo(pid)
|
|
val noiseKey = info?.noisePublicKey ?: return@forEach
|
|
val noiseHex = noiseKey.joinToString("") { b -> "%02x".format(b) }
|
|
|
|
// Derive temp nostr key from favorites npub
|
|
val npub = com.bitchat.android.favorites.FavoritesPersistenceService.shared.findNostrPubkey(noiseKey)
|
|
val tempNostrKey: String? = try {
|
|
if (npub != null) {
|
|
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(npub)
|
|
if (hrp == "npub") "nostr_${data.joinToString("") { b -> "%02x".format(b) }.take(16)}" else null
|
|
} else null
|
|
} catch (_: Exception) { null }
|
|
|
|
unifyChatsIntoPeer(pid, listOfNotNull(noiseHex, tempNostrKey))
|
|
} catch (_: Exception) { }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merge any chats stored under the given keys into the connected peer's chat entry.
|
|
*/
|
|
private fun unifyChatsIntoPeer(targetPeerID: String, keysToMerge: List<String>) {
|
|
com.bitchat.android.services.ConversationAliasResolver.unifyChatsIntoPeer(state, targetPeerID, keysToMerge)
|
|
}
|
|
|
|
override fun didReceiveChannelLeave(channel: String, fromPeer: String) {
|
|
coroutineScope.launch {
|
|
channelManager.removeChannelMember(channel, fromPeer)
|
|
}
|
|
}
|
|
|
|
override fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) {
|
|
coroutineScope.launch {
|
|
messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Delivered(recipientPeerID, Date()))
|
|
}
|
|
}
|
|
|
|
override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) {
|
|
coroutineScope.launch {
|
|
messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Read(recipientPeerID, Date()))
|
|
}
|
|
}
|
|
|
|
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {
|
|
// Handled by ChatViewModel for verification flow
|
|
}
|
|
|
|
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {
|
|
// Handled by ChatViewModel for verification flow
|
|
}
|
|
|
|
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
|
return channelManager.decryptChannelMessage(encryptedContent, channel)
|
|
}
|
|
|
|
override fun getNickname(): String? = state.getNicknameValue()
|
|
|
|
override fun isFavorite(peerID: String): Boolean {
|
|
return privateChatManager.isFavorite(peerID)
|
|
}
|
|
|
|
/**
|
|
* Check for mentions in mesh messages and trigger notifications
|
|
*/
|
|
private fun checkAndTriggerMeshMentionNotification(message: BitchatMessage) {
|
|
try {
|
|
// Get user's current nickname
|
|
val currentNickname = state.getNicknameValue()
|
|
if (currentNickname.isNullOrEmpty()) {
|
|
return
|
|
}
|
|
|
|
// Check if this message mentions the current user using @username format
|
|
val isMention = checkForMeshMention(message.content, currentNickname)
|
|
|
|
if (isMention) {
|
|
android.util.Log.d("MeshDelegateHandler", "🔔 Triggering mesh mention notification from ${message.sender}")
|
|
|
|
notificationManager.showMeshMentionNotification(
|
|
senderNickname = message.sender,
|
|
messageContent = message.content,
|
|
senderPeerID = message.senderPeerID
|
|
)
|
|
}
|
|
} catch (e: Exception) {
|
|
android.util.Log.e("MeshDelegateHandler", "Error checking mesh mentions: ${e.message}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the content mentions the current user with @username format (simple, no hash suffix)
|
|
*/
|
|
private fun checkForMeshMention(content: String, currentNickname: String): Boolean {
|
|
// Simple mention pattern for mesh: @username (no hash suffix like geohash)
|
|
val mentionPattern = "@([\\p{L}0-9_]+)".toRegex()
|
|
|
|
return mentionPattern.findAll(content).any { match ->
|
|
val mentionedUsername = match.groupValues[1]
|
|
// Direct comparison for mesh mentions (no hash suffix to remove)
|
|
mentionedUsername.equals(currentNickname, ignoreCase = true)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send read receipts reactively based on UI focus state.
|
|
* Uses same logic as notification system - send read receipt if user is currently
|
|
* viewing the private chat with this sender AND app is in foreground.
|
|
*/
|
|
private fun sendReadReceiptIfFocused(message: BitchatMessage) {
|
|
// Get notification manager's focus state (mirror the notification logic)
|
|
val isAppInBackground = notificationManager.getAppBackgroundState()
|
|
val currentPrivateChatPeer = notificationManager.getCurrentPrivateChatPeer()
|
|
|
|
// Send read receipt if user is currently focused on this specific chat
|
|
val senderPeerID = message.senderPeerID
|
|
val shouldSendReadReceipt = !isAppInBackground && senderPeerID != null && currentPrivateChatPeer == senderPeerID
|
|
|
|
if (shouldSendReadReceipt) {
|
|
android.util.Log.d("MeshDelegateHandler", "Sending reactive read receipt for focused chat with $senderPeerID (message=${message.id})")
|
|
val nickname = state.getNicknameValue() ?: "unknown"
|
|
val mesh = getMeshService()
|
|
val sent = try {
|
|
val hasMesh = mesh.getPeerInfo(senderPeerID!!)?.isConnected == true && mesh.hasEstablishedSession(senderPeerID)
|
|
if (hasMesh) {
|
|
mesh.sendReadReceipt(message.id, senderPeerID, nickname)
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
} catch (_: Exception) {
|
|
false
|
|
}
|
|
if (sent) {
|
|
// Ensure unread badge is cleared for this peer immediately
|
|
try {
|
|
val current = state.getUnreadPrivateMessagesValue().toMutableSet()
|
|
if (current.remove(senderPeerID)) {
|
|
state.setUnreadPrivateMessages(current)
|
|
}
|
|
} catch (_: Exception) { }
|
|
}
|
|
} else {
|
|
android.util.Log.d("MeshDelegateHandler", "Skipping read receipt - chat not focused (background: $isAppInBackground, current peer: $currentPrivateChatPeer, sender: $senderPeerID)")
|
|
}
|
|
}
|
|
|
|
// registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager
|
|
|
|
/**
|
|
* Expose mesh peer info for components that need to resolve identities (e.g., Nostr mapping)
|
|
*/
|
|
fun getPeerInfo(peerID: String): com.bitchat.android.mesh.PeerInfo? {
|
|
return getMeshService().getPeerInfo(peerID)
|
|
}
|
|
|
|
}
|