import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import SyncCore import TelegramPresentationData import AccountContext import RadialStatusNode import ShareController import PhotoResources import GalleryUI import TelegramUniversalVideoContent private struct PeerAvatarImageGalleryThumbnailItem: GalleryThumbnailItem { let account: Account let peer: Peer let content: [ImageRepresentationWithReference] init(account: Account, peer: Peer, content: [ImageRepresentationWithReference]) { self.account = account self.peer = peer self.content = content } var image: (Signal<(TransformImageArguments) -> DrawingContext?, NoError>, CGSize) { if let representation = largestImageRepresentation(self.content.map({ $0.representation })) { return (avatarGalleryThumbnailPhoto(account: self.account, representations: self.content), 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: Peer let presentationData: PresentationData let entry: AvatarGalleryEntry let sourceHasRoundCorners: Bool let delete: (() -> Void)? let setMain: (() -> Void)? init(context: AccountContext, peer: Peer, presentationData: PresentationData, entry: AvatarGalleryEntry, sourceHasRoundCorners: Bool, delete: (() -> Void)?, setMain: (() -> Void)?) { self.context = context self.peer = peer self.presentationData = presentationData self.entry = entry self.sourceHasRoundCorners = sourceHasRoundCorners self.delete = delete self.setMain = setMain } func node(synchronous: Bool) -> GalleryItemNode { let node = PeerAvatarImageGalleryItemNode(context: self.context, presentationData: self.presentationData, peer: self.peer, sourceHasRoundCorners: self.sourceHasRoundCorners) if let indexData = self.entry.indexData { node._title.set(.single(self.presentationData.strings.Items_NOfM("\(indexData.position + 1)", "\(indexData.totalCount)").0)) } node.setEntry(self.entry, synchronous: synchronous) node.footerContentNode.delete = self.delete node.footerContentNode.setMain = self.setMain 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)").0)) } node.setEntry(self.entry, synchronous: synchronous) node.footerContentNode.delete = self.delete node.footerContentNode.setMain = self.setMain } } 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 peer: Peer private let sourceHasRoundCorners: Bool private var entry: AvatarGalleryEntry? private let contentNode: PeerAvatarImageGalleryContentNode private let imageNode: TransformImageNode private var videoNode: UniversalVideoNode? private var videoContent: NativeVideoContent? fileprivate let _ready = Promise() fileprivate let _title = Promise() private let statusNodeContainer: HighlightableButtonNode private let statusNode: RadialStatusNode fileprivate let footerContentNode: AvatarGalleryItemFooterContentNode private let fetchDisposable = MetaDisposable() private let statusDisposable = MetaDisposable() private var status: MediaResourceStatus? init(context: AccountContext, presentationData: PresentationData, peer: Peer, sourceHasRoundCorners: Bool) { self.context = context self.peer = peer self.sourceHasRoundCorners = sourceHasRoundCorners 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.imageUpdated = { [weak self] _ in self?._ready.set(.single(Void())) } 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 if let video = entry.videoRepresentations.last { let videoFileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) subject = .media(videoFileReference.abstract) } else { subject = .image(entry.representations) } let shareController = ShareController(context: strongSelf.context, subject: subject, preferredAction: .saveToCameraRoll) interaction.presentController(shareController, nil) } } } deinit { self.fetchDisposable.dispose() self.statusDisposable.dispose() } override func ready() -> Signal { 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) { if self.entry != entry { self.entry = entry var footerContent: AvatarGalleryItemFooterContent if self.peer.id == self.context.account.peerId { footerContent = .own((entry.indexData?.position ?? 0) == 0) } else { footerContent = .info } 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: [ImageRepresentationWithReference] switch entry { case let .topImage(topRepresentations, _): representations = topRepresentations case let .image(_, _, imageRepresentations, _, _, _, _, _): representations = imageRepresentations } self.imageNode.setSignal(chatAvatarGalleryPhoto(account: self.context.account, representations: representations, attemptSynchronously: synchronous), attemptSynchronously: synchronous, dispatchOnDisplayLink: false) self.zoomableContent = (largestSize.dimensions.cgSize, self.contentNode) if let largestIndex = representations.firstIndex(where: { $0.representation == largestSize }) { self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, reference: representations[largestIndex].reference).start()) } // self.statusDisposable.set((self.context.account.postbox.mediaBox.resourceStatus(largestSize.resource) // |> deliverOnMainQueue).start(next: { [weak self] status in // if let strongSelf = self { // let previousStatus = strongSelf.status // strongSelf.status = status // switch status { // case .Remote: // strongSelf.statusNode.isHidden = false // strongSelf.statusNodeContainer.isUserInteractionEnabled = true // strongSelf.statusNode.transitionToState(.download(.white), completion: {}) // case let .Fetching(_, progress): // strongSelf.statusNode.isHidden = false // strongSelf.statusNodeContainer.isUserInteractionEnabled = true // let adjustedProgress = max(progress, 0.027) // strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: CGFloat(adjustedProgress), cancelEnabled: true), completion: {}) // case .Local: // if let previousStatus = previousStatus, case .Fetching = previousStatus { // strongSelf.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: 1.0, cancelEnabled: true), completion: { // if let strongSelf = self { // strongSelf.statusNode.alpha = 0.0 // strongSelf.statusNodeContainer.isUserInteractionEnabled = false // strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in // if let strongSelf = self { // strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) // } // }) // } // }) // } else if !strongSelf.statusNode.isHidden && !strongSelf.statusNode.alpha.isZero { // strongSelf.statusNode.alpha = 0.0 // strongSelf.statusNodeContainer.isUserInteractionEnabled = false // strongSelf.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { _ in // if let strongSelf = self { // strongSelf.statusNode.transitionToState(.none, animated: false, completion: {}) // } // }) // } // } // } // })) var id: Int64? if case let .image(image) = entry { id = image.0.id } if let video = entry.videoRepresentations.last, let id = id { let mediaManager = self.context.sharedContext.mediaManager let videoFileReference = FileMediaReference.standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.dimensions, flags: [])])) let videoContent = NativeVideoContent(id: .profileVideo(id), fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear) let videoNode = UniversalVideoNode(postbox: self.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: videoContent, priority: .embedded) videoNode.isUserInteractionEnabled = false videoNode.ownsContentNodeUpdated = { [weak self] owns in if let strongSelf = self { strongSelf.videoNode?.isHidden = !owns } } videoNode.canAttachContent = true if videoNode.hasAttachedContext { videoNode.play() } self.videoContent = videoContent self.videoNode = videoNode videoNode.updateLayout(size: largestSize.dimensions.cgSize, transition: .immediate) self.contentNode.addSubnode(videoNode) } else if let videoNode = self.videoNode { self.videoContent = nil self.videoNode = nil videoNode.removeFromSupernode() } self.imageNode.frame = self.contentNode.bounds self.videoNode?.frame = self.contentNode.bounds } else { self._ready.set(.single(Void())) } } } 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 self.sourceHasRoundCorners { 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() }) 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 self.sourceHasRoundCorners { 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 } }) } 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) var positionCompleted = false var boundsCompleted = false var copyCompleted = false var surfaceCopyCompleted = false let copyView = node.2().0! if self.sourceHasRoundCorners { self.view.insertSubview(copyView, belowSubview: self.scrollNode.view) } copyView.frame = transformedSelfFrame let surfaceCopyView = node.2().0! if !self.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 surfaceCopyCompleted = true intermediateCompletion() }) } else { surfaceCopyCompleted = true } 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() }) 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() }) self.contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25 * durationFactor, removeOnCompletion: false) 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 self.sourceHasRoundCorners { 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) } 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 { return self._title.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.account.postbox.mediaBox.cancelInteractiveResourceFetch(largestSize.resource) 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(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, reference: representations[largestIndex].reference).start()) } default: break } } } override func footerContent() -> Signal<(GalleryFooterContentNode?, GalleryOverlayContentNode?), NoError> { return .single((self.footerContentNode, nil)) } }