2024-04-23 12:01:28 +04:00

926 lines
50 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
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, EnginePeer?)
}
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, ASScrollViewDelegate {
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
}
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 = 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 {
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.wrappedScrollViewDelegate
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 = makeContextMenuController(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
var fieldFrame = CGRect(x: inset, y: textFrame.maxY + 24.0, width: width - inset * 2.0, height: fieldItemHeight * 2.0)
if !(self.ipValueNode.attributedText?.string ?? "").isEmpty {
fieldFrame.size.height += fieldItemHeight
self.ipTitleNode.isHidden = false
self.ipValueNode.isHidden = false
self.secondSeparatorNode.isHidden = false
} else {
self.ipTitleNode.isHidden = true
self.ipValueNode.isHidden = true
self.secondSeparatorNode.isHidden = true
}
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.maxY - 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.maxY - 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)
}
}