Own channel settings requests and guard nested dialogs
This commit is contained in:
1 parent
271cfcd2f8
commit
74bcf91407
2 files changed
+111
-49
No files matched your search
@@ -14,6 +14,7 @@ extern "C" {
|
||||
#include <QLabel>
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QScopeGuard>
|
||||
#include <cstring>
|
||||
|
||||
ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager* db, QWidget* parent)
|
||||
@@ -21,6 +22,10 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager
|
||||
, m_channelId(channelId)
|
||||
, m_db(db)
|
||||
{
|
||||
const quint64 group = channelId.toULongLong();
|
||||
m_validChannel = group && QString::number(group) == channelId;
|
||||
if (!m_validChannel) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "channel settings: invalid channel ID bytes=%d", int(channelId.toUtf8().size()));
|
||||
if (db) connect(db, &QObject::destroyed, this, &QDialog::reject);
|
||||
setWindowTitle(QCoreApplication::translate("ChatGui", "Channel Settings"));
|
||||
setMinimumWidth(340);
|
||||
setModal(true);
|
||||
@@ -38,7 +43,7 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager
|
||||
chIdLabel->setWordWrap(true);
|
||||
layout->addWidget(chIdLabel);
|
||||
|
||||
if (auto* avatars = GroupAvatarManager::instance()) {
|
||||
if (QPointer<GroupAvatarManager> avatars = GroupAvatarManager::instance()) {
|
||||
auto* preview = new QLabel(this); preview->setFixedSize(64, 64); layout->addWidget(preview);
|
||||
auto* buttons = new QHBoxLayout;
|
||||
auto* upload = new QPushButton(QCoreApplication::translate("ChatGui", "Upload group avatar"), this);
|
||||
@@ -47,11 +52,13 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager
|
||||
bool hasKey = false;
|
||||
if (db && db->isOpen()) for (const auto& row : db->getChannels())
|
||||
if (row.channelId == channelId) hasKey = row.ed25519_privkey.size() == 32;
|
||||
upload->setEnabled(hasKey); remove->setEnabled(hasKey);
|
||||
upload->setEnabled(m_validChannel && hasKey); remove->setEnabled(m_validChannel && hasKey);
|
||||
connect(avatars, &QObject::destroyed, upload, [upload, remove] { upload->setEnabled(false); remove->setEnabled(false); });
|
||||
auto* info = new QLabel(QCoreApplication::translate("ChatGui", "Group avatar: up to 16 KiB. Changes apply immediately and require the group key."), this);
|
||||
info->setWordWrap(true); auto* result = new QLabel(this); result->setWordWrap(true);
|
||||
buttons->addWidget(upload); buttons->addWidget(remove); layout->addLayout(buttons); layout->addWidget(info); layout->addWidget(result);
|
||||
auto refresh = [avatars, preview, channelId] {
|
||||
if (!avatars) return;
|
||||
const QImage image = avatars->image(channelId);
|
||||
if (image.isNull()) { preview->setPixmap(QPixmap()); preview->setText(QCoreApplication::translate("ChatGui", "No avatar")); }
|
||||
else preview->setPixmap(QPixmap::fromImage(image).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
@@ -61,11 +68,19 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager
|
||||
if (channel == channelId) { result->setText(message); refresh(); }
|
||||
});
|
||||
connect(upload, &QPushButton::clicked, this, [this, avatars, channelId] {
|
||||
const QString file = QFileDialog::getOpenFileName(this, QCoreApplication::translate("ChatGui", "Choose avatar"), {},
|
||||
QCoreApplication::translate("ChatGui", "Images (*.png *.jpg *.jpeg *.webp)"));
|
||||
if (!file.isEmpty()) avatars->upload(channelId, file);
|
||||
if (!avatars) return;
|
||||
QPointer<ChannelSettingsDialog> self(this);
|
||||
QPointer<QFileDialog> chooser = new QFileDialog(this, QCoreApplication::translate("ChatGui", "Choose avatar"));
|
||||
auto release = qScopeGuard([&] { delete chooser.data(); });
|
||||
chooser->setNameFilter(QCoreApplication::translate("ChatGui", "Images (*.png *.jpg *.jpeg *.webp)"));
|
||||
chooser->setFileMode(QFileDialog::ExistingFile);
|
||||
connect(avatars, &QObject::destroyed, chooser, &QDialog::reject);
|
||||
if (m_db) connect(m_db, &QObject::destroyed, chooser, &QDialog::reject);
|
||||
if (chooser->exec() != QDialog::Accepted || !self || !chooser || !avatars) return;
|
||||
const auto files = chooser->selectedFiles();
|
||||
if (!files.isEmpty()) avatars->upload(channelId, files.first());
|
||||
});
|
||||
connect(remove, &QPushButton::clicked, this, [avatars, channelId] { avatars->remove(channelId); }); refresh();
|
||||
connect(remove, &QPushButton::clicked, this, [avatars, channelId] { if (avatars) avatars->remove(channelId); }); refresh();
|
||||
}
|
||||
|
||||
layout->addSpacing(8);
|
||||
@@ -77,8 +92,11 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager
|
||||
layout->addWidget(m_autoPlayVoice);
|
||||
|
||||
m_soundOnMessage = new QCheckBox(QCoreApplication::translate("ChatGui", "Play sound on incoming message"), this);
|
||||
m_soundOnMessage->setChecked(true);
|
||||
m_soundOnMessage->setToolTip(QCoreApplication::translate("ChatGui", "Play a notification sound when a message arrives in this channel."));
|
||||
layout->addWidget(m_soundOnMessage);
|
||||
m_status = new QLabel(this); m_status->setObjectName("channelSettingsStatus");
|
||||
m_status->setTextFormat(Qt::PlainText); m_status->setWordWrap(true); layout->addWidget(m_status);
|
||||
|
||||
layout->addStretch();
|
||||
|
||||
@@ -88,7 +106,7 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager
|
||||
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());
|
||||
deleteBtn->setEnabled(m_validChannel && m_db && m_db->isOpen() && gui_bridge_is_uasync_ready());
|
||||
connect(deleteBtn, &QPushButton::clicked, this, &ChannelSettingsDialog::onDelete);
|
||||
btnLayout->addWidget(deleteBtn);
|
||||
btnLayout->addStretch();
|
||||
@@ -103,6 +121,7 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager
|
||||
btnLayout->addWidget(cancelBtn);
|
||||
|
||||
auto* saveBtn = new QPushButton(QCoreApplication::translate("ChatGui", "Save"), this);
|
||||
saveBtn->setEnabled(m_validChannel && m_db && m_db->isOpen());
|
||||
saveBtn->setMinimumWidth(90);
|
||||
saveBtn->setDefault(true);
|
||||
saveBtn->setStyleSheet(
|
||||
@@ -121,16 +140,30 @@ ChannelSettingsDialog::ChannelSettingsDialog(const QString& channelId, DbManager
|
||||
QString soundKey = QStringLiteral("ch_%1_sound_on_message").arg(channelId);
|
||||
m_soundOnMessage->setChecked(m_db->getUiStateInt(soundKey, 1) != 0);
|
||||
}
|
||||
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "channel settings opened: channel=%s valid=%d db=%d autoplay=%d sound=%d",
|
||||
qPrintable(channelId), m_validChannel, bool(m_db && m_db->isOpen()), m_autoPlayVoice->isChecked(), m_soundOnMessage->isChecked());
|
||||
}
|
||||
|
||||
/* Как в Android: удаляются только локальные данные; ядро уведомит GUI по завершении. */
|
||||
void ChannelSettingsDialog::onDelete() {
|
||||
if (!m_validChannel || !m_db || !m_db->isOpen()) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "channel delete: unavailable channel=%s", qPrintable(m_channelId));
|
||||
m_status->setText(QCoreApplication::translate("ChatGui", "Channel is unavailable.")); return;
|
||||
}
|
||||
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; }
|
||||
bool found = false;
|
||||
for (const auto& channel : m_db->getChannels())
|
||||
if (channel.channelId == m_channelId) { name = channel.name; found = true; break; }
|
||||
if (!found) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "channel delete: channel removed before confirmation channel=%s", qPrintable(m_channelId));
|
||||
m_status->setText(QCoreApplication::translate("ChatGui", "Channel is unavailable.")); return;
|
||||
}
|
||||
QPointer<ChannelSettingsDialog> self(this);
|
||||
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);
|
||||
.arg(name), QMessageBox::NoButton);
|
||||
connect(this, &QObject::destroyed, &confirmation, &QDialog::reject);
|
||||
connect(m_db, &QObject::destroyed, &confirmation, &QDialog::reject);
|
||||
confirmation.setTextFormat(Qt::PlainText);
|
||||
auto* remove = confirmation.addButton(QCoreApplication::translate("ChatGui", "Delete"), QMessageBox::DestructiveRole);
|
||||
remove->setObjectName("confirmDeleteChannel");
|
||||
@@ -138,57 +171,72 @@ void ChannelSettingsDialog::onDelete() {
|
||||
confirmation.setDefaultButton(cancel);
|
||||
confirmation.setEscapeButton(cancel);
|
||||
confirmation.exec();
|
||||
if (confirmation.clickedButton() != remove) return;
|
||||
if (!self || confirmation.clickedButton() != remove) return;
|
||||
found = false;
|
||||
if (m_db && m_db->isOpen()) for (const auto& channel : m_db->getChannels())
|
||||
if (channel.channelId == m_channelId) { found = true; break; }
|
||||
if (!found) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "channel delete: channel removed during confirmation channel=%s", qPrintable(m_channelId));
|
||||
m_status->setText(QCoreApplication::translate("ChatGui", "Channel is unavailable.")); 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."));
|
||||
m_status->setText(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."));
|
||||
m_status->setText(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);
|
||||
if (gui_bridge_post_uasync_owned_fn(chat_core_delete_channel_trampoline, request) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel delete: request rejected channel=%s", channel.constData());
|
||||
u_free(request); m_status->setText(QCoreApplication::translate("ChatGui", "Failed to delete channel.")); return;
|
||||
}
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "channel delete queued: channel=%s scope=local", channel.constData());
|
||||
accept();
|
||||
}
|
||||
|
||||
void ChannelSettingsDialog::onSave() {
|
||||
auto saveKey = [&](const char* suffix, int val) {
|
||||
QString key = QStringLiteral("ch_%1_%2").arg(m_channelId, QString::fromUtf8(suffix));
|
||||
QByteArray keyBA = key.toUtf8();
|
||||
QByteArray valBA = QByteArray::number(val);
|
||||
size_t klen = (size_t)keyBA.size(), vlen = (size_t)valBA.size();
|
||||
size_t total = klen + 1 + vlen + 1;
|
||||
if (total <= 256) {
|
||||
struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg));
|
||||
if (!arg) return;
|
||||
arg->inst = gui_bridge_get_inst();
|
||||
memcpy(arg->data, keyBA.constData(), klen + 1);
|
||||
memcpy(arg->data + klen + 1, valBA.constData(), vlen + 1);
|
||||
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
|
||||
}
|
||||
};
|
||||
saveKey("auto_play_voice", m_autoPlayVoice->isChecked() ? 1 : 0);
|
||||
|
||||
/* sound_on_message: сохранить + разослать пирам (троттлинг db_sync на сервере) */
|
||||
{
|
||||
int on = m_soundOnMessage->isChecked() ? 1 : 0;
|
||||
QByteArray chIdBA = m_channelId.toUtf8();
|
||||
struct chat_sound_arg* arg = (struct chat_sound_arg*)u_malloc(sizeof(struct chat_sound_arg));
|
||||
if (arg) {
|
||||
memset(arg, 0, sizeof(*arg));
|
||||
arg->inst = gui_bridge_get_inst();
|
||||
strncpy(arg->ch_id, chIdBA.constData(), sizeof(arg->ch_id) - 1);
|
||||
arg->on = on;
|
||||
gui_bridge_post_uasync_fn(chat_core_set_sound_on_message_trampoline, arg);
|
||||
}
|
||||
if (!m_validChannel || !m_db || !m_db->isOpen()) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "channel settings save: unavailable channel=%s", qPrintable(m_channelId));
|
||||
m_status->setText(QCoreApplication::translate("ChatGui", "Channel is unavailable.")); return;
|
||||
}
|
||||
auto* inst = gui_bridge_get_inst();
|
||||
if (!inst || !gui_bridge_is_uasync_ready()) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "channel settings save: core unavailable channel=%s", qPrintable(m_channelId));
|
||||
m_status->setText(QCoreApplication::translate("ChatGui", "The core is not running.")); return;
|
||||
}
|
||||
auto* voice = static_cast<save_ui_state_arg*>(u_calloc(1, sizeof(save_ui_state_arg)));
|
||||
auto* sound = static_cast<chat_sound_arg*>(u_calloc(1, sizeof(chat_sound_arg)));
|
||||
auto release = qScopeGuard([&] { u_free(voice); u_free(sound); });
|
||||
if (!voice || !sound) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel settings save: allocation failed channel=%s voice=%d sound=%d",
|
||||
qPrintable(m_channelId), bool(voice), bool(sound));
|
||||
m_status->setText(QCoreApplication::translate("ChatGui", "Failed to save channel settings. Please try again.")); return;
|
||||
}
|
||||
voice->inst = sound->inst = inst;
|
||||
const QByteArray key = QStringLiteral("ch_%1_auto_play_voice").arg(m_channelId).toUtf8();
|
||||
memcpy(voice->data, key.constData(), size_t(key.size()));
|
||||
voice->data[key.size() + 1] = m_autoPlayVoice->isChecked() ? '1' : '0';
|
||||
const QByteArray channel = m_channelId.toUtf8();
|
||||
memcpy(sound->ch_id, channel.constData(), size_t(channel.size())); sound->on = m_soundOnMessage->isChecked();
|
||||
if (gui_bridge_post_uasync_owned_fn(chat_core_save_ui_state_trampoline, voice) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel settings save: autoplay request rejected channel=%s", channel.constData());
|
||||
m_status->setText(QCoreApplication::translate("ChatGui", "Failed to save channel settings. Please try again.")); return;
|
||||
}
|
||||
voice = nullptr;
|
||||
if (gui_bridge_post_uasync_owned_fn(chat_core_set_sound_on_message_trampoline, sound) < 0) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "channel settings save: sound request rejected after autoplay accepted channel=%s", channel.constData());
|
||||
m_status->setText(QCoreApplication::translate("ChatGui", "Failed to save channel settings. Please try again.")); return;
|
||||
}
|
||||
sound = nullptr;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "channel settings queued: channel=%s autoplay=%d sound=%d", channel.constData(),
|
||||
m_autoPlayVoice->isChecked(), m_soundOnMessage->isChecked());
|
||||
accept();
|
||||
}
|
||||
@@ -1,24 +1,38 @@
|
||||
/* Локальные настройки канала: autoplay, звук и удаление с устройства после подтверждения. */
|
||||
/* Настройки одной группы, использовать в GUI-потоке: autoplay, звук, аватар и локальное удаление.
|
||||
* DbManager заимствуется через QPointer; его уничтожение закрывает диалог с reject.
|
||||
* Save отправляет две независимые owned-команды ядру; accept означает приём обеих в очередь,
|
||||
* а не завершение SQL. Ошибки commit логируются ядром, повторное сохранение идемпотентно.
|
||||
* При отказе очереди окно остаётся открытым для повторной попытки; ранее принятая команда может выполниться.
|
||||
* Изменения аватара применяются сразу через GroupAvatarManager и не отменяются кнопкой Cancel. */
|
||||
#pragma once
|
||||
|
||||
#include <QDialog>
|
||||
#include <QCheckBox>
|
||||
#include <QString>
|
||||
#include <QPointer>
|
||||
|
||||
class DbManager;
|
||||
class QLabel;
|
||||
|
||||
class ChannelSettingsDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
/* channelId — каноническая десятичная строка ненулевого uint64 ID; parent владеет диалогом.
|
||||
* Без открытой db/с допустимым ID только просмотр: Save/Delete недоступны. Дочерние поля принадлежат диалогу. */
|
||||
explicit ChannelSettingsDialog(const QString& channelId, DbManager* db, QWidget* parent = nullptr);
|
||||
|
||||
private slots:
|
||||
/* Читает поля и передаёт autoplay/звук в поток ядра; отказы выделения/очереди показывает без закрытия. */
|
||||
void onSave();
|
||||
/* Подтверждает локальное удаление (Cancel по умолчанию), заново проверяет наличие группы и очередь.
|
||||
* Принятие команды закрывает окно; CHAT_EVT_CHANNEL_DELETED приходит контроллеру от ядра. */
|
||||
void onDelete();
|
||||
|
||||
private:
|
||||
QString m_channelId;
|
||||
DbManager* m_db;
|
||||
QCheckBox* m_autoPlayVoice;
|
||||
QCheckBox* m_soundOnMessage;
|
||||
QString m_channelId; /* Собственная копия ID целевой группы. */
|
||||
QPointer<DbManager> m_db; /* Не владеет QObject и SQLite. */
|
||||
bool m_validChannel = false; /* ID ненулевой, канонический и помещается в uint64. */
|
||||
QCheckBox* m_autoPlayVoice; /* Дочерний autoplay-флаг; по умолчанию false. */
|
||||
QCheckBox* m_soundOnMessage; /* Дочерний sound-флаг; по умолчанию true. */
|
||||
QLabel* m_status; /* Дочерняя строка ошибки подготовки/отправки команды. */
|
||||
};
|
||||
Reference in new issue
Block a user