chat: локальное удаление канала (Android) + db_sync инстансы в куче

This commit is contained in:
evgeny committed 2026-09-13 22:08:33 +03:00
1 parent ddfffad1ed
commit 7c8d65db78
30 files changed
+466 -42

No files matched your search

@@ -67,7 +67,7 @@ class MainActivity : ComponentActivity() {
val scope = rememberCoroutineScope()
val showInvite: (Channel) -> Unit = { ch ->
if (ch.id.toLongOrNull() == null) {
if (ch.id.toULongOrNull() == null) {
LogManager.addLog("ERROR", "INVITE", "channel id is not numeric: ${ch.id}")
scope.launch { snackbarHostState.showSnackbar("Invalid channel id") }
} else {
@@ -111,9 +111,10 @@ class MainActivity : ComponentActivity() {
connected = connected,
isStuck = stuck,
onSelectChannel = { ch -> vm.selectChannel(ch); screen = Screen.Chat },
onCreateChannel = { vm.createChannel("general", "channel_1") },
onCreateChannel = { name -> vm.createChannel(name, "channel_1") },
onJoinChannel = { showJoinDialog = true; joinAutoLink = null },
onInviteClick = showInvite,
onDeleteChannel = { vm.deleteChannel(it.id) },
onSettings = { screen = Screen.Settings },
onLogs = { screen = Screen.Logs },
onRefresh = { vm.requestRefresh() }
@@ -460,6 +460,8 @@ class ChatRepository {
fun transferAdmin(channelId: String, nodeId: Long) = NativeLib.transferAdmin(channelId, nodeId)
fun deleteChannel(channelId: String) = NativeLib.deleteChannel(channelId)
fun getMemberLinks(nodeId: Long): MemberLinks {
try {
val obj = JSONObject(NativeLib.getMemberLinks(nodeId))
@@ -100,6 +100,7 @@ object NativeLib {
nativeSetMemberFlags(channelId, nodeId, admin, supernode, moder, storage, verified, deleted)
}
fun transferAdmin(channelId: String, nodeId: Long) { nativeTransferAdmin(channelId, nodeId) }
fun deleteChannel(channelId: String) { nativeDeleteChannel(channelId) }
fun getMemberLinks(nodeId: Long): String = nativeGetMemberLinks(nodeId) ?: "{}"
/* ── Local sockets (for auto-mode display) ── */
@@ -178,6 +179,7 @@ object NativeLib {
private external fun nativeSetMemberFlags(channelId: String, nodeId: Long, admin: Boolean, supernode: Boolean, moder: Boolean, storage: Boolean,
verified: Boolean, deleted: Boolean)
private external fun nativeTransferAdmin(channelId: String, nodeId: Long)
private external fun nativeDeleteChannel(channelId: String)
private external fun nativeGetMemberLinks(nodeId: Long): String?
/* ── Local sockets JNI ── */
@@ -34,9 +34,10 @@ fun ChannelListScreen(
connected: Boolean,
isStuck: Boolean = false,
onSelectChannel: (Channel) -> Unit,
onCreateChannel: () -> Unit,
onCreateChannel: (String) -> Unit,
onJoinChannel: () -> Unit,
onInviteClick: (Channel) -> Unit,
onDeleteChannel: (Channel) -> Unit,
onSettings: () -> Unit,
onLogs: () -> Unit,
onRefresh: () -> Unit = {}
@@ -44,6 +45,7 @@ fun ChannelListScreen(
var showCreateDialog by remember { mutableStateOf(false) }
var menuChannelId by remember { mutableStateOf<String?>(null) }
var optionsChannel by remember { mutableStateOf<Channel?>(null) }
var deleteConfirmChannel by remember { mutableStateOf<Channel?>(null) }
LaunchedEffect(Unit) { onRefresh() }
@@ -108,6 +110,10 @@ fun ChannelListScreen(
text = { Text("Options") },
onClick = { menuChannelId = null; optionsChannel = ch }
)
DropdownMenuItem(
text = { Text("Delete channel", color = MaterialTheme.colorScheme.error) },
onClick = { menuChannelId = null; deleteConfirmChannel = ch }
)
}
if (badgeText.isNotEmpty()) {
val badgeSize = 18.dp
@@ -151,7 +157,7 @@ fun ChannelListScreen(
onDismiss = { showCreateDialog = false },
onCreate = { name ->
showCreateDialog = false
onCreateChannel()
onCreateChannel(name)
}
)
}
@@ -162,6 +168,23 @@ fun ChannelListScreen(
onDismiss = { optionsChannel = null }
)
}
deleteConfirmChannel?.let { ch ->
AlertDialog(
onDismissRequest = { deleteConfirmChannel = null },
title = { Text("Delete channel") },
text = { Text("Delete \"${ch.name}\" and all its local messages and files? Other members keep the channel.") },
confirmButton = {
TextButton(onClick = { deleteConfirmChannel = null; onDeleteChannel(ch) },
colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)) {
Text("Delete")
}
},
dismissButton = {
TextButton(onClick = { deleteConfirmChannel = null }) { Text("Cancel") }
}
)
}
}
@Composable
@@ -403,6 +403,20 @@ class ChatViewModel : ViewModel() {
_inviteLink.value = link
_inviteLinkPending.value = false
}
31 -> { /* CHANNEL_DELETED: [ch_id_len:1][ch_id:var] */
if (repo == null || data == null || data.size < 2) return
val cl = data[0].toInt() and 0xFF
if (data.size < 1 + cl) return
val chId = String(data, 1, cl)
LogManager.addLog("INFO", "VM", "CHANNEL_DELETED ch=$chId")
if (_currentChannel.value?.id == chId) {
_currentChannel.value = null
_messages.value = emptyList()
_members.value = emptyList()
_channelOwner.value = false
}
refreshChannels()
}
}
}
@@ -556,6 +570,20 @@ class ChatViewModel : ViewModel() {
refreshChannels()
}
fun deleteChannel(channelId: String) {
val r = repo ?: return
LogManager.addLog("INFO", "VM", "deleteChannel ch=$channelId")
r.deleteChannel(channelId)
if (_currentChannel.value?.id == channelId) {
_currentChannel.value = null
_messages.value = emptyList()
_members.value = emptyList()
_channelOwner.value = false
}
NotificationSound.setMuted(channelId, false)
refreshChannels()
}
fun connectNode(address: String, port: Int, pubkeyHex: String) {
val r = repo ?: return
r.connectNode(address, port, pubkeyHex)
@@ -203,6 +203,12 @@ void utun_bridge_send_message(const char* channel_id, const char* text) {
void utun_bridge_create_channel(const char* name, const char* channel_id) {
bridge_log(BLEV_INFO, "create_channel name=%s id=%s", name ? name : "", channel_id ? channel_id : "");
if (!name || !name[0]) { bridge_log(BLEV_ERROR, "create_channel: empty name"); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "create_channel: uasync not running"); return; }
char* n = u_strdup(name);
if (!n) { bridge_log(BLEV_ERROR, "create_channel: OOM"); return; }
uasync_post(ua, chat_core_create_channel_auto_trampoline, n);
}
void utun_bridge_connect_node(const char* address, int port, const char* pubkey_hex) {
@@ -911,6 +917,21 @@ void utun_bridge_transfer_admin(const char* channel_id, uint64_t node_id) {
bridge_log(BLEV_INFO, "transfer_admin queued ch=%s → 0x%016llx", channel_id, (unsigned long long)node_id);
}
/* ──────────────────────────────────────────────────────────────────
* Удаление канала (локальное)
* ────────────────────────────────────────────────────────────────── */
void utun_bridge_delete_channel(const char* channel_id) {
if (!channel_id || !channel_id[0]) { bridge_log(BLEV_ERROR, "delete_channel: empty ch_id"); return; }
struct UASYNC* ua = instance_lite_get_uasync();
if (!uasync_is_running(ua)) { bridge_log(BLEV_ERROR, "delete_channel: uasync not running"); return; }
struct chat_delete_channel_req* req = u_calloc(1, sizeof(*req));
if (!req) { bridge_log(BLEV_ERROR, "delete_channel: OOM"); return; }
strncpy(req->ch_id, channel_id, sizeof(req->ch_id) - 1);
uasync_post(ua, chat_core_delete_channel_trampoline, req);
bridge_log(BLEV_INFO, "delete_channel queued ch=%s", channel_id);
}
/* ──────────────────────────────────────────────────────────────────
* Local sockets state (for auto-mode UI display)
* ────────────────────────────────────────────────────────────────── */
@@ -1912,6 +1933,14 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeTransferAdmin(
(*env)->ReleaseStringUTFChars(env, channelId, ch);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeDeleteChannel(
JNIEnv* env, jobject thiz, jstring channelId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_delete_channel(ch);
(*env)->ReleaseStringUTFChars(env, channelId, ch);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetMemberFlags(
JNIEnv* env, jobject thiz, jstring channelId, jlong nodeId,
jboolean admin, jboolean supernode, jboolean moder, jboolean storage,
@@ -147,6 +147,9 @@ char* utun_bridge_get_member_links_json(uint64_t node_id);
/* Передать канальный приватный ключ (права админа) другому узлу. */
void utun_bridge_transfer_admin(const char* channel_id, uint64_t node_id);
/* Локально удалить канал (drop таблиц, соединений, медиа, db_sync). */
void utun_bridge_delete_channel(const char* channel_id);
/* ── Local sockets state (for auto-mode display) ── */
char* utun_bridge_get_local_sockets_json(void);
@@ -7,6 +7,7 @@
*
* Mirrors UtunNode::runLoop() from tools/chatgui/transport/utun_node.cpp:221-372.
*/
#define _GNU_SOURCE 1 /* dladdr (Linux); no-op on Android/bionic */
#include "instance_lite.h"
#include "utun_instance.h"
#include "config_parser.h"
@@ -34,6 +35,8 @@
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <unwind.h>
#include <dlfcn.h>
#include <time.h>
#include <errno.h>
#include <openssl/evp.h>
@@ -203,10 +206,36 @@ static void on_link_status_changed(struct ETCP_CONN* conn, struct ETCP_LINK* lin
static volatile int g_signal_caught = 0;
static volatile int g_signal_num = 0;
struct crash_bt_state { void** frames; int count; int max; };
static _Unwind_Reason_Code crash_bt_cb(struct _Unwind_Context* ctx, void* arg) {
struct crash_bt_state* st = (struct crash_bt_state*)arg;
if (st->count < st->max) {
void* pc = (void*)_Unwind_GetIP(ctx);
if (pc) st->frames[st->count++] = pc;
return _URC_NO_REASON;
}
return _URC_END_OF_STACK;
}
static void crash_signal_handler(int sig, siginfo_t* info, void* ctx) {
(void)ctx;
g_signal_caught = 1; g_signal_num = sig;
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "CRASH signal=%d code=%d addr=%p thread still alive", sig, info->si_code, info->si_addr);
void* frames[64];
struct crash_bt_state st = { frames, 0, 64 };
_Unwind_Backtrace(crash_bt_cb, &st);
IL_LOGE("CRASH backtrace %d frames:", st.count);
for (int i = 0; i < st.count; i++) {
Dl_info dli;
if (dladdr(frames[i], &dli) && dli.dli_sname) {
IL_LOGE(" #%02d %p %s+%p (%s)", i, frames[i], dli.dli_sname,
(void*)((char*)frames[i] - (char*)dli.dli_saddr),
dli.dli_fname ? dli.dli_fname : "?");
} else {
IL_LOGE(" #%02d %p (no symbol)", i, frames[i]);
}
}
signal(sig, SIG_DFL);
raise(sig);
}