How the Self-Destruct Messaging Feature Works in Telegram Desktop: A Technical Deep Dive
The self-destruct messaging feature in Telegram Desktop implements Time-To-Live (TTL) through three coordinated subsystems: an API layer for server configuration, a HistoryServiceSelfDestruct component attached to individual messages, and a central Session::_selfDestructTimer that monitors expiration and triggers removal when getSelfDestructIn() returns zero.
Telegram Desktop's self-destruct messaging feature enables users to send photos and videos that automatically disappear after a specified duration. According to the telegramdesktop/tdesktop source code, this TTL system relies on a tight integration between the MTProto API layer, message-level components, and session-wide timers to ensure synchronized expiration across clients.
Configuring TTL via the API Layer
User-facing configuration begins in the settings dialogs. The Settings UI (settings/sections/settings_privacy_security.cpp and settings/sections/settings_global_ttl.cpp) provides interfaces for selecting default message TTL and account self-destruction intervals.
When a user modifies these values, the application invokes the API wrapper defined in api/api_self_destruct.h:
session->api().selfDestruct().updateDefaultHistoryTTL(ttl); // settings_global_ttl.cpp
session->api().selfDestruct().updateAccountTTL(days); // settings_privacy_security.cpp
The implementation in api/api_self_destruct.cpp transmits the corresponding MTProto requests (messages_SetDefaultHistoryTTL and account_SetAccountTTL) and stores the server response in reactive variables (_defaultHistoryTTL.period and _accountTTL.days). This ensures the client maintains the authoritative TTL values for subsequent message operations.
Attaching Self-Destruct Components to Messages
When composing a self-destructing media message, the client attaches a specialized component to the HistoryItem. The method HistoryItem::setSelfDestruct() in history/history_item.cpp (lines 7296–7307) initializes this data:
void HistoryItem::setSelfDestruct(
HistorySelfDestructType type,
MTPint mtpTTLvalue) {
UpdateComponents(HistoryServiceSelfDestruct::Bit());
const auto selfdestruct = Get<HistoryServiceSelfDestruct>();
if (mtpTTLvalue.v == TimeId(0x7FFFFFFF)) {
selfdestruct->timeToLive = TimeToLiveSingleView(); // “forever”
} else {
selfdestruct->timeToLive = mtpTTLvalue.v * crl::time(1000);
}
selfdestruct->type = type;
}
The component structure is defined in history/history_item_components.h (lines 28–35):
struct HistoryServiceSelfDestruct
: RuntimeComponent<HistoryServiceSelfDestruct, HistoryItem> {
using Type = HistorySelfDestructType;
Type type = Type::Photo;
std::variant<crl::time, TimeToLiveSingleView> timeToLive = crl::time();
std::variant<crl::time, TimeToLiveSingleView> destructAt = crl::time();
};
This component stores both the timeToLive duration and the calculated destructAt timestamp. When the server echoes the message to recipients, they receive the same destructAt value, ensuring consistent expiration across all clients.
The Session Timer and Expiration Tracking
All pending self-destruct messages are tracked centrally by the Session class in data/data_session.cpp. The system maintains a vector Session::_selfDestructItems containing FullMsgId identifiers and a single base::Timer instance called _selfDestructTimer.
When a new self-destructing message is created, the system registers it via Session::selfDestructIn() (lines 3201–3206):
void Session::selfDestructIn(not_null<HistoryItem*> item, crl::time delay) {
_selfDestructItems.push_back(item->fullId());
if (!_selfDestructTimer.isActive()
|| _selfDestructTimer.remainingTime() > delay) {
_selfDestructTimer.callOnce(delay);
}
}
The timer callback Session::checkSelfDestructItems() (lines 3210–3229) processes the queue:
void Session::checkSelfDestructItems() {
const auto now = crl::now();
crl::time nextDestructIn = 0;
for (auto i = _selfDestructItems.begin(); i != _selfDestructItems.cend();) {
if (const auto item = message(*i)) {
if (const auto destructIn = item->getSelfDestructIn(now)) {
if (nextDestructIn > 0) {
accumulate_min(nextDestructIn, destructIn);
} else {
nextDestructIn = destructIn;
}
++i;
} else {
i = _selfDestructItems.erase(i);
}
} else {
i = _selfDestructItems.erase(i);
}
}
if (nextDestructIn > 0) {
_selfDestructTimer.callOnce(nextDestructIn);
}
}
This implementation efficiently batches expiration checks by rescheduling the timer only for the nearest future expiry, minimizing CPU wakeups.
Computing Message Lifetime
Individual messages calculate their remaining lifespan through HistoryItem::getSelfDestructIn() in history/history_item.cpp (lines 7830–7850):
crl::time HistoryItem::getSelfDestructIn(crl::time now) {
if (const auto selfdestruct = Get<HistoryServiceSelfDestruct>()) {
const auto at = std::get_if<crl::time>(&selfdestruct->destructAt);
if (at && (*at) > 0) {
const auto destruct = *at;
if (destruct <= now) {
// Message already expired – replace service text with “expired”
setServiceText({ TextWithEntities{ .text = expiredMessage() } });
return 0;
}
return destruct - now; // ms remaining
}
}
return 0;
}
When this method returns zero, the calling code in checkSelfDestructItems() removes the message from the tracking vector. Simultaneously, the UI updates the message bubble to display a localized "photo expired" or "video expired" string via setServiceText().
UI Implementation and Countdown Display
The visual countdown is rendered using components in ui/effects/ttl_icon.cpp, which draws the timer badge next to the message bubble. Human-readable time formatting is handled by Ui::FormatTTL() in ui/text/format_values.cpp (lines 438–470), converting millisecond deltas into strings like "5 min" or "30 sec".
The TTL selection dialog (boxes/self_destruction_box.cpp, lines 148–173) provides the interface for choosing expiration intervals, invoking the same Api::SelfDestruct methods used in the settings panels.
Key Source Files
| Component | File Path | Purpose |
|---|---|---|
| API Interface | api/api_self_destruct.h |
Declares SelfDestruct class with updateDefaultHistoryTTL |
| API Implementation | api/api_self_destruct.cpp |
MTProto request construction and reactive state storage |
| Session Timer | data/data_session.cpp |
selfDestructIn and checkSelfDestructItems logic |
| Message Component | history/history_item_components.h |
HistoryServiceSelfDestruct struct definition |
| TTL Logic | history/history_item.cpp |
setSelfDestruct and getSelfDestructIn implementations |
| Settings UI | settings/sections/settings_global_ttl.cpp |
Default TTL configuration interface |
| Formatting | ui/text/format_values.cpp |
FormatTTL helper for countdown strings |
| Visual Effects | ui/effects/ttl_icon.cpp |
Timer icon rendering |
Summary
- Configuration: The
Api::SelfDestructclass manages server communication for both default message TTL and account self-destruction settings. - Message Tagging: The
HistoryServiceSelfDestructcomponent attaches expiration metadata toHistoryItemobjects, storing bothtimeToLiveduration and absolutedestructAttimestamps. - Centralized Tracking:
Session::_selfDestructTimermonitors all pending expirations through the_selfDestructItemsvector, using efficient batch processing to minimize resource usage. - Expiration Logic:
HistoryItem::getSelfDestructIn()calculates remaining milliseconds and triggers UI updates when deadlines pass. - Visual Feedback: The UI layer combines
ttl_icon.cppfor graphics andformat_values.cppfor human-readable countdown text.
Frequently Asked Questions
What triggers a self-destruct message to disappear in Telegram Desktop?
The disappearance is triggered by Session::checkSelfDestructItems() when it detects that HistoryItem::getSelfDestructIn() returns zero, indicating the current time has exceeded the destructAt timestamp. At this point, the message is removed from the tracking list and its service text is replaced with an expiration notice.
How does Telegram Desktop handle "View Once" or infinite TTL settings?
When the server specifies a TTL value of 0x7FFFFFFF, the client stores a TimeToLiveSingleView() variant instead of a numeric timestamp in the HistoryServiceSelfDestruct component. This special value indicates the content should remain until manually viewed once, rather than expiring after a fixed duration.
What is the difference between default history TTL and account TTL?
Default history TTL (updateDefaultHistoryTTL) sets the automatic expiration timer for new messages sent in chats, while account TTL (updateAccountTTL) specifies how long the account itself should remain inactive before Telegram automatically deletes it. Both are managed through the api_self_destruct module but control distinct server-side behaviors.
Which source files contain the core logic for message expiration?
The expiration engine spans history/history_item.cpp (message-level TTL calculations), data/data_session.cpp (centralized timer and tracking), and history/history_item_components.h (data structures). UI components reside in ui/effects/ttl_icon.cpp and ui/text/format_values.cpp.
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 →