diff --git a/submodules/AnimationUI/Sources/AnimatedStickerNode.swift b/submodules/AnimationUI/Sources/AnimatedStickerNode.swift index e6a1df697b..9efaa25127 100644 --- a/submodules/AnimationUI/Sources/AnimatedStickerNode.swift +++ b/submodules/AnimationUI/Sources/AnimatedStickerNode.swift @@ -301,6 +301,11 @@ public struct AnimatedStickerStatus: Equatable { } } +public enum AnimatedStickerNodeResource { + case resource(MediaResource) + case localFile(String) +} + public final class AnimatedStickerNode: ASDisplayNode { private let queue: Queue private var account: Account? @@ -381,28 +386,38 @@ public final class AnimatedStickerNode: ASDisplayNode { self.addSubnode(self.renderer!) } - public func setup(account: Account, resource: MediaResource, fitzModifier: EmojiFitzModifier? = nil, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) { + public func setup(account: Account, resource: AnimatedStickerNodeResource, fitzModifier: EmojiFitzModifier? = nil, width: Int, height: Int, playbackMode: AnimatedStickerPlaybackMode = .loop, mode: AnimatedStickerMode) { if width < 2 || height < 2 { return } self.playbackMode = playbackMode switch mode { - case .direct: + case .direct: + let f: (MediaResourceData) -> Void = { [weak self] data in + guard let strongSelf = self, data.complete else { + return + } + if let directData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.mappedRead]) { + strongSelf.directData = Tuple(directData, data.path, width, height) + } + if strongSelf.isPlaying { + strongSelf.play() + } else if strongSelf.canDisplayFirstFrame { + strongSelf.play(firstFrame: true) + } + } + switch resource { + case let .resource(resource): self.disposable.set((account.postbox.mediaBox.resourceData(resource) - |> deliverOnMainQueue).start(next: { [weak self] data in - guard let strongSelf = self, data.complete else { - return - } - if let directData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.mappedRead]) { - strongSelf.directData = Tuple(directData, data.path, width, height) - } - if strongSelf.isPlaying { - strongSelf.play() - } else if strongSelf.canDisplayFirstFrame { - strongSelf.play(firstFrame: true) - } + |> deliverOnMainQueue).start(next: { data in + f(data) })) - case .cached: + case let .localFile(path): + f(MediaResourceData(path: path, offset: 0, size: Int(Int32.max - 1), complete: true)) + } + case .cached: + switch resource { + case let .resource(resource): self.disposable.set((chatMessageAnimationData(postbox: account.postbox, resource: resource, fitzModifier: fitzModifier, width: width, height: height, synchronousLoad: false) |> deliverOnMainQueue).start(next: { [weak self] data in if let strongSelf = self, data.complete { @@ -414,6 +429,9 @@ public final class AnimatedStickerNode: ASDisplayNode { } } })) + case .localFile: + break + } } } diff --git a/submodules/ContextUI/ContextUI_Xcode.xcodeproj/project.pbxproj b/submodules/ContextUI/ContextUI_Xcode.xcodeproj/project.pbxproj index 9f3333daec..1bb4fb705e 100644 --- a/submodules/ContextUI/ContextUI_Xcode.xcodeproj/project.pbxproj +++ b/submodules/ContextUI/ContextUI_Xcode.xcodeproj/project.pbxproj @@ -13,6 +13,8 @@ D038AC7522F8A06200320981 /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D038AC7422F8A06200320981 /* Display.framework */; }; D038AC7722F8A07000320981 /* ContextController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D038AC7622F8A07000320981 /* ContextController.swift */; }; D038AC7922F8A08A00320981 /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */; }; + D0624F93230C0CB7000FC2BD /* ReactionSelectionNode.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0624F92230C0CB7000FC2BD /* ReactionSelectionNode.framework */; }; + D0624F95230C0D2C000FC2BD /* TelegramCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0624F94230C0D2C000FC2BD /* TelegramCore.framework */; }; D09E777F22F8E47000B9CCA7 /* TelegramPresentationData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E777E22F8E47000B9CCA7 /* TelegramPresentationData.framework */; }; D09E778122F8E62000B9CCA7 /* ContextActionsContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E778022F8E62000B9CCA7 /* ContextActionsContainerNode.swift */; }; D09E778322F8E67300B9CCA7 /* ContextActionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E778222F8E67300B9CCA7 /* ContextActionNode.swift */; }; @@ -30,6 +32,8 @@ D038AC7422F8A06200320981 /* Display.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Display.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D038AC7622F8A07000320981 /* ContextController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextController.swift; sourceTree = ""; }; D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0624F92230C0CB7000FC2BD /* ReactionSelectionNode.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = ReactionSelectionNode.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D0624F94230C0D2C000FC2BD /* TelegramCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D09E777E22F8E47000B9CCA7 /* TelegramPresentationData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramPresentationData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D09E778022F8E62000B9CCA7 /* ContextActionsContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextActionsContainerNode.swift; sourceTree = ""; }; D09E778222F8E67300B9CCA7 /* ContextActionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextActionNode.swift; sourceTree = ""; }; @@ -43,6 +47,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D0624F95230C0D2C000FC2BD /* TelegramCore.framework in Frameworks */, + D0624F93230C0CB7000FC2BD /* ReactionSelectionNode.framework in Frameworks */, D0C9CBE42302D45F00FAB518 /* TextSelectionNode.framework in Frameworks */, D09E777F22F8E47000B9CCA7 /* TelegramPresentationData.framework in Frameworks */, D038AC7922F8A08A00320981 /* AsyncDisplayKit.framework in Frameworks */, @@ -89,6 +95,8 @@ D038AC6F22F8A05A00320981 /* Frameworks */ = { isa = PBXGroup; children = ( + D0624F94230C0D2C000FC2BD /* TelegramCore.framework */, + D0624F92230C0CB7000FC2BD /* ReactionSelectionNode.framework */, D0C9CBE32302D45F00FAB518 /* TextSelectionNode.framework */, D09E777E22F8E47000B9CCA7 /* TelegramPresentationData.framework */, D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */, diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/ContextUI/Sources/ContextActionNode.swift index 7881690856..46ca6f16b0 100644 --- a/submodules/ContextUI/Sources/ContextActionNode.swift +++ b/submodules/ContextUI/Sources/ContextActionNode.swift @@ -103,7 +103,7 @@ final class ContextActionNode: ASDisplayNode { func updateLayout(constrainedWidth: CGFloat, previous: ContextActionSibling, next: ContextActionSibling) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) { let sideInset: CGFloat = 16.0 - let iconSideInset: CGFloat = 8.0 + let iconSideInset: CGFloat = 12.0 let verticalInset: CGFloat = 12.0 let iconSize = self.iconNode.image.flatMap({ $0.size }) ?? CGSize() diff --git a/submodules/ContextUI/Sources/ContextContentSourceNode.swift b/submodules/ContextUI/Sources/ContextContentSourceNode.swift index 3c37d7090b..a2f91e3cd3 100644 --- a/submodules/ContextUI/Sources/ContextContentSourceNode.swift +++ b/submodules/ContextUI/Sources/ContextContentSourceNode.swift @@ -12,6 +12,7 @@ public final class ContextContentContainingNode: ASDisplayNode { public var applyAbsoluteOffset: ((CGFloat, ContainedViewLayoutTransitionCurve, Double) -> Void)? public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)? public var layoutUpdated: ((CGSize) -> Void)? + public var updateDistractionFreeMode: ((Bool) -> Void)? public override init() { self.contentNode = ContextContentNode() diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index 2e045b0cdf..e1a9e3b786 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -4,6 +4,8 @@ import AsyncDisplayKit import Display import TelegramPresentationData import TextSelectionNode +import ReactionSelectionNode +import TelegramCore public enum ContextMenuActionItemTextLayout { case singleLine @@ -48,6 +50,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private let source: ContextControllerContentSource private var items: [ContextMenuItem] private let beginDismiss: (ContextMenuActionResult) -> Void + private let reactionSelected: (String) -> Void private var validLayout: ContainerViewLayout? @@ -64,20 +67,26 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi private var contentParentNode: ContextContentContainingNode? private let contentContainerNode: ContextContentContainerNode private var actionsContainerNode: ContextActionsContainerNode + private var reactionContextNode: ReactionContextNode? + private var reactionContextNodeIsAnimatingOut = false private var didCompleteAnimationIn = false private var initialContinueGesturePoint: CGPoint? private var didMoveFromInitialGesturePoint = false private var highlightedActionNode: ContextActionNode? + private var highlightedReaction: String? private let hapticFeedback = HapticFeedback() - init(controller: ContextController, theme: PresentationTheme, strings: PresentationStrings, source: ContextControllerContentSource, items: [ContextMenuItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?) { + private var isAnimatingOut = false + + init(account: Account, controller: ContextController, theme: PresentationTheme, strings: PresentationStrings, source: ContextControllerContentSource, items: [ContextMenuItem], reactionItems: [ReactionContextItem], beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, reactionSelected: @escaping (String) -> Void) { self.theme = theme self.strings = strings self.source = source self.items = items self.beginDismiss = beginDismiss + self.reactionSelected = reactionSelected self.effectView = UIVisualEffectView() if #available(iOS 9.0, *) { @@ -114,16 +123,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi beginDismiss(result) }) - super.init() + if !reactionItems.isEmpty { + let reactionContextNode = ReactionContextNode(account: account, theme: theme, items: reactionItems) + self.reactionContextNode = reactionContextNode + } else { + self.reactionContextNode = nil + } - /*if #available(iOS 10.0, *) { - let propertyAnimator = UIViewPropertyAnimator(duration: 0.4, curve: .linear) - propertyAnimator.isInterruptible = true - propertyAnimator.addAnimations { - self.effectView.effect = makeCustomZoomBlurEffect() - } - self.propertyAnimator = propertyAnimator - }*/ + super.init() self.scrollNode.view.delegate = self @@ -136,6 +143,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi self.scrollNode.addSubnode(self.actionsContainerNode) self.scrollNode.addSubnode(self.contentContainerNode) + self.reactionContextNode.flatMap(self.addSubnode) getController = { [weak controller] in return controller @@ -172,6 +180,24 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi strongSelf.hapticFeedback.tap() } } + + if let reactionContextNode = strongSelf.reactionContextNode { + let highlightedReaction = reactionContextNode.reaction(at: strongSelf.view.convert(localPoint, to: reactionContextNode.view)).flatMap { value -> String? in + switch value { + case let .reaction(reaction, _, _): + return reaction + default: + return nil + } + } + if strongSelf.highlightedReaction != highlightedReaction { + strongSelf.highlightedReaction = highlightedReaction + reactionContextNode.setHighlightedReaction(highlightedReaction) + if let _ = highlightedReaction { + strongSelf.hapticFeedback.tap() + } + } + } } } } @@ -181,21 +207,43 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } recognizer.externalUpdated = nil if strongSelf.didMoveFromInitialGesturePoint { - if let (view, point) = viewAndPoint { - let _ = strongSelf.view.convert(point, from: view) + if let (_, _) = viewAndPoint { if let highlightedActionNode = strongSelf.highlightedActionNode { strongSelf.highlightedActionNode = nil highlightedActionNode.performAction() } + if let _ = strongSelf.reactionContextNode { + if let reaction = strongSelf.highlightedReaction { + strongSelf.reactionSelected(reaction) + } + } } else { if let highlightedActionNode = strongSelf.highlightedActionNode { strongSelf.highlightedActionNode = nil highlightedActionNode.setIsHighlighted(false) } + if let reactionContextNode = strongSelf.reactionContextNode, let _ = strongSelf.highlightedReaction { + strongSelf.highlightedReaction = nil + reactionContextNode.setHighlightedReaction(nil) + } } } } } + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.reactionSelected = { [weak self] reaction in + guard let _ = self else { + return + } + switch reaction { + case let .reaction(value, _, _): + reactionSelected(value) + default: + break + } + } + } } deinit { @@ -229,11 +277,34 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi guard let strongSelf = self, let contentParentNode = contentParentNode, let parentSupernode = contentParentNode.supernode else { return } + if strongSelf.isAnimatingOut { + return + } strongSelf.originalProjectedContentViewFrame = (parentSupernode.view.convert(contentParentNode.frame, to: strongSelf.view), contentParentNode.view.convert(contentParentNode.contentRect, to: strongSelf.view)) if let validLayout = strongSelf.validLayout { strongSelf.updateLayout(layout: validLayout, transition: .animated(duration: 0.2, curve: .easeInOut), previousActionsContainerNode: nil) } } + takenViewInfo.contentContainingNode.updateDistractionFreeMode = { [weak self] value in + guard let strongSelf = self, let reactionContextNode = strongSelf.reactionContextNode else { + return + } + if value { + if !reactionContextNode.alpha.isZero { + reactionContextNode.alpha = 0.0 + reactionContextNode.allowsGroupOpacity = true + reactionContextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, completion: { [weak reactionContextNode] _ in + reactionContextNode?.allowsGroupOpacity = false + }) + } + } else if reactionContextNode.alpha != 1.0 { + reactionContextNode.alpha = 1.0 + reactionContextNode.allowsGroupOpacity = true + reactionContextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, completion: { [weak reactionContextNode] _ in + reactionContextNode?.allowsGroupOpacity = false + }) + } + } self.contentContainerNode.contentNode = takenViewInfo.contentContainingNode.contentNode self.contentAreaInScreenSpace = takenViewInfo.contentAreaInScreenSpace self.contentContainerNode.addSubnode(takenViewInfo.contentContainingNode.contentNode) @@ -264,6 +335,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } else { UIView.animate(withDuration: 0.2, animations: { self.effectView.effect = makeCustomZoomBlurEffect() + }, completion: { [weak self] _ in + self?.didCompleteAnimationIn = true }) } self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -274,6 +347,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame, let contentParentNode = self.contentParentNode { let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.animateIn(from: CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: contentParentNode.contentRect.size)) + } + self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) @@ -283,6 +360,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) { self.isUserInteractionEnabled = false + self.isAnimatingOut = true var completedEffect = false var completedContentNode = false @@ -357,6 +435,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi }) contentParentNode.updateAbsoluteRect?(self.contentContainerNode.frame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y + contentContainerOffset.y), self.bounds.size) contentParentNode.applyAbsoluteOffset?(-contentContainerOffset.y, .easeInOut, 0.2) + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.animateOut(to: CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: contentParentNode.contentRect.size), animatingOutToReaction: self.reactionContextNodeIsAnimatingOut) + } } else if let contentParentNode = self.contentParentNode { if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree() { self.contentContainerNode.view.addSubview(snapshotView) @@ -370,9 +452,46 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi completedContentNode = true intermediateCompletion() }) + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.animateOut(to: nil, animatingOutToReaction: self.reactionContextNodeIsAnimatingOut) + } } } + func animateOutToReaction(value: String, into targetNode: ASImageNode, hideNode: Bool, completion: @escaping () -> Void) { + guard let reactionContextNode = self.reactionContextNode else { + self.animateOut(result: .default, completion: completion) + return + } + var contentCompleted = false + var reactionCompleted = false + let intermediateCompletion: () -> Void = { + if contentCompleted && reactionCompleted { + completion() + } + } + + self.reactionContextNodeIsAnimatingOut = true + self.animateOut(result: .default, completion: { + contentCompleted = true + intermediateCompletion() + }) + reactionContextNode.animateOutToReaction(value: value, targetNode: targetNode, hideNode: hideNode, completion: { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.reactionContextNode?.removeFromSupernode() + strongSelf.reactionContextNode = nil + reactionCompleted = true + intermediateCompletion() + /*strongSelf.animateOut(result: .default, completion: { + reactionCompleted = true + intermediateCompletion() + })*/ + }) + } + func setItems(controller: ContextController, items: [ContextMenuItem]) { self.items = items @@ -406,9 +525,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) - let contentActionsSpacing: CGFloat = 11.0 + let contentActionsSpacing: CGFloat = 7.0 let actionsSideInset: CGFloat = 11.0 - let contentTopInset: CGFloat = max(11.0, layout.statusBarHeight ?? 0.0) + var contentTopInset: CGFloat = max(11.0, layout.statusBarHeight ?? 0.0) + if let _ = self.reactionContextNode { + contentTopInset += 34.0 + } let actionsBottomInset: CGFloat = 11.0 if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame, let contentParentNode = self.contentParentNode { @@ -452,7 +574,13 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi } } - contentParentNode.updateAbsoluteRect?(contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y), layout.size) + let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) + + contentParentNode.updateAbsoluteRect?(absoluteContentRect, layout.size) + + if let reactionContextNode = self.reactionContextNode { + reactionContextNode.updateLayout(size: layout.size, anchorRect: CGRect(origin: CGPoint(x: absoluteContentRect.minX + contentParentNode.contentRect.minX, y: absoluteContentRect.minY + contentParentNode.contentRect.minY), size: contentParentNode.contentRect.size), transition: transition) + } } if let previousActionsContainerNode = previousActionsContainerNode { @@ -483,6 +611,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi if !self.bounds.contains(point) { return nil } + if let reactionContextNode = self.reactionContextNode { + if let result = reactionContextNode.hitTest(self.view.convert(point, to: reactionContextNode.view), with: event) { + return result + } + } let mappedPoint = self.view.convert(point, to: self.scrollNode.view) if self.actionsContainerNode.frame.contains(mappedPoint) { return self.actionsContainerNode.hitTest(self.view.convert(point, to: self.actionsContainerNode.view), with: event) @@ -524,10 +657,12 @@ public protocol ContextControllerContentSource: class { } public final class ContextController: ViewController { + private let account: Account private var theme: PresentationTheme private var strings: PresentationStrings private let source: ContextControllerContentSource private var items: [ContextMenuItem] + private var reactionItems: [ReactionContextItem] private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer? @@ -538,11 +673,15 @@ public final class ContextController: ViewController { return self.displayNode as! ContextControllerNode } - public init(theme: PresentationTheme, strings: PresentationStrings, source: ContextControllerContentSource, items: [ContextMenuItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil) { + public var reactionSelected: ((String) -> Void)? + + public init(account: Account, theme: PresentationTheme, strings: PresentationStrings, source: ContextControllerContentSource, items: [ContextMenuItem], reactionItems: [ReactionContextItem], recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil) { + self.account = account self.theme = theme self.strings = strings self.source = source self.items = items + self.reactionItems = reactionItems self.recognizer = recognizer super.init(navigationBarPresentationData: nil) @@ -555,9 +694,14 @@ public final class ContextController: ViewController { } override public func loadDisplayNode() { - self.displayNode = ContextControllerNode(controller: self, theme: self.theme, strings: self.strings, source: self.source, items: self.items, beginDismiss: { [weak self] result in + self.displayNode = ContextControllerNode(account: self.account, controller: self, theme: self.theme, strings: self.strings, source: self.source, items: self.items, reactionItems: self.reactionItems, beginDismiss: { [weak self] result in self?.dismiss(result: result, completion: nil) - }, recognizer: self.recognizer) + }, recognizer: self.recognizer, reactionSelected: { [weak self] value in + guard let strongSelf = self else { + return + } + strongSelf.reactionSelected?(value) + }) self.displayNodeDidLoad() } @@ -600,4 +744,14 @@ public final class ContextController: ViewController { override public func dismiss(completion: (() -> Void)? = nil) { self.dismiss(result: .default, completion: completion) } + + public func dismissWithReaction(value: String, into targetNode: ASImageNode, hideNode: Bool, completion: (() -> Void)?) { + if !self.wasDismissed { + self.wasDismissed = true + self.controllerNode.animateOutToReaction(value: value, into: targetNode, hideNode: hideNode, completion: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + completion?() + }) + } + } } diff --git a/submodules/Display/Display/ContainedViewLayoutTransition.swift b/submodules/Display/Display/ContainedViewLayoutTransition.swift index ec438b5a21..147c8e762a 100644 --- a/submodules/Display/Display/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Display/ContainedViewLayoutTransition.swift @@ -378,7 +378,7 @@ public extension ContainedViewLayoutTransition { } } - func updateAlpha(node: ASDisplayNode, alpha: CGFloat, completion: ((Bool) -> Void)? = nil) { + func updateAlpha(node: ASDisplayNode, alpha: CGFloat, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if node.alpha.isEqual(to: alpha) { if let completion = completion { completion(true) @@ -393,7 +393,12 @@ public extension ContainedViewLayoutTransition { completion(true) } case let .animated(duration, curve): - let previousAlpha = node.alpha + let previousAlpha: CGFloat + if beginWithCurrentState, let presentation = node.layer.presentation() { + previousAlpha = CGFloat(presentation.opacity) + } else { + previousAlpha = node.alpha + } node.alpha = alpha node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in if let completion = completion { diff --git a/submodules/Display/Display/TapLongTapOrDoubleTapGestureRecognizer.swift b/submodules/Display/Display/TapLongTapOrDoubleTapGestureRecognizer.swift index 930240dcfd..add9373781 100644 --- a/submodules/Display/Display/TapLongTapOrDoubleTapGestureRecognizer.swift +++ b/submodules/Display/Display/TapLongTapOrDoubleTapGestureRecognizer.swift @@ -120,9 +120,9 @@ public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, self.lastRecognizedGestureAndLocation = (.longTap, location) if let longTap = self.longTap { self.recognizedLongTap = true + self.state = .began longTap(location, self) cancelScrollViewGestures(view: self.view?.superview) - self.state = .began return } } else { diff --git a/submodules/Display/Display/UIKitUtils.swift b/submodules/Display/Display/UIKitUtils.swift index db471b3670..3c9f5ac627 100644 --- a/submodules/Display/Display/UIKitUtils.swift +++ b/submodules/Display/Display/UIKitUtils.swift @@ -329,6 +329,7 @@ private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? { for sublayer in sublayers { let subtree = makeLayerSubtreeSnapshot(layer: sublayer) if let subtree = subtree { + subtree.transform = sublayer.transform subtree.frame = sublayer.frame subtree.bounds = sublayer.bounds layer.addSublayer(subtree) diff --git a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift index 91b50ea909..ce5115a171 100644 --- a/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift +++ b/submodules/ItemListStickerPackItem/Sources/ItemListStickerPackItem.swift @@ -582,7 +582,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode { animationNode = AnimatedStickerNode() strongSelf.animationNode = animationNode strongSelf.addSubnode(animationNode) - animationNode.setup(account: item.account, resource: resource, width: 80, height: 80, mode: .cached) + animationNode.setup(account: item.account, resource: .resource(resource), width: 80, height: 80, mode: .cached) } animationNode.visibility = strongSelf.visibility != .none && item.playAnimatedStickers animationNode.isHidden = !item.playAnimatedStickers diff --git a/submodules/ReactionSelectionNode/ReactionSelectionNode_Xcode.xcodeproj/project.pbxproj b/submodules/ReactionSelectionNode/ReactionSelectionNode_Xcode.xcodeproj/project.pbxproj index e16fea7138..e892d700cf 100644 --- a/submodules/ReactionSelectionNode/ReactionSelectionNode_Xcode.xcodeproj/project.pbxproj +++ b/submodules/ReactionSelectionNode/ReactionSelectionNode_Xcode.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 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 */; }; + D0624F8F230C0316000FC2BD /* ReactionContextNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0624F8E230C0316000FC2BD /* ReactionContextNode.swift */; }; + D0624F91230C0472000FC2BD /* TelegramPresentationData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0624F90230C0472000FC2BD /* TelegramPresentationData.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -36,6 +38,8 @@ 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 = ""; }; + D0624F8E230C0316000FC2BD /* ReactionContextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContextNode.swift; sourceTree = ""; }; + D0624F90230C0472000FC2BD /* TelegramPresentationData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramPresentationData.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -43,6 +47,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D0624F91230C0472000FC2BD /* TelegramPresentationData.framework in Frameworks */, D03E46042305EE790049C28B /* TelegramCore.framework in Frameworks */, D03E46022305EE750049C28B /* Postbox.framework in Frameworks */, D03E46002305EE710049C28B /* AnimationUI.framework in Frameworks */, @@ -78,10 +83,11 @@ isa = PBXGroup; children = ( D03E45F42305EB7C0049C28B /* ReactionSelectionNode.swift */, - D03E45E82305E3F30049C28B /* ReactionSelectionNode.h */, + D0624F8E230C0316000FC2BD /* ReactionContextNode.swift */, D03E46072305EEDD0049C28B /* ReactionSelectionParentNode.swift */, D03E46092305EF900049C28B /* ReactionSwipeGestureRecognizer.swift */, D03E460B2305EFD80049C28B /* ReactionGestureItem.swift */, + D03E45E82305E3F30049C28B /* ReactionSelectionNode.h */, ); path = Sources; sourceTree = ""; @@ -89,6 +95,7 @@ D03E45F62305EB870049C28B /* Frameworks */ = { isa = PBXGroup; children = ( + D0624F90230C0472000FC2BD /* TelegramPresentationData.framework */, D03E46032305EE790049C28B /* TelegramCore.framework */, D03E46012305EE750049C28B /* Postbox.framework */, D03E45FF2305EE710049C28B /* AnimationUI.framework */, @@ -183,6 +190,7 @@ D03E460A2305EF900049C28B /* ReactionSwipeGestureRecognizer.swift in Sources */, D03E46082305EEDD0049C28B /* ReactionSelectionParentNode.swift in Sources */, D03E460C2305EFD80049C28B /* ReactionGestureItem.swift in Sources */, + D0624F8F230C0316000FC2BD /* ReactionContextNode.swift in Sources */, D03E45F52305EB7C0049C28B /* ReactionSelectionNode.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift new file mode 100644 index 0000000000..8c958722c4 --- /dev/null +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -0,0 +1,423 @@ +import Foundation +import AsyncDisplayKit +import Display +import AnimationUI +import TelegramCore +import TelegramPresentationData + +public final class ReactionContextItem { + public let value: String + public let text: String + public let path: String + + public init(value: String, text: String, path: String) { + self.value = value + self.text = text + self.path = path + } +} + +private let largeCircleSize: CGFloat = 16.0 +private let smallCircleSize: CGFloat = 8.0 + +private func generateBackgroundImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter * 2.0 + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setBlendMode(.copy) + context.setFillColor(foreground.cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur + diameter, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: shadowBlur + diameter / 2.0, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(diameter + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) +} + +private func generateBackgroundShadowImage(shadow: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { + return generateImage(CGSize(width: diameter * 2.0 + 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.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur + diameter, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: shadowBlur + diameter / 2.0, 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))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: shadowBlur + diameter, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + context.fill(CGRect(origin: CGPoint(x: shadowBlur + diameter / 2.0, y: shadowBlur), size: CGSize(width: diameter, height: diameter))) + })?.stretchableImage(withLeftCapWidth: Int(diameter + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) +} + +private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: 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, shadowBlur: 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)) +} + +public final class ReactionContextNode: ASDisplayNode { + private let theme: PresentationTheme + private let items: [ReactionContextItem] + + private let backgroundNode: ASImageNode + private let backgroundShadowNode: ASImageNode + private let backgroundContainerNode: ASDisplayNode + + private let largeCircleNode: ASImageNode + private let largeCircleShadowNode: ASImageNode + + private let smallCircleNode: ASImageNode + private let smallCircleShadowNode: ASImageNode + + private var itemNodes: [ReactionNode] = [] + + private var isExpanded: Bool = false + private var highlightedReaction: String? + private var validLayout: (CGSize, CGRect)? + + public var reactionSelected: ((ReactionGestureItem) -> Void)? + + private let hapticFeedback = HapticFeedback() + + public init(account: Account, theme: PresentationTheme, items: [ReactionContextItem]) { + self.theme = theme + self.items = items + + let shadowBlur: CGFloat = 5.0 + + self.backgroundNode = ASImageNode() + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.displaysAsynchronously = false + + self.backgroundShadowNode = ASImageNode() + self.backgroundShadowNode.displayWithoutProcessing = true + self.backgroundShadowNode.displaysAsynchronously = false + + self.backgroundContainerNode = ASDisplayNode() + self.backgroundContainerNode.allowsGroupOpacity = true + + self.largeCircleNode = ASImageNode() + self.largeCircleNode.displayWithoutProcessing = true + self.largeCircleNode.displaysAsynchronously = false + + self.largeCircleShadowNode = ASImageNode() + self.largeCircleShadowNode.displayWithoutProcessing = true + self.largeCircleShadowNode.displaysAsynchronously = false + + self.smallCircleNode = ASImageNode() + self.smallCircleNode.displayWithoutProcessing = true + self.smallCircleNode.displaysAsynchronously = false + + self.smallCircleShadowNode = ASImageNode() + self.smallCircleShadowNode.displayWithoutProcessing = true + self.smallCircleShadowNode.displaysAsynchronously = false + + self.backgroundNode.image = generateBackgroundImage(foreground: theme.contextMenu.backgroundColor.withAlphaComponent(1.0), diameter: 52.0, shadowBlur: shadowBlur) + + self.backgroundShadowNode.image = generateBackgroundShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: 52.0, shadowBlur: shadowBlur) + + self.largeCircleNode.image = generateBubbleImage(foreground: theme.contextMenu.backgroundColor.withAlphaComponent(1.0), diameter: largeCircleSize, shadowBlur: shadowBlur) + self.smallCircleNode.image = generateBubbleImage(foreground: theme.contextMenu.backgroundColor.withAlphaComponent(1.0), diameter: smallCircleSize, shadowBlur: shadowBlur) + + self.largeCircleShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: largeCircleSize, shadowBlur: shadowBlur) + self.smallCircleShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: smallCircleSize, shadowBlur: shadowBlur) + + super.init() + + self.addSubnode(self.smallCircleShadowNode) + self.addSubnode(self.largeCircleShadowNode) + self.addSubnode(self.backgroundShadowNode) + + self.backgroundContainerNode.addSubnode(self.smallCircleNode) + self.backgroundContainerNode.addSubnode(self.largeCircleNode) + self.backgroundContainerNode.addSubnode(self.backgroundNode) + self.addSubnode(self.backgroundContainerNode) + + self.itemNodes = self.items.map { item in + return ReactionNode(account: account, theme: theme, reaction: .reaction(value: item.value, text: item.text, path: item.path), maximizedReactionSize: 30.0 - 18.0, loadFirstFrame: false) + } + self.itemNodes.forEach(self.addSubnode) + } + + override public func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + public func updateLayout(size: CGSize, anchorRect: CGRect, transition: ContainedViewLayoutTransition) { + self.updateLayout(size: size, anchorRect: anchorRect, transition: transition, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) + } + + private func calculateBackgroundFrame(containerSize: CGSize, anchorRect: CGRect, contentSize: CGSize) -> (CGRect, Bool) { + let sideInset: CGFloat = 12.0 + let backgroundOffset: CGPoint = CGPoint(x: 22.0, y: -7.0) + + var rect: CGRect + let isLeftAligned: Bool + if anchorRect.maxX < containerSize.width - backgroundOffset.x - sideInset { + rect = CGRect(origin: CGPoint(x: anchorRect.maxX - contentSize.width + backgroundOffset.x, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize) + isLeftAligned = true + } else { + rect = CGRect(origin: CGPoint(x: anchorRect.minX - backgroundOffset.x, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize) + isLeftAligned = false + } + rect.origin.x = max(sideInset, rect.origin.x) + rect.origin.y = max(sideInset, rect.origin.y) + rect.origin.x = min(containerSize.width - contentSize.width - sideInset, rect.origin.x) + return (rect, isLeftAligned) + } + + private func updateLayout(size: CGSize, anchorRect: CGRect, transition: ContainedViewLayoutTransition, animateInFromAnchorRect: CGRect?, animateOutToAnchorRect: CGRect?, animateReactionHighlight: Bool = false) { + self.validLayout = (size, anchorRect) + + let sideInset: CGFloat = 10.0 + let itemSpacing: CGFloat = 6.0 + let minimizedItemSize: CGFloat = 30.0 + let maximizedItemSize: CGFloat = 30.0 - 18.0 + let shadowBlur: CGFloat = 5.0 + let rowHeight: CGFloat = 52.0 + + let columnCount = min(7, self.items.count) + let contentWidth = CGFloat(columnCount) * minimizedItemSize + (CGFloat(columnCount) - 1.0) * itemSpacing + sideInset * 2.0 + let rowCount = self.items.count / columnCount + (self.items.count % columnCount == 0 ? 0 : 1) + let contentHeight = rowHeight * CGFloat(rowCount) + + let (backgroundFrame, isLeftAligned) = self.calculateBackgroundFrame(containerSize: size, anchorRect: anchorRect, contentSize: CGSize(width: contentWidth, height: contentHeight)) + + for i in 0 ..< self.items.count { + let row = CGFloat(i / columnCount) + let column = CGFloat(i % columnCount) + + var reactionValue: String? + switch self.itemNodes[i].reaction { + case let .reaction(value, _, _): + reactionValue = value + default: + break + } + + let isHighlighted = reactionValue != nil && self.highlightedReaction == reactionValue + + var itemSize: CGFloat = minimizedItemSize + var itemOffset: CGFloat = 0.0 + if isHighlighted { + let updatedSize = itemSize * 1.15 + itemOffset = (updatedSize - itemSize) / 2.0 + itemSize = updatedSize + } + + transition.updateFrame(node: self.itemNodes[i], frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + sideInset + column * (minimizedItemSize + itemSpacing) - itemOffset, y: backgroundFrame.minY + row * rowHeight + floor((rowHeight - minimizedItemSize) / 2.0) - itemOffset), size: CGSize(width: itemSize, height: itemSize)), beginWithCurrentState: true) + self.itemNodes[i].updateLayout(size: CGSize(width: itemSize, height: itemSize), scale: itemSize / (maximizedItemSize + 18.0), transition: transition, displayText: false) + self.itemNodes[i].updateIsAnimating(true, animated: false) + if row != 0 { + if self.isExpanded { + self.itemNodes[i].alpha = 1.0 + } else { + self.itemNodes[i].alpha = 0.0 + } + } else { + self.itemNodes[i].alpha = 1.0 + } + } + + let isInOverflow = backgroundFrame.maxY > anchorRect.minY + let backgroundAlpha: CGFloat = isInOverflow ? 1.0 : 0.8 + let shadowAlpha: CGFloat = isInOverflow ? 1.0 : 0.0 + transition.updateAlpha(node: self.backgroundContainerNode, alpha: backgroundAlpha) + transition.updateAlpha(node: self.backgroundShadowNode, alpha: shadowAlpha) + transition.updateAlpha(node: self.largeCircleShadowNode, alpha: shadowAlpha) + transition.updateAlpha(node: self.smallCircleShadowNode, alpha: shadowAlpha) + + transition.updateFrame(node: self.backgroundContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) + + transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) + transition.updateFrame(node: self.backgroundShadowNode, frame: backgroundFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) + + let largeCircleFrame: CGRect + let smallCircleFrame: CGRect + if isLeftAligned { + largeCircleFrame = CGRect(origin: CGPoint(x: anchorRect.maxX + 22.0 - rowHeight + floor((rowHeight - largeCircleSize) / 2.0), y: backgroundFrame.maxY - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) + smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.maxX - 3.0, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) + } else { + largeCircleFrame = CGRect(origin: CGPoint(x: anchorRect.minX - 24.0 + floor((rowHeight - largeCircleSize) / 2.0), y: backgroundFrame.maxY - largeCircleSize / 2.0), size: CGSize(width: largeCircleSize, height: largeCircleSize)) + smallCircleFrame = CGRect(origin: CGPoint(x: largeCircleFrame.minX + 3.0 - smallCircleSize, y: largeCircleFrame.maxY + 2.0), size: CGSize(width: smallCircleSize, height: smallCircleSize)) + } + + transition.updateFrame(node: self.largeCircleNode, frame: largeCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) + transition.updateFrame(node: self.largeCircleShadowNode, frame: largeCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) + transition.updateFrame(node: self.smallCircleNode, frame: smallCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) + transition.updateFrame(node: self.smallCircleShadowNode, frame: smallCircleFrame.insetBy(dx: -shadowBlur, dy: -shadowBlur)) + + if let animateInFromAnchorRect = animateInFromAnchorRect { + let springDuration: Double = 0.42 + let springDamping: CGFloat = 104.0 + + let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: contentWidth, height: contentHeight)).0 + + self.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.minX - backgroundFrame.minX, y: sourceBackgroundFrame.minY - backgroundFrame.minY)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) + } else if let animateOutToAnchorRect = animateOutToAnchorRect { + let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: contentWidth, height: contentHeight)).0 + + self.layer.animatePosition(from: CGPoint(), to: CGPoint(x: targetBackgroundFrame.minX - backgroundFrame.minX, y: targetBackgroundFrame.minY - backgroundFrame.minY), duration: 0.2, removeOnCompletion: false, additive: true) + } + } + + public func animateIn(from sourceAnchorRect: CGRect) { + self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + + if let (size, anchorRect) = self.validLayout { + self.updateLayout(size: size, anchorRect: anchorRect, transition: .immediate, animateInFromAnchorRect: sourceAnchorRect, animateOutToAnchorRect: nil) + } + } + + public func animateOut(to targetAnchorRect: CGRect?, animatingOutToReaction: Bool) { + for itemNode in self.itemNodes { + self.backgroundNode.layer.animateAlpha(from: self.backgroundNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.backgroundShadowNode.layer.animateAlpha(from: self.backgroundShadowNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.largeCircleNode.layer.animateAlpha(from: self.largeCircleNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.largeCircleShadowNode.layer.animateAlpha(from: self.largeCircleShadowNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.smallCircleNode.layer.animateAlpha(from: self.smallCircleNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.smallCircleShadowNode.layer.animateAlpha(from: self.smallCircleShadowNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) + itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } + + if let targetAnchorRect = targetAnchorRect, let (size, anchorRect) = self.validLayout { + self.updateLayout(size: size, anchorRect: anchorRect, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: targetAnchorRect) + } + } + + public func animateOutToReaction(value: String, targetNode: ASImageNode, hideNode: Bool, completion: @escaping () -> Void) { + for itemNode in self.itemNodes { + switch itemNode.reaction { + case let .reaction(itemValue, _, _): + if itemValue == value { + if let snapshotView = itemNode.view.snapshotContentTree(keepTransform: true) { + let targetSnapshotView = UIImageView() + targetSnapshotView.image = targetNode.image + targetSnapshotView.frame = self.view.convert(targetNode.bounds, from: targetNode.view) + itemNode.isHidden = true + self.view.addSubview(targetSnapshotView) + self.view.addSubview(snapshotView) + + var completedTarget = false + let intermediateCompletion: () -> Void = { + if completedTarget { + completion() + } + } + + let targetPosition = self.view.convert(targetNode.bounds.center, from: targetNode.view) + let duration: Double = 0.3 + if hideNode { + targetNode.isHidden = true + } + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) + targetSnapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + targetSnapshotView.layer.animateScale(from: snapshotView.bounds.width / targetSnapshotView.bounds.width, to: 0.5, duration: 0.3, removeOnCompletion: false) + + let sourcePoint = snapshotView.center + let midPoint = CGPoint(x: (sourcePoint.x + targetPosition.x) / 2.0, y: sourcePoint.y - 30.0) + + let x1 = sourcePoint.x + let y1 = sourcePoint.y + let x2 = midPoint.x + let y2 = midPoint.y + let x3 = targetPosition.x + let y3 = targetPosition.y + + let a = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + let b = (x1 * x1 * (y2 - y3) + x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + let c = (x2 * x2 * (x3 * y1 - x1 * y3) + x2 * (x1 * x1 * y3 - x3 * x3 * y1) + x1 * x3 * (x3 - x1) * y2) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + + var keyframes: [AnyObject] = [] + for i in 0 ..< 10 { + let k = CGFloat(i) / CGFloat(10 - 1) + let x = sourcePoint.x * (1.0 - k) + targetPosition.x * k + let y = a * x * x + b * x + c + keyframes.append(NSValue(cgPoint: CGPoint(x: x, y: y))) + } + + snapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.hapticFeedback.tap() + } + completedTarget = true + if hideNode { + targetNode.isHidden = false + targetNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0) + } + intermediateCompletion() + }) + targetSnapshotView.layer.animateKeyframes(values: keyframes, duration: 0.3, keyPath: "position", removeOnCompletion: false) + + snapshotView.layer.animateScale(from: 1.0, to: (targetSnapshotView.bounds.width * 0.5) / snapshotView.bounds.width, duration: 0.3, removeOnCompletion: false) + } else { + completion() + } + return + } + default: + break + } + } + completion() + } + + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + for itemNode in self.itemNodes { + if itemNode.frame.contains(point) { + return self.view + } + } + return nil + } + + @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + let point = recognizer.location(in: self.view) + if let reaction = self.reaction(at: point) { + self.reactionSelected?(reaction) + } + } + } + + public func reaction(at point: CGPoint) -> ReactionGestureItem? { + for itemNode in self.itemNodes { + if itemNode.frame.contains(point) { + return itemNode.reaction + } + } + for itemNode in self.itemNodes { + if itemNode.frame.insetBy(dx: -8.0, dy: -8.0).contains(point) { + return itemNode.reaction + } + } + return nil + } + + public func setHighlightedReaction(_ value: String?) { + self.highlightedReaction = value + if let (size, anchorRect) = self.validLayout { + self.updateLayout(size: size, anchorRect: anchorRect, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) + } + } +} diff --git a/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift b/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift index 5a941f316c..9b058db8ca 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionGestureItem.swift @@ -3,6 +3,6 @@ import Postbox import TelegramCore public enum ReactionGestureItem { - case reaction(value: String, text: String, file: TelegramMediaFile) + case reaction(value: String, text: String, path: String) case reply } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift index 1694dca4de..d8d0a005a3 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionNode.swift @@ -4,6 +4,7 @@ import AnimationUI import Display import Postbox import TelegramCore +import TelegramPresentationData private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? { return generateImage(CGSize(width: diameter + shadowBlur * 2.0, height: diameter + shadowBlur * 2.0), rotatedContext: { size, context in @@ -27,41 +28,85 @@ private func generateBubbleShadowImage(shadow: UIColor, diameter: CGFloat, shado })?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0 + shadowBlur / 2.0), topCapHeight: Int(diameter / 2.0 + shadowBlur / 2.0)) } -private final class ReactionNode: ASDisplayNode { +private let font = Font.medium(13.0) + +final class ReactionNode: ASDisplayNode { let reaction: ReactionGestureItem + private let textBackgroundNode: ASImageNode + private let textNode: ImmediateTextNode private let animationNode: AnimatedStickerNode private let imageNode: ASImageNode var isMaximized: Bool? private let intrinsicSize: CGSize private let intrinsicOffset: CGPoint - init(account: Account, reaction: ReactionGestureItem, maximizedReactionSize: CGFloat) { + init(account: Account, theme: PresentationTheme, reaction: ReactionGestureItem, maximizedReactionSize: CGFloat, loadFirstFrame: Bool) { self.reaction = reaction + self.textBackgroundNode = ASImageNode() + self.textBackgroundNode.displaysAsynchronously = false + self.textBackgroundNode.displayWithoutProcessing = true + self.textBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: theme.chat.serviceMessage.components.withDefaultWallpaper.dateFillFloating.withAlphaComponent(0.8)) + self.textBackgroundNode.alpha = 0.0 + + self.textNode = ImmediateTextNode() + self.textNode.displaysAsynchronously = false + self.textNode.isUserInteractionEnabled = false + + let reactionText: String + switch reaction { + case let .reaction(_, text, _): + reactionText = text + case .reply: + reactionText = "Reply" + } + + self.textNode.attributedText = NSAttributedString(string: reactionText, font: font, textColor: theme.chat.serviceMessage.dateTextColor.withWallpaper) + let textSize = self.textNode.updateLayout(CGSize(width: 200.0, height: 100.0)) + let textBackgroundSize = CGSize(width: textSize.width + 12.0, height: 20.0) + let textBackgroundFrame = CGRect(origin: CGPoint(), size: textBackgroundSize) + let textFrame = CGRect(origin: CGPoint(x: floor((textBackgroundFrame.width - textSize.width) / 2.0), y: floor((textBackgroundFrame.height - textSize.height) / 2.0)), size: textSize) + self.textBackgroundNode.frame = textBackgroundFrame + self.textNode.frame = textFrame + self.textNode.alpha = 0.0 + self.animationNode = AnimatedStickerNode() - self.animationNode.automaticallyLoadFirstFrame = true + self.animationNode.automaticallyLoadFirstFrame = loadFirstFrame self.animationNode.playToCompletionOnStop = true - //self.animationNode.backgroundColor = .lightGray var intrinsicSize = CGSize(width: maximizedReactionSize + 18.0, height: maximizedReactionSize + 18.0) self.imageNode = ASImageNode() switch reaction { - case let .reaction(value, _, file): + case let .reaction(value, _, path): switch value { + case "😒": + intrinsicSize.width *= 1.7 + intrinsicSize.height *= 1.7 + self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0) case "😳": - intrinsicSize.width += 8.0 - intrinsicSize.height += 8.0 - self.intrinsicOffset = CGPoint(x: 0.0, y: -4.0) + intrinsicSize.width *= 1.15 + intrinsicSize.height *= 1.15 + self.intrinsicOffset = CGPoint(x: 0.0, y: -0.05 * intrinsicSize.width) + case "😂": + intrinsicSize.width *= 1.2 + intrinsicSize.height *= 1.2 + self.intrinsicOffset = CGPoint(x: 0.0 * intrinsicSize.width, y: 0.0 * intrinsicSize.width) case "👍": - intrinsicSize.width += 20.0 - intrinsicSize.height += 20.0 - self.intrinsicOffset = CGPoint(x: 0.0, y: 4.0) + intrinsicSize.width *= 1.256 + intrinsicSize.height *= 1.256 + self.intrinsicOffset = CGPoint(x: 0.0, y: 0.05 * intrinsicSize.width) default: self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0) } - self.animationNode.visibility = true - self.animationNode.setup(account: account, resource: file.resource, width: Int(intrinsicSize.width) * 2, height: Int(intrinsicSize.height) * 2, mode: .direct) + + var renderSize: CGSize = CGSize(width: intrinsicSize.width * 2.0, height: intrinsicSize.height * 2.0) + if UIScreen.main.scale.isEqual(to: 3.0) { + if maximizedReactionSize < 40.0 { + renderSize = CGSize(width: intrinsicSize.width * 2.5, height: intrinsicSize.height * 2.5) + } + } + self.animationNode.setup(account: account, resource: .localFile(path), width: Int(renderSize.width), height: Int(renderSize.height), mode: .direct) case .reply: self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0) self.imageNode.image = UIImage(named: "Chat/Context Menu/ReactionReply", in: Bundle(for: ReactionNode.self), compatibleWith: nil) @@ -71,7 +116,8 @@ private final class ReactionNode: ASDisplayNode { super.init() - //self.backgroundColor = .green + self.textBackgroundNode.addSubnode(self.textNode) + self.addSubnode(self.textBackgroundNode) self.addSubnode(self.animationNode) self.addSubnode(self.imageNode) @@ -80,11 +126,17 @@ private final class ReactionNode: ASDisplayNode { self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize) } - func updateLayout(size: CGSize, scale: CGFloat, transition: ContainedViewLayoutTransition) { + func updateLayout(size: CGSize, scale: CGFloat, transition: ContainedViewLayoutTransition, displayText: Bool) { 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) transition.updatePosition(node: self.imageNode, 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.imageNode, scale: scale, beginWithCurrentState: true) + + transition.updatePosition(node: self.textBackgroundNode, position: CGPoint(x: size.width / 2.0, y: displayText ? -24.0 : (size.height / 2.0)), beginWithCurrentState: true) + transition.updateTransformScale(node: self.textBackgroundNode, scale: displayText ? 1.0 : 0.1, beginWithCurrentState: true) + + transition.updateAlpha(node: self.textBackgroundNode, alpha: displayText ? 1.0 : 0.0, beginWithCurrentState: true) + transition.updateAlpha(node: self.textNode, alpha: displayText ? 1.0 : 0.0, beginWithCurrentState: true) } func updateIsAnimating(_ isAnimating: Bool, animated: Bool) { @@ -98,6 +150,7 @@ private final class ReactionNode: ASDisplayNode { final class ReactionSelectionNode: ASDisplayNode { private let account: Account + private let theme: PresentationTheme private let reactions: [ReactionGestureItem] private let backgroundNode: ASImageNode @@ -113,8 +166,11 @@ final class ReactionSelectionNode: ASDisplayNode { private var maximizedReactionSize: CGFloat = 60.0 private var smallCircleSize: CGFloat = 8.0 - public init(account: Account, reactions: [ReactionGestureItem]) { + private var isRightAligned: Bool = false + + public init(account: Account, theme: PresentationTheme, reactions: [ReactionGestureItem]) { self.account = account + self.theme = theme self.reactions = reactions self.backgroundNode = ASImageNode() @@ -153,12 +209,7 @@ final class ReactionSelectionNode: ASDisplayNode { let initialAnchorX = startingPoint.x if isInitial && self.reactionNodes.isEmpty { - //let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing - - //contentWidth = CGFloat(self.reactionNodes.count - 1) * X + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * 0.2 * X - // contentWidth - maximizedReactionSize = CGFloat(self.reactionNodes.count - 1) * X + CGFloat(self.reactionNodes.count + 1) * 0.2 * X - // (contentWidth - maximizedReactionSize) / (CGFloat(self.reactionNodes.count - 1) + CGFloat(self.reactionNodes.count + 1) * 0.2) = X - let availableContentWidth = max(100.0, initialAnchorX) + let availableContentWidth = constrainedSize.width //max(100.0, initialAnchorX) var minimizedReactionSize = (availableContentWidth - self.maximizedReactionSize) / (CGFloat(self.reactions.count - 1) + CGFloat(self.reactions.count + 1) * 0.2) minimizedReactionSize = max(16.0, floor(minimizedReactionSize)) minimizedReactionSize = min(30.0, minimizedReactionSize) @@ -177,7 +228,7 @@ final class ReactionSelectionNode: ASDisplayNode { } self.reactionNodes = self.reactions.map { reaction -> ReactionNode in - return ReactionNode(account: self.account, reaction: reaction, maximizedReactionSize: self.maximizedReactionSize) + return ReactionNode(account: self.account, theme: self.theme, reaction: reaction, maximizedReactionSize: self.maximizedReactionSize, loadFirstFrame: true) } self.reactionNodes.forEach(self.addSubnode(_:)) } @@ -190,8 +241,15 @@ final class ReactionSelectionNode: ASDisplayNode { 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: initialAnchorX - contentWidth + backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0) + var isRightAligned = false + if initialAnchorX > constrainedSize.width / 2.0 { + isRightAligned = true + backgroundFrame = backgroundFrame.offsetBy(dx: initialAnchorX - contentWidth + backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0) + } else { + backgroundFrame = backgroundFrame.offsetBy(dx: initialAnchorX - backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0) + } + self.isRightAligned = isRightAligned self.backgroundNode.frame = backgroundFrame self.backgroundShadowNode.frame = backgroundFrame @@ -200,7 +258,7 @@ final class ReactionSelectionNode: ASDisplayNode { let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart)) var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing - if offsetFromStart > backgroundFrame.maxX - shadowBlur { + if offsetFromStart > backgroundFrame.maxX - shadowBlur || offsetFromStart < backgroundFrame.minX { self.hasSelectedNode = false } else { self.hasSelectedNode = true @@ -209,8 +267,12 @@ final class ReactionSelectionNode: ASDisplayNode { 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 { + for iterationIndex in 0 ..< self.reactionNodes.count { + var i = iterationIndex let isMaximized = i == maximizedIndex + if !isRightAligned { + i = self.reactionNodes.count - 1 - i + } let reactionSize: CGFloat if isMaximized { @@ -239,7 +301,7 @@ final class ReactionSelectionNode: ASDisplayNode { 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) + self.reactionNodes[i].updateLayout(size: reactionFrame.size, scale: reactionFrame.size.width / (maximizedReactionSize + 18.0), transition: transition, displayText: isMaximized) transition.updateFrame(node: self.reactionNodes[i], frame: reactionFrame, beginWithCurrentState: true) @@ -250,7 +312,7 @@ final class ReactionSelectionNode: ASDisplayNode { self.bubbleNodes[1].0.frame = mainBubbleFrame self.bubbleNodes[1].1.frame = mainBubbleFrame - let secondaryBubbleFrame = CGRect(origin: CGPoint(x: mainBubbleFrame.midX - floor(self.smallCircleSize * 0.88) - (self.smallCircleSize + shadowBlur * 2.0) / 2.0, y: mainBubbleFrame.midY + floor(self.smallCircleSize * 4.0 / 3.0) - (self.smallCircleSize + shadowBlur * 2.0) / 2.0), size: CGSize(width: self.smallCircleSize + shadowBlur * 2.0, height: self.smallCircleSize + shadowBlur * 2.0)) + let secondaryBubbleFrame = CGRect(origin: CGPoint(x: mainBubbleFrame.midX - 10.0 - (self.smallCircleSize + shadowBlur * 2.0) / 2.0, y: mainBubbleFrame.midY + 10.0 - (self.smallCircleSize + shadowBlur * 2.0) / 2.0), size: CGSize(width: self.smallCircleSize + shadowBlur * 2.0, height: self.smallCircleSize + shadowBlur * 2.0)) self.bubbleNodes[0].0.frame = secondaryBubbleFrame self.bubbleNodes[0].1.frame = secondaryBubbleFrame } @@ -262,15 +324,25 @@ final class ReactionSelectionNode: ASDisplayNode { 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 backgroundOffset: CGPoint + if self.isRightAligned { + backgroundOffset = CGPoint(x: (self.backgroundNode.frame.width - shadowBlur) / 2.0 - 42.0, y: (self.backgroundNode.frame.height - shadowBlur) / 2.0) + } else { + 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) - var 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) + var nodeOffset: CGPoint + if self.isRightAligned { + 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) + } else { + nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.minX + shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur) + } nodeOffset.x = -nodeOffset.x nodeOffset.y = 30.0 - self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5 + animationOffset * 0.08, initialVelocity: 0.0, damping: damping) + self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5 + animationOffset * 0.28, 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) } diff --git a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift index 8a0f78779a..02719c5884 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionSelectionParentNode.swift @@ -3,17 +3,20 @@ import AsyncDisplayKit import Display import Postbox import TelegramCore +import TelegramPresentationData public final class ReactionSelectionParentNode: ASDisplayNode { private let account: Account + private let theme: PresentationTheme private var currentNode: ReactionSelectionNode? private var currentLocation: (CGPoint, CGFloat)? private var validLayout: (size: CGSize, insets: UIEdgeInsets)? - public init(account: Account) { + public init(account: Account, theme: PresentationTheme) { self.account = account + self.theme = theme super.init() } @@ -24,7 +27,7 @@ public final class ReactionSelectionParentNode: ASDisplayNode { self.currentNode = nil } - let reactionNode = ReactionSelectionNode(account: self.account, reactions: reactions) + let reactionNode = ReactionSelectionNode(account: self.account, theme: self.theme, reactions: reactions) self.addSubnode(reactionNode) self.currentNode = reactionNode self.currentLocation = (point, point.x) diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift index d8f5105cf4..cab64f5a93 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPackPreviewGridItem.swift @@ -108,7 +108,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { } } let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.0)) - self.animationNode?.setup(account: account, resource: stickerItem.file.resource, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + self.animationNode?.setup(account: account, resource: .resource(stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start()) } else { diff --git a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift index d7ee4cdc8c..9fc4f6d0e6 100644 --- a/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift +++ b/submodules/StickerPackPreviewUI/Sources/StickerPreviewPeekContent.swift @@ -91,7 +91,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController let dimensions = item.file.dimensions ?? CGSize(width: 512.0, height: 512.0) let fittedDimensions = dimensions.aspectFitted(CGSize(width: 400.0, height: 400.0)) - self.animationNode?.setup(account: account, resource: item.file.resource, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct) + self.animationNode?.setup(account: account, resource: .resource(item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct) self.animationNode?.visibility = true self.animationNode?.addSubnode(self.textNode) } else { diff --git a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Report.imageset/ic_lt_report.pdf b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Report.imageset/ic_lt_report.pdf index e64504330f..baf196a440 100644 Binary files a/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Report.imageset/ic_lt_report.pdf and b/submodules/TelegramUI/Images.xcassets/Chat/Context Menu/Report.imageset/ic_lt_report.pdf differ diff --git a/submodules/TelegramUI/TelegramUI/ChatController.swift b/submodules/TelegramUI/TelegramUI/ChatController.swift index 9d97d9bba8..f18def73d2 100644 --- a/submodules/TelegramUI/TelegramUI/ChatController.swift +++ b/submodules/TelegramUI/TelegramUI/ChatController.swift @@ -43,6 +43,7 @@ import PeerAvatarGalleryUI import PeerInfoUI import RaiseToListen import UrlHandling +import ReactionSelectionNode public enum ChatControllerPeekActions { case standard @@ -523,40 +524,49 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G break } } - let _ = contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction).start(next: { actions in + let _ = combineLatest(queue: .mainQueue(), contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: strongSelf.presentationInterfaceState, context: strongSelf.context, messages: updatedMessages, controllerInteraction: strongSelf.controllerInteraction, selectAll: selectAll, interfaceInteraction: strongSelf.interfaceInteraction), loadedStickerPack(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, reference: .animatedEmoji, forceActualized: false)).start(next: { actions, animatedEmojiStickers in guard let strongSelf = self, !actions.isEmpty else { return } - var actions = actions - if ![Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal].contains(message.id.namespace) { - actions.insert(.action(ContextMenuActionItem(text: "Reaction", icon: { _ in nil }, action: { _, f in - guard let strongSelf = self else { - return - } - let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme) - var items: [ActionSheetItem] = [] - let emojis = ["👍", "😊", "🤔", "😔", "❤️"] - for emoji in emojis { - items.append(ActionSheetButtonItem(title: "\(emoji)", color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - guard let strongSelf = self else { - return - } - let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: updatedMessages[0].id, reaction: emoji).start() - })) - } - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - strongSelf.chatDisplayNode.dismissInput() - strongSelf.present(actionSheet, in: .window(.root)) - f(.dismissWithoutContent) - })), at: 0) + let reactions: [(String, String, String)] = [ + ("😒", "Sad", "sad"), + ("😳", "Surprised", "surprised"), + ("😂", "Fun", "lol"), + ("👍", "Like", "thumbsup"), + ("❤", "Love", "heart"), + ] + + var reactionItems: [ReactionContextItem] = [] + for (value, text, name) in reactions { + if let path = frameworkBundle.path(forResource: name, ofType: "tgs", inDirectory: "BuiltinReactions") { + reactionItems.append(ReactionContextItem(value: value, text: text, path: path)) + } } - let controller = ContextController(theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: ChatMessageContextControllerContentSource(chatNode: strongSelf.chatDisplayNode, message: message), items: actions, recognizer: recognizer) + let controller = ContextController(account: strongSelf.context.account, theme: strongSelf.presentationData.theme, strings: strongSelf.presentationData.strings, source: ChatMessageContextControllerContentSource(chatNode: strongSelf.chatDisplayNode, message: message), items: actions, reactionItems: reactionItems, recognizer: recognizer) strongSelf.currentContextController = controller + controller.reactionSelected = { [weak controller] value in + guard let strongSelf = self, let message = updatedMessages.first else { + return + } + strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { + if item.message.id == message.id { + itemNode.awaitingAppliedReaction = (value, { [weak itemNode] in + guard let controller = controller else { + return + } + if let itemNode = itemNode, let (targetNode, count) = itemNode.targetReactionNode(value: value) { + controller.dismissWithReaction(value: value, into: targetNode, hideNode: count == 1, completion: { + }) + } else { + controller.dismiss() + } + }) + } + } + } + let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: value).start() + } strongSelf.window?.presentInGlobalOverlay(controller) }) } diff --git a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift index 67793a7438..3eca14e4cf 100644 --- a/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatControllerNode.swift @@ -212,7 +212,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.historyNodeContainer = ASDisplayNode() self.historyNodeContainer.addSubnode(self.historyNode) - self.reactionContainerNode = ReactionSelectionParentNode(account: context.account) + self.reactionContainerNode = ReactionSelectionParentNode(account: context.account, theme: chatPresentationInterfaceState.theme) self.historyNodeContainer.addSubnode(self.reactionContainerNode) self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper) diff --git a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift index 596232963e..1ea930f970 100644 --- a/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -592,7 +592,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && isAction { actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.primaryTextColor) + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { controller, f in interfaceInteraction.deleteMessages(messages, controller, f) }))) @@ -654,7 +654,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && !isAction { let title = message.flags.isSending ? chatPresentationInterfaceState.strings.Conversation_ContextMenuCancelSending : chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete actions.append(.action(ContextMenuActionItem(text: title, textColor: .destructive, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.primaryTextColor) + return generateTintedImage(image: UIImage(bundleImageName: message.flags.isSending ? "Chat/Context Menu/Clear" : "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { controller, f in interfaceInteraction.deleteMessages(selectAll ? messages : [message], controller, f) }))) diff --git a/submodules/TelegramUI/TelegramUI/ChatMediaInputStickerGridItem.swift b/submodules/TelegramUI/TelegramUI/ChatMediaInputStickerGridItem.swift index d1b0ab25f5..8fa2250297 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMediaInputStickerGridItem.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -299,7 +299,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { self.didSetUpAnimationNode = true let dimensions = item.stickerItem.file.dimensions ?? CGSize(width: 512.0, height: 512.0) let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.0)) - self.animationNode?.setup(account: item.account, resource: item.stickerItem.file.resource, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + self.animationNode?.setup(account: item.account, resource: .resource(item.stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) } } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMediaInputStickerPackItem.swift b/submodules/TelegramUI/TelegramUI/ChatMediaInputStickerPackItem.swift index 6314859f33..762479af04 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMediaInputStickerPackItem.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMediaInputStickerPackItem.swift @@ -177,7 +177,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode { self.animatedStickerNode = animatedStickerNode animatedStickerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0) self.addSubnode(animatedStickerNode) - animatedStickerNode.setup(account: account, resource: resource, width: 80, height: 80, mode: .cached) + animatedStickerNode.setup(account: account, resource: .resource(resource), width: 80, height: 80, mode: .cached) } animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers if let animatedStickerNode = self.animatedStickerNode { diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift index 9c1ba28f25..bc06ed281d 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageAnimatedStickerItemNode.swift @@ -304,7 +304,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { if let file = file { let dimensions = file.dimensions ?? CGSize(width: 512.0, height: 512.0) let fittedSize = isEmoji ? dimensions.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.aspectFitted(CGSize(width: 384.0, height: 384.0)) - self.animationNode.setup(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier, width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: .cached) + self.animationNode.setup(account: item.context.account, resource: .resource(file.resource), fitzModifier: fitzModifier, width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: .cached) } } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift index 9c64baca75..c1ac9f8340 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleContentNode.swift @@ -108,6 +108,8 @@ class ChatMessageBubbleContentNode: ASDisplayNode { var item: ChatMessageBubbleContentItem? + var updateIsTextSelectionActive: ((Bool) -> Void)? + required override init() { super.init() } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift index 6326abbc23..32e0f85787 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageBubbleItemNode.swift @@ -186,8 +186,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer? private var reactionRecognizer: ReactionSwipeGestureRecognizer? - private var awaitingAppliedReaction: String? - override var visibility: ListViewItemNodeVisibility { didSet { if self.visibility != oldValue { @@ -419,24 +417,24 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } } - let reactions: [(String, String)] = [ - ("😒", "Sad"), - ("😳", "Surprised"), - //("🥳", "Fun"), - ("👍", "Like"), - ("❤", "Love"), + let reactions: [(String, String, String)] = [ + ("😒", "Sad", "sad"), + ("😳", "Surprised", "surprised"), + ("😂", "Fun", "lol"), + ("👍", "Like", "thumbsup"), + ("❤", "Love", "heart"), ] - var result: [ReactionGestureItem] = [] - for (value, text) in reactions { - if let file = item.associatedData.animatedEmojiStickers[value]?.file { - result.append(.reaction(value: value, text: text, file: file)) + var reactionItems: [ReactionGestureItem] = [] + for (value, text, name) in reactions { + if let path = frameworkBundle.path(forResource: name, ofType: "tgs", inDirectory: "BuiltinReactions") { + reactionItems.append(.reaction(value: value, text: text, path: path)) } } if item.controllerInteraction.canSetupReply(item.message) { - result.append(.reply) + reactionItems.append(.reply) } - return result + return reactionItems } reactionRecognizer.getReactionContainer = { [weak self] in return self?.item?.controllerInteraction.reactionContainerNode() @@ -505,7 +503,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode if let item = strongSelf.item, let reaction = reaction { switch reaction { case let .reaction(value, _, _): - strongSelf.awaitingAppliedReaction = value + strongSelf.awaitingAppliedReaction = (value, {}) item.controllerInteraction.updateMessageReaction(item.message.id, value) case .reply: strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false) @@ -1763,6 +1761,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode strongSelf.contextSourceNode.contentNode.addSubnode(contentNode) contentNode.visibility = strongSelf.visibility + contentNode.updateIsTextSelectionActive = { [weak strongSelf] value in + strongSelf?.contextSourceNode.updateDistractionFreeMode?(value) + } contentNode.updateIsExtractedToContextPreview(strongSelf.contextSourceNode.isExtractedToContextPreview) } } @@ -1945,7 +1946,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode strongSelf.updateSearchTextHighlightState() - if let awaitingAppliedReaction = strongSelf.awaitingAppliedReaction { + if let (awaitingAppliedReaction, f) = strongSelf.awaitingAppliedReaction { var bounds = strongSelf.bounds let offset = bounds.origin.x bounds.origin.x = 0.0 @@ -1972,6 +1973,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode } } strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget) + f() } } @@ -2801,4 +2803,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) { self.contextSourceNode.contentNode.addSubnode(accessoryItemNode) } + + override func targetReactionNode(value: String) -> (ASImageNode, Int)? { + for contentNode in self.contentNodes { + if let (reactionNode, count) = contentNode.reactionTargetNode(value: value) { + return (reactionNode, count) + } + } + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift index 8dca768f0c..84061192a0 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -715,7 +715,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio strongSelf.animatedStickerNode = animatedStickerNode let dimensions = updatedAnimatedStickerFile.dimensions ?? CGSize(width: 512.0, height: 512.0) let fittedDimensions = dimensions.aspectFitted(CGSize(width: 384.0, height: 384.0)) - animatedStickerNode.setup(account: context.account, resource: updatedAnimatedStickerFile.resource, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + animatedStickerNode.setup(account: context.account, resource: .resource(updatedAnimatedStickerFile.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) strongSelf.insertSubnode(animatedStickerNode, aboveSubnode: strongSelf.imageNode) animatedStickerNode.visibility = strongSelf.visibility } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift b/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift index d61b357811..592d3a429e 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageItemView.swift @@ -611,6 +611,8 @@ public class ChatMessageItemView: ListViewItemNode { var item: ChatMessageItem? var accessibilityData: ChatMessageAccessibilityData? + var awaitingAppliedReaction: (String, () -> Void)? + public required convenience init() { self.init(layerBacked: false) } @@ -788,4 +790,8 @@ public class ChatMessageItemView: ListViewItemNode { } } } + + func targetReactionNode(value: String) -> (ASImageNode, Int)? { + return nil + } } diff --git a/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift index 2168a835ff..37b0c219c8 100644 --- a/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -521,7 +521,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override func updateIsExtractedToContextPreview(_ value: Bool) { if value { if self.textSelectionNode == nil, let item = self.item, let rootNode = item.controllerInteraction.chatControllerNode() { - let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: item.presentationData.theme.theme.list.itemAccentColor.withAlphaComponent(0.5), knob: item.presentationData.theme.theme.list.itemAccentColor), textNode: self.textNode, present: { [weak self] c, a in + let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: item.presentationData.theme.theme.list.itemAccentColor.withAlphaComponent(0.5), knob: item.presentationData.theme.theme.list.itemAccentColor), textNode: self.textNode, updateIsActive: { [weak self] value in + self?.updateIsTextSelectionActive?(value) + }, present: { [weak self] c, a in self?.item?.controllerInteraction.presentGlobalOverlayController(c, a) }, rootNode: rootNode, performAction: { [weak self] text, action in guard let strongSelf = self, let item = strongSelf.item else { @@ -535,6 +537,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } else if let textSelectionNode = self.textSelectionNode { self.textSelectionNode = nil + self.updateIsTextSelectionActive?(false) textSelectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in textSelectionNode?.removeFromSupernode() }) diff --git a/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift b/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift index 923495c013..fdc1f556ef 100644 --- a/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift +++ b/submodules/TelegramUI/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift @@ -386,7 +386,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode } let dimensions = animatedStickerFile.dimensions ?? CGSize(width: 512.0, height: 512.0) let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.0)) - animationNode.setup(account: item.account, resource: animatedStickerFile.resource, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + animationNode.setup(account: item.account, resource: .resource(animatedStickerFile.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) } } diff --git a/submodules/TelegramUI/TelegramUI/HorizontalStickerGridItem.swift b/submodules/TelegramUI/TelegramUI/HorizontalStickerGridItem.swift index 44d68c2eb5..bba9baf876 100755 --- a/submodules/TelegramUI/TelegramUI/HorizontalStickerGridItem.swift +++ b/submodules/TelegramUI/TelegramUI/HorizontalStickerGridItem.swift @@ -111,7 +111,7 @@ final class HorizontalStickerGridItemNode: GridItemNode { } let dimensions = item.file.dimensions ?? CGSize(width: 512.0, height: 512.0) let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.0)) - animationNode.setup(account: account, resource: item.file.resource, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + animationNode.setup(account: account, resource: .resource(item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start()) } else { diff --git a/submodules/TelegramUI/TelegramUI/MediaInputPaneTrendingItem.swift b/submodules/TelegramUI/TelegramUI/MediaInputPaneTrendingItem.swift index ba165be0e8..c305fa7165 100644 --- a/submodules/TelegramUI/TelegramUI/MediaInputPaneTrendingItem.swift +++ b/submodules/TelegramUI/TelegramUI/MediaInputPaneTrendingItem.swift @@ -118,7 +118,7 @@ final class TrendingTopItemNode: ASDisplayNode { } let dimensions = item.file.dimensions ?? CGSize(width: 512.0, height: 512.0) let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.0)) - animationNode.setup(account: account, resource: item.file.resource, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + animationNode.setup(account: account, resource: .resource(item.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) self.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start()) } else { self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads) diff --git a/submodules/TelegramUI/TelegramUI/NotificationContentContext.swift b/submodules/TelegramUI/TelegramUI/NotificationContentContext.swift index 9cc9abaa99..55a05c1cbc 100644 --- a/submodules/TelegramUI/TelegramUI/NotificationContentContext.swift +++ b/submodules/TelegramUI/TelegramUI/NotificationContentContext.swift @@ -286,7 +286,7 @@ public final class NotificationViewControllerImpl { let dimensions = fileReference.media.dimensions ?? CGSize(width: 512.0, height: 512.0) let fittedDimensions = dimensions.aspectFitted(CGSize(width: 512.0, height: 512.0)) strongSelf.imageNode.setSignal(chatMessageAnimatedSticker(postbox: accountAndImage.0.postbox, file: fileReference.media, small: false, size: fittedDimensions)) - animatedStickerNode.setup(account: accountAndImage.0, resource: fileReference.media.resource, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct) + animatedStickerNode.setup(account: accountAndImage.0, resource: .resource(fileReference.media.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct) animatedStickerNode.visibility = true accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true)) diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/celebrate.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/celebrate.tgs new file mode 100755 index 0000000000..1b27435dc8 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/celebrate.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/cry.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/cry.tgs new file mode 100755 index 0000000000..b0abe2305b Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/cry.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/heart.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/heart.tgs new file mode 100755 index 0000000000..f1f182cb54 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/heart.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/lol.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/lol.tgs new file mode 100755 index 0000000000..0ef8e8530e Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/lol.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/meh.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/meh.tgs new file mode 100755 index 0000000000..380208cf36 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/meh.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/ok.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/ok.tgs new file mode 100755 index 0000000000..55e8b5e99b Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/ok.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/poker.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/poker.tgs new file mode 100755 index 0000000000..1cf9720162 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/poker.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/poop.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/poop.tgs new file mode 100755 index 0000000000..6fd992da0c Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/poop.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/sad.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/sad.tgs new file mode 100755 index 0000000000..65af870f4f Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/sad.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/smile.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/smile.tgs new file mode 100755 index 0000000000..7b618ed752 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/smile.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/surprised.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/surprised.tgs new file mode 100755 index 0000000000..a6d874da62 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/surprised.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/thumbsup.tgs b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/thumbsup.tgs new file mode 100755 index 0000000000..614c2fe0e7 Binary files /dev/null and b/submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/thumbsup.tgs differ diff --git a/submodules/TelegramUI/TelegramUI/StickerPaneSearchStickerItem.swift b/submodules/TelegramUI/TelegramUI/StickerPaneSearchStickerItem.swift index e2aeaeed17..b4d5087ef4 100644 --- a/submodules/TelegramUI/TelegramUI/StickerPaneSearchStickerItem.swift +++ b/submodules/TelegramUI/TelegramUI/StickerPaneSearchStickerItem.swift @@ -164,7 +164,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode { } let dimensions = stickerItem.file.dimensions ?? CGSize(width: 512.0, height: 512.0) let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.0)) - self.animationNode?.setup(account: account, resource: stickerItem.file.resource, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) + self.animationNode?.setup(account: account, resource: .resource(stickerItem.file.resource), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .cached) self.animationNode?.visibility = self.isVisibleInGrid self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start()) } else { diff --git a/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj b/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj index 79fe602828..994c9fab79 100644 --- a/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj +++ b/submodules/TelegramUI/TelegramUI_Xcode.xcodeproj/project.pbxproj @@ -212,6 +212,7 @@ D0943B051FDDFDA0001522CC /* OverlayInstantVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */; }; D0943B071FDEC529001522CC /* InstantVideoRadialStatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943B061FDEC528001522CC /* InstantVideoRadialStatusNode.swift */; }; D0955FB521912B6000F89427 /* PresentationStrings.mapping in Resources */ = {isa = PBXBuildFile; fileRef = D0955FB32191278C00F89427 /* PresentationStrings.mapping */; }; + D095EF51230C7D2C00CB6167 /* BuiltinReactions in Resources */ = {isa = PBXBuildFile; fileRef = D095EF4F230C767D00CB6167 /* BuiltinReactions */; }; D099E220229405BB00561B75 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099E21F229405BB00561B75 /* Weak.swift */; }; D09E637C1F0E7C28003444CD /* SharedMediaPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */; }; D09E637F1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E637E1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift */; }; @@ -868,6 +869,7 @@ D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayInstantVideoNode.swift; sourceTree = ""; }; D0943B061FDEC528001522CC /* InstantVideoRadialStatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantVideoRadialStatusNode.swift; sourceTree = ""; }; D0955FB32191278C00F89427 /* PresentationStrings.mapping */ = {isa = PBXFileReference; lastKnownFileType = file; name = PresentationStrings.mapping; path = TelegramUI/Resources/PresentationStrings.mapping; sourceTree = ""; }; + D095EF4F230C767D00CB6167 /* BuiltinReactions */ = {isa = PBXFileReference; lastKnownFileType = folder; name = BuiltinReactions; path = TelegramUI/Resources/BuiltinReactions; sourceTree = ""; }; D099E21F229405BB00561B75 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; D099EA1E1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputContextPanelNode.swift; sourceTree = ""; }; D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputPanelItem.swift; sourceTree = ""; }; @@ -1643,6 +1645,7 @@ D0471B521EFD8EBC0074D609 /* Resources */ = { isa = PBXGroup; children = ( + D095EF4F230C767D00CB6167 /* BuiltinReactions */, 09E2D9ED226F1AF300EA0AA4 /* Emoji.mapping */, D0955FB32191278C00F89427 /* PresentationStrings.mapping */, 09310D13213BC5DE0020033A /* Animations */, @@ -2756,6 +2759,7 @@ D0C12A1D1F33A85600B3F66D /* ChatWallpaperBuiltin0.jpg in Resources */, D0E9BAAC1F056F4C00F079A4 /* stp_card_jcb@3x.png in Resources */, D0E9BA911F056F4C00F079A4 /* stp_card_amex@2x.png in Resources */, + D095EF51230C7D2C00CB6167 /* BuiltinReactions in Resources */, D0E9BA931F056F4C00F079A4 /* stp_card_amex_template@2x.png in Resources */, D0E9BAA91F056F4C00F079A4 /* stp_card_form_front@2x.png in Resources */, D0E9BAA41F056F4C00F079A4 /* stp_card_discover_template@3x.png in Resources */, diff --git a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift index 620678cbe5..858b521718 100644 --- a/submodules/TextSelectionNode/Sources/TextSelectionNode.swift +++ b/submodules/TextSelectionNode/Sources/TextSelectionNode.swift @@ -186,6 +186,7 @@ public enum TextSelectionAction { public final class TextSelectionNode: ASDisplayNode { private let theme: TextSelectionTheme private let textNode: TextNode + private let updateIsActive: (Bool) -> Void private let present: (ViewController, Any?) -> Void private weak var rootNode: ASDisplayNode? private let performAction: (String, TextSelectionAction) -> Void @@ -196,9 +197,10 @@ public final class TextSelectionNode: ASDisplayNode { private var currentRange: (Int, Int)? private var currentRects: [CGRect]? - public init(theme: TextSelectionTheme, textNode: TextNode, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (String, TextSelectionAction) -> Void) { + public init(theme: TextSelectionTheme, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (String, TextSelectionAction) -> Void) { self.theme = theme self.textNode = textNode + self.updateIsActive = updateIsActive self.present = present self.rootNode = rootNode self.performAction = performAction @@ -311,9 +313,11 @@ public final class TextSelectionNode: ASDisplayNode { } strongSelf.updateSelection(range: resultRange) strongSelf.displayMenu() + strongSelf.updateIsActive(true) } recognizer.clearSelection = { [weak self] in self?.dismissSelection() + self?.updateIsActive(false) } self.view.addGestureRecognizer(recognizer) }