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 chosenForwardOptionsflags.Data::ResolvedForwardDraft: Contains concreteHistoryItem*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 viaowner().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:
-
Selection: When the user selects messages and taps "Forward", the UI calls
History::setForwardDraft()with aForwardDraftcontaining the raw message IDs and defaultPreserveInfooptions. -
Resolution: Upon opening the target chat,
ForwardPanel::update()triggersHistory::resolveForwardDraft(), which converts IDs toHistoryItem*pointers viaowner().idsToItems(). -
Option Toggles: As the user interacts with
FillForwardOptions, the callback updates the storedForwardDraftviaHistory::setForwardDraft()with new flags. -
Comment Draft: Simultaneously, any text typed into the compose box updates a regular
DraftthroughComposeControls::draftKey()andHistory::setDraft(). -
Dispatch: Sending combines both drafts—the resolved forward items and the comment text—into an
MTPmessages_forwardMessagesAPI 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
Summary
- Dual-map storage:
Historymaintains separateflat_mapinstances for regular drafts and forward drafts, indexed byDraftKey. - Lazy resolution: Forward drafts store only message IDs until
resolveForwardDraft()converts them to concrete items for UI rendering or API calls. - Bit-packed keys:
DraftKeyencodes 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 inapplyCloudDraft()prevent stale server data from overwriting recent local changes. - UI-model separation:
ForwardPanelandComposeControlsact as thin adapters, delegating persistence toHistoryand 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →