import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import AccountContext import SolidRoundedButtonNode import TelegramPresentationData import TelegramUIPreferences import TelegramStringFormatting import PresentationDataUtils import AnimationUI import MergeLists import MediaResources import StickerResources import AnimatedStickerNode import TelegramAnimatedStickerNode import AvatarNode import UndoUI private func closeButtonImage(theme: PresentationTheme) -> UIImage? { return generateImage(CGSize(width: 30.0, height: 30.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor(rgb: 0x808084, alpha: 0.1).cgColor) context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) context.setLineWidth(2.0) context.setLineCap(.round) context.setStrokeColor(theme.actionSheet.inputClearButtonColor.cgColor) context.move(to: CGPoint(x: 10.0, y: 10.0)) context.addLine(to: CGPoint(x: 20.0, y: 20.0)) context.strokePath() context.move(to: CGPoint(x: 20.0, y: 10.0)) context.addLine(to: CGPoint(x: 10.0, y: 20.0)) context.strokePath() }) } final class RecentSessionScreen: ViewController { enum Subject { case session(RecentAccountSession) case website(WebAuthorization, Peer?) } private var controllerNode: RecentSessionScreenNode { return self.displayNode as! RecentSessionScreenNode } private var animatedIn = false private let context: AccountContext private let subject: RecentSessionScreen.Subject private let remove: (@escaping () -> Void) -> Void private let updateAcceptSecretChats: (Bool) -> Void private let updateAcceptIncomingCalls: (Bool) -> Void private var presentationData: PresentationData private var presentationDataDisposable: Disposable? var dismissed: (() -> Void)? var passthroughHitTestImpl: ((CGPoint) -> UIView?)? { didSet { if self.isNodeLoaded { self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl } } } init(context: AccountContext, subject: RecentSessionScreen.Subject, updateAcceptSecretChats: @escaping (Bool) -> Void, updateAcceptIncomingCalls: @escaping (Bool) -> Void, remove: @escaping (@escaping () -> Void) -> Void) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.subject = subject self.remove = remove self.updateAcceptSecretChats = updateAcceptSecretChats self.updateAcceptIncomingCalls = updateAcceptIncomingCalls super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Ignore self.blocksBackgroundWhenInOverlay = true self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { strongSelf.presentationData = presentationData strongSelf.controllerNode.updatePresentationData(presentationData) } }) self.statusBar.statusBarStyle = .Ignore } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.presentationDataDisposable?.dispose() } override public func loadDisplayNode() { self.displayNode = RecentSessionScreenNode(context: self.context, presentationData: self.presentationData, controller: self, subject: self.subject) self.controllerNode.passthroughHitTestImpl = self.passthroughHitTestImpl self.controllerNode.present = { [weak self] c in self?.present(c, in: .current) } self.controllerNode.dismiss = { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) } self.controllerNode.remove = { [weak self] in self?.remove({ self?.controllerNode.animateOut() }) } self.controllerNode.updateAcceptSecretChats = { [weak self] value in self?.updateAcceptSecretChats(value) } self.controllerNode.updateAcceptIncomingCalls = { [weak self] value in self?.updateAcceptIncomingCalls(value) } } override public func loadView() { super.loadView() self.view.disablesInteractiveTransitionGestureRecognizer = true } override public func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if !self.animatedIn { self.animatedIn = true self.controllerNode.animateIn() } } override public func dismiss(completion: (() -> Void)? = nil) { self.controllerNode.animateOut(completion: completion) self.dismissed?() } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } } private class RecentSessionScreenNode: ViewControllerTracingNode, UIScrollViewDelegate { private let context: AccountContext private var presentationData: PresentationData private weak var controller: RecentSessionScreen? private let subject: RecentSessionScreen.Subject private let dimNode: ASDisplayNode private let wrappingScrollNode: ASScrollNode private let contentContainerNode: ASDisplayNode private let topContentContainerNode: SparseNode private let backgroundNode: ASDisplayNode private let contentBackgroundNode: ASDisplayNode private var iconNode: ASImageNode? private var animationBackgroundNode: ASDisplayNode? private var animationNode: AnimationNode? private var avatarNode: AvatarNode? private let titleNode: ImmediateTextNode private let textNode: ImmediateTextNode private let fieldBackgroundNode: ASDisplayNode private let deviceTitleNode: ImmediateTextNode private let deviceValueNode: ImmediateTextNode private let firstSeparatorNode: ASDisplayNode private let ipTitleNode: ImmediateTextNode private let ipValueNode: ImmediateTextNode private let secondSeparatorNode: ASDisplayNode private let locationTitleNode: ImmediateTextNode private let locationValueNode: ImmediateTextNode private let locationInfoNode: ImmediateTextNode private let acceptBackgroundNode: ASDisplayNode private let acceptHeaderNode: ImmediateTextNode private let secretChatsTitleNode: ImmediateTextNode private let secretChatsSwitchNode: SwitchNode private let secretChatsActivateAreaNode: AccessibilityAreaNode private let incomingCallsTitleNode: ImmediateTextNode private let incomingCallsSwitchNode: SwitchNode private let incomingCallsActivateAreaNode: AccessibilityAreaNode private let acceptSeparatorNode: ASDisplayNode private let cancelButton: HighlightableButtonNode private let terminateButton: SolidRoundedButtonNode private var containerLayout: (ContainerViewLayout, CGFloat)? var present: ((ViewController) -> Void)? var remove: (() -> Void)? var dismiss: (() -> Void)? var updateAcceptSecretChats: ((Bool) -> Void)? var updateAcceptIncomingCalls: ((Bool) -> Void)? init(context: AccountContext, presentationData: PresentationData, controller: RecentSessionScreen, subject: RecentSessionScreen.Subject) { self.context = context self.controller = controller self.presentationData = presentationData self.subject = subject self.wrappingScrollNode = ASScrollNode() self.wrappingScrollNode.view.alwaysBounceVertical = true self.wrappingScrollNode.view.delaysContentTouches = false self.wrappingScrollNode.view.showsVerticalScrollIndicator = false self.wrappingScrollNode.view.canCancelContentTouches = true self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) self.contentContainerNode = ASDisplayNode() self.contentContainerNode.isOpaque = false self.topContentContainerNode = SparseNode() self.topContentContainerNode.isOpaque = false self.backgroundNode = ASDisplayNode() self.backgroundNode.clipsToBounds = true self.backgroundNode.cornerRadius = 16.0 let backgroundColor = self.presentationData.theme.list.blocksBackgroundColor let textColor = self.presentationData.theme.list.itemPrimaryTextColor let accentColor = self.presentationData.theme.list.itemAccentColor let secondaryTextColor = self.presentationData.theme.list.itemSecondaryTextColor self.contentBackgroundNode = ASDisplayNode() self.contentBackgroundNode.backgroundColor = backgroundColor self.titleNode = ImmediateTextNode() self.titleNode.maximumNumberOfLines = 2 self.titleNode.textAlignment = .center self.textNode = ImmediateTextNode() self.textNode.maximumNumberOfLines = 1 self.textNode.textAlignment = .center self.fieldBackgroundNode = ASDisplayNode() self.fieldBackgroundNode.clipsToBounds = true self.fieldBackgroundNode.cornerRadius = 11 self.fieldBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor self.deviceTitleNode = ImmediateTextNode() self.deviceValueNode = ImmediateTextNode() self.ipTitleNode = ImmediateTextNode() self.ipValueNode = ImmediateTextNode() self.locationTitleNode = ImmediateTextNode() self.locationValueNode = ImmediateTextNode() self.locationInfoNode = ImmediateTextNode() self.acceptHeaderNode = ImmediateTextNode() self.secretChatsTitleNode = ImmediateTextNode() self.secretChatsSwitchNode = SwitchNode() self.incomingCallsTitleNode = ImmediateTextNode() self.incomingCallsSwitchNode = SwitchNode() self.secretChatsActivateAreaNode = AccessibilityAreaNode() self.incomingCallsActivateAreaNode = AccessibilityAreaNode() self.cancelButton = HighlightableButtonNode() self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) self.cancelButton.accessibilityLabel = presentationData.strings.Common_Close self.cancelButton.accessibilityTraits = [.button] self.terminateButton = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: self.presentationData.theme.list.itemDestructiveColor), font: .regular, height: 44.0, cornerRadius: 11.0, gloss: false) var hasSecretChats = false var hasIncomingCalls = false let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let title: String let subtitle: String let subtitleActive: Bool let device: String let deviceTitle: String let location: String let ip: String switch subject { case let .session(session): self.terminateButton.title = self.presentationData.strings.AuthSessions_View_TerminateSession var appVersion = session.appVersion appVersion = appVersion.replacingOccurrences(of: "APPSTORE", with: "").replacingOccurrences(of: "BETA", with: "Beta").trimmingTrailingSpaces() if session.isCurrent { subtitle = presentationData.strings.Presence_online subtitleActive = true } else { subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: session.activityDate, relativeTo: timestamp) subtitleActive = false } deviceTitle = presentationData.strings.AuthSessions_View_Application var deviceString = "" if !session.deviceModel.isEmpty { deviceString = session.deviceModel } // if !session.platform.isEmpty { // if !deviceString.isEmpty { // deviceString += ", " // } // deviceString += session.platform // } // if !session.systemVersion.isEmpty { // if !deviceString.isEmpty { // deviceString += ", " // } // deviceString += session.systemVersion // } title = deviceString device = "\(session.appName) \(appVersion)" location = session.country ip = session.ip let (icon, backgroundColor, animationName, colorsArray) = iconForSession(session) if let animationName = animationName { var colors: [String: UIColor] = [:] if let colorsArray = colorsArray { for color in colorsArray { colors[color] = backgroundColor } } let animationNode = AnimationNode(animation: animationName, colors: colors, scale: 1.0) self.animationNode = animationNode let animationBackgroundNode = ASDisplayNode() animationBackgroundNode.cornerRadius = 20.0 animationBackgroundNode.backgroundColor = backgroundColor self.animationBackgroundNode = animationBackgroundNode } else if let icon = icon { let iconNode = ASImageNode() iconNode.displaysAsynchronously = false iconNode.image = icon self.iconNode = iconNode } self.secretChatsSwitchNode.isOn = session.flags.contains(.acceptsSecretChats) self.incomingCallsSwitchNode.isOn = session.flags.contains(.acceptsIncomingCalls) self.secretChatsActivateAreaNode.accessibilityValue = self.secretChatsSwitchNode.isOn ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off self.incomingCallsActivateAreaNode.accessibilityValue = self.incomingCallsSwitchNode.isOn ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off if !session.flags.contains(.passwordPending) && session.apiId != 22 { hasIncomingCalls = true if ![2040, 2496].contains(session.apiId) { hasSecretChats = true } } case let .website(website, peer): self.terminateButton.title = self.presentationData.strings.AuthSessions_View_Logout if let peer = peer { title = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) } else { title = "" } subtitle = website.domain subtitleActive = false deviceTitle = presentationData.strings.AuthSessions_View_Browser var deviceString = "" if !website.browser.isEmpty { deviceString += website.browser } if !website.platform.isEmpty { if !deviceString.isEmpty { deviceString += ", " } deviceString += website.platform } device = deviceString location = website.region ip = website.ip let avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 12.0)) avatarNode.clipsToBounds = true avatarNode.cornerRadius = 17.0 if let peer = peer.flatMap({ EnginePeer($0) }) { avatarNode.setPeer(context: context, theme: presentationData.theme, peer: peer, authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false, displayDimensions: CGSize(width: 72.0, height: 72.0), storeUnrounded: false) } self.avatarNode = avatarNode } self.titleNode.attributedText = NSAttributedString(string: title, font: Font.regular(30.0), textColor: textColor) self.titleNode.accessibilityLabel = title self.titleNode.isAccessibilityElement = true self.textNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: subtitleActive ? accentColor : secondaryTextColor) self.textNode.accessibilityLabel = subtitle self.textNode.isAccessibilityElement = true self.deviceTitleNode.attributedText = NSAttributedString(string: deviceTitle, font: Font.regular(17.0), textColor: textColor) self.deviceValueNode.attributedText = NSAttributedString(string: device, font: Font.regular(17.0), textColor: secondaryTextColor) self.deviceValueNode.accessibilityLabel = deviceTitle self.deviceValueNode.accessibilityValue = device self.deviceValueNode.isAccessibilityElement = true self.firstSeparatorNode = ASDisplayNode() self.firstSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor self.ipTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_IP, font: Font.regular(17.0), textColor: textColor) self.ipValueNode.attributedText = NSAttributedString(string: ip, font: Font.regular(17.0), textColor: secondaryTextColor) self.ipValueNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_IP self.ipValueNode.accessibilityValue = ip self.ipValueNode.isAccessibilityElement = true self.secondSeparatorNode = ASDisplayNode() self.secondSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor self.locationTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_Location, font: Font.regular(17.0), textColor: textColor) self.locationValueNode.attributedText = NSAttributedString(string: location, font: Font.regular(17.0), textColor: secondaryTextColor) self.locationValueNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_Location self.locationValueNode.accessibilityValue = location self.locationValueNode.isAccessibilityElement = true self.locationInfoNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_LocationInfo, font: Font.regular(13.0), textColor: secondaryTextColor) self.locationInfoNode.maximumNumberOfLines = 4 self.locationInfoNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_LocationInfo self.locationInfoNode.isAccessibilityElement = true self.acceptBackgroundNode = ASDisplayNode() self.acceptBackgroundNode.clipsToBounds = true self.acceptBackgroundNode.cornerRadius = 11 self.acceptBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor self.acceptHeaderNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptTitle.uppercased(), font: Font.regular(17.0), textColor: textColor) self.acceptHeaderNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptTitle self.acceptHeaderNode.isAccessibilityElement = true self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptSecretChats, font: Font.regular(17.0), textColor: textColor) self.incomingCallsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptIncomingCalls, font: Font.regular(17.0), textColor: textColor) self.secretChatsActivateAreaNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptSecretChats self.secretChatsActivateAreaNode.accessibilityHint = self.presentationData.strings.VoiceOver_Common_SwitchHint self.incomingCallsActivateAreaNode.accessibilityLabel = self.presentationData.strings.AuthSessions_View_AcceptIncomingCalls self.incomingCallsActivateAreaNode.accessibilityHint = self.presentationData.strings.VoiceOver_Common_SwitchHint self.acceptSeparatorNode = ASDisplayNode() self.acceptSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor super.init() self.backgroundColor = nil self.isOpaque = false self.addSubnode(self.dimNode) self.wrappingScrollNode.view.delegate = self self.addSubnode(self.wrappingScrollNode) self.wrappingScrollNode.addSubnode(self.backgroundNode) self.wrappingScrollNode.addSubnode(self.contentContainerNode) self.wrappingScrollNode.addSubnode(self.topContentContainerNode) self.backgroundNode.addSubnode(self.contentBackgroundNode) self.contentContainerNode.addSubnode(self.titleNode) self.contentContainerNode.addSubnode(self.textNode) self.contentContainerNode.addSubnode(self.fieldBackgroundNode) self.contentContainerNode.addSubnode(self.deviceTitleNode) self.contentContainerNode.addSubnode(self.deviceValueNode) self.contentContainerNode.addSubnode(self.ipTitleNode) self.contentContainerNode.addSubnode(self.ipValueNode) self.contentContainerNode.addSubnode(self.locationTitleNode) self.contentContainerNode.addSubnode(self.locationValueNode) self.contentContainerNode.addSubnode(self.locationInfoNode) self.contentContainerNode.addSubnode(self.firstSeparatorNode) self.contentContainerNode.addSubnode(self.secondSeparatorNode) self.contentContainerNode.addSubnode(self.terminateButton) self.topContentContainerNode.addSubnode(self.cancelButton) self.iconNode.flatMap { self.contentContainerNode.addSubnode($0) } self.animationBackgroundNode.flatMap { self.contentContainerNode.addSubnode($0) } self.animationNode.flatMap { self.contentContainerNode.addSubnode($0) } self.avatarNode.flatMap { self.contentContainerNode.addSubnode($0) } if hasIncomingCalls { self.contentContainerNode.addSubnode(self.acceptBackgroundNode) self.contentContainerNode.addSubnode(self.acceptHeaderNode) if hasSecretChats { self.contentContainerNode.addSubnode(self.secretChatsTitleNode) self.contentContainerNode.addSubnode(self.secretChatsSwitchNode) self.contentContainerNode.addSubnode(self.secretChatsActivateAreaNode) self.secretChatsSwitchNode.valueUpdated = { [weak self] value in if let strongSelf = self { strongSelf.updateAcceptSecretChats?(value) strongSelf.secretChatsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off } } self.secretChatsActivateAreaNode.activate = { [weak self] in guard let strongSelf = self else { return false } let value = !strongSelf.secretChatsSwitchNode.isOn strongSelf.updateAcceptSecretChats?(value) strongSelf.secretChatsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off return true } self.contentContainerNode.addSubnode(self.acceptSeparatorNode) } self.contentContainerNode.addSubnode(self.incomingCallsTitleNode) self.contentContainerNode.addSubnode(self.incomingCallsSwitchNode) self.contentContainerNode.addSubnode(self.incomingCallsActivateAreaNode) self.incomingCallsSwitchNode.valueUpdated = { [weak self] value in if let strongSelf = self { strongSelf.updateAcceptIncomingCalls?(value) strongSelf.incomingCallsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off } } self.incomingCallsActivateAreaNode.activate = { [weak self] in guard let strongSelf = self else { return false } let value = !strongSelf.incomingCallsSwitchNode.isOn strongSelf.updateAcceptIncomingCalls?(value) strongSelf.incomingCallsActivateAreaNode.accessibilityValue = value ? presentationData.strings.VoiceOver_Common_On : presentationData.strings.VoiceOver_Common_Off return true } } self.cancelButton.addTarget(self, action: #selector(self.cancelButtonPressed), forControlEvents: .touchUpInside) self.terminateButton.pressed = { [weak self] in if let strongSelf = self { strongSelf.remove?() } } } override func didLoad() { super.didLoad() if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { self.wrappingScrollNode.view.contentInsetAdjustmentBehavior = .never } self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture))) let titleGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleTitleLongPress(_:))) self.titleNode.view.addGestureRecognizer(titleGestureRecognizer) let deviceGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleDeviceLongPress(_:))) self.deviceValueNode.view.addGestureRecognizer(deviceGestureRecognizer) let locationGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLocationLongPress(_:))) self.locationValueNode.view.addGestureRecognizer(locationGestureRecognizer) let ipGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleIpLongPress(_:))) self.ipValueNode.view.addGestureRecognizer(ipGestureRecognizer) if let animationNode = self.animationNode { animationNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.animationPressed))) } } @objc private func handleTitleLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if gestureRecognizer.state == .began { self.displayCopyContextMenu(self.titleNode, self.titleNode.attributedText?.string ?? "") } } @objc private func handleDeviceLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if gestureRecognizer.state == .began { self.displayCopyContextMenu(self.deviceValueNode, self.deviceValueNode.attributedText?.string ?? "") } } @objc private func handleLocationLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if gestureRecognizer.state == .began { self.displayCopyContextMenu(self.locationValueNode, self.locationValueNode.attributedText?.string ?? "") } } @objc private func handleIpLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) { if gestureRecognizer.state == .began { self.displayCopyContextMenu(self.ipValueNode, self.ipValueNode.attributedText?.string ?? "") } } private func displayCopyContextMenu(_ node: ASDisplayNode, _ string: String) { if !string.isEmpty { var actions: [ContextMenuAction] = [] actions.append(ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { [weak self] in UIPasteboard.general.string = string if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } strongSelf.controller?.present(UndoOverlayController(presentationData: presentationData, content: .copy(text: presentationData.strings.Conversation_TextCopied), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), in: .window(.root)) } })) let contextMenuController = ContextMenuController(actions: actions) self.controller?.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { return (node, node.bounds.insetBy(dx: 0.0, dy: -2.0), strongSelf, strongSelf.view.bounds) } else { return nil } })) } } func updatePresentationData(_ presentationData: PresentationData) { guard !self.animatedOut else { return } let previousTheme = self.presentationData.theme self.presentationData = presentationData self.contentBackgroundNode.backgroundColor = self.presentationData.theme.list.blocksBackgroundColor self.titleNode.attributedText = NSAttributedString(string: self.titleNode.attributedText?.string ?? "", font: Font.regular(30.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) let subtitleColor: UIColor if case let .session(session) = self.subject, session.isCurrent { subtitleColor = self.presentationData.theme.list.itemAccentColor } else { subtitleColor = self.presentationData.theme.list.itemSecondaryTextColor } self.textNode.attributedText = NSAttributedString(string: self.textNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: subtitleColor) self.fieldBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor self.firstSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor self.secondSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor self.acceptSeparatorNode.backgroundColor = self.presentationData.theme.list.itemBlocksSeparatorColor self.deviceTitleNode.attributedText = NSAttributedString(string: self.deviceTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.locationTitleNode.attributedText = NSAttributedString(string: self.locationTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.ipTitleNode.attributedText = NSAttributedString(string: self.ipTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.deviceValueNode.attributedText = NSAttributedString(string: self.deviceValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) self.locationValueNode.attributedText = NSAttributedString(string: self.locationValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) self.ipValueNode.attributedText = NSAttributedString(string: self.ipValueNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) self.locationInfoNode.attributedText = NSAttributedString(string: self.locationInfoNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) self.acceptHeaderNode.attributedText = NSAttributedString(string: self.acceptHeaderNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.secretChatsTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.incomingCallsTitleNode.attributedText = NSAttributedString(string: self.incomingCallsTitleNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor) self.acceptBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor if previousTheme !== presentationData.theme, let (layout, navigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) self.terminateButton.updateTheme(SolidRoundedButtonTheme(backgroundColor: self.presentationData.theme.list.itemBlocksBackgroundColor, foregroundColor: self.presentationData.theme.list.itemDestructiveColor)) } @objc func animationPressed() { if let animationNode = self.animationNode, !animationNode.isPlaying { animationNode.playOnce() } } @objc func cancelButtonPressed() { self.animateOut() } @objc func dimTapGesture() { self.cancelButtonPressed() } private var animatedOut = false func animateIn() { self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY let dimPosition = self.dimNode.layer.position let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) let targetBounds = self.bounds self.bounds = self.bounds.offsetBy(dx: 0.0, dy: -offset) self.dimNode.position = CGPoint(x: dimPosition.x, y: dimPosition.y - offset) transition.animateView({ self.bounds = targetBounds self.dimNode.position = dimPosition }) } func animateOut(completion: (() -> Void)? = nil) { self.animatedOut = true var dimCompleted = false var offsetCompleted = false let internalCompletion: () -> Void = { [weak self] in if let strongSelf = self, dimCompleted && offsetCompleted { strongSelf.dismiss?() } completion?() } self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in dimCompleted = true internalCompletion() }) let offset = self.bounds.size.height - self.contentBackgroundNode.frame.minY self.wrappingScrollNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: -offset, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in offsetCompleted = true internalCompletion() }) self.controller?.window?.forEachController { c in if let c = c as? UndoOverlayController { c.dismiss() } } } var passthroughHitTestImpl: ((CGPoint) -> UIView?)? override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.bounds.contains(point) { if !self.contentBackgroundNode.bounds.contains(self.convert(point, to: self.contentBackgroundNode)) { return self.dimNode.view } } return super.hitTest(point, with: event) } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { let contentOffset = scrollView.contentOffset let additionalTopHeight = max(0.0, -contentOffset.y) if additionalTopHeight >= 30.0 { self.cancelButtonPressed() } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { let isFirstTime = self.containerLayout == nil self.containerLayout = (layout, navigationBarHeight) var insets = layout.insets(options: [.statusBar, .input]) let cleanInsets = layout.insets(options: [.statusBar]) insets.top = max(10.0, insets.top) let bottomInset: CGFloat = 10.0 + cleanInsets.bottom let width = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 0.0) transition.updateFrame(node: self.wrappingScrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) let iconSize = CGSize(width: 72.0, height: 72.0) let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - iconSize.width) / 2.0), y: 36.0), size: iconSize) if let iconNode = self.iconNode { transition.updateFrame(node: iconNode, frame: iconFrame) } else if let animationNode = self.animationNode, let animationBackgroundNode = self.animationBackgroundNode { transition.updateFrame(node: animationNode, frame: iconFrame) transition.updateFrame(node: animationBackgroundNode, frame: iconFrame) if #available(iOS 13.0, *) { animationBackgroundNode.layer.cornerCurve = .continuous } if isFirstTime { Queue.mainQueue().after(0.5) { animationNode.playOnce() } } } else if let avatarNode = self.avatarNode { transition.updateFrame(node: avatarNode, frame: iconFrame) } let inset: CGFloat = 16.0 let titleSize = self.titleNode.updateLayout(CGSize(width: width - inset * 2.0, height: 100.0)) let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) / 2.0), y: 120.0), size: titleSize) transition.updateFrame(node: self.titleNode, frame: titleFrame) let textSize = self.textNode.updateLayout(CGSize(width: width - inset * 2.0, height: 60.0)) let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - textSize.width) / 2.0), y: titleFrame.maxY), size: textSize) transition.updateFrame(node: self.textNode, frame: textFrame) let cancelSize = CGSize(width: 44.0, height: 44.0) let cancelFrame = CGRect(origin: CGPoint(x: width - cancelSize.width - 3.0, y: 6.0), size: cancelSize) transition.updateFrame(node: self.cancelButton, frame: cancelFrame) let fieldItemHeight: CGFloat = 44.0 let fieldFrame = CGRect(x: inset, y: textFrame.maxY + 24.0, width: width - inset * 2.0, height: fieldItemHeight * 3.0) transition.updateFrame(node: self.fieldBackgroundNode, frame: fieldFrame) let maxFieldTitleWidth = (width - inset * 4.0) * 0.4 let deviceTitleTextSize = self.deviceTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight)) let deviceTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.minY + floorToScreenPixels((fieldItemHeight - deviceTitleTextSize.height) / 2.0)), size: deviceTitleTextSize) transition.updateFrame(node: self.deviceTitleNode, frame: deviceTitleTextFrame) let deviceValueTextSize = self.deviceValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - deviceTitleTextSize.width - 10.0, height: fieldItemHeight)) let deviceValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - deviceValueTextSize.width - inset, y: fieldFrame.minY + floorToScreenPixels((fieldItemHeight - deviceValueTextSize.height) / 2.0)), size: deviceValueTextSize) transition.updateFrame(node: self.deviceValueNode, frame: deviceValueTextFrame) transition.updateFrame(node: self.firstSeparatorNode, frame: CGRect(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) let ipTitleTextSize = self.ipTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight)) let ipTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + floorToScreenPixels((fieldItemHeight - ipTitleTextSize.height) / 2.0)), size: ipTitleTextSize) transition.updateFrame(node: self.ipTitleNode, frame: ipTitleTextFrame) let ipValueTextSize = self.ipValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - ipTitleTextSize.width - 10.0, height: fieldItemHeight)) let ipValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - ipValueTextSize.width - inset, y: fieldFrame.minY + fieldItemHeight + floorToScreenPixels((fieldItemHeight - ipValueTextSize.height) / 2.0)), size: ipValueTextSize) transition.updateFrame(node: self.ipValueNode, frame: ipValueTextFrame) transition.updateFrame(node: self.secondSeparatorNode, frame: CGRect(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) let locationTitleTextSize = self.locationTitleNode.updateLayout(CGSize(width: maxFieldTitleWidth, height: fieldItemHeight)) let locationTitleTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.minY + fieldItemHeight + fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationTitleTextSize.height) / 2.0)), size: locationTitleTextSize) transition.updateFrame(node: self.locationTitleNode, frame: locationTitleTextFrame) let locationValueTextSize = self.locationValueNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0 - locationTitleTextSize.width - 10.0, height: fieldItemHeight)) let locationValueTextFrame = CGRect(origin: CGPoint(x: fieldFrame.maxX - locationValueTextSize.width - inset, y: fieldFrame.minY + fieldItemHeight + fieldItemHeight + floorToScreenPixels((fieldItemHeight - locationValueTextSize.height) / 2.0)), size: locationValueTextSize) transition.updateFrame(node: self.locationValueNode, frame: locationValueTextFrame) let locationInfoTextSize = self.locationInfoNode.updateLayout(CGSize(width: fieldFrame.width - inset * 2.0, height: fieldItemHeight * 2.0)) let locationInfoTextFrame = CGRect(origin: CGPoint(x: fieldFrame.minX + inset, y: fieldFrame.maxY + 6.0), size: locationInfoTextSize) transition.updateFrame(node: self.locationInfoNode, frame: locationInfoTextFrame) var contentHeight = locationInfoTextFrame.maxY + bottomInset + 64.0 var secretFrame = CGRect(x: inset, y: locationInfoTextFrame.maxY + 59.0, width: width - inset * 2.0, height: fieldItemHeight) if let _ = self.secretChatsTitleNode.supernode { secretFrame.size.height += fieldItemHeight } transition.updateFrame(node: self.acceptBackgroundNode, frame: secretFrame) let secretChatsHeaderTextSize = self.acceptHeaderNode.updateLayout(CGSize(width: secretFrame.width - inset * 2.0, height: fieldItemHeight)) let secretChatsHeaderTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY - secretChatsHeaderTextSize.height - 6.0), size: secretChatsHeaderTextSize) transition.updateFrame(node: self.acceptHeaderNode, frame: secretChatsHeaderTextFrame) if let _ = self.secretChatsTitleNode.supernode { let secretChatsTitleTextSize = self.secretChatsTitleNode.updateLayout(CGSize(width: width - inset * 4.0 - 80.0, height: fieldItemHeight)) let secretChatsTitleTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - secretChatsTitleTextSize.height) / 2.0)), size: secretChatsTitleTextSize) transition.updateFrame(node: self.secretChatsTitleNode, frame: secretChatsTitleTextFrame) if let switchView = self.secretChatsSwitchNode.view as? UISwitch { if self.secretChatsSwitchNode.bounds.size.width.isZero { switchView.sizeToFit() } let switchSize = switchView.bounds.size self.secretChatsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.minY + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize) self.secretChatsActivateAreaNode.frame = CGRect(origin: CGPoint(x: secretFrame.minX, y: secretFrame.minY), size: CGSize(width: fieldFrame.width, height: fieldItemHeight)) } } let incomingCallsTitleTextSize = self.incomingCallsTitleNode.updateLayout(CGSize(width: width - inset * 4.0 - 80.0, height: fieldItemHeight)) let incomingCallsTitleTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - incomingCallsTitleTextSize.height) / 2.0)), size: incomingCallsTitleTextSize) transition.updateFrame(node: self.incomingCallsTitleNode, frame: incomingCallsTitleTextFrame) transition.updateFrame(node: self.acceptSeparatorNode, frame: CGRect(x: secretFrame.minX + inset, y: secretFrame.minY + fieldItemHeight, width: fieldFrame.width - inset, height: UIScreenPixel)) if let switchView = self.incomingCallsSwitchNode.view as? UISwitch { if self.incomingCallsSwitchNode.bounds.size.width.isZero { switchView.sizeToFit() } let switchSize = switchView.bounds.size self.incomingCallsSwitchNode.frame = CGRect(origin: CGPoint(x: fieldFrame.maxX - switchSize.width - inset, y: secretFrame.maxY - fieldItemHeight + floorToScreenPixels((fieldItemHeight - switchSize.height) / 2.0)), size: switchSize) self.incomingCallsActivateAreaNode.frame = CGRect(origin: CGPoint(x: secretFrame.minX, y: secretFrame.maxY - fieldItemHeight), size: CGSize(width: fieldFrame.width, height: fieldItemHeight)) } if let _ = self.acceptBackgroundNode.supernode { contentHeight += secretFrame.maxY - locationInfoTextFrame.maxY } contentHeight += 40.0 let isCurrent: Bool if case let .session(session) = self.subject, session.isCurrent { isCurrent = true } else { isCurrent = false } if isCurrent { contentHeight -= 68.0 self.terminateButton.isHidden = true self.terminateButton.isAccessibilityElement = false } else { self.terminateButton.isHidden = false self.terminateButton.isAccessibilityElement = true } let sideInset = floor((layout.size.width - width) / 2.0) let scrollContentHeight = max(layout.size.height, contentHeight) let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: max(layout.statusBarHeight ?? 20.0, layout.size.height - contentHeight)), size: CGSize(width: width, height: contentHeight)) let contentFrame = contentContainerFrame self.wrappingScrollNode.view.contentSize = CGSize(width: layout.size.width, height: scrollContentHeight) var backgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX, y: contentFrame.minY), size: CGSize(width: width, height: contentFrame.height + 2000.0)) if backgroundFrame.minY < contentFrame.minY { backgroundFrame.origin.y = contentFrame.minY } transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) transition.updateFrame(node: self.contentBackgroundNode, frame: CGRect(origin: CGPoint(), size: backgroundFrame.size)) let doneButtonHeight = self.terminateButton.updateLayout(width: width - inset * 2.0, transition: transition) transition.updateFrame(node: self.terminateButton, frame: CGRect(x: inset, y: contentHeight - doneButtonHeight - 40.0 - insets.bottom - 6.0, width: width, height: doneButtonHeight)) transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) } }