android: кнопка invite в чате + фикс тап-зоны #, вывод протокола линков участника
This commit is contained in:
1 parent
bcea4bc335
commit
b0753bc19d
6 files changed
+100
-72
No files matched your search
@@ -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,72 +73,67 @@ class MainActivity : ComponentActivity() {
|
||||
onDispose { NativeLib.subscribeNativeLogs(false) }
|
||||
}
|
||||
|
||||
when (screen) {
|
||||
Screen.ChannelList -> {
|
||||
val channels by vm.channels.collectAsState()
|
||||
val stuck by vm.isStuck.collectAsState()
|
||||
ChannelListScreen(
|
||||
channels = channels,
|
||||
connected = connected,
|
||||
isStuck = stuck,
|
||||
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")
|
||||
}
|
||||
},
|
||||
onSettings = { screen = Screen.Settings },
|
||||
onLogs = { screen = Screen.Logs },
|
||||
onRefresh = { vm.requestRefresh() }
|
||||
)
|
||||
}
|
||||
Screen.Chat -> {
|
||||
val messages by vm.messages.collectAsState()
|
||||
val channel = vm.currentChannel.collectAsState().value
|
||||
if (channel != null) {
|
||||
ChatScreen(
|
||||
channel = channel,
|
||||
messages = messages,
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
when (screen) {
|
||||
Screen.ChannelList -> {
|
||||
val channels by vm.channels.collectAsState()
|
||||
val stuck by vm.isStuck.collectAsState()
|
||||
ChannelListScreen(
|
||||
channels = channels,
|
||||
connected = connected,
|
||||
viewModel = vm,
|
||||
onSendMessage = { vm.sendMessage(it) },
|
||||
onMembersClick = { screen = Screen.MemberList },
|
||||
isStuck = stuck,
|
||||
onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat },
|
||||
onCreateChannel = { vm.createChannel("general", "channel_1") },
|
||||
onJoinChannel = { showJoinDialog = true; joinAutoLink = null },
|
||||
onInviteClick = showInvite,
|
||||
onSettings = { screen = Screen.Settings },
|
||||
onLogs = { screen = Screen.Logs },
|
||||
onRefresh = { vm.requestRefresh() }
|
||||
)
|
||||
}
|
||||
Screen.Chat -> {
|
||||
val messages by vm.messages.collectAsState()
|
||||
val channel = vm.currentChannel.collectAsState().value
|
||||
if (channel != null) {
|
||||
ChatScreen(
|
||||
channel = channel,
|
||||
messages = messages,
|
||||
connected = connected,
|
||||
viewModel = vm,
|
||||
onSendMessage = { vm.sendMessage(it) },
|
||||
onMembersClick = { screen = Screen.MemberList },
|
||||
onInvite = { showInvite(channel) },
|
||||
onBack = { screen = Screen.ChannelList }
|
||||
)
|
||||
}
|
||||
}
|
||||
Screen.MemberList -> {
|
||||
val members by vm.members.collectAsState()
|
||||
val selectedMember by vm.selectedMember.collectAsState()
|
||||
val channel = vm.currentChannel.collectAsState().value
|
||||
if (channel != null) {
|
||||
MemberListScreen(
|
||||
channel = channel,
|
||||
members = members,
|
||||
selectedMember = selectedMember,
|
||||
viewModel = vm,
|
||||
onBack = { screen = Screen.Chat }
|
||||
)
|
||||
}
|
||||
}
|
||||
Screen.Settings -> SettingsScreen(vm = vm, onBack = { screen = Screen.ChannelList }, onExit = { finish() })
|
||||
Screen.Logs -> LogScreen(onBack = { screen = Screen.ChannelList })
|
||||
Screen.QrScan -> {
|
||||
QrScanScreen(
|
||||
onResult = { link ->
|
||||
qrResult = link
|
||||
screen = Screen.ChannelList
|
||||
},
|
||||
onBack = { screen = Screen.ChannelList }
|
||||
)
|
||||
}
|
||||
}
|
||||
Screen.MemberList -> {
|
||||
val members by vm.members.collectAsState()
|
||||
val selectedMember by vm.selectedMember.collectAsState()
|
||||
val channel = vm.currentChannel.collectAsState().value
|
||||
if (channel != null) {
|
||||
MemberListScreen(
|
||||
channel = channel,
|
||||
members = members,
|
||||
selectedMember = selectedMember,
|
||||
viewModel = vm,
|
||||
onBack = { screen = Screen.Chat }
|
||||
)
|
||||
}
|
||||
}
|
||||
Screen.Settings -> SettingsScreen(vm = vm, onBack = { screen = Screen.ChannelList }, onExit = { finish() })
|
||||
Screen.Logs -> LogScreen(onBack = { screen = Screen.ChannelList })
|
||||
Screen.QrScan -> {
|
||||
QrScanScreen(
|
||||
onResult = { link ->
|
||||
qrResult = link
|
||||
screen = Screen.ChannelList
|
||||
},
|
||||
onBack = { screen = Screen.ChannelList }
|
||||
)
|
||||
}
|
||||
SnackbarHost(snackbarHostState, Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
|
||||
/* Show join dialog after QR scan returns */
|
||||
|
||||
@@ -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", "")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -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
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user