Confirm atomic member property saves before closing the dialog
This commit is contained in:
1 parent
7e412d950a
commit
5842855583
2 files changed
+82
-87
No files matched your search
@@ -1,18 +1,19 @@
|
||||
#include <QCoreApplication>
|
||||
#include "memberpropsdialog.h"
|
||||
#include "../db/db_manager.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include "../transport/member_edit.h"
|
||||
#include "debug_config.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <cstring>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
extern "C" {
|
||||
#include "../../../src/chat/chat_core.h"
|
||||
#include "../../../src/chat/chat_member.h"
|
||||
#include "../../../lib/mem.h"
|
||||
}
|
||||
|
||||
MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
|
||||
@@ -31,15 +32,15 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
|
||||
setModal(true);
|
||||
|
||||
/* channel private key (for enabling admin/super checkboxes) */
|
||||
auto chs = m_db->getChannels();
|
||||
auto chs = m_db ? m_db->getChannels() : QList<ChannelRow>();
|
||||
for (const auto& ch : chs) {
|
||||
if (ch.channelId == channelId) {
|
||||
m_edPrivkey = ch.ed25519_privkey;
|
||||
m_haveAdminKey = ch.ed25519_privkey.size() == 32;
|
||||
break;
|
||||
}
|
||||
}
|
||||
bool haveKey = (m_edPrivkey.size() == 32);
|
||||
bool isOwn = (m_nodeId == (uint64_t)m_db->myNodeId());
|
||||
bool haveKey = m_haveAdminKey;
|
||||
bool isOwn = m_isOwn = m_db && m_nodeId == (uint64_t)m_db->myNodeId();
|
||||
|
||||
bool isAdmin = (m_flags & CHAT_MEMBER_FLAG_ADMIN) != 0;
|
||||
bool isSuper = (m_flags & CHAT_MEMBER_FLAG_SUPERNODE) != 0;
|
||||
@@ -50,30 +51,29 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
|
||||
bool curDeleted = false;
|
||||
bool curStorage = false;
|
||||
m_currentVer = 0;
|
||||
if (m_db->m_db) {
|
||||
bool loaded = false;
|
||||
if (m_db && m_db->m_db) {
|
||||
QString tbl = m_db->peersTableName(channelId);
|
||||
QByteArray sql = QString(
|
||||
"SELECT adm_tags, local_nick FROM \"%1\" WHERE node_id=?"
|
||||
).arg(tbl).toUtf8();
|
||||
sqlite3_stmt* st = nullptr;
|
||||
if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &st, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
const char* tags = (const char*)sqlite3_column_text(st, 0);
|
||||
if (tags) {
|
||||
const char* vp = strstr(tags, "\"ver\":\"");
|
||||
if (vp) { vp += 7; m_currentVer = atoi(vp); }
|
||||
const char* vr = strstr(tags, "\"verified\":\"");
|
||||
if (vr) { vr += 12; curVerified = (strncmp(vr, "yes", 3) == 0); }
|
||||
const char* dl = strstr(tags, "\"deleted\":\"");
|
||||
if (dl) { dl += 11; curDeleted = (strncmp(dl, "yes", 3) == 0); }
|
||||
const char* sg = strstr(tags, "\"storage\":\"");
|
||||
if (sg) { sg += 11; curStorage = (strncmp(sg, "yes", 3) == 0); }
|
||||
}
|
||||
curNick = DbManager::colText(st, 1);
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
int rc = sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &st, nullptr);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)nodeId);
|
||||
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
||||
if (rc == SQLITE_ROW) {
|
||||
const QJsonObject tags = QJsonDocument::fromJson(DbManager::colText(st, 0).toUtf8()).object();
|
||||
m_currentVer = tags.value("ver").toString().toInt();
|
||||
curVerified = tags.value("verified").toString() == "yes";
|
||||
curDeleted = tags.value("deleted").toString() == "yes";
|
||||
curStorage = tags.value("storage").toString() == "yes";
|
||||
curNick = DbManager::colText(st, 1);
|
||||
}
|
||||
int finish = sqlite3_finalize(st); loaded = rc == SQLITE_ROW && finish == SQLITE_OK;
|
||||
if (!loaded) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member properties: read ch=%s node=%016llx rc=%d finalize=%d: %s",
|
||||
qPrintable(channelId), (unsigned long long)nodeId, rc, finish, sqlite3_errmsg(m_db->m_db));
|
||||
} else {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member properties: database unavailable at open");
|
||||
}
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
@@ -134,6 +134,9 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
|
||||
"QPushButton:disabled { color: palette(midlight); }");
|
||||
connect(m_transferBtn, &QPushButton::clicked, this, &MemberPropsDialog::onTransferAdmin);
|
||||
layout->addWidget(m_transferBtn);
|
||||
m_errorLabel = new QLabel(this); m_errorLabel->setWordWrap(true);
|
||||
if (!loaded) m_errorLabel->setText(QCoreApplication::translate("ChatGui", "Could not load member details."));
|
||||
layout->addWidget(m_errorLabel);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
@@ -150,6 +153,7 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
|
||||
btnLayout->addWidget(cancelBtn);
|
||||
|
||||
auto* saveBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Save"), this);
|
||||
m_saveBtn = saveBtn;
|
||||
saveBtn->setMinimumWidth(90);
|
||||
saveBtn->setDefault(true);
|
||||
saveBtn->setStyleSheet(
|
||||
@@ -162,29 +166,25 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId,
|
||||
btnLayout->addWidget(saveBtn);
|
||||
|
||||
layout->addLayout(btnLayout);
|
||||
connect(this, &QDialog::finished, this, [this] { delete m_saveContext; m_saveContext = nullptr; setSaving(false); });
|
||||
}
|
||||
|
||||
void MemberPropsDialog::setSaving(bool saving) {
|
||||
m_saveBtn->setEnabled(!saving); m_nickEdit->setEnabled(!saving && !m_isOwn);
|
||||
for (auto* box : {m_adminCb, m_superCb, m_storageCb, m_verifiedCb, m_deletedCb}) box->setEnabled(!saving && m_haveAdminKey);
|
||||
m_transferBtn->setEnabled(!saving && m_haveAdminKey && !m_isOwn);
|
||||
}
|
||||
|
||||
void MemberPropsDialog::finishSave(int result) {
|
||||
delete m_saveContext; m_saveContext = nullptr; setSaving(false);
|
||||
if (!result) { accept(); return; }
|
||||
m_errorLabel->setText(QCoreApplication::translate("ChatGui", "Could not save member changes. Reopen the dialog if the member has changed."));
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member properties: save refused ch=%s node=%016llx result=%d",
|
||||
qPrintable(m_channelId), (unsigned long long)m_nodeId, result);
|
||||
}
|
||||
|
||||
void MemberPropsDialog::onSave() {
|
||||
if (!m_db->m_db) return;
|
||||
|
||||
/* local_nick — always save local (unsigned, local only) */
|
||||
QString nick = m_nickEdit->text().trimmed();
|
||||
QString tbl = m_db->peersTableName(m_channelId);
|
||||
QByteArray nickSql = QString(
|
||||
"UPDATE \"%1\" SET local_nick=? WHERE node_id=?"
|
||||
).arg(tbl).toUtf8();
|
||||
sqlite3_stmt* ns = nullptr;
|
||||
if (sqlite3_prepare_v2(m_db->m_db, nickSql.constData(), -1, &ns, nullptr) == SQLITE_OK) {
|
||||
sqlite3_bind_text(ns, 1, nick.isEmpty() ? nullptr : nick.toUtf8().constData(),
|
||||
-1, nick.isEmpty() ? SQLITE_STATIC : SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int64(ns, 2, (sqlite3_int64)m_nodeId);
|
||||
sqlite3_step(ns);
|
||||
sqlite3_finalize(ns);
|
||||
}
|
||||
|
||||
/* signed fields — через chat_member_tags_commit_trampoline */
|
||||
if (m_edPrivkey.size() != 32) { accept(); return; }
|
||||
|
||||
if (m_saveContext) return;
|
||||
int ver = m_currentVer;
|
||||
QString json;
|
||||
json += QString("{\"ver\":\"%1\"").arg(ver);
|
||||
@@ -195,23 +195,15 @@ void MemberPropsDialog::onSave() {
|
||||
json += QString(",\"deleted\":\"%1\"").arg(m_deletedCb->isChecked() ? "yes" : "no");
|
||||
json += "}";
|
||||
|
||||
struct chat_member_tags_req* req = (struct chat_member_tags_req*)
|
||||
u_calloc(1, sizeof(struct chat_member_tags_req));
|
||||
if (!req) { accept(); return; }
|
||||
|
||||
req->inst = gui_bridge_get_inst();
|
||||
QByteArray ch_id_utf8 = m_channelId.toUtf8();
|
||||
strncpy(req->ch_id, ch_id_utf8.constData(), sizeof(req->ch_id) - 1);
|
||||
req->node_id = m_nodeId;
|
||||
strncpy(req->json, json.toUtf8().constData(), sizeof(req->json) - 1);
|
||||
|
||||
gui_bridge_post_uasync_fn(chat_member_tags_commit_trampoline, req);
|
||||
accept();
|
||||
m_errorLabel->clear(); setSaving(true); m_saveContext = new QObject(this);
|
||||
if (!gui_bridge_save_member(m_db ? m_db->m_db : nullptr, m_channelId, m_nodeId, m_nickEdit->text().trimmed(),
|
||||
m_haveAdminKey ? json.toUtf8() : QByteArray(), m_saveContext, [this](int result) { finishSave(result); })) finishSave(-1);
|
||||
}
|
||||
|
||||
void MemberPropsDialog::onTransferAdmin() {
|
||||
if (m_edPrivkey.size() != 32 || m_nodeId == (uint64_t)m_db->myNodeId()) return;
|
||||
if (!m_haveAdminKey || m_isOwn || m_saveContext) return;
|
||||
|
||||
QPointer<MemberPropsDialog> alive(this);
|
||||
QString name = m_memberName.isEmpty() ? QString("0x%1").arg(m_nodeId, 16, 16, QChar('0')) : m_memberName;
|
||||
QMessageBox::StandardButton answer = QMessageBox::question(
|
||||
this,
|
||||
@@ -222,16 +214,7 @@ void MemberPropsDialog::onTransferAdmin() {
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::No);
|
||||
|
||||
if (answer != QMessageBox::Yes) return;
|
||||
|
||||
struct chat_transfer_admin_req* req = (struct chat_transfer_admin_req*)
|
||||
u_calloc(1, sizeof(struct chat_transfer_admin_req));
|
||||
if (!req) return;
|
||||
|
||||
req->inst = gui_bridge_get_inst();
|
||||
QByteArray ch_id_utf8 = m_channelId.toUtf8();
|
||||
strncpy(req->ch_id, ch_id_utf8.constData(), sizeof(req->ch_id) - 1);
|
||||
req->target_node_id = m_nodeId;
|
||||
|
||||
gui_bridge_post_uasync_fn(chat_core_transfer_admin_trampoline, req);
|
||||
if (!alive || answer != QMessageBox::Yes) return;
|
||||
if (!gui_bridge_transfer_admin(m_db ? m_db->m_db : nullptr, m_channelId, m_nodeId))
|
||||
m_errorLabel->setText(QCoreApplication::translate("ChatGui", "Could not request admin key transfer."));
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
/* Диалог имени и прав участника канала; сохранение подписанных изменений и передача admin-ключа через ядро. */
|
||||
/* GUI-диалог локального ника и подписанных прав участника; оба изменения фиксируются одной транзакцией ядра.
|
||||
* Accepted означает подтверждённую запись, отказ оставляет черновик. Закрытие снимает callback, не отменяя принятую запись.
|
||||
* Передача admin-ключа запрашивается отдельно с подтверждением пользователя; ключи остаются в ядре. */
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
@@ -6,35 +8,45 @@
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QString>
|
||||
#include <QPointer>
|
||||
#include <stdint.h>
|
||||
|
||||
class DbManager;
|
||||
class QLabel;
|
||||
|
||||
class MemberPropsDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
/* db заимствован через QPointer, parent владеет диалогом; channelId/nodeId фиксируют адресата.
|
||||
* memberName/flags — начальный снимок отображения, JSON/ver и локальный ник читаются из БД. */
|
||||
explicit MemberPropsDialog(DbManager* db, const QString& channelId,
|
||||
uint64_t nodeId, const QString& memberName,
|
||||
uint8_t flags, QWidget* parent = nullptr);
|
||||
|
||||
private slots:
|
||||
void onSave();
|
||||
void onTransferAdmin();
|
||||
void onSave(); // Снимок формы передаётся ядру один раз до результата; private key в запрос не включается.
|
||||
void onTransferAdmin(); // После Yes ставит собственный запрос ядру, не подтверждает получение ключа адресатом.
|
||||
|
||||
private:
|
||||
DbManager* m_db;
|
||||
QString m_channelId;
|
||||
uint64_t m_nodeId;
|
||||
QByteArray m_edPrivkey;
|
||||
uint8_t m_flags;
|
||||
int m_currentVer;
|
||||
QString m_memberName;
|
||||
void setSaving(bool saving); // Блокирует редактирование/повторную передачу ключа, оставляя возможность закрыть форму.
|
||||
void finishSave(int result); // Успех закрывает форму; ошибка восстанавливает ввод и показывает отказ.
|
||||
QPointer<DbManager> m_db; // Заимствованная read-only проекция; не передаётся в worker.
|
||||
QString m_channelId; // Собственный ID канала.
|
||||
uint64_t m_nodeId; // Адресат сохранения/передачи ключа.
|
||||
bool m_haveAdminKey = false; // На момент открытия БД содержала ключ; ядро проверяет полномочия заново.
|
||||
bool m_isOwn = false; // Собственный ник не редактируется, передача ключа себе запрещена.
|
||||
uint8_t m_flags; // Исходные флаги для admin/super checkboxes.
|
||||
int m_currentVer; // Версия owner-блока при открытии: предотвращает перезапись устаревшей формой.
|
||||
QString m_memberName; // Имя в предупреждении о передаче ключа.
|
||||
|
||||
QLineEdit* m_nickEdit;
|
||||
QCheckBox* m_adminCb;
|
||||
QCheckBox* m_superCb;
|
||||
QCheckBox* m_storageCb;
|
||||
QCheckBox* m_verifiedCb;
|
||||
QCheckBox* m_deletedCb;
|
||||
QPushButton* m_transferBtn;
|
||||
QLineEdit* m_nickEdit; // Локальный неподписанный псевдоним, пустой снимает его.
|
||||
QCheckBox* m_adminCb; // Желаемая роль admin; историю и подпись строит ядро.
|
||||
QCheckBox* m_superCb; // supernode=yes/no.
|
||||
QCheckBox* m_storageCb; // storage=yes/no.
|
||||
QCheckBox* m_verifiedCb; // verified=yes/no.
|
||||
QCheckBox* m_deletedCb; // deleted=yes/no.
|
||||
QPushButton* m_transferBtn; // Доступна с admin-ключом для другого участника.
|
||||
QPushButton* m_saveBtn; // Отключена во время записи.
|
||||
QLabel* m_errorLabel; // Ошибка без технических подробностей; причина остаётся в логе.
|
||||
QObject* m_saveContext = nullptr; // Собственный context запроса; уничтожение снимает callback в мосте.
|
||||
};
|
||||
Reference in new issue
Block a user