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

179 lines
7.6 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import TelegramPresentationData
import PeerInfoAvatarListNode
import SwiftSignalKit
import TelegramCore
import ContextUI
import AccountContext
import Display
final class PeerInfoAvatarListNode: ASDisplayNode {
private let isSettings: Bool
let containerNode: ASDisplayNode
let pinchSourceNode: PinchSourceContainerNode
let bottomCoverNode: ASDisplayNode
let maskNode: DynamicIslandMaskNode
let topCoverNode: DynamicIslandBlurNode
let avatarContainerNode: PeerInfoAvatarTransformContainerNode
let listContainerTransformNode: ASDisplayNode
let listContainerNode: PeerInfoAvatarListContainerNode
let isReady = Promise<Bool>()
var arguments: (EnginePeer?, Int64?, EngineMessageHistoryThread.Info?, PresentationTheme, CGFloat, Bool)?
var item: PeerInfoAvatarListItem?
var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
var animateOverlaysFadeIn: (() -> Void)?
var openStories: (() -> Void)?
init(context: AccountContext, readyWhenGalleryLoads: Bool, isSettings: Bool) {
self.isSettings = isSettings
self.containerNode = ASDisplayNode()
self.bottomCoverNode = ASDisplayNode()
self.maskNode = DynamicIslandMaskNode()
self.pinchSourceNode = PinchSourceContainerNode()
self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context)
self.listContainerTransformNode = ASDisplayNode()
self.listContainerNode = PeerInfoAvatarListContainerNode(context: context, isSettings: isSettings)
self.listContainerNode.clipsToBounds = true
self.listContainerNode.isHidden = true
self.topCoverNode = DynamicIslandBlurNode()
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.bottomCoverNode)
self.containerNode.addSubnode(self.pinchSourceNode)
self.pinchSourceNode.contentNode.addSubnode(self.avatarContainerNode)
self.listContainerTransformNode.addSubnode(self.listContainerNode)
self.pinchSourceNode.contentNode.addSubnode(self.listContainerTransformNode)
self.containerNode.addSubnode(self.topCoverNode)
let avatarReady = (self.avatarContainerNode.avatarNode.ready
|> mapToSignal { _ -> Signal<Bool, NoError> in
return .complete()
}
|> then(.single(true)))
let galleryReady = self.listContainerNode.isReady.get()
|> filter { value in
return value
}
|> take(1)
let combinedSignal: Signal<Bool, NoError>
if !"".isEmpty, readyWhenGalleryLoads {
combinedSignal = combineLatest(queue: .mainQueue(),
avatarReady,
galleryReady
)
|> map { lhs, rhs in
return lhs && rhs
}
} else {
combinedSignal = avatarReady
}
self.isReady.set(combinedSignal
|> filter { value in
return value
}
|> take(1))
self.listContainerNode.itemsUpdated = { [weak self] items in
if let strongSelf = self {
strongSelf.item = items.first
strongSelf.itemsUpdated?(items)
if let (peer, threadId, threadInfo, theme, avatarSize, isExpanded) = strongSelf.arguments {
strongSelf.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: strongSelf.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: strongSelf.isSettings)
}
}
}
self.pinchSourceNode.activate = { [weak self] sourceNode in
guard let strongSelf = self, let (_, _, _, _, _, isExpanded) = strongSelf.arguments, isExpanded else {
return
}
let pinchController = makePinchController(sourceNode: sourceNode, getContentAreaInScreenSpace: {
return UIScreen.main.bounds
})
context.sharedContext.mainWindow?.presentInGlobalOverlay(pinchController)
strongSelf.listContainerNode.bottomShadowNode.alpha = 0.0
strongSelf.listContainerNode.contentNode.updateIsInPinchMode(true)
}
self.pinchSourceNode.animatedOut = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animateOverlaysFadeIn?()
strongSelf.listContainerNode.contentNode.updateIsInPinchMode(false)
}
self.listContainerNode.openStories = { [weak self] in
guard let self else {
return
}
self.openStories?()
}
}
func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: EnginePeer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded)
self.maskNode.isForum = isForum
self.pinchSourceNode.update(size: size, transition: transition)
self.containerNode.frame = CGRect(origin: CGPoint(), size: size)
self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size)
self.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.listContainerNode.isHidden {
if let result = self.listContainerNode.view.hitTest(self.view.convert(point, to: self.listContainerNode.view), with: event) {
return result
}
} else {
if let result = self.avatarContainerNode.avatarNode.view.hitTest(self.view.convert(point, to: self.avatarContainerNode.avatarNode.view), with: event) {
return result
} else if let result = self.avatarContainerNode.iconView?.view?.hitTest(self.view.convert(point, to: self.avatarContainerNode.iconView?.view), with: event) {
return result
}
}
return super.hitTest(point, with: event)
}
func animateAvatarCollapse(transition: ContainedViewLayoutTransition) {
if let currentItemNode = self.listContainerNode.currentItemNode, case .animated = transition {
if let _ = self.avatarContainerNode.videoNode {
} else if let _ = self.avatarContainerNode.markupNode {
} else if let unroundedImage = self.avatarContainerNode.avatarNode.unroundedImage {
let avatarCopyView = UIImageView()
avatarCopyView.image = unroundedImage
avatarCopyView.frame = self.avatarContainerNode.avatarNode.frame
avatarCopyView.center = currentItemNode.imageNode.position
currentItemNode.view.addSubview(avatarCopyView)
let scale = currentItemNode.imageNode.bounds.height / avatarCopyView.bounds.height
avatarCopyView.layer.transform = CATransform3DMakeScale(scale, scale, scale)
avatarCopyView.alpha = 0.0
transition.updateAlpha(layer: avatarCopyView.layer, alpha: 1.0, completion: { [weak avatarCopyView] _ in
Queue.mainQueue().after(0.1, {
avatarCopyView?.removeFromSuperview()
})
})
}
}
}
}