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 var displayLinkAnimator: ConstantDisplayLinkAnimator?
private let hierarchyTrackingNode: HierarchyTrackingNode
private var isCurrentlyInHierarchy = true
private var audioLevel: CGFloat = 0
public var presentationAudioLevel: CGFloat = 0
@ -93,8 +96,15 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
scaleSpeed: 0.2,
isCircle: false
)
var updateInHierarchy: ((Bool) -> Void)?
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
updateInHierarchy?(value)
})
super.init(frame: frame)
self.addSubnode(self.hierarchyTrackingNode)
self.addSubnode(self.bigBlob)
self.addSubnode(self.mediumBlob)
@ -109,6 +119,12 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel
strongSelf.bigBlob.level = strongSelf.presentationAudioLevel
}
updateInHierarchy = { [weak self] value in
if let strongSelf = self {
strongSelf.isCurrentlyInHierarchy = value
}
}
}
required init?(coder: NSCoder) {
@ -266,6 +282,9 @@ final class BlobNode: ASDisplayNode {
)
}
}
private let hierarchyTrackingNode: HierarchyTrackingNode
private var isCurrentlyInHierarchy = true
init(
pointsCount: Int,
@ -290,12 +309,24 @@ final class BlobNode: ASDisplayNode {
let angle = (CGFloat.pi * 2) / CGFloat(pointsCount)
self.smoothness = ((4 / 3) * tan(angle / 4)) / sin(angle / 2) / 2
var updateInHierarchy: ((Bool) -> Void)?
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
updateInHierarchy?(value)
})
super.init()
self.addSubnode(self.hierarchyTrackingNode)
self.layer.addSublayer(self.shapeLayer)
self.shapeLayer.transform = CATransform3DMakeScale(minScale, minScale, 1)
updateInHierarchy = { [weak self] value in
if let strongSelf = self {
strongSelf.isCurrentlyInHierarchy = value
}
}
}
required init?(coder: NSCoder) {
@ -305,7 +336,7 @@ final class BlobNode: ASDisplayNode {
func setColor(_ color: UIColor, animated: Bool) {
let previousColor = self.shapeLayer.fillColor
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)
}
}

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) {
switch self.animation {
case .none:

View File

@ -138,7 +138,7 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
}
private func setupGradientAnimations() {
if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
/*if let _ = self.foregroundGradientLayer.animation(forKey: "movement") {
} else {
let previousValue = self.foregroundGradientLayer.startPoint
let newValue: CGPoint
@ -162,7 +162,7 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
self.foregroundGradientLayer.add(animation, forKey: "movement")
CATransaction.commit()
}
}*/
}
func updateAnimations() {
@ -172,7 +172,9 @@ private class CallStatusBarBackgroundNode: ASDisplayNode {
return
}
self.setupGradientAnimations()
self.maskCurveView.startAnimating()
if isCurrentlyInHierarchy {
self.maskCurveView.startAnimating()
}
}
}
@ -206,6 +208,9 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
private var currentScheduleTimestamp: Int32?
private var currentMembers: PresentationGroupCallMembers?
private var currentIsConnected = true
private let hierarchyTrackingNode: HierarchyTrackingNode
private var isCurrentlyInHierarchy = true
public override init() {
self.backgroundNode = CallStatusBarBackgroundNode()
@ -213,13 +218,29 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
self.subtitleNode = ImmediateAnimatedCountLabelNode()
self.subtitleNode.reverseAnimationDirection = true
self.speakerNode = ImmediateTextNode()
var updateInHierarchy: ((Bool) -> Void)?
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
updateInHierarchy?(value)
})
super.init()
self.addSubnode(self.hierarchyTrackingNode)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.speakerNode)
updateInHierarchy = { [weak self] value in
if let strongSelf = self {
strongSelf.isCurrentlyInHierarchy = value
if value {
strongSelf.update()
}
}
}
}
deinit {
@ -231,13 +252,17 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
public func update(content: Content) {
self.currentContent = content
self.update()
if self.isCurrentlyInHierarchy {
self.update()
}
}
public override func update(size: CGSize) {
self.currentSize = size
self.update()
}
private let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
private func update() {
guard let size = self.currentSize, let content = self.currentContent else {
@ -329,8 +354,10 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
currentIsConnected = false
}
strongSelf.currentIsConnected = currentIsConnected
strongSelf.update()
if strongSelf.isCurrentlyInHierarchy {
strongSelf.update()
}
}
}))
self.audioLevelDisposable.set((combineLatest(call.myAudioLevel, .single([]) |> then(call.audioLevels))
@ -351,8 +378,7 @@ public class CallStatusBarNodeImpl: CallStatusBarNode {
var title: String = ""
var speakerSubtitle: String = ""
let textFont = Font.with(size: 13.0, design: .regular, weight: .regular, traits: [.monospacedNumbers])
let textColor = UIColor.white
var segments: [AnimatedCountLabelNode.Segment] = []
var displaySpeakerSubtitle = false

View File

@ -1249,6 +1249,9 @@ private final class VoiceBlobView: UIView {
private(set) var isAnimating = false
public typealias BlobRange = (min: CGFloat, max: CGFloat)
private let hierarchyTrackingNode: HierarchyTrackingNode
private var isCurrentlyInHierarchy = true
public init(
frame: CGRect,
@ -1256,6 +1259,11 @@ private final class VoiceBlobView: UIView {
mediumBlobRange: BlobRange,
bigBlobRange: BlobRange
) {
var updateInHierarchy: ((Bool) -> Void)?
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
updateInHierarchy?(value)
})
self.maxLevel = maxLevel
self.mediumBlob = BlobView(
@ -1278,18 +1286,30 @@ private final class VoiceBlobView: UIView {
)
super.init(frame: frame)
addSubnode(hierarchyTrackingNode)
addSubview(bigBlob)
addSubview(mediumBlob)
displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
guard let strongSelf = self else { return }
if !strongSelf.isCurrentlyInHierarchy {
return
}
strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel
strongSelf.bigBlob.level = strongSelf.presentationAudioLevel
}
updateInHierarchy = { [weak self] value in
if let strongSelf = self {
strongSelf.isCurrentlyInHierarchy = value
}
}
}
required init?(coder: NSCoder) {

View File

@ -202,6 +202,15 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
var item: VoiceChatFullscreenParticipantItem? {
return self.layoutParams?.0
}
private var isCurrentlyInHierarchy = false {
didSet {
if self.isCurrentlyInHierarchy != oldValue {
self.highlightNode.isCurrentlyInHierarchy = self.isCurrentlyInHierarchy
}
}
}
private var isCurrentlyInHierarchyDisposable: Disposable?
init() {
self.contextSourceNode = ContextExtractedContentContainingNode()
@ -247,7 +256,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
self.actionContainerNode = ASDisplayNode()
self.actionButtonNode = HighlightableButtonNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.isAccessibilityElement = true
@ -293,6 +302,7 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
self.audioLevelDisposable.dispose()
self.raiseHandTimer?.invalidate()
self.silenceTimer?.invalidate()
self.isCurrentlyInHierarchyDisposable?.dispose()
}
override func selected() {
@ -971,6 +981,16 @@ class VoiceChatFullscreenParticipantItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.actionButtonNode, frame: animationFrame)
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 let audioLevelDisposable = MetaDisposable()
private let hierarchyTrackingNode: HierarchyTrackingNode
private var isCurrentlyInHierarchy = false
init(context: AccountContext) {
self.context = context
@ -201,7 +204,14 @@ final class VoiceChatTileItemNode: ASDisplayNode {
self.placeholderIconNode.contentMode = .scaleAspectFit
self.placeholderIconNode.displaysAsynchronously = false
var updateInHierarchy: ((Bool) -> Void)?
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
updateInHierarchy?(value)
})
super.init()
self.addSubnode(self.hierarchyTrackingNode)
self.containerNode.addSubnode(self.contextSourceNode)
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
@ -236,6 +246,13 @@ final class VoiceChatTileItemNode: ASDisplayNode {
}
strongSelf.updateIsExtracted(isExtracted, transition: transition)
}
updateInHierarchy = { [weak self] value in
if let strongSelf = self {
strongSelf.isCurrentlyInHierarchy = value
strongSelf.highlightNode.isCurrentlyInHierarchy = value
}
}
}
deinit {
@ -634,9 +651,14 @@ class VoiceChatTileHighlightNode: ASDisplayNode {
private let maskLayer = CALayer()
private let foregroundGradientLayer = CAGradientLayer()
private let hierarchyTrackingNode: HierarchyTrackingNode
private var isCurrentlyInHierarchy = false
var isCurrentlyInHierarchy = false {
didSet {
if self.isCurrentlyInHierarchy != oldValue && self.isCurrentlyInHierarchy {
self.updateAnimations()
}
}
}
private var audioLevel: 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.endPoint = CGPoint(x: 0.0, y: 1.0)
var updateInHierarchy: ((Bool) -> Void)?
self.hierarchyTrackingNode = HierarchyTrackingNode({ value in
updateInHierarchy?(value)
})
super.init()
updateInHierarchy = { [weak self] value in
if let strongSelf = self {
strongSelf.isCurrentlyInHierarchy = value
strongSelf.updateAnimations()
}
}
self.displayLinkAnimator = ConstantDisplayLinkAnimator() { [weak self] in
guard let strongSelf = self else { return }
strongSelf.presentationAudioLevel = strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
}
self.addSubnode(self.hierarchyTrackingNode)
}
override func didLoad() {
@ -733,8 +741,11 @@ class VoiceChatTileHighlightNode: ASDisplayNode {
animation.toValue = newValue
CATransaction.setCompletionBlock { [weak self] in
if let isCurrentlyInHierarchy = self?.isCurrentlyInHierarchy, isCurrentlyInHierarchy {
self?.setupGradientAnimations()
guard let strongSelf = self else {
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)
}
public func getNextUnreadChannel(peerId: PeerId, filter: ChatListFilterPredicate?) -> Signal<EnginePeer?, NoError> {
return self.account.postbox.transaction { transaction -> EnginePeer? in
public func getNextUnreadChannel(peerId: PeerId, filter: ChatListFilterPredicate?) -> Signal<(peer: EnginePeer, unreadCount: Int)?, NoError> {
return self.account.postbox.transaction { transaction -> (peer: EnginePeer, unreadCount: Int)? in
var results: [(EnginePeer, Int32)] = []
var peerIds: [PeerId] = []
@ -525,7 +525,12 @@ public extension TelegramEngine {
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.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
let nextPeerId = nextPeer?.id
let nextPeerId = nextPeer?.peer.id
if strongSelf.preloadNextChatPeerId != nextPeerId {
strongSelf.preloadNextChatPeerId = nextPeerId
@ -7508,7 +7510,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let avatarSnapshotState = snapshotState.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 inputPanelNode: ChatInputPanelNode?
private(set) var inputPanelOverscrollNode: ChatInputPanelOverscrollNode?
private weak var currentDismissedInputPanelNode: ASDisplayNode?
private var secondaryInputPanelNode: ChatInputPanelNode?
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 {
fileprivate let historySnapshotState: ChatHistoryListNode.SnapshotState
let titleViewSnapshotState: ChatTitleView.SnapshotState?
@ -2534,8 +2547,8 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
)
}
func animateFromSnapshot(_ snapshotState: SnapshotState) {
self.historyNode.animateFromSnapshot(snapshotState.historySnapshotState)
func animateFromSnapshot(_ snapshotState: SnapshotState, completion: @escaping () -> Void) {
self.historyNode.animateFromSnapshot(snapshotState.historySnapshotState, completion: completion)
self.navigateButtons.animateFromSnapshot(snapshotState.navigationButtonsSnapshotState)
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
private var overscrollView: ComponentHostView<Empty>?
var nextChannelToRead: EnginePeer?
var nextChannelToRead: (peer: EnginePeer, unreadCount: Int)?
var offerNextChannelToRead: Bool = false
var nextChannelToReadDisplayName: Bool = false
private var currentOverscrollExpandProgress: CGFloat = 0.0
private var freezeOverscrollControl: Bool = false
private var feedback: HapticFeedback?
var openNextChannelToRead: ((EnginePeer) -> Void)?
@ -628,7 +629,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
}
self.preloadPages = true
self.preloadPages = false
switch self.mode {
case .bubbles:
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 {
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)
}
}
@ -1206,7 +1208,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
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>
if let current = self.overscrollView {
overscrollView = current
@ -1220,24 +1225,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let expandDistance = max(-offset - 12.0, 0.0)
let expandProgress: CGFloat = min(1.0, expandDistance / 90.0)
let text: String
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"
}
}
if let _ = nextChannelToRead {
let previousType = self.currentOverscrollExpandProgress >= 0.99
let currentType = expandProgress >= 0.99
@ -1249,27 +1237,45 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
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 {
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(
transition: .immediate,
component: AnyComponent(ChatOverscrollControl(
text: text,
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),
peer: self.nextChannelToRead,
peer: self.nextChannelToRead?.peer,
unreadCount: self.nextChannelToRead?.unreadCount ?? 0,
context: self.context,
expandDistance: expandDistance
)),
environment: {},
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 {
self.overscrollView = nil
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 snapshotBottomInset: CGFloat = 0.0
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?.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)

View File

@ -1,6 +1,7 @@
import UIKit
import ComponentFlow
import Display
import AsyncDisplayKit
import TelegramCore
import Postbox
import AccountContext
@ -196,6 +197,9 @@ final class CheckComponent: Component {
}
final class View: UIView {
private var currentValue: CGFloat?
private var animator: DisplayLinkAnimator?
init() {
super.init(frame: CGRect())
}
@ -204,10 +208,8 @@ final class CheckComponent: Component {
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) {
let size = availableSize
let diameter = size.width
let factor = diameter / 50.0
@ -215,19 +217,17 @@ final class CheckComponent: Component {
context.saveGState()
context.setBlendMode(.normal)
context.setFillColor(component.color.cgColor)
context.setStrokeColor(component.color.cgColor)
context.setFillColor(color.cgColor)
context.setStrokeColor(color.cgColor)
let center = CGPoint(x: diameter / 2.0, y: diameter / 2.0)
let lineWidth = component.lineWidth
context.setLineWidth(max(1.7, lineWidth * factor))
context.setLineCap(.round)
context.setLineJoin(.round)
context.setMiterLimit(10.0)
let progress = component.value
let progress = value
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)
@ -257,7 +257,7 @@ final class CheckComponent: Component {
}
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
UIGraphicsPushContext(context.cgContext)
draw(context: context.cgContext)
@ -265,11 +265,37 @@ final class CheckComponent: Component {
}
self.layer.contents = image.cgImage
} else {
UIGraphicsBeginImageContextWithOptions(availableSize, false, 0.0)
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
draw(context: UIGraphicsGetCurrentContext()!)
self.layer.contents = UIGraphicsGetImageFromCurrentImageContext()?.cgImage
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
}
@ -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 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 peer: EnginePeer
let badge: Badge?
init(context: AccountContext, peer: EnginePeer) {
init(context: AccountContext, peer: EnginePeer, badge: Badge?) {
self.context = context
self.peer = peer
self.badge = badge
}
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
@ -300,14 +414,20 @@ final class AvatarComponent: Component {
if lhs.peer != rhs.peer {
return false
}
if lhs.badge != rhs.badge {
return false
}
return true
}
final class View: UIView {
private let avatarNode: AvatarNode
private let avatarMask: CAShapeLayer
private var badgeView: ComponentHostView<Empty>?
init() {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0))
self.avatarMask = CAShapeLayer()
super.init(frame: CGRect())
@ -322,6 +442,56 @@ final class AvatarComponent: Component {
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)
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
}
}
@ -335,32 +505,32 @@ final class AvatarComponent: Component {
}
}
final class ChatOverscrollControl: CombinedComponent {
let text: String
final class OverscrollContentsComponent: Component {
let context: AccountContext
let backgroundColor: UIColor
let foregroundColor: UIColor
let peer: EnginePeer?
let context: AccountContext
let expandDistance: CGFloat
let unreadCount: Int
let expandOffset: CGFloat
init(
text: String,
context: AccountContext,
backgroundColor: UIColor,
foregroundColor: UIColor,
peer: EnginePeer?,
context: AccountContext,
expandDistance: CGFloat
unreadCount: Int,
expandOffset: CGFloat
) {
self.text = text
self.context = context
self.backgroundColor = backgroundColor
self.foregroundColor = foregroundColor
self.peer = peer
self.context = context
self.expandDistance = expandDistance
self.unreadCount = unreadCount
self.expandOffset = expandOffset
}
static func ==(lhs: ChatOverscrollControl, rhs: ChatOverscrollControl) -> Bool {
if lhs.text != rhs.text {
static func ==(lhs: OverscrollContentsComponent, rhs: OverscrollContentsComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if !lhs.backgroundColor.isEqual(rhs.backgroundColor) {
@ -372,6 +542,265 @@ final class ChatOverscrollControl: CombinedComponent {
if lhs.peer != rhs.peer {
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 {
return false
}
@ -382,156 +811,51 @@ final class ChatOverscrollControl: CombinedComponent {
}
static var body: Body {
let avatarBackground = Child(BlurredRoundedRectangle.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)
let contents = Child(OverscrollContentsComponent.self)
return { context in
let text = text.update(
component: Text(
text: context.component.text,
font: Font.regular(12.0),
color: context.component.foregroundColor
let contents = contents.update(
component: OverscrollContentsComponent(
context: context.component.context,
backgroundColor: context.component.backgroundColor,
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
)
let textHorizontalPadding: CGFloat = 6.0
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 size = CGSize(width: context.availableSize.width, height: contents.size.height)
let avatarBackgroundSize: CGFloat = context.component.peer != nil ? (avatarSize + avatarPadding * 2.0) : avatarSize
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
))
context.add(contents
.position(CGPoint(x: size.width / 2.0, y: size.height / 2.0))
)
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.clipsToBounds = true
super.init()
@ -528,6 +529,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
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))
historyNode.clipsToBounds = true
historyNode.preloadPages = true
historyNode.stackFromBottom = true
historyNode.updateFloatingHeaderOffset = { [weak self] offset, _ in

View File

@ -60,6 +60,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
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.clipsToBounds = true
self.listNode.defaultToSynchronousTransactionWhileScrolling = true
self.listNode.scroller.bounces = false