diff --git a/Images.xcassets/Media Gallery/SlowDown.imageset/Contents.json b/Images.xcassets/Media Gallery/SlowDown.imageset/Contents.json new file mode 100644 index 0000000000..5e1dbdf364 --- /dev/null +++ b/Images.xcassets/Media Gallery/SlowDown.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "SlowDown@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "SlowDown@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Media Gallery/SlowDown.imageset/SlowDown@2x.png b/Images.xcassets/Media Gallery/SlowDown.imageset/SlowDown@2x.png new file mode 100644 index 0000000000..c8526e4520 Binary files /dev/null and b/Images.xcassets/Media Gallery/SlowDown.imageset/SlowDown@2x.png differ diff --git a/Images.xcassets/Media Gallery/SlowDown.imageset/SlowDown@3x.png b/Images.xcassets/Media Gallery/SlowDown.imageset/SlowDown@3x.png new file mode 100644 index 0000000000..6d3e8addab Binary files /dev/null and b/Images.xcassets/Media Gallery/SlowDown.imageset/SlowDown@3x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 303a296ffa..3a90a1df7b 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 0900678F21ED8E0E00530762 /* HexColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0900678E21ED8E0E00530762 /* HexColor.swift */; }; 0902838821931D960067EFBD /* LanguageSuggestionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0902838721931D960067EFBD /* LanguageSuggestionController.swift */; }; 0902838D2194AEB90067EFBD /* ImageTransparency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0902838C2194AEB90067EFBD /* ImageTransparency.swift */; }; + 090A22172273713000694CB0 /* ChatAnimationGalleryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090A22162273713000694CB0 /* ChatAnimationGalleryItem.swift */; }; 090B48C82200BCA8005083FA /* WallpaperUploadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090B48C72200BCA8005083FA /* WallpaperUploadManager.swift */; }; 090E63E62195880F00E3C035 /* ContactAddItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090E63E52195880F00E3C035 /* ContactAddItem.swift */; }; 090E63EE2196FE3A00E3C035 /* OpenAddContact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090E63ED2196FE3A00E3C035 /* OpenAddContact.swift */; }; @@ -1193,6 +1194,7 @@ 0900678E21ED8E0E00530762 /* HexColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexColor.swift; sourceTree = ""; }; 0902838721931D960067EFBD /* LanguageSuggestionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageSuggestionController.swift; sourceTree = ""; }; 0902838C2194AEB90067EFBD /* ImageTransparency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTransparency.swift; sourceTree = ""; }; + 090A22162273713000694CB0 /* ChatAnimationGalleryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAnimationGalleryItem.swift; sourceTree = ""; }; 090B48C72200BCA8005083FA /* WallpaperUploadManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperUploadManager.swift; sourceTree = ""; }; 090E63E52195880F00E3C035 /* ContactAddItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactAddItem.swift; sourceTree = ""; }; 090E63ED2196FE3A00E3C035 /* OpenAddContact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenAddContact.swift; sourceTree = ""; }; @@ -4844,6 +4846,7 @@ D0104F2B1F471EEB004E4881 /* InstantPageGalleryFooterContentNode.swift */, D0A8BBA01F61EE83000F03FD /* UniversalVideoGalleryItem.swift */, 09F79A0221C8225600820234 /* WebSearchVideoGalleryItem.swift */, + 090A22162273713000694CB0 /* ChatAnimationGalleryItem.swift */, ); name = Items; sourceTree = ""; @@ -5556,6 +5559,7 @@ 09CE95002232729A00A7D2C3 /* StickerPaneSearchContentNode.swift in Sources */, D053DADC201AAAB100993D32 /* ChatTextInputMenu.swift in Sources */, 0962E66321B3513100245FD9 /* WebSearchControllerNode.swift in Sources */, + 090A22172273713000694CB0 /* ChatAnimationGalleryItem.swift in Sources */, D0EC6D1A1EB9F58800EBF1C3 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, D0EC6D1B1EB9F58800EBF1C3 /* FFMpegMediaVideoFrameDecoder.swift in Sources */, D01C06AF1FBB461E001561AB /* JoinLinkPreviewController.swift in Sources */, diff --git a/TelegramUI/AnimationNode.swift b/TelegramUI/AnimationNode.swift index c7c62f08d4..34ea01f048 100644 --- a/TelegramUI/AnimationNode.swift +++ b/TelegramUI/AnimationNode.swift @@ -4,17 +4,26 @@ import Lottie final class AnimationNode : ASDisplayNode { private let scale: CGFloat + var speed: CGFloat = 1.0 { + didSet { + if let animationView = animationView() { + animationView.animationSpeed = speed + } + } + } + var played = false var completion: (() -> Void)? - init(animation: String, keysToColor: [String]?, color: UIColor, scale: CGFloat) { + init(animation: String? = nil, keysToColor: [String]? = nil, color: UIColor = .black, scale: CGFloat = 1.0) { self.scale = scale super.init() self.setViewBlock({ - if let url = frameworkBundle.url(forResource: animation, withExtension: "json"), let composition = LOTComposition(filePath: url.path) { + if let animation = animation, let url = frameworkBundle.url(forResource: animation, withExtension: "json"), let composition = LOTComposition(filePath: url.path) { let view = LOTAnimationView(model: composition, in: frameworkBundle) + view.animationSpeed = self.speed view.backgroundColor = .clear view.isOpaque = false @@ -27,11 +36,21 @@ final class AnimationNode : ASDisplayNode { return view } else { - return UIView() + return LOTAnimationView() } }) } + func setAnimation(name: String) { + if let url = frameworkBundle.url(forResource: name, withExtension: "json"), let composition = LOTComposition(filePath: url.path) { + self.animationView()?.sceneModel = composition + } + } + + func setAnimation(json: [AnyHashable: Any]) { + self.animationView()?.setAnimation(json: json) + } + func animationView() -> LOTAnimationView? { return self.view as? LOTAnimationView } @@ -45,6 +64,13 @@ final class AnimationNode : ASDisplayNode { } } + func loop() { + if let animationView = animationView() { + animationView.loopAnimation = true + animationView.play() + } + } + func reset() { if self.played, let animationView = animationView() { self.played = false diff --git a/TelegramUI/ChatAnimationGalleryItem.swift b/TelegramUI/ChatAnimationGalleryItem.swift new file mode 100644 index 0000000000..554600ee89 --- /dev/null +++ b/TelegramUI/ChatAnimationGalleryItem.swift @@ -0,0 +1,339 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import Lottie + +class ChatAnimationGalleryItem: GalleryItem { + let context: AccountContext + let presentationData: PresentationData + let message: Message + let location: MessageHistoryEntryLocation? + + init(context: AccountContext, presentationData: PresentationData, message: Message, location: MessageHistoryEntryLocation?) { + self.context = context + self.presentationData = presentationData + self.message = message + self.location = location + } + + func node() -> GalleryItemNode { + let node = ChatAnimationGalleryItemNode(context: self.context, presentationData: self.presentationData) + + for media in self.message.media { + if let file = media as? TelegramMediaFile { + node.setFile(context: self.context, fileReference: .message(message: MessageReference(self.message), media: file)) + break + } + } + + node.setMessage(self.message) + + return node + } + + func updateNode(node: GalleryItemNode) { + if let node = node as? ChatAnimationGalleryItemNode { + node.setMessage(self.message) + } + } + + func thumbnailItem() -> (Int64, GalleryThumbnailItem)? { + return nil + } +} + +private var backgroundButtonIcon: UIImage = { + return generateImage(CGSize(width: 20.0, height: 20.0), rotatedContext: { size, context in + let bounds = CGRect(origin: CGPoint(), size: size) + context.clear(bounds) + + context.setLineWidth(1.0) + context.setStrokeColor(UIColor.white.cgColor) + context.setFillColor(UIColor.white.cgColor) + context.strokeEllipse(in: bounds.insetBy(dx: 0.5, dy: 0.5)) + + context.addEllipse(in: bounds.insetBy(dx: 0.5, dy: 0.5)) + context.clip() + + context.fill(CGRect(x: 0.0, y: 0.0, width: 10.0, height: 20.0)) + })! +}() + +final class ChatAnimationGalleryItemNode: ZoomableContentGalleryItemNode { + private let context: AccountContext + private var message: Message? + + fileprivate let _title = Promise() + fileprivate let _rightBarButtonItems = Promise<[UIBarButtonItem]?>() + + private let containerNode: ASDisplayNode + private let animationNode: AnimationNode + + private let statusNodeContainer: HighlightableButtonNode + private let statusNode: RadialStatusNode + private let footerContentNode: ChatItemGalleryFooterContentNode + + private var contextAndMedia: (AccountContext, AnyMediaReference)? + + private var disposable = MetaDisposable() + private var fetchDisposable = MetaDisposable() + private let statusDisposable = MetaDisposable() + private var status: MediaResourceStatus? + + init(context: AccountContext, presentationData: PresentationData) { + self.context = context + + self.containerNode = ASDisplayNode() + self.containerNode.backgroundColor = .black + + self.animationNode = AnimationNode() + self.containerNode.addSubnode(self.animationNode) + + self.footerContentNode = ChatItemGalleryFooterContentNode(context: context, presentationData: presentationData) + + self.statusNodeContainer = HighlightableButtonNode() + self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.5)) + self.statusNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 50.0, height: 50.0)) + self.statusNode.isHidden = true + + super.init() + + self.statusNodeContainer.addSubnode(self.statusNode) + self.addSubnode(self.statusNodeContainer) + + self.statusNodeContainer.addTarget(self, action: #selector(self.statusPressed), forControlEvents: .touchUpInside) + + self.statusNodeContainer.isUserInteractionEnabled = false + } + + deinit { + self.disposable.dispose() + self.fetchDisposable.dispose() + self.statusDisposable.dispose() + } + + 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 setMessage(_ message: Message) { + self.footerContentNode.setMessage(message) + } + + func setFile(context: AccountContext, fileReference: FileMediaReference) { + if self.contextAndMedia == nil || !self.contextAndMedia!.1.media.isEqual(to: fileReference.media) { + let signal = chatMessageAnimationData(postbox: context.account.postbox, fileReference: fileReference, synchronousLoad: false) + |> mapToSignal { data, completed -> Signal in + if completed, let data = data { + return .single(data) + } else { + return .complete() + } + } + self.disposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] next in + guard let strongSelf = self else { + return + } + if let object = try? JSONSerialization.jsonObject(with: next, options: []) as? [AnyHashable: Any], let json = object { + let containerSize = CGSize(width: 640.0, height: 640.0) + strongSelf.animationNode.setAnimation(json: json) + strongSelf.zoomableContent = (containerSize, strongSelf.containerNode) + + if let animationSize = strongSelf.animationNode.preferredSize() { + let size = animationSize.fitted(containerSize) + strongSelf.animationNode.frame = CGRect(origin: CGPoint(x: floor((containerSize.width - size.width) / 2.0), y: floor((containerSize.height - size.height) / 2.0)), size: size) + } + + strongSelf.animationNode.loop() + } + })) + self.setupStatus(resource: fileReference.media.resource) + + let speedItem = UIBarButtonItem(image: UIImage(bundleImageName: "Media Gallery/SlowDown"), style: .plain, target: self, action: #selector(self.toggleSpeedButtonPressed)) + let backgroundItem = UIBarButtonItem(image: backgroundButtonIcon, style: .plain, target: self, action: #selector(self.toggleBackgroundButtonPressed)) + self._rightBarButtonItems.set(.single([speedItem, backgroundItem])) + } + self.contextAndMedia = (context, fileReference.abstract) + } + + @objc private func toggleSpeedButtonPressed() { + if self.animationNode.speed == 1.0 { + self.animationNode.speed = 0.1 + } else { + self.animationNode.speed = 1.0 + } + } + + @objc private func toggleBackgroundButtonPressed() { + let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut) + if self.containerNode.backgroundColor == .white { + transition.updateBackgroundColor(node: self.containerNode, color: .black) + } else { + transition.updateBackgroundColor(node: self.containerNode, color: .white) + } + } + + private func setupStatus(resource: MediaResource) { + self.statusDisposable.set((self.context.account.postbox.mediaBox.resourceStatus(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.statusNode.alpha = 1.0 + strongSelf.statusNodeContainer.isUserInteractionEnabled = true + strongSelf.statusNode.transitionToState(.download(.white), completion: {}) + case let .Fetching(isActive, progress): + strongSelf.statusNode.isHidden = false + strongSelf.statusNode.alpha = 1.0 + 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: {}) + } + }) + } + } + } + })) + } + + override func animateIn(from node: (ASDisplayNode, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view.superview) + + self.containerNode.layer.animatePosition(from: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), to: self.containerNode.layer.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(self.containerNode.layer.transform, transformedFrame.size.width / self.containerNode.layer.bounds.size.width, transformedFrame.size.height / self.containerNode.layer.bounds.size.height, 1.0) + self.containerNode.layer.animate(from: NSValue(caTransform3D: transform), to: NSValue(caTransform3D: self.containerNode.layer.transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + + 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, () -> (UIView?, UIView?)), addToTransitionSurface: (UIView) -> Void, completion: @escaping () -> Void) { + var transformedFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view) + let transformedSuperFrame = node.0.view.convert(node.0.view.bounds, to: self.containerNode.view.superview) + let transformedSelfFrame = node.0.view.convert(node.0.view.bounds, to: self.view) + let transformedCopyViewInitialFrame = self.containerNode.view.convert(self.containerNode.view.bounds, to: self.view) + + var positionCompleted = false + var boundsCompleted = false + var copyCompleted = false + + let (maybeCopyView, copyViewBackgrond) = node.1() + copyViewBackgrond?.alpha = 0.0 + let copyView = maybeCopyView! + + self.view.insertSubview(copyView, belowSubview: self.containerNode.view) + copyView.frame = transformedSelfFrame + + let intermediateCompletion = { [weak copyView] in + if positionCompleted && boundsCompleted && copyCompleted { + copyView?.removeFromSuperview() + completion() + } + } + + copyView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, removeOnCompletion: false) + + copyView.layer.animatePosition(from: CGPoint(x: transformedCopyViewInitialFrame.midX, y: transformedCopyViewInitialFrame.midY), to: CGPoint(x: transformedSelfFrame.midX, y: transformedSelfFrame.midY), duration: 0.25, 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, removeOnCompletion: false, completion: { _ in + copyCompleted = true + intermediateCompletion() + }) + + self.containerNode.layer.animatePosition(from: self.containerNode.layer.position, to: CGPoint(x: transformedSuperFrame.midX, y: transformedSuperFrame.midY), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + positionCompleted = true + intermediateCompletion() + }) + + self.containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + + transformedFrame.origin = CGPoint() + + let transform = CATransform3DScale(self.containerNode.layer.transform, transformedFrame.size.width / self.containerNode.layer.bounds.size.width, transformedFrame.size.height / self.containerNode.layer.bounds.size.height, 1.0) + self.containerNode.layer.animate(from: NSValue(caTransform3D: self.containerNode.layer.transform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25, removeOnCompletion: false, completion: { _ in + boundsCompleted = true + intermediateCompletion() + }) + + 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: kCAMediaTimingFunctionEaseIn, removeOnCompletion: false) + } + + override func visibilityUpdated(isVisible: Bool) { + super.visibilityUpdated(isVisible: isVisible) + + if let (context, mediaReference) = self.contextAndMedia, let fileReference = mediaReference.concrete(TelegramMediaFile.self) { + if isVisible { + } else { + self.fetchDisposable.set(nil) + } + } + } + + override func title() -> Signal { + return self._title.get() + } + + override func rightBarButtonItems() -> Signal<[UIBarButtonItem]?, NoError> { + return self._rightBarButtonItems.get() + } + + override func footerContent() -> Signal { + return .single(self.footerContentNode) + } + + @objc func statusPressed() { + if let (_, mediaReference) = self.contextAndMedia, let status = self.status { + var resource: MediaResourceReference? + var statsCategory: MediaResourceStatsCategory? + if let fileReference = mediaReference.concrete(TelegramMediaFile.self) { + resource = fileReference.resourceReference(fileReference.media.resource) + statsCategory = statsCategoryForFileWithAttributes(fileReference.media.attributes) + } + if let resource = resource { + switch status { + case .Fetching: + self.context.account.postbox.mediaBox.cancelInteractiveResourceFetch(resource.resource) + case .Remote: + self.fetchDisposable.set(fetchedMediaResource(postbox: self.context.account.postbox, reference: resource, statsCategory: statsCategory ?? .generic).start()) + default: + break + } + } + } + } +} diff --git a/TelegramUI/ChatImageGalleryItem.swift b/TelegramUI/ChatImageGalleryItem.swift index 0a7e0b6af3..b24e42904f 100644 --- a/TelegramUI/ChatImageGalleryItem.swift +++ b/TelegramUI/ChatImageGalleryItem.swift @@ -160,7 +160,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode { private let statusDisposable = MetaDisposable() private var status: MediaResourceStatus? - init(context: AccountContext, presentationData: PresentationData,performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void) { + init(context: AccountContext, presentationData: PresentationData, performAction: @escaping (GalleryControllerInteractionTapAction) -> Void, openActionOptions: @escaping (GalleryControllerInteractionTapAction) -> Void) { self.context = context self.imageNode = TransformImageNode() diff --git a/TelegramUI/ChatMessageAnimatedStickerItemNode.swift b/TelegramUI/ChatMessageAnimatedStickerItemNode.swift index f6f3c948c4..3241be1f09 100644 --- a/TelegramUI/ChatMessageAnimatedStickerItemNode.swift +++ b/TelegramUI/ChatMessageAnimatedStickerItemNode.swift @@ -141,7 +141,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { for media in item.message.media { if let telegramFile = media as? TelegramMediaFile { if self.telegramFile != telegramFile { - let signal = chatMessageAnimatedStickerDatas(postbox: item.context.account.postbox, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: telegramFile), synchronousLoad: false) + let signal = chatMessageAnimationData(postbox: item.context.account.postbox, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: telegramFile), synchronousLoad: false) |> mapToSignal { data, completed -> Signal in if completed, let data = data { return .single(data) diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index dfa59c18db..5f51f91a81 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -166,7 +166,10 @@ func galleryItemForEntry(context: AccountContext, presentationData: Presentation let caption = galleryCaptionStringWithAppliedEntities(text, entities: entities) return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: GalleryItemOriginData(title: message.author?.displayTitle, timestamp: message.timestamp), indexData: location.flatMap { GalleryItemIndexData(position: Int32($0.index), totalCount: Int32($0.count)) }, contentInfo: .message(message), caption: caption, hideControls: hideControls, fromPlayingVideo: fromPlayingVideo, landscape: landscape, timecode: timecode, playbackCompleted: playbackCompleted, performAction: performAction, openActionOptions: openActionOptions) } else { - if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" { + if let fileName = file.fileName, (fileName as NSString).pathExtension.lowercased() == "json" { + return ChatAnimationGalleryItem(context: context, presentationData: presentationData, message: message, location: location) + } + else if file.mimeType.hasPrefix("image/") && file.mimeType != "image/gif" { var pixelsCount: Int = 0 if let dimensions = file.dimensions { pixelsCount = Int(dimensions.width) * Int(dimensions.height) @@ -317,6 +320,7 @@ class GalleryController: ViewController { private let centralItemTitle = Promise() private let centralItemTitleView = Promise() private let centralItemRightBarButtonItem = Promise() + private let centralItemRightBarButtonItems = Promise<[UIBarButtonItem]?>() private let centralItemNavigationStyle = Promise() private let centralItemFooterContentNode = Promise() private let centralItemAttributesDisposable = DisposableSet(); @@ -501,6 +505,10 @@ class GalleryController: ViewController { self?.navigationItem.rightBarButtonItem = rightBarButtonItem })) + self.centralItemAttributesDisposable.add(self.centralItemRightBarButtonItems.get().start(next: { [weak self] rightBarButtonItems in + self?.navigationItem.rightBarButtonItems = rightBarButtonItems + })) + self.centralItemAttributesDisposable.add(self.centralItemFooterContentNode.get().start(next: { [weak self] footerContentNode in self?.galleryNode.updatePresentationState({ $0.withUpdatedFooterContentNode(footerContentNode) @@ -869,6 +877,7 @@ class GalleryController: ViewController { strongSelf.centralItemTitle.set(node.title()) strongSelf.centralItemTitleView.set(node.titleView()) strongSelf.centralItemRightBarButtonItem.set(node.rightBarButtonItem()) + strongSelf.centralItemRightBarButtonItems.set(node.rightBarButtonItems()) strongSelf.centralItemNavigationStyle.set(node.navigationStyle()) strongSelf.centralItemFooterContentNode.set(node.footerContent()) } @@ -906,6 +915,7 @@ class GalleryController: ViewController { self.centralItemTitle.set(centralItemNode.title()) self.centralItemTitleView.set(centralItemNode.titleView()) self.centralItemRightBarButtonItem.set(centralItemNode.rightBarButtonItem()) + self.centralItemRightBarButtonItems.set(centralItemNode.rightBarButtonItems()) self.centralItemNavigationStyle.set(centralItemNode.navigationStyle()) self.centralItemFooterContentNode.set(centralItemNode.footerContent()) diff --git a/TelegramUI/GalleryItemNode.swift b/TelegramUI/GalleryItemNode.swift index 3367e8a389..2ffa4ca8f5 100644 --- a/TelegramUI/GalleryItemNode.swift +++ b/TelegramUI/GalleryItemNode.swift @@ -49,6 +49,10 @@ open class GalleryItemNode: ASDisplayNode { return .single(nil) } + open func rightBarButtonItems() -> Signal<[UIBarButtonItem]?, NoError> { + return .single(nil) + } + open func footerContent() -> Signal { return .single(nil) } diff --git a/TelegramUI/OpenChatMessage.swift b/TelegramUI/OpenChatMessage.swift index be4e7fe777..e4fe96d80a 100644 --- a/TelegramUI/OpenChatMessage.swift +++ b/TelegramUI/OpenChatMessage.swift @@ -5,6 +5,7 @@ import Postbox import TelegramCore import SwiftSignalKit import PassKit +import Lottie private enum ChatMessageGalleryControllerData { case url(String) @@ -125,6 +126,13 @@ private func chatMessageGalleryControllerData(context: AccountContext, message: let ext = (fileName as NSString).pathExtension.lowercased() if ext == "wav" || ext == "opus" { return .audio(file) + } else if ext == "json", let fileSize = file.size, fileSize < 1024 * 1024 { + if let path = context.account.postbox.mediaBox.completedResourcePath(file.resource), let _ = LOTComposition(filePath: path) { + let gallery = GalleryController(context: context, source: .standaloneMessage(message), invertItemOrder: reverseMessageGalleryOrder, streamSingleVideo: stream, fromPlayingVideo: autoplayingVideo, landscape: landscape, timecode: timecode, synchronousLoad: synchronousLoad, replaceRootController: { [weak navigationController] controller, ready in + navigationController?.replaceTopController(controller, animated: false, ready: ready) + }, baseNavigationController: navigationController, actionInteraction: actionInteraction) + return .gallery(gallery) + } } #if DEBUG if ext == "mkv" { diff --git a/TelegramUI/StickerResources.swift b/TelegramUI/StickerResources.swift index 582b754fb1..ae285b70d2 100644 --- a/TelegramUI/StickerResources.swift +++ b/TelegramUI/StickerResources.swift @@ -131,7 +131,7 @@ private func chatMessageStickerPackThumbnailData(postbox: Postbox, representatio } } -func chatMessageAnimatedStickerDatas(postbox: Postbox, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal<(Data?, Bool), NoError> { +func chatMessageAnimationData(postbox: Postbox, fileReference: FileMediaReference, synchronousLoad: Bool) -> Signal<(Data?, Bool), NoError> { let resource = fileReference.media.resource let maybeFetched = postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad)