How Telegram Desktop Manages and Stores Stickers, Masks, and Custom Emoji Locally

Telegram Desktop uses a centralized Data::Stickers manager for sticker sets and a dedicated CustomEmojiManager for emoji, persisting data in binary cache files under tdata/stickers and media files under tdata/files with flag-based differentiation for masks.

The telegramdesktop/tdesktop codebase treats stickers, masks, and custom emoji as distinct media families within a unified subsystem. While regular stickers, masks, and emoji packs share common infrastructure, each type follows specific persistence rules and storage strategies optimized for their usage patterns in the Qt-based client.

The Stickers Subsystem Architecture

The architecture relies on three primary components defined in Telegram/SourceFiles/data/stickers/.

Data::Stickers acts as the central manager tracking installation state, ordering, and update signals for all sticker sets. StickersSet represents individual sets, storing metadata, DocumentData* pointers to covers, and set flags including the Masks identifier. CustomEmojiManager handles emoji resolution, frame caching, and UI object creation.

The manager distinguishes three StickersType values declared in data_stickers.h:

  • Stickers – standard animated and static packs
  • Masks – face-painting mask packs
  • Emoji – collectible emoji packs

How Sticker Masks Are Managed and Stored

Flag-Based Identification

Mask sets are identified by the StickersSetFlag::Masks bit set during parsing from MTProto DstickerSet objects. The flag extraction occurs in data_stickers_set.cpp:

[[nodiscard]] StickersSetFlags ParseStickersSetFlags(
    const MTPDstickerSet &data);

This function parses server responses to determine if a set should be categorized as masks rather than standard stickers.

Separate Ordering and Persistence

Unlike regular stickers, masks maintain an independent ordering list stored in Data::Stickers::_maskSetsOrder. The StickersSetsOrder type (a QList<uint64> of set IDs) exposes two accessors in data_stickers.h:

[[nodiscard]] const StickersSetsOrder &maskSetsOrder() const;
[[nodiscard]] StickersSetsOrder &maskSetsOrderRef();

When users install, remove, or reorder masks, the manager updates this list and persists the entire state via Data::Stickers::save() in data_stickers.cpp. This writes a binary blob using QDataStream to the local tdata/stickers cache file.

UI Integration

UI components query maskSetsOrder() to build mask-specific interfaces. The StickersBox::Inner::rebuild(bool masks) method in boxes/stickers_box.cpp (line 2308) uses this list to populate the Masks tab, while StickersListWidget::populate() in chat_helpers/stickers_list_widget.cpp (lines 3155-3161) renders the mask grid based on this persisted order.

How Custom Emoji Are Managed and Stored

Public API and Factory Methods

The CustomEmojiManager class in data_custom_emoji.h provides the primary interface for emoji creation. Key entry points include:

  • factory(SizeTag tag, int sizeOverride) – Creates a Ui::Text::CustomEmojiFactory for text renderers
  • create(DocumentId id, ...) – Returns std::unique_ptr<Ui::Text::CustomEmoji> instances

These methods handle document resolution and size-specific rendering preparation.

Instance Caching and Loading

CustomEmojiManager maintains per-size caches for performance:

std::array<std::unordered_map<DocumentId, std::unique_ptr<Ui::CustomEmoji::Instance>>,
           kSizeCount> _instances;
std::array<base::flat_map<DocumentId,
           std::vector<base::weak_ptr<CustomEmojiLoader>>>,
           kSizeCount> _loaders;

Instances cache rendered frames (PNG or Lottie) for specific documents and size tags, while Loaders manage asynchronous fetching from the network or local file cache. When requesting an emoji, the manager returns cached instances or creates a CustomEmojiLoader that downloads the underlying DocumentData via the generic file-download subsystem (storage/file_download.h).

Serialization and Local Persistence

Custom emoji IDs serialize into strings embeddable in TL messages:

[[nodiscard]] QString SerializeCustomEmojiId(DocumentId id);
[[nodiscard]] QString SerializeCustomEmojiId(not_null<DocumentData*> document);

Deserialization uses:

[[nodiscard]] DocumentId ParseCustomEmojiData(QStringView data);

These strings persist in the local database within Data::Message entities, reactions, and peer icons. The actual image data resides in Data::DocumentData, downloaded via api().request(MTPmessages_GetDocument(...)) and stored in tdata/files. The CustomEmojiLoader checks cacheKey(document) before initiating network requests.

Reactive UI Rendering

The manager implements reactive patterns using rpl::producer<> callbacks. It emits repaintLater() requests batched by a timer (_repaintTimer), allowing UI widgets in history_view_custom_emoji.cpp and ui/text/text_custom_emoji.cpp to update frames asynchronously without blocking the main thread.

Local Storage Layout and File Structure

Telegram Desktop organizes sticker and emoji data across specific directories:

  • tdata/files – Contains downloaded sticker documents, mask thumbnails, and custom emoji media (PNG/Lottie). Written by Storage::FileDownload when CustomEmojiLoader or thumbnail requests complete.

  • tdata/stickers – Binary cache storing serialized Data::Stickers state including installed set IDs, mask order (maskSetsOrder), recent stickers, and saved GIFs. Written by Data::Stickers::save().

  • tdata/emoji – Stores serialized custom emoji ID strings attached to messages, profiles, or forum topics via SerializeCustomEmojiId.

  • tdata/cache – Lottie frames and pre-rendered bitmap caches for fast UI painting. Managed by Ui::CustomEmoji::Loader using cacheKey() logic.

The auto-clear logic in storage/storage_account.cpp (lines 2436-2680) evicts these caches when users exceed configured storage limits.

Practical Code Examples

Creating a Custom Emoji Instance

// Assume we have a DocumentId for a custom emoji.
DocumentId emojiId = ...;

// Obtain the manager from the session.
auto &customEmoji = session->data().customEmojiManager();

// Create a UI-ready custom emoji with normal size.
auto custom = customEmoji.create(
    emojiId,
    [] { /* UI should repaint when the emoji loads */ },
    CustomEmojiManager::SizeTag::Normal);

// Use it in a text widget.
auto text = Ui::Text::CustomEmoji(custom.get());
someLabel->setText(text);

Source: data_custom_emoji.h (lines 40-55) and data_custom_emoji.cpp (creation logic).

Listing Installed Mask Packs

auto &stickers = session->data().stickers();
const auto &maskOrder = stickers.maskSetsOrder(); // QList<uint64> of mask set ids.

for (const uint64 setId : maskOrder) {
    const auto *set = stickers.set(setId);
    if (set && set->type() == StickersType::Masks) {
        qDebug() << "Mask pack:" << set->title;
    }
}

Source: data_stickers.h (mask order getters, lines 169-173).

Summary

  • Masks are standard sticker sets flagged with StickersSetFlag::Masks, stored with a dedicated maskSetsOrder list in the binary tdata/stickers cache, and rendered by UI components querying Data::Stickers::maskSetsOrder().

  • Custom emoji use CustomEmojiManager with per-size instance caches, document-based loading via CustomEmojiLoader, and ID serialization through SerializeCustomEmojiId().

  • Storage separates metadata (binary caches in tdata/stickers) from media (files in tdata/files), with automatic eviction managed by storage_account.cpp.

  • Integration follows reactive patterns where managers emit repaint signals and UI widgets subscribe to updates for asynchronous frame rendering.

Frequently Asked Questions

How does Telegram Desktop distinguish between regular stickers and masks?

Telegram Desktop checks the StickersSetFlag::Masks bit parsed from MTProto DstickerSet objects via ParseStickersSetFlags() in data_stickers_set.cpp. Sets with this flag populate the separate maskSetsOrder list rather than the standard sticker order list.

Where are custom emoji files actually stored on disk?

Custom emoji media files reside in tdata/files alongside other documents, while their IDs serialize into strings stored in the local message database. Pre-rendered frames cache in tdata/cache using keys generated by cacheKey() in data_custom_emoji.cpp.

What happens when sticker cache storage limits are exceeded?

The auto-clear logic in storage/storage_account.cpp (lines 2436-2680) automatically evicts files from tdata/files and cache entries based on age and size constraints, preserving the binary metadata in tdata/stickers but removing the underlying media blobs.

Can developers access the mask set order programmatically?

Yes, the Data::Stickers class exposes maskSetsOrder() and maskSetsOrderRef() methods in data_stickers.h (lines 169-173), returning a QList<uint64> of mask set IDs that reflects the user's current installation and ordering preferences.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →