diff --git a/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt b/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt index ceb87a91..8d036b4b 100644 --- a/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt +++ b/app/src/main/java/com/bitchat/android/services/meshgraph/MeshGraphService.kt @@ -6,18 +6,18 @@ import kotlinx.coroutines.flow.asStateFlow import java.util.concurrent.ConcurrentHashMap /** - * Maintains an internal undirected graph of the mesh based on gossip. + * Maintains an internal graph of the mesh based on gossip. * Nodes are peers (peerID), edges are direct connections. */ class MeshGraphService private constructor() { data class GraphNode(val peerID: String, val nickname: String?) - data class GraphEdge(val a: String, val b: String) + data class GraphEdge(val a: String, val b: String, val isConfirmed: Boolean, val confirmedBy: String? = null) data class GraphSnapshot(val nodes: List, val edges: List) // Map peerID -> nickname (may be null if unknown) private val nicknames = ConcurrentHashMap() - // Adjacency (undirected): peerID -> set of neighbor peerIDs - private val adjacency = ConcurrentHashMap>() + // Announcements: peerID -> set of neighbor peerIDs that *this* peer claims to see + private val announcements = ConcurrentHashMap>() // Latest announcement timestamp per peer (ULong from packet) private val lastUpdate = ConcurrentHashMap() @@ -47,20 +47,10 @@ class MeshGraphService private constructor() { } lastUpdate[originPeerID] = timestamp - // Remove old symmetric edges contributed by this origin - val prevNeighbors = adjacency[originPeerID]?.toSet().orEmpty() - prevNeighbors.forEach { n -> - adjacency[n]?.remove(originPeerID) - } - - // Replace origin's adjacency with new set (may be empty) - val newSet = neighborsOrNull.distinct().take(10).filter { it != originPeerID }.toMutableSet() - adjacency[originPeerID] = newSet - // Ensure undirected edges - newSet.forEach { n -> - adjacency.putIfAbsent(n, mutableSetOf()) - adjacency[n]?.add(originPeerID) - } + // Update what originPeerID announces + // Filter out self-loops just in case + val newSet = neighborsOrNull.distinct().take(10).filter { it != originPeerID }.toSet() + announcements[originPeerID] = newSet publishSnapshot() } @@ -73,24 +63,43 @@ class MeshGraphService private constructor() { } private fun publishSnapshot() { - val nodes = mutableSetOf() - adjacency.forEach { (a, neighbors) -> - nodes.add(a) - nodes.addAll(neighbors) + // Collect all known nodes from nicknames and announcements + val allNodes = mutableSetOf() + allNodes.addAll(nicknames.keys) + announcements.forEach { (origin, neighbors) -> + allNodes.add(origin) + allNodes.addAll(neighbors) } - // Merge in nicknames-only nodes - nodes.addAll(nicknames.keys) - val nodeList = nodes.map { GraphNode(it, nicknames[it]) }.sortedBy { it.peerID } - val edgeSet = mutableSetOf>() - adjacency.forEach { (a, ns) -> - ns.forEach { b -> - val (x, y) = if (a <= b) a to b else b to a - edgeSet.add(x to y) + val nodeList = allNodes.map { GraphNode(it, nicknames[it]) }.sortedBy { it.peerID } + + val edges = mutableListOf() + val processedPairs = mutableSetOf>() + + // We only care about connections that exist in at least one direction. + // So iterating through all entries in `announcements` covers every declared edge. + announcements.forEach { (source, targets) -> + targets.forEach { target -> + val pair = if (source <= target) source to target else target to source + if (processedPairs.add(pair)) { + // This is a new pair we haven't evaluated yet + val (a, b) = pair + val aAnnouncesB = announcements[a]?.contains(b) == true + val bAnnouncesA = announcements[b]?.contains(a) == true + + if (aAnnouncesB && bAnnouncesA) { + edges.add(GraphEdge(a, b, isConfirmed = true)) + } else if (aAnnouncesB) { + edges.add(GraphEdge(a, b, isConfirmed = false, confirmedBy = a)) + } else if (bAnnouncesA) { + edges.add(GraphEdge(a, b, isConfirmed = false, confirmedBy = b)) + } + } } } - val edges = edgeSet.map { GraphEdge(it.first, it.second) }.sortedWith(compareBy({ it.a }, { it.b })) - _graphState.value = GraphSnapshot(nodeList, edges) + + val sortedEdges = edges.sortedWith(compareBy({ it.a }, { it.b })) + _graphState.value = GraphSnapshot(nodeList, sortedEdges) } companion object { diff --git a/app/src/main/java/com/bitchat/android/services/meshgraph/RoutePlanner.kt b/app/src/main/java/com/bitchat/android/services/meshgraph/RoutePlanner.kt index ae279405..78624f87 100644 --- a/app/src/main/java/com/bitchat/android/services/meshgraph/RoutePlanner.kt +++ b/app/src/main/java/com/bitchat/android/services/meshgraph/RoutePlanner.kt @@ -17,7 +17,9 @@ object RoutePlanner { if (src == dst) return listOf(src) val snapshot = MeshGraphService.getInstance().graphState.value val neighbors = mutableMapOf>() - snapshot.edges.forEach { e -> + + // Only consider confirmed edges for routing + snapshot.edges.filter { it.isConfirmed }.forEach { e -> neighbors.getOrPut(e.a) { mutableSetOf() }.add(e.b) neighbors.getOrPut(e.b) { mutableSetOf() }.add(e.a) } @@ -63,4 +65,3 @@ object RoutePlanner { return path } } - diff --git a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt index 25be4a21..760bdb12 100644 --- a/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/debug/DebugSettingsSheet.kt @@ -80,7 +80,29 @@ fun MeshTopologySection() { val p1 = positions[e.a] val p2 = positions[e.b] if (p1 != null && p2 != null) { - drawLine(color = Color(0xFF4A90E2), start = p1, end = p2, strokeWidth = 2f) + if (e.isConfirmed) { + drawLine(color = Color(0xFF4A90E2), start = p1, end = p2, strokeWidth = 2f) + } else { + // Unconfirmed: draw "solid" from declarer, "dashed" from other + val start = if (e.confirmedBy == e.a) p1 else p2 + val end = if (e.confirmedBy == e.a) p2 else p1 + + val midX = (start.x + end.x) / 2 + val midY = (start.y + end.y) / 2 + val mid = androidx.compose.ui.geometry.Offset(midX, midY) + + // Solid half + drawLine(color = Color(0xFF4A90E2), start = start, end = mid, strokeWidth = 2f) + + // Dotted half + drawLine( + color = Color(0xFF4A90E2), + start = mid, + end = end, + strokeWidth = 2f, + pathEffect = androidx.compose.ui.graphics.PathEffect.dashPathEffect(floatArrayOf(5f, 5f), 0f) + ) + } } }