Show channel member inviter in Android details

This commit is contained in:
evgeny committed 2026-10-04 18:34:17 +02:00
1 parent b6fd489b8e
commit 93c0c5bdfc
6 files changed
+104 -12

No files matched your search

@@ -92,7 +92,8 @@ data class MemberDetail(val nodeId: Long, val name: String = "",
val directPresence: Boolean = false, val directUp: Boolean = false,
val indirectPresence: Boolean = false, val indirectUp: Boolean = false,
val bgpPresence: Boolean = false, val bgpRtt: Int = 0,
val bgpNodes: Int = 0)
val bgpNodes: Int = 0, val channelId: String = "",
val inviterId: Long? = null, val inviterName: String = "")
data class MemberAddr(val family: Int = 0, val protocol: String = "", val address: String = "",
val port: Int = 0, val socketId: Int = 0, val addrType: Int = 0, val rtt: Int = 0) {
@@ -459,7 +460,9 @@ class ChatRepository {
online = obj.optInt("on", 0) != 0,
lastSeen = obj.optLong("ls", 0),
created = obj.optLong("cr", 0),
addresses = addrs
addresses = addrs, channelId = channelId,
inviterId = if (obj.isNull("inviter_id")) null else obj.getLong("inviter_id"),
inviterName = obj.optString("inviter_name", "")
)
} catch (e: Exception) { Log.w("utun-gui", "getMemberDetail error", e) }
return null
@@ -320,6 +320,16 @@ private fun MemberDetailCard(
Text(stringResource(R.string.offline), fontSize = 13.sp, lineHeight = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
val inviter = when (detail.inviterId) {
null -> stringResource(R.string.member_inviter_unknown)
0L -> stringResource(R.string.member_inviter_root)
else -> {
val id = "[${detail.inviterId.toString(16).padStart(16, '0')}]"
if (detail.inviterName.isNotEmpty()) "${detail.inviterName} $id" else id
}
}
Text(stringResource(R.string.member_inviter, inviter), fontSize = 13.sp, lineHeight = 16.sp,
color = MaterialTheme.colorScheme.onSurface)
}
if (!isSelf) TextButton(onClick = onConnect, enabled = !detail.directUp) { Text(stringResource(R.string.connect)) }
TextButton(onClick = onEdit, enabled = canEdit) { Text(stringResource(R.string.edit)) }
@@ -134,6 +134,8 @@ class ChatViewModel : ViewModel() {
private val _selectedMember = MutableStateFlow<MemberDetail?>(null)
val selectedMember: StateFlow<MemberDetail?> = _selectedMember
private var memberDetailTarget: Pair<String, Long>? = null
private var memberDetailJob: kotlinx.coroutines.Job? = null
private val _memberLinks = MutableStateFlow<MemberLinks>(MemberLinks())
val memberLinks: StateFlow<MemberLinks> = _memberLinks
@@ -313,6 +315,7 @@ class ChatViewModel : ViewModel() {
val chId = String(data, 1, chLen)
val cur = _currentChannel.value
if (cur != null && cur.id == chId) refreshMembers(chId)
refreshSelectedMemberDetail(chId)
}
}
8 -> if (repo != null) refreshChannels() /* CHANNEL_PEERS_ONLINE */
@@ -502,8 +505,9 @@ class ChatViewModel : ViewModel() {
}
}
val sel = _selectedMember.value
if (sel != null && sel.nodeId == w.nodeId)
if (sel != null && sel.channelId == chId && sel.nodeId == w.nodeId)
_selectedMember.value = sel.copy(online = w.online != 0)
refreshSelectedMemberDetail(chId, w.nodeId)
}
25 -> { /* MEMBER_REMOVED: [ch_id_len:1][ch_id:var][node_id:8] */
if (data == null || data.size < 10) return
@@ -515,8 +519,10 @@ class ChatViewModel : ViewModel() {
val cur = _currentChannel.value
if (cur != null && cur.id == String(data, 1, cl))
_members.value = _members.value.filter { it.nodeId != nodeId }
if (_selectedMember.value?.nodeId == nodeId)
if (memberDetailTarget == (chId to nodeId)) {
memberDetailJob?.cancel(); memberDetailTarget = null
_selectedMember.value = null
} else refreshSelectedMemberDetail(chId, nodeId)
}
22 -> { /* MEMBER_DETAIL: бинарный снапшот links+sockets */
if (data == null || data.size < 12) return
@@ -936,6 +942,9 @@ class ChatViewModel : ViewModel() {
fun selectChannel(channel: Channel) {
val r = repo ?: return
if (_currentChannel.value?.id != channel.id) {
memberDetailJob?.cancel(); memberDetailTarget = null; _selectedMember.value = null
}
_currentChannel.value = channel
_radioActive.value = (radioChannelId == channel.id)
if (radioChannelId != channel.id) _radioTalking.value = null
@@ -1097,10 +1106,21 @@ class ChatViewModel : ViewModel() {
}
fun showMemberDetail(channelId: String, nodeId: Long) {
memberDetailTarget = channelId to nodeId
refreshSelectedMemberDetail(channelId)
}
/* Перечитываем родителя при изменении участника или его инвайтера, сохраняя актуальный live-статус. */
private fun refreshSelectedMemberDetail(channelId: String, changedNodeId: Long? = null) {
val target = memberDetailTarget ?: return
if (target.first != channelId || _currentChannel.value?.id != channelId) return
if (changedNodeId != null && changedNodeId != target.second && changedNodeId != _selectedMember.value?.inviterId) return
val r = repo ?: return
val ns = _nodeStatuses.value[nodeId]
viewModelScope.launch {
val d = withContext(Dispatchers.IO) { r.getMemberDetail(channelId, nodeId) }
memberDetailJob?.cancel()
memberDetailJob = viewModelScope.launch {
val d = withContext(Dispatchers.IO) { r.getMemberDetail(channelId, target.second) }
if (memberDetailTarget != target || _currentChannel.value?.id != channelId) return@launch
val ns = _nodeStatuses.value[target.second]
if (d != null && ns != null) _selectedMember.value = d.copy(
online = ns.online, bestRtt = ns.bestRtt,
directPresence = ns.directPresence, directUp = ns.directUp,
@@ -1116,6 +1136,7 @@ class ChatViewModel : ViewModel() {
val r = repo ?: return
r.setMemberNick(channelId, nodeId, nick)
refreshMembers(channelId)
refreshSelectedMemberDetail(channelId, nodeId)
}
fun setMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, moder: Boolean, storage: Boolean,
@@ -1440,6 +1461,7 @@ class ChatViewModel : ViewModel() {
}
fun clearState() {
memberDetailJob?.cancel(); memberDetailTarget = null; _selectedMember.value = null
memberRadios.clear()
dismissPmInvite(); _linkJoin.value = null; _joinedChannel.value = null
_attachmentPreparations.value = emptyMap()