mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Combo update
This commit is contained in:
parent
759f1c79bb
commit
e170f2fe5a
@ -43,6 +43,9 @@ public final class VoiceBlobView: UIView, TGModernConversationInputMicButtonDeco
|
||||
private let maxLevel: CGFloat
|
||||
|
||||
private 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)
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Chat/OverscrollArrow.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Chat/OverscrollArrow.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "drag.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
3
submodules/TelegramUI/Images.xcassets/Chat/OverscrollArrow.imageset/drag.svg
vendored
Normal file
3
submodules/TelegramUI/Images.xcassets/Chat/OverscrollArrow.imageset/drag.svg
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.9393 3.93934C14.5251 3.35355 15.4749 3.35355 16.0607 3.93934L26.0607 13.9393C26.6464 14.5251 26.6464 15.4749 26.0607 16.0607C25.4749 16.6464 24.5251 16.6464 23.9393 16.0607L16.5 8.62132V25C16.5 25.8284 15.8284 26.5 15 26.5C14.1716 26.5 13.5 25.8284 13.5 25V8.62132L6.06066 16.0607C5.47487 16.6464 4.52513 16.6464 3.93934 16.0607C3.35355 15.4749 3.35355 14.5251 3.93934 13.9393L13.9393 3.93934Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 526 B |
@ -3324,10 +3324,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
strongSelf.chatDisplayNode.historyNode.offerNextChannelToRead = true
|
||||
strongSelf.chatDisplayNode.historyNode.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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user