|
|
|
|
@ -126,6 +126,8 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
|
|
|
|
|
private val _members = MutableStateFlow<List<ChatMember>>(emptyList()) |
|
|
|
|
val members: StateFlow<List<ChatMember>> = _members |
|
|
|
|
/* Live-подписки по каналам: JSON-refresh не должен стирать более свежие BGP-события. Только UI-поток. */ |
|
|
|
|
private val memberRadios = mutableMapOf<String, Map<Long, Boolean>>() |
|
|
|
|
|
|
|
|
|
private val _channelOwner = MutableStateFlow(false) |
|
|
|
|
val channelOwner: StateFlow<Boolean> = _channelOwner |
|
|
|
|
@ -351,6 +353,8 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
4 -> { if (repo != null) { LogManager.addLog("INFO", "VM", "CHANNEL_UPDATED — refreshChannels"); refreshChannels() } else { LogManager.addLog("WARN", "VM", "CHANNEL_UPDATED skipped — repo is NULL") } } |
|
|
|
|
12 -> LogManager.addLog("INFO", "VM", "service started") |
|
|
|
|
13 -> { |
|
|
|
|
memberRadios.clear() |
|
|
|
|
_members.value = _members.value.map { it.copy(radioActive = false) } |
|
|
|
|
_attachmentPreparations.value = emptyMap() |
|
|
|
|
_pmInvite.value?.let { state -> |
|
|
|
|
if (state.pending) _pmInvite.value = state.copy(pending = false, errorRes = R.string.invite_core_unavailable) |
|
|
|
|
@ -449,16 +453,52 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
if (cur != null) refreshMembers(cur.id) |
|
|
|
|
} |
|
|
|
|
19 -> { if (repo != null) refreshLocalSockets() } /* LOCAL_SOCKETS */ |
|
|
|
|
23 -> { /* MEMBER_LIST: live-снимок дополняет расширенный JSON-список. */ |
|
|
|
|
if (data == null || data.size < 3) { |
|
|
|
|
LogManager.addLog("ERROR", "VM", "MEMBER_LIST: missing header"); return |
|
|
|
|
} |
|
|
|
|
val cl = data[0].toInt() and 0xFF |
|
|
|
|
if (cl == 0 || data.size < 1 + cl + 2) { |
|
|
|
|
LogManager.addLog("ERROR", "VM", "MEMBER_LIST: invalid channel length=$cl bytes=${data.size}"); return |
|
|
|
|
} |
|
|
|
|
val chId = String(data, 1, cl) |
|
|
|
|
val count = (data[1 + cl].toInt() and 0xFF) or ((data[2 + cl].toInt() and 0xFF) shl 8) |
|
|
|
|
val offset = cl + 3 |
|
|
|
|
if (data.size != offset + count * 79) { |
|
|
|
|
LogManager.addLog("ERROR", "VM", "MEMBER_LIST: invalid size ch=$chId count=$count bytes=${data.size}"); return |
|
|
|
|
} |
|
|
|
|
val radios = (0 until count).associate { i -> |
|
|
|
|
val w = requireNotNull(parseMemberWire(data, offset + i * 79)) |
|
|
|
|
w.nodeId to ((w.flags and 0x10) != 0) |
|
|
|
|
} |
|
|
|
|
memberRadios[chId] = radios |
|
|
|
|
if (_currentChannel.value?.id == chId) |
|
|
|
|
_members.value = _members.value.map { it.copy(radioActive = radios[it.nodeId] == true) } |
|
|
|
|
LogManager.addLog("DEBUG", "VM", "MEMBER_LIST: ch=$chId members=$count radios=${radios.values.count { it }}") |
|
|
|
|
} |
|
|
|
|
24 -> { /* MEMBER_UPDATED: [ch_id_len:1][ch_id:var][count:1][member:79B] */ |
|
|
|
|
if (data == null || data.size < 3 + 79) return |
|
|
|
|
if (data == null || data.size < 3 + 79) { |
|
|
|
|
LogManager.addLog("ERROR", "VM", "MEMBER_UPDATED: missing header/member"); return |
|
|
|
|
} |
|
|
|
|
val cl = data[0].toInt() and 0xFF |
|
|
|
|
val w = parseMemberWire(data, 1 + cl + 1) ?: return |
|
|
|
|
val m = w.toChatMember() |
|
|
|
|
if (cl == 0 || data.size != cl + 2 + 79 || data[cl + 1].toInt() != 1) { |
|
|
|
|
LogManager.addLog("ERROR", "VM", "MEMBER_UPDATED: invalid payload channelLength=$cl bytes=${data.size}"); return |
|
|
|
|
} |
|
|
|
|
val chId = String(data, 1, cl) |
|
|
|
|
val w = requireNotNull(parseMemberWire(data, cl + 2)) |
|
|
|
|
val radio = (w.flags and 0x10) != 0 |
|
|
|
|
val previousRadio = memberRadios[chId]?.get(w.nodeId) |
|
|
|
|
memberRadios[chId] = memberRadios[chId].orEmpty() + (w.nodeId to radio) |
|
|
|
|
if (previousRadio != radio) |
|
|
|
|
LogManager.addLog("DEBUG", "VM", "Member radio: ch=$chId node=${w.nodeId} enabled=$radio self=${w.isSelf}") |
|
|
|
|
val cur = _currentChannel.value |
|
|
|
|
if (cur != null && cur.id == String(data, 1, cl)) { |
|
|
|
|
if (cur != null && cur.id == chId) { |
|
|
|
|
_members.value = _members.value.toMutableList().apply { |
|
|
|
|
val idx = indexOfFirst { it.nodeId == w.nodeId } |
|
|
|
|
if (idx >= 0) set(idx, m) else add(m) |
|
|
|
|
val m = if (idx >= 0) w.toChatMember(this[idx]) else w.toChatMember() |
|
|
|
|
if (m.isDeleted && !_channelOwner.value) { |
|
|
|
|
if (idx >= 0) removeAt(idx) |
|
|
|
|
} else if (idx >= 0) set(idx, m) else add(m) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
val sel = _selectedMember.value |
|
|
|
|
@ -470,6 +510,8 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
val cl = data[0].toInt() and 0xFF |
|
|
|
|
val buf = java.nio.ByteBuffer.wrap(data, 1 + cl, 8).order(java.nio.ByteOrder.LITTLE_ENDIAN) |
|
|
|
|
val nodeId = buf.getLong() |
|
|
|
|
val chId = String(data, 1, cl) |
|
|
|
|
memberRadios[chId] = memberRadios[chId].orEmpty() - nodeId |
|
|
|
|
val cur = _currentChannel.value |
|
|
|
|
if (cur != null && cur.id == String(data, 1, cl)) |
|
|
|
|
_members.value = _members.value.filter { it.nodeId != nodeId } |
|
|
|
|
@ -567,6 +609,7 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
if (data.size < 1 + cl) return |
|
|
|
|
val chId = String(data, 1, cl) |
|
|
|
|
LogManager.addLog("INFO", "VM", "CHANNEL_DELETED ch=$chId") |
|
|
|
|
memberRadios.remove(chId) |
|
|
|
|
if (radioChannelId == chId) turnRadioOff() |
|
|
|
|
if (_currentChannel.value?.id == chId) { |
|
|
|
|
_currentChannel.value = null |
|
|
|
|
@ -1036,6 +1079,7 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
val connecting = _connecting.value[chId] ?: emptySet() |
|
|
|
|
viewModelScope.launch { |
|
|
|
|
val (raw, owner) = withContext(Dispatchers.IO) { r.getMembers(chId) to r.isChannelOwner(chId) } |
|
|
|
|
if (_currentChannel.value?.id != chId) return@launch |
|
|
|
|
_channelOwner.value = owner |
|
|
|
|
_members.value = raw.filter { owner || !it.isDeleted }.map { m -> |
|
|
|
|
val ns = statuses[m.nodeId] |
|
|
|
|
@ -1046,8 +1090,9 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
bgpPresence = ns.bgpPresence, bgpRtt = ns.bgpRtt, |
|
|
|
|
bgpNodes = ns.bgpNodes |
|
|
|
|
) else m |
|
|
|
|
base.copy(connecting = base.nodeId in connecting) |
|
|
|
|
base.copy(connecting = base.nodeId in connecting, radioActive = memberRadios[chId]?.get(base.nodeId) == true) |
|
|
|
|
} |
|
|
|
|
NativeLib.requestMemberList(chId) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -1395,6 +1440,7 @@ class ChatViewModel : ViewModel() {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fun clearState() { |
|
|
|
|
memberRadios.clear() |
|
|
|
|
dismissPmInvite(); _linkJoin.value = null; _joinedChannel.value = null |
|
|
|
|
_attachmentPreparations.value = emptyMap() |
|
|
|
|
repo?.close() |
|
|
|
|
|