Telegram-iOS/submodules/PeerAvatarGalleryUI/Sources/PeerAvatarImageGalleryItem.swift
isaac d1aa0db537 Postbox -> TelegramEngine waves 46-93 (squashed)
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>
2026-04-25 20:48:15 +04:00

628 lines
36 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import AccountContext
import RadialStatusNode
import PhotoResources
import GalleryUI
import TelegramUniversalVideoContent
import UndoUI
private struct PeerAvatarImageGalleryThumbnailItem: GalleryThumbnailItem {
let account: Account
let peer: EnginePeer
let content: [ImageRepresentationWithReference]
init(account: Account, peer: EnginePeer, content: [ImageRepresentationWithReference]) {
self.account = account
self.peer = peer
self.content = content
}
func image(synchronous: Bool) -> (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) {
if let representation = largestImageRepresentation(self.content.map({ $0.representation })) {
return (avatarGalleryThumbnailPhoto(account: self.account, representations: self.content, synchronousLoad: synchronous), representation.dimensions.cgSize)
} else {
return (.single({ _ in return nil }), CGSize(width: 128.0, height: 128.0))
}
}
func isEqual(to: GalleryThumbnailItem) -> Bool {
if let to = to as? PeerAvatarImageGalleryThumbnailItem {
return self.content == to.content
} else {
return false
}
}
}
class PeerAvatarImageGalleryItem: GalleryItem {
var id: AnyHashable {
return self.entry.id
}
let context: AccountContext
let peer: EnginePeer
let presentationData: PresentationData
let entry: AvatarGalleryEntry
let sourceCorners: AvatarGalleryController.SourceCorners
let delete: (() -> Void)?
let setMain: (() -> Void)?
let edit: (() -> Void)?
init(context: AccountContext, peer: EnginePeer, presentationData: PresentationData, entry: AvatarGalleryEntry, sourceCorners: AvatarGalleryController.SourceCorners, delete: (() -> Void)?, setMain: (() -> Void)?, edit: (() -> Void)?) {
self.context = context
self.peer = peer
self.presentationData = presentationData
self.entry = entry
self.sourceCorners = sourceCorners
self.delete = delete
self.setMain = setMain
self.edit = edit
}
func node(synchronous: Bool) -> GalleryItemNode {
let node = PeerAvatarImageGalleryItemNode(context: self.context, presentationData: self.presentationData, peer: self.peer, sourceCorners: self.sourceCorners)
if let indexData = self.entry.indexData {
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").string))
}
node.setEntry(self.entry, synchronous: synchronous)
node.footerContentNode.delete = self.delete
node.footerContentNode.setMain = self.setMain
node.edit = self.edit
return node
}
func updateNode(node: GalleryItemNode, synchronous: Bool) {
if let node = node as? PeerAvatarImageGalleryItemNode {
if let indexData = self.entry.indexData {
node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").string))
}
let previousContentAnimations = node.imageNode.contentAnimations
if synchronous {
node.imageNode.contentAnimations = []
}
node.setEntry(self.entry, synchronous: synchronous)
if synchronous {
node.imageNode.contentAnimations = previousContentAnimations
}
node.footerContentNode.delete = self.delete
node.footerContentNode.setMain = self.setMain
node.edit = self.edit
}
}
func thumbnailItem() -> (Int64, GalleryThumbnailItem)? {
let content: [ImageRepresentationWithReference]
switch self.entry {
case let .topImage(representations, _, _, _, _, _):
content = representations
case let .image(_, _, representations, _, _, _, _, _, _, _, _, _):
content = representations
}
return (0, PeerAvatarImageGalleryThumbnailItem(account: self.context.account, peer: self.peer, content: content))
}
}
private class PeerAvatarImageGalleryContentNode: ASDisplayNode {
override func layout() {
super.layout()
if let subnodes = self.subnodes {
for node in subnodes {
node.frame = self.bounds
}
}
}
}
final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
private let context: AccountContext
private let presentationData: PresentationData
private let peer: EnginePeer
private let sourceCorners: AvatarGalleryController.SourceCorners
private var entry: AvatarGalleryEntry?
private let contentNode: PeerAvatarImageGalleryContentNode
fileprivate let imageNode: TransformImageNode
private var videoNode: UniversalVideoNode?
private var videoContent: NativeVideoContent?
private var videoStartTimestamp: Double?
fileprivate let _ready = Promise<Void>()
fileprivate let _title = Promise<String>()
fileprivate let _rightBarButtonItems = Promise<[UIBarButtonItem]?>()
private let statusNodeContainer: HighlightableButtonNode
private let statusNode: RadialStatusNode
fileprivate let footerContentNode: AvatarGalleryItemFooterContentNode
private let fetchDisposable = MetaDisposable()
private let statusDisposable = MetaDisposable()
private var status: EngineMediaResource.FetchStatus?
private let playbackStatusDisposable = MetaDisposable()
fileprivate var edit: (() -> Void)?
init(context: AccountContext, presentationData: PresentationData, peer: EnginePeer, sourceCorners: AvatarGalleryController.SourceCorners) {
self.context = context
self.presentationData = presentationData
self.peer = peer
self.sourceCorners = sourceCorners
self.contentNode = PeerAvatarImageGalleryContentNode()
self.imageNode = TransformImageNode()
self.footerContentNode = AvatarGalleryItemFooterContentNode(context: context, presentationData: presentationData)
self.statusNodeContainer = HighlightableButtonNode()
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5))
self.statusNode.isHidden = true
super.init()
self.contentNode.addSubnode(self.imageNode)
self.imageNode.contentAnimations = .subsequentUpdates
self.imageNode.view.contentMode = .scaleAspectFill
self.imageNode.clipsToBounds = true
self.statusNodeContainer.addSubnode(self.statusNode)
self.addSubnode(self.statusNodeContainer)
self.statusNodeContainer.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside)
self.statusNodeContainer.isUserInteractionEnabled = false
self.footerContentNode.share = { [weak self] interaction in
if let strongSelf = self, let entry = strongSelf.entry, !entry.representations.isEmpty {
let subject: ShareControllerSubject
var actionCompletionText: String?
if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer) {
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
subject = .media(videoFileReference.abstract, nil)
actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved
} else {
subject = .image(entry.representations)
actionCompletionText = strongSelf.presentationData.strings.Gallery_ImageSaved
}
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
var forceTheme: PresentationTheme?
if !presentationData.theme.overallDarkAppearance {
forceTheme = defaultDarkColorPresentationTheme
}
let shareController = strongSelf.context.sharedContext.makeShareController(context: strongSelf.context, params: ShareControllerParams(subject: subject, preferredAction: .saveToCameraRoll, forceTheme: forceTheme, actionCompleted: {
if let actionCompletionText = actionCompletionText {
interaction.presentController(UndoOverlayController(presentationData: presentationData, content: .mediaSaved(text: actionCompletionText), elevatedLayout: true, animateInAsReplacement: false, action: { _ in return true }), nil)
}
}))
interaction.presentController(shareController, nil)
}
}
}
deinit {
self.fetchDisposable.dispose()
self.statusDisposable.dispose()
self.playbackStatusDisposable.dispose()
}
override func ready() -> Signal<Void, NoError> {
return self._ready.get()
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
let statusSize = CGSize(width: 50.0, height: 50.0)
transition.updateFrame(node: self.statusNodeContainer, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - statusSize.width) / 2.0), y: floor((layout.size.height - statusSize.height) / 2.0)), size: statusSize))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(), size: statusSize))
}
fileprivate func setEntry(_ entry: AvatarGalleryEntry, synchronous: Bool) {
let previousRepresentations = self.entry?.representations
let previousVideoRepresentations = self.entry?.videoRepresentations
if self.entry != entry {
self.entry = entry
var barButtonItems: [UIBarButtonItem] = []
let footerContent: AvatarGalleryItemFooterContent = .info
if self.peer.id == self.context.account.peerId {
let rightBarButtonItem = UIBarButtonItem(title: entry.videoRepresentations.isEmpty ? self.presentationData.strings.Settings_EditPhoto : self.presentationData.strings.Settings_EditVideo, style: .plain, target: self, action: #selector(self.editPressed))
barButtonItems.append(rightBarButtonItem)
}
self._rightBarButtonItems.set(.single(barButtonItems))
self.footerContentNode.setEntry(entry, content: footerContent)
if let largestSize = largestImageRepresentation(entry.representations.map({ $0.representation })) {
let displaySize = largestSize.dimensions.cgSize.fitted(CGSize(width: 1280.0, height: 1280.0)).dividedByScreenScale().integralFloor
self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: displaySize, boundingSize: displaySize, intrinsicInsets: UIEdgeInsets()))()
let representations = entry.representations
if representations.last != previousRepresentations?.last {
self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.context.account, representations: representations, immediateThumbnailData: entry.immediateThumbnailData, attemptSynchronously: synchronous), attemptSynchronously: synchronous, dispatchOnDisplayLink: false)
if entry.videoRepresentations.isEmpty {
self.imageNode.imageUpdated = { [weak self] _ in
self?._ready.set(.single(Void()))
}
}
}
self.zoomableContent = (largestSize.dimensions.cgSize, self.contentNode)
if let largestIndex = representations.firstIndex(where: { $0.representation == largestSize }) {
self.fetchDisposable.set(self.context.engine.resources.fetch(reference: representations[largestIndex].reference, userLocation: .other, userContentType: .image).start())
}
var id: Int64
var category: String?
if case let .image(mediaId, _, _, _, _, _, _, _, _, categoryValue, _, _) = entry {
id = mediaId.id
category = categoryValue
} else {
id = Int64(entry.peer?.id.id._internalGetInt64Value() ?? 0)
if let resource = entry.videoRepresentations.first?.representation.resource as? CloudPhotoSizeMediaResource {
id = id &+ resource.photoId
}
}
if let video = entry.videoRepresentations.last, let peerReference = PeerReference(self.peer) {
if video != previousVideoRepresentations?.last {
let mediaManager = self.context.sharedContext.mediaManager
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.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(id, category), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: true, useLargeThumbnail: true, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil)
let videoNode = UniversalVideoNode(context: self.context, postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .overlay)
videoNode.isUserInteractionEnabled = false
videoNode.isHidden = true
self.videoStartTimestamp = video.representation.startTimestamp
self.videoContent = videoContent
self.videoNode = videoNode
self.playVideoIfCentral()
videoNode.updateLayout(size: largestSize.dimensions.cgSize, transition: .immediate)
self.contentNode.addSubnode(videoNode)
self._ready.set(videoNode.ready)
}
} else if let videoNode = self.videoNode {
self.videoContent = nil
self.videoNode = nil
Queue.mainQueue().after(0.1) {
videoNode.removeFromSupernode()
}
}
self.imageNode.frame = self.contentNode.bounds
self.videoNode?.frame = self.contentNode.bounds
} else {
self._ready.set(.single(Void()))
}
}
}
private func playVideoIfCentral() {
guard let videoNode = self.videoNode, self.isCentral else {
return
}
if let videoStartTimestamp = self.videoStartTimestamp {
videoNode.isHidden = true
self.playbackStatusDisposable.set((videoNode.status
|> castError(Bool.self)
|> mapToSignal { status -> Signal<Bool, Bool> in
if let status = status, case .playing = status.status {
if videoStartTimestamp > 0.0 && videoStartTimestamp > status.duration - 1.0 {
return .fail(true)
}
return .single(true)
} else {
return .single(false)
}
}
|> filter { playing in
return playing
}
|> take(1)
|> deliverOnMainQueue).start(error: { [weak self] _ in
if let strongSelf = self {
if let _ = strongSelf.videoNode {
videoNode.seek(0.0)
Queue.mainQueue().after(0.1) {
strongSelf.videoNode?.layer.allowsGroupOpacity = true
strongSelf.videoNode?.alpha = 0.0
strongSelf.videoNode?.isHidden = false
strongSelf.videoNode?.alpha = 1.0
strongSelf.videoNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.01)
}
}
}
}, completed: { [weak self] in
if let strongSelf = self {
Queue.mainQueue().after(0.1) {
strongSelf.videoNode?.isHidden = false
}
}
}))
} else {
self.playbackStatusDisposable.set(nil)
videoNode.isHidden = false
}
let hadAttachedContent = videoNode.hasAttachedContext
videoNode.canAttachContent = true
if videoNode.hasAttachedContext {
if let startTimestamp = self.videoStartTimestamp, !hadAttachedContent {
videoNode.seek(startTimestamp)
}
videoNode.play()
}
}
var isCentral = false
override func centralityUpdated(isCentral: Bool) {
super.centralityUpdated(isCentral: isCentral)
if self.isCentral != isCentral {
self.isCentral = isCentral
if isCentral {
self.playVideoIfCentral()
} else if let videoNode = self.videoNode {
videoNode.pause()
if let startTimestamp = self.videoStartTimestamp {
videoNode.seek(startTimestamp)
} else {
videoNode.seek(0.0)
}
videoNode.isHidden = true
}
}
}
override func animateIn(from node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.contentNode.view)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.contentNode.view.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewFinalFrame = self.contentNode.view.convert(self.contentNode.view.bounds, to: self.view)
let scaledLocalImageViewBounds = self.contentNode.view.bounds
let copyViewContents = node.2().0!
let copyView = UIView()
copyView.addSubview(copyViewContents)
copyViewContents.frame = CGRect(origin: CGPoint(x: (transformedSelfFrame.width - copyViewContents.frame.width) / 2.0, y: (transformedSelfFrame.height - copyViewContents.frame.height) / 2.0), size: copyViewContents.frame.size)
copyView.layer.sublayerTransform = CATransform3DMakeScale(transformedSelfFrame.width / copyViewContents.frame.width, transformedSelfFrame.height / copyViewContents.frame.height, 1.0)
let surfaceCopyViewContents = node.2().0!
let surfaceCopyView = UIView()
surfaceCopyView.addSubview(surfaceCopyViewContents)
addToTransitionSurface(surfaceCopyView)
var transformedSurfaceFrame: CGRect?
var transformedSurfaceFinalFrame: CGRect?
if let contentSurface = surfaceCopyView.superview {
transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface)
transformedSurfaceFinalFrame = self.contentNode.view.convert(scaledLocalImageViewBounds, to: contentSurface)
}
if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceFinalFrame = transformedSurfaceFinalFrame {
surfaceCopyViewContents.frame = CGRect(origin: CGPoint(x: (transformedSurfaceFrame.width - surfaceCopyViewContents.frame.width) / 2.0, y: (transformedSurfaceFrame.height - surfaceCopyViewContents.frame.height) / 2.0), size: surfaceCopyViewContents.frame.size)
surfaceCopyView.layer.sublayerTransform = CATransform3DMakeScale(transformedSurfaceFrame.width / surfaceCopyViewContents.frame.width, transformedSurfaceFrame.height / surfaceCopyViewContents.frame.height, 1.0)
surfaceCopyView.frame = transformedSurfaceFrame
surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), to: CGPoint(x: transformedSurfaceFinalFrame.midX, y: transformedSurfaceFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedSurfaceFinalFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceFrame.size.height / transformedSelfFrame.size.height)
surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
surfaceCopyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak surfaceCopyView] _ in
surfaceCopyView?.removeFromSuperview()
})
}
if case .round = self.sourceCorners {
self.view.insertSubview(copyView, belowSubview: self.scrollNode.view)
}
copyView.frame = transformedSelfFrame
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
copyView.layer.animatePosition(from: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), to: CGPoint(x: transformedCopyViewFinalFrame.midX, y: transformedCopyViewFinalFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewFinalFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewFinalFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DIdentity), to: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false)
self.contentNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.contentNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
completion()
})
if let _ = self.videoNode {
self.contentNode.view.superview?.bringSubviewToFront(self.contentNode.view)
} else {
self.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.07)
}
transformedFrame.origin = CGPoint()
//self.imageNode.layer.animateBounds(from: transformedFrame, to: self.imageNode.layer.bounds, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
let transform = CATransform3DScale(self.contentNode.layer.transform, transformedFrame.size.width / self.contentNode.layer.bounds.size.width, transformedFrame.size.height / self.contentNode.layer.bounds.size.height, 1.0)
self.contentNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.contentNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25)
self.contentNode.clipsToBounds = true
if case .round = self.sourceCorners {
self.contentNode.layer.animate(from: (self.contentNode.frame.width / 2.0) as NSNumber, to: 0.0 as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18, removeOnCompletion: false, completion: { [weak self] value in
if value {
self?.contentNode.clipsToBounds = false
}
})
} else if case let .roundRect(cornerRadius) = self.sourceCorners {
let scale = scaledLocalImageViewBounds.width / transformedCopyViewFinalFrame.width
let selfScale = transformedCopyViewFinalFrame.width / transformedSelfFrame.width
self.contentNode.layer.animate(from: (cornerRadius * scale * selfScale) as NSNumber, to: 0.0 as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18, removeOnCompletion: false, completion: { [weak self] value in
if value {
self?.contentNode.clipsToBounds = false
}
})
} else {
self.contentNode.clipsToBounds = false
}
self.statusNodeContainer.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.statusNodeContainer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusNodeContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
self.statusNodeContainer.layer.animateScale(from: 0.5, to: 1.0, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
}
override func animateOut(to node: (ASDisplayNode, CGRect, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) {
var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.contentNode.view)
let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.contentNode.view.superview)
let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view)
let transformedCopyViewInitialFrame = self.contentNode.view.convert(self.contentNode.view.bounds, to: self.view)
let scaledLocalImageViewBounds = self.contentNode.view.bounds
var positionCompleted = false
var boundsCompleted = false
var copyCompleted = false
let (maybeCopyView, copyViewBackground) = node.2()
copyViewBackground?.alpha = 1.0
let copyView = maybeCopyView!
var sourceHasRoundCorners = false
if case .none = self.sourceCorners {
} else {
sourceHasRoundCorners = true
}
if sourceHasRoundCorners {
self.view.insertSubview(copyView, belowSubview: self.scrollNode.view)
}
copyView.frame = transformedSelfFrame
let surfaceCopyView = node.2().0!
if !sourceHasRoundCorners {
addToTransitionSurface(surfaceCopyView)
}
var transformedSurfaceFrame: CGRect?
var transformedSurfaceCopyViewInitialFrame: CGRect?
if let contentSurface = surfaceCopyView.superview {
transformedSurfaceFrame = node.0.view.convert(node.0.view.bounds, to: contentSurface)
transformedSurfaceCopyViewInitialFrame = self.contentNode.view.convert(self.contentNode.view.bounds, to: contentSurface)
}
let durationFactor = 1.0
let intermediateCompletion = { [weak copyView, weak surfaceCopyView] in
if positionCompleted && boundsCompleted && copyCompleted {
copyView?.removeFromSuperview()
surfaceCopyView?.removeFromSuperview()
completion()
}
}
if let transformedSurfaceFrame = transformedSurfaceFrame, let transformedSurfaceCopyViewInitialFrame = transformedSurfaceCopyViewInitialFrame {
surfaceCopyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1 * durationFactor, removeOnCompletion: false)
surfaceCopyView.layer.animatePosition(from: CGPoint(x: transformedSurfaceCopyViewInitialFrame.midX, y: transformedSurfaceCopyViewInitialFrame.midY), to: CGPoint(x: transformedSurfaceFrame.midX, y: transformedSurfaceFrame.midY), duration: 0.25 * durationFactor, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedSurfaceCopyViewInitialFrame.size.width / transformedSurfaceFrame.size.width, height: transformedSurfaceCopyViewInitialFrame.size.height / transformedSurfaceFrame.size.height)
surfaceCopyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25 * durationFactor, removeOnCompletion: false, completion: { _ in
intermediateCompletion()
})
}
copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1 * durationFactor, removeOnCompletion: false)
copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25 * durationFactor, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
let scale = CGSize(width: transformedCopyViewInitialFrame.size.width / transformedSelfFrame.size.width, height: transformedCopyViewInitialFrame.size.height / transformedSelfFrame.size.height)
copyView.layer.animate(from: NSValue(caTransform3D: CATransform3DMakeScale(scale.width, scale.height, 1.0)), to: NSValue(caTransform3D: CATransform3DIdentity), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25 * durationFactor, removeOnCompletion: false, completion: { _ in
copyCompleted = true
intermediateCompletion()
})
if let _ = self.videoNode {
self.contentNode.view.superview?.bringSubviewToFront(self.contentNode.view)
} else {
self.contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25 * durationFactor, removeOnCompletion: false)
}
self.contentNode.layer.animatePosition(from: self.contentNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25 * durationFactor, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
positionCompleted = true
intermediateCompletion()
})
transformedFrame.origin = CGPoint()
let transform = CATransform3DScale(self.contentNode.layer.transform, transformedFrame.size.width / self.contentNode.layer.bounds.size.width, transformedFrame.size.height / self.contentNode.layer.bounds.size.height, 1.0)
self.contentNode.layer.animate(from: NSValue(caTransform3D: self.contentNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25 * durationFactor, removeOnCompletion: false, completion: { _ in
boundsCompleted = true
intermediateCompletion()
})
self.contentNode.clipsToBounds = true
if case .round = self.sourceCorners {
self.contentNode.layer.animate(from: 0.0 as NSNumber, to: (self.contentNode.frame.width / 2.0) as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18 * durationFactor, removeOnCompletion: false)
} else if case let .roundRect(cornerRadius) = self.sourceCorners {
let scale = scaledLocalImageViewBounds.width / transformedCopyViewInitialFrame.width
let selfScale = transformedCopyViewInitialFrame.width / transformedSelfFrame.width
self.contentNode.layer.animate(from: 0.0 as NSNumber, to: (cornerRadius * scale * selfScale) as NSNumber, keyPath: "cornerRadius", timingFunction: CAMediaTimingFunctionName.default.rawValue, duration: 0.18 * durationFactor, removeOnCompletion: false)
}
self.statusNodeContainer.layer.animatePosition(from: self.statusNodeContainer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.statusNodeContainer.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue, removeOnCompletion: false)
}
override func visibilityUpdated(isVisible: Bool) {
super.visibilityUpdated(isVisible: isVisible)
}
override func title() -> Signal<String, NoError> {
return self._title.get()
}
override func rightBarButtonItems() -> Signal<[UIBarButtonItem]?, NoError> {
return self._rightBarButtonItems.get()
}
@objc func statusPressed() {
if let entry = self.entry, let largestSize = largestImageRepresentation(entry.representations.map({ $0.representation })), let status = self.status {
switch status {
case .Fetching:
self.context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(largestSize.resource.id))
case .Remote:
let representations: [ImageRepresentationWithReference]
switch entry {
case let .topImage(topRepresentations, _, _, _, _, _):
representations = topRepresentations
case let .image(_, _, imageRepresentations, _, _, _, _, _, _, _, _, _):
representations = imageRepresentations
}
if let largestIndex = representations.firstIndex(where: { $0.representation == largestSize }) {
self.fetchDisposable.set(self.context.engine.resources.fetch(reference: representations[largestIndex].reference, userLocation: .other, userContentType: .image).start())
}
default:
break
}
}
}
@objc private func editPressed() {
self.edit?()
}
override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> {
return .single((self.footerContentNode, nil))
}
}