Add local channel deletion to desktop channel settings

This commit is contained in:
evgeny committed 2026-10-26 10:47:39 +01:00
1 parent d67e0b0c23
commit 962a2df3f8
12 files changed
+203 -8

No files matched your search

+2 -1
View File
@@ -530,7 +530,8 @@ target_link_libraries(test_avatars PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun pthrea
add_test(NAME test_avatars COMMAND test_avatars)
set_tests_properties(test_avatars PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 60)
add_executable(test_group_avatars tests/test_group_avatars.cpp src/avatars/groupavatarmanager.cpp src/channelsettingsdialog.cpp db/db_manager.cpp)
add_executable(test_group_avatars tests/test_group_avatars.cpp src/avatars/groupavatarmanager.cpp src/channelsettingsdialog.cpp
src/channellist.cpp src/channeldelegate.cpp db/db_manager.cpp)
target_link_libraries(test_group_avatars PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun pthread)
add_test(NAME test_group_avatars COMMAND test_group_avatars)
set_tests_properties(test_group_avatars PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30)
+9 -4
View File
@@ -382,10 +382,15 @@ void ChannelList::loadChannels() {
if (!selectedCid.isEmpty()) {
selectChannel(selectedCid);
} else if (m_model->rowCount() > 0) {
QModelIndex idx = m_model->index(0, 0);
sel->select(idx, QItemSelectionModel::ClearAndSelect);
sel->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
}
if (!m_listView->currentIndex().isValid()) {
for (int row = 0; row < m_model->rowCount(); ++row) {
QModelIndex idx = m_model->index(row, 0);
if (idx.data(ChannelIsSectionRole).toBool()) continue;
sel->select(idx, QItemSelectionModel::ClearAndSelect);
sel->setCurrentIndex(idx, QItemSelectionModel::NoUpdate);
break;
}
}
}
@@ -4,6 +4,7 @@
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
extern "C" {
#include "chat/chat_core.h"
}
@@ -12,6 +13,7 @@ extern "C" {
#include <QPushButton>
#include <QLabel>
#include <QFileDialog>
#include <QMessageBox>
#include <cstring>
ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager* db, QWidget* parent)
@@ -82,6 +84,13 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager
auto* btnLayout = new QHBoxLayout();
btnLayout->setSpacing(8);
auto* deleteBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Delete channel"), this);
deleteBtn->setObjectName("deleteChannel");
deleteBtn->setAutoDefault(false);
deleteBtn->setStyleSheet("QPushButton { color: #EF5350; padding: 6px 12px; }");
deleteBtn->setEnabled(m_db && m_db->isOpen() && channelId.toULongLong() != 0 && gui_bridge_is_uasync_ready());
connect(deleteBtn, &QPushButton::clicked, this, &ChannelSettingsDialog::onDelete);
btnLayout->addWidget(deleteBtn);
btnLayout->addStretch();
auto* cancelBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Cancel"), this);
@@ -114,6 +123,42 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager
}
}
/* Как в Android: удаляются только локальные данные; ядро уведомит GUI по завершении. */
void ChannelSettingsDialog::onDelete() {
QString name = m_channelId;
if (m_db && m_db->isOpen()) for (const auto& channel : m_db->getChannels())
if (channel.channelId == m_channelId) { name = channel.name; break; }
QMessageBox confirmation(QMessageBox::Warning, QCoreApplication::translate("ChatGui", "Delete channel"),
QCoreApplication::translate("ChatGui", "Delete “%1” and all its local messages and files? Other members keep the channel.")
.arg(name), QMessageBox::NoButton, this);
confirmation.setTextFormat(Qt::PlainText);
auto* remove = confirmation.addButton(QCoreApplication::translate("ChatGui", "Delete"), QMessageBox::DestructiveRole);
remove->setObjectName("confirmDeleteChannel");
auto* cancel = confirmation.addButton(QMessageBox::Cancel);
confirmation.setDefaultButton(cancel);
confirmation.setEscapeButton(cancel);
confirmation.exec();
if (confirmation.clickedButton() != remove) return;
auto* inst = gui_bridge_get_inst();
if (!inst || !gui_bridge_is_uasync_ready()) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel delete: core unavailable channel=%s", qPrintable(m_channelId));
QMessageBox::warning(this, confirmation.windowTitle(), QCoreApplication::translate("ChatGui", "The core is not running."));
return;
}
const QByteArray channel = m_channelId.toUtf8();
auto* request = static_cast<chat_delete_channel_req*>(u_calloc(1, sizeof(chat_delete_channel_req)));
if (!request) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel delete: request allocation failed channel=%s", channel.constData());
QMessageBox::warning(this, confirmation.windowTitle(), QCoreApplication::translate("ChatGui", "Failed to delete channel."));
return;
}
request->inst = inst;
snprintf(request->ch_id, sizeof(request->ch_id), "%s", channel.constData());
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "channel delete confirmed: channel=%s scope=local", channel.constData());
gui_bridge_post_uasync_fn(chat_core_delete_channel_trampoline, request);
accept();
}
void ChannelSettingsDialog::onSave() {
auto saveKey = [&](const char* suffix, int val) {
QString key = QStringLiteral("ch_%1_%2").arg(m_channelId, QString::fromUtf8(suffix));
+2 -1
View File
@@ -1,4 +1,4 @@
/* Локальные настройки выбранного канала: autoplay голосовых и звук сообщений. */
/* Локальные настройки канала: autoplay, звук и удаление с устройства после подтверждения. */
#pragma once
#include <QDialog>
@@ -14,6 +14,7 @@ public:
private slots:
void onSave();
void onDelete();
private:
QString m_channelId;
+17
View File
@@ -1355,6 +1355,23 @@ void MainWindow::onChannelPeersOnline(const char* ch_id, int ch_id_len, uint16_t
}
void MainWindow::reloadChannels() {
if (m_db && m_db->isOpen() && !m_currentChannelId.isEmpty() && !m_currentChannelId.startsWith(QStringLiteral("dm:"))) {
bool exists = false;
for (const auto& channel : m_db->getChannels()) if (channel.channelId == m_currentChannelId) { exists = true; break; }
if (!exists) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "channel removed from GUI: channel=%s", qPrintable(m_currentChannelId));
for (int i = m_autoPlayQueue.size() - 1; i >= 0; --i) if (m_autoPlayQueue[i].channelId == m_currentChannelId) {
m_autoPlayDownloading.remove(m_autoPlayQueue[i].msgId);
m_autoPlayQueue.removeAt(i);
}
if (m_pendingChannelSelect == m_currentChannelId) m_pendingChannelSelect.clear();
m_messageList->loadChannel(QString());
m_accountList->setChannel(QString());
m_currentChannelId.clear();
saveLastChannelId(QString());
if (m_messageNotification) m_messageNotification->dismiss("channel deleted");
}
}
if (m_channelList) {
m_channelList->loadChannels();
if (!m_pendingChannelSelect.isEmpty()) {
+6 -1
View File
@@ -886,7 +886,8 @@ void MessageList::loadChannel(const QString& channelId) {
m_callBtn->setVisible(isDmChannel(channelId));
m_inviteBtn->setVisible(isDmChannel(channelId));
m_peerStatus->setVisible(isDmChannel(channelId));
m_radioBtn->setVisible(!isDmChannel(channelId));
m_radioBtn->setVisible(!channelId.isEmpty() && !isDmChannel(channelId));
m_inputBar->setEnabled(!channelId.isEmpty());
m_inputBar->setRecordingContext(m_db ? m_db->dbPath() + "/media" : QString(), channelId);
m_readDebounce->stop();
@@ -900,6 +901,10 @@ void MessageList::loadChannel(const QString& channelId) {
if (m_channelTitle) m_channelTitle->setText(title);
m_inputBar->setDmMode(isDmChannel(channelId));
if (channelId.isEmpty()) {
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "message list cleared: no channel selected");
return;
}
/* История и состояния PM поступают из ядра в одном снимке. */
if (isDmChannel(channelId)) {
m_radioBtn->setEnabled(false);
@@ -1,25 +1,33 @@
#include "../src/avatars/groupavatarmanager.h"
#include "../src/channelsettingsdialog.h"
#include "../src/channellist.h"
#include "../src/channeldelegate.h"
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include <QtTest>
#include <QTemporaryDir>
#include <QPushButton>
#include <QMessageBox>
#include <QAbstractItemView>
extern "C" {
#include "utun_instance.h"
#include "chat/chat_core.h"
#include "chat/chat_event.h"
#include "chat/group_avatar.h"
#include "dm/dm_core.h"
#include "routing_layer/topo_node_sqlite.h"
#include "mem.h"
#include "debug_config.h"
}
static UTUN_INSTANCE* instance;
static QStringList deletedChannels;
extern "C" int gui_bridge_is_uasync_ready(void) { return instance != nullptr; }
extern "C" UTUN_INSTANCE* gui_bridge_get_inst(void) { return instance; }
extern "C" void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg) { uasync_call_soon(instance->ua, arg, fn); }
static void coreEvent(UTUN_INSTANCE*, int type, const uint8_t* data, int length) {
if (type == CHAT_EVT_GROUP_AVATAR_RESULT && GroupAvatarManager::instance()) GroupAvatarManager::instance()->onResult(data, length);
if (type == CHAT_EVT_CHANNEL_DELETED && length > 1 && length == 1 + data[0])
deletedChannels.append(QString::fromUtf8((const char*)data + 1, data[0]));
}
class GroupAvatarTest : public QObject {
Q_OBJECT
@@ -69,6 +77,57 @@ private slots:
poll.stop(); chat_event_set_handler(instance, nullptr); db.setDb(nullptr);
utun_instance_destroy(instance); instance = nullptr; uasync_destroy(ua, 0);
}
void channelDeletionRequiresConfirmationAndKeepsOtherChannels() {
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
UASYNC* ua = uasync_create(); QVERIFY(ua);
const char* config = "[global]\n"
"my_private_key=704f2e012c8fa8768130cb0f988a997dccb628372bc5ceccacc78dcbfec5916f\n"
"my_public_key=b3193173def895bd0fcea6f86af077c7d77216f10395275f627ac18242ec0f01\n"
"[chatserver]\ngroup_autoconnect=0\nmember_files_enabled=0\nstorage_autoload=0\n";
instance = utun_instance_create_from_str(ua, config); QVERIFY(instance);
QCOMPARE(utun_core_start(instance), 0); QCOMPARE(chat_service_start(instance), 0);
chat_core_create_channel_auto(instance, "Delete me"); chat_core_create_channel_auto(instance, "Keep me");
DbManager db; db.setDb(instance->topo_sqlite_db); db.setMyNodeId(instance->node_id);
QCOMPARE(db.getChannels().size(), 2);
const QString channel = db.getChannels()[0].channelId, survivor = db.getChannels()[1].channelId;
ChannelList list(&db); list.loadChannels(); list.selectChannel(channel);
auto* view = list.findChild<QAbstractItemView*>(); QVERIFY(view);
const QByteArray sql = "UPDATE channels SET ed25519_privkey=NULL WHERE channel_id='" + channel.toLatin1() + "'";
QCOMPARE(sqlite3_exec(instance->topo_sqlite_db, sql.constData(), nullptr, nullptr, nullptr), SQLITE_OK);
deletedChannels.clear(); chat_event_set_handler(instance, coreEvent);
QTimer poll; connect(&poll, &QTimer::timeout, this, [ua] { uasync_poll(ua, 0); }); poll.start(1);
ChannelSettingsDialog dialog(channel, &db); dialog.show();
auto* remove = dialog.findChild<QPushButton*>("deleteChannel"); QVERIFY(remove && remove->isEnabled());
bool confirmed = false;
QTimer::singleShot(0, &dialog, [&] {
auto* box = qobject_cast<QMessageBox*>(QApplication::activeModalWidget()); QVERIFY(box);
QVERIFY(box->text().contains("Delete me"));
QCOMPARE(box->defaultButton(), box->button(QMessageBox::Cancel));
box->button(QMessageBox::Cancel)->click();
});
QTest::mouseClick(remove, Qt::LeftButton);
QCoreApplication::processEvents();
QCOMPARE(db.getChannels().size(), 2); QVERIFY(deletedChannels.isEmpty());
QTimer::singleShot(0, &dialog, [&] {
auto* box = qobject_cast<QMessageBox*>(QApplication::activeModalWidget()); QVERIFY(box);
auto* button = box->findChild<QPushButton*>("confirmDeleteChannel"); QVERIFY(button);
confirmed = true; button->click();
});
QTest::mouseClick(remove, Qt::LeftButton); QVERIFY(confirmed);
QCOMPARE(dialog.result(), int(QDialog::Accepted));
QTRY_COMPARE(db.getChannels().size(), 1);
QCOMPARE(db.getChannels()[0].channelId, survivor); QCOMPARE(deletedChannels, QStringList({channel}));
list.loadChannels(); QCOMPARE(view->currentIndex().data(ChannelChannelIdRole).toString(), survivor);
const QByteArray peer = QByteArray::fromHex("ce8871f07fa056c636d297115f231b08c29cdf94e0d440fce83a07c34416d36a");
QCOMPARE(dm_start(instance, 7, (const uint8_t*)peer.constData(), (const uint8_t*)peer.constData(), "PM", nullptr), 0);
chat_core_delete_channel(instance, survivor.toLatin1().constData());
list.loadChannels();
QVERIFY(!view->currentIndex().data(ChannelIsSectionRole).toBool());
QVERIFY(view->currentIndex().data(ChannelChannelIdRole).toString().startsWith("dm:"));
poll.stop(); chat_event_set_handler(instance, nullptr); db.setDb(nullptr);
utun_instance_destroy(instance); instance = nullptr; uasync_destroy(ua, 0);
}
};
QTEST_MAIN(GroupAvatarTest)
#include "test_group_avatars.moc"
@@ -15,6 +15,7 @@ struct ReceivedMessage {
static QList<ReceivedMessage> received;
static QList<int> cpuLoads;
static bool cpuOnGuiThread = true;
static QStringList changedChannels;
static void cpu_received(int percent) {
cpuLoads.append(percent);
@@ -98,6 +99,27 @@ private slots:
QCoreApplication::processEvents();
QCOMPARE(cpuLoads.size(), 6);
}
void deletionRefreshesChannelsOnlyAfterValidEvent() {
changedChannels.clear();
gui_bridge_set_channel_updated_cb([](const char* channel, int length) {
QVERIFY(QThread::currentThread() == qApp->thread());
changedChannels.append(QString::fromUtf8(channel, length));
});
std::thread worker([] {
const uint8_t valid[] = {3, '1', '2', '3'};
gui_bridge_post(CHAT_EVT_CHANNEL_DELETED, valid, sizeof(valid));
gui_bridge_post(CHAT_EVT_CHANNEL_DELETED, valid, sizeof(valid) - 1);
const uint8_t empty[] = {0};
gui_bridge_post(CHAT_EVT_CHANNEL_DELETED, empty, sizeof(empty));
gui_bridge_post(CHAT_EVT_CHANNEL_DELETED, nullptr, 0);
});
worker.join();
QTRY_COMPARE(changedChannels, QStringList({"123"}));
QCoreApplication::processEvents();
QCOMPARE(changedChannels.size(), 1);
gui_bridge_set_channel_updated_cb(nullptr);
}
};
QTEST_MAIN(MessageEventTest)
+16
View File
@@ -2619,5 +2619,21 @@ See log:
See log:
%2</translation>
</message>
<message>
<source>Delete channel</source>
<translation>Delete channel</translation>
</message>
<message>
<source>Delete</source>
<translation>Delete</translation>
</message>
<message>
<source>Delete “%1” and all its local messages and files? Other members keep the channel.</source>
<translation>Delete “%1” and all its local messages and files? Other members keep the channel.</translation>
</message>
<message>
<source>Failed to delete channel.</source>
<translation>Failed to delete channel.</translation>
</message>
</context>
</TS>
+16
View File
@@ -2628,5 +2628,21 @@ See log:
Подробности в логе:
%2</translation>
</message>
<message>
<source>Delete channel</source>
<translation>Удалить канал</translation>
</message>
<message>
<source>Delete</source>
<translation>Удалить</translation>
</message>
<message>
<source>Delete “%1” and all its local messages and files? Other members keep the channel.</source>
<translation>Удалить «%1» и все его локальные сообщения и файлы? У других участников канал останется.</translation>
</message>
<message>
<source>Failed to delete channel.</source>
<translation>Не удалось удалить канал.</translation>
</message>
</context>
</TS>
+1 -1
View File
@@ -92,7 +92,7 @@ void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb);
typedef void (*gui_msg_received_fn)(const char* ch_id, int ch_id_len, uint64_t author, int initial_sync);
void gui_bridge_set_msg_received_cb(gui_msg_received_fn cb);
/* Callback для обновления канала (вызывается из GUI-потока) */
/* Обновление списка после изменения или завершённого локального удаления канала; GUI-поток. */
typedef void (*gui_channel_updated_fn)(const char* ch_id, int ch_id_len);
void gui_bridge_set_channel_updated_cb(gui_channel_updated_fn cb);
@@ -196,6 +196,14 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) {
DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "gui_bridge: NEW_PEER data too short %d", dlen);
}
break;
case CHAT_EVT_CHANNEL_DELETED:
if (dlen < 2 || !d[0] || dlen != 1 + d[0]) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "gui_bridge: CHANNEL_DELETED invalid data length %d", dlen);
break;
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "gui_bridge: CHANNEL_DELETED ch=%.*s", int(d[0]), (const char*)d + 1);
if (g_channel_updated_cb) g_channel_updated_cb((const char*)d + 1, d[0]);
break;
case GUI_EVT_CHANNEL_UPDATED:
if (dlen >= 2) {
uint8_t chLen = d[0];