From e8920fe00edf152bb3acd676f1f6b9227abbce1f Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 24 Jul 2026 15:55:34 +0300 Subject: [PATCH] chatgui: MsgIsOutgoingRole support for outgoing message alignment --- tools/chatgui/src/emoji.cpp | 39 ++++++ tools/chatgui/src/emoji.h | 2 + tools/chatgui/src/messagedelegate.cpp | 181 +++++++++++++++++++++----- tools/chatgui/src/messagedelegate.h | 5 + tools/chatgui/src/messagelist.cpp | 4 + 5 files changed, 198 insertions(+), 33 deletions(-) diff --git a/tools/chatgui/src/emoji.cpp b/tools/chatgui/src/emoji.cpp index e9c36822..8e71bd34 100644 --- a/tools/chatgui/src/emoji.cpp +++ b/tools/chatgui/src/emoji.cpp @@ -139,3 +139,42 @@ QVector builtinEmojiSet() { {"Animations", animations()}, }; } + +#include + +static QSet buildAllEmojiSet() { + QSet s; + for (const auto &cat : builtinEmojiSet()) + for (const auto &e : cat.items) + s.insert(e.unicode); + return s; +} + +int detectEmojiOnly(const QString &text) { + static const QSet emojiSet = buildAllEmojiSet(); + + QString t = text.trimmed(); + if (t.isEmpty()) return 0; + + int pos = 0; + int count = 0; + while (pos < t.length()) { + const QChar *ch = t.constData() + pos; + if (ch->isSpace()) { ++pos; continue; } + + bool matched = false; + for (const QString &e : emojiSet) { + if (e.length() <= t.length() - pos + && QStringView(t).mid(pos, e.length()) == e) + { + pos += e.length(); + ++count; + matched = true; + break; + } + } + if (!matched) return 0; + if (count > 3) return 0; + } + return (count >= 1 && count <= 3) ? count : 0; +} diff --git a/tools/chatgui/src/emoji.h b/tools/chatgui/src/emoji.h index 89d2ed1e..046e71ef 100644 --- a/tools/chatgui/src/emoji.h +++ b/tools/chatgui/src/emoji.h @@ -25,3 +25,5 @@ struct EmojiCategory { QVector builtinEmojiSet(); extern QHash g_animatedEmojiMap; + +int detectEmojiOnly(const QString &text); diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index e060b701..e408148f 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -65,6 +65,33 @@ MessageDelegate::Layout MessageDelegate::calcLayout( bool hasQuote = index.data(MsgHasQuoteRole).toBool(); QString text = index.data(MsgTextRole).toString(); + L.isOutgoing = index.data(MsgIsOutgoingRole).toBool(); + + L.emojiCount = detectEmojiOnly(text); + L.emojiOnly = (L.emojiCount >= 1); + + if (L.emojiOnly) { + QFontMetrics fm(option.font); + int lineH = fm.lineSpacing(); + + int availW = L.viewWidth - kMarginH - kAvatar - kGap - kMarginH; + bool narrow = (availW < 400); + int contentEdge = !L.isOutgoing + ? (narrow ? kMarginH : (kMarginH + kAvatar + kGap)) + : L.viewWidth - (narrow ? kMarginH : (kMarginH + kAvatar + kGap)); + + int eSize = (L.emojiCount == 1) ? lineH * 4 : lineH * 2; + int emojiGap = lineH / 2; + int totalW = L.emojiCount * eSize + (L.emojiCount - 1) * emojiGap; + int x = !L.isOutgoing + ? (contentEdge + kPadH) + : (contentEdge - kPadH - totalW); + + L.emojiArea = QRect(x, kGap * 2, totalW, eSize); + L.totalHeight = eSize + kGap * 2 + kItemGap; + L.bubbleRect = QRect(); + return L; + } QString qText = hasQuote ? index.data(MsgQuoteTextRole).toString() : QString(); int availW = L.viewWidth - kMarginH - kAvatar - kGap - kMarginH; @@ -77,14 +104,18 @@ MessageDelegate::Layout MessageDelegate::calcLayout( QFontMetrics fm(option.font); int contentW = calcMaxLineWidth(option.font, text, limitW); - QString author = index.data(MsgAuthorRole).toString(); - QFont boldFm = option.font; - boldFm.setBold(true); - int authorW = QFontMetrics(boldFm).horizontalAdvance(author); - contentW = qMax(contentW, authorW); + if (!L.isOutgoing) { + QString author = index.data(MsgAuthorRole).toString(); + QFont boldFm = option.font; + boldFm.setBold(true); + int authorW = QFontMetrics(boldFm).horizontalAdvance(author); + contentW = qMax(contentW, authorW); + } if (hasQuote) { QString qAuthor = index.data(MsgQuoteAuthorRole).toString(); + QFont boldFm = option.font; + boldFm.setBold(true); int qAuthorW = QFontMetrics(boldFm).horizontalAdvance(qAuthor); contentW = qMax(contentW, qAuthorW + kQuoteBarW + kQuotePadH + 4); int qTextW = qMin(contentW + 8, limitW); @@ -107,23 +138,39 @@ MessageDelegate::Layout MessageDelegate::calcLayout( int textH = measureTextHeight(option.font, text, textW); int authorH = fm.height(); - int bubbleH = kPadTop + authorH + 2 + (hasQuote ? qH + kQuoteGap : 0) + textH + kStatusH + kPadBot; + int bubbleH = L.isOutgoing + ? kPadTop + (hasQuote ? qH + kQuoteGap : 0) + textH + kStatusH + kPadBot + : kPadTop + authorH + 2 + (hasQuote ? qH + kQuoteGap : 0) + textH + kStatusH + kPadBot; L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap; - int contentLeft = L.narrow ? kMarginH : (kMarginH + kAvatar + kGap); + int contentEdge = L.isOutgoing + ? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap)) + : (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap)); - L.bubbleRect = QRect(contentLeft, 0, bubbleW, bubbleH); + if (!L.isOutgoing) + L.bubbleRect = QRect(contentEdge, 0, bubbleW, bubbleH); + else + L.bubbleRect = QRect(contentEdge - bubbleW, 0, bubbleW, bubbleH); int contentTop = kPadTop; - L.authorRect = QRect(contentLeft + kPadH, contentTop, textW, authorH); - contentTop += authorH + 2; + if (!L.isOutgoing) { + L.authorRect = QRect(contentEdge + kPadH, contentTop, textW, authorH); + contentTop += authorH + 2; + } if (hasQuote) { - L.quoteRect = QRect(contentLeft + kPadH, contentTop, bubbleW - 2 * kPadH, qH); - L.quoteBarRect = QRect(L.quoteRect.left(), L.quoteRect.top(), - kQuoteBarW, qH); - int tx = L.quoteBarRect.right() + kQuotePadH; + int qX = !L.isOutgoing + ? (contentEdge + kPadH) + : (contentEdge - kPadH - (bubbleW - 2 * kPadH)); + L.quoteRect = QRect(qX, contentTop, bubbleW - 2 * kPadH, qH); + if (!L.isOutgoing) + L.quoteBarRect = QRect(L.quoteRect.left(), L.quoteRect.top(), kQuoteBarW, qH); + else + L.quoteBarRect = QRect(L.quoteRect.right() - kQuoteBarW, L.quoteRect.top(), kQuoteBarW, qH); + int tx = !L.isOutgoing + ? (L.quoteBarRect.right() + kQuotePadH) + : (L.quoteRect.left() + kQuotePadH); L.quoteAuthorRect = QRect(tx, L.quoteRect.top() + kQuotePadV, L.quoteRect.width() - kQuoteBarW - kQuotePadH, QFontMetrics(option.font).height()); @@ -134,15 +181,18 @@ MessageDelegate::Layout MessageDelegate::calcLayout( contentTop += qH + kQuoteGap; } - L.textRect = QRect(contentLeft + kPadH, contentTop, - textW, textH); - L.statusRect = QRect(contentLeft + kPadH, - contentTop + textH + 2, - textW, kStatusH); + int txRect = !L.isOutgoing + ? (contentEdge + kPadH) + : (contentEdge - kPadH - textW); + L.textRect = QRect(txRect, contentTop, textW, textH); + L.statusRect = QRect(txRect, contentTop + textH + 2, textW, kStatusH); if (!L.narrow) { int avatarY = L.bubbleRect.bottom() - kAvatar; - L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar); + if (!L.isOutgoing) + L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar); + else + L.avatarRect = QRect(L.viewWidth - kMarginH - kAvatar, avatarY, kAvatar, kAvatar); } return L; @@ -349,6 +399,56 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio Layout L = calcLayout(option, index); + if (L.emojiOnly) { + QString msgText = index.data(MsgTextRole).toString(); + QString trimmed = msgText.trimmed(); + QFontMetrics fm(option.font); + int lineH = fm.lineSpacing(); + int eSize = (L.emojiCount == 1) ? lineH * 4 : lineH * 2; + + int x = L.emojiArea.left(); + int y = L.emojiArea.top(); + int pos = 0; + while (pos < trimmed.length()) { + if (trimmed.at(pos).isSpace()) { ++pos; continue; } + QString emojiStr; + for (const auto &cat : builtinEmojiSet()) + for (const auto &e : cat.items) { + if (e.unicode.length() > trimmed.length() - pos) continue; + if (QStringView(trimmed).mid(pos, e.unicode.length()) == e.unicode) { + emojiStr = e.unicode; + pos += e.unicode.length(); + break; + } + } + if (emojiStr.isEmpty()) { ++pos; continue; } + + bool animated = g_animatedEmojiMap.contains(emojiStr); + if (animated) { + LottieIcon *icon = AnimTimer::instance()->icon(g_animatedEmojiMap[emojiStr]); + if (icon && icon->isValid()) { + QImage frame = icon->renderFrame(icon->currentFrame(), QSize(eSize, eSize)); + painter->drawImage(x, y, frame); + } else { + QFont big = option.font; + big.setPointSize((int)(big.pointSize() * (L.emojiCount == 1 ? 4.0 : 2.0))); + painter->setFont(big); + painter->setPen(option.palette.text().color()); + painter->drawText(QRect(x, y, eSize, eSize), Qt::AlignCenter, emojiStr); + } + } else { + QFont big = option.font; + big.setPointSize((int)(big.pointSize() * (L.emojiCount == 1 ? 4.0 : 2.0))); + painter->setFont(big); + painter->setPen(option.palette.text().color()); + painter->drawText(QRect(x, y, eSize, eSize), Qt::AlignCenter, emojiStr); + } + x += eSize + lineH / 2; + } + painter->restore(); + return; + } + auto *cv = qobject_cast(option.widget); bool singleTextSel = false; bool multiRowSel = false; @@ -379,12 +479,14 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio if (!L.narrow) drawAvatar(painter, L, index); - QFont authorFont = option.font; - authorFont.setBold(true); - painter->setFont(authorFont); - painter->setPen(QColor(0x33, 0x90, 0xEC)); - painter->drawText(L.authorRect, Qt::AlignLeft | Qt::AlignVCenter, - index.data(MsgAuthorRole).toString()); + if (!L.isOutgoing) { + QFont authorFont = option.font; + authorFont.setBold(true); + painter->setFont(authorFont); + painter->setPen(QColor(0x33, 0x90, 0xEC)); + painter->drawText(L.authorRect, Qt::AlignLeft | Qt::AlignVCenter, + index.data(MsgAuthorRole).toString()); + } painter->setPen(option.palette.text().color()); QFont textFont = option.font; @@ -458,17 +560,30 @@ void MessageDelegate::drawBubble(QPainter *p, const Layout &L, const QStyleOptio body.addRoundedRect(b, r, r); QPainterPath tail; - qreal tx = b.left(); - qreal ty = b.bottom(); - tail.moveTo(tx, ty - r); - tail.lineTo(tx - t, ty - t / 2.0); - tail.lineTo(tx + r, ty); + if (!L.isOutgoing) { + qreal tx = b.left(); + qreal ty = b.bottom(); + tail.moveTo(tx, ty - r); + tail.lineTo(tx - t, ty - t / 2.0); + tail.lineTo(tx + r, ty); + } else { + qreal tx = b.right(); + qreal ty = b.bottom(); + tail.moveTo(tx, ty - r); + tail.lineTo(tx + t, ty - t / 2.0); + tail.lineTo(tx - r, ty); + } tail.closeSubpath(); QPainterPath combined = body.united(tail); - QColor base = opt.palette.window().color(); - QColor bubbleFill = base.lighter(135); + QColor bubbleFill; + if (!L.isOutgoing) { + QColor base = opt.palette.window().color(); + bubbleFill = base.lighter(135); + } else { + bubbleFill = QColor(0xE1, 0xF5, 0xE1); + } p->setPen(Qt::NoPen); p->setBrush(bubbleFill); p->drawPath(combined); diff --git a/tools/chatgui/src/messagedelegate.h b/tools/chatgui/src/messagedelegate.h index f10dcd5b..f77dd073 100644 --- a/tools/chatgui/src/messagedelegate.h +++ b/tools/chatgui/src/messagedelegate.h @@ -13,6 +13,7 @@ enum MessageDataRole { MsgQuoteTextRole = Qt::UserRole + 8, MsgReactionsRole = Qt::UserRole + 9, MsgIdRole = Qt::UserRole + 10, + MsgIsOutgoingRole = Qt::UserRole + 11, }; class MessageDelegate : public QStyledItemDelegate { @@ -40,6 +41,10 @@ private: QRect statusRect; int totalHeight; bool narrow = false; + bool emojiOnly = false; + int emojiCount = 0; + QRect emojiArea; + bool isOutgoing = false; }; Layout calcLayout(const QStyleOptionViewItem &option, diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index 478b4034..95a4fd12 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -219,6 +219,7 @@ void MessageList::loadChannel(const QString& channelId) { setMsg(item, author, QString::fromUtf8(m.data), time, avatar, false, false, QString(), QString(), reacts); item->setData(m.id, MsgIdRole); + item->setData(m.isOutgoing ? 1 : 0, MsgIsOutgoingRole); m_model->appendRow(item); } @@ -278,6 +279,7 @@ void MessageList::refresh() { setMsg(item, author, QString::fromUtf8(m.data), time, avatar, false, false, QString(), QString(), QVariantList{}); item->setData(m.id, MsgIdRole); + item->setData(m.isOutgoing ? 1 : 0, MsgIsOutgoingRole); m_model->appendRow(item); while (m_model->rowCount() > 100) m_model->removeRow(0); @@ -308,6 +310,8 @@ void MessageList::addMessage(const QString& channelId, quint64 authorNodeId, auto *item = new QStandardItem(); setMsg(item, author, QString::fromUtf8(content), time, avatar, false, false, QString(), QString(), QVariantList{}); + bool isSelf = m_db && (authorNodeId == m_db->myNodeId()); + item->setData(isSelf ? 1 : 0, MsgIsOutgoingRole); m_model->appendRow(item); if (m_model->rowCount() > 50)