diff --git a/Telegram-iOS.xcworkspace/contents.xcworkspacedata b/Telegram-iOS.xcworkspace/contents.xcworkspacedata index b2dc8b259e..f8cbbb7c7c 100644 --- a/Telegram-iOS.xcworkspace/contents.xcworkspacedata +++ b/Telegram-iOS.xcworkspace/contents.xcworkspacedata @@ -372,6 +372,13 @@ location = "group:submodules/AvatarNode/AvatarNode_Xcode.xcodeproj"> + + + + diff --git a/submodules/AnimationUI/Sources/AnimatedStickerNode.swift b/submodules/AnimationUI/Sources/AnimatedStickerNode.swift index 460ca98e2c..e6a1df697b 100644 --- a/submodules/AnimationUI/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimationUI/Sources/AnimatedStickerNode.swift @@ -309,6 +309,9 @@ public final class AnimatedStickerNode: ASDisplayNode { private let fetchDisposable = MetaDisposable() private let eventsNode: AnimatedStickerNodeDisplayEvents + public var automaticallyLoadFirstFrame: Bool = false + public var playToCompletionOnStop: Bool = false + public var started: () -> Void = {} private var reportedStarted = false @@ -320,6 +323,7 @@ public final class AnimatedStickerNode: ASDisplayNode { private var renderer: (AnimationRenderer & ASDisplayNode)? private var isPlaying: Bool = false + private var canDisplayFirstFrame: Bool = false private var playbackMode: AnimatedStickerPlaybackMode = .loop private let playbackStatus = Promise() @@ -394,6 +398,8 @@ public final class AnimatedStickerNode: ASDisplayNode { } if strongSelf.isPlaying { strongSelf.play() + } else if strongSelf.canDisplayFirstFrame { + strongSelf.play(firstFrame: true) } })) case .cached: @@ -403,6 +409,8 @@ public final class AnimatedStickerNode: ASDisplayNode { strongSelf.cachedData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.mappedRead]) if strongSelf.isPlaying { strongSelf.play() + } else if strongSelf.canDisplayFirstFrame { + strongSelf.play(firstFrame: true) } } })) @@ -424,9 +432,16 @@ public final class AnimatedStickerNode: ASDisplayNode { self.stop() } } + let canDisplayFirstFrame = self.automaticallyLoadFirstFrame && self.isDisplaying + if self.canDisplayFirstFrame != canDisplayFirstFrame { + self.canDisplayFirstFrame = canDisplayFirstFrame + if canDisplayFirstFrame { + self.play(firstFrame: true) + } + } } - public func play() { + public func play(firstFrame: Bool = false) { let directData = self.directData let cachedData = self.cachedData let queue = self.queue @@ -451,7 +466,7 @@ public final class AnimatedStickerNode: ASDisplayNode { let duration: Double = frameSource.frameRate > 0 ? Double(frameSource.frameCount) / Double(frameSource.frameRate) : 0 let frameRate = frameSource.frameRate - let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: true, completion: { + let timer = SwiftSignalKit.Timer(timeout: 1.0 / Double(frameRate), repeat: !firstFrame, completion: { let maybeFrame = frameQueue.syncWith { frameQueue in return frameQueue.take() } @@ -492,6 +507,9 @@ public final class AnimatedStickerNode: ASDisplayNode { public func stop() { self.reportedStarted = false self.timer.swap(nil)?.invalidate() + if self.playToCompletionOnStop { + self.seekToStart() + } } public func seekToStart() { diff --git a/submodules/Display/Display/ContainedViewLayoutTransition.swift b/submodules/Display/Display/ContainedViewLayoutTransition.swift index 20c2978053..ec438b5a21 100644 --- a/submodules/Display/Display/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Display/ContainedViewLayoutTransition.swift @@ -63,24 +63,29 @@ public enum ContainedViewLayoutTransition { } public extension ContainedViewLayoutTransition { - func updateFrame(node: ASDisplayNode, frame: CGRect, force: Bool = false, completion: ((Bool) -> Void)? = nil) { + func updateFrame(node: ASDisplayNode, frame: CGRect, force: Bool = false, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.frame.equalTo(frame) && !force { completion?(true) } else { switch self { - case .immediate: - node.frame = frame + case .immediate: + node.frame = frame + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let previousFrame: CGRect + if beginWithCurrentState, let presentation = node.layer.presentation() { + previousFrame = presentation.frame + } else { + previousFrame = node.frame + } + node.frame = frame + node.layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in if let completion = completion { - completion(true) + completion(result) } - case let .animated(duration, curve): - let previousFrame = node.frame - node.frame = frame - node.layer.animateFrame(from: previousFrame, to: frame, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, force: force, completion: { result in - if let completion = completion { - completion(result) - } - }) + }) } } } @@ -147,7 +152,7 @@ public extension ContainedViewLayoutTransition { } } - func updatePosition(node: ASDisplayNode, position: CGPoint, completion: ((Bool) -> Void)? = nil) { + func updatePosition(node: ASDisplayNode, position: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.position.equalTo(position) { completion?(true) } else { @@ -158,7 +163,12 @@ public extension ContainedViewLayoutTransition { completion(true) } case let .animated(duration, curve): - let previousPosition = node.position + let previousPosition: CGPoint + if beginWithCurrentState { + previousPosition = node.layer.presentation()?.position ?? node.position + } else { + previousPosition = node.position + } node.position = position node.layer.animatePosition(from: previousPosition, to: position, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { @@ -498,7 +508,7 @@ public extension ContainedViewLayoutTransition { } } - func updateTransformScale(node: ASDisplayNode, scale: CGFloat, completion: ((Bool) -> Void)? = nil) { + func updateTransformScale(node: ASDisplayNode, scale: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { let t = node.layer.transform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) if currentScale.isEqual(to: scale) { @@ -515,8 +525,15 @@ public extension ContainedViewLayoutTransition { completion(true) } case let .animated(duration, curve): + let previousScale: CGFloat + if beginWithCurrentState, let presentation = node.layer.presentation() { + let t = presentation.transform + previousScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + } else { + previousScale = currentScale + } node.layer.transform = CATransform3DMakeScale(scale, scale, 1.0) - node.layer.animateScale(from: currentScale, to: scale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in + node.layer.animateScale(from: previousScale, to: scale, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { completion(result) } @@ -581,6 +598,47 @@ public extension ContainedViewLayoutTransition { } } + func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + if !node.isNodeLoaded { + node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) + return + } + let t = node.layer.sublayerTransform + let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) + let currentOffset = CGPoint(x: t.m41, y: t.m42) + if currentScale.isEqual(to: scale) && currentOffset == offset { + if let completion = completion { + completion(true) + } + return + } + + let transform = CATransform3DTranslate(CATransform3DMakeScale(scale, scale, 1.0), offset.x, offset.y, 0.0) + + switch self { + case .immediate: + node.layer.sublayerTransform = transform + if let completion = completion { + completion(true) + } + case let .animated(duration, curve): + let initialTransform: CATransform3D + if beginWithCurrentState, node.isNodeLoaded { + initialTransform = node.layer.presentation()?.sublayerTransform ?? t + } else { + initialTransform = t + } + + node.layer.sublayerTransform = transform + node.layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + result in + if let completion = completion { + completion(result) + } + }) + } + } + func updateSublayerTransformScale(node: ASDisplayNode, scale: CGPoint, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) diff --git a/submodules/LegacyComponents/LegacyComponents_Xcode.xcodeproj/project.pbxproj b/submodules/LegacyComponents/LegacyComponents_Xcode.xcodeproj/project.pbxproj index 28f52bcca5..2b1ca8e737 100644 --- a/submodules/LegacyComponents/LegacyComponents_Xcode.xcodeproj/project.pbxproj +++ b/submodules/LegacyComponents/LegacyComponents_Xcode.xcodeproj/project.pbxproj @@ -5131,7 +5131,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -5211,7 +5211,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -5257,7 +5257,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5365,7 +5365,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -5439,7 +5439,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5513,7 +5513,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5588,7 +5588,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -5662,7 +5662,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5742,7 +5742,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -5816,7 +5816,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -5890,7 +5890,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/submodules/ReactionSelectionNode/Info.plist b/submodules/ReactionSelectionNode/Info.plist new file mode 100644 index 0000000000..e1fe4cfb7b --- /dev/null +++ b/submodules/ReactionSelectionNode/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/submodules/ReactionSelectionNode/ReactionSelectionNode_Xcode.xcodeproj/project.pbxproj b/submodules/ReactionSelectionNode/ReactionSelectionNode_Xcode.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..e16fea7138 --- /dev/null +++ b/submodules/ReactionSelectionNode/ReactionSelectionNode_Xcode.xcodeproj/project.pbxproj @@ -0,0 +1,579 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + D03E45EA2305E3F30049C28B /* ReactionSelectionNode.h in Headers */ = {isa = PBXBuildFile; fileRef = D03E45E82305E3F30049C28B /* ReactionSelectionNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D03E45F52305EB7C0049C28B /* ReactionSelectionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E45F42305EB7C0049C28B /* ReactionSelectionNode.swift */; }; + D03E45F82305EB880049C28B /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E45F72305EB880049C28B /* Foundation.framework */; }; + D03E45FA2305EB8B0049C28B /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E45F92305EB8B0049C28B /* AsyncDisplayKit.framework */; }; + D03E45FC2305EB8E0049C28B /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E45FB2305EB8E0049C28B /* UIKit.framework */; }; + D03E45FE2305EB910049C28B /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E45FD2305EB910049C28B /* Display.framework */; }; + D03E46002305EE710049C28B /* AnimationUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E45FF2305EE710049C28B /* AnimationUI.framework */; }; + D03E46022305EE750049C28B /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E46012305EE750049C28B /* Postbox.framework */; }; + D03E46042305EE790049C28B /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E46032305EE790049C28B /* TelegramCore.framework */; }; + D03E46082305EEDD0049C28B /* ReactionSelectionParentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E46072305EEDD0049C28B /* ReactionSelectionParentNode.swift */; }; + D03E460A2305EF900049C28B /* ReactionSwipeGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E46092305EF900049C28B /* ReactionSwipeGestureRecognizer.swift */; }; + D03E460C2305EFD80049C28B /* ReactionGestureItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E460B2305EFD80049C28B /* ReactionGestureItem.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + D03E45E52305E3F30049C28B /* ReactionSelectionNode.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ReactionSelectionNode.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E45E82305E3F30049C28B /* ReactionSelectionNode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactionSelectionNode.h; sourceTree = ""; }; + D03E45E92305E3F30049C28B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + D03E45F42305EB7C0049C28B /* ReactionSelectionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionSelectionNode.swift; sourceTree = ""; }; + D03E45F72305EB880049C28B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + D03E45F92305EB8B0049C28B /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E45FB2305EB8E0049C28B /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; + D03E45FD2305EB910049C28B /* Display.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E45FF2305EE710049C28B /* AnimationUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AnimationUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E46012305EE750049C28B /* Postbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Postbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E46032305EE790049C28B /* TelegramCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E46072305EEDD0049C28B /* ReactionSelectionParentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionSelectionParentNode.swift; sourceTree = ""; }; + D03E46092305EF900049C28B /* ReactionSwipeGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionSwipeGestureRecognizer.swift; sourceTree = ""; }; + D03E460B2305EFD80049C28B /* ReactionGestureItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionGestureItem.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + D03E45E22305E3F30049C28B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D03E46042305EE790049C28B /* TelegramCore.framework in Frameworks */, + D03E46022305EE750049C28B /* Postbox.framework in Frameworks */, + D03E46002305EE710049C28B /* AnimationUI.framework in Frameworks */, + D03E45FE2305EB910049C28B /* Display.framework in Frameworks */, + D03E45FC2305EB8E0049C28B /* UIKit.framework in Frameworks */, + D03E45FA2305EB8B0049C28B /* AsyncDisplayKit.framework in Frameworks */, + D03E45F82305EB880049C28B /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + D03E45DB2305E3F30049C28B = { + isa = PBXGroup; + children = ( + D03E45E92305E3F30049C28B /* Info.plist */, + D03E45E72305E3F30049C28B /* Sources */, + D03E45E62305E3F30049C28B /* Products */, + D03E45F62305EB870049C28B /* Frameworks */, + ); + sourceTree = ""; + }; + D03E45E62305E3F30049C28B /* Products */ = { + isa = PBXGroup; + children = ( + D03E45E52305E3F30049C28B /* ReactionSelectionNode.framework */, + ); + name = Products; + sourceTree = ""; + }; + D03E45E72305E3F30049C28B /* Sources */ = { + isa = PBXGroup; + children = ( + D03E45F42305EB7C0049C28B /* ReactionSelectionNode.swift */, + D03E45E82305E3F30049C28B /* ReactionSelectionNode.h */, + D03E46072305EEDD0049C28B /* ReactionSelectionParentNode.swift */, + D03E46092305EF900049C28B /* ReactionSwipeGestureRecognizer.swift */, + D03E460B2305EFD80049C28B /* ReactionGestureItem.swift */, + ); + path = Sources; + sourceTree = ""; + }; + D03E45F62305EB870049C28B /* Frameworks */ = { + isa = PBXGroup; + children = ( + D03E46032305EE790049C28B /* TelegramCore.framework */, + D03E46012305EE750049C28B /* Postbox.framework */, + D03E45FF2305EE710049C28B /* AnimationUI.framework */, + D03E45FD2305EB910049C28B /* Display.framework */, + D03E45FB2305EB8E0049C28B /* UIKit.framework */, + D03E45F92305EB8B0049C28B /* AsyncDisplayKit.framework */, + D03E45F72305EB880049C28B /* Foundation.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + D03E45E02305E3F30049C28B /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + D03E45EA2305E3F30049C28B /* ReactionSelectionNode.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + D03E45E42305E3F30049C28B /* ReactionSelectionNode */ = { + isa = PBXNativeTarget; + buildConfigurationList = D03E45ED2305E3F30049C28B /* Build configuration list for PBXNativeTarget "ReactionSelectionNode" */; + buildPhases = ( + D03E45E02305E3F30049C28B /* Headers */, + D03E45E12305E3F30049C28B /* Sources */, + D03E45E22305E3F30049C28B /* Frameworks */, + D03E45E32305E3F30049C28B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = ReactionSelectionNode; + productName = ReactionSelectionNode; + productReference = D03E45E52305E3F30049C28B /* ReactionSelectionNode.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D03E45DC2305E3F30049C28B /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Latest; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "Telegram Messenger LLP"; + TargetAttributes = { + D03E45E42305E3F30049C28B = { + CreatedOnToolsVersion = 10.3; + LastSwiftMigration = 1030; + }; + }; + }; + buildConfigurationList = D03E45DF2305E3F30049C28B /* Build configuration list for PBXProject "ReactionSelectionNode_Xcode" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = D03E45DB2305E3F30049C28B; + productRefGroup = D03E45E62305E3F30049C28B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + D03E45E42305E3F30049C28B /* ReactionSelectionNode */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + D03E45E32305E3F30049C28B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + D03E45E12305E3F30049C28B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D03E460A2305EF900049C28B /* ReactionSwipeGestureRecognizer.swift in Sources */, + D03E46082305EEDD0049C28B /* ReactionSelectionParentNode.swift in Sources */, + D03E460C2305EFD80049C28B /* ReactionGestureItem.swift in Sources */, + D03E45F52305EB7C0049C28B /* ReactionSelectionNode.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D03E45EB2305E3F30049C28B /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugAppStoreLLC; + }; + D03E45EC2305E3F30049C28B /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseAppStoreLLC; + }; + D03E45EE2305E3F30049C28B /* DebugAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.ReactionSelectionNode; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugAppStoreLLC; + }; + D03E45EF2305E3F30049C28B /* ReleaseAppStoreLLC */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.ReactionSelectionNode; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseAppStoreLLC; + }; + D03E45F02305E4200049C28B /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugHockeyapp; + }; + D03E45F12305E4200049C28B /* DebugHockeyapp */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.ReactionSelectionNode; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = DebugHockeyapp; + }; + D03E45F22305E4290049C28B /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = ReleaseHockeyappInternal; + }; + D03E45F32305E4290049C28B /* ReleaseHockeyappInternal */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Manual; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + PRODUCT_BUNDLE_IDENTIFIER = org.telegram.ReactionSelectionNode; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = ReleaseHockeyappInternal; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + D03E45DF2305E3F30049C28B /* Build configuration list for PBXProject "ReactionSelectionNode_Xcode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D03E45EB2305E3F30049C28B /* DebugAppStoreLLC */, + D03E45F02305E4200049C28B /* DebugHockeyapp */, + D03E45EC2305E3F30049C28B /* ReleaseAppStoreLLC */, + D03E45F22305E4290049C28B /* ReleaseHockeyappInternal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStoreLLC; + }; + D03E45ED2305E3F30049C28B /* Build configuration list for PBXNativeTarget "ReactionSelectionNode" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D03E45EE2305E3F30049C28B /* DebugAppStoreLLC */, + D03E45F12305E4200049C28B /* DebugHockeyapp */, + D03E45EF2305E3F30049C28B /* ReleaseAppStoreLLC */, + D03E45F32305E4290049C28B /* ReleaseHockeyappInternal */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = ReleaseAppStoreLLC; + }; +/* End XCConfigurationList section */ + }; + rootObject = D03E45DC2305E3F30049C28B /* Project object */; +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift b/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift new file mode 100644 index 0000000000..a9ec62c03f --- /dev/null +++ b/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift @@ -0,0 +1,23 @@ +import Foundation +import Postbox +import TelegramCore + +public struct ReactionGestureItemValue { + public var value: String + public var text: String + public var file: TelegramMediaFile + + public init(value: String, text: String, file: TelegramMediaFile) { + self.value = value + self.text = text + self.file = file + } +} + +public final class ReactionGestureItem { + public let value: ReactionGestureItemValue + + public init(value: ReactionGestureItemValue) { + self.value = value + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.h b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.h new file mode 100644 index 0000000000..69ee588b7d --- /dev/null +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.h @@ -0,0 +1,19 @@ +// +// ReactionSelectionNode.h +// ReactionSelectionNode +// +// Created by Peter on 8/15/19. +// Copyright © 2019 Telegram Messenger LLP. All rights reserved. +// + +#import + +//! Project version number for ReactionSelectionNode. +FOUNDATION_EXPORT double ReactionSelectionNodeVersionNumber; + +//! Project version string for ReactionSelectionNode. +FOUNDATION_EXPORT const unsigned char ReactionSelectionNodeVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift new file mode 100644 index 0000000000..e46aa7ca51 --- /dev/null +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -0,0 +1,226 @@ +import Foundation +import AsyncDisplayKit +import AnimationUI +import Display +import Postbox +import TelegramCore + +private let shadowBlur: CGFloat = 8.0 +private let minimizedReactionSize: CGFloat = 30.0 +private let maximizedReactionSize: CGFloat = 60.0 + +private func generateBubbleImage(foreground: UIColor, diameter: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(foreground.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) +} + +private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor.white.cgColor) + context.setShadow(offset: CGSize(), blur: shadowBlur, color: shadow.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.setShadow(offset: CGSize(), blur: 1.0, color: shadow.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.setFillColor(UIColor.clear.cgColor) + context.setBlendMode(.copy) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) +} + +private final class ReactionNode: ASDisplayNode { + private let reaction: ReactionGestureItem + private let animationNode: AnimatedStickerNode + var isMaximized: Bool? + private let intrinsicSize: CGSize + private let intrinsicOffset: CGPoint + + init(account: Account, reaction: ReactionGestureItem) { + self.reaction = reaction + + self.animationNode = AnimatedStickerNode() + self.animationNode.automaticallyLoadFirstFrame = true + self.animationNode.playToCompletionOnStop = true + //self.animationNode.backgroundColor = .lightGray + + var intrinsicSize = CGSize(width: maximizedReactionSize + 18.0, height: maximizedReactionSize + 18.0) + switch reaction.value.value { + case "😳": + intrinsicSize.width += 8.0 + intrinsicSize.height += 8.0 + self.intrinsicOffset = CGPoint(x: 0.0, y: -4.0) + case "👍": + intrinsicSize.width += 20.0 + intrinsicSize.height += 20.0 + self.intrinsicOffset = CGPoint(x: 0.0, y: 4.0) + default: + self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0) + } + self.intrinsicSize = intrinsicSize + + super.init() + + //self.backgroundColor = .green + + self.addSubnode(self.animationNode) + self.animationNode.visibility = true + self.animationNode.setup(account: account, resource: reaction.value.file.resource, width: Int(self.intrinsicSize.width) * 2, height: Int(self.intrinsicSize.height) * 2, mode: .direct) + self.animationNode.updateLayout(size: self.intrinsicSize) + self.animationNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize) + } + + func updateLayout(size: CGSize, scale: CGFloat, transition: ContainedViewLayoutTransition) { + transition.updatePosition(node: self.animationNode, position: CGPoint(x: size.width / 2.0 + self.intrinsicOffset.x * scale, y: size.height / 2.0 + self.intrinsicOffset.y * scale), beginWithCurrentState: true) + transition.updateTransformScale(node: self.animationNode, scale: scale, beginWithCurrentState: true) + } + + func updateIsAnimating(_ isAnimating: Bool, animated: Bool) { + if isAnimating { + self.animationNode.visibility = true + } else { + self.animationNode.visibility = false + } + } +} + +final class ReactionSelectionNode: ASDisplayNode { + private let backgroundNode: ASImageNode + private let backgroundShadowNode: ASImageNode + private let bubbleNodes: [(ASImageNode, ASImageNode)] + private let reactionNodes: [ReactionNode] + + public init(account: Account, reactions: [ReactionGestureItem]) { + self.backgroundNode = ASImageNode() + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: 42.0) + + self.backgroundShadowNode = ASImageNode() + self.backgroundShadowNode.displaysAsynchronously = false + self.backgroundShadowNode.displayWithoutProcessing = true + self.backgroundShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: 42.0) + + self.bubbleNodes = (0 ..< 2).map { i -> (ASImageNode, ASImageNode) in + let imageNode = ASImageNode() + imageNode.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * 8.0) + imageNode.displaysAsynchronously = false + imageNode.displayWithoutProcessing = true + + let shadowNode = ASImageNode() + shadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * 8.0) + shadowNode.displaysAsynchronously = false + shadowNode.displayWithoutProcessing = true + + return (imageNode, shadowNode) + } + + self.reactionNodes = reactions.map { reaction -> ReactionNode in + return ReactionNode(account: account, reaction: reaction) + } + + super.init() + + self.bubbleNodes.forEach { _, shadow in + self.addSubnode(shadow) + } + self.addSubnode(self.backgroundShadowNode) + self.bubbleNodes.forEach { foreground, _ in + self.addSubnode(foreground) + } + self.addSubnode(self.backgroundNode) + self.reactionNodes.forEach(self.addSubnode(_:)) + } + + func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool) { + let backgroundHeight: CGFloat = 42.0 + let reactionSpacing: CGFloat = 6.0 + let minimizedReactionVerticalInset: CGFloat = floor((backgroundHeight - minimizedReactionSize) / 2.0) + + let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing + + var backgroundFrame = CGRect(origin: CGPoint(x: -shadowBlur, y: -shadowBlur), size: CGSize(width: contentWidth + shadowBlur * 2.0, height: backgroundHeight + shadowBlur * 2.0)) + backgroundFrame = backgroundFrame.offsetBy(dx: startingPoint.x - contentWidth + backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0) + + self.backgroundNode.frame = backgroundFrame + self.backgroundShadowNode.frame = backgroundFrame + + let anchorMinX = backgroundFrame.minX + shadowBlur + backgroundHeight / 2.0 + let anchorMaxX = backgroundFrame.maxX - shadowBlur - backgroundHeight / 2.0 + let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart)) + + var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing + var maximizedIndex = Int(((anchorX - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count)) + maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex)) + for i in 0 ..< self.reactionNodes.count { + let isMaximized = i == maximizedIndex + + let reactionSize: CGFloat + if isMaximized { + reactionSize = maximizedReactionSize + } else { + reactionSize = minimizedReactionSize + } + + let transition: ContainedViewLayoutTransition + if isInitial { + transition = .immediate + } else { + transition = .animated(duration: 0.18, curve: .easeInOut) + } + + if self.reactionNodes[i].isMaximized != isMaximized { + self.reactionNodes[i].isMaximized = isMaximized + self.reactionNodes[i].updateIsAnimating(isMaximized, animated: !isInitial) + } + + var reactionFrame = CGRect(origin: CGPoint(x: reactionX, y: backgroundFrame.maxY - shadowBlur - minimizedReactionVerticalInset - reactionSize), size: CGSize(width: reactionSize, height: reactionSize)) + if isMaximized { + reactionFrame.origin.x -= 9.0 + reactionFrame.size.width += 18.0 + } + self.reactionNodes[i].updateLayout(size: reactionFrame.size, scale: reactionFrame.size.width / (maximizedReactionSize + 18.0), transition: transition) + + transition.updateFrame(node: self.reactionNodes[i], frame: reactionFrame, beginWithCurrentState: true) + + reactionX += reactionSize + reactionSpacing + } + + let mainBubbleFrame = CGRect(origin: CGPoint(x: anchorX - 8.0 - shadowBlur, y: backgroundFrame.maxY - shadowBlur - 8.0 - shadowBlur), size: CGSize(width: 16.0 + shadowBlur * 2.0, height: 16.0 + shadowBlur * 2.0)) + self.bubbleNodes[1].0.frame = mainBubbleFrame + self.bubbleNodes[1].1.frame = mainBubbleFrame + + let secondaryBubbleFrame = CGRect(origin: CGPoint(x: mainBubbleFrame.midX - 9.0 - (8.0 + shadowBlur * 2.0) / 2.0, y: mainBubbleFrame.midY + 12.0 - (8.0 + shadowBlur * 2.0) / 2.0), size: CGSize(width: 8.0 + shadowBlur * 2.0, height: 8.0 + shadowBlur * 2.0)) + self.bubbleNodes[0].0.frame = secondaryBubbleFrame + self.bubbleNodes[0].1.frame = secondaryBubbleFrame + } + + func animateIn() { + self.bubbleNodes[1].0.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + self.bubbleNodes[1].1.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + + self.bubbleNodes[0].0.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.05, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + self.bubbleNodes[0].1.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.05, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue) + + let backgroundOffset = CGPoint(x: (self.backgroundNode.frame.width - shadowBlur) / 2.0 - 42.0, y: (self.backgroundNode.frame.height - shadowBlur) / 2.0) + let damping: CGFloat = 100.0 + + for i in 0 ..< self.reactionNodes.count { + let animationOffset: Double = 1.0 - Double(i) / Double(self.reactionNodes.count - 1) + let nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.maxX - shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur) + self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5 + animationOffset * 0.05, initialVelocity: 0.0, damping: damping) + self.reactionNodes[i].layer.animateSpring(from: NSValue(cgPoint: nodeOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) + } + + self.backgroundNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: damping) + self.backgroundNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) + self.backgroundShadowNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: damping) + self.backgroundShadowNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true) + } + + func animateOut(completion: @escaping () -> Void) { + completion() + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift new file mode 100644 index 0000000000..547cdee6c2 --- /dev/null +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift @@ -0,0 +1,74 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore + +public final class ReactionSelectionParentNode: ASDisplayNode { + private let account: Account + + private var currentNode: ReactionSelectionNode? + private var currentLocation: (CGPoint, CGFloat)? + + private var validLayout: (size: CGSize, insets: UIEdgeInsets)? + + public init(account: Account) { + self.account = account + + super.init() + } + + func displayReactions(_ reactions: [ReactionGestureItem], at point: CGPoint) { + if let currentNode = self.currentNode { + currentNode.removeFromSupernode() + self.currentNode = nil + } + + let reactionNode = ReactionSelectionNode(account: self.account, reactions: reactions) + self.addSubnode(reactionNode) + self.currentNode = reactionNode + self.currentLocation = (point, point.x) + + if let (size, insets) = self.validLayout { + self.update(size: size, insets: insets, isInitial: true) + + reactionNode.animateIn() + } + } + + func dismissReactions() { + if let currentNode = self.currentNode { + currentNode.animateOut(completion: { [weak currentNode] in + currentNode?.removeFromSupernode() + }) + self.currentNode = nil + } + } + + func updateReactionsAnchor(point: CGPoint) { + if let (currentPoint, _) = self.currentLocation { + self.currentLocation = (currentPoint, point.x) + + if let (size, insets) = self.validLayout { + self.update(size: size, insets: insets, isInitial: false) + } + } + } + + public func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { + self.validLayout = (size, insets) + + self.update(size: size, insets: insets, isInitial: false) + } + + private func update(size: CGSize, insets: UIEdgeInsets, isInitial: Bool) { + if let currentNode = self.currentNode, let (point, offset) = currentLocation { + currentNode.updateLayout(constrainedSize: size, startingPoint: point, offsetFromStart: offset, isInitial: isInitial) + currentNode.frame = CGRect(origin: CGPoint(), size: size) + } + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + return nil + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift b/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift new file mode 100644 index 0000000000..23e40513ff --- /dev/null +++ b/submodules/ReactionSelectionNode/Sources/ReactionSwipeGestureRecognizer.swift @@ -0,0 +1,103 @@ +import Foundation +import UIKit +import Display + +public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer { + private var validatedGesture = false + + private var firstLocation: CGPoint = CGPoint() + private var currentReactions: [ReactionGestureItem] = [] + private var isActivated = false + private weak var currentContainer: ReactionSelectionParentNode? + + public var availableReactions: (() -> [ReactionGestureItem])? + public var getReactionContainer: (() -> ReactionSelectionParentNode?)? + public var updateOffset: ((CGFloat, Bool) -> Void)? + public var completed: (() -> Void)? + + override public init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.maximumNumberOfTouches = 1 + } + + override public func reset() { + super.reset() + + self.validatedGesture = false + self.currentReactions = [] + self.isActivated = false + } + + override public func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if let availableReactions = self.availableReactions?(), !availableReactions.isEmpty { + self.currentReactions = availableReactions + let touch = touches.first! + self.firstLocation = touch.location(in: nil) + } else { + self.state = .failed + } + } + + override public func touchesMoved(_ touches: Set, with event: UIEvent) { + guard let _ = self.view else { + return + } + guard let location = touches.first?.location(in: nil) else { + return + } + + let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y) + + let absTranslationX: CGFloat = abs(translation.x) + let absTranslationY: CGFloat = abs(translation.y) + + var updatedOffset = false + + if !self.validatedGesture { + if translation.x > 0.0 { + self.state = .failed + } else if absTranslationY > 2.0 && absTranslationY > absTranslationX * 2.0 { + self.state = .failed + } else if absTranslationX > 2.0 && absTranslationY * 2.0 < absTranslationX { + self.validatedGesture = true + self.updateOffset?(translation.x, true) + updatedOffset = true + } + } + + if self.validatedGesture { + if !updatedOffset { + self.updateOffset?(-min(0.0, translation.x), false) + } + if !self.isActivated { + if absTranslationX > 40.0 { + self.isActivated = true + if !self.currentReactions.isEmpty, let reactionContainer = self.getReactionContainer?() { + self.currentContainer = reactionContainer + let reactionContainerLocation = reactionContainer.view.convert(location, from: nil) + reactionContainer.displayReactions(self.currentReactions, at: reactionContainerLocation) + } + } + } else { + if let reactionContainer = self.currentContainer { + let reactionContainerLocation = reactionContainer.view.convert(location, from: nil) + reactionContainer.updateReactionsAnchor(point: reactionContainerLocation) + } + } + super.touchesMoved(touches, with: event) + } + } + + override public func touchesEnded(_ touches: Set, with event: UIEvent) { + if self.validatedGesture { + self.completed?() + } + self.currentContainer?.dismissReactions() + self.state = .ended + + super.touchesEnded(touches, with: event) + } +} diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 6fcfac0e9c..b6386235a7 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -1017,6 +1017,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return self?.navigationController as? NavigationController }, chatControllerNode: { [weak self] in return self?.chatDisplayNode + }, reactionContainerNode: { [weak self] in + return self?.chatDisplayNode.reactionContainerNode }, presentGlobalOverlayController: { [weak self] controller, arguments in self?.presentInGlobalOverlay(controller, with: arguments) }, callPeer: { [weak self] peerId in diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift b/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift index 7a6a586e2d..3f20eaa0fc 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerInteraction.swift @@ -7,6 +7,7 @@ import Display import TelegramUIPreferences import AccountContext import TextSelectionNode +import ReactionSelectionNode struct ChatInterfaceHighlightedState: Equatable { let messageStableId: UInt32 @@ -73,6 +74,7 @@ public final class ChatControllerInteraction { let presentController: (ViewController, Any?) -> Void let navigationController: () -> NavigationController? let chatControllerNode: () -> ASDisplayNode? + let reactionContainerNode: () -> ReactionSelectionParentNode? let presentGlobalOverlayController: (ViewController, Any?) -> Void let callPeer: (PeerId) -> Void let longTap: (ChatControllerInteractionLongTapAction, Message?) -> Void @@ -106,7 +108,7 @@ public final class ChatControllerInteraction { var searchTextHighightState: String? var seenOneTimeAnimatedMedia = Set() - init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { + init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOption: @escaping (MessageId, Data) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) { self.openMessage = openMessage self.openPeer = openPeer self.openPeerMention = openPeerMention @@ -134,6 +136,7 @@ public final class ChatControllerInteraction { self.presentController = presentController self.navigationController = navigationController self.chatControllerNode = chatControllerNode + self.reactionContainerNode = reactionContainerNode self.presentGlobalOverlayController = presentGlobalOverlayController self.callPeer = callPeer self.longTap = longTap @@ -170,6 +173,8 @@ public final class ChatControllerInteraction { return nil }, chatControllerNode: { return nil + }, reactionContainerNode: { + return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in }, openCheckoutOrReceipt: { _ in }, openSearch: { }, setupReply: { _ in }, canSetupReply: { _ in return false diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift index 13aad4715b..67793a7438 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift @@ -10,6 +10,7 @@ import TelegramUIPreferences import TextFormat import AccountContext import TelegramNotices +import ReactionSelectionNode private final class ChatControllerNodeView: UITracingLayerView, WindowInputAccessoryHeightProvider, PreviewingHostView { var inputAccessoryHeight: (() -> CGFloat)? @@ -76,6 +77,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let backgroundNode: WallpaperbackgroundNode let historyNode: ChatHistoryListNode + let reactionContainerNode: ReactionSelectionParentNode let historyNodeContainer: ASDisplayNode let loadingNode: ChatLoadingNode private var emptyNode: ChatEmptyNode? @@ -209,6 +211,10 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.historyNode.rotated = true self.historyNodeContainer = ASDisplayNode() self.historyNodeContainer.addSubnode(self.historyNode) + + self.reactionContainerNode = ReactionSelectionParentNode(account: context.account) + self.historyNodeContainer.addSubnode(self.reactionContainerNode) + self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper) self.inputPanelBackgroundNode = ASDisplayNode() @@ -839,6 +845,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { transition.updateFrame(node: emptyNode, frame: contentBounds) } + transition.updateFrame(node: self.reactionContainerNode, frame: contentBounds) + self.reactionContainerNode.updateLayout(size: contentBounds.size, insets: UIEdgeInsets(), transition: transition) + var contentBottomInset: CGFloat = inputPanelsHeight + 4.0 if let scrollContainerNode = self.scrollContainerNode { diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift index ed99325865..9d9ae21399 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift @@ -16,6 +16,7 @@ import MosaicLayout import TextSelectionNode import PlatformRestrictionMatching import Emoji +import ReactionSelectionNode private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(Message, AnyClass)] { var result: [(Message, AnyClass)] = [] @@ -365,7 +366,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { self.tapRecognizer = recognizer self.view.addGestureRecognizer(recognizer) - let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) + /*let replyRecognizer = ChatSwipeToReplyRecognizer(target: self, action: #selector(self.swipeToReplyGesture(_:))) replyRecognizer.shouldBegin = { [weak self] in if let strongSelf = self, let item = strongSelf.item { if strongSelf.selectionNode != nil { @@ -387,7 +388,74 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } return false } - self.view.addGestureRecognizer(replyRecognizer) + self.view.addGestureRecognizer(replyRecognizer)*/ + + let reactionRecognizer = ReactionSwipeGestureRecognizer(target: nil, action: nil) + reactionRecognizer.availableReactions = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item else { + return [] + } + if strongSelf.selectionNode != nil { + return [] + } + for media in item.content.firstMessage.media { + if let _ = media as? TelegramMediaExpiredContent { + return [] + } + else if let media = media as? TelegramMediaAction { + if case .phoneCall = media.action { + } else { + return [] + } + } + } + if !item.controllerInteraction.canSetupReply(item.message) { + return [] + } + + let reactions: [(String, String)] = [ + ("😒", "Sad"), + ("😳", "Surprised"), + //("🥳", "Fun"), + ("👍", "Like"), + ("❤", "Love"), + ] + + var result: [ReactionGestureItem] = [] + for (value, text) in reactions { + if let file = item.associatedData.animatedEmojiStickers[value]?.file { + result.append(ReactionGestureItem(value: ReactionGestureItemValue(value: value, text: text, file: file))) + } + } + return result + } + reactionRecognizer.getReactionContainer = { [weak self] in + return self?.item?.controllerInteraction.reactionContainerNode() + } + reactionRecognizer.updateOffset = { [weak self] offset, animated in + guard let strongSelf = self else { + return + } + var bounds = strongSelf.bounds + bounds.origin.x = offset + strongSelf.bounds = bounds + if animated { + strongSelf.layer.animateBoundsOriginXAdditive(from: -offset, to: 0.0, duration: 0.1, mediaTimingFunction: CAMediaTimingFunction(name: .easeOut)) + } + } + reactionRecognizer.completed = { [weak self] in + guard let strongSelf = self else { + return + } + var bounds = strongSelf.bounds + let offset = bounds.origin.x + bounds.origin.x = 0.0 + strongSelf.bounds = bounds + if !offset.isZero { + strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring) + } + } + self.view.addGestureRecognizer(reactionRecognizer) } override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) { @@ -2504,7 +2572,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } @objc func swipeToReplyGesture(_ recognizer: ChatSwipeToReplyRecognizer) { - switch recognizer.state { + /*switch recognizer.state { case .began: self.currentSwipeToReplyTranslation = 0.0 if self.swipeToReplyFeedback == nil { @@ -2563,7 +2631,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } default: break - } + }*/ } private var absoluteRect: (CGRect, CGSize)? diff --git a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift index f8436d5c2e..059c5cc92b 100644 --- a/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatRecentActionsControllerNode.swift @@ -236,6 +236,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { return self?.getNavigationController() }, chatControllerNode: { [weak self] in return self + }, reactionContainerNode: { + return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] action, message in if let strongSelf = self { switch action { diff --git a/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift b/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift index 249ad1b565..15bc05eb88 100644 --- a/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/OverlayPlayerControllerNode.swift @@ -90,6 +90,8 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu return nil }, chatControllerNode: { return nil + }, reactionContainerNode: { + return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { _, _ in diff --git a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift index ae332ef3f2..04d64427e7 100644 --- a/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift +++ b/submodules/TelegramUI/TelegramUI/PeerMediaCollectionController.swift @@ -219,6 +219,8 @@ public class PeerMediaCollectionController: TelegramBaseController { return nil }, chatControllerNode: { return nil + }, reactionContainerNode: { + return nil }, presentGlobalOverlayController: { _, _ in }, callPeer: { _ in }, longTap: { [weak self] content, _ in if let strongSelf = self { diff --git a/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj b/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj index 2ee0f07eba..2a6ac4b0a8 100644 --- a/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj +++ b/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj @@ -195,6 +195,7 @@ D03E449E2305B6A00049C28B /* WatchBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E449D2305B6A00049C28B /* WatchBridge.framework */; }; D03E44E22305BC900049C28B /* LegacyDataImport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E44E12305BC900049C28B /* LegacyDataImport.framework */; }; D03E45252305C07A0049C28B /* ShareItems.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E45242305C07A0049C28B /* ShareItems.framework */; }; + D03E46102305FD360049C28B /* ReactionSelectionNode.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D03E460F2305FD360049C28B /* ReactionSelectionNode.framework */; }; D04203152037162700490EA5 /* MediaInputPaneTrendingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04203142037162700490EA5 /* MediaInputPaneTrendingItem.swift */; }; D04281F4200E5AB0009DDE36 /* ChatRecentActionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281F3200E5AB0009DDE36 /* ChatRecentActionsController.swift */; }; D04281F6200E5AC2009DDE36 /* ChatRecentActionsControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04281F5200E5AC2009DDE36 /* ChatRecentActionsControllerNode.swift */; }; @@ -915,6 +916,7 @@ D03E449D2305B6A00049C28B /* WatchBridge.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = WatchBridge.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D03E44E12305BC900049C28B /* LegacyDataImport.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LegacyDataImport.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D03E45242305C07A0049C28B /* ShareItems.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ShareItems.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D03E460F2305FD360049C28B /* ReactionSelectionNode.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactionSelectionNode.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D03E5E081E55C49C0029569A /* DebugAccountsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugAccountsController.swift; sourceTree = ""; }; D04203142037162700490EA5 /* MediaInputPaneTrendingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaInputPaneTrendingItem.swift; sourceTree = ""; }; D04281F3200E5AB0009DDE36 /* ChatRecentActionsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRecentActionsController.swift; sourceTree = ""; }; @@ -1377,6 +1379,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D03E46102305FD360049C28B /* ReactionSelectionNode.framework in Frameworks */, D03E45252305C07A0049C28B /* ShareItems.framework in Frameworks */, D03E44E22305BC900049C28B /* LegacyDataImport.framework in Frameworks */, D03E449E2305B6A00049C28B /* WatchBridge.framework in Frameworks */, @@ -2085,6 +2088,7 @@ D08D45281D5E340200A7428A /* Frameworks */ = { isa = PBXGroup; children = ( + D03E460F2305FD360049C28B /* ReactionSelectionNode.framework */, D03E45242305C07A0049C28B /* ShareItems.framework */, D03E44E12305BC900049C28B /* LegacyDataImport.framework */, D03E449D2305B6A00049C28B /* WatchBridge.framework */,