# Implementing Reactions and Reaction Previews in Telegram Desktop: A Complete Workflow Guide

> Discover the three-layer workflow for implementing Telegram Desktop reactions and their preview overlays. Learn how data models UI handlers and overlay widgets work together.

- Repository: [Telegram Desktop/tdesktop](https://github.com/telegramdesktop/tdesktop)
- Tags: how-to-guide
- Published: 2026-04-05

---

**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`](https://github.com/telegramdesktop/tdesktop/blob/main/data/data_message_reactions.h):

```cpp
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:

```cpp
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`](https://github.com/telegramdesktop/tdesktop/blob/main/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:

```cpp
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`](https://github.com/telegramdesktop/tdesktop/blob/main/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:

```cpp
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:

```cpp
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:

```cpp
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 `clickable` button 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`](https://github.com/telegramdesktop/tdesktop/blob/main/settings/sections/settings_notifications_reactions.cpp). The settings store a `bool _showPreviews` flag that gates the overlay creation:

```cpp
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`](https://github.com/telegramdesktop/tdesktop/blob/main/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:

```cpp
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:

```cpp
const auto& settings = controller->session().api().reactionsNotifySettings();
if (settings.showPreviewsCurrent()) {
    ShowReactionPreview(controller, origin, reactionId, true);
}

```

## Summary

- **Data structures**: `Data::ReactionId` and `Data::Reaction` in [`data/data_message_reactions.h`](https://github.com/telegramdesktop/tdesktop/blob/main/data/data_message_reactions.h) define reaction identifiers and animation documents, while `Data::Reactions` provides temporary reaction lookup.
- **Click handling**: [`history/view/history_view_text_helper.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/history/view/history_view_text_helper.cpp) installs custom emoji click handlers that parse document IDs and invoke `ShowReactionPreview()`.
- **Overlay construction**: [`history/view/history_view_reaction_preview.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/history/view/history_view_reaction_preview.cpp) implements `CreatePreviewOverlay()` to build `Window::MediaPreviewWidget` instances 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::ReactionsNotifySettings` class in [`api/api_reactions_notify_settings.h`](https://github.com/telegramdesktop/tdesktop/blob/main/api/api_reactions_notify_settings.h) stores the `showPreviews` preference 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`](https://github.com/telegramdesktop/tdesktop/blob/main/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`](https://github.com/telegramdesktop/tdesktop/blob/main/settings/sections/settings_notifications_reactions.cpp), and the guard check typically appears at the call site in [`history_view_text_helper.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/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.