From 00ca006d260a54b2dd069b0a46b0bdd0b8c5f7ee Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 7 Oct 2026 01:21:52 +0200 Subject: [PATCH] Render and hit-test reaction footer for every desktop message type --- tools/chatgui/src/messagedelegate.cpp | 153 +++++++++++++------------- tools/chatgui/src/messagedelegate.h | 11 ++ 2 files changed, 90 insertions(+), 74 deletions(-) diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index a8c6a796..80f950f5 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -137,10 +137,21 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, Layout L = calcLayout(option, index); const auto* mouse = static_cast(event); + const QPoint point = mouse->pos() - option.rect.topLeft(); + int reaction = L.reactionHeart.contains(point) ? -1 : -2; + for (const auto& chip : L.reactionChips) if (chip.rect.contains(point)) { reaction = chip.id; break; } + const bool onReaction = mouse->button() == Qt::LeftButton && reaction != -2; const bool onAvatar=mouse->button()==Qt::LeftButton && !L.narrow && L.avatarRect.contains(mouse->pos()-option.rect.topLeft()); if (event->type()==QEvent::MouseButtonPress) { - m_avatarPress=onAvatar ? index : QModelIndex(); m_avatarPressPosition=mouse->pos(); return onAvatar; + m_reactionPress = onReaction ? index : QModelIndex(); m_pressedReaction = reaction; m_reactionPressPosition = mouse->pos(); + m_avatarPress=onAvatar ? index : QModelIndex(); m_avatarPressPosition=mouse->pos(); return onAvatar || onReaction; } + const bool hadReactionPress = m_reactionPress.isValid(); + const bool activateReaction = onReaction && m_reactionPress == index && m_pressedReaction == reaction + && (mouse->pos() - m_reactionPressPosition).manhattanLength() < 5; + m_reactionPress = QPersistentModelIndex(); m_pressedReaction = -2; + if (activateReaction) { emit reactionRequested(index, reaction); return true; } + if (hadReactionPress || onReaction || (L.reactionHeart.isValid() && point.y() >= L.reactionHeart.top())) return true; const bool activateAvatar=onAvatar && m_avatarPress==index && (mouse->pos()-m_avatarPressPosition).manhattanLength()<5; m_avatarPress=QPersistentModelIndex(); if (activateAvatar) { @@ -391,6 +402,29 @@ static int calcMaxLineWidth(const QFont &font, const QString &text, int availW) } MessageDelegate::Layout MessageDelegate::calcLayout( + const QStyleOptionViewItem& option, const QModelIndex& index) const { + Layout layout = calcBodyLayout(option, index); + const QString channel = index.data(MsgChannelIdRole).toString(); + if (layout.emojiOnly || channel.isEmpty() || channel.startsWith("dm:") + || 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; + layout.reactionHeart = QRect(left, y, height, height); + for (const auto& value : index.data(MsgReactionsRole).toList()) { + const auto reaction = value.toMap(); + font.setBold(reaction.value("mine").toBool()); + const int width = qMin(layout.bubbleRect.width(), 32 + QFontMetrics(font).horizontalAdvance(reaction.value("count").toString())); + if (x + width > left + layout.bubbleRect.width()) { x = left; y += height + 4; } + layout.reactionChips.append({QRect(x, y, width, height), reaction.value("id").toInt(), + reaction.value("emoji").toString(), reaction.value("count").toInt(), reaction.value("mine").toBool()}); + x += width + 4; + } + layout.totalHeight = qMax(layout.totalHeight, y + height + kItemGap); + return layout; +} + +MessageDelegate::Layout MessageDelegate::calcBodyLayout( const QStyleOptionViewItem &option, const QModelIndex &index) const { Layout L{}; @@ -897,11 +931,20 @@ static void drawAnimatedEmojiOverlay(QPainter *p, const QFont &font, void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + const Layout layout = calcLayout(option, index); + paintBody(painter, option, index, layout); + if (!layout.reactionHeart.isValid()) return; + painter->save(); painter->translate(option.rect.topLeft()); + painter->setClipRect(QRect(QPoint(0, 0), option.rect.size())); + drawReactions(painter, layout, option); painter->restore(); +} + +void MessageDelegate::paintBody(QPainter* painter, const QStyleOptionViewItem& option, + const QModelIndex& index, const Layout& L) const { painter->save(); painter->translate(option.rect.topLeft()); painter->setClipRect(QRect(QPoint(0, 0), option.rect.size())); - Layout L = calcLayout(option, index); if (L.deliveryRect.isValid()) { QFont font = option.font; font.setPointSize(qMax(1, font.pointSize() - 2)); @@ -1533,80 +1576,42 @@ void MessageDelegate::drawBubble(QPainter *p, const Layout &L, const QStyleOptio p->restore(); } -void MessageDelegate::drawStatusBar(QPainter *p, const Layout &L, - const QModelIndex &index, - const QStyleOptionViewItem &opt) const { - p->save(); - - QRect r = L.statusRect; - - QVariant reactionsVar = index.data(MsgReactionsRole); - QVariantList reactions = reactionsVar.toList(); - - int rx = r.left(); - if (!reactions.isEmpty()) { - for (const QVariant &rv : reactions) { - QVariantMap rm = rv.toMap(); - QString emoji = rm["emoji"].toString(); - int count = rm["count"].toInt(); - QVariant colorsVar = rm["colors"]; - QList colors = colorsVar.value>(); - - if (count < 4 && !colors.isEmpty()) { - int iconSize = 16; - for (int i = colors.size() - 1; i >= 0; --i) { - int cx = rx + i * 11; - p->setPen(Qt::NoPen); - p->setBrush(colors[i]); - p->drawEllipse(cx, r.top() + 1, iconSize, iconSize); - } - rx += colors.size() * 11 + 8; - } else { - LottieIcon *rIcon = nullptr; - if (g_animatedEmojiMap.contains(emoji)) - rIcon = AnimTimer::instance()->icon(g_animatedEmojiMap[emoji]); - - if (rIcon && rIcon->isValid()) { - int iconSz = qMin((int)(20 * 1.3), r.height()); - QImage frame = rIcon->renderFrame(rIcon->currentFrame(), QSize(iconSz, iconSz)); - p->drawImage(rx, r.top() + (r.height() - iconSz) / 2, frame); - rx += iconSz + 2; - } else { - QFont emojiFont = opt.font; - emojiFont.setPointSize(emojiFont.pointSize() - 1); - p->setFont(emojiFont); - p->setPen(opt.palette.text().color()); - QRect emojiRect(rx, r.top(), 20, r.height()); - p->drawText(emojiRect, Qt::AlignLeft | Qt::AlignVCenter, emoji); - rx += 20; - } - - QFont countFont = opt.font; - countFont.setBold(rm.value("mine").toBool()); - countFont.setPointSize(countFont.pointSize() - 2); - p->setFont(countFont); - p->setPen(QColor(0x80, 0x80, 0x80)); - QRect cntRect(rx, r.top(), 24, r.height()); - p->drawText(cntRect, Qt::AlignLeft | Qt::AlignVCenter, - QString::number(count)); - rx = cntRect.right() + 8; - } - } +void MessageDelegate::drawReactions(QPainter* p, const Layout& layout, const QStyleOptionViewItem& option) const { + p->save(); p->setRenderHint(QPainter::Antialiasing); + const QPointF origin = QRectF(layout.reactionHeart).center() - QPointF(12, 12); + QPainterPath heart; + heart.moveTo(origin + QPointF(12, 21)); + heart.cubicTo(origin + QPointF(10.5, 19.5), origin + QPointF(3, 14.2), origin + QPointF(3, 8.5)); + heart.cubicTo(origin + QPointF(3, 3), origin + QPointF(9, 2), origin + QPointF(12, 6)); + heart.cubicTo(origin + QPointF(15, 2), origin + QPointF(21, 3), origin + QPointF(21, 8.5)); + heart.cubicTo(origin + QPointF(21, 14.2), origin + QPointF(13.5, 19.5), origin + QPointF(12, 21)); + heart.closeSubpath(); + p->setBrush(Qt::NoBrush); + p->setPen(QPen(QColor(0, 0, 0, 153), 1.8 + 2.0 / p->device()->devicePixelRatioF(), Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); + p->drawPath(heart); + p->setPen(QPen(Qt::white, 1.8, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); p->drawPath(heart); + for (const auto& chip : layout.reactionChips) { + p->setPen(Qt::NoPen); p->setBrush(chip.mine ? QColor(51, 144, 236, 100) : QColor(0, 0, 0, 40)); + p->drawRoundedRect(chip.rect, 9, 9); + const QRect iconRect(chip.rect.left() + 5, chip.rect.top() + (chip.rect.height() - 20) / 2, 20, 20); + LottieIcon* icon = g_animatedEmojiMap.contains(chip.emoji) ? AnimTimer::instance()->icon(g_animatedEmojiMap[chip.emoji]) : nullptr; + QFont font = option.font; font.setPointSize(qMax(1, font.pointSize() - 1)); p->setFont(font); + if (icon && icon->isValid()) p->drawImage(iconRect, icon->renderFrame(icon->currentFrame(), QSize(20, 20))); + else { p->setPen(option.palette.text().color()); p->drawText(iconRect, Qt::AlignCenter, chip.emoji); } + font.setBold(chip.mine); p->setFont(font); p->setPen(option.palette.text().color()); + p->drawText(chip.rect.adjusted(27, 0, -5, 0), Qt::AlignLeft | Qt::AlignVCenter, QString::number(chip.count)); } + p->restore(); +} - QFont smallFont = opt.font; - smallFont.setPointSize(smallFont.pointSize() - 2); - p->setFont(smallFont); - p->setPen(QColor(0x80, 0x80, 0x80)); - - QString statusText; - if (index.data(MsgEditedRole).toBool()) { - statusText = QCoreApplication::translate("ChatGui", "edited "); - } - statusText += index.data(MsgTimeRole).toString(); - p->drawText(QRect(r.left(), r.top(), r.width(), r.height()), - Qt::AlignRight | Qt::AlignVCenter, statusText); - +void MessageDelegate::drawStatusBar(QPainter* p, const Layout& layout, const QModelIndex& index, + const QStyleOptionViewItem& option) const { + p->save(); + QFont font = option.font; font.setPointSize(qMax(1, font.pointSize() - 2)); p->setFont(font); + p->setPen(QColor(128, 128, 128)); + QString text = index.data(MsgEditedRole).toBool() ? QCoreApplication::translate("ChatGui", "edited ") : QString(); + text += index.data(MsgTimeRole).toString(); + p->drawText(layout.statusRect, Qt::AlignRight | Qt::AlignVCenter, text); p->restore(); } diff --git a/tools/chatgui/src/messagedelegate.h b/tools/chatgui/src/messagedelegate.h index cab3d54f..e7d10350 100644 --- a/tools/chatgui/src/messagedelegate.h +++ b/tools/chatgui/src/messagedelegate.h @@ -5,6 +5,7 @@ #include #include +#include #include enum MessageDataRole { @@ -73,8 +74,10 @@ public: void setDownloadCallback(std::function cb) { m_downloadCb = std::move(cb); } signals: void avatarRequested(const QString& channel, quint64 owner); + void reactionRequested(const QModelIndex& index, int emoji); /* -1 открывает выбор реакции */ private: + struct ReactionChip { QRect rect; int id; QString emoji; int count; bool mine; }; struct Layout { int viewWidth; int bubbleWidth; @@ -112,16 +115,24 @@ private: QRect videoDurBadgeRect; QRect imageThumbRect; QRect imageBadgeRect; + QRect reactionHeart; + QVector reactionChips; }; std::function m_videoOpenCb; std::function m_imageOpenCb; QPersistentModelIndex m_avatarPress; QPoint m_avatarPressPosition; + QPersistentModelIndex m_reactionPress; + QPoint m_reactionPressPosition; + int m_pressedReaction = -2; std::function m_downloadCb; Layout calcLayout(const QStyleOptionViewItem &option, const QModelIndex &index) const; + Layout calcBodyLayout(const QStyleOptionViewItem& option, const QModelIndex& index) const; + void paintBody(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index, const Layout& layout) const; + void drawReactions(QPainter* painter, const Layout& layout, const QStyleOptionViewItem& option) const; void drawQuote(QPainter *p, const Layout &L, const QModelIndex &index, const QStyleOptionViewItem &opt) const; void drawBubble(QPainter *p, const Layout &L, const QStyleOptionViewItem &opt) const;