Confirm atomic member property saves before closing the dialog

This commit is contained in:
evgeny committed 2026-10-28 00:58:59 +01:00
1 parent 7e412d950a
commit 5842855583
2 files changed
+82 -87

No files matched your search

+53 -70
View File
@@ -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."));
}
+29 -17
View File
@@ -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 в мосте.
};