Nip17 dms to extend the mesh (#313)

* favorite each other

* show favorites as system messsages

* wip show glove when offline but mayb edoesnt work

* send and receive works

* wip kinda works but no

* getting there

* kinda

* show offline peers in peer list

* kinda wonky but almost works

* fixes

* nostr user goes offline works

* nostr -> mesh works

* handoff works

* background message processing

* read works

* seen message store was missing
This commit is contained in:
callebtc
2025-08-25 18:38:10 +02:00
committed by GitHub
parent 941be1b98f
commit 6fe6e049ad
15 changed files with 1153 additions and 173 deletions
@@ -86,18 +86,100 @@ class MeshDelegateHandler(
coroutineScope.launch {
state.setConnectedPeers(peers)
state.setIsConnected(peers.isNotEmpty())
// Flush router outbox for any peers that just connected (and their noiseHex aliases)
runCatching { com.bitchat.android.services.MessageRouter.tryGetInstance()?.onPeersUpdated(peers) }
// Clean up channel members who disconnected
channelManager.cleanupDisconnectedMembers(peers, getMyPeerID())
// Exit private chat if peer disconnected
// Handle chat view migration based on current selection and new peer list
state.getSelectedPrivateChatPeerValue()?.let { currentPeer ->
if (!peers.contains(currentPeer)) {
privateChatManager.cleanupDisconnectedPeer(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 = peers,
meshNoiseKeyForPeer = { pid -> getPeerInfo(pid)?.noisePublicKey },
meshHasPeer = { pid -> peers.contains(pid) },
nostrPubHexForAlias = { alias ->
// Best-effort: derive pub hex from favorites mapping
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 && !peers.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.
peers.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 {
@@ -191,4 +273,11 @@ class MeshDelegateHandler(
}
// 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)
}
}