diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index 6ef22693..f7bdd526 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -410,7 +410,8 @@ MessageDelegate::Layout MessageDelegate::calcLayout( || index.data(MsgMessageKeyRole).toString().size() != 64 || layout.bubbleRect.isEmpty()) return layout; QFont font = option.font; font.setPointSize(qMax(1, font.pointSize() - 1)); const int height = qMax(28, QFontMetrics(font).height() + 8), left = layout.bubbleRect.left(); - int y = layout.bubbleRect.bottom() + 4, x = left + height + 4; + int y = qMax(layout.bubbleRect.bottom(), qMax(layout.statusRect.bottom(), layout.deliveryRect.bottom())) + 4; + int x = left + height + 4; layout.reactionHeart = QRect(left, y, height, height); for (const auto& value : index.data(MsgReactionsRole).toList()) { const auto reaction = value.toMap(); @@ -435,6 +436,7 @@ MessageDelegate::Layout MessageDelegate::calcBodyLayout( if (v) L.viewWidth = v->viewport()->width(); } if (L.viewWidth <= 0) L.viewWidth = 300; + L.narrow = L.viewWidth - 2 * kMarginH - kAvatar - kGap < 400; bool hasQuote = index.data(MsgHasQuoteRole).toBool(); QString text = index.data(MsgTextRole).toString(); diff --git a/tools/chatgui/tests/test_invite_links.cpp b/tools/chatgui/tests/test_invite_links.cpp index 19b63ed5..34cc3cbb 100644 --- a/tools/chatgui/tests/test_invite_links.cpp +++ b/tools/chatgui/tests/test_invite_links.cpp @@ -6,6 +6,7 @@ #include #include #include +#include extern "C" { #include "chat/invite_link.h" } @@ -13,6 +14,75 @@ extern "C" { class InviteLinksTest : public QObject { Q_OBJECT private slots: + 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"}) + for (bool outgoing : {false, true}) for (int width : {280, 600}) { + const QByteArray name = QString("%1-%2-%3").arg(type).arg(outgoing).arg(width).toUtf8(); + QTest::newRow(name.constData()) << QString(type) << outgoing << width; + } + } + + void reactionFooter() { + QFETCH(QString, type); QFETCH(bool, outgoing); QFETCH(int, width); + QStandardItemModel model; auto* message = new QStandardItem; + message->setData(type == "text/plain" || type == "poll" ? QString("Example message") : QString::fromUtf8("😀"), MsgTextRole); + message->setData(type, MsgContentTypeRole); message->setData(outgoing, MsgIsOutgoingRole); + message->setData("1001", MsgChannelIdRole); message->setData(QString(64, 'a'), MsgMessageKeyRole); + message->setData("Alice", MsgAuthorRole); message->setData("12:34", MsgTimeRole); + message->setData("00:05", MsgVoiceDurationRole); message->setData("example.dat", MsgFileDisplayNameRole); + message->setData(1, MsgIdRole); message->setData(1, MsgFileDownloadStateRole); + message->setData(QVariantList{QVariantMap{{"id", 7}, {"emoji", QString::fromUtf8("👍")}, {"count", 2}, {"mine", true}}}, MsgReactionsRole); + model.appendRow(message); + ChatView view; MessageDelegate delegate; view.setModel(&model); view.setItemDelegate(&delegate); + view.setSelectionMode(QAbstractItemView::NoSelection); view.resize(width, 480); view.show(); QTest::qWait(10); + int downloads = 0; delegate.setDownloadCallback([&](const QModelIndex&) { downloads++; return true; }); + const auto index = model.index(0, 0); QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; + option.rect = view.visualRect(index); option.rect.setHeight(delegate.sizeHint(option, index).height()); + QSignalSpy requested(&delegate, &MessageDelegate::reactionRequested); + QMap points; QRect heartHits; + /* Ищем области по фактическим событиям, не повторяя вычисления layout. */ + for (int y = 1; y < option.rect.height(); y += 4) for (int x = 1; x < view.viewport()->width(); x += 4) { + const QPoint point(x, y); + QMouseEvent press(QEvent::MouseButtonPress, point, view.viewport()->mapToGlobal(point), Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + QMouseEvent release(QEvent::MouseButtonRelease, point, view.viewport()->mapToGlobal(point), Qt::LeftButton, Qt::NoButton, Qt::NoModifier); + requested.clear(); delegate.editorEvent(&press, &model, option, index); delegate.editorEvent(&release, &model, option, index); + if (!requested.isEmpty()) { + QCOMPARE(requested.count(), 1); const int emoji = requested.first().at(1).toInt(); + if (!points.contains(emoji)) points[emoji] = point; + if (emoji == -1) heartHits = heartHits.isValid() ? heartHits.united(QRect(point, QSize(1, 1))) : QRect(point, QSize(1, 1)); + } + } + QVERIFY(points.contains(-1) && points.contains(7)); downloads = 0; requested.clear(); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, points[-1]); + QCOMPARE(requested.count(), 1); QCOMPARE(requested.last().at(1).toInt(), -1); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, points[7]); + QTest::mouseClick(view.viewport(), Qt::LeftButton, Qt::NoModifier, points[7]); + QCOMPARE(requested.count(), 3); QCOMPARE(requested.last().at(1).toInt(), 7); QCOMPARE(downloads, 0); + QTest::mouseClick(view.viewport(), Qt::RightButton, Qt::NoModifier, points[-1]); QCOMPARE(requested.count(), 3); + QTest::mousePress(view.viewport(), Qt::LeftButton, Qt::NoModifier, points[-1]); + QTest::mouseRelease(view.viewport(), Qt::LeftButton, Qt::NoModifier, points[-1] + QPoint(20, 0)); + QCOMPARE(requested.count(), 3); QCOMPARE(downloads, 0); + + const QColor background(70, 80, 90); QImage image(option.rect.size(), QImage::Format_ARGB32); image.fill(background); + QPainter painter(&image); delegate.paint(&painter, option, index); painter.end(); + QCOMPARE(image.pixelColor(heartHits.center()), background); /* внутри сердечко прозрачно */ + bool whiteContour = false; + for (int y = heartHits.top(); y <= heartHits.bottom(); y++) for (int x = heartHits.left(); x <= heartHits.right(); x++) + if (image.pixelColor(x, y) == QColor(Qt::white)) whiteContour = true; + QVERIFY(whiteContour); + if (type == "text/plain" && !outgoing && width == 600) QVERIFY(image.save("/tmp/utun2-reaction-heart-preview.png")); + } + + void largeEmojiHasNoReactionFooter() { + QStandardItemModel model; auto* message = new QStandardItem; + message->setData(QString::fromUtf8("😀"), MsgTextRole); message->setData("1001", MsgChannelIdRole); model.appendRow(message); + ChatView view; MessageDelegate delegate; view.setModel(&model); view.setItemDelegate(&delegate); view.resize(600, 240); + QStyleOptionViewItem option; option.initFrom(&view); option.widget = &view; + const auto index = model.index(0, 0); const auto size = delegate.sizeHint(option, index); + message->setData(QString(64, 'a'), MsgMessageKeyRole); QCOMPARE(delegate.sizeHint(option, index), size); + } + void messageLinkBoundaries() { QFile fixture(QFINDTESTDATA("message_links_cases.tsv")); QVERIFY(fixture.open(QIODevice::ReadOnly));