import Foundation import UIKit import SwiftSignalKit import AsyncDisplayKit import Display import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext import AppBundle import AvatarNode import EmojiTextAttachmentView import TextFormat import Markdown private final class IconsNode: ASDisplayNode { private let context: AccountContext private var animationLayer: InlineStickerItemLayer? private var files: [TelegramMediaFile.Accessor] private var currentIndex = 0 private var switchingToNext = false private var timer: SwiftSignalKit.Timer? private var currentParams: (size: CGSize, theme: PresentationTheme)? init(context: AccountContext, files: [TelegramMediaFile.Accessor]) { self.context = context self.files = files super.init() } deinit { self.timer?.invalidate() } func updateLayout(size: CGSize, theme: PresentationTheme) { self.currentParams = (size, theme) if self.timer == nil { self.timer = SwiftSignalKit.Timer(timeout: 2.5, repeat: true, completion: { [weak self] in guard let self else { return } self.switchingToNext = true if let (size, theme) = self.currentParams { self.updateLayout(size: size, theme: theme) } }, queue: Queue.mainQueue()) self.timer?.start() } let animationLayer: InlineStickerItemLayer var disappearingAnimationLayer: InlineStickerItemLayer? if let current = self.animationLayer, !self.switchingToNext { animationLayer = current } else { if self.switchingToNext { self.currentIndex = (self.currentIndex + 1) % self.files.count disappearingAnimationLayer = self.animationLayer self.switchingToNext = false } let file = self.files[self.currentIndex]._parse() let emoji = ChatTextInputTextCustomEmojiAttribute( interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file ) animationLayer = InlineStickerItemLayer( context: .account(self.context), userLocation: .other, attemptSynchronousLoad: false, emoji: emoji, file: file, cache: self.context.animationCache, renderer: self.context.animationRenderer, unique: true, placeholderColor: theme.list.mediaPlaceholderColor, pointSize: CGSize(width: 20.0, height: 20.0), loopCount: 1 ) animationLayer.isVisibleForAnimations = true animationLayer.dynamicColor = theme.actionSheet.controlAccentColor self.view.layer.addSublayer(animationLayer) self.animationLayer = animationLayer animationLayer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) animationLayer.animatePosition(from: CGPoint(x: 0.0, y: 10.0), to: .zero, duration: 0.2, additive: true) animationLayer.animateScale(from: 0.01, to: 1.0, duration: 0.2) } animationLayer.frame = CGRect(origin: .zero, size: CGSize(width: 20.0, height: 20.0)) if let disappearingAnimationLayer { disappearingAnimationLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in disappearingAnimationLayer.removeFromSuperlayer() }) disappearingAnimationLayer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -10.0), duration: 0.2, removeOnCompletion: false, additive: true) disappearingAnimationLayer.animateScale(from: 1.0, to: 0.01, duration: 0.2, removeOnCompletion: false) } } } private final class WebAppEmojiStatusAlertContentNode: AlertContentNode { private let strings: PresentationStrings private let presentationTheme: PresentationTheme private let botName: String private let textNode: ASTextNode private let iconBackgroundNode: ASImageNode private let iconAvatarNode: AvatarNode private let iconNameNode: ASTextNode private let iconAnimationNode: IconsNode private let actionNodesSeparator: ASDisplayNode private let actionNodes: [TextAlertContentActionNode] private let actionVerticalSeparators: [ASDisplayNode] private var validLayout: CGSize? override var dismissOnOutsideTap: Bool { return self.isUserInteractionEnabled } init( context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, accountPeer: EnginePeer, botName: String, icons: [TelegramMediaFile.Accessor], actions: [TextAlertAction] ) { self.strings = strings self.presentationTheme = ptheme self.botName = botName self.textNode = ASTextNode() self.textNode.maximumNumberOfLines = 0 self.iconBackgroundNode = ASImageNode() self.iconBackgroundNode.displaysAsynchronously = false self.iconBackgroundNode.image = generateStretchableFilledCircleImage(radius: 16.0, color: theme.separatorColor) self.iconAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 14.0)) self.iconAvatarNode.setPeer(context: context, theme: ptheme, peer: accountPeer) self.iconNameNode = ASTextNode() self.iconNameNode.attributedText = NSAttributedString(string: accountPeer.compactDisplayTitle, font: Font.medium(15.0), textColor: theme.primaryColor) self.iconAnimationNode = IconsNode(context: context, files: icons) self.actionNodesSeparator = ASDisplayNode() self.actionNodesSeparator.isLayerBacked = true self.actionNodes = actions.map { action -> TextAlertContentActionNode in return TextAlertContentActionNode(theme: theme, action: action) } var actionVerticalSeparators: [ASDisplayNode] = [] if actions.count > 1 { for _ in 0 ..< actions.count - 1 { let separatorNode = ASDisplayNode() separatorNode.isLayerBacked = true actionVerticalSeparators.append(separatorNode) } } self.actionVerticalSeparators = actionVerticalSeparators super.init() self.addSubnode(self.textNode) self.addSubnode(self.iconBackgroundNode) self.addSubnode(self.iconAvatarNode) self.addSubnode(self.iconNameNode) self.addSubnode(self.iconAnimationNode) self.addSubnode(self.actionNodesSeparator) for actionNode in self.actionNodes { self.addSubnode(actionNode) } for separatorNode in self.actionVerticalSeparators { self.addSubnode(separatorNode) } self.updateTheme(theme) } override func updateTheme(_ theme: AlertControllerTheme) { let string = self.strings.WebApp_EmojiPermission_Text(self.botName, self.botName).string let attributedText = parseMarkdownIntoAttributedString(string, attributes: MarkdownAttributes( body: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), bold: MarkdownAttributeSet(font: Font.semibold(13.0), textColor: theme.primaryColor), link: MarkdownAttributeSet(font: Font.regular(13.0), textColor: theme.primaryColor), linkAttribute: { url in return ("URL", url) } ), textAlignment: .center) self.textNode.attributedText = attributedText self.actionNodesSeparator.backgroundColor = theme.separatorColor for actionNode in self.actionNodes { actionNode.updateTheme(theme) } for separatorNode in self.actionVerticalSeparators { separatorNode.backgroundColor = theme.separatorColor } if let size = self.validLayout { _ = self.updateLayout(size: size, transition: .immediate) } } override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { var size = size size.width = min(size.width , 270.0) self.validLayout = size var origin: CGPoint = CGPoint(x: 0.0, y: 20.0) let iconSpacing: CGFloat = 6.0 let iconSize = CGSize(width: 32.0, height: 32.0) let nameSize = self.iconNameNode.measure(size) let totalIconWidth = iconSize.width + iconSpacing + nameSize.width + 4.0 + iconSize.width let iconBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - totalIconWidth) / 2.0), y: origin.y), size: CGSize(width: totalIconWidth, height: iconSize.height)) transition.updateFrame(node: self.iconBackgroundNode, frame: iconBackgroundFrame) transition.updateFrame(node: self.iconAvatarNode, frame: CGRect(origin: iconBackgroundFrame.origin, size: iconSize).insetBy(dx: 1.0, dy: 1.0)) transition.updateFrame(node: self.iconNameNode, frame: CGRect(origin: CGPoint(x: iconBackgroundFrame.minX + iconSize.width + iconSpacing, y: iconBackgroundFrame.minY + floorToScreenPixels((iconBackgroundFrame.height - nameSize.height) / 2.0)), size: nameSize)) self.iconAnimationNode.updateLayout(size: CGSize(width: 20.0, height: 20.0), theme: self.presentationTheme) self.iconAnimationNode.frame = CGRect(origin: CGPoint(x: iconBackgroundFrame.maxX - iconSize.width - 3.0, y: iconBackgroundFrame.minY), size: iconSize).insetBy(dx: 6.0, dy: 6.0) origin.y += iconSize.height + 16.0 let textSize = self.textNode.measure(CGSize(width: size.width - 32.0, height: size.height)) transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: origin.y), size: textSize)) let actionButtonHeight: CGFloat = 44.0 var minActionsWidth: CGFloat = 0.0 let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count)) let actionTitleInsets: CGFloat = 8.0 var effectiveActionLayout = TextAlertContentActionLayout.horizontal for actionNode in self.actionNodes { let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionButtonHeight)) if case .horizontal = effectiveActionLayout, actionTitleSize.height > actionButtonHeight * 0.6667 { effectiveActionLayout = .vertical } switch effectiveActionLayout { case .horizontal: minActionsWidth += actionTitleSize.width + actionTitleInsets case .vertical: minActionsWidth = max(minActionsWidth, actionTitleSize.width + actionTitleInsets) } } let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0) var contentWidth = minActionsWidth contentWidth = max(contentWidth, 234.0) var actionsHeight: CGFloat = 0.0 switch effectiveActionLayout { case .horizontal: actionsHeight = actionButtonHeight case .vertical: actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) } let resultWidth = contentWidth + insets.left + insets.right let resultSize = CGSize(width: resultWidth, height: iconSize.height + textSize.height + actionsHeight + 16.0 + insets.top + insets.bottom) transition.updateFrame(node: self.actionNodesSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) var actionOffset: CGFloat = 0.0 let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count)) var separatorIndex = -1 var nodeIndex = 0 for actionNode in self.actionNodes { if separatorIndex >= 0 { let separatorNode = self.actionVerticalSeparators[separatorIndex] switch effectiveActionLayout { case .horizontal: transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel))) case .vertical: transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))) } } separatorIndex += 1 let currentActionWidth: CGFloat switch effectiveActionLayout { case .horizontal: if nodeIndex == self.actionNodes.count - 1 { currentActionWidth = resultSize.width - actionOffset } else { currentActionWidth = actionWidth } case .vertical: currentActionWidth = resultSize.width } let actionNodeFrame: CGRect switch effectiveActionLayout { case .horizontal: actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) actionOffset += currentActionWidth case .vertical: actionNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight + actionOffset), size: CGSize(width: currentActionWidth, height: actionButtonHeight)) actionOffset += actionButtonHeight } transition.updateFrame(node: actionNode, frame: actionNodeFrame) nodeIndex += 1 } return resultSize } } func webAppEmojiStatusAlertController( context: AccountContext, accountPeer: EnginePeer, botName: String, icons: [TelegramMediaFile.Accessor], completion: @escaping (Bool) -> Void ) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let theme = presentationData.theme let strings = presentationData.strings var dismissImpl: ((Bool) -> Void)? var contentNode: WebAppEmojiStatusAlertContentNode? let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: strings.WebApp_EmojiPermission_Decline, action: { dismissImpl?(true) completion(false) }), TextAlertAction(type: .defaultAction, title: strings.WebApp_EmojiPermission_Allow, action: { dismissImpl?(true) completion(true) })] contentNode = WebAppEmojiStatusAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: theme, strings: strings, accountPeer: accountPeer, botName: botName, icons: icons, actions: actions) let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: contentNode!) dismissImpl = { [weak controller] animated in if animated { controller?.dismissAnimated() } else { controller?.dismiss() } } return controller }