mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Reactions
This commit is contained in:
parent
96b6864a51
commit
d7e8737a92
@ -16,6 +16,8 @@ swift_library(
|
|||||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||||
"//submodules/TextSelectionNode:TextSelectionNode",
|
"//submodules/TextSelectionNode:TextSelectionNode",
|
||||||
"//submodules/AppBundle:AppBundle",
|
"//submodules/AppBundle:AppBundle",
|
||||||
|
"//submodules/AccountContext:AccountContext",
|
||||||
|
"//submodules/ReactionSelectionNode:ReactionSelectionNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -4,8 +4,10 @@ import AsyncDisplayKit
|
|||||||
import Display
|
import Display
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import TextSelectionNode
|
import TextSelectionNode
|
||||||
|
import ReactionSelectionNode
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import AccountContext
|
||||||
|
|
||||||
private let animationDurationFactor: Double = 1.0
|
private let animationDurationFactor: Double = 1.0
|
||||||
|
|
||||||
@ -201,11 +203,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
private var contentAreaInScreenSpace: CGRect?
|
private var contentAreaInScreenSpace: CGRect?
|
||||||
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: ContextActionNodeProtocol?
|
private var highlightedActionNode: ContextActionNodeProtocol?
|
||||||
|
private var highlightedReaction: ReactionContextItem.Reaction?
|
||||||
|
|
||||||
private let hapticFeedback = HapticFeedback()
|
private let hapticFeedback = HapticFeedback()
|
||||||
|
|
||||||
@ -216,7 +221,18 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
|
|
||||||
private let blurBackground: Bool
|
private let blurBackground: Bool
|
||||||
|
|
||||||
init(account: Account, controller: ContextController, presentationData: PresentationData, source: ContextContentSource, items: Signal<ContextController.Items, NoError>, beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, beganAnimatingOut: @escaping () -> Void, attemptTransitionControllerIntoNavigation: @escaping () -> Void) {
|
init(
|
||||||
|
account: Account,
|
||||||
|
controller: ContextController,
|
||||||
|
presentationData: PresentationData,
|
||||||
|
source: ContextContentSource,
|
||||||
|
items: Signal<ContextController.Items, NoError>,
|
||||||
|
beginDismiss: @escaping (ContextMenuActionResult) -> Void,
|
||||||
|
recognizer: TapLongTapOrDoubleTapGestureRecognizer?,
|
||||||
|
gesture: ContextGesture?,
|
||||||
|
beganAnimatingOut: @escaping () -> Void,
|
||||||
|
attemptTransitionControllerIntoNavigation: @escaping () -> Void
|
||||||
|
) {
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.source = source
|
self.source = source
|
||||||
self.items = items
|
self.items = items
|
||||||
@ -653,6 +669,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
localContentSourceFrame = localSourceFrame
|
localContentSourceFrame = localSourceFrame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 + actionsOffset)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: actionsDuration, 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 + actionsOffset)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: actionsDuration, initialVelocity: 0.0, damping: springDamping, additive: true)
|
||||||
let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY)
|
let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.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: contentDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in
|
self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: contentDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in
|
||||||
@ -922,6 +942,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?(CGPoint(x: 0.0, y: -contentContainerOffset.y), transitionCurve, transitionDuration)
|
contentParentNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: -contentContainerOffset.y), transitionCurve, transitionDuration)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut))
|
contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut))
|
||||||
} else {
|
} else {
|
||||||
if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree(keepTransform: true) {
|
if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree(keepTransform: true) {
|
||||||
@ -940,6 +964,10 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
|
|
||||||
contentParentNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
contentParentNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut))
|
contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut))
|
||||||
|
|
||||||
|
if let reactionContextNode = self.reactionContextNode {
|
||||||
|
reactionContextNode.animateOut(to: nil, animatingOutToReaction: self.reactionContextNodeIsAnimatingOut)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case let .controller(source):
|
case let .controller(source):
|
||||||
guard let maybeContentNode = self.contentContainerNode.contentNode, case let .controller(controller) = maybeContentNode else {
|
guard let maybeContentNode = self.contentContainerNode.contentNode, case let .controller(controller) = maybeContentNode else {
|
||||||
@ -1078,9 +1106,43 @@ 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, targetEmptyNode: ASDisplayNode, targetFilledNode: ASDisplayNode, 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, targetEmptyNode: targetEmptyNode, targetFilledNode: targetFilledNode, hideNode: hideNode, completion: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.reactionContextNode?.removeFromSupernode()
|
||||||
|
strongSelf.reactionContextNode = nil
|
||||||
|
reactionCompleted = true
|
||||||
|
intermediateCompletion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func getActionsMinHeight() -> ContextController.ActionsHeight? {
|
func getActionsMinHeight() -> ContextController.ActionsHeight? {
|
||||||
if !self.actionsContainerNode.bounds.height.isZero {
|
if !self.actionsContainerNode.bounds.height.isZero {
|
||||||
@ -1112,6 +1174,29 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
self.currentItems = items
|
self.currentItems = items
|
||||||
self.currentActionsMinHeight = minHeight
|
self.currentActionsMinHeight = minHeight
|
||||||
|
|
||||||
|
if let reactionContextNode = self.reactionContextNode {
|
||||||
|
self.reactionContextNode = nil
|
||||||
|
reactionContextNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !items.reactionItems.isEmpty, let context = items.context {
|
||||||
|
let reactionContextNode = ReactionContextNode(account: context.account, theme: self.presentationData.theme, items: items.reactionItems)
|
||||||
|
self.reactionContextNode = reactionContextNode
|
||||||
|
self.addSubnode(reactionContextNode)
|
||||||
|
|
||||||
|
reactionContextNode.reactionSelected = { [weak self] reaction in
|
||||||
|
guard let strongSelf = self, let controller = strongSelf.getController() as? ContextController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch reaction {
|
||||||
|
case .like:
|
||||||
|
controller.reactionSelected?(.like)
|
||||||
|
case .unlike:
|
||||||
|
controller.reactionSelected?(.unlike)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let previousActionsContainerNode = self.actionsContainerNode
|
let previousActionsContainerNode = self.actionsContainerNode
|
||||||
let previousActionsContainerFrame = previousActionsContainerNode.view.convert(previousActionsContainerNode.bounds, to: self.view)
|
let previousActionsContainerFrame = previousActionsContainerNode.view.convert(previousActionsContainerNode.bounds, to: self.view)
|
||||||
self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: items, getController: { [weak self] in
|
self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: items, getController: { [weak self] in
|
||||||
@ -1202,7 +1287,11 @@ 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 actionsSideInset: CGFloat = layout.safeInsets.left + 11.0
|
let actionsSideInset: CGFloat = layout.safeInsets.left + 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
|
||||||
|
|
||||||
@ -1432,6 +1521,12 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y)
|
let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y)
|
||||||
|
|
||||||
contentParentNode.updateAbsoluteRect?(absoluteContentRect, layout.size)
|
contentParentNode.updateAbsoluteRect?(absoluteContentRect, layout.size)
|
||||||
|
|
||||||
|
if let reactionContextNode = self.reactionContextNode {
|
||||||
|
let insets = layout.insets(options: [.statusBar])
|
||||||
|
transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
|
reactionContextNode.updateLayout(size: layout.size, insets: insets, anchorRect: CGRect(origin: CGPoint(x: absoluteContentRect.minX + contentParentNode.contentRect.minX, y: absoluteContentRect.minY + contentParentNode.contentRect.minY), size: contentParentNode.contentRect.size), transition: transition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case let .controller(contentParentNode):
|
case let .controller(contentParentNode):
|
||||||
var projectedFrame: CGRect = convertFrame(contentParentNode.sourceNode.bounds, from: contentParentNode.sourceNode.view, to: self.view)
|
var projectedFrame: CGRect = convertFrame(contentParentNode.sourceNode.bounds, from: contentParentNode.sourceNode.view, to: self.view)
|
||||||
@ -1562,6 +1657,14 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
|||||||
transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY)
|
transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y)
|
||||||
|
|
||||||
|
if let reactionContextNode = self.reactionContextNode {
|
||||||
|
let insets = layout.insets(options: [.statusBar])
|
||||||
|
transition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
|
reactionContextNode.updateLayout(size: layout.size, insets: insets, anchorRect: CGRect(origin: CGPoint(x: absoluteContentRect.minX, y: absoluteContentRect.minY), size: contentSize), transition: transition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1646,6 +1749,13 @@ 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)
|
||||||
var maybePassthrough: ContextController.HandledTouchEvent?
|
var maybePassthrough: ContextController.HandledTouchEvent?
|
||||||
if let maybeContentNode = self.contentContainerNode.contentNode {
|
if let maybeContentNode = self.contentContainerNode.contentNode {
|
||||||
@ -1807,15 +1917,21 @@ public enum ContextContentSource {
|
|||||||
public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol {
|
public final class ContextController: ViewController, StandalonePresentableController, ContextControllerProtocol {
|
||||||
public struct Items {
|
public struct Items {
|
||||||
public var items: [ContextMenuItem]
|
public var items: [ContextMenuItem]
|
||||||
|
public var context: AccountContext?
|
||||||
|
public var reactionItems: [ReactionContextItem]
|
||||||
public var tip: Tip?
|
public var tip: Tip?
|
||||||
|
|
||||||
public init(items: [ContextMenuItem], tip: Tip? = nil) {
|
public init(items: [ContextMenuItem], context: AccountContext? = nil, reactionItems: [ReactionContextItem] = [], tip: Tip? = nil) {
|
||||||
self.items = items
|
self.items = items
|
||||||
|
self.context = context
|
||||||
|
self.reactionItems = reactionItems
|
||||||
self.tip = tip
|
self.tip = tip
|
||||||
}
|
}
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
self.items = []
|
self.items = []
|
||||||
|
self.context = nil
|
||||||
|
self.reactionItems = []
|
||||||
self.tip = nil
|
self.tip = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1880,6 +1996,8 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
|
|
||||||
private var shouldBeDismissedDisposable: Disposable?
|
private var shouldBeDismissedDisposable: Disposable?
|
||||||
|
|
||||||
|
public var reactionSelected: ((ReactionContextItem.Reaction) -> Void)?
|
||||||
|
|
||||||
public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<ContextController.Items, NoError>, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil) {
|
public init(account: Account, presentationData: PresentationData, source: ContextContentSource, items: Signal<ContextController.Items, NoError>, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil) {
|
||||||
self.account = account
|
self.account = account
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
@ -2022,4 +2140,15 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
|||||||
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, targetEmptyNode: ASDisplayNode, targetFilledNode: ASDisplayNode, hideNode: Bool, completion: (() -> Void)?) {
|
||||||
|
if !self.wasDismissed {
|
||||||
|
self.wasDismissed = true
|
||||||
|
self.controllerNode.animateOutToReaction(value: value, targetEmptyNode: targetEmptyNode, targetFilledNode: targetFilledNode, hideNode: hideNode, completion: { [weak self] in
|
||||||
|
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||||
|
completion?()
|
||||||
|
})
|
||||||
|
self.dismissed?()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ optimization_flags = select({
|
|||||||
|
|
||||||
swift_optimization_flags = select({
|
swift_optimization_flags = select({
|
||||||
":debug_build": [
|
":debug_build": [
|
||||||
"-O",
|
#"-O",
|
||||||
],
|
],
|
||||||
"//conditions:default": [],
|
"//conditions:default": [],
|
||||||
})
|
})
|
||||||
@ -75,6 +75,7 @@ swift_library(
|
|||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
":LottieMeshBinding",
|
":LottieMeshBinding",
|
||||||
|
"//submodules/Postbox:Postbox",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -1,122 +1,49 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import Postbox
|
||||||
|
|
||||||
private let emptyMemory = malloc(1)!
|
private let emptyMemory = malloc(1)!
|
||||||
|
|
||||||
public class MeshMemoryBuffer: Equatable, CustomStringConvertible {
|
public class MeshMemoryBuffer {
|
||||||
public internal(set) var memory: UnsafeMutableRawPointer
|
public internal(set) var data: Data
|
||||||
var capacity: Int
|
|
||||||
public internal(set) var length: Int
|
public internal(set) var length: Int
|
||||||
var freeWhenDone: Bool
|
|
||||||
|
|
||||||
public init(copyOf buffer: MeshMemoryBuffer) {
|
|
||||||
self.memory = malloc(buffer.length)
|
|
||||||
memcpy(self.memory, buffer.memory, buffer.length)
|
|
||||||
self.capacity = buffer.length
|
|
||||||
self.length = buffer.length
|
|
||||||
self.freeWhenDone = true
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(memory: UnsafeMutableRawPointer, capacity: Int, length: Int, freeWhenDone: Bool) {
|
|
||||||
self.memory = memory
|
|
||||||
self.capacity = capacity
|
|
||||||
self.length = length
|
|
||||||
self.freeWhenDone = freeWhenDone
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(data: Data) {
|
public init(data: Data) {
|
||||||
if data.count == 0 {
|
self.data = data
|
||||||
self.memory = emptyMemory
|
|
||||||
self.capacity = 0
|
|
||||||
self.length = 0
|
|
||||||
self.freeWhenDone = false
|
|
||||||
} else {
|
|
||||||
self.memory = malloc(data.count)!
|
|
||||||
data.copyBytes(to: self.memory.assumingMemoryBound(to: UInt8.self), count: data.count)
|
|
||||||
self.capacity = data.count
|
|
||||||
self.length = data.count
|
self.length = data.count
|
||||||
self.freeWhenDone = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public init() {
|
|
||||||
self.memory = emptyMemory
|
|
||||||
self.capacity = 0
|
|
||||||
self.length = 0
|
|
||||||
self.freeWhenDone = false
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
if self.freeWhenDone {
|
|
||||||
free(self.memory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var description: String {
|
|
||||||
let hexString = NSMutableString()
|
|
||||||
let bytes = self.memory.assumingMemoryBound(to: UInt8.self)
|
|
||||||
for i in 0 ..< self.length {
|
|
||||||
hexString.appendFormat("%02x", UInt(bytes[i]))
|
|
||||||
}
|
|
||||||
|
|
||||||
return hexString as String
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func makeData() -> Data {
|
public func makeData() -> Data {
|
||||||
if self.length == 0 {
|
if self.data.count == self.length {
|
||||||
return Data()
|
return self.data
|
||||||
} else {
|
} else {
|
||||||
return Data(bytes: self.memory, count: self.length)
|
return self.data.subdata(in: 0 ..< self.length)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func withDataNoCopy(_ f: (Data) -> Void) {
|
|
||||||
f(Data(bytesNoCopy: self.memory, count: self.length, deallocator: .none))
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func ==(lhs: MeshMemoryBuffer, rhs: MeshMemoryBuffer) -> Bool {
|
|
||||||
return lhs.length == rhs.length && memcmp(lhs.memory, rhs.memory, lhs.length) == 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class MeshWriteBuffer: MeshMemoryBuffer {
|
extension WriteBuffer {
|
||||||
public var offset = 0
|
func writeInt32(_ value: Int32) {
|
||||||
|
var value = value
|
||||||
public override init() {
|
self.write(&value, length: 4)
|
||||||
super.init(memory: malloc(32), capacity: 32, length: 0, freeWhenDone: true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func makeReadBufferAndReset() -> MeshReadBuffer {
|
func writeFloat(_ value: Float) {
|
||||||
let buffer = MeshReadBuffer(memory: self.memory, length: self.offset, freeWhenDone: true)
|
var value: Float32 = value
|
||||||
self.memory = malloc(32)
|
self.write(&value, length: 4)
|
||||||
self.capacity = 32
|
|
||||||
self.offset = 0
|
|
||||||
return buffer
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func readBufferNoCopy() -> MeshReadBuffer {
|
public final class MeshWriteBuffer {
|
||||||
return MeshReadBuffer(memory: self.memory, length: self.offset, freeWhenDone: false)
|
let file: ManagedFile
|
||||||
}
|
private(set) var offset: Int = 0
|
||||||
|
|
||||||
override public func makeData() -> Data {
|
public init(file: ManagedFile) {
|
||||||
return Data(bytes: self.memory.assumingMemoryBound(to: UInt8.self), count: self.offset)
|
self.file = file
|
||||||
}
|
|
||||||
|
|
||||||
public func reset() {
|
|
||||||
self.offset = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func write(_ data: UnsafeRawPointer, length: Int) {
|
public func write(_ data: UnsafeRawPointer, length: Int) {
|
||||||
if self.offset + length > self.capacity {
|
let _ = self.file.write(data, count: length)
|
||||||
self.capacity = self.offset + length + 256
|
|
||||||
if self.length == 0 {
|
|
||||||
self.memory = malloc(self.capacity)!
|
|
||||||
} else {
|
|
||||||
self.memory = realloc(self.memory, self.capacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
memcpy(self.memory + self.offset, data, length)
|
|
||||||
self.offset += length
|
self.offset += length
|
||||||
self.length = self.offset
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func writeInt8(_ value: Int8) {
|
public func writeInt8(_ value: Int8) {
|
||||||
@ -135,18 +62,20 @@ public final class MeshWriteBuffer: MeshMemoryBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func write(_ data: Data) {
|
public func write(_ data: Data) {
|
||||||
let length = data.count
|
data.withUnsafeBytes { bytes in
|
||||||
if self.offset + length > self.capacity {
|
self.write(bytes.baseAddress!, length: bytes.count)
|
||||||
self.capacity = self.offset + length + 256
|
|
||||||
if self.length == 0 {
|
|
||||||
self.memory = malloc(self.capacity)!
|
|
||||||
} else {
|
|
||||||
self.memory = realloc(self.memory, self.capacity)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
data.copyBytes(to: self.memory.advanced(by: offset).assumingMemoryBound(to: UInt8.self), count: length)
|
|
||||||
self.offset += length
|
func write(_ data: DataRange) {
|
||||||
self.length = self.offset
|
data.data.withUnsafeBytes { bytes in
|
||||||
|
self.write(bytes.baseAddress!.advanced(by: data.range.lowerBound), length: data.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func seek(offset: Int) {
|
||||||
|
self.file.seek(position: Int64(offset))
|
||||||
|
self.offset = offset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,23 +86,17 @@ public final class MeshReadBuffer: MeshMemoryBuffer {
|
|||||||
super.init(data: data)
|
super.init(data: data)
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(memory: UnsafeMutableRawPointer, length: Int, freeWhenDone: Bool) {
|
|
||||||
super.init(memory: memory, capacity: length, length: length, freeWhenDone: freeWhenDone)
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(memoryBufferNoCopy: MeshMemoryBuffer) {
|
|
||||||
super.init(memory: memoryBufferNoCopy.memory, capacity: memoryBufferNoCopy.length, length: memoryBufferNoCopy.length, freeWhenDone: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func dataNoCopy() -> Data {
|
|
||||||
return Data(bytesNoCopy: self.memory.assumingMemoryBound(to: UInt8.self), count: self.length, deallocator: .none)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func read(_ data: UnsafeMutableRawPointer, length: Int) {
|
public func read(_ data: UnsafeMutableRawPointer, length: Int) {
|
||||||
memcpy(data, self.memory.advanced(by: self.offset), length)
|
self.data.copyBytes(to: data.assumingMemoryBound(to: UInt8.self), from: self.offset ..< (self.offset + length))
|
||||||
self.offset += length
|
self.offset += length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readDataRange(count: Int) -> DataRange {
|
||||||
|
let result = DataRange(data: self.data, range: self.offset ..< (self.offset + count))
|
||||||
|
self.offset += count
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
public func readInt8() -> Int8 {
|
public func readInt8() -> Int8 {
|
||||||
var result: Int8 = 0
|
var result: Int8 = 0
|
||||||
self.read(&result, length: 1)
|
self.read(&result, length: 1)
|
||||||
@ -195,12 +118,4 @@ public final class MeshReadBuffer: MeshMemoryBuffer {
|
|||||||
public func skip(_ length: Int) {
|
public func skip(_ length: Int) {
|
||||||
self.offset += length
|
self.offset += length
|
||||||
}
|
}
|
||||||
|
|
||||||
public func reset() {
|
|
||||||
self.offset = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
public func sharedBufferNoCopy() -> MeshReadBuffer {
|
|
||||||
return MeshReadBuffer(memory: memory, length: length, freeWhenDone: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ import Foundation
|
|||||||
import Metal
|
import Metal
|
||||||
import MetalKit
|
import MetalKit
|
||||||
import LottieMeshBinding
|
import LottieMeshBinding
|
||||||
|
import Postbox
|
||||||
|
|
||||||
enum TriangleFill {
|
enum TriangleFill {
|
||||||
struct Color {
|
struct Color {
|
||||||
@ -109,15 +110,29 @@ enum MeshOption {
|
|||||||
case stroke(lineWidth: CGFloat, miterLimit: CGFloat, lineJoin: CGLineJoin, lineCap: CGLineCap)
|
case stroke(lineWidth: CGFloat, miterLimit: CGFloat, lineJoin: CGLineJoin, lineCap: CGLineCap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class DataRange {
|
||||||
|
let data: Data
|
||||||
|
let range: Range<Int>
|
||||||
|
|
||||||
|
init(data: Data, range: Range<Int>) {
|
||||||
|
self.data = data
|
||||||
|
self.range = range
|
||||||
|
}
|
||||||
|
|
||||||
|
var count: Int {
|
||||||
|
return self.range.upperBound - self.range.lowerBound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public final class MeshAnimation {
|
public final class MeshAnimation {
|
||||||
final class Frame {
|
final class Frame {
|
||||||
final class Segment {
|
final class Segment {
|
||||||
let vertices: Data
|
let vertices: DataRange
|
||||||
let triangles: Data
|
let triangles: DataRange
|
||||||
let fill: TriangleFill
|
let fill: TriangleFill
|
||||||
let transform: CGAffineTransform
|
let transform: CGAffineTransform
|
||||||
|
|
||||||
init(vertices: Data, triangles: Data, fill: TriangleFill, transform: CGAffineTransform) {
|
init(vertices: DataRange, triangles: DataRange, fill: TriangleFill, transform: CGAffineTransform) {
|
||||||
self.vertices = vertices
|
self.vertices = vertices
|
||||||
self.triangles = triangles
|
self.triangles = triangles
|
||||||
self.fill = fill
|
self.fill = fill
|
||||||
@ -126,16 +141,10 @@ public final class MeshAnimation {
|
|||||||
|
|
||||||
static func read(buffer: MeshReadBuffer) -> Segment {
|
static func read(buffer: MeshReadBuffer) -> Segment {
|
||||||
let vertCount = Int(buffer.readInt32())
|
let vertCount = Int(buffer.readInt32())
|
||||||
var vertices = Data(count: vertCount)
|
let vertices = buffer.readDataRange(count: vertCount)
|
||||||
vertices.withUnsafeMutableBytes { bytes in
|
|
||||||
buffer.read(bytes.baseAddress!, length: bytes.count)
|
|
||||||
}
|
|
||||||
|
|
||||||
let triCount = Int(buffer.readInt32())
|
let triCount = Int(buffer.readInt32())
|
||||||
var triangles = Data(count: triCount)
|
let triangles = buffer.readDataRange(count: triCount)
|
||||||
triangles.withUnsafeMutableBytes { bytes in
|
|
||||||
buffer.read(bytes.baseAddress!, length: bytes.count)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Segment(vertices: vertices, triangles: triangles, fill: TriangleFill.read(buffer: buffer), transform: CGAffineTransform(a: CGFloat(buffer.readFloat()), b: CGFloat(buffer.readFloat()), c: CGFloat(buffer.readFloat()), d: CGFloat(buffer.readFloat()), tx: CGFloat(buffer.readFloat()), ty: CGFloat(buffer.readFloat())))
|
return Segment(vertices: vertices, triangles: triangles, fill: TriangleFill.read(buffer: buffer), transform: CGAffineTransform(a: CGFloat(buffer.readFloat()), b: CGFloat(buffer.readFloat()), c: CGFloat(buffer.readFloat()), d: CGFloat(buffer.readFloat()), tx: CGFloat(buffer.readFloat()), ty: CGFloat(buffer.readFloat())))
|
||||||
}
|
}
|
||||||
@ -171,12 +180,12 @@ public final class MeshAnimation {
|
|||||||
return Frame(segments: segments)
|
return Frame(segments: segments)
|
||||||
}
|
}
|
||||||
|
|
||||||
func write(buffer: MeshWriteBuffer) {
|
/*func write(buffer: MeshWriteBuffer) {
|
||||||
buffer.writeInt32(Int32(self.segments.count))
|
buffer.writeInt32(Int32(self.segments.count))
|
||||||
for segment in self.segments {
|
for segment in self.segments {
|
||||||
segment.write(buffer: buffer)
|
segment.write(buffer: buffer)
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
let frames: [Frame]
|
let frames: [Frame]
|
||||||
@ -194,12 +203,12 @@ public final class MeshAnimation {
|
|||||||
return MeshAnimation(frames: frames)
|
return MeshAnimation(frames: frames)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func write(buffer: MeshWriteBuffer) {
|
/*public func write(buffer: MeshWriteBuffer) {
|
||||||
buffer.writeInt32(Int32(self.frames.count))
|
buffer.writeInt32(Int32(self.frames.count))
|
||||||
for frame in self.frames {
|
for frame in self.frames {
|
||||||
frame.write(buffer: buffer)
|
frame.write(buffer: buffer)
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 13.0, *)
|
@available(iOS 13.0, *)
|
||||||
@ -456,15 +465,15 @@ public final class MeshRenderer: MTKView {
|
|||||||
let startVertexIndex = nextVertexIndex
|
let startVertexIndex = nextVertexIndex
|
||||||
let startIndexIndex = nextIndexIndex
|
let startIndexIndex = nextIndexIndex
|
||||||
|
|
||||||
segment.vertices.withUnsafeBytes { vertices in
|
segment.vertices.data.withUnsafeBytes { vertices in
|
||||||
let _ = memcpy(vertexData.advanced(by: nextVertexIndex * 2), vertices.baseAddress!, vertices.count)
|
let _ = memcpy(vertexData.advanced(by: nextVertexIndex * 2), vertices.baseAddress!.advanced(by: segment.vertices.range.lowerBound), segment.vertices.count)
|
||||||
}
|
}
|
||||||
nextVertexIndex += segment.vertices.count / (4 * 2)
|
nextVertexIndex += segment.vertices.count / (4 * 2)
|
||||||
|
|
||||||
let baseVertexIndex = Int32(startVertexIndex)
|
let baseVertexIndex = Int32(startVertexIndex)
|
||||||
|
|
||||||
segment.triangles.withUnsafeBytes { triangles in
|
segment.triangles.data.withUnsafeBytes { triangles in
|
||||||
let _ = memcpy(indexData.advanced(by: nextIndexIndex), triangles.baseAddress!, triangles.count)
|
let _ = memcpy(indexData.advanced(by: nextIndexIndex), triangles.baseAddress!.advanced(by: segment.triangles.range.lowerBound), segment.triangles.count)
|
||||||
}
|
}
|
||||||
nextIndexIndex += segment.triangles.count / 4
|
nextIndexIndex += segment.triangles.count / 4
|
||||||
|
|
||||||
@ -534,8 +543,6 @@ public final class MeshRenderer: MTKView {
|
|||||||
|
|
||||||
renderEncoder.setVertexBuffer(mesh.vertexBuffer, offset: 0, index: 0)
|
renderEncoder.setVertexBuffer(mesh.vertexBuffer, offset: 0, index: 0)
|
||||||
renderEncoder.drawIndexedPrimitives(type: .triangle, indexCount: iCount, indexType: .uint32, indexBuffer: mesh.indexBuffer, indexBufferOffset: iStart * 4)
|
renderEncoder.drawIndexedPrimitives(type: .triangle, indexCount: iCount, indexType: .uint32, indexBuffer: mesh.indexBuffer, indexBufferOffset: iStart * 4)
|
||||||
//renderEncoder.drawIndexedPrimitives(type: .triangle, indexCount: iCount, indexType: .uint32, indexBuffer: mesh.indexBuffer, indexBufferOffset: iStart, instanceCount: 1)
|
|
||||||
//renderEncoder.drawPrimitives(type: .triangle, vertexStart: startIndex, vertexCount: count, instanceCount: 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextFrame = mesh.currentFrame + 1
|
let nextFrame = mesh.currentFrame + 1
|
||||||
@ -566,18 +573,13 @@ public final class MeshRenderer: MTKView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func generateSegments(geometry: CapturedGeometryNode, superAlpha: CGFloat = 1.0, path: [Int] = []) -> [MeshAnimation.Frame.Segment] {
|
private func generateSegments(geometry: CapturedGeometryNode, superAlpha: CGFloat, superTransform: CGAffineTransform, writeSegment: (MeshAnimation.Frame.Segment) -> Void) {
|
||||||
if geometry.isHidden || geometry.alpha.isZero {
|
if geometry.isHidden || geometry.alpha.isZero {
|
||||||
return []
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var result: [MeshAnimation.Frame.Segment] = []
|
|
||||||
|
|
||||||
for i in 0 ..< geometry.subnodes.count {
|
for i in 0 ..< geometry.subnodes.count {
|
||||||
let subResult = generateSegments(geometry: geometry.subnodes[i], superAlpha: superAlpha * geometry.alpha, path: path + [i]).map { segment in
|
generateSegments(geometry: geometry.subnodes[i], superAlpha: superAlpha * geometry.alpha, superTransform: CATransform3DGetAffineTransform(geometry.transform).concatenating(superTransform), writeSegment: writeSegment)
|
||||||
return MeshAnimation.Frame.Segment(vertices: segment.vertices, triangles: segment.triangles, fill: segment.fill, transform: segment.transform.concatenating(CATransform3DGetAffineTransform(geometry.transform)))
|
|
||||||
}
|
|
||||||
result.append(contentsOf: subResult)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let displayItem = geometry.displayItem {
|
if let displayItem = geometry.displayItem {
|
||||||
@ -615,7 +617,7 @@ private func generateSegments(geometry: CapturedGeometryNode, superAlpha: CGFloa
|
|||||||
meshData = LottieMeshData.generate(with: UIBezierPath(cgPath: displayItem.path), fill: nil, stroke: LottieMeshStroke(lineWidth: stroke.lineWidth, lineJoin: stroke.lineJoin, lineCap: stroke.lineCap, miterLimit: stroke.miterLimit))
|
meshData = LottieMeshData.generate(with: UIBezierPath(cgPath: displayItem.path), fill: nil, stroke: LottieMeshStroke(lineWidth: stroke.lineWidth, lineJoin: stroke.lineJoin, lineCap: stroke.lineCap, miterLimit: stroke.miterLimit))
|
||||||
}
|
}
|
||||||
if let meshData = meshData, meshData.triangleCount() != 0 {
|
if let meshData = meshData, meshData.triangleCount() != 0 {
|
||||||
let mappedTriangles = MeshWriteBuffer()
|
let mappedTriangles = WriteBuffer()
|
||||||
for i in 0 ..< meshData.triangleCount() {
|
for i in 0 ..< meshData.triangleCount() {
|
||||||
var v0: Int = 0
|
var v0: Int = 0
|
||||||
var v1: Int = 0
|
var v1: Int = 0
|
||||||
@ -626,7 +628,7 @@ private func generateSegments(geometry: CapturedGeometryNode, superAlpha: CGFloa
|
|||||||
mappedTriangles.writeInt32(Int32(v2))
|
mappedTriangles.writeInt32(Int32(v2))
|
||||||
}
|
}
|
||||||
|
|
||||||
let mappedVertices = MeshWriteBuffer()
|
let mappedVertices = WriteBuffer()
|
||||||
for i in 0 ..< meshData.vertexCount() {
|
for i in 0 ..< meshData.vertexCount() {
|
||||||
var x: Float = 0.0
|
var x: Float = 0.0
|
||||||
var y: Float = 0.0
|
var y: Float = 0.0
|
||||||
@ -635,20 +637,32 @@ private func generateSegments(geometry: CapturedGeometryNode, superAlpha: CGFloa
|
|||||||
mappedVertices.writeFloat(y)
|
mappedVertices.writeFloat(y)
|
||||||
}
|
}
|
||||||
|
|
||||||
result.append(MeshAnimation.Frame.Segment(vertices: mappedVertices.makeData(), triangles: mappedTriangles.makeData(), fill: triangleFill, transform: CATransform3DGetAffineTransform(geometry.transform)))
|
let verticesData = mappedVertices.makeData()
|
||||||
}
|
let trianglesData = mappedTriangles.makeData()
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
let segment = MeshAnimation.Frame.Segment(vertices: DataRange(data: verticesData, range: 0 ..< verticesData.count), triangles: DataRange(data: trianglesData, range: 0 ..< trianglesData.count), fill: triangleFill, transform: CATransform3DGetAffineTransform(geometry.transform).concatenating(superTransform))
|
||||||
|
|
||||||
|
writeSegment(segment)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func generateMeshAnimation(data: Data) -> MeshAnimation? {
|
public func generateMeshAnimation(data: Data) -> TempBoxFile? {
|
||||||
guard let animation = try? JSONDecoder().decode(Animation.self, from: data) else {
|
guard let animation = try? JSONDecoder().decode(Animation.self, from: data) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
let container = MyAnimationContainer(animation: animation)
|
let container = MyAnimationContainer(animation: animation)
|
||||||
|
|
||||||
var frames: [MeshAnimation.Frame] = []
|
let tempFile = TempBox.shared.tempFile(fileName: "data")
|
||||||
|
guard let file = ManagedFile(queue: nil, path: tempFile.path, mode: .readwrite) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let writeBuffer = MeshWriteBuffer(file: file)
|
||||||
|
|
||||||
|
let frameCountOffset = writeBuffer.offset
|
||||||
|
writeBuffer.writeInt32(0)
|
||||||
|
|
||||||
|
var frameCount: Int = 0
|
||||||
|
|
||||||
for i in 0 ..< Int(animation.endFrame) {
|
for i in 0 ..< Int(animation.endFrame) {
|
||||||
container.setFrame(frame: CGFloat(i))
|
container.setFrame(frame: CGFloat(i))
|
||||||
@ -656,11 +670,36 @@ public func generateMeshAnimation(data: Data) -> MeshAnimation? {
|
|||||||
print("Frame \(i) / \(Int(animation.endFrame))")
|
print("Frame \(i) / \(Int(animation.endFrame))")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
let segmentCountOffset = writeBuffer.offset
|
||||||
|
writeBuffer.writeInt32(0)
|
||||||
|
var segmentCount: Int = 0
|
||||||
|
|
||||||
let geometry = container.captureGeometry()
|
let geometry = container.captureGeometry()
|
||||||
geometry.transform = CATransform3DMakeTranslation(256.0, 256.0, 0.0)
|
geometry.transform = CATransform3DMakeTranslation(256.0, 256.0, 0.0)
|
||||||
let segments = generateSegments(geometry: geometry)
|
|
||||||
frames.append(MeshAnimation.Frame(segments: segments))
|
generateSegments(geometry: geometry, superAlpha: 1.0, superTransform: .identity, writeSegment: { segment in
|
||||||
|
segment.write(buffer: writeBuffer)
|
||||||
|
|
||||||
|
segmentCount += 1
|
||||||
|
})
|
||||||
|
|
||||||
|
let currentOffset = writeBuffer.offset
|
||||||
|
writeBuffer.seek(offset: segmentCountOffset)
|
||||||
|
writeBuffer.writeInt32(Int32(segmentCount))
|
||||||
|
|
||||||
|
writeBuffer.seek(offset: currentOffset)
|
||||||
|
|
||||||
|
frameCount += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return MeshAnimation(frames: frames)
|
let currentOffset = writeBuffer.offset
|
||||||
|
writeBuffer.seek(offset: frameCountOffset)
|
||||||
|
writeBuffer.writeInt32(Int32(frameCount))
|
||||||
|
writeBuffer.seek(offset: currentOffset)
|
||||||
|
|
||||||
|
return tempFile
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class MeshRenderingContext {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ public final class MeshAnimationCache {
|
|||||||
if let item = self.items[resource.id.stringRepresentation] {
|
if let item = self.items[resource.id.stringRepresentation] {
|
||||||
if let animation = item.animation {
|
if let animation = item.animation {
|
||||||
return animation
|
return animation
|
||||||
} else if let readyPath = item.readyPath, let data = try? Data(contentsOf: URL(fileURLWithPath: readyPath)) {
|
} else if let readyPath = item.readyPath, let data = try? Data(contentsOf: URL(fileURLWithPath: readyPath), options: [.alwaysMapped]) {
|
||||||
let buffer = MeshReadBuffer(data: data)
|
let buffer = MeshReadBuffer(data: data)
|
||||||
let animation = MeshAnimation.read(buffer: buffer)
|
let animation = MeshAnimation.read(buffer: buffer)
|
||||||
item.animation = animation
|
item.animation = animation
|
||||||
@ -38,7 +38,7 @@ public final class MeshAnimationCache {
|
|||||||
self.items[resource.id.stringRepresentation] = item
|
self.items[resource.id.stringRepresentation] = item
|
||||||
|
|
||||||
let path = self.mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "mesh-animation", keepDuration: .general)
|
let path = self.mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "mesh-animation", keepDuration: .general)
|
||||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.alwaysMapped]) {
|
||||||
let animation = MeshAnimation.read(buffer: MeshReadBuffer(data: data))
|
let animation = MeshAnimation.read(buffer: MeshReadBuffer(data: data))
|
||||||
item.readyPath = path
|
item.readyPath = path
|
||||||
item.animation = animation
|
item.animation = animation
|
||||||
@ -54,7 +54,7 @@ public final class MeshAnimationCache {
|
|||||||
if let item = self.items[bundleName] {
|
if let item = self.items[bundleName] {
|
||||||
if let animation = item.animation {
|
if let animation = item.animation {
|
||||||
return animation
|
return animation
|
||||||
} else if let readyPath = item.readyPath, let data = try? Data(contentsOf: URL(fileURLWithPath: readyPath)) {
|
} else if let readyPath = item.readyPath, let data = try? Data(contentsOf: URL(fileURLWithPath: readyPath), options: [.alwaysMapped]) {
|
||||||
let buffer = MeshReadBuffer(data: data)
|
let buffer = MeshReadBuffer(data: data)
|
||||||
let animation = MeshAnimation.read(buffer: buffer)
|
let animation = MeshAnimation.read(buffer: buffer)
|
||||||
item.animation = animation
|
item.animation = animation
|
||||||
@ -67,7 +67,7 @@ public final class MeshAnimationCache {
|
|||||||
self.items[bundleName] = item
|
self.items[bundleName] = item
|
||||||
|
|
||||||
let path = self.mediaBox.cachedRepresentationPathForId(bundleName, representationId: "mesh-animation", keepDuration: .general)
|
let path = self.mediaBox.cachedRepresentationPathForId(bundleName, representationId: "mesh-animation", keepDuration: .general)
|
||||||
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.alwaysMapped]) {
|
||||||
let animation = MeshAnimation.read(buffer: MeshReadBuffer(data: data))
|
let animation = MeshAnimation.read(buffer: MeshReadBuffer(data: data))
|
||||||
item.readyPath = path
|
item.readyPath = path
|
||||||
item.animation = animation
|
item.animation = animation
|
||||||
@ -89,18 +89,22 @@ public final class MeshAnimationCache {
|
|||||||
|> take(1)
|
|> take(1)
|
||||||
|> mapToSignal { data -> Signal<(MeshAnimation, String)?, NoError> in
|
|> mapToSignal { data -> Signal<(MeshAnimation, String)?, NoError> in
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
guard let zippedData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) else {
|
guard let zippedData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.alwaysMapped]) else {
|
||||||
subscriber.putNext(nil)
|
subscriber.putNext(nil)
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
}
|
}
|
||||||
let jsonData = TGGUnzipData(zippedData, 1 * 1024 * 1024) ?? zippedData
|
let jsonData = TGGUnzipData(zippedData, 1 * 1024 * 1024) ?? zippedData
|
||||||
if let animation = generateMeshAnimation(data: jsonData) {
|
if let tempFile = generateMeshAnimation(data: jsonData) {
|
||||||
let buffer = MeshWriteBuffer()
|
mediaBox.storeCachedResourceRepresentation(resource.id.stringRepresentation, representationId: "mesh-animation", keepDuration: .general, tempFile: tempFile, completion: { path in
|
||||||
animation.write(buffer: buffer)
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.alwaysMapped]) {
|
||||||
mediaBox.storeCachedResourceRepresentation(resource, representationId: "mesh-animation", keepDuration: .general, data: buffer.makeData(), completion: { path in
|
let animation = MeshAnimation.read(buffer: MeshReadBuffer(data: data))
|
||||||
subscriber.putNext((animation, path))
|
subscriber.putNext((animation, path))
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
|
} else {
|
||||||
|
subscriber.putNext(nil)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
subscriber.putNext(nil)
|
subscriber.putNext(nil)
|
||||||
@ -136,18 +140,22 @@ public final class MeshAnimationCache {
|
|||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let zippedData = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
guard let zippedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.alwaysMapped]) else {
|
||||||
subscriber.putNext(nil)
|
subscriber.putNext(nil)
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
}
|
}
|
||||||
let jsonData = TGGUnzipData(zippedData, 1 * 1024 * 1024) ?? zippedData
|
let jsonData = TGGUnzipData(zippedData, 1 * 1024 * 1024) ?? zippedData
|
||||||
if let animation = generateMeshAnimation(data: jsonData) {
|
if let tempFile = generateMeshAnimation(data: jsonData) {
|
||||||
let buffer = MeshWriteBuffer()
|
mediaBox.storeCachedResourceRepresentation(bundleName, representationId: "mesh-animation", keepDuration: .general, tempFile: tempFile, completion: { path in
|
||||||
animation.write(buffer: buffer)
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.alwaysMapped]) {
|
||||||
mediaBox.storeCachedResourceRepresentation(bundleName, representationId: "mesh-animation", keepDuration: .general, data: buffer.makeData(), completion: { path in
|
let animation = MeshAnimation.read(buffer: MeshReadBuffer(data: data))
|
||||||
subscriber.putNext((animation, path))
|
subscriber.putNext((animation, path))
|
||||||
subscriber.putCompletion()
|
subscriber.putCompletion()
|
||||||
|
} else {
|
||||||
|
subscriber.putNext(nil)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
subscriber.putNext(nil)
|
subscriber.putNext(nil)
|
||||||
|
@ -2,9 +2,11 @@ import Foundation
|
|||||||
|
|
||||||
public struct NotificationsPresentationData: Codable, Equatable {
|
public struct NotificationsPresentationData: Codable, Equatable {
|
||||||
public var applicationLockedMessageString: String
|
public var applicationLockedMessageString: String
|
||||||
|
public var incomingCallString: String
|
||||||
|
|
||||||
public init(applicationLockedMessageString: String) {
|
public init(applicationLockedMessageString: String, incomingCallString: String) {
|
||||||
self.applicationLockedMessageString = applicationLockedMessageString
|
self.applicationLockedMessageString = applicationLockedMessageString
|
||||||
|
self.incomingCallString = incomingCallString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -805,6 +805,14 @@ public final class MediaBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func storeCachedResourceRepresentation(_ resourceId: String, representationId: String, keepDuration: CachedMediaRepresentationKeepDuration, tempFile: TempBoxFile, completion: @escaping (String) -> Void = { _ in }) {
|
||||||
|
self.dataQueue.async {
|
||||||
|
let path = self.cachedRepresentationPathsForId(resourceId, representationId: representationId, keepDuration: keepDuration).complete
|
||||||
|
let _ = try? FileManager.default.moveItem(atPath: tempFile.path, toPath: path)
|
||||||
|
completion(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func cachedResourceRepresentation(_ resource: MediaResource, representation: CachedMediaResourceRepresentation, pathExtension: String? = nil, complete: Bool, fetch: Bool = true, attemptSynchronously: Bool = false) -> Signal<MediaResourceData, NoError> {
|
public func cachedResourceRepresentation(_ resource: MediaResource, representation: CachedMediaResourceRepresentation, pathExtension: String? = nil, complete: Bool, fetch: Bool = true, attemptSynchronously: Bool = false) -> Signal<MediaResourceData, NoError> {
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
let disposable = MetaDisposable()
|
let disposable = MetaDisposable()
|
||||||
|
25
submodules/ReactionSelectionNode/BUILD
Normal file
25
submodules/ReactionSelectionNode/BUILD
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "ReactionSelectionNode",
|
||||||
|
module_name = "ReactionSelectionNode",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/Postbox:Postbox",
|
||||||
|
"//submodules/TelegramCore:TelegramCore",
|
||||||
|
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||||
|
"//submodules/Display:Display",
|
||||||
|
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||||
|
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||||
|
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,529 @@
|
|||||||
|
import Foundation
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import AnimatedStickerNode
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
|
public enum ReactionGestureItem {
|
||||||
|
case like
|
||||||
|
case unlike
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class ReactionContextItem {
|
||||||
|
public enum Reaction {
|
||||||
|
case like
|
||||||
|
case unlike
|
||||||
|
}
|
||||||
|
|
||||||
|
public let reaction: ReactionContextItem.Reaction
|
||||||
|
|
||||||
|
public init(reaction: ReactionContextItem.Reaction) {
|
||||||
|
self.reaction = reaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + 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(shadowBlur + diameter / 2.0), topCapHeight: Int(shadowBlur + diameter / 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(shadow.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(shadow.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 let contentContainer: ASDisplayNode
|
||||||
|
private var itemNodes: [ReactionNode] = []
|
||||||
|
private let disclosureButton: HighlightTrackingButtonNode
|
||||||
|
|
||||||
|
private var isExpanded: Bool = true
|
||||||
|
private var highlightedReaction: ReactionContextItem.Reaction?
|
||||||
|
private var validLayout: (CGSize, UIEdgeInsets, 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)
|
||||||
|
|
||||||
|
self.contentContainer = ASDisplayNode()
|
||||||
|
self.contentContainer.clipsToBounds = true
|
||||||
|
|
||||||
|
self.disclosureButton = HighlightTrackingButtonNode()
|
||||||
|
self.disclosureButton.hitTestSlop = UIEdgeInsets(top: -6.0, left: -6.0, bottom: -6.0, right: -6.0)
|
||||||
|
let buttonImage = generateImage(CGSize(width: 30.0, height: 30.0), rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.setFillColor(theme.contextMenu.dimColor.cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.setBlendMode(.copy)
|
||||||
|
context.setStrokeColor(UIColor.clear.cgColor)
|
||||||
|
context.setLineWidth(2.0)
|
||||||
|
context.setLineCap(.round)
|
||||||
|
context.setLineJoin(.round)
|
||||||
|
context.beginPath()
|
||||||
|
context.move(to: CGPoint(x: 8.0, y: size.height / 2.0 + 3.0))
|
||||||
|
context.addLine(to: CGPoint(x: size.width / 2.0, y: 11.0))
|
||||||
|
context.addLine(to: CGPoint(x: size.width - 8.0, y: size.height / 2.0 + 3.0))
|
||||||
|
context.strokePath()
|
||||||
|
})
|
||||||
|
self.disclosureButton.setImage(buttonImage, for: [])
|
||||||
|
|
||||||
|
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.contentContainer.addSubnode(self.disclosureButton)
|
||||||
|
|
||||||
|
self.itemNodes = self.items.map { item in
|
||||||
|
let reactionItem: ReactionGestureItem
|
||||||
|
switch item.reaction {
|
||||||
|
case .like:
|
||||||
|
reactionItem = .like
|
||||||
|
case .unlike:
|
||||||
|
reactionItem = .unlike
|
||||||
|
}
|
||||||
|
return ReactionNode(account: account, theme: theme, reaction: reactionItem, maximizedReactionSize: 30.0, loadFirstFrame: true)
|
||||||
|
}
|
||||||
|
self.itemNodes.forEach(self.contentContainer.addSubnode)
|
||||||
|
|
||||||
|
self.addSubnode(self.contentContainer)
|
||||||
|
|
||||||
|
self.disclosureButton.addTarget(self, action: #selector(self.disclosurePressed), forControlEvents: .touchUpInside)
|
||||||
|
self.disclosureButton.highligthedChanged = { [weak self] highlighted in
|
||||||
|
if highlighted {
|
||||||
|
self?.disclosureButton.layer.animateScale(from: 1.0, to: 0.8, duration: 0.15, removeOnCompletion: false)
|
||||||
|
} else {
|
||||||
|
self?.disclosureButton.layer.animateScale(from: 0.8, to: 1.0, duration: 0.25)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func didLoad() {
|
||||||
|
super.didLoad()
|
||||||
|
|
||||||
|
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: transition, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func calculateBackgroundFrame(containerSize: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, contentSize: CGSize) -> (CGRect, Bool) {
|
||||||
|
var contentSize = contentSize
|
||||||
|
contentSize.width = max(52.0, contentSize.width)
|
||||||
|
contentSize.height = 52.0
|
||||||
|
|
||||||
|
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 - 4.0, y: anchorRect.minY - contentSize.height + backgroundOffset.y), size: contentSize)
|
||||||
|
isLeftAligned = false
|
||||||
|
}
|
||||||
|
rect.origin.x = max(sideInset, rect.origin.x)
|
||||||
|
rect.origin.y = max(insets.top + sideInset, rect.origin.y)
|
||||||
|
rect.origin.x = min(containerSize.width - contentSize.width - sideInset, rect.origin.x)
|
||||||
|
return (rect, isLeftAligned)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, transition: ContainedViewLayoutTransition, animateInFromAnchorRect: CGRect?, animateOutToAnchorRect: CGRect?, animateReactionHighlight: Bool = false) {
|
||||||
|
self.validLayout = (size, insets, anchorRect)
|
||||||
|
|
||||||
|
let sideInset: CGFloat = 12.0
|
||||||
|
let itemSpacing: CGFloat = 6.0
|
||||||
|
let minimizedItemSize: CGFloat = 30.0
|
||||||
|
let maximizedItemSize: CGFloat = 30.0 - 18.0
|
||||||
|
let shadowBlur: CGFloat = 5.0
|
||||||
|
let verticalInset: CGFloat = 13.0
|
||||||
|
let rowHeight: CGFloat = 30.0
|
||||||
|
let rowSpacing: CGFloat = itemSpacing
|
||||||
|
|
||||||
|
let columnCount = min(6, 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 expandedRowCount = self.isExpanded ? rowCount : 1
|
||||||
|
|
||||||
|
let contentHeight = verticalInset * 2.0 + rowHeight * CGFloat(expandedRowCount) + CGFloat(expandedRowCount - 1) * rowSpacing
|
||||||
|
|
||||||
|
let (backgroundFrame, isLeftAligned) = self.calculateBackgroundFrame(containerSize: size, insets: insets, anchorRect: anchorRect, contentSize: CGSize(width: contentWidth, height: contentHeight))
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.contentContainer, frame: backgroundFrame)
|
||||||
|
|
||||||
|
for i in 0 ..< self.items.count {
|
||||||
|
let rowIndex = i / columnCount
|
||||||
|
let columnIndex = i % columnCount
|
||||||
|
let row = CGFloat(rowIndex)
|
||||||
|
let column = CGFloat(columnIndex)
|
||||||
|
|
||||||
|
let itemSize: CGFloat = minimizedItemSize
|
||||||
|
let itemOffset: CGFloat = 0.0
|
||||||
|
|
||||||
|
let itemFrame = CGRect(origin: CGPoint(x: sideInset + column * (minimizedItemSize + itemSpacing) - itemOffset, y: verticalInset + row * (rowHeight + rowSpacing) + floor((rowHeight - minimizedItemSize) / 2.0) - itemOffset), size: CGSize(width: itemSize, height: itemSize))
|
||||||
|
transition.updateFrame(node: self.itemNodes[i], frame: itemFrame, 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(false, animated: false)
|
||||||
|
if rowIndex != 0 || columnIndex == columnCount - 1 {
|
||||||
|
if self.isExpanded {
|
||||||
|
if self.itemNodes[i].alpha.isZero {
|
||||||
|
self.itemNodes[i].alpha = 1.0
|
||||||
|
if transition.isAnimated {
|
||||||
|
let delayOffset: Double = 1.0 - Double(columnIndex) / Double(columnCount - 1)
|
||||||
|
self.itemNodes[i].layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4 + delayOffset * 0.32, initialVelocity: 0.0, damping: 95.0)
|
||||||
|
self.itemNodes[i].layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.05)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.itemNodes[i].alpha = 0.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.itemNodes[i].alpha = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowIndex == 0 && columnIndex == columnCount - 1 {
|
||||||
|
transition.updateFrame(node: self.disclosureButton, frame: itemFrame)
|
||||||
|
if self.isExpanded {
|
||||||
|
if self.disclosureButton.alpha.isEqual(to: 1.0) {
|
||||||
|
self.disclosureButton.alpha = 0.0
|
||||||
|
if transition.isAnimated {
|
||||||
|
self.disclosureButton.layer.animateScale(from: 0.8, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||||
|
self.disclosureButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak self] _ in
|
||||||
|
self?.disclosureButton.layer.removeAnimation(forKey: "scale")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.disclosureButton.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: backgroundFrame.midX - floor(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: backgroundFrame.midX - floor(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, insets: insets, 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, insets: insets, 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.15)
|
||||||
|
|
||||||
|
if let (size, insets, anchorRect) = self.validLayout {
|
||||||
|
self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .immediate, animateInFromAnchorRect: sourceAnchorRect, animateOutToAnchorRect: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let smallCircleDuration: Double = 0.5
|
||||||
|
let largeCircleDuration: Double = 0.5
|
||||||
|
let largeCircleDelay: Double = 0.08
|
||||||
|
let mainCircleDuration: Double = 0.5
|
||||||
|
let mainCircleDelay: Double = 0.1
|
||||||
|
|
||||||
|
self.smallCircleNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: smallCircleDuration)
|
||||||
|
self.smallCircleShadowNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: smallCircleDuration)
|
||||||
|
|
||||||
|
self.largeCircleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: largeCircleDelay)
|
||||||
|
self.largeCircleNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: largeCircleDuration, delay: largeCircleDelay)
|
||||||
|
self.largeCircleShadowNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: largeCircleDuration, delay: largeCircleDelay)
|
||||||
|
|
||||||
|
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: mainCircleDelay)
|
||||||
|
self.backgroundNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay)
|
||||||
|
self.backgroundShadowNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay)
|
||||||
|
|
||||||
|
if let itemNode = self.itemNodes.first {
|
||||||
|
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: mainCircleDelay)
|
||||||
|
itemNode.didAppear()
|
||||||
|
itemNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: mainCircleDuration, delay: mainCircleDelay, completion: { _ in
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func animateOut(to targetAnchorRect: CGRect?, animatingOutToReaction: Bool) {
|
||||||
|
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)
|
||||||
|
for itemNode in self.itemNodes {
|
||||||
|
itemNode.layer.animateAlpha(from: itemNode.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
self.disclosureButton.layer.animateAlpha(from: self.disclosureButton.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||||
|
|
||||||
|
if let targetAnchorRect = targetAnchorRect, let (size, insets, anchorRect) = self.validLayout {
|
||||||
|
self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: targetAnchorRect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func animateOutToReaction(value: String, targetEmptyNode: ASDisplayNode, targetFilledNode: ASDisplayNode, hideNode: Bool, completion: @escaping () -> Void) {
|
||||||
|
for itemNode in self.itemNodes {
|
||||||
|
switch itemNode.reaction {
|
||||||
|
case .like:
|
||||||
|
if let snapshotView = itemNode.view.snapshotContentTree(keepTransform: true), let targetSnapshotView = targetFilledNode.view.snapshotContentTree() {
|
||||||
|
targetSnapshotView.frame = self.view.convert(targetFilledNode.bounds, from: targetFilledNode.view)
|
||||||
|
itemNode.isHidden = true
|
||||||
|
self.view.addSubview(targetSnapshotView)
|
||||||
|
self.view.addSubview(snapshotView)
|
||||||
|
snapshotView.frame = itemNode.view.convert(itemNode.view.bounds, to: self.view).offsetBy(dx: 25.0, dy: 1.0)
|
||||||
|
|
||||||
|
var completedTarget = false
|
||||||
|
let intermediateCompletion: () -> Void = {
|
||||||
|
if completedTarget {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetPosition = self.view.convert(targetFilledNode.bounds.center, from: targetFilledNode.view)
|
||||||
|
let duration: Double = 0.3
|
||||||
|
if hideNode {
|
||||||
|
targetFilledNode.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 {
|
||||||
|
targetFilledNode.isHidden = false
|
||||||
|
targetFilledNode.layer.animateSpring(from: 0.5 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, initialVelocity: 0.0, damping: 90.0)
|
||||||
|
targetEmptyNode.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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
let contentPoint = self.contentContainer.view.convert(point, from: self.view)
|
||||||
|
if !self.disclosureButton.alpha.isZero {
|
||||||
|
if let result = self.disclosureButton.hitTest(self.disclosureButton.view.convert(point, from: self.view), with: event) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for itemNode in self.itemNodes {
|
||||||
|
if !itemNode.alpha.isZero && itemNode.frame.contains(contentPoint) {
|
||||||
|
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? {
|
||||||
|
let contentPoint = self.contentContainer.view.convert(point, from: self.view)
|
||||||
|
for itemNode in self.itemNodes {
|
||||||
|
if !itemNode.alpha.isZero && itemNode.frame.contains(contentPoint) {
|
||||||
|
return itemNode.reaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for itemNode in self.itemNodes {
|
||||||
|
if !itemNode.alpha.isZero && itemNode.frame.insetBy(dx: -8.0, dy: -8.0).contains(contentPoint) {
|
||||||
|
return itemNode.reaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setHighlightedReaction(_ value: ReactionContextItem.Reaction?) {
|
||||||
|
self.highlightedReaction = value
|
||||||
|
if let (size, insets, anchorRect) = self.validLayout {
|
||||||
|
self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func disclosurePressed() {
|
||||||
|
self.isExpanded = true
|
||||||
|
if let (size, insets, anchorRect) = self.validLayout {
|
||||||
|
self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, transition: .animated(duration: 0.3, curve: .spring), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,488 @@
|
|||||||
|
import Foundation
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AppBundle
|
||||||
|
import AnimatedStickerNode
|
||||||
|
import TelegramAnimatedStickerNode
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
private let font = Font.medium(13.0)
|
||||||
|
|
||||||
|
final class ReactionNode: ASDisplayNode {
|
||||||
|
let reaction: ReactionGestureItem
|
||||||
|
private let textBackgroundNode: ASImageNode
|
||||||
|
private let textNode: ImmediateTextNode
|
||||||
|
private let animationNode: AnimatedStickerNode
|
||||||
|
private let imageNode: ASImageNode
|
||||||
|
private let additionalImageNode: ASImageNode
|
||||||
|
var isMaximized: Bool?
|
||||||
|
private let intrinsicSize: CGSize
|
||||||
|
private let intrinsicOffset: CGPoint
|
||||||
|
|
||||||
|
init(account: Account, theme: PresentationTheme, reaction: ReactionGestureItem, maximizedReactionSize: CGFloat, loadFirstFrame: Bool) {
|
||||||
|
self.reaction = reaction
|
||||||
|
|
||||||
|
self.textBackgroundNode = ASImageNode()
|
||||||
|
self.textBackgroundNode.displaysAsynchronously = false
|
||||||
|
self.textBackgroundNode.displayWithoutProcessing = true
|
||||||
|
self.textBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 20.0, color: theme.chat.serviceMessage.components.withDefaultWallpaper.dateFillFloating.withAlphaComponent(0.8))
|
||||||
|
self.textBackgroundNode.alpha = 0.0
|
||||||
|
|
||||||
|
self.textNode = ImmediateTextNode()
|
||||||
|
self.textNode.displaysAsynchronously = false
|
||||||
|
self.textNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
let reactionText: String = ""
|
||||||
|
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: reactionText, font: font, textColor: theme.chat.serviceMessage.dateTextColor.withWallpaper)
|
||||||
|
let textSize = self.textNode.updateLayout(CGSize(width: 200.0, height: 100.0))
|
||||||
|
let textBackgroundSize = CGSize(width: textSize.width + 12.0, height: 20.0)
|
||||||
|
let textBackgroundFrame = CGRect(origin: CGPoint(), size: textBackgroundSize)
|
||||||
|
let textFrame = CGRect(origin: CGPoint(x: floor((textBackgroundFrame.width - textSize.width) / 2.0), y: floor((textBackgroundFrame.height - textSize.height) / 2.0)), size: textSize)
|
||||||
|
self.textBackgroundNode.frame = textBackgroundFrame
|
||||||
|
self.textNode.frame = textFrame
|
||||||
|
self.textNode.alpha = 0.0
|
||||||
|
|
||||||
|
self.animationNode = AnimatedStickerNode()
|
||||||
|
self.animationNode.automaticallyLoadFirstFrame = loadFirstFrame
|
||||||
|
self.animationNode.playToCompletionOnStop = true
|
||||||
|
|
||||||
|
var intrinsicSize = CGSize(width: maximizedReactionSize + 14.0, height: maximizedReactionSize + 14.0)
|
||||||
|
|
||||||
|
self.imageNode = ASImageNode()
|
||||||
|
self.additionalImageNode = ASImageNode()
|
||||||
|
switch reaction {
|
||||||
|
case .like:
|
||||||
|
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0)
|
||||||
|
self.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Reactions/ContextHeartFilled"), color: UIColor(rgb: 0xfe1512))
|
||||||
|
case .unlike:
|
||||||
|
self.intrinsicOffset = CGPoint(x: 0.0, y: 0.0)
|
||||||
|
self.imageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Reactions/ContextHeartBrokenL"), color: UIColor(rgb: 0xfe1512))
|
||||||
|
self.additionalImageNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Reactions/ContextHeartBrokenR"), color: UIColor(rgb: 0xfe1512))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = self.imageNode.image {
|
||||||
|
intrinsicSize = image.size
|
||||||
|
}
|
||||||
|
|
||||||
|
self.intrinsicSize = intrinsicSize
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
//self.backgroundColor = .gray
|
||||||
|
|
||||||
|
//self.textBackgroundNode.addSubnode(self.textNode)
|
||||||
|
//self.addSubnode(self.textBackgroundNode)
|
||||||
|
|
||||||
|
//self.addSubnode(self.animationNode)
|
||||||
|
self.addSubnode(self.imageNode)
|
||||||
|
self.addSubnode(self.additionalImageNode)
|
||||||
|
self.animationNode.updateLayout(size: self.intrinsicSize)
|
||||||
|
self.animationNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
||||||
|
|
||||||
|
switch reaction {
|
||||||
|
case .like:
|
||||||
|
self.imageNode.frame = CGRect(origin: CGPoint(x: -5.0, y: -5.0), size: self.intrinsicSize)
|
||||||
|
case .unlike:
|
||||||
|
self.imageNode.frame = CGRect(origin: CGPoint(x: -6.0, y: -5.0), size: self.intrinsicSize)
|
||||||
|
self.additionalImageNode.frame = CGRect(origin: CGPoint(x: -3.0, y: -5.0), size: self.intrinsicSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(size: CGSize, scale: CGFloat, transition: ContainedViewLayoutTransition, displayText: Bool) {
|
||||||
|
/*transition.updatePosition(node: self.animationNode, position: CGPoint(x: size.width / 2.0 + self.intrinsicOffset.x * scale, y: size.height / 2.0 + self.intrinsicOffset.y * scale), beginWithCurrentState: true)
|
||||||
|
transition.updateTransformScale(node: self.animationNode, scale: scale, beginWithCurrentState: true)
|
||||||
|
transition.updatePosition(node: self.imageNode, position: CGPoint(x: size.width / 2.0 + self.intrinsicOffset.x * scale, y: size.height / 2.0 + self.intrinsicOffset.y * scale), beginWithCurrentState: true)
|
||||||
|
transition.updateTransformScale(node: self.imageNode, scale: scale, beginWithCurrentState: true)
|
||||||
|
|
||||||
|
transition.updatePosition(node: self.textBackgroundNode, position: CGPoint(x: size.width / 2.0, y: displayText ? -24.0 : (size.height / 2.0)), beginWithCurrentState: true)
|
||||||
|
transition.updateTransformScale(node: self.textBackgroundNode, scale: displayText ? 1.0 : 0.1, beginWithCurrentState: true)
|
||||||
|
|
||||||
|
transition.updateAlpha(node: self.textBackgroundNode, alpha: displayText ? 1.0 : 0.0, beginWithCurrentState: true)
|
||||||
|
transition.updateAlpha(node: self.textNode, alpha: displayText ? 1.0 : 0.0, beginWithCurrentState: true)*/
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateIsAnimating(_ isAnimating: Bool, animated: Bool) {
|
||||||
|
if isAnimating {
|
||||||
|
self.animationNode.visibility = true
|
||||||
|
} else {
|
||||||
|
self.animationNode.visibility = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func didAppear() {
|
||||||
|
switch self.reaction {
|
||||||
|
case .like:
|
||||||
|
self.imageNode.layer.animateScale(from: 1.0, to: 1.08, duration: 0.12, delay: 0.22, removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.imageNode.layer.animateScale(from: 1.08, to: 1.0, duration: 0.12)
|
||||||
|
})
|
||||||
|
case .unlike:
|
||||||
|
self.imageNode.layer.animatePosition(from: CGPoint(x: -2.5, y: 0.0), to: CGPoint(), duration: 0.2, delay: 0.15, additive: true)
|
||||||
|
self.additionalImageNode.layer.animatePosition(from: CGPoint(x: 2.5, y: 0.0), to: CGPoint(), duration: 0.2, delay: 0.15, additive: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ReactionSelectionNode: ASDisplayNode {
|
||||||
|
private let account: Account
|
||||||
|
private let theme: PresentationTheme
|
||||||
|
private let reactions: [ReactionGestureItem]
|
||||||
|
|
||||||
|
private let backgroundNode: ASImageNode
|
||||||
|
private let backgroundShadowNode: ASImageNode
|
||||||
|
private let bubbleNodes: [(ASImageNode, ASImageNode)]
|
||||||
|
private var reactionNodes: [ReactionNode] = []
|
||||||
|
private var hasSelectedNode = false
|
||||||
|
|
||||||
|
private let hapticFeedback = HapticFeedback()
|
||||||
|
|
||||||
|
private var shadowBlur: CGFloat = 8.0
|
||||||
|
private var minimizedReactionSize: CGFloat = 28.0
|
||||||
|
private var smallCircleSize: CGFloat = 14.0
|
||||||
|
|
||||||
|
private var isRightAligned: Bool = false
|
||||||
|
|
||||||
|
public init(account: Account, theme: PresentationTheme, reactions: [ReactionGestureItem]) {
|
||||||
|
self.account = account
|
||||||
|
self.theme = theme
|
||||||
|
self.reactions = reactions
|
||||||
|
|
||||||
|
self.backgroundNode = ASImageNode()
|
||||||
|
self.backgroundNode.displaysAsynchronously = false
|
||||||
|
self.backgroundNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
|
self.backgroundShadowNode = ASImageNode()
|
||||||
|
self.backgroundShadowNode.displaysAsynchronously = false
|
||||||
|
self.backgroundShadowNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
|
self.bubbleNodes = (0 ..< 2).map { i -> (ASImageNode, ASImageNode) in
|
||||||
|
let imageNode = ASImageNode()
|
||||||
|
imageNode.displaysAsynchronously = false
|
||||||
|
imageNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
|
let shadowNode = ASImageNode()
|
||||||
|
shadowNode.displaysAsynchronously = false
|
||||||
|
shadowNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
|
return (imageNode, shadowNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.bubbleNodes.forEach { _, shadow in
|
||||||
|
//self.addSubnode(shadow)
|
||||||
|
}
|
||||||
|
self.addSubnode(self.backgroundShadowNode)
|
||||||
|
self.bubbleNodes.forEach { foreground, _ in
|
||||||
|
//self.addSubnode(foreground)
|
||||||
|
}
|
||||||
|
self.addSubnode(self.backgroundNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool, touchPoint: CGPoint) {
|
||||||
|
let initialAnchorX = startingPoint.x
|
||||||
|
|
||||||
|
var isRightAligned = false
|
||||||
|
if initialAnchorX > constrainedSize.width / 2.0 {
|
||||||
|
isRightAligned = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let reactionSideInset: CGFloat = 10.0
|
||||||
|
let reactionSpacing: CGFloat = 6.0
|
||||||
|
let minReactionSpacing: CGFloat = 2.0
|
||||||
|
let minimizedReactionSize = self.minimizedReactionSize
|
||||||
|
let contentWidth: CGFloat = CGFloat(self.reactions.count) * (minimizedReactionSize) + CGFloat(self.reactions.count - 1) * reactionSpacing + reactionSideInset * 2.0
|
||||||
|
let spaceForMaximizedReaction = CGFloat(self.reactions.count - 1) * reactionSpacing - CGFloat(self.reactions.count - 1) * minReactionSpacing
|
||||||
|
let maximizedReactionSize: CGFloat = minimizedReactionSize + spaceForMaximizedReaction
|
||||||
|
let backgroundHeight: CGFloat = floor(self.minimizedReactionSize * 1.8)
|
||||||
|
|
||||||
|
var backgroundFrame = CGRect(origin: CGPoint(x: -shadowBlur, y: -shadowBlur), size: CGSize(width: contentWidth + shadowBlur * 2.0, height: backgroundHeight + shadowBlur * 2.0))
|
||||||
|
if constrainedSize.width > 500.0 {
|
||||||
|
backgroundFrame = backgroundFrame.offsetBy(dx: constrainedSize.width - contentWidth - 44.0, dy: startingPoint.y - backgroundHeight - 12.0)
|
||||||
|
} else {
|
||||||
|
backgroundFrame = backgroundFrame.offsetBy(dx: floor((constrainedSize.width - contentWidth) / 2.0), dy: startingPoint.y - backgroundHeight - 12.0)
|
||||||
|
}
|
||||||
|
backgroundFrame.origin.x = max(0.0, backgroundFrame.minX)
|
||||||
|
backgroundFrame.origin.x = min(constrainedSize.width - backgroundFrame.width, backgroundFrame.minX)
|
||||||
|
|
||||||
|
let anchorMinX = backgroundFrame.minX + shadowBlur + backgroundHeight / 2.0
|
||||||
|
let anchorMaxX = backgroundFrame.maxX - shadowBlur - backgroundHeight / 2.0
|
||||||
|
let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart))
|
||||||
|
|
||||||
|
var maximizedIndex = -1
|
||||||
|
/*if let reaction = self.reactions.last, case .reply = reaction {
|
||||||
|
maximizedIndex = self.reactions.count - 1
|
||||||
|
}*/
|
||||||
|
if backgroundFrame.insetBy(dx: -10.0, dy: -10.0).offsetBy(dx: 0.0, dy: 10.0).contains(touchPoint) {
|
||||||
|
maximizedIndex = Int(((touchPoint.x - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count))
|
||||||
|
maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
let interReactionSpacing: CGFloat
|
||||||
|
if maximizedIndex != -1 {
|
||||||
|
interReactionSpacing = minReactionSpacing
|
||||||
|
} else {
|
||||||
|
interReactionSpacing = reactionSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
if isInitial && self.reactionNodes.isEmpty {
|
||||||
|
self.shadowBlur = floor(minimizedReactionSize * 0.26)
|
||||||
|
self.smallCircleSize = 14.0
|
||||||
|
|
||||||
|
self.backgroundNode.image = generateBubbleImage(foreground: .white, diameter: backgroundHeight, shadowBlur: self.shadowBlur)
|
||||||
|
self.backgroundShadowNode.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: backgroundHeight, shadowBlur: self.shadowBlur)
|
||||||
|
for i in 0 ..< self.bubbleNodes.count {
|
||||||
|
self.bubbleNodes[i].0.image = generateBubbleImage(foreground: .white, diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur)
|
||||||
|
self.bubbleNodes[i].1.image = generateBubbleShadowImage(shadow: UIColor(white: 0.0, alpha: 0.2), diameter: CGFloat(i + 1) * self.smallCircleSize, shadowBlur: self.shadowBlur)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.reactionNodes = self.reactions.map { reaction -> ReactionNode in
|
||||||
|
return ReactionNode(account: self.account, theme: self.theme, reaction: reaction, maximizedReactionSize: maximizedReactionSize - 12.0, loadFirstFrame: true)
|
||||||
|
}
|
||||||
|
self.reactionNodes.forEach(self.addSubnode(_:))
|
||||||
|
}
|
||||||
|
|
||||||
|
let minimizedReactionVerticalInset: CGFloat = floor((backgroundHeight - minimizedReactionSize) / 2.0)
|
||||||
|
|
||||||
|
|
||||||
|
/*if maximizedIndex == -1 {
|
||||||
|
backgroundFrame.size.width -= maximizedReactionSize - minimizedReactionSize
|
||||||
|
backgroundFrame.origin.x += maximizedReactionSize - minimizedReactionSize
|
||||||
|
}*/
|
||||||
|
|
||||||
|
self.isRightAligned = isRightAligned
|
||||||
|
|
||||||
|
let backgroundTransition: ContainedViewLayoutTransition
|
||||||
|
if isInitial {
|
||||||
|
backgroundTransition = .immediate
|
||||||
|
} else {
|
||||||
|
backgroundTransition = .animated(duration: 0.18, curve: .easeInOut)
|
||||||
|
}
|
||||||
|
backgroundTransition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||||
|
backgroundTransition.updateFrame(node: self.backgroundShadowNode, frame: backgroundFrame)
|
||||||
|
|
||||||
|
var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSideInset
|
||||||
|
if maximizedIndex != -1 {
|
||||||
|
self.hasSelectedNode = false
|
||||||
|
} else {
|
||||||
|
self.hasSelectedNode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for iterationIndex in 0 ..< self.reactionNodes.count {
|
||||||
|
var i = iterationIndex
|
||||||
|
let isMaximized = i == maximizedIndex
|
||||||
|
if !isRightAligned {
|
||||||
|
i = self.reactionNodes.count - 1 - i
|
||||||
|
}
|
||||||
|
|
||||||
|
let reactionSize: CGFloat
|
||||||
|
if isMaximized {
|
||||||
|
reactionSize = maximizedReactionSize
|
||||||
|
} else {
|
||||||
|
reactionSize = minimizedReactionSize
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition: ContainedViewLayoutTransition
|
||||||
|
if isInitial {
|
||||||
|
transition = .immediate
|
||||||
|
} else {
|
||||||
|
transition = .animated(duration: 0.18, curve: .easeInOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.reactionNodes[i].isMaximized != isMaximized {
|
||||||
|
self.reactionNodes[i].isMaximized = isMaximized
|
||||||
|
self.reactionNodes[i].updateIsAnimating(isMaximized, animated: !isInitial)
|
||||||
|
if isMaximized && !isInitial {
|
||||||
|
self.hapticFeedback.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reactionFrame = CGRect(origin: CGPoint(x: reactionX, y: backgroundFrame.maxY - shadowBlur - minimizedReactionVerticalInset - reactionSize), size: CGSize(width: reactionSize, height: reactionSize))
|
||||||
|
if isMaximized {
|
||||||
|
reactionFrame.origin.x -= 7.0
|
||||||
|
reactionFrame.size.width += 14.0
|
||||||
|
}
|
||||||
|
self.reactionNodes[i].updateLayout(size: reactionFrame.size, scale: reactionFrame.size.width / (maximizedReactionSize + 14.0), transition: transition, displayText: isMaximized)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.reactionNodes[i], frame: reactionFrame, beginWithCurrentState: true)
|
||||||
|
|
||||||
|
reactionX += reactionSize + interReactionSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
let mainBubbleFrame = CGRect(origin: CGPoint(x: anchorX - self.smallCircleSize - shadowBlur, y: backgroundFrame.maxY - shadowBlur - self.smallCircleSize - shadowBlur), size: CGSize(width: self.smallCircleSize * 2.0 + shadowBlur * 2.0, height: self.smallCircleSize * 2.0 + shadowBlur * 2.0))
|
||||||
|
self.bubbleNodes[1].0.frame = mainBubbleFrame
|
||||||
|
self.bubbleNodes[1].1.frame = mainBubbleFrame
|
||||||
|
|
||||||
|
let secondaryBubbleFrame = CGRect(origin: CGPoint(x: mainBubbleFrame.midX - 10.0 - (self.smallCircleSize + shadowBlur * 2.0) / 2.0, y: mainBubbleFrame.midY + 10.0 - (self.smallCircleSize + shadowBlur * 2.0) / 2.0), size: CGSize(width: self.smallCircleSize + shadowBlur * 2.0, height: self.smallCircleSize + shadowBlur * 2.0))
|
||||||
|
self.bubbleNodes[0].0.frame = secondaryBubbleFrame
|
||||||
|
self.bubbleNodes[0].1.frame = secondaryBubbleFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateIn() {
|
||||||
|
self.bubbleNodes[1].0.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
||||||
|
self.bubbleNodes[1].1.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
||||||
|
|
||||||
|
self.bubbleNodes[0].0.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.05, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
||||||
|
self.bubbleNodes[0].1.layer.animateScale(from: 0.01, to: 1.0, duration: 0.11, delay: 0.05, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
||||||
|
|
||||||
|
let backgroundOffset: CGPoint
|
||||||
|
if self.isRightAligned {
|
||||||
|
backgroundOffset = CGPoint(x: (self.backgroundNode.frame.width - shadowBlur) / 2.0 - 42.0, y: 10.0)
|
||||||
|
} else {
|
||||||
|
backgroundOffset = CGPoint(x: -(self.backgroundNode.frame.width - shadowBlur) / 2.0 + 42.0, y: 10.0)
|
||||||
|
}
|
||||||
|
let damping: CGFloat = 100.0
|
||||||
|
|
||||||
|
for i in 0 ..< self.reactionNodes.count {
|
||||||
|
let animationOffset: Double = 1.0 - Double(i) / Double(self.reactionNodes.count - 1)
|
||||||
|
var nodeOffset: CGPoint
|
||||||
|
if self.isRightAligned {
|
||||||
|
nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.maxX - shadowBlur) / 2.0 - 42.0, y: 10.0)
|
||||||
|
} else {
|
||||||
|
nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.minX + shadowBlur) / 2.0 - 42.0, y: 10.0)
|
||||||
|
}
|
||||||
|
nodeOffset.x = 0.0
|
||||||
|
nodeOffset.y = 30.0
|
||||||
|
self.reactionNodes[i].layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.04, delay: animationOffset * 0.1)
|
||||||
|
self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, delay: animationOffset * 0.1, initialVelocity: 0.0, damping: damping)
|
||||||
|
//self.reactionNodes[i].layer.animateSpring(from: NSValue(cgPoint: nodeOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, delay: animationOffset * 0.1, initialVelocity: 0.0, damping: damping, additive: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.backgroundNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: damping)
|
||||||
|
self.backgroundNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true)
|
||||||
|
self.backgroundShadowNode.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 0.0, damping: damping)
|
||||||
|
self.backgroundShadowNode.layer.animateSpring(from: NSValue(cgPoint: backgroundOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateOut(into targetNode: ASDisplayNode?, hideTarget: Bool, completion: @escaping () -> Void) {
|
||||||
|
self.hapticFeedback.prepareTap()
|
||||||
|
|
||||||
|
var completedContainer = false
|
||||||
|
var completedTarget = true
|
||||||
|
|
||||||
|
let intermediateCompletion: () -> Void = {
|
||||||
|
if completedContainer && completedTarget {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let targetNode = targetNode {
|
||||||
|
for i in 0 ..< self.reactionNodes.count {
|
||||||
|
if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized {
|
||||||
|
targetNode.recursivelyEnsureDisplaySynchronously(true)
|
||||||
|
if let snapshotView = self.reactionNodes[i].view.snapshotContentTree(), let targetSnapshotView = targetNode.view.snapshotContentTree() {
|
||||||
|
targetSnapshotView.frame = self.view.convert(targetNode.bounds, from: targetNode.view)
|
||||||
|
self.reactionNodes[i].isHidden = true
|
||||||
|
self.view.addSubview(targetSnapshotView)
|
||||||
|
self.view.addSubview(snapshotView)
|
||||||
|
completedTarget = false
|
||||||
|
let targetPosition = self.view.convert(targetNode.bounds.center, from: targetNode.view)
|
||||||
|
let duration: Double = 0.3
|
||||||
|
if hideTarget {
|
||||||
|
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 hideTarget {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//self.backgroundNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||||
|
self.backgroundShadowNode.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||||
|
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||||
|
self.backgroundShadowNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||||
|
completedContainer = true
|
||||||
|
intermediateCompletion()
|
||||||
|
})
|
||||||
|
for (node, shadow) in self.bubbleNodes {
|
||||||
|
node.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||||
|
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||||
|
shadow.layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||||
|
shadow.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
for i in 0 ..< self.reactionNodes.count {
|
||||||
|
self.reactionNodes[i].layer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false)
|
||||||
|
self.reactionNodes[i].layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectedReaction() -> ReactionGestureItem? {
|
||||||
|
for i in 0 ..< self.reactionNodes.count {
|
||||||
|
if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized {
|
||||||
|
return self.reactionNodes[i].reaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
|||||||
|
/*import Foundation
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
|
||||||
|
public final class ReactionSelectionParentNode: ASDisplayNode {
|
||||||
|
private let account: Account
|
||||||
|
private let theme: PresentationTheme
|
||||||
|
|
||||||
|
private var currentNode: ReactionSelectionNode?
|
||||||
|
private var currentLocation: (CGPoint, CGFloat, CGPoint)?
|
||||||
|
|
||||||
|
private var validLayout: (size: CGSize, insets: UIEdgeInsets)?
|
||||||
|
|
||||||
|
public init(account: Account, theme: PresentationTheme) {
|
||||||
|
self.account = account
|
||||||
|
self.theme = theme
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func displayReactions(_ reactions: [ReactionGestureItem], at point: CGPoint, touchPoint: CGPoint) {
|
||||||
|
if let currentNode = self.currentNode {
|
||||||
|
currentNode.removeFromSupernode()
|
||||||
|
self.currentNode = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let reactionNode = ReactionSelectionNode(account: self.account, theme: self.theme, reactions: reactions)
|
||||||
|
self.addSubnode(reactionNode)
|
||||||
|
self.currentNode = reactionNode
|
||||||
|
self.currentLocation = (point, point.x, touchPoint)
|
||||||
|
|
||||||
|
if let (size, insets) = self.validLayout {
|
||||||
|
self.update(size: size, insets: insets, isInitial: true)
|
||||||
|
|
||||||
|
reactionNode.animateIn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectedReaction() -> ReactionGestureItem? {
|
||||||
|
if let currentNode = self.currentNode {
|
||||||
|
return currentNode.selectedReaction()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dismissReactions(into targetNode: ASDisplayNode?, hideTarget: Bool) {
|
||||||
|
if let currentNode = self.currentNode {
|
||||||
|
currentNode.animateOut(into: targetNode, hideTarget: hideTarget, completion: { [weak currentNode] in
|
||||||
|
currentNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
self.currentNode = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateReactionsAnchor(point: CGPoint, touchPoint: CGPoint) {
|
||||||
|
if let (currentPoint, _, _) = self.currentLocation {
|
||||||
|
self.currentLocation = (currentPoint, point.x, touchPoint)
|
||||||
|
|
||||||
|
if let (size, insets) = self.validLayout {
|
||||||
|
self.update(size: size, insets: insets, isInitial: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.validLayout = (size, insets)
|
||||||
|
|
||||||
|
self.update(size: size, insets: insets, isInitial: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func update(size: CGSize, insets: UIEdgeInsets, isInitial: Bool) {
|
||||||
|
if let currentNode = self.currentNode, let (point, offset, touchPoint) = self.currentLocation {
|
||||||
|
currentNode.updateLayout(constrainedSize: size, startingPoint: CGPoint(x: size.width - 32.0, y: point.y), offsetFromStart: offset, isInitial: isInitial, touchPoint: touchPoint)
|
||||||
|
currentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
@ -1045,6 +1045,7 @@ public class Account {
|
|||||||
self.managedOperationsDisposable.add(managedSynchronizeConsumeMessageContentOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
|
self.managedOperationsDisposable.add(managedSynchronizeConsumeMessageContentOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
|
||||||
self.managedOperationsDisposable.add(managedConsumePersonalMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
|
self.managedOperationsDisposable.add(managedConsumePersonalMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
|
||||||
self.managedOperationsDisposable.add(managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
|
self.managedOperationsDisposable.add(managedSynchronizeMarkAllUnseenPersonalMessagesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
|
||||||
|
self.managedOperationsDisposable.add(managedApplyPendingMessageReactionsActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
|
||||||
self.managedOperationsDisposable.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.postbox, network: self.network).start())
|
self.managedOperationsDisposable.add(managedSynchronizeEmojiKeywordsOperations(postbox: self.postbox, network: self.network).start())
|
||||||
self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
|
self.managedOperationsDisposable.add(managedApplyPendingScheduledMessagesActions(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start())
|
||||||
|
|
||||||
|
@ -258,6 +258,10 @@ struct AccountMutableState {
|
|||||||
self.addOperation(.UpdateMessagePoll(id, poll, results))
|
self.addOperation(.UpdateMessagePoll(id, poll, results))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*mutating func updateMessageReactions(_ messageId: MessageId, reactions: Api.MessageReactions) {
|
||||||
|
self.addOperation(.UpdateMessageReactions(messageId, reactions))
|
||||||
|
}*/
|
||||||
|
|
||||||
mutating func updateMedia(_ id: MediaId, media: Media?) {
|
mutating func updateMedia(_ id: MediaId, media: Media?) {
|
||||||
self.addOperation(.UpdateMedia(id, media))
|
self.addOperation(.UpdateMedia(id, media))
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,102 @@
|
|||||||
|
import Foundation
|
||||||
|
import Postbox
|
||||||
|
import TelegramApi
|
||||||
|
|
||||||
|
|
||||||
|
/*extension ReactionsMessageAttribute {
|
||||||
|
func withUpdatedResults(_ reactions: Api.MessageReactions) -> ReactionsMessageAttribute {
|
||||||
|
switch reactions {
|
||||||
|
case let .messageReactions(flags, results):
|
||||||
|
let min = (flags & (1 << 0)) != 0
|
||||||
|
var reactions = results.map { result -> MessageReaction in
|
||||||
|
switch result {
|
||||||
|
case let .reactionCount(flags, reaction, count):
|
||||||
|
return MessageReaction(value: reaction, count: count, isSelected: (flags & (1 << 0)) != 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if min {
|
||||||
|
var currentSelectedReaction: String?
|
||||||
|
for reaction in self.reactions {
|
||||||
|
if reaction.isSelected {
|
||||||
|
currentSelectedReaction = reaction.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let currentSelectedReaction = currentSelectedReaction {
|
||||||
|
for i in 0 ..< reactions.count {
|
||||||
|
if reactions[i].value == currentSelectedReaction {
|
||||||
|
reactions[i].isSelected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReactionsMessageAttribute(reactions: reactions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
public func mergedMessageReactions(attributes: [MessageAttribute]) -> ReactionsMessageAttribute? {
|
||||||
|
var current: ReactionsMessageAttribute?
|
||||||
|
var pending: PendingReactionsMessageAttribute?
|
||||||
|
for attribute in attributes {
|
||||||
|
if let attribute = attribute as? ReactionsMessageAttribute {
|
||||||
|
current = attribute
|
||||||
|
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
|
||||||
|
pending = attribute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let pending = pending {
|
||||||
|
var reactions = current?.reactions ?? []
|
||||||
|
if let value = pending.value {
|
||||||
|
var found = false
|
||||||
|
for i in 0 ..< reactions.count {
|
||||||
|
if reactions[i].value == value {
|
||||||
|
found = true
|
||||||
|
if !reactions[i].isSelected {
|
||||||
|
reactions[i].isSelected = true
|
||||||
|
reactions[i].count += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
reactions.append(MessageReaction(value: value, count: 1, isSelected: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i in (0 ..< reactions.count).reversed() {
|
||||||
|
if reactions[i].isSelected, pending.value != reactions[i].value {
|
||||||
|
if reactions[i].count == 1 {
|
||||||
|
reactions.remove(at: i)
|
||||||
|
} else {
|
||||||
|
reactions[i].isSelected = false
|
||||||
|
reactions[i].count -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !reactions.isEmpty {
|
||||||
|
return ReactionsMessageAttribute(reactions: reactions)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else if let current = current {
|
||||||
|
return current
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*extension ReactionsMessageAttribute {
|
||||||
|
convenience init(apiReactions: Api.MessageReactions) {
|
||||||
|
switch apiReactions {
|
||||||
|
case let .messageReactions(_, results):
|
||||||
|
self.init(reactions: results.map { result in
|
||||||
|
switch result {
|
||||||
|
case let .reactionCount(flags, reaction, count):
|
||||||
|
return MessageReaction(value: reaction, count: count, isSelected: (flags & (1 << 0)) != 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
@ -546,6 +546,10 @@ extension StoreMessage {
|
|||||||
attributes.append(ContentRequiresValidationMessageAttribute())
|
attributes.append(ContentRequiresValidationMessageAttribute())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*if let reactions = reactions {
|
||||||
|
attributes.append(ReactionsMessageAttribute(apiReactions: reactions))
|
||||||
|
}*/
|
||||||
|
|
||||||
if let replies = replies {
|
if let replies = replies {
|
||||||
let recentRepliersPeerIds: [PeerId]?
|
let recentRepliersPeerIds: [PeerId]?
|
||||||
switch replies {
|
switch replies {
|
||||||
|
@ -812,6 +812,89 @@ public final class AccountViewTracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func updateReactionsForMessageIds(messageIds: Set<MessageId>) {
|
||||||
|
/*self.queue.async {
|
||||||
|
var addedMessageIds: [MessageId] = []
|
||||||
|
let timestamp = Int32(CFAbsoluteTimeGetCurrent())
|
||||||
|
for messageId in messageIds {
|
||||||
|
let messageTimestamp = self.updatedReactionsMessageIdsAndTimestamps[messageId]
|
||||||
|
if messageTimestamp == nil || messageTimestamp! < timestamp - 5 * 60 {
|
||||||
|
self.updatedReactionsMessageIdsAndTimestamps[messageId] = timestamp
|
||||||
|
addedMessageIds.append(messageId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !addedMessageIds.isEmpty {
|
||||||
|
for (peerId, messageIds) in messagesIdsGroupedByPeerId(Set(addedMessageIds)) {
|
||||||
|
let disposableId = self.nextUpdatedReactionsDisposableId
|
||||||
|
self.nextUpdatedReactionsDisposableId += 1
|
||||||
|
|
||||||
|
if let account = self.account {
|
||||||
|
let signal = (account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
||||||
|
if let peer = transaction.getPeer(peerId), let inputPeer = apiInputPeer(peer) {
|
||||||
|
return account.network.request(Api.functions.messages.getMessagesReactions(peer: inputPeer, id: messageIds.map { $0.id }))
|
||||||
|
|> map(Optional.init)
|
||||||
|
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
|
||||||
|
return .single(nil)
|
||||||
|
}
|
||||||
|
|> mapToSignal { updates -> Signal<Void, NoError> in
|
||||||
|
guard let updates = updates else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
return account.postbox.transaction { transaction -> Void in
|
||||||
|
let updateList: [Api.Update]
|
||||||
|
switch updates {
|
||||||
|
case let .updates(updates, _, _, _, _):
|
||||||
|
updateList = updates
|
||||||
|
case let .updatesCombined(updates, _, _, _, _, _):
|
||||||
|
updateList = updates
|
||||||
|
case let .updateShort(update, _):
|
||||||
|
updateList = [update]
|
||||||
|
default:
|
||||||
|
updateList = []
|
||||||
|
}
|
||||||
|
for update in updateList {
|
||||||
|
switch update {
|
||||||
|
case let .updateMessageReactions(peer, msgId, reactions):
|
||||||
|
transaction.updateMessage(MessageId(peerId: peer.peerId, namespace: Namespaces.Message.Cloud, id: msgId), update: { currentMessage in
|
||||||
|
|
||||||
|
let updatedReactions = ReactionsMessageAttribute(apiReactions: reactions)
|
||||||
|
|
||||||
|
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
|
||||||
|
var attributes = currentMessage.attributes
|
||||||
|
loop: for j in 0 ..< attributes.count {
|
||||||
|
if let attribute = attributes[j] as? ReactionsMessageAttribute {
|
||||||
|
if updatedReactions.reactions == attribute.reactions {
|
||||||
|
return .skip
|
||||||
|
}
|
||||||
|
attributes[j] = updatedReactions
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|> switchToLatest)
|
||||||
|
|> afterDisposed { [weak self] in
|
||||||
|
self?.queue.async {
|
||||||
|
self?.updatedReactionsDisposables.set(nil, forKey: disposableId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.updatedReactionsDisposables.set(signal.start(), forKey: disposableId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
public func updateSeenLiveLocationForMessageIds(messageIds: Set<MessageId>) {
|
public func updateSeenLiveLocationForMessageIds(messageIds: Set<MessageId>) {
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
var addedMessageIds: [MessageId] = []
|
var addedMessageIds: [MessageId] = []
|
||||||
|
228
submodules/TelegramCore/Sources/State/MessageReactions.swift
Normal file
228
submodules/TelegramCore/Sources/State/MessageReactions.swift
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import Foundation
|
||||||
|
import Postbox
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramApi
|
||||||
|
import MtProtoKit
|
||||||
|
|
||||||
|
|
||||||
|
public func updateMessageReactionsInteractively(postbox: Postbox, messageId: MessageId, reaction: String?) -> Signal<Never, NoError> {
|
||||||
|
return postbox.transaction { transaction -> Void in
|
||||||
|
transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction())
|
||||||
|
transaction.updateMessage(messageId, update: { currentMessage in
|
||||||
|
var storeForwardInfo: StoreMessageForwardInfo?
|
||||||
|
if let forwardInfo = currentMessage.forwardInfo {
|
||||||
|
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||||
|
}
|
||||||
|
var attributes = currentMessage.attributes
|
||||||
|
loop: for j in 0 ..< attributes.count {
|
||||||
|
if let _ = attributes[j] as? PendingReactionsMessageAttribute {
|
||||||
|
attributes.remove(at: j)
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attributes.append(PendingReactionsMessageAttribute(value: reaction))
|
||||||
|
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|> ignoreValues
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum RequestUpdateMessageReactionError {
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestUpdateMessageReaction(postbox: Postbox, network: Network, stateManager: AccountStateManager, messageId: MessageId) -> Signal<Never, RequestUpdateMessageReactionError> {
|
||||||
|
return .complete()
|
||||||
|
/*return postbox.transaction { transaction -> (Peer, String?)? in
|
||||||
|
guard let peer = transaction.getPeer(messageId.peerId) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let message = transaction.getMessage(messageId) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var value: String?
|
||||||
|
for attribute in message.attributes {
|
||||||
|
if let attribute = attribute as? PendingReactionsMessageAttribute {
|
||||||
|
value = attribute.value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (peer, value)
|
||||||
|
}
|
||||||
|
|> castError(RequestUpdateMessageReactionError.self)
|
||||||
|
|> mapToSignal { peerAndValue in
|
||||||
|
guard let (peer, value) = peerAndValue else {
|
||||||
|
return .fail(.generic)
|
||||||
|
}
|
||||||
|
guard let inputPeer = apiInputPeer(peer) else {
|
||||||
|
return .fail(.generic)
|
||||||
|
}
|
||||||
|
if messageId.namespace != Namespaces.Message.Cloud {
|
||||||
|
return .fail(.generic)
|
||||||
|
}
|
||||||
|
return network.request(Api.functions.messages.sendReaction(flags: value == nil ? 0 : 1, peer: inputPeer, msgId: messageId.id, reaction: value))
|
||||||
|
|> mapError { _ -> RequestUpdateMessageReactionError in
|
||||||
|
return .generic
|
||||||
|
}
|
||||||
|
|> mapToSignal { result -> Signal<Never, RequestUpdateMessageReactionError> in
|
||||||
|
return postbox.transaction { transaction -> Void in
|
||||||
|
transaction.setPendingMessageAction(type: .updateReaction, id: messageId, action: UpdateMessageReactionsAction())
|
||||||
|
transaction.updateMessage(messageId, update: { currentMessage in
|
||||||
|
var storeForwardInfo: StoreMessageForwardInfo?
|
||||||
|
if let forwardInfo = currentMessage.forwardInfo {
|
||||||
|
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||||
|
}
|
||||||
|
let reactions = mergedMessageReactions(attributes: currentMessage.attributes)
|
||||||
|
var attributes = currentMessage.attributes
|
||||||
|
for j in (0 ..< attributes.count).reversed() {
|
||||||
|
if attributes[j] is PendingReactionsMessageAttribute || attributes[j] is ReactionsMessageAttribute {
|
||||||
|
attributes.remove(at: j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let reactions = reactions {
|
||||||
|
attributes.append(reactions)
|
||||||
|
}
|
||||||
|
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||||
|
})
|
||||||
|
stateManager.addUpdates(result)
|
||||||
|
}
|
||||||
|
|> castError(RequestUpdateMessageReactionError.self)
|
||||||
|
|> ignoreValues
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ManagedApplyPendingMessageReactionsActionsHelper {
|
||||||
|
var operationDisposables: [MessageId: Disposable] = [:]
|
||||||
|
|
||||||
|
func update(entries: [PendingMessageActionsEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) {
|
||||||
|
var disposeOperations: [Disposable] = []
|
||||||
|
var beginOperations: [(PendingMessageActionsEntry, MetaDisposable)] = []
|
||||||
|
|
||||||
|
var hasRunningOperationForPeerId = Set<PeerId>()
|
||||||
|
var validIds = Set<MessageId>()
|
||||||
|
for entry in entries {
|
||||||
|
if !hasRunningOperationForPeerId.contains(entry.id.peerId) {
|
||||||
|
hasRunningOperationForPeerId.insert(entry.id.peerId)
|
||||||
|
validIds.insert(entry.id)
|
||||||
|
|
||||||
|
if self.operationDisposables[entry.id] == nil {
|
||||||
|
let disposable = MetaDisposable()
|
||||||
|
beginOperations.append((entry, disposable))
|
||||||
|
self.operationDisposables[entry.id] = disposable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeMergedIds: [MessageId] = []
|
||||||
|
for (id, disposable) in self.operationDisposables {
|
||||||
|
if !validIds.contains(id) {
|
||||||
|
removeMergedIds.append(id)
|
||||||
|
disposeOperations.append(disposable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for id in removeMergedIds {
|
||||||
|
self.operationDisposables.removeValue(forKey: id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (disposeOperations, beginOperations)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reset() -> [Disposable] {
|
||||||
|
let disposables = Array(self.operationDisposables.values)
|
||||||
|
self.operationDisposables.removeAll()
|
||||||
|
return disposables
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func withTakenAction(postbox: Postbox, type: PendingMessageActionType, id: MessageId, _ f: @escaping (Transaction, PendingMessageActionsEntry?) -> Signal<Never, NoError>) -> Signal<Never, NoError> {
|
||||||
|
return postbox.transaction { transaction -> Signal<Never, NoError> in
|
||||||
|
var result: PendingMessageActionsEntry?
|
||||||
|
|
||||||
|
if let action = transaction.getPendingMessageAction(type: type, id: id) as? UpdateMessageReactionsAction {
|
||||||
|
result = PendingMessageActionsEntry(id: id, action: action)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f(transaction, result)
|
||||||
|
}
|
||||||
|
|> switchToLatest
|
||||||
|
}
|
||||||
|
|
||||||
|
func managedApplyPendingMessageReactionsActions(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal<Void, NoError> {
|
||||||
|
return Signal { _ in
|
||||||
|
let helper = Atomic<ManagedApplyPendingMessageReactionsActionsHelper>(value: ManagedApplyPendingMessageReactionsActionsHelper())
|
||||||
|
|
||||||
|
let actionsKey = PostboxViewKey.pendingMessageActions(type: .updateReaction)
|
||||||
|
let disposable = postbox.combinedView(keys: [actionsKey]).start(next: { view in
|
||||||
|
var entries: [PendingMessageActionsEntry] = []
|
||||||
|
if let v = view.views[actionsKey] as? PendingMessageActionsView {
|
||||||
|
entries = v.entries
|
||||||
|
}
|
||||||
|
|
||||||
|
let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PendingMessageActionsEntry, MetaDisposable)]) in
|
||||||
|
return helper.update(entries: entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
for disposable in disposeOperations {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entry, disposable) in beginOperations {
|
||||||
|
let signal = withTakenAction(postbox: postbox, type: .updateReaction, id: entry.id, { transaction, entry -> Signal<Never, NoError> in
|
||||||
|
if let entry = entry {
|
||||||
|
if let _ = entry.action as? UpdateMessageReactionsAction {
|
||||||
|
return synchronizeMessageReactions(transaction: transaction, postbox: postbox, network: network, stateManager: stateManager, id: entry.id)
|
||||||
|
} else {
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .complete()
|
||||||
|
})
|
||||||
|
|> then(
|
||||||
|
postbox.transaction { transaction -> Void in
|
||||||
|
transaction.setPendingMessageAction(type: .updateReaction, id: entry.id, action: nil)
|
||||||
|
}
|
||||||
|
|> ignoreValues
|
||||||
|
)
|
||||||
|
|
||||||
|
disposable.set(signal.start())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return ActionDisposable {
|
||||||
|
let disposables = helper.with { helper -> [Disposable] in
|
||||||
|
return helper.reset()
|
||||||
|
}
|
||||||
|
for disposable in disposables {
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
disposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func synchronizeMessageReactions(transaction: Transaction, postbox: Postbox, network: Network, stateManager: AccountStateManager, id: MessageId) -> Signal<Never, NoError> {
|
||||||
|
return requestUpdateMessageReaction(postbox: postbox, network: network, stateManager: stateManager, messageId: id)
|
||||||
|
|> `catch` { _ -> Signal<Never, NoError> in
|
||||||
|
return postbox.transaction { transaction -> Void in
|
||||||
|
transaction.setPendingMessageAction(type: .updateReaction, id: id, action: nil)
|
||||||
|
transaction.updateMessage(id, update: { currentMessage in
|
||||||
|
var storeForwardInfo: StoreMessageForwardInfo?
|
||||||
|
if let forwardInfo = currentMessage.forwardInfo {
|
||||||
|
storeForwardInfo = StoreMessageForwardInfo(authorId: forwardInfo.author?.id, sourceId: forwardInfo.source?.id, sourceMessageId: forwardInfo.sourceMessageId, date: forwardInfo.date, authorSignature: forwardInfo.authorSignature, psaType: forwardInfo.psaType, flags: forwardInfo.flags)
|
||||||
|
}
|
||||||
|
var attributes = currentMessage.attributes
|
||||||
|
loop: for j in 0 ..< attributes.count {
|
||||||
|
if let _ = attributes[j] as? PendingReactionsMessageAttribute {
|
||||||
|
attributes.remove(at: j)
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|> ignoreValues
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,42 @@
|
|||||||
import Postbox
|
import Postbox
|
||||||
|
|
||||||
public final class ReactionsMessageAttribute: MessageAttribute {
|
public struct MessageReaction: Equatable, PostboxCoding {
|
||||||
public init() {
|
public var value: String
|
||||||
|
public var count: Int32
|
||||||
|
public var isSelected: Bool
|
||||||
|
|
||||||
|
public init(value: String, count: Int32, isSelected: Bool) {
|
||||||
|
self.value = value
|
||||||
|
self.count = count
|
||||||
|
self.isSelected = isSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init(decoder: PostboxDecoder) {
|
public init(decoder: PostboxDecoder) {
|
||||||
|
self.value = decoder.decodeStringForKey("v", orElse: "")
|
||||||
|
self.count = decoder.decodeInt32ForKey("c", orElse: 0)
|
||||||
|
self.isSelected = decoder.decodeInt32ForKey("s", orElse: 0) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(_ encoder: PostboxEncoder) {
|
public func encode(_ encoder: PostboxEncoder) {
|
||||||
|
encoder.encodeString(self.value, forKey: "v")
|
||||||
|
encoder.encodeInt32(self.count, forKey: "c")
|
||||||
|
encoder.encodeInt32(self.isSelected ? 1 : 0, forKey: "s")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class ReactionsMessageAttribute: MessageAttribute {
|
||||||
|
public let reactions: [MessageReaction]
|
||||||
|
|
||||||
|
public init(reactions: [MessageReaction]) {
|
||||||
|
self.reactions = reactions
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(decoder: PostboxDecoder) {
|
||||||
|
self.reactions = decoder.decodeObjectArrayWithDecoderForKey("r")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(_ encoder: PostboxEncoder) {
|
||||||
|
encoder.encodeObjectArray(self.reactions, forKey: "r")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,7 +517,7 @@ public extension TelegramEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var results: [(EnginePeer, Int32)] = []
|
var results: [(EnginePeer, PeerGroupId, ChatListIndex)] = []
|
||||||
|
|
||||||
for listId in peerIds {
|
for listId in peerIds {
|
||||||
guard let peer = transaction.getPeer(listId) else {
|
guard let peer = transaction.getPeer(listId) else {
|
||||||
@ -532,14 +532,14 @@ public extension TelegramEngine {
|
|||||||
guard let readState = transaction.getCombinedPeerReadState(channel.id), readState.count != 0 else {
|
guard let readState = transaction.getCombinedPeerReadState(channel.id), readState.count != 0 else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
guard let topMessageIndex = transaction.getTopPeerMessageIndex(peerId: channel.id) else {
|
guard let (groupId, index) = transaction.getPeerChatListIndex(channel.id) else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
results.append((EnginePeer(channel), topMessageIndex.timestamp))
|
results.append((EnginePeer(channel), groupId, index))
|
||||||
}
|
}
|
||||||
|
|
||||||
results.sort(by: { $0.1 > $1.1 })
|
results.sort(by: { $0.2 > $1.2 })
|
||||||
|
|
||||||
if let peer = results.first?.0 {
|
if let peer = results.first?.0 {
|
||||||
let unreadCount: Int32 = transaction.getCombinedPeerReadState(peer.id)?.count ?? 0
|
let unreadCount: Int32 = transaction.getCombinedPeerReadState(peer.id)?.count ?? 0
|
||||||
|
@ -64,6 +64,8 @@ import UniversalMediaPlayer
|
|||||||
import WallpaperBackgroundNode
|
import WallpaperBackgroundNode
|
||||||
import ChatListUI
|
import ChatListUI
|
||||||
import CalendarMessageScreen
|
import CalendarMessageScreen
|
||||||
|
import ReactionSelectionNode
|
||||||
|
import LottieMeshSwift
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
import os.signpost
|
import os.signpost
|
||||||
@ -964,9 +966,103 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
actions.tip = tip
|
actions.tip = tip
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actions.context = strongSelf.context
|
||||||
|
|
||||||
|
var hasLike = false
|
||||||
|
let hearts: [String] = ["❤", "❤️"]
|
||||||
|
for attribute in messages[0].attributes {
|
||||||
|
if let attribute = attribute as? ReactionsMessageAttribute {
|
||||||
|
for reaction in attribute.reactions {
|
||||||
|
if hearts.contains(reaction.value) {
|
||||||
|
if reaction.isSelected {
|
||||||
|
hasLike = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
|
||||||
|
if let value = attribute.value, hearts.contains(value) {
|
||||||
|
hasLike = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.reactionItems = [ReactionContextItem(reaction: hasLike ? .unlike : .like)]
|
||||||
|
|
||||||
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), recognizer: recognizer, gesture: gesture)
|
let controller = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: .extracted(ChatMessageContextExtractedContentSource(chatNode: strongSelf.chatDisplayNode, postbox: strongSelf.context.account.postbox, message: message, selectAll: selectAll)), items: .single(actions), recognizer: recognizer, gesture: gesture)
|
||||||
strongSelf.currentContextController = controller
|
strongSelf.currentContextController = controller
|
||||||
|
|
||||||
|
let _ = strongSelf.context.meshAnimationCache.get(bundleName: "Hearts")
|
||||||
|
|
||||||
|
controller.reactionSelected = { [weak controller] value in
|
||||||
|
guard let strongSelf = self, let message = updatedMessages.first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let hearts: [String] = ["❤", "❤️"]
|
||||||
|
strongSelf.chatDisplayNode.historyNode.forEachItemNode { itemNode in
|
||||||
|
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item {
|
||||||
|
if item.message.id == message.id {
|
||||||
|
switch value {
|
||||||
|
case .like:
|
||||||
|
itemNode.awaitingAppliedReaction = (hearts[0], { [weak itemNode] in
|
||||||
|
guard let controller = controller else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let itemNode = itemNode, let (targetEmptyNode, targetFilledNode) = itemNode.targetReactionNode(value: hearts[0]) {
|
||||||
|
controller.dismissWithReaction(value: hearts[0], targetEmptyNode: targetEmptyNode, targetFilledNode: targetFilledNode, hideNode: true, completion: { [weak itemNode, weak targetFilledNode] in
|
||||||
|
guard let strongSelf = self, let itemNode = itemNode, let targetFilledNode = targetFilledNode else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetFrame = targetFilledNode.view.convert(targetFilledNode.bounds, to: itemNode.view).offsetBy(dx: 0.0, dy: itemNode.insets.top)
|
||||||
|
|
||||||
|
if #available(iOS 13.0, *), let meshAnimation = strongSelf.context.meshAnimationCache.get(bundleName: "Hearts") {
|
||||||
|
if let animationView = MeshRenderer() {
|
||||||
|
let animationFrame = CGRect(origin: CGPoint(x: targetFrame.midX - 200.0 / 2.0, y: targetFrame.midY - 200.0 / 2.0), size: CGSize(width: 200.0, height: 200.0)).offsetBy(dx: -50.0, dy: 0.0)
|
||||||
|
animationView.frame = animationFrame
|
||||||
|
|
||||||
|
var removeNode: (() -> Void)?
|
||||||
|
|
||||||
|
animationView.allAnimationsCompleted = {
|
||||||
|
removeNode?()
|
||||||
|
}
|
||||||
|
|
||||||
|
let overlayMeshAnimationNode = strongSelf.chatDisplayNode.messageTransitionNode.add(decorationView: animationView, itemNode: itemNode)
|
||||||
|
|
||||||
|
removeNode = { [weak overlayMeshAnimationNode] in
|
||||||
|
guard let strongSelf = self, let overlayMeshAnimationNode = overlayMeshAnimationNode else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.chatDisplayNode.messageTransitionNode.remove(decorationNode: overlayMeshAnimationNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
animationView.add(mesh: meshAnimation, offset: CGPoint())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
controller.dismiss()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
case .unlike:
|
||||||
|
itemNode.awaitingAppliedReaction = (nil, {
|
||||||
|
guard let controller = controller else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
controller.dismiss()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch value {
|
||||||
|
case .like:
|
||||||
|
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: hearts[0]).start()
|
||||||
|
case .unlike:
|
||||||
|
let _ = updateMessageReactionsInteractively(postbox: strongSelf.context.account.postbox, messageId: message.id, reaction: nil).start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
strongSelf.forEachController({ controller in
|
strongSelf.forEachController({ controller in
|
||||||
if let controller = controller as? TooltipScreen {
|
if let controller = controller as? TooltipScreen {
|
||||||
controller.dismiss()
|
controller.dismiss()
|
||||||
|
@ -865,6 +865,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
var edited = false
|
var edited = false
|
||||||
var viewCount: Int? = nil
|
var viewCount: Int? = nil
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let _ = attribute as? EditedMessageAttribute, isEmoji {
|
if let _ = attribute as? EditedMessageAttribute, isEmoji {
|
||||||
edited = true
|
edited = true
|
||||||
@ -884,7 +885,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
isReplyThread = true
|
isReplyThread = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
||||||
|
|
||||||
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
||||||
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
||||||
|
@ -322,6 +322,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
var viewCount: Int?
|
var viewCount: Int?
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in message.attributes {
|
for attribute in message.attributes {
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
if let attribute = attribute as? EditedMessageAttribute {
|
||||||
edited = !attribute.isHidden
|
edited = !attribute.isHidden
|
||||||
@ -497,6 +498,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
type: statusType,
|
type: statusType,
|
||||||
edited: edited,
|
edited: edited,
|
||||||
viewCount: viewCount,
|
viewCount: viewCount,
|
||||||
|
dateReactions: dateReactions,
|
||||||
dateReplies: dateReplies,
|
dateReplies: dateReplies,
|
||||||
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread,
|
||||||
dateText: dateText
|
dateText: dateText
|
||||||
@ -617,7 +619,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
|
|||||||
let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom)
|
let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom)
|
||||||
|
|
||||||
if let textStatusType = textStatusType {
|
if let textStatusType = textStatusType {
|
||||||
statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, textStatusType, textConstrainedSize, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring)
|
statusSizeAndApply = statusLayout(context, presentationData, edited, viewCount, dateText, textStatusType, textConstrainedSize, dateReactions, dateReplies, message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring)
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge?
|
var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge?
|
||||||
|
@ -210,6 +210,10 @@ class ChatMessageBubbleContentNode: ASDisplayNode {
|
|||||||
func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
|
func applyAbsoluteOffsetSpring(value: CGFloat, duration: Double, damping: CGFloat) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getStatusNode() -> ASDisplayNode? {
|
func getStatusNode() -> ASDisplayNode? {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -909,7 +909,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode),
|
forwardInfoLayout: (ChatPresentationData, PresentationStrings, ChatMessageForwardInfoType, Peer?, String?, String?, CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode),
|
||||||
replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode),
|
replyInfoLayout: (ChatPresentationData, PresentationStrings, AccountContext, ChatMessageReplyInfoType, Message, CGSize) -> (CGSize, () -> ChatMessageReplyInfoNode),
|
||||||
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)),
|
actionButtonsLayout: (AccountContext, ChatPresentationThemeData, PresentationChatBubbleCorners, PresentationStrings, ReplyMarkupMessageAttribute, Message, CGFloat) -> (minWidth: CGFloat, layout: (CGFloat) -> (CGSize, (Bool) -> ChatMessageActionButtonsNode)),
|
||||||
mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, Int, Bool, Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode),
|
mosaicStatusLayout: (AccountContext, ChatPresentationData, Bool, Int?, String, ChatMessageDateAndStatusType, CGSize, [MessageReaction], Int, Bool, Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode),
|
||||||
layoutConstants: ChatMessageItemLayoutConstants,
|
layoutConstants: ChatMessageItemLayoutConstants,
|
||||||
currentItem: ChatMessageItem?,
|
currentItem: ChatMessageItem?,
|
||||||
currentForwardInfo: (Peer?, String?)?,
|
currentForwardInfo: (Peer?, String?)?,
|
||||||
@ -1470,6 +1470,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
}
|
}
|
||||||
var viewCount: Int?
|
var viewCount: Int?
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in message.attributes {
|
for attribute in message.attributes {
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
if let attribute = attribute as? EditedMessageAttribute {
|
||||||
edited = !attribute.isHidden
|
edited = !attribute.isHidden
|
||||||
@ -1508,7 +1509,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
isReplyThread = true
|
isReplyThread = true
|
||||||
}
|
}
|
||||||
|
|
||||||
mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReplies, message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring)
|
mosaicStatusSizeAndApply = mosaicStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: 200.0, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, message.isSelfExpiring)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2682,6 +2683,46 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.updateSearchTextHighlightState()
|
strongSelf.updateSearchTextHighlightState()
|
||||||
|
|
||||||
|
if let (_, f) = strongSelf.awaitingAppliedReaction {
|
||||||
|
/*var bounds = strongSelf.bounds
|
||||||
|
let offset = bounds.origin.x
|
||||||
|
bounds.origin.x = 0.0
|
||||||
|
strongSelf.bounds = bounds
|
||||||
|
var shadowBounds = strongSelf.shadowNode.bounds
|
||||||
|
let shadowOffset = shadowBounds.origin.x
|
||||||
|
shadowBounds.origin.x = 0.0
|
||||||
|
strongSelf.shadowNode.bounds = shadowBounds
|
||||||
|
if !offset.isZero {
|
||||||
|
strongSelf.layer.animateBoundsOriginXAdditive(from: offset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
}
|
||||||
|
if !shadowOffset.isZero {
|
||||||
|
strongSelf.shadowNode.layer.animateBoundsOriginXAdditive(from: shadowOffset, to: 0.0, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring)
|
||||||
|
}
|
||||||
|
if let swipeToReplyNode = strongSelf.swipeToReplyNode {
|
||||||
|
strongSelf.swipeToReplyNode = nil
|
||||||
|
swipeToReplyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak swipeToReplyNode] _ in
|
||||||
|
swipeToReplyNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
swipeToReplyNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
strongSelf.awaitingAppliedReaction = nil
|
||||||
|
/*var targetNode: ASDisplayNode?
|
||||||
|
var hideTarget = false
|
||||||
|
if let awaitingAppliedReaction = awaitingAppliedReaction {
|
||||||
|
for contentNode in strongSelf.contentNodes {
|
||||||
|
if let (reactionNode, count) = contentNode.reactionTargetNode(value: awaitingAppliedReaction) {
|
||||||
|
targetNode = reactionNode
|
||||||
|
hideTarget = count == 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strongSelf.reactionRecognizer?.complete(into: targetNode, hideTarget: hideTarget)*/
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
|
override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
|
||||||
@ -3613,6 +3654,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
|||||||
return self.mainContextSourceNode.isExtractedToContextPreview || hasWallpaper || isPreview
|
return self.mainContextSourceNode.isExtractedToContextPreview || hasWallpaper || isPreview
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func targetReactionNode(value: String) -> (ASDisplayNode, ASDisplayNode)? {
|
||||||
|
for contentNode in self.contentNodes {
|
||||||
|
if let (emptyNode, filledNode) = contentNode.reactionTargetNode(value: value) {
|
||||||
|
return (emptyNode, filledNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func animateQuizInvalidOptionSelected() {
|
func animateQuizInvalidOptionSelected() {
|
||||||
if let supernode = self.supernode, let subnodes = supernode.subnodes {
|
if let supernode = self.supernode, let subnodes = supernode.subnodes {
|
||||||
for i in 0 ..< subnodes.count {
|
for i in 0 ..< subnodes.count {
|
||||||
|
@ -154,6 +154,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
var viewCount: Int?
|
var viewCount: Int?
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
if let attribute = attribute as? EditedMessageAttribute {
|
||||||
edited = !attribute.isHidden
|
edited = !attribute.isHidden
|
||||||
@ -195,7 +196,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
isReplyThread = true
|
isReplyThread = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,114 @@ enum ChatMessageDateAndStatusType: Equatable {
|
|||||||
case FreeOutgoing(ChatMessageDateAndStatusOutgoingType)
|
case FreeOutgoing(ChatMessageDateAndStatusOutgoingType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let reactionCountFont = Font.semibold(11.0)
|
||||||
|
private let reactionFont = Font.regular(12.0)
|
||||||
|
|
||||||
|
private final class StatusReactionNode: ASDisplayNode {
|
||||||
|
let emptyImageNode: ASImageNode
|
||||||
|
let selectedImageNode: ASImageNode
|
||||||
|
|
||||||
|
private var theme: PresentationTheme?
|
||||||
|
private var isSelected: Bool?
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
self.emptyImageNode = ASImageNode()
|
||||||
|
self.emptyImageNode.displaysAsynchronously = false
|
||||||
|
self.selectedImageNode = ASImageNode()
|
||||||
|
self.selectedImageNode.displaysAsynchronously = false
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.emptyImageNode)
|
||||||
|
self.addSubnode(self.selectedImageNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(type: ChatMessageDateAndStatusType, isSelected: Bool, count: Int, theme: PresentationTheme, wallpaper: TelegramWallpaper, animated: Bool) {
|
||||||
|
if self.theme !== theme {
|
||||||
|
self.theme = theme
|
||||||
|
|
||||||
|
let emptyImage: UIImage?
|
||||||
|
let selectedImage: UIImage?
|
||||||
|
switch type {
|
||||||
|
case .BubbleIncoming:
|
||||||
|
emptyImage = PresentationResourcesChat.chatMessageLike(theme, incoming: true, isSelected: false)
|
||||||
|
selectedImage = PresentationResourcesChat.chatMessageLike(theme, incoming: true, isSelected: true)
|
||||||
|
case .BubbleOutgoing:
|
||||||
|
emptyImage = PresentationResourcesChat.chatMessageLike(theme, incoming: false, isSelected: false)
|
||||||
|
selectedImage = PresentationResourcesChat.chatMessageLike(theme, incoming: false, isSelected: true)
|
||||||
|
case .ImageIncoming, .ImageOutgoing:
|
||||||
|
emptyImage = PresentationResourcesChat.chatMessageMediaLike(theme, isSelected: false)
|
||||||
|
selectedImage = PresentationResourcesChat.chatMessageMediaLike(theme, isSelected: true)
|
||||||
|
case .FreeIncoming, .FreeOutgoing:
|
||||||
|
emptyImage = PresentationResourcesChat.chatMessageFreeLike(theme, wallpaper: wallpaper, isSelected: false)
|
||||||
|
selectedImage = PresentationResourcesChat.chatMessageFreeLike(theme, wallpaper: wallpaper, isSelected: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let emptyImage = emptyImage, let selectedImage = selectedImage {
|
||||||
|
self.emptyImageNode.image = emptyImage
|
||||||
|
self.emptyImageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: emptyImage.size)
|
||||||
|
|
||||||
|
self.selectedImageNode.image = selectedImage
|
||||||
|
self.selectedImageNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: selectedImage.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.isSelected != isSelected {
|
||||||
|
let wasSelected = self.isSelected
|
||||||
|
self.isSelected = isSelected
|
||||||
|
|
||||||
|
self.emptyImageNode.isHidden = isSelected && count <= 1
|
||||||
|
self.selectedImageNode.isHidden = !isSelected
|
||||||
|
|
||||||
|
if let wasSelected = wasSelected, wasSelected, !isSelected {
|
||||||
|
if let image = self.selectedImageNode.image {
|
||||||
|
let leftImage = generateImage(image.size, rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
UIGraphicsPushContext(context)
|
||||||
|
image.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
UIGraphicsPopContext()
|
||||||
|
context.clear(CGRect(origin: CGPoint(x: size.width / 2.0, y: 0.0), size: CGSize(width: size.width / 2.0, height: size.height)))
|
||||||
|
})
|
||||||
|
let rightImage = generateImage(image.size, rotatedContext: { size, context in
|
||||||
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
|
UIGraphicsPushContext(context)
|
||||||
|
image.draw(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
UIGraphicsPopContext()
|
||||||
|
context.clear(CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width / 2.0, height: size.height)))
|
||||||
|
})
|
||||||
|
if let leftImage = leftImage, let rightImage = rightImage {
|
||||||
|
let leftView = UIImageView()
|
||||||
|
leftView.image = leftImage
|
||||||
|
leftView.frame = self.selectedImageNode.frame
|
||||||
|
let rightView = UIImageView()
|
||||||
|
rightView.image = rightImage
|
||||||
|
rightView.frame = self.selectedImageNode.frame
|
||||||
|
self.view.addSubview(leftView)
|
||||||
|
self.view.addSubview(rightView)
|
||||||
|
|
||||||
|
let duration: Double = 0.3
|
||||||
|
|
||||||
|
leftView.layer.animateRotation(from: 0.0, to: -CGFloat.pi * 0.7, duration: duration, removeOnCompletion: false)
|
||||||
|
rightView.layer.animateRotation(from: 0.0, to: CGFloat.pi * 0.7, duration: duration, removeOnCompletion: false)
|
||||||
|
|
||||||
|
leftView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -6.0, y: 8.0), duration: duration, removeOnCompletion: false, additive: true)
|
||||||
|
rightView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 6.0, y: 8.0), duration: duration, removeOnCompletion: false, additive: true)
|
||||||
|
|
||||||
|
leftView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak leftView] _ in
|
||||||
|
leftView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
|
||||||
|
rightView.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { [weak rightView] _ in
|
||||||
|
rightView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ChatMessageDateAndStatusNode: ASDisplayNode {
|
class ChatMessageDateAndStatusNode: ASDisplayNode {
|
||||||
private var backgroundNode: ASImageNode?
|
private var backgroundNode: ASImageNode?
|
||||||
private var blurredBackgroundNode: NavigationBackgroundNode?
|
private var blurredBackgroundNode: NavigationBackgroundNode?
|
||||||
@ -49,6 +157,9 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
private var clockMinNode: ASImageNode?
|
private var clockMinNode: ASImageNode?
|
||||||
private let dateNode: TextNode
|
private let dateNode: TextNode
|
||||||
private var impressionIcon: ASImageNode?
|
private var impressionIcon: ASImageNode?
|
||||||
|
private var reactionNodes: [StatusReactionNode] = []
|
||||||
|
private var reactionCountNode: TextNode?
|
||||||
|
private var reactionButtonNode: HighlightTrackingButtonNode?
|
||||||
private var repliesIcon: ASImageNode?
|
private var repliesIcon: ASImageNode?
|
||||||
private var selfExpiringIcon: ASImageNode?
|
private var selfExpiringIcon: ASImageNode?
|
||||||
private var replyCountNode: TextNode?
|
private var replyCountNode: TextNode?
|
||||||
@ -91,7 +202,7 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ replies: Int, _ isPinned: Bool, _ hasAutoremove: Bool) -> (CGSize, (Bool) -> Void) {
|
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool, _ hasAutoremove: Bool) -> (CGSize, (Bool) -> Void) {
|
||||||
let dateLayout = TextNode.asyncLayout(self.dateNode)
|
let dateLayout = TextNode.asyncLayout(self.dateNode)
|
||||||
|
|
||||||
var checkReadNode = self.checkReadNode
|
var checkReadNode = self.checkReadNode
|
||||||
@ -107,10 +218,11 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
let currentTheme = self.theme
|
let currentTheme = self.theme
|
||||||
|
|
||||||
let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode)
|
let makeReplyCountLayout = TextNode.asyncLayout(self.replyCountNode)
|
||||||
|
let makeReactionCountLayout = TextNode.asyncLayout(self.reactionCountNode)
|
||||||
|
|
||||||
let previousLayoutSize = self.layoutSize
|
let previousLayoutSize = self.layoutSize
|
||||||
|
|
||||||
return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, replyCount, isPinned, hasAutoremove in
|
return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replyCount, isPinned, hasAutoremove in
|
||||||
let dateColor: UIColor
|
let dateColor: UIColor
|
||||||
var backgroundImage: UIImage?
|
var backgroundImage: UIImage?
|
||||||
var blurredBackgroundColor: (UIColor, Bool)?
|
var blurredBackgroundColor: (UIColor, Bool)?
|
||||||
@ -416,7 +528,33 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
|
|
||||||
var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
var replyCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||||
|
|
||||||
|
let reactionSize: CGFloat = 14.0
|
||||||
|
var reactionCountLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||||
|
let reactionSpacing: CGFloat = -4.0
|
||||||
|
let reactionTrailingSpacing: CGFloat = 4.0
|
||||||
|
|
||||||
var reactionInset: CGFloat = 0.0
|
var reactionInset: CGFloat = 0.0
|
||||||
|
if !reactions.isEmpty {
|
||||||
|
reactionInset = -1.0 + CGFloat(reactions.count) * reactionSize + CGFloat(reactions.count - 1) * reactionSpacing + reactionTrailingSpacing
|
||||||
|
|
||||||
|
var count = 0
|
||||||
|
for reaction in reactions {
|
||||||
|
count += Int(reaction.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
let countString: String
|
||||||
|
if count > 1000000 {
|
||||||
|
countString = "\(count / 1000000)M"
|
||||||
|
} else if count > 1000 {
|
||||||
|
countString = "\(count / 1000)K"
|
||||||
|
} else {
|
||||||
|
countString = "\(count)"
|
||||||
|
}
|
||||||
|
|
||||||
|
let layoutAndApply = makeReactionCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: countString, font: dateFont, textColor: dateColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: 100.0)))
|
||||||
|
reactionInset += max(10.0, layoutAndApply.0.size.width) + 2.0
|
||||||
|
reactionCountLayoutAndApply = layoutAndApply
|
||||||
|
}
|
||||||
|
|
||||||
if replyCount > 0 {
|
if replyCount > 0 {
|
||||||
let countString: String
|
let countString: String
|
||||||
@ -599,6 +737,80 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var reactionOffset: CGFloat = leftInset - reactionInset + backgroundInsets.left
|
var reactionOffset: CGFloat = leftInset - reactionInset + backgroundInsets.left
|
||||||
|
for i in 0 ..< reactions.count {
|
||||||
|
let node: StatusReactionNode
|
||||||
|
if strongSelf.reactionNodes.count > i {
|
||||||
|
node = strongSelf.reactionNodes[i]
|
||||||
|
} else {
|
||||||
|
node = StatusReactionNode()
|
||||||
|
if strongSelf.reactionNodes.count > i {
|
||||||
|
let previousNode = strongSelf.reactionNodes[i]
|
||||||
|
if animated {
|
||||||
|
previousNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousNode] _ in
|
||||||
|
previousNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
previousNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
strongSelf.reactionNodes[i] = node
|
||||||
|
} else {
|
||||||
|
strongSelf.reactionNodes.append(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node.update(type: type, isSelected: reactions[i].isSelected, count: Int(reactions[i].count), theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper, animated: false)
|
||||||
|
if node.supernode == nil {
|
||||||
|
strongSelf.addSubnode(node)
|
||||||
|
if animated {
|
||||||
|
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.frame = CGRect(origin: CGPoint(x: reactionOffset, y: backgroundInsets.top + offset + 1.0), size: CGSize(width: reactionSize, height: reactionSize))
|
||||||
|
reactionOffset += reactionSize + reactionSpacing
|
||||||
|
}
|
||||||
|
if !reactions.isEmpty {
|
||||||
|
reactionOffset += reactionTrailingSpacing
|
||||||
|
}
|
||||||
|
for _ in reactions.count ..< strongSelf.reactionNodes.count {
|
||||||
|
let node = strongSelf.reactionNodes.removeLast()
|
||||||
|
if animated {
|
||||||
|
if let previousLayoutSize = previousLayoutSize {
|
||||||
|
node.frame = node.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
|
||||||
|
}
|
||||||
|
node.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
|
||||||
|
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
|
||||||
|
node?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
node.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (layout, apply) = reactionCountLayoutAndApply {
|
||||||
|
let node = apply()
|
||||||
|
if strongSelf.reactionCountNode !== node {
|
||||||
|
strongSelf.reactionCountNode?.removeFromSupernode()
|
||||||
|
strongSelf.addSubnode(node)
|
||||||
|
strongSelf.reactionCountNode = node
|
||||||
|
if animated {
|
||||||
|
node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node.frame = CGRect(origin: CGPoint(x: reactionOffset + 1.0, y: backgroundInsets.top + 1.0 + offset), size: layout.size)
|
||||||
|
reactionOffset += 1.0 + layout.size.width
|
||||||
|
} else if let reactionCountNode = strongSelf.reactionCountNode {
|
||||||
|
strongSelf.reactionCountNode = nil
|
||||||
|
if animated {
|
||||||
|
if let previousLayoutSize = previousLayoutSize {
|
||||||
|
reactionCountNode.frame = reactionCountNode.frame.offsetBy(dx: layoutSize.width - previousLayoutSize.width, dy: 0.0)
|
||||||
|
}
|
||||||
|
reactionCountNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak reactionCountNode] _ in
|
||||||
|
reactionCountNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
reactionCountNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let currentRepliesIcon = currentRepliesIcon {
|
if let currentRepliesIcon = currentRepliesIcon {
|
||||||
currentRepliesIcon.displaysAsynchronously = !presentationData.isPreview
|
currentRepliesIcon.displaysAsynchronously = !presentationData.isPreview
|
||||||
@ -658,17 +870,17 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ replies: Int, _ isPinned: Bool, _ hasAutoremove: Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) {
|
static func asyncLayout(_ node: ChatMessageDateAndStatusNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ edited: Bool, _ impressionCount: Int?, _ dateText: String, _ type: ChatMessageDateAndStatusType, _ constrainedSize: CGSize, _ reactions: [MessageReaction], _ replies: Int, _ isPinned: Bool, _ hasAutoremove: Bool) -> (CGSize, (Bool) -> ChatMessageDateAndStatusNode) {
|
||||||
let currentLayout = node?.asyncLayout()
|
let currentLayout = node?.asyncLayout()
|
||||||
return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, replies, isPinned, hasAutoremove in
|
return { context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned, hasAutoremove in
|
||||||
let resultNode: ChatMessageDateAndStatusNode
|
let resultNode: ChatMessageDateAndStatusNode
|
||||||
let resultSizeAndApply: (CGSize, (Bool) -> Void)
|
let resultSizeAndApply: (CGSize, (Bool) -> Void)
|
||||||
if let node = node, let currentLayout = currentLayout {
|
if let node = node, let currentLayout = currentLayout {
|
||||||
resultNode = node
|
resultNode = node
|
||||||
resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, replies, isPinned, hasAutoremove)
|
resultSizeAndApply = currentLayout(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned, hasAutoremove)
|
||||||
} else {
|
} else {
|
||||||
resultNode = ChatMessageDateAndStatusNode()
|
resultNode = ChatMessageDateAndStatusNode()
|
||||||
resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, replies, isPinned, hasAutoremove)
|
resultSizeAndApply = resultNode.asyncLayout()(context, presentationData, edited, impressionCount, dateText, type, constrainedSize, reactions, replies, isPinned, hasAutoremove)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (resultSizeAndApply.0, { animated in
|
return (resultSizeAndApply.0, { animated in
|
||||||
@ -678,6 +890,13 @@ class ChatMessageDateAndStatusNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reactionNode(value: String) -> (ASDisplayNode, ASDisplayNode)? {
|
||||||
|
for node in self.reactionNodes {
|
||||||
|
return (node.emptyImageNode, node.selectedImageNode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
if self.pressed != nil {
|
if self.pressed != nil {
|
||||||
if self.bounds.contains(point) {
|
if self.bounds.contains(point) {
|
||||||
|
@ -305,6 +305,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
var viewCount: Int?
|
var viewCount: Int?
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in message.attributes {
|
for attribute in message.attributes {
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
if let attribute = attribute as? EditedMessageAttribute {
|
||||||
edited = !attribute.isHidden
|
edited = !attribute.isHidden
|
||||||
@ -322,7 +323,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
|
|||||||
|
|
||||||
let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings)
|
let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings)
|
||||||
|
|
||||||
let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReplies, isPinned && !associatedData.isInPinnedListMode, message.isSelfExpiring)
|
let (size, apply) = statusLayout(context, presentationData, edited, viewCount, dateText, statusType, constrainedSize, dateReactions, dateReplies, isPinned && !associatedData.isInPinnedListMode, message.isSelfExpiring)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -262,6 +262,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
let sentViaBot = false
|
let sentViaBot = false
|
||||||
var viewCount: Int? = nil
|
var viewCount: Int? = nil
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
if let attribute = attribute as? EditedMessageAttribute {
|
||||||
edited = !attribute.isHidden
|
edited = !attribute.isHidden
|
||||||
@ -288,7 +289,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
|||||||
isReplyThread = true
|
isReplyThread = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited && !sentViaBot, viewCount, dateText, statusType, CGSize(width: max(1.0, maxDateAndStatusWidth), height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
||||||
|
|
||||||
var displayVideoFrame = videoFrame
|
var displayVideoFrame = videoFrame
|
||||||
displayVideoFrame.size.width *= imageScale
|
displayVideoFrame.size.width *= imageScale
|
||||||
|
@ -68,6 +68,7 @@ struct ChatMessageDateAndStatus {
|
|||||||
var type: ChatMessageDateAndStatusType
|
var type: ChatMessageDateAndStatusType
|
||||||
var edited: Bool
|
var edited: Bool
|
||||||
var viewCount: Int?
|
var viewCount: Int?
|
||||||
|
var dateReactions: [MessageReaction]
|
||||||
var dateReplies: Int
|
var dateReplies: Int
|
||||||
var isPinned: Bool
|
var isPinned: Bool
|
||||||
var dateText: String
|
var dateText: String
|
||||||
@ -467,7 +468,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
|||||||
var statusApply: ((Bool) -> Void)?
|
var statusApply: ((Bool) -> Void)?
|
||||||
|
|
||||||
if let dateAndStatus = dateAndStatus {
|
if let dateAndStatus = dateAndStatus {
|
||||||
let (size, apply) = statusLayout(context, presentationData, dateAndStatus.edited, dateAndStatus.viewCount, dateAndStatus.dateText, dateAndStatus.type, CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateAndStatus.dateReplies, dateAndStatus.isPinned, message.isSelfExpiring)
|
let (size, apply) = statusLayout(context, presentationData, dateAndStatus.edited, dateAndStatus.viewCount, dateAndStatus.dateText, dateAndStatus.type, CGSize(width: nativeSize.width - 30.0, height: CGFloat.greatestFiniteMagnitude), dateAndStatus.dateReactions, dateAndStatus.dateReplies, dateAndStatus.isPinned, message.isSelfExpiring)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -183,6 +183,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
var viewCount: Int?
|
var viewCount: Int?
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
if let attribute = attribute as? EditedMessageAttribute {
|
||||||
edited = !attribute.isHidden
|
edited = !attribute.isHidden
|
||||||
@ -244,7 +245,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
isReplyThread = true
|
isReplyThread = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: constrainedSize.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -152,6 +152,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
var viewCount: Int?
|
var viewCount: Int?
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
if let attribute = attribute as? EditedMessageAttribute {
|
||||||
if case .mosaic = preparePosition {
|
if case .mosaic = preparePosition {
|
||||||
@ -199,6 +200,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
type: statusType,
|
type: statusType,
|
||||||
edited: edited,
|
edited: edited,
|
||||||
viewCount: viewCount,
|
viewCount: viewCount,
|
||||||
|
dateReactions: dateReactions,
|
||||||
dateReplies: dateReplies,
|
dateReplies: dateReplies,
|
||||||
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
|
isPinned: item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread,
|
||||||
dateText: dateText
|
dateText: dateText
|
||||||
|
@ -1024,6 +1024,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
var viewCount: Int?
|
var viewCount: Int?
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
if let attribute = attribute as? EditedMessageAttribute {
|
||||||
edited = !attribute.isHidden
|
edited = !attribute.isHidden
|
||||||
@ -1065,7 +1066,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
isReplyThread = true
|
isReplyThread = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -53,6 +53,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
var viewCount: Int?
|
var viewCount: Int?
|
||||||
var rawText = ""
|
var rawText = ""
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
if let attribute = attribute as? EditedMessageAttribute {
|
||||||
edited = !attribute.isHidden
|
edited = !attribute.isHidden
|
||||||
@ -96,7 +97,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
isReplyThread = true
|
isReplyThread = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, item.message.isSelfExpiring)
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && isReplyThread, item.message.isSelfExpiring)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
|
@ -459,6 +459,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
var edited = false
|
var edited = false
|
||||||
var viewCount: Int? = nil
|
var viewCount: Int? = nil
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
let dateReactions: [MessageReaction] = []
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let _ = attribute as? EditedMessageAttribute, isEmoji {
|
if let _ = attribute as? EditedMessageAttribute, isEmoji {
|
||||||
edited = true
|
edited = true
|
||||||
@ -478,7 +479,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
|||||||
isReplyThread = true
|
isReplyThread = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
let (dateAndStatusSize, dateAndStatusApply) = makeDateAndStatusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
||||||
|
|
||||||
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
var viaBotApply: (TextNodeLayout, () -> TextNode)?
|
||||||
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
var replyInfoApply: (CGSize, () -> ChatMessageReplyInfoNode)?
|
||||||
|
@ -107,6 +107,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
var viewCount: Int?
|
var viewCount: Int?
|
||||||
var dateReplies = 0
|
var dateReplies = 0
|
||||||
|
var dateReactions: [MessageReaction] = []
|
||||||
|
|
||||||
for attribute in item.message.attributes {
|
for attribute in item.message.attributes {
|
||||||
if let attribute = attribute as? EditedMessageAttribute {
|
if let attribute = attribute as? EditedMessageAttribute {
|
||||||
edited = !attribute.isHidden
|
edited = !attribute.isHidden
|
||||||
@ -116,6 +118,10 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
|
if let channel = item.message.peers[item.message.id.peerId] as? TelegramChannel, case .group = channel.info {
|
||||||
dateReplies = Int(attribute.count)
|
dateReplies = Int(attribute.count)
|
||||||
}
|
}
|
||||||
|
} else if let attribute = attribute as? PendingReactionsMessageAttribute {
|
||||||
|
if let value = attribute.value {
|
||||||
|
dateReactions = [MessageReaction(value: value, count: 1, isSelected: true)]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,7 +170,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
isReplyThread = true
|
isReplyThread = true
|
||||||
}
|
}
|
||||||
|
|
||||||
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
let (size, apply) = statusLayout(item.context, item.presentationData, edited, viewCount, dateText, statusType, textConstrainedSize, dateReactions, dateReplies, item.message.tags.contains(.pinned) && !item.associatedData.isInPinnedListMode && !isReplyThread, item.message.isSelfExpiring)
|
||||||
statusSize = size
|
statusSize = size
|
||||||
statusApply = apply
|
statusApply = apply
|
||||||
}
|
}
|
||||||
@ -616,6 +622,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func reactionTargetNode(value: String) -> (ASDisplayNode, ASDisplayNode)? {
|
||||||
|
if !self.statusNode.isHidden {
|
||||||
|
return self.statusNode.reactionNode(value: value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
override func getStatusNode() -> ASDisplayNode? {
|
override func getStatusNode() -> ASDisplayNode? {
|
||||||
return self.statusNode
|
return self.statusNode
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user