Conversation
Telegram support volunteers answer the queue generated when a user taps
"Ask a Question". They are ordinary accounts that authorised with a support
phone number (`+42` + region code + the volunteer's own number). This enables
the behaviour that queue work requires, scoped strictly to those accounts:
a regular account's experience is unchanged.
Upstream already contains most of the plumbing (`AccountSupportUserInfo`,
`Account.isSupportUser`, read-suppression guards in `ChatHistoryListNode`),
but it is disabled and its detection is wrong. This corrects and enables it
rather than introducing a parallel mechanism, to keep future upstream merges
cheap.
Detection
Adds `SupportAccount.isSupportPhoneNumber` as the single predicate and
routes all nine authorisation sites through it. The previous inline checks
had drifted into two variants: two sites tested `hasPrefix("42") && count <= 5`,
which rejects real 17-digit support numbers, while five tested `hasPrefix("42")`
with no length bound, which matches Czech (+420) and Slovak (+421) numbers.
The predicate now tests the `424` prefix with no length constraint — `424` is
unassigned in E.164, so it cannot collide with a real country, and it accepts
both the short and full forms the server may report.
`AuthTransfer` previously hardcoded `isSupportUser: false`, so a volunteer
signing in by QR code silently got a normal account; it now detects properly.
Enablement and reconciliation
`SharedAccountContext` gated the persisted attribute behind `!"".isEmpty`,
an always-false sentinel that pinned `Account.isSupportUser` to false in the
main app while extensions read the real value. Removed.
`AccountContext.isSupportUser` is seeded synchronously from the persisted
account record — it must be readable before the first chat can open, or a
cold start marks the queue read — then reconciled against the account peer's
phone number, which also covers accounts authorised before this existed. An
absent phone means *unknown*, never *not support*, so reconciliation cannot
revoke the flag and silently re-enable read marking.
Read state
Read state must move only when the volunteer replies or explicitly marks a
chat read. History read position was already guarded. Personal mentions and
reactions were not, on either of their two routes: the immediate one and the
deferred replay that drains the scheduled sets once a chat becomes readable.
Both now funnel through `markMentionsSeen` / `markReactionsAndVotesSeen`, so
the deferred path cannot clear a badge that the immediate path suppressed.
Chat-list "Mark as Read", Siri's `INSetMessageAttributeIntent` and inline
push replies remain unguarded by design: they are explicit user intent.
Login
Registers a hidden `+42` / `TS` "TSupport" country so the code resolves while
typing without adding a fake country to the picker every user browses. The
entry is reapplied at all four sites that replace the country list, since an
entry present only in `PhoneCountries.txt` is discarded once the server list
loads. It also claims every `424` prefix key, because the server list ships
Telegram's own test country "Y-land" on `+42` under longer sub-prefix keys
and prefix matching is longest-first.
Restrictions
Hides settings rows that are meaningless for a shared support identity
(emoji status, profile colour, profile photo, username, Premium, Stars,
My TON, Business, Send Gift, Passport, Apple Watch, Recent Calls) and blocks
profile editing — name, bio, username and avatar — at both the affordance and
the save handler. Avatar blocking is scoped to the account's own peer so that
group avatars and custom contact photos, which share the same entry point,
keep working.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Support volunteers answer tickets with text, screenshots and files. This removes the composition affordances that are not part of that job. Sending only — receiving, playback and viewing are untouched throughout, since a volunteer must still be able to open whatever a user sends them to answer the ticket. Regular accounts are unaffected. Input panel Hides the voice / round-video recording button. Both share a single button and a single visibility path, so they cannot be suppressed independently; both are blocked, so this is moot. The right-hand slot is shared between the mic (empty field) and Send (non-empty), so hiding the mic would leave a visible gap. Rather than add a branch, this reuses `keepSendButtonEnabled` — the existing "show Send without text" mechanism — so Send occupies the slot through an already exercised path and the shared layout code is untouched. Attachment menu Reduced to Gallery and File; drops location, poll, contact, to-do and audio (a music-file attachment, distinct from the mic button), and skips the dynamically injected attach-menu bots. The fork's rich-text attachment picker is reduced likewise, since it independently offered audio and location. Gallery and File are load-bearing: screenshots and logs are the substance of a support reply. Gifts `.gift` is not part of the attachment menu, so the reachable gift-sending surfaces in chat are the message context-menu action and the gift button in the compose surface. Both are suppressed. The Stars and My TON settings rows were already hidden. Stories Posting is blocked; viewing is not. Guarded at `openStoryCamera` rather than at each caller, because seven call sites reach it — camera button, pull gesture and deep links — and at the "Post Story" profile button. Groups and channels Removes New Group and New Channel from the compose menu, and the equivalent entries in the community peer-selection screen. The compose button, the contact list and New Contact are deliberately kept: volunteers legitimately start one-to-one chats, including to reach other volunteers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Three further adjustments to keep a support account focused on the ticket queue. As before, every change is gated on the support flag; a regular account is unaffected. Notifications Message notifications are disabled at login. These are per-account settings, so the volunteer's other accounts keep their own configuration, and because the values are stored rather than forced the volunteer can re-enable them in Settings. They are written to `toBeSynchronized`, not only `remote`: Telegram holds notification settings server-side, so a local-only value is silently overwritten by the next sync — which would quietly switch notifications back on. The trade-off is that the change propagates to the volunteer's other clients for this same account. The in-app banner, sound and vibration are suppressed separately, at the point of presentation. `InAppNotificationSettings` lives in account-manager shared data and is global to the app, so persisting a change there would silence the volunteer's personal account too. A runtime check on the support path is the only correctly scoped option; the cost is that in-app banners cannot be re-enabled from Settings for a support account. Server-provided suggestions All of them are suppressed, not only "add your birthday". None applies to a shared regional identity, and one — the profile-photo prompt — actively contradicted the profile-editing restriction by asking the volunteer to set a photo the app then refuses to let them change. Filtering in `_internal_getServerProvidedSuggestions` rather than at each presentation site also excludes suggestions the server adds in future. Peer photos Outside the profile screen, peer photos are replaced by the letter monogram Telegram already shows for peers without one — so chat list, chat title bar, search and contacts render initials, and layout is unchanged. Photos still load when the volunteer opens a profile, which uses a separate component. This needed three guards, not one: `setPeer`, `setPeerV2` (which resolves the photo independently) and the `DirectMediaImageCache` fast path, which reads the peer's image directly and would otherwise have bypassed both. The motivation is as much safety as bandwidth: volunteers see a stream of strangers, and unsolicited profile imagery is a real exposure. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A support account accumulates thousands of pending updates while unused. On launch the client replays the whole gap through `updates.getDifference`, which is slow and largely pointless: the events are for tickets the volunteer will never scroll to, and the chat list only ever renders the most recent chats regardless. `updates.getDifference` accepts `ptsTotalLimit`, which asks the server to answer `differenceTooLong` rather than send a backlog larger than the given size. The existing handler then runs `_internal_resetAccountState`: jump to the server's current state and re-fetch the top chats. Upstream already implements all of this and disables it — the limit sits behind an always-false condition and is cleared unconditionally two lines later. This enables it for support accounts only, at upstream's own value of 1000. Nothing is lost from the volunteer's queue. `resetAccountState` takes read state and unread counts from the server, so every ticket needing a reply still shows as unread; only the local message cache is dropped, and it is refilled per chat on demand. The visible cost is that opening an older chat re-fetches its recent history over the network instead of reading from disk. The limit counts message-box *events* — new messages, edits, deletions and mention/reaction consumption — not messages or chats, so it trips somewhat before 1000 literal new messages. Only cold start and catch-up are affected. During an active session updates arrive through the update loop and never reach `getDifference`. Note there are two `getDifference` call sites and only this one handles the reset; the other treats `differenceTooLong` as a `preconditionFailure`, so the limit must not be set there. Also logs the reset, which was previously silent — without it there is no way to tell whether a fast launch came from this path or from something else. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fixes found by running the previous build on a support account, plus one new restriction. Avatars rendered blank rather than showing the letter monogram, but only for peers that actually have a photo. The chat list picks between two rendering paths depending on exactly that (`ChatListItem`): peers with a photo go to `setPeerV2`, an image-only fast path with no monogram drawing at all, and peers without one go to `setPeer`, which draws letters. The previous change suppressed the image inside `setPeerV2`, leaving nothing to render. `setPeerV2` now delegates to `setPeer` for support accounts, so the monogram path is used everywhere — this also covers avatars in message bubbles, forward headers and date headers, not just the chat list. Animated and video avatars continued to play in the chat list: they are a separate `AvatarVideoNode` layered over the avatar, which was never guarded. Story rings were likewise still drawn; volunteers neither post nor browse stories from the queue, so the ring is noise on every row. The voice-recording tooltip still appeared even though the button is hidden, because it is triggered from interface state rather than from the button. Automatic media downloading is now off for support accounts, which receive a high volume of unsolicited media from strangers. As with in-app notifications, the stored settings are shared across every account in the app, so this is a runtime override rather than a written preference — persisting it would disable downloading on the volunteer's other accounts. That override has to be applied in three places, not one: the chat controller's initial value, the subscription that tracks later changes to the shared settings (which would otherwise immediately undo it), and the prefetch manager, which would otherwise fetch the same media ahead of time. Stickers still download; `shouldDownloadMediaAutomatically` short-circuits for them and they are needed to render messages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nisarul
marked this pull request as ready for review
September 11, 2026 19:04
Jame-Kiadtisak
approved these changes
Sep 11, 2026
Blocking profile editing also removed the only route to signing out: upstream places Log Out in the editing view (`settingsEditingItems`) rather than the main settings list, so hiding the former took the latter with it. Support accounts had no way to log out at all. Adds a logout section to the main settings list, shown only for support accounts, leaving edit mode blocked. Audited the rest of the editing view for the same problem. It also holds the name, bio and birthday fields, which are meant to be unavailable, and "Add Another Account", which already appears in the main list. Log Out was the only unintended loss. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a support-account mode for Telegram support volunteers — the people who answer the queue generated when a user taps Ask a Question. Equivalents already exist for Android and Web; this is the iOS counterpart.
A volunteer is not a distinct kind of user to the server: they are an ordinary account that authorised with a support phone number (
+42+ region code + their own number). Everything here keys off that single fact.Why much of this is re-enabling rather than adding
The tree already contains most of the plumbing —
AccountSupportUserInfo,Account.isSupportUser, and read-suppression guards inChatHistoryListNode— but it is disabled and its detection is incorrect. Where possible this corrects and enables the existing mechanism rather than introducing a parallel one, to keep the diff small and future merges cheap.Three separate features turned out to be switched off behind an always-false
!"".isEmptycondition:SharedAccountContextAccount.isSupportUserpinned tofalsein the main appAccountStateManagerptsTotalLimitnever sent, so large update backlogs are always replayedChatControllerOpenAttachmentMenuHappy to drop any of those re-enablements if they were disabled deliberately — that history isn't visible from the outside.
What changed
Detection. A single predicate replaces nine inline copies. Those copies had drifted into two variants: two tested
hasPrefix("42") && count <= 5, which rejects real support numbers; the other five testedhasPrefix("42")with no length bound, which matches Czech (+420) and Slovak (+421) numbers. The predicate now tests the424prefix — unassigned in E.164 — with no length constraint.AuthTransferpreviously hardcodedfalse, so QR sign-in silently produced a normal account.Read state. For support accounts, read state moves only when the volunteer replies or explicitly marks a chat read — never as a side effect of opening one. History read position was already guarded; personal mentions and reactions were not, on either of their two routes (immediate, and the deferred replay that drains when a chat becomes readable).
Composition. Voice/video recording, location, polls, contacts, to-do, audio attachments, attach-menu bots and gift sending are removed; Gallery and File remain, since screenshots and logs are the substance of a support reply. Story posting is blocked at
openStoryCamera(seven call sites reach it). Group and channel creation are removed; the compose button and contact list are kept.Identity. Settings rows irrelevant to a shared regional identity are hidden, and profile editing is blocked at both the affordance and the save handler. Peer photos outside the profile screen render as the letter monogram — as much a safety measure as a bandwidth one, since volunteers face a stream of strangers.
Quieting. Message notifications are disabled at login (per-account, so re-enableable in Settings). In-app banners and automatic media downloading are runtime overrides rather than stored preferences, because those settings live in account-manager shared data and are global — persisting them would affect the volunteer's other accounts. Server-provided suggestions are suppressed wholesale; one of them prompts the user to set a profile photo, which this change otherwise forbids.
Backlog. A support account accumulates thousands of pending updates while idle, and replaying them on launch is slow and pointless. Setting
ptsTotalLimitasks the server fordifferenceTooLonginstead, routing to the existing reset path. Unread state comes from the server, so no ticket is lost — only the local message cache, refilled per chat on demand.Scope
Every change is gated on the support flag. For a regular account the intent is a byte-identical experience, and that constraint drove several decisions — notably using runtime overrides where the underlying setting is global.
Testing
+420,+421,+423, unassigned+422/+425, ordinary numbers,niland""do not.Notes for review
updates.getDifferencehas two call sites; only one handlesdifferenceTooLong. The other treats it aspreconditionFailure, so the limit must not be set there.AvatarNodehas two rendering paths and only one draws monograms; the chat list picks between them based on whether the peer has a photo.ptsTotalLimitcounts message-box events — messages, edits, deletions, consumption — not messages or chats.