// // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @objc class ConversationSplitViewController: UISplitViewController { private let conversationListVC = ConversationListViewController() private let detailPlaceholderVC = NoSelectedConversationViewController() private lazy var primaryNavController = OWSNavigationController(rootViewController: conversationListVC) private lazy var detailNavController = OWSNavigationController() @objc private(set) weak var selectedConversationViewController: ConversationViewController? /// The thread, if any, that is currently presented in the view hieararchy. It may be currently /// covered by a modal presentation or a pushed view controller. @objc var selectedThread: TSThread? { // If the placeholder view is in the view hierarchy, there is no selected thread. guard detailPlaceholderVC.view.superview == nil else { return nil } guard let selectedConversationViewController = selectedConversationViewController else { return nil } // In order to not show selected when collapsed during an interactive dismissal, // we verify the conversation is still in the nav stack when collapsed. There is // no interactive dismissal when expanded, so we don't have to do any special check. guard !isCollapsed || primaryNavController.viewControllers.contains(selectedConversationViewController) else { return nil } return selectedConversationViewController.thread } /// Returns the currently selected thread if it is visible on screen, otherwise /// returns nil. @objc var visibleThread: TSThread? { guard view.window?.isKeyWindow == true else { return nil } guard selectedConversationViewController?.isViewVisible == true else { return nil } return selectedThread } @objc var topViewController: UIViewController? { guard !isCollapsed else { return primaryNavController.topViewController } return detailNavController.topViewController ?? primaryNavController.topViewController } @objc init() { super.init(nibName: nil, bundle: nil) viewControllers = [primaryNavController, detailPlaceholderVC] primaryNavController.delegate = self delegate = self preferredDisplayMode = .allVisible NotificationCenter.default.addObserver(self, selector: #selector(applyTheme), name: .ThemeDidChange, object: nil) applyTheme() } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override var preferredStatusBarStyle: UIStatusBarStyle { return Theme.isDarkThemeEnabled ? .lightContent : .default } @objc func applyTheme() { view.backgroundColor = Theme.secondaryBackgroundColor applyNavBarStyle(collapsed: isCollapsed) } func applyNavBarStyle(collapsed: Bool) { guard let owsNavBar = primaryNavController.navigationBar as? OWSNavigationBar else { return owsFailDebug("unexpected nav bar") } owsNavBar.switchToStyle(collapsed ? .default : .secondaryBar) } private var hasHiddenExtraSubivew = false override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() // HACK: UISplitViewController adds an extra subview behind the navigation // bar area that extends across both views. As far as I can tell, it's not // possible to adjust the color of this view. It gets reset constantly. // Without this fix, the space between the primary and detail view has a // hairline of the wrong color, most apparent in dark mode. guard !hasHiddenExtraSubivew, let firstSubview = view.subviews.first, !viewControllers.map({ $0.view }).contains(firstSubview) else { return } hasHiddenExtraSubivew = true firstSubview.isHidden = true } @objc(closeSelectedConversationAnimated:) func closeSelectedConversation(animated: Bool) { guard let selectedConversationViewController = selectedConversationViewController else { return } if isCollapsed { // If we're currently displaying the conversation in the primary nav controller, remove it // and everything it pushed to the navigation stack from the nav controller. We don't want // to just pop to root as we might have opened this conversation from the archive. if let selectedConversationIndex = primaryNavController.viewControllers.firstIndex(of: selectedConversationViewController) { let trimmedViewControllers = Array(primaryNavController.viewControllers[0.. Bool { applyNavBarStyle(collapsed: true) // If we're currently showing the placeholder view, we want to do nothing with in // when collapsing into a signle nav controller without a side panel. guard secondaryViewController != detailPlaceholderVC else { return true } assert(secondaryViewController == detailNavController) // Move all the views from the detail nav controller onto the primary nav controller. primaryNavController.viewControllers += detailNavController.viewControllers return true } func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? { assert(primaryViewController == primaryNavController) applyNavBarStyle(collapsed: false) // See if the current conversation is currently in the view hierarchy. If not, // show the placeholder view as no conversation is selected. The conversation // was likely popped from the stack while the split view was collapsed. guard let currentConversationVC = selectedConversationViewController, let conversationVCIndex = primaryNavController.viewControllers.firstIndex(of: currentConversationVC) else { self.selectedConversationViewController = nil return detailPlaceholderVC } // Move everything on the nav stack from the conversation view on back onto // the detail nav controller. let allViewControllers = primaryNavController.viewControllers primaryNavController.viewControllers = Array(allViewControllers[0..