mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-16 03:09:56 +00:00
Another reactions experiment
This commit is contained in:
parent
d10f5c493b
commit
73eaba0960
@ -587,6 +587,17 @@
|
|||||||
<key>sourceTree</key>
|
<key>sourceTree</key>
|
||||||
<string>SOURCE_ROOT</string>
|
<string>SOURCE_ROOT</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>1DD70E29B0C2D20A00000000</key>
|
||||||
|
<dict>
|
||||||
|
<key>isa</key>
|
||||||
|
<string>PBXFileReference</string>
|
||||||
|
<key>name</key>
|
||||||
|
<string>MessageReactionListLoadingPlaceholder.swift</string>
|
||||||
|
<key>path</key>
|
||||||
|
<string>Sources/MessageReactionListLoadingPlaceholder.swift</string>
|
||||||
|
<key>sourceTree</key>
|
||||||
|
<string>SOURCE_ROOT</string>
|
||||||
|
</dict>
|
||||||
<key>B401C979EAB5339800000000</key>
|
<key>B401C979EAB5339800000000</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isa</key>
|
<key>isa</key>
|
||||||
@ -599,6 +610,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>1DD70E298DEA121500000000</string>
|
<string>1DD70E298DEA121500000000</string>
|
||||||
<string>1DD70E29C02D1D4F00000000</string>
|
<string>1DD70E29C02D1D4F00000000</string>
|
||||||
|
<string>1DD70E29B0C2D20A00000000</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<key>B401C979639FD30200000000</key>
|
<key>B401C979639FD30200000000</key>
|
||||||
@ -671,6 +683,13 @@
|
|||||||
<key>fileRef</key>
|
<key>fileRef</key>
|
||||||
<string>1DD70E29C02D1D4F00000000</string>
|
<string>1DD70E29C02D1D4F00000000</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>E7A30F04B0C2D20A00000000</key>
|
||||||
|
<dict>
|
||||||
|
<key>isa</key>
|
||||||
|
<string>PBXBuildFile</string>
|
||||||
|
<key>fileRef</key>
|
||||||
|
<string>1DD70E29B0C2D20A00000000</string>
|
||||||
|
</dict>
|
||||||
<key>1870857F0000000000000000</key>
|
<key>1870857F0000000000000000</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isa</key>
|
<key>isa</key>
|
||||||
@ -679,6 +698,7 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>E7A30F048DEA121500000000</string>
|
<string>E7A30F048DEA121500000000</string>
|
||||||
<string>E7A30F04C02D1D4F00000000</string>
|
<string>E7A30F04C02D1D4F00000000</string>
|
||||||
|
<string>E7A30F04B0C2D20A00000000</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<key>E7A30F04D65BA68200000000</key>
|
<key>E7A30F04D65BA68200000000</key>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import ItemListPeerItem
|
|||||||
public final class MessageReactionListController: ViewController {
|
public final class MessageReactionListController: ViewController {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let messageId: MessageId
|
private let messageId: MessageId
|
||||||
private let presentatonData: PresentationData
|
private let presentationData: PresentationData
|
||||||
private let initialReactions: [MessageReaction]
|
private let initialReactions: [MessageReaction]
|
||||||
|
|
||||||
private var controllerNode: MessageReactionListControllerNode {
|
private var controllerNode: MessageReactionListControllerNode {
|
||||||
@ -28,7 +28,7 @@ public final class MessageReactionListController: ViewController {
|
|||||||
public init(context: AccountContext, messageId: MessageId, initialReactions: [MessageReaction]) {
|
public init(context: AccountContext, messageId: MessageId, initialReactions: [MessageReaction]) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.messageId = messageId
|
self.messageId = messageId
|
||||||
self.presentatonData = context.sharedContext.currentPresentationData.with { $0 }
|
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||||
self.initialReactions = initialReactions
|
self.initialReactions = initialReactions
|
||||||
|
|
||||||
super.init(navigationBarPresentationData: nil)
|
super.init(navigationBarPresentationData: nil)
|
||||||
@ -41,7 +41,7 @@ public final class MessageReactionListController: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override public func loadDisplayNode() {
|
override public func loadDisplayNode() {
|
||||||
self.displayNode = MessageReactionListControllerNode(context: self.context, presentatonData: self.presentatonData, messageId: messageId, initialReactions: initialReactions, dismiss: { [weak self] in
|
self.displayNode = MessageReactionListControllerNode(context: self.context, presentationData: self.presentationData, messageId: messageId, initialReactions: initialReactions, dismiss: { [weak self] in
|
||||||
self?.dismiss()
|
self?.dismiss()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -113,14 +113,14 @@ private let itemHeight: CGFloat = 50.0
|
|||||||
|
|
||||||
private func topInsetForLayout(layout: ContainerViewLayout, itemCount: Int) -> CGFloat {
|
private func topInsetForLayout(layout: ContainerViewLayout, itemCount: Int) -> CGFloat {
|
||||||
let contentHeight = CGFloat(itemCount) * itemHeight
|
let contentHeight = CGFloat(itemCount) * itemHeight
|
||||||
let minimumItemHeights: CGFloat = contentHeight
|
let minimumItemHeights: CGFloat = max(contentHeight, itemHeight * 5.0)
|
||||||
|
|
||||||
return max(layout.size.height - layout.intrinsicInsets.bottom - minimumItemHeights, headerHeight)
|
return max(layout.size.height - layout.intrinsicInsets.bottom - minimumItemHeights, headerHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class MessageReactionListControllerNode: ViewControllerTracingNode {
|
private final class MessageReactionListControllerNode: ViewControllerTracingNode {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let presentatonData: PresentationData
|
private let presentationData: PresentationData
|
||||||
private let dismiss: () -> Void
|
private let dismiss: () -> Void
|
||||||
|
|
||||||
private let listContext: MessageReactionListContext
|
private let listContext: MessageReactionListContext
|
||||||
@ -129,9 +129,12 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
private let backgroundNode: ASDisplayNode
|
private let backgroundNode: ASDisplayNode
|
||||||
private let contentHeaderContainerNode: ASDisplayNode
|
private let contentHeaderContainerNode: ASDisplayNode
|
||||||
private let contentHeaderContainerBackgroundNode: ASImageNode
|
private let contentHeaderContainerBackgroundNode: ASImageNode
|
||||||
|
private let contentHeaderContainerSeparatorNode: ASDisplayNode
|
||||||
private var categoryItemNodes: [MessageReactionCategoryNode] = []
|
private var categoryItemNodes: [MessageReactionCategoryNode] = []
|
||||||
private let categoryScrollNode: ASScrollNode
|
private let categoryScrollNode: ASScrollNode
|
||||||
private let listNode: ListView
|
private let listNode: ListView
|
||||||
|
private var placeholderNode: MessageReactionListLoadingPlaceholder?
|
||||||
|
private var placeholderNodeIsAnimatingOut = false
|
||||||
|
|
||||||
private var validLayout: ContainerViewLayout?
|
private var validLayout: ContainerViewLayout?
|
||||||
|
|
||||||
@ -146,26 +149,29 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
|
|
||||||
private var forceHeaderTransition: ContainedViewLayoutTransition?
|
private var forceHeaderTransition: ContainedViewLayoutTransition?
|
||||||
|
|
||||||
init(context: AccountContext, presentatonData: PresentationData, messageId: MessageId, initialReactions: [MessageReaction], dismiss: @escaping () -> Void) {
|
init(context: AccountContext, presentationData: PresentationData, messageId: MessageId, initialReactions: [MessageReaction], dismiss: @escaping () -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.presentatonData = presentatonData
|
self.presentationData = presentationData
|
||||||
self.dismiss = dismiss
|
self.dismiss = dismiss
|
||||||
|
|
||||||
self.dimNode = ASDisplayNode()
|
self.dimNode = ASDisplayNode()
|
||||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||||
|
|
||||||
self.backgroundNode = ASDisplayNode()
|
self.backgroundNode = ASDisplayNode()
|
||||||
self.backgroundNode.backgroundColor = self.presentatonData.theme.actionSheet.opaqueItemBackgroundColor
|
self.backgroundNode.backgroundColor = self.presentationData.theme.actionSheet.opaqueItemBackgroundColor
|
||||||
|
|
||||||
self.contentHeaderContainerNode = ASDisplayNode()
|
self.contentHeaderContainerNode = ASDisplayNode()
|
||||||
self.contentHeaderContainerBackgroundNode = ASImageNode()
|
self.contentHeaderContainerBackgroundNode = ASImageNode()
|
||||||
self.contentHeaderContainerBackgroundNode.displaysAsynchronously = false
|
self.contentHeaderContainerBackgroundNode.displaysAsynchronously = false
|
||||||
|
|
||||||
|
self.contentHeaderContainerSeparatorNode = ASDisplayNode()
|
||||||
|
self.contentHeaderContainerSeparatorNode.backgroundColor = self.presentationData.theme.list.itemPlainSeparatorColor
|
||||||
|
|
||||||
self.categoryScrollNode = ASScrollNode()
|
self.categoryScrollNode = ASScrollNode()
|
||||||
self.contentHeaderContainerBackgroundNode.displayWithoutProcessing = true
|
self.contentHeaderContainerBackgroundNode.displayWithoutProcessing = true
|
||||||
self.contentHeaderContainerBackgroundNode.image = generateImage(CGSize(width: 10.0, height: 10.0), rotatedContext: { size, context in
|
self.contentHeaderContainerBackgroundNode.image = generateImage(CGSize(width: 10.0, height: 10.0), rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||||
context.setFillColor(presentatonData.theme.rootController.navigationBar.backgroundColor.cgColor)
|
context.setFillColor(presentationData.theme.rootController.navigationBar.backgroundColor.cgColor)
|
||||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||||
context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height / 2.0), size: CGSize(width: size.width, height: size.height / 2.0)))
|
context.fill(CGRect(origin: CGPoint(x: 0.0, y: size.height / 2.0), size: CGSize(width: size.width, height: size.height / 2.0)))
|
||||||
})?.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5)
|
})?.stretchableImage(withLeftCapWidth: 5, topCapHeight: 5)
|
||||||
@ -173,6 +179,9 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
self.listNode = ListView()
|
self.listNode = ListView()
|
||||||
self.listNode.limitHitTestToNodes = true
|
self.listNode.limitHitTestToNodes = true
|
||||||
|
|
||||||
|
self.placeholderNode = MessageReactionListLoadingPlaceholder(theme: presentationData.theme, itemHeight: itemHeight)
|
||||||
|
self.placeholderNode?.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.listContext = MessageReactionListContext(postbox: self.context.account.postbox, network: self.context.account.network, messageId: messageId, initialReactions: initialReactions)
|
self.listContext = MessageReactionListContext(postbox: self.context.account.postbox, network: self.context.account.network, messageId: messageId, initialReactions: initialReactions)
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
@ -182,9 +191,11 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
|
|
||||||
self.listNode.stackFromBottom = false
|
self.listNode.stackFromBottom = false
|
||||||
self.addSubnode(self.listNode)
|
self.addSubnode(self.listNode)
|
||||||
|
self.placeholderNode.flatMap(self.addSubnode)
|
||||||
|
|
||||||
self.addSubnode(self.contentHeaderContainerNode)
|
self.addSubnode(self.contentHeaderContainerNode)
|
||||||
self.contentHeaderContainerNode.addSubnode(self.contentHeaderContainerBackgroundNode)
|
self.contentHeaderContainerNode.addSubnode(self.contentHeaderContainerBackgroundNode)
|
||||||
|
self.contentHeaderContainerNode.addSubnode(self.contentHeaderContainerSeparatorNode)
|
||||||
self.contentHeaderContainerNode.addSubnode(self.categoryScrollNode)
|
self.contentHeaderContainerNode.addSubnode(self.categoryScrollNode)
|
||||||
|
|
||||||
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in
|
self.listNode.updateFloatingHeaderOffset = { [weak self] offset, listTransition in
|
||||||
@ -198,6 +209,10 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
let topOffset = offset
|
let topOffset = offset
|
||||||
transition.updateFrame(node: strongSelf.contentHeaderContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset - headerHeight), size: CGSize(width: layout.size.width, height: headerHeight)))
|
transition.updateFrame(node: strongSelf.contentHeaderContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset - headerHeight), size: CGSize(width: layout.size.width, height: headerHeight)))
|
||||||
transition.updateFrame(node: strongSelf.contentHeaderContainerBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: headerHeight)))
|
transition.updateFrame(node: strongSelf.contentHeaderContainerBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: headerHeight)))
|
||||||
|
transition.updateFrame(node: strongSelf.contentHeaderContainerSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: headerHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
||||||
|
if let placeholderNode = strongSelf.placeholderNode {
|
||||||
|
transition.updateFrame(node: placeholderNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset), size: placeholderNode.bounds.size))
|
||||||
|
}
|
||||||
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset - headerHeight / 2.0), size: CGSize(width: layout.size.width, height: layout.size.height + 300.0)))
|
transition.updateFrame(node: strongSelf.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topOffset - headerHeight / 2.0), size: CGSize(width: layout.size.width, height: layout.size.height + 300.0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,11 +238,8 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
|
|
||||||
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size))
|
||||||
|
|
||||||
//transition.updateBounds(node: self.listNode, bounds: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height))
|
transition.updateBounds(node: self.listNode, bounds: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height))
|
||||||
//transition.updatePosition(node: self.listNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0))
|
transition.updatePosition(node: self.listNode, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0))
|
||||||
|
|
||||||
self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
|
||||||
self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
|
||||||
|
|
||||||
var currentCategoryItemCount = 0
|
var currentCategoryItemCount = 0
|
||||||
if let currentState = self.currentState {
|
if let currentState = self.currentState {
|
||||||
@ -243,6 +255,12 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
insets.top = topInsetForLayout(layout: layout, itemCount: currentCategoryItemCount)
|
insets.top = topInsetForLayout(layout: layout, itemCount: currentCategoryItemCount)
|
||||||
insets.bottom = layout.intrinsicInsets.bottom
|
insets.bottom = layout.intrinsicInsets.bottom
|
||||||
|
|
||||||
|
if let placeholderNode = self.placeholderNode, !self.placeholderNodeIsAnimatingOut {
|
||||||
|
let placeholderHeight = min(CGFloat(currentCategoryItemCount) * itemHeight, layout.size.height) + UIScreenPixel
|
||||||
|
placeholderNode.frame = CGRect(origin: placeholderNode.frame.origin, size: CGSize(width: layout.size.width, height: placeholderHeight))
|
||||||
|
placeholderNode.updateLayout(size: CGSize(width: layout.size.width, height: placeholderHeight))
|
||||||
|
}
|
||||||
|
|
||||||
var duration: Double = 0.0
|
var duration: Double = 0.0
|
||||||
var curve: UInt = 0
|
var curve: UInt = 0
|
||||||
switch transition {
|
switch transition {
|
||||||
@ -322,7 +340,7 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
let states = self.currentState?.states ?? []
|
let states = self.currentState?.states ?? []
|
||||||
for (category, categoryState) in states {
|
for (category, categoryState) in states {
|
||||||
if self.categoryItemNodes.count <= index {
|
if self.categoryItemNodes.count <= index {
|
||||||
let itemNode = MessageReactionCategoryNode(theme: self.presentatonData.theme, category: category, count: categoryState.count, action: { [weak self] in
|
let itemNode = MessageReactionCategoryNode(theme: self.presentationData.theme, category: category, count: categoryState.count, action: { [weak self] in
|
||||||
self?.setCategory(category)
|
self?.setCategory(category)
|
||||||
})
|
})
|
||||||
self.categoryItemNodes.append(itemNode)
|
self.categoryItemNodes.append(itemNode)
|
||||||
@ -341,7 +359,7 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
}
|
}
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
let transaction = preparedTransition(from: self.currentEntries ?? [], to: entries, context: self.context, presentationData: self.presentatonData)
|
let transaction = preparedTransition(from: self.currentEntries ?? [], to: entries, context: self.context, presentationData: self.presentationData)
|
||||||
let previousWasEmpty = self.currentEntries == nil || self.currentEntries?.count == 0
|
let previousWasEmpty = self.currentEntries == nil || self.currentEntries?.count == 0
|
||||||
let isEmpty = entries.isEmpty
|
let isEmpty = entries.isEmpty
|
||||||
self.currentEntries = entries
|
self.currentEntries = entries
|
||||||
@ -350,7 +368,20 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
self.dequeueTransaction()
|
self.dequeueTransaction()
|
||||||
|
|
||||||
if previousWasEmpty && !isEmpty {
|
if previousWasEmpty && !isEmpty {
|
||||||
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
if let placeholderNode = self.placeholderNode {
|
||||||
|
self.placeholderNodeIsAnimatingOut = true
|
||||||
|
placeholderNode.allowsGroupOpacity = true
|
||||||
|
placeholderNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak self] _ in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.placeholderNode?.removeFromSupernode()
|
||||||
|
strongSelf.placeholderNode = nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
self.listNode.forEachItemNode({ itemNode in
|
||||||
|
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -380,9 +411,8 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
|
|
||||||
var options = ListViewDeleteAndInsertOptions()
|
var options = ListViewDeleteAndInsertOptions()
|
||||||
options.insert(.Synchronous)
|
options.insert(.Synchronous)
|
||||||
//options.insert(.AnimateTopItemPosition)
|
|
||||||
//options.insert(.AnimateCrossfade)
|
|
||||||
options.insert(.PreferSynchronousResourceLoading)
|
options.insert(.PreferSynchronousResourceLoading)
|
||||||
|
options.insert(.PreferSynchronousDrawing)
|
||||||
|
|
||||||
var currentCategoryItemCount = 0
|
var currentCategoryItemCount = 0
|
||||||
if let currentState = self.currentState {
|
if let currentState = self.currentState {
|
||||||
@ -417,6 +447,15 @@ private final class MessageReactionListControllerNode: ViewControllerTracingNode
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return super.hitTest(point, with: event)
|
if let result = self.listNode.hitTest(self.view.convert(point, to: self.listNode.view), with: event) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
if point.y >= self.contentHeaderContainerNode.frame.minY && point.y < self.bounds.height {
|
||||||
|
return self.listNode.view
|
||||||
|
}
|
||||||
|
if point.y >= 0 && point.y < self.contentHeaderContainerNode.frame.minY {
|
||||||
|
return self.dimNode.view
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,80 @@
|
|||||||
|
import Foundation
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import TelegramPresentationData
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
|
final class MessageReactionListLoadingPlaceholder: ASDisplayNode {
|
||||||
|
private let theme: PresentationTheme
|
||||||
|
private let itemHeight: CGFloat
|
||||||
|
private let itemImage: UIImage?
|
||||||
|
|
||||||
|
private let backgroundNode: ASDisplayNode
|
||||||
|
private let separatorNode: ASDisplayNode
|
||||||
|
private let highlightNode: ASImageNode
|
||||||
|
private var itemNodes: [ASImageNode] = []
|
||||||
|
|
||||||
|
init(theme: PresentationTheme, itemHeight: CGFloat) {
|
||||||
|
self.theme = theme
|
||||||
|
self.itemHeight = itemHeight
|
||||||
|
|
||||||
|
self.backgroundNode = ASDisplayNode()
|
||||||
|
self.backgroundNode.backgroundColor = UIColor(white: 0.92, alpha: 1.0)
|
||||||
|
|
||||||
|
self.separatorNode = ASDisplayNode()
|
||||||
|
self.separatorNode.backgroundColor = theme.list.itemPlainSeparatorColor
|
||||||
|
|
||||||
|
self.highlightNode = ASImageNode()
|
||||||
|
self.highlightNode.displaysAsynchronously = false
|
||||||
|
self.highlightNode.displayWithoutProcessing = true
|
||||||
|
|
||||||
|
let leftInset: CGFloat = 15.0
|
||||||
|
let avatarSize: CGFloat = 40.0
|
||||||
|
let avatarSpacing: CGFloat = 11.0
|
||||||
|
let contentWidth: CGFloat = 4.0
|
||||||
|
let contentHeight: CGFloat = 14.0
|
||||||
|
let rightInset: CGFloat = 54.0
|
||||||
|
self.itemImage = generateImage(CGSize(width: leftInset + avatarSize + avatarSpacing + contentWidth + rightInset, height: itemHeight), rotatedContext: { size, context in
|
||||||
|
context.setFillColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor)
|
||||||
|
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||||
|
context.setFillColor(theme.list.itemPlainSeparatorColor.cgColor)
|
||||||
|
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: UIScreenPixel)))
|
||||||
|
context.setBlendMode(.copy)
|
||||||
|
context.setFillColor(UIColor.clear.cgColor)
|
||||||
|
context.fillEllipse(in: CGRect(origin: CGPoint(x: leftInset, y: floor((itemHeight - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize)))
|
||||||
|
let contentOrigin = leftInset + avatarSize + avatarSpacing
|
||||||
|
context.fill(CGRect(origin: CGPoint(x: contentOrigin, y: floor((size.height - contentHeight) / 2.0)), size: CGSize(width: size.width - contentOrigin - rightInset, height: contentHeight)))
|
||||||
|
})?.stretchableImage(withLeftCapWidth: Int(leftInset + avatarSize + avatarSpacing + 1), topCapHeight: 0)
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.backgroundNode)
|
||||||
|
self.addSubnode(self.highlightNode)
|
||||||
|
self.addSubnode(self.separatorNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLayout(size: CGSize) {
|
||||||
|
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
|
|
||||||
|
var verticalOffset: CGFloat = 0.0
|
||||||
|
var index = 0
|
||||||
|
while verticalOffset < size.height - 1.0 {
|
||||||
|
if self.itemNodes.count >= index {
|
||||||
|
let itemNode = ASImageNode()
|
||||||
|
itemNode.image = self.itemImage
|
||||||
|
self.itemNodes.append(itemNode)
|
||||||
|
self.addSubnode(itemNode)
|
||||||
|
}
|
||||||
|
self.itemNodes[index].frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: size.width, height: self.itemHeight))
|
||||||
|
verticalOffset += self.itemHeight
|
||||||
|
index += 1
|
||||||
|
}
|
||||||
|
self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: CGSize(width: size.width, height: UIScreenPixel))
|
||||||
|
if index < self.itemNodes.count {
|
||||||
|
for i in index ..< self.itemNodes.count {
|
||||||
|
self.itemNodes[i].removeFromSupernode()
|
||||||
|
}
|
||||||
|
self.itemNodes.removeLast(self.itemNodes.count - index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -283,6 +283,17 @@
|
|||||||
<key>explicitFileType</key>
|
<key>explicitFileType</key>
|
||||||
<string>text.script.python</string>
|
<string>text.script.python</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>1DD70E2982B4C61400000000</key>
|
||||||
|
<dict>
|
||||||
|
<key>isa</key>
|
||||||
|
<string>PBXFileReference</string>
|
||||||
|
<key>name</key>
|
||||||
|
<string>ReactionAttachedNode.swift</string>
|
||||||
|
<key>path</key>
|
||||||
|
<string>Sources/ReactionAttachedNode.swift</string>
|
||||||
|
<key>sourceTree</key>
|
||||||
|
<string>SOURCE_ROOT</string>
|
||||||
|
</dict>
|
||||||
<key>1DD70E29CCCE0EED00000000</key>
|
<key>1DD70E29CCCE0EED00000000</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isa</key>
|
<key>isa</key>
|
||||||
@ -348,6 +359,7 @@
|
|||||||
<string><![CDATA[<group>]]></string>
|
<string><![CDATA[<group>]]></string>
|
||||||
<key>children</key>
|
<key>children</key>
|
||||||
<array>
|
<array>
|
||||||
|
<string>1DD70E2982B4C61400000000</string>
|
||||||
<string>1DD70E29CCCE0EED00000000</string>
|
<string>1DD70E29CCCE0EED00000000</string>
|
||||||
<string>1DD70E2930A5FA5800000000</string>
|
<string>1DD70E2930A5FA5800000000</string>
|
||||||
<string>1DD70E29691388CA00000000</string>
|
<string>1DD70E29691388CA00000000</string>
|
||||||
@ -385,6 +397,13 @@
|
|||||||
<string>B401C979E09F1DE500000000</string>
|
<string>B401C979E09F1DE500000000</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>E7A30F0482B4C61400000000</key>
|
||||||
|
<dict>
|
||||||
|
<key>isa</key>
|
||||||
|
<string>PBXBuildFile</string>
|
||||||
|
<key>fileRef</key>
|
||||||
|
<string>1DD70E2982B4C61400000000</string>
|
||||||
|
</dict>
|
||||||
<key>E7A30F04CCCE0EED00000000</key>
|
<key>E7A30F04CCCE0EED00000000</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isa</key>
|
<key>isa</key>
|
||||||
@ -426,6 +445,7 @@
|
|||||||
<string>PBXSourcesBuildPhase</string>
|
<string>PBXSourcesBuildPhase</string>
|
||||||
<key>files</key>
|
<key>files</key>
|
||||||
<array>
|
<array>
|
||||||
|
<string>E7A30F0482B4C61400000000</string>
|
||||||
<string>E7A30F04CCCE0EED00000000</string>
|
<string>E7A30F04CCCE0EED00000000</string>
|
||||||
<string>E7A30F0430A5FA5800000000</string>
|
<string>E7A30F0430A5FA5800000000</string>
|
||||||
<string>E7A30F04691388CA00000000</string>
|
<string>E7A30F04691388CA00000000</string>
|
||||||
|
|||||||
@ -0,0 +1,322 @@
|
|||||||
|
import Foundation
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import AnimationUI
|
||||||
|
import Display
|
||||||
|
import Postbox
|
||||||
|
import TelegramCore
|
||||||
|
import TelegramPresentationData
|
||||||
|
import AppBundle
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ReactionAttachedNode: 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 = 30.0
|
||||||
|
private var maximizedReactionSize: CGFloat = 60.0
|
||||||
|
private var smallCircleSize: CGFloat = 8.0
|
||||||
|
|
||||||
|
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) {
|
||||||
|
let initialAnchorX = startingPoint.x
|
||||||
|
|
||||||
|
if isInitial && self.reactionNodes.isEmpty {
|
||||||
|
let availableContentWidth = constrainedSize.width
|
||||||
|
var minimizedReactionSize = (availableContentWidth - self.maximizedReactionSize) / (CGFloat(self.reactions.count - 1) + CGFloat(self.reactions.count + 1) * 0.2)
|
||||||
|
minimizedReactionSize = max(16.0, floor(minimizedReactionSize))
|
||||||
|
minimizedReactionSize = min(30.0, minimizedReactionSize)
|
||||||
|
|
||||||
|
self.minimizedReactionSize = minimizedReactionSize
|
||||||
|
self.shadowBlur = floor(minimizedReactionSize * 0.26)
|
||||||
|
self.smallCircleSize = 8.0
|
||||||
|
|
||||||
|
let backgroundHeight = floor(minimizedReactionSize * 1.4)
|
||||||
|
|
||||||
|
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: self.maximizedReactionSize, loadFirstFrame: true)
|
||||||
|
}
|
||||||
|
self.reactionNodes.forEach(self.addSubnode(_:))
|
||||||
|
}
|
||||||
|
|
||||||
|
let backgroundHeight: CGFloat = floor(self.minimizedReactionSize * 1.4)
|
||||||
|
|
||||||
|
let reactionSpacing: CGFloat = floor(self.minimizedReactionSize * 0.2)
|
||||||
|
let minimizedReactionVerticalInset: CGFloat = floor((backgroundHeight - minimizedReactionSize) / 2.0)
|
||||||
|
|
||||||
|
let contentWidth: CGFloat = CGFloat(self.reactionNodes.count - 1) * (minimizedReactionSize) + maximizedReactionSize + CGFloat(self.reactionNodes.count + 1) * reactionSpacing
|
||||||
|
|
||||||
|
var backgroundFrame = CGRect(origin: CGPoint(x: -shadowBlur, y: -shadowBlur), size: CGSize(width: contentWidth + shadowBlur * 2.0, height: backgroundHeight + shadowBlur * 2.0))
|
||||||
|
backgroundFrame = backgroundFrame.offsetBy(dx: initialAnchorX - contentWidth + backgroundHeight / 2.0, dy: startingPoint.y - backgroundHeight - 16.0)
|
||||||
|
backgroundFrame.origin.x = max(0.0, backgroundFrame.minX)
|
||||||
|
backgroundFrame.origin.x = min(constrainedSize.width - backgroundFrame.width, backgroundFrame.minX)
|
||||||
|
|
||||||
|
self.backgroundNode.frame = backgroundFrame
|
||||||
|
self.backgroundShadowNode.frame = backgroundFrame
|
||||||
|
|
||||||
|
let anchorMinX = backgroundFrame.minX + shadowBlur + backgroundHeight / 2.0
|
||||||
|
let anchorMaxX = backgroundFrame.maxX - shadowBlur - backgroundHeight / 2.0
|
||||||
|
let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart))
|
||||||
|
|
||||||
|
var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing
|
||||||
|
if offsetFromStart > backgroundFrame.maxX - shadowBlur || offsetFromStart < backgroundFrame.minX {
|
||||||
|
self.hasSelectedNode = false
|
||||||
|
} else {
|
||||||
|
self.hasSelectedNode = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var maximizedIndex = Int(((anchorX - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count))
|
||||||
|
maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex))
|
||||||
|
|
||||||
|
for iterationIndex in 0 ..< self.reactionNodes.count {
|
||||||
|
var i = iterationIndex
|
||||||
|
let isMaximized = i == maximizedIndex
|
||||||
|
|
||||||
|
let reactionSize: CGFloat
|
||||||
|
if isMaximized {
|
||||||
|
reactionSize = maximizedReactionSize
|
||||||
|
} else {
|
||||||
|
reactionSize = minimizedReactionSize
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition: ContainedViewLayoutTransition
|
||||||
|
if isInitial {
|
||||||
|
transition = .immediate
|
||||||
|
} else {
|
||||||
|
transition = .animated(duration: 0.18, curve: .easeInOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.reactionNodes[i].isMaximized != isMaximized {
|
||||||
|
self.reactionNodes[i].isMaximized = isMaximized
|
||||||
|
self.reactionNodes[i].updateIsAnimating(isMaximized, animated: !isInitial)
|
||||||
|
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 -= 9.0
|
||||||
|
reactionFrame.size.width += 18.0
|
||||||
|
}
|
||||||
|
self.reactionNodes[i].updateLayout(size: reactionFrame.size, scale: reactionFrame.size.width / (maximizedReactionSize + 18.0), transition: transition, displayText: isMaximized)
|
||||||
|
|
||||||
|
transition.updateFrame(node: self.reactionNodes[i], frame: reactionFrame, beginWithCurrentState: true)
|
||||||
|
|
||||||
|
reactionX += reactionSize + reactionSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
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(x: -(self.backgroundNode.frame.width - shadowBlur) / 2.0 + 42.0, y: (self.backgroundNode.frame.height - shadowBlur) / 2.0)
|
||||||
|
let damping: CGFloat = 100.0
|
||||||
|
|
||||||
|
for i in 0 ..< self.reactionNodes.count {
|
||||||
|
let animationOffset: Double = 1.0 - Double(i) / Double(self.reactionNodes.count - 1)
|
||||||
|
var nodeOffset = CGPoint(x: self.reactionNodes[i].frame.minX - (self.backgroundNode.frame.minX + shadowBlur) / 2.0 - 42.0, y: self.reactionNodes[i].frame.minY - self.backgroundNode.frame.maxY - shadowBlur)
|
||||||
|
nodeOffset.x = -nodeOffset.x
|
||||||
|
nodeOffset.y = 30.0
|
||||||
|
self.reactionNodes[i].layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5 + animationOffset * 0.28, initialVelocity: 0.0, damping: damping)
|
||||||
|
self.reactionNodes[i].layer.animateSpring(from: NSValue(cgPoint: nodeOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: 0.5, initialVelocity: 0.0, damping: damping, additive: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.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: ASImageNode?, 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 {
|
||||||
|
if let snapshotView = self.reactionNodes[i].view.snapshotContentTree() {
|
||||||
|
let targetSnapshotView = UIImageView()
|
||||||
|
targetSnapshotView.image = targetNode.image
|
||||||
|
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? {
|
||||||
|
if !self.hasSelectedNode {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i in 0 ..< self.reactionNodes.count {
|
||||||
|
if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized {
|
||||||
|
return self.reactionNodes[i].reaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -227,16 +227,16 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
self.bubbleNodes.forEach { _, shadow in
|
self.bubbleNodes.forEach { _, shadow in
|
||||||
self.addSubnode(shadow)
|
//self.addSubnode(shadow)
|
||||||
}
|
}
|
||||||
self.addSubnode(self.backgroundShadowNode)
|
self.addSubnode(self.backgroundShadowNode)
|
||||||
self.bubbleNodes.forEach { foreground, _ in
|
self.bubbleNodes.forEach { foreground, _ in
|
||||||
self.addSubnode(foreground)
|
//self.addSubnode(foreground)
|
||||||
}
|
}
|
||||||
self.addSubnode(self.backgroundNode)
|
self.addSubnode(self.backgroundNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool) {
|
func updateLayout(constrainedSize: CGSize, startingPoint: CGPoint, offsetFromStart: CGFloat, isInitial: Bool, touchPoint: CGPoint) {
|
||||||
let initialAnchorX = startingPoint.x
|
let initialAnchorX = startingPoint.x
|
||||||
|
|
||||||
var isRightAligned = false
|
var isRightAligned = false
|
||||||
@ -285,23 +285,40 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
backgroundFrame.origin.x = max(0.0, backgroundFrame.minX)
|
backgroundFrame.origin.x = max(0.0, backgroundFrame.minX)
|
||||||
backgroundFrame.origin.x = min(constrainedSize.width - backgroundFrame.width, backgroundFrame.minX)
|
backgroundFrame.origin.x = min(constrainedSize.width - backgroundFrame.width, backgroundFrame.minX)
|
||||||
|
|
||||||
self.isRightAligned = isRightAligned
|
|
||||||
self.backgroundNode.frame = backgroundFrame
|
|
||||||
self.backgroundShadowNode.frame = backgroundFrame
|
|
||||||
|
|
||||||
let anchorMinX = backgroundFrame.minX + shadowBlur + backgroundHeight / 2.0
|
let anchorMinX = backgroundFrame.minX + shadowBlur + backgroundHeight / 2.0
|
||||||
let anchorMaxX = backgroundFrame.maxX - shadowBlur - backgroundHeight / 2.0
|
let anchorMaxX = backgroundFrame.maxX - shadowBlur - backgroundHeight / 2.0
|
||||||
let anchorX = max(anchorMinX, min(anchorMaxX, offsetFromStart))
|
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).contains(touchPoint) {
|
||||||
|
maximizedIndex = Int(((touchPoint.x - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count))
|
||||||
|
maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex))
|
||||||
|
}
|
||||||
|
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 + reactionSpacing
|
var reactionX: CGFloat = backgroundFrame.minX + shadowBlur + reactionSpacing
|
||||||
if offsetFromStart > backgroundFrame.maxX - shadowBlur || offsetFromStart < backgroundFrame.minX {
|
if offsetFromStart > backgroundFrame.maxX - shadowBlur || offsetFromStart < backgroundFrame.minX {
|
||||||
self.hasSelectedNode = false
|
self.hasSelectedNode = false
|
||||||
} else {
|
} else {
|
||||||
self.hasSelectedNode = true
|
self.hasSelectedNode = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var maximizedIndex = Int(((anchorX - anchorMinX) / (anchorMaxX - anchorMinX)) * CGFloat(self.reactionNodes.count))
|
|
||||||
maximizedIndex = max(0, min(self.reactionNodes.count - 1, maximizedIndex))
|
|
||||||
|
|
||||||
for iterationIndex in 0 ..< self.reactionNodes.count {
|
for iterationIndex in 0 ..< self.reactionNodes.count {
|
||||||
var i = iterationIndex
|
var i = iterationIndex
|
||||||
@ -484,9 +501,6 @@ final class ReactionSelectionNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func selectedReaction() -> ReactionGestureItem? {
|
func selectedReaction() -> ReactionGestureItem? {
|
||||||
if !self.hasSelectedNode {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for i in 0 ..< self.reactionNodes.count {
|
for i in 0 ..< self.reactionNodes.count {
|
||||||
if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized {
|
if let isMaximized = self.reactionNodes[i].isMaximized, isMaximized {
|
||||||
return self.reactionNodes[i].reaction
|
return self.reactionNodes[i].reaction
|
||||||
|
|||||||
@ -10,7 +10,7 @@ public final class ReactionSelectionParentNode: ASDisplayNode {
|
|||||||
private let theme: PresentationTheme
|
private let theme: PresentationTheme
|
||||||
|
|
||||||
private var currentNode: ReactionSelectionNode?
|
private var currentNode: ReactionSelectionNode?
|
||||||
private var currentLocation: (CGPoint, CGFloat)?
|
private var currentLocation: (CGPoint, CGFloat, CGPoint)?
|
||||||
|
|
||||||
private var validLayout: (size: CGSize, insets: UIEdgeInsets)?
|
private var validLayout: (size: CGSize, insets: UIEdgeInsets)?
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ public final class ReactionSelectionParentNode: ASDisplayNode {
|
|||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayReactions(_ reactions: [ReactionGestureItem], at point: CGPoint) {
|
func displayReactions(_ reactions: [ReactionGestureItem], at point: CGPoint, touchPoint: CGPoint) {
|
||||||
if let currentNode = self.currentNode {
|
if let currentNode = self.currentNode {
|
||||||
currentNode.removeFromSupernode()
|
currentNode.removeFromSupernode()
|
||||||
self.currentNode = nil
|
self.currentNode = nil
|
||||||
@ -30,7 +30,7 @@ public final class ReactionSelectionParentNode: ASDisplayNode {
|
|||||||
let reactionNode = ReactionSelectionNode(account: self.account, theme: self.theme, reactions: reactions)
|
let reactionNode = ReactionSelectionNode(account: self.account, theme: self.theme, reactions: reactions)
|
||||||
self.addSubnode(reactionNode)
|
self.addSubnode(reactionNode)
|
||||||
self.currentNode = reactionNode
|
self.currentNode = reactionNode
|
||||||
self.currentLocation = (point, point.x)
|
self.currentLocation = (point, point.x, touchPoint)
|
||||||
|
|
||||||
if let (size, insets) = self.validLayout {
|
if let (size, insets) = self.validLayout {
|
||||||
self.update(size: size, insets: insets, isInitial: true)
|
self.update(size: size, insets: insets, isInitial: true)
|
||||||
@ -55,9 +55,9 @@ public final class ReactionSelectionParentNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateReactionsAnchor(point: CGPoint) {
|
func updateReactionsAnchor(point: CGPoint, touchPoint: CGPoint) {
|
||||||
if let (currentPoint, _) = self.currentLocation {
|
if let (currentPoint, _, _) = self.currentLocation {
|
||||||
self.currentLocation = (currentPoint, point.x)
|
self.currentLocation = (currentPoint, point.x, touchPoint)
|
||||||
|
|
||||||
if let (size, insets) = self.validLayout {
|
if let (size, insets) = self.validLayout {
|
||||||
self.update(size: size, insets: insets, isInitial: false)
|
self.update(size: size, insets: insets, isInitial: false)
|
||||||
@ -72,8 +72,8 @@ public final class ReactionSelectionParentNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func update(size: CGSize, insets: UIEdgeInsets, isInitial: Bool) {
|
private func update(size: CGSize, insets: UIEdgeInsets, isInitial: Bool) {
|
||||||
if let currentNode = self.currentNode, let (point, offset) = currentLocation {
|
if let currentNode = self.currentNode, let (point, offset, touchPoint) = self.currentLocation {
|
||||||
currentNode.updateLayout(constrainedSize: size, startingPoint: point, offsetFromStart: offset, isInitial: isInitial)
|
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)
|
currentNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,12 +15,16 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
|
|
||||||
public var availableReactions: (() -> [ReactionGestureItem])?
|
public var availableReactions: (() -> [ReactionGestureItem])?
|
||||||
public var getReactionContainer: (() -> ReactionSelectionParentNode?)?
|
public var getReactionContainer: (() -> ReactionSelectionParentNode?)?
|
||||||
|
public var getAnchorPoint: (() -> CGPoint?)?
|
||||||
public var began: (() -> Void)?
|
public var began: (() -> Void)?
|
||||||
public var updateOffset: ((CGFloat, Bool) -> Void)?
|
public var updateOffset: ((CGFloat, Bool) -> Void)?
|
||||||
public var completed: ((ReactionGestureItem?) -> Void)?
|
public var completed: ((ReactionGestureItem?) -> Void)?
|
||||||
public var displayReply: ((CGFloat) -> Void)?
|
public var displayReply: ((CGFloat) -> Void)?
|
||||||
public var activateReply: (() -> Void)?
|
public var activateReply: (() -> Void)?
|
||||||
|
|
||||||
|
private var currentAnchorPoint: CGPoint?
|
||||||
|
private var currentAnchorStartPoint: CGPoint?
|
||||||
|
|
||||||
override public init(target: Any?, action: Selector?) {
|
override public init(target: Any?, action: Selector?) {
|
||||||
super.init(target: target, action: action)
|
super.init(target: target, action: action)
|
||||||
|
|
||||||
@ -95,17 +99,20 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
self.f()
|
self.f()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let activationTimer = Timer(timeInterval: 0.3, target: TimerTarget { [weak self] in
|
let activationTimer = Timer(timeInterval: 0.1, target: TimerTarget { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
strongSelf.activationTimer = nil
|
strongSelf.activationTimer = nil
|
||||||
if strongSelf.validatedGesture {
|
if strongSelf.validatedGesture {
|
||||||
let location = strongSelf.currentLocation
|
let location = strongSelf.currentLocation
|
||||||
if !strongSelf.currentReactions.isEmpty, let reactionContainer = strongSelf.getReactionContainer?() {
|
if !strongSelf.currentReactions.isEmpty, let reactionContainer = strongSelf.getReactionContainer?(), let localAnchorPoint = strongSelf.getAnchorPoint?() {
|
||||||
strongSelf.currentContainer = reactionContainer
|
strongSelf.currentContainer = reactionContainer
|
||||||
let reactionContainerLocation = reactionContainer.view.convert(location, from: nil)
|
let reactionContainerLocation = reactionContainer.view.convert(localAnchorPoint, from: strongSelf.view)
|
||||||
reactionContainer.displayReactions(strongSelf.currentReactions, at: reactionContainerLocation)
|
let reactionContainerTouchPoint = reactionContainer.view.convert(location, from: nil)
|
||||||
|
strongSelf.currentAnchorPoint = reactionContainerLocation
|
||||||
|
strongSelf.currentAnchorStartPoint = location
|
||||||
|
reactionContainer.displayReactions(strongSelf.currentReactions, at: reactionContainerLocation, touchPoint: reactionContainerTouchPoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, selector: #selector(TimerTarget.event), userInfo: nil, repeats: false)
|
}, selector: #selector(TimerTarget.event), userInfo: nil, repeats: false)
|
||||||
@ -124,9 +131,11 @@ public final class ReactionSwipeGestureRecognizer: UIPanGestureRecognizer {
|
|||||||
self.displayReply?(-min(0.0, translation.x))
|
self.displayReply?(-min(0.0, translation.x))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let reactionContainer = self.currentContainer {
|
if let reactionContainer = self.currentContainer, let currentAnchorPoint = self.currentAnchorPoint, let currentAnchorStartPoint = self.currentAnchorStartPoint {
|
||||||
let reactionContainerLocation = reactionContainer.view.convert(location, from: nil)
|
let anchorPoint = CGPoint(x: currentAnchorPoint.x + location.x - currentAnchorStartPoint.x, y: currentAnchorPoint.y)
|
||||||
reactionContainer.updateReactionsAnchor(point: reactionContainerLocation)
|
let reactionContainerLocation = anchorPoint
|
||||||
|
let reactionContainerTouchPoint = reactionContainer.view.convert(location, from: nil)
|
||||||
|
reactionContainer.updateReactionsAnchor(point: reactionContainerLocation, touchPoint: reactionContainerTouchPoint)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
super.touchesMoved(touches, with: event)
|
super.touchesMoved(touches, with: event)
|
||||||
|
|||||||
@ -93,7 +93,7 @@ private final class MessageReactionCategoryContext {
|
|||||||
}
|
}
|
||||||
let messageId = self.messageId
|
let messageId = self.messageId
|
||||||
let offset = self.state.nextOffset
|
let offset = self.state.nextOffset
|
||||||
let request = self.postbox.transaction { transaction -> Api.InputPeer? in
|
var request = self.postbox.transaction { transaction -> Api.InputPeer? in
|
||||||
let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
|
let inputPeer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer)
|
||||||
return inputPeer
|
return inputPeer
|
||||||
}
|
}
|
||||||
@ -107,6 +107,9 @@ private final class MessageReactionCategoryContext {
|
|||||||
return .generic
|
return .generic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//#if DEBUG
|
||||||
|
request = request |> delay(1.0, queue: .mainQueue())
|
||||||
|
//#endif
|
||||||
self.loadingDisposable.set((request
|
self.loadingDisposable.set((request
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] result in
|
|> deliverOnMainQueue).start(next: { [weak self] result in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
|
|||||||
@ -1185,7 +1185,10 @@ final class SharedApplicationContext {
|
|||||||
BITHockeyManager.shared().configure(withIdentifier: hockeyAppId, delegate: self)
|
BITHockeyManager.shared().configure(withIdentifier: hockeyAppId, delegate: self)
|
||||||
BITHockeyManager.shared().crashManager.crashManagerStatus = .alwaysAsk
|
BITHockeyManager.shared().crashManager.crashManagerStatus = .alwaysAsk
|
||||||
BITHockeyManager.shared().start()
|
BITHockeyManager.shared().start()
|
||||||
|
#if targetEnvironment(simulator)
|
||||||
|
#else
|
||||||
BITHockeyManager.shared().authenticator.authenticateInstallation()
|
BITHockeyManager.shared().authenticator.authenticateInstallation()
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(forName: UIWindow.didBecomeHiddenNotification, object: nil, queue: nil, using: { notification in
|
NotificationCenter.default.addObserver(forName: UIWindow.didBecomeHiddenNotification, object: nil, queue: nil, using: { notification in
|
||||||
|
|||||||
@ -1667,6 +1667,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !initialReactions.isEmpty {
|
if !initialReactions.isEmpty {
|
||||||
|
strongSelf.chatDisplayNode.dismissInput()
|
||||||
strongSelf.present(MessageReactionListController(context: strongSelf.context, messageId: message.id, initialReactions: initialReactions), in: .window(.root))
|
strongSelf.present(MessageReactionListController(context: strongSelf.context, messageId: message.id, initialReactions: initialReactions), in: .window(.root))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1045,13 +1045,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
let previousInputPanelBackgroundFrame = self.inputPanelBackgroundNode.frame
|
let previousInputPanelBackgroundFrame = self.inputPanelBackgroundNode.frame
|
||||||
transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame)
|
transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame)
|
||||||
let separatorOffset: CGFloat
|
transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel)))
|
||||||
if apparentInputBackgroundFrame.maxY >= layout.size.height {
|
|
||||||
separatorOffset = UIScreenPixel
|
|
||||||
} else {
|
|
||||||
separatorOffset = -UIScreenPixel
|
|
||||||
}
|
|
||||||
transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y + separatorOffset), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel)))
|
|
||||||
transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame)
|
transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame)
|
||||||
|
|
||||||
if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame = titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) {
|
if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame = titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) {
|
||||||
|
|||||||
@ -428,7 +428,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
}
|
}
|
||||||
|
|
||||||
let reactions: [(String, String, String)] = [
|
let reactions: [(String, String, String)] = [
|
||||||
("😒", "Sad", "sad"),
|
("😔", "Sad", "sad"),
|
||||||
("😳", "Surprised", "surprised"),
|
("😳", "Surprised", "surprised"),
|
||||||
("😂", "Fun", "lol"),
|
("😂", "Fun", "lol"),
|
||||||
("👍", "Like", "thumbsup"),
|
("👍", "Like", "thumbsup"),
|
||||||
@ -436,19 +436,25 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
]
|
]
|
||||||
|
|
||||||
var reactionItems: [ReactionGestureItem] = []
|
var reactionItems: [ReactionGestureItem] = []
|
||||||
for (value, text, name) in reactions {
|
for (value, text, name) in reactions.reversed() {
|
||||||
if let path = getAppBundle().path(forResource: name, ofType: "tgs") {
|
if let path = getAppBundle().path(forResource: name, ofType: "tgs") {
|
||||||
reactionItems.append(.reaction(value: value, text: text, path: path))
|
reactionItems.append(.reaction(value: value, text: text, path: path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if item.controllerInteraction.canSetupReply(item.message) {
|
if item.controllerInteraction.canSetupReply(item.message) {
|
||||||
reactionItems.append(.reply)
|
//reactionItems.append(.reply)
|
||||||
}
|
}
|
||||||
return reactionItems
|
return reactionItems
|
||||||
}
|
}
|
||||||
reactionRecognizer.getReactionContainer = { [weak self] in
|
reactionRecognizer.getReactionContainer = { [weak self] in
|
||||||
return self?.item?.controllerInteraction.reactionContainerNode()
|
return self?.item?.controllerInteraction.reactionContainerNode()
|
||||||
}
|
}
|
||||||
|
reactionRecognizer.getAnchorPoint = { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return CGPoint(x: strongSelf.backgroundNode.frame.maxX, y: strongSelf.backgroundNode.frame.minY)
|
||||||
|
}
|
||||||
reactionRecognizer.began = { [weak self] in
|
reactionRecognizer.began = { [weak self] in
|
||||||
guard let strongSelf = self, let item = strongSelf.item else {
|
guard let strongSelf = self, let item = strongSelf.item else {
|
||||||
return
|
return
|
||||||
@ -497,7 +503,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
|
|||||||
strongSelf.swipeToReplyFeedback = HapticFeedback()
|
strongSelf.swipeToReplyFeedback = HapticFeedback()
|
||||||
}
|
}
|
||||||
strongSelf.swipeToReplyFeedback?.tap()
|
strongSelf.swipeToReplyFeedback?.tap()
|
||||||
if strongSelf.swipeToReplyNode == nil, false {
|
if strongSelf.swipeToReplyNode == nil {
|
||||||
let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonStrokeColor, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper))
|
let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonStrokeColor, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.message.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper))
|
||||||
strongSelf.swipeToReplyNode = swipeToReplyNode
|
strongSelf.swipeToReplyNode = swipeToReplyNode
|
||||||
strongSelf.insertSubnode(swipeToReplyNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
strongSelf.insertSubnode(swipeToReplyNode, belowSubnode: strongSelf.messageAccessibilityArea)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user