Add an Android panel for own and peer DM devices
This commit is contained in:
1 parent
cb354c4474
commit
317ea6565f
3 files changed
+103
No files matched your search
@@ -0,0 +1,29 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/** Текущий состав закреплённых сторон PM; собственная копия общего member snapshot. */
|
||||
data class DmMember(val side: Int, val member: ChatMember)
|
||||
|
||||
/** Атомарный decode CHAT_EVT_DM_MEMBERS; чужое поколение и неполный/повторный список отклоняются. */
|
||||
fun parseDmMembers(data: ByteArray, target: ConversationTarget.Pm): List<DmMember> {
|
||||
require(data.size >= 12) { "Missing DM member header" }
|
||||
val length = data[0].toInt() and 255
|
||||
require(length in 1..63 && data.size >= 11 + length) { "Invalid DM member conversation length" }
|
||||
require(String(data, 1, length, Charsets.UTF_8) == target.id) { "Different DM member conversation" }
|
||||
val header = ByteBuffer.wrap(data, 1 + length, 10).order(ByteOrder.LITTLE_ENDIAN)
|
||||
require(header.long == target.generation && target.generation > 0) { "Stale DM member generation" }
|
||||
val count = header.short.toInt() and 65535
|
||||
require(count <= 128 && data.size == 11 + length + count * (1 + MEMBER_WIRE_SIZE)) { "Invalid DM member count/size" }
|
||||
val nodes = mutableSetOf<Long>()
|
||||
return List(count) { index ->
|
||||
val offset = 11 + length + index * (1 + MEMBER_WIRE_SIZE)
|
||||
val side = data[offset].toInt() and 255
|
||||
require(side in 0..1) { "Invalid DM member side" }
|
||||
val wire = requireNotNull(parseMemberWire(data, offset + 1))
|
||||
require(wire.nodeId != 0L && nodes.add(wire.nodeId)) { "Invalid/duplicate DM member node" }
|
||||
DmMember(side, wire.toChatMember())
|
||||
}.sortedWith(compareBy<DmMember> { it.side }.thenByDescending { it.member.isSelf }
|
||||
.thenByDescending { it.member.online }.thenBy { it.member.nodeId.toULong() })
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.utun.chat.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.utun.chat.R
|
||||
import com.utun.chat.data.*
|
||||
import com.utun.chat.ui.components.PeerStatusDot
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/** Панель GROUPS: опрос живёт ровно столько, сколько открыта панель выбранного поколения. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DmMembersSheet(conv: DmConversation, onDismiss: () -> Unit) {
|
||||
val target = ConversationTarget.Pm(conv.convId, conv.generation)
|
||||
var members by remember(target) { mutableStateOf<List<DmMember>>(emptyList()) }
|
||||
var unavailable by remember(target) { mutableStateOf(false) }
|
||||
LaunchedEffect(target, conv.closed) {
|
||||
if (conv.closed) { members = emptyList(); return@LaunchedEffect }
|
||||
while (true) {
|
||||
try {
|
||||
val snapshot = withContext(Dispatchers.IO) {
|
||||
parseDmMembers(checkNotNull(NativeLib.dmMembers(target.id, target.generation)), target)
|
||||
}
|
||||
if (snapshot != members) LogManager.addLog("DEBUG", "PM", "Members updated conv=${target.id} count=${snapshot.size}")
|
||||
members = snapshot; unavailable = false
|
||||
} catch (e: CancellationException) { throw e }
|
||||
catch (e: Exception) {
|
||||
LogManager.addLog("WARN", "PM", "Members unavailable conv=${target.id}: $e"); unavailable = true
|
||||
}
|
||||
delay(2000)
|
||||
}
|
||||
}
|
||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
||||
Text(stringResource(R.string.dm_members_title), style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(16.dp))
|
||||
if (unavailable) Text(stringResource(R.string.dm_members_unavailable), modifier = Modifier.padding(horizontal = 16.dp))
|
||||
LazyColumn(Modifier.fillMaxWidth().heightIn(max = 480.dp)) {
|
||||
for (side in 0..1) {
|
||||
item(key = "side:$side") {
|
||||
HorizontalDivider()
|
||||
Text(stringResource(if (side == 0) R.string.dm_my_devices else R.string.dm_peer_devices),
|
||||
fontWeight = FontWeight.Bold, modifier = Modifier.padding(16.dp))
|
||||
}
|
||||
items(members.filter { it.side == side }, key = { "node:${it.member.nodeId}" }) { entry ->
|
||||
val member = entry.member
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
PeerStatusDot(member.online)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(member.name, fontWeight = if (member.isSelf) FontWeight.Bold else FontWeight.Normal)
|
||||
Text("0x%016x".format(member.nodeId), style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Text(stringResource(if (member.online) R.string.online else R.string.offline))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="dm_members_title">Conversation devices</string>
|
||||
<string name="dm_my_devices">My devices</string>
|
||||
<string name="dm_peer_devices">Peer devices</string>
|
||||
<string name="dm_members_unavailable">Could not refresh the device list</string>
|
||||
<string name="dm_mode_title">Devices for this conversation</string>
|
||||
<string name="dm_mode_groups">With device groups</string>
|
||||
<string name="dm_mode_device">Only these devices</string>
|
||||
|
||||
Reference in new issue
Block a user