Telegram Desktop Message Forwarding and Draft System Architecture Explained

Telegram Desktop separates message forwarding and draft concerns into distinct data structures—Data::Draft for text input and Data::ForwardDraft for message selection—storing them in per-chat maps within the History class and synchronizing cloud state through DraftKey abstractions.

The telegramdesktop/tdesktop repository implements a modular message composition system where drafts and forward operations follow strict architectural boundaries. Understanding how Data::DraftKey identifies storage slots, how History maintains both regular and forward drafts, and how the UI resolves message IDs into concrete items reveals a design optimized for multi-device synchronization and flexible user options.

Core Data Structures

Draft Identification with Data::DraftKey

Data::DraftKey serves as a compact 64-bit identifier that uniquely addresses a draft for a specific context—whether a chat, topic, monoforum sub-list, scheduled message, or shortcut. Defined in Telegram/SourceFiles/data/data_drafts.h, this class uses bit-packing to encode the topic root ID, monoforum peer ID, and draft type into a single int64 value.

// Telegram/SourceFiles/data/data_drafts.h
class DraftKey {
public:
    [[nodiscard]] static constexpr DraftKey Local(MsgId topicRootId,
                                                 PeerId monoforumPeerId);
    [[nodiscard]] static constexpr DraftKey Cloud(MsgId topicRootId,
                                                 PeerId monoforumPeerId);
    [[nodiscard]] static constexpr DraftKey LocalEdit(MsgId topicRootId,
                                                     PeerId monoforumPeerId);
    [[nodiscard]] constexpr qint64 serialize() const { return _value; }
    [[nodiscard]] static constexpr DraftKey None();
    // ...
};

The lower bits encode the topic and monoforum identifiers, while high bits distinguish between local, cloud, and edit draft types. This design allows the same History container to manage multiple draft variants without code duplication.

Draft Content Storage in Data::Draft

The Data::Draft structure holds all UI-visible composition state: text content, reply-to references, webpage previews, cursor position, and cloud synchronization metadata.

// Telegram/SourceFiles/data/data_drafts.h
struct Draft {
    TimeId date = 0;
    TextWithTags textWithTags;
    FullReplyTo reply;      // reply-to message or edit target
    SuggestOptions suggest; // "send as" suggestions
    MessageCursor cursor;
    WebPageDraft webpage;
    mtpRequestId saveRequestId = 0; // tracks cloud-save requests
};

The date field tracks last modification time to resolve conflicts during cloud synchronization, while saveRequestId prevents duplicate upstream requests when the user types rapidly.

Forward Draft Types

Forwarding uses two distinct structures defined in Telegram/SourceFiles/data/data_types.h:

  • Data::ForwardDraft: Stores the raw list of message IDs selected by the user and the chosen ForwardOptions flags.
  • Data::ResolvedForwardDraft: Contains concrete HistoryItem* pointers resolved from those IDs, used for UI previews and API calls.
// Telegram/SourceFiles/data/data_types.h
namespace Data {
    struct ForwardDraft {
        MessageIdsList ids;
        ForwardOptions options = ForwardOptions::PreserveInfo;
    };
    
    struct ResolvedForwardDraft {
        HistoryItemsList items;
        ForwardOptions options = ForwardOptions::PreserveInfo;
    };
    
    enum class ForwardOptions {
        PreserveInfo,       // keep original sender info & captions
        NoSenderNames,      // hide sender names
        NoNamesAndCaptions, // hide names & captions
    };
}

This separation ensures that the persistent storage layer deals only with lightweight IDs, while the presentation layer works with fully resolved message objects that reflect current state (including deletion or edits).

History Layer Storage and Management

Per-Chat Draft Maps

The History class maintains two parallel hash maps in Telegram/SourceFiles/history/history.h:

// Telegram/SourceFiles/history/history.h
using HistoryDrafts = base::flat_map<Data::DraftKey, std::unique_ptr<Data::Draft>>;
HistoryDrafts _drafts;        // regular text drafts
HistoryDrafts _forwardDrafts; // forward drafts

The public API provides type-safe access:

  • Data::Draft *draft(Data::DraftKey key) const – Retrieves a draft by key.
  • void setDraft(Data::DraftKey key, std::unique_ptr<Data::Draft> &&draft) – Inserts or replaces a draft.
  • const Data::ForwardDraft &forwardDraft(MsgId topicRootId, PeerId monoforumPeerId) const – Retrieves forward draft.
  • void setForwardDraft(MsgId topicRootId, PeerId monoforumPeerId, Data::ForwardDraft &&draft) – Stores forward selection.
  • Data::ResolvedForwardDraft resolveForwardDraft(const Data::ForwardDraft &draft) const – Converts IDs to item pointers via owner().idsToItems().

Cloud Synchronization Logic

Draft modifications trigger deferred cloud synchronization to prevent excessive network traffic. The History class implements conflict resolution through timestamp tracking:

// Telegram/SourceFiles/history/history.cpp
void History::applyCloudDraft(const Data::Draft &draft, TimeId date) {
    if (date <= _acceptCloudDraftsAfter) return; // stale data
    // merge logic...
}

void History::startSavingCloudDraft(Data::DraftKey key, 
                                    std::unique_ptr<Data::Draft> draft) {
    // assigns saveRequestId and queues MTPmessages_saveDraft
}

The saveDraftToCloudDelayed() method (called from ComposeControls) debounces rapid edits, while _acceptCloudDraftsAfter ensures that local changes made while offline do not overwrite newer server drafts when reconnecting.

Forward Message UI Architecture

ForwardPanel Preview Component

When users initiate forwarding, the HistoryView::Controls::ForwardPanel class renders a preview at the top of the compose area. Defined in Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h, this component holds a Data::ResolvedForwardDraft and manages option changes.

// Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h
class ForwardPanel final : public base::has_weak_ptr {
public:
    void update(Data::Thread *to, Data::ResolvedForwardDraft draft);
    void applyOptions(Data::ForwardOptions options);
    void editToNextOption(); // UI shortcut support
    rpl::producer<> itemsUpdated() const;
    // ...
private:
    Data::ResolvedForwardDraft _data;
};

The update() method receives resolved items from History::resolveForwardDraft(), while applyOptions() propagates user changes back to the persistent ForwardDraft storage.

Configuring Forward Options

The options UI resides in Telegram/SourceFiles/ui/chat/forward_options_box.cpp. The FillForwardOptions function generates checkboxes for "Show sender names" and "Show captions", binding them to callbacks that update the stored flags:

// Telegram/SourceFiles/ui/chat/forward_options_box.cpp
void FillForwardOptions(
    Fn<not_null<AbstractCheckView*>(rpl::producer<QString> &&, bool)> createView,
    ForwardOptions options,
    Fn<void(ForwardOptions)> optionsChanged,
    rpl::lifetime &lifetime) {
    
    const auto names = createView(
        tr::lng_forward_show_sender(),
        !options.dropNames);
        
    names->checkedChanges() | rpl::start_with_next([=](bool checked) {
        auto newOpts = options;
        newOpts.dropNames = !checked;
        optionsChanged(newOpts);
    }, lifetime);
}

When the user toggles these options, the ForwardPanel receives the new ForwardOptions value and calls History::setForwardDraft() to persist the change.

Draft Key Computation in ComposeControls

The ComposeControls class determines which draft key applies to the current context based on the active tab or mode:

// Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp
Data::DraftKey ComposeControls::draftKey(DraftType type) const {
    using Key = Data::DraftKey;
    switch (type) {
        case DraftType::Normal: 
            return Key::Local(_topicRootId, _monoforumPeerId);
        case DraftType::Edit: 
            return Key::LocalEdit(_topicRootId, _monoforumPeerId);
        case DraftType::Scheduled: 
            return Key::Scheduled();
        // ...
    }
}

This key then routes read/write operations to the correct slot in History::_drafts, ensuring that edits to a scheduled message do not corrupt the regular draft for the same chat.

End-to-End Workflow Example

The complete flow from message selection to dispatch demonstrates how these components interact:

  1. Selection: When the user selects messages and taps "Forward", the UI calls History::setForwardDraft() with a ForwardDraft containing the raw message IDs and default PreserveInfo options.

  2. Resolution: Upon opening the target chat, ForwardPanel::update() triggers History::resolveForwardDraft(), which converts IDs to HistoryItem* pointers via owner().idsToItems().

  3. Option Toggles: As the user interacts with FillForwardOptions, the callback updates the stored ForwardDraft via History::setForwardDraft() with new flags.

  4. Comment Draft: Simultaneously, any text typed into the compose box updates a regular Draft through ComposeControls::draftKey() and History::setDraft().

  5. Dispatch: Sending combines both drafts—the resolved forward items and the comment text—into an MTPmessages_forwardMessages API request:

// Simplified from apiwrap.cpp
const auto &fwd = history->forwardDraft(topicRootId, monoforumPeerId);
const auto resolved = history->resolveForwardDraft(fwd);

api().request(MTPmessages_forwardMessages(
    MTP_flags(0),
    MTP_inputPeer(toPeer),
    MTP_vector<MTPint>(fwd.ids),
    MTP_long(randomId()),
    MTP_bool(resolved.options != Data::ForwardOptions::PreserveInfo)
)).send();

Key Implementation Files

Component File Path Purpose
Draft definitions [data/data_drafts.h](https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/data/data_drafts.h) DraftKey, Draft struct, serialization
Forward types [data/data_types.h](https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/data/data_types.h) ForwardDraft, ResolvedForwardDraft, ForwardOptions
History storage [history/history.h](https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/history/history.h) & .cpp Draft maps, CRUD operations, cloud sync
Compose controls [history/view/controls/history_view_compose_controls.cpp](https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp) Draft key computation, draft updates
Forward panel [history/view/controls/history_view_forward_panel.h](https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h) Preview rendering, option application
Options UI [ui/chat/forward_options_box.cpp](https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/ui/chat/forward_options_box.cpp) Checkbox generation for forward flags
Cloud persistence [storage/storage_account.cpp](https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/storage/storage_account.cpp) Local database serialization

Summary

  • Dual-map storage: History maintains separate flat_map instances for regular drafts and forward drafts, indexed by DraftKey.
  • Lazy resolution: Forward drafts store only message IDs until resolveForwardDraft() converts them to concrete items for UI rendering or API calls.
  • Bit-packed keys: DraftKey encodes chat context, topic IDs, and draft type into a 64-bit integer, enabling type-safe storage of heterogeneous draft variants.
  • Debounced cloud sync: Local edits trigger saveDraftToCloudDelayed(), while timestamp checks in applyCloudDraft() prevent stale server data from overwriting recent local changes.
  • UI-model separation: ForwardPanel and ComposeControls act as thin adapters, delegating persistence to History and conflict resolution to the cloud sync layer.

Frequently Asked Questions

How does Telegram Desktop prevent cloud drafts from overwriting local changes?

The History class tracks the timestamp of the last local edit in _acceptCloudDraftsAfter. When a cloud draft arrives via applyCloudDraft(), the implementation compares the server's timestamp against this value. If the cloud draft is older than the local modification, the update is discarded, ensuring that offline edits are not accidentally overwritten when the client reconnects.

What is the difference between ForwardDraft and ResolvedForwardDraft?

ForwardDraft contains only the message IDs (MessageIdsList) and forwarding options, making it lightweight for storage in History::_forwardDrafts. ResolvedForwardDraft contains actual HistoryItem* pointers obtained by calling History::resolveForwardDraft(), which looks up the current message objects in the data cache. This resolution happens on-demand for UI previews and sending, ensuring that deletions or edits to the original messages are reflected immediately.

How does the system handle forwarding to multiple different chats simultaneously?

Each chat maintains its own ForwardDraft entry in History::_forwardDrafts, keyed by the combination of topic root ID and monoforum peer ID. When the user switches between target chats, ComposeControls computes the appropriate DraftKey for the new context, and the ForwardPanel loads the specific draft for that destination. This allows different forwarding options (such as hiding sender names) to be configured independently per destination.

Why are DraftKeys used instead of simple chat IDs?

DraftKey encodes not only the chat identifier but also the specific context within that chat—such as topic threads, monoforum sub-lists, scheduled message drafts, and message editing sessions. This allows a single History instance to store multiple concurrent drafts (e.g., a regular message draft and a separate draft for editing an existing message) without collision, while maintaining a uniform interface through the 64-bit serialization format.

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 →