Add desktop invitation policy preview and member preservation dialog

This commit is contained in:
evgeny committed 2026-11-03 16:05:40 +01:00
1 parent ac5fbf0ffb
commit a141fc7215
3 files changed
+131

No files matched your search

+3
View File
@@ -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)
+123
View File
@@ -0,0 +1,123 @@
#include "grouppolicydialog.h"
#include "../transport/gui_bridge.h"
#include <QApplication>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QListWidget>
#include <QPointer>
#include <QPushButton>
#include <QTimer>
#include <QVBoxLayout>
#include <cstring>
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<quint64, QPointer<GroupPolicyDialog>> 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<int>(&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<group_policy_request*>(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<const char*>(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();
}
+5
View File
@@ -0,0 +1,5 @@
/* Общие права приглашения: асинхронный preview, выбор переподписания и подтверждённый commit ядра. */
#pragma once
#include <QString>
class QWidget;
void openGroupPolicyDialog(const QString& channel, QWidget* parent);