import Foundation import UIKit import AsyncDisplayKit import Display import TelegramCore import SwiftSignalKit import Postbox import AVFoundation import RadialStatusNode import StickerResources import PhotoResources import AnimatedStickerNode import TelegramAnimatedStickerNode import TelegramPresentationData import AccountContext import ShimmerEffect import SoftwareVideo final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { let account: Account let theme: PresentationTheme let result: ChatContextResult let resultSelected: (ChatContextResult, ASDisplayNode, CGRect) -> Bool let selectable: Bool = true public init(account: Account, theme: PresentationTheme, result: ChatContextResult, resultSelected: @escaping (ChatContextResult, ASDisplayNode, CGRect) -> Bool) { self.account = account self.theme = theme self.result = result self.resultSelected = resultSelected } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { let configure = { () -> Void in let node = HorizontalListContextResultsChatInputPanelItemNode() let nodeLayout = node.asyncLayout() let (top, bottom) = (previousItem != nil, nextItem != nil) let (layout, apply) = nodeLayout(self, params, top, bottom) node.contentSize = layout.contentSize node.insets = layout.insets Queue.mainQueue().async { completion(node, { return (nil, { _ in apply(synchronousLoads, .None) }) }) } } if Thread.isMainThread { async { configure() } } else { configure() } } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? HorizontalListContextResultsChatInputPanelItemNode { let nodeLayout = nodeValue.asyncLayout() async { let (top, bottom) = (previousItem != nil, nextItem != nil) let (layout, apply) = nodeLayout(self, params, top, bottom) Queue.mainQueue().async { completion(layout, { _ in apply(false, animation) }) } } } else { assertionFailure() } } } } private let titleFont = Font.medium(16.0) private let textFont = Font.regular(15.0) private let iconFont = Font.medium(25.0) private let iconTextBackgroundImage = generateStretchableFilledCircleImage(radius: 2.0, color: UIColor(rgb: 0xdfdfdf)) final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode { private let imageNodeBackground: ASDisplayNode private let imageNode: TransformImageNode private var animationNode: AnimatedStickerNode? private var placeholderNode: StickerShimmerEffectNode? private var videoLayer: (SoftwareVideoThumbnailNode, SoftwareVideoLayerFrameManager, SampleBufferLayer)? private var currentImageResource: TelegramMediaResource? private var currentVideoFile: TelegramMediaFile? private var currentAnimatedStickerFile: TelegramMediaFile? private var resourceStatus: MediaResourceStatus? private(set) var item: HorizontalListContextResultsChatInputPanelItem? private var statusDisposable = MetaDisposable() private let statusNode: RadialStatusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) private let fetchDisposable = MetaDisposable() override var visibility: ListViewItemNodeVisibility { didSet { switch visibility { case .visible: self.ticking = true default: self.ticking = false } } } private let timebase: CMTimebase private var displayLink: CADisplayLink? private var ticking: Bool = false { didSet { if self.ticking != oldValue { if self.ticking { class DisplayLinkProxy: NSObject { weak var target: HorizontalListContextResultsChatInputPanelItemNode? init(target: HorizontalListContextResultsChatInputPanelItemNode) { self.target = target } @objc func displayLinkEvent() { self.target?.displayLinkEvent() } } let displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent)) self.displayLink = displayLink displayLink.add(to: RunLoop.main, forMode: .common) if #available(iOS 10.0, *) { displayLink.preferredFramesPerSecond = 25 } else { displayLink.frameInterval = 2 } displayLink.isPaused = false CMTimebaseSetRate(self.timebase, rate: 1.0) } else if let displayLink = self.displayLink { self.displayLink = nil displayLink.isPaused = true displayLink.invalidate() CMTimebaseSetRate(self.timebase, rate: 0.0) } } } } private func displayLinkEvent() { let timestamp = CMTimebaseGetTime(self.timebase).seconds self.videoLayer?.1.tick(timestamp: timestamp) } init() { self.imageNodeBackground = ASDisplayNode() self.imageNodeBackground.isLayerBacked = true self.placeholderNode = StickerShimmerEffectNode() self.imageNode = TransformImageNode() self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNode.isLayerBacked = !smartInvertColorsEnabled() self.imageNode.displaysAsynchronously = false var timebase: CMTimebase? CMTimebaseCreateWithSourceClock(allocator: nil, sourceClock: CMClockGetHostTimeClock(), timebaseOut: &timebase) CMTimebaseSetRate(timebase!, rate: 0.0) self.timebase = timebase! super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.imageNodeBackground) self.imageNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.imageNode.contentAnimations = [.firstUpdate, .subsequentUpdates] self.addSubnode(self.imageNode) var firstTime = true self.imageNode.imageUpdated = { [weak self] image in guard let strongSelf = self else { return } if image != nil { strongSelf.removePlaceholder(animated: !firstTime) } firstTime = false } if let placeholderNode = self.placeholderNode { placeholderNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.addSubnode(placeholderNode) } } deinit { if let displayLink = self.displayLink { displayLink.isPaused = true displayLink.invalidate() } self.statusDisposable.dispose() self.fetchDisposable.dispose() } private func removePlaceholder(animated: Bool) { if let placeholderNode = self.placeholderNode { self.placeholderNode = nil if !animated { placeholderNode.removeFromSupernode() } else { placeholderNode.allowsGroupOpacity = true placeholderNode.alpha = 0.0 placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak placeholderNode] _ in placeholderNode?.removeFromSupernode() placeholderNode?.allowsGroupOpacity = false }) } } } override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? HorizontalListContextResultsChatInputPanelItem { let doLayout = self.asyncLayout() let merged = (top: previousItem != nil, bottom: nextItem != nil) let (layout, apply) = doLayout(item, params, merged.top, merged.bottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(false, .None) } } func asyncLayout() -> (_ item: HorizontalListContextResultsChatInputPanelItem, _ params: ListViewItemLayoutParams, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (Bool, ListViewItemUpdateAnimation) -> Void) { let imageLayout = self.imageNode.asyncLayout() let currentImageResource = self.currentImageResource let currentVideoFile = self.currentVideoFile let currentAnimatedStickerFile = self.currentAnimatedStickerFile return { [weak self] item, params, mergedTop, mergedBottom in let height = params.width let sideInset: CGFloat = 4.0 var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>? var updatedStatusSignal: Signal? var imageResource: TelegramMediaResource? var stickerFile: TelegramMediaFile? var animatedStickerFile: TelegramMediaFile? var videoFile: TelegramMediaFile? var imageDimensions: CGSize? switch item.result { case let .externalReference(externalReference): if let content = externalReference.content { imageResource = content.resource } else if let thumbnail = externalReference.thumbnail { imageResource = thumbnail.resource } imageDimensions = externalReference.content?.dimensions?.cgSize if externalReference.type == "gif", let thumbnailResource = externalReference.thumbnail?.resource, let content = externalReference.content, let dimensions = content.dimensions { videoFile = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [])]) imageResource = nil } if let file = videoFile { updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(file.resource) } else if let imageResource = imageResource { updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(imageResource) } case let .internalReference(internalReference): if let image = internalReference.image { if let largestRepresentation = largestImageRepresentation(image.representations) { imageDimensions = largestRepresentation.dimensions.cgSize } imageResource = imageRepresentationLargerThan(image.representations, size: PixelDimensions(width: 200, height: 100))?.resource } else if let file = internalReference.file { if let dimensions = file.dimensions { imageDimensions = dimensions.cgSize } else if let largestRepresentation = largestImageRepresentation(file.previewRepresentations) { imageDimensions = largestRepresentation.dimensions.cgSize } if file.isAnimatedSticker { animatedStickerFile = file imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource } else if file.isSticker { stickerFile = file imageResource = file.resource } else { imageResource = smallestImageRepresentation(file.previewRepresentations)?.resource } } if let file = internalReference.file { if file.isVideo && file.isAnimated { videoFile = file imageResource = nil updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(file.resource) } else if let imageResource = imageResource { updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(imageResource) } } else if let imageResource = imageResource { updatedStatusSignal = item.account.postbox.mediaBox.resourceStatus(imageResource) } } let fittedImageDimensions: CGSize let croppedImageDimensions: CGSize if let imageDimensions = imageDimensions { fittedImageDimensions = imageDimensions.fitted(CGSize(width: 1000.0, height: height - sideInset - sideInset)) } else { fittedImageDimensions = CGSize(width: height - sideInset - sideInset, height: height - sideInset - sideInset) } croppedImageDimensions = fittedImageDimensions.cropped(CGSize(width: floor(height * 4.0 / 3.0), height: 1000.0)) var imageApply: (() -> Void)? if let _ = imageResource { let imageCorners = ImageCorners() let arguments = TransformImageArguments(corners: imageCorners, imageSize: fittedImageDimensions, boundingSize: croppedImageDimensions, intrinsicInsets: UIEdgeInsets()) imageApply = imageLayout(arguments) } var updatedImageResource = false if let currentImageResource = currentImageResource, let imageResource = imageResource { if !currentImageResource.isEqual(to: imageResource) { updatedImageResource = true } } else if (currentImageResource != nil) != (imageResource != nil) { updatedImageResource = true } var updatedVideoFile = false if let currentVideoFile = currentVideoFile, let videoFile = videoFile { if !currentVideoFile.isEqual(to: videoFile) { updatedVideoFile = true } } else if (currentVideoFile != nil) != (videoFile != nil) { updatedVideoFile = true } var updatedAnimatedStickerFile = false if let currentAnimatedStickerFile = currentAnimatedStickerFile, let animatedStickerFile = animatedStickerFile { if !currentAnimatedStickerFile.isEqual(to: animatedStickerFile) { updatedAnimatedStickerFile = true } } else if (currentAnimatedStickerFile != nil) != (animatedStickerFile != nil) { updatedAnimatedStickerFile = true } if updatedImageResource { if let imageResource = imageResource { if let stickerFile = stickerFile { updateImageSignal = chatMessageSticker(account: item.account, file: stickerFile, small: false, fetched: true) } else { let tmpRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(CGSize(width: fittedImageDimensions.width * 2.0, height: fittedImageDimensions.height * 2.0)), resource: imageResource, progressiveSizes: [], immediateThumbnailData: nil) let tmpImage = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [tmpRepresentation], immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) updateImageSignal = chatMessagePhoto(postbox: item.account.postbox, photoReference: .standalone(media: tmpImage), synchronousLoad: true) } } else { updateImageSignal = .complete() } } let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: height, height: croppedImageDimensions.width + sideInset), insets: UIEdgeInsets()) return (nodeLayout, { synchronousLoads, _ in if let strongSelf = self { strongSelf.item = item strongSelf.currentImageResource = imageResource strongSelf.currentVideoFile = videoFile strongSelf.currentAnimatedStickerFile = currentAnimatedStickerFile if let imageApply = imageApply { if let updateImageSignal = updateImageSignal { strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: true) } strongSelf.imageNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) strongSelf.imageNode.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) strongSelf.imageNodeBackground.frame = CGRect(origin: CGPoint(x: sideInset, y: sideInset), size: CGSize(width: croppedImageDimensions.height, height: croppedImageDimensions.width)) imageApply() } if updatedVideoFile { if let (thumbnailLayer, _, layer) = strongSelf.videoLayer { strongSelf.videoLayer = nil thumbnailLayer.removeFromSupernode() layer.layer.removeFromSuperlayer() } if let videoFile = videoFile { let thumbnailLayer = SoftwareVideoThumbnailNode(account: item.account, fileReference: .standalone(media: videoFile), synchronousLoad: synchronousLoads) thumbnailLayer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) strongSelf.addSubnode(thumbnailLayer) let layerHolder = takeSampleBufferLayer() layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) strongSelf.layer.addSublayer(layerHolder.layer) let manager = SoftwareVideoLayerFrameManager(account: item.account, fileReference: .standalone(media: videoFile), layerHolder: layerHolder) strongSelf.videoLayer = (thumbnailLayer, manager, layerHolder) thumbnailLayer.ready = { [weak thumbnailLayer, weak manager] in if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager { if strongSelf.videoLayer?.0 === thumbnailLayer && strongSelf.videoLayer?.1 === manager { manager.start() } } } } } if updatedAnimatedStickerFile { if let animationNode = strongSelf.animationNode { strongSelf.animationNode = nil animationNode.removeFromSupernode() } if let animatedStickerFile = animatedStickerFile { let animationNode: AnimatedStickerNode if let currentAnimationNode = strongSelf.animationNode { animationNode = currentAnimationNode } else { animationNode = DefaultAnimatedStickerNodeImpl() animationNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) animationNode.visibility = true if let placeholderNode = strongSelf.placeholderNode { strongSelf.insertSubnode(animationNode, belowSubnode: placeholderNode) } else { strongSelf.addSubnode(animationNode) } strongSelf.animationNode = animationNode } animationNode.started = { [weak self] in self?.imageNode.alpha = 0.0 } let dimensions = animatedStickerFile.dimensions ?? PixelDimensions(width: 512, height: 512) let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)) strongSelf.fetchDisposable.set(freeMediaFileResourceInteractiveFetched(account: item.account, fileReference: stickerPackFileReference(animatedStickerFile), resource: animatedStickerFile.resource).start()) animationNode.setup(source: AnimatedStickerResourceSource(account: item.account, resource: animatedStickerFile.resource, isVideo: animatedStickerFile.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .cached) } } let progressSize = CGSize(width: 24.0, height: 24.0) let progressFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((nodeLayout.contentSize.width - progressSize.width) / 2.0), y: floorToScreenPixels((nodeLayout.contentSize.height - progressSize.height) / 2.0)), size: progressSize) strongSelf.statusNode.removeFromSupernode() //strongSelf.addSubnode(strongSelf.statusNode) strongSelf.statusNode.frame = progressFrame if let updatedStatusSignal = updatedStatusSignal { strongSelf.statusDisposable.set((updatedStatusSignal |> deliverOnMainQueue).start(next: { [weak strongSelf] status in displayLinkDispatcher.dispatch { if let strongSelf = strongSelf { strongSelf.resourceStatus = status let state: RadialStatusNodeState let statusForegroundColor: UIColor = .white switch status { case let .Fetching(_, progress): state = .progress(color: statusForegroundColor, lineWidth: nil, value: CGFloat(max(progress, 0.2)), cancelEnabled: false, animateRotation: true) case .Remote, .Paused: //state = .download(statusForegroundColor) state = .none case .Local: state = .none } strongSelf.statusNode.transitionToState(state, completion: { }) } } })) } else { strongSelf.statusNode.transitionToState(.none, completion: { }) } if let (thumbnailLayer, _, layer) = strongSelf.videoLayer { thumbnailLayer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) thumbnailLayer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) layer.layer.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) layer.layer.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) } if let animationNode = strongSelf.animationNode { animationNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) animationNode.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) animationNode.updateLayout(size: croppedImageDimensions) } var immediateThumbnailData: Data? if case let .internalReference(internalReference) = item.result, internalReference.file?.isSticker == true { immediateThumbnailData = internalReference.file?.immediateThumbnailData } if let placeholderNode = strongSelf.placeholderNode { placeholderNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) placeholderNode.position = CGPoint(x: height / 2.0, y: (nodeLayout.contentSize.height - sideInset) / 2.0 + sideInset) placeholderNode.update(backgroundColor: item.theme.list.plainBackgroundColor, foregroundColor: item.theme.list.mediaPlaceholderColor.mixedWith(item.theme.list.plainBackgroundColor, alpha: 0.4), shimmeringColor: item.theme.list.mediaPlaceholderColor.withAlphaComponent(0.3), data: immediateThumbnailData, size: CGSize(width: croppedImageDimensions.width, height: croppedImageDimensions.height)) } } }) } } override func selected() { guard let item = self.item else { return } let _ = item.resultSelected(item.result, self, self.bounds) } }