From 658c248867c329fb7806eceb2a8dfc8fd613ec29 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sun, 4 Oct 2026 16:00:41 +0200 Subject: [PATCH] Request Android member snapshots on the core thread --- .../java/com/utun/chat/data/ChatRepository.kt | 9 ++++--- .../main/java/com/utun/chat/data/NativeLib.kt | 3 +++ .../jni_bridge/android_jni_bridge.c | 25 +++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt index 453a1f1e..6df75452 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt @@ -72,6 +72,7 @@ data class Message(val id: Long, val author: String, val text: String, val ts: L data class ChatMember(val nodeId: Long, val name: String, val online: Boolean = false, val isAdmin: Boolean = false, val isSuper: Boolean = false, val isModer: Boolean = false, val isStorage: Boolean = false, + val radioActive: Boolean = false, val isVerified: Boolean = false, val isDeleted: Boolean = false, val nodeType: Int = 0, val rtt: Int = 0, val isSelf: Boolean = false, @@ -167,7 +168,7 @@ data class MemberWire( ) fun parseMemberWire(data: ByteArray, offset: Int = 0): MemberWire? { - if (offset + 79 > data.size) return null + if (offset < 0 || offset > data.size - 79) return null val buf = ByteBuffer.wrap(data, offset, 79).order(ByteOrder.LITTLE_ENDIAN) val nodeId = buf.getLong() val online = buf.get().toInt() and 0xFF @@ -181,10 +182,12 @@ fun parseMemberWire(data: ByteArray, offset: Int = 0): MemberWire? { return MemberWire(nodeId, online, isSelf, flags, rtt, natType, storage, name) } -fun MemberWire.toChatMember(): ChatMember = ChatMember( - nodeId = nodeId, name = name, online = online != 0, +/* Wire не содержит verified/nodeType и состояния транспортов: сохраняем их из текущей строки. */ +fun MemberWire.toChatMember(previous: ChatMember = ChatMember(nodeId, name)): ChatMember = previous.copy( + 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, rtt = if (rtt == 0xFFFF) 0 else rtt, isSelf = isSelf ) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt index 10b2d6c7..4152ff11 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt @@ -115,6 +115,8 @@ object NativeLib { /* ── Channel members ── */ fun getChannelMembers(channelId: String): String = nativeGetChannelMembers(channelId) ?: "[]" + /** Асинхронный live-снимок; ответ — событие MEMBER_LIST. */ + fun requestMemberList(channelId: String): Boolean = nativeRequestMemberList(channelId) fun getMemberDetail(channelId: String, nodeId: Long): String = nativeGetMemberDetail(channelId, nodeId) ?: "{}" fun setMemberNick(channelId: String, nodeId: Long, nick: String) { nativeSetMemberNick(channelId, nodeId, nick) } fun isChannelOwner(channelId: String): Boolean = nativeIsChannelOwner(channelId) @@ -240,6 +242,7 @@ object NativeLib { /* ── Channel members JNI ── */ private external fun nativeGetChannelMembers(channelId: String): String? + private external fun nativeRequestMemberList(channelId: String): Boolean private external fun nativeGetMemberDetail(channelId: String, nodeId: Long): String? private external fun nativeSetMemberNick(channelId: String, nodeId: Long, nick: String) private external fun nativeIsChannelOwner(channelId: String): Boolean diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index ec36891c..de224da3 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -27,6 +27,7 @@ #include "../../../lib/mem.h" #include "../../../lib/sqlite3.h" #include "../../../src/chat/chat_core.h" +#include "../../../src/chat/chat_member.h" #include "../../../src/chat/invite_build.h" #include "../../../src/chat/chat_setting.h" #include "../../../src/routing_layer/topo_node_sqlite.h" @@ -2294,6 +2295,30 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeVoiceDecodeClose( /* ── Channel members JNI ── */ +/* Живой снимок группы читается в uasync; результат приходит как MEMBER_LIST. */ +JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeRequestMemberList( + JNIEnv* env, jobject thiz, jstring channelId) { + (void)thiz; + struct UTUN_INSTANCE* inst = instance_lite_get_instance(); + struct UASYNC* ua = instance_lite_get_uasync(); + if (!inst || !ua || !channelId) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: missing instance/uasync/channel"); return JNI_FALSE; + } + const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL); + if (!ch) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: cannot read channel"); return JNI_FALSE; } + struct chat_member_list_req* req = u_calloc(1, sizeof(*req)); + if (!req || !ch[0] || strlen(ch) >= sizeof(req->ch_id)) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: allocation/invalid channel ch=%s", ch); + u_free(req); (*env)->ReleaseStringUTFChars(env, channelId, ch); return JNI_FALSE; + } + req->inst = inst; + memcpy(req->ch_id, ch, strlen(ch) + 1); + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "request_member_list: queued ch=%s", ch); + (*env)->ReleaseStringUTFChars(env, channelId, ch); + uasync_post(ua, chat_core_request_member_list_trampoline, req); + return JNI_TRUE; +} + JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetChannelMembers( JNIEnv* env, jobject thiz, jstring channelId) { (void)thiz;