Files
isaacandClaude Opus 4.7 86d1456552 Postbox -> TelegramEngine waves 107-137 (squashed)
31 waves of consumer-side migration from `import Postbox` to TelegramEngine
typealiases. Net: 173 import drops + 39 BUILD-dep drops + 1 new typealias
(`EngineStoryId = StoryId`, wave 113).

Wave shapes used:
- Orphan-import sweeps (107, 108, 128): drop `import Postbox` from files
  whose only Postbox-symbol reference was the import line itself, then
  resolve build failures. Methodology requires token-level (`grep -oE`)
  filtering, not line-level, to avoid masking real Postbox usage on lines
  that also contain `Namespaces.X` references.
- Identifier-swap mini-waves (109-127, 129-134, 136-137): rename
  Postbox-typealiased identifiers to engine equivalents
  (PeerId -> EnginePeer.Id, MessageId -> EngineMessage.Id,
  MediaId -> EngineMedia.Id, MessageIndex -> EngineMessage.Index,
  StoryId -> EngineStoryId, ItemCollectionId -> EngineItemCollectionId,
  PreferencesEntry -> EnginePreferencesEntry,
  FetchResourceSourceType/Error -> EngineFetchResourceSourceType/Error,
  MemoryBuffer -> EngineMemoryBuffer, MessageTags -> EngineMessage.Tags,
  MessageAttribute -> EngineMessage.Attribute,
  TempBox -> EngineTempBox).
- Asset-string FP-only orphans (124).
- Typealias addition + drain (113): added `EngineStoryId` typealias to
  TelegramCore, then drained 3+11 consumer sites.

Hard blockers identified during these waves (must restore `import Postbox`
when present): MediaResource[A-Za-z]* (any suffix -- the literal
`MediaResource` matches don't catch MediaResourceData/MediaResourceId/etc.),
Postbox/MediaBox/MediaResource raw types, PostboxCoding/PostboxEncoder/
PostboxDecoder, TempBoxFile, ValueBoxKey, PostboxView, combinedView,
HashFunctions, postboxLog, openPostbox, declareEncodable, PeerView,
MessageHistoryView, MessageHistoryThreadData, CachedPeerData, RenderedPeer,
SelectivePrivacyPeer, SimpleDictionary, ItemCollectionInfosView,
ItemCollectionItem, ItemCollectionItemIndex, ItemCollectionViewEntryIndex,
ChatListIndex, ChatListEntrySummaryComponents, CodableEntry,
MessageHistoryThread, MessageHistoryAnchorIndex,
MessageHistoryEntryLocation, PeerStoryStats, PeerNameIndex,
PeerSummaryCounterTags, ChatListTotalUnreadStateCategory/Stats,
arePeersEqual. Protocol-shape blockers: bare `Peer`/`Message`/`Media`
in function signatures, generic args, enum-case payloads, or dict value
types (e.g., `[PeerId: Peer]`, `case messages([Message])`,
`Signal<(Peer?, ...), NoError>`).

`replace_all PeerId -> EnginePeer.Id` is dangerous: mangles compound
names like `failedPeerId`, `ContactListPeerId`, `nextRemoteMediaId`,
`replyToMessageId`. Pre-flight grep `\b[a-z][a-zA-Z]*PeerId\b` and only
replace_all if 0 matches.

Also removes unneeded design/plan docs from a separate (link-highlighting)
feature branch:
- docs/superpowers/plans/2026-05-02-link-highlighting-modern-path-fixes.md
- docs/superpowers/specs/2026-05-02-link-highlighting-modern-path-fixes-design.md

Squashed commits: 6d82c2980d..e6de5d53a3 (59 commits, including
per-wave content commits and per-wave CLAUDE.md bumps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:28:50 +02:00

364 lines
20 KiB
Swift

import Foundation
import UIKit
import Display
import AccountContext
import SwiftSignalKit
import TelegramCore
import SettingsUI
import PeerInfoStoryGridScreen
import CallListUI
import PassportUI
import AccountUtils
import OverlayStatusController
import PremiumUI
import TelegramPresentationData
import PresentationDataUtils
import PasswordSetupUI
import InstantPageCache
extension PeerInfoScreenNode {
func openSettings(section: PeerInfoSettingsSection) {
let push: (ViewController) -> Void = { [weak self] c in
guard let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController else {
return
}
if strongSelf.isMyProfile {
navigationController.pushViewController(c)
} else {
var updatedControllers = navigationController.viewControllers
for controller in navigationController.viewControllers.reversed() {
if controller !== strongSelf && !(controller is TabBarController) {
updatedControllers.removeLast()
} else {
break
}
}
updatedControllers.append(c)
var animated = true
if let validLayout = strongSelf.validLayout?.0, case .regular = validLayout.metrics.widthClass {
animated = false
}
navigationController.setViewControllers(updatedControllers, animated: animated)
}
}
switch section {
case .avatar:
self.controller?.openAvatarForEditing()
case .edit:
self.headerNode.navigationButtonContainer.performAction?(.edit, nil, nil)
case .proxy:
self.controller?.push(proxySettingsController(context: self.context))
case .profile:
self.controller?.push(PeerInfoScreenImpl(
context: self.context,
updatedPresentationData: self.controller?.updatedPresentationData,
peerId: self.context.account.peerId,
avatarInitiallyExpanded: false,
isOpenedFromChat: false,
reactionSourceMessageId: nil,
callMessages: [],
isMyProfile: true,
profileGiftsContext: self.data?.profileGiftsContext
))
case .stories:
push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .saved))
case .savedMessages:
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
guard let self, let peer = peer else {
return
}
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer)))
}
})
case .recentCalls:
push(CallListController(context: context, mode: .navigation))
case .devices:
let _ = (self.activeSessionsContextAndCount.get()
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] activeSessionsContextAndCount in
if let strongSelf = self, let activeSessionsContextAndCount = activeSessionsContextAndCount {
let (activeSessionsContext, _, webSessionsContext) = activeSessionsContextAndCount
push(recentSessionsController(context: strongSelf.context, activeSessionsContext: activeSessionsContext, webSessionsContext: webSessionsContext, websitesOnly: false))
}
})
case .chatFolders:
let controller = self.context.sharedContext.makeFilterSettingsController(context: self.context, modal: false, scrollToTags: false, dismissed: nil)
push(controller)
case .notificationsAndSounds:
if let settings = self.data?.globalSettings {
push(notificationsAndSoundsController(context: self.context, exceptionsList: settings.notificationExceptions))
}
case .privacyAndSecurity:
if let settings = self.data?.globalSettings {
let _ = (combineLatest(self.blockedPeers.get(), self.hasTwoStepAuth.get())
|> take(1)
|> deliverOnMainQueue).startStandalone(next: { [weak self] blockedPeersContext, hasTwoStepAuth in
if let strongSelf = self {
let loginEmailPattern = strongSelf.twoStepAuthData.get() |> map { data -> String? in
return data?.loginEmailPattern
}
push(privacyAndSecurityController(context: strongSelf.context, initialSettings: settings.privacySettings, updatedSettings: { [weak self] settings in
self?.privacySettings.set(.single(settings))
}, updatedBlockedPeers: { [weak self] blockedPeersContext in
self?.blockedPeers.set(.single(blockedPeersContext))
}, updatedHasTwoStepAuth: { [weak self] hasTwoStepAuthValue in
self?.hasTwoStepAuth.set(.single(hasTwoStepAuthValue))
}, focusOnItemTag: nil, activeSessionsContext: settings.activeSessionsContext, webSessionsContext: settings.webSessionsContext, blockedPeersContext: blockedPeersContext, hasTwoStepAuth: hasTwoStepAuth, loginEmailPattern: loginEmailPattern, updatedTwoStepAuthData: { [weak self] in
if let strongSelf = self {
strongSelf.twoStepAuthData.set(
strongSelf.context.engine.auth.twoStepAuthData()
|> map(Optional.init)
|> `catch` { _ -> Signal<TwoStepAuthData?, NoError> in
return .single(nil)
}
)
}
}, requestPublicPhotoSetup: { [weak self] completion in
if let self {
self.controller?.openAvatarForEditing(mode: .fallback, completion: completion)
}
}, requestPublicPhotoRemove: { [weak self] completion in
if let self {
self.controller?.openAvatarRemoval(mode: .fallback, completion: completion)
}
}))
}
})
}
case .passwordSetup:
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.6, execute: { [weak self] in
guard let self else {
return
}
let _ = self.context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.setupPassword.id).startStandalone()
})
let controller = self.context.sharedContext.makeSetupTwoFactorAuthController(context: self.context)
push(controller)
case .dataAndStorage:
push(dataAndStorageController(context: self.context))
case .appearance:
push(themeSettingsController(context: self.context))
case .language:
push(LocalizationListController(context: self.context))
case .premium:
let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .settings, forceDark: false, dismissed: nil)
self.controller?.push(controller)
case .premiumGift:
guard let controller = self.controller, !controller.presentAccountFrozenInfoIfNeeded() else {
return
}
let _ = (self.context.account.stateManager.contactBirthdays
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] birthdays in
guard let self else {
return
}
let giftsController = self.context.sharedContext.makePremiumGiftController(context: self.context, source: .settings(birthdays), completion: nil)
self.controller?.push(giftsController)
})
case .stickers:
if let settings = self.data?.globalSettings {
push(installedStickerPacksController(context: self.context, mode: .general, archivedPacks: settings.archivedStickerPacks, updatedPacks: { [weak self] packs in
self?.archivedPacks.set(.single(packs))
}))
}
case .passport:
self.controller?.push(SecureIdAuthController(context: self.context, mode: .list))
case .watch:
push(watchSettingsController(context: self.context))
case .support:
let supportPeer = Promise<EnginePeer.Id?>()
supportPeer.set(context.engine.peers.supportPeerId())
self.controller?.present(textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.Settings_FAQ_Intro, actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Settings_FAQ_Button, action: { [weak self] in
self?.openFaq()
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: { [weak self] in
guard let self else {
return
}
self.supportPeerDisposable.set((supportPeer.get() |> take(1) |> deliverOnMainQueue).startStrict(next: { [weak self] peerId in
if let strongSelf = self, let peerId = peerId {
push(strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(.default), params: nil))
}
}))
})]), in: .window(.root))
case .faq:
self.openFaq()
case .tips:
self.openTips()
case .phoneNumber:
guard let controller = self.controller, !controller.presentAccountFrozenInfoIfNeeded() else {
return
}
if case let .user(user) = self.data?.peer, let phoneNumber = user.phone {
let introController = PrivacyIntroController(context: self.context, mode: .changePhoneNumber(phoneNumber), proceedAction: { [weak self] in
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
navigationController.replaceTopController(ChangePhoneNumberController(context: strongSelf.context), animated: true)
}
})
push(introController)
}
case .username:
guard let controller = self.controller, !controller.presentAccountFrozenInfoIfNeeded() else {
return
}
push(usernameSetupController(context: self.context))
case .addAccount:
let _ = (activeAccountsAndPeers(context: context)
|> take(1)
|> deliverOnMainQueue
).startStandalone(next: { [weak self] accountAndPeer, accountsAndPeers in
guard let strongSelf = self else {
return
}
var maximumAvailableAccounts: Int = 3
if accountAndPeer?.1.isPremium == true && !strongSelf.context.account.testingEnvironment {
maximumAvailableAccounts = 4
}
var count: Int = 1
for (accountContext, peer, _) in accountsAndPeers {
if !accountContext.account.testingEnvironment {
if peer.isPremium {
maximumAvailableAccounts = 4
}
count += 1
}
}
if count >= maximumAvailableAccounts {
var replaceImpl: ((ViewController) -> Void)?
let controller = PremiumLimitScreen(context: strongSelf.context, subject: .accounts, count: Int32(count), action: {
let controller = PremiumIntroScreen(context: strongSelf.context, source: .accounts)
replaceImpl?(controller)
return true
})
replaceImpl = { [weak controller] c in
controller?.replace(with: c)
}
if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(controller)
}
} else {
strongSelf.context.sharedContext.beginNewAuth(testingEnvironment: strongSelf.context.account.testingEnvironment)
}
})
case .logout:
if case let .user(user) = self.data?.peer, let phoneNumber = user.phone {
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController {
self.controller?.push(logoutOptionsController(context: self.context, navigationController: navigationController, canAddAccounts: true, phoneNumber: phoneNumber))
}
}
case .rememberPassword:
let context = self.context
let controller = TwoFactorDataInputScreen(sharedContext: self.context.sharedContext, engine: .authorized(self.context.engine), mode: .rememberPassword(doneText: self.presentationData.strings.TwoFactorSetup_Done_Action), stateUpdated: { _ in
}, presentation: .modalInLargeLayout)
controller.twoStepAuthSettingsController = { configuration in
return twoStepVerificationUnlockSettingsController(context: context, mode: .access(intro: false, data: .single(TwoStepVerificationUnlockSettingsControllerData.access(configuration: TwoStepVerificationAccessConfiguration(configuration: configuration, password: nil)))))
}
controller.passwordRemembered = {
let _ = context.engine.notices.dismissServerProvidedSuggestion(suggestion: ServerProvidedSuggestion.validatePassword.id).startStandalone()
}
push(controller)
case .emojiStatus:
self.headerNode.invokeDisplayPremiumIntro()
case .profileColor:
self.interaction.editingOpenNameColorSetup()
case .powerSaving:
push(energySavingSettingsScreen(context: self.context))
case .businessSetup:
guard let controller = self.controller, !controller.presentAccountFrozenInfoIfNeeded() else {
return
}
push(self.context.sharedContext.makeBusinessSetupScreen(context: self.context))
case .premiumManagement:
guard let controller = self.controller else {
return
}
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
let url = premiumConfiguration.subscriptionManagementUrl
guard !url.isEmpty else {
return
}
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: !url.hasPrefix("tg://") && !url.contains("?start="), presentationData: self.context.sharedContext.currentPresentationData.with({$0}), navigationController: controller.navigationController as? NavigationController, dismissInput: {})
case .stars:
if let starsContext = self.controller?.starsContext {
push(self.context.sharedContext.makeStarsTransactionsScreen(context: self.context, starsContext: starsContext))
}
case .ton:
if let tonContext = self.controller?.tonContext {
push(self.context.sharedContext.makeStarsTransactionsScreen(context: self.context, starsContext: tonContext))
}
}
}
func setupFaqIfNeeded() {
if !self.didSetCachedFaq {
self.cachedFaq.set(.single(nil) |> then(cachedFaqInstantPage(context: self.context) |> map(Optional.init)))
self.didSetCachedFaq = true
}
}
func openFaq(anchor: String? = nil) {
self.setupFaqIfNeeded()
let presentationData = self.presentationData
let progressSignal = Signal<Never, NoError> { [weak self] subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
self?.controller?.present(controller, in: .window(.root))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let _ = (self.cachedFaq.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] resolvedUrl in
progressDisposable.dispose()
if let strongSelf = self, let resolvedUrl = resolvedUrl {
var resolvedUrl = resolvedUrl
if case let .instantView(webPage, _) = resolvedUrl, let customAnchor = anchor {
resolvedUrl = .instantView(webPage, customAnchor)
}
strongSelf.context.sharedContext.openResolvedUrl(resolvedUrl, context: strongSelf.context, urlContext: .generic, navigationController: strongSelf.controller?.navigationController as? NavigationController, forceExternal: false, forceUpdate: false, openPeer: { peer, navigation in
}, sendFile: nil, sendSticker: nil, sendEmoji: nil, requestMessageActionUrlAuth: nil, joinVoiceChat: nil, present: { [weak self] controller, arguments in
self?.controller?.push(controller)
}, dismissInput: {}, contentContext: nil, progress: nil, completion: nil)
}
})
}
private func openTips() {
let controller = OverlayStatusController(theme: self.presentationData.theme, type: .loading(cancelled: nil))
self.controller?.present(controller, in: .window(.root))
let context = self.context
let navigationController = self.controller?.navigationController as? NavigationController
self.tipsPeerDisposable.set((self.context.engine.peers.resolvePeerByName(name: self.presentationData.strings.Settings_TipsUsername, referrer: nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
|> deliverOnMainQueue).startStrict(next: { [weak controller] peer in
controller?.dismiss()
if let peer = peer, let navigationController = navigationController {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer)))
}
}))
}
}