mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-15 06:56:30 +00:00
* feat: stabilize private chat identity across mesh and Nostr Introduce ContactDirectory and ContactIdentityResolver so DMs and favorites use stable contact_* conversation IDs instead of ephemeral mesh peer IDs. * fix: preserve canonical private message routing * test: complete private chat regression fixtures
92 lines
3.7 KiB
Kotlin
92 lines
3.7 KiB
Kotlin
package com.bitchat.android.services
|
|
|
|
import com.bitchat.android.ui.ChatState
|
|
|
|
object ConversationAliasResolver {
|
|
|
|
fun resolveCanonicalPeerID(
|
|
selectedPeerID: String,
|
|
connectedPeers: List<String>,
|
|
meshNoiseKeyForPeer: (String) -> ByteArray?,
|
|
nostrPubHexForAlias: (String) -> String?,
|
|
findNoiseKeyForNostr: (String) -> ByteArray?
|
|
): String {
|
|
var peer = selectedPeerID
|
|
try {
|
|
if (ContactIdentityResolver.isNostrAlias(peer)) {
|
|
val pubHex = nostrPubHexForAlias(peer)
|
|
if (pubHex != null) {
|
|
val noiseKey = findNoiseKeyForNostr(pubHex)
|
|
if (noiseKey != null) {
|
|
val noiseHex = ContactIdentityResolver.noiseKeyHex(noiseKey)
|
|
val meshPeer = connectedPeers.firstOrNull { pid ->
|
|
meshNoiseKeyForPeer(pid)?.contentEquals(noiseKey) == true
|
|
}
|
|
peer = meshPeer ?: noiseHex
|
|
}
|
|
}
|
|
} else if (ContactIdentityResolver.isNoiseKeyHex(peer)) {
|
|
val noiseKey = ContactIdentityResolver.bytesFromHex(peer) ?: return peer
|
|
val meshPeer = connectedPeers.firstOrNull { pid ->
|
|
meshNoiseKeyForPeer(pid)?.contentEquals(noiseKey) == true
|
|
}
|
|
if (meshPeer != null) {
|
|
peer = meshPeer
|
|
}
|
|
}
|
|
} catch (_: Exception) { /* no-op */ }
|
|
return ContactDirectory.canonicalConversationId(peer)
|
|
}
|
|
|
|
fun unifyChatsIntoPeer(
|
|
state: ChatState,
|
|
targetPeerID: String,
|
|
keysToMerge: List<String>
|
|
) {
|
|
if (keysToMerge.isEmpty()) return
|
|
val targetConversationID = ContactDirectory.canonicalConversationId(targetPeerID)
|
|
val mergeKeys = (keysToMerge + targetPeerID)
|
|
.flatMap { ContactDirectory.aliasesForConversation(it) }
|
|
.distinct()
|
|
AppStateStore.unifyPrivateChatsIntoPeer(targetConversationID, mergeKeys)
|
|
|
|
val currentChats = state.getPrivateChatsValue().toMutableMap()
|
|
val targetList = currentChats[targetConversationID]?.toMutableList() ?: mutableListOf()
|
|
var didMerge = false
|
|
mergeKeys.forEach { key ->
|
|
if (key == targetConversationID) return@forEach
|
|
val list = currentChats[key]
|
|
if (!list.isNullOrEmpty()) {
|
|
targetList.addAll(list)
|
|
currentChats.remove(key)
|
|
didMerge = true
|
|
}
|
|
}
|
|
if (didMerge) {
|
|
currentChats[targetConversationID] = targetList
|
|
.distinctBy { it.id }
|
|
.sortedBy { it.timestamp.time }
|
|
state.setPrivateChats(ContactDirectory.canonicalizePrivateChats(currentChats))
|
|
|
|
// Move unread flags
|
|
val unread = state.getUnreadPrivateMessagesValue().toMutableSet()
|
|
var hadUnread = false
|
|
mergeKeys.forEach { key -> if (unread.remove(key)) hadUnread = true }
|
|
if (hadUnread) unread.add(targetConversationID)
|
|
state.setUnreadPrivateMessages(unread)
|
|
|
|
// Switch selection if currently viewing an alias that got merged
|
|
val selected = state.getSelectedPrivateChatPeerValue()
|
|
if (selected != null && mergeKeys.contains(selected)) {
|
|
state.setSelectedPrivateChatPeer(targetConversationID)
|
|
}
|
|
|
|
// Switch sheet peer if currently viewing an alias that got merged
|
|
val sheetPeer = state.getPrivateChatSheetPeerValue()
|
|
if (sheetPeer != null && mergeKeys.contains(sheetPeer)) {
|
|
state.setPrivateChatSheetPeer(targetConversationID)
|
|
}
|
|
}
|
|
}
|
|
}
|