mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-05 19:28:46 +02:00
Squash of 63 commits spanning waves 46-93 (plus interspersed docs commits) of the gradual Postbox->TelegramEngine consumer-side migration. Scope: 139 files changed, 2123 insertions(+), 452 deletions(-). ## Themes by wave-block **Waves 46-58 — Peer field migrations + facade additions** Foundational EnginePeer convenience init additions (PeerReference, RenderedPeer, SelectivePrivacyPeer). Multiple `peer: Peer` field migrations across PeerInfo, ChatList, and SettingsUI components. **Waves 59-73 — peer field cascade + EnginePeer wrap drops** Series of single- to two-file peer-field migrations; consumer-side wrap removal (`EnginePeer(peer)` -> direct EnginePeer use); `as? TelegramUser` cast conversion to `case let .user(...)` enum match. Wave 64: RenderedPeer convenience init. Wave 68: SelectivePrivacyPeer convenience init. **Waves 74-83 — controller-Node bridge cleanup + small migrations** Wave-71 shadow-pattern cleanup at controller->Node bridges. Migrations of ChatRecentActionsController.peer (74), PeerInfoMember (75), MentionChatInputPanelItem (76), PassportUI SecureIdAuthController (77), AccountWithInfo + ShareController (78), peerInputActivitiesPromise (79), InactiveChannel (80), BlockedPeers (81), openHashtag resolveSignal (82), NotificationExceptionsList (83). **Waves 84-90 — TelegramEngine.Resources facade migrations** Per-method Shape-A/B sweeps converting `<ctx>.account.postbox.mediaBox.X(...)` to `<ctx>.engine.resources.X(...)`. Wave 90 was a single-commit big sweep: 40 fetchedMediaResource sites in 25 files migrated to engine.resources.fetch facade in one atomic pass with first-pass-clean build. Methods covered: storeResourceData, completedResourcePath, cancelInteractiveResourceFetch, resourceRangesStatus, resourceStatus, fetch (fetchedMediaResource). **Waves 91-92 — additional type migrations** Wave 91: ItemListWebsiteItem.peer + RecentSessionsController enum-case payload + openWebSession callback Peer? -> EnginePeer?. Wave 92: ChatListController StateHolder.EntryContext status type MediaResourceStatus -> EngineMediaResource.FetchStatus. **Wave 93 — speculative `import Postbox` drop sweep** Drop import from 7 wave-touched files where it became unused; restore in 5 files where bare PeerId/Message/MediaId/StoryId references escaped the pre-flight regex. Includes one MediaId(...) -> EngineMedia.Id(...) swap in InAppPurchaseManager to unlock its import drop. ## Build state Final state at squash: clean Telegram/Telegram build at debug_sim_arm64. ## Persistent-state notes - Pre-existing WIP unchanged across the squashed range: - build-system/bazel-rules/sourcekit-bazel-bsp submodule marker - Untracked: build-system/tulsi/, submodules/TgVoip/, third-party/libx264/ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
235 lines
12 KiB
Swift
235 lines
12 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import TelegramPresentationData
|
|
import AccountContext
|
|
import AvatarNode
|
|
import UniversalMediaPlayer
|
|
import PeerInfoAvatarListNode
|
|
import AvatarVideoNode
|
|
import TelegramUniversalVideoContent
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import Display
|
|
import GalleryUI
|
|
|
|
final class PeerInfoEditingAvatarNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
let avatarNode: AvatarNode
|
|
fileprivate var videoNode: UniversalVideoNode?
|
|
fileprivate var markupNode: AvatarVideoNode?
|
|
private var videoContent: NativeVideoContent?
|
|
private var videoStartTimestamp: Double?
|
|
var item: PeerInfoAvatarListItem?
|
|
|
|
var tapped: ((Bool) -> Void)?
|
|
|
|
var canAttachVideo: Bool = true
|
|
|
|
init(context: AccountContext) {
|
|
self.context = context
|
|
let avatarFont = avatarPlaceholderFont(size: floor(100.0 * 16.0 / 37.0))
|
|
self.avatarNode = AvatarNode(font: avatarFont)
|
|
|
|
super.init()
|
|
|
|
self.addSubnode(self.avatarNode)
|
|
self.avatarNode.frame = CGRect(origin: CGPoint(x: -50.0, y: -50.0), size: CGSize(width: 100.0, height: 100.0))
|
|
|
|
self.avatarNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
|
}
|
|
|
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
self.tapped?(false)
|
|
}
|
|
}
|
|
|
|
func reset() {
|
|
guard let videoNode = self.videoNode else {
|
|
return
|
|
}
|
|
videoNode.isHidden = true
|
|
videoNode.seek(self.videoStartTimestamp ?? 0.0)
|
|
Queue.mainQueue().after(0.15) {
|
|
videoNode.isHidden = false
|
|
}
|
|
}
|
|
|
|
var removedPhotoResourceIds = Set<String>()
|
|
func update(peer: EnginePeer?, threadData: MessageHistoryThreadData?, chatLocation: ChatLocation, item: PeerInfoAvatarListItem?, updatingAvatar: PeerInfoUpdatingAvatar?, uploadProgress: AvatarUploadProgress?, theme: PresentationTheme, avatarSize: CGFloat, isEditing: Bool) {
|
|
guard let peer = peer else {
|
|
return
|
|
}
|
|
|
|
let canEdit = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)
|
|
|
|
let previousItem = self.item
|
|
var item = item
|
|
self.item = item
|
|
|
|
let overrideImage: AvatarNodeImageOverride?
|
|
if canEdit, peer.profileImageRepresentations.isEmpty {
|
|
overrideImage = .editAvatarIcon(forceNone: true)
|
|
} else if let previousItem = previousItem, item == nil {
|
|
if case let .image(_, representations, _, _, _, _) = previousItem, let rep = representations.last {
|
|
self.removedPhotoResourceIds.insert(rep.representation.resource.id.stringRepresentation)
|
|
}
|
|
overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none
|
|
item = nil
|
|
} else if let representation = peer.profileImageRepresentations.last, self.removedPhotoResourceIds.contains(representation.resource.id.stringRepresentation) {
|
|
overrideImage = canEdit ? .editAvatarIcon(forceNone: true) : AvatarNodeImageOverride.none
|
|
item = nil
|
|
} else {
|
|
overrideImage = item == nil && canEdit ? .editAvatarIcon(forceNone: true) : nil
|
|
}
|
|
self.avatarNode.font = avatarPlaceholderFont(size: floor(avatarSize * 16.0 / 37.0))
|
|
self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
|
self.avatarNode.frame = CGRect(origin: CGPoint(x: -avatarSize / 2.0, y: -avatarSize / 2.0), size: CGSize(width: avatarSize, height: avatarSize))
|
|
|
|
var isForum = false
|
|
let avatarCornerRadius: CGFloat
|
|
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
|
|
isForum = true
|
|
avatarCornerRadius = floor(avatarSize * 0.25)
|
|
} else {
|
|
avatarCornerRadius = avatarSize / 2.0
|
|
}
|
|
if self.avatarNode.layer.cornerRadius != 0.0 {
|
|
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut).updateCornerRadius(layer: self.avatarNode.layer, cornerRadius: avatarCornerRadius)
|
|
} else {
|
|
self.avatarNode.layer.cornerRadius = avatarCornerRadius
|
|
}
|
|
self.avatarNode.layer.masksToBounds = true
|
|
|
|
if let item = item {
|
|
let representations: [ImageRepresentationWithReference]
|
|
let videoRepresentations: [VideoRepresentationWithReference]
|
|
let immediateThumbnailData: Data?
|
|
var videoId: Int64
|
|
let markup: TelegramMediaImage.EmojiMarkup?
|
|
switch item {
|
|
case .custom:
|
|
representations = []
|
|
videoRepresentations = []
|
|
immediateThumbnailData = nil
|
|
videoId = 0
|
|
markup = nil
|
|
case let .topImage(topRepresentations, videoRepresentationsValue, immediateThumbnail):
|
|
representations = topRepresentations
|
|
videoRepresentations = videoRepresentationsValue
|
|
immediateThumbnailData = immediateThumbnail
|
|
videoId = peer.id.id._internalGetInt64Value()
|
|
if let resource = videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource {
|
|
videoId = videoId &+ resource.photoId
|
|
}
|
|
markup = nil
|
|
case let .image(reference, imageRepresentations, videoRepresentationsValue, immediateThumbnail, _, markupValue):
|
|
representations = imageRepresentations
|
|
videoRepresentations = videoRepresentationsValue
|
|
immediateThumbnailData = immediateThumbnail
|
|
if case let .cloud(imageId, _, _) = reference {
|
|
videoId = imageId
|
|
} else {
|
|
videoId = peer.id.id._internalGetInt64Value()
|
|
}
|
|
markup = markupValue
|
|
}
|
|
|
|
if let markup {
|
|
if let videoNode = self.videoNode {
|
|
self.videoContent = nil
|
|
self.videoStartTimestamp = nil
|
|
self.videoNode = nil
|
|
|
|
videoNode.removeFromSupernode()
|
|
}
|
|
|
|
let markupNode: AvatarVideoNode
|
|
if let current = self.markupNode {
|
|
markupNode = current
|
|
} else {
|
|
markupNode = AvatarVideoNode(context: self.context)
|
|
self.avatarNode.contentNode.addSubnode(markupNode)
|
|
self.markupNode = markupNode
|
|
}
|
|
markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0))
|
|
markupNode.updateVisibility(true)
|
|
} else if threadData == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) {
|
|
if let markupNode = self.markupNode {
|
|
self.markupNode = nil
|
|
markupNode.removeFromSupernode()
|
|
}
|
|
|
|
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
|
|
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil)
|
|
if videoContent.id != self.videoContent?.id {
|
|
self.videoNode?.removeFromSupernode()
|
|
|
|
let mediaManager = self.context.sharedContext.mediaManager
|
|
let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .gallery)
|
|
videoNode.isUserInteractionEnabled = false
|
|
self.videoStartTimestamp = video.representation.startTimestamp
|
|
self.videoContent = videoContent
|
|
self.videoNode = videoNode
|
|
|
|
let maskPath: UIBezierPath
|
|
if isForum {
|
|
maskPath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size), cornerRadius: avatarCornerRadius)
|
|
} else {
|
|
maskPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(), size: self.avatarNode.frame.size))
|
|
}
|
|
let shape = CAShapeLayer()
|
|
shape.path = maskPath.cgPath
|
|
videoNode.layer.mask = shape
|
|
|
|
self.avatarNode.contentNode.addSubnode(videoNode)
|
|
}
|
|
} else {
|
|
if let markupNode = self.markupNode {
|
|
self.markupNode = nil
|
|
markupNode.removeFromSupernode()
|
|
}
|
|
if let videoNode = self.videoNode {
|
|
self.videoStartTimestamp = nil
|
|
self.videoContent = nil
|
|
self.videoNode = nil
|
|
|
|
DispatchQueue.main.async {
|
|
videoNode.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
} else if let videoNode = self.videoNode {
|
|
self.videoStartTimestamp = nil
|
|
self.videoContent = nil
|
|
self.videoNode = nil
|
|
|
|
videoNode.removeFromSupernode()
|
|
}
|
|
|
|
if let markupNode = self.markupNode {
|
|
markupNode.frame = self.avatarNode.bounds
|
|
markupNode.updateLayout(size: self.avatarNode.bounds.size, cornerRadius: avatarCornerRadius, transition: .immediate)
|
|
}
|
|
|
|
if let videoNode = self.videoNode {
|
|
if self.canAttachVideo {
|
|
videoNode.updateLayout(size: self.avatarNode.bounds.size, transition: .immediate)
|
|
}
|
|
videoNode.frame = self.avatarNode.bounds
|
|
|
|
if isEditing != videoNode.canAttachContent {
|
|
videoNode.canAttachContent = isEditing && self.canAttachVideo
|
|
}
|
|
}
|
|
}
|
|
|
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
if self.avatarNode.frame.contains(point) {
|
|
return self.avatarNode.view
|
|
}
|
|
return super.hitTest(point, with: event)
|
|
}
|
|
}
|