Browse Source

Render clickable compact replies for text and media messages

master
evgeny 4 days ago
parent
commit
eebef4d14f
  1. 65
      tools/chatgui/src/messagedelegate.cpp
  2. 5
      tools/chatgui/src/messagedelegate.h

65
tools/chatgui/src/messagedelegate.cpp

@ -139,21 +139,27 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
const auto* mouse = static_cast<QMouseEvent*>(event);
if (mouse->button() != Qt::LeftButton) return false;
const QPoint point = mouse->pos() - option.rect.topLeft();
const bool onQuote = L.quoteRect.contains(point);
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());
const bool onPoll = index.data(MsgContentTypeRole).toString() == "poll"
const bool onPoll = !onQuote && index.data(MsgContentTypeRole).toString() == "poll"
&& !index.data(MsgChannelIdRole).toString().startsWith("dm:")
&& (L.bubbleRect.contains(point) || L.emojiArea.contains(point))
&& (!L.reactionHeart.isValid() || point.y() < L.reactionHeart.top());
if (event->type()==QEvent::MouseButtonPress) {
m_quotePress = onQuote ? index : QModelIndex(); m_quotePressPosition = mouse->pos();
m_reactionPress = onReaction ? index : QModelIndex(); m_pressedReaction = reaction; m_reactionPressPosition = mouse->pos();
m_avatarPress=onAvatar ? index : QModelIndex(); m_avatarPressPosition=mouse->pos();
m_pollPress = onPoll ? index : QModelIndex(); m_pollPressPosition = mouse->pos();
return onAvatar || onReaction || onPoll;
return onQuote || onAvatar || onReaction || onPoll;
}
const auto* view = qobject_cast<const ChatView*>(option.widget);
const bool hadQuotePress = m_quotePress.isValid();
const bool activateQuote = onQuote && m_quotePress == index
&& (mouse->pos() - m_quotePressPosition).manhattanLength() < 5 && (!view || !view->isSelecting());
m_quotePress = QPersistentModelIndex();
const bool activatePoll = onPoll && m_pollPress == index && (mouse->pos() - m_pollPressPosition).manhattanLength() < 5
&& (!view || !view->textSelection().active);
m_pollPress = QPersistentModelIndex();
@ -163,6 +169,8 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
const bool activateReaction = onReaction && m_reactionPress == index && m_pressedReaction == reaction
&& (mouse->pos() - m_reactionPressPosition).manhattanLength() < 5;
m_reactionPress = QPersistentModelIndex(); m_pressedReaction = -2;
if (activateQuote) { emit replyRequested(index.data(MsgReplyToRole).toString()); return true; }
if (hadQuotePress || onQuote) return true;
if (activateReaction) { emit reactionRequested(index, reaction); return true; }
if (hadReactionPress || onReaction) return true;
if (activateAvatar) {
@ -428,6 +436,24 @@ 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);
// Медиа и крупные emoji получают ту же цитату над своим содержимым.
if (index.data(MsgHasQuoteRole).toBool() && !layout.quoteRect.isValid()) {
const int line = QFontMetrics(option.font).height(), height = 2 * line + 2 * kQuotePadV + 2;
const QRect body = layout.emojiOnly ? layout.emojiArea : layout.bubbleRect;
const int width = qMin(qMax(200, body.width()), qMax(40, layout.viewWidth - 2 * kMarginH));
const int x = layout.isOutgoing ? body.right() - width + 1 : body.left();
layout.quoteRect = QRect(qMax(kMarginH, x), 0, width, height);
layout.quoteBarRect = QRect(layout.quoteRect.left(), 0, kQuoteBarW, height);
layout.quoteAuthorRect = QRect(layout.quoteRect.left() + kQuotePadH + kQuoteBarW, kQuotePadV,
width - 2 * kQuotePadH - kQuoteBarW, line);
layout.quoteTextRect = layout.quoteAuthorRect.translated(0, line + 2);
for (QRect* rect : {&layout.bubbleRect, &layout.avatarRect, &layout.authorRect, &layout.textRect, &layout.statusRect,
&layout.deliveryRect, &layout.emojiArea, &layout.voicePlayBtnRect, &layout.voiceWaveformRect,
&layout.voiceDurationRect, &layout.voiceTimeRect, &layout.mediaBarRect, &layout.progressBarRect,
&layout.videoThumbRect, &layout.videoPlayBtnRect, &layout.videoDurBadgeRect, &layout.imageThumbRect,
&layout.imageBadgeRect}) if (rect->isValid()) rect->translate(0, height + kQuoteGap);
layout.totalHeight += height + kQuoteGap;
}
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;
@ -458,7 +484,7 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
}
layout.reactionTime = QRect(layout.bubbleRect.right() - kPadH - timeWidth + 1, y, timeWidth, height);
layout.bubbleRect.setBottom(y + height + kPadBot - 1);
layout.totalHeight = qMax(layout.bubbleRect.height(), kAvatar) + kItemGap;
layout.totalHeight = qMax(layout.bubbleRect.bottom() + 1, kAvatar) + kItemGap;
if (layout.avatarRect.isValid()) layout.avatarRect.moveTop(qMax(0, layout.bubbleRect.bottom() + 1 - kAvatar));
return layout;
}
@ -705,8 +731,7 @@ MessageDelegate::Layout MessageDelegate::calcBodyLayout(
int qH = 0;
if (hasQuote) {
int qTextW = bubbleW - 2 * kPadH - kQuoteBarW - kQuotePadH;
int qTextH = measureTextHeight(option.font, qText, qTextW);
int qTextH = fm.height();
int qOneLineH = fm.height();
qH = kQuotePadV + qOneLineH + (qText.isEmpty() ? 0 : 2 + qTextH) + kQuotePadV;
}
@ -973,10 +998,15 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
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();
if (layout.quoteRect.isValid()) drawQuote(painter, layout, index, option);
if (layout.reactionHeart.isValid()) drawReactions(painter, layout, option);
if (index.data(MsgHighlightRole).toBool()) {
QColor color = option.palette.highlight().color(); color.setAlpha(55);
painter->fillRect(QRect(QPoint(0, 0), option.rect.size()), color);
}
painter->restore();
}
void MessageDelegate::paintBody(QPainter* painter, const QStyleOptionViewItem& option,
@ -1388,7 +1418,7 @@ void MessageDelegate::paintBody(QPainter* painter, const QStyleOptionViewItem& o
iconFont.setPointSize(iconFont.pointSize() + 6);
painter->setFont(iconFont);
painter->setPen(option.palette.text().color());
QRect iconRect(L.bubbleRect.left() + kPadH, kPadTop, 24, 24);
QRect iconRect(L.bubbleRect.left() + kPadH, L.bubbleRect.top() + kPadTop, 24, 24);
if (dlState == 2)
painter->drawText(iconRect, Qt::AlignCenter, QString::fromUtf8("\xE2\x9C\x85"));
else
@ -1457,10 +1487,6 @@ void MessageDelegate::paintBody(QPainter* painter, const QStyleOptionViewItem& o
painter->restore();
}
if (index.data(MsgHasQuoteRole).toBool()) {
drawQuote(painter, L, index, option);
}
if (!L.narrow)
drawAvatar(painter, L, index);
@ -1547,24 +1573,25 @@ void MessageDelegate::drawQuote(QPainter *p, const Layout &L,
p->save();
p->setPen(Qt::NoPen);
p->setBrush(QColor(0xFF, 0xEE, 0xEE));
QColor background = opt.palette.highlight().color(); background.setAlpha(28);
p->setBrush(background);
p->drawRoundedRect(L.quoteRect, 5, 5);
p->setPen(Qt::NoPen);
p->setBrush(QColor(0xE0, 0x40, 0x40));
p->setBrush(opt.palette.highlight());
p->drawRect(L.quoteBarRect);
QFont boldFont = opt.font;
boldFont.setBold(true);
p->setFont(boldFont);
p->setPen(QColor(0xE0, 0x40, 0x40));
p->setPen(opt.palette.text().color());
p->drawText(L.quoteAuthorRect, Qt::AlignLeft | Qt::AlignVCenter,
index.data(MsgQuoteAuthorRole).toString());
QFontMetrics(boldFont).elidedText(index.data(MsgQuoteAuthorRole).toString(), Qt::ElideRight, L.quoteAuthorRect.width()));
p->setFont(opt.font);
p->setPen(QColor(0x99, 0x55, 0x55));
p->drawText(L.quoteTextRect, Qt::AlignLeft | Qt::TextWordWrap,
index.data(MsgQuoteTextRole).toString());
p->setPen(opt.palette.text().color());
p->drawText(L.quoteTextRect, Qt::AlignLeft | Qt::AlignVCenter,
QFontMetrics(opt.font).elidedText(index.data(MsgQuoteTextRole).toString(), Qt::ElideRight, L.quoteTextRect.width()));
p->restore();
}

5
tools/chatgui/src/messagedelegate.h

@ -48,6 +48,8 @@ enum MessageDataRole {
MsgInviteLinkRole = Qt::UserRole + 37, /* полная ссылка; MsgTextRole содержит локализованный заголовок и ссылку */
MsgMessageKeyRole = Qt::UserRole + 38,
MsgRawTimestampRole = Qt::UserRole + 39,
MsgReplyToRole = Qt::UserRole + 40,
MsgHighlightRole = Qt::UserRole + 41,
};
class MessageDelegate : public QStyledItemDelegate {
@ -76,6 +78,7 @@ signals:
void avatarRequested(const QString& channel, quint64 owner);
void reactionRequested(const QModelIndex& index, int emoji); /* -1 открывает выбор реакции */
void pollRequested(const QString& messageId);
void replyRequested(const QString& messageId);
private:
struct ReactionChip { QRect rect; int id; QString emoji; int count; bool mine; };
@ -131,6 +134,8 @@ private:
int m_pressedReaction = -2;
QPersistentModelIndex m_pollPress;
QPoint m_pollPressPosition;
QPersistentModelIndex m_quotePress;
QPoint m_quotePressPosition;
std::function<bool(const QModelIndex&)> m_downloadCb;
Layout calcLayout(const QStyleOptionViewItem &option,

Loading…
Cancel
Save