# Telegram Desktop Message Forwarding and Draft System Architecture Explained

> Explore the Telegram Desktop message forwarding and draft system architecture. Understand how distinct data structures and cloud synchronization optimize user experience.

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

---

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

```cpp
// 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.

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

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

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

```cpp
// 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`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/history/view/controls/history_view_forward_panel.h), this component holds a `Data::ResolvedForwardDraft` and manages option changes.

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

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

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

```cpp
// 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/main/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/main/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/main/history/history.h)](https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/history/history.h) & [`.cpp`](https://github.com/telegramdesktop/tdesktop/blob/dev/Telegram/SourceFiles/history/history.cpp) | Draft maps, CRUD operations, cloud sync |
| Compose controls | [[`history/view/controls/history_view_compose_controls.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/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/main/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/main/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/main/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.