Browse Source

chat: автоподключение к узлам группы (вкл/выкл) + ручной connect к узлу (desktop+android); фикс reinit при QR-join

v2
evgeny 3 weeks ago
parent
commit
d7bb58c6fb
  1. 60
      src/chat/chat_channel.c
  2. 1
      src/chat/chat_core.c
  3. 9
      src/chat/chat_core.h
  4. 4
      src/chat/chat_core_priv.h
  5. 1
      src/chat/chat_setting.c
  6. 3
      src/routing_layer/topo_group.c
  7. 57
      src/routing_layer/topo_group_connect.c
  8. 5
      src/routing_layer/topo_group_connect.h
  9. 5
      src/transport_layer/etcp.c
  10. 1
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt
  11. 4
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt
  12. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  13. 5
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt
  14. 16
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt
  15. 6
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  16. 28
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  17. 3
      tools/chatgui-android/jni_bridge/android_jni_bridge.h
  18. 12
      tools/chatgui/src/accountlist.cpp
  19. 22
      tools/chatgui/src/settingsdialog.cpp
  20. 2
      tools/chatgui/src/settingsdialog.h

60
src/chat/chat_channel.c

@ -6,6 +6,7 @@
#include "chat_core_priv.h" #include "chat_core_priv.h"
#include "chat_event.h" #include "chat_event.h"
#include "chat_setting.h"
#include "../utun_instance.h" #include "../utun_instance.h"
#include "../ntp_time.h" #include "../ntp_time.h"
@ -224,6 +225,12 @@ void chat_core_connect_channel(const char* ch_id) {
return; return;
} }
if (!chat_setting_get_int("group_autoconnect", 1)) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel ch=%s — group_autoconnect disabled, skip auto-connect loop",
CC_ID, ch_id);
return;
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel ch=%s gid=%016llx group_type=%d — starting connect_init", CC_ID, ch_id, (unsigned long long)gid, group->group_type); DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_channel ch=%s gid=%016llx group_type=%d — starting connect_init", CC_ID, ch_id, (unsigned long long)gid, group->group_type);
topo_group_connect_init(group); topo_group_connect_init(group);
} }
@ -235,3 +242,56 @@ void chat_core_connect_channel_trampoline(void* arg) {
chat_core_connect_channel(ch_id); chat_core_connect_channel(ch_id);
u_free(ch_id); u_free(ch_id);
} }
/* ─── однократное подключение к конкретному узлу канала (GUI → uasync) ─── */
void chat_core_connect_node(const char* ch_id, uint64_t node_id) {
if (!g_cc.initialized || !g_cc.inst || !ch_id || !ch_id[0] || !node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_node — skip (not ready or invalid args)", CC_ID);
return;
}
if (node_id == g_cc.inst->node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_node — skip self node=0x%016llx", CC_ID, (unsigned long long)node_id);
return;
}
chat_core_ensure_channel_ready(ch_id);
uint64_t gid = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* group = topo_groups_find(g_cc.inst->topo_groups, gid);
if (!group) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: connect_node — group not found ch=%s", CC_ID, ch_id);
return;
}
topo_group_connect_node_once(group, node_id);
}
void chat_core_connect_node_trampoline(void* arg) {
struct chat_connect_node_req* r = (struct chat_connect_node_req*)arg;
if (!r) return;
chat_core_connect_node(r->ch_id, r->node_id);
u_free(r);
}
/* ─── применение настройки group_autoconnect ко всем CHAT-группам ─── */
void chat_core_apply_group_autoconnect(void) {
int enabled = chat_setting_get_int("group_autoconnect", 1);
struct UTUN_INSTANCE* inst = g_cc.inst;
if (!inst || !inst->topo_groups || !inst->topo_groups->group_list) return;
struct ll_entry* e = inst->topo_groups->group_list->head;
while (e) {
struct TOPO_GROUP* g = (struct TOPO_GROUP*)e;
if (g->group_type == TOPO_GROUP_TYPE_CHAT) {
if (enabled && !g->connect) {
topo_group_connect_init(g);
} else if (!enabled && g->connect) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: group_autoconnect OFF — stop auto-connect ch=%s", CC_ID, g->channel_id);
topo_group_connect_destroy(g);
}
}
e = e->next;
}
}

1
src/chat/chat_core.c

@ -260,6 +260,7 @@ int chat_core_is_initialized(void) { return g_cc.initialized; }
void chat_core_set_setting(const char* name, const char* value) { void chat_core_set_setting(const char* name, const char* value) {
chat_setting_set(name, value); chat_setting_set(name, value);
if (name && strcmp(name, "group_autoconnect") == 0) chat_core_apply_group_autoconnect();
} }
struct chat_setting_arg { char data[256]; }; struct chat_setting_arg { char data[256]; };

9
src/chat/chat_core.h

@ -96,6 +96,15 @@ void chat_core_set_setting_trampoline(void* arg);
void chat_core_connect_channel(const char* ch_id); void chat_core_connect_channel(const char* ch_id);
void chat_core_connect_channel_trampoline(void* arg); void chat_core_connect_channel_trampoline(void* arg);
/* Однократная попытка подключения к конкретному узлу канала (GUI → uasync) */
void chat_core_connect_node(const char* ch_id, uint64_t node_id);
void chat_core_connect_node_trampoline(void* arg);
struct chat_connect_node_req {
char ch_id[64];
uint64_t node_id;
};
/* Трамплин для gui_bridge_post_uasync (GUI → uasync) */ /* Трамплин для gui_bridge_post_uasync (GUI → uasync) */
void chat_core_submit_trampoline(void* arg); void chat_core_submit_trampoline(void* arg);

4
src/chat/chat_core_priv.h

@ -105,4 +105,8 @@ void si_register(struct DB_SYNC_INSTANCE* si, const char* ch_id);
void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg); void on_msg_inserted(struct DB_SYNC_INSTANCE* si, uint64_t record_ts, const char* data, size_t len, uint64_t author, void* arg);
/* ── применить настройку group_autoconnect ко всем CHAT-группам (chat_channel.c) ── */
void chat_core_apply_group_autoconnect(void);
#endif /* CHAT_CORE_PRIV_H */ #endif /* CHAT_CORE_PRIV_H */

1
src/chat/chat_setting.c

@ -53,6 +53,7 @@ static const struct chat_setting_def g_setting_defs[] = {
{"join_policy", CHAT_SETTING_INT, 1, 0, 2}, /* 0=autojoin, 1=ask, 2=deny */ {"join_policy", CHAT_SETTING_INT, 1, 0, 2}, /* 0=autojoin, 1=ask, 2=deny */
{"whisper_enabled", CHAT_SETTING_BOOL, 0, 0, 1}, {"whisper_enabled", CHAT_SETTING_BOOL, 0, 0, 1},
{"whisper_model_path", CHAT_SETTING_STRING, 0, 0, 0}, {"whisper_model_path", CHAT_SETTING_STRING, 0, 0, 0},
{"group_autoconnect", CHAT_SETTING_BOOL, 1, 0, 1},
}; };
#define CHAT_SETTING_COUNT (sizeof(g_setting_defs) / sizeof(g_setting_defs[0])) #define CHAT_SETTING_COUNT (sizeof(g_setting_defs) / sizeof(g_setting_defs[0]))

3
src/routing_layer/topo_group.c

@ -26,6 +26,7 @@
#include "topo_group_connect.h" #include "topo_group_connect.h"
#include "topo_recovery.h" #include "topo_recovery.h"
#include "broadcast.h" #include "broadcast.h"
#include "../chat/chat_setting.h"
#define TOPO_GROUP_UTUN 0x8000000000000000ULL #define TOPO_GROUP_UTUN 0x8000000000000000ULL
@ -522,7 +523,7 @@ struct TOPO_GROUP* topo_groups_create_group(struct TOPO_GROUPS* g, uint64_t grou
} }
DEBUG_INFO(DEBUG_CATEGORY_BGP, "Group created: group_id=%016llx type=%u ch_id=%s", (unsigned long long)group_id, group_type, group->channel_id); DEBUG_INFO(DEBUG_CATEGORY_BGP, "Group created: group_id=%016llx type=%u ch_id=%s", (unsigned long long)group_id, group_type, group->channel_id);
if (group_type == TOPO_GROUP_TYPE_CHAT) topo_group_connect_init(group); if (group_type == TOPO_GROUP_TYPE_CHAT && chat_setting_get_int("group_autoconnect", 1)) topo_group_connect_init(group);
return group; return group;
} }

57
src/routing_layer/topo_group_connect.c

@ -26,6 +26,7 @@
#include "topo_group.h" #include "topo_group.h"
#include "topo_node.h" #include "topo_node.h"
#include "topo_node_sqlite.h" #include "topo_node_sqlite.h"
#include "../utun_instance.h"
#include "../transport_layer/node_conn_direct.h" #include "../transport_layer/node_conn_direct.h"
#include "../lib/debug_config.h" #include "../lib/debug_config.h"
#include "../lib/mem.h" #include "../lib/mem.h"
@ -523,3 +524,59 @@ static void tgc_cycle_restart(struct TOPO_GROUP_CONNECT* gc) {
gc->phase_timer = uasync_set_timeout(gc->group->instance->ua, gc->phase_timer = uasync_set_timeout(gc->group->instance->ua,
TGC_DIRECT_TIMEOUT_MS * 10, gc, tgc_phase1_timeout, "tgc_phase1"); TGC_DIRECT_TIMEOUT_MS * 10, gc, tgc_phase1_timeout, "tgc_phase1");
} }
/* ═══════════════════════════════════════════════════════════════════════
* Однократное подключение к конкретному узлу (ручной "Connect" из GUI)
* ══════════════════════════════════════════════════════════════════════ */
struct tgc_once_ctx {
struct TOPO_GROUP* group;
uint64_t node_id;
struct NODE_CONN_DIRECT* h;
};
static void tgc_once_cb(struct NODE_CONN_DIRECT* h, enum ncd_event ev, void* arg) {
struct tgc_once_ctx* c = (struct tgc_once_ctx*)arg;
struct ETCP_CONN* conn = node_conn_direct_get_conn(h);
if (ev == NCD_EVENT_UP && conn) {
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(c->group, c->node_id);
if (nq) nq->handle = h;
node_conn_direct_set_callback(h, NULL, NULL);
topo_group_new_conn(c->group, conn);
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: once-connect UP node=0x%016llx ch=%s",
TGC_ID, (unsigned long long)c->node_id, c->group->channel_id);
} else if (ev == NCD_EVENT_TIMEOUT) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "%s: once-connect TIMEOUT node=0x%016llx ch=%s",
TGC_ID, (unsigned long long)c->node_id, c->group->channel_id);
node_conn_direct_close(h);
} else if (ev == NCD_EVENT_DOWN) {
DEBUG_WARN(DEBUG_CATEGORY_BGP, "%s: once-connect DOWN node=0x%016llx ch=%s",
TGC_ID, (unsigned long long)c->node_id, c->group->channel_id);
node_conn_direct_close(h);
}
u_free(c);
}
int topo_group_connect_node_once(struct TOPO_GROUP* group, uint64_t node_id) {
if (!group || group->group_type != TOPO_GROUP_TYPE_CHAT || !node_id) return -1;
if (node_id == group->instance->node_id) return -1;
struct ETCP_CONN* ex = instance_find_conn(group->instance, node_id);
if (ex && ex->links_up && ex->initialized) {
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: once-connect node=0x%016llx already connected — add to group ch=%s",
TGC_ID, (unsigned long long)node_id, group->channel_id);
topo_group_new_conn(group, ex);
return 0;
}
struct tgc_once_ctx* c = u_calloc(1, sizeof(*c));
if (!c) return -1;
c->group = group; c->node_id = node_id;
int r = node_conn_direct_open(group->instance, node_id, tgc_once_cb, c, &c->h, NULL);
if (r == NCD_ERR || !c->h) { u_free(c); return -1; }
DEBUG_INFO(DEBUG_CATEGORY_BGP, "%s: once-connect node=0x%016llx ch=%s started (rc=%d)",
TGC_ID, (unsigned long long)node_id, group->channel_id, r);
return 0;
}

5
src/routing_layer/topo_group_connect.h

@ -33,4 +33,9 @@ void topo_group_connect_on_down(struct TOPO_GROUP* group, struct ETCP_CONN* conn
int topo_group_connect_active_count(struct TOPO_GROUP* group); int topo_group_connect_active_count(struct TOPO_GROUP* group);
void topo_group_connect_restart(struct TOPO_GROUP* group); void topo_group_connect_restart(struct TOPO_GROUP* group);
/* Однократная попытка подключения к конкретному узлу группы (без бесконечного
* цикла). При успехе соединение добавляется в BGP-группу (topo_group_new_conn),
* handle сохраняется в nq->handle. Используется для ручного "Connect" из GUI. */
int topo_group_connect_node_once(struct TOPO_GROUP* group, uint64_t node_id);
#endif #endif

5
src/transport_layer/etcp.c

@ -640,10 +640,11 @@ void etcp_conn_reinit_id(struct ETCP_CONN* etcp, const char* reason, uint64_t re
// Применение чужого reset_id (эпохи ресета пира). Симметрично: // Применение чужого reset_id (эпохи ресета пира). Симметрично:
// если uid пира изменился — сбрасываем своё состояние (свой uid сохраняем). // если uid пира изменился — сбрасываем своё состояние (свой uid сохраняем).
void etcp_conn_apply_peer_reset_id(struct ETCP_CONN* conn, uint64_t peer_reset_id) { void etcp_conn_apply_peer_reset_id(struct ETCP_CONN* conn, uint64_t peer_reset_id) {
if (!conn) return; if (!conn || peer_reset_id == 0) return; /* 0 = неизвестно (STCP-сервер ещё не создал conn) */
if (peer_reset_id == conn->peer_reset_id) return; // uid пира не изменился if (peer_reset_id == conn->peer_reset_id) return; // uid пира не изменился
uint64_t old = conn->peer_reset_id;
conn->peer_reset_id = peer_reset_id; conn->peer_reset_id = peer_reset_id;
if (conn->got_initial_pkt) { if (old != 0 && conn->got_initial_pkt) {
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] peer reset id changed to %016llx — resetting self", DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] peer reset id changed to %016llx — resetting self",
conn->log_name, (unsigned long long)peer_reset_id); conn->log_name, (unsigned long long)peer_reset_id);
etcp_conn_reinit(conn, "peer reset epoch"); etcp_conn_reinit(conn, "peer reset epoch");

1
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt

@ -385,6 +385,7 @@ class ChatRepository {
fun joinChannel(channelId: Long, nodeId: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int) = fun joinChannel(channelId: Long, nodeId: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int) =
NativeLib.joinChannel(channelId, nodeId, pubkey, addrs, addrCount) NativeLib.joinChannel(channelId, nodeId, pubkey, addrs, addrCount)
fun connectChannel(channelId: String) = NativeLib.connectChannel(channelId) fun connectChannel(channelId: String) = NativeLib.connectChannel(channelId)
fun connectMemberNode(channelId: String, nodeId: Long) = NativeLib.connectMemberNode(channelId, nodeId)
fun isReady() = NativeLib.isReady() fun isReady() = NativeLib.isReady()
fun getMembers(channelId: String): List<ChatMember> { fun getMembers(channelId: String): List<ChatMember> {

4
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt

@ -46,6 +46,7 @@ class ConfigProvider(private val context: Context) {
private val chatStandbyActiveSec = intPreferencesKey("chat_standby_active_sec") private val chatStandbyActiveSec = intPreferencesKey("chat_standby_active_sec")
private val chatStandbySleepSec = intPreferencesKey("chat_standby_sleep_sec") private val chatStandbySleepSec = intPreferencesKey("chat_standby_sleep_sec")
private val chatStandbyMinSleepSec = intPreferencesKey("chat_standby_min_sleep_sec") private val chatStandbyMinSleepSec = intPreferencesKey("chat_standby_min_sleep_sec")
private val chatGroupAutoconnect = intPreferencesKey("chat_group_autoconnect")
private val firstLaunchDone = booleanPreferencesKey("first_launch_done") private val firstLaunchDone = booleanPreferencesKey("first_launch_done")
private val connectionModeKey = stringPreferencesKey("connection_mode") private val connectionModeKey = stringPreferencesKey("connection_mode")
private val serviceEnabledKey = booleanPreferencesKey("service_enabled") private val serviceEnabledKey = booleanPreferencesKey("service_enabled")
@ -127,6 +128,7 @@ class ConfigProvider(private val context: Context) {
key == "chat.standby_active_sec" -> context.dataStore.data.first()[chatStandbyActiveSec] ?: 2 key == "chat.standby_active_sec" -> context.dataStore.data.first()[chatStandbyActiveSec] ?: 2
key == "chat.standby_sleep_sec" -> context.dataStore.data.first()[chatStandbySleepSec] ?: 60 key == "chat.standby_sleep_sec" -> context.dataStore.data.first()[chatStandbySleepSec] ?: 60
key == "chat.standby_min_sleep_sec" -> context.dataStore.data.first()[chatStandbyMinSleepSec] ?: 15 key == "chat.standby_min_sleep_sec" -> context.dataStore.data.first()[chatStandbyMinSleepSec] ?: 15
key == "chat.group_autoconnect" -> context.dataStore.data.first()[chatGroupAutoconnect] ?: 1
else -> 0 else -> 0
} }
} }
@ -157,6 +159,7 @@ class ConfigProvider(private val context: Context) {
"chat.standby_active_sec" -> prefs[chatStandbyActiveSec] = value.toIntOrNull() ?: 2 "chat.standby_active_sec" -> prefs[chatStandbyActiveSec] = value.toIntOrNull() ?: 2
"chat.standby_sleep_sec" -> prefs[chatStandbySleepSec] = value.toIntOrNull() ?: 60 "chat.standby_sleep_sec" -> prefs[chatStandbySleepSec] = value.toIntOrNull() ?: 60
"chat.standby_min_sleep_sec" -> prefs[chatStandbyMinSleepSec] = value.toIntOrNull() ?: 15 "chat.standby_min_sleep_sec" -> prefs[chatStandbyMinSleepSec] = value.toIntOrNull() ?: 15
"chat.group_autoconnect" -> prefs[chatGroupAutoconnect] = value.toIntOrNull() ?: 1
} }
} }
} }
@ -387,6 +390,7 @@ class ConfigProvider(private val context: Context) {
appendLine("standby_active_sec=${getInt("chat.standby_active_sec")}") appendLine("standby_active_sec=${getInt("chat.standby_active_sec")}")
appendLine("standby_sleep_sec=${getInt("chat.standby_sleep_sec")}") appendLine("standby_sleep_sec=${getInt("chat.standby_sleep_sec")}")
appendLine("standby_min_sleep_sec=${getInt("chat.standby_min_sleep_sec")}") appendLine("standby_min_sleep_sec=${getInt("chat.standby_min_sleep_sec")}")
appendLine("group_autoconnect=${getInt("chat.group_autoconnect")}")
appendLine() appendLine()
appendLine("[debug]") appendLine("[debug]")
appendLine("console_level=info") appendLine("console_level=info")

2
tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

@ -75,6 +75,7 @@ object NativeLib {
} }
fun deriveNodeId(pubkey: ByteArray): Long = nativeDeriveNodeId(pubkey) fun deriveNodeId(pubkey: ByteArray): Long = nativeDeriveNodeId(pubkey)
fun connectChannel(channelId: String) { nativeConnectChannel(channelId) } fun connectChannel(channelId: String) { nativeConnectChannel(channelId) }
fun connectMemberNode(channelId: String, nodeId: Long) { nativeConnectMemberNode(channelId, nodeId) }
fun getChannels(): String = nativeGetChannels() ?: "[]" fun getChannels(): String = nativeGetChannels() ?: "[]"
fun getMessages(channelId: String, limit: Int = 50): String = nativeGetMessages(channelId, limit) ?: "[]" fun getMessages(channelId: String, limit: Int = 50): String = nativeGetMessages(channelId, limit) ?: "[]"
fun clearDatabase(): Boolean = nativeClearDatabase() fun clearDatabase(): Boolean = nativeClearDatabase()
@ -148,6 +149,7 @@ object NativeLib {
private external fun nativeJoinChannel(channelId: Long, nodeId: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int) private external fun nativeJoinChannel(channelId: Long, nodeId: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int)
private external fun nativeDeriveNodeId(pubkey: ByteArray): Long private external fun nativeDeriveNodeId(pubkey: ByteArray): Long
private external fun nativeConnectChannel(channelId: String) private external fun nativeConnectChannel(channelId: String)
private external fun nativeConnectMemberNode(channelId: String, nodeId: Long)
private external fun nativeGetChannels(): String? private external fun nativeGetChannels(): String?
private external fun nativeGetMessages(channelId: String, limit: Int): String? private external fun nativeGetMessages(channelId: String, limit: Int): String?
private external fun nativeSetDebugLevel(category: String, level: String) private external fun nativeSetDebugLevel(category: String, level: String)

5
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt

@ -83,6 +83,7 @@ fun MemberListScreen(
member = m, member = m,
isSelected = selectedMember?.nodeId == m.nodeId, isSelected = selectedMember?.nodeId == m.nodeId,
onClick = { viewModel.showMemberDetail(channel.id, m.nodeId) }, onClick = { viewModel.showMemberDetail(channel.id, m.nodeId) },
onConnect = { viewModel.connectMemberNode(channel.id, m.nodeId) },
onEdit = { onEdit = {
editMember = m editMember = m
showEditDialog = true showEditDialog = true
@ -133,6 +134,7 @@ private fun MemberListItem(
member: ChatMember, member: ChatMember,
isSelected: Boolean = false, isSelected: Boolean = false,
onClick: () -> Unit, onClick: () -> Unit,
onConnect: () -> Unit,
onEdit: () -> Unit onEdit: () -> Unit
) { ) {
val onlineColor = when { val onlineColor = when {
@ -223,6 +225,9 @@ private fun MemberListItem(
} }
} }
if (!member.isSelf) {
TextButton(onClick = onConnect) { Text("Connect") }
}
TextButton(onClick = onEdit) { Text("Edit") } TextButton(onClick = onEdit) { Text("Edit") }
} }
} }

16
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt

@ -118,6 +118,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
var servers by remember { mutableStateOf<List<ServerEntry>>(emptyList()) } var servers by remember { mutableStateOf<List<ServerEntry>>(emptyList()) }
var availableIfaces by remember { mutableStateOf<List<Pair<String, String>>>(emptyList()) } var availableIfaces by remember { mutableStateOf<List<Pair<String, String>>>(emptyList()) }
var connectionMode by remember { mutableStateOf("manual") } var connectionMode by remember { mutableStateOf("manual") }
var autoConnect by remember { mutableStateOf(true) }
val localSocketStates by vm.localSockets.collectAsState() val localSocketStates by vm.localSockets.collectAsState()
val serviceEnabled by provider.serviceEnabledFlow.collectAsState(initial = true) val serviceEnabled by provider.serviceEnabledFlow.collectAsState(initial = true)
@ -143,6 +144,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
servers = provider.loadServersFromConfig() servers = provider.loadServersFromConfig()
availableIfaces = provider.getAvailableInterfaces() availableIfaces = provider.getAvailableInterfaces()
connectionMode = provider.getConnectionMode() connectionMode = provider.getConnectionMode()
autoConnect = provider.getInt("chat.group_autoconnect") != 0
} }
val save = { s: List<ServerEntry> -> onDirty(true); servers = s; provider.saveServersToConfig(s) } val save = { s: List<ServerEntry> -> onDirty(true); servers = s; provider.saveServersToConfig(s) }
@ -336,6 +338,20 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
HorizontalDivider(Modifier.padding(vertical = 8.dp)) HorizontalDivider(Modifier.padding(vertical = 8.dp))
/* ── Chat ── */
Text("Chat", style = MaterialTheme.typography.titleMedium)
Row(verticalAlignment = Alignment.CenterVertically) {
Text("Auto-connect to group nodes", modifier = Modifier.weight(1f))
Switch(checked = autoConnect, onCheckedChange = {
autoConnect = it
val s = if (it) "1" else "0"
scope.launch { provider.setValue("chat.group_autoconnect", s) }
NativeLib.setChatSetting("group_autoconnect", s)
})
}
HorizontalDivider(Modifier.padding(vertical = 8.dp))
/* Debug */ /* Debug */
Text("Debug", style = MaterialTheme.typography.titleMedium) Text("Debug", style = MaterialTheme.typography.titleMedium)
DebugLevelDropdown(label = "Global Level", current = debugLevel.uppercase(), onSelect = setDebug) DebugLevelDropdown(label = "Global Level", current = debugLevel.uppercase(), onSelect = setDebug)

6
tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

@ -519,6 +519,12 @@ class ChatViewModel : ViewModel() {
r.transferAdmin(channelId, nodeId) r.transferAdmin(channelId, nodeId)
} }
fun connectMemberNode(channelId: String, nodeId: Long) {
val r = repo ?: return
r.connectMemberNode(channelId, nodeId)
refreshMembers(channelId)
}
fun refreshMemberLinks(nodeId: Long) { fun refreshMemberLinks(nodeId: Long) {
if (nodeId == 0L) { _memberLinks.value = MemberLinks(); return } if (nodeId == 0L) { _memberLinks.value = MemberLinks(); return }
val r = repo ?: return val r = repo ?: return

28
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -274,6 +274,22 @@ void utun_bridge_connect_channel(const char* channel_id) {
uasync_post(ua, chat_core_connect_channel_trampoline, cid); uasync_post(ua, chat_core_connect_channel_trampoline, cid);
} }
void utun_bridge_connect_member_node(const char* channel_id, uint64_t node_id) {
if (!channel_id || !channel_id[0] || node_id == 0) {
bridge_log(BLEV_ERROR, "connect_member_node: invalid args ch=%s node=0x%016llx",
channel_id ? channel_id : "(null)", (unsigned long long)node_id);
return;
}
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) return;
struct chat_connect_node_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "connect_member_node: OOM"); return; }
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
req->node_id = node_id;
bridge_log(BLEV_INFO, "connect_member_node ch=%s node=0x%016llx", channel_id, (unsigned long long)node_id);
uasync_post(ua, chat_core_connect_node_trampoline, req);
}
static char* json_escape_alloc(const char* str) { static char* json_escape_alloc(const char* str) {
if (!str) return u_strdup(""); if (!str) return u_strdup("");
size_t len = strlen(str), cap = len * 2 + 8; size_t len = strlen(str), cap = len * 2 + 8;
@ -518,7 +534,7 @@ void utun_bridge_set_debug_level(const char* category, const char* level) {
} }
void utun_bridge_chat_setting_set(const char* name, const char* value) { void utun_bridge_chat_setting_set(const char* name, const char* value) {
chat_setting_set(name, value); chat_core_set_setting(name, value);
} }
int utun_bridge_chat_setting_get_int(const char* name, int def) { int utun_bridge_chat_setting_get_int(const char* name, int def) {
@ -1561,6 +1577,14 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectChannel(
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch); if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
} }
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectMemberNode(
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_connect_member_node(ch, (uint64_t)nodeId);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetDebugLevel( JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetDebugLevel(
JNIEnv* env, jobject thiz, jstring category, jstring level) { JNIEnv* env, jobject thiz, jstring category, jstring level) {
(void)thiz; (void)thiz;
@ -1576,7 +1600,7 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetChatSetting(
(void)thiz; (void)thiz;
const char* n = (*env)->GetStringUTFChars(env, name, NULL); const char* n = (*env)->GetStringUTFChars(env, name, NULL);
const char* v = (*env)->GetStringUTFChars(env, value, NULL); const char* v = (*env)->GetStringUTFChars(env, value, NULL);
chat_setting_set(n, v); chat_core_set_setting(n, v);
if (v) (*env)->ReleaseStringUTFChars(env, value, v); if (v) (*env)->ReleaseStringUTFChars(env, value, v);
if (n) (*env)->ReleaseStringUTFChars(env, name, n); if (n) (*env)->ReleaseStringUTFChars(env, name, n);
} }

3
tools/chatgui-android/jni_bridge/android_jni_bridge.h

@ -60,6 +60,9 @@ void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id,
/* Connect to all peers of a channel (auto-connect on select) */ /* Connect to all peers of a channel (auto-connect on select) */
void utun_bridge_connect_channel(const char* channel_id); void utun_bridge_connect_channel(const char* channel_id);
/* Однократная попытка подключения к конкретному узлу канала (ручной "Connect"). */
void utun_bridge_connect_member_node(const char* channel_id, uint64_t node_id);
/* ── Queries ── */ /* ── Queries ── */
char* utun_bridge_get_channels_json(void); char* utun_bridge_get_channels_json(void);

12
tools/chatgui/src/accountlist.cpp

@ -458,11 +458,23 @@ void AccountList::onMemberContextMenu(const QPoint& pos) {
QString name = m_model->data(index, Qt::DisplayRole).toString(); QString name = m_model->data(index, Qt::DisplayRole).toString();
QMenu menu(m_listView); QMenu menu(m_listView);
QAction* connectAction = menu.addAction("Connect");
if (nodeId == m_db->myNodeId()) connectAction->setEnabled(false);
QAction* renameAction = menu.addAction("Rename"); QAction* renameAction = menu.addAction("Rename");
if (nodeId == m_db->myNodeId()) renameAction->setEnabled(false); if (nodeId == m_db->myNodeId()) renameAction->setEnabled(false);
QAction* editAction = menu.addAction("Edit"); QAction* editAction = menu.addAction("Edit");
QAction* chosen = menu.exec(m_listView->viewport()->mapToGlobal(pos)); QAction* chosen = menu.exec(m_listView->viewport()->mapToGlobal(pos));
if (chosen == connectAction) {
struct chat_connect_node_req* r = (struct chat_connect_node_req*)u_malloc(sizeof(*r));
if (!r) return;
memset(r, 0, sizeof(*r));
strncpy(r->ch_id, m_channelId.toUtf8().constData(), sizeof(r->ch_id) - 1);
r->node_id = nodeId;
gui_bridge_post_uasync_fn(chat_core_connect_node_trampoline, r);
return;
}
if (chosen == renameAction) { if (chosen == renameAction) {
RenameDialog dlg(m_db, m_channelId, nodeId, name, this); RenameDialog dlg(m_db, m_channelId, nodeId, name, this);
if (dlg.exec() == QDialog::Accepted) { if (dlg.exec() == QDialog::Accepted) {

22
tools/chatgui/src/settingsdialog.cpp

@ -96,6 +96,10 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
"QComboBox { border: 1px solid palette(mid); border-radius: 4px;" "QComboBox { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 6px 10px; font-size: 13px; background: palette(base); }"); " padding: 6px 10px; font-size: 13px; background: palette(base); }");
profLayout->addRow(joinLabel, m_joinPolicyCombo); profLayout->addRow(joinLabel, m_joinPolicyCombo);
m_autoConnectCheck = new QCheckBox("Auto-connect to group nodes", profilePage);
m_autoConnectCheck->setStyleSheet("font-size: 13px;");
profLayout->addRow(m_autoConnectCheck);
m_pages->addWidget(profilePage); m_pages->addWidget(profilePage);
// Storage page // Storage page
@ -181,6 +185,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
m_sortCombo->setCurrentIndex(sortMode == 1 ? 1 : 0); m_sortCombo->setCurrentIndex(sortMode == 1 ? 1 : 0);
int joinPolicy = db->getUiStateInt("join_policy", 1); int joinPolicy = db->getUiStateInt("join_policy", 1);
m_joinPolicyCombo->setCurrentIndex(joinPolicy >= 0 && joinPolicy <= 2 ? joinPolicy : 1); m_joinPolicyCombo->setCurrentIndex(joinPolicy >= 0 && joinPolicy <= 2 ? joinPolicy : 1);
m_autoConnectCheck->setChecked(db->getUiStateInt("group_autoconnect", 1) != 0);
} }
m_networkPage->loadFromConfig(m_configPath); m_networkPage->loadFromConfig(m_configPath);
m_soundPage->loadFromDb(); m_soundPage->loadFromDb();
@ -295,6 +300,23 @@ void SettingsDialog::onSave() {
postChatSetting(m_configPath, "join_policy", v); postChatSetting(m_configPath, "join_policy", v);
} }
} }
/* save group_autoconnect */
{
const char* key = "group_autoconnect";
int v = m_autoConnectCheck->isChecked() ? 1 : 0;
QByteArray val = QByteArray::number(v);
size_t klen = strlen(key), vl = (size_t)val.size(), total = klen + 1 + vl + 1;
if (total <= 256) {
void* arg = u_malloc(total);
memcpy(arg, key, klen + 1);
memcpy((char*)arg + klen + 1, val.constData(), vl + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
}
if (!m_configPath.isEmpty()) {
postChatSetting(m_configPath, "group_autoconnect", v);
}
}
accept(); accept();
} }

2
tools/chatgui/src/settingsdialog.h

@ -7,6 +7,7 @@
#include <QPushButton> #include <QPushButton>
#include <QLineEdit> #include <QLineEdit>
#include <QComboBox> #include <QComboBox>
#include <QCheckBox>
#include <QString> #include <QString>
class NetworkSettingsPage; class NetworkSettingsPage;
@ -42,6 +43,7 @@ private:
QLineEdit* m_nicknameEdit; QLineEdit* m_nicknameEdit;
QComboBox* m_sortCombo; QComboBox* m_sortCombo;
QComboBox* m_joinPolicyCombo; QComboBox* m_joinPolicyCombo;
QCheckBox* m_autoConnectCheck;
QString m_oldNickname; QString m_oldNickname;
NetworkSettingsPage* m_networkPage; NetworkSettingsPage* m_networkPage;
DatabaseSettingsPage* m_databasePage; DatabaseSettingsPage* m_databasePage;

Loading…
Cancel
Save