diff --git a/src/chat/chat_channel.c b/src/chat/chat_channel.c index f911e08c..b6a0b487 100644 --- a/src/chat/chat_channel.c +++ b/src/chat/chat_channel.c @@ -6,6 +6,7 @@ #include "chat_core_priv.h" #include "chat_event.h" +#include "chat_setting.h" #include "../utun_instance.h" #include "../ntp_time.h" @@ -224,6 +225,12 @@ void chat_core_connect_channel(const char* ch_id) { 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); topo_group_connect_init(group); } @@ -235,3 +242,56 @@ void chat_core_connect_channel_trampoline(void* arg) { chat_core_connect_channel(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; + } +} diff --git a/src/chat/chat_core.c b/src/chat/chat_core.c index 77aba1b9..e3d5157c 100644 --- a/src/chat/chat_core.c +++ b/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) { chat_setting_set(name, value); + if (name && strcmp(name, "group_autoconnect") == 0) chat_core_apply_group_autoconnect(); } struct chat_setting_arg { char data[256]; }; diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index ccc10675..ca8a5496 100644 --- a/src/chat/chat_core.h +++ b/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_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) */ void chat_core_submit_trampoline(void* arg); diff --git a/src/chat/chat_core_priv.h b/src/chat/chat_core_priv.h index 1c03a193..8c5e6587 100644 --- a/src/chat/chat_core_priv.h +++ b/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); +/* ── применить настройку group_autoconnect ко всем CHAT-группам (chat_channel.c) ── */ + +void chat_core_apply_group_autoconnect(void); + #endif /* CHAT_CORE_PRIV_H */ diff --git a/src/chat/chat_setting.c b/src/chat/chat_setting.c index d542364d..f8b0e391 100644 --- a/src/chat/chat_setting.c +++ b/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 */ {"whisper_enabled", CHAT_SETTING_BOOL, 0, 0, 1}, {"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])) diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index c28e4b9b..487238fa 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -26,6 +26,7 @@ #include "topo_group_connect.h" #include "topo_recovery.h" #include "broadcast.h" +#include "../chat/chat_setting.h" #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); - 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; } diff --git a/src/routing_layer/topo_group_connect.c b/src/routing_layer/topo_group_connect.c index 75292c41..aee6ec84 100644 --- a/src/routing_layer/topo_group_connect.c +++ b/src/routing_layer/topo_group_connect.c @@ -26,6 +26,7 @@ #include "topo_group.h" #include "topo_node.h" #include "topo_node_sqlite.h" +#include "../utun_instance.h" #include "../transport_layer/node_conn_direct.h" #include "../lib/debug_config.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, 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; +} diff --git a/src/routing_layer/topo_group_connect.h b/src/routing_layer/topo_group_connect.h index 55d40a5c..f72db192 100644 --- a/src/routing_layer/topo_group_connect.h +++ b/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); 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 diff --git a/src/transport_layer/etcp.c b/src/transport_layer/etcp.c index e0dd855d..f91cb253 100644 --- a/src/transport_layer/etcp.c +++ b/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 (эпохи ресета пира). Симметрично: // если uid пира изменился — сбрасываем своё состояние (свой uid сохраняем). 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 пира не изменился + uint64_t old = conn->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", conn->log_name, (unsigned long long)peer_reset_id); etcp_conn_reinit(conn, "peer reset epoch"); 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 83363868..fa045539 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 @@ -385,6 +385,7 @@ class ChatRepository { fun joinChannel(channelId: Long, nodeId: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int) = NativeLib.joinChannel(channelId, nodeId, pubkey, addrs, addrCount) fun connectChannel(channelId: String) = NativeLib.connectChannel(channelId) + fun connectMemberNode(channelId: String, nodeId: Long) = NativeLib.connectMemberNode(channelId, nodeId) fun isReady() = NativeLib.isReady() fun getMembers(channelId: String): List { diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt index 164d6c33..465a37d9 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/ConfigProvider.kt +++ b/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 chatStandbySleepSec = intPreferencesKey("chat_standby_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 connectionModeKey = stringPreferencesKey("connection_mode") 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_sleep_sec" -> context.dataStore.data.first()[chatStandbySleepSec] ?: 60 key == "chat.standby_min_sleep_sec" -> context.dataStore.data.first()[chatStandbyMinSleepSec] ?: 15 + key == "chat.group_autoconnect" -> context.dataStore.data.first()[chatGroupAutoconnect] ?: 1 else -> 0 } } @@ -157,6 +159,7 @@ class ConfigProvider(private val context: Context) { "chat.standby_active_sec" -> prefs[chatStandbyActiveSec] = value.toIntOrNull() ?: 2 "chat.standby_sleep_sec" -> prefs[chatStandbySleepSec] = value.toIntOrNull() ?: 60 "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_sleep_sec=${getInt("chat.standby_sleep_sec")}") appendLine("standby_min_sleep_sec=${getInt("chat.standby_min_sleep_sec")}") + appendLine("group_autoconnect=${getInt("chat.group_autoconnect")}") appendLine() appendLine("[debug]") appendLine("console_level=info") 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 3f3ed32b..f6c83050 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 @@ -75,6 +75,7 @@ object NativeLib { } fun deriveNodeId(pubkey: ByteArray): Long = nativeDeriveNodeId(pubkey) fun connectChannel(channelId: String) { nativeConnectChannel(channelId) } + fun connectMemberNode(channelId: String, nodeId: Long) { nativeConnectMemberNode(channelId, nodeId) } fun getChannels(): String = nativeGetChannels() ?: "[]" fun getMessages(channelId: String, limit: Int = 50): String = nativeGetMessages(channelId, limit) ?: "[]" 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 nativeDeriveNodeId(pubkey: ByteArray): Long private external fun nativeConnectChannel(channelId: String) + private external fun nativeConnectMemberNode(channelId: String, nodeId: Long) private external fun nativeGetChannels(): String? private external fun nativeGetMessages(channelId: String, limit: Int): String? private external fun nativeSetDebugLevel(category: String, level: String) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt index 0de409d9..066ab154 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/MemberListScreen.kt @@ -83,6 +83,7 @@ fun MemberListScreen( member = m, isSelected = selectedMember?.nodeId == m.nodeId, onClick = { viewModel.showMemberDetail(channel.id, m.nodeId) }, + onConnect = { viewModel.connectMemberNode(channel.id, m.nodeId) }, onEdit = { editMember = m showEditDialog = true @@ -133,6 +134,7 @@ private fun MemberListItem( member: ChatMember, isSelected: Boolean = false, onClick: () -> Unit, + onConnect: () -> Unit, onEdit: () -> Unit ) { val onlineColor = when { @@ -223,6 +225,9 @@ private fun MemberListItem( } } + if (!member.isSelf) { + TextButton(onClick = onConnect) { Text("Connect") } + } TextButton(onClick = onEdit) { Text("Edit") } } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt index 3196302d..1306eea4 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/SettingsScreen.kt +++ b/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>(emptyList()) } var availableIfaces by remember { mutableStateOf>>(emptyList()) } var connectionMode by remember { mutableStateOf("manual") } + var autoConnect by remember { mutableStateOf(true) } val localSocketStates by vm.localSockets.collectAsState() val serviceEnabled by provider.serviceEnabledFlow.collectAsState(initial = true) @@ -143,6 +144,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool servers = provider.loadServersFromConfig() availableIfaces = provider.getAvailableInterfaces() connectionMode = provider.getConnectionMode() + autoConnect = provider.getInt("chat.group_autoconnect") != 0 } val save = { s: List -> 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)) + /* ── 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 */ Text("Debug", style = MaterialTheme.typography.titleMedium) DebugLevelDropdown(label = "Global Level", current = debugLevel.uppercase(), onSelect = setDebug) 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 072a138c..004052bb 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 @@ -519,6 +519,12 @@ class ChatViewModel : ViewModel() { r.transferAdmin(channelId, nodeId) } + fun connectMemberNode(channelId: String, nodeId: Long) { + val r = repo ?: return + r.connectMemberNode(channelId, nodeId) + refreshMembers(channelId) + } + fun refreshMemberLinks(nodeId: Long) { if (nodeId == 0L) { _memberLinks.value = MemberLinks(); return } val r = repo ?: return diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 684d6c83..a75e29a4 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/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); } +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) { if (!str) return u_strdup(""); 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) { - chat_setting_set(name, value); + chat_core_set_setting(name, value); } 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); } +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( JNIEnv* env, jobject thiz, jstring category, jstring level) { (void)thiz; @@ -1576,7 +1600,7 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetChatSetting( (void)thiz; const char* n = (*env)->GetStringUTFChars(env, name, 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 (n) (*env)->ReleaseStringUTFChars(env, name, n); } diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.h b/tools/chatgui-android/jni_bridge/android_jni_bridge.h index a7dde845..2369b0d2 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.h +++ b/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) */ 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 ── */ char* utun_bridge_get_channels_json(void); diff --git a/tools/chatgui/src/accountlist.cpp b/tools/chatgui/src/accountlist.cpp index 28cd4313..805a570c 100644 --- a/tools/chatgui/src/accountlist.cpp +++ b/tools/chatgui/src/accountlist.cpp @@ -458,11 +458,23 @@ void AccountList::onMemberContextMenu(const QPoint& pos) { QString name = m_model->data(index, Qt::DisplayRole).toString(); QMenu menu(m_listView); + QAction* connectAction = menu.addAction("Connect"); + if (nodeId == m_db->myNodeId()) connectAction->setEnabled(false); QAction* renameAction = menu.addAction("Rename"); if (nodeId == m_db->myNodeId()) renameAction->setEnabled(false); QAction* editAction = menu.addAction("Edit"); 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) { RenameDialog dlg(m_db, m_channelId, nodeId, name, this); if (dlg.exec() == QDialog::Accepted) { diff --git a/tools/chatgui/src/settingsdialog.cpp b/tools/chatgui/src/settingsdialog.cpp index e1fb78e7..49dac9d4 100644 --- a/tools/chatgui/src/settingsdialog.cpp +++ b/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;" " padding: 6px 10px; font-size: 13px; background: palette(base); }"); 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); // Storage page @@ -181,6 +185,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod m_sortCombo->setCurrentIndex(sortMode == 1 ? 1 : 0); int joinPolicy = db->getUiStateInt("join_policy", 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_soundPage->loadFromDb(); @@ -295,6 +300,23 @@ void SettingsDialog::onSave() { 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(); } diff --git a/tools/chatgui/src/settingsdialog.h b/tools/chatgui/src/settingsdialog.h index 8204dfca..565ef870 100644 --- a/tools/chatgui/src/settingsdialog.h +++ b/tools/chatgui/src/settingsdialog.h @@ -7,6 +7,7 @@ #include #include #include +#include #include class NetworkSettingsPage; @@ -42,6 +43,7 @@ private: QLineEdit* m_nicknameEdit; QComboBox* m_sortCombo; QComboBox* m_joinPolicyCombo; + QCheckBox* m_autoConnectCheck; QString m_oldNickname; NetworkSettingsPage* m_networkPage; DatabaseSettingsPage* m_databasePage;