Swiftgram/submodules/TelegramUI/Sources/ChatLoadingNode.swift
2022-10-13 19:49:15 +03:00

329 lines
15 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import TelegramPresentationData
import ActivityIndicator
import WallpaperBackgroundNode
import ShimmerEffect
final class ChatLoadingNode: ASDisplayNode {
private let backgroundNode: NavigationBackgroundNode
private let activityIndicator: ActivityIndicator
private let offset: CGPoint
init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners) {
self.backgroundNode = NavigationBackgroundNode(color: selectDateFillStaticColor(theme: theme, wallpaper: chatWallpaper), enableBlur: 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)
}
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))
}
var progressFrame: CGRect {
return self.backgroundNode.frame
}
}
private let avatarSize = CGSize(width: 38.0, height: 38.0)
final class ChatLoadingPlaceholderMessageContainer {
let avatarNode: ASImageNode?
let avatarBorderNode: ASImageNode?
let bubbleNode: ASImageNode
let bubbleBorderNode: ASImageNode
var parentView: UIView? {
return self.bubbleNode.supernode?.view
}
var frame: CGRect {
return self.bubbleNode.frame
}
init(hasAvatar: Bool, bubbleImage: UIImage?, bubbleBorderImage: UIImage?) {
if hasAvatar {
self.avatarNode = ASImageNode()
self.avatarNode?.displaysAsynchronously = false
self.avatarNode?.image = generateFilledCircleImage(diameter: avatarSize.width, color: .white)
self.avatarBorderNode = ASImageNode()
self.avatarBorderNode?.displaysAsynchronously = false
self.avatarBorderNode?.image = generateCircleImage(diameter: avatarSize.width, lineWidth: 1.0 - UIScreenPixel, color: .red)
} else {
self.avatarNode = nil
self.avatarBorderNode = nil
}
self.bubbleNode = ASImageNode()
self.bubbleNode.displaysAsynchronously = false
self.bubbleNode.image = bubbleImage
self.bubbleBorderNode = ASImageNode()
self.bubbleBorderNode.displaysAsynchronously = false
self.bubbleBorderNode.image = bubbleBorderImage
}
func setup(maskNode: ASDisplayNode, borderMaskNode: ASDisplayNode) {
if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode {
maskNode.addSubnode(avatarNode)
borderMaskNode.addSubnode(avatarBorderNode)
}
maskNode.addSubnode(self.bubbleNode)
borderMaskNode.addSubnode(self.bubbleBorderNode)
}
func animateWith(_ listItemNode: ListViewItemNode, transition: ContainedViewLayoutTransition) {
if let bubbleItemNode = listItemNode as? ChatMessageBubbleItemNode {
bubbleItemNode.animateFromLoadingPlaceholder(messageContainer: self, transition: transition)
} else if let stickerItemNode = listItemNode as? ChatMessageAnimatedStickerItemNode {
stickerItemNode.animateFromLoadingPlaceholder(messageContainer: self, transition: transition)
}
}
func animateBackgroundFrame(to frame: CGRect, transition: ContainedViewLayoutTransition) {
let targetFrame = CGRect(origin: CGPoint(x: self.bubbleNode.frame.minX, y: frame.minY), size: frame.size)
transition.updateFrame(node: self.bubbleNode, frame: targetFrame)
transition.updateFrame(node: self.bubbleBorderNode, frame: targetFrame)
if let avatarNode = self.avatarNode, let avatarBorderNode = self.avatarBorderNode {
let avatarFrame = CGRect(origin: CGPoint(x: 3.0, y: frame.maxY + 1.0 - avatarSize.height), size: avatarSize)
transition.updateFrame(node: avatarNode, frame: avatarFrame)
transition.updateFrame(node: avatarBorderNode, frame: avatarFrame)
}
}
func update(size: CGSize, rect: CGRect, transition: ContainedViewLayoutTransition) {
var avatarOffset: CGFloat = 0.0
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)
}
}
final class ChatLoadingPlaceholderNode: ASDisplayNode {
private weak var backgroundNode: WallpaperBackgroundNode?
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)?
init(theme: PresentationTheme, chatWallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners, backgroundNode: WallpaperBackgroundNode, hasAvatar: Bool) {
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 ..< 8 {
let container = ChatLoadingPlaceholderMessageContainer(hasAvatar: hasAvatar, 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)
self.containerNode.addSubnode(self.effectNode)
self.addSubnode(self.borderNode)
self.borderNode.addSubnode(self.borderEffectNode)
// self.addSubnode(self.maskNode)
// self.addSubnode(self.borderMaskNode)
}
override func didLoad() {
super.didLoad()
self.containerNode.view.mask = self.maskNode.view
self.borderNode.view.mask = self.borderMaskNode.view
// self.backgroundNode?.updateIsLooping(true)
}
func animateOut(_ historyNode: ChatHistoryNode) {
guard let listNode = historyNode as? ListView, let (size, _) = self.validLayout else {
return
}
self.backgroundNode?.updateIsLooping(false)
let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut)
var lastFrame: CGRect?
var i = 0
listNode.forEachVisibleItemNode { itemNode in
guard i < self.messageContainers.count, let listItemNode = itemNode as? ListViewItemNode else {
return
}
if let bubbleItemNode = itemNode as? ChatMessageBubbleItemNode,
bubbleItemNode.contentNodes.contains(where: { $0 is ChatMessageActionBubbleContentNode }) {
return
}
let messageContainer = self.messageContainers[i]
messageContainer.animateWith(listItemNode, transition: transition)
lastFrame = messageContainer.frame
i += 1
}
if let lastFrame = lastFrame, i < self.messageContainers.count {
var offset = lastFrame.minY
for k in i ..< self.messageContainers.count {
let messageContainer = self.messageContainers[k]
let messageSize = messageContainer.frame.size
messageContainer.update(size: size, rect: CGRect(origin: CGPoint(x: 0.0, y: offset - messageSize.height), size: messageSize), transition: transition)
offset -= messageSize.height
}
}
}
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)
}
}
func updateLayout(size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, insets)
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.15), horizontal: true, effectSize: 280.0, globalTimeOffset: false, duration: 1.6)
self.borderEffectNode.update(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.45), horizontal: true, effectSize: 320.0, globalTimeOffset: false, duration: 1.6)
let shortHeight: CGFloat = 71.0
let tallHeight: CGFloat = 93.0
let dimensions: [CGSize] = [
CGSize(width: floorToScreenPixels(0.47 * size.width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.57 * size.width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.73 * size.width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.47 * size.width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.57 * size.width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.36 * size.width), height: shortHeight),
CGSize(width: floorToScreenPixels(0.47 * size.width), height: tallHeight),
CGSize(width: floorToScreenPixels(0.57 * size.width), height: tallHeight),
]
var offset: CGFloat = 5.0
var i = 0
for messageContainer in self.messageContainers {
let messageSize = dimensions[i % 12]
messageContainer.update(size: size, rect: CGRect(origin: CGPoint(x: 0.0, y: size.height - insets.bottom - offset - messageSize.height), size: messageSize), transition: transition)
offset += messageSize.height
i += 1
}
if backgroundNode?.hasExtraBubbleBackground() == true {
self.backgroundColorNode.isHidden = true
} else {
self.backgroundColorNode.isHidden = false
}
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 {
var backgroundFrame = backgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
}
}