Add local per-member radio mute controls on desktop and Android
This commit is contained in:
1 parent
a069d11ef9
commit
7e19cbaf3a
33 files changed
+735
-27
No files matched your search
@@ -74,6 +74,7 @@ data class ChatMember(val nodeId: Long, val name: String, val online: Boolean =
|
||||
val isAdmin: Boolean = false, val isSuper: Boolean = false,
|
||||
val isModer: Boolean = false, val isStorage: Boolean = false,
|
||||
val radioActive: Boolean = false,
|
||||
val radioMuted: Boolean = false,
|
||||
val isVerified: Boolean = false, val isDeleted: Boolean = false,
|
||||
val nodeType: Int = 0,
|
||||
val rtt: Int = 0, val isSelf: Boolean = false,
|
||||
@@ -196,7 +197,7 @@ fun MemberWire.toChatMember(previous: ChatMember = ChatMember(nodeId, name)): Ch
|
||||
name = name, online = online != 0,
|
||||
isAdmin = (flags and 2) != 0, isSuper = (flags and 1) != 0,
|
||||
isModer = (flags and 4) != 0, isStorage = storage,
|
||||
isDeleted = (flags and 8) != 0, radioActive = (flags and 0x10) != 0,
|
||||
isDeleted = (flags and 8) != 0, radioActive = (flags and 0x10) != 0, radioMuted = (flags and 0x20) != 0,
|
||||
rtt = if (rtt == 0xFFFF) 0 else rtt, isSelf = isSelf,
|
||||
avatarServer = avatarServer, avatarFile = avatarFile, storageRole = storageRole
|
||||
)
|
||||
@@ -442,6 +443,7 @@ class ChatRepository {
|
||||
isSuper = obj.optInt("sp", 0) != 0,
|
||||
isModer = obj.optInt("md", 0) != 0,
|
||||
isStorage = obj.optInt("st", 0) != 0,
|
||||
radioMuted = obj.optInt("rm", 0) != 0,
|
||||
isVerified = obj.optInt("vd", 0) != 0,
|
||||
isDeleted = obj.optInt("dl", 0) != 0,
|
||||
nodeType = obj.optInt("tp", 0),
|
||||
|
||||
@@ -137,6 +137,7 @@ object NativeLib {
|
||||
fun cancelAvatar(id: Long): Boolean = nativeAvatarOperation(7, id, 0, 0, 0, null, null, null)
|
||||
fun getMemberDetail(channelId: String, nodeId: Long): String = nativeGetMemberDetail(channelId, nodeId) ?: "{}"
|
||||
fun setMemberNick(channelId: String, nodeId: Long, nick: String) { nativeSetMemberNick(channelId, nodeId, nick) }
|
||||
fun toggleMemberRadioMute(channelId: String, nodeId: Long): Boolean = nativeToggleMemberRadioMute(channelId, nodeId)
|
||||
fun isChannelOwner(channelId: String): Boolean = nativeIsChannelOwner(channelId)
|
||||
fun setMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, moder: Boolean, storage: Boolean,
|
||||
verified: Boolean, deleted: Boolean) {
|
||||
@@ -266,6 +267,7 @@ object NativeLib {
|
||||
remote: String?, local: String?, data: ByteArray?): Boolean
|
||||
private external fun nativeGetMemberDetail(channelId: String, nodeId: Long): String?
|
||||
private external fun nativeSetMemberNick(channelId: String, nodeId: Long, nick: String)
|
||||
private external fun nativeToggleMemberRadioMute(channelId: String, nodeId: Long): Boolean
|
||||
private external fun nativeIsChannelOwner(channelId: String): Boolean
|
||||
private external fun nativeSetMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, moder: Boolean, storage: Boolean,
|
||||
verified: Boolean, deleted: Boolean)
|
||||
|
||||
+19
-2
@@ -27,6 +27,7 @@ import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.Shield
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -68,6 +69,7 @@ fun MemberListScreen(
|
||||
val memberLinks by viewModel.memberLinks.collectAsState()
|
||||
val localSockets by viewModel.localSockets.collectAsState()
|
||||
val channelOwner by viewModel.channelOwner.collectAsState()
|
||||
val radioMuteFailed by viewModel.radioMuteFailed.collectAsState()
|
||||
|
||||
LaunchedEffect(selectedMember?.nodeId) {
|
||||
val nodeId = selectedMember?.nodeId
|
||||
@@ -97,7 +99,8 @@ fun MemberListScreen(
|
||||
onClick = { viewModel.showMemberDetail(channel.id, m.nodeId) },
|
||||
onCall = { viewModel.startCall(channel.id, m.nodeId, m.name) },
|
||||
onMessage = { viewModel.startDm(channel.id, m.nodeId, m.name) },
|
||||
onInvite = { viewModel.showPmInvite(m.nodeId, m.name, channel.id) }
|
||||
onInvite = { viewModel.showPmInvite(m.nodeId, m.name, channel.id) },
|
||||
onRadioMute = { viewModel.toggleMemberRadioMute(channel.id, m.nodeId) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -131,6 +134,14 @@ fun MemberListScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (radioMuteFailed) {
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::dismissRadioMuteError,
|
||||
text = { Text(stringResource(R.string.radio_member_mute_failed)) },
|
||||
confirmButton = { TextButton(onClick = viewModel::dismissRadioMuteError) { Text(stringResource(android.R.string.ok)) } }
|
||||
)
|
||||
}
|
||||
|
||||
if (showEditDialog && editMember != null) {
|
||||
MemberEditDialog(
|
||||
member = editMember!!,
|
||||
@@ -159,7 +170,8 @@ private fun MemberListItem(
|
||||
onClick: () -> Unit,
|
||||
onCall: () -> Unit,
|
||||
onMessage: () -> Unit,
|
||||
onInvite: () -> Unit
|
||||
onInvite: () -> Unit,
|
||||
onRadioMute: () -> Unit
|
||||
) {
|
||||
val onlineColor = when {
|
||||
member.connecting -> Color(0xFFFFEB3B) // yellow: connecting
|
||||
@@ -239,6 +251,11 @@ private fun MemberListItem(
|
||||
}
|
||||
|
||||
if (!member.isSelf) {
|
||||
IconToggleButton(checked = member.radioMuted, onCheckedChange = { onRadioMute() }) {
|
||||
Icon(Icons.AutoMirrored.Filled.VolumeUp,
|
||||
stringResource(if (member.radioMuted) R.string.radio_member_listen else R.string.radio_member_mute),
|
||||
tint = if (member.radioMuted) Color(0xFFEF5350) else Color(0xFF4CAF50))
|
||||
}
|
||||
IconButton(onClick = onInvite) { Icon(Icons.Filled.Add, stringResource(R.string.invite_to_group)) }
|
||||
IconButton(onClick = onCall, enabled = member.online) { Icon(Icons.Filled.Call, stringResource(R.string.call)) }
|
||||
IconButton(onClick = onMessage) { Icon(Icons.AutoMirrored.Filled.Message, stringResource(R.string.message)) }
|
||||
|
||||
@@ -118,6 +118,8 @@ class ChatViewModel : ViewModel() {
|
||||
val reactionCatalog: StateFlow<List<ReactionState>> = _reactionCatalog
|
||||
private val _interactionFailed = MutableStateFlow(false)
|
||||
val interactionFailed: StateFlow<Boolean> = _interactionFailed
|
||||
private val _radioMuteFailed = MutableStateFlow(false)
|
||||
val radioMuteFailed: StateFlow<Boolean> = _radioMuteFailed
|
||||
private var nextInteractionId = 0L
|
||||
|
||||
private val _currentChannel = MutableStateFlow<Channel?>(null)
|
||||
@@ -136,6 +138,7 @@ class ChatViewModel : ViewModel() {
|
||||
val members: StateFlow<List<ChatMember>> = _members
|
||||
/* Live-подписки по каналам: JSON-refresh не должен стирать более свежие BGP-события. Только UI-поток. */
|
||||
private val memberRadios = mutableMapOf<String, Map<Long, Boolean>>()
|
||||
private val memberRadioMutes = mutableMapOf<String, Map<Long, Boolean>>()
|
||||
|
||||
private val _channelOwner = MutableStateFlow(false)
|
||||
val channelOwner: StateFlow<Boolean> = _channelOwner
|
||||
@@ -292,6 +295,12 @@ class ChatViewModel : ViewModel() {
|
||||
_dmConversations.value = _dmConversations.value.map { it.copy(online = false) }
|
||||
} else if (type in listOf(4, 5, 12, 16, 17, 24, 25, 31, 33)) refreshDmCallRoute()
|
||||
when (type) {
|
||||
55 -> { /* RADIO_MUTE_RESULT: group_id:8LE, node_id:8LE, success. */
|
||||
if (data == null || data.size != 17) {
|
||||
LogManager.addLog("ERROR", "Radio", "Invalid member mute result"); _radioMuteFailed.value = true; return
|
||||
}
|
||||
if (data[16].toInt() == 0) _radioMuteFailed.value = true
|
||||
}
|
||||
53 -> { /* FEEDBACK_CHANGED: channel, message_id, kind. */
|
||||
if (data == null || data.size < 34) return
|
||||
val size = data[0].toInt() and 255
|
||||
@@ -394,6 +403,7 @@ class ChatViewModel : ViewModel() {
|
||||
12 -> LogManager.addLog("INFO", "VM", "service started")
|
||||
13 -> {
|
||||
memberRadios.clear()
|
||||
memberRadioMutes.clear()
|
||||
_members.value = _members.value.map { it.copy(radioActive = false) }
|
||||
_attachmentPreparations.value = emptyMap()
|
||||
_pmInvite.value?.let { state ->
|
||||
@@ -513,6 +523,7 @@ class ChatViewModel : ViewModel() {
|
||||
}
|
||||
val radios = snapshot.mapValues { (_, w) -> (w.flags and 0x10) != 0 }
|
||||
memberRadios[chId] = radios
|
||||
memberRadioMutes[chId] = snapshot.mapValues { (_, w) -> (w.flags and 0x20) != 0 }
|
||||
if (_currentChannel.value?.id == chId)
|
||||
_members.value = _members.value.map { snapshot[it.nodeId]?.toChatMember(it) ?: it }
|
||||
LogManager.addLog("DEBUG", "VM", "MEMBER_LIST: ch=$chId members=$count radios=${radios.values.count { it }}")
|
||||
@@ -530,6 +541,7 @@ class ChatViewModel : ViewModel() {
|
||||
val radio = (w.flags and 0x10) != 0
|
||||
val previousRadio = memberRadios[chId]?.get(w.nodeId)
|
||||
memberRadios[chId] = memberRadios[chId].orEmpty() + (w.nodeId to radio)
|
||||
memberRadioMutes[chId] = memberRadioMutes[chId].orEmpty() + (w.nodeId to ((w.flags and 0x20) != 0))
|
||||
if (previousRadio != radio)
|
||||
LogManager.addLog("DEBUG", "VM", "Member radio: ch=$chId node=${w.nodeId} enabled=$radio self=${w.isSelf}")
|
||||
val cur = _currentChannel.value
|
||||
@@ -554,6 +566,7 @@ class ChatViewModel : ViewModel() {
|
||||
val nodeId = buf.getLong()
|
||||
val chId = String(data, 1, cl)
|
||||
memberRadios[chId] = memberRadios[chId].orEmpty() - nodeId
|
||||
memberRadioMutes[chId] = memberRadioMutes[chId].orEmpty() - nodeId
|
||||
val cur = _currentChannel.value
|
||||
if (cur != null && cur.id == String(data, 1, cl))
|
||||
_members.value = _members.value.filter { it.nodeId != nodeId }
|
||||
@@ -654,6 +667,7 @@ class ChatViewModel : ViewModel() {
|
||||
val chId = String(data, 1, cl)
|
||||
LogManager.addLog("INFO", "VM", "CHANNEL_DELETED ch=$chId")
|
||||
memberRadios.remove(chId)
|
||||
memberRadioMutes.remove(chId)
|
||||
if (radioChannelId == chId) turnRadioOff()
|
||||
if (_currentChannel.value?.id == chId) {
|
||||
_currentChannel.value = null
|
||||
@@ -1170,7 +1184,8 @@ class ChatViewModel : ViewModel() {
|
||||
bgpPresence = ns.bgpPresence, bgpRtt = ns.bgpRtt,
|
||||
bgpNodes = ns.bgpNodes
|
||||
) else m
|
||||
base.copy(connecting = base.nodeId in connecting, radioActive = memberRadios[chId]?.get(base.nodeId) == true)
|
||||
base.copy(connecting = base.nodeId in connecting, radioActive = memberRadios[chId]?.get(base.nodeId) == true,
|
||||
radioMuted = memberRadioMutes[chId]?.get(base.nodeId) ?: base.radioMuted)
|
||||
}
|
||||
NativeLib.requestMemberList(chId)
|
||||
}
|
||||
@@ -1210,6 +1225,12 @@ class ChatViewModel : ViewModel() {
|
||||
refreshSelectedMemberDetail(channelId, nodeId)
|
||||
}
|
||||
|
||||
fun toggleMemberRadioMute(channelId: String, nodeId: Long) {
|
||||
if (repo == null || !NativeLib.toggleMemberRadioMute(channelId, nodeId)) _radioMuteFailed.value = true
|
||||
}
|
||||
|
||||
fun dismissRadioMuteError() { _radioMuteFailed.value = false }
|
||||
|
||||
fun setMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, moder: Boolean, storage: Boolean,
|
||||
verified: Boolean, deleted: Boolean) {
|
||||
val r = repo ?: return
|
||||
@@ -1535,6 +1556,7 @@ class ChatViewModel : ViewModel() {
|
||||
fun clearState() {
|
||||
memberDetailJob?.cancel(); memberDetailTarget = null; _selectedMember.value = null
|
||||
memberRadios.clear()
|
||||
memberRadioMutes.clear()
|
||||
dismissPmInvite(); _linkJoin.value = null; _joinedChannel.value = null
|
||||
_attachmentPreparations.value = emptyMap()
|
||||
repo?.close()
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
<string name="radio_ptt">"Рация PTT"</string>
|
||||
<string name="radio_overlay_description">"Оверлей PTT рации"</string>
|
||||
<string name="radio_active">"Рация активна"</string>
|
||||
<string name="radio_member_mute">Не слушать этого участника в рации</string>
|
||||
<string name="radio_member_listen">Слушать этого участника в рации</string>
|
||||
<string name="radio_member_mute_failed">Не удалось изменить настройку прослушивания рации</string>
|
||||
<string name="radio_notification">"Рация: PTT / автоматическая передача по голосу"</string>
|
||||
<string name="overlay_stopped">"Плавающая кнопка остановлена"</string>
|
||||
<string name="ongoing_call">"Текущий звонок"</string>
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
<string name="radio_ptt">"PTT radio"</string>
|
||||
<string name="radio_overlay_description">"Radio PTT overlay"</string>
|
||||
<string name="radio_active">"Radio is active"</string>
|
||||
<string name="radio_member_mute">Mute this member in radio</string>
|
||||
<string name="radio_member_listen">Listen to this member in radio</string>
|
||||
<string name="radio_member_mute_failed">Cannot change radio listening setting</string>
|
||||
<string name="radio_notification">"Radio: PTT / automatic voice transmission"</string>
|
||||
<string name="overlay_stopped">"Floating button stopped"</string>
|
||||
<string name="ongoing_call">"Ongoing call"</string>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.utun.chat.data
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
class MemberRadioMuteTest {
|
||||
@Test fun muteUpdatesKeepRadioSubscriptionAndOtherProperties() {
|
||||
val bytes = ByteArray(MEMBER_WIRE_SIZE)
|
||||
ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).putLong(0, 42)
|
||||
bytes[8] = 1; bytes[10] = 0x32
|
||||
val previous = ChatMember(42, "Alice", isVerified = true, directUp = true, nodeType = 4)
|
||||
val muted = requireNotNull(parseMemberWire(bytes)).toChatMember(previous)
|
||||
assertTrue(muted.radioMuted); assertTrue(muted.radioActive); assertTrue(muted.isAdmin)
|
||||
assertTrue(muted.isVerified); assertTrue(muted.directUp); assertEquals(4, muted.nodeType)
|
||||
bytes[10] = 0x12
|
||||
val listening = requireNotNull(parseMemberWire(bytes)).toChatMember(muted)
|
||||
assertFalse(listening.radioMuted); assertTrue(listening.radioActive); assertTrue(listening.isVerified)
|
||||
bytes[10] = 0x20
|
||||
val offlineRadio = requireNotNull(parseMemberWire(bytes)).toChatMember(listening)
|
||||
assertTrue(offlineRadio.radioMuted); assertFalse(offlineRadio.radioActive)
|
||||
}
|
||||
}
|
||||
@@ -705,7 +705,7 @@ char* utun_bridge_get_channel_members_json(const char* channel_id) {
|
||||
"SELECT p.node_id, COALESCE(n.name,''),"
|
||||
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
|
||||
" COALESCE(p.node_type,0), COALESCE(p.node_RTT,0), COALESCE(p.storage,0),"
|
||||
" COALESCE(p.userinfo,''),p.source,length(p.adm_tags_sig),COALESCE(p.deleted,0)"
|
||||
" COALESCE(p.userinfo,''),p.source,length(p.adm_tags_sig),COALESCE(p.deleted,0),p.radio_muted"
|
||||
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
|
||||
" ORDER BY p.node_id ASC", peers_tbl);
|
||||
|
||||
@@ -765,19 +765,19 @@ char* utun_bridge_get_channel_members_json(const char* channel_id) {
|
||||
|
||||
size_t needed = snprintf(NULL, 0,
|
||||
"%s{\"n\":\"%s\",\"ni\":%llu,\"on\":0,\"ad\":%d,\"sp\":%d,\"md\":%d,\"st\":%d,"
|
||||
"\"vd\":%d,\"dl\":%d,\"tp\":%d,\"rt\":%d,\"is\":%d,\"ui\":\"%s\",\"sr\":%d}",
|
||||
"\"vd\":%d,\"dl\":%d,\"tp\":%d,\"rt\":%d,\"is\":%d,\"ui\":\"%s\",\"sr\":%d,\"rm\":%d}",
|
||||
sep, esc_name, (unsigned long long)node_id,
|
||||
is_admin, is_super, is_moder, storage_flag,
|
||||
is_verified, is_deleted, node_type, node_rtt, (node_id == my_id) ? 1 : 0, esc_userinfo, storage_role);
|
||||
is_verified, is_deleted, node_type, node_rtt, (node_id == my_id) ? 1 : 0, esc_userinfo, storage_role, sqlite3_column_int(st, 11));
|
||||
|
||||
while (pos + needed + 2 > cap) { char* tmp = u_realloc(json, cap * 2); if (!tmp) break; json = tmp; cap *= 2; }
|
||||
if (pos + needed + 2 <= cap)
|
||||
pos += (size_t)snprintf(json + pos, cap - pos,
|
||||
"%s{\"n\":\"%s\",\"ni\":%llu,\"on\":0,\"ad\":%d,\"sp\":%d,\"md\":%d,\"st\":%d,"
|
||||
"\"vd\":%d,\"dl\":%d,\"tp\":%d,\"rt\":%d,\"is\":%d,\"ui\":\"%s\",\"sr\":%d}",
|
||||
"\"vd\":%d,\"dl\":%d,\"tp\":%d,\"rt\":%d,\"is\":%d,\"ui\":\"%s\",\"sr\":%d,\"rm\":%d}",
|
||||
sep, esc_name, (unsigned long long)node_id,
|
||||
is_admin, is_super, is_moder, storage_flag,
|
||||
is_verified, is_deleted, node_type, node_rtt, (node_id == my_id) ? 1 : 0, esc_userinfo, storage_role);
|
||||
is_verified, is_deleted, node_type, node_rtt, (node_id == my_id) ? 1 : 0, esc_userinfo, storage_role, sqlite3_column_int(st, 11));
|
||||
u_free(esc_userinfo);
|
||||
u_free(esc_name);
|
||||
}
|
||||
@@ -910,6 +910,23 @@ char* utun_bridge_get_member_detail_json(const char* channel_id, uint64_t node_i
|
||||
return u_strdup(buf);
|
||||
}
|
||||
|
||||
int utun_bridge_toggle_member_radio_mute(const char* channel_id, uint64_t node_id) {
|
||||
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
|
||||
struct UASYNC* ua = instance_lite_get_uasync();
|
||||
if (!inst || !uasync_is_running(ua) || !channel_id || !channel_id[0] || strlen(channel_id) >= 64 || !node_id) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: Android mute request rejected: core unavailable or invalid member"); return -1;
|
||||
}
|
||||
struct chat_member_radio_mute_req* req = u_calloc(1, sizeof(*req));
|
||||
struct posted_task* post = u_calloc(1, sizeof(*post));
|
||||
if (!req || !post) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: Android mute request allocation failed"); u_free(req); u_free(post); return -1;
|
||||
}
|
||||
req->inst = inst; req->node_id = node_id; strcpy(req->ch_id, channel_id);
|
||||
post->callback = chat_core_toggle_member_radio_mute_trampoline; post->arg = req;
|
||||
uasync_post_reserved(ua, post);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void utun_bridge_set_member_nick(const char* channel_id, uint64_t node_id, const char* nick) {
|
||||
struct UTUN_INSTANCE* inst = instance_lite_get_instance();
|
||||
sqlite3* db = chat_core_get_db(inst);
|
||||
@@ -2417,6 +2434,17 @@ JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMemberDetai
|
||||
return result;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeToggleMemberRadioMute(
|
||||
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId) {
|
||||
(void)thiz;
|
||||
if (!channelId) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: null Android mute channel"); return JNI_FALSE; }
|
||||
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
|
||||
if (!ch) { DEBUG_ERROR(DEBUG_CATEGORY_RADIO, "radio: Android mute channel conversion failed"); return JNI_FALSE; }
|
||||
int rc = utun_bridge_toggle_member_radio_mute(ch, (uint64_t)nodeId);
|
||||
(*env)->ReleaseStringUTFChars(env, channelId, ch);
|
||||
return rc == 0 ? JNI_TRUE : JNI_FALSE;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetMemberNick(
|
||||
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId, jstring nick) {
|
||||
(void)thiz;
|
||||
|
||||
@@ -166,6 +166,7 @@ int utun_bridge_chat_setting_get_int(const char* name, int def);
|
||||
char* utun_bridge_get_channel_members_json(const char* channel_id);
|
||||
char* utun_bridge_get_member_detail_json(const char* channel_id, uint64_t node_id);
|
||||
void utun_bridge_set_member_nick(const char* channel_id, uint64_t node_id, const char* nick);
|
||||
int utun_bridge_toggle_member_radio_mute(const char* channel_id, uint64_t node_id);
|
||||
int utun_bridge_is_channel_owner(const char* channel_id);
|
||||
void utun_bridge_set_member_flags(const char* channel_id, uint64_t node_id,
|
||||
int admin, int supernode, int moder, int storage,
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import com.utun.chat.data.ChatMember
|
||||
import com.utun.chat.data.parseMemberWire
|
||||
import com.utun.chat.data.toChatMember
|
||||
import com.utun.chat.data.MEMBER_WIRE_SIZE
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/* Проверяет реальные классы из app/build/tmp/kotlin-classes/debug после assembleDebug. */
|
||||
fun main() {
|
||||
val offset = 7
|
||||
val data = ByteArray(offset + 79)
|
||||
ByteBuffer.wrap(data, offset, 79).order(ByteOrder.LITTLE_ENDIAN).apply {
|
||||
val data = ByteArray(offset + MEMBER_WIRE_SIZE)
|
||||
ByteBuffer.wrap(data, offset, MEMBER_WIRE_SIZE).order(ByteOrder.LITTLE_ENDIAN).apply {
|
||||
putLong(42L); put(1); put(1); put(0x12); putShort(250); put(1); put(1)
|
||||
put("Member".toByteArray())
|
||||
}
|
||||
@@ -23,15 +24,20 @@ fun main() {
|
||||
check(enabled.directPresence && enabled.indirectPresence && enabled.bgpPresence)
|
||||
check(enabled.bgpRtt == 321 && enabled.bgpNodes == 5 && enabled.connecting)
|
||||
check(enabled.rtt == 250 && enabled.name == "Member")
|
||||
check(!enabled.radioMuted)
|
||||
data[offset + 10] = 0x32
|
||||
val muted = requireNotNull(parseMemberWire(data, offset)).toChatMember(enabled)
|
||||
check(muted.radioMuted && muted.radioActive && muted.isVerified && muted.directUp)
|
||||
|
||||
data[offset + 10] = 2 // Повторное обновление выключает рацию, сохраняя остальные сведения.
|
||||
val disabled = requireNotNull(parseMemberWire(data, offset)).toChatMember(enabled)
|
||||
val disabled = requireNotNull(parseMemberWire(data, offset)).toChatMember(muted)
|
||||
check(!disabled.radioActive && disabled.isVerified && disabled.directUp && disabled.connecting)
|
||||
check(!disabled.radioMuted)
|
||||
data[offset + 10] = 8
|
||||
check(requireNotNull(parseMemberWire(data, offset)).toChatMember(disabled).isDeleted)
|
||||
check(parseMemberWire(data, -1) == null)
|
||||
check(parseMemberWire(data, Int.MAX_VALUE) == null)
|
||||
check(parseMemberWire(data, offset + 1) == null)
|
||||
check(parseMemberWire(ByteArray(78)) == null)
|
||||
println("PASS: radio on/off; wire offsets; updates preserve verified/type/transports; deleted flag; truncated records")
|
||||
check(parseMemberWire(ByteArray(MEMBER_WIRE_SIZE - 1)) == null)
|
||||
println("PASS: radio subscription/mute; wire offsets; preserved properties; deleted flag; truncated records")
|
||||
}
|
||||
Reference in new issue
Block a user