Skip to content
Prev Previous commit
Next Next commit
feat(tsupport): quieten support accounts and hide peer photos
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
nisarul and Copilot committed Sep 30, 2026
commit 9ba74fd0a2a9796be40222c73f7da2fe50102abb
14 changes: 11 additions & 3 deletions submodules/AvatarNode/Sources/AvatarNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,9 @@ public final class AvatarNode: ASDisplayNode {
displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0),
storeUnrounded: Bool = false
) {
let smallProfileImage = peer?.smallProfileImage
// Peer photos are suppressed for support volunteers outside the profile screen
// (D19). Nil here makes the monogram render, exactly as for a peer with no photo.
let smallProfileImage = genericContext.isSupportUser ? nil : peer?.smallProfileImage
let params = Params(
peerId: peer?.id,
resourceId: smallProfileImage?.resource.id.stringRepresentation,
Expand Down Expand Up @@ -763,7 +765,7 @@ public final class AvatarNode: ASDisplayNode {
self.imageNode.view.mask = nil
}

if let imageCache = genericContext.imageCache as? DirectMediaImageCache, let peer, let smallProfileImage = peer.smallProfileImage, let peerReference = PeerReference(peer) {
if !genericContext.isSupportUser, let imageCache = genericContext.imageCache as? DirectMediaImageCache, let peer, let smallProfileImage = peer.smallProfileImage, let peerReference = PeerReference(peer) {
if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: peer.profileImageRepresentations.first?.immediateThumbnailData, size: Int(displayDimensions.width * UIScreenScale), synchronous: synchronousLoad) {
if let image = result.image {
self.imageNode.contents = image.cgImage
Expand Down Expand Up @@ -839,7 +841,13 @@ public final class AvatarNode: ASDisplayNode {
icon = .storyIcon
}
} else if peer?.restrictionText(platform: "ios", contentSettings: genericContext.currentContentSettings.with { $0 }) == nil {
representation = peer?.smallProfileImage
// Support volunteers see a stream of strangers; peer photos are suppressed
// outside the profile screen (D19). Leaving `representation` nil makes
// AvatarNode fall back to its letter monogram, so layout is unchanged.
// The profile screen uses PeerInfoAvatarListNode and is unaffected.
if !genericContext.isSupportUser {
representation = peer?.smallProfileImage
}
}

let updatedState: AvatarNodeState = .peerAvatar(peer?.id ?? EnginePeer.Id(0), peer?.nameColor, peer?.displayLetters ?? [], representation, clipStyle, cutoutRect)
Expand Down
2 changes: 1 addition & 1 deletion submodules/TelegramCore/Sources/Account/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ public class UnauthorizedAccount {
let user = TelegramUser(user: apiUser)
let isSupportUser = SupportAccount.isSupportPhoneNumber(user.phone)
let state = AuthorizedAccountState(isTestingEnvironment: testingEnvironment, masterDatacenterId: masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: networkArguments.appVersion, syncContacts: syncContacts)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: networkArguments.appVersion, syncContacts: syncContacts, isSupportUser: isSupportUser)
transaction.setState(state)
return accountManager.transaction { [weak self] transaction -> SendAuthorizationCodeResult in
if let self {
Expand Down
12 changes: 6 additions & 6 deletions submodules/TelegramCore/Sources/Authorization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ public func sendAuthorizationCode(accountManager: AccountManager<TelegramAccount
let user = TelegramUser(user: apiUser)
let isSupportUser = SupportAccount.isSupportPhoneNumber(user.phone)
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts, isSupportUser: isSupportUser)
transaction.setState(state)
if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) {
transaction.setNoticeEntry(key: value.0, value: value.1)
Expand Down Expand Up @@ -1095,7 +1095,7 @@ public func authorizeWithCode(accountManager: AccountManager<TelegramAccountMana
let user = TelegramUser(user: apiUser)
let isSupportUser = SupportAccount.isSupportPhoneNumber(user.phone)
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts, isSupportUser: isSupportUser)
transaction.setState(state)
if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) {
transaction.setNoticeEntry(key: value.0, value: value.1)
Expand Down Expand Up @@ -1165,7 +1165,7 @@ public func authorizeWithPassword(accountManager: AccountManager<TelegramAccount
return peer
})*/
let isSupportUser = SupportAccount.isSupportPhoneNumber(user.phone)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts, isSupportUser: isSupportUser)
transaction.setState(state)

return accountManager.transaction { transaction -> Void in
Expand Down Expand Up @@ -1288,7 +1288,7 @@ public func authorizeWithPasskey(accountManager: AccountManager<TelegramAccountM
let user = TelegramUser(user: apiUser)
let isSupportUser = SupportAccount.isSupportPhoneNumber(user.phone)
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts, isSupportUser: isSupportUser)
transaction.setState(state)
if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) {
transaction.setNoticeEntry(key: value.0, value: value.1)
Expand Down Expand Up @@ -1353,7 +1353,7 @@ public func loginWithRecoveredAccountData(accountManager: AccountManager<Telegra
let isSupportUser = SupportAccount.isSupportPhoneNumber(user.phone)
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])

initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts, isSupportUser: isSupportUser)
transaction.setState(state)
return accountManager.transaction { transaction -> Void in
switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser)
Expand Down Expand Up @@ -1510,7 +1510,7 @@ public func signUpWithName(accountManager: AccountManager<TelegramAccountManager
if let hole = account.postbox.seedConfiguration.initializeChatListWithHole.topLevel {
transaction.replaceChatListHole(groupId: .root, index: hole.index, hole: nil)
}
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts, isSupportUser: isSupportUser)
transaction.setState(state)
if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) {
transaction.setNoticeEntry(key: value.0, value: value.1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftSignalKit
import Postbox


func initializedAppSettingsAfterLogin(transaction: Transaction, appVersion: String, syncContacts: Bool) {
func initializedAppSettingsAfterLogin(transaction: Transaction, appVersion: String, syncContacts: Bool, isSupportUser: Bool = false) {
updateAppChangelogState(transaction: transaction, { state in
var state = state
state.checkedVersion = appVersion
Expand All @@ -13,5 +13,33 @@ func initializedAppSettingsAfterLogin(transaction: Transaction, appVersion: Stri
transaction.updatePreferencesEntry(key: PreferencesKeys.contactsSettings, { _ in
return PreferencesEntry(ContactsSettings(synchronizeContacts: syncContacts))
})
if isSupportUser {
// Support volunteers work a queue; message notifications are off by default so the
// device is not driven by ticket traffic. These are per-account settings, so this
// cannot affect the volunteer's other accounts — and because they are stored rather
// than forced, the volunteer can re-enable them in Settings.
//
// `toBeSynchronized` (not just `remote`) is required: Telegram holds notification
// settings server-side, so a local-only value would be silently overwritten by the
// next sync, quietly re-enabling notifications. The trade-off is that this does
// propagate to the volunteer's other clients for *this* account.
let silenced = MessageNotificationSettings(
enabled: false,
displayPreviews: false,
sound: defaultCloudPeerNotificationSound,
storySettings: PeerStoryNotificationSettings.default
)
let settings = GlobalNotificationSettingsSet(
privateChats: silenced,
groupChats: silenced,
channels: silenced,
reactionSettings: .default,
contactsJoined: false
)
transaction.updatePreferencesEntry(key: PreferencesKeys.globalNotifications, { _ in
return PreferencesEntry(GlobalNotificationSettings(toBeSynchronized: settings, remote: settings))
})
transaction.globalNotificationSettingsUpdated()
}
}

8 changes: 8 additions & 0 deletions submodules/TelegramCore/Sources/Suggestions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ private var dismissedSuggestions: [AccountRecordId: Set<String>] = [:] {
}

func _internal_getServerProvidedSuggestions(account: Account) -> Signal<[ServerProvidedSuggestion], NoError> {
// Support volunteers use a shared regional identity; none of these prompts applies, and
// USERPIC_SETUP actively contradicts the profile-editing restriction by asking them to
// set a photo they are not allowed to change. Filtering here rather than at each
// presentation site also excludes suggestions added by the server in future.
if account.isSupportUser {
return .single([])
}

let key: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.serverSuggestionInfo()]))
return combineLatest(account.postbox.combinedView(keys: [key]), dismissedSuggestionsPromise.get())
|> map { views, dismissedSuggestionsValue -> [ServerProvidedSuggestion] in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func _internal_exportAuthTransferToken(accountManager: AccountManager<TelegramAc
let user = TelegramUser(user: apiUser)
let isSupportUser = SupportAccount.isSupportPhoneNumber(user.phone)
let state = AuthorizedAccountState(isTestingEnvironment: updatedAccount.testingEnvironment, masterDatacenterId: updatedAccount.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: updatedAccount.networkArguments.appVersion, syncContacts: syncContacts)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: updatedAccount.networkArguments.appVersion, syncContacts: syncContacts, isSupportUser: isSupportUser)
transaction.setState(state)
return accountManager.transaction { transaction -> ExportAuthTransferTokenResult in
switchToAuthorizedAccount(transaction: transaction, account: updatedAccount, isSupportUser: isSupportUser)
Expand Down Expand Up @@ -138,7 +138,7 @@ func _internal_exportAuthTransferToken(accountManager: AccountManager<TelegramAc
let user = TelegramUser(user: apiUser)
let isSupportUser = SupportAccount.isSupportPhoneNumber(user.phone)
let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: [])
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts)
initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts, isSupportUser: isSupportUser)
transaction.setState(state)
return accountManager.transaction { transaction -> ExportAuthTransferTokenResult in
switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser)
Expand Down
9 changes: 9 additions & 0 deletions submodules/TelegramUI/Sources/ApplicationContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,15 @@ final class AuthorizedApplicationContext {
inAppNotificationSettings = InAppNotificationSettings.defaultSettings
}

// Support volunteers get no in-app banner, sound or vibration while
// working the queue. Overridden at runtime rather than written to the
// stored settings: InAppNotificationSettings lives in accountManager
// shared data and is global, so persisting it would silence the
// volunteer's other accounts too.
if strongSelf.context.isSupportUser {
return
}

if let appLockContext = strongSelf.context.sharedContext.appLockContext as? AppLockContextImpl {
let _ = (appLockContext.isCurrentlyLocked
|> take(1)
Expand Down