From d20b9cfe2dec8a8a3d5ac28438274e5822d5ffc2 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Fri, 26 Apr 2019 23:41:09 +0400 Subject: [PATCH] Added Lottie animation viewer --- .../SlowDown.imageset/Contents.json | 22 ++ .../SlowDown.imageset/SlowDown@2x.png | Bin 0 -> 727 bytes .../SlowDown.imageset/SlowDown@3x.png | Bin 0 -> 1095 bytes TelegramUI.xcodeproj/project.pbxproj | 4 + TelegramUI/AnimationNode.swift | 32 +- TelegramUI/ChatAnimationGalleryItem.swift | 339 ++++++++++++++++++ TelegramUI/ChatImageGalleryItem.swift | 2 +- .../ChatMessageAnimatedStickerItemNode.swift | 2 +- TelegramUI/GalleryController.swift | 12 +- TelegramUI/GalleryItemNode.swift | 4 + TelegramUI/OpenChatMessage.swift | 8 + TelegramUI/StickerResources.swift | 2 +- 12 files changed, 420 insertions(+), 7 deletions(-) create mode 100644 Images.xcassets/Media Gallery/SlowDown.imageset/Contents.json create mode 100644 Images.xcassets/Media Gallery/SlowDown.imageset/SlowDown@2x.png create mode 100644 Images.xcassets/Media Gallery/SlowDown.imageset/SlowDown@3x.png create mode 100644 TelegramUI/ChatAnimationGalleryItem.swift 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 0000000000000000000000000000000000000000..c8526e4520e5f4c4067e91780cd41c8a4a37bb44 GIT binary patch literal 727 zcmV;|0x127P)Dfdj0007>NklUE2&%1{|iBS}qNqnWP*sHtS zgOm*=fLL4LS>fMW$SbM}#eli@8v-d9NWe(o?u>zLQ!rJ!4a&PU=qp~-KQzZym~nmF z>4MRBX6oJmOgv_ZG6dm|>bbyDIV+S1YcD?>|7eW0N)|9}Y_1EUAvRazC`<5m^^_uP zzB0>`hNi0gd5dP~XJYpdD}~eP8Usj-zg#o~nB_8(7*qG@1;7>yLy-w!_Ma6rMVJ}P zqd7W9b?<|Ly*=_mGi)_Zon)EilBI@3X^%2MA~@-_`>vM$*g}-cRQj0gp1Rm7ec5Y% z8o5TsFnJ49grJh%`@DG55ZZyJ2Y#ZQ%UHTdKyYMVWGSsE9k+y3_OrCAFGyts)@EXn z4{g;{R`n3)ZldEUs$i?5cy<#=nRGK5pnGoJ{Qz&bM^Uim%+9BmLb<8L7@g~|!r+hG zFJi3Do*Y$eG0~F08Ax8+9IOA$r56jpSVuLpD^6WqQ(Fv}DqePeZJ0;^@yRdiYHBK; z53S_M)8Vb^RozQE9i$EmZ{IEEtBxi1#T5CTW``-TGBOhB3KV`?-X28O9#f``E;eb9I(m~_dHV*TlKjs5XHdE@Yi%tp^<8V~qjMe1 z6lrZP+0S*HDiK_J!3{(fHrSZ^xG*#Kn!5V>M!Po9R?hvc`UmMA!XNUpKDvT`_`0( z8U_YtXHOT$kP61P*IAQ3y<%#4*tz@BI@kMu*RSVlRjL=>QS{{IfhV6k|Hc1(9e>g4 z=9}L;<1d}=J$vTUKV!{z3eVWsmaNBf;UY<+UycIVeJKJTP=}t^3H|gB=`B_K# z(`S*97Z*L5d-2t)c7>PBQ<9_mKbP4#+_+Tg$|kV=>xx9S3Av@W%~&K#zD}s*mk{0c z{wT{Mr>~X14Lkp>n9Sf8WS_y+qW#xRkoodCW=HA2L0JrKYprHEIPbsCaA@7C+04nI z51AIf3Jl}q4!z9e6n(YCZGxxiqHnT)r<^N)XLYHRbIPi#up3)tFo@aC%(SD8qIuV2hfW$0{VzAv75q)EG5BJ$I9mL+H~*s$|KNuAFS)KW)_=XDXqx|a=ABEQO%A+& z#r3~H)$SzUjF+<*FI?K}BX%Gty!5w2#0$+^mIg0g?9yO4G5hs8xkpQTWft7Ls5{r~ zWc$TKLA;;O7HyPisW}l};y+~u!}8dh(rk^n)w@n@UYt;5@lEmmnMn8gq_=E#4kZ=; z_^bZSzGKr>yT9dq{G{Xwm-EHu)ur#SnZnr|&+#>lX>aB2-1TcawK8hAGVssYH}m(t z!=ZiizgH)CzH+b(O#QsxZ0fu%XFC6?arN!mcK2shMq2#l=~uM&Jw806ZI?dt>(^Jc zgF{Py_(>XEh-ED3Ib#!Q(y;Yjq{G5$spINr7ybG(_scXEw|VbgIDg!`D>Y!RSLxS< zukYudckb|?p0hH`Pb!G}(Yd>7TP_^rnzL@zhlVp-7j>pDU;FQ_{C1Xar(Z6z^qW*z zED)||qn3WBI{nWa;}F%0Tb``&DTz)$ueo{csy9ECPFMHY&)OAxS5)-vH0#@@QoC 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)