import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import AppBundle import LocalizedPeerData import TelegramStringFormatting import AccountContext import ChatPresentationInterfaceState import WallpaperBackgroundNode import ComponentFlow import EmojiStatusComponent import ChatLoadingNode import MultilineTextComponent import BalancedTextComponent import Markdown import ReactionSelectionNode import ChatMediaInputStickerGridItem private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize } private let titleFont = Font.semibold(15.0) private let messageFont = Font.regular(13.0) private final class ChatEmptyNodeRegularChatContent: ASDisplayNode, ChatEmptyNodeContent { private let textNode: ImmediateTextNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? override init() { self.textNode = ImmediateTextNode() super.init() self.addSubnode(self.textNode) } func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) let text: String if case .detailsPlaceholder = subject { text = interfaceState.strings.ChatList_StartMessaging } else { switch interfaceState.chatLocation { case .peer, .replyThread, .customChatContents: if case .scheduledMessages = interfaceState.subject { text = interfaceState.strings.ScheduledMessages_EmptyPlaceholder } else { text = interfaceState.strings.Conversation_EmptyPlaceholder } } } self.textNode.attributedText = NSAttributedString(string: text, font: messageFont, textColor: serviceColor.primaryText) } let insets = UIEdgeInsets(top: 6.0, left: 10.0, bottom: 6.0, right: 10.0) let textSize = self.textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude)) let contentWidth = textSize.width let contentHeight = textSize.height let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: insets.top), size: textSize)) return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size } } public protocol ChatEmptyNodeStickerContentNode: ASDisplayNode { var stickerNode: ChatMediaInputStickerGridItemNode { get } } public final class ChatEmptyNodeGreetingChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { private let context: AccountContext private let interaction: ChatPanelInterfaceInteraction? private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode private var stickerItem: ChatMediaInputStickerGridItem? public var stickerNode: ChatMediaInputStickerGridItemNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? private var didSetupSticker = false private let disposable = MetaDisposable() private var currentCustomStickerFile: TelegramMediaFile? public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { self.context = context self.interaction = interaction self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 0 self.titleNode.lineSpacing = 0.15 self.titleNode.textAlignment = .center self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 0 self.textNode.lineSpacing = 0.15 self.textNode.textAlignment = .center self.textNode.isUserInteractionEnabled = false self.textNode.displaysAsynchronously = false self.stickerNode = ChatMediaInputStickerGridItemNode() super.init() self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.stickerNode) } override public func didLoad() { super.didLoad() let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:))) tapRecognizer.delegate = self self.stickerNode.view.addGestureRecognizer(tapRecognizer) } deinit { self.disposable.dispose() } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @objc private func stickerTapGesture(_ gestureRecognizer: UITapGestureRecognizer) { guard let stickerItem = self.stickerItem else { return } let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds, nil, []) } public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let isFirstTime = self.currentTheme == nil if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings } var customStickerFile: TelegramMediaFile? let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) if case let .emptyChat(emptyChat) = subject, case let .customGreeting(stickerFile, title, text) = emptyChat { customStickerFile = stickerFile self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: serviceColor.primaryText) self.textNode.attributedText = NSAttributedString(string: text, font: messageFont, textColor: serviceColor.primaryText) } else if let businessIntro = interfaceState.businessIntro { self.titleNode.attributedText = NSAttributedString(string: !businessIntro.title.isEmpty ? businessIntro.title : interfaceState.strings.Conversation_EmptyPlaceholder, font: titleFont, textColor: serviceColor.primaryText) self.textNode.attributedText = NSAttributedString(string: !businessIntro.text.isEmpty ? businessIntro.text : interfaceState.strings.Conversation_GreetingText, font: messageFont, textColor: serviceColor.primaryText) customStickerFile = businessIntro.stickerFile } else { self.titleNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_EmptyPlaceholder, font: titleFont, textColor: serviceColor.primaryText) self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_GreetingText, font: messageFont, textColor: serviceColor.primaryText) } let previousCustomStickerFile = self.currentCustomStickerFile self.currentCustomStickerFile = customStickerFile let stickerSize: CGSize let inset: CGFloat if size.width == 320.0 { stickerSize = CGSize(width: 106.0, height: 106.0) inset = 8.0 } else { stickerSize = CGSize(width: 160.0, height: 160.0) inset = 15.0 } if let item = self.stickerItem, previousCustomStickerFile == customStickerFile { self.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) } else if !self.didSetupSticker || previousCustomStickerFile != customStickerFile { let sticker: Signal if let customStickerFile { sticker = .single(customStickerFile) } else if let preloadedSticker = interfaceState.greetingData?.sticker { sticker = preloadedSticker } else { sticker = self.context.engine.stickers.randomGreetingSticker() |> map { item -> TelegramMediaFile? in return item?.file } } if !isFirstTime, case let .emptyChat(emptyChat) = subject, case .customGreeting = emptyChat { let previousStickerNode = self.stickerNode previousStickerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousStickerNode] _ in previousStickerNode?.removeFromSupernode() }) previousStickerNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) self.stickerNode = ChatMediaInputStickerGridItemNode() self.addSubnode(self.stickerNode) self.stickerNode.layer.animateSpring(from: 0.001 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5) self.stickerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } self.didSetupSticker = true self.disposable.set((sticker |> deliverOnMainQueue).startStrict(next: { [weak self] sticker in if let strongSelf = self, let sticker = sticker { let inputNodeInteraction = ChatMediaInputNodeInteraction( navigateToCollectionId: { _ in }, navigateBackToStickers: { }, setGifMode: { _ in }, openSettings: { }, openTrending: { _ in }, dismissTrendingPacks: { _ in }, toggleSearch: { _, _, _ in }, openPeerSpecificSettings: { }, dismissPeerSpecificSettings: { }, clearRecentlyUsedStickers: { } ) inputNodeInteraction.displayStickerPlaceholder = false let index = ItemCollectionItemIndex(index: 0, id: 0) let collectionId = ItemCollectionId(namespace: 0, id: 0) let stickerPackItem = StickerPackItem(index: index, file: sticker, indexKeys: []) let item = ChatMediaInputStickerGridItem(context: strongSelf.context, collectionId: collectionId, stickerPackInfo: nil, index: ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: collectionId, itemIndex: index), stickerItem: stickerPackItem, canManagePeerSpecificPack: nil, interfaceInteraction: nil, inputNodeInteraction: inputNodeInteraction, hasAccessory: false, theme: interfaceState.theme, large: true, selected: {}) strongSelf.stickerItem = item if isFirstTime { } strongSelf.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) strongSelf.stickerNode.isVisibleInGrid = true strongSelf.stickerNode.updateIsPanelVisible(true) } })) } let insets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) let titleSpacing: CGFloat = 5.0 let stickerSpacing: CGFloat = 5.0 var contentWidth: CGFloat = 220.0 var contentHeight: CGFloat = 0.0 let titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) let textSize = self.textNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, max(titleSize.width, textSize.width)) contentHeight += titleSize.height + titleSpacing + textSize.height + stickerSpacing + stickerSize.height let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - titleSize.width) / 2.0), y: contentRect.minY), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) let textFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize) transition.updateFrame(node: self.textNode, frame: textFrame) let stickerFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - stickerSize.width) / 2.0), y: textFrame.maxY + stickerSpacing), size: stickerSize) transition.updateFrame(node: self.stickerNode, frame: stickerFrame) return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size } } public final class ChatEmptyNodeNearbyChatContent: ASDisplayNode, ChatEmptyNodeStickerContentNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { private let context: AccountContext private let interaction: ChatPanelInterfaceInteraction? private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode private var stickerItem: ChatMediaInputStickerGridItem? public let stickerNode: ChatMediaInputStickerGridItemNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? private var didSetupSticker = false private let disposable = MetaDisposable() public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { self.context = context self.interaction = interaction self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 0 self.titleNode.lineSpacing = 0.15 self.titleNode.textAlignment = .center self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 0 self.textNode.lineSpacing = 0.15 self.textNode.textAlignment = .center self.textNode.isUserInteractionEnabled = false self.textNode.displaysAsynchronously = false self.stickerNode = ChatMediaInputStickerGridItemNode() super.init() self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.stickerNode) } override public func didLoad() { super.didLoad() let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.stickerTapGesture(_:))) tapRecognizer.delegate = self self.stickerNode.view.addGestureRecognizer(tapRecognizer) } deinit { self.disposable.dispose() } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } @objc private func stickerTapGesture(_ gestureRecognizer: UITapGestureRecognizer) { guard let stickerItem = self.stickerItem else { return } let _ = self.interaction?.sendSticker(.standalone(media: stickerItem.stickerItem.file), false, self.view, self.stickerNode.bounds, nil, []) } public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings var displayName = "" let distance = interfaceState.peerNearbyData?.distance ?? 0 if let renderedPeer = interfaceState.renderedPeer { if let chatPeer = renderedPeer.peers[renderedPeer.peerId] { displayName = EnginePeer(chatPeer).compactDisplayTitle } } let titleString = interfaceState.strings.Conversation_PeerNearbyTitle(displayName, shortStringForDistance(strings: interfaceState.strings, distance: distance)).string let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText) self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_PeerNearbyText, font: messageFont, textColor: serviceColor.primaryText) } let stickerSize = CGSize(width: 160.0, height: 160.0) if let item = self.stickerItem { self.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) } else if !self.didSetupSticker { let sticker: Signal if let preloadedSticker = interfaceState.greetingData?.sticker { sticker = preloadedSticker } else { sticker = self.context.engine.stickers.randomGreetingSticker() |> map { item -> TelegramMediaFile? in return item?.file } } self.didSetupSticker = true self.disposable.set((sticker |> deliverOnMainQueue).startStrict(next: { [weak self] sticker in if let strongSelf = self, let sticker = sticker { let inputNodeInteraction = ChatMediaInputNodeInteraction( navigateToCollectionId: { _ in }, navigateBackToStickers: { }, setGifMode: { _ in }, openSettings: { }, openTrending: { _ in }, dismissTrendingPacks: { _ in }, toggleSearch: { _, _, _ in }, openPeerSpecificSettings: { }, dismissPeerSpecificSettings: { }, clearRecentlyUsedStickers: { } ) inputNodeInteraction.displayStickerPlaceholder = false let index = ItemCollectionItemIndex(index: 0, id: 0) let collectionId = ItemCollectionId(namespace: 0, id: 0) let stickerPackItem = StickerPackItem(index: index, file: sticker, indexKeys: []) let item = ChatMediaInputStickerGridItem(context: strongSelf.context, collectionId: collectionId, stickerPackInfo: nil, index: ItemCollectionViewEntryIndex(collectionIndex: 0, collectionId: collectionId, itemIndex: index), stickerItem: stickerPackItem, canManagePeerSpecificPack: nil, interfaceInteraction: nil, inputNodeInteraction: inputNodeInteraction, hasAccessory: false, theme: interfaceState.theme, large: true, selected: {}) strongSelf.stickerItem = item strongSelf.stickerNode.updateLayout(item: item, size: stickerSize, isVisible: true, synchronousLoads: true) strongSelf.stickerNode.isVisibleInGrid = true strongSelf.stickerNode.updateIsPanelVisible(true) } })) } let insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) let titleSpacing: CGFloat = 5.0 let stickerSpacing: CGFloat = 5.0 var contentWidth: CGFloat = 210.0 var contentHeight: CGFloat = 0.0 let titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) let textSize = self.textNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, max(titleSize.width, textSize.width)) contentHeight += titleSize.height + titleSpacing + textSize.height + stickerSpacing + stickerSize.height let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - titleSize.width) / 2.0), y: contentRect.minY), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) let textFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize) transition.updateFrame(node: self.textNode, frame: textFrame) let stickerFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - stickerSize.width) / 2.0), y: textFrame.maxY + stickerSpacing), size: stickerSize) transition.updateFrame(node: self.stickerNode, frame: stickerFrame) return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size } } private final class ChatEmptyNodeSecretChatContent: ASDisplayNode, ChatEmptyNodeContent { private let titleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode private var lineNodes: [(ASImageNode, ImmediateTextNode)] = [] private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? override init() { self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 0 self.titleNode.lineSpacing = 0.25 self.titleNode.textAlignment = .center self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.subtitleNode = ImmediateTextNode() self.subtitleNode.maximumNumberOfLines = 0 self.subtitleNode.lineSpacing = 0.25 self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.displaysAsynchronously = false super.init() self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode) } func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings var title = " " var incoming = false if let renderedPeer = interfaceState.renderedPeer { if let chatPeer = renderedPeer.peers[renderedPeer.peerId] as? TelegramSecretChat { if case .participant = chatPeer.role { incoming = true } if let user = renderedPeer.peers[chatPeer.regularPeerId] { title = EnginePeer(user).compactDisplayTitle } } } let titleString: String if incoming { titleString = interfaceState.strings.Conversation_EncryptedPlaceholderTitleIncoming(title).string } else { titleString = interfaceState.strings.Conversation_EncryptedPlaceholderTitleOutgoing(title).string } let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText) self.subtitleNode.attributedText = NSAttributedString(string: interfaceState.strings.Conversation_EncryptedDescriptionTitle, font: messageFont, textColor: serviceColor.primaryText) let strings: [String] = [ interfaceState.strings.Conversation_EncryptedDescription1, interfaceState.strings.Conversation_EncryptedDescription2, interfaceState.strings.Conversation_EncryptedDescription3, interfaceState.strings.Conversation_EncryptedDescription4 ] let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) } let graphics = PresentationResourcesChat.additionalGraphics(interfaceState.theme, wallpaper: interfaceState.chatWallpaper, bubbleCorners: interfaceState.bubbleCorners) let lockIcon = graphics.chatEmptyItemLockIcon for i in 0 ..< lines.count { if i >= self.lineNodes.count { let iconNode = ASImageNode() iconNode.isLayerBacked = true iconNode.displaysAsynchronously = false iconNode.displayWithoutProcessing = true let textNode = ImmediateTextNode() textNode.maximumNumberOfLines = 0 textNode.isUserInteractionEnabled = false textNode.displaysAsynchronously = false self.addSubnode(iconNode) self.addSubnode(textNode) self.lineNodes.append((iconNode, textNode)) } self.lineNodes[i].0.image = lockIcon self.lineNodes[i].1.attributedText = lines[i] } } let insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) let titleSpacing: CGFloat = 5.0 let subtitleSpacing: CGFloat = 11.0 let iconInset: CGFloat = 14.0 var contentWidth: CGFloat = 100.0 var contentHeight: CGFloat = 0.0 var lineNodes: [(CGSize, ASImageNode, ImmediateTextNode)] = [] for (iconNode, textNode) in self.lineNodes { let textSize = textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, iconInset + textSize.width) contentHeight += textSize.height + subtitleSpacing lineNodes.append((textSize, iconNode, textNode)) } let titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, max(titleSize.width, subtitleSize.width)) contentHeight += titleSize.height + titleSpacing + subtitleSize.height let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - titleSize.width) / 2.0), y: contentRect.minY), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) let subtitleFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: titleFrame.maxY + titleSpacing), size: subtitleSize) transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame) var lineOffset = subtitleFrame.maxY + subtitleSpacing / 2.0 for (textSize, iconNode, textNode) in lineNodes { if let image = iconNode.image { transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.minX, y: lineOffset + 1.0), size: image.size)) } transition.updateFrame(node: textNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + iconInset, y: lineOffset), size: textSize)) lineOffset += textSize.height + subtitleSpacing } return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size } } private final class ChatEmptyNodeGroupChatContent: ASDisplayNode, ChatEmptyNodeContent { private let titleNode: ImmediateTextNode private let subtitleNode: ImmediateTextNode private var lineNodes: [(ASImageNode, ImmediateTextNode)] = [] private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? override init() { self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 0 self.titleNode.lineSpacing = 0.25 self.titleNode.textAlignment = .center self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.subtitleNode = ImmediateTextNode() self.subtitleNode.maximumNumberOfLines = 0 self.subtitleNode.lineSpacing = 0.25 self.subtitleNode.isUserInteractionEnabled = false self.subtitleNode.displaysAsynchronously = false super.init() self.addSubnode(self.titleNode) self.addSubnode(self.subtitleNode) } func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings let titleString: String = interfaceState.strings.EmptyGroupInfo_Title let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText) self.subtitleNode.attributedText = NSAttributedString(string: interfaceState.strings.EmptyGroupInfo_Subtitle, font: messageFont, textColor: serviceColor.primaryText) let strings: [String] = [ interfaceState.strings.EmptyGroupInfo_Line1("\(interfaceState.limitsConfiguration.maxSupergroupMemberCount)").string, interfaceState.strings.EmptyGroupInfo_Line2, interfaceState.strings.EmptyGroupInfo_Line3, interfaceState.strings.EmptyGroupInfo_Line4 ] let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) } let graphics = PresentationResourcesChat.additionalGraphics(interfaceState.theme, wallpaper: interfaceState.chatWallpaper, bubbleCorners: interfaceState.bubbleCorners) let lockIcon = graphics.emptyChatListCheckIcon for i in 0 ..< lines.count { if i >= self.lineNodes.count { let iconNode = ASImageNode() iconNode.isLayerBacked = true iconNode.displaysAsynchronously = false iconNode.displayWithoutProcessing = true let textNode = ImmediateTextNode() textNode.maximumNumberOfLines = 0 textNode.isUserInteractionEnabled = false textNode.displaysAsynchronously = false self.addSubnode(iconNode) self.addSubnode(textNode) self.lineNodes.append((iconNode, textNode)) } self.lineNodes[i].0.image = lockIcon self.lineNodes[i].1.attributedText = lines[i] } } let insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) let titleSpacing: CGFloat = 5.0 let subtitleSpacing: CGFloat = 11.0 let iconInset: CGFloat = 19.0 var contentWidth: CGFloat = 100.0 var contentHeight: CGFloat = 0.0 var lineNodes: [(CGSize, ASImageNode, ImmediateTextNode)] = [] for (iconNode, textNode) in self.lineNodes { let textSize = textNode.updateLayout(CGSize(width: size.width - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, iconInset + textSize.width) contentHeight += textSize.height + subtitleSpacing lineNodes.append((textSize, iconNode, textNode)) } let titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) let subtitleSize = self.subtitleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, max(titleSize.width, subtitleSize.width)) contentHeight += titleSize.height + titleSpacing + subtitleSize.height let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - titleSize.width) / 2.0), y: contentRect.minY), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) let subtitleFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: titleFrame.maxY + titleSpacing), size: subtitleSize) transition.updateFrame(node: self.subtitleNode, frame: subtitleFrame) var lineOffset = subtitleFrame.maxY + subtitleSpacing / 2.0 for (textSize, iconNode, textNode) in lineNodes { if let image = iconNode.image { transition.updateFrame(node: iconNode, frame: CGRect(origin: CGPoint(x: contentRect.minX, y: lineOffset + 2.0), size: image.size)) } transition.updateFrame(node: textNode, frame: CGRect(origin: CGPoint(x: contentRect.minX + iconInset, y: lineOffset), size: textSize)) lineOffset += textSize.height + subtitleSpacing } return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size } } private final class ChatEmptyNodeCloudChatContent: ASDisplayNode, ChatEmptyNodeContent { private let iconNode: ASImageNode private let titleNode: ImmediateTextNode private var lineNodes: [ImmediateTextNode] = [] private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? override init() { self.iconNode = ASImageNode() self.iconNode.isLayerBacked = true self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 0 self.titleNode.lineSpacing = 0.15 self.titleNode.textAlignment = .center self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false super.init() self.addSubnode(self.iconNode) self.addSubnode(self.titleNode) } func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { var maxWidth: CGFloat = size.width var centerText = false var insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) var imageSpacing: CGFloat = 12.0 var titleSpacing: CGFloat = 4.0 if case let .customChatContents(customChatContents) = interfaceState.subject { maxWidth = min(240.0, maxWidth) switch customChatContents.kind { case .quickReplyMessageInput: insets.top = 10.0 imageSpacing = 5.0 titleSpacing = 5.0 } } if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) var iconName = "Chat/Empty Chat/Cloud" let titleString: String let strings: [String] if case let .customChatContents(customChatContents) = interfaceState.subject { switch customChatContents.kind { case let .quickReplyMessageInput(shortcut, shortcutType): switch shortcutType { case .generic: iconName = "Chat/Empty Chat/QuickReplies" centerText = false titleString = interfaceState.strings.Chat_EmptyState_QuickReply_Title strings = [ interfaceState.strings.Chat_EmptyState_QuickReply_Text1(shortcut).string, interfaceState.strings.Chat_EmptyState_QuickReply_Text2 ] case .greeting: iconName = "Chat/Empty Chat/GreetingShortcut" centerText = true titleString = interfaceState.strings.EmptyState_GreetingMessage_Title strings = [ interfaceState.strings.EmptyState_GreetingMessage_Text ] case .away: iconName = "Chat/Empty Chat/AwayShortcut" centerText = true titleString = interfaceState.strings.EmptyState_AwayMessage_Title strings = [ interfaceState.strings.EmptyState_AwayMessage_Text ] } } } else { titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title strings = [ interfaceState.strings.Conversation_ClousStorageInfo_Description1, interfaceState.strings.Conversation_ClousStorageInfo_Description2, interfaceState.strings.Conversation_ClousStorageInfo_Description3, interfaceState.strings.Conversation_ClousStorageInfo_Description4 ] } self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: serviceColor.primaryText) self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText) let lines: [NSAttributedString] = strings.map { return parseMarkdownIntoAttributedString($0, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText), bold: MarkdownAttributeSet(font: Font.semibold(14.0), textColor: serviceColor.primaryText), link: MarkdownAttributeSet(font: Font.regular(14.0), textColor: serviceColor.primaryText), linkAttribute: { url in return ("URL", url) } ), textAlignment: centerText ? .center : .natural) } for i in 0 ..< lines.count { if i >= self.lineNodes.count { let textNode = ImmediateTextNode() textNode.maximumNumberOfLines = 0 textNode.isUserInteractionEnabled = false textNode.displaysAsynchronously = false textNode.textAlignment = centerText ? .center : .natural self.addSubnode(textNode) self.lineNodes.append(textNode) } self.lineNodes[i].attributedText = lines[i] } } var contentWidth: CGFloat = 100.0 var contentHeight: CGFloat = 0.0 if let image = self.iconNode.image { contentHeight += image.size.height contentHeight += imageSpacing contentWidth = max(contentWidth, image.size.width) } var lineNodes: [(CGSize, ImmediateTextNode)] = [] for textNode in self.lineNodes { let textSize = textNode.updateLayout(CGSize(width: maxWidth - insets.left - insets.right - 10.0, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, textSize.width) contentHeight += textSize.height + titleSpacing lineNodes.append((textSize, textNode)) } let titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, titleSize.width) contentHeight += titleSize.height + titleSpacing var imageAreaHeight: CGFloat = 0.0 if let image = self.iconNode.image { imageAreaHeight += image.size.height imageAreaHeight += imageSpacing transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - image.size.width) / 2.0), y: insets.top), size: image.size)) } let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top + imageAreaHeight), size: CGSize(width: contentWidth, height: contentHeight)) let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - titleSize.width) / 2.0), y: contentRect.minY), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) var lineOffset = titleFrame.maxY + titleSpacing for (textSize, textNode) in lineNodes { let isRTL = textNode.cachedLayout?.hasRTL ?? false transition.updateFrame(node: textNode, frame: CGRect(origin: CGPoint(x: isRTL ? contentRect.maxX - textSize.width : contentRect.minX, y: lineOffset), size: textSize)) lineOffset += textSize.height + 4.0 } return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size } } public final class ChatEmptyNodeTopicChatContent: ASDisplayNode, ChatEmptyNodeContent, UIGestureRecognizerDelegate { private let context: AccountContext private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? private let iconView: ComponentView public init(context: AccountContext) { self.context = context self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 0 self.titleNode.lineSpacing = 0.15 self.titleNode.textAlignment = .center self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 0 self.textNode.lineSpacing = 0.15 self.textNode.textAlignment = .center self.textNode.isUserInteractionEnabled = false self.textNode.displaysAsynchronously = false self.iconView = ComponentView() super.init() self.addSubnode(self.titleNode) self.addSubnode(self.textNode) } public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings self.titleNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_EmptyTopicPlaceholder_Title, font: titleFont, textColor: serviceColor.primaryText) self.textNode.attributedText = NSAttributedString(string: interfaceState.strings.Chat_EmptyTopicPlaceholder_Text, font: messageFont, textColor: serviceColor.primaryText) } let inset: CGFloat if size.width == 320.0 { inset = 8.0 } else { inset = 15.0 } let iconContent: EmojiStatusComponent.Content if let fileId = interfaceState.threadData?.icon { iconContent = .animation(content: .customEmoji(fileId: fileId), size: CGSize(width: 96.0, height: 96.0), placeholderColor: .clear, themeColor: serviceColor.primaryText, loopMode: .count(2)) } else { let title = interfaceState.threadData?.title ?? "" let iconColor = interfaceState.threadData?.iconColor ?? 0 iconContent = .topic(title: String(title.prefix(1)), color: iconColor, size: CGSize(width: 64.0, height: 64.0)) } let insets = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset) let titleSpacing: CGFloat = 6.0 let iconSpacing: CGFloat = 9.0 let iconSize = self.iconView.update( transition: .easeInOut(duration: 0.2), component: AnyComponent(EmojiStatusComponent( context: self.context, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer, content: iconContent, isVisibleForAnimations: true, action: nil )), environment: {}, containerSize: CGSize(width: 54.0, height: 54.0) ) var contentWidth: CGFloat = 196.0 var contentHeight: CGFloat = 0.0 let titleSize = self.titleNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) let textSize = self.textNode.updateLayout(CGSize(width: contentWidth, height: CGFloat.greatestFiniteMagnitude)) contentWidth = max(contentWidth, max(titleSize.width, textSize.width)) contentHeight += titleSize.height + titleSpacing + textSize.height + iconSpacing + iconSize.height let contentRect = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: contentWidth, height: contentHeight)) let iconFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - iconSize.width) / 2.0), y: contentRect.minY), size: iconSize) if let iconComponentView = self.iconView.view { if iconComponentView.superview == nil { self.view.addSubview(iconComponentView) } transition.updateFrame(view: iconComponentView, frame: iconFrame) } let titleFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - titleSize.width) / 2.0), y: iconFrame.maxY + iconSpacing), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) let textFrame = CGRect(origin: CGPoint(x: contentRect.minX + floor((contentRect.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize) transition.updateFrame(node: self.textNode, frame: textFrame) return contentRect.insetBy(dx: -insets.left, dy: -insets.top).size } } public final class ChatEmptyNodePremiumRequiredChatContent: ASDisplayNode, ChatEmptyNodeContent { private let isPremiumDisabled: Bool private let interaction: ChatPanelInterfaceInteraction? private let iconBackground: SimpleLayer private let icon: UIImageView private let text = ComponentView() private let buttonTitle = ComponentView() private let button: HighlightTrackingButton private let buttonStarsNode: PremiumStarsNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) self.isPremiumDisabled = premiumConfiguration.isPremiumDisabled self.interaction = interaction self.iconBackground = SimpleLayer() self.icon = UIImageView(image: UIImage(bundleImageName: "Chat/Empty Chat/PremiumRequiredIcon")?.withRenderingMode(.alwaysTemplate)) self.button = HighlightTrackingButton() self.button.clipsToBounds = true self.buttonStarsNode = PremiumStarsNode() self.buttonStarsNode.isUserInteractionEnabled = false super.init() self.layer.addSublayer(self.iconBackground) self.view.addSubview(self.icon) if !self.isPremiumDisabled { self.view.addSubview(self.button) self.button.addSubnode(self.buttonStarsNode) self.button.highligthedChanged = { [weak self] highlighted in guard let self else { return } if highlighted { self.button.layer.removeAnimation(forKey: "opacity") self.button.alpha = 0.6 } else { self.button.alpha = 1.0 self.button.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside) } } @objc private func buttonPressed() { if let interaction = self.interaction { interaction.openPremiumRequiredForMessaging() } } public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: ChatEmptyNode.Subject, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let serviceColor = serviceMessageColorComponents(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper) let maxWidth = min(200.0, size.width) let sideInset: CGFloat = 22.0 let topInset: CGFloat = 16.0 let bottomInset: CGFloat = 16.0 let iconBackgroundSize: CGFloat = 120.0 let iconTextSpacing: CGFloat = 16.0 let textButtonSpacing: CGFloat = 12.0 let peerTitle: String if let peer = interfaceState.renderedPeer?.chatMainPeer { peerTitle = EnginePeer(peer).compactDisplayTitle } else { peerTitle = " " } let text: String if self.isPremiumDisabled { text = interfaceState.strings.Chat_EmptyStateMessagingRestrictedToPremiumDisabled_Text(peerTitle).string } else { text = interfaceState.strings.Chat_EmptyStateMessagingRestrictedToPremium_Text(peerTitle).string } let textSize = self.text.update( transition: .immediate, component: AnyComponent(BalancedTextComponent( text: .markdown(text: text, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(15.0), textColor: serviceColor.primaryText), bold: MarkdownAttributeSet(font: Font.semibold(15.0), textColor: serviceColor.primaryText), link: MarkdownAttributeSet(font: Font.regular(15.0), textColor: serviceColor.primaryText), linkAttribute: { url in return ("URL", url) } )), horizontalAlignment: .center, maximumNumberOfLines: 0 )), environment: {}, containerSize: CGSize(width: maxWidth - sideInset * 2.0, height: 500.0) ) let buttonTitleSize = self.buttonTitle.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString(string: interfaceState.strings.Chat_EmptyStateMessagingRestrictedToPremium_Action, font: Font.semibold(15.0), textColor: serviceColor.primaryText)) )), environment: {}, containerSize: CGSize(width: 200.0, height: 100.0) ) let buttonSize = CGSize(width: buttonTitleSize.width + 20.0 * 2.0, height: buttonTitleSize.height + 9.0 * 2.0) var contentsWidth: CGFloat = 0.0 contentsWidth = max(contentsWidth, iconBackgroundSize + sideInset * 2.0) contentsWidth = max(contentsWidth, textSize.width + sideInset * 2.0) if !self.isPremiumDisabled { contentsWidth = max(contentsWidth, buttonSize.width + sideInset * 2.0) } var contentsHeight: CGFloat = 0.0 contentsHeight += topInset let iconBackgroundFrame = CGRect(origin: CGPoint(x: floor((contentsWidth - iconBackgroundSize) * 0.5), y: contentsHeight), size: CGSize(width: iconBackgroundSize, height: iconBackgroundSize)) transition.updateFrame(layer: self.iconBackground, frame: iconBackgroundFrame) transition.updateCornerRadius(layer: self.iconBackground, cornerRadius: iconBackgroundSize * 0.5) self.iconBackground.backgroundColor = (interfaceState.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)).cgColor contentsHeight += iconBackgroundSize contentsHeight += iconTextSpacing self.icon.tintColor = serviceColor.primaryText if let image = self.icon.image { transition.updateFrame(view: self.icon, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + floor((iconBackgroundFrame.width - image.size.width) * 0.5), y: iconBackgroundFrame.minY + floor((iconBackgroundFrame.height - image.size.height) * 0.5)), size: image.size)) } let textFrame = CGRect(origin: CGPoint(x: floor((contentsWidth - textSize.width) * 0.5), y: contentsHeight), size: textSize) if let textView = self.text.view { if textView.superview == nil { textView.isUserInteractionEnabled = false self.view.addSubview(textView) } textView.frame = textFrame } contentsHeight += textSize.height if self.isPremiumDisabled { contentsHeight += bottomInset } else { contentsHeight += textButtonSpacing let buttonFrame = CGRect(origin: CGPoint(x: floor((contentsWidth - buttonSize.width) * 0.5), y: contentsHeight), size: buttonSize) transition.updateFrame(view: self.button, frame: buttonFrame) transition.updateCornerRadius(layer: self.button.layer, cornerRadius: buttonFrame.height * 0.5) if let buttonTitleView = self.buttonTitle.view { if buttonTitleView.superview == nil { buttonTitleView.isUserInteractionEnabled = false self.button.addSubview(buttonTitleView) } transition.updateFrame(view: buttonTitleView, frame: CGRect(origin: CGPoint(x: floor((buttonSize.width - buttonTitleSize.width) * 0.5), y: floor((buttonSize.height - buttonTitleSize.height) * 0.5)), size: buttonTitleSize)) } self.button.backgroundColor = interfaceState.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12) self.buttonStarsNode.frame = CGRect(origin: CGPoint(), size: buttonSize) contentsHeight += buttonSize.height contentsHeight += bottomInset } return CGSize(width: contentsWidth, height: contentsHeight) } } private enum ChatEmptyNodeContentType: Equatable { case regular case secret case group case cloud case peerNearby case greeting case topic case premiumRequired } public final class ChatEmptyNode: ASDisplayNode { public enum Subject { public enum EmptyType: Equatable { case generic case joined case clearedHistory case topic case botInfo case customGreeting(sticker: TelegramMediaFile?, title: String, text: String) } case emptyChat(EmptyType) case detailsPlaceholder } private let context: AccountContext private let interaction: ChatPanelInterfaceInteraction? private let backgroundNode: NavigationBackgroundNode private var wallpaperBackgroundNode: WallpaperBackgroundNode? private var backgroundContent: WallpaperBubbleBackgroundNode? private var absolutePosition: (CGRect, CGSize)? private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? private var content: (ChatEmptyNodeContentType, ASDisplayNode & ChatEmptyNodeContent)? public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { self.context = context self.interaction = interaction self.backgroundNode = NavigationBackgroundNode(color: .clear) super.init() self.isUserInteractionEnabled = false self.addSubnode(self.backgroundNode) } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { guard let result = super.hitTest(point, with: event) else { return nil } return result } public func animateFromLoadingNode(_ loadingNode: ChatLoadingNode) { guard let (_, node) = self.content else { return } let duration: Double = 0.3 node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) node.layer.animateScale(from: 0.01, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) let targetCornerRadius = self.backgroundNode.backgroundCornerRadius let targetFrame = self.backgroundNode.frame let initialFrame = loadingNode.convert(loadingNode.progressFrame, to: self) let transition = ContainedViewLayoutTransition.animated(duration: duration, curve: .spring) self.backgroundNode.layer.animateFrame(from: initialFrame, to: targetFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) self.backgroundNode.update(size: initialFrame.size, cornerRadius: initialFrame.size.width / 2.0, transition: .immediate) self.backgroundNode.update(size: targetFrame.size, cornerRadius: targetCornerRadius, transition: transition) if let backgroundContent = self.backgroundContent { backgroundContent.layer.animateFrame(from: initialFrame, to: targetFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) backgroundContent.cornerRadius = initialFrame.size.width / 2.0 transition.updateCornerRadius(layer: backgroundContent.layer, cornerRadius: targetCornerRadius) } } public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: Subject, loadingNode: ChatLoadingNode?, backgroundNode: WallpaperBackgroundNode?, size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { self.wallpaperBackgroundNode = backgroundNode if self.currentTheme !== interfaceState.theme || self.currentStrings !== interfaceState.strings { self.currentTheme = interfaceState.theme self.currentStrings = interfaceState.strings self.backgroundNode.updateColor(color: selectDateFillStaticColor(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper), enableBlur: self.context.sharedContext.energyUsageSettings.fullTranslucency && dateFillNeedsBlur(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper), transition: .immediate) } var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true } let contentType: ChatEmptyNodeContentType switch subject { case .detailsPlaceholder: contentType = .regular case let .emptyChat(emptyType): if case .customGreeting = emptyType { contentType = .greeting } else if case .customChatContents = interfaceState.subject { contentType = .cloud } else if case .replyThread = interfaceState.chatLocation { if case .topic = emptyType { contentType = .topic } else { contentType = .regular } } else if let peer = interfaceState.renderedPeer?.peer, !isScheduledMessages { if peer.id == self.context.account.peerId { contentType = .cloud } else if let _ = peer as? TelegramSecretChat { contentType = .secret } else if let group = peer as? TelegramGroup, case .creator = group.role { contentType = .group } else if let channel = peer as? TelegramChannel, case .group = channel.info, channel.flags.contains(.isCreator) && !channel.flags.contains(.isGigagroup) { contentType = .group } else if let _ = interfaceState.peerNearbyData { contentType = .peerNearby } else if let peer = peer as? TelegramUser { if interfaceState.isPremiumRequiredForMessaging { contentType = .premiumRequired } else { if peer.isDeleted || peer.botInfo != nil || peer.flags.contains(.isSupport) || peer.isScam || interfaceState.peerIsBlocked { contentType = .regular } else { contentType = .greeting } } } else { contentType = .regular } } else { contentType = .regular } } var updateGreetingSticker = false var contentTransition = transition if self.content?.0 != contentType { var animateContentIn = false if let node = self.content?.1 { node.removeFromSupernode() if self.content?.0 != nil, case .greeting = contentType, transition.isAnimated { animateContentIn = true } } let node: ASDisplayNode & ChatEmptyNodeContent switch contentType { case .regular: node = ChatEmptyNodeRegularChatContent() case .secret: node = ChatEmptyNodeSecretChatContent() case .group: node = ChatEmptyNodeGroupChatContent() case .cloud: node = ChatEmptyNodeCloudChatContent() case .peerNearby: node = ChatEmptyNodeNearbyChatContent(context: self.context, interaction: self.interaction) case .greeting: node = ChatEmptyNodeGreetingChatContent(context: self.context, interaction: self.interaction) updateGreetingSticker = true case .topic: node = ChatEmptyNodeTopicChatContent(context: self.context) case .premiumRequired: node = ChatEmptyNodePremiumRequiredChatContent(context: self.context, interaction: self.interaction) } self.content = (contentType, node) self.addSubnode(node) contentTransition = .immediate if animateContentIn, case let .animated(duration, curve) = transition { node.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) node.layer.animateScale(from: 0.0, to: 1.0, duration: duration, timingFunction: curve.timingFunction) } } self.isUserInteractionEnabled = [.peerNearby, .greeting, .premiumRequired].contains(contentType) let displayRect = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: size.width, height: size.height - insets.top - insets.bottom)) var contentSize = CGSize() if let contentNode = self.content?.1 { contentSize = contentNode.updateLayout(interfaceState: interfaceState, subject: subject, size: displayRect.size, transition: contentTransition) if updateGreetingSticker { self.context.prefetchManager?.prepareNextGreetingSticker() } } let contentFrame = CGRect(origin: CGPoint(x: displayRect.minX + floor((displayRect.width - contentSize.width) / 2.0), y: displayRect.minY + floor((displayRect.height - contentSize.height) / 2.0)), size: contentSize) if let contentNode = self.content?.1 { contentTransition.updateFrame(node: contentNode, frame: contentFrame) } transition.updateFrame(node: self.backgroundNode, frame: contentFrame) self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(20.0, self.backgroundNode.bounds.height / 2.0), transition: transition) if backgroundNode?.hasExtraBubbleBackground() == true { if self.backgroundContent == nil, let backgroundContent = backgroundNode?.makeBubbleBackground(for: .free) { backgroundContent.clipsToBounds = true self.backgroundContent = backgroundContent self.insertSubnode(backgroundContent, at: 0) } } else { self.backgroundContent?.removeFromSupernode() self.backgroundContent = nil } if let backgroundContent = self.backgroundContent { self.backgroundNode.isHidden = true backgroundContent.cornerRadius = min(20.0, self.backgroundNode.bounds.height / 2.0) transition.updateFrame(node: backgroundContent, frame: contentFrame) 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) } } else { self.backgroundNode.isHidden = false } if let loadingNode = loadingNode { self.animateFromLoadingNode(loadingNode) } } 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) } } }