How Telegram Desktop Renders Chat Messages with Reactions and Replies in history_view_message.cpp
Telegram Desktop renders chat messages with reactions and replies by first calculating geometry in performCountOptimalSize(), then painting components in a strict order inside Message::draw()—replies first, followed by text and media, with reactions either embedded inside or drawn beneath the bubble depending on the message layout.
The history_view_message.cpp file in the telegramdesktop/tdesktop repository contains the core rendering logic for individual chat items. When a message contains quoted replies or emoji reactions, the HistoryView::Message class coordinates multiple sub-components—including HistoryView::Reactions and HistoryView::Reply—to composite the final visual output. Understanding this pipeline requires examining both the layout calculation phase and the actual painting routines that execute during every frame refresh.
Layout Calculation: Positioning Reactions and Replies
Before any pixels are drawn, Message::performCountOptimalSize() (approximately lines 1200–1300) determines the spatial relationships between the bubble, content, and interactive elements. This method decides whether reactions appear inside or outside the bubble and reserves vertical space for reply metadata.
Embedding reactions depends on the boolean result of embedReactionsInBubble():
const auto reactionsInBubble = _reactions && embedReactionsInBubble();
if (reactionsInBubble) {
const auto reactionsMaxWidth = st::msgPadding.left()
+ _reactions->maxWidth()
+ st::msgPadding.right();
accumulate_max(maxWidth, std::min(st::msgMaxWidth, reactionsMaxWidth));
}
When reactionsInBubble is false (the default for standard messages), the reaction block height is subtracted from the main geometry and painted separately below the bubble.
Reply blocks are handled through component flags:
if (replyData && !_hideReply) {
AddComponents(Reply::Bit());
} else {
RemoveComponents(Reply::Bit());
}
Enabling the Reply component reserves reply->height() pixels above the main text area, ensuring the quoted message preview renders before the message body.
The Rendering Pipeline in Message::draw()
The Message::draw() method (lines 1080–1150) serves as the orchestration point, executing painting operations in a specific Z-order to ensure proper layering. The method retrieves the message geometry via countGeometry(), then proceeds through distinct rendering phases.
The general execution flow follows this sequence:
- Paint reactions below the bubble (if not embedded)
- Paint the bubble background via
Ui::PaintBubble - Paint the reply block via
paintReplyInfo() - Paint text and media content
Rendering Replies with paintReplyInfo()
Reply information renders as a distinct quoted block positioned at the top of the message content area. The Message::paintReplyInfo() method (lines 2070–2075) delegates to the Reply component:
void Message::paintReplyInfo(Painter &p,
QRect &trect,
const PaintContext &context) const {
if (const auto reply = Get<Reply>()) {
reply->paint(p, this, context,
trect.x(), trect.y(), trect.width(), true);
trect.setY(trect.y() + reply->height());
}
}
The Reply component—implemented in history_view_reply.cpp—draws a rounded rectangle containing the original sender's name, a preview of the quoted text, and a clickable "reply-to" link. By advancing trect.setY() after painting, the method ensures subsequent content (text and media) begins below the reply block.
Rendering Reactions Below the Bubble
For the default non-embedded case, reactions are painted after the main bubble but appear visually beneath it. The code (lines 1090–1120) calculates positioning and adjusts the available content rectangle:
if (_reactions && !reactionsInBubble) {
const auto reactionsHeight = st::mediaInBubbleSkip + _reactions->height();
const auto reactionsLeft = (!bubble && mediaDisplayed)
? media->contentRectForReactions().x()
: 0;
g.setHeight(g.height() - reactionsHeight);
const auto reactionsPosition = QPoint(
reactionsLeft + g.left(),
g.top() + g.height() + st::mediaInBubbleSkip);
p.translate(reactionsPosition);
_reactions->paint(p, context, g.width(),
context.clip.translated(-reactionsPosition));
p.translate(-reactionsPosition);
}
This logic shrinks the message rectangle g to prevent text overlap, then translates the painter to the calculated position before invoking _reactions->paint().
Embedding Reactions Inside the Bubble
When embedReactionsInBubble() returns true, reactions render at the bottom of the bubble's interior (lines 1160–1175):
if (reactionsInBubble) {
const auto reactionsTop = (reactionsInBubble && !_viewButton)
? st::mediaInBubbleSkip
: 0;
const auto reactionsHeight = reactionsTop + _reactions->height();
trect.setHeight(trect.height() - reactionsHeight);
const auto reactionsPosition = QPoint(
trect.left(),
trect.top() + trect.height() + reactionsTop);
p.translate(reactionsPosition);
_reactions->paint(p, context, g.width(),
context.clip.translated(-reactionsPosition));
p.translate(-reactionsPosition);
}
Here, trect represents the inner content area of the bubble. The code reserves space at the bottom, then paints the reaction row within the bubble boundaries.
The Reactions and Reply Components
The actual drawing of emoji reactions resides in history_view_reactions.cpp, not within the main message file. The Reactions::paint() method iterates through stored reaction data to render individual emoji icons and count labels:
void Reactions::paint(Painter &p,
const PaintContext &context,
int width,
const QRect &clip) const {
for (const auto &r : _list) {
r.icon.paint(p, x, y, width);
p.drawText(x + iconWidth, y, countString);
if (r.ripple) r.ripple->paint(p, ...);
}
}
Each Reaction object contains the emoji graphic (or custom emoji), the user count, and an optional ripple animation state. Similarly, the Reply component in history_view_reply.cpp encapsulates all logic for drawing quoted message previews, including text elision and author name styling.
Handling User Interactions and Ripples
Both reactions and reply blocks support press animations via the Ui::RippleAnimation system. When a user presses a reaction, the event propagates through Message::clickHandlerPressedChanged() (lines 1095–1105), which forwards to the reactions component:
// Inside Message::clickHandlerPressedChanged
if (_reactions) {
_reactions->clickHandlerPressedChanged(...);
}
For reply blocks, Message::toggleReplyRipple() (lines 2590–2599) explicitly manages ripple state:
void Message::toggleReplyRipple() {
if (const auto reply = Get<Reply>()) {
reply->toggleRipple(...);
}
}
These ripples overlay the painted content during subsequent frame renders, providing visual feedback without altering the underlying message geometry.
Summary
- Layout precedes painting:
performCountOptimalSize()determines whether reactions fit inside the bubble or require space below it, while theReplycomponent reserves vertical space for quoted content. - Strict rendering order:
Message::draw()executes in Z-order sequence—reactions below (if applicable), bubble background, reply block, then text and media. - Component architecture: Actual drawing logic is delegated to specialized classes in
history_view_reactions.cppandhistory_view_reply.cpp, keepinghistory_view_message.cppfocused on orchestration. - Interactive feedback: Both reactions and replies utilize
Ui::RippleAnimationfor press effects, managed through dedicated toggle methods in theMessageclass.
Frequently Asked Questions
How does Telegram Desktop decide whether to put reactions inside or below the message bubble?
The decision occurs in performCountOptimalSize() based on the embedReactionsInBubble() method. When this returns true, the layout reserves space within the bubble's bottom padding and the reactions render inside. By default, most messages return false, causing the geometry calculator to subtract reaction height from the main rectangle and paint the reaction row beneath the bubble with additional spacing (st::mediaInBubbleSkip).
What is the exact painting order for a message containing both a reply and reactions?
According to the source in Message::draw(), the painting order is: (1) reactions below the bubble (if not embedded), (2) bubble background and outline, (3) reply block via paintReplyInfo(), (4) main message text and media, and (5) embedded reactions (if enabled). The reply block always precedes text rendering because paintReplyInfo() advances the text rectangle's Y-coordinate before returning control to the main draw loop.
Where is the actual code that draws the emoji reaction icons?
The emoji rendering implementation lives in history_view_reactions.cpp, specifically within the Reactions::paint() method. This component receives a Painter object and the available width from Message::draw(), then iterates through its internal _list of reaction data to paint each emoji icon and its associated count. The Message class only calculates positioning and delegates the actual icon drawing to this specialized component.
How does the reply block handle click events and visual feedback?
The reply block uses the Reply component defined in history_view_reply.h and implemented in history_view_reply.cpp. When users press the reply area, Message::toggleReplyRipple() (lines 2590–2599) creates a Ui::RippleAnimation that paints over the reply background. Click handling routes through the Reply component's clickHandler, which navigates to the original message being quoted.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →