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 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, remove: @escaping (@escaping () -> Void) -> Void) { self.context = context self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.subject = subject self.remove = remove self.updateAcceptSecretChats = updateAcceptSecretChats 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) } } 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 secretChatsBackgroundNode: ASDisplayNode private let secretChatsHeaderNode: ImmediateTextNode private let secretChatsTitleNode: ImmediateTextNode private let secretChatsSwitchNode: SwitchNode private let secretChatsInfoNode: ImmediateTextNode 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)? 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.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.secretChatsHeaderNode = ImmediateTextNode() self.secretChatsTitleNode = ImmediateTextNode() self.secretChatsSwitchNode = SwitchNode() self.secretChatsInfoNode = ImmediateTextNode() self.cancelButton = HighlightableButtonNode() self.cancelButton.setImage(closeButtonImage(theme: self.presentationData.theme), for: .normal) 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 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 animationNode.animationView()?.logHierarchyKeypaths() 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) if !session.flags.contains(.passwordPending) && ![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.textNode.attributedText = NSAttributedString(string: subtitle, font: Font.regular(17.0), textColor: subtitleActive ? accentColor : secondaryTextColor) 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.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.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.locationInfoNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_LocationInfo, font: Font.regular(13.0), textColor: secondaryTextColor) self.locationInfoNode.maximumNumberOfLines = 3 self.secretChatsBackgroundNode = ASDisplayNode() self.secretChatsBackgroundNode.clipsToBounds = true self.secretChatsBackgroundNode.cornerRadius = 11 self.secretChatsBackgroundNode.backgroundColor = self.presentationData.theme.list.itemBlocksBackgroundColor self.secretChatsHeaderNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptSecretChatsTitle.uppercased(), font: Font.regular(17.0), textColor: textColor) self.secretChatsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptSecretChats, font: Font.regular(17.0), textColor: textColor) self.secretChatsInfoNode.attributedText = NSAttributedString(string: self.presentationData.strings.AuthSessions_View_AcceptSecretChatsInfo, font: Font.regular(17.0), textColor: secondaryTextColor) self.secretChatsInfoNode.maximumNumberOfLines = 3 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 hasSecretChats { self.contentContainerNode.addSubnode(self.secretChatsBackgroundNode) self.contentContainerNode.addSubnode(self.secretChatsHeaderNode) self.contentContainerNode.addSubnode(self.secretChatsTitleNode) self.contentContainerNode.addSubnode(self.secretChatsSwitchNode) self.contentContainerNode.addSubnode(self.secretChatsInfoNode) self.secretChatsSwitchNode.valueUpdated = { [weak self] value in if let strongSelf = self { strongSelf.updateAcceptSecretChats?(value) } } } 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.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.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.secretChatsHeaderNode.attributedText = NSAttributedString(string: self.secretChatsHeaderNode.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.secretChatsInfoNode.attributedText = NSAttributedString(string: self.secretChatsInfoNode.attributedText?.string ?? "", font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor) self.secretChatsBackgroundNode.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)) 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 if let _ = self.secretChatsBackgroundNode.supernode { let secretFrame = CGRect(x: inset, y: locationInfoTextFrame.maxY + 59.0, width: width - inset * 2.0, height: fieldItemHeight) transition.updateFrame(node: self.secretChatsBackgroundNode, frame: secretFrame) let secretChatsHeaderTextSize = self.secretChatsHeaderNode.updateLayout(CGSize(width: secretFrame.width - inset * 2.0 - locationTitleTextSize.width - 10.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.secretChatsHeaderNode, frame: secretChatsHeaderTextFrame) 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) let secretChatsInfoTextSize = self.secretChatsInfoNode.updateLayout(CGSize(width: secretFrame.width - inset * 2.0, height: fieldItemHeight)) let secretChatsInfoTextFrame = CGRect(origin: CGPoint(x: secretFrame.minX + inset, y: secretFrame.maxY + 6.0), size: secretChatsInfoTextSize) transition.updateFrame(node: self.secretChatsInfoNode, frame: secretChatsInfoTextFrame) 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) } contentHeight += secretChatsInfoTextFrame.maxY - locationInfoTextFrame.maxY } let isCurrent: Bool if case let .session(session) = self.subject, session.isCurrent { isCurrent = true } else { isCurrent = false } if isCurrent || (layout.size.width > layout.size.height && layout.metrics.widthClass == .compact) { contentHeight -= 68.0 self.terminateButton.isHidden = true } else { self.terminateButton.isHidden = false } let sideInset = floor((layout.size.width - width) / 2.0) let contentContainerFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - contentHeight), size: CGSize(width: width, height: contentHeight)) let contentFrame = contentContainerFrame 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 - insets.bottom - 6.0, width: width, height: doneButtonHeight)) transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) transition.updateFrame(node: self.topContentContainerNode, frame: contentContainerFrame) } }