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

This commit is contained in:
evgeny committed 2026-09-11 19:46:02 +03:00
1 parent 9ef501a2fb
commit d7bb58c6fb
20 files changed
+241 -5

No files matched your search

@@ -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<ChatMember> {
@@ -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")
@@ -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)
@@ -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") }
}
}
@@ -118,6 +118,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
var servers by remember { mutableStateOf<List<ServerEntry>>(emptyList()) }
var availableIfaces by remember { mutableStateOf<List<Pair<String, String>>>(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<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))
/* ── 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)
@@ -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
@@ -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);
}
@@ -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);