diff --git a/Tusker/Screens/Main/MainSidebarViewController.swift b/Tusker/Screens/Main/MainSidebarViewController.swift index d35363f5..3391bbd5 100644 --- a/Tusker/Screens/Main/MainSidebarViewController.swift +++ b/Tusker/Screens/Main/MainSidebarViewController.swift @@ -371,7 +371,7 @@ extension MainSidebarViewController { case let .savedInstance(url): return url.host! case .addSavedInstance: - return "Find An Instance..." + return "Find an Instance..." } } diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index da54db08..10124a4f 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -25,6 +25,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { private var myProfileTab: UITab! private var listsGroup: UITabGroup! private var hashtagsGroup: UITabGroup! + private var instancesGroup: UITabGroup! private var cancellables = Set() @@ -79,6 +80,17 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { ] reloadHashtags() + instancesGroup = UITabGroup(title: "Instance Timelines", image: nil, identifier: Tab.instances.rawValue, children: []) { _ in + return AdaptableNavigationController() + } + instancesGroup.preferredPlacement = .sidebarOnly + instancesGroup.sidebarActions = [ + UIAction(title: "Find an Instance…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in + self.showAddSavedInstance() + }) + ] + reloadSavedInstances() + if UIDevice.current.userInterfaceIdiom == .phone { self.tabs = [ homeTab, @@ -105,6 +117,8 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { .merge(with: NotificationCenter.default.publisher(for: .savedHashtagsChanged).map { _ in () }) .sink { [unowned self] in self.reloadHashtags() } .store(in: &cancellables) + + NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil) } setupFastAccountSwitcher() @@ -118,7 +132,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { var exploreNavStack: [UIViewController]? = nil if let parent = selectedTab?.parent, - parent === listsGroup || parent === hashtagsGroup { + parent === listsGroup || parent === hashtagsGroup || parent === instancesGroup { let nav = parent.viewController as! any NavigationControllerProtocol exploreNavStack = nav.viewControllers nav.viewControllers = [] @@ -180,6 +194,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { composeTab, listsGroup, hashtagsGroup, + instancesGroup, ] if let (tab, navStack) = newTabAndNavigationStack { @@ -222,7 +237,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { root = FavoritesViewController(mastodonController: mastodonController) case .myProfile: root = MyProfileViewController(mastodonController: mastodonController) - case .lists, .hashtags: + case .lists, .hashtags, .instances: fatalError("unreachable") } return embedInNavigationController(root) @@ -279,6 +294,19 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { hashtagsGroup.children = tabs } + @objc private func reloadSavedInstances() { + let viewControllerProvider = { [unowned self] (tab: UITab) in + let tab = tab as! InstanceTab + return InstanceTimelineViewController(for: tab.instance.url, parentMastodonController: self.mastodonController) + } + let req = SavedInstance.fetchRequest(account: mastodonController.accountInfo!) + req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)] + let instances = (try? mastodonController.persistentContainer.viewContext.fetch(req).uniques(by: \.url)) ?? [] + instancesGroup.children = instances.map { + InstanceTab(instance: $0, viewControllerProvider: viewControllerProvider) + } + } + @objc func handleComposeKeyCommand() { compose(editing: nil) } @@ -305,6 +333,13 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { present(nav, animated: true) } + private func showAddSavedInstance() { + let findController = FindInstanceViewController(parentMastodonController: mastodonController) + findController.instanceTimelineDelegate = self + let nav = EnhancedNavigationViewController(rootViewController: findController) + present(nav, animated: true) + } + fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) { guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else { return @@ -379,6 +414,7 @@ extension NewMainTabBarViewController { case lists case hashtags + case instances } } @@ -418,7 +454,7 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { // get the new transition animation. // This would be much less complicated if the controller just used the individual VCs of items in a group. if let group = newTab.parent, - group === listsGroup || group === hashtagsGroup, + group === listsGroup || group === hashtagsGroup || group === instancesGroup, let nav = group.viewController as? any NavigationControllerProtocol { updateViewControllerSafeAreaInsets(nav) @@ -499,6 +535,9 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { } else { return nil } + } else if tab is InstanceTab { + // don't currently have a scene type for this + return nil } else if let tabID = Tab(rawValue: tab.identifier) { switch tabID { case .home: @@ -517,7 +556,7 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { return nil case .compose: activity = UserActivityManager.newPostActivity(accountID: id) - case .lists, .hashtags: + case .lists, .hashtags, .instances: return nil } } else { @@ -638,6 +677,20 @@ extension NewMainTabBarViewController: AccountSwitchableViewController { } } +@available(iOS 18.0, *) +extension NewMainTabBarViewController: InstanceTimelineViewControllerDelegate { + func didSaveInstance(url: URL) { + dismiss(animated: true) { + let tab = self.instancesGroup.tab(forIdentifier: InstanceTab.identifier(for: url))! + self.selectedTab = tab + } + } + + func didUnsaveInstance(url: URL) { + dismiss(animated: true) + } +} + private struct MyProfileContentConfiguration: UIContentConfiguration { let wrapped: any UIContentConfiguration @Box var view: UIView? @@ -754,3 +807,21 @@ private class HashtagTab: UITab { "hashtag:\(name)" } } + +@available(iOS 18.0, *) +private class InstanceTab: UITab { + let instance: SavedInstance + + init(instance: SavedInstance, viewControllerProvider: @escaping (UITab) -> UIViewController) { + self.instance = instance + super.init(title: instance.url.host!, image: UIImage(systemName: "globe"), identifier: Self.identifier(for: instance), viewControllerProvider: viewControllerProvider) + } + + static func identifier(for instance: SavedInstance) -> String { + "instance:\(instance.url.host!)" + } + + static func identifier(for instanceURL: URL) -> String { + "instance:\(instanceURL.host!)" + } +}