mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Reactions in context menu
This commit is contained in:
parent
0230fa9b27
commit
606edad6cd
@ -301,6 +301,11 @@ public struct AnimatedStickerStatus: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum AnimatedStickerNodeResource {
|
||||||
|
case resource(MediaResource)
|
||||||
|
case localFile(String)
|
||||||
|
}
|
||||||
|
|
||||||
public final class AnimatedStickerNode: ASDisplayNode {
|
public final class AnimatedStickerNode: ASDisplayNode {
|
||||||
private let queue: Queue
|
private let queue: Queue
|
||||||
private var account: Account?
|
private var account: Account?
|
||||||
@ -381,28 +386,38 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
self.addSubnode(self.renderer!)
|
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 {
|
if width < 2 || height < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.playbackMode = playbackMode
|
self.playbackMode = playbackMode
|
||||||
switch mode {
|
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)
|
self.disposable.set((account.postbox.mediaBox.resourceData(resource)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
|> deliverOnMainQueue).start(next: { data in
|
||||||
guard let strongSelf = self, data.complete else {
|
f(data)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}))
|
}))
|
||||||
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)
|
self.disposable.set((chatMessageAnimationData(postbox: account.postbox, resource: resource, fitzModifier: fitzModifier, width: width, height: height, synchronousLoad: false)
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] data in
|
|> deliverOnMainQueue).start(next: { [weak self] data in
|
||||||
if let strongSelf = self, data.complete {
|
if let strongSelf = self, data.complete {
|
||||||
@ -414,6 +429,9 @@ public final class AnimatedStickerNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
case .localFile:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
D038AC7522F8A06200320981 /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D038AC7422F8A06200320981 /* Display.framework */; };
|
D038AC7522F8A06200320981 /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D038AC7422F8A06200320981 /* Display.framework */; };
|
||||||
D038AC7722F8A07000320981 /* ContextController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D038AC7622F8A07000320981 /* ContextController.swift */; };
|
D038AC7722F8A07000320981 /* ContextController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D038AC7622F8A07000320981 /* ContextController.swift */; };
|
||||||
D038AC7922F8A08A00320981 /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */; };
|
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 */; };
|
D09E777F22F8E47000B9CCA7 /* TelegramPresentationData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D09E777E22F8E47000B9CCA7 /* TelegramPresentationData.framework */; };
|
||||||
D09E778122F8E62000B9CCA7 /* ContextActionsContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E778022F8E62000B9CCA7 /* ContextActionsContainerNode.swift */; };
|
D09E778122F8E62000B9CCA7 /* ContextActionsContainerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E778022F8E62000B9CCA7 /* ContextActionsContainerNode.swift */; };
|
||||||
D09E778322F8E67300B9CCA7 /* ContextActionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E778222F8E67300B9CCA7 /* ContextActionNode.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; };
|
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 = "<group>"; };
|
D038AC7622F8A07000320981 /* ContextController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextController.swift; sourceTree = "<group>"; };
|
||||||
D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
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; };
|
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 = "<group>"; };
|
D09E778022F8E62000B9CCA7 /* ContextActionsContainerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextActionsContainerNode.swift; sourceTree = "<group>"; };
|
||||||
D09E778222F8E67300B9CCA7 /* ContextActionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextActionNode.swift; sourceTree = "<group>"; };
|
D09E778222F8E67300B9CCA7 /* ContextActionNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextActionNode.swift; sourceTree = "<group>"; };
|
||||||
@ -43,6 +47,8 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D0624F95230C0D2C000FC2BD /* TelegramCore.framework in Frameworks */,
|
||||||
|
D0624F93230C0CB7000FC2BD /* ReactionSelectionNode.framework in Frameworks */,
|
||||||
D0C9CBE42302D45F00FAB518 /* TextSelectionNode.framework in Frameworks */,
|
D0C9CBE42302D45F00FAB518 /* TextSelectionNode.framework in Frameworks */,
|
||||||
D09E777F22F8E47000B9CCA7 /* TelegramPresentationData.framework in Frameworks */,
|
D09E777F22F8E47000B9CCA7 /* TelegramPresentationData.framework in Frameworks */,
|
||||||
D038AC7922F8A08A00320981 /* AsyncDisplayKit.framework in Frameworks */,
|
D038AC7922F8A08A00320981 /* AsyncDisplayKit.framework in Frameworks */,
|
||||||
@ -89,6 +95,8 @@
|
|||||||
D038AC6F22F8A05A00320981 /* Frameworks */ = {
|
D038AC6F22F8A05A00320981 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D0624F94230C0D2C000FC2BD /* TelegramCore.framework */,
|
||||||
|
D0624F92230C0CB7000FC2BD /* ReactionSelectionNode.framework */,
|
||||||
D0C9CBE32302D45F00FAB518 /* TextSelectionNode.framework */,
|
D0C9CBE32302D45F00FAB518 /* TextSelectionNode.framework */,
|
||||||
D09E777E22F8E47000B9CCA7 /* TelegramPresentationData.framework */,
|
D09E777E22F8E47000B9CCA7 /* TelegramPresentationData.framework */,
|
||||||
D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */,
|
D038AC7822F8A08A00320981 /* AsyncDisplayKit.framework */,
|
||||||
|
@ -103,7 +103,7 @@ final class ContextActionNode: ASDisplayNode {
|
|||||||
|
|
||||||
func updateLayout(constrainedWidth: CGFloat, previous: ContextActionSibling, next: ContextActionSibling) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
func updateLayout(constrainedWidth: CGFloat, previous: ContextActionSibling, next: ContextActionSibling) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
|
||||||
let sideInset: CGFloat = 16.0
|
let sideInset: CGFloat = 16.0
|
||||||
let iconSideInset: CGFloat = 8.0
|
let iconSideInset: CGFloat = 12.0
|
||||||
let verticalInset: CGFloat = 12.0
|
let verticalInset: CGFloat = 12.0
|
||||||
|
|
||||||
let iconSize = self.iconNode.image.flatMap({ $0.size }) ?? CGSize()
|
let iconSize = self.iconNode.image.flatMap({ $0.size }) ?? CGSize()
|
||||||
|
@ -12,6 +12,7 @@ public final class ContextContentContainingNode: ASDisplayNode {
|
|||||||
public var applyAbsoluteOffset: ((CGFloat, ContainedViewLayoutTransitionCurve, Double) -> Void)?
|
public var applyAbsoluteOffset: ((CGFloat, ContainedViewLayoutTransitionCurve, Double) -> Void)?
|
||||||
public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)?
|
public var applyAbsoluteOffsetSpring: ((CGFloat, Double, CGFloat) -> Void)?
|
||||||
public var layoutUpdated: ((CGSize) -> Void)?
|
public var layoutUpdated: ((CGSize) -> Void)?
|
||||||
|
public var updateDistractionFreeMode: ((Bool) -> Void)?
|
||||||
|
|
||||||
public override init() {
|
public override init() {
|
||||||
self.contentNode = ContextContentNode()
|
self.contentNode = ContextContentNode()
|
||||||
|
@ -4,6 +4,8 @@ import AsyncDisplayKit
|
|||||||
import Display
|
import Display
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TextSelectionNode
|
import TextSelectionNode
|
||||||
|
import ReactionSelectionNode
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
public enum ContextMenuActionItemTextLayout {
|
public enum ContextMenuActionItemTextLayout {
|
||||||
case singleLine
|
case singleLine
|
||||||
@ -48,6 +50,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
private let source: ContextControllerContentSource
|
private let source: ContextControllerContentSource
|
||||||
private var items: [ContextMenuItem]
|
private var items: [ContextMenuItem]
|
||||||
private let beginDismiss: (ContextMenuActionResult) -> Void
|
private let beginDismiss: (ContextMenuActionResult) -> Void
|
||||||
|
private let reactionSelected: (String) -> Void
|
||||||
|
|
||||||
private var validLayout: ContainerViewLayout?
|
private var validLayout: ContainerViewLayout?
|
||||||
|
|
||||||
@ -64,20 +67,26 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
private var contentParentNode: ContextContentContainingNode?
|
private var contentParentNode: ContextContentContainingNode?
|
||||||
private let contentContainerNode: ContextContentContainerNode
|
private let contentContainerNode: ContextContentContainerNode
|
||||||
private var actionsContainerNode: ContextActionsContainerNode
|
private var actionsContainerNode: ContextActionsContainerNode
|
||||||
|
private var reactionContextNode: ReactionContextNode?
|
||||||
|
private var reactionContextNodeIsAnimatingOut = false
|
||||||
|
|
||||||
private var didCompleteAnimationIn = false
|
private var didCompleteAnimationIn = false
|
||||||
private var initialContinueGesturePoint: CGPoint?
|
private var initialContinueGesturePoint: CGPoint?
|
||||||
private var didMoveFromInitialGesturePoint = false
|
private var didMoveFromInitialGesturePoint = false
|
||||||
private var highlightedActionNode: ContextActionNode?
|
private var highlightedActionNode: ContextActionNode?
|
||||||
|
private var highlightedReaction: String?
|
||||||
|
|
||||||
private let hapticFeedback = HapticFeedback()
|
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.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.source = source
|
self.source = source
|
||||||
self.items = items
|
self.items = items
|
||||||
self.beginDismiss = beginDismiss
|
self.beginDismiss = beginDismiss
|
||||||
|
self.reactionSelected = reactionSelected
|
||||||
|
|
||||||
self.effectView = UIVisualEffectView()
|
self.effectView = UIVisualEffectView()
|
||||||
if #available(iOS 9.0, *) {
|
if #available(iOS 9.0, *) {
|
||||||
@ -114,16 +123,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
beginDismiss(result)
|
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, *) {
|
super.init()
|
||||||
let propertyAnimator = UIViewPropertyAnimator(duration: 0.4, curve: .linear)
|
|
||||||
propertyAnimator.isInterruptible = true
|
|
||||||
propertyAnimator.addAnimations {
|
|
||||||
self.effectView.effect = makeCustomZoomBlurEffect()
|
|
||||||
}
|
|
||||||
self.propertyAnimator = propertyAnimator
|
|
||||||
}*/
|
|
||||||
|
|
||||||
self.scrollNode.view.delegate = self
|
self.scrollNode.view.delegate = self
|
||||||
|
|
||||||
@ -136,6 +143,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
|
|
||||||
self.scrollNode.addSubnode(self.actionsContainerNode)
|
self.scrollNode.addSubnode(self.actionsContainerNode)
|
||||||
self.scrollNode.addSubnode(self.contentContainerNode)
|
self.scrollNode.addSubnode(self.contentContainerNode)
|
||||||
|
self.reactionContextNode.flatMap(self.addSubnode)
|
||||||
|
|
||||||
getController = { [weak controller] in
|
getController = { [weak controller] in
|
||||||
return controller
|
return controller
|
||||||
@ -172,6 +180,24 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
strongSelf.hapticFeedback.tap()
|
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
|
recognizer.externalUpdated = nil
|
||||||
if strongSelf.didMoveFromInitialGesturePoint {
|
if strongSelf.didMoveFromInitialGesturePoint {
|
||||||
if let (view, point) = viewAndPoint {
|
if let (_, _) = viewAndPoint {
|
||||||
let _ = strongSelf.view.convert(point, from: view)
|
|
||||||
if let highlightedActionNode = strongSelf.highlightedActionNode {
|
if let highlightedActionNode = strongSelf.highlightedActionNode {
|
||||||
strongSelf.highlightedActionNode = nil
|
strongSelf.highlightedActionNode = nil
|
||||||
highlightedActionNode.performAction()
|
highlightedActionNode.performAction()
|
||||||
}
|
}
|
||||||
|
if let _ = strongSelf.reactionContextNode {
|
||||||
|
if let reaction = strongSelf.highlightedReaction {
|
||||||
|
strongSelf.reactionSelected(reaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if let highlightedActionNode = strongSelf.highlightedActionNode {
|
if let highlightedActionNode = strongSelf.highlightedActionNode {
|
||||||
strongSelf.highlightedActionNode = nil
|
strongSelf.highlightedActionNode = nil
|
||||||
highlightedActionNode.setIsHighlighted(false)
|
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 {
|
deinit {
|
||||||
@ -229,11 +277,34 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
guard let strongSelf = self, let contentParentNode = contentParentNode, let parentSupernode = contentParentNode.supernode else {
|
guard let strongSelf = self, let contentParentNode = contentParentNode, let parentSupernode = contentParentNode.supernode else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if strongSelf.isAnimatingOut {
|
||||||
|
return
|
||||||
|
}
|
||||||
strongSelf.originalProjectedContentViewFrame = (parentSupernode.view.convert(contentParentNode.frame, to: strongSelf.view), contentParentNode.view.convert(contentParentNode.contentRect, to: strongSelf.view))
|
strongSelf.originalProjectedContentViewFrame = (parentSupernode.view.convert(contentParentNode.frame, to: strongSelf.view), contentParentNode.view.convert(contentParentNode.contentRect, to: strongSelf.view))
|
||||||
if let validLayout = strongSelf.validLayout {
|
if let validLayout = strongSelf.validLayout {
|
||||||
strongSelf.updateLayout(layout: validLayout, transition: .animated(duration: 0.2, curve: .easeInOut), previousActionsContainerNode: nil)
|
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.contentContainerNode.contentNode = takenViewInfo.contentContainingNode.contentNode
|
||||||
self.contentAreaInScreenSpace = takenViewInfo.contentAreaInScreenSpace
|
self.contentAreaInScreenSpace = takenViewInfo.contentAreaInScreenSpace
|
||||||
self.contentContainerNode.addSubnode(takenViewInfo.contentContainingNode.contentNode)
|
self.contentContainerNode.addSubnode(takenViewInfo.contentContainingNode.contentNode)
|
||||||
@ -264,6 +335,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
} else {
|
} else {
|
||||||
UIView.animate(withDuration: 0.2, animations: {
|
UIView.animate(withDuration: 0.2, animations: {
|
||||||
self.effectView.effect = makeCustomZoomBlurEffect()
|
self.effectView.effect = makeCustomZoomBlurEffect()
|
||||||
|
}, completion: { [weak self] _ in
|
||||||
|
self?.didCompleteAnimationIn = true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
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 {
|
if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame, let contentParentNode = self.contentParentNode {
|
||||||
let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view)
|
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)
|
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)
|
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)
|
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) {
|
func animateOut(result: ContextMenuActionResult, completion: @escaping () -> Void) {
|
||||||
self.isUserInteractionEnabled = false
|
self.isUserInteractionEnabled = false
|
||||||
|
self.isAnimatingOut = true
|
||||||
|
|
||||||
var completedEffect = false
|
var completedEffect = false
|
||||||
var completedContentNode = 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.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)
|
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 {
|
} else if let contentParentNode = self.contentParentNode {
|
||||||
if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree() {
|
if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree() {
|
||||||
self.contentContainerNode.view.addSubview(snapshotView)
|
self.contentContainerNode.view.addSubview(snapshotView)
|
||||||
@ -370,9 +452,46 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
completedContentNode = true
|
completedContentNode = true
|
||||||
intermediateCompletion()
|
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]) {
|
func setItems(controller: ContextController, items: [ContextMenuItem]) {
|
||||||
self.items = items
|
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))
|
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 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
|
let actionsBottomInset: CGFloat = 11.0
|
||||||
|
|
||||||
if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame, let contentParentNode = self.contentParentNode {
|
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 {
|
if let previousActionsContainerNode = previousActionsContainerNode {
|
||||||
@ -483,6 +611,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
if !self.bounds.contains(point) {
|
if !self.bounds.contains(point) {
|
||||||
return nil
|
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)
|
let mappedPoint = self.view.convert(point, to: self.scrollNode.view)
|
||||||
if self.actionsContainerNode.frame.contains(mappedPoint) {
|
if self.actionsContainerNode.frame.contains(mappedPoint) {
|
||||||
return self.actionsContainerNode.hitTest(self.view.convert(point, to: self.actionsContainerNode.view), with: event)
|
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 {
|
public final class ContextController: ViewController {
|
||||||
|
private let account: Account
|
||||||
private var theme: PresentationTheme
|
private var theme: PresentationTheme
|
||||||
private var strings: PresentationStrings
|
private var strings: PresentationStrings
|
||||||
private let source: ContextControllerContentSource
|
private let source: ContextControllerContentSource
|
||||||
private var items: [ContextMenuItem]
|
private var items: [ContextMenuItem]
|
||||||
|
private var reactionItems: [ReactionContextItem]
|
||||||
|
|
||||||
private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||||
|
|
||||||
@ -538,11 +673,15 @@ public final class ContextController: ViewController {
|
|||||||
return self.displayNode as! ContextControllerNode
|
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.theme = theme
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
self.source = source
|
self.source = source
|
||||||
self.items = items
|
self.items = items
|
||||||
|
self.reactionItems = reactionItems
|
||||||
self.recognizer = recognizer
|
self.recognizer = recognizer
|
||||||
|
|
||||||
super.init(navigationBarPresentationData: nil)
|
super.init(navigationBarPresentationData: nil)
|
||||||
@ -555,9 +694,14 @@ public final class ContextController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override public func loadDisplayNode() {
|
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)
|
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()
|
self.displayNodeDidLoad()
|
||||||
}
|
}
|
||||||
@ -600,4 +744,14 @@ public final class ContextController: ViewController {
|
|||||||
override public func dismiss(completion: (() -> Void)? = nil) {
|
override public func dismiss(completion: (() -> Void)? = nil) {
|
||||||
self.dismiss(result: .default, completion: completion)
|
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?()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 node.alpha.isEqual(to: alpha) {
|
||||||
if let completion = completion {
|
if let completion = completion {
|
||||||
completion(true)
|
completion(true)
|
||||||
@ -393,7 +393,12 @@ public extension ContainedViewLayoutTransition {
|
|||||||
completion(true)
|
completion(true)
|
||||||
}
|
}
|
||||||
case let .animated(duration, curve):
|
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.alpha = alpha
|
||||||
node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
node.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration, timingFunction: curve.timingFunction, mediaTimingFunction: curve.mediaTimingFunction, completion: { result in
|
||||||
if let completion = completion {
|
if let completion = completion {
|
||||||
|
@ -120,9 +120,9 @@ public final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer,
|
|||||||
self.lastRecognizedGestureAndLocation = (.longTap, location)
|
self.lastRecognizedGestureAndLocation = (.longTap, location)
|
||||||
if let longTap = self.longTap {
|
if let longTap = self.longTap {
|
||||||
self.recognizedLongTap = true
|
self.recognizedLongTap = true
|
||||||
|
self.state = .began
|
||||||
longTap(location, self)
|
longTap(location, self)
|
||||||
cancelScrollViewGestures(view: self.view?.superview)
|
cancelScrollViewGestures(view: self.view?.superview)
|
||||||
self.state = .began
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -329,6 +329,7 @@ private func makeLayerSubtreeSnapshot(layer: CALayer) -> CALayer? {
|
|||||||
for sublayer in sublayers {
|
for sublayer in sublayers {
|
||||||
let subtree = makeLayerSubtreeSnapshot(layer: sublayer)
|
let subtree = makeLayerSubtreeSnapshot(layer: sublayer)
|
||||||
if let subtree = subtree {
|
if let subtree = subtree {
|
||||||
|
subtree.transform = sublayer.transform
|
||||||
subtree.frame = sublayer.frame
|
subtree.frame = sublayer.frame
|
||||||
subtree.bounds = sublayer.bounds
|
subtree.bounds = sublayer.bounds
|
||||||
layer.addSublayer(subtree)
|
layer.addSublayer(subtree)
|
||||||
|
@ -582,7 +582,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||||||
animationNode = AnimatedStickerNode()
|
animationNode = AnimatedStickerNode()
|
||||||
strongSelf.animationNode = animationNode
|
strongSelf.animationNode = animationNode
|
||||||
strongSelf.addSubnode(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.visibility = strongSelf.visibility != .none && item.playAnimatedStickers
|
||||||
animationNode.isHidden = !item.playAnimatedStickers
|
animationNode.isHidden = !item.playAnimatedStickers
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
D03E46082305EEDD0049C28B /* ReactionSelectionParentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E46072305EEDD0049C28B /* ReactionSelectionParentNode.swift */; };
|
D03E46082305EEDD0049C28B /* ReactionSelectionParentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E46072305EEDD0049C28B /* ReactionSelectionParentNode.swift */; };
|
||||||
D03E460A2305EF900049C28B /* ReactionSwipeGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E46092305EF900049C28B /* ReactionSwipeGestureRecognizer.swift */; };
|
D03E460A2305EF900049C28B /* ReactionSwipeGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E46092305EF900049C28B /* ReactionSwipeGestureRecognizer.swift */; };
|
||||||
D03E460C2305EFD80049C28B /* ReactionGestureItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03E460B2305EFD80049C28B /* ReactionGestureItem.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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -36,6 +38,8 @@
|
|||||||
D03E46072305EEDD0049C28B /* ReactionSelectionParentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionSelectionParentNode.swift; sourceTree = "<group>"; };
|
D03E46072305EEDD0049C28B /* ReactionSelectionParentNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionSelectionParentNode.swift; sourceTree = "<group>"; };
|
||||||
D03E46092305EF900049C28B /* ReactionSwipeGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionSwipeGestureRecognizer.swift; sourceTree = "<group>"; };
|
D03E46092305EF900049C28B /* ReactionSwipeGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionSwipeGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||||
D03E460B2305EFD80049C28B /* ReactionGestureItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionGestureItem.swift; sourceTree = "<group>"; };
|
D03E460B2305EFD80049C28B /* ReactionGestureItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionGestureItem.swift; sourceTree = "<group>"; };
|
||||||
|
D0624F8E230C0316000FC2BD /* ReactionContextNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionContextNode.swift; sourceTree = "<group>"; };
|
||||||
|
D0624F90230C0472000FC2BD /* TelegramPresentationData.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = TelegramPresentationData.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -43,6 +47,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D0624F91230C0472000FC2BD /* TelegramPresentationData.framework in Frameworks */,
|
||||||
D03E46042305EE790049C28B /* TelegramCore.framework in Frameworks */,
|
D03E46042305EE790049C28B /* TelegramCore.framework in Frameworks */,
|
||||||
D03E46022305EE750049C28B /* Postbox.framework in Frameworks */,
|
D03E46022305EE750049C28B /* Postbox.framework in Frameworks */,
|
||||||
D03E46002305EE710049C28B /* AnimationUI.framework in Frameworks */,
|
D03E46002305EE710049C28B /* AnimationUI.framework in Frameworks */,
|
||||||
@ -78,10 +83,11 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D03E45F42305EB7C0049C28B /* ReactionSelectionNode.swift */,
|
D03E45F42305EB7C0049C28B /* ReactionSelectionNode.swift */,
|
||||||
D03E45E82305E3F30049C28B /* ReactionSelectionNode.h */,
|
D0624F8E230C0316000FC2BD /* ReactionContextNode.swift */,
|
||||||
D03E46072305EEDD0049C28B /* ReactionSelectionParentNode.swift */,
|
D03E46072305EEDD0049C28B /* ReactionSelectionParentNode.swift */,
|
||||||
D03E46092305EF900049C28B /* ReactionSwipeGestureRecognizer.swift */,
|
D03E46092305EF900049C28B /* ReactionSwipeGestureRecognizer.swift */,
|
||||||
D03E460B2305EFD80049C28B /* ReactionGestureItem.swift */,
|
D03E460B2305EFD80049C28B /* ReactionGestureItem.swift */,
|
||||||
|
D03E45E82305E3F30049C28B /* ReactionSelectionNode.h */,
|
||||||
);
|
);
|
||||||
path = Sources;
|
path = Sources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -89,6 +95,7 @@
|
|||||||
D03E45F62305EB870049C28B /* Frameworks */ = {
|
D03E45F62305EB870049C28B /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D0624F90230C0472000FC2BD /* TelegramPresentationData.framework */,
|
||||||
D03E46032305EE790049C28B /* TelegramCore.framework */,
|
D03E46032305EE790049C28B /* TelegramCore.framework */,
|
||||||
D03E46012305EE750049C28B /* Postbox.framework */,
|
D03E46012305EE750049C28B /* Postbox.framework */,
|
||||||
D03E45FF2305EE710049C28B /* AnimationUI.framework */,
|
D03E45FF2305EE710049C28B /* AnimationUI.framework */,
|
||||||
@ -183,6 +190,7 @@
|
|||||||
D03E460A2305EF900049C28B /* ReactionSwipeGestureRecognizer.swift in Sources */,
|
D03E460A2305EF900049C28B /* ReactionSwipeGestureRecognizer.swift in Sources */,
|
||||||
D03E46082305EEDD0049C28B /* ReactionSelectionParentNode.swift in Sources */,
|
D03E46082305EEDD0049C28B /* ReactionSelectionParentNode.swift in Sources */,
|
||||||
D03E460C2305EFD80049C28B /* ReactionGestureItem.swift in Sources */,
|
D03E460C2305EFD80049C28B /* ReactionGestureItem.swift in Sources */,
|
||||||
|
D0624F8F230C0316000FC2BD /* ReactionContextNode.swift in Sources */,
|
||||||
D03E45F52305EB7C0049C28B /* ReactionSelectionNode.swift in Sources */,
|
D03E45F52305EB7C0049C28B /* ReactionSelectionNode.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,6 @@ import Postbox
|
|||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
|
||||||
public enum ReactionGestureItem {
|
public enum ReactionGestureItem {
|
||||||
case reaction(value: String, text: String, file: TelegramMediaFile)
|
case reaction(value: String, text: String, path: String)
|
||||||
case reply
|
case reply
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import AnimationUI
|
|||||||
import Display
|
import Display
|
||||||
import Postbox
|
import Postbox
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
private func generateBubbleImage(foreground: UIColor, diameter: CGFloat, shadowBlur: CGFloat) -> UIImage? {
|
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
|
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))
|
})?.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
|
let reaction: ReactionGestureItem
|
||||||
|
private let textBackgroundNode: ASImageNode
|
||||||
|
private let textNode: ImmediateTextNode
|
||||||
private let animationNode: AnimatedStickerNode
|
private let animationNode: AnimatedStickerNode
|
||||||
private let imageNode: ASImageNode
|
private let imageNode: ASImageNode
|
||||||
var isMaximized: Bool?
|
var isMaximized: Bool?
|
||||||
private let intrinsicSize: CGSize
|
private let intrinsicSize: CGSize
|
||||||
private let intrinsicOffset: CGPoint
|
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.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 = AnimatedStickerNode()
|
||||||
self.animationNode.automaticallyLoadFirstFrame = true
|
self.animationNode.automaticallyLoadFirstFrame = loadFirstFrame
|
||||||
self.animationNode.playToCompletionOnStop = true
|
self.animationNode.playToCompletionOnStop = true
|
||||||
//self.animationNode.backgroundColor = .lightGray
|
|
||||||
|
|
||||||
var intrinsicSize = CGSize(width: maximizedReactionSize + 18.0, height: maximizedReactionSize + 18.0)
|
var intrinsicSize = CGSize(width: maximizedReactionSize + 18.0, height: maximizedReactionSize + 18.0)
|
||||||
|
|
||||||
self.imageNode = ASImageNode()
|
self.imageNode = ASImageNode()
|
||||||
switch reaction {
|
switch reaction {
|
||||||
case let .reaction(value, _, file):
|
case let .reaction(value, _, path):
|
||||||
switch value {
|
switch value {
|
||||||
|
case "😒":
|
||||||
|
intrinsicSize.width *= 1.7
|
||||||
|
intrinsicSize.height *= 1.7
|
||||||
|
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0)
|
||||||
case "😳":
|
case "😳":
|
||||||
intrinsicSize.width += 8.0
|
intrinsicSize.width *= 1.15
|
||||||
intrinsicSize.height += 8.0
|
intrinsicSize.height *= 1.15
|
||||||
self.intrinsicOffset = CGPoint(x: 0.0, y: -4.0)
|
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 "👍":
|
case "👍":
|
||||||
intrinsicSize.width += 20.0
|
intrinsicSize.width *= 1.256
|
||||||
intrinsicSize.height += 20.0
|
intrinsicSize.height *= 1.256
|
||||||
self.intrinsicOffset = CGPoint(x: 0.0, y: 4.0)
|
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.05 * intrinsicSize.width)
|
||||||
default:
|
default:
|
||||||
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0)
|
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:
|
case .reply:
|
||||||
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0)
|
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)
|
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()
|
super.init()
|
||||||
|
|
||||||
//self.backgroundColor = .green
|
self.textBackgroundNode.addSubnode(self.textNode)
|
||||||
|
self.addSubnode(self.textBackgroundNode)
|
||||||
|
|
||||||
self.addSubnode(self.animationNode)
|
self.addSubnode(self.animationNode)
|
||||||
self.addSubnode(self.imageNode)
|
self.addSubnode(self.imageNode)
|
||||||
@ -80,11 +126,17 @@ private final class ReactionNode: ASDisplayNode {
|
|||||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
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.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.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.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.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) {
|
func updateIsAnimating(_ isAnimating: Bool, animated: Bool) {
|
||||||
@ -98,6 +150,7 @@ private final class ReactionNode: ASDisplayNode {
|
|||||||
|
|
||||||
final class ReactionSelectionNode: ASDisplayNode {
|
final class ReactionSelectionNode: ASDisplayNode {
|
||||||
private let account: Account
|
private let account: Account
|
||||||
|
private let theme: PresentationTheme
|
||||||
private let reactions: [ReactionGestureItem]
|
private let reactions: [ReactionGestureItem]
|
||||||
|
|
||||||
private let backgroundNode: ASImageNode
|
private let backgroundNode: ASImageNode
|
||||||
@ -113,8 +166,11 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
private var maximizedReactionSize: CGFloat = 60.0
|
private var maximizedReactionSize: CGFloat = 60.0
|
||||||
private var smallCircleSize: CGFloat = 8.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.account = account
|
||||||
|
self.theme = theme
|
||||||
self.reactions = reactions
|
self.reactions = reactions
|
||||||
|
|
||||||
self.backgroundNode = ASImageNode()
|
self.backgroundNode = ASImageNode()
|
||||||
@ -153,12 +209,7 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
let initialAnchorX = startingPoint.x
|
let initialAnchorX = startingPoint.x
|
||||||
|
|
||||||
if isInitial && self.reactionNodes.isEmpty {
|
if isInitial && self.reactionNodes.isEmpty {
|
||||||
//let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing
|
let availableContentWidth = constrainedSize.width //max(100.0, initialAnchorX)
|
||||||
|
|
||||||
//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)
|
|
||||||
var minimizedReactionSize = (availableContentWidth - self.maximizedReactionSize) / (CGFloat(self.reactions.count - 1) + CGFloat(self.reactions.count + 1) * 0.2)
|
var minimizedReactionSize = (availableContentWidth - self.maximizedReactionSize) / (CGFloat(self.reactions.count - 1) + CGFloat(self.reactions.count + 1) * 0.2)
|
||||||
minimizedReactionSize = max(16.0, floor(minimizedReactionSize))
|
minimizedReactionSize = max(16.0, floor(minimizedReactionSize))
|
||||||
minimizedReactionSize = min(30.0, minimizedReactionSize)
|
minimizedReactionSize = min(30.0, minimizedReactionSize)
|
||||||
@ -177,7 +228,7 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.reactionNodes = self.reactions.map { reaction -> ReactionNode in
|
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(_:))
|
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
|
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))
|
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.backgroundNode.frame = backgroundFrame
|
||||||
self.backgroundShadowNode.frame = backgroundFrame
|
self.backgroundShadowNode.frame = backgroundFrame
|
||||||
|
|
||||||
@ -200,7 +258,7 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart))
|
let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart))
|
||||||
|
|
||||||
var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing
|
var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing
|
||||||
if offsetFromStart > backgroundFrame.maxX - shadowBlur {
|
if offsetFromStart > backgroundFrame.maxX - shadowBlur || offsetFromStart < backgroundFrame.minX {
|
||||||
self.hasSelectedNode = false
|
self.hasSelectedNode = false
|
||||||
} else {
|
} else {
|
||||||
self.hasSelectedNode = true
|
self.hasSelectedNode = true
|
||||||
@ -209,8 +267,12 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
var maximizedIndex = Int(((anchorX - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count))
|
var maximizedIndex = Int(((anchorX - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count))
|
||||||
maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex))
|
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
|
let isMaximized = i == maximizedIndex
|
||||||
|
if !isRightAligned {
|
||||||
|
i = self.reactionNodes.count - 1 - i
|
||||||
|
}
|
||||||
|
|
||||||
let reactionSize: CGFloat
|
let reactionSize: CGFloat
|
||||||
if isMaximized {
|
if isMaximized {
|
||||||
@ -239,7 +301,7 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
reactionFrame.origin.x -= 9.0
|
reactionFrame.origin.x -= 9.0
|
||||||
reactionFrame.size.width += 18.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)
|
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].0.frame = mainBubbleFrame
|
||||||
self.bubbleNodes[1].1.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].0.frame = secondaryBubbleFrame
|
||||||
self.bubbleNodes[0].1.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].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)
|
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
|
let damping: CGFloat = 100.0
|
||||||
|
|
||||||
for i in 0 ..< self.reactionNodes.count {
|
for i in 0 ..< self.reactionNodes.count {
|
||||||
let animationOffset: Double = 1.0 - Double(i) / Double(self.reactionNodes.count - 1)
|
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.x = -nodeOffset.x
|
||||||
nodeOffset.y = 30.0
|
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)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,17 +3,20 @@ import AsyncDisplayKit
|
|||||||
import Display
|
import Display
|
||||||
import Postbox
|
import Postbox
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
public final class ReactionSelectionParentNode: ASDisplayNode {
|
public final class ReactionSelectionParentNode: ASDisplayNode {
|
||||||
private let account: Account
|
private let account: Account
|
||||||
|
private let theme: PresentationTheme
|
||||||
|
|
||||||
private var currentNode: ReactionSelectionNode?
|
private var currentNode: ReactionSelectionNode?
|
||||||
private var currentLocation: (CGPoint, CGFloat)?
|
private var currentLocation: (CGPoint, CGFloat)?
|
||||||
|
|
||||||
private var validLayout: (size: CGSize, insets: UIEdgeInsets)?
|
private var validLayout: (size: CGSize, insets: UIEdgeInsets)?
|
||||||
|
|
||||||
public init(account: Account) {
|
public init(account: Account, theme: PresentationTheme) {
|
||||||
self.account = account
|
self.account = account
|
||||||
|
self.theme = theme
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
@ -24,7 +27,7 @@ public final class ReactionSelectionParentNode: ASDisplayNode {
|
|||||||
self.currentNode = nil
|
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.addSubnode(reactionNode)
|
||||||
self.currentNode = reactionNode
|
self.currentNode = reactionNode
|
||||||
self.currentLocation = (point, point.x)
|
self.currentLocation = (point, point.x)
|
||||||
|
@ -108,7 +108,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.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.interaction?.playAnimatedStickers ?? true
|
self.animationNode?.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
|
||||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
||||||
} else {
|
} else {
|
||||||
|
@ -91,7 +91,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
|
|||||||
let dimensions = item.file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
let dimensions = item.file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
||||||
let fittedDimensions = dimensions.aspectFitted(CGSize(width: 400.0, height: 400.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?.visibility = true
|
||||||
self.animationNode?.addSubnode(self.textNode)
|
self.animationNode?.addSubnode(self.textNode)
|
||||||
} else {
|
} else {
|
||||||
|
Binary file not shown.
@ -43,6 +43,7 @@ import PeerAvatarGalleryUI
|
|||||||
import PeerInfoUI
|
import PeerInfoUI
|
||||||
import RaiseToListen
|
import RaiseToListen
|
||||||
import UrlHandling
|
import UrlHandling
|
||||||
|
import ReactionSelectionNode
|
||||||
|
|
||||||
public enum ChatControllerPeekActions {
|
public enum ChatControllerPeekActions {
|
||||||
case standard
|
case standard
|
||||||
@ -523,40 +524,49 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
break
|
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 {
|
guard let strongSelf = self, !actions.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var actions = actions
|
let reactions: [(String, String, String)] = [
|
||||||
if ![Namespaces.Message.ScheduledCloud, Namespaces.Message.ScheduledLocal].contains(message.id.namespace) {
|
("😒", "Sad", "sad"),
|
||||||
actions.insert(.action(ContextMenuActionItem(text: "Reaction", icon: { _ in nil }, action: { _, f in
|
("😳", "Surprised", "surprised"),
|
||||||
guard let strongSelf = self else {
|
("😂", "Fun", "lol"),
|
||||||
return
|
("👍", "Like", "thumbsup"),
|
||||||
}
|
("❤", "Love", "heart"),
|
||||||
let actionSheet = ActionSheetController(presentationTheme: strongSelf.presentationData.theme)
|
]
|
||||||
var items: [ActionSheetItem] = []
|
|
||||||
let emojis = ["👍", "😊", "🤔", "😔", "❤️"]
|
var reactionItems: [ReactionContextItem] = []
|
||||||
for emoji in emojis {
|
for (value, text, name) in reactions {
|
||||||
items.append(ActionSheetButtonItem(title: "\(emoji)", color: .accent, action: { [weak actionSheet] in
|
if let path = frameworkBundle.path(forResource: name, ofType: "tgs", inDirectory: "BuiltinReactions") {
|
||||||
actionSheet?.dismissAnimated()
|
reactionItems.append(ReactionContextItem(value: value, text: text, path: path))
|
||||||
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 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
|
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)
|
strongSelf.window?.presentInGlobalOverlay(controller)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
self.historyNodeContainer = ASDisplayNode()
|
self.historyNodeContainer = ASDisplayNode()
|
||||||
self.historyNodeContainer.addSubnode(self.historyNode)
|
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.historyNodeContainer.addSubnode(self.reactionContainerNode)
|
||||||
|
|
||||||
self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper)
|
self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper)
|
||||||
|
@ -592,7 +592,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
|
|||||||
}
|
}
|
||||||
if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && isAction {
|
if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && isAction {
|
||||||
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in
|
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
|
}, action: { controller, f in
|
||||||
interfaceInteraction.deleteMessages(messages, controller, f)
|
interfaceInteraction.deleteMessages(messages, controller, f)
|
||||||
})))
|
})))
|
||||||
@ -654,7 +654,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
|
|||||||
if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && !isAction {
|
if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && !isAction {
|
||||||
let title = message.flags.isSending ? chatPresentationInterfaceState.strings.Conversation_ContextMenuCancelSending : chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete
|
let title = message.flags.isSending ? chatPresentationInterfaceState.strings.Conversation_ContextMenuCancelSending : chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete
|
||||||
actions.append(.action(ContextMenuActionItem(text: title, textColor: .destructive, icon: { theme in
|
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
|
}, action: { controller, f in
|
||||||
interfaceInteraction.deleteMessages(selectAll ? messages : [message], controller, f)
|
interfaceInteraction.deleteMessages(selectAll ? messages : [message], controller, f)
|
||||||
})))
|
})))
|
||||||
|
@ -299,7 +299,7 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode {
|
|||||||
self.didSetUpAnimationNode = true
|
self.didSetUpAnimationNode = true
|
||||||
let dimensions = item.stickerItem.file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
let dimensions = item.stickerItem.file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
||||||
let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,7 @@ final class ChatMediaInputStickerPackItemNode: ListViewItemNode {
|
|||||||
self.animatedStickerNode = animatedStickerNode
|
self.animatedStickerNode = animatedStickerNode
|
||||||
animatedStickerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
animatedStickerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||||
self.addSubnode(animatedStickerNode)
|
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
|
animatedStickerNode.visibility = self.visibilityStatus && loopAnimatedStickers
|
||||||
if let animatedStickerNode = self.animatedStickerNode {
|
if let animatedStickerNode = self.animatedStickerNode {
|
||||||
|
@ -304,7 +304,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
if let file = file {
|
if let file = file {
|
||||||
let dimensions = file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
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))
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,8 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
|
|||||||
|
|
||||||
var item: ChatMessageBubbleContentItem?
|
var item: ChatMessageBubbleContentItem?
|
||||||
|
|
||||||
|
var updateIsTextSelectionActive: ((Bool) -> Void)?
|
||||||
|
|
||||||
required override init() {
|
required override init() {
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
@ -186,8 +186,6 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
private var tapRecognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||||
private var reactionRecognizer: ReactionSwipeGestureRecognizer?
|
private var reactionRecognizer: ReactionSwipeGestureRecognizer?
|
||||||
|
|
||||||
private var awaitingAppliedReaction: String?
|
|
||||||
|
|
||||||
override var visibility: ListViewItemNodeVisibility {
|
override var visibility: ListViewItemNodeVisibility {
|
||||||
didSet {
|
didSet {
|
||||||
if self.visibility != oldValue {
|
if self.visibility != oldValue {
|
||||||
@ -419,24 +417,24 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let reactions: [(String, String)] = [
|
let reactions: [(String, String, String)] = [
|
||||||
("😒", "Sad"),
|
("😒", "Sad", "sad"),
|
||||||
("😳", "Surprised"),
|
("😳", "Surprised", "surprised"),
|
||||||
//("🥳", "Fun"),
|
("😂", "Fun", "lol"),
|
||||||
("👍", "Like"),
|
("👍", "Like", "thumbsup"),
|
||||||
("❤", "Love"),
|
("❤", "Love", "heart"),
|
||||||
]
|
]
|
||||||
|
|
||||||
var result: [ReactionGestureItem] = []
|
var reactionItems: [ReactionGestureItem] = []
|
||||||
for (value, text) in reactions {
|
for (value, text, name) in reactions {
|
||||||
if let file = item.associatedData.animatedEmojiStickers[value]?.file {
|
if let path = frameworkBundle.path(forResource: name, ofType: "tgs", inDirectory: "BuiltinReactions") {
|
||||||
result.append(.reaction(value: value, text: text, file: file))
|
reactionItems.append(.reaction(value: value, text: text, path: path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if item.controllerInteraction.canSetupReply(item.message) {
|
if item.controllerInteraction.canSetupReply(item.message) {
|
||||||
result.append(.reply)
|
reactionItems.append(.reply)
|
||||||
}
|
}
|
||||||
return result
|
return reactionItems
|
||||||
}
|
}
|
||||||
reactionRecognizer.getReactionContainer = { [weak self] in
|
reactionRecognizer.getReactionContainer = { [weak self] in
|
||||||
return self?.item?.controllerInteraction.reactionContainerNode()
|
return self?.item?.controllerInteraction.reactionContainerNode()
|
||||||
@ -505,7 +503,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
if let item = strongSelf.item, let reaction = reaction {
|
if let item = strongSelf.item, let reaction = reaction {
|
||||||
switch reaction {
|
switch reaction {
|
||||||
case let .reaction(value, _, _):
|
case let .reaction(value, _, _):
|
||||||
strongSelf.awaitingAppliedReaction = value
|
strongSelf.awaitingAppliedReaction = (value, {})
|
||||||
item.controllerInteraction.updateMessageReaction(item.message.id, value)
|
item.controllerInteraction.updateMessageReaction(item.message.id, value)
|
||||||
case .reply:
|
case .reply:
|
||||||
strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false)
|
strongSelf.reactionRecognizer?.complete(into: nil, hideTarget: false)
|
||||||
@ -1763,6 +1761,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
strongSelf.contextSourceNode.contentNode.addSubnode(contentNode)
|
strongSelf.contextSourceNode.contentNode.addSubnode(contentNode)
|
||||||
|
|
||||||
contentNode.visibility = strongSelf.visibility
|
contentNode.visibility = strongSelf.visibility
|
||||||
|
contentNode.updateIsTextSelectionActive = { [weak strongSelf] value in
|
||||||
|
strongSelf?.contextSourceNode.updateDistractionFreeMode?(value)
|
||||||
|
}
|
||||||
contentNode.updateIsExtractedToContextPreview(strongSelf.contextSourceNode.isExtractedToContextPreview)
|
contentNode.updateIsExtractedToContextPreview(strongSelf.contextSourceNode.isExtractedToContextPreview)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1945,7 +1946,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
|
|
||||||
strongSelf.updateSearchTextHighlightState()
|
strongSelf.updateSearchTextHighlightState()
|
||||||
|
|
||||||
if let awaitingAppliedReaction = strongSelf.awaitingAppliedReaction {
|
if let (awaitingAppliedReaction, f) = strongSelf.awaitingAppliedReaction {
|
||||||
var bounds = strongSelf.bounds
|
var bounds = strongSelf.bounds
|
||||||
let offset = bounds.origin.x
|
let offset = bounds.origin.x
|
||||||
bounds.origin.x = 0.0
|
bounds.origin.x = 0.0
|
||||||
@ -1972,6 +1973,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget)
|
strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget)
|
||||||
|
f()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2801,4 +2803,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
|
override func addAccessoryItemNode(_ accessoryItemNode: ListViewAccessoryItemNode) {
|
||||||
self.contextSourceNode.contentNode.addSubnode(accessoryItemNode)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -715,7 +715,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
strongSelf.animatedStickerNode = animatedStickerNode
|
strongSelf.animatedStickerNode = animatedStickerNode
|
||||||
let dimensions = updatedAnimatedStickerFile.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
let dimensions = updatedAnimatedStickerFile.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
||||||
let fittedDimensions = dimensions.aspectFitted(CGSize(width: 384.0, height: 384.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)
|
strongSelf.insertSubnode(animatedStickerNode, aboveSubnode: strongSelf.imageNode)
|
||||||
animatedStickerNode.visibility = strongSelf.visibility
|
animatedStickerNode.visibility = strongSelf.visibility
|
||||||
}
|
}
|
||||||
|
@ -611,6 +611,8 @@ public class ChatMessageItemView: ListViewItemNode {
|
|||||||
var item: ChatMessageItem?
|
var item: ChatMessageItem?
|
||||||
var accessibilityData: ChatMessageAccessibilityData?
|
var accessibilityData: ChatMessageAccessibilityData?
|
||||||
|
|
||||||
|
var awaitingAppliedReaction: (String, () -> Void)?
|
||||||
|
|
||||||
public required convenience init() {
|
public required convenience init() {
|
||||||
self.init(layerBacked: false)
|
self.init(layerBacked: false)
|
||||||
}
|
}
|
||||||
@ -788,4 +790,8 @@ public class ChatMessageItemView: ListViewItemNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func targetReactionNode(value: String) -> (ASImageNode, Int)? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -521,7 +521,9 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
override func updateIsExtractedToContextPreview(_ value: Bool) {
|
override func updateIsExtractedToContextPreview(_ value: Bool) {
|
||||||
if value {
|
if value {
|
||||||
if self.textSelectionNode == nil, let item = self.item, let rootNode = item.controllerInteraction.chatControllerNode() {
|
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)
|
self?.item?.controllerInteraction.presentGlobalOverlayController(c, a)
|
||||||
}, rootNode: rootNode, performAction: { [weak self] text, action in
|
}, rootNode: rootNode, performAction: { [weak self] text, action in
|
||||||
guard let strongSelf = self, let item = strongSelf.item else {
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
@ -535,6 +537,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
} else if let textSelectionNode = self.textSelectionNode {
|
} else if let textSelectionNode = self.textSelectionNode {
|
||||||
self.textSelectionNode = nil
|
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.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak textSelectionNode] _ in
|
||||||
textSelectionNode?.removeFromSupernode()
|
textSelectionNode?.removeFromSupernode()
|
||||||
})
|
})
|
||||||
|
@ -386,7 +386,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
|||||||
}
|
}
|
||||||
let dimensions = animatedStickerFile.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
let dimensions = animatedStickerFile.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
||||||
let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ final class HorizontalStickerGridItemNode: GridItemNode {
|
|||||||
}
|
}
|
||||||
let dimensions = item.file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
let dimensions = item.file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
||||||
let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.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())
|
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start())
|
||||||
} else {
|
} else {
|
||||||
|
@ -118,7 +118,7 @@ final class TrendingTopItemNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
let dimensions = item.file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
let dimensions = item.file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
||||||
let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.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())
|
self.loadDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(item.file), resource: item.file.resource).start())
|
||||||
} else {
|
} else {
|
||||||
self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads)
|
self.imageNode.setSignal(chatMessageSticker(account: account, file: item.file, small: true, synchronousLoad: synchronousLoads), attemptSynchronously: synchronousLoads)
|
||||||
|
@ -286,7 +286,7 @@ public final class NotificationViewControllerImpl {
|
|||||||
let dimensions = fileReference.media.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
let dimensions = fileReference.media.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
||||||
let fittedDimensions = dimensions.aspectFitted(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))
|
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
|
animatedStickerNode.visibility = true
|
||||||
|
|
||||||
accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true))
|
accountAndImage.0.network.shouldExplicitelyKeepWorkerConnections.set(.single(true))
|
||||||
|
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/celebrate.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/celebrate.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/cry.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/cry.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/heart.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/heart.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/lol.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/lol.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/meh.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/meh.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/ok.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/ok.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/poker.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/poker.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/poop.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/poop.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/sad.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/sad.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/smile.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/smile.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/surprised.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/surprised.tgs
Executable file
Binary file not shown.
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/thumbsup.tgs
Executable file
BIN
submodules/TelegramUI/TelegramUI/Resources/BuiltinReactions/thumbsup.tgs
Executable file
Binary file not shown.
@ -164,7 +164,7 @@ final class StickerPaneSearchStickerItemNode: GridItemNode {
|
|||||||
}
|
}
|
||||||
let dimensions = stickerItem.file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
let dimensions = stickerItem.file.dimensions ?? CGSize(width: 512.0, height: 512.0)
|
||||||
let fittedDimensions = dimensions.aspectFitted(CGSize(width: 160.0, height: 160.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.animationNode?.visibility = self.isVisibleInGrid
|
||||||
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: account, fileReference: stickerPackFileReference(stickerItem.file), resource: stickerItem.file.resource).start())
|
||||||
} else {
|
} else {
|
||||||
|
@ -212,6 +212,7 @@
|
|||||||
D0943B051FDDFDA0001522CC /* OverlayInstantVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */; };
|
D0943B051FDDFDA0001522CC /* OverlayInstantVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */; };
|
||||||
D0943B071FDEC529001522CC /* InstantVideoRadialStatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943B061FDEC528001522CC /* InstantVideoRadialStatusNode.swift */; };
|
D0943B071FDEC529001522CC /* InstantVideoRadialStatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0943B061FDEC528001522CC /* InstantVideoRadialStatusNode.swift */; };
|
||||||
D0955FB521912B6000F89427 /* PresentationStrings.mapping in Resources */ = {isa = PBXBuildFile; fileRef = D0955FB32191278C00F89427 /* PresentationStrings.mapping */; };
|
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 */; };
|
D099E220229405BB00561B75 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099E21F229405BB00561B75 /* Weak.swift */; };
|
||||||
D09E637C1F0E7C28003444CD /* SharedMediaPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */; };
|
D09E637C1F0E7C28003444CD /* SharedMediaPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E637B1F0E7C28003444CD /* SharedMediaPlayer.swift */; };
|
||||||
D09E637F1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09E637E1F0E8C9F003444CD /* PeerMessagesMediaPlaylist.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 = "<group>"; };
|
D0943B041FDDFDA0001522CC /* OverlayInstantVideoNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayInstantVideoNode.swift; sourceTree = "<group>"; };
|
||||||
D0943B061FDEC528001522CC /* InstantVideoRadialStatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantVideoRadialStatusNode.swift; sourceTree = "<group>"; };
|
D0943B061FDEC528001522CC /* InstantVideoRadialStatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstantVideoRadialStatusNode.swift; sourceTree = "<group>"; };
|
||||||
D0955FB32191278C00F89427 /* PresentationStrings.mapping */ = {isa = PBXFileReference; lastKnownFileType = file; name = PresentationStrings.mapping; path = TelegramUI/Resources/PresentationStrings.mapping; sourceTree = "<group>"; };
|
D0955FB32191278C00F89427 /* PresentationStrings.mapping */ = {isa = PBXFileReference; lastKnownFileType = file; name = PresentationStrings.mapping; path = TelegramUI/Resources/PresentationStrings.mapping; sourceTree = "<group>"; };
|
||||||
|
D095EF4F230C767D00CB6167 /* BuiltinReactions */ = {isa = PBXFileReference; lastKnownFileType = folder; name = BuiltinReactions; path = TelegramUI/Resources/BuiltinReactions; sourceTree = "<group>"; };
|
||||||
D099E21F229405BB00561B75 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
D099E21F229405BB00561B75 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
||||||
D099EA1E1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputContextPanelNode.swift; sourceTree = "<group>"; };
|
D099EA1E1DE7450B001AF5A8 /* HorizontalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputContextPanelNode.swift; sourceTree = "<group>"; };
|
||||||
D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputPanelItem.swift; sourceTree = "<group>"; };
|
D099EA201DE7451D001AF5A8 /* HorizontalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalListContextResultsChatInputPanelItem.swift; sourceTree = "<group>"; };
|
||||||
@ -1643,6 +1645,7 @@
|
|||||||
D0471B521EFD8EBC0074D609 /* Resources */ = {
|
D0471B521EFD8EBC0074D609 /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D095EF4F230C767D00CB6167 /* BuiltinReactions */,
|
||||||
09E2D9ED226F1AF300EA0AA4 /* Emoji.mapping */,
|
09E2D9ED226F1AF300EA0AA4 /* Emoji.mapping */,
|
||||||
D0955FB32191278C00F89427 /* PresentationStrings.mapping */,
|
D0955FB32191278C00F89427 /* PresentationStrings.mapping */,
|
||||||
09310D13213BC5DE0020033A /* Animations */,
|
09310D13213BC5DE0020033A /* Animations */,
|
||||||
@ -2756,6 +2759,7 @@
|
|||||||
D0C12A1D1F33A85600B3F66D /* ChatWallpaperBuiltin0.jpg in Resources */,
|
D0C12A1D1F33A85600B3F66D /* ChatWallpaperBuiltin0.jpg in Resources */,
|
||||||
D0E9BAAC1F056F4C00F079A4 /* stp_card_jcb@3x.png in Resources */,
|
D0E9BAAC1F056F4C00F079A4 /* stp_card_jcb@3x.png in Resources */,
|
||||||
D0E9BA911F056F4C00F079A4 /* stp_card_amex@2x.png in Resources */,
|
D0E9BA911F056F4C00F079A4 /* stp_card_amex@2x.png in Resources */,
|
||||||
|
D095EF51230C7D2C00CB6167 /* BuiltinReactions in Resources */,
|
||||||
D0E9BA931F056F4C00F079A4 /* stp_card_amex_template@2x.png in Resources */,
|
D0E9BA931F056F4C00F079A4 /* stp_card_amex_template@2x.png in Resources */,
|
||||||
D0E9BAA91F056F4C00F079A4 /* stp_card_form_front@2x.png in Resources */,
|
D0E9BAA91F056F4C00F079A4 /* stp_card_form_front@2x.png in Resources */,
|
||||||
D0E9BAA41F056F4C00F079A4 /* stp_card_discover_template@3x.png in Resources */,
|
D0E9BAA41F056F4C00F079A4 /* stp_card_discover_template@3x.png in Resources */,
|
||||||
|
@ -186,6 +186,7 @@ public enum TextSelectionAction {
|
|||||||
public final class TextSelectionNode: ASDisplayNode {
|
public final class TextSelectionNode: ASDisplayNode {
|
||||||
private let theme: TextSelectionTheme
|
private let theme: TextSelectionTheme
|
||||||
private let textNode: TextNode
|
private let textNode: TextNode
|
||||||
|
private let updateIsActive: (Bool) -> Void
|
||||||
private let present: (ViewController, Any?) -> Void
|
private let present: (ViewController, Any?) -> Void
|
||||||
private weak var rootNode: ASDisplayNode?
|
private weak var rootNode: ASDisplayNode?
|
||||||
private let performAction: (String, TextSelectionAction) -> Void
|
private let performAction: (String, TextSelectionAction) -> Void
|
||||||
@ -196,9 +197,10 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
private var currentRange: (Int, Int)?
|
private var currentRange: (Int, Int)?
|
||||||
private var currentRects: [CGRect]?
|
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.theme = theme
|
||||||
self.textNode = textNode
|
self.textNode = textNode
|
||||||
|
self.updateIsActive = updateIsActive
|
||||||
self.present = present
|
self.present = present
|
||||||
self.rootNode = rootNode
|
self.rootNode = rootNode
|
||||||
self.performAction = performAction
|
self.performAction = performAction
|
||||||
@ -311,9 +313,11 @@ public final class TextSelectionNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
strongSelf.updateSelection(range: resultRange)
|
strongSelf.updateSelection(range: resultRange)
|
||||||
strongSelf.displayMenu()
|
strongSelf.displayMenu()
|
||||||
|
strongSelf.updateIsActive(true)
|
||||||
}
|
}
|
||||||
recognizer.clearSelection = { [weak self] in
|
recognizer.clearSelection = { [weak self] in
|
||||||
self?.dismissSelection()
|
self?.dismissSelection()
|
||||||
|
self?.updateIsActive(false)
|
||||||
}
|
}
|
||||||
self.view.addGestureRecognizer(recognizer)
|
self.view.addGestureRecognizer(recognizer)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user