Browse Source

android: кнопка invite в чате + фикс тап-зоны #, вывод протокола линков участника

topo_upd
evgeny 2 months ago
parent
commit
b0753bc19d
  1. 42
      tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
  2. 14
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt
  3. 5
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt
  4. 3
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt
  5. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt
  6. 4
      tools/chatgui-android/jni_bridge/android_jni_bridge.c

42
tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt

@ -1,19 +1,27 @@
package com.utun.chat
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.utun.chat.data.Channel
import com.utun.chat.data.InviteData
import com.utun.chat.data.LogManager
import com.utun.chat.data.NativeLib
import com.utun.chat.headless.HeadlessService
import com.utun.chat.ui.screens.*
import com.utun.chat.viewmodel.ChatViewModel
import android.content.Intent
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
enum class Screen { ChannelList, Chat, MemberList, Settings, Logs, QrScan }
@ -32,6 +40,23 @@ class MainActivity : ComponentActivity() {
var showJoinDialog by remember { mutableStateOf(false) }
var joinAutoLink by remember { mutableStateOf<String?>(null) }
var inviteLinkText by remember { mutableStateOf<String?>(null) }
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val showInvite: (Channel) -> Unit = { ch ->
val chId = ch.id.toLongOrNull()
if (chId == null) {
LogManager.addLog("ERROR", "INVITE", "channel id is not numeric: ${ch.id}")
scope.launch { snackbarHostState.showSnackbar("Invalid channel id") }
} else {
val link = NativeLib.encodeInviteLink(chId)
if (link != null) inviteLinkText = link
else {
LogManager.addLog("ERROR", "INVITE", "encodeInviteLink returned null for ch=$chId")
scope.launch { snackbarHostState.showSnackbar("Could not generate invite link") }
}
}
}
BackHandler(enabled = screen != Screen.ChannelList) {
when (screen) {
@ -48,6 +73,7 @@ class MainActivity : ComponentActivity() {
onDispose { NativeLib.subscribeNativeLogs(false) }
}
Box(modifier = Modifier.fillMaxSize()) {
when (screen) {
Screen.ChannelList -> {
val channels by vm.channels.collectAsState()
@ -59,16 +85,7 @@ class MainActivity : ComponentActivity() {
onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat },
onCreateChannel = { vm.createChannel("general", "channel_1") },
onJoinChannel = { showJoinDialog = true; joinAutoLink = null },
onInviteClick = { ch ->
val chId = ch.id.toLongOrNull()
if (chId == null) {
LogManager.addLog("ERROR", "INVITE", "channel id is not numeric: ${ch.id}")
} else {
val link = NativeLib.encodeInviteLink(chId)
if (link != null) inviteLinkText = link
else LogManager.addLog("ERROR", "INVITE", "encodeInviteLink returned null for ch=$chId")
}
},
onInviteClick = showInvite,
onSettings = { screen = Screen.Settings },
onLogs = { screen = Screen.Logs },
onRefresh = { vm.requestRefresh() }
@ -85,6 +102,7 @@ class MainActivity : ComponentActivity() {
viewModel = vm,
onSendMessage = { vm.sendMessage(it) },
onMembersClick = { screen = Screen.MemberList },
onInvite = { showInvite(channel) },
onBack = { screen = Screen.ChannelList }
)
}
@ -115,6 +133,8 @@ class MainActivity : ComponentActivity() {
)
}
}
SnackbarHost(snackbarHostState, Modifier.align(Alignment.BottomCenter))
}
/* Show join dialog after QR scan returns */
LaunchedEffect(qrResult) {

14
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt

@ -87,7 +87,7 @@ data class MemberAddr(val family: Int = 0, val protocol: String = "", val addres
data class LinkStatus(val linkId: Int = 0, val localFamily: Int = 0, val localAddr: String = "", val localPort: Int = 0,
val remoteFamily: Int = 0, val remoteAddr: String = "", val remotePort: Int = 0,
val linkState: Int = 0, val linkStatus: Int = 0,
val isServer: Boolean = false, val socketName: String = "") {
val isServer: Boolean = false, val socketName: String = "", val protocol: String = "") {
val stateLabel: String get() = when (linkState) { 2 -> "UP"; 1 -> "CONNECTING"; else -> "DOWN" }
}
@ -170,8 +170,8 @@ fun MemberWire.toChatMember(): ChatMember = ChatMember(
fun cleanSocketName(raw: String): String =
raw.removePrefix("as_").replace(Regex("_v[46]_(tcp|udp)$"), " $1")
/* ── MEMBER_DETAIL (event 22) link format: 43 bytes per link ── */
private const val MEMBER_DETAIL_LINK_SIZE = 43
/* ── MEMBER_DETAIL (event 22) link format: 63 bytes per link ── */
private const val MEMBER_DETAIL_LINK_SIZE = 63
private fun addrFromBytes(family: Int, bytes: ByteArray): String {
if (family == 4 && bytes.size >= 4)
@ -203,12 +203,15 @@ fun parseMemberDetail(data: ByteArray): MemberLinks? {
val lp = lbuf.getShort().toInt() and 0xFFFF
val rf = lbuf.get().toInt() and 0xFF; val ra = ByteArray(16); lbuf.get(ra)
val rp = lbuf.getShort().toInt() and 0xFFFF
val snBytes = ByteArray(20); lbuf.get(snBytes)
val sn = String(snBytes).trimEnd('\u0000').trimEnd()
val effState = if (isTcp != 0 && tcpReady != 0) 3 else state
val effStatus = if (isTcp != 0 && tcpReady != 0) 1 else status
links.add(LinkStatus(
localFamily = lf, localAddr = addrFromBytes(lf, la), localPort = lp,
remoteFamily = rf, remoteAddr = addrFromBytes(rf, ra), remotePort = rp,
linkState = effState, linkStatus = effStatus, isServer = isServer != 0
linkState = effState, linkStatus = effStatus, isServer = isServer != 0,
socketName = sn, protocol = if (isTcp != 0) "TCP" else "UDP"
))
offset += MEMBER_DETAIL_LINK_SIZE
}
@ -421,7 +424,8 @@ class ChatRepository {
linkState = a.optInt("st", 0),
linkStatus = a.optInt("ls", 0),
isServer = a.optInt("is", 0) != 0,
socketName = a.optString("sn", "")
socketName = a.optString("sn", ""),
protocol = a.optString("pt", "")
))
}
}

5
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt

@ -88,9 +88,8 @@ fun ChannelListScreen(
Box(Modifier.width(4.dp).fillMaxHeight().background(barColor))
Row(Modifier.fillMaxWidth().padding(12.dp)) {
Box(Modifier.size(44.dp), contentAlignment = Alignment.Center) {
Surface(shape = MaterialTheme.shapes.small,
modifier = Modifier.clickable { onInviteClick(ch) }) {
Text("#", fontSize = 22.sp, modifier = Modifier.padding(8.dp))
IconButton(onClick = { onInviteClick(ch) }, modifier = Modifier.size(44.dp)) {
Text("#", fontSize = 22.sp)
}
if (badgeText.isNotEmpty()) {
val badgeSize = 18.dp

3
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt

@ -11,6 +11,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.People
import androidx.compose.material3.*
import androidx.compose.runtime.*
@ -37,6 +38,7 @@ fun ChatScreen(
viewModel: ChatViewModel,
onSendMessage: (String) -> Unit,
onMembersClick: () -> Unit,
onInvite: () -> Unit,
onBack: () -> Unit
) {
var text by remember { mutableStateOf("") }
@ -112,6 +114,7 @@ fun ChatScreen(
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") }
},
actions = {
IconButton(onClick = onInvite) { Icon(Icons.Filled.Link, "Invite") }
IconButton(onClick = onMembersClick) { Icon(Icons.Filled.People, "Members") }
}
)

2
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt

@ -305,6 +305,8 @@ private fun MemberDetailCard(
Row(verticalAlignment = Alignment.CenterVertically) {
Box(Modifier.size(8.dp).clip(CircleShape).background(dcol))
Spacer(Modifier.width(6.dp))
Text(lk.protocol, fontFamily = FontFamily.Monospace, fontSize = 12.sp, color = Color.Gray)
Spacer(Modifier.width(6.dp))
Text("${formatAddr(lk.localAddr)}:${lk.localPort}",
modifier = Modifier.weight(1f),
textAlign = TextAlign.Start,

4
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -1052,8 +1052,8 @@ char* utun_bridge_get_member_links_json(uint64_t node_id) {
const char* sep = first ? "" : ","; first = 0;
const char* sn = lk->conn ? lk->conn->name : "";
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos,
"%s{\"id\":%d,\"lf\":%d,\"la\":\"%s\",\"lp\":%d,\"rf\":%d,\"ra\":\"%s\",\"rp\":%d,\"st\":%d,\"ls\":%d,\"is\":%d,\"sn\":\"%s\"}",
sep, lk->local_link_id, lf, la, (int)lp, rf, ra, (int)rp, st, ls, lk->is_server, sn);
"%s{\"id\":%d,\"pt\":\"%s\",\"lf\":%d,\"la\":\"%s\",\"lp\":%d,\"rf\":%d,\"ra\":\"%s\",\"rp\":%d,\"st\":%d,\"ls\":%d,\"is\":%d,\"sn\":\"%s\"}",
sep, lk->local_link_id, lk->is_tcp ? "TCP" : "UDP", lf, la, (int)lp, rf, ra, (int)rp, st, ls, lk->is_server, sn);
}
if (pos < (int)sizeof(buf))
pos += snprintf(buf + pos, sizeof(buf) - (size_t)pos, "]}");

Loading…
Cancel
Save