mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Combo update
This commit is contained in:
parent
759f1c79bb
commit
e170f2fe5a
@ -43,6 +43,9 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
|
|||||||
private let maxLevel: CGFloat
|
private let maxLevel: CGFloat
|
||||||
|
|
||||||
private var displayLinkAnimator: ConstantDisplayLinkAnimator?
|
private var displayLinkAnimator: ConstantDisplayLinkAnimator?
|
||||||
|
|
||||||
|
private let hierarchyTrackingNode: HierarchyTrackingNode
|
||||||
|
private var isCurrentlyInHierarchy = true
|
||||||
|
|
||||||
private var audioLevel: CGFloat = 0
|
private var audioLevel: CGFloat = 0
|
||||||
public var presentationAudioLevel: CGFloat = 0
|
public var presentationAudioLevel: CGFloat = 0
|
||||||
@ -93,8 +96,15 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
|
|||||||
scaleSpeed: 0.2,
|
scaleSpeed: 0.2,
|
||||||
isCircle: false
|
isCircle: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var updateInHierarchy: ((Bool) -> Void)?
|
||||||
|
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
|
||||||
|
updateInHierarchy?(value)
|
||||||
|
})
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubnode(self.hierarchyTrackingNode)
|
||||||
|
|
||||||
self.addSubnode(self.bigBlob)
|
self.addSubnode(self.bigBlob)
|
||||||
self.addSubnode(self.mediumBlob)
|
self.addSubnode(self.mediumBlob)
|
||||||
@ -109,6 +119,12 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
|
|||||||
strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel
|
strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel
|
||||||
strongSelf.bigBlob.level = strongSelf.presentationAudioLevel
|
strongSelf.bigBlob.level = strongSelf.presentationAudioLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateInHierarchy = { [weak self] value in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.isCurrentlyInHierarchy = value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -266,6 +282,9 @@ final class BlobNode: ASDisplayNode {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let hierarchyTrackingNode: HierarchyTrackingNode
|
||||||
|
private var isCurrentlyInHierarchy = true
|
||||||
|
|
||||||
init(
|
init(
|
||||||
pointsCount: Int,
|
pointsCount: Int,
|
||||||
@ -290,12 +309,24 @@ final class BlobNode: ASDisplayNode {
|
|||||||
|
|
||||||
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
|
||||||
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
|
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
|
||||||
|
|
||||||
|
var updateInHierarchy: ((Bool) -> Void)?
|
||||||
|
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
|
||||||
|
updateInHierarchy?(value)
|
||||||
|
})
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.hierarchyTrackingNode)
|
||||||
self.layer.addSublayer(self.shapeLayer)
|
self.layer.addSublayer(self.shapeLayer)
|
||||||
|
|
||||||
self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
|
self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
|
||||||
|
|
||||||
|
updateInHierarchy = { [weak self] value in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.isCurrentlyInHierarchy = value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
@ -305,7 +336,7 @@ final class BlobNode: ASDisplayNode {
|
|||||||
func setColor(_ color: UIColor, animated: Bool) {
|
func setColor(_ color: UIColor, animated: Bool) {
|
||||||
let previousColor = self.shapeLayer.fillColor
|
let previousColor = self.shapeLayer.fillColor
|
||||||
self.shapeLayer.fillColor = color.cgColor
|
self.shapeLayer.fillColor = color.cgColor
|
||||||
if animated, let previousColor = previousColor {
|
if animated, let previousColor = previousColor, self.isCurrentlyInHierarchy {
|
||||||
self.shapeLayer.animate(from: previousColor, to: color.cgColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
|
self.shapeLayer.animate(from: previousColor, to: color.cgColor, keyPath: "fillColor", timingFunction: CAMediaTimingFunctionName.linear.rawValue, duration: 0.3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,6 +192,28 @@ public struct Transition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func setSublayerTransform(view: UIView, transform: CATransform3D, completion: ((Bool) -> Void)? = nil) {
|
||||||
|
switch self.animation {
|
||||||
|
case .none:
|
||||||
|
view.layer.sublayerTransform = transform
|
||||||
|
completion?(true)
|
||||||
|
case let .curve(duration, curve):
|
||||||
|
let previousValue = view.layer.sublayerTransform
|
||||||
|
view.layer.sublayerTransform = transform
|
||||||
|
view.layer.animate(
|
||||||
|
from: NSValue(caTransform3D: previousValue),
|
||||||
|
to: NSValue(caTransform3D: transform),
|
||||||
|
keyPath: "transform",
|
||||||
|
duration: duration,
|
||||||
|
delay: 0.0,
|
||||||
|
curve: curve,
|
||||||
|
removeOnCompletion: true,
|
||||||
|
additive: false,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
public func animateScale(view: UIView, from fromValue: CGFloat, to toValue: CGFloat, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||||
switch self.animation {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
|
@ -138,7 +138,7 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setupGradientAnimations() {
|
private func setupGradientAnimations() {
|
||||||
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
|
/*if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
|
||||||
} else {
|
} else {
|
||||||
let previousValue = self.foregroundGradientLayer.startPoint
|
let previousValue = self.foregroundGradientLayer.startPoint
|
||||||
let newValue: CGPoint
|
let newValue: CGPoint
|
||||||
@ -162,7 +162,7 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
|
|||||||
|
|
||||||
self.foregroundGradientLayer.add(animation, forKey: "movement")
|
self.foregroundGradientLayer.add(animation, forKey: "movement")
|
||||||
CATransaction.commit()
|
CATransaction.commit()
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAnimations() {
|
func updateAnimations() {
|
||||||
@ -172,7 +172,9 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.setupGradientAnimations()
|
self.setupGradientAnimations()
|
||||||
self.maskCurveView.startAnimating()
|
if isCurrentlyInHierarchy {
|
||||||
|
self.maskCurveView.startAnimating()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,6 +208,9 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
|||||||
private var currentScheduleTimestamp: Int32?
|
private var currentScheduleTimestamp: Int32?
|
||||||
private var currentMembers: PresentationGroupCallMembers?
|
private var currentMembers: PresentationGroupCallMembers?
|
||||||
private var currentIsConnected = true
|
private var currentIsConnected = true
|
||||||
|
|
||||||
|
private let hierarchyTrackingNode: HierarchyTrackingNode
|
||||||
|
private var isCurrentlyInHierarchy = true
|
||||||
|
|
||||||
public override init() {
|
public override init() {
|
||||||
self.backgroundNode = CallStatusBarBackgroundNode()
|
self.backgroundNode = CallStatusBarBackgroundNode()
|
||||||
@ -213,13 +218,29 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
|||||||
self.subtitleNode = ImmediateAnimatedCountLabelNode()
|
self.subtitleNode = ImmediateAnimatedCountLabelNode()
|
||||||
self.subtitleNode.reverseAnimationDirection = true
|
self.subtitleNode.reverseAnimationDirection = true
|
||||||
self.speakerNode = ImmediateTextNode()
|
self.speakerNode = ImmediateTextNode()
|
||||||
|
|
||||||
|
var updateInHierarchy: ((Bool) -> Void)?
|
||||||
|
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
|
||||||
|
updateInHierarchy?(value)
|
||||||
|
})
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.hierarchyTrackingNode)
|
||||||
|
|
||||||
self.addSubnode(self.backgroundNode)
|
self.addSubnode(self.backgroundNode)
|
||||||
self.addSubnode(self.titleNode)
|
self.addSubnode(self.titleNode)
|
||||||
self.addSubnode(self.subtitleNode)
|
self.addSubnode(self.subtitleNode)
|
||||||
self.addSubnode(self.speakerNode)
|
self.addSubnode(self.speakerNode)
|
||||||
|
|
||||||
|
updateInHierarchy = { [weak self] value in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.isCurrentlyInHierarchy = value
|
||||||
|
if value {
|
||||||
|
strongSelf.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -231,13 +252,17 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
|||||||
|
|
||||||
public func update(content: Content) {
|
public func update(content: Content) {
|
||||||
self.currentContent = content
|
self.currentContent = content
|
||||||
self.update()
|
if self.isCurrentlyInHierarchy {
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func update(size: CGSize) {
|
public override func update(size: CGSize) {
|
||||||
self.currentSize = size
|
self.currentSize = size
|
||||||
self.update()
|
self.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
|
||||||
|
|
||||||
private func update() {
|
private func update() {
|
||||||
guard let size = self.currentSize, let content = self.currentContent else {
|
guard let size = self.currentSize, let content = self.currentContent else {
|
||||||
@ -329,8 +354,10 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
|||||||
currentIsConnected = false
|
currentIsConnected = false
|
||||||
}
|
}
|
||||||
strongSelf.currentIsConnected = currentIsConnected
|
strongSelf.currentIsConnected = currentIsConnected
|
||||||
|
|
||||||
strongSelf.update()
|
if strongSelf.isCurrentlyInHierarchy {
|
||||||
|
strongSelf.update()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
self.audioLevelDisposable.set((combineLatest(call.myAudioLevel, .single([]) |> then(call.audioLevels))
|
self.audioLevelDisposable.set((combineLatest(call.myAudioLevel, .single([]) |> then(call.audioLevels))
|
||||||
@ -351,8 +378,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
|
|||||||
|
|
||||||
var title: String = ""
|
var title: String = ""
|
||||||
var speakerSubtitle: String = ""
|
var speakerSubtitle: String = ""
|
||||||
|
|
||||||
let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
|
|
||||||
let textColor = UIColor.white
|
let textColor = UIColor.white
|
||||||
var segments: [AnimatedCountLabelNode.Segment] = []
|
var segments: [AnimatedCountLabelNode.Segment] = []
|
||||||
var displaySpeakerSubtitle = false
|
var displaySpeakerSubtitle = false
|
||||||
|
@ -1249,6 +1249,9 @@ private final class VoiceBlobView: UIView {
|
|||||||
private(set) var isAnimating = false
|
private(set) var isAnimating = false
|
||||||
|
|
||||||
public typealias BlobRange = (min: CGFloat, max: CGFloat)
|
public typealias BlobRange = (min: CGFloat, max: CGFloat)
|
||||||
|
|
||||||
|
private let hierarchyTrackingNode: HierarchyTrackingNode
|
||||||
|
private var isCurrentlyInHierarchy = true
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
frame: CGRect,
|
frame: CGRect,
|
||||||
@ -1256,6 +1259,11 @@ private final class VoiceBlobView: UIView {
|
|||||||
mediumBlobRange: BlobRange,
|
mediumBlobRange: BlobRange,
|
||||||
bigBlobRange: BlobRange
|
bigBlobRange: BlobRange
|
||||||
) {
|
) {
|
||||||
|
var updateInHierarchy: ((Bool) -> Void)?
|
||||||
|
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
|
||||||
|
updateInHierarchy?(value)
|
||||||
|
})
|
||||||
|
|
||||||
self.maxLevel = maxLevel
|
self.maxLevel = maxLevel
|
||||||
|
|
||||||
self.mediumBlob = BlobView(
|
self.mediumBlob = BlobView(
|
||||||
@ -1278,18 +1286,30 @@ private final class VoiceBlobView: UIView {
|
|||||||
)
|
)
|
||||||
|
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
addSubnode(hierarchyTrackingNode)
|
||||||
|
|
||||||
addSubview(bigBlob)
|
addSubview(bigBlob)
|
||||||
addSubview(mediumBlob)
|
addSubview(mediumBlob)
|
||||||
|
|
||||||
displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
|
displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
|
||||||
guard let strongSelf = self else { return }
|
guard let strongSelf = self else { return }
|
||||||
|
|
||||||
|
if !strongSelf.isCurrentlyInHierarchy {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
|
strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
|
||||||
|
|
||||||
strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel
|
strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel
|
||||||
strongSelf.bigBlob.level = strongSelf.presentationAudioLevel
|
strongSelf.bigBlob.level = strongSelf.presentationAudioLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateInHierarchy = { [weak self] value in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.isCurrentlyInHierarchy = value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -202,6 +202,15 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||||||
var item: VoiceChatFullscreenParticipantItem? {
|
var item: VoiceChatFullscreenParticipantItem? {
|
||||||
return self.layoutParams?.0
|
return self.layoutParams?.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isCurrentlyInHierarchy = false {
|
||||||
|
didSet {
|
||||||
|
if self.isCurrentlyInHierarchy != oldValue {
|
||||||
|
self.highlightNode.isCurrentlyInHierarchy = self.isCurrentlyInHierarchy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var isCurrentlyInHierarchyDisposable: Disposable?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||||
@ -247,7 +256,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||||||
|
|
||||||
self.actionContainerNode = ASDisplayNode()
|
self.actionContainerNode = ASDisplayNode()
|
||||||
self.actionButtonNode = HighlightableButtonNode()
|
self.actionButtonNode = HighlightableButtonNode()
|
||||||
|
|
||||||
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
||||||
|
|
||||||
self.isAccessibilityElement = true
|
self.isAccessibilityElement = true
|
||||||
@ -293,6 +302,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||||||
self.audioLevelDisposable.dispose()
|
self.audioLevelDisposable.dispose()
|
||||||
self.raiseHandTimer?.invalidate()
|
self.raiseHandTimer?.invalidate()
|
||||||
self.silenceTimer?.invalidate()
|
self.silenceTimer?.invalidate()
|
||||||
|
self.isCurrentlyInHierarchyDisposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func selected() {
|
override func selected() {
|
||||||
@ -971,6 +981,16 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
|
|||||||
transition.updateFrame(node: strongSelf.actionButtonNode, frame: animationFrame)
|
transition.updateFrame(node: strongSelf.actionButtonNode, frame: animationFrame)
|
||||||
|
|
||||||
strongSelf.updateIsHighlighted(transition: transition)
|
strongSelf.updateIsHighlighted(transition: transition)
|
||||||
|
|
||||||
|
if strongSelf.isCurrentlyInHierarchyDisposable == nil {
|
||||||
|
strongSelf.isCurrentlyInHierarchyDisposable = (item.context.sharedContext.applicationBindings.applicationInForeground
|
||||||
|
|> deliverOnMainQueue).start(next: { value in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.isCurrentlyInHierarchy = value
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -157,6 +157,9 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
|||||||
private var isExtracted = false
|
private var isExtracted = false
|
||||||
|
|
||||||
private let audioLevelDisposable = MetaDisposable()
|
private let audioLevelDisposable = MetaDisposable()
|
||||||
|
|
||||||
|
private let hierarchyTrackingNode: HierarchyTrackingNode
|
||||||
|
private var isCurrentlyInHierarchy = false
|
||||||
|
|
||||||
init(context: AccountContext) {
|
init(context: AccountContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
@ -201,7 +204,14 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
|||||||
self.placeholderIconNode.contentMode = .scaleAspectFit
|
self.placeholderIconNode.contentMode = .scaleAspectFit
|
||||||
self.placeholderIconNode.displaysAsynchronously = false
|
self.placeholderIconNode.displaysAsynchronously = false
|
||||||
|
|
||||||
|
var updateInHierarchy: ((Bool) -> Void)?
|
||||||
|
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
|
||||||
|
updateInHierarchy?(value)
|
||||||
|
})
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.hierarchyTrackingNode)
|
||||||
|
|
||||||
self.containerNode.addSubnode(self.contextSourceNode)
|
self.containerNode.addSubnode(self.contextSourceNode)
|
||||||
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||||
@ -236,6 +246,13 @@ final class VoiceChatTileItemNode: ASDisplayNode {
|
|||||||
}
|
}
|
||||||
strongSelf.updateIsExtracted(isExtracted, transition: transition)
|
strongSelf.updateIsExtracted(isExtracted, transition: transition)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateInHierarchy = { [weak self] value in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.isCurrentlyInHierarchy = value
|
||||||
|
strongSelf.highlightNode.isCurrentlyInHierarchy = value
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@ -634,9 +651,14 @@ class VoiceChatTileHighlightNode: ASDisplayNode {
|
|||||||
private let maskLayer = CALayer()
|
private let maskLayer = CALayer()
|
||||||
|
|
||||||
private let foregroundGradientLayer = CAGradientLayer()
|
private let foregroundGradientLayer = CAGradientLayer()
|
||||||
|
|
||||||
private let hierarchyTrackingNode: HierarchyTrackingNode
|
var isCurrentlyInHierarchy = false {
|
||||||
private var isCurrentlyInHierarchy = false
|
didSet {
|
||||||
|
if self.isCurrentlyInHierarchy != oldValue && self.isCurrentlyInHierarchy {
|
||||||
|
self.updateAnimations()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var audioLevel: CGFloat = 0.0
|
private var audioLevel: CGFloat = 0.0
|
||||||
private var presentationAudioLevel: CGFloat = 0.0
|
private var presentationAudioLevel: CGFloat = 0.0
|
||||||
@ -650,27 +672,13 @@ class VoiceChatTileHighlightNode: ASDisplayNode {
|
|||||||
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
self.foregroundGradientLayer.startPoint = CGPoint(x: 1.0, y: 0.0)
|
||||||
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
self.foregroundGradientLayer.endPoint = CGPoint(x: 0.0, y: 1.0)
|
||||||
|
|
||||||
var updateInHierarchy: ((Bool) -> Void)?
|
|
||||||
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
|
|
||||||
updateInHierarchy?(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
updateInHierarchy = { [weak self] value in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.isCurrentlyInHierarchy = value
|
|
||||||
strongSelf.updateAnimations()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
|
self.displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
|
||||||
guard let strongSelf = self else { return }
|
guard let strongSelf = self else { return }
|
||||||
|
|
||||||
strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
|
strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addSubnode(self.hierarchyTrackingNode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
@ -733,8 +741,11 @@ class VoiceChatTileHighlightNode: ASDisplayNode {
|
|||||||
animation.toValue = newValue
|
animation.toValue = newValue
|
||||||
|
|
||||||
CATransaction.setCompletionBlock { [weak self] in
|
CATransaction.setCompletionBlock { [weak self] in
|
||||||
if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy {
|
guard let strongSelf = self else {
|
||||||
self?.setupGradientAnimations()
|
return
|
||||||
|
}
|
||||||
|
if strongSelf.isCurrentlyInHierarchy {
|
||||||
|
strongSelf.setupGradientAnimations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -495,8 +495,8 @@ public extension TelegramEngine {
|
|||||||
return _internal_updatePeerDescription(account: self.account, peerId: peerId, description: description)
|
return _internal_updatePeerDescription(account: self.account, peerId: peerId, description: description)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getNextUnreadChannel(peerId: PeerId, filter: ChatListFilterPredicate?) -> Signal<EnginePeer?, NoError> {
|
public func getNextUnreadChannel(peerId: PeerId, filter: ChatListFilterPredicate?) -> Signal<(peer: EnginePeer, unreadCount: Int)?, NoError> {
|
||||||
return self.account.postbox.transaction { transaction -> EnginePeer? in
|
return self.account.postbox.transaction { transaction -> (peer: EnginePeer, unreadCount: Int)? in
|
||||||
var results: [(EnginePeer, Int32)] = []
|
var results: [(EnginePeer, Int32)] = []
|
||||||
|
|
||||||
var peerIds: [PeerId] = []
|
var peerIds: [PeerId] = []
|
||||||
@ -525,7 +525,12 @@ public extension TelegramEngine {
|
|||||||
|
|
||||||
results.sort(by: { $0.1 > $1.1 })
|
results.sort(by: { $0.1 > $1.1 })
|
||||||
|
|
||||||
return results.first?.0
|
if let peer = results.first?.0 {
|
||||||
|
let unreadCount: Int32 = transaction.getCombinedPeerReadState(peer.id)?.count ?? 0
|
||||||
|
return (peer: peer, unreadCount: Int(unreadCount))
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/OverscrollArrow.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/OverscrollArrow.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "drag.svg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
3
submodules/TelegramUI/Images.xcassets/Chat/OverscrollArrow.imageset/drag.svg
vendored
Normal file
3
submodules/TelegramUI/Images.xcassets/Chat/OverscrollArrow.imageset/drag.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.9393 3.93934C14.5251 3.35355 15.4749 3.35355 16.0607 3.93934L26.0607 13.9393C26.6464 14.5251 26.6464 15.4749 26.0607 16.0607C25.4749 16.6464 24.5251 16.6464 23.9393 16.0607L16.5 8.62132V25C16.5 25.8284 15.8284 26.5 15 26.5C14.1716 26.5 13.5 25.8284 13.5 25V8.62132L6.06066 16.0607C5.47487 16.6464 4.52513 16.6464 3.93934 16.0607C3.35355 15.4749 3.35355 14.5251 3.93934 13.9393L13.9393 3.93934Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 526 B |
@ -3324,10 +3324,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.chatDisplayNode.historyNode.offerNextChannelToRead = true
|
strongSelf.chatDisplayNode.historyNode.offerNextChannelToRead = true
|
||||||
strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer
|
strongSelf.chatDisplayNode.historyNode.nextChannelToRead = nextPeer.flatMap { nextPeer -> (peer: EnginePeer, unreadCount: Int) in
|
||||||
|
return (peer: nextPeer.peer, unreadCount: nextPeer.unreadCount)
|
||||||
|
}
|
||||||
strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3
|
strongSelf.chatDisplayNode.historyNode.nextChannelToReadDisplayName = nextChatSuggestionTip >= 3
|
||||||
|
|
||||||
let nextPeerId = nextPeer?.id
|
let nextPeerId = nextPeer?.peer.id
|
||||||
|
|
||||||
if strongSelf.preloadNextChatPeerId != nextPeerId {
|
if strongSelf.preloadNextChatPeerId != nextPeerId {
|
||||||
strongSelf.preloadNextChatPeerId = nextPeerId
|
strongSelf.preloadNextChatPeerId = nextPeerId
|
||||||
@ -7508,7 +7510,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
if let avatarSnapshotState = snapshotState.avatarSnapshotState {
|
if let avatarSnapshotState = snapshotState.avatarSnapshotState {
|
||||||
(self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshotState)
|
(self.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.animateFromSnapshot(avatarSnapshotState)
|
||||||
}
|
}
|
||||||
self.chatDisplayNode.animateFromSnapshot(snapshotState)
|
self.chatDisplayNode.animateFromSnapshot(snapshotState, completion: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.chatDisplayNode.historyNode.preloadPages = true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
self.chatDisplayNode.historyNode.preloadPages = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +107,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
private var titleAccessoryPanelNode: ChatTitleAccessoryPanelNode?
|
private var titleAccessoryPanelNode: ChatTitleAccessoryPanelNode?
|
||||||
|
|
||||||
private var inputPanelNode: ChatInputPanelNode?
|
private var inputPanelNode: ChatInputPanelNode?
|
||||||
|
private(set) var inputPanelOverscrollNode: ChatInputPanelOverscrollNode?
|
||||||
private weak var currentDismissedInputPanelNode: ASDisplayNode?
|
private weak var currentDismissedInputPanelNode: ASDisplayNode?
|
||||||
private var secondaryInputPanelNode: ChatInputPanelNode?
|
private var secondaryInputPanelNode: ChatInputPanelNode?
|
||||||
private(set) var accessoryPanelNode: AccessoryPanelNode?
|
private(set) var accessoryPanelNode: AccessoryPanelNode?
|
||||||
@ -2490,6 +2491,18 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldAllowOverscrollActions: Bool {
|
||||||
|
if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode {
|
||||||
|
if inputPanelNode.isFocused {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !inputPanelNode.text.isEmpty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
final class SnapshotState {
|
final class SnapshotState {
|
||||||
fileprivate let historySnapshotState: ChatHistoryListNode.SnapshotState
|
fileprivate let historySnapshotState: ChatHistoryListNode.SnapshotState
|
||||||
let titleViewSnapshotState: ChatTitleView.SnapshotState?
|
let titleViewSnapshotState: ChatTitleView.SnapshotState?
|
||||||
@ -2534,8 +2547,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateFromSnapshot(_ snapshotState: SnapshotState) {
|
func animateFromSnapshot(_ snapshotState: SnapshotState, completion: @escaping () -> Void) {
|
||||||
self.historyNode.animateFromSnapshot(snapshotState.historySnapshotState)
|
self.historyNode.animateFromSnapshot(snapshotState.historySnapshotState, completion: completion)
|
||||||
self.navigateButtons.animateFromSnapshot(snapshotState.navigationButtonsSnapshotState)
|
self.navigateButtons.animateFromSnapshot(snapshotState.navigationButtonsSnapshotState)
|
||||||
|
|
||||||
if let titleAccessoryPanelSnapshot = snapshotState.titleAccessoryPanelSnapshot {
|
if let titleAccessoryPanelSnapshot = snapshotState.titleAccessoryPanelSnapshot {
|
||||||
@ -2571,4 +2584,54 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var preivousChatInputPanelOverscrollNodeTimestamp: Double = 0.0
|
||||||
|
|
||||||
|
func setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode?) {
|
||||||
|
let directionUp: Bool
|
||||||
|
if let overscrollNode = overscrollNode {
|
||||||
|
if let current = self.inputPanelOverscrollNode {
|
||||||
|
directionUp = current.priority > overscrollNode.priority
|
||||||
|
} else {
|
||||||
|
directionUp = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
directionUp = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let transition: ContainedViewLayoutTransition = .animated(duration: 0.15, curve: .easeInOut)
|
||||||
|
|
||||||
|
let timestamp = CFAbsoluteTimeGetCurrent()
|
||||||
|
if self.preivousChatInputPanelOverscrollNodeTimestamp > timestamp - 0.05 {
|
||||||
|
if let inputPanelOverscrollNode = self.inputPanelOverscrollNode {
|
||||||
|
self.inputPanelOverscrollNode = nil
|
||||||
|
inputPanelOverscrollNode.removeFromSupernode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.preivousChatInputPanelOverscrollNodeTimestamp = timestamp
|
||||||
|
|
||||||
|
if let inputPanelOverscrollNode = self.inputPanelOverscrollNode {
|
||||||
|
self.inputPanelOverscrollNode = nil
|
||||||
|
inputPanelOverscrollNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: directionUp ? -5.0 : 5.0), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true)
|
||||||
|
inputPanelOverscrollNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak inputPanelOverscrollNode] _ in
|
||||||
|
inputPanelOverscrollNode?.removeFromSupernode()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if let inputPanelNode = self.inputPanelNode, let overscrollNode = overscrollNode {
|
||||||
|
self.inputPanelOverscrollNode = overscrollNode
|
||||||
|
inputPanelNode.supernode?.insertSubnode(overscrollNode, aboveSubnode: inputPanelNode)
|
||||||
|
|
||||||
|
overscrollNode.frame = inputPanelNode.frame
|
||||||
|
overscrollNode.update(size: overscrollNode.bounds.size)
|
||||||
|
|
||||||
|
overscrollNode.layer.animatePosition(from: CGPoint(x: 0.0, y: directionUp ? 5.0 : -5.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, additive: true)
|
||||||
|
overscrollNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let inputPanelNode = self.inputPanelNode {
|
||||||
|
transition.updateAlpha(node: inputPanelNode, alpha: overscrollNode == nil ? 1.0 : 0.0)
|
||||||
|
transition.updateSublayerTransformOffset(layer: inputPanelNode.layer, offset: CGPoint(x: 0.0, y: overscrollNode == nil ? 0.0 : -5.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -549,10 +549,11 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
var isSelectionGestureEnabled = true
|
var isSelectionGestureEnabled = true
|
||||||
|
|
||||||
private var overscrollView: ComponentHostView<Empty>?
|
private var overscrollView: ComponentHostView<Empty>?
|
||||||
var nextChannelToRead: EnginePeer?
|
var nextChannelToRead: (peer: EnginePeer, unreadCount: Int)?
|
||||||
var offerNextChannelToRead: Bool = false
|
var offerNextChannelToRead: Bool = false
|
||||||
var nextChannelToReadDisplayName: Bool = false
|
var nextChannelToReadDisplayName: Bool = false
|
||||||
private var currentOverscrollExpandProgress: CGFloat = 0.0
|
private var currentOverscrollExpandProgress: CGFloat = 0.0
|
||||||
|
private var freezeOverscrollControl: Bool = false
|
||||||
private var feedback: HapticFeedback?
|
private var feedback: HapticFeedback?
|
||||||
var openNextChannelToRead: ((EnginePeer) -> Void)?
|
var openNextChannelToRead: ((EnginePeer) -> Void)?
|
||||||
|
|
||||||
@ -628,7 +629,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.preloadPages = true
|
self.preloadPages = false
|
||||||
switch self.mode {
|
switch self.mode {
|
||||||
case .bubbles:
|
case .bubbles:
|
||||||
self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
|
self.transform = CATransform3DMakeRotation(CGFloat(Double.pi), 0.0, 0.0, 1.0)
|
||||||
@ -1170,7 +1171,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let channel = strongSelf.nextChannelToRead, strongSelf.currentOverscrollExpandProgress >= 0.99 {
|
if let channel = strongSelf.nextChannelToRead?.peer, strongSelf.currentOverscrollExpandProgress >= 0.99 {
|
||||||
|
strongSelf.freezeOverscrollControl = true
|
||||||
strongSelf.openNextChannelToRead?(channel)
|
strongSelf.openNextChannelToRead?(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1206,7 +1208,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func maybeUpdateOverscrollAction(offset: CGFloat?) {
|
private func maybeUpdateOverscrollAction(offset: CGFloat?) {
|
||||||
if let offset = offset, offset < 0.0, self.offerNextChannelToRead {
|
if self.freezeOverscrollControl {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let offset = offset, offset < 0.0, self.offerNextChannelToRead, let chatControllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode, chatControllerNode.shouldAllowOverscrollActions {
|
||||||
let overscrollView: ComponentHostView<Empty>
|
let overscrollView: ComponentHostView<Empty>
|
||||||
if let current = self.overscrollView {
|
if let current = self.overscrollView {
|
||||||
overscrollView = current
|
overscrollView = current
|
||||||
@ -1220,24 +1225,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
let expandDistance = max(-offset - 12.0, 0.0)
|
let expandDistance = max(-offset - 12.0, 0.0)
|
||||||
let expandProgress: CGFloat = min(1.0, expandDistance / 90.0)
|
let expandProgress: CGFloat = min(1.0, expandDistance / 90.0)
|
||||||
|
|
||||||
let text: String
|
if let _ = nextChannelToRead {
|
||||||
if let nextChannelToRead = nextChannelToRead {
|
|
||||||
if self.nextChannelToReadDisplayName {
|
|
||||||
if expandProgress >= 0.99 {
|
|
||||||
//TODO:localize
|
|
||||||
text = "Release to go to \(nextChannelToRead.compactDisplayTitle)"
|
|
||||||
} else {
|
|
||||||
text = "Swipe up to go to \(nextChannelToRead.compactDisplayTitle)"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if expandProgress >= 0.99 {
|
|
||||||
//TODO:localize
|
|
||||||
text = "Release to go to the next unread channel"
|
|
||||||
} else {
|
|
||||||
text = "Swipe up to go to the next unread channel"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let previousType = self.currentOverscrollExpandProgress >= 0.99
|
let previousType = self.currentOverscrollExpandProgress >= 0.99
|
||||||
let currentType = expandProgress >= 0.99
|
let currentType = expandProgress >= 0.99
|
||||||
|
|
||||||
@ -1249,27 +1237,45 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.currentOverscrollExpandProgress = expandProgress
|
self.currentOverscrollExpandProgress = expandProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
if expandProgress < 0.1 || self.nextChannelToRead == nil {
|
||||||
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: nil)
|
||||||
|
} else if expandProgress >= 0.99 {
|
||||||
|
//TODO:localize
|
||||||
|
let text: String = "Release to go to the next unread channel"
|
||||||
|
if chatControllerNode.inputPanelOverscrollNode?.text != text {
|
||||||
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(text: text, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 1))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
text = "You have no unread channels"
|
//TODO:localize
|
||||||
|
let text: String = "Swipe up to go to the next unread channel"
|
||||||
|
if chatControllerNode.inputPanelOverscrollNode?.text != text {
|
||||||
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: ChatInputPanelOverscrollNode(text: text, color: self.currentPresentationData.theme.theme.rootController.navigationBar.secondaryTextColor, priority: 2))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let overscrollSize = overscrollView.update(
|
let overscrollSize = overscrollView.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(ChatOverscrollControl(
|
component: AnyComponent(ChatOverscrollControl(
|
||||||
text: text,
|
|
||||||
backgroundColor: selectDateFillStaticColor(theme: self.currentPresentationData.theme.theme, wallpaper: self.currentPresentationData.theme.wallpaper),
|
backgroundColor: selectDateFillStaticColor(theme: self.currentPresentationData.theme.theme, wallpaper: self.currentPresentationData.theme.wallpaper),
|
||||||
foregroundColor: bubbleVariableColor(variableColor: self.currentPresentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: self.currentPresentationData.theme.wallpaper),
|
foregroundColor: bubbleVariableColor(variableColor: self.currentPresentationData.theme.theme.chat.serviceMessage.dateTextColor, wallpaper: self.currentPresentationData.theme.wallpaper),
|
||||||
peer: self.nextChannelToRead,
|
peer: self.nextChannelToRead?.peer,
|
||||||
|
unreadCount: self.nextChannelToRead?.unreadCount ?? 0,
|
||||||
context: self.context,
|
context: self.context,
|
||||||
expandDistance: expandDistance
|
expandDistance: expandDistance
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: self.bounds.width, height: 200.0)
|
containerSize: CGSize(width: self.bounds.width, height: 200.0)
|
||||||
)
|
)
|
||||||
overscrollView.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - overscrollSize.width) / 2.0), y: -offset + self.insets.top - overscrollSize.height - 10.0), size: overscrollSize)
|
overscrollView.frame = CGRect(origin: CGPoint(x: floor((self.bounds.width - overscrollSize.width) / 2.0), y: self.insets.top), size: overscrollSize)
|
||||||
} else if let overscrollView = self.overscrollView {
|
} else if let overscrollView = self.overscrollView {
|
||||||
self.overscrollView = nil
|
self.overscrollView = nil
|
||||||
overscrollView.removeFromSuperview()
|
overscrollView.removeFromSuperview()
|
||||||
|
|
||||||
|
if let chatControllerNode = self.controllerInteraction.chatControllerNode() as? ChatControllerNode {
|
||||||
|
chatControllerNode.setChatInputPanelOverscrollNode(overscrollNode: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2463,7 +2469,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func animateFromSnapshot(_ snapshotState: SnapshotState) {
|
func animateFromSnapshot(_ snapshotState: SnapshotState, completion: @escaping () -> Void) {
|
||||||
var snapshotTopInset: CGFloat = 0.0
|
var snapshotTopInset: CGFloat = 0.0
|
||||||
var snapshotBottomInset: CGFloat = 0.0
|
var snapshotBottomInset: CGFloat = 0.0
|
||||||
self.forEachItemNode { itemNode in
|
self.forEachItemNode { itemNode in
|
||||||
@ -2485,6 +2491,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
|||||||
|
|
||||||
snapshotParentView.layer.animatePosition(from: CGPoint(x: 0.0, y: 0.0), to: CGPoint(x: 0.0, y: -self.view.bounds.height - snapshotState.snapshotBottomInset - snapshotTopInset), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak snapshotParentView] _ in
|
snapshotParentView.layer.animatePosition(from: CGPoint(x: 0.0, y: 0.0), to: CGPoint(x: 0.0, y: -self.view.bounds.height - snapshotState.snapshotBottomInset - snapshotTopInset), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak snapshotParentView] _ in
|
||||||
snapshotParentView?.removeFromSuperview()
|
snapshotParentView?.removeFromSuperview()
|
||||||
|
completion()
|
||||||
})
|
})
|
||||||
|
|
||||||
self.view.layer.animatePosition(from: CGPoint(x: 0.0, y: self.view.bounds.height + snapshotTopInset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
|
self.view.layer.animatePosition(from: CGPoint(x: 0.0, y: self.view.bounds.height + snapshotTopInset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: true, additive: true)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import ComponentFlow
|
import ComponentFlow
|
||||||
import Display
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
import TelegramCore
|
import TelegramCore
|
||||||
import Postbox
|
import Postbox
|
||||||
import AccountContext
|
import AccountContext
|
||||||
@ -196,6 +197,9 @@ final class CheckComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class View: UIView {
|
final class View: UIView {
|
||||||
|
private var currentValue: CGFloat?
|
||||||
|
private var animator: DisplayLinkAnimator?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(frame: CGRect())
|
super.init(frame: CGRect())
|
||||||
}
|
}
|
||||||
@ -204,10 +208,8 @@ final class CheckComponent: Component {
|
|||||||
preconditionFailure()
|
preconditionFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
private func updateContent(size: CGSize, color: UIColor, lineWidth: CGFloat, value: CGFloat) {
|
||||||
func draw(context: CGContext) {
|
func draw(context: CGContext) {
|
||||||
let size = availableSize
|
|
||||||
|
|
||||||
let diameter = size.width
|
let diameter = size.width
|
||||||
|
|
||||||
let factor = diameter / 50.0
|
let factor = diameter / 50.0
|
||||||
@ -215,19 +217,17 @@ final class CheckComponent: Component {
|
|||||||
context.saveGState()
|
context.saveGState()
|
||||||
|
|
||||||
context.setBlendMode(.normal)
|
context.setBlendMode(.normal)
|
||||||
context.setFillColor(component.color.cgColor)
|
context.setFillColor(color.cgColor)
|
||||||
context.setStrokeColor(component.color.cgColor)
|
context.setStrokeColor(color.cgColor)
|
||||||
|
|
||||||
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
|
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
|
||||||
|
|
||||||
let lineWidth = component.lineWidth
|
|
||||||
|
|
||||||
context.setLineWidth(max(1.7, lineWidth * factor))
|
context.setLineWidth(max(1.7, lineWidth * factor))
|
||||||
context.setLineCap(.round)
|
context.setLineCap(.round)
|
||||||
context.setLineJoin(.round)
|
context.setLineJoin(.round)
|
||||||
context.setMiterLimit(10.0)
|
context.setMiterLimit(10.0)
|
||||||
|
|
||||||
let progress = component.value
|
let progress = value
|
||||||
let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
|
let firstSegment: CGFloat = max(0.0, min(1.0, progress * 3.0))
|
||||||
|
|
||||||
var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor)
|
var s = CGPoint(x: center.x - 10.0 * factor, y: center.y + 1.0 * factor)
|
||||||
@ -257,7 +257,7 @@ final class CheckComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if #available(iOS 10.0, *) {
|
if #available(iOS 10.0, *) {
|
||||||
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: availableSize))
|
let renderer = UIGraphicsImageRenderer(bounds: CGRect(origin: CGPoint(), size: size))
|
||||||
let image = renderer.image { context in
|
let image = renderer.image { context in
|
||||||
UIGraphicsPushContext(context.cgContext)
|
UIGraphicsPushContext(context.cgContext)
|
||||||
draw(context: context.cgContext)
|
draw(context: context.cgContext)
|
||||||
@ -265,11 +265,37 @@ final class CheckComponent: Component {
|
|||||||
}
|
}
|
||||||
self.layer.contents = image.cgImage
|
self.layer.contents = image.cgImage
|
||||||
} else {
|
} else {
|
||||||
UIGraphicsBeginImageContextWithOptions(availableSize, false, 0.0)
|
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||||
draw(context: UIGraphicsGetCurrentContext()!)
|
draw(context: UIGraphicsGetCurrentContext()!)
|
||||||
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
|
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
|
||||||
UIGraphicsEndImageContext()
|
UIGraphicsEndImageContext()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: CheckComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||||
|
if let currentValue = self.currentValue, currentValue != component.value, case .curve = transition.animation {
|
||||||
|
self.animator?.invalidate()
|
||||||
|
|
||||||
|
let animator = DisplayLinkAnimator(duration: 0.15, from: currentValue, to: component.value, update: { [weak self] value in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.updateContent(size: availableSize, color: component.color, lineWidth: component.lineWidth, value: value)
|
||||||
|
}, completion: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.animator?.invalidate()
|
||||||
|
strongSelf.animator = nil
|
||||||
|
})
|
||||||
|
self.animator = animator
|
||||||
|
} else {
|
||||||
|
if self.animator == nil {
|
||||||
|
self.updateContent(size: availableSize, color: component.color, lineWidth: component.lineWidth, value: component.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.currentValue = component.value
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
}
|
}
|
||||||
@ -284,13 +310,101 @@ final class CheckComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class BadgeComponent: CombinedComponent {
|
||||||
|
let count: Int
|
||||||
|
let backgroundColor: UIColor
|
||||||
|
let foregroundColor: UIColor
|
||||||
|
|
||||||
|
init(count: Int, backgroundColor: UIColor, foregroundColor: UIColor) {
|
||||||
|
self.count = count
|
||||||
|
self.backgroundColor = backgroundColor
|
||||||
|
self.foregroundColor = foregroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: BadgeComponent, rhs: BadgeComponent) -> Bool {
|
||||||
|
if lhs.count != rhs.count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static var body: Body {
|
||||||
|
let background = Child(BlurredRoundedRectangle.self)
|
||||||
|
let text = Child(Text.self)
|
||||||
|
|
||||||
|
return { context in
|
||||||
|
let text = text.update(
|
||||||
|
component: Text(
|
||||||
|
text: "\(context.component.count)",
|
||||||
|
font: Font.regular(13.0),
|
||||||
|
color: context.component.foregroundColor
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: 100.0, height: 100.0),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
|
||||||
|
let height = text.size.height + 4.0
|
||||||
|
let backgroundSize = CGSize(width: max(height, text.size.width + 8.0), height: height)
|
||||||
|
|
||||||
|
let background = background.update(
|
||||||
|
component: BlurredRoundedRectangle(color: context.component.backgroundColor),
|
||||||
|
availableSize: backgroundSize,
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(background
|
||||||
|
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
context.add(text
|
||||||
|
.position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
return backgroundSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class AvatarComponent: Component {
|
final class AvatarComponent: Component {
|
||||||
|
final class Badge: Equatable {
|
||||||
|
let count: Int
|
||||||
|
let backgroundColor: UIColor
|
||||||
|
let foregroundColor: UIColor
|
||||||
|
|
||||||
|
init(count: Int, backgroundColor: UIColor, foregroundColor: UIColor) {
|
||||||
|
self.count = count
|
||||||
|
self.backgroundColor = backgroundColor
|
||||||
|
self.foregroundColor = foregroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: Badge, rhs: Badge) -> Bool {
|
||||||
|
if lhs.count != rhs.count {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let peer: EnginePeer
|
let peer: EnginePeer
|
||||||
|
let badge: Badge?
|
||||||
|
|
||||||
init(context: AccountContext, peer: EnginePeer) {
|
init(context: AccountContext, peer: EnginePeer, badge: Badge?) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
|
self.badge = badge
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
|
||||||
@ -300,14 +414,20 @@ final class AvatarComponent: Component {
|
|||||||
if lhs.peer != rhs.peer {
|
if lhs.peer != rhs.peer {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.badge != rhs.badge {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
final class View: UIView {
|
final class View: UIView {
|
||||||
private let avatarNode: AvatarNode
|
private let avatarNode: AvatarNode
|
||||||
|
private let avatarMask: CAShapeLayer
|
||||||
|
private var badgeView: ComponentHostView<Empty>?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
|
||||||
|
self.avatarMask = CAShapeLayer()
|
||||||
|
|
||||||
super.init(frame: CGRect())
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
@ -322,6 +442,56 @@ final class AvatarComponent: Component {
|
|||||||
self.avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize)
|
self.avatarNode.frame = CGRect(origin: CGPoint(), size: availableSize)
|
||||||
self.avatarNode.setPeer(context: component.context, theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, peer: component.peer, synchronousLoad: true)
|
self.avatarNode.setPeer(context: component.context, theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme, peer: component.peer, synchronousLoad: true)
|
||||||
|
|
||||||
|
if let badge = component.badge {
|
||||||
|
let badgeView: ComponentHostView<Empty>
|
||||||
|
let animateIn = self.badgeView == nil
|
||||||
|
if let current = self.badgeView {
|
||||||
|
badgeView = current
|
||||||
|
} else {
|
||||||
|
badgeView = ComponentHostView<Empty>()
|
||||||
|
self.badgeView = badgeView
|
||||||
|
self.addSubview(badgeView)
|
||||||
|
}
|
||||||
|
|
||||||
|
let badgeSize = badgeView.update(
|
||||||
|
transition: .immediate,
|
||||||
|
component: AnyComponent(BadgeComponent(
|
||||||
|
count: badge.count,
|
||||||
|
backgroundColor: badge.backgroundColor,
|
||||||
|
foregroundColor: badge.foregroundColor
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: 100.0, height: 100.0
|
||||||
|
))
|
||||||
|
let badgeDiameter = min(badgeSize.width, badgeSize.height)
|
||||||
|
let circlePoint = CGPoint(
|
||||||
|
x: availableSize.width / 2.0 + cos(CGFloat.pi / 4) * availableSize.width / 2.0,
|
||||||
|
y: availableSize.height / 2.0 - sin(CGFloat.pi / 4) * availableSize.width / 2.0
|
||||||
|
)
|
||||||
|
badgeView.frame = CGRect(origin: CGPoint(x: circlePoint.x - badgeDiameter / 2.0, y: circlePoint.y - badgeDiameter / 2.0), size: badgeSize)
|
||||||
|
|
||||||
|
self.avatarMask.frame = self.avatarNode.bounds
|
||||||
|
self.avatarMask.fillRule = .evenOdd
|
||||||
|
|
||||||
|
let path = UIBezierPath(rect: self.avatarMask.bounds)
|
||||||
|
path.append(UIBezierPath(roundedRect: badgeView.frame.insetBy(dx: -2.0, dy: -2.0), cornerRadius: badgeDiameter / 2.0))
|
||||||
|
self.avatarMask.path = path.cgPath
|
||||||
|
|
||||||
|
self.avatarNode.view.layer.mask = self.avatarMask
|
||||||
|
|
||||||
|
if animateIn {
|
||||||
|
badgeView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.14)
|
||||||
|
}
|
||||||
|
} else if let badgeView = self.badgeView {
|
||||||
|
self.badgeView = nil
|
||||||
|
|
||||||
|
badgeView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.14, removeOnCompletion: false, completion: { [weak badgeView] _ in
|
||||||
|
badgeView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.avatarNode.view.layer.mask = nil
|
||||||
|
}
|
||||||
|
|
||||||
return availableSize
|
return availableSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -335,32 +505,32 @@ final class AvatarComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ChatOverscrollControl: CombinedComponent {
|
final class OverscrollContentsComponent: Component {
|
||||||
let text: String
|
let context: AccountContext
|
||||||
let backgroundColor: UIColor
|
let backgroundColor: UIColor
|
||||||
let foregroundColor: UIColor
|
let foregroundColor: UIColor
|
||||||
let peer: EnginePeer?
|
let peer: EnginePeer?
|
||||||
let context: AccountContext
|
let unreadCount: Int
|
||||||
let expandDistance: CGFloat
|
let expandOffset: CGFloat
|
||||||
|
|
||||||
init(
|
init(
|
||||||
text: String,
|
context: AccountContext,
|
||||||
backgroundColor: UIColor,
|
backgroundColor: UIColor,
|
||||||
foregroundColor: UIColor,
|
foregroundColor: UIColor,
|
||||||
peer: EnginePeer?,
|
peer: EnginePeer?,
|
||||||
context: AccountContext,
|
unreadCount: Int,
|
||||||
expandDistance: CGFloat
|
expandOffset: CGFloat
|
||||||
) {
|
) {
|
||||||
self.text = text
|
self.context = context
|
||||||
self.backgroundColor = backgroundColor
|
self.backgroundColor = backgroundColor
|
||||||
self.foregroundColor = foregroundColor
|
self.foregroundColor = foregroundColor
|
||||||
self.peer = peer
|
self.peer = peer
|
||||||
self.context = context
|
self.unreadCount = unreadCount
|
||||||
self.expandDistance = expandDistance
|
self.expandOffset = expandOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool {
|
static func ==(lhs: OverscrollContentsComponent, rhs: OverscrollContentsComponent) -> Bool {
|
||||||
if lhs.text != rhs.text {
|
if lhs.context !== rhs.context {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
|
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
|
||||||
@ -372,6 +542,265 @@ final class ChatOverscrollControl: CombinedComponent {
|
|||||||
if lhs.peer != rhs.peer {
|
if lhs.peer != rhs.peer {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.unreadCount != rhs.unreadCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.expandOffset != rhs.expandOffset {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
final class View: UIView {
|
||||||
|
private let backgroundScalingContainer: ASDisplayNode
|
||||||
|
private let backgroundNode: NavigationBackgroundNode
|
||||||
|
private let backgroundClippingNode: ASDisplayNode
|
||||||
|
private let avatarView = ComponentHostView<Empty>()
|
||||||
|
private let checkView = ComponentHostView<Empty>()
|
||||||
|
private let arrowNode: ASImageNode
|
||||||
|
private let avatarScalingContainer: ASDisplayNode
|
||||||
|
private let avatarExtraScalingContainer: ASDisplayNode
|
||||||
|
private let avatarOffsetContainer: ASDisplayNode
|
||||||
|
private let arrowOffsetContainer: ASDisplayNode
|
||||||
|
|
||||||
|
private let titleOffsetContainer: ASDisplayNode
|
||||||
|
private let titleBackgroundNode: NavigationBackgroundNode
|
||||||
|
private let titleNode: ImmediateTextNode
|
||||||
|
|
||||||
|
private var isFullyExpanded: Bool = false
|
||||||
|
|
||||||
|
private var validForegroundColor: UIColor?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.backgroundScalingContainer = ASDisplayNode()
|
||||||
|
self.backgroundNode = NavigationBackgroundNode(color: .clear)
|
||||||
|
self.backgroundNode.clipsToBounds = true
|
||||||
|
self.backgroundClippingNode = ASDisplayNode()
|
||||||
|
self.backgroundClippingNode.clipsToBounds = true
|
||||||
|
self.arrowNode = ASImageNode()
|
||||||
|
self.avatarScalingContainer = ASDisplayNode()
|
||||||
|
self.avatarExtraScalingContainer = ASDisplayNode()
|
||||||
|
self.avatarOffsetContainer = ASDisplayNode()
|
||||||
|
self.arrowOffsetContainer = ASDisplayNode()
|
||||||
|
|
||||||
|
self.titleOffsetContainer = ASDisplayNode()
|
||||||
|
self.titleBackgroundNode = NavigationBackgroundNode(color: .clear)
|
||||||
|
self.titleNode = ImmediateTextNode()
|
||||||
|
|
||||||
|
super.init(frame: CGRect())
|
||||||
|
|
||||||
|
self.addSubview(self.backgroundScalingContainer.view)
|
||||||
|
|
||||||
|
self.backgroundClippingNode.addSubnode(self.backgroundNode)
|
||||||
|
self.backgroundScalingContainer.addSubnode(self.backgroundClippingNode)
|
||||||
|
|
||||||
|
self.avatarScalingContainer.view.addSubview(self.avatarView)
|
||||||
|
self.avatarScalingContainer.view.addSubview(self.checkView)
|
||||||
|
self.avatarExtraScalingContainer.addSubnode(self.avatarScalingContainer)
|
||||||
|
self.avatarOffsetContainer.addSubnode(self.avatarExtraScalingContainer)
|
||||||
|
self.arrowOffsetContainer.addSubnode(self.arrowNode)
|
||||||
|
self.backgroundNode.addSubnode(self.arrowOffsetContainer)
|
||||||
|
self.addSubnode(self.avatarOffsetContainer)
|
||||||
|
|
||||||
|
self.titleOffsetContainer.addSubnode(self.titleBackgroundNode)
|
||||||
|
self.titleOffsetContainer.addSubnode(self.titleNode)
|
||||||
|
self.addSubnode(self.titleOffsetContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
preconditionFailure()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: OverscrollContentsComponent, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||||
|
if let _ = component.peer {
|
||||||
|
self.avatarView.isHidden = false
|
||||||
|
self.checkView.isHidden = true
|
||||||
|
} else {
|
||||||
|
self.avatarView.isHidden = true
|
||||||
|
self.checkView.isHidden = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let fullHeight: CGFloat = 90.0
|
||||||
|
let backgroundWidth: CGFloat = 50.0
|
||||||
|
let minBackgroundHeight: CGFloat = backgroundWidth + 34.0
|
||||||
|
let avatarInset: CGFloat = 6.0
|
||||||
|
|
||||||
|
let isFullyExpanded = component.expandOffset >= fullHeight
|
||||||
|
|
||||||
|
let backgroundHeight: CGFloat = max(minBackgroundHeight, min(fullHeight, component.expandOffset))
|
||||||
|
|
||||||
|
let backgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - backgroundWidth) / 2.0), y: fullHeight - backgroundHeight), size: CGSize(width: backgroundWidth, height: backgroundHeight))
|
||||||
|
|
||||||
|
let expandProgress: CGFloat = max(0.1, min(1.0, component.expandOffset / minBackgroundHeight))
|
||||||
|
let alphaProgress: CGFloat = max(0.0, min(1.0, component.expandOffset / 10.0))
|
||||||
|
|
||||||
|
let maxAvatarScale: CGFloat = 1.0
|
||||||
|
let avatarExpandProgress: CGFloat = max(0.01, min(maxAvatarScale, component.expandOffset / fullHeight))
|
||||||
|
|
||||||
|
transition.setAlpha(view: self.backgroundScalingContainer.view, alpha: alphaProgress)
|
||||||
|
transition.setFrame(view: self.backgroundScalingContainer.view, frame: CGRect(origin: CGPoint(x: floor(availableSize.width / 2.0), y: fullHeight), size: CGSize(width: 0.0, height: 0.0)))
|
||||||
|
transition.setSublayerTransform(view: self.backgroundScalingContainer.view, transform: CATransform3DMakeScale(expandProgress, expandProgress, 1.0))
|
||||||
|
|
||||||
|
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(x: 0.0, y: fullHeight - backgroundFrame.size.height), size: backgroundFrame.size))
|
||||||
|
self.backgroundNode.updateColor(color: component.backgroundColor, transition: .immediate)
|
||||||
|
self.backgroundNode.update(size: backgroundFrame.size, cornerRadius: backgroundWidth / 2.0, transition: .immediate)
|
||||||
|
|
||||||
|
self.avatarView.frame = CGRect(origin: CGPoint(x: floor(-backgroundWidth / 2.0), y: floor(-backgroundWidth / 2.0)), size: CGSize(width: backgroundWidth, height: backgroundWidth))
|
||||||
|
|
||||||
|
transition.setFrame(view: self.avatarOffsetContainer.view, frame: CGRect())
|
||||||
|
transition.setFrame(view: self.avatarScalingContainer.view, frame: CGRect())
|
||||||
|
transition.setFrame(view: self.avatarExtraScalingContainer.view, frame: CGRect(origin: CGPoint(x: availableSize.width / 2.0, y: fullHeight - backgroundWidth / 2.0), size: CGSize()).offsetBy(dx: 0.0, dy: (1.0 - avatarExpandProgress) * backgroundWidth * 0.5))
|
||||||
|
transition.setSublayerTransform(view: self.avatarScalingContainer.view, transform: CATransform3DMakeScale(avatarExpandProgress, avatarExpandProgress, 1.0))
|
||||||
|
|
||||||
|
let titleText: String
|
||||||
|
if let peer = component.peer {
|
||||||
|
titleText = peer.compactDisplayTitle
|
||||||
|
} else {
|
||||||
|
//TODO:localize
|
||||||
|
titleText = "You have no unread channels"
|
||||||
|
}
|
||||||
|
self.titleNode.attributedText = NSAttributedString(string: titleText, font: Font.semibold(13.0), textColor: component.foregroundColor)
|
||||||
|
let titleSize = self.titleNode.updateLayout(CGSize(width: availableSize.width - 32.0, height: 100.0))
|
||||||
|
let titleBackgroundSize = CGSize(width: titleSize.width + 18.0, height: titleSize.height + 8.0)
|
||||||
|
let titleBackgroundFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleBackgroundSize.width) / 2.0), y: fullHeight - titleBackgroundSize.height - 8.0), size: titleBackgroundSize)
|
||||||
|
self.titleBackgroundNode.frame = titleBackgroundFrame
|
||||||
|
self.titleBackgroundNode.updateColor(color: component.backgroundColor, transition: .immediate)
|
||||||
|
self.titleBackgroundNode.update(size: titleBackgroundFrame.size, cornerRadius: titleBackgroundFrame.size.height / 2.0, transition: .immediate)
|
||||||
|
self.titleNode.frame = titleSize.centered(in: titleBackgroundFrame)
|
||||||
|
|
||||||
|
let backgroundClippingFrame = CGRect(origin: CGPoint(x: floor(-backgroundWidth / 2.0), y: -fullHeight), size: CGSize(width: backgroundWidth, height: isFullyExpanded ? backgroundWidth : fullHeight))
|
||||||
|
self.backgroundClippingNode.cornerRadius = backgroundWidth / 2.0
|
||||||
|
self.backgroundNode.cornerRadius = backgroundWidth / 2.0
|
||||||
|
|
||||||
|
if !(self.validForegroundColor?.isEqual(component.foregroundColor) ?? false) {
|
||||||
|
self.validForegroundColor = component.foregroundColor
|
||||||
|
self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/OverscrollArrow"), color: component.foregroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let arrowImage = self.arrowNode.image {
|
||||||
|
self.arrowNode.frame = CGRect(origin: CGPoint(x: floor((backgroundWidth - arrowImage.size.width) / 2.0), y: floor((backgroundWidth - arrowImage.size.width) / 2.0)), size: arrowImage.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
let transformTransition: ContainedViewLayoutTransition
|
||||||
|
if self.isFullyExpanded != isFullyExpanded {
|
||||||
|
self.isFullyExpanded = isFullyExpanded
|
||||||
|
transformTransition = .animated(duration: 0.12, curve: .easeInOut)
|
||||||
|
|
||||||
|
if isFullyExpanded {
|
||||||
|
func animateBounce(layer: CALayer) {
|
||||||
|
layer.animateScale(from: 1.0, to: 1.1, duration: 0.1, removeOnCompletion: false, completion: { [weak layer] _ in
|
||||||
|
layer?.animateScale(from: 1.1, to: 1.0, duration: 0.14, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
animateBounce(layer: self.backgroundClippingNode.layer)
|
||||||
|
animateBounce(layer: self.avatarExtraScalingContainer.layer)
|
||||||
|
|
||||||
|
func animateOffsetBounce(layer: CALayer) {
|
||||||
|
let firstAnimation = layer.makeAnimation(from: 0.0 as NSNumber, to: -5.0 as NSNumber, keyPath: "transform.translation.y", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.1, removeOnCompletion: false, additive: true, completion: { [weak layer] _ in
|
||||||
|
guard let layer = layer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let secondAnimation = layer.makeAnimation(from: -5.0 as NSNumber, to: 0.0 as NSNumber, keyPath: "transform.translation.y", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.14, removeOnCompletion: true, additive: true)
|
||||||
|
layer.add(secondAnimation, forKey: "bounceY")
|
||||||
|
})
|
||||||
|
layer.add(firstAnimation, forKey: "bounceY")
|
||||||
|
}
|
||||||
|
|
||||||
|
animateOffsetBounce(layer: self.layer)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
transformTransition = .immediate
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkSize: CGFloat = 50.0
|
||||||
|
self.checkView.frame = CGRect(origin: CGPoint(x: floor(-checkSize / 2.0), y: floor(-checkSize / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||||
|
let _ = self.checkView.update(
|
||||||
|
transition: Transition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none),
|
||||||
|
component: AnyComponent(CheckComponent(
|
||||||
|
color: component.foregroundColor,
|
||||||
|
lineWidth: 3.0,
|
||||||
|
value: isFullyExpanded ? 1.0 : 0.0
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: checkSize, height: checkSize)
|
||||||
|
)
|
||||||
|
|
||||||
|
if let peer = component.peer {
|
||||||
|
let _ = self.avatarView.update(
|
||||||
|
transition: Transition(animation: transformTransition.isAnimated ? .curve(duration: 0.2, curve: .easeInOut) : .none),
|
||||||
|
component: AnyComponent(AvatarComponent(
|
||||||
|
context: component.context,
|
||||||
|
peer: peer,
|
||||||
|
badge: isFullyExpanded ? AvatarComponent.Badge(count: component.unreadCount, backgroundColor: component.backgroundColor, foregroundColor: component.foregroundColor) : nil
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: self.avatarView.bounds.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
transformTransition.updateAlpha(node: self.backgroundNode, alpha: (isFullyExpanded && component.peer != nil) ? 0.0 : 1.0)
|
||||||
|
transformTransition.updateAlpha(node: self.arrowNode, alpha: isFullyExpanded ? 0.0 : 1.0)
|
||||||
|
|
||||||
|
transformTransition.updateSublayerTransformOffset(layer: self.avatarOffsetContainer.layer, offset: CGPoint(x: 0.0, y: isFullyExpanded ? -(fullHeight - backgroundWidth) : 0.0))
|
||||||
|
transformTransition.updateSublayerTransformOffset(layer: self.arrowOffsetContainer.layer, offset: CGPoint(x: 0.0, y: isFullyExpanded ? -(fullHeight - backgroundWidth) : 0.0))
|
||||||
|
|
||||||
|
transformTransition.updateSublayerTransformOffset(layer: self.titleOffsetContainer.layer, offset: CGPoint(x: 0.0, y: isFullyExpanded ? 0.0 : (titleBackgroundSize.height + 50.0)))
|
||||||
|
|
||||||
|
transformTransition.updateSublayerTransformScale(node: self.avatarExtraScalingContainer, scale: isFullyExpanded ? 1.0 : ((backgroundWidth - avatarInset * 2.0) / backgroundWidth))
|
||||||
|
|
||||||
|
transformTransition.updateFrame(node: self.backgroundClippingNode, frame: backgroundClippingFrame)
|
||||||
|
|
||||||
|
return CGSize(width: availableSize.width, height: fullHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeView() -> View {
|
||||||
|
return View()
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(view: View, availableSize: CGSize, transition: Transition) -> CGSize {
|
||||||
|
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ChatOverscrollControl: CombinedComponent {
|
||||||
|
let backgroundColor: UIColor
|
||||||
|
let foregroundColor: UIColor
|
||||||
|
let peer: EnginePeer?
|
||||||
|
let unreadCount: Int
|
||||||
|
let context: AccountContext
|
||||||
|
let expandDistance: CGFloat
|
||||||
|
|
||||||
|
init(
|
||||||
|
backgroundColor: UIColor,
|
||||||
|
foregroundColor: UIColor,
|
||||||
|
peer: EnginePeer?,
|
||||||
|
unreadCount: Int,
|
||||||
|
context: AccountContext,
|
||||||
|
expandDistance: CGFloat
|
||||||
|
) {
|
||||||
|
self.backgroundColor = backgroundColor
|
||||||
|
self.foregroundColor = foregroundColor
|
||||||
|
self.peer = peer
|
||||||
|
self.unreadCount = unreadCount
|
||||||
|
self.context = context
|
||||||
|
self.expandDistance = expandDistance
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool {
|
||||||
|
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !lhs.foregroundColor.isEqual(rhs.foregroundColor) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.peer != rhs.peer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.unreadCount != rhs.unreadCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.context !== rhs.context {
|
if lhs.context !== rhs.context {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -382,156 +811,51 @@ final class ChatOverscrollControl: CombinedComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var body: Body {
|
static var body: Body {
|
||||||
let avatarBackground = Child(BlurredRoundedRectangle.self)
|
let contents = Child(OverscrollContentsComponent.self)
|
||||||
let avatarExpandProgress = Child(RadialProgressComponent.self)
|
|
||||||
let avatarCheck = Child(CheckComponent.self)
|
|
||||||
let avatar = Child(AvatarComponent.self)
|
|
||||||
let textBackground = Child(BlurredRoundedRectangle.self)
|
|
||||||
let text = Child(Text.self)
|
|
||||||
|
|
||||||
return { context in
|
return { context in
|
||||||
let text = text.update(
|
let contents = contents.update(
|
||||||
component: Text(
|
component: OverscrollContentsComponent(
|
||||||
text: context.component.text,
|
context: context.component.context,
|
||||||
font: Font.regular(12.0),
|
backgroundColor: context.component.backgroundColor,
|
||||||
color: context.component.foregroundColor
|
foregroundColor: context.component.foregroundColor,
|
||||||
|
peer: context.component.peer,
|
||||||
|
unreadCount: context.component.unreadCount,
|
||||||
|
expandOffset: context.component.expandDistance
|
||||||
),
|
),
|
||||||
availableSize: CGSize(width: context.availableSize.width, height: 100.0),
|
availableSize: context.availableSize,
|
||||||
transition: context.transition
|
transition: context.transition
|
||||||
)
|
)
|
||||||
|
|
||||||
let textHorizontalPadding: CGFloat = 6.0
|
let size = CGSize(width: context.availableSize.width, height: contents.size.height)
|
||||||
let textVerticalPadding: CGFloat = 2.0
|
|
||||||
let avatarSize: CGFloat = 48.0
|
|
||||||
let avatarPadding: CGFloat = 8.0
|
|
||||||
let avatarTextSpacing: CGFloat = 8.0
|
|
||||||
let avatarProgressPadding: CGFloat = 2.5
|
|
||||||
|
|
||||||
let avatarBackgroundSize: CGFloat = context.component.peer != nil ? (avatarSize + avatarPadding * 2.0) : avatarSize
|
context.add(contents
|
||||||
|
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
|
||||||
let avatarBackground = avatarBackground.update(
|
|
||||||
component: BlurredRoundedRectangle(
|
|
||||||
color: context.component.backgroundColor
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: avatarBackgroundSize, height: avatarBackgroundSize),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
|
|
||||||
let avatarCheck = Condition(context.component.peer == nil, { () -> _UpdatedChildComponent in
|
|
||||||
let avatarCheckSize = avatarBackgroundSize + 2.0
|
|
||||||
|
|
||||||
return avatarCheck.update(
|
|
||||||
component: CheckComponent(
|
|
||||||
color: context.component.foregroundColor,
|
|
||||||
lineWidth: 2.5,
|
|
||||||
value: 1.0
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: avatarCheckSize, height: avatarCheckSize),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
let progressSize = avatarBackground.size.width - avatarProgressPadding * 2.0
|
|
||||||
|
|
||||||
let halfDistance = progressSize
|
|
||||||
let quarterDistance = halfDistance / 2.0
|
|
||||||
|
|
||||||
let clippedDistance = max(0.0, min(halfDistance * 2.0, context.component.expandDistance))
|
|
||||||
|
|
||||||
var mappedProgress: CGFloat
|
|
||||||
if clippedDistance <= quarterDistance {
|
|
||||||
mappedProgress = acos(1.0 - clippedDistance / quarterDistance) / (CGFloat.pi * 2.0)
|
|
||||||
} else if clippedDistance <= halfDistance {
|
|
||||||
let sectionDistance = halfDistance - clippedDistance
|
|
||||||
mappedProgress = 0.25 + asin(1.0 - sectionDistance / quarterDistance) / (CGFloat.pi * 2.0)
|
|
||||||
} else {
|
|
||||||
let restDistance = clippedDistance - halfDistance
|
|
||||||
mappedProgress = min(1.0, 0.5 + restDistance / 60.0)
|
|
||||||
}
|
|
||||||
mappedProgress = max(0.01, mappedProgress)
|
|
||||||
|
|
||||||
let avatarExpandProgress = avatarExpandProgress.update(
|
|
||||||
component: RadialProgressComponent(
|
|
||||||
color: context.component.foregroundColor,
|
|
||||||
lineWidth: 2.5,
|
|
||||||
value: context.component.peer == nil ? 0.0 : mappedProgress
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: progressSize, height: progressSize),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
|
|
||||||
let textBackground = textBackground.update(
|
|
||||||
component: BlurredRoundedRectangle(
|
|
||||||
color: context.component.backgroundColor
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: text.size.width + textHorizontalPadding * 2.0, height: text.size.height + textVerticalPadding * 2.0),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
|
|
||||||
let size = CGSize(width: context.availableSize.width, height: avatarBackground.size.height + avatarTextSpacing + textBackground.size.height)
|
|
||||||
|
|
||||||
let avatarBackgroundFrame = avatarBackground.size.topCentered(in: CGRect(origin: CGPoint(), size: size))
|
|
||||||
|
|
||||||
let avatar = context.component.peer.flatMap { peer in
|
|
||||||
avatar.update(
|
|
||||||
component: AvatarComponent(
|
|
||||||
context: context.component.context,
|
|
||||||
peer: peer
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: avatarSize, height: avatarSize),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.add(avatarBackground
|
|
||||||
.position(CGPoint(
|
|
||||||
x: avatarBackgroundFrame.midX,
|
|
||||||
y: avatarBackgroundFrame.midY
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
if let avatarCheck = avatarCheck {
|
|
||||||
context.add(avatarCheck
|
|
||||||
.position(CGPoint(
|
|
||||||
x: avatarBackgroundFrame.midX,
|
|
||||||
y: avatarBackgroundFrame.midY
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.add(avatarExpandProgress
|
|
||||||
.position(CGPoint(
|
|
||||||
x: avatarBackgroundFrame.midX,
|
|
||||||
y: avatarBackgroundFrame.midY
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
if let avatar = avatar {
|
|
||||||
context.add(avatar
|
|
||||||
.position(CGPoint(
|
|
||||||
x: avatarBackgroundFrame.midX,
|
|
||||||
y: avatarBackgroundFrame.midY
|
|
||||||
))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let textBackgroundFrame = textBackground.size.bottomCentered(in: CGRect(origin: CGPoint(), size: size))
|
|
||||||
context.add(textBackground
|
|
||||||
.position(CGPoint(
|
|
||||||
x: textBackgroundFrame.midX,
|
|
||||||
y: textBackgroundFrame.midY
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
let textFrame = text.size.centered(in: textBackgroundFrame)
|
|
||||||
context.add(text
|
|
||||||
.position(CGPoint(
|
|
||||||
x: textFrame.midX,
|
|
||||||
y: textFrame.midY
|
|
||||||
))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ChatInputPanelOverscrollNode: ASDisplayNode {
|
||||||
|
let text: String
|
||||||
|
let priority: Int
|
||||||
|
private let titleNode: ImmediateTextNode
|
||||||
|
|
||||||
|
init(text: String, color: UIColor, priority: Int) {
|
||||||
|
self.text = text
|
||||||
|
self.priority = priority
|
||||||
|
self.titleNode = ImmediateTextNode()
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.titleNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: color)
|
||||||
|
self.addSubnode(self.titleNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(size: CGSize) {
|
||||||
|
let titleSize = self.titleNode.updateLayout(size)
|
||||||
|
self.titleNode.frame = titleSize.centered(in: CGRect(origin: CGPoint(), size: size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -187,6 +187,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.historyNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, source: source, subject: .message(id: initialMessageId, highlight: true, timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch))
|
self.historyNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, source: source, subject: .message(id: initialMessageId, highlight: true, timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch))
|
||||||
|
self.historyNode.clipsToBounds = true
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
@ -528,6 +529,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
|
|||||||
|
|
||||||
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
|
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
|
||||||
let historyNode = ChatHistoryListNode(context: self.context, chatLocation: .peer(self.peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: .message(id: messageId, highlight: true, timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch))
|
let historyNode = ChatHistoryListNode(context: self.context, chatLocation: .peer(self.peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: .message(id: messageId, highlight: true, timecode: nil), controllerInteraction: self.controllerInteraction, selectedMessages: .single(nil), mode: .list(search: false, reversed: self.currentIsReversed, displayHeaders: .none, hintLinks: false, isGlobalSearch: self.isGlobalSearch))
|
||||||
|
historyNode.clipsToBounds = true
|
||||||
historyNode.preloadPages = true
|
historyNode.preloadPages = true
|
||||||
historyNode.stackFromBottom = true
|
historyNode.stackFromBottom = true
|
||||||
historyNode.updateFloatingHeaderOffset = { [weak self] offset, _ in
|
historyNode.updateFloatingHeaderOffset = { [weak self] offset, _ in
|
||||||
|
@ -60,6 +60,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
|||||||
|
|
||||||
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
|
let chatLocationContextHolder = Atomic<ChatLocationContextHolder?>(value: nil)
|
||||||
self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false, displayHeaders: .allButLast, hintLinks: tagMask == .webPage, isGlobalSearch: false))
|
self.listNode = ChatHistoryListNode(context: context, chatLocation: .peer(peerId), chatLocationContextHolder: chatLocationContextHolder, tagMask: tagMask, subject: nil, controllerInteraction: chatControllerInteraction, selectedMessages: self.selectedMessagesPromise.get(), mode: .list(search: false, reversed: false, displayHeaders: .allButLast, hintLinks: tagMask == .webPage, isGlobalSearch: false))
|
||||||
|
self.listNode.clipsToBounds = true
|
||||||
self.listNode.defaultToSynchronousTransactionWhileScrolling = true
|
self.listNode.defaultToSynchronousTransactionWhileScrolling = true
|
||||||
self.listNode.scroller.bounces = false
|
self.listNode.scroller.bounces = false
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user