Implementing Reactions and Reaction Previews in Telegram Desktop: A Complete Workflow Guide
Telegram Desktop implements reactions and their preview overlays through a three-layer architecture spanning data models, UI interaction handlers, and transient overlay widgets.
The telegramdesktop/tdesktop codebase handles animated emoji reactions using a sophisticated workflow that bridges custom emoji parsing with temporary media preview widgets. Understanding this implementation requires tracing how user clicks on custom emoji spans trigger full-screen preview overlays while respecting user notification settings.
Data Layer: Modeling Reactions and Custom Emoji IDs
The foundation of the reaction system resides in the data layer, where reaction identifiers and animation documents are defined and cached.
ReactionId and Reaction Structures
According to the telegramdesktop/tdesktop source code, reactions are identified using Data::ReactionId, a lightweight wrapper that represents either a built-in emoji string or a custom emoji document ID. The actual reaction data lives in Data::Reaction, defined in data/data_message_reactions.h:
struct Reaction {
ReactionId id;
QString title;
not_null<DocumentData*> appearAnimation;
not_null<DocumentData*> selectAnimation;
DocumentData *centerIcon = nullptr; // optional
// ... flags and counters
};
The selectAnimation field specifically powers the preview overlay, while appearAnimation handles the in-chat reaction effect.
The Reactions Manager
The Data::Reactions class acts as a session-scoped singleton that manages available reactions and provides fast lookups for temporary or unsynced reactions:
class Reactions final : private CustomEmojiManager::Listener {
public:
const std::vector<Reaction> &list(Type type) const;
DocumentData *lookupTemporary(const ReactionId &id);
// ...
};
This manager caches reaction lists and resolves custom emoji documents through lookupTemporary(), which is critical for previewing reactions before they are fully synchronized with the server.
UI Interaction Layer: Detecting Clicks on Custom Emoji
When users interact with reactions in the chat history, the application must distinguish between triggering a reaction and previewing its animation.
Custom Emoji Click Handlers
In history/view/history_view_text_helper.cpp, the text rendering system installs click handlers for custom emoji spans using setCustomEmojiClickHandler(). The handler extracts the underlying document ID and validates it before triggering the preview:
if (text.hasCustomEmoji()) {
text.setCustomEmojiClickHandler(
[](QStringView entityData) {
return Data::ParseCustomEmojiData(entityData) != 0;
},
[weak = base::make_weak(view)](
QStringView entityData,
ClickContext context) {
const auto view = weak.get();
if (!view) { return; }
const auto my = context.other.value<ClickHandlerContext>();
if (const auto controller = my.sessionWindow.get()) {
const auto documentId = Data::ParseCustomEmojiData(entityData);
if (documentId) {
ShowReactionPreview(
controller,
my.itemId,
Data::ReactionId{ documentId },
true); // emojiPreview = true
}
}
});
}
The ShowReactionPreview Entry Point
The ShowReactionPreview() function serves as the primary entry point for the preview workflow. Defined in history/view/history_view_reaction_preview.cpp, this function accepts a Window::SessionController*, the originating message ID (FullMsgId), a Data::ReactionId, and a boolean emojiPreview flag that determines whether to display the "Animated emoji preview" label.
Preview Overlay Layer: Building the Reaction Preview Widget
The preview overlay creates a transient fullscreen widget that displays the reaction animation while capturing escape key and click-outside events to dismiss itself.
Resolving Animation Documents
Before constructing the UI, ShowReactionPreview() resolves the actual document to display. For custom emoji reactions, it fetches the document directly; for temporary reactions, it queries the Reactions manager:
DocumentData *document = nullptr;
if (const auto custom = reactionId.custom()) {
document = controller->session().data().document(custom);
} else if (const auto temp = controller->session().data()
.reactions()
.lookupTemporary(reactionId)) {
document = temp->selectAnimation;
}
if (!document) { return false; }
Creating the Media Preview Overlay
The CreatePreviewOverlay() template function instantiates a Window::MediaPreviewWidget and wraps it in a PreviewOverlayState structure:
template <typename MediaData>
[[nodiscard]] PreviewOverlay CreatePreviewOverlay(
not_null<Window::SessionController*> controller,
FullMsgId origin,
MediaData media) {
const auto state = std::make_shared<PreviewOverlayState>();
const auto mainwidget = controller->widget()->bodyWidget();
state->mediaPreview = base::make_unique_q<Window::MediaPreviewWidget>(
mainwidget, controller);
state->mediaPreview->setCustomDuration(st::defaultToggle.duration);
state->mediaPreview->showPreview(origin, media);
state->clickable = base::make_unique_q<Ui::AbstractButton>(mainwidget);
SetupOverlayHideOnEscape(state->clickable.get(), [&]{ /* hide logic */ });
// ...
}
For custom emoji stickers, the overlay optionally adds a background button that navigates to the sticker set when clicked, along with a label displaying the pack name.
Geometry Management and Window Resize
The overlay synchronizes its geometry with the main window through reactive programming. Both CreatePreviewOverlay and ShowReactionPreview subscribe to mainwidget->sizeValue() to maintain centering:
mainwidget->sizeValue() | rpl::on_next([=](QSize size) {
mediaPreviewRaw->setGeometry(Rect(size));
// Background and label positioning calculations...
}, mediaPreviewRaw->lifetime());
This subscription ensures the preview remains centered and properly sized during window resize operations.
Dismissing the Overlay
The overlay hides through three mechanisms implemented in SetupOverlayHideOnEscape():
- ESC key press: Captured by a global event filter attached to the clickable button
- Click outside: The fullscreen
clickablebutton receives mouse events outside the animation bounds - Window resize: The size subscription triggers
hideAll()to prevent stale geometry
The hide operation sets Qt::WA_TransparentForMouseEvents on the clickable layer, fades the media preview using st::defaultToggle.duration, and finally clears the state after the animation completes.
User Settings: Controlling Preview Visibility
User preferences for reaction previews are managed through Api::ReactionsNotifySettings, with the UI implemented in settings/sections/settings_notifications_reactions.cpp. The settings store a bool _showPreviews flag that gates the overlay creation:
if (controller->session().api().reactionsNotifySettings()
.showPreviewsCurrent()) {
HistoryView::ShowReactionPreview(controller, origin, id, true);
}
When disabled, the click handler still executes but returns before constructing the preview widget, allowing the reaction to be applied without showing the animation preview.
Reaction Strip Integration
The reaction strip component in history/view/reactions/history_view_reactions_strip.h creates the inline reaction buttons beneath messages. While not part of the preview overlay itself, this component uses the same lookupTemporary() mechanism to resolve animation documents for temporary reactions, forwarding clicks through the custom emoji handler system described above.
Code Examples
Triggering a Reaction Preview Programmatically
To manually invoke the preview from a custom context menu:
auto controller = window->sessionController();
FullMsgId origin = message->fullId();
Data::ReactionId id{ customEmojiDocumentId };
HistoryView::ShowReactionPreview(
controller,
origin,
id,
true); // Show "Animated emoji preview" label
Respecting User Preview Settings
Always check the notification settings before displaying the overlay:
const auto& settings = controller->session().api().reactionsNotifySettings();
if (settings.showPreviewsCurrent()) {
ShowReactionPreview(controller, origin, reactionId, true);
}
Summary
- Data structures:
Data::ReactionIdandData::Reactionindata/data_message_reactions.hdefine reaction identifiers and animation documents, whileData::Reactionsprovides temporary reaction lookup. - Click handling:
history/view/history_view_text_helper.cppinstalls custom emoji click handlers that parse document IDs and invokeShowReactionPreview(). - Overlay construction:
history/view/history_view_reaction_preview.cppimplementsCreatePreviewOverlay()to buildWindow::MediaPreviewWidgetinstances with fullscreen clickable backgrounds. - Lifecycle management: The overlay hides on ESC key, outside clicks, or window resize via
SetupOverlayHideOnEscape()and reactive size subscriptions. - User control: The
Api::ReactionsNotifySettingsclass inapi/api_reactions_notify_settings.hstores theshowPreviewspreference checked before overlay creation.
Frequently Asked Questions
How does Telegram Desktop resolve the animation document for a reaction preview?
The system resolves documents through Data::Reactions::lookupTemporary() for unsynced reactions or directly from the session's document cache for custom emoji. In ShowReactionPreview(), the code checks reactionId.custom() first; if present, it retrieves the document via controller->session().data().document(), otherwise it queries the temporary reaction list for the selectAnimation document.
What happens when a user clicks outside the reaction preview overlay?
A fullscreen Ui::AbstractButton named clickable covers the entire window behind the animation. This button captures all mouse events outside the media preview widget. When clicked, it executes the hideAll lambda, which sets Qt::WA_TransparentForMouseEvents, hides the MediaPreviewWidget, and schedules state cleanup after the fade animation completes.
Where is the user setting for disabling reaction previews stored?
The preference lives in Api::ReactionsNotifySettings (declared in api/api_reactions_notify_settings.h), which stores a bool _showPreviews flag synchronized with the server. The UI toggle resides in settings/sections/settings_notifications_reactions.cpp, and the guard check typically appears at the call site in history_view_text_helper.cpp or within ShowReactionPreview() itself.
How does the preview overlay handle window resizing?
The overlay subscribes to mainwidget->sizeValue() using reactive programming (via rpl::on_next). This subscription updates the geometry of MediaPreviewWidget and any background labels whenever the window size changes. If the window resizes while the preview is active, the subscription typically triggers hideAll() to prevent layout artifacts, requiring the user to re-invoke the preview.
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 →