Postbox -> TelegramEngine wave 15: SelectablePeerNode stateManager collapse

Applies the wave-11/12 AccountStateManager collapse pattern to
SelectablePeerNode. Module is now fully Postbox-free (source + BUILD).

- SelectablePeerNode.setup(... postbox: Postbox, network: Network, ...)
  -> setup(... stateManager: AccountStateManager, ...). Same for
  setupStoryRepost. AvatarNode.setPeer and EmojiStatusComponent are
  forwarded as stateManager.postbox / .network without naming Postbox.
- Three Namespaces.Peer.SecretChat == checks rewritten to use the
  existing PeerId.isSecretChat extension (as in wave 13).
- ShareControllerPeerGridItem.setup / setupStoryRepost and
  HorizontalPeerItem.setup call sites collapse to stateManager:.
  JoinLinkPreviewPeerContentNode uses the convenience
  setup(context: AccountContext) and is unchanged.
- Drop import Postbox in SelectablePeerNode.swift and the
  //submodules/Postbox:Postbox dep in SelectablePeerNode/BUILD.

The stateManager fallback (over the normally-preferred engine:
TelegramEngine) is used because SelectablePeerNode crosses the Share
Extension boundary: ShareControllerAccountContextExtension has no
Account, so TelegramEngine(account:) is physically unreachable there.
This matches the "rare but genuine fallback" clause of
feedback_postbox_refactor_handle.md.

Build verified green (debug_sim_arm64, 193 actions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
isaac 2026-04-20 20:37:41 +02:00
parent b20cd3502b
commit 46566e7155
5 changed files with 38 additions and 17 deletions

View file

@ -358,6 +358,31 @@ Net (wave 14 alone): 98 files changed, 0 insertions / 98 deletions.
Plan / record: (no plan doc this wave — mechanical sweep).
### Wave 15 outcome (2026-04-20)
Applies the wave-11/12 `stateManager: AccountStateManager` collapse pattern to `SelectablePeerNode` — another wave-1-era candidate listed in the post-wave-14 shortlist. Module becomes fully Postbox-free (source + BUILD).
**`SelectablePeerNode` fully Postbox-free.** Two public setup methods migrated:
- `setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, …)``setup(accountPeerId:, stateManager: AccountStateManager, …)`.
- `setupStoryRepost(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, …)``setupStoryRepost(accountPeerId:, stateManager: AccountStateManager, …)`.
Internal forwards rewired: `AvatarNode.setPeer(…, postbox: stateManager.postbox, network: stateManager.network, …)` and `EmojiStatusComponent(postbox: stateManager.postbox, …)`. Neither site names `Postbox` in the consumer — Swift infers through transitive module visibility.
**Namespaces.Peer.SecretChat fixup (×3).** Replaced `peer.peerId.namespace == Namespaces.Peer.SecretChat` checks with `peer.peerId.isSecretChat` at three sites, matching the wave-13 pattern (`PeerId.isSecretChat` at `TelegramCore/Sources/Utils/PeerUtils.swift:611`). The third site (`updateSelection` in the not-selected branch) additionally needed an `?? false` fallback — previous expression was `self.peer?.peerId.namespace == Namespaces.Peer.SecretChat` (optional-equals-non-optional produces `Bool`), new expression is `(self.peer?.peerId.isSecretChat ?? false)`.
**Share-Extension boundary — `stateManager:` over `engine:`.** `SelectablePeerNode` is used by `ShareControllerPeerGridItem`, whose context is `ShareControllerAccountContext`. That protocol exposes `stateManager: AccountStateManager` and `engineData: TelegramEngine.EngineData`, but **no `engine: TelegramEngine`** — and the Share Extension's `ShareControllerAccountContextExtension` concrete impl has no `Account`, so constructing a full `TelegramEngine` (`init(account: Account)`) is physically unreachable there. This is the documented "rare but genuine" fallback to `stateManager:` from the user-preference memory (`feedback_postbox_refactor_handle.md`) — prefer `engine:` except when crossing the Share-Extension boundary.
**Three external call sites migrated:**
- `HorizontalPeerItem/Sources/HorizontalPeerItem.swift:227` (wave 12's `stateManager:` field now forwards directly): `postbox: item.stateManager.postbox, network: item.stateManager.network``stateManager: item.stateManager`.
- `ShareController/Sources/ShareControllerPeerGridItem.swift:237` (setup): `postbox: context.stateManager.postbox, network: context.stateManager.network``stateManager: context.stateManager`.
- `ShareController/Sources/ShareControllerPeerGridItem.swift:273` (setupStoryRepost): same.
**Convenience init unchanged.** `setup(context: AccountContext, …)` now delegates with `stateManager: context.account.stateManager`; signature unchanged — `JoinLinkPreviewPeerContentNode.swift:147` (the one caller using the convenience init) needed no edit.
Net: 4 files changed, +12 / -17 lines. Build verified green (193 actions, 131s — Telegram.ipa target built successfully).
Plan / record: (no plan doc this wave — pattern-application, low-complexity).
### Modules currently free of `import Postbox` (running tally)
Consumer modules that no longer import Postbox, across all waves and standalone commits:
@ -376,6 +401,7 @@ Consumer modules that no longer import Postbox, across all waves and standalone
- `StorageUsageScreen` (waves 810)
- `ActionSheetPeerItem` (wave 11; revisits wave-1 abandonment)
- `HorizontalPeerItem` (wave 12; applies wave-11 pattern)
- `SelectablePeerNode` (wave 15; applies wave-11 pattern; ShareExtension-boundary stateManager fallback)
- `AttachmentTextInputPanelNode` BUILD cleanup (wave 13; source was already clean from wave 6)
- **Wave 14 BUILD-dep sweep: 98 modules' BUILDs cleaned** — same modules as the wave-6 batch; this sweep fixed their leftover `//submodules/Postbox:Postbox` BUILD deps. Candidate list in `/tmp/postbox-dep-candidates.txt` at commit time; derivable by the script in "Wave 14 outcome".

View file

@ -224,7 +224,7 @@ public final class HorizontalPeerItemNode: ListViewItemNode {
} else {
strongSelf.peerNode.compact = false
}
strongSelf.peerNode.setup(accountPeerId: item.accountPeerId, postbox: item.stateManager.postbox, network: item.stateManager.network, energyUsageSettings: item.energyUsageSettings, contentSettings: item.contentSettings, animationCache: item.animationCache, animationRenderer: item.animationRenderer, resolveInlineStickers: item.resolveInlineStickers, theme: item.theme, strings: item.strings, peer: EngineRenderedPeer(peer: item.peer), requiresPremiumForMessaging: false, numberOfLines: 1, synchronousLoad: synchronousLoads)
strongSelf.peerNode.setup(accountPeerId: item.accountPeerId, stateManager: item.stateManager, energyUsageSettings: item.energyUsageSettings, contentSettings: item.contentSettings, animationCache: item.animationCache, animationRenderer: item.animationRenderer, resolveInlineStickers: item.resolveInlineStickers, theme: item.theme, strings: item.strings, peer: EngineRenderedPeer(peer: item.peer), requiresPremiumForMessaging: false, numberOfLines: 1, synchronousLoad: synchronousLoads)
strongSelf.peerNode.frame = CGRect(origin: CGPoint(), size: itemLayout.size)
strongSelf.peerNode.updateSelection(selected: item.isPeerSelected(item.peer.id), animated: false)

View file

@ -12,7 +12,6 @@ swift_library(
deps = [
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/TelegramCore:TelegramCore",
"//submodules/Postbox:Postbox",
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/TelegramPresentationData:TelegramPresentationData",

View file

@ -3,7 +3,6 @@ import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import AvatarNode
@ -108,7 +107,7 @@ public final class SelectablePeerNode: ASDisplayNode {
didSet {
if !self.theme.isEqual(to: oldValue) {
if let peer = self.peer, let mainPeer = peer.chatMainPeer {
self.textNode.attributedText = NSAttributedString(string: mainPeer.debugDisplayTitle, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : (peer.peerId.namespace == Namespaces.Peer.SecretChat ? self.theme.secretTextColor : self.theme.textColor), paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: mainPeer.debugDisplayTitle, font: textFont, textColor: self.currentSelected ? self.theme.selectedTextColor : (peer.peerId.isSecretChat ? self.theme.secretTextColor : self.theme.textColor), paragraphAlignment: .center)
}
}
}
@ -159,8 +158,7 @@ public final class SelectablePeerNode: ASDisplayNode {
public func setup(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
self.setup(
accountPeerId: context.account.peerId,
postbox: context.account.postbox,
network: context.account.network,
stateManager: context.account.stateManager,
energyUsageSettings: context.sharedContext.energyUsageSettings,
contentSettings: context.currentContentSettings.with { $0 },
animationCache: context.animationCache,
@ -182,7 +180,7 @@ public final class SelectablePeerNode: ASDisplayNode {
)
}
public func setupStoryRepost(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool, storyMode: StoryMode) {
public func setupStoryRepost(accountPeerId: EnginePeer.Id, stateManager: AccountStateManager, theme: PresentationTheme, strings: PresentationStrings, synchronousLoad: Bool, storyMode: StoryMode) {
self.peer = nil
let title: String
@ -202,14 +200,14 @@ public final class SelectablePeerNode: ASDisplayNode {
self.textNode.maximumNumberOfLines = 2
self.textNode.attributedText = NSAttributedString(string: title, font: textFont, textColor: self.theme.textColor, paragraphAlignment: .center)
self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: ContentSettings.default, theme: theme, peer: nil, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: .round, synchronousLoad: synchronousLoad)
self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: stateManager.postbox, network: stateManager.network, contentSettings: ContentSettings.default, theme: theme, peer: nil, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: .round, synchronousLoad: synchronousLoad)
if case .repostIcon = overrideImage {
self.avatarNode.playRepostAnimation()
}
}
public func setup(accountPeerId: EnginePeer.Id, postbox: Postbox, network: Network, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
public func setup(accountPeerId: EnginePeer.Id, stateManager: AccountStateManager, energyUsageSettings: EnergyUsageSettings, contentSettings: ContentSettings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, resolveInlineStickers: @escaping ([Int64]) -> Signal<[Int64: TelegramMediaFile], NoError>, theme: PresentationTheme, strings: PresentationStrings, peer: EngineRenderedPeer, requiresPremiumForMessaging: Bool, requiresStars: Int64? = nil, customTitle: String? = nil, iconId: Int64? = nil, iconColor: Int32? = nil, online: Bool = false, numberOfLines: Int = 2, synchronousLoad: Bool) {
let isFirstTime = self.peer == nil
self.peer = peer
guard let mainPeer = peer.chatOrMonoforumMainPeer else {
@ -222,7 +220,7 @@ public final class SelectablePeerNode: ASDisplayNode {
if requiresPremiumForMessaging {
defaultColor = self.theme.textColor.withMultipliedAlpha(0.4)
} else {
defaultColor = peer.peerId.namespace == Namespaces.Peer.SecretChat ? self.theme.secretTextColor : self.theme.textColor
defaultColor = peer.peerId.isSecretChat ? self.theme.secretTextColor : self.theme.textColor
}
var isForum = false
@ -256,7 +254,7 @@ public final class SelectablePeerNode: ASDisplayNode {
} else {
clipStyle = .round
}
self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: postbox, network: network, contentSettings: contentSettings, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad)
self.avatarNode.setPeer(accountPeerId: accountPeerId, postbox: stateManager.postbox, network: stateManager.network, contentSettings: contentSettings, theme: theme, peer: mainPeer, overrideImage: overrideImage, emptyColor: self.theme.avatarPlaceholderColor, clipStyle: clipStyle, synchronousLoad: synchronousLoad)
if let requiresStars {
let avatarBadgeOutline: UIImageView
@ -373,7 +371,7 @@ public final class SelectablePeerNode: ASDisplayNode {
let iconSize = self.iconView.update(
transition: .easeInOut(duration: 0.2),
component: AnyComponent(EmojiStatusComponent(
postbox: postbox,
postbox: stateManager.postbox,
energyUsageSettings: energyUsageSettings,
resolveInlineStickers: resolveInlineStickers,
animationCache: animationCache,
@ -404,7 +402,7 @@ public final class SelectablePeerNode: ASDisplayNode {
self.currentSelected = selected
if let attributedText = self.textNode.attributedText {
self.textNode.attributedText = NSAttributedString(string: attributedText.string, font: textFont, textColor: selected ? self.theme.selectedTextColor : (self.peer?.peerId.namespace == Namespaces.Peer.SecretChat ? self.theme.secretTextColor : self.theme.textColor), paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: attributedText.string, font: textFont, textColor: selected ? self.theme.selectedTextColor : ((self.peer?.peerId.isSecretChat ?? false) ? self.theme.secretTextColor : self.theme.textColor), paragraphAlignment: .center)
}
var isForum = false

View file

@ -236,8 +236,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
let resolveInlineStickers = context.resolveInlineStickers
self.peerNode.setup(
accountPeerId: context.accountPeerId,
postbox: context.stateManager.postbox,
network: context.stateManager.network,
stateManager: context.stateManager,
energyUsageSettings: environment.energyUsageSettings,
contentSettings: context.contentSettings,
animationCache: context.animationCache,
@ -272,8 +271,7 @@ final class ShareControllerPeerGridItemNode: GridItemNode {
}
self.peerNode.setupStoryRepost(
accountPeerId: context.accountPeerId,
postbox: context.stateManager.postbox,
network: context.stateManager.network,
stateManager: context.stateManager,
theme: theme,
strings: strings,
synchronousLoad: synchronousLoad,