# How Telegram Desktop Contact Sync and Last Seen Privacy Work: A Deep Dive into the Source Code

> Explore Telegram Desktop's source code to understand contact sync via MTPcontacts ImportContacts and last seen privacy using the UserPrivacy API.

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

---

**Telegram Desktop implements contact synchronization through the `AddContactBox` class which sends `MTPcontacts_ImportContacts` requests, while last seen privacy is managed by the `UserPrivacy` API for server-side rules and `GlobalPrivacy` for the client-side "hide read-time" flag.**

Telegram Desktop maintains separate subsystems for managing your address book and privacy preferences. Understanding how **Telegram Desktop contact sync and last seen privacy** work requires examining the interplay between MTProto API calls, local data models in `Data::Session`, and specialized controllers in the settings UI. This article explores the actual implementation in the `telegramdesktop/tdesktop` repository.

## Contact Synchronization Architecture

The contact synchronization pipeline converts phone numbers into `UserData` objects through a series of MTProto requests and local state updates. The system keeps a specialized index of contacts that have no active chats to populate UI components throughout the application.

### The Import Flow in AddContactBox

When a user adds a contact manually, the UI collection happens in [`Telegram/SourceFiles/boxes/add_contact_box.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/boxes/add_contact_box.cpp). The `AddContactBox` class constructs an `MTP_inputPhoneContact` object and dispatches an `MTPcontacts_ImportContacts` request through the session's API wrapper.

```cpp
// boxes/add_contact_box.cpp – line 57
_addRequest = _session->api().request(
    MTPcontacts_ImportContacts(
        MTP_vector<MTPInputContact>(1,
            MTP_inputPhoneContact(
                MTP_flags(0),
                MTP_long(_contactId),
                MTP_string(phone),
                MTP_string(firstName),
                MTP_string(lastName),
                MTPTextWithEntities()))));

```

The server responds with `MTPcontacts_ImportedContacts`, which triggers `Data::Session::processUsers` to update the local user cache. This mechanism ensures that contact information is immediately available to other UI components without waiting for the next full sync cycle.

### Updating UserData and Session State

Once the server response arrives, `processUsers` iterates through the returned user vector and updates the corresponding `UserData` objects. The critical state change occurs in `UserData::setIsContact`, defined in [`Telegram/SourceFiles/data/data_user.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/data/data_user.cpp).

```cpp
// data/data_user.cpp – line 241
if (user->isContact() || user->session().supportMode()) {
    // UI will display the user in the contacts list.
}

```

This flag determines whether the user appears in contact selection dialogs, share sheets, and the main contacts list. The synchronization happens atomically: the MTProto response updates the local model, and the UI observes these changes through reactive programming patterns (`rpl::producer` streams).

### Maintaining the contactsNoChatsList Index

`Data::Session` maintains a specialized index called `contactsNoChatsList` that tracks contacts who do not have an active chat thread. This optimization prevents the UI from scanning the entire user database when displaying pure contact lists (as opposed to chat lists).

```cpp
// data/data_session.cpp – line 5089
not_null<Dialogs::IndexedList*> Session::contactsNoChatsList() {
    return &_contactsNoChatsList;
}

```

The index updates whenever `UserData::setIsContact` changes a user's contact status. This design allows components like the "New Message" dialog or "Share" menus to display contacts instantly without filtering the full dialog list.

## Last Seen Privacy Implementation

Last seen privacy operates through two distinct mechanisms: server-side privacy rules that control who can view your online status, and a client-side flag that hides read receipts from other users.

### Server-Side Privacy Rules via UserPrivacy

The core privacy logic resides in [`Telegram/SourceFiles/api/api_user_privacy.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/api/api_user_privacy.cpp). Telegram represents last seen settings as `MTP_inputPrivacyKeyStatusTimestamp`, which maps to internal `UserPrivacy::Rule` structures containing the option (Everyone, Contacts, Close Friends, Nobody) plus exception lists.

The conversion between MTProto TL types and internal C++ objects happens through two key functions:

- **RulesToTL**: Converts a `UserPrivacy::Rule` into a vector of `MTPInputPrivacyRule` objects for transmission to the server
- **TLToRules**: Parses server responses into `UserPrivacy::Rule` structures with populated exception lists

```cpp
// api/api_user_privacy.cpp – rule → TL
return MTP_vector<MTPInputPrivacyRule>(std::move(result));

```

When saving changes, the system calls `UserPrivacy::save(Key key, const Rule &rule)`, which constructs and sends an `MTPaccount_SetPrivacy` request. This ensures that privacy preferences persist across all Telegram clients linked to the account.

### Client-Side Hide Read-Time Flag in GlobalPrivacy

The "hide read time" feature (hiding the "last seen just now" timestamps in chats) operates differently from standard privacy rules. It is stored in the account's `globalPrivacySettings` rather than the standard privacy key system.

The `Api::GlobalPrivacy` class in [`Telegram/SourceFiles/api/api_global_privacy.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/api/api_global_privacy.cpp) manages this setting:

- `updateHideReadTime(bool)` sends `MTPaccount_SetGlobalPrivacySettings` with the `f_hide_read_marks` flag
- `hideReadTime()` returns an `rpl::producer<bool>` stream for UI observation

```cpp
// api/api_global_privacy.cpp – line 224
| (hideReadTime ? Flag::f_hide_read_marks : Flag())

```

This separation allows the feature to function as a global account setting independent of the per-key privacy rule system.

### The LastSeenPrivacyController UI Layer

The settings interface binds these APIs together through `LastSeenPrivacyController` in [`Telegram/SourceFiles/settings/settings_privacy_controllers.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/settings/settings_privacy_controllers.cpp). This controller creates the toggle for hiding read times and wires it to the `GlobalPrivacy` API:

```cpp
// settings_privacy_controllers.cpp – line 96-104
const auto privacy = &controller->session().api().globalPrivacy();
hideReadTimeButton->toggleOn(privacy->hideReadTime())
    ->toggledValue() | rpl::on_next([=](bool value) {
        _hideReadTime = value;
    });

```

When the user clicks Save, the controller checks for changes and commits them:

```cpp
// settings_privacy_controllers.cpp – line 73-75
if (privacy->hideReadTimeCurrent() != _hideReadTime) {
    privacy->updateHideReadTime(_hideReadTime);
}

```

The controller registration happens in [`Telegram/SourceFiles/settings/sections/settings_privacy_security.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/Telegram/SourceFiles/settings/sections/settings_privacy_security.cpp), which maps the "Last Seen" settings key to this specific controller implementation.

## Practical Code Examples

### Importing Contacts Programmatically

To import a contact manually through the API layer:

```cpp
// Assume we have a running Main::Session* session.
auto request = session->api().request(
    MTPcontacts_ImportContacts(
        MTP_vector<MTPInputContact>(1,
            MTP_inputPhoneContact(
                MTP_flags(0),
                MTP_long(base::RandomValue<quint64>()), // client-side id
                MTP_string(u"+1234567890"_q),          // phone
                MTP_string(u"John"_q),                 // first name
                MTP_string(u"Doe"_q),                  // last name
                MTPTextWithEntities()))));

request.done([=](const MTPcontacts_ImportedContacts &result) {
    // The server returned the imported users.
    session->data().processUsers(result.data().vusers());
}).send();

```

### Reading Current Last Seen Rules

To observe privacy rule changes reactively:

```cpp
auto &privacy = session->api().userPrivacy();
privacy.value(Api::UserPrivacy::Key::LastSeen)
    | rpl::start_with_next([](const Api::UserPrivacy::Rule &rule) {
        // `rule.option` tells you Everyone/Contacts/CloseFriends/Nobody.
        // `rule.always.peers` & `rule.never.peers` hold exception lists.
    }, lifetime);

```

### Toggling Hide Read-Time

To programmatically toggle the read-time hiding feature:

```cpp
auto &global = session->api().globalPrivacy();
global.hideReadTime() | rpl::start_with_next([](bool hidden) {
    qDebug() << "Hide read-time currently:" << hidden;
}, lifetime);

// To change it:
global.updateHideReadTime(true);   // hide the read-time indicator

```

## Summary

- **Contact synchronization** flows through `AddContactBox` → `MTPcontacts_ImportContacts` → `Data::Session::processUsers` → `UserData::setIsContact`, with the global `contactsNoChatsList` index maintaining UI performance.
- **Last seen privacy** splits into server-side rules managed by `Api::UserPrivacy` (converting between TL types and internal Rule objects) and the client-side "hide read-time" flag managed by `Api::GlobalPrivacy`.
- The **settings UI** binds these APIs through `LastSeenPrivacyController`, which handles both the privacy rule selection and the additional hide-read-time toggle.
- All contact data lives in `Data::Session`, making it available to dialogs, share sheets, and peer menus throughout the application lifecycle.

## Frequently Asked Questions

### How does Telegram Desktop store contacts locally?

Telegram Desktop stores contacts as `UserData` objects within `Data::Session`, accessible via the global session instance. The `contactsNoChatsList` index in [`data/data_session.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/data/data_session.cpp) maintains a specialized list of contacts without active chats, while the `isContact` flag on individual `UserData` objects determines contact status. This design separates the contact relationship (who is in your address book) from the chat history (who you have messaged).

### What is the difference between UserPrivacy and GlobalPrivacy?

`Api::UserPrivacy` manages server-side privacy rules like last seen visibility settings, sending `MTPaccount_SetPrivacy` requests with TL-encoded rules for specific privacy keys. `Api::GlobalPrivacy` handles account-wide settings stored in `globalPrivacySettings`, such as the hide-read-time flag, using `MTPaccount_SetGlobalPrivacySettings`. The former controls who can see your data; the latter controls whether specific metadata (like exact read times) is transmitted to other users at all.

### How does the app handle privacy rule exceptions for specific users?

The `UserPrivacy::Rule` structure contains `always.peers` and `never.peers` vectors that store exception lists. When converting between TL types and internal rules in [`api/api_user_privacy.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/api/api_user_privacy.cpp), the system preserves these exception UserIDs and ChatIDs. When the server evaluates whether to show your last seen status to a specific viewer, it checks these exception lists after applying the base rule (Everyone, Contacts, etc.), allowing granular control over privacy even within broad categories.

### Where does the contact list UI populate its data from?

The contact list UI consumes data from `Session::contactsNoChatsList()`, which returns a `Dialogs::IndexedList` pointer maintained in [`data/data_session.cpp`](https://github.com/telegramdesktop/tdesktop/blob/main/data/data_session.cpp). This index updates reactively whenever `UserData::setIsContact` changes a user's contact status, ensuring that menus like "Share to Contact" or "New Message" display current contact information without requiring manual refreshes or redundant database queries.