chat: локальное удаление канала (Android) + db_sync инстансы в куче
This commit is contained in:
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 ── */
|
||||
|
||||
+25
-2
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user