Add local channel deletion to desktop channel settings
This commit is contained in:
1 parent
d67e0b0c23
commit
962a2df3f8
12 files changed
+203
-8
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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];
|
||||
|
||||
Reference in new issue
Block a user