topo_group_connect: infinite reconnect cycle + connecting status for GUI

- dedup double ETCP_CONN_STATUS_UP (UDP+TCP links) in topo_group_new_conn
- close conn_mgr handle on TIMEOUT in Phase2/3 (fix stuck 'already connecting')
- infinite Phase1→2→3 cycle with pause (1s active / 30s standby)
- CHAT_EVT_CONNECTING_NODES: yellow circle in member list while connecting
- Android: client_activity wiring via onStart/onStop + nativeSetActive
- function-level comments in topo_group_connect.c
This commit is contained in:
evgeny committed 2026-08-12 18:49:16 +03:00
1 parent 54ad03185b
commit 0492b19468
9 files changed
+159 -6

No files matched your search

@@ -146,4 +146,14 @@ class MainActivity : ComponentActivity() {
}
}
}
override fun onStart() {
super.onStart()
if (NativeLib.isReady()) NativeLib.setActive(true)
}
override fun onStop() {
super.onStop()
if (NativeLib.isReady()) NativeLib.setActive(false)
}
}
@@ -68,7 +68,7 @@ data class ChatMember(val nodeId: Long, val name: String, val online: Boolean =
val directUp: Boolean = false, val indirectUp: Boolean = false,
val directPresence: Boolean = false, val indirectPresence: Boolean = false,
val bgpPresence: Boolean = false, val bgpRtt: Int = 0,
val bgpNodes: Int = 0) {
val bgpNodes: Int = 0, val connecting: Boolean = false) {
val flags: Int get() = (if (isSuper) 1 else 0) or (if (isAdmin) 2 else 0) or (if (isModer) 4 else 0)
}
@@ -53,6 +53,9 @@ object NativeLib {
fun ping() { nativePing() }
fun isResponsive(): Boolean = nativeIsResponsive()
/* ── App foreground/background (Android lifecycle) ── */
fun setActive(active: Boolean) { nativeSetActive(active) }
fun sendMessage(channelId: String, text: String) { nativeSendMessage(channelId, text) }
fun createChannel(name: String, channelId: String) { nativeCreateChannel(name, channelId) }
fun connectNode(address: String, port: Int, pubkeyHex: String) { nativeConnectNode(address, port, pubkeyHex) }
@@ -140,6 +143,7 @@ object NativeLib {
private external fun nativePing()
private external fun nativeIsResponsive(): Boolean
private external fun nativeIsRunning(): Boolean
private external fun nativeSetActive(active: Boolean)
/* ── Channel members JNI ── */
private external fun nativeGetChannelMembers(channelId: String): String?
@@ -122,11 +122,13 @@ private fun MemberListItem(
onEdit: () -> Unit
) {
val onlineColor = when {
member.connecting -> Color(0xFFFFEB3B) // yellow: connecting
member.directUp || member.indirectUp -> Color(0xFF4CAF50) // green: up
member.directPresence || member.indirectPresence || member.bgpPresence -> Color(0xFFFFA726) // orange: presence but down
else -> Color(0xFF9E9E9E) // gray: offline
}
val connLabel = when {
member.connecting -> "connecting"
member.directUp -> "direct"
member.indirectUp -> "indirect"
member.directPresence || member.indirectPresence || member.bgpPresence -> "offline"
@@ -171,6 +173,7 @@ private fun MemberListItem(
if (member.isSuper) parts.add("Super")
if (member.isStorage) parts.add("Storage")
if (member.isSelf) parts.add("self")
else if (member.connecting) parts.add("connecting…")
else if (member.directUp) parts.add("direct↑")
else if (member.indirectUp) parts.add("indirect↑")
else if (member.directPresence || member.indirectPresence || member.bgpPresence) parts.add("down")
@@ -65,6 +65,9 @@ class ChatViewModel : ViewModel() {
private val _nodeStatuses = MutableStateFlow<Map<Long, NodeStatus>>(emptyMap())
val nodeStatuses: StateFlow<Map<Long, NodeStatus>> = _nodeStatuses
private val _connecting = MutableStateFlow<Map<String, Set<Long>>>(emptyMap())
val connecting: StateFlow<Map<String, Set<Long>>> = _connecting
private val _localSockets = MutableStateFlow<List<LocalSocketState>>(emptyList())
val localSockets: StateFlow<List<LocalSocketState>> = _localSockets
@@ -281,6 +284,24 @@ class ChatViewModel : ViewModel() {
_selectedMember.value = _selectedMember.value?.copy(online = ml.connected && ml.linksUp > 0)
}
}
27 -> { /* CONNECTING_NODES: [ch_id_len:1][ch_id:var][count:2][node_id:8]* */
if (data == null || data.size < 3) return
val cl = data[0].toInt() and 0xFF
if (data.size < 1 + cl + 2) return
val chId = String(data, 1, cl)
val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, data.size - 1 - cl).order(java.nio.ByteOrder.LITTLE_ENDIAN)
val count = buf.getShort().toInt() and 0xFFFF
val set = HashSet<Long>()
for (i in 0 until count) {
if (buf.position() + 8 > data.size) break
set.add(buf.getLong())
}
_connecting.value = _connecting.value + (chId to set)
val cur = _currentChannel.value
if (cur != null && cur.id == chId) {
_members.value = _members.value.map { it.copy(connecting = it.nodeId in set) }
}
}
}
}
@@ -324,17 +345,19 @@ class ChatViewModel : ViewModel() {
fun refreshMembers(chId: String) {
val r = repo ?: return
val statuses = _nodeStatuses.value
val connecting = _connecting.value[chId] ?: emptySet()
viewModelScope.launch {
val raw = withContext(Dispatchers.IO) { r.getMembers(chId) }
_members.value = raw.map { m ->
val ns = statuses[m.nodeId]
if (ns != null) m.copy(
val base = if (ns != null) m.copy(
online = ns.online, rtt = ns.bestRtt,
directUp = ns.directUp, indirectUp = ns.indirectUp,
directPresence = ns.directPresence, indirectPresence = ns.indirectPresence,
bgpPresence = ns.bgpPresence, bgpRtt = ns.bgpRtt,
bgpNodes = ns.bgpNodes
) else m
base.copy(connecting = base.nodeId in connecting)
}
}
}