Own feedback requests and fence poll dialogs to their channel and lifetime
This commit is contained in:
1 parent
d0bf402824
commit
8b2b68d383
3 files changed
+44
-24
No files matched your search
@@ -23,6 +23,7 @@
|
||||
#include <QWidgetAction>
|
||||
#include <QScrollArea>
|
||||
#include <QHBoxLayout>
|
||||
#include <QScopeGuard>
|
||||
#include <QToolButton>
|
||||
#include <cstring>
|
||||
|
||||
@@ -65,7 +66,8 @@ void MessageList::requestReaction(const QModelIndex& index, int emoji) {
|
||||
sendFeedback({{"cmd", "react"}, {"ch", channel}, {"message_id", messageId}, {"emoji", emoji}}); return;
|
||||
}
|
||||
GUI_DEBUG("reaction: picker opened ch=%s message=%s", qPrintable(channel), qPrintable(messageId));
|
||||
QMenu menu(m_view); addReactionActions(&menu, channel, messageId); menu.exec(QCursor::pos());
|
||||
QMenu menu; connect(this, &QObject::destroyed, &menu, &QMenu::close);
|
||||
addReactionActions(&menu, channel, messageId); menu.exec(QCursor::pos());
|
||||
}
|
||||
|
||||
/* Итог для баббла и открытого диалога; строковые поля показываем как обычный текст. */
|
||||
@@ -81,15 +83,18 @@ static QString pollSummary(const QJsonObject& poll) {
|
||||
}
|
||||
|
||||
/* Команды владеют только C-данными; QObject остаются в GUI-потоке. */
|
||||
void MessageList::sendFeedback(QJsonObject command, bool userAction) {
|
||||
if (!gui_bridge_get_inst()) { GUI_ERROR("feedback: core unavailable"); return; }
|
||||
bool MessageList::sendFeedback(QJsonObject command, bool userAction) {
|
||||
if (!command.contains("ch")) command["ch"] = m_currentChannelId;
|
||||
if (command.value("ch").toString().isEmpty() || (userAction && command.value("ch").toString() != m_currentChannelId)) {
|
||||
GUI_WARN("feedback: stale/empty channel rejected"); return false;
|
||||
}
|
||||
QByteArray json = QJsonDocument(command).toJson(QJsonDocument::Compact);
|
||||
auto* req = static_cast<chat_feedback_request*>(u_malloc(sizeof(chat_feedback_request) + json.size() + 1));
|
||||
if (!req) { GUI_ERROR("feedback: command allocation failed bytes=%d", json.size()); return; }
|
||||
req->inst = gui_bridge_get_inst(); req->request_id = userAction ? ++m_feedbackSequence : 0;
|
||||
if (!req) { GUI_ERROR("feedback: command allocation failed bytes=%d", json.size()); return false; }
|
||||
req->request_id = userAction ? ++m_feedbackSequence : 0;
|
||||
memcpy(req + 1, json.constData(), json.size()); reinterpret_cast<char*>(req + 1)[json.size()] = 0;
|
||||
gui_bridge_post_uasync_fn(chat_feedback_command_trampoline, req);
|
||||
if (gui_bridge_post_bound_fn(chat_feedback_command_trampoline, req, &req->inst) < 0) { u_free(req); return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
void MessageList::setupFeedback(QStandardItem* item, const MessageRow& message, const QString& channel) {
|
||||
@@ -124,12 +129,15 @@ void MessageList::onFeedbackChanged(const uint8_t* data, int len) {
|
||||
}
|
||||
|
||||
void MessageList::onFeedbackResult(const uint8_t* data, int len) {
|
||||
if (!data || len < 9) { GUI_ERROR("feedback: invalid result event bytes=%d", len); return; }
|
||||
if (!data || len < 9 || data[8] > 1) { GUI_ERROR("feedback: invalid result event bytes=%d", len); return; }
|
||||
quint64 request; memcpy(&request, data, 8);
|
||||
QJsonObject state = QJsonDocument::fromJson(QByteArray(reinterpret_cast<const char*>(data + 9), len - 9)).object();
|
||||
QJsonParseError error;
|
||||
const auto document = QJsonDocument::fromJson(QByteArray(reinterpret_cast<const char*>(data + 9), len - 9), &error);
|
||||
if (error.error != QJsonParseError::NoError || !document.isObject()) { GUI_ERROR("feedback: invalid result JSON"); return; }
|
||||
QJsonObject state = document.object();
|
||||
if (!data[8]) {
|
||||
GUI_ERROR("feedback: command failed request=%llu reason=%s", (unsigned long long)request, qPrintable(state.value("error").toString()));
|
||||
if (request) {
|
||||
if (request && gui_bridge_is_uasync_ready()) {
|
||||
m_pendingPoll.clear(); QByteArray reason = state.value("error").toString().toUtf8();
|
||||
QMessageBox::warning(this, feedbackText("Polls and reactions"), feedbackText(reason.constData()));
|
||||
}
|
||||
@@ -175,7 +183,10 @@ void MessageList::applyFeedback(const QJsonObject& state) {
|
||||
|
||||
void MessageList::createPoll() {
|
||||
if (m_currentChannelId.isEmpty() || isDmChannel(m_currentChannelId)) return;
|
||||
QDialog dialog(this); dialog.setWindowTitle(feedbackText("Create poll")); auto* layout = new QVBoxLayout(&dialog);
|
||||
const QString channel = m_currentChannelId;
|
||||
QPointer<QDialog> holder = new QDialog(this); auto release = qScopeGuard([&] { delete holder.data(); });
|
||||
QDialog& dialog = *holder;
|
||||
dialog.setWindowTitle(feedbackText("Create poll")); auto* layout = new QVBoxLayout(&dialog);
|
||||
auto* question = new QPlainTextEdit(&dialog); question->setPlaceholderText(feedbackText("Question")); question->setMaximumHeight(70);
|
||||
auto* options = new QPlainTextEdit(&dialog); options->setPlaceholderText(feedbackText("One option per line"));
|
||||
auto* mode = new QComboBox(&dialog); mode->addItem(feedbackText("Single choice")); mode->addItem(feedbackText("Multiple choices"));
|
||||
@@ -183,7 +194,7 @@ void MessageList::createPoll() {
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
|
||||
layout->addWidget(question); layout->addWidget(options); layout->addWidget(mode); layout->addWidget(custom); layout->addWidget(buttons);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dialog, [&, this]() {
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dialog, [&, this, channel]() {
|
||||
const QString title = question->toPlainText().trimmed(); QJsonArray values; QSet<QString> unique;
|
||||
int bytes = 6 + title.toUtf8().size(); bool valid = !title.isEmpty() && title.toUtf8().size() <= CHAT_POLL_QUESTION_MAX;
|
||||
for (const auto& line : options->toPlainText().split('\n')) {
|
||||
@@ -195,9 +206,8 @@ void MessageList::createPoll() {
|
||||
QMessageBox::warning(&dialog, feedbackText("Create poll"),
|
||||
feedbackText("Enter a question and distinct options within the size limit.")); return;
|
||||
}
|
||||
sendFeedback({{"cmd", "poll_create"}, {"question", title}, {"options", values},
|
||||
{"mode", mode->currentIndex() ? "multiple" : "single"}, {"allow_custom_answer", custom->isChecked()}});
|
||||
dialog.accept();
|
||||
if (sendFeedback({{"cmd", "poll_create"}, {"ch", channel}, {"question", title}, {"options", values},
|
||||
{"mode", mode->currentIndex() ? "multiple" : "single"}, {"allow_custom_answer", custom->isChecked()}})) dialog.accept();
|
||||
});
|
||||
dialog.resize(420, 360); dialog.exec();
|
||||
}
|
||||
@@ -210,9 +220,13 @@ void MessageList::showPoll(const QString& messageId) {
|
||||
auto state = m_feedback.value(messageId), poll = state.value("poll").toObject();
|
||||
if (poll.isEmpty()) {
|
||||
GUI_DEBUG("poll: requesting state ch=%s message=%s", qPrintable(m_currentChannelId), qPrintable(messageId));
|
||||
m_pendingPoll = messageId; sendFeedback({{"cmd", "feedback"}, {"message_id", messageId}}); return;
|
||||
m_pendingPoll = messageId;
|
||||
if (!sendFeedback({{"cmd", "feedback"}, {"message_id", messageId}})) m_pendingPoll.clear();
|
||||
return;
|
||||
}
|
||||
QDialog dialog(this); dialog.setWindowTitle(feedbackText("Vote / results"));
|
||||
const QString channel = m_currentChannelId; QPointer<MessageList> self(this);
|
||||
QPointer<QDialog> holder = new QDialog(this); auto release = qScopeGuard([&] { delete holder.data(); });
|
||||
QDialog& dialog = *holder; dialog.setWindowTitle(feedbackText("Vote / results"));
|
||||
m_pollDialog = &dialog; m_pollDialogMessage = messageId; auto* layout = new QVBoxLayout(&dialog);
|
||||
auto* title = new QLabel(poll.value("question").toString(), &dialog); title->setTextFormat(Qt::PlainText); title->setWordWrap(true);
|
||||
layout->addWidget(title);
|
||||
@@ -243,20 +257,22 @@ void MessageList::showPoll(const QString& messageId) {
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, &dialog);
|
||||
auto* withdraw = buttons->addButton(feedbackText("Withdraw vote"), QDialogButtonBox::ActionRole);
|
||||
layout->addWidget(buttons);
|
||||
connect(withdraw, &QPushButton::clicked, &dialog, [&, this]() {
|
||||
sendFeedback({{"cmd", "vote"}, {"message_id", messageId}, {"choices", QJsonArray{}}, {"custom", ""}}); dialog.accept();
|
||||
connect(withdraw, &QPushButton::clicked, &dialog, [&, this, channel]() {
|
||||
if (sendFeedback({{"cmd", "vote"}, {"ch", channel}, {"message_id", messageId}, {"choices", QJsonArray{}}, {"custom", ""}})) dialog.accept();
|
||||
});
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dialog, [&, this]() {
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dialog, [&, this, channel]() {
|
||||
QJsonArray ids; for (int i = 0; i < choices.size(); i++) if (choices[i]->isChecked()) ids.append(i);
|
||||
QString text = poll.value("allow_custom_answer").toBool() && (multiple || (other && other->isChecked()))
|
||||
? own->toPlainText().trimmed() : QString();
|
||||
if (text.toUtf8().size() > CHAT_POLL_CUSTOM_MAX || (other && other->isChecked() && text.isEmpty())) {
|
||||
QMessageBox::warning(&dialog, feedbackText("Your answer"), feedbackText("Enter a shorter nonempty answer.")); return;
|
||||
}
|
||||
sendFeedback({{"cmd", "vote"}, {"message_id", messageId}, {"choices", ids}, {"custom", text}}); dialog.accept();
|
||||
if (sendFeedback({{"cmd", "vote"}, {"ch", channel}, {"message_id", messageId}, {"choices", ids}, {"custom", text}})) dialog.accept();
|
||||
});
|
||||
GUI_INFO("poll: opening dialog ch=%s message=%s options=%d", qPrintable(m_currentChannelId), qPrintable(messageId), int(values.size()));
|
||||
applyFeedback(state); dialog.resize(450, 560); dialog.exec(); m_pollDialog = nullptr; m_pollDialogMessage.clear();
|
||||
applyFeedback(state); dialog.resize(450, 560); dialog.exec();
|
||||
if (!self) return;
|
||||
m_pollDialog = nullptr; m_pollDialogMessage.clear();
|
||||
GUI_DEBUG("poll: dialog closed message=%s", qPrintable(messageId));
|
||||
}
|
||||
@@ -426,6 +426,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
||||
m_peerStatus->hide();
|
||||
hl->addWidget(m_peerStatus);
|
||||
m_channelTitle = new QLabel(header);
|
||||
m_channelTitle->setTextFormat(Qt::PlainText);
|
||||
m_channelTitle->setStyleSheet("font-weight: bold; font-size: 14px;");
|
||||
m_channelTitle->setText(QCoreApplication::translate("ChatGui", "Channel"));
|
||||
hl->addWidget(m_channelTitle, 1);
|
||||
@@ -435,6 +436,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
||||
connect(m_keyRequestsButton, &QPushButton::clicked, this, &MessageList::showKeyRequests);
|
||||
hl->addWidget(m_keyRequestsButton);
|
||||
m_talkingLabel = new QLabel(header);
|
||||
m_talkingLabel->setTextFormat(Qt::PlainText);
|
||||
m_talkingLabel->setStyleSheet("color: #27ae60; font-size: 12px;");
|
||||
m_talkingLabel->hide();
|
||||
hl->addWidget(m_talkingLabel);
|
||||
@@ -503,7 +505,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
||||
qPrintable(channel), (unsigned long long)source.id, (unsigned long long)source.epoch);
|
||||
emit streamRequested(channel.toULongLong(), source.id, source.epoch, source.name);
|
||||
};
|
||||
QMenu menu(this);
|
||||
QMenu menu; connect(this, &QObject::destroyed, &menu, &QMenu::close);
|
||||
auto* all = menu.addAction(QCoreApplication::translate("ChatGui", "Watch all active streams"));
|
||||
connect(all, &QAction::triggered, this, [this, channel] {
|
||||
if (channel == m_currentChannelId) emit allStreamsRequested(channel.toULongLong());
|
||||
@@ -553,7 +555,7 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
||||
const QString channel = m_currentChannelId;
|
||||
if (!channel.toULongLong()) return;
|
||||
auto text = [](const char* s) { return QCoreApplication::translate("ChatGui", s); };
|
||||
QMenu menu(this);
|
||||
QMenu menu; connect(this, &QObject::destroyed, &menu, &QMenu::close);
|
||||
const bool own = m_streamPublisherGroup != 0;
|
||||
auto* publish = menu.addAction(text(own ? "Manage stream" : "Start stream"));
|
||||
menu.addSeparator();
|
||||
@@ -893,6 +895,7 @@ void MessageList::loadChannel(const QString& channelId) {
|
||||
}
|
||||
|
||||
if (!sameChannel) m_historyAnchor.clear();
|
||||
if (!sameChannel && m_pollDialog) m_pollDialog->reject();
|
||||
m_view->setHistoricalWindow(!m_historyAnchor.isEmpty());
|
||||
m_model->clear();
|
||||
m_avatarCache.clear();
|
||||
@@ -1386,6 +1389,7 @@ void MessageList::onMessageContextMenu(const QPoint& pos) {
|
||||
bool downloaded = !filePath.isEmpty() && QFile::exists(filePath);
|
||||
|
||||
QMenu menu;
|
||||
connect(this, &QObject::destroyed, &menu, &QMenu::close);
|
||||
QPointer<MessageList> guard(this);
|
||||
QString messageId = idx.data(MsgMessageKeyRole).toString();
|
||||
if (!isDm && messageId.size() == 64) {
|
||||
|
||||
@@ -123,7 +123,7 @@ private:
|
||||
void refreshReplies();
|
||||
void jumpToMessage(const QString& messageId);
|
||||
|
||||
void sendFeedback(QJsonObject command, bool userAction = true);
|
||||
bool sendFeedback(QJsonObject command, bool userAction = true); // true — запрос принят; ch фиксируется до модального UI.
|
||||
void setupFeedback(QStandardItem* item, const MessageRow& message, const QString& channel);
|
||||
void createPoll();
|
||||
void showPoll(const QString& messageId);
|
||||
|
||||
Reference in new issue
Block a user