2023-10-15 16:48:21 +04:00

516 lines
23 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import Display
import TelegramCore
import TelegramPresentationData
import ActivityIndicator
import WallpaperBackgroundNode
import ShimmerEffect
import ChatPresentationInterfaceState
import AccountContext
import ChatMessageItem
import ChatMessageItemView
import ChatMessageStickerItemNode
import ChatMessageInstantVideoItemNode
import ChatMessageAnimatedStickerItemNode
import ChatMessageBubbleItemNode
import ChatMessageItemImpl
public final class ChatLoadingNode: ASDisplayNode {
private let backgroundNode: NavigationBackgroundNode
private let activityIndicator: ActivityIndicator
private let offset: CGPoint
public init(context: AccountContext, theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) {
self.backgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: theme, wallpaper: chatWallpaper), enableBlur: context.sharedContext.energyUsageSettings.fullTranslucency && dateFillNeedsBlur(theme: theme, wallpaper: chatWallpaper))
let serviceColor = serviceMessageColorComponents(theme: theme, wallpaper: chatWallpaper)
self.activityIndicator = ActivityIndicator(type: .custom(serviceColor.primaryText, 22.0, 2.0, false), speed: .regular)
if serviceColor.primaryText != .white {
self.offset = CGPoint(x: 0.5, y: 0.5)
} else {
self.offset = CGPoint()
}
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.activityIndicator)
}
public func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom))
let backgroundSize: CGFloat = 30.0
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - backgroundSize) / 2.0), y: displayRect.minY + floor((displayRect.height - backgroundSize) / 2.0)), size: CGSize(width: backgroundSize, height: backgroundSize)))
self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: self.backgroundNode.bounds.height / 2.0, transition: transition)
let activitySize = self.activityIndicator.measure(size)
transition.updateFrame(node: self.activityIndicator, frame: CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - activitySize.width) / 2.0) + self.offset.x, y: displayRect.minY + floor((displayRect.height - activitySize.height) / 2.0) + self.offset.y), size: activitySize))
}
public var progressFrame: CGRect {
return self.backgroundNode.frame
}
}
private let avatarSize = CGSize(width: 38.0, height: 38.0)
private let avatarImage = generateFilledCircleImage(diameter: avatarSize.width, color: .white)
private let avatarBorderImage = generateCircleImage(diameter: avatarSize.width, lineWidth: 1.0 - UIScreenPixel, color: .white)
public final class ChatLoadingPlaceholderMessageContainer {
public var avatarNode: ASImageNode?
public var avatarBorderNode: ASImageNode?
public let bubbleNode: ASImageNode
public let bubbleBorderNode: ASImageNode
public var parentView: UIView? {
return self.bubbleNode.supernode?.view
}
public var frame: CGRect {
return self.bubbleNode.frame
}
public init(bubbleImage: UIImage?, bubbleBorderImage: UIImage?) {
self.bubbleNode = ASImageNode()
self.bubbleNode.displaysAsynchronously = false
self.bubbleNode.image = bubbleImage
self.bubbleBorderNode = ASImageNode()
self.bubbleBorderNode.displaysAsynchronously = false
self.bubbleBorderNode.image = bubbleBorderImage
}
public func setup(maskNode: ASDisplayNode, borderMaskNode: ASDisplayNode) {
maskNode.addSubnode(self.bubbleNode)
borderMaskNode.addSubnode(self.bubbleBorderNode)
}
public func animateWith(_ listItemNode: ListViewItemNode, delay: Double, transition: ContainedViewLayoutTransition) {
listItemNode.allowsGroupOpacity = true
listItemNode.alpha = 1.0
listItemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: delay, completion: { _ in
listItemNode.allowsGroupOpacity = false
})
if let bubbleItemNode = listItemNode as? ChatMessageBubbleItemNode {
bubbleItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition)
} else if let stickerItemNode = listItemNode as? ChatMessageStickerItemNode {
stickerItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition)
} else if let stickerItemNode = listItemNode as? ChatMessageAnimatedStickerItemNode {
stickerItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition)
} else if let videoItemNode = listItemNode as? ChatMessageInstantVideoItemNode {
videoItemNode.animateFromLoadingPlaceholder(delay: delay, transition: transition)
}
}
public func update(size: CGSize, hasAvatar: Bool, rect: CGRect, transition: ContainedViewLayoutTransition) {
var avatarOffset: CGFloat = 0.0
if hasAvatar && self.avatarNode == nil {
let avatarNode = ASImageNode()
avatarNode.displaysAsynchronously = false
avatarNode.image = avatarImage
self.bubbleNode.supernode?.addSubnode(avatarNode)
self.avatarNode = avatarNode
let avatarBorderNode = ASImageNode()
avatarBorderNode.displaysAsynchronously = false
avatarBorderNode.image = avatarBorderImage
self.bubbleBorderNode.supernode?.addSubnode(avatarBorderNode)
self.avatarBorderNode = avatarBorderNode
}
if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode {
let avatarFrame = CGRect(origin: CGPoint(x: 3.0, y: rect.maxY + 1.0 - avatarSize.height), size: avatarSize)
transition.updateFrame(node: avatarNode, frame: avatarFrame)
transition.updateFrame(node: avatarBorderNode, frame: avatarFrame)
avatarOffset += avatarSize.width - 1.0
}
let bubbleFrame = CGRect(origin: CGPoint(x: 3.0 + avatarOffset, y: rect.origin.y), size: CGSize(width: rect.width, height: rect.height))
transition.updateFrame(node: self.bubbleNode, frame: bubbleFrame)
transition.updateFrame(node: self.bubbleBorderNode, frame: bubbleFrame)
}
}
public final class ChatLoadingPlaceholderNode: ASDisplayNode {
private weak var backgroundNode: WallpaperBackgroundNode?
private let context: AccountContext
private let maskNode: ASDisplayNode
private let borderMaskNode: ASDisplayNode
private let containerNode: ASDisplayNode
private var backgroundContent: WallpaperBubbleBackgroundNode?
private let backgroundColorNode: ASDisplayNode
private let effectNode: ShimmerEffectForegroundNode
private let borderNode: ASDisplayNode
private let borderEffectNode: ShimmerEffectForegroundNode
private let messageContainers: [ChatLoadingPlaceholderMessageContainer]
private var absolutePosition: (CGRect, CGSize)?
private var validLayout: (CGSize, UIEdgeInsets, LayoutMetrics)?
public init(context: AccountContext, theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners, backgroundNode: WallpaperBackgroundNode) {
self.context = context
self.backgroundNode = backgroundNode
self.maskNode = ASDisplayNode()
self.borderMaskNode = ASDisplayNode()
let bubbleImage = messageBubbleImage(maxCornerRadius: bubbleCorners.mainRadius, minCornerRadius: bubbleCorners.auxiliaryRadius, incoming: true, fillColor: .white, strokeColor: .clear, neighbors: .none, theme: theme.chat, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true)
let bubbleBorderImage = messageBubbleImage(maxCornerRadius: bubbleCorners.mainRadius, minCornerRadius: bubbleCorners.auxiliaryRadius, incoming: true, fillColor: .clear, strokeColor: .red, neighbors: .none, theme: theme.chat, wallpaper: .color(0xffffff), knockout: true, mask: true, extendedEdges: true, onlyOutline: true)
var messageContainers: [ChatLoadingPlaceholderMessageContainer] = []
for _ in 0 ..< 14 {
let container = ChatLoadingPlaceholderMessageContainer(bubbleImage: bubbleImage, bubbleBorderImage: bubbleBorderImage)
container.setup(maskNode: self.maskNode, borderMaskNode: self.borderMaskNode)
messageContainers.append(container)
}
self.messageContainers = messageContainers
self.containerNode = ASDisplayNode()
self.borderNode = ASDisplayNode()
self.backgroundColorNode = ASDisplayNode()
self.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: theme, wallpaper: chatWallpaper)
self.effectNode = ShimmerEffectForegroundNode()
self.effectNode.layer.compositingFilter = "screenBlendMode"
self.borderEffectNode = ShimmerEffectForegroundNode()
self.borderEffectNode.layer.compositingFilter = "screenBlendMode"
super.init()
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundColorNode)
if context.sharedContext.energyUsageSettings.fullTranslucency {
self.containerNode.addSubnode(self.effectNode)
}
self.addSubnode(self.borderNode)
if context.sharedContext.energyUsageSettings.fullTranslucency {
self.borderNode.addSubnode(self.borderEffectNode)
}
}
override public func didLoad() {
super.didLoad()
self.containerNode.view.mask = self.maskNode.view
self.borderNode.view.mask = self.borderMaskNode.view
if self.context.sharedContext.energyUsageSettings.fullTranslucency {
Queue.mainQueue().after(0.3) {
if !self.didAnimateOut {
self.backgroundNode?.updateIsLooping(true)
}
}
}
}
private var bottomInset: (Int, CGFloat)?
public func setup(_ historyNode: ListView, updating: Bool = false) {
let listNode = historyNode
var listItemNodes: [ASDisplayNode] = []
var count = 0
var inset: CGFloat = 0.0
listNode.forEachVisibleItemNode { itemNode in
inset += itemNode.frame.height
count += 1
listItemNodes.append(itemNode)
}
if updating {
let heightNorm = listNode.bounds.height - listNode.insets.top
listNode.forEachItemHeaderNode { itemNode in
var animateScale = true
if itemNode is ChatMessageAvatarHeaderNode {
animateScale = false
}
let delayFactor = itemNode.frame.minY / heightNorm
let delay = Double(delayFactor * 0.2)
itemNode.allowsGroupOpacity = true
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay, completion: { [weak itemNode] _ in
itemNode?.allowsGroupOpacity = false
})
if animateScale {
itemNode.layer.animateScale(from: 0.94, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
}
}
}
if count > 0 {
self.bottomInset = (count, inset)
}
if updating {
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
transition.animateOffsetAdditive(node: self.maskNode, offset: -inset)
transition.animateOffsetAdditive(node: self.borderMaskNode, offset: -inset)
for listItemNode in listItemNodes {
var incoming = false
if let itemNode = listItemNode as? ChatMessageItemView, let item = itemNode.item, item.message.effectivelyIncoming(item.context.account.peerId) {
incoming = true
}
transition.animatePositionAdditive(node: listItemNode, offset: CGPoint(x: incoming ? 30.0 : -30.0, y: -30.0))
transition.animateTransformScale(node: listItemNode, from: CGPoint(x: 0.85, y: 0.85))
listItemNode.allowsGroupOpacity = true
listItemNode.alpha = 1.0
listItemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, completion: { _ in
listItemNode.allowsGroupOpacity = false
})
}
}
self.maskNode.bounds = self.maskNode.bounds.offsetBy(dx: 0.0, dy: inset)
self.borderMaskNode.bounds = self.borderMaskNode.bounds.offsetBy(dx: 0.0, dy: inset)
}
private var didAnimateOut = false
public func animateOut(_ historyNode: ListView, completion: @escaping () -> Void = {}) {
guard let (size, _, _) = self.validLayout else {
return
}
let listNode = historyNode
self.didAnimateOut = true
self.backgroundNode?.updateIsLooping(false)
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .spring)
var lastFrame: CGRect?
let heightNorm = listNode.bounds.height - listNode.insets.top
var index = 0
var skipCount = self.bottomInset?.0 ?? 0
listNode.forEachVisibleItemNode { itemNode in
guard index < self.messageContainers.count, let listItemNode = itemNode as? ListViewItemNode else {
return
}
let delayFactor = listItemNode.frame.minY / heightNorm
let delay = Double(delayFactor * 0.1)
if skipCount > 0 {
skipCount -= 1
return
}
if let itemNode = itemNode as? ChatUnreadItemNode {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0)
return
}
if let itemNode = itemNode as? ChatReplyCountItemNode {
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: 0.0)
return
}
let messageContainer = self.messageContainers[index]
messageContainer.animateWith(listItemNode, delay: delay, transition: transition)
lastFrame = messageContainer.frame
index += 1
}
skipCount = self.bottomInset?.0 ?? 0
listNode.forEachItemHeaderNode { itemNode in
var animateScale = true
if itemNode is ChatMessageAvatarHeaderNode {
animateScale = false
if skipCount > 0 {
return
}
}
if itemNode is ChatMessageDateHeaderNode {
if skipCount > 0 {
skipCount -= 1
return
}
}
let delayFactor = itemNode.frame.minY / heightNorm
let delay = Double(delayFactor * 0.2)
itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15, delay: delay)
if animateScale {
itemNode.layer.animateScale(from: 0.94, to: 1.0, duration: 0.4, delay: delay, timingFunction: kCAMediaTimingFunctionSpring)
}
}
self.alpha = 0.0
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { _ in
completion()
})
if let lastFrame = lastFrame, index < self.messageContainers.count {
var offset = lastFrame.minY
for k in index ..< self.messageContainers.count {
let messageContainer = self.messageContainers[k]
let messageSize = messageContainer.frame.size
messageContainer.update(size: size, hasAvatar: self.chatType != .channel && self.chatType != .user, rect: CGRect(origin: CGPoint(x: 0.0, y: offset - messageSize.height), size: messageSize), transition: transition)
offset -= messageSize.height
}
}
}
public func addContentOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) {
self.maskNode.bounds = self.maskNode.bounds.offsetBy(dx: 0.0, dy: -offset)
self.borderMaskNode.bounds = self.borderMaskNode.bounds.offsetBy(dx: 0.0, dy: -offset)
transition.animateOffsetAdditive(node: self.maskNode, offset: offset)
transition.animateOffsetAdditive(node: self.borderMaskNode, offset: offset)
if let (rect, containerSize) = self.absolutePosition {
self.update(rect: rect, within: containerSize)
}
}
public func update(rect: CGRect, within containerSize: CGSize, transition: ContainedViewLayoutTransition = .immediate) {
self.absolutePosition = (rect, containerSize)
if let backgroundContent = self.backgroundContent {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition)
}
}
public enum ChatType: Equatable {
case generic
case user
case group
case channel
}
private var chatType: ChatType = .channel
public func updatePresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState) {
var chatType: ChatType = .channel
if let peer = chatPresentationInterfaceState.renderedPeer?.peer {
if peer is TelegramUser {
chatType = .user
} else if peer is TelegramGroup {
chatType = .group
} else if let channel = peer as? TelegramChannel {
if case .group = channel.info {
chatType = .group
} else {
chatType = .channel
}
}
}
if self.chatType != chatType {
self.chatType = chatType
if let (size, insets, metrics) = self.validLayout {
self.updateLayout(size: size, insets: insets, metrics: metrics, transition: .immediate)
}
}
}
public func updateLayout(size: CGSize, insets: UIEdgeInsets, metrics: LayoutMetrics, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets, metrics)
let bounds = CGRect(origin: .zero, size: size)
transition.updateFrame(node: self.maskNode, frame: bounds)
transition.updateFrame(node: self.borderMaskNode, frame: bounds)
transition.updateFrame(node: self.containerNode, frame: bounds)
transition.updateFrame(node: self.borderNode, frame: bounds)
transition.updateFrame(node: self.backgroundColorNode, frame: bounds)
transition.updateFrame(node: self.effectNode, frame: bounds)
transition.updateFrame(node: self.borderEffectNode, frame: bounds)
self.effectNode.updateAbsoluteRect(bounds, within: bounds.size)
self.borderEffectNode.updateAbsoluteRect(bounds, within: bounds.size)
self.effectNode.update(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.14), horizontal: true, effectSize: 280.0, globalTimeOffset: false, duration: 1.6)
self.borderEffectNode.update(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.35), horizontal: true, effectSize: 320.0, globalTimeOffset: false, duration: 1.6)
let shortHeight: CGFloat = 71.0
let tallHeight: CGFloat = 93.0
var width = size.width
if case .regular = metrics.widthClass, abs(size.width - size.height) < 0.2 * size.height {
width *= 0.7
}
let dimensions: [CGSize] = [
CGSize(width: floorToScreenPixels(0.47 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.58 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.69 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.47 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.58 * width), height: shortHeight),
CGSize(width: floorToScreenPixels(0.36 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.47 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.36 * width), height: shortHeight),
CGSize(width: floorToScreenPixels(0.58 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.69 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.58 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.36 * width), height: shortHeight),
CGSize(width: floorToScreenPixels(0.47 * width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.58 * width), height: tallHeight)
].map {
if self.chatType == .channel {
return CGSize(width: floor($0.width * 1.3), height: floor($0.height * 1.8))
} else {
return $0
}
}
var offset: CGFloat = 5.0
var index = 0
for messageContainer in self.messageContainers {
let messageSize = dimensions[index % 14]
messageContainer.update(size: bounds.size, hasAvatar: self.chatType != .channel && self.chatType != .user, rect: CGRect(origin: CGPoint(x: 0.0, y: bounds.size.height - insets.bottom - offset - messageSize.height), size: messageSize), transition: transition)
offset += messageSize.height
index += 1
}
if self.backgroundNode?.hasExtraBubbleBackground() == true {
self.backgroundColorNode.isHidden = true
} else {
self.backgroundColorNode.isHidden = true
}
if let backgroundNode = self.backgroundNode, let backgroundContent = backgroundNode.makeBubbleBackground(for: .free) {
if self.backgroundContent == nil {
self.backgroundContent = backgroundContent
self.containerNode.insertSubnode(backgroundContent, at: 0)
}
} else {
self.backgroundContent?.removeFromSupernode()
self.backgroundContent = nil
}
if let backgroundContent = self.backgroundContent {
transition.updateFrame(node: backgroundContent, frame: bounds)
if let (rect, containerSize) = self.absolutePosition {
self.update(rect: rect, within: containerSize)
}
}
}
}