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 Markdown import CheckNode private func generateBoostIcon(theme: PresentationTheme) -> UIImage? { if let image = UIImage(bundleImageName: "Premium/AvatarBoost") { let size = CGSize(width: image.size.width + 4.0, height: image.size.height + 4.0) return generateImage(size, contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) if let cgImage = image.cgImage { context.draw(cgImage, in: CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: image.size)) } let lineWidth = 2.0 - UIScreenPixel context.setLineWidth(lineWidth) context.setStrokeColor(theme.actionSheet.opaqueItemBackgroundColor.cgColor) context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0 + UIScreenPixel, dy: lineWidth / 2.0 + UIScreenPixel)) }, opaque: false) } return nil } private final class PreviousBoostNode: ASDisplayNode { let checkNode: InteractiveCheckNode let avatarNode: AvatarNode let labelNode: ImmediateTextNode var pressed: (PreviousBoostNode) -> Void = { _ in } init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, peer: EnginePeer, badge: String?) { self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: theme.accentColor, strokeColor: theme.contrastColor, borderColor: theme.controlBorderColor, overlayBorder: false, hasInset: false, hasShadow: false)) self.checkNode.setSelected(false, animated: false) self.labelNode = ImmediateTextNode() self.labelNode.maximumNumberOfLines = 4 self.labelNode.isUserInteractionEnabled = true self.labelNode.attributedText = NSAttributedString(string: peer.compactDisplayTitle, font: Font.semibold(13.0), textColor: theme.primaryColor) self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 13.0)) super.init() self.addSubnode(self.checkNode) self.addSubnode(self.avatarNode) self.addSubnode(self.labelNode) self.avatarNode.setPeer(context: context, theme: ptheme, peer: peer) self.checkNode.valueChanged = { [weak self] value in if let self { if value { self.pressed(self) } } } } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize { let checkSize = CGSize(width: 22.0, height: 22.0) let condensedSize = CGSize(width: size.width - 76.0, height: size.height) let avatarSize = CGSize(width: 30.0, height: 30.0) let labelSize = self.labelNode.updateLayout(condensedSize) transition.updateFrame(node: self.checkNode, frame: CGRect(origin: CGPoint(x: 12.0, y: -2.0), size: checkSize)) transition.updateFrame(node: self.avatarNode, frame: CGRect(origin: CGPoint(x: 46.0, y: -8.0), size: avatarSize)) transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: 84.0, y: 0.0), size: labelSize)) return CGSize(width: size.width, height: checkSize.height) } func setChecked(_ checked: Bool) { self.checkNode.setSelected(checked, animated: false) } } private final class ReplaceBoostConfirmationAlertContentNode: AlertContentNode { private let strings: PresentationStrings private let text: String private let textNode: ASTextNode private let avatarNode: AvatarNode private let arrowNode: ASImageNode private let secondAvatarNode: AvatarNode private let iconNode: ASImageNode private let actionNodesSeparator: ASDisplayNode private let actionNodes: [TextAlertContentActionNode] private let actionVerticalSeparators: [ASDisplayNode] private var boostNodes: [PreviousBoostNode] = [] private var validLayout: CGSize? override var dismissOnOutsideTap: Bool { return self.isUserInteractionEnabled } init(context: AccountContext, theme: AlertControllerTheme, ptheme: PresentationTheme, strings: PresentationStrings, fromPeers: [EnginePeer], toPeer: EnginePeer, text: String, actions: [TextAlertAction]) { self.strings = strings self.text = text self.textNode = ASTextNode() self.textNode.maximumNumberOfLines = 0 self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) self.arrowNode = ASImageNode() self.arrowNode.displaysAsynchronously = false self.arrowNode.displayWithoutProcessing = true self.secondAvatarNode = AvatarNode(font: avatarPlaceholderFont(size: 26.0)) self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false self.iconNode.image = generateBoostIcon(theme: ptheme) 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 var boostNodes: [PreviousBoostNode] = [] if fromPeers.count > 1 { for peer in fromPeers { let boostNode = PreviousBoostNode(context: context, theme: theme, ptheme: ptheme, peer: peer, badge: nil) if boostNodes.isEmpty { boostNode.setChecked(true) } boostNodes.append(boostNode) } } self.boostNodes = boostNodes super.init() self.addSubnode(self.textNode) self.addSubnode(self.avatarNode) self.addSubnode(self.arrowNode) self.addSubnode(self.secondAvatarNode) self.addSubnode(self.iconNode) self.addSubnode(self.actionNodesSeparator) for actionNode in self.actionNodes { self.addSubnode(actionNode) } for separatorNode in self.actionVerticalSeparators { self.addSubnode(separatorNode) } for boostNode in self.boostNodes { boostNode.pressed = { [weak self] sender in if let self { for node in self.boostNodes { node.setChecked(node === sender) } } } self.addSubnode(boostNode) } self.updateTheme(theme) self.avatarNode.setPeer(context: context, theme: ptheme, peer: fromPeers.first!) self.secondAvatarNode.setPeer(context: context, theme: ptheme, peer: toPeer) } override func updateTheme(_ theme: AlertControllerTheme) { self.textNode.attributedText = parseMarkdownIntoAttributedString(self.text, 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.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Peer Info/AlertArrow"), color: theme.secondaryColor) 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 avatarSize = CGSize(width: 60.0, height: 60.0) self.avatarNode.updateSize(size: avatarSize) let avatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) - 44.0, y: origin.y), size: avatarSize) transition.updateFrame(node: self.avatarNode, frame: avatarFrame) if let arrowImage = self.arrowNode.image { let arrowFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - arrowImage.size.width) / 2.0), y: origin.y + floorToScreenPixels((avatarSize.height - arrowImage.size.height) / 2.0)), size: arrowImage.size) transition.updateFrame(node: self.arrowNode, frame: arrowFrame) } let secondAvatarFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - avatarSize.width) / 2.0) + 44.0, y: origin.y), size: avatarSize) transition.updateFrame(node: self.secondAvatarNode, frame: secondAvatarFrame) if let icon = self.iconNode.image { let iconFrame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 4.0 - icon.size.width, y: avatarFrame.maxY + 4.0 - icon.size.height), size: icon.size) transition.updateFrame(node: self.iconNode, frame: iconFrame) } origin.y += avatarSize.height + 10.0 var entriesHeight: CGFloat = 0.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)) origin.y += textSize.height + 10.0 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) let contentWidth = max(size.width, minActionsWidth) if !self.boostNodes.isEmpty { origin.y += 17.0 for boostNode in self.boostNodes { let boostSize = boostNode.updateLayout(size: size, transition: transition) transition.updateFrame(node: boostNode, frame: CGRect(origin: CGPoint(x: 36.0, y: origin.y), size: boostSize)) entriesHeight += boostSize.height + 20.0 origin.y += boostSize.height + 20.0 } } var actionsHeight: CGFloat = 0.0 switch effectiveActionLayout { case .horizontal: actionsHeight = actionButtonHeight case .vertical: actionsHeight = actionButtonHeight * CGFloat(self.actionNodes.count) } let resultSize = CGSize(width: contentWidth, height: avatarSize.height + textSize.height + entriesHeight + 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 replaceBoostConfirmationController(context: AccountContext, fromPeers: [EnginePeer], toPeer: EnginePeer, commit: @escaping () -> Void) -> AlertController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let strings = presentationData.strings let text = strings.ChannelBoost_ReplaceBoost(fromPeers.first!.compactDisplayTitle, toPeer.compactDisplayTitle).string var dismissImpl: ((Bool) -> Void)? var contentNode: ReplaceBoostConfirmationAlertContentNode? let actions: [TextAlertAction] = [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { dismissImpl?(true) }), TextAlertAction(type: .defaultAction, title: strings.ChannelBoost_Replace, action: { dismissImpl?(true) commit() })] contentNode = ReplaceBoostConfirmationAlertContentNode(context: context, theme: AlertControllerTheme(presentationData: presentationData), ptheme: presentationData.theme, strings: strings, fromPeers: fromPeers, toPeer: toPeer, text: text, 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 }