From 15d0772a20309182d17ac31c52a123a183d7c967 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 6 Oct 2026 16:29:06 +0200 Subject: [PATCH] android: preserve avatar references in JSON refresh and live member snapshots --- .../java/com/utun/chat/data/ChatRepository.kt | 13 ++++++++++++- .../com/utun/chat/viewmodel/ChatViewModel.kt | 8 +++++--- .../jni_bridge/android_jni_bridge.c | 18 ++++++++++++------ 3 files changed, 29 insertions(+), 10 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 0b09b441..10910137 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 @@ -424,6 +424,14 @@ class ChatRepository { val arr = JSONArray(NativeLib.getChannelMembers(channelId)) for (i in 0 until arr.length()) { val obj = arr.getJSONObject(i) + val userinfo = try { JSONObject(obj.optString("ui").ifEmpty { "{}" }) } + catch (e: Exception) { + LogManager.addLog("WARN", "Avatar", "Invalid userinfo group=$channelId owner=${obj.optLong("ni")}: ${e.message}") + JSONObject() + } + val avatarFile = userinfo.optString("avatar_file") + val avatarServer = AvatarProtocol.node(userinfo.optString("avatar_server")) + val validAvatar = avatarServer != 0L && AvatarProtocol.validFile(avatarFile) list.add(ChatMember( nodeId = obj.getLong("ni"), name = obj.optString("n", ""), @@ -436,7 +444,10 @@ class ChatRepository { isDeleted = obj.optInt("dl", 0) != 0, nodeType = obj.optInt("tp", 0), rtt = obj.optInt("rt", 0), - isSelf = obj.optInt("is", 0) != 0 + isSelf = obj.optInt("is", 0) != 0, + avatarServer = if (validAvatar) avatarServer else 0, + avatarFile = if (validAvatar) avatarFile else "", + storageRole = obj.optInt("sr", 0) != 0 )) } } catch (e: Exception) { Log.w("utun-gui", "getMembers error", e) } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index 488602b8..77f2f5db 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -470,13 +470,14 @@ class ChatViewModel : ViewModel() { if (data.size != offset + count * MEMBER_WIRE_SIZE) { LogManager.addLog("ERROR", "VM", "MEMBER_LIST: invalid size ch=$chId count=$count bytes=${data.size}"); return } - val radios = (0 until count).associate { i -> + val snapshot = (0 until count).associate { i -> val w = requireNotNull(parseMemberWire(data, offset + i * MEMBER_WIRE_SIZE)) - w.nodeId to ((w.flags and 0x10) != 0) + w.nodeId to w } + val radios = snapshot.mapValues { (_, w) -> (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) } + _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 }}") } 24 -> { /* MEMBER_UPDATED: [ch_id_len:1][ch_id:var][count:1][member:MEMBER_WIRE_SIZE] */ @@ -952,6 +953,7 @@ class ChatViewModel : ViewModel() { NotificationSound.setMuted(channel.id, !_soundOn.value) r.connectChannel(channel.id) refreshMessages(channel.id) + refreshMembers(channel.id) } /* ── per-channel «play sound on message» ── */ diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 4e85289d..2a5fa5a8 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -727,7 +727,8 @@ char* utun_bridge_get_channel_members_json(const char* channel_id) { snprintf(sql, sizeof(sql), "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.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)" " FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id" " ORDER BY p.node_id ASC", peers_tbl); @@ -755,7 +756,8 @@ char* utun_bridge_get_channel_members_json(const char* channel_id) { int node_rtt = sqlite3_column_int(st, 5); int storage_flag = sqlite3_column_int(st, 6); - int is_admin = 0, is_super = 0, is_moder = 0, is_verified = 0, is_deleted = 0; + int is_admin = 0, is_super = 0, is_moder = 0, is_verified = 0, is_deleted = sqlite3_column_int(st, 10); + int storage_role = 0; if (adm_tags) { char buf[256]; if (json_flat_get(adm_tags, "supernode", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_super = 1; @@ -769,28 +771,32 @@ char* utun_bridge_get_channel_members_json(const char* channel_id) { } if (json_flat_get(adm_tags, "verified", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_verified = 1; if (json_flat_get(adm_tags, "deleted", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) is_deleted = 1; + if (!is_deleted && sqlite3_column_int(st, 8) == 0 && sqlite3_column_int(st, 9) == 64 && + json_flat_get(adm_tags, "storage", buf, sizeof(buf)) == 0 && strcmp(buf, "yes") == 0) storage_role = 1; } char* esc_name = json_escape_alloc(local_nick && local_nick[0] ? local_nick : (name && name[0] ? name : "")); + char* esc_userinfo = json_escape_alloc((const char*)sqlite3_column_text(st, 7)); const char* sep = first ? "" : ","; first = 0; 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}", + "\"vd\":%d,\"dl\":%d,\"tp\":%d,\"rt\":%d,\"is\":%d,\"ui\":\"%s\",\"sr\":%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); + is_verified, is_deleted, node_type, node_rtt, (node_id == my_id) ? 1 : 0, esc_userinfo, storage_role); 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}", + "\"vd\":%d,\"dl\":%d,\"tp\":%d,\"rt\":%d,\"is\":%d,\"ui\":\"%s\",\"sr\":%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); + is_verified, is_deleted, node_type, node_rtt, (node_id == my_id) ? 1 : 0, esc_userinfo, storage_role); + u_free(esc_userinfo); u_free(esc_name); } sqlite3_finalize(st);