Combo update

This commit is contained in:
Ali 2021-08-06 19:11:22 +02:00
parent 759f1c79bb
commit e170f2fe5a
15 changed files with 784 additions and 228 deletions

View File

@ -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)
} }
} }

View File

@ -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:

View File

@ -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

View File

@ -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) {

View File

@ -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
})
}
} }
}) })
} }

View File

@ -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()
} }
} }

View File

@ -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
}
} }
} }

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "drag.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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

View File

@ -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
} }
} }

View File

@ -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))
}
}
} }

View File

@ -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)

View File

@ -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))
}
}

View File

@ -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

View File

@ -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