From a141fc7215944da12e628ad305e00ab6ad24eb97 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 16:05:40 +0100 Subject: [PATCH] Add desktop invitation policy preview and member preservation dialog --- tools/chatgui/CMakeLists.txt | 3 + tools/chatgui/src/grouppolicydialog.cpp | 123 ++++++++++++++++++++++++ tools/chatgui/src/grouppolicydialog.h | 5 + 3 files changed, 131 insertions(+) create mode 100644 tools/chatgui/src/grouppolicydialog.cpp create mode 100644 tools/chatgui/src/grouppolicydialog.h diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 7db008bd0..6bbfc021f 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -328,6 +328,7 @@ add_executable(vibechat WIN32 src/creategroupdialog.cpp src/channeldelegate.cpp src/channelsettingsdialog.cpp + src/grouppolicydialog.cpp src/chatview.cpp src/messagedelegate.cpp src/messagelist.cpp @@ -548,6 +549,7 @@ add_test(NAME test_channel_list COMMAND test_channel_list) set_tests_properties(test_channel_list PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen" TIMEOUT 30) add_executable(test_channel_settings tests/test_channel_settings.cpp src/channelsettingsdialog.cpp + src/grouppolicydialog.cpp src/avatars/groupavatarmanager.cpp db/db_manager.cpp transport/gui_bridge_impl.cpp) target_link_libraries(test_channel_settings PRIVATE ${QT_LIBS} ${QT_TEST_LIB} utun utun_voice pthread ${CMAKE_DL_LIBS}) if(CMAKE_SYSTEM_NAME STREQUAL "Linux") @@ -753,6 +755,7 @@ 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 + src/grouppolicydialog.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) diff --git a/tools/chatgui/src/grouppolicydialog.cpp b/tools/chatgui/src/grouppolicydialog.cpp new file mode 100644 index 000000000..6c0922cc5 --- /dev/null +++ b/tools/chatgui/src/grouppolicydialog.cpp @@ -0,0 +1,123 @@ +#include "grouppolicydialog.h" +#include "../transport/gui_bridge.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include "chat/group_policy.h" +#include "member_files/protocol.h" +#include "mem.h" +#include "debug_config.h" +} + +static QString text(const char* s) { return QCoreApplication::translate("ChatGui", s); } +class GroupPolicyDialog; +static QHash> requests; +static quint64 sequence = 0; + +class GroupPolicyDialog : public QDialog { +public: + GroupPolicyDialog(const QString& channel, QWidget* parent) : QDialog(parent), channel(channel) { + setWindowTitle(text("Who may invite members")); resize(530, 440); + auto* layout = new QVBoxLayout(this); + mode = new QComboBox(this); + mode->addItem(text("Owner and administrators only"), 0); mode->addItem(text("All group members"), 1); + layout->addWidget(mode); + auto* explanation = new QLabel(text("When permissions are restricted, members whose invitation chain no longer qualifies leave the group at the next synchronization. Select members below to re-sign with the group key and keep them."), this); + explanation->setWordWrap(true); layout->addWidget(explanation); + members = new QListWidget(this); layout->addWidget(members, 1); + status = new QLabel(this); status->setWordWrap(true); status->setTextFormat(Qt::PlainText); layout->addWidget(status); + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Apply | QDialogButtonBox::Close, this); + apply = buttons->button(QDialogButtonBox::Apply); layout->addWidget(buttons); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(apply, &QPushButton::clicked, this, [this] { request(true); }); + connect(mode, qOverload(&QComboBox::currentIndexChanged), this, [this] { if (loaded) request(false); }); + timer.setSingleShot(true); timer.setInterval(15000); + connect(&timer, &QTimer::timeout, this, [this] { + requests.remove(pending); pending = 0; token.clear(); setBusy(false); + status->setText(text("The request timed out. Reopen settings to check the result.")); + }); + request(false); + } + ~GroupPolicyDialog() override { requests.remove(pending); } + + void response(quint64 id, const QByteArray& bytes) { + if (id != pending) return; + requests.remove(pending); pending = 0; timer.stop(); + QJsonParseError error; const auto doc = QJsonDocument::fromJson(bytes, &error); + const auto data = doc.object(); + if (error.error != QJsonParseError::NoError || !data.value("ok").toBool()) { + token.clear(); setBusy(false); + const auto reason = data.value("error").toString(); + status->setText(reason == "conflict" ? text("Membership changed. Review the refreshed list and apply again.") : + reason == "sync_pending" ? text("Membership is synchronizing. Try again after synchronization.") : text("Could not change invitation permissions.")); + if (reason == "conflict") { conflict = true; request(false); } + return; + } + canEdit = data.value("can_edit").toBool(); current = data.value("all_members").toBool(); + if (!loaded || data.value("applied").toBool()) { + loaded = false; mode->setCurrentIndex(current ? 1 : 0); loaded = true; + } + token = data.value("token").toString(); members->clear(); + if (mode->currentIndex() != int(current) && !mode->currentIndex()) { + for (const auto& value : data.value("affected").toArray()) { + const auto member = value.toObject(); const auto node = member.value("node").toString(); + auto* item = new QListWidgetItem(member.value("name").toString() + " (" + node + ")", members); + item->setData(Qt::UserRole, node); item->setCheckState(Qt::Unchecked); + item->setToolTip(text("Invited by: %1").arg(member.value("inviter").toString())); + } + } + status->setText(data.value("applied").toBool() ? text("Invitation permissions saved.") : conflict ? + text("Membership changed. Review the refreshed list and apply again.") : canEdit ? + text("Members affected: %1").arg(members->count()) : text("Only a superadministrator can change this setting.")); + conflict = false; setBusy(false); + } +private: + void setBusy(bool busy) { + mode->setEnabled(!busy && canEdit); members->setEnabled(!busy && canEdit); + apply->setEnabled(!busy && canEdit && !token.isEmpty() && mode->currentIndex() != int(current)); + } + void request(bool save) { + if (pending) return; + QJsonArray keep; + if (save) for (int i = 0; i < members->count(); i++) + if (members->item(i)->checkState() == Qt::Checked) keep.append(members->item(i)->data(Qt::UserRole).toString()); + const QByteArray selection = QJsonDocument(keep).toJson(QJsonDocument::Compact); + auto* q = static_cast(u_calloc(1, sizeof(group_policy_request) + size_t(selection.size()) + 1)); + if (!q) { status->setText(text("Could not change invitation permissions.")); return; } + q->id = ++sequence; q->group = channel.toULongLong(); q->all_members = mode->currentIndex(); + if (save) { const auto t = token.toLatin1(); snprintf(q->token, sizeof(q->token), "%s", t.constData()); } + memcpy(q->keep, selection.constData(), size_t(selection.size())); + pending = q->id; requests.insert(pending, this); setBusy(true); + if (gui_bridge_post_bound_fn(group_policy_gui_command, q, &q->inst)) { + requests.remove(pending); pending = 0; u_free(q); setBusy(false); + status->setText(text("The core is not running.")); return; + } + timer.start(); + } + QString channel, token; + QComboBox* mode; QListWidget* members; QLabel* status; QPushButton* apply; + QTimer timer; quint64 pending = 0; bool loaded = false, current = false, canEdit = false, conflict = false; +}; + +void openGroupPolicyDialog(const QString& channel, QWidget* parent) { + gui_bridge_set_group_policy_result_cb([](const uint8_t* data, int size) { + if (!data || size < 18) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "group policy invalid UI result bytes=%d", size); return; } + const quint64 id = mf_get64(data); const QByteArray json(reinterpret_cast(data + 16), size - 16); + QMetaObject::invokeMethod(qApp, [id, json] { if (auto dialog = requests.value(id)) dialog->response(id, json); }, Qt::QueuedConnection); + }); + auto* dialog = new GroupPolicyDialog(channel, parent); + dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setWindowModality(Qt::WindowModal); dialog->show(); +} diff --git a/tools/chatgui/src/grouppolicydialog.h b/tools/chatgui/src/grouppolicydialog.h new file mode 100644 index 000000000..4eac880ef --- /dev/null +++ b/tools/chatgui/src/grouppolicydialog.h @@ -0,0 +1,5 @@ +/* Общие права приглашения: асинхронный preview, выбор переподписания и подтверждённый commit ядра. */ +#pragma once +#include +class QWidget; +void openGroupPolicyDialog(const QString& channel, QWidget* parent);