From b6d8e822b4f2c84f4290215de7beda0ed36d1e1a Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 7 Oct 2026 17:18:40 +0200 Subject: [PATCH] Verify poll clicks preserve selection and message controls --- tools/chatgui/chatgui_doc.md | 3 ++ tools/chatgui/src/messagefeedback.cpp | 12 ++++- tools/chatgui/tests/test_invite_links.cpp | 60 +++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/tools/chatgui/chatgui_doc.md b/tools/chatgui/chatgui_doc.md index a73aed99..3f293fd8 100644 --- a/tools/chatgui/chatgui_doc.md +++ b/tools/chatgui/chatgui_doc.md @@ -107,6 +107,9 @@ X11 idle time в Wayland-сессии не используется: он не - Inline-анимированные эмодзи: при hover над сообщением — перебор кадров из LottieIcon, отрисовка поверх текста через QTextLayout. - Растягивающийся InputBar (QTextEdit, Enter — отправка, Shift+Enter — перевод строки). +Карточка опроса открывает «Голосование / результаты» одиночным левым кликом по телу сообщения. +Перетаскивание выделяет текст; аватар и строка реакций обрабатывают свои действия. + **EmojiPanel** (`src/emojipanel.h`) — QTabWidget с категориями (смайлики, жесты, символы, анимации). Каждая категория — QGridLayout из EmojiButton (QPushButton без Q_OBJECT, hover-анимация через per-widget QTimer). Статические эмодзи — Unicode символы. Анимированные — TGS-файлы, рендерятся в EmojiButton::paintEvent. **LottieIcon** (`src/lottieicon.h`) — загрузка и рендер TGS-анимаций: diff --git a/tools/chatgui/src/messagefeedback.cpp b/tools/chatgui/src/messagefeedback.cpp index 726e26d9..fb834d43 100644 --- a/tools/chatgui/src/messagefeedback.cpp +++ b/tools/chatgui/src/messagefeedback.cpp @@ -189,9 +189,15 @@ void MessageList::createPoll() { } void MessageList::showPoll(const QString& messageId) { - if (m_pollDialog) { m_pollDialog->raise(); return; } + if (m_pollDialog) { + GUI_DEBUG("poll: raising open dialog message=%s", qPrintable(m_pollDialogMessage)); + m_pollDialog->raise(); return; + } auto state = m_feedback.value(messageId), poll = state.value("poll").toObject(); - if (poll.isEmpty()) { m_pendingPoll = messageId; sendFeedback({{"cmd", "feedback"}, {"message_id", messageId}}); return; } + 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; + } QDialog dialog(this); 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); @@ -236,5 +242,7 @@ void MessageList::showPoll(const QString& messageId) { } sendFeedback({{"cmd", "vote"}, {"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(); + GUI_DEBUG("poll: dialog closed message=%s", qPrintable(messageId)); } diff --git a/tools/chatgui/tests/test_invite_links.cpp b/tools/chatgui/tests/test_invite_links.cpp index 72aca425..60d275c5 100644 --- a/tools/chatgui/tests/test_invite_links.cpp +++ b/tools/chatgui/tests/test_invite_links.cpp @@ -3,11 +3,13 @@ #include "../src/chatview.h" #include "../src/accountdelegate.h" #include "../src/message_links.h" +#include "../../../lib/debug_config.h" #include #include #include #include #include +#include extern "C" { #include "chat/invite_link.h" #include "chat/chat_member.h" @@ -16,6 +18,64 @@ extern "C" { class InviteLinksTest : public QObject { Q_OBJECT private slots: + void pollLeftClick_data() { + QTest::addColumn("outgoing"); QTest::addColumn("width"); + for (bool outgoing : {false, true}) for (int width : {280, 600}) + QTest::newRow(qPrintable(QString("%1-%2").arg(outgoing).arg(width))) << outgoing << width; + } + + void pollLeftClick() { + QFETCH(bool, outgoing); QFETCH(int, width); + QScopedValueRollback logging(g_debug_config.category_levels[DEBUG_CATEGORY_GENERAL], DEBUG_LEVEL_INFO); + QStandardItemModel model; + auto* message = new QStandardItem; + const QString key(64, 'a'); + message->setData("Which of these desktop options should everyone choose today?\nOption A\nOption B", MsgTextRole); + message->setData("poll", MsgContentTypeRole); message->setData(outgoing, MsgIsOutgoingRole); + message->setData("1001", MsgChannelIdRole); message->setData(key, MsgMessageKeyRole); + message->setData("Alice", MsgAuthorRole); message->setData("12:34", MsgTimeRole); + model.appendRow(message); + ChatView view; MessageDelegate delegate; + view.setModel(&model); view.setItemDelegate(&delegate); view.setFrameShape(QFrame::NoFrame); + view.setSelectionMode(QAbstractItemView::NoSelection); view.setEditTriggers(QAbstractItemView::NoEditTriggers); + QFont font = view.font(); font.setPixelSize(14); view.setFont(font); + view.resize(width, 400); view.show(); QTest::qWait(20); + QSignalSpy polls(&delegate, &MessageDelegate::pollRequested); + const QModelIndex index = model.index(0, 0); + const QPoint body(view.viewport()->width() / 2, QFontMetrics(view.font()).height() + 12); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); + QCOMPARE(polls.count(), 1); QCOMPARE(polls.first().first().toString(), key); + + QTest::mouseClick(view.viewport(), Qt::RightButton, Qt::NoModifier, body); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(0, body.y())); + const QPoint footer(body.x(), view.visualRect(index).bottom() - 10); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, footer); + if (width == 600) { + const QPoint avatar(outgoing ? view.viewport()->width() - 35 : 35, view.visualRect(index).bottom() - 30); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, avatar); + } + QCOMPARE(polls.count(), 1); + + QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); + QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, body + QPoint(20, 0)); + QCOMPARE(polls.count(), 1); + QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); + for (const QPoint point : {body + QPoint(20, 0), body}) { + QMouseEvent drag(QEvent::MouseMove, point, view.viewport()->mapToGlobal(point), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); + QApplication::sendEvent(view.viewport(), &drag); + } + QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); + QVERIFY(view.hasSelection()); QCOMPARE(polls.count(), 1); // Возврат мыши не превращает выделение в клик. + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); + QCOMPARE(polls.count(), 2); + message->setData(QString(), MsgMessageKeyRole); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); + QCOMPARE(polls.count(), 2); + message->setData(key, MsgMessageKeyRole); message->setData("text/plain", MsgContentTypeRole); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, body); + QCOMPARE(polls.count(), 2); + } + void reactionFooter_data() { QTest::addColumn("type"); QTest::addColumn("outgoing"); QTest::addColumn("width"); for (const auto& type : {"text/plain", "audio/opus", "image/png", "video/mp4", "application/octet-stream", "poll"})