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 private protocol ChatEmptyNodeContent { func updateLayout(interfaceState: ChatPresentationInterfaceState, size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize } private let titleFont = Font.medium(15.0) private let messageFont = Font.regular(14.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, 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 switch interfaceState.chatLocation { case .peer, .replyThread, .feed: 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 } } protocol ChatEmptyNodeStickerContentNode: ASDisplayNode { var stickerNode: ChatMediaInputStickerGridItemNode { get } } 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? let stickerNode: ChatMediaInputStickerGridItemNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? private var didSetupSticker = false private let disposable = MetaDisposable() 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 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() } 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, []) } func updateLayout(interfaceState: ChatPresentationInterfaceState, 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) 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 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 { 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).start(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(account: strongSelf.context.account, 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: 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 } } 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? let stickerNode: ChatMediaInputStickerGridItemNode private var currentTheme: PresentationTheme? private var currentStrings: PresentationStrings? private var didSetupSticker = false private let disposable = MetaDisposable() 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 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() } 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, []) } func updateLayout(interfaceState: ChatPresentationInterfaceState, 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).start(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(account: strongSelf.context.account, 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, 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, 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, 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) self.iconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Empty Chat/Cloud"), color: serviceColor.primaryText) let titleString = interfaceState.strings.Conversation_CloudStorageInfo_Title self.titleNode.attributedText = NSAttributedString(string: titleString, font: titleFont, textColor: serviceColor.primaryText) let strings: [String] = [ interfaceState.strings.Conversation_ClousStorageInfo_Description1, interfaceState.strings.Conversation_ClousStorageInfo_Description2, interfaceState.strings.Conversation_ClousStorageInfo_Description3, interfaceState.strings.Conversation_ClousStorageInfo_Description4 ] let lines: [NSAttributedString] = strings.map { NSAttributedString(string: $0, font: messageFont, textColor: serviceColor.primaryText) } for i in 0 ..< lines.count { if i >= self.lineNodes.count { let textNode = ImmediateTextNode() textNode.maximumNumberOfLines = 0 textNode.isUserInteractionEnabled = false textNode.displaysAsynchronously = false self.addSubnode(textNode) self.lineNodes.append(textNode) } self.lineNodes[i].attributedText = lines[i] } } let insets = UIEdgeInsets(top: 15.0, left: 15.0, bottom: 15.0, right: 15.0) let imageSpacing: CGFloat = 12.0 let titleSpacing: CGFloat = 4.0 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: size.width - 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 } } 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 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) } func updateLayout(interfaceState: ChatPresentationInterfaceState, 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 //TODO:localize self.titleNode.attributedText = NSAttributedString(string: "Almost done!", font: titleFont, textColor: serviceColor.primaryText) self.textNode.attributedText = NSAttributedString(string: "Send the first message to\nstart this topic.", 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 } } private enum ChatEmptyNodeContentType: Equatable { case regular case secret case group case cloud case peerNearby case greeting case topic(Int64?) } final class ChatEmptyNode: ASDisplayNode { 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)? 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) } func animateFromLoadingNode(_ loadingNode: ChatLoadingNode) { guard let (_, node) = content else { return } let duration: Double = 0.2 node.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) node.layer.animateScale(from: 0.0, to: 1.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) 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: .easeInOut) self.backgroundNode.layer.animateFrame(from: initialFrame, to: targetFrame, duration: duration) 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) backgroundContent.cornerRadius = initialFrame.size.width / 2.0 transition.updateCornerRadius(layer: backgroundContent.layer, cornerRadius: targetCornerRadius) } } func updateLayout(interfaceState: ChatPresentationInterfaceState, emptyType: ChatHistoryNodeLoadState.EmptyType, 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: dateFillNeedsBlur(theme: interfaceState.theme, wallpaper: interfaceState.chatWallpaper), transition: .immediate) } var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true } let contentType: ChatEmptyNodeContentType if case .replyThread = interfaceState.chatLocation { if case .topic = emptyType { contentType = .topic(nil) } 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 peer.isDeleted || peer.botInfo != nil || peer.flags.contains(.isSupport) || peer.isScam || interfaceState.peerIsBlocked { contentType = .regular } else if case .clearedHistory = emptyType { 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) } 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].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, 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) } } }