From 3f0dd599b3b2d9344d0cf9a3c2af2bb7f42b793c Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 11 Aug 2024 21:28:05 -0700 Subject: [PATCH 01/56] Fix compiling with Xcode 16 --- Tusker/Screens/Main/MainSplitViewController.swift | 4 ++-- Tusker/Screens/Main/MainTabBarViewController.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 6fafe047..24bd1670 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -447,10 +447,10 @@ extension MainSplitViewController: UISplitViewControllerDelegate { } // Transfer the selected tab from the tab bar VC to the sidebar - switch tabBarViewController.selectedTab { + switch tabBarViewController.currentTab { case .timelines, .notifications, .myProfile: // These tabs map 1 <-> 1 with sidebar items - let item = MainSidebarViewController.Item.tab(tabBarViewController.selectedTab) + let item = MainSidebarViewController.Item.tab(tabBarViewController.currentTab) sidebar.select(item: item, animated: false) doSelect(item: item) diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 2216542b..cbf00b12 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -21,7 +21,7 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { private var fastSwitcherConstraints: [NSLayoutConstraint] = [] #endif - var selectedTab: Tab { + var currentTab: Tab { return Tab(rawValue: selectedIndex)! } @@ -353,7 +353,7 @@ extension MainTabBarViewController: TuskerRootViewController { guard presentedViewController == nil else { return .stop } - guard let vc = viewController(for: selectedTab) as? StatusBarTappableViewController else { + guard let vc = viewController(for: currentTab) as? StatusBarTappableViewController else { return .continue } return vc.handleStatusBarTapped(xPosition: xPosition) From fdbfe49a7c4686770a2e82a33daa553dc646e62f Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 19 Aug 2024 11:32:29 -0400 Subject: [PATCH 02/56] Improve tab switching animation in non-pure-black dark mode on iOS 18 --- Tusker/Screens/Main/MainTabBarViewController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index cbf00b12..bcac3df3 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -80,6 +80,8 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { #endif tabBar.isSpringLoaded = true + + view.backgroundColor = .appBackground } // Fast account switcher is not supported on visionOS From 703f6f695b1a20ea94662a10055d1deda1fac85b Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 11 Aug 2024 21:27:33 -0700 Subject: [PATCH 03/56] Update Sentry and swift-url --- Packages/Pachyderm/Package.swift | 2 +- Tusker.xcodeproj/project.pbxproj | 6 +++--- Tusker/AppDelegate.swift | 2 ++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Packages/Pachyderm/Package.swift b/Packages/Pachyderm/Package.swift index f77100f1..d7aeb347 100644 --- a/Packages/Pachyderm/Package.swift +++ b/Packages/Pachyderm/Package.swift @@ -16,7 +16,7 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/karwa/swift-url.git", branch: "main"), + .package(url: "https://github.com/karwa/swift-url.git", exact: "0.4.2"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index e4190ff0..5151e065 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -3268,7 +3268,7 @@ repositoryURL = "https://github.com/getsentry/sentry-cocoa.git"; requirement = { kind = upToNextMinorVersion; - minimumVersion = 8.21.0; + minimumVersion = 8.33.0; }; }; D6552365289870790048A653 /* XCRemoteSwiftPackageReference "ScreenCorners" */ = { @@ -3283,8 +3283,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/karwa/swift-url"; requirement = { - branch = main; - kind = branch; + kind = exactVersion; + version = 0.4.2; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 374d7b78..8810bf13 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -110,6 +110,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // we don't care about events like battery, keyboard show/hide options.enableAutoBreadcrumbTracking = false options.enableUserInteractionTracing = false + options.profilesSampleRate = nil + options.tracesSampleRate = nil options.beforeSend = { event in // just no, why would anyone need this information From 348dcc558cef54e9912c61b88f0e00cd71736998 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 11 Jul 2024 22:29:34 -0700 Subject: [PATCH 04/56] Fix profile page switching on iOS 18 --- .../ProfileHeaderCollectionViewCell.swift | 6 ++- .../ProfileStatusesViewController.swift | 42 ++++++++++--------- .../Profile/ProfileViewController.swift | 9 ++-- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/Tusker/Screens/Profile/ProfileHeaderCollectionViewCell.swift b/Tusker/Screens/Profile/ProfileHeaderCollectionViewCell.swift index bc7303d0..e74141b6 100644 --- a/Tusker/Screens/Profile/ProfileHeaderCollectionViewCell.swift +++ b/Tusker/Screens/Profile/ProfileHeaderCollectionViewCell.swift @@ -38,8 +38,10 @@ class ProfileHeaderCollectionViewCell: UICollectionViewCell { header.translatesAutoresizingMaskIntoConstraints = false contentView.embedSubview(header) self.state = .view(header) - case .view(_): - fatalError("profile header collection view cell already has view") + case .view(let existing): + if existing !== header { + fatalError("profile header collection view cell already has view") + } } } diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index c7764ed9..d1f6d3c9 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -17,7 +17,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie let filterer: Filterer private(set) var accountID: String! let kind: Kind - var initialHeaderMode: HeaderMode? + var headerViewMode: HeaderMode? weak var profileHeaderDelegate: ProfileHeaderViewDelegate? private(set) var controller: TimelineLikeController! @@ -30,7 +30,9 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie view as? UICollectionView } private(set) var dataSource: UICollectionViewDiffableDataSource! - private(set) var headerCell: ProfileHeaderCollectionViewCell? + var headerCell: ProfileHeaderCollectionViewCell? { + collectionView.cellForItem(at: IndexPath(item: 0, section: 0)) as? ProfileHeaderCollectionViewCell + } var reconfigureVisibleItemsOnEndDecelerating: Bool = false @@ -173,29 +175,29 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in switch itemIdentifier { case .header(let id): - if let headerCell = self.headerCell { - headerCell.view?.updateUI(for: id) - return headerCell - } else { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "headerCell", for: indexPath) as! ProfileHeaderCollectionViewCell - switch self.initialHeaderMode { - case nil: - fatalError("missing initialHeaderMode") - case .createView: + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "headerCell", for: indexPath) as! ProfileHeaderCollectionViewCell + switch self.headerViewMode { + case nil: + fatalError("missing headerViewMode") + case .createViewIfNeeded: + if let view = cell.view { + view.updateUI(for: id) + self.headerViewMode = .useExistingView(view) + } else { let view = ProfileHeaderView.create() view.delegate = self.profileHeaderDelegate view.updateUI(for: id) view.pagesSegmentedControl.setSelectedOption(self.owner!.currentPage, animated: false) cell.addHeader(view) - case .useExistingView(let view): - view.updateUI(for: id) - cell.addHeader(view) - case .placeholder(height: let height): - _ = cell.addConstraint(height: height) + self.headerViewMode = .useExistingView(view) } - self.headerCell = cell - return cell + case .useExistingView(let view): + view.updateUI(for: id) + cell.addHeader(view) + case .placeholder(height: let height): + _ = cell.addConstraint(height: height) } + return cell case .status(id: let id, collapseState: let collapseState, filterState: let filterState, pinned: let pinned): let (result, precomputedContent) = filterResult(state: filterState, statusID: id) switch result { @@ -411,7 +413,9 @@ extension ProfileStatusesViewController { case statuses, withReplies, onlyMedia } enum HeaderMode { - case createView, useExistingView(ProfileHeaderView), placeholder(height: CGFloat) + case createViewIfNeeded + case useExistingView(ProfileHeaderView) + case placeholder(height: CGFloat) } } diff --git a/Tusker/Screens/Profile/ProfileViewController.swift b/Tusker/Screens/Profile/ProfileViewController.swift index f4266c74..7711e66d 100644 --- a/Tusker/Screens/Profile/ProfileViewController.swift +++ b/Tusker/Screens/Profile/ProfileViewController.swift @@ -178,7 +178,7 @@ class ProfileViewController: UIViewController, StateRestorableViewController { guard let currentIndex else { assert(!animated) // if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary - new.initialHeaderMode = .createView + new.headerViewMode = .createViewIfNeeded new.view.translatesAutoresizingMaskIntoConstraints = false addChild(new) view.addSubview(new.view) @@ -213,11 +213,14 @@ class ProfileViewController: UIViewController, StateRestorableViewController { // old header cell must have the header view let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)! + // Set the outgoing VC's header view mode to placeholder, so that it does steal the header view back + // in case it updates the cell in the background. + old.headerViewMode = .placeholder(height: oldHeaderCell.bounds.height) if let newHeaderCell = new.headerCell { _ = newHeaderCell.addConstraint(height: oldHeaderCell.bounds.height) } else { - new.initialHeaderMode = .placeholder(height: oldHeaderCell.bounds.height) + new.headerViewMode = .placeholder(height: oldHeaderCell.bounds.height) } // disable user interaction during animation, to avoid any potential weird race conditions @@ -285,7 +288,7 @@ class ProfileViewController: UIViewController, StateRestorableViewController { if let newHeaderCell = new.headerCell { newHeaderCell.addHeader(headerView) } else { - new.initialHeaderMode = .useExistingView(headerView) + new.headerViewMode = .useExistingView(headerView) } self.state = .idle From a8f6aa6ed7b8ed7e14f0b281cb9227db88844264 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 19 Aug 2024 13:29:48 -0400 Subject: [PATCH 05/56] Use new UITabBarController API on iOS 18 --- Packages/Duckable/Sources/Duckable/API.swift | 4 +- .../DuckableContainerViewController.swift | 2 +- Tusker.xcodeproj/project.pbxproj | 8 + Tusker/Scenes/MainSceneDelegate.swift | 19 +- ...ountSwitchingContainerViewController.swift | 9 +- .../Main/BaseMainTabBarViewController.swift | 184 +++++++++++++++ Tusker/Screens/Main/Duckable+Root.swift | 8 +- .../Main/MainSplitViewController.swift | 15 +- .../Main/MainTabBarViewController.swift | 195 ++------------- .../Main/NewMainTabBarViewController.swift | 222 ++++++++++++++++++ .../Main/TuskerRootViewController.swift | 3 +- Tusker/Shortcuts/AppShortcutItems.swift | 2 +- .../UserActivityHandlingContext.swift | 6 +- Tusker/TuskerNavigationDelegate.swift | 7 +- 14 files changed, 461 insertions(+), 223 deletions(-) create mode 100644 Tusker/Screens/Main/BaseMainTabBarViewController.swift create mode 100644 Tusker/Screens/Main/NewMainTabBarViewController.swift diff --git a/Packages/Duckable/Sources/Duckable/API.swift b/Packages/Duckable/Sources/Duckable/API.swift index 86b682fe..4f102149 100644 --- a/Packages/Duckable/Sources/Duckable/API.swift +++ b/Packages/Duckable/Sources/Duckable/API.swift @@ -33,11 +33,11 @@ public enum DuckAttemptAction { extension UIViewController { @available(iOS 16.0, *) - public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false) -> Bool { + public func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool = false, completion: (() -> Void)? = nil) -> Bool { var cur: UIViewController? = self while let vc = cur { if let container = vc as? DuckableContainerViewController { - container.presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: nil) + container._presentDuckable(viewController, animated: animated, isDucked: isDucked, completion: completion) return true } else { cur = vc.parent diff --git a/Packages/Duckable/Sources/Duckable/DuckableContainerViewController.swift b/Packages/Duckable/Sources/Duckable/DuckableContainerViewController.swift index 705ab3da..adc0ca80 100644 --- a/Packages/Duckable/Sources/Duckable/DuckableContainerViewController.swift +++ b/Packages/Duckable/Sources/Duckable/DuckableContainerViewController.swift @@ -58,7 +58,7 @@ public class DuckableContainerViewController: UIViewController { ]) } - func presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) { + func _presentDuckable(_ viewController: DuckableViewController, animated: Bool, isDucked: Bool, completion: (() -> Void)?) { guard case .idle = state else { if animated, case .ducked(_, placeholder: let placeholder) = state { diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 5151e065..f147eef4 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -129,6 +129,8 @@ D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; }; D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; }; D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; }; + D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */; }; + D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */; }; D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; }; D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; }; D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; }; @@ -560,6 +562,8 @@ D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = ""; }; D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = ""; }; D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = ""; }; + D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMainTabBarViewController.swift; sourceTree = ""; }; + D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMainTabBarViewController.swift; sourceTree = ""; }; D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = ""; }; D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = ""; }; @@ -1124,7 +1128,9 @@ D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */, D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */, D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */, + D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */, 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */, + D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */, D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */, D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */, D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */, @@ -2141,6 +2147,7 @@ D61F75A5293ABD6F00C0B37F /* EditFilterView.swift in Sources */, D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */, D6ADB6EC28EA73CB009924AB /* StatusContentContainer.swift in Sources */, + D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */, D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */, D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */, D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, @@ -2193,6 +2200,7 @@ D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */, D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */, D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */, + D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */, D68A76E329524D2A001DA1B3 /* ListMO.swift in Sources */, D63CC70C2910AADB000E19DE /* TuskerSceneDelegate.swift in Sources */, D61F759D2938574B00C0B37F /* FilterRow.swift in Sources */, diff --git a/Tusker/Scenes/MainSceneDelegate.swift b/Tusker/Scenes/MainSceneDelegate.swift index 7b0966a7..73e4c157 100644 --- a/Tusker/Scenes/MainSceneDelegate.swift +++ b/Tusker/Scenes/MainSceneDelegate.swift @@ -63,7 +63,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate let draft = mastodonController.createDraft() let text = components.queryItems?.first(where: { $0.name == "text" })?.value draft.text = text ?? "" - rootViewController.compose(editing: draft, animated: true, isDucked: false) + rootViewController.compose(editing: draft, animated: true, isDucked: false, completion: nil) } } else { // Assume anything else is a search query @@ -266,15 +266,24 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate mastodonController.initialize() #if os(visionOS) - return MainTabBarViewController(mastodonController: mastodonController) + if #available(visionOS 2.0, *) { + return NewMainTabBarViewController(mastodonController: mastodonController) + } else { + return MainTabBarViewController(mastodonController: mastodonController) + } #else - let split = MainSplitViewController(mastodonController: mastodonController) + let mainVC: UIViewController & AccountSwitchableViewController + if #available(iOS 18.0, *) { + mainVC = NewMainTabBarViewController(mastodonController: mastodonController) + } else { + mainVC = MainSplitViewController(mastodonController: mastodonController) + } if UIDevice.current.userInterfaceIdiom == .phone, #available(iOS 16.0, *) { // TODO: maybe the duckable container should be outside the account switching container - return DuckableContainerViewController(child: split) + return DuckableContainerViewController(child: mainVC) } else { - return split + return mainVC } #endif } diff --git a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift index 6f6aede0..d2a47211 100644 --- a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift +++ b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift @@ -147,9 +147,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController { return root.stateRestorationActivity() } - func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) { + func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) { loadViewIfNeeded() - root.compose(editing: draft, animated: animated, isDucked: isDucked) + root.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion) } func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { @@ -157,11 +157,6 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController { root.select(route: route, animated: animated, completion: completion) } - func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? { - loadViewIfNeeded() - return root.getTabController(tab: tab) - } - func getNavigationDelegate() -> TuskerNavigationDelegate? { loadViewIfNeeded() return root.getNavigationDelegate() diff --git a/Tusker/Screens/Main/BaseMainTabBarViewController.swift b/Tusker/Screens/Main/BaseMainTabBarViewController.swift new file mode 100644 index 00000000..59a94cf9 --- /dev/null +++ b/Tusker/Screens/Main/BaseMainTabBarViewController.swift @@ -0,0 +1,184 @@ +// +// BaseMainTabBarViewController.swift +// Tusker +// +// Created by Shadowfacts on 8/19/24. +// Copyright © 2024 Shadowfacts. All rights reserved. +// + +import UIKit + +class BaseMainTabBarViewController: UITabBarController { + + let mastodonController: MastodonController + + #if !os(visionOS) + private(set) var fastAccountSwitcher: FastAccountSwitcherViewController! + private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView! + private var fastSwitcherConstraints: [NSLayoutConstraint] = [] + #endif + + init(mastodonController: MastodonController) { + self.mastodonController = mastodonController + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func show(_ vc: UIViewController, sender: Any?) { + if let nav = selectedViewController as? UINavigationController { + nav.pushViewController(vc, animated: true) + } else { + present(vc, animated: true) + } + } + + // Fast account switcher is not supported on visionOS + #if !os(visionOS) + func setupFastAccountSwitcher() { + fastAccountSwitcher = FastAccountSwitcherViewController() + fastAccountSwitcher.delegate = self + fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false + + tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture()) + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped)) + tapRecognizer.cancelsTouchesInView = false + tabBar.addGestureRecognizer(tapRecognizer) + + if findMyProfileTabBarButton() != nil { + fastSwitcherIndicator = FastAccountSwitcherIndicatorView() + fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(fastSwitcherIndicator) + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // i hate that we have to do this so often :S + // but doing it only in viewWillAppear makes it not appear initially + // doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed + repositionFastSwitcherIndicator() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + repositionFastSwitcherIndicator() + } + + private func repositionFastSwitcherIndicator() { + guard let myProfileButton = findMyProfileTabBarButton() else { + return + } + NSLayoutConstraint.deactivate(fastSwitcherConstraints) + let isPortrait = view.bounds.width < view.bounds.height + if traitCollection.horizontalSizeClass == .compact && isPortrait { + fastSwitcherConstraints = [ + fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor, constant: -4), + // tab bar button image width is 30 + fastSwitcherIndicator.leftAnchor.constraint(equalTo: myProfileButton.centerXAnchor, constant: 15 + 2), + ] + } else { + fastSwitcherConstraints = [ + fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor), + fastSwitcherIndicator.trailingAnchor.constraint(equalTo: myProfileButton.trailingAnchor), + ] + } + NSLayoutConstraint.activate(fastSwitcherConstraints) + } + + private func findMyProfileTabBarButton() -> UIView? { + let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") } + let tabCount: Int? + if #available(iOS 18.0, *) { + tabCount = viewControllers?.count ?? tabs.count + } else { + tabCount = viewControllers?.count + } + // sanity check that there is 1 button per VC + guard tabBarButtons.count == tabCount, + let myProfileButton = tabBarButtons.last else { + return nil + } + return myProfileButton + } + + @objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) { + fastAccountSwitcher.hide() + } + #endif // !os(visionOS) + +} + +#if !os(visionOS) +extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegate { + func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { + fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(fastAccountSwitcher.view) + NSLayoutConstraint.activate([ + fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor), + + fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor), + fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor), + + // The safe area insets don't automatically propagate for some reason, so do it ourselves. + fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + ]) + } + + func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { + guard let myProfileButton = findMyProfileTabBarButton() else { + return false + } + let locationInButton = myProfileButton.convert(point, from: tabBar) + return myProfileButton.bounds.contains(locationInButton) + } +} +#endif // !os(visionOS) + +extension BaseMainTabBarViewController: TuskerNavigationDelegate { + var apiController: MastodonController! { mastodonController } +} + +extension BaseMainTabBarViewController: StateRestorableViewController { + func stateRestorationActivity() -> NSUserActivity? { + var activity: NSUserActivity? + if let presentedNav = presentedViewController as? UINavigationController, + let compose = presentedNav.viewControllers.first as? ComposeHostingController { + let draft = compose.controller.draft + activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID) + } else if let vc = (selectedViewController as! UINavigationController).topViewController as? StateRestorableViewController { + activity = vc.stateRestorationActivity() + } + if activity == nil { + stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController") + } + return activity + } +} + +extension BaseMainTabBarViewController: BackgroundableViewController { + func sceneDidEnterBackground() { + if let selectedVC = selectedViewController as? BackgroundableViewController { + selectedVC.sceneDidEnterBackground() + } + } +} + +extension BaseMainTabBarViewController: StatusBarTappableViewController { + func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { + guard presentedViewController == nil else { + return .stop + } + guard let vc = selectedViewController as? StatusBarTappableViewController else { + return .continue + } + return vc.handleStatusBarTapped(xPosition: xPosition) + } +} diff --git a/Tusker/Screens/Main/Duckable+Root.swift b/Tusker/Screens/Main/Duckable+Root.swift index 893ce9e2..5ab80ca5 100644 --- a/Tusker/Screens/Main/Duckable+Root.swift +++ b/Tusker/Screens/Main/Duckable+Root.swift @@ -23,8 +23,8 @@ extension DuckableContainerViewController: AccountSwitchableViewController { return activity } - func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) { - (child as? TuskerRootViewController)?.compose(editing: draft, animated: animated, isDucked: isDucked) + func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) { + (child as? TuskerRootViewController)?.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion) } func getNavigationDelegate() -> TuskerNavigationDelegate? { @@ -39,10 +39,6 @@ extension DuckableContainerViewController: AccountSwitchableViewController { (child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion) } - func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? { - return (child as? TuskerRootViewController)?.getTabController(tab: tab) - } - func performSearch(query: String) { (child as? TuskerRootViewController)?.performSearch(query: query) } diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 24bd1670..14f8a35b 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import TuskerPreferences +@available(iOS, obsoleted: 18.0) class MainSplitViewController: UISplitViewController { private let mastodonController: MastodonController @@ -578,20 +579,6 @@ extension MainSplitViewController: TuskerRootViewController { completion?() } - func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? { - if traitCollection.horizontalSizeClass == .compact { - return tabBarViewController?.getTabController(tab: tab) - } else { - if tab == .compose { - return nil - } else if case .tab(tab) = sidebar.selectedItem { - return secondaryNavController - } else { - return nil - } - } - } - func getNavigationDelegate() -> TuskerNavigationDelegate? { if traitCollection.horizontalSizeClass == .compact { return tabBarViewController.getNavigationDelegate() diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index bcac3df3..ec4cb40b 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -9,18 +9,11 @@ import UIKit import ComposeUI -class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { - - private let mastodonController: MastodonController +@available(iOS, obsoleted: 18.0) +class MainTabBarViewController: BaseMainTabBarViewController { private var composePlaceholder: UIViewController! - #if !os(visionOS) - private var fastAccountSwitcher: FastAccountSwitcherViewController! - private var fastSwitcherIndicator: FastAccountSwitcherIndicatorView! - private var fastSwitcherConstraints: [NSLayoutConstraint] = [] - #endif - var currentTab: Tab { return Tab(rawValue: selectedIndex)! } @@ -33,16 +26,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { } } - init(mastodonController: MastodonController) { - self.mastodonController = mastodonController - - super.init(nibName: nil, bundle: nil) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - override func viewDidLoad() { super.viewDidLoad() @@ -62,46 +45,13 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { embedInNavigationController(Tab.myProfile.createViewController(mastodonController)), ] - #if !os(visionOS) - fastAccountSwitcher = FastAccountSwitcherViewController() - fastAccountSwitcher.delegate = self - fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false - - tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture()) - let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped)) - tapRecognizer.cancelsTouchesInView = false - tabBar.addGestureRecognizer(tapRecognizer) - - if findMyProfileTabBarButton() != nil { - fastSwitcherIndicator = FastAccountSwitcherIndicatorView() - fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(fastSwitcherIndicator) - } - #endif + setupFastAccountSwitcher() tabBar.isSpringLoaded = true view.backgroundColor = .appBackground } - // Fast account switcher is not supported on visionOS - #if !os(visionOS) - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - // i hate that we have to do this so often :S - // but doing it only in viewWillAppear makes it not appear initially - // doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed - repositionFastSwitcherIndicator() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - repositionFastSwitcherIndicator() - } - #endif - func select(tab: Tab, dismissPresented: Bool) { if tab == .compose { compose(editing: nil) @@ -119,53 +69,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { } } - override func show(_ vc: UIViewController, sender: Any?) { - if let nav = selectedViewController as? UINavigationController { - nav.pushViewController(vc, animated: true) - } else { - present(vc, animated: true) - } - } - - #if !os(visionOS) - private func repositionFastSwitcherIndicator() { - guard let myProfileButton = findMyProfileTabBarButton() else { - return - } - NSLayoutConstraint.deactivate(fastSwitcherConstraints) - let isPortrait = view.bounds.width < view.bounds.height - if traitCollection.horizontalSizeClass == .compact && isPortrait { - fastSwitcherConstraints = [ - fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor, constant: -4), - // tab bar button image width is 30 - fastSwitcherIndicator.leftAnchor.constraint(equalTo: myProfileButton.centerXAnchor, constant: 15 + 2), - ] - } else { - fastSwitcherConstraints = [ - fastSwitcherIndicator.centerYAnchor.constraint(equalTo: myProfileButton.centerYAnchor), - fastSwitcherIndicator.trailingAnchor.constraint(equalTo: myProfileButton.trailingAnchor), - ] - } - NSLayoutConstraint.activate(fastSwitcherConstraints) - } - #endif - - private func findMyProfileTabBarButton() -> UIView? { - let tabBarButtons = tabBar.subviews.filter { String(describing: type(of: $0)).lowercased().contains("button") } - // sanity check that there is 1 button per VC - guard tabBarButtons.count == viewControllers!.count, - let myProfileButton = tabBarButtons.last else { - return nil - } - return myProfileButton - } - - #if !os(visionOS) - @objc private func tabBarTapped(_ recognizer: UITapGestureRecognizer) { - fastAccountSwitcher.hide() - } - #endif - @objc func handleComposeKeyCommand() { compose(editing: nil) } @@ -179,22 +82,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate { } } - func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { - if viewController == composePlaceholder { - compose(editing: nil) - return false - } - if selectedIndex != NSNotFound, - viewController == viewControllers![selectedIndex], - let nav = viewController as? UINavigationController, - nav.viewControllers.count == 1, - let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController { - scrollableVC.tabBarScrollToTop() - return false - } - return true - } - func setViewController(_ viewController: UIViewController, forTab tab: Tab) { viewControllers![tab.rawValue] = viewController } @@ -229,7 +116,7 @@ extension MainTabBarViewController { } } - func getTabController(tab: Tab) -> UIViewController? { + private func getTabController(tab: Tab) -> UIViewController? { if tab == .compose { return nil } else { @@ -240,53 +127,21 @@ extension MainTabBarViewController { } } -#if !os(visionOS) -extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate { - func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { - fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(fastAccountSwitcher.view) - NSLayoutConstraint.activate([ - fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor), - - fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor), - fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor), - - // The safe area insets don't automatically propagate for some reason, so do it ourselves. - fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), - fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), - ]) - } - - func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { - guard let myProfileButton = findMyProfileTabBarButton() else { +extension MainTabBarViewController: UITabBarControllerDelegate { + func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { + if viewController == composePlaceholder { + compose(editing: nil) return false } - let locationInButton = myProfileButton.convert(point, from: tabBar) - return myProfileButton.bounds.contains(locationInButton) - } -} -#endif - -extension MainTabBarViewController: TuskerNavigationDelegate { - var apiController: MastodonController! { mastodonController } -} - -extension MainTabBarViewController: StateRestorableViewController { - func stateRestorationActivity() -> NSUserActivity? { - var activity: NSUserActivity? - if let presentedNav = presentedViewController as? UINavigationController, - let compose = presentedNav.viewControllers.first as? ComposeHostingController { - let draft = compose.controller.draft - activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID) - } else if let vc = (selectedViewController as! UINavigationController).topViewController as? StateRestorableViewController { - activity = vc.stateRestorationActivity() + if selectedIndex != NSNotFound, + viewController == viewControllers![selectedIndex], + let nav = viewController as? UINavigationController, + nav.viewControllers.count == 1, + let scrollableVC = nav.viewControllers.first as? TabBarScrollableViewController { + scrollableVC.tabBarScrollToTop() + return false } - if activity == nil { - stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController") - } - return activity + return true } } @@ -350,24 +205,6 @@ extension MainTabBarViewController: TuskerRootViewController { present(vc, animated: true, completion: completion) return vc } - - func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { - guard presentedViewController == nil else { - return .stop - } - guard let vc = viewController(for: currentTab) as? StatusBarTappableViewController else { - return .continue - } - return vc.handleStatusBarTapped(xPosition: xPosition) - } -} - -extension MainTabBarViewController: BackgroundableViewController { - func sceneDidEnterBackground() { - if let selectedVC = selectedViewController as? BackgroundableViewController { - selectedVC.sceneDidEnterBackground() - } - } } extension MainTabBarViewController: AccountSwitchableViewController { diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift new file mode 100644 index 00000000..6ddce3f3 --- /dev/null +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -0,0 +1,222 @@ +// +// NewMainTabBarViewController.swift +// Tusker +// +// Created by Shadowfacts on 8/19/24. +// Copyright © 2024 Shadowfacts. All rights reserved. +// + +import UIKit + +@available(iOS 18.0, *) +class NewMainTabBarViewController: BaseMainTabBarViewController { + + private let composePlaceholder = UIViewController() + + override func viewDidLoad() { + super.viewDidLoad() + + mode = .tabSidebar + delegate = self + tabBar.isSpringLoaded = true + view.backgroundColor = .appBackground + + let viewControllerProvider = { [unowned self] (tab: UITab) -> UIViewController in + self.makeViewController(for: tab) + } + + let topLevelTabs = [ + Tab.home, + .notifications, + .compose, + .explore, + .myProfile + ].map { + UITab(title: $0.title, image: UIImage(systemName: $0.imageName), identifier: $0.rawValue, viewControllerProvider: viewControllerProvider) + } + + self.tabs = topLevelTabs + + setupFastAccountSwitcher() + } + + private func makeViewController(for tab: UITab) -> UIViewController { + guard let tab = Tab(rawValue: tab.identifier) else { + fatalError("unreachable") + } + let root: UIViewController + switch tab { + case .home: + root = TimelinesPageViewController(mastodonController: mastodonController) + case .notifications: + root = NotificationsPageViewController(mastodonController: mastodonController) + case .compose: + return composePlaceholder + case .explore: + root = ExploreViewController(mastodonController: mastodonController) + case .myProfile: + root = MyProfileViewController(mastodonController: mastodonController) + } + return EnhancedNavigationViewController(rootViewController: root) + } + + @objc func handleComposeKeyCommand() { + compose(editing: nil) + } + +} + +@available(iOS 18.0, *) +extension NewMainTabBarViewController { + enum Tab: String, Hashable, CaseIterable { + case home + case notifications + case compose + case explore + case myProfile + + var title: String { + switch self { + case .home: + "Home" + case .notifications: + "Notifications" + case .compose: + "Compose" + case .explore: + "Explore" + case .myProfile: + "My Profile" + } + } + + var imageName: String { + switch self { + case .home: + "house" + case .notifications: + "bell" + case .compose: + "pencil" + case .explore: + "magnifyingglass" + case .myProfile: + "person" + } + } + } +} + +@available(iOS 18.0, *) +extension NewMainTabBarViewController: UITabBarControllerDelegate { + func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool { + if tab.identifier == Tab.compose.rawValue { + let currentTab = selectedTab + compose(editing: nil) { + // returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254) + // but we need it to change to _something_ so that we can change back to the current tab + self.selectedTab = tab + self.selectedTab = currentTab + } + return false + } else if let selectedTab, + selectedTab == tab, + let nav = selectedViewController as? any NavigationControllerProtocol, + nav.viewControllers.count == 1, + let scrollableVC = nav.viewControllers[0] as? TabBarScrollableViewController { + scrollableVC.tabBarScrollToTop() + return false + } else { + return true + } + } +} + +@available(iOS 18.0, *) +extension NewMainTabBarViewController: TuskerRootViewController { + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { + func doSelect() { + switch route { + case .timelines: + selectedTab = tab(forIdentifier: Tab.home.rawValue) + case .notifications: + selectedTab = tab(forIdentifier: Tab.notifications.rawValue) + case .myProfile: + selectedTab = tab(forIdentifier: Tab.myProfile.rawValue) + case .explore: + selectedTab = tab(forIdentifier: Tab.explore.rawValue) + case .bookmarks: + selectedTab = tab(forIdentifier: Tab.explore.rawValue) + let nav = getNavigationController() + nav.popToRootViewController(animated: animated) + nav.pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated) + case .list(let id): + selectedTab = tab(forIdentifier: Tab.explore.rawValue) + if let list = mastodonController.getCachedList(id: id) { + let nav = getNavigationController() + nav.popToRootViewController(animated: animated) + nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated) + } + } + completion?() + } + if presentedViewController != nil { + dismiss(animated: animated) { + doSelect() + } + } else { + doSelect() + } + } + + func getNavigationDelegate() -> (any TuskerNavigationDelegate)? { + return self + } + + func getNavigationController() -> any NavigationControllerProtocol { + return selectedViewController as! any NavigationControllerProtocol + } + + func performSearch(query: String) { + selectedTab = tab(forIdentifier: Tab.explore.rawValue) + guard let exploreNavController = selectedViewController as? any NavigationControllerProtocol, + let exploreController = exploreNavController.viewControllers.first as? ExploreViewController else { + return + } + + exploreNavController.popToRootViewController(animated: false) + + // setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time + if exploreController.isViewLoaded { + exploreController.searchController.isActive = true + } else { + exploreController.searchControllerStatusOnAppearance = true + // we still need to load the view so that we can setup the search query + exploreController.loadViewIfNeeded() + } + + exploreController.searchController.searchBar.text = query + exploreController.resultsController.performSearch(query: query) + } + + func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? { + let vc = PreferencesNavigationController(mastodonController: mastodonController) + present(vc, animated: true, completion: completion) + return vc + } +} + +@available(iOS 18.0, *) +extension NewMainTabBarViewController: AccountSwitchableViewController { + var isFastAccountSwitcherActive: Bool { + #if os(visionOS) + return false + #else + if let fastAccountSwitcher { + return !fastAccountSwitcher.view.isHidden + } else { + return false + } + #endif + } +} diff --git a/Tusker/Screens/Main/TuskerRootViewController.swift b/Tusker/Screens/Main/TuskerRootViewController.swift index 4b3fc83e..d0435430 100644 --- a/Tusker/Screens/Main/TuskerRootViewController.swift +++ b/Tusker/Screens/Main/TuskerRootViewController.swift @@ -11,9 +11,8 @@ import ComposeUI @MainActor protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController { - func compose(editing draft: Draft?, animated: Bool, isDucked: Bool) + func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) - func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? func getNavigationDelegate() -> TuskerNavigationDelegate? func getNavigationController() -> NavigationControllerProtocol func performSearch(query: String) diff --git a/Tusker/Shortcuts/AppShortcutItems.swift b/Tusker/Shortcuts/AppShortcutItems.swift index 9a3a62fb..af6b6883 100644 --- a/Tusker/Shortcuts/AppShortcutItems.swift +++ b/Tusker/Shortcuts/AppShortcutItems.swift @@ -48,7 +48,7 @@ enum AppShortcutItem: String, CaseIterable { case .showNotifications: root.select(route: .notifications, animated: false, completion: nil) case .composePost: - root.compose(editing: nil, animated: false, isDucked: false) + root.compose(editing: nil, animated: false, isDucked: false, completion: nil) } } } diff --git a/Tusker/Shortcuts/UserActivityHandlingContext.swift b/Tusker/Shortcuts/UserActivityHandlingContext.swift index 000a0793..312223c7 100644 --- a/Tusker/Shortcuts/UserActivityHandlingContext.swift +++ b/Tusker/Shortcuts/UserActivityHandlingContext.swift @@ -109,10 +109,10 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { func compose(editing draft: Draft) { if #available(iOS 16.0, *), UIDevice.current.userInterfaceIdiom == .phone { - self.root.compose(editing: draft, animated: false, isDucked: true) + self.root.compose(editing: draft, animated: false, isDucked: true, completion: nil) } else { DispatchQueue.main.async { - self.root.compose(editing: draft, animated: true, isDucked: false) + self.root.compose(editing: draft, animated: true, isDucked: false, completion: nil) } } state = .presented @@ -123,7 +123,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { #if !os(visionOS) if #available(iOS 16.0, *), let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) { - self.root.compose(editing: duckedDraft, animated: false, isDucked: true) + self.root.compose(editing: duckedDraft, animated: false, isDucked: true, completion: nil) } #endif } diff --git a/Tusker/TuskerNavigationDelegate.swift b/Tusker/TuskerNavigationDelegate.swift index 7837b1f7..ca1e462d 100644 --- a/Tusker/TuskerNavigationDelegate.swift +++ b/Tusker/TuskerNavigationDelegate.swift @@ -96,7 +96,7 @@ extension TuskerNavigationDelegate { show(ConversationViewController(for: statusID, state: state, mastodonController: apiController), sender: self) } - func compose(editing draft: Draft?, animated: Bool = true, isDucked: Bool = false) { + func compose(editing draft: Draft?, animated: Bool = true, isDucked: Bool = false, completion: (() -> Void)? = nil) { let draft = draft ?? apiController.createDraft() let visionIdiom = UIUserInterfaceIdiom(rawValue: 6) // .vision is not available pre-iOS 17 :S if [visionIdiom, .pad, .mac].contains(UIDevice.current.userInterfaceIdiom) { @@ -108,16 +108,17 @@ extension TuskerNavigationDelegate { options.preferredPresentationStyle = .prominent #endif UIApplication.shared.requestSceneSessionActivation(nil, userActivity: compose, options: options, errorHandler: nil) + completion?() } else { let compose = ComposeHostingController(draft: draft, mastodonController: apiController) #if os(visionOS) fatalError("unreachable") #else if #available(iOS 16.0, *), - presentDuckable(compose, animated: animated, isDucked: isDucked) { + presentDuckable(compose, animated: animated, isDucked: isDucked, completion: completion) { return } else { - present(compose, animated: animated) + present(compose, animated: animated, completion: completion) } #endif } From 9891b601a8dd18837763af281c3e405cf93cedf5 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 19 Aug 2024 19:10:31 -0400 Subject: [PATCH 06/56] Initial tab bar/sidebar implementation --- .../Main/BaseMainTabBarViewController.swift | 5 +- .../Main/NewMainTabBarViewController.swift | 53 ++++++++++++++++--- .../MultiColumnNavigationController.swift | 7 ++- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/Tusker/Screens/Main/BaseMainTabBarViewController.swift b/Tusker/Screens/Main/BaseMainTabBarViewController.swift index 59a94cf9..02ddc472 100644 --- a/Tusker/Screens/Main/BaseMainTabBarViewController.swift +++ b/Tusker/Screens/Main/BaseMainTabBarViewController.swift @@ -70,9 +70,12 @@ class BaseMainTabBarViewController: UITabBarController { } private func repositionFastSwitcherIndicator() { - guard let myProfileButton = findMyProfileTabBarButton() else { + guard let myProfileButton = findMyProfileTabBarButton(), + myProfileButton.window != nil else { + fastSwitcherIndicator?.isHidden = true return } + fastSwitcherIndicator?.isHidden = false NSLayoutConstraint.deactivate(fastSwitcherConstraints) let isPortrait = view.bounds.width < view.bounds.height if traitCollection.horizontalSizeClass == .compact && isPortrait { diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 6ddce3f3..d3311cd6 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -18,6 +18,7 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { mode = .tabSidebar delegate = self + sidebar.delegate = self tabBar.isSpringLoaded = true view.backgroundColor = .appBackground @@ -57,13 +58,33 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { case .myProfile: root = MyProfileViewController(mastodonController: mastodonController) } - return EnhancedNavigationViewController(rootViewController: root) + let nav: any NavigationControllerProtocol + if UIDevice.current.userInterfaceIdiom == .phone { + nav = EnhancedNavigationViewController() + } else { + // TODO: need to figure out how to update the navigation controller if the pref changes + switch Preferences.shared.widescreenNavigationMode { + case .stack: + nav = EnhancedNavigationViewController() + case .splitScreen: + nav = SplitNavigationController() + case .multiColumn: + nav = MultiColumnNavigationController() + } + } + nav.viewControllers = [root] + return nav } @objc func handleComposeKeyCommand() { compose(editing: nil) } + fileprivate func updateViewControllerSafeAreaInsets(_ vc: MultiColumnNavigationController) { + // When in sidebar mode, for multi column mode, don't leave an inset for the floating tab bar, because it leaves a massive gap. + // The floating tab bar seems to always be 88pt tall, regardless of, e.g., Dynamic Type size. + vc.additionalSafeAreaInsets = UIEdgeInsets(top: sidebar.isHidden ? 0 : -88, left: 0, bottom: 0, right: 0) + } } @available(iOS 18.0, *) @@ -112,12 +133,12 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool { if tab.identifier == Tab.compose.rawValue { let currentTab = selectedTab - compose(editing: nil) { - // returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254) - // but we need it to change to _something_ so that we can change back to the current tab - self.selectedTab = tab - self.selectedTab = currentTab - } + // returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254) + // but we need it to change to _something_ so that we can change back to the current tab + self.selectedTab = tab + self.selectedTab = currentTab + + compose(editing: nil) return false } else if let selectedTab, selectedTab == tab, @@ -130,6 +151,24 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { return true } } + + func tabBarController(_ tabBarController: UITabBarController, didSelectTab newTab: UITab, previousTab: UITab?) { + if let vc = newTab.viewController as? MultiColumnNavigationController { + self.updateViewControllerSafeAreaInsets(vc) + } + } +} + +@available(iOS 18.0, *) +extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { + func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) { + if let vc = selectedViewController as? MultiColumnNavigationController { + animator.addAnimations { + self.updateViewControllerSafeAreaInsets(vc) + vc.view.layoutIfNeeded() + } + } + } } @available(iOS 18.0, *) diff --git a/Tusker/Screens/Utilities/MultiColumnNavigationController.swift b/Tusker/Screens/Utilities/MultiColumnNavigationController.swift index e43bd7f7..7292a553 100644 --- a/Tusker/Screens/Utilities/MultiColumnNavigationController.swift +++ b/Tusker/Screens/Utilities/MultiColumnNavigationController.swift @@ -155,7 +155,7 @@ class MultiColumnNavigationController: UIViewController { if columnFrame.maxX < scrollView.bounds.width - scrollView.adjustedTrailingContentInset { offset = -scrollView.adjustedLeadingContentInset } else { - offset = columnFrame.minX + scrollView.adjustedLeadingContentInset - (scrollView.bounds.width - columnFrame.width) + offset = scrollView.contentSize.width - scrollView.bounds.width + scrollView.adjustedTrailingContentInset } scrollView.setContentOffset(CGPoint(x: offset, y: -scrollView.adjustedContentInset.top), animated: animated) } @@ -185,6 +185,11 @@ class MultiColumnNavigationController: UIViewController { } animator.startAnimation() } + + // blergh, overriding private method on UIViewController + @objc func _shouldOverlayTabBar() -> Bool { + false + } } extension MultiColumnNavigationController: NavigationControllerProtocol { From dffa5d8f7581c4d8bac10fd11bd026e840b86d3d Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 20 Aug 2024 11:55:19 -0400 Subject: [PATCH 07/56] Lists in new sidebar --- .../Main/NewMainTabBarViewController.swift | 143 +++++++++++++----- 1 file changed, 103 insertions(+), 40 deletions(-) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index d3311cd6..16058737 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -7,11 +7,19 @@ // import UIKit +import Combine +import Pachyderm @available(iOS 18.0, *) class NewMainTabBarViewController: BaseMainTabBarViewController { private let composePlaceholder = UIViewController() + + private var listsGroup: UITabGroup! + + private var cancellables = Set() + + private var navigationStacks = [String: [UIViewController]]() override func viewDidLoad() { super.viewDidLoad() @@ -26,19 +34,54 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { self.makeViewController(for: tab) } - let topLevelTabs = [ - Tab.home, - .notifications, - .compose, - .explore, - .myProfile - ].map { - UITab(title: $0.title, image: UIImage(systemName: $0.imageName), identifier: $0.rawValue, viewControllerProvider: viewControllerProvider) + let homeTab = UITab(title: "Home", image: UIImage(systemName: "house"), identifier: Tab.home.rawValue, viewControllerProvider: viewControllerProvider) + let notificationsTab = UITab(title: "Notifications", image: UIImage(systemName: "bell"), identifier: Tab.notifications.rawValue, viewControllerProvider: viewControllerProvider) + let composeTab = UITab(title: "Compose", image: UIImage(systemName: "pencil"), identifier: Tab.compose.rawValue, viewControllerProvider: viewControllerProvider) + let exploreTab = UITab(title: "Explore", image: UIImage(systemName: "magnifyingglass"), identifier: Tab.explore.rawValue, viewControllerProvider: viewControllerProvider) + let bookmarksTab = UITab(title: "Bookmarks", image: UIImage(systemName: "bookmark"), identifier: Tab.bookmarks.rawValue, viewControllerProvider: viewControllerProvider) + bookmarksTab.preferredPlacement = .optional + let favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider) + favoritesTab.preferredPlacement = .optional + let myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) + + listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in + // this closure is necessary to prevent UIKit from crashing (FB14860961) + return MultiColumnNavigationController() + } + listsGroup.preferredPlacement = .sidebarOnly + listsGroup.sidebarActions = [ + UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { _ in + fatalError("TODO") + }) + ] + reloadLists(mastodonController.lists) + + if UIDevice.current.userInterfaceIdiom == .phone { + self.tabs = [ + homeTab, + notificationsTab, + composeTab, + exploreTab, + myProfileTab, + ] + } else { + self.tabs = [ + homeTab, + notificationsTab, + exploreTab, + bookmarksTab, + favoritesTab, + myProfileTab, + composeTab, + listsGroup, + ] } - self.tabs = topLevelTabs - setupFastAccountSwitcher() + + mastodonController.$lists + .sink { [unowned self] in self.reloadLists($0) } + .store(in: &cancellables) } private func makeViewController(for tab: UITab) -> UIViewController { @@ -55,9 +98,19 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { return composePlaceholder case .explore: root = ExploreViewController(mastodonController: mastodonController) + case .bookmarks: + root = BookmarksViewController(mastodonController: mastodonController) + case .favorites: + root = FavoritesViewController(mastodonController: mastodonController) case .myProfile: root = MyProfileViewController(mastodonController: mastodonController) + case .lists: + fatalError("unreachable") } + return NewMainTabBarViewController.embedInNavigationController(root) + } + + private static func embedInNavigationController(_ vc: UIViewController) -> UIViewController { let nav: any NavigationControllerProtocol if UIDevice.current.userInterfaceIdiom == .phone { nav = EnhancedNavigationViewController() @@ -72,10 +125,18 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { nav = MultiColumnNavigationController() } } - nav.viewControllers = [root] + nav.viewControllers = [vc] return nav } + private func reloadLists(_ lists: [List]) { + listsGroup.children = lists.map { list in + UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in + NewMainTabBarViewController.embedInNavigationController(ListTimelineViewController(for: list, mastodonController: self.mastodonController)) + } + } + } + @objc func handleComposeKeyCommand() { compose(editing: nil) } @@ -94,37 +155,11 @@ extension NewMainTabBarViewController { case notifications case compose case explore + case bookmarks + case favorites case myProfile - var title: String { - switch self { - case .home: - "Home" - case .notifications: - "Notifications" - case .compose: - "Compose" - case .explore: - "Explore" - case .myProfile: - "My Profile" - } - } - - var imageName: String { - switch self { - case .home: - "house" - case .notifications: - "bell" - case .compose: - "pencil" - case .explore: - "magnifyingglass" - case .myProfile: - "person" - } - } + case lists } } @@ -156,6 +191,34 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { if let vc = newTab.viewController as? MultiColumnNavigationController { self.updateViewControllerSafeAreaInsets(vc) } + + // All tabs in a tab group deliberately share the same view controller, so we have to do this ourselves. + // I think this is pretty unfortunate API design--half the time, the tab bar controller takes care of + // this, but the rest of the time it's up to you. + // The managingNavigationController API would theoretically solve this, but split-screen/multi-column + // nav can't straightforwardly be implemented as UINavigationController subclasses. + // Unfortunately this, in turn, means that when switching between tabs in the same group, we don't + // 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.identifier == Tab.lists.rawValue, + let nav = group.viewController as? any NavigationControllerProtocol { + if let multiColumn = nav as? MultiColumnNavigationController { + updateViewControllerSafeAreaInsets(multiColumn) + } + + if let previousTab { + navigationStacks[previousTab.identifier] = nav.viewControllers + } + + if let existing = navigationStacks[newTab.identifier] { + nav.viewControllers = existing + } else if let newNav = newTab.viewController as? any NavigationControllerProtocol { + nav.viewControllers = newNav.viewControllers + } else { + fatalError("unreachable") + } + } } } From fda0c187949d0e7c8d792d616bb90f1bd90c3d10 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 20 Aug 2024 12:31:06 -0400 Subject: [PATCH 08/56] Fix insets with new sidebar --- Tusker.xcodeproj/project.pbxproj | 8 ++-- .../AccountFollowsListViewController.swift | 21 ++++++--- .../AccountListViewController.swift | 35 +++++++++------ ...ConversationCollectionViewController.swift | 22 ++++++---- .../Explore/ExploreViewController.swift | 10 ++++- .../FindInstanceViewController.swift | 0 .../SuggestedProfilesViewController.swift | 4 +- .../TrendingHashtagsViewController.swift | 10 ++++- .../Explore/TrendingLinksViewController.swift | 6 ++- .../TrendingStatusesViewController.swift | 28 +++++++----- .../Explore/TrendsViewController.swift | 12 ++++- ...LocalPredicateStatusesViewController.swift | 44 ++++++++++--------- .../Main/NewMainTabBarViewController.swift | 6 ++- ...otificationsCollectionViewController.swift | 4 +- .../ProfileStatusesViewController.swift | 22 ++++++---- .../Search/SearchResultsViewController.swift | 22 ++++++---- ...nAccountListCollectionViewController.swift | 21 ++++++--- .../StatusEditHistoryViewController.swift | 6 ++- .../Timeline/TimelineViewController.swift | 4 +- 19 files changed, 183 insertions(+), 102 deletions(-) rename Tusker/Screens/{ => Explore}/FindInstanceViewController.swift (100%) diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index f147eef4..25f20be3 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -131,6 +131,7 @@ D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; }; D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */; }; D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */; }; + D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */; }; D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; }; D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; }; D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; }; @@ -225,7 +226,6 @@ D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */; }; D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; }; D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; }; - D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */; }; D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */; }; D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */; }; D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; }; @@ -564,6 +564,7 @@ D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = ""; }; D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMainTabBarViewController.swift; sourceTree = ""; }; D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMainTabBarViewController.swift; sourceTree = ""; }; + D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInstanceViewController.swift; sourceTree = ""; }; D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = ""; }; D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = ""; }; @@ -661,7 +662,6 @@ D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = ""; }; D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = ""; }; D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = ""; }; - D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FindInstanceViewController.swift; path = Tusker/Screens/FindInstanceViewController.swift; sourceTree = SOURCE_ROOT; }; D6958F3C2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidescreenNavigationPrefsView.swift; sourceTree = ""; }; D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowSceneDelegate+Close.swift"; sourceTree = ""; }; D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = ""; }; @@ -981,7 +981,7 @@ D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */, D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */, D6093F9A25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift */, - D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */, + D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */, D6114E0C27F7FEB30080E273 /* TrendingStatusesViewController.swift */, D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */, D6114E1027F899B30080E273 /* TrendingLinksViewController.swift */, @@ -2241,6 +2241,7 @@ D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, D6A6C11B25B63CEE00298D0F /* MemoryCache.swift in Sources */, + D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */, D6114E0D27F7FEB30080E273 /* TrendingStatusesViewController.swift in Sources */, D6BC9DDA232D8BE5002CA326 /* SearchResultsViewController.swift in Sources */, D698F4672BD079800054DB14 /* AnnouncementsHostingController.swift in Sources */, @@ -2280,7 +2281,6 @@ D63CC7102911F1E4000E19DE /* UIScrollView+Top.swift in Sources */, D6F6A557291F4F1600F496A8 /* MuteAccountView.swift in Sources */, D65B4B8B297879E900DABDFB /* AccountFollowsViewController.swift in Sources */, - D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */, D65B4B6429771EFF00DABDFB /* ConversationViewController.swift in Sources */, D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */, D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */, diff --git a/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift b/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift index 32d95c26..07cd7670 100644 --- a/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift +++ b/Tusker/Screens/Account Follows/AccountFollowsListViewController.swift @@ -17,9 +17,7 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll let mastodonController: MastodonController let mode: AccountFollowsViewController.Mode - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! private var state: State = .unloaded @@ -40,7 +38,11 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .appGroupedBackground + var config = UICollectionLayoutListConfiguration(appearance: .plain) config.backgroundColor = .appBackground config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in @@ -65,10 +67,19 @@ class AccountFollowsListViewController: UIViewController, CollectionViewControll section.readableContentInset(in: environment) return section } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() } diff --git a/Tusker/Screens/Account List/AccountListViewController.swift b/Tusker/Screens/Account List/AccountListViewController.swift index c598abac..f0b533db 100644 --- a/Tusker/Screens/Account List/AccountListViewController.swift +++ b/Tusker/Screens/Account List/AccountListViewController.swift @@ -14,9 +14,7 @@ class AccountListViewController: UIViewController, CollectionViewController { private let mastodonController: MastodonController private let accountIDs: [String] - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! init(accountIDs: [String], mastodonController: MastodonController) { @@ -30,7 +28,11 @@ class AccountListViewController: UIViewController, CollectionViewController { fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .appGroupedBackground + var config = UICollectionLayoutListConfiguration(appearance: .grouped) config.backgroundColor = .appGroupedBackground config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets @@ -40,11 +42,25 @@ class AccountListViewController: UIViewController, CollectionViewController { section.readableContentInset(in: environment) return section } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.accounts]) + snapshot.appendItems(accountIDs) + dataSource.apply(snapshot, animatingDifferences: false) } private func createDataSource() -> UICollectionViewDiffableDataSource { @@ -56,16 +72,7 @@ class AccountListViewController: UIViewController, CollectionViewController { return collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: itemIdentifier) } } - - override func viewDidLoad() { - super.viewDidLoad() - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.accounts]) - snapshot.appendItems(accountIDs) - dataSource.apply(snapshot, animatingDifferences: false) - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) diff --git a/Tusker/Screens/Conversation/ConversationCollectionViewController.swift b/Tusker/Screens/Conversation/ConversationCollectionViewController.swift index 8f16309e..b3559e30 100644 --- a/Tusker/Screens/Conversation/ConversationCollectionViewController.swift +++ b/Tusker/Screens/Conversation/ConversationCollectionViewController.swift @@ -19,9 +19,7 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont var statusIDToScrollToOnLoad: String var showStatusesAutomatically = false - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! init(for mainStatusID: String, state: CollapseState, conversationViewController: ConversationViewController) { @@ -38,7 +36,9 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + var config = UICollectionLayoutListConfiguration(appearance: .plain) config.backgroundColor = .appSecondaryBackground config.leadingSwipeActionsConfigurationProvider = { [unowned self] in @@ -66,13 +66,19 @@ class ConversationCollectionViewController: UIViewController, CollectionViewCont return section } viewRespectsSystemMinimumLayoutMargins = false - view = UICollectionView(frame: .zero, collectionViewLayout: layout) - // something about the autoresizing mask breaks resizing the vc - view.translatesAutoresizingMaskIntoConstraints = false + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true - + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + #if !targetEnvironment(macCatalyst) collectionView.refreshControl = UIRefreshControl() collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index 75c58f65..b9ee9a09 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -48,12 +48,18 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect configuration.headerMode = .supplementary let layout = UICollectionViewCompositionalLayout.list(using: configuration) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) - collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) - + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() applyInitialSnapshot() diff --git a/Tusker/Screens/FindInstanceViewController.swift b/Tusker/Screens/Explore/FindInstanceViewController.swift similarity index 100% rename from Tusker/Screens/FindInstanceViewController.swift rename to Tusker/Screens/Explore/FindInstanceViewController.swift diff --git a/Tusker/Screens/Explore/SuggestedProfilesViewController.swift b/Tusker/Screens/Explore/SuggestedProfilesViewController.swift index b9c07e77..b1da2f89 100644 --- a/Tusker/Screens/Explore/SuggestedProfilesViewController.swift +++ b/Tusker/Screens/Explore/SuggestedProfilesViewController.swift @@ -46,8 +46,8 @@ class SuggestedProfilesViewController: UIViewController, CollectionViewControlle collectionView.allowsFocus = true view.addSubview(collectionView) NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) diff --git a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift index 6e5f8c14..28691fa4 100644 --- a/Tusker/Screens/Explore/TrendingHashtagsViewController.swift +++ b/Tusker/Screens/Explore/TrendingHashtagsViewController.swift @@ -53,12 +53,18 @@ class TrendingHashtagsViewController: UIViewController, CollectionViewController } let layout = UICollectionViewCompositionalLayout.list(using: config) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) - collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) - + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + let loadingCell = UICollectionView.CellRegistration { cell, indexPath, itemIdentifier in cell.indicator.startAnimating() } diff --git a/Tusker/Screens/Explore/TrendingLinksViewController.swift b/Tusker/Screens/Explore/TrendingLinksViewController.swift index 3859fad2..af73cbdf 100644 --- a/Tusker/Screens/Explore/TrendingLinksViewController.swift +++ b/Tusker/Screens/Explore/TrendingLinksViewController.swift @@ -40,6 +40,8 @@ class TrendingLinksViewController: UIViewController, CollectionViewController { title = NSLocalizedString("Trending Links", comment: "trending links screen title") + view.backgroundColor = .appGroupedBackground + let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in switch dataSource.sectionIdentifier(for: sectionIndex) { case nil: @@ -80,8 +82,8 @@ class TrendingLinksViewController: UIViewController, CollectionViewController { collectionView.allowsFocus = true view.addSubview(collectionView) NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) diff --git a/Tusker/Screens/Explore/TrendingStatusesViewController.swift b/Tusker/Screens/Explore/TrendingStatusesViewController.swift index 63d23a48..db6e5cac 100644 --- a/Tusker/Screens/Explore/TrendingStatusesViewController.swift +++ b/Tusker/Screens/Explore/TrendingStatusesViewController.swift @@ -14,9 +14,7 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController private let mastodonController: MastodonController let filterer: Filterer - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! private var loaded = false @@ -34,7 +32,9 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + var config = UICollectionLayoutListConfiguration(appearance: .plain) config.leadingSwipeActionsConfigurationProvider = { [unowned self] in (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions() @@ -62,12 +62,22 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController section.readableContentInset(in: environment) return section } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true - + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() + + NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController) } private func createDataSource() -> UICollectionViewDiffableDataSource { @@ -96,12 +106,6 @@ class TrendingStatusesViewController: UIViewController, CollectionViewController } } - override func viewDidLoad() { - super.viewDidLoad() - - NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController) - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) diff --git a/Tusker/Screens/Explore/TrendsViewController.swift b/Tusker/Screens/Explore/TrendsViewController.swift index 52c7786d..4b09e2e7 100644 --- a/Tusker/Screens/Explore/TrendsViewController.swift +++ b/Tusker/Screens/Explore/TrendsViewController.swift @@ -44,6 +44,8 @@ class TrendsViewController: UIViewController, CollectionViewController { override func viewDidLoad() { super.viewDidLoad() + + view.backgroundColor = .appGroupedBackground let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[sectionIndex] @@ -114,13 +116,19 @@ class TrendsViewController: UIViewController, CollectionViewController { } } collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout) - collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.delegate = self collectionView.dragDelegate = self collectionView.backgroundColor = .appGroupedBackground collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) - + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) diff --git a/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift b/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift index a743da7e..1d5081b2 100644 --- a/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift +++ b/Tusker/Screens/Local Predicate Statuses List/LocalPredicateStatusesViewController.swift @@ -19,9 +19,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont private let predicateTitle: String private let request: (RequestRange) -> Request<[TryDecode]> - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! private var state = State.unloaded @@ -43,7 +41,9 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + var config = UICollectionLayoutListConfiguration(appearance: .plain) config.backgroundColor = .appBackground config.leadingSwipeActionsConfigurationProvider = { [unowned self] in @@ -71,12 +71,30 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont section.readableContentInset(in: environment) return section } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true - + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() + + #if !targetEnvironment(macCatalyst) + collectionView.refreshControl = UIRefreshControl() + collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) + #endif + + addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh \(predicateTitle)")) + + NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController) + NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext) } private func createDataSource() -> UICollectionViewDiffableDataSource { @@ -97,20 +115,6 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont } } - override func viewDidLoad() { - super.viewDidLoad() - - #if !targetEnvironment(macCatalyst) - collectionView.refreshControl = UIRefreshControl() - collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) - #endif - - addKeyCommand(MenuController.refreshCommand(discoverabilityTitle: "Refresh \(predicateTitle)")) - - NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController) - NotificationCenter.default.addObserver(self, selector: #selector(managedObjectsDidChange), name: .NSManagedObjectContextObjectsDidChange, object: mastodonController.persistentContainer.viewContext) - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 16058737..76853fc5 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -97,7 +97,11 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { case .compose: return composePlaceholder case .explore: - root = ExploreViewController(mastodonController: mastodonController) + if UIDevice.current.userInterfaceIdiom == .phone { + root = ExploreViewController(mastodonController: mastodonController) + } else { + root = InlineTrendsViewController(mastodonController: mastodonController) + } case .bookmarks: root = BookmarksViewController(mastodonController: mastodonController) case .favorites: diff --git a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift index 561c9534..eb042b72 100644 --- a/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsCollectionViewController.swift @@ -108,8 +108,8 @@ class NotificationsCollectionViewController: UIViewController, TimelineLikeColle collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) diff --git a/Tusker/Screens/Profile/ProfileStatusesViewController.swift b/Tusker/Screens/Profile/ProfileStatusesViewController.swift index d1f6d3c9..802b9da3 100644 --- a/Tusker/Screens/Profile/ProfileStatusesViewController.swift +++ b/Tusker/Screens/Profile/ProfileStatusesViewController.swift @@ -26,9 +26,7 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie private var older: RequestRange? private var cancellables = Set() - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private(set) var dataSource: UICollectionViewDiffableDataSource! var headerCell: ProfileHeaderCollectionViewCell? { collectionView.cellForItem(at: IndexPath(item: 0, section: 0)) as? ProfileHeaderCollectionViewCell @@ -56,7 +54,9 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + var config = UICollectionLayoutListConfiguration(appearance: .plain) config.backgroundColor = .appBackground config.leadingSwipeActionsConfigurationProvider = { [unowned self] in @@ -103,10 +103,18 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie return section } } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) registerTimelineLikeCells() dataSource = createDataSource() @@ -115,10 +123,6 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie collectionView.refreshControl = UIRefreshControl() collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged) #endif - } - - override func viewDidLoad() { - super.viewDidLoad() mastodonController.persistentContainer.accountSubject .receive(on: DispatchQueue.main) diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index 9d22138a..e7394b97 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -36,7 +36,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController { weak var delegate: SearchResultsViewControllerDelegate? var tokenHandler: ((String, SearchOperatorType) -> Void)? - var collectionView: UICollectionView! { view as? UICollectionView } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! /// Types of results to search for. @@ -62,7 +62,9 @@ class SearchResultsViewController: UIViewController, CollectionViewController { fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in let sectionIdentifier = self.dataSource.sectionIdentifier(for: sectionIndex)! switch sectionIdentifier { @@ -102,7 +104,7 @@ class SearchResultsViewController: UIViewController, CollectionViewController { return .list(using: config, layoutEnvironment: environment) } } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true @@ -110,12 +112,16 @@ class SearchResultsViewController: UIViewController, CollectionViewController { #if !os(visionOS) collectionView.keyboardDismissMode = .interactive #endif - + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() - } - - override func viewDidLoad() { - super.viewDidLoad() searchCancellable = searchSubject .debounce(for: .seconds(1), scheduler: RunLoop.main) diff --git a/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift b/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift index 6fe62d02..da96e394 100644 --- a/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift +++ b/Tusker/Screens/Status Action Account List/StatusActionAccountListCollectionViewController.swift @@ -19,9 +19,7 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect private var needsInaccurateCountWarning = false - var collectionView: UICollectionView! { - view as? UICollectionView - } + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! private var state: State = .unloaded @@ -45,7 +43,11 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect fatalError("init(coder:) has not been implemented") } - override func loadView() { + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .appGroupedBackground + var accountsConfig = UICollectionLayoutListConfiguration(appearance: .grouped) accountsConfig.backgroundColor = .appGroupedBackground accountsConfig.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in @@ -85,10 +87,19 @@ class StatusActionAccountListCollectionViewController: UIViewController, Collect section.readableContentInset(in: environment) return section } - view = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.delegate = self collectionView.dragDelegate = self collectionView.allowsFocus = true + collectionView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionView) + NSLayoutConstraint.activate([ + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + collectionView.topAnchor.constraint(equalTo: view.topAnchor), + collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + dataSource = createDataSource() } diff --git a/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift b/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift index 2d2f0389..0ff810e4 100644 --- a/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift +++ b/Tusker/Screens/Status Edit History/StatusEditHistoryViewController.swift @@ -35,6 +35,8 @@ class StatusEditHistoryViewController: UIViewController, CollectionViewControlle override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .appGroupedBackground + var config = UICollectionLayoutListConfiguration(appearance: .grouped) config.backgroundColor = .appGroupedBackground config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in @@ -62,8 +64,8 @@ class StatusEditHistoryViewController: UIViewController, CollectionViewControlle collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index 19781956..df2f0893 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -123,8 +123,8 @@ class TimelineViewController: UIViewController, TimelineLikeCollectionViewContro collectionView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(collectionView) NSLayoutConstraint.activate([ - collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) From 3d9a1086b6cea4c83794d28be628285a0a5bc450 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 20 Aug 2024 12:31:29 -0400 Subject: [PATCH 09/56] Remove dead code --- Tusker.xcodeproj/project.pbxproj | 8 - .../FeaturedProfileCollectionViewCell.swift | 152 ------------------ .../FeaturedProfileCollectionViewCell.xib | 113 ------------- 3 files changed, 273 deletions(-) delete mode 100644 Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift delete mode 100644 Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.xib diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 25f20be3..26137216 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -218,8 +218,6 @@ D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; }; D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */; }; D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; }; - D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; }; - D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */; }; D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; }; D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; }; D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C2E23AC47C3005C403C /* SavedDataManager.swift */; }; @@ -654,8 +652,6 @@ D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = ""; }; D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = ""; }; D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = ""; }; - D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = ""; }; - D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeaturedProfileCollectionViewCell.xib; sourceTree = ""; }; D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = ""; }; D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = ""; }; D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = ""; }; @@ -991,8 +987,6 @@ D601FA81297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift */, D601FA82297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib */, D6BC74852AFC4772000DD603 /* SuggestedProfileCardView.swift */, - D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */, - D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */, D6C3F4FA299035650009FCFF /* TrendsViewController.swift */, D68329EE299540050026EB24 /* MoreTrendsFooterCollectionViewCell.swift */, D6DD996A2998611A0015C962 /* SuggestedProfilesViewController.swift */, @@ -2020,7 +2014,6 @@ buildActionMask = 2147483647; files = ( D6E77D0D286E6B7300D8B732 /* TrendingLinkCardCollectionViewCell.xib in Resources */, - D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */, D61AC1D9232EA42D00C54D2D /* InstanceTableViewCell.xib in Resources */, D691296E2BA75ADF005C58ED /* PrivacyInfo.xcprivacy in Resources */, D6412B0B24B0D4C600F5412E /* ProfileHeaderView.xib in Resources */, @@ -2261,7 +2254,6 @@ D61DC84D28F500D200B82C6E /* ProfileViewController.swift in Sources */, D6187BED2BFA840B00B3A281 /* FollowRequestNotificationViewController.swift in Sources */, D600891F29848DE2005B4D00 /* AddInstancePinnedTimelineView.swift in Sources */, - D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */, D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */, D64B96812BC3279D002C8990 /* PrefsAccountView.swift in Sources */, D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */, diff --git a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift deleted file mode 100644 index b253e068..00000000 --- a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.swift +++ /dev/null @@ -1,152 +0,0 @@ -// -// FeaturedProfileCollectionViewCell.swift -// Tusker -// -// Created by Shadowfacts on 2/6/21. -// Copyright © 2021 Shadowfacts. All rights reserved. -// - -import UIKit -import Pachyderm - -class FeaturedProfileCollectionViewCell: UICollectionViewCell { - - @IBOutlet weak var clippingView: UIView! - @IBOutlet weak var headerImageView: UIImageView! - @IBOutlet weak var avatarContainerView: UIView! - @IBOutlet weak var avatarImageView: UIImageView! - @IBOutlet weak var displayNameLabel: AccountDisplayNameLabel! - @IBOutlet weak var noteTextView: StatusContentTextView! - - var account: Account? - - private var accountImagesTask: Task? - - deinit { - accountImagesTask?.cancel() - } - - override func awakeFromNib() { - super.awakeFromNib() - - avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) - avatarContainerView.layer.cornerCurve = .continuous - avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) - avatarImageView.layer.cornerCurve = .continuous - - displayNameLabel.font = UIFontMetrics(forTextStyle: .title1).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) - displayNameLabel.adjustsFontForContentSizeCategory = true - - noteTextView.adjustsFontForContentSizeCategory = true - noteTextView.textContainer.lineBreakMode = .byTruncatingTail - noteTextView.textContainerInset = UIEdgeInsets(top: 16, left: 4, bottom: 16, right: 4) - - backgroundColor = .clear - clippingView.backgroundColor = .appBackground - clippingView.layer.cornerRadius = 5 - clippingView.layer.cornerCurve = .continuous - clippingView.layer.borderWidth = 1 - clippingView.layer.masksToBounds = true - layer.shadowOpacity = 0.2 - layer.shadowRadius = 8 - layer.shadowOffset = .zero - layer.masksToBounds = false - updateLayerColors() - - NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) - } - - func updateUI(account: Account) { - self.account = account - - displayNameLabel.updateForAccountDisplayName(account: account) - - noteTextView.setBodyTextFromHTML(account.note) - noteTextView.setEmojis(account.emojis, identifier: account.id) - - avatarImageView.image = nil - headerImageView.image = nil - - accountImagesTask?.cancel() - accountImagesTask = Task { - await updateImages(account: account) - } - } - - private nonisolated func updateImages(account: Account) async { - await withTaskGroup(of: Void.self) { group in - group.addTask { - guard let avatar = account.avatar, - let image = await ImageCache.avatars.get(avatar).1 else { - return - } - await MainActor.run { - self.avatarImageView.image = image - } - } - group.addTask { - guard let header = account.header, - let image = await ImageCache.headers.get(header).1 else { - return - } - await MainActor.run { - self.headerImageView.image = image - } - } - await group.waitForAll() - } - } - - private func updateLayerColors() { - if traitCollection.userInterfaceStyle == .dark { - clippingView.layer.borderColor = UIColor.darkGray.withAlphaComponent(0.5).cgColor - layer.shadowColor = UIColor.darkGray.cgColor - } else { - clippingView.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor - layer.shadowColor = UIColor.black.cgColor - } - } - - // Unneeded on visionOS because there is no light/dark mode - #if !os(visionOS) - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - updateLayerColors() - } - #endif - - override func layoutSubviews() { - super.layoutSubviews() - - layer.shadowPath = CGPath(roundedRect: bounds, cornerWidth: 5, cornerHeight: 5, transform: nil) - } - - @objc private func preferencesChanged() { - avatarContainerView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarContainerView) - avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadius(for: avatarImageView) - - if let account = account { - displayNameLabel.updateForAccountDisplayName(account: account) - } - } - - // MARK: Accessibility - - override var isAccessibilityElement: Bool { - get { true } - set {} - } - - override var accessibilityAttributedLabel: NSAttributedString? { - get { - guard let account else { - return nil - } - let s = NSMutableAttributedString(string: "\(account.displayNameWithoutCustomEmoji), ") - s.append(noteTextView.attributedText) - return s - } - set {} - } - -} diff --git a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.xib b/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.xib deleted file mode 100644 index 0c1d02e4..00000000 --- a/Tusker/Screens/Explore/FeaturedProfileCollectionViewCell.xib +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 67e9c1245e4777201e0dfc887449befa5f72c37a Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 20 Aug 2024 22:39:40 -0400 Subject: [PATCH 10/56] Size class switching fixes for new tab/side bar --- Tusker.xcodeproj/project.pbxproj | 4 + .../Main/BaseMainTabBarViewController.swift | 7 +- .../Main/NewMainTabBarViewController.swift | 161 +++++++++++++----- .../AdaptableNavigationController.swift | 136 +++++++++++++++ 4 files changed, 258 insertions(+), 50 deletions(-) create mode 100644 Tusker/Screens/Utilities/AdaptableNavigationController.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 26137216..c4656be1 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -132,6 +132,7 @@ D64A50462C739DC0009D7193 /* NewMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */; }; D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */; }; D64A50BC2C74F8F4009D7193 /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */; }; + D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */; }; D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; }; D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; }; D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; }; @@ -563,6 +564,7 @@ D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMainTabBarViewController.swift; sourceTree = ""; }; D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMainTabBarViewController.swift; sourceTree = ""; }; D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInstanceViewController.swift; sourceTree = ""; }; + D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptableNavigationController.swift; sourceTree = ""; }; D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = ""; }; D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = ""; }; @@ -1556,6 +1558,7 @@ D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */, D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */, D61F759129365C6C00C0B37F /* CollectionViewController.swift */, + D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */, ); path = Utilities; sourceTree = ""; @@ -2341,6 +2344,7 @@ D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, + D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */, D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */, D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */, D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */, diff --git a/Tusker/Screens/Main/BaseMainTabBarViewController.swift b/Tusker/Screens/Main/BaseMainTabBarViewController.swift index 02ddc472..1885ff0a 100644 --- a/Tusker/Screens/Main/BaseMainTabBarViewController.swift +++ b/Tusker/Screens/Main/BaseMainTabBarViewController.swift @@ -71,11 +71,12 @@ class BaseMainTabBarViewController: UITabBarController { private func repositionFastSwitcherIndicator() { guard let myProfileButton = findMyProfileTabBarButton(), - myProfileButton.window != nil else { + myProfileButton.window != nil, + let fastSwitcherIndicator else { fastSwitcherIndicator?.isHidden = true return } - fastSwitcherIndicator?.isHidden = false + fastSwitcherIndicator.isHidden = false NSLayoutConstraint.deactivate(fastSwitcherConstraints) let isPortrait = view.bounds.width < view.bounds.height if traitCollection.horizontalSizeClass == .compact && isPortrait { @@ -156,7 +157,7 @@ extension BaseMainTabBarViewController: StateRestorableViewController { let compose = presentedNav.viewControllers.first as? ComposeHostingController { let draft = compose.controller.draft activity = UserActivityManager.editDraftActivity(id: draft.id, accountID: draft.accountID) - } else if let vc = (selectedViewController as! UINavigationController).topViewController as? StateRestorableViewController { + } else if let vc = (selectedViewController as? any NavigationControllerProtocol)?.topViewController as? StateRestorableViewController { activity = vc.stateRestorationActivity() } if activity == nil { diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 76853fc5..7ff101d6 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -15,11 +15,19 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { private let composePlaceholder = UIViewController() + private var homeTab: UITab! + private var notificationsTab: UITab! + private var composeTab: UITab! + private var exploreTab: UITab! + private var bookmarksTab: UITab! + private var favoritesTab: UITab! + private var myProfileTab: UITab! private var listsGroup: UITabGroup! private var cancellables = Set() private var navigationStacks = [String: [UIViewController]]() + private var isCompact: Bool? override func viewDidLoad() { super.viewDidLoad() @@ -34,19 +42,19 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { self.makeViewController(for: tab) } - let homeTab = UITab(title: "Home", image: UIImage(systemName: "house"), identifier: Tab.home.rawValue, viewControllerProvider: viewControllerProvider) - let notificationsTab = UITab(title: "Notifications", image: UIImage(systemName: "bell"), identifier: Tab.notifications.rawValue, viewControllerProvider: viewControllerProvider) - let composeTab = UITab(title: "Compose", image: UIImage(systemName: "pencil"), identifier: Tab.compose.rawValue, viewControllerProvider: viewControllerProvider) - let exploreTab = UITab(title: "Explore", image: UIImage(systemName: "magnifyingglass"), identifier: Tab.explore.rawValue, viewControllerProvider: viewControllerProvider) - let bookmarksTab = UITab(title: "Bookmarks", image: UIImage(systemName: "bookmark"), identifier: Tab.bookmarks.rawValue, viewControllerProvider: viewControllerProvider) + homeTab = UITab(title: "Home", image: UIImage(systemName: "house"), identifier: Tab.home.rawValue, viewControllerProvider: viewControllerProvider) + notificationsTab = UITab(title: "Notifications", image: UIImage(systemName: "bell"), identifier: Tab.notifications.rawValue, viewControllerProvider: viewControllerProvider) + composeTab = UITab(title: "Compose", image: UIImage(systemName: "pencil"), identifier: Tab.compose.rawValue, viewControllerProvider: viewControllerProvider) + exploreTab = UITab(title: "Explore", image: UIImage(systemName: "magnifyingglass"), identifier: Tab.explore.rawValue, viewControllerProvider: viewControllerProvider) + bookmarksTab = UITab(title: "Bookmarks", image: UIImage(systemName: "bookmark"), identifier: Tab.bookmarks.rawValue, viewControllerProvider: viewControllerProvider) bookmarksTab.preferredPlacement = .optional - let favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider) + favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider) favoritesTab.preferredPlacement = .optional - let myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) + myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in // this closure is necessary to prevent UIKit from crashing (FB14860961) - return MultiColumnNavigationController() + return AdaptableNavigationController() } listsGroup.preferredPlacement = .sidebarOnly listsGroup.sidebarActions = [ @@ -65,6 +73,72 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { myProfileTab, ] } else { + self.updatePadTabs() + registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: NewMainTabBarViewController, previousTraitCollection) in + self.updatePadTabs() + + let vcToUpdate = self.selectedTab!.parent?.viewController ?? self.selectedTab!.viewController! + self.updateViewControllerSafeAreaInsets(vcToUpdate) + } + } + + setupFastAccountSwitcher() + + mastodonController.$lists + .sink { [unowned self] in self.reloadLists($0) } + .store(in: &cancellables) + } + + private func updatePadTabs() { + let wasCompact = isCompact + + if self.traitCollection.horizontalSizeClass == .compact { + isCompact = true + + var exploreNavStack: [UIViewController]? = nil + if selectedTab?.parent == listsGroup { + let nav = listsGroup.viewController as! any NavigationControllerProtocol + exploreNavStack = nav.viewControllers + nav.viewControllers = [] + } + + self.tabs = [ + homeTab, + notificationsTab, + composeTab, + exploreTab, + myProfileTab, + ] + + if let exploreNavStack { + selectedTab = exploreTab + let nav = exploreTab.viewController as! any NavigationControllerProtocol + nav.viewControllers = exploreNavStack + } + } else { + isCompact = false + + var newTab: (UITab, [UIViewController])? = nil + if wasCompact == true, + selectedTab == exploreTab { + let nav = exploreTab.viewController as! any NavigationControllerProtocol + // skip over the ExploreViewController + if nav.viewControllers.count > 1 { + switch nav.viewControllers[1] { + case let listVC as ListTimelineViewController: + if let tab = listsGroup.tab(forIdentifier: "list:\(listVC.list.id)") { + newTab = (tab, Array(nav.viewControllers[1...])) + nav.viewControllers = [ + nav.viewControllers[0], // leave the ExploreVC in place + InlineTrendsViewController(mastodonController: mastodonController), // re-insert an InlineTrendsVC + ] + } + default: + break + } + } + } + self.tabs = [ homeTab, notificationsTab, @@ -75,13 +149,17 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { composeTab, listsGroup, ] + + if let (tab, navStack) = newTab { + let nav = tab.parent!.viewController as! any NavigationControllerProtocol + nav.viewControllers = navStack + // Setting the tab now seems to be clobbered by the UITabBarController itself updating in response + // to the size class change. So wait until it finishes to do so. + DispatchQueue.main.async { + self.selectedTab = tab + } + } } - - setupFastAccountSwitcher() - - mastodonController.$lists - .sink { [unowned self] in self.reloadLists($0) } - .store(in: &cancellables) } private func makeViewController(for tab: UITab) -> UIViewController { @@ -100,7 +178,11 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { if UIDevice.current.userInterfaceIdiom == .phone { root = ExploreViewController(mastodonController: mastodonController) } else { - root = InlineTrendsViewController(mastodonController: mastodonController) + let nav = AdaptableNavigationController(viewControllersToPrependInCompact: [ + ExploreViewController(mastodonController: mastodonController) + ]) + nav.viewControllers = [InlineTrendsViewController(mastodonController: mastodonController)] + return nav } case .bookmarks: root = BookmarksViewController(mastodonController: mastodonController) @@ -111,24 +193,11 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { case .lists: fatalError("unreachable") } - return NewMainTabBarViewController.embedInNavigationController(root) + return embedInNavigationController(root) } - private static func embedInNavigationController(_ vc: UIViewController) -> UIViewController { - let nav: any NavigationControllerProtocol - if UIDevice.current.userInterfaceIdiom == .phone { - nav = EnhancedNavigationViewController() - } else { - // TODO: need to figure out how to update the navigation controller if the pref changes - switch Preferences.shared.widescreenNavigationMode { - case .stack: - nav = EnhancedNavigationViewController() - case .splitScreen: - nav = SplitNavigationController() - case .multiColumn: - nav = MultiColumnNavigationController() - } - } + private func embedInNavigationController(_ vc: UIViewController) -> UIViewController { + let nav = AdaptableNavigationController() nav.viewControllers = [vc] return nav } @@ -136,7 +205,7 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { private func reloadLists(_ lists: [List]) { listsGroup.children = lists.map { list in UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in - NewMainTabBarViewController.embedInNavigationController(ListTimelineViewController(for: list, mastodonController: self.mastodonController)) + return ListTimelineViewController(for: list, mastodonController: self.mastodonController) } } } @@ -145,7 +214,10 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { compose(editing: nil) } - fileprivate func updateViewControllerSafeAreaInsets(_ vc: MultiColumnNavigationController) { + fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) { + guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else { + return + } // When in sidebar mode, for multi column mode, don't leave an inset for the floating tab bar, because it leaves a massive gap. // The floating tab bar seems to always be 88pt tall, regardless of, e.g., Dynamic Type size. vc.additionalSafeAreaInsets = UIEdgeInsets(top: sidebar.isHidden ? 0 : -88, left: 0, bottom: 0, right: 0) @@ -192,10 +264,8 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { } func tabBarController(_ tabBarController: UITabBarController, didSelectTab newTab: UITab, previousTab: UITab?) { - if let vc = newTab.viewController as? MultiColumnNavigationController { - self.updateViewControllerSafeAreaInsets(vc) - } - + self.updateViewControllerSafeAreaInsets(newTab.viewController!) + // All tabs in a tab group deliberately share the same view controller, so we have to do this ourselves. // I think this is pretty unfortunate API design--half the time, the tab bar controller takes care of // this, but the rest of the time it's up to you. @@ -207,9 +277,7 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { if let group = newTab.parent, group.identifier == Tab.lists.rawValue, let nav = group.viewController as? any NavigationControllerProtocol { - if let multiColumn = nav as? MultiColumnNavigationController { - updateViewControllerSafeAreaInsets(multiColumn) - } + updateViewControllerSafeAreaInsets(nav) if let previousTab { navigationStacks[previousTab.identifier] = nav.viewControllers @@ -217,8 +285,8 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { if let existing = navigationStacks[newTab.identifier] { nav.viewControllers = existing - } else if let newNav = newTab.viewController as? any NavigationControllerProtocol { - nav.viewControllers = newNav.viewControllers + } else if let newVC = newTab.viewController { + nav.viewControllers = [newVC] } else { fatalError("unreachable") } @@ -229,11 +297,10 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { @available(iOS 18.0, *) extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) { - if let vc = selectedViewController as? MultiColumnNavigationController { - animator.addAnimations { - self.updateViewControllerSafeAreaInsets(vc) - vc.view.layoutIfNeeded() - } + let vc = selectedTab!.parent?.viewController ?? selectedTab!.viewController! + animator.addAnimations { + self.updateViewControllerSafeAreaInsets(vc) + vc.view.layoutIfNeeded() } } } diff --git a/Tusker/Screens/Utilities/AdaptableNavigationController.swift b/Tusker/Screens/Utilities/AdaptableNavigationController.swift new file mode 100644 index 00000000..0a39fcab --- /dev/null +++ b/Tusker/Screens/Utilities/AdaptableNavigationController.swift @@ -0,0 +1,136 @@ +// +// AdaptableNavigationController.swift +// Tusker +// +// Created by Shadowfacts on 8/20/24. +// Copyright © 2024 Shadowfacts. All rights reserved. +// + +import UIKit +import Combine + +@available(iOS 17.0, *) +class AdaptableNavigationController: UIViewController { + + private let viewControllersToPrependInCompact: [UIViewController] + + private var initialViewControllers: [UIViewController] = [] + private lazy var regular = makeRegularNavigationController() + private lazy var compact = makeCompactNavigationController() + private var _current: (any NavigationControllerProtocol)? + var current: any NavigationControllerProtocol { + traitCollection.horizontalSizeClass == .regular ? regular : compact + } + + init(viewControllersToPrependInCompact: [UIViewController] = []) { + self.viewControllersToPrependInCompact = viewControllersToPrependInCompact + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + updateNavigationController() + registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: AdaptableNavigationController, previousTraitCollection) in + self.updateNavigationController() + } + } + + private func updateNavigationController() { + let isTransferring: Bool + var stack: [UIViewController] + if let _current { + _current.removeViewAndController() + stack = _current.viewControllers + isTransferring = true + } else { + stack = initialViewControllers + initialViewControllers = [] + isTransferring = false + } + + if traitCollection.horizontalSizeClass == .regular { + if isTransferring { + stack.removeFirst(viewControllersToPrependInCompact.count) + } + } else { + stack.insert(contentsOf: viewControllersToPrependInCompact, at: 0) + } + + _current = current + current.viewControllers = stack + + addChild(current) + current.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(current.view) + NSLayoutConstraint.activate([ + current.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + current.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + current.view.topAnchor.constraint(equalTo: view.topAnchor), + current.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + current.didMove(toParent: self) + } + + private func makeRegularNavigationController() -> any NavigationControllerProtocol { + // TODO: need to figure out how to update the navigation controller if the pref changes + switch Preferences.shared.widescreenNavigationMode { + case .stack: + return EnhancedNavigationViewController() + case .splitScreen: + return SplitNavigationController() + case .multiColumn: + return MultiColumnNavigationController() + } + } + + private func makeCompactNavigationController() -> any NavigationControllerProtocol { + EnhancedNavigationViewController() + } +} + +@available(iOS 17.0, *) +extension AdaptableNavigationController: NavigationControllerProtocol { + var viewControllers: [UIViewController] { + get { + _current?.viewControllers ?? initialViewControllers + } + set { + if let _current { + _current.viewControllers = newValue + } else { + initialViewControllers = newValue + } + } + } + + var topViewController: UIViewController? { + if let _current { + return _current.topViewController + } else { + return initialViewControllers.last + } + } + + func popToRootViewController(animated: Bool) -> [UIViewController]? { + if let _current { + return _current.popToRootViewController(animated: animated) + } else { + defer { initialViewControllers = [] } + return initialViewControllers + } + } + + func pushViewController(_ vc: UIViewController, animated: Bool) { + if let _current { + _current.pushViewController(vc, animated: animated) + } else { + initialViewControllers.append(vc) + } + } +} From 4249ab30ca415a422915a717e2486939c217efed Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 14:10:59 -0400 Subject: [PATCH 11/56] Fix crash when hashtag search results include duplicate --- Tusker/Screens/Search/SearchResultsViewController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tusker/Screens/Search/SearchResultsViewController.swift b/Tusker/Screens/Search/SearchResultsViewController.swift index 9d22138a..3c096538 100644 --- a/Tusker/Screens/Search/SearchResultsViewController.swift +++ b/Tusker/Screens/Search/SearchResultsViewController.swift @@ -295,7 +295,10 @@ class SearchResultsViewController: UIViewController, CollectionViewController { } if !results.hashtags.isEmpty && resultTypes.contains(.hashtags) { snapshot.appendSections([.hashtags]) - snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .hashtags) + // mastodon sometimes includes duplicate hashtags with the same name but different urls + // (e.g., containing %C3%B8 vs o) + let uniqueHashtags = results.hashtags.uniques(by: \.name) + snapshot.appendItems(uniqueHashtags.map { .hashtag($0) }, toSection: .hashtags) } if !results.statuses.isEmpty && resultTypes.contains(.statuses) { snapshot.appendSections([.statuses]) From cb32c66a595554b11b6126756d4a770b5123d746 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 14:48:47 -0400 Subject: [PATCH 12/56] Support fast account switching with new sidebar --- Tusker/Box.swift | 6 +- .../FastAccountSwitcherViewController.swift | 6 +- ...ountSwitchingContainerViewController.swift | 11 +- .../Main/BaseMainTabBarViewController.swift | 21 ++-- .../Main/MainSplitViewController.swift | 6 +- .../Main/NewMainTabBarViewController.swift | 109 ++++++++++++++++++ 6 files changed, 148 insertions(+), 11 deletions(-) diff --git a/Tusker/Box.swift b/Tusker/Box.swift index d1dce9f6..f39690d6 100644 --- a/Tusker/Box.swift +++ b/Tusker/Box.swift @@ -9,10 +9,14 @@ import Foundation @propertyWrapper -class Box { +final class Box { var wrappedValue: Value init(wrappedValue: Value) { self.wrappedValue = wrappedValue } + + var projectedValue: Box { + self + } } diff --git a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift index 692cf23a..f4df41f3 100644 --- a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift +++ b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift @@ -11,6 +11,7 @@ import UserAccounts @MainActor protocol FastAccountSwitcherViewControllerDelegate: AnyObject { + func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) /// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached. func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool @@ -31,7 +32,7 @@ class FastAccountSwitcherViewController: UIViewController { #endif private var touchBeganFeedbackWorkItem: DispatchWorkItem? - var itemOrientation: ItemOrientation = .iconsTrailing + private var itemOrientation: ItemOrientation = .iconsTrailing init() { super.init(nibName: "FastAccountSwitcherViewController", bundle: .main) @@ -60,6 +61,9 @@ class FastAccountSwitcherViewController: UIViewController { } func show() { + if let delegate { + itemOrientation = delegate.fastAccountSwitcherItemOrientation(self) + } createAccountViews() // add after creating account views so that the presenter can align based on them delegate?.fastAccountSwitcherAddToViewHierarchy(self) diff --git a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift index d2a47211..45a7d263 100644 --- a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift +++ b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift @@ -39,7 +39,16 @@ class AccountSwitchingContainerViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - embedChild(root) + addChild(root) + root.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(root.view) + NSLayoutConstraint.activate([ + root.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + root.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + root.view.topAnchor.constraint(equalTo: view.topAnchor), + root.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + root.didMove(toParent: self) } override func didReceiveMemoryWarning() { diff --git a/Tusker/Screens/Main/BaseMainTabBarViewController.swift b/Tusker/Screens/Main/BaseMainTabBarViewController.swift index 1885ff0a..990ef8c0 100644 --- a/Tusker/Screens/Main/BaseMainTabBarViewController.swift +++ b/Tusker/Screens/Main/BaseMainTabBarViewController.swift @@ -8,7 +8,7 @@ import UIKit -class BaseMainTabBarViewController: UITabBarController { +class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewControllerDelegate { let mastodonController: MastodonController @@ -114,12 +114,15 @@ class BaseMainTabBarViewController: UITabBarController { fastAccountSwitcher.hide() } #endif // !os(visionOS) - -} - -#if !os(visionOS) -extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegate { + + // MARK: FastAccountSwitcherViewControllerDelegate + + func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation { + return .iconsTrailing + } + func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { + #if !os(visionOS) fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(fastAccountSwitcher.view) NSLayoutConstraint.activate([ @@ -134,17 +137,21 @@ extension BaseMainTabBarViewController: FastAccountSwitcherViewControllerDelegat fastAccountSwitcher.view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), fastAccountSwitcher.view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), ]) + #endif // !os(visionOS) } func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { + #if !os(visionOS) guard let myProfileButton = findMyProfileTabBarButton() else { return false } let locationInButton = myProfileButton.convert(point, from: tabBar) return myProfileButton.bounds.contains(locationInButton) + #else + return false + #endif // !os(visionOS) } } -#endif // !os(visionOS) extension BaseMainTabBarViewController: TuskerNavigationDelegate { var apiController: MastodonController! { mastodonController } diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 14f8a35b..23cf8973 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -93,7 +93,6 @@ class MainSplitViewController: UISplitViewController { if UIDevice.current.userInterfaceIdiom != .mac { let switcher = FastAccountSwitcherViewController() fastAccountSwitcher = switcher - switcher.itemOrientation = .iconsLeading switcher.view.translatesAutoresizingMaskIntoConstraints = false switcher.delegate = self // accessing .view unconditionally loads the view, which we don't want to happen @@ -664,6 +663,10 @@ extension MainSplitViewController: BackgroundableViewController { } extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate { + func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation { + return .iconsLeading + } + func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { view.addSubview(fastAccountSwitcher.view) let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)! @@ -677,6 +680,7 @@ extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate { fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } + func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { guard !isCollapsed, let cell = sidebar.myProfileCell() else { diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 7ff101d6..4726f44d 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -28,6 +28,8 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { private var navigationStacks = [String: [UIViewController]]() private var isCompact: Bool? + @Box fileprivate var myProfileCell: UIView? + private var sidebarTapRecognizer: UITapGestureRecognizer? override func viewDidLoad() { super.viewDidLoad() @@ -202,6 +204,17 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { return nav } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if sidebarTapRecognizer == nil, + let sidebarView = findSidebarView() { + sidebarTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped)) + sidebarTapRecognizer!.cancelsTouchesInView = false + sidebarView.addGestureRecognizer(sidebarTapRecognizer!) + } + } + private func reloadLists(_ lists: [List]) { listsGroup.children = lists.map { list in UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in @@ -214,6 +227,10 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { compose(editing: nil) } + @objc private func sidebarTapped() { + fastAccountSwitcher?.hide() + } + fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) { guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else { return @@ -222,6 +239,57 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { // The floating tab bar seems to always be 88pt tall, regardless of, e.g., Dynamic Type size. vc.additionalSafeAreaInsets = UIEdgeInsets(top: sidebar.isHidden ? 0 : -88, left: 0, bottom: 0, right: 0) } + + private func findSidebarView() -> UIView? { + var next = myProfileCell + while let cur = next { + if cur.superview?.superview === self.view { + return cur + } else { + next = cur.superview + } + } + return nil + } + + #if !os(visionOS) + override func fastAccountSwitcherItemOrientation(_ fastAccountSwitcher: FastAccountSwitcherViewController) -> FastAccountSwitcherViewController.ItemOrientation { + guard !sidebar.isHidden, + myProfileCell != nil else { + return super.fastAccountSwitcherItemOrientation(fastAccountSwitcher) + } + return .iconsLeading + } + + override func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) { + guard !sidebar.isHidden, + let myProfileCell else { + super.fastAccountSwitcherAddToViewHierarchy(fastAccountSwitcher) + return + } + + fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(fastAccountSwitcher.view) + + let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)! + NSLayoutConstraint.activate([ + currentAccount.centerYAnchor.constraint(equalTo: myProfileCell.centerYAnchor), + + fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: selectedTab!.viewController!.view.safeAreaLayoutGuide.leadingAnchor), + fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor), + fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + override func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool { + guard !sidebar.isHidden, + myProfileCell != nil else { + return super.fastAccountSwitcher(fastAccountSwitcher, triggerZoneContains: point) + } + return true + } + #endif } @available(iOS 18.0, *) @@ -303,6 +371,24 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { vc.view.layoutIfNeeded() } } + + func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, itemFor request: UITabSidebarItem.Request) -> UITabSidebarItem { + let item = UITabSidebarItem(request: request) + if case .tab(let tab) = request.content, + UIDevice.current.userInterfaceIdiom != .mac, + tab.identifier == Tab.myProfile.rawValue { + let indicator = FastAccountSwitcherIndicatorView() + // need to explicitly set the frame to get it vertically centered + indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize) + item.accessories = [ + .customView(configuration: .init(customView: indicator, placement: .trailing())) + ] + item.contentConfiguration = MyProfileContentConfiguration(wrapped: item.contentConfiguration, view: $myProfileCell) { [unowned self] in + $0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture()) + } + } + return item + } } @available(iOS 18.0, *) @@ -393,3 +479,26 @@ extension NewMainTabBarViewController: AccountSwitchableViewController { #endif } } + +private struct MyProfileContentConfiguration: UIContentConfiguration { + let wrapped: any UIContentConfiguration + @Box var view: UIView? + let configureView: (UIView) -> Void + + init(wrapped: any UIContentConfiguration, view: Box, configureView: @escaping (UIView) -> Void) { + self.wrapped = wrapped + self._view = view + self.configureView = configureView + } + + func makeContentView() -> any UIView & UIContentView { + let view = wrapped.makeContentView() + self.view = view + configureView(view) + return view + } + + func updated(for state: any UIConfigurationState) -> Self { + return .init(wrapped: wrapped.updated(for: state), view: $view, configureView: configureView) + } +} From 7c7af945e4dbcbec15488f6daf055b1ef07e0a9f Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:12:05 -0400 Subject: [PATCH 13/56] Show avatar in tab/side bar when using new API --- ...inSidebarMyProfileCollectionViewCell.swift | 14 +-- .../Main/NewMainTabBarViewController.swift | 103 ++++++++++++++++-- 2 files changed, 99 insertions(+), 18 deletions(-) diff --git a/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift b/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift index 8e9d66b0..48ed96df 100644 --- a/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift +++ b/Tusker/Screens/Main/MainSidebarMyProfileCollectionViewCell.swift @@ -11,14 +11,14 @@ import UserAccounts class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { - private var verticalImageInset: CGFloat { + static var verticalImageInset: CGFloat { if UIDevice.current.userInterfaceIdiom == .mac { return (28 - avatarImageSize) / 2 } else { return (44 - avatarImageSize) / 2 } } - private var avatarImageSize: CGFloat { + static var avatarImageSize: CGFloat { if UIDevice.current.userInterfaceIdiom == .mac { return 20 } else { @@ -72,11 +72,11 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { return } config.image = image - config.directionalLayoutMargins.top = self.verticalImageInset - config.directionalLayoutMargins.bottom = self.verticalImageInset - config.imageProperties.maximumSize = CGSize(width: self.avatarImageSize, height: self.avatarImageSize) + config.directionalLayoutMargins.top = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.directionalLayoutMargins.bottom = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize) config.imageProperties.reservedLayoutSize = CGSize(width: UIListContentConfiguration.ImageProperties.standardDimension, height: 0) - config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * self.avatarImageSize + config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize self.contentConfiguration = config } } @@ -86,7 +86,7 @@ class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell { guard var config = self.contentConfiguration as? UIListContentConfiguration else { return } - config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize + config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize self.contentConfiguration = config } diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 4726f44d..4b48d0cd 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -9,6 +9,7 @@ import UIKit import Combine import Pachyderm +import TuskerPreferences @available(iOS 18.0, *) class NewMainTabBarViewController: BaseMainTabBarViewController { @@ -52,7 +53,7 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { bookmarksTab.preferredPlacement = .optional favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider) favoritesTab.preferredPlacement = .optional - myProfileTab = UITab(title: "My Profile", image: UIImage(systemName: "person"), identifier: Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) + myProfileTab = MyProfileTab(mastodonController: mastodonController, viewControllerProvider: viewControllerProvider) listsGroup = UITabGroup(title: "Lists", image: nil, identifier: Tab.lists.rawValue, children: []) { _ in // this closure is necessary to prevent UIKit from crashing (FB14860961) @@ -362,6 +363,13 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { } } +private var fastAccountSwitcherIndicator: UIView = { + let indicator = FastAccountSwitcherIndicatorView() + // need to explicitly set the frame to get it vertically centered + indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize) + return indicator +}() + @available(iOS 18.0, *) extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) { @@ -375,16 +383,22 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, itemFor request: UITabSidebarItem.Request) -> UITabSidebarItem { let item = UITabSidebarItem(request: request) if case .tab(let tab) = request.content, - UIDevice.current.userInterfaceIdiom != .mac, - tab.identifier == Tab.myProfile.rawValue { - let indicator = FastAccountSwitcherIndicatorView() - // need to explicitly set the frame to get it vertically centered - indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize) - item.accessories = [ - .customView(configuration: .init(customView: indicator, placement: .trailing())) - ] - item.contentConfiguration = MyProfileContentConfiguration(wrapped: item.contentConfiguration, view: $myProfileCell) { [unowned self] in - $0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture()) + tab.identifier == Tab.myProfile.rawValue, + var config = item.contentConfiguration as? UIListContentConfiguration { + config.directionalLayoutMargins.top = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.directionalLayoutMargins.bottom = MainSidebarMyProfileCollectionViewCell.verticalImageInset + config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize) + config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize + + if UIDevice.current.userInterfaceIdiom != .mac { + item.accessories = [ + .customView(configuration: .init(customView: fastAccountSwitcherIndicator, placement: .trailing())) + ] + item.contentConfiguration = MyProfileContentConfiguration(wrapped: config, view: $myProfileCell) { [unowned self] in + $0.addGestureRecognizer(self.fastAccountSwitcher.createSwitcherGesture()) + } + } else { + item.contentConfiguration = config } } return item @@ -502,3 +516,70 @@ private struct MyProfileContentConfiguration: UIContentConfiguration { return .init(wrapped: wrapped.updated(for: state), view: $view, configureView: configureView) } } + +@available(iOS 18.0, *) +private class MyProfileTab: UITab { + private let mastodonController: MastodonController + private var avatarStyle: AvatarStyle? + + init(mastodonController: MastodonController, viewControllerProvider: @escaping (UITab) -> UIViewController) { + self.mastodonController = mastodonController + + // try to add the avatar image synchronously if possible + var avatarImage: UIImage? + if !Preferences.shared.grayscaleImages, + let account = mastodonController.account, + let avatarURL = account.avatar, + let avatar = ImageCache.avatars.get(avatarURL) { + avatarImage = Self.renderAvatar(avatar.image) + self.avatarStyle = Preferences.shared.avatarStyle + } + + let image = avatarImage ?? UIImage(systemName: "person")! + super.init(title: "My Profile", image: image, identifier: NewMainTabBarViewController.Tab.myProfile.rawValue, viewControllerProvider: viewControllerProvider) + + if avatarImage == nil { + Task { + await updateAvatar() + } + } + + NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) + } + + private func updateAvatar() async { + guard let account = try? await mastodonController.getOwnAccount(), + let avatarURL = account.avatar, + let image = await ImageCache.avatars.get(avatarURL).1 else { + return + } + + let maybeGrayscale = await ImageGrayscalifier.convertIfNecessary(url: avatarURL, image: image) ?? image + let rendered = Self.renderAvatar(maybeGrayscale) + + self.avatarStyle = Preferences.shared.avatarStyle + self.image = rendered + } + + private static func renderAvatar(_ image: UIImage) -> UIImage { + let size = MainSidebarMyProfileCollectionViewCell.avatarImageSize + let radius = Preferences.shared.avatarStyle.cornerRadiusFraction * size + let rect = CGRect(x: 0, y: 0, width: size, height: size) + let renderer = UIGraphicsImageRenderer(bounds: rect) + let rendered = renderer.image { ctx in + UIBezierPath(roundedRect: rect, cornerRadius: radius).addClip() + image.draw(in: rect) + } + return rendered.withRenderingMode(.alwaysOriginal) + } + + @objc private func preferencesChanged() { + if avatarStyle != nil, + avatarStyle != Preferences.shared.avatarStyle { + Task { + await updateAvatar() + } + } + } + +} From 37b9673b128ae597fdc01df0fdb60df8c32201dc Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:17:57 -0400 Subject: [PATCH 14/56] Fix list timeline no content view being added repetedly on refresh --- Tusker/Screens/Lists/ListTimelineViewController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tusker/Screens/Lists/ListTimelineViewController.swift b/Tusker/Screens/Lists/ListTimelineViewController.swift index 678f2fc6..b18eda7d 100644 --- a/Tusker/Screens/Lists/ListTimelineViewController.swift +++ b/Tusker/Screens/Lists/ListTimelineViewController.swift @@ -56,6 +56,10 @@ class ListTimelineViewController: TimelineViewController { } private func createNoContentView() { + guard noContentView == nil else { + return + } + let title = UILabel() title.textColor = .secondaryLabel title.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)! @@ -133,6 +137,9 @@ class ListTimelineViewController: TimelineViewController { override func handleReplaceAllItems(_ timelineItems: [String]) async { if timelineItems.isEmpty { createNoContentView() + } else { + noContentView?.removeFromSuperview() + noContentView = nil } await super.handleReplaceAllItems(timelineItems) } From ce10c7d6e28c43bdf40674a5efa77e112dc7f093 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:16:34 -0400 Subject: [PATCH 15/56] Implement adding list using new sidebar --- .../Main/MainSidebarViewController.swift | 5 ++-- .../Main/NewMainTabBarViewController.swift | 26 +++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Tusker/Screens/Main/MainSidebarViewController.swift b/Tusker/Screens/Main/MainSidebarViewController.swift index 6b0908bc..d35363f5 100644 --- a/Tusker/Screens/Main/MainSidebarViewController.swift +++ b/Tusker/Screens/Main/MainSidebarViewController.swift @@ -269,8 +269,9 @@ class MainSidebarViewController: UIViewController { } private func showAddList() { - let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true - ) }) { list in + let service = CreateListService(mastodonController: mastodonController, present: { + self.present($0, animated: true) + }) { list in let oldItem = self.selectedItem self.select(item: .list(list), animated: false) let list = ListTimelineViewController(for: list, mastodonController: self.mastodonController) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 4b48d0cd..ebfbc6bd 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -12,7 +12,7 @@ import Pachyderm import TuskerPreferences @available(iOS 18.0, *) -class NewMainTabBarViewController: BaseMainTabBarViewController { +final class NewMainTabBarViewController: BaseMainTabBarViewController { private let composePlaceholder = UIViewController() @@ -61,8 +61,8 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { } listsGroup.preferredPlacement = .sidebarOnly listsGroup.sidebarActions = [ - UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { _ in - fatalError("TODO") + UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in + self.showAddList() }) ] reloadLists(mastodonController.lists) @@ -129,7 +129,7 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { if nav.viewControllers.count > 1 { switch nav.viewControllers[1] { case let listVC as ListTimelineViewController: - if let tab = listsGroup.tab(forIdentifier: "list:\(listVC.list.id)") { + if let tab = listsGroup.tab(forIdentifier: Self.listTabIdentifier(listVC.list)) { newTab = (tab, Array(nav.viewControllers[1...])) nav.viewControllers = [ nav.viewControllers[0], // leave the ExploreVC in place @@ -218,12 +218,16 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { private func reloadLists(_ lists: [List]) { listsGroup.children = lists.map { list in - UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: "list:\(list.id)") { [unowned self] _ in + UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: Self.listTabIdentifier(list)) { [unowned self] _ in return ListTimelineViewController(for: list, mastodonController: self.mastodonController) } } } + private static func listTabIdentifier(_ list: List) -> String { + "list:\(list.id)" + } + @objc func handleComposeKeyCommand() { compose(editing: nil) } @@ -232,6 +236,18 @@ class NewMainTabBarViewController: BaseMainTabBarViewController { fastAccountSwitcher?.hide() } + private func showAddList() { + let service = CreateListService(mastodonController: mastodonController, present: { + self.present($0, animated: true) + }) { list in + let tab = self.listsGroup.tab(forIdentifier: Self.listTabIdentifier(list))! + let listVC = tab.viewController as! ListTimelineViewController + listVC.presentEditOnAppear = true + self.selectedTab = tab + } + service.run() + } + fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) { guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else { return From d321c317766aa78d0bcc16aa7d20f15a8d2c2d77 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:23:05 -0400 Subject: [PATCH 16/56] Implement more protocols for AdaptableNavigationController --- .../Utilities/AdaptableNavigationController.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tusker/Screens/Utilities/AdaptableNavigationController.swift b/Tusker/Screens/Utilities/AdaptableNavigationController.swift index 0a39fcab..145e75cd 100644 --- a/Tusker/Screens/Utilities/AdaptableNavigationController.swift +++ b/Tusker/Screens/Utilities/AdaptableNavigationController.swift @@ -134,3 +134,17 @@ extension AdaptableNavigationController: NavigationControllerProtocol { } } } + +@available(iOS 17.0, *) +extension AdaptableNavigationController: BackgroundableViewController { + func sceneDidEnterBackground() { + (topViewController as? BackgroundableViewController)?.sceneDidEnterBackground() + } +} + +@available(iOS 17.0, *) +extension AdaptableNavigationController: StatusBarTappableViewController { + func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { + (topViewController as? StatusBarTappableViewController)?.handleStatusBarTapped(xPosition: xPosition) ?? .continue + } +} From 59d43fd3f626e46154d9428ae2174784898bae34 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:38:24 -0400 Subject: [PATCH 17/56] Open in New Window context menu actions for new sidebar --- .../Main/NewMainTabBarViewController.swift | 88 +++++++++++++++++-- 1 file changed, 80 insertions(+), 8 deletions(-) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index ebfbc6bd..c0a7f02f 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -129,7 +129,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { if nav.viewControllers.count > 1 { switch nav.viewControllers[1] { case let listVC as ListTimelineViewController: - if let tab = listsGroup.tab(forIdentifier: Self.listTabIdentifier(listVC.list)) { + if let tab = listsGroup.tab(forIdentifier: ListTab.identifier(for: listVC.list)) { newTab = (tab, Array(nav.viewControllers[1...])) nav.viewControllers = [ nav.viewControllers[0], // leave the ExploreVC in place @@ -218,16 +218,12 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { private func reloadLists(_ lists: [List]) { listsGroup.children = lists.map { list in - UITab(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: Self.listTabIdentifier(list)) { [unowned self] _ in + ListTab(list: list) { [unowned self] _ in return ListTimelineViewController(for: list, mastodonController: self.mastodonController) } } } - private static func listTabIdentifier(_ list: List) -> String { - "list:\(list.id)" - } - @objc func handleComposeKeyCommand() { compose(editing: nil) } @@ -240,7 +236,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true) }) { list in - let tab = self.listsGroup.tab(forIdentifier: Self.listTabIdentifier(list))! + let tab = self.listsGroup.tab(forIdentifier: ListTab.identifier(for: list))! let listVC = tab.viewController as! ListTimelineViewController listVC.presentEditOnAppear = true self.selectedTab = tab @@ -419,6 +415,69 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { } return item } + + func tabBarController(_ tabBarController: UITabBarController, sidebar: UITabBarController.Sidebar, contextMenuConfigurationFor tab: UITab) -> UIContextMenuConfiguration? { + guard let id = mastodonController.accountInfo?.id else { + return nil + } + + let activity: NSUserActivity + + if let listTab = tab as? ListTab { + let timelineActivity = UserActivityManager.showTimelineActivity(timeline: .list(id: listTab.list.id), accountID: id) + if let timelineActivity { + activity = timelineActivity + } else { + return nil + } + } else if let tabID = Tab(rawValue: tab.identifier) { + switch tabID { + case .home: + return nil + case .notifications: + activity = UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode, accountID: id) + case .explore: + activity = UserActivityManager.searchActivity(query: nil, accountID: id) + case .bookmarks: + activity = UserActivityManager.bookmarksActivity(accountID: id) + case .favorites: + // TODO + return nil + case .myProfile: + // no 'Open in New Window' activity for my profile, because the context menu clashes with the fast account switcher + return nil + case .compose: + activity = UserActivityManager.newPostActivity(accountID: id) + case .lists: + return nil + } + } else { + return nil + } + + activity.displaysAuxiliaryScene = true + + return UIContextMenuConfiguration(actionProvider: { _ in + var actions: [UIAction] = [ + UIWindowScene.ActivationAction({ action in + return UIWindowScene.ActivationConfiguration(userActivity: activity) + }) + ] + + if let listTab = tab as? ListTab { + actions.append(UIAction(title: "Delete List", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { [unowned self] _ in + Task { + let service = DeleteListService(list: listTab.list, mastodonController: self.mastodonController) { + self.present($0, animated: true) + } + await service.run() + } + })) + } + + return UIMenu(children: actions) + }) + } } @available(iOS 18.0, *) @@ -597,5 +656,18 @@ private class MyProfileTab: UITab { } } } - +} + +@available(iOS 18.0, *) +private class ListTab: UITab { + let list: List + + init(list: List, viewControllerProvider: @escaping (UITab) -> UIViewController) { + self.list = list + super.init(title: list.title, image: UIImage(systemName: "list.bullet"), identifier: Self.identifier(for: list), viewControllerProvider: viewControllerProvider) + } + + static func identifier(for list: List) -> String { + "list:\(list.id)" + } } From 0d9eed73dda16194525433ccbdf74b1fa08ddb40 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 16:58:16 -0400 Subject: [PATCH 18/56] Add saved/followed hashtags to new sidebar --- .../Main/NewMainTabBarViewController.swift | 123 +++++++++++++++--- 1 file changed, 103 insertions(+), 20 deletions(-) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index c0a7f02f..da54db08 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -24,6 +24,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { private var favoritesTab: UITab! private var myProfileTab: UITab! private var listsGroup: UITabGroup! + private var hashtagsGroup: UITabGroup! private var cancellables = Set() @@ -67,6 +68,17 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { ] reloadLists(mastodonController.lists) + hashtagsGroup = UITabGroup(title: "Hashtags", image: nil, identifier: Tab.hashtags.rawValue, children: []) { _ in + return AdaptableNavigationController() + } + hashtagsGroup.preferredPlacement = .sidebarOnly + hashtagsGroup.sidebarActions = [ + UIAction(title: "Add Hashtag…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in + self.showAddSavedHashtag() + }) + ] + reloadHashtags() + if UIDevice.current.userInterfaceIdiom == .phone { self.tabs = [ homeTab, @@ -83,13 +95,19 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { let vcToUpdate = self.selectedTab!.parent?.viewController ?? self.selectedTab!.viewController! self.updateViewControllerSafeAreaInsets(vcToUpdate) } + + mastodonController.$lists + .sink { [unowned self] in self.reloadLists($0) } + .store(in: &cancellables) + + mastodonController.$followedHashtags + .map { _ in () } + .merge(with: NotificationCenter.default.publisher(for: .savedHashtagsChanged).map { _ in () }) + .sink { [unowned self] in self.reloadHashtags() } + .store(in: &cancellables) } setupFastAccountSwitcher() - - mastodonController.$lists - .sink { [unowned self] in self.reloadLists($0) } - .store(in: &cancellables) } private func updatePadTabs() { @@ -99,8 +117,9 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { isCompact = true var exploreNavStack: [UIViewController]? = nil - if selectedTab?.parent == listsGroup { - let nav = listsGroup.viewController as! any NavigationControllerProtocol + if let parent = selectedTab?.parent, + parent === listsGroup || parent === hashtagsGroup { + let nav = parent.viewController as! any NavigationControllerProtocol exploreNavStack = nav.viewControllers nav.viewControllers = [] } @@ -121,24 +140,33 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { } else { isCompact = false - var newTab: (UITab, [UIViewController])? = nil + var newTabAndNavigationStack: (UITab, [UIViewController])? = nil if wasCompact == true, selectedTab == exploreTab { let nav = exploreTab.viewController as! any NavigationControllerProtocol // skip over the ExploreViewController if nav.viewControllers.count > 1 { + var newTab: UITab? switch nav.viewControllers[1] { case let listVC as ListTimelineViewController: if let tab = listsGroup.tab(forIdentifier: ListTab.identifier(for: listVC.list)) { - newTab = (tab, Array(nav.viewControllers[1...])) - nav.viewControllers = [ - nav.viewControllers[0], // leave the ExploreVC in place - InlineTrendsViewController(mastodonController: mastodonController), // re-insert an InlineTrendsVC - ] + newTab = tab + } + case let hashtagVC as HashtagTimelineViewController: + if let tab = hashtagsGroup.tab(forIdentifier: HashtagTab.identifier(for: hashtagVC.hashtagName)) { + newTab = tab } default: break } + + if let newTab { + newTabAndNavigationStack = (newTab, Array(nav.viewControllers[1...])) + nav.viewControllers = [ + nav.viewControllers[0], // leave the ExploreVC in place + InlineTrendsViewController(mastodonController: mastodonController), // re-insert an InlineTrendsVC + ] + } } } @@ -151,9 +179,10 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { myProfileTab, composeTab, listsGroup, + hashtagsGroup, ] - if let (tab, navStack) = newTab { + if let (tab, navStack) = newTabAndNavigationStack { let nav = tab.parent!.viewController as! any NavigationControllerProtocol nav.viewControllers = navStack // Setting the tab now seems to be clobbered by the UITabBarController itself updating in response @@ -193,7 +222,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { root = FavoritesViewController(mastodonController: mastodonController) case .myProfile: root = MyProfileViewController(mastodonController: mastodonController) - case .lists: + case .lists, .hashtags: fatalError("unreachable") } return embedInNavigationController(root) @@ -217,11 +246,37 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { } private func reloadLists(_ lists: [List]) { - listsGroup.children = lists.map { list in - ListTab(list: list) { [unowned self] _ in - return ListTimelineViewController(for: list, mastodonController: self.mastodonController) - } + let viewControllerProvider = { [unowned self] (tab: UITab) in + let tab = tab as! ListTab + return ListTimelineViewController(for: tab.list, mastodonController: self.mastodonController) } + listsGroup.children = lists.map { list in + ListTab(list: list, viewControllerProvider: viewControllerProvider) + } + } + + private func reloadHashtags() { + let viewControllerProvider = { [unowned self] (tab: UITab) in + let tab = tab as! HashtagTab + return HashtagTimelineViewController(forNamed: tab.hashtagName, mastodonController: self.mastodonController) + } + var seenTags: Set = [] + var tabs: [UITab] = [] + let savedReq = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!) + let saved = (try? mastodonController.persistentContainer.viewContext.fetch(savedReq)) ?? [] + for hashtag in saved { + seenTags.insert(hashtag.name) + tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider)) + } + + let followedReq = FollowedHashtag.fetchRequest() + let followed = (try? mastodonController.persistentContainer.viewContext.fetch(followedReq)) ?? [] + for hashtag in followed where !seenTags.contains(hashtag.name) { + tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider)) + } + + tabs.sort(using: SemiCaseSensitiveComparator.keyPath(\.title)) + hashtagsGroup.children = tabs } @objc func handleComposeKeyCommand() { @@ -244,6 +299,12 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { service.run() } + private func showAddSavedHashtag() { + let addController = AddSavedHashtagViewController(mastodonController: mastodonController) + let nav = EnhancedNavigationViewController(rootViewController: addController) + present(nav, animated: true) + } + fileprivate func updateViewControllerSafeAreaInsets(_ vc: UIViewController) { guard vc is MultiColumnNavigationController || (vc as? AdaptableNavigationController)?.current is MultiColumnNavigationController else { return @@ -317,6 +378,7 @@ extension NewMainTabBarViewController { case myProfile case lists + case hashtags } } @@ -356,7 +418,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.identifier == Tab.lists.rawValue, + group === listsGroup || group === hashtagsGroup, let nav = group.viewController as? any NavigationControllerProtocol { updateViewControllerSafeAreaInsets(nav) @@ -430,6 +492,13 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { } else { return nil } + } else if let hashtagTab = tab as? HashtagTab { + let timelineActivity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtagTab.hashtagName), accountID: id) + if let timelineActivity { + activity = timelineActivity + } else { + return nil + } } else if let tabID = Tab(rawValue: tab.identifier) { switch tabID { case .home: @@ -448,7 +517,7 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { return nil case .compose: activity = UserActivityManager.newPostActivity(accountID: id) - case .lists: + case .lists, .hashtags: return nil } } else { @@ -671,3 +740,17 @@ private class ListTab: UITab { "list:\(list.id)" } } + +@available(iOS 18.0, *) +private class HashtagTab: UITab { + let hashtagName: String + + init(hashtagName: String, viewControllerProvider: @escaping (UITab) -> UIViewController) { + self.hashtagName = hashtagName + super.init(title: hashtagName, image: UIImage(systemName: "number"), identifier: Self.identifier(for: hashtagName), viewControllerProvider: viewControllerProvider) + } + + static func identifier(for name: String) -> String { + "hashtag:\(name)" + } +} From 18172470772d3eb324f907632a0361f195a49e84 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 17:10:01 -0400 Subject: [PATCH 19/56] Add saved instances to new sidebar --- .../Main/MainSidebarViewController.swift | 2 +- .../Main/NewMainTabBarViewController.swift | 79 ++++++++++++++++++- 2 files changed, 76 insertions(+), 5 deletions(-) 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!)" + } +} From 494708a36218e34bc2927c8021a204cfae4e53a7 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 18:27:30 -0400 Subject: [PATCH 20/56] Fix compiling on visionOS --- Tusker/Screens/Main/MainTabBarViewController.swift | 2 ++ Tusker/Screens/Main/NewMainTabBarViewController.swift | 10 +++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index ec4cb40b..57808a2b 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -45,7 +45,9 @@ class MainTabBarViewController: BaseMainTabBarViewController { embedInNavigationController(Tab.myProfile.createViewController(mastodonController)), ] + #if !os(visionOS) setupFastAccountSwitcher() + #endif tabBar.isSpringLoaded = true diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 10124a4f..633e1f72 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -91,7 +91,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { ] reloadSavedInstances() - if UIDevice.current.userInterfaceIdiom == .phone { + if UIDevice.current.userInterfaceIdiom == .phone || UIDevice.current.userInterfaceIdiom == .vision { self.tabs = [ homeTab, notificationsTab, @@ -121,7 +121,9 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil) } + #if !os(visionOS) setupFastAccountSwitcher() + #endif } private func updatePadTabs() { @@ -312,7 +314,9 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { } @objc private func sidebarTapped() { + #if !os(visionOS) fastAccountSwitcher?.hide() + #endif } private func showAddList() { @@ -500,6 +504,9 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { config.imageProperties.maximumSize = CGSize(width: MainSidebarMyProfileCollectionViewCell.avatarImageSize, height: MainSidebarMyProfileCollectionViewCell.avatarImageSize) config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * MainSidebarMyProfileCollectionViewCell.avatarImageSize + #if os(visionOS) + item.contentConfiguration = config + #else if UIDevice.current.userInterfaceIdiom != .mac { item.accessories = [ .customView(configuration: .init(customView: fastAccountSwitcherIndicator, placement: .trailing())) @@ -510,6 +517,7 @@ extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate { } else { item.contentConfiguration = config } + #endif } return item } From 0e95cd0adf91866565ce9893583201212f1facce Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 18:34:49 -0400 Subject: [PATCH 21/56] Update AdaptableNavigationController when interface preference changes --- .../AdaptableNavigationController.swift | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Tusker/Screens/Utilities/AdaptableNavigationController.swift b/Tusker/Screens/Utilities/AdaptableNavigationController.swift index 145e75cd..f991570f 100644 --- a/Tusker/Screens/Utilities/AdaptableNavigationController.swift +++ b/Tusker/Screens/Utilities/AdaptableNavigationController.swift @@ -7,7 +7,7 @@ // import UIKit -import Combine +import TuskerPreferences @available(iOS 17.0, *) class AdaptableNavigationController: UIViewController { @@ -15,6 +15,7 @@ class AdaptableNavigationController: UIViewController { private let viewControllersToPrependInCompact: [UIViewController] private var initialViewControllers: [UIViewController] = [] + private var currentWidescreenNavigationMode: WidescreenNavigationMode? private lazy var regular = makeRegularNavigationController() private lazy var compact = makeCompactNavigationController() private var _current: (any NavigationControllerProtocol)? @@ -39,6 +40,8 @@ class AdaptableNavigationController: UIViewController { registerForTraitChanges([UITraitHorizontalSizeClass.self]) { (self: AdaptableNavigationController, previousTraitCollection) in self.updateNavigationController() } + + NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) } private func updateNavigationController() { @@ -47,6 +50,7 @@ class AdaptableNavigationController: UIViewController { if let _current { _current.removeViewAndController() stack = _current.viewControllers + _current.viewControllers = [] isTransferring = true } else { stack = initialViewControllers @@ -79,6 +83,7 @@ class AdaptableNavigationController: UIViewController { private func makeRegularNavigationController() -> any NavigationControllerProtocol { // TODO: need to figure out how to update the navigation controller if the pref changes + self.currentWidescreenNavigationMode = Preferences.shared.widescreenNavigationMode switch Preferences.shared.widescreenNavigationMode { case .stack: return EnhancedNavigationViewController() @@ -92,6 +97,18 @@ class AdaptableNavigationController: UIViewController { private func makeCompactNavigationController() -> any NavigationControllerProtocol { EnhancedNavigationViewController() } + + @objc private func preferencesChanged() { + if currentWidescreenNavigationMode != Preferences.shared.widescreenNavigationMode { + if let _current, + _current === regular { + regular = makeRegularNavigationController() + updateNavigationController() + } else { + regular = makeRegularNavigationController() + } + } + } } @available(iOS 17.0, *) From c11390398000ddc792635e7f9586bb7c94169a5b Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 18:37:20 -0400 Subject: [PATCH 22/56] Fix SplitNavigationController layout with new sidebar --- .../Screens/Utilities/SplitNavigationController.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tusker/Screens/Utilities/SplitNavigationController.swift b/Tusker/Screens/Utilities/SplitNavigationController.swift index 2e0cf2bb..63e8433c 100644 --- a/Tusker/Screens/Utilities/SplitNavigationController.swift +++ b/Tusker/Screens/Utilities/SplitNavigationController.swift @@ -87,7 +87,7 @@ class SplitNavigationController: UIViewController { NSLayoutConstraint.activate([ rootNav.view.topAnchor.constraint(equalTo: view.topAnchor), rootNav.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), - rootNav.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + rootNav.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), separatorView.topAnchor.constraint(equalTo: view.topAnchor), separatorView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -196,13 +196,13 @@ class SplitNavigationController: UIViewController { NSLayoutConstraint.deactivate(constraints) if visible { constraints = [ - rootNav.view.trailingAnchor.constraint(equalTo: view.centerXAnchor), - secondaryNav.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + rootNav.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), + secondaryNav.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), ] } else { constraints = [ - rootNav.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - secondaryNav.view.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5), + rootNav.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + secondaryNav.view.widthAnchor.constraint(equalTo: rootNav.view.widthAnchor), ] } NSLayoutConstraint.activate(constraints) From 230696f4568e27554b708387c1e1971308906843 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 18:52:36 -0400 Subject: [PATCH 23/56] Bump build number and update changelog --- CHANGELOG.md | 8 ++++++++ Version.xcconfig | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdc0161c..17779378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2024.4 (134) +Features/Improvements: +- iOS 18: New floating sidebar/tab bar + +Bugfixes: +- Fix crash when hashtag search results include duplicates +- Fix "no content" text not being removed from list timeline after refreshing + ## 2024.3 (133) - Add additional info to Tip Jar diff --git a/Version.xcconfig b/Version.xcconfig index 7bfc35c6..0bd6d9e3 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -9,8 +9,8 @@ // Configuration settings file format documentation can be found at: // https://help.apple.com/xcode/#/dev745c5c974 -MARKETING_VERSION = 2024.3 -CURRENT_PROJECT_VERSION = 133 +MARKETING_VERSION = 2024.4 +CURRENT_PROJECT_VERSION = 134 CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION)) CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev From 4945a234e705d8da46eda726f3105da25d7dcc53 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 19:28:12 -0400 Subject: [PATCH 24/56] Fix new tab bar VC getting stuck in bad state after presenting Compose --- .../Main/NewMainTabBarViewController.swift | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 633e1f72..de660811 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -246,9 +246,13 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { } private func embedInNavigationController(_ vc: UIViewController) -> UIViewController { - let nav = AdaptableNavigationController() - nav.viewControllers = [vc] - return nav + if UIDevice.current.userInterfaceIdiom == .phone { + return UINavigationController(rootViewController: vc) + } else { + let nav = AdaptableNavigationController() + nav.viewControllers = [vc] + return nav + } } override func viewDidAppear(_ animated: Bool) { @@ -428,12 +432,13 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { if tab.identifier == Tab.compose.rawValue { let currentTab = selectedTab // returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254) - // but we need it to change to _something_ so that we can change back to the current tab - self.selectedTab = tab - self.selectedTab = currentTab - + // returning false and then setting selectedTab=tab and selectedTab=currentTab seems to leave things in a bad state (currentTab's VC is on screen but in the disappeared state) + // so return true, and then after the tab bar VC has finished updating, go back to currentTab + DispatchQueue.main.async { + self.selectedTab = currentTab + } compose(editing: nil) - return false + return true } else if let selectedTab, selectedTab == tab, let nav = selectedViewController as? any NavigationControllerProtocol, From 805e5eddd0ab7adf8ddf470436dbe5bd21925036 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 21 Aug 2024 19:30:54 -0400 Subject: [PATCH 25/56] Bump build number and update changelog --- CHANGELOG.md | 2 +- Version.xcconfig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17779378..79a91274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2024.4 (134) +## 2024.4 (135) Features/Improvements: - iOS 18: New floating sidebar/tab bar diff --git a/Version.xcconfig b/Version.xcconfig index 0bd6d9e3..05fd3047 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -10,7 +10,7 @@ // https://help.apple.com/xcode/#/dev745c5c974 MARKETING_VERSION = 2024.4 -CURRENT_PROJECT_VERSION = 134 +CURRENT_PROJECT_VERSION = 135 CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION)) CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev From 6de255681c7ebe88886b6c1be199a629dc84e184 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 22 Aug 2024 11:08:27 -0400 Subject: [PATCH 26/56] Fix assorted warnings when building with Xcode 16 --- Tusker.xcodeproj/project.pbxproj | 4 ---- .../MastodonCachePersistentStore.swift | 2 +- Tusker/Extensions/Mastodon+Equatable.swift | 21 ------------------- .../Announcements/AnnouncementListRow.swift | 2 +- .../VideoGalleryContentViewController.swift | 12 ++++++----- .../PushInstanceSettingsView.swift | 12 +++++------ Tusker/Views/Attachments/GifvController.swift | 4 +++- 7 files changed, 18 insertions(+), 39 deletions(-) delete mode 100644 Tusker/Extensions/Mastodon+Equatable.swift diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index c4656be1..e65fce05 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -165,7 +165,6 @@ D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; }; D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; }; D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; }; - D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; }; D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; }; D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; }; D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; }; @@ -599,7 +598,6 @@ D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = ""; }; D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = ""; }; D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = ""; }; - D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = ""; }; D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = ""; }; D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = ""; }; D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = ""; }; @@ -1324,7 +1322,6 @@ D667E5F62135C2ED0057A976 /* Extensions */ = { isa = PBXGroup; children = ( - D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */, D663626B21361C6700C9CBA2 /* Account+Preferences.swift */, D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */, D6333B362137838300CE884A /* AttributedString+Helpers.swift */, @@ -2216,7 +2213,6 @@ D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */, D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */, D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */, - D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */, D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */, D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */, diff --git a/Tusker/CoreData/MastodonCachePersistentStore.swift b/Tusker/CoreData/MastodonCachePersistentStore.swift index 44fd8bc9..0c9746f3 100644 --- a/Tusker/CoreData/MastodonCachePersistentStore.swift +++ b/Tusker/CoreData/MastodonCachePersistentStore.swift @@ -19,7 +19,7 @@ import UserAccounts fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore") -class MastodonCachePersistentStore: NSPersistentCloudKitContainer { +class MastodonCachePersistentStore: NSPersistentCloudKitContainer, @unchecked Sendable { private let accountInfo: UserAccountInfo? diff --git a/Tusker/Extensions/Mastodon+Equatable.swift b/Tusker/Extensions/Mastodon+Equatable.swift deleted file mode 100644 index 3ee2f750..00000000 --- a/Tusker/Extensions/Mastodon+Equatable.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Status+Equatable.swift -// Tusker -// -// Created by Shadowfacts on 8/28/18. -// Copyright © 2018 Shadowfacts. All rights reserved. -// - -import Pachyderm - -extension Status: Equatable { - public static func ==(lhs: Status, rhs: Status) -> Bool { - return lhs.id == rhs.id - } -} - -extension Account: Equatable { - public static func ==(lhs: Account, rhs: Account) -> Bool { - return lhs.id == rhs.id - } -} diff --git a/Tusker/Screens/Announcements/AnnouncementListRow.swift b/Tusker/Screens/Announcements/AnnouncementListRow.swift index 646c6edf..41233877 100644 --- a/Tusker/Screens/Announcements/AnnouncementListRow.swift +++ b/Tusker/Screens/Announcements/AnnouncementListRow.swift @@ -88,7 +88,7 @@ struct AnnouncementListRow: View { Button(role: .destructive) { Task { await dismissAnnouncement() - await removeAnnouncement() + removeAnnouncement() } } label: { Label("Dismiss", systemImage: "xmark") diff --git a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift index 8301787b..c693b888 100644 --- a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift +++ b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift @@ -89,12 +89,14 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon hideControlsWorkItem?.cancel() if player.rate > 0 && info.oldValue == 0 { hideControlsWorkItem = DispatchWorkItem { [weak self] in - guard let self, - let container = self.container, - container.galleryControlsVisible else { - return + MainActor.runUnsafely { + guard let self, + let container = self.container, + container.galleryControlsVisible else { + return + } + container.setGalleryControlsVisible(false, animated: true) } - container.setGalleryControlsVisible(false, animated: true) } DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!) } diff --git a/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift b/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift index 916b141f..98c68d6d 100644 --- a/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift +++ b/Tusker/Screens/Preferences/Notifications/PushInstanceSettingsView.swift @@ -87,7 +87,7 @@ struct PushInstanceSettingsView: View { } let subscription = try await PushManager.shared.createSubscription(account: account) - let mastodonController = await MastodonController.getForAccount(account) + let mastodonController = MastodonController.getForAccount(account) do { let result = try await mastodonController.createPushSubscription(subscription: subscription) PushManager.logger.debug("Push subscription \(result.id, privacy: .public) created on \(account.instanceURL) with endpoint \(result.endpoint, privacy: .public)") @@ -95,25 +95,25 @@ struct PushInstanceSettingsView: View { return true } catch { // if creation failed, remove the subscription locally as well - await PushManager.shared.removeSubscription(account: account) + PushManager.shared.removeSubscription(account: account) throw error } } private func disableNotifications() async throws { - let mastodonController = await MastodonController.getForAccount(account) + let mastodonController = MastodonController.getForAccount(account) try await mastodonController.deletePushSubscription() - await PushManager.shared.removeSubscription(account: account) + PushManager.shared.removeSubscription(account: account) subscription = nil PushManager.logger.debug("Push subscription removed on \(account.instanceURL)") } private func updateSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async -> Bool { - let mastodonController = await MastodonController.getForAccount(account) + let mastodonController = MastodonController.getForAccount(account) do { let result = try await mastodonController.updatePushSubscription(alerts: alerts, policy: policy) PushManager.logger.debug("Push subscription \(result.id, privacy: .public) updated on \(account.instanceURL)") - await PushManager.shared.updateSubscription(account: account, alerts: alerts, policy: policy) + PushManager.shared.updateSubscription(account: account, alerts: alerts, policy: policy) subscription?.alerts = alerts subscription?.policy = policy return true diff --git a/Tusker/Views/Attachments/GifvController.swift b/Tusker/Views/Attachments/GifvController.swift index cc4b2849..b89bd2d5 100644 --- a/Tusker/Views/Attachments/GifvController.swift +++ b/Tusker/Views/Attachments/GifvController.swift @@ -61,7 +61,9 @@ class GifvController { private func updatePresentationSizeObservation() { presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in - self.presentationSizeSubject.send(item.presentationSize) + DispatchQueue.main.async { + self.presentationSizeSubject.send(item.presentationSize) + } }) } From 9b2e6140a3c0f41e7f4b5fcb688bd60c1a284385 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 22 Aug 2024 11:39:39 -0400 Subject: [PATCH 27/56] Fix reselecting current sidebar item not popping to root on Catalyst and new sidebar Closes #525 --- Tusker/Screens/Main/MainSidebarViewController.swift | 4 ++-- Tusker/Screens/Main/MainSplitViewController.swift | 8 ++++++-- Tusker/Screens/Main/NewMainTabBarViewController.swift | 10 ++++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Tusker/Screens/Main/MainSidebarViewController.swift b/Tusker/Screens/Main/MainSidebarViewController.swift index 3391bbd5..38a88385 100644 --- a/Tusker/Screens/Main/MainSidebarViewController.swift +++ b/Tusker/Screens/Main/MainSidebarViewController.swift @@ -15,7 +15,7 @@ protocol MainSidebarViewControllerDelegate: AnyObject { func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController) func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item, previousItem: MainSidebarViewController.Item?) func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController, previousItem: MainSidebarViewController.Item?) - func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item) + func sidebar(_ sidebarViewController: MainSidebarViewController, didReselectItem item: MainSidebarViewController.Item) } class MainSidebarViewController: UIViewController { @@ -452,7 +452,7 @@ extension MainSidebarViewController: UICollectionViewDelegate { } itemLastSelectedTimestamps[item] = Date() if previouslySelectedItem == item { - sidebarDelegate?.sidebar(self, scrollToTopFor: item) + sidebarDelegate?.sidebar(self, didReselectItem: item) } else if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) { if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) { collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically) diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 23cf8973..23f62fe9 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -493,8 +493,12 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate { secondaryNavController.viewControllers = [viewController] } - func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item) { - (secondaryNavController as? TabBarScrollableViewController)?.tabBarScrollToTop() + func sidebar(_ sidebarViewController: MainSidebarViewController, didReselectItem item: MainSidebarViewController.Item) { + if secondaryNavController.viewControllers.count == 1 { + (secondaryNavController.topViewController as? TabBarScrollableViewController)?.tabBarScrollToTop() + } else { + secondaryNavController.popToRootViewController(animated: true) + } } } diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index de660811..17b19e1e 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -441,10 +441,12 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate { return true } else if let selectedTab, selectedTab == tab, - let nav = selectedViewController as? any NavigationControllerProtocol, - nav.viewControllers.count == 1, - let scrollableVC = nav.viewControllers[0] as? TabBarScrollableViewController { - scrollableVC.tabBarScrollToTop() + let nav = selectedViewController as? any NavigationControllerProtocol { + if nav.viewControllers.count == 1 { + (nav.viewControllers[0] as? TabBarScrollableViewController)?.tabBarScrollToTop() + } else { + nav.popToRootViewController(animated: true) + } return false } else { return true From 9547bd291384d846b9348da2b3d1bf379b2a5a38 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 22 Aug 2024 12:08:43 -0400 Subject: [PATCH 28/56] Fix incorrect split nav layout when closing split with new sidebar --- .../Utilities/SplitNavigationController.swift | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/Tusker/Screens/Utilities/SplitNavigationController.swift b/Tusker/Screens/Utilities/SplitNavigationController.swift index 63e8433c..28200572 100644 --- a/Tusker/Screens/Utilities/SplitNavigationController.swift +++ b/Tusker/Screens/Utilities/SplitNavigationController.swift @@ -241,13 +241,19 @@ class SplitNavigationController: UIViewController { // otherwise the secondary nav's contents disappear immediately, rather than sliding off-screen let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) { self.isLayingOutForAnimation = true - self.setSecondaryVisible(false) + NSLayoutConstraint.deactivate(self.constraints) + self.constraints = [ + self.rootNav.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: self.rootNav.view.bounds.minX), + self.rootNav.view.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor), + self.secondaryNav.view.widthAnchor.constraint(equalToConstant: self.secondaryNav.view.bounds.width), + ] + NSLayoutConstraint.activate(self.constraints) self.view.layoutIfNeeded() } animator.addCompletion { _ in - self.secondaryNav.viewControllers = [] self.isLayingOutForAnimation = false -// self.updateSecondaryNavVisibility() + self.secondaryNav.viewControllers = [] + self.updateSecondaryNavVisibility() } animator.startAnimation() } else { From 9ce6bd566ff3231f65e864f5bf33559827111ff4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 22 Aug 2024 13:33:02 -0400 Subject: [PATCH 29/56] Show errors when video loading fails Closes #532 --- .../VideoGalleryContentViewController.swift | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift index c693b888..2549b224 100644 --- a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift +++ b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift @@ -116,12 +116,45 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon MainActor.runUnsafely { if item.status == .readyToPlay { self.container?.setGalleryContentLoading(false) - statusObservation = nil + self.statusObservation = nil + } else if item.status == .failed, + let error = item.error { + self.container?.setGalleryContentLoading(false) + self.showErrorView(error) + self.statusObservation = nil } } }) } + private func showErrorView(_ error: any Error) { + let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!) + image.tintColor = .secondaryLabel + image.contentMode = .scaleAspectFit + + let label = UILabel() + label.text = "Error Loading" + label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)! + label.textColor = .secondaryLabel + label.adjustsFontForContentSizeCategory = true + + let stackView = UIStackView(arrangedSubviews: [ + image, + label, + ]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .center + stackView.spacing = 8 + view.addSubview(stackView) + NSLayoutConstraint.activate([ + image.widthAnchor.constraint(equalToConstant: 64), + image.heightAnchor.constraint(equalToConstant: 64), + stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + ]) + } + @objc private func preferencesChanged() { if isGrayscale != Preferences.shared.grayscaleImages { let isPlaying = player.rate > 0 From b663335c6d17d38b89bdbf12fa35c79dd4196714 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 22 Aug 2024 13:54:03 -0400 Subject: [PATCH 30/56] Use the image description from imported image when possible Closes #523 --- .../ComposeUI/CoreData/DraftAttachment.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftAttachment.swift b/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftAttachment.swift index 64ecf945..d71fb082 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftAttachment.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/CoreData/DraftAttachment.swift @@ -167,11 +167,23 @@ extension DraftAttachment: NSItemProviderReading { type = .png } + // Read the caption from the image itself, if there is one. + let caption: String + if let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceTypeIdentifierHint: typeIdentifier as CFString] as CFDictionary), + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any], + // This is the dictionary for TIFF properties, but it's present for other image types too + let tiffProperties = properties[kCGImagePropertyTIFFDictionary as String] as? [String: Any], + let imageDescription = tiffProperties[kCGImagePropertyTIFFImageDescription as String] as? String { + caption = imageDescription + } else { + caption = "" + } + let attachment = DraftAttachment(entity: DraftsPersistentContainer.shared.persistentStoreCoordinator.managedObjectModel.entitiesByName["DraftAttachment"]!, insertInto: nil) attachment.id = UUID() attachment.fileURL = try writeDataToFile(data, id: attachment.id, type: type) attachment.fileType = type.identifier - attachment.attachmentDescription = "" + attachment.attachmentDescription = caption return attachment } From 2eead1f9debbba696afb42b000cd9572ab062558 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 22 Aug 2024 14:17:04 -0400 Subject: [PATCH 31/56] Revert "Fix crash when opening push notification while VC modally presented" This reverts commit 0f2a85b1088cd7d8a27924b37715c465c2a52420. This fixes state restoration happening asynchronously and causing the new tab bar animation to run. --- Tusker/AppDelegate.swift | 7 ++-- Tusker/Scenes/MainSceneDelegate.swift | 10 ++---- ...ountSwitchingContainerViewController.swift | 14 ++++---- Tusker/Screens/Main/Duckable+Root.swift | 4 +-- .../Main/MainSplitViewController.swift | 7 ++-- .../Main/MainTabBarViewController.swift | 3 +- .../Main/TuskerRootViewController.swift | 3 +- Tusker/Shortcuts/AppShortcutItems.swift | 4 +-- .../Shortcuts/NSUserActivity+Extensions.swift | 4 +-- .../UserActivityHandlingContext.swift | 25 ++++--------- Tusker/Shortcuts/UserActivityManager.swift | 36 +++++++++---------- Tusker/Shortcuts/UserActivityType.swift | 2 +- 12 files changed, 49 insertions(+), 70 deletions(-) diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 8810bf13..aa2826fd 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -295,10 +295,9 @@ extension AppDelegate: UNUserNotificationCenterDelegate { // if the scene is already active, then we animate the account switching if necessary delegate.activateAccount(account, animated: scene.activationState == .foregroundActive) - rootViewController.select(route: .notifications, animated: false) { - let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController) - rootViewController.getNavigationController().pushViewController(vc, animated: false) - } + rootViewController.select(route: .notifications, animated: false) + let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController) + rootViewController.getNavigationController().pushViewController(vc, animated: false) } else { let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID) if #available(iOS 17.0, *) { diff --git a/Tusker/Scenes/MainSceneDelegate.swift b/Tusker/Scenes/MainSceneDelegate.swift index 73e4c157..31e5b667 100644 --- a/Tusker/Scenes/MainSceneDelegate.swift +++ b/Tusker/Scenes/MainSceneDelegate.swift @@ -83,9 +83,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate } else { context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!) } - Task(priority: .userInitiated) { - _ = await userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context)) - } + _ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context)) } func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { @@ -193,10 +191,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate if let activity = launchActivity { func doRestoreActivity(context: UserActivityHandlingContext) { - Task(priority: .userInitiated) { - _ = await activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context)) - context.finalize(activity: activity) - } + _ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context)) + context.finalize(activity: activity) } if activity.isStateRestorationActivity { doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!)) diff --git a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift index 45a7d263..32d78808 100644 --- a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift +++ b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift @@ -79,12 +79,10 @@ class AccountSwitchingContainerViewController: UIViewController { stateRestorationLogger.debug("AccountSwitchingContainer: reusing existing VC for \(account.id, privacy: .public)") } else { newRoot = newRootProvider() - Task(priority: .userInitiated) { - stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)") - let context = StateRestorationUserActivityHandlingContext(root: newRoot) - _ = await activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context)) - context.finalize(activity: activity) - } + stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)") + let context = StateRestorationUserActivityHandlingContext(root: newRoot) + _ = activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context)) + context.finalize(activity: activity) } } else { newRoot = newRootProvider() @@ -161,9 +159,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController { root.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion) } - func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { + func select(route: TuskerRoute, animated: Bool) { loadViewIfNeeded() - root.select(route: route, animated: animated, completion: completion) + root.select(route: route, animated: animated) } func getNavigationDelegate() -> TuskerNavigationDelegate? { diff --git a/Tusker/Screens/Main/Duckable+Root.swift b/Tusker/Screens/Main/Duckable+Root.swift index 5ab80ca5..69d36369 100644 --- a/Tusker/Screens/Main/Duckable+Root.swift +++ b/Tusker/Screens/Main/Duckable+Root.swift @@ -35,8 +35,8 @@ extension DuckableContainerViewController: AccountSwitchableViewController { (child as! TuskerRootViewController).getNavigationController() } - func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { - (child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion) + func select(route: TuskerRoute, animated: Bool) { + (child as? TuskerRootViewController)?.select(route: route, animated: animated) } func performSearch(query: String) { diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index 23f62fe9..c78a2c6b 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -546,14 +546,14 @@ extension MainSplitViewController: StateRestorableViewController { } extension MainSplitViewController: TuskerRootViewController { - func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { + func select(route: TuskerRoute, animated: Bool) { guard traitCollection.horizontalSizeClass != .compact else { - tabBarViewController?.select(route: route, animated: animated, completion: completion) + tabBarViewController?.select(route: route, animated: animated) return } guard presentedViewController == nil else { dismiss(animated: animated) { - self.select(route: route, animated: animated, completion: completion) + self.select(route: route, animated: animated) } return } @@ -579,7 +579,6 @@ extension MainSplitViewController: TuskerRootViewController { let oldItem = sidebar.selectedItem sidebar.select(item: item, animated: false) select(newItem: item, oldItem: oldItem) - completion?() } func getNavigationDelegate() -> TuskerNavigationDelegate? { diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 57808a2b..e3473b28 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -148,7 +148,7 @@ extension MainTabBarViewController: UITabBarControllerDelegate { } extension MainTabBarViewController: TuskerRootViewController { - func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { + func select(route: TuskerRoute, animated: Bool) { switch route { case .timelines: select(tab: .timelines, dismissPresented: true) @@ -169,7 +169,6 @@ extension MainTabBarViewController: TuskerRootViewController { nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated) } } - completion?() } func getNavigationDelegate() -> TuskerNavigationDelegate? { diff --git a/Tusker/Screens/Main/TuskerRootViewController.swift b/Tusker/Screens/Main/TuskerRootViewController.swift index d0435430..aa392d95 100644 --- a/Tusker/Screens/Main/TuskerRootViewController.swift +++ b/Tusker/Screens/Main/TuskerRootViewController.swift @@ -12,7 +12,8 @@ import ComposeUI @MainActor protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController { func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) - func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) + func select(route: TuskerRoute, animated: Bool) + func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? func getNavigationDelegate() -> TuskerNavigationDelegate? func getNavigationController() -> NavigationControllerProtocol func performSearch(query: String) diff --git a/Tusker/Shortcuts/AppShortcutItems.swift b/Tusker/Shortcuts/AppShortcutItems.swift index af6b6883..baa5905a 100644 --- a/Tusker/Shortcuts/AppShortcutItems.swift +++ b/Tusker/Shortcuts/AppShortcutItems.swift @@ -44,9 +44,9 @@ enum AppShortcutItem: String, CaseIterable { } switch self { case .showHomeTimeline: - root.select(route: .timelines, animated: false, completion: nil) + root.select(route: .timelines, animated: false) case .showNotifications: - root.select(route: .notifications, animated: false, completion: nil) + root.select(route: .notifications, animated: false) case .composePost: root.compose(editing: nil, animated: false, isDucked: false, completion: nil) } diff --git a/Tusker/Shortcuts/NSUserActivity+Extensions.swift b/Tusker/Shortcuts/NSUserActivity+Extensions.swift index 027211f3..7a460a72 100644 --- a/Tusker/Shortcuts/NSUserActivity+Extensions.swift +++ b/Tusker/Shortcuts/NSUserActivity+Extensions.swift @@ -43,9 +43,9 @@ extension NSUserActivity { } @MainActor - func handleResume(manager: UserActivityManager) async -> Bool { + func handleResume(manager: UserActivityManager) -> Bool { guard let type = UserActivityType(rawValue: activityType) else { return false } - await type.handle(manager)(self) + type.handle(manager)(self) return true } diff --git a/Tusker/Shortcuts/UserActivityHandlingContext.swift b/Tusker/Shortcuts/UserActivityHandlingContext.swift index 312223c7..5ee9f185 100644 --- a/Tusker/Shortcuts/UserActivityHandlingContext.swift +++ b/Tusker/Shortcuts/UserActivityHandlingContext.swift @@ -16,8 +16,7 @@ import ComposeUI protocol UserActivityHandlingContext { var isHandoff: Bool { get } - func select(route: TuskerRoute) async - func select(route: TuskerRoute, completion: (() -> Void)?) + func select(route: TuskerRoute) func present(_ vc: UIViewController) var topViewController: UIViewController? { get } @@ -29,16 +28,6 @@ protocol UserActivityHandlingContext { func finalize(activity: NSUserActivity) } -extension UserActivityHandlingContext { - func select(route: TuskerRoute) async { - await withCheckedContinuation { continuation in - select(route: route) { - continuation.resume() - } - } - } -} - struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext { let isHandoff: Bool let root: TuskerRootViewController @@ -46,8 +35,8 @@ struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext { root.getNavigationDelegate()! } - func select(route: TuskerRoute, completion: (() -> Void)?) { - root.select(route: route, animated: true, completion: completion) + func select(route: TuskerRoute) { + root.select(route: route, animated: true) } func present(_ vc: UIViewController) { @@ -82,11 +71,9 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { var isHandoff: Bool { false } - func select(route: TuskerRoute, completion: (() -> Void)?) { - root.select(route: route, animated: false) { - self.state = .selectedRoute - completion?() - } + func select(route: TuskerRoute) { + root.select(route: route, animated: false) + state = .selectedRoute } var topViewController: UIViewController? { root.getNavigationController().topViewController } diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index 9a018447..18b7fbd1 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -133,8 +133,8 @@ class UserActivityManager { return activity } - func handleCheckNotifications(activity: NSUserActivity) async { - await context.select(route: .notifications) + func handleCheckNotifications(activity: NSUserActivity) { + context.select(route: .notifications) context.popToRoot() if let notificationsPageController = context.topViewController as? NotificationsPageViewController { notificationsPageController.loadViewIfNeeded() @@ -204,22 +204,22 @@ class UserActivityManager { return (timeline, positionInfo) } - func handleShowTimeline(activity: NSUserActivity) async { + func handleShowTimeline(activity: NSUserActivity) { guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return } var timelineVC: TimelineViewController? if let pinned = PinnedTimeline(timeline: timeline), mastodonController.accountPreferences.pinnedTimelines.contains(pinned) { - await context.select(route: .timelines) + context.select(route: .timelines) context.popToRoot() let pageController = context.topViewController as! TimelinesPageViewController pageController.selectTimeline(pinned, animated: false) timelineVC = pageController.currentViewController as? TimelineViewController } else if case .list(let id) = timeline { - await context.select(route: .list(id: id)) + context.select(route: .list(id: id)) timelineVC = context.topViewController as? TimelineViewController } else { - await context.select(route: .explore) + context.select(route: .explore) context.popToRoot() timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController) context.push(timelineVC!) @@ -249,11 +249,11 @@ class UserActivityManager { return activity.userInfo?["mainStatusID"] as? String } - func handleShowConversation(activity: NSUserActivity) async { + func handleShowConversation(activity: NSUserActivity) { guard let mainStatusID = Self.getConversationStatus(from: activity) else { return } - await context.select(route: .timelines) + context.select(route: .timelines) context.push(ConversationViewController(for: mainStatusID, state: .unknown, mastodonController: mastodonController)) } @@ -274,8 +274,8 @@ class UserActivityManager { return activity.userInfo?["query"] as? String } - func handleSearch(activity: NSUserActivity) async { - await context.select(route: .explore) + func handleSearch(activity: NSUserActivity) { + context.select(route: .explore) context.popToRoot() let searchController: UISearchController @@ -311,8 +311,8 @@ class UserActivityManager { return activity } - func handleBookmarks(activity: NSUserActivity) async { - await context.select(route: .bookmarks) + func handleBookmarks(activity: NSUserActivity) { + context.select(route: .bookmarks) } // MARK: - My Profile @@ -325,8 +325,8 @@ class UserActivityManager { return activity } - func handleMyProfile(activity: NSUserActivity) async { - await context.select(route: .myProfile) + func handleMyProfile(activity: NSUserActivity) { + context.select(route: .myProfile) } // MARK: - Show Profile @@ -344,11 +344,11 @@ class UserActivityManager { return activity.userInfo?["profileID"] as? String } - func handleShowProfile(activity: NSUserActivity) async { + func handleShowProfile(activity: NSUserActivity) { guard let accountID = Self.getProfile(from: activity) else { return } - await context.select(route: .timelines) + context.select(route: .timelines) context.push(ProfileViewController(accountID: accountID, mastodonController: mastodonController)) } @@ -361,11 +361,11 @@ class UserActivityManager { return activity } - func handleShowNotification(activity: NSUserActivity) async { + func handleShowNotification(activity: NSUserActivity) { guard let notificationID = activity.userInfo?["notificationID"] as? String else { return } - await context.select(route: .notifications) + context.select(route: .notifications) context.push(NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)) } diff --git a/Tusker/Shortcuts/UserActivityType.swift b/Tusker/Shortcuts/UserActivityType.swift index 090cf8c2..6d7e991f 100644 --- a/Tusker/Shortcuts/UserActivityType.swift +++ b/Tusker/Shortcuts/UserActivityType.swift @@ -23,7 +23,7 @@ enum UserActivityType: String { extension UserActivityType { @MainActor - var handle: (UserActivityManager) -> @MainActor (NSUserActivity) async -> Void { + var handle: (UserActivityManager) -> @MainActor (NSUserActivity) -> Void { switch self { case .mainScene: fatalError("cannot handle main scene activity") From 960ba8468382a39d9a2033c651714da4d7f6e8ac Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 22 Aug 2024 14:32:01 -0400 Subject: [PATCH 32/56] New way of sequencing navigation operations Better fix for #484 --- Tusker/AppDelegate.swift | 14 +++-- ...ountSwitchingContainerViewController.swift | 4 +- Tusker/Screens/Main/Duckable+Root.swift | 4 +- .../Main/MainSplitViewController.swift | 13 ++--- .../Main/MainTabBarViewController.swift | 25 +++++---- .../Main/TuskerRootViewController.swift | 54 ++++++++++++++++++- Tusker/Shortcuts/AppShortcutItems.swift | 4 +- .../UserActivityHandlingContext.swift | 4 +- 8 files changed, 90 insertions(+), 32 deletions(-) diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index aa2826fd..5778348b 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -292,12 +292,16 @@ extension AppDelegate: UNUserNotificationCenterDelegate { let rootViewController = delegate.rootViewController { let mastodonController = MastodonController.getForAccount(account) - // if the scene is already active, then we animate the account switching if necessary - delegate.activateAccount(account, animated: scene.activationState == .foregroundActive) + // if the scene is already active, then we animate things + let animated = scene.activationState == .foregroundActive - rootViewController.select(route: .notifications, animated: false) - let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController) - rootViewController.getNavigationController().pushViewController(vc, animated: false) + delegate.activateAccount(account, animated: animated) + + rootViewController.runNavigation(animated: animated) { navigation in + navigation.select(route: .notifications) + let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController) + navigation.push(viewController: vc) + } } else { let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID) if #available(iOS 17.0, *) { diff --git a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift index 32d78808..afdc0002 100644 --- a/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift +++ b/Tusker/Screens/Main/AccountSwitchingContainerViewController.swift @@ -159,9 +159,9 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController { root.compose(editing: draft, animated: animated, isDucked: isDucked, completion: completion) } - func select(route: TuskerRoute, animated: Bool) { + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { loadViewIfNeeded() - root.select(route: route, animated: animated) + root.select(route: route, animated: animated, completion: completion) } func getNavigationDelegate() -> TuskerNavigationDelegate? { diff --git a/Tusker/Screens/Main/Duckable+Root.swift b/Tusker/Screens/Main/Duckable+Root.swift index 69d36369..5ab80ca5 100644 --- a/Tusker/Screens/Main/Duckable+Root.swift +++ b/Tusker/Screens/Main/Duckable+Root.swift @@ -35,8 +35,8 @@ extension DuckableContainerViewController: AccountSwitchableViewController { (child as! TuskerRootViewController).getNavigationController() } - func select(route: TuskerRoute, animated: Bool) { - (child as? TuskerRootViewController)?.select(route: route, animated: animated) + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { + (child as? TuskerRootViewController)?.select(route: route, animated: animated, completion: completion) } func performSearch(query: String) { diff --git a/Tusker/Screens/Main/MainSplitViewController.swift b/Tusker/Screens/Main/MainSplitViewController.swift index c78a2c6b..687be122 100644 --- a/Tusker/Screens/Main/MainSplitViewController.swift +++ b/Tusker/Screens/Main/MainSplitViewController.swift @@ -333,14 +333,14 @@ extension MainSplitViewController: UISplitViewControllerDelegate { // Transfer the navigation stack, dropping the search VC, to keep anything the user has opened transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true) - tabBarViewController.select(tab: .explore, dismissPresented: false) + tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false) case let .tab(tab): // sidebar items that map 1 <-> 1 can be transferred directly - tabBarViewController.select(tab: tab, dismissPresented: false) + tabBarViewController.select(tab: tab, dismissPresented: false, animated: false) case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_): - tabBarViewController.select(tab: .explore, dismissPresented: false) + tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false) // Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously // in compact mode and performing a search. let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController @@ -546,14 +546,14 @@ extension MainSplitViewController: StateRestorableViewController { } extension MainSplitViewController: TuskerRootViewController { - func select(route: TuskerRoute, animated: Bool) { + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { guard traitCollection.horizontalSizeClass != .compact else { - tabBarViewController?.select(route: route, animated: animated) + tabBarViewController?.select(route: route, animated: animated, completion: completion) return } guard presentedViewController == nil else { dismiss(animated: animated) { - self.select(route: route, animated: animated) + self.select(route: route, animated: animated, completion: completion) } return } @@ -579,6 +579,7 @@ extension MainSplitViewController: TuskerRootViewController { let oldItem = sidebar.selectedItem sidebar.select(item: item, animated: false) select(newItem: item, oldItem: oldItem) + completion?() } func getNavigationDelegate() -> TuskerNavigationDelegate? { diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index e3473b28..6436050d 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -54,19 +54,22 @@ class MainTabBarViewController: BaseMainTabBarViewController { view.backgroundColor = .appBackground } - func select(tab: Tab, dismissPresented: Bool) { + func select(tab: Tab, dismissPresented: Bool, animated: Bool, completion: (() -> Void)? = nil) { if tab == .compose { - compose(editing: nil) + compose(editing: nil, completion: completion) } else { // when switching tabs, dismiss the currently presented VC // otherwise the selected tab changes behind the presented VC if presentedViewController != nil && dismissPresented { - dismiss(animated: true) { + dismiss(animated: animated) { + stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)") self.selectedIndex = tab.rawValue + completion?() } } else { stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)") selectedIndex = tab.rawValue + completion?() } } } @@ -148,21 +151,21 @@ extension MainTabBarViewController: UITabBarControllerDelegate { } extension MainTabBarViewController: TuskerRootViewController { - func select(route: TuskerRoute, animated: Bool) { + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) { switch route { case .timelines: - select(tab: .timelines, dismissPresented: true) + select(tab: .timelines, dismissPresented: true, animated: animated, completion: completion) case .notifications: - select(tab: .notifications, dismissPresented: true) + select(tab: .notifications, dismissPresented: true, animated: animated, completion: completion) case .myProfile: - select(tab: .myProfile, dismissPresented: true) + select(tab: .myProfile, dismissPresented: true, animated: animated, completion: completion) case .explore: - select(tab: .explore, dismissPresented: true) + select(tab: .explore, dismissPresented: true, animated: animated, completion: completion) case .bookmarks: - select(tab: .explore, dismissPresented: true) + select(tab: .explore, dismissPresented: true, animated: animated, completion: completion) getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated) case .list(id: let id): - select(tab: .explore, dismissPresented: true) + select(tab: .explore, dismissPresented: true, animated: animated, completion: completion) if let list = mastodonController.getCachedList(id: id) { let nav = getNavigationController() _ = nav.popToRootViewController(animated: animated) @@ -185,7 +188,7 @@ extension MainTabBarViewController: TuskerRootViewController { return } - select(tab: .explore, dismissPresented: true) + select(tab: .explore, dismissPresented: true, animated: false) exploreNavController.popToRootViewController(animated: false) // setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time diff --git a/Tusker/Screens/Main/TuskerRootViewController.swift b/Tusker/Screens/Main/TuskerRootViewController.swift index aa392d95..e58a1d58 100644 --- a/Tusker/Screens/Main/TuskerRootViewController.swift +++ b/Tusker/Screens/Main/TuskerRootViewController.swift @@ -12,8 +12,7 @@ import ComposeUI @MainActor protocol TuskerRootViewController: UIViewController, StateRestorableViewController, StatusBarTappableViewController { func compose(editing draft: Draft?, animated: Bool, isDucked: Bool, completion: (() -> Void)?) - func select(route: TuskerRoute, animated: Bool) - func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController? + func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) func getNavigationDelegate() -> TuskerNavigationDelegate? func getNavigationController() -> NavigationControllerProtocol func performSearch(query: String) @@ -21,6 +20,14 @@ protocol TuskerRootViewController: UIViewController, StateRestorableViewControll func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController? } +extension TuskerRootViewController { + func runNavigation(animated: Bool, _ builder: (_ navigation: TuskerNavigationSequence) -> Void) { + let sequence = TuskerNavigationSequence(root: self, animated: animated) + builder(sequence) + sequence.run() + } +} + enum TuskerRoute { case timelines case notifications @@ -30,6 +37,49 @@ enum TuskerRoute { case list(id: String) } +/// A class that manages running a sequence of navigation operations on a ``TuskerRootViewController``. +/// +/// Use this type, rather than calling multiple methods on the root VC in a row, because it manages waiting until each previous step finishes. +@MainActor +final class TuskerNavigationSequence { + let root: any TuskerRootViewController + let animated: Bool + private var operations = [() -> Void]() + + init(root: any TuskerRootViewController, animated: Bool) { + self.root = root + self.animated = animated + } + + func select(route: TuskerRoute) { + operations.append { + self.root.select(route: route, animated: self.animated, completion: self.run) + } + } + + func push(viewController: UIViewController) { + operations.append { + let nav = self.root.getNavigationController() + nav.pushViewController(viewController, animated: self.animated) + self.run() + } + } + + func popToRoot() { + operations.append { + let nav = self.root.getNavigationController() + nav.popToRootViewController(animated: self.animated) + self.run() + } + } + + func run() { + if !operations.isEmpty { + operations.removeFirst()() + } + } +} + @MainActor protocol NavigationControllerProtocol: UIViewController { var viewControllers: [UIViewController] { get set } diff --git a/Tusker/Shortcuts/AppShortcutItems.swift b/Tusker/Shortcuts/AppShortcutItems.swift index baa5905a..af6b6883 100644 --- a/Tusker/Shortcuts/AppShortcutItems.swift +++ b/Tusker/Shortcuts/AppShortcutItems.swift @@ -44,9 +44,9 @@ enum AppShortcutItem: String, CaseIterable { } switch self { case .showHomeTimeline: - root.select(route: .timelines, animated: false) + root.select(route: .timelines, animated: false, completion: nil) case .showNotifications: - root.select(route: .notifications, animated: false) + root.select(route: .notifications, animated: false, completion: nil) case .composePost: root.compose(editing: nil, animated: false, isDucked: false, completion: nil) } diff --git a/Tusker/Shortcuts/UserActivityHandlingContext.swift b/Tusker/Shortcuts/UserActivityHandlingContext.swift index 5ee9f185..e4ae550d 100644 --- a/Tusker/Shortcuts/UserActivityHandlingContext.swift +++ b/Tusker/Shortcuts/UserActivityHandlingContext.swift @@ -36,7 +36,7 @@ struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext { } func select(route: TuskerRoute) { - root.select(route: route, animated: true) + root.select(route: route, animated: true, completion: nil) } func present(_ vc: UIViewController) { @@ -72,7 +72,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { var isHandoff: Bool { false } func select(route: TuskerRoute) { - root.select(route: route, animated: false) + root.select(route: route, animated: false, completion: nil) state = .selectedRoute } From cd8f0e79264b8080a036a14dae3af6c2e4661bb7 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 22 Aug 2024 14:49:27 -0400 Subject: [PATCH 33/56] Use navigation sequencing for user activity handling --- .../Main/TuskerRootViewController.swift | 22 ++++- .../UserActivityHandlingContext.swift | 59 ++++++++----- Tusker/Shortcuts/UserActivityManager.swift | 86 +++++++++++-------- 3 files changed, 107 insertions(+), 60 deletions(-) diff --git a/Tusker/Screens/Main/TuskerRootViewController.swift b/Tusker/Screens/Main/TuskerRootViewController.swift index e58a1d58..c779648c 100644 --- a/Tusker/Screens/Main/TuskerRootViewController.swift +++ b/Tusker/Screens/Main/TuskerRootViewController.swift @@ -42,8 +42,8 @@ enum TuskerRoute { /// Use this type, rather than calling multiple methods on the root VC in a row, because it manages waiting until each previous step finishes. @MainActor final class TuskerNavigationSequence { - let root: any TuskerRootViewController - let animated: Bool + private let root: any TuskerRootViewController + private let animated: Bool private var operations = [() -> Void]() init(root: any TuskerRootViewController, animated: Bool) { @@ -73,6 +73,24 @@ final class TuskerNavigationSequence { } } + func present(viewController: UIViewController) { + operations.append { + self.root.present(viewController, animated: self.animated, completion: self.run) + } + } + + func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) { + operations.append { + block(self.root.getNavigationController().topViewController, self.run) + } + } + + func addOperation(_ operation: @escaping (_ completion: @escaping () -> Void) -> Void) { + operations.append { + operation(self.run) + } + } + func run() { if !operations.isEmpty { operations.removeFirst()() diff --git a/Tusker/Shortcuts/UserActivityHandlingContext.swift b/Tusker/Shortcuts/UserActivityHandlingContext.swift index e4ae550d..5903bb8e 100644 --- a/Tusker/Shortcuts/UserActivityHandlingContext.swift +++ b/Tusker/Shortcuts/UserActivityHandlingContext.swift @@ -17,12 +17,11 @@ protocol UserActivityHandlingContext { var isHandoff: Bool { get } func select(route: TuskerRoute) - func present(_ vc: UIViewController) - - var topViewController: UIViewController? { get } func popToRoot() func push(_ vc: UIViewController) - + func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) + + func present(_ vc: UIViewController) func compose(editing draft: Draft) func finalize(activity: NSUserActivity) @@ -30,66 +29,81 @@ protocol UserActivityHandlingContext { struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext { let isHandoff: Bool - let root: TuskerRootViewController - var navigationDelegate: TuskerNavigationDelegate { - root.getNavigationDelegate()! + private let root: TuskerRootViewController + private let navigation: TuskerNavigationSequence + + init(isHandoff: Bool, root: TuskerRootViewController) { + self.isHandoff = isHandoff + self.root = root + self.navigation = TuskerNavigationSequence(root: root, animated: true) } func select(route: TuskerRoute) { - root.select(route: route, animated: true, completion: nil) + navigation.select(route: route) } func present(_ vc: UIViewController) { - navigationDelegate.present(vc, animated: true) + navigation.present(viewController: vc) } - var topViewController: UIViewController? { root.getNavigationController().topViewController } - func popToRoot() { - _ = root.getNavigationController().popToRootViewController(animated: true) + navigation.popToRoot() } func push(_ vc: UIViewController) { - navigationDelegate.show(vc, sender: nil) + navigation.push(viewController: vc) } + func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) { + navigation.withTopViewController(block) + } + func compose(editing draft: Draft) { - navigationDelegate.compose(editing: draft, animated: true, isDucked: true) + navigation.addOperation { completion in + root.compose(editing: draft, animated: true, isDucked: true, completion: completion) + } } func finalize(activity: NSUserActivity) { + navigation.run() } } class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { private var state = State.initial - let root: TuskerRootViewController + private let root: TuskerRootViewController + private let navigation: TuskerNavigationSequence init(root: TuskerRootViewController) { self.root = root + self.navigation = TuskerNavigationSequence(root: root, animated: false) } - var isHandoff: Bool { false } + var isHandoff: Bool { + false + } func select(route: TuskerRoute) { - root.select(route: route, animated: false, completion: nil) + navigation.select(route: route) state = .selectedRoute } - var topViewController: UIViewController? { root.getNavigationController().topViewController } - func popToRoot() { - // unnecessary during state restoration + navigation.popToRoot() } func push(_ vc: UIViewController) { precondition(state >= .selectedRoute) - root.getNavigationController().pushViewController(vc, animated: false) + navigation.push(viewController: vc) state = .pushed } + func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) { + navigation.withTopViewController(block) + } + func present(_ vc: UIViewController) { - root.present(vc, animated: false) + navigation.present(viewController: vc) state = .presented } @@ -107,6 +121,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext { func finalize(activity: NSUserActivity) { precondition(state > .initial) + navigation.run() #if !os(visionOS) if #available(iOS 16.0, *), let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) { diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index 18b7fbd1..350f9774 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -136,9 +136,12 @@ class UserActivityManager { func handleCheckNotifications(activity: NSUserActivity) { context.select(route: .notifications) context.popToRoot() - if let notificationsPageController = context.topViewController as? NotificationsPageViewController { - notificationsPageController.loadViewIfNeeded() - notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode) + context.withTopViewController { topViewController, completion in + if let notificationsPageController = topViewController as? NotificationsPageViewController { + notificationsPageController.loadViewIfNeeded() + notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode) + } + completion() } } @@ -207,29 +210,38 @@ class UserActivityManager { func handleShowTimeline(activity: NSUserActivity) { guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return } - var timelineVC: TimelineViewController? if let pinned = PinnedTimeline(timeline: timeline), mastodonController.accountPreferences.pinnedTimelines.contains(pinned) { context.select(route: .timelines) context.popToRoot() - let pageController = context.topViewController as! TimelinesPageViewController - pageController.selectTimeline(pinned, animated: false) - timelineVC = pageController.currentViewController as? TimelineViewController + context.withTopViewController { topViewController, completion in + let pageController = topViewController as! TimelinesPageViewController + pageController.selectTimeline(pinned, animated: false) + } } else if case .list(let id) = timeline { context.select(route: .list(id: id)) - timelineVC = context.topViewController as? TimelineViewController } else { context.select(route: .explore) context.popToRoot() - timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController) - context.push(timelineVC!) + let timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController) + context.push(timelineVC) } - if let timelineVC, - let positionInfo, + if let positionInfo, context.isHandoff { - Task { - await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID) + context.withTopViewController { topViewController, completion in + let timelineVC: TimelineViewController + if let topViewController = topViewController as? TimelineViewController { + timelineVC = topViewController + } else if let topViewController = topViewController as? TimelinesPageViewController { + timelineVC = topViewController.currentViewController as! TimelineViewController + } else { + return + } + Task { + await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID) + completion() + } } } } @@ -278,28 +290,30 @@ class UserActivityManager { context.select(route: .explore) context.popToRoot() - let searchController: UISearchController - let resultsController: SearchResultsViewController - if let explore = context.topViewController as? ExploreViewController { - explore.loadViewIfNeeded() - explore.searchControllerStatusOnAppearance = true - searchController = explore.searchController - resultsController = explore.resultsController - } else if let inlineTrends = context.topViewController as? InlineTrendsViewController { - inlineTrends.loadViewIfNeeded() - inlineTrends.searchControllerStatusOnAppearance = true - searchController = inlineTrends.searchController - resultsController = inlineTrends.resultsController - } else { - return - } - - if let query = Self.getSearchQuery(from: activity), - !query.isEmpty { - searchController.searchBar.text = query - resultsController.performSearch(query: query) - } else { - searchController.searchBar.becomeFirstResponder() + context.withTopViewController { topViewController, completion in + let searchController: UISearchController + let resultsController: SearchResultsViewController + if let explore = topViewController as? ExploreViewController { + explore.loadViewIfNeeded() + explore.searchControllerStatusOnAppearance = true + searchController = explore.searchController + resultsController = explore.resultsController + } else if let inlineTrends = topViewController as? InlineTrendsViewController { + inlineTrends.loadViewIfNeeded() + inlineTrends.searchControllerStatusOnAppearance = true + searchController = inlineTrends.searchController + resultsController = inlineTrends.resultsController + } else { + return + } + + if let query = Self.getSearchQuery(from: activity), + !query.isEmpty { + searchController.searchBar.text = query + resultsController.performSearch(query: query) + } else { + searchController.searchBar.becomeFirstResponder() + } } } From 3d1f506684bffba106216b5908543f1fffdd4964 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Thu, 22 Aug 2024 14:54:16 -0400 Subject: [PATCH 34/56] Actually show the error message when video loading fails See #531 --- .../Gallery/VideoGalleryContentViewController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift index 2549b224..ae157703 100644 --- a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift +++ b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift @@ -138,9 +138,16 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon label.textColor = .secondaryLabel label.adjustsFontForContentSizeCategory = true + let reason = UILabel() + reason.text = error.localizedDescription + reason.font = .preferredFont(forTextStyle: .subheadline) + reason.textColor = .secondaryLabel + reason.adjustsFontForContentSizeCategory = true + let stackView = UIStackView(arrangedSubviews: [ image, label, + reason, ]) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical From d7be2048af8bc8e36afa5d1fa6d1f7247d4f042d Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Fri, 23 Aug 2024 01:14:28 -0400 Subject: [PATCH 35/56] Whoops Closes #533 Closes #534 --- Tusker/Screens/Main/NewMainTabBarViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 17b19e1e..a0c99a97 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -247,7 +247,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { private func embedInNavigationController(_ vc: UIViewController) -> UIViewController { if UIDevice.current.userInterfaceIdiom == .phone { - return UINavigationController(rootViewController: vc) + return EnhancedNavigationViewController(rootViewController: vc) } else { let nav = AdaptableNavigationController() nav.viewControllers = [vc] From d873b157ee961510b49cfe75419111001d0557bd Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 26 Aug 2024 10:25:28 -0400 Subject: [PATCH 36/56] Fix video gallery controls not auto hiding #535 --- .../GalleryContentViewController.swift | 4 +- .../GalleryDismissAnimationController.swift | 2 +- .../GalleryVC/GalleryDismissInteraction.swift | 2 +- .../GalleryVC/GalleryItemViewController.swift | 18 ++++----- ...lleryPresentationAnimationController.swift | 4 +- .../GalleryVC/GalleryViewController.swift | 2 +- .../ImageGalleryContentViewController.swift | 2 +- .../LoadingGalleryContentViewController.swift | 6 +-- .../VideoGalleryContentViewController.swift | 40 ++++++++++++------- 9 files changed, 45 insertions(+), 35 deletions(-) diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift index a20dfacd..e2bbbfa5 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift @@ -17,7 +17,7 @@ public protocol GalleryContentViewController: UIViewController { var bottomControlsAccessoryViewController: UIViewController? { get } var canAnimateFromSourceView: Bool { get } - func setControlsVisible(_ visible: Bool, animated: Bool) + func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) func galleryContentDidAppear() func galleryContentWillDisappear() } @@ -35,7 +35,7 @@ public extension GalleryContentViewController { true } - func setControlsVisible(_ visible: Bool, animated: Bool) { + func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { } func galleryContentDidAppear() { diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissAnimationController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissAnimationController.swift index 10f5b541..df960cb1 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissAnimationController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissAnimationController.swift @@ -106,7 +106,7 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans content.view.frame = sourceFrameInContainer content.view.layer.opacity = 0 - itemViewController.setControlsVisible(false, animated: false) + itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false) } animator.addCompletion { _ in diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissInteraction.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissInteraction.swift index f49b9621..e7d1ee36 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissInteraction.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryDismissInteraction.swift @@ -42,7 +42,7 @@ class GalleryDismissInteraction: NSObject { origControlsVisible = viewController.currentItemViewController.controlsVisible if origControlsVisible! { - viewController.currentItemViewController.setControlsVisible(false, animated: true) + viewController.currentItemViewController.setControlsVisible(false, animated: true, dueToUserInteraction: false) } case .changed: diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift index 5578e623..36ac26d5 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift @@ -213,7 +213,7 @@ class GalleryItemViewController: UIViewController { updateZoomScale(resetZoom: false) // Ensure the transform is correct if the controls are hidden - setControlsVisible(controlsVisible, animated: false) + setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false) updateTopControlsInsets() } @@ -229,7 +229,7 @@ class GalleryItemViewController: UIViewController { } centerContent() // Ensure the transform is correct if the controls are hidden and their size changed. - setControlsVisible(controlsVisible, animated: false) + setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false) } override func viewDidAppear(_ animated: Bool) { @@ -250,7 +250,7 @@ class GalleryItemViewController: UIViewController { func addContent() { content.loadViewIfNeeded() - content.setControlsVisible(controlsVisible, animated: false) + content.setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false) content.view.translatesAutoresizingMaskIntoConstraints = false if content.parent != self { @@ -290,7 +290,7 @@ class GalleryItemViewController: UIViewController { content.view.layoutIfNeeded() } - func setControlsVisible(_ visible: Bool, animated: Bool) { + func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { controlsVisible = visible guard let topControlsView, @@ -301,7 +301,7 @@ class GalleryItemViewController: UIViewController { func updateControlsViews() { topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height) bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height) - content.setControlsVisible(visible, animated: animated) + content.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction) } if animated { let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters()) @@ -429,7 +429,7 @@ class GalleryItemViewController: UIViewController { scrollView.zoomScale > scrollView.minimumZoomScale { animateZoomOut() } else { - setControlsVisible(!controlsVisible, animated: true) + setControlsVisible(!controlsVisible, animated: true, dueToUserInteraction: true) } } @@ -531,7 +531,7 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer { } func setGalleryControlsVisible(_ visible: Bool, animated: Bool) { - setControlsVisible(visible, animated: animated) + setControlsVisible(visible, animated: animated, dueToUserInteraction: false) } } @@ -546,9 +546,9 @@ extension GalleryItemViewController: UIScrollViewDelegate { func scrollViewDidZoom(_ scrollView: UIScrollView) { if scrollView.zoomScale <= scrollView.minimumZoomScale { - setControlsVisible(true, animated: true) + setControlsVisible(true, animated: true, dueToUserInteraction: true) } else { - setControlsVisible(false, animated: true) + setControlsVisible(false, animated: true, dueToUserInteraction: true) } centerContent() diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift index 1a58ec06..ebd26f72 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryPresentationAnimationController.swift @@ -75,7 +75,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated container.layoutIfNeeded() // This needs to take place after the layout, so that the transform is correct. - itemViewController.setControlsVisible(false, animated: false) + itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false) let duration = self.transitionDuration(using: transitionContext) // rougly equivalent to duration: 0.35, bounce: 0.3 @@ -90,7 +90,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated content.view.frame = destFrameInContainer content.view.layer.opacity = 1 - itemViewController.setControlsVisible(true, animated: false) + itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false) if let sourceToDestTransform { self.sourceView.transform = sourceToDestTransform diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryViewController.swift index e53e90ea..8013c3fa 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryViewController.swift @@ -126,7 +126,7 @@ extension GalleryViewController: UIPageViewControllerDelegate { public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { currentItemViewController.content.galleryContentWillDisappear() let new = pendingViewControllers[0] as! GalleryItemViewController - new.setControlsVisible(currentItemViewController.controlsVisible, animated: false) + new.setControlsVisible(currentItemViewController.controlsVisible, animated: false, dueToUserInteraction: false) } public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { diff --git a/Tusker/Screens/Gallery/ImageGalleryContentViewController.swift b/Tusker/Screens/Gallery/ImageGalleryContentViewController.swift index b728f4c3..c7a5b2e0 100644 --- a/Tusker/Screens/Gallery/ImageGalleryContentViewController.swift +++ b/Tusker/Screens/Gallery/ImageGalleryContentViewController.swift @@ -128,7 +128,7 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon } } - func setControlsVisible(_ visible: Bool, animated: Bool) { + func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { if #available(iOS 16.0, macCatalyst 17.0, *), let analysisInteraction { analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated) diff --git a/Tusker/Screens/Gallery/LoadingGalleryContentViewController.swift b/Tusker/Screens/Gallery/LoadingGalleryContentViewController.swift index 89920637..77199efe 100644 --- a/Tusker/Screens/Gallery/LoadingGalleryContentViewController.swift +++ b/Tusker/Screens/Gallery/LoadingGalleryContentViewController.swift @@ -52,7 +52,7 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC if let wrapped = await provider() { self.wrapped = wrapped wrapped.container = container - wrapped.setControlsVisible(container?.galleryControlsVisible ?? false, animated: false) + wrapped.setControlsVisible(container?.galleryControlsVisible ?? false, animated: false, dueToUserInteraction: false) addChild(wrapped) wrapped.view.translatesAutoresizingMaskIntoConstraints = false @@ -102,8 +102,8 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC ]) } - func setControlsVisible(_ visible: Bool, animated: Bool) { - wrapped?.setControlsVisible(visible, animated: animated) + func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { + wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction) } func galleryContentDidAppear() { diff --git a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift index ae157703..b45af243 100644 --- a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift +++ b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift @@ -86,19 +86,10 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon updateItemObservations() rateObservation = player.observe(\.rate, options: .old, changeHandler: { [unowned self] player, info in - hideControlsWorkItem?.cancel() - if player.rate > 0 && info.oldValue == 0 { - hideControlsWorkItem = DispatchWorkItem { [weak self] in - MainActor.runUnsafely { - guard let self, - let container = self.container, - container.galleryControlsVisible else { - return - } - container.setGalleryControlsVisible(false, animated: true) - } - } - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!) + if player.rate == 0 { + hideControlsWorkItem?.cancel() + } else if player.rate > 0 && info.oldValue == 0 { + scheduleControlsHide() } }) @@ -179,6 +170,20 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon } } + private func scheduleControlsHide() { + hideControlsWorkItem = DispatchWorkItem { [weak self] in + MainActor.runUnsafely { + guard let self, + let container = self.container, + container.galleryControlsVisible else { + return + } + container.setGalleryControlsVisible(false, animated: true) + } + } + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!) + } + // MARK: GalleryContentViewController weak var container: (any GalleryVC.GalleryContentViewControllerContainer)? @@ -206,9 +211,14 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed) #endif - func setControlsVisible(_ visible: Bool, animated: Bool) { + func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { overlayVC.setVisible(visible) - hideControlsWorkItem?.cancel() + + if !visible { + hideControlsWorkItem?.cancel() + } else if dueToUserInteraction { + scheduleControlsHide() + } } func galleryContentDidAppear() { From d1ffab3e42c597464ec2778763256a1a9e7de69c Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 26 Aug 2024 19:08:44 -0400 Subject: [PATCH 37/56] Only hide gallery controls automatically while playing --- Tusker/Screens/Gallery/VideoGalleryContentViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift index b45af243..797c72a3 100644 --- a/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift +++ b/Tusker/Screens/Gallery/VideoGalleryContentViewController.swift @@ -216,7 +216,8 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon if !visible { hideControlsWorkItem?.cancel() - } else if dueToUserInteraction { + } else if dueToUserInteraction, + player.rate > 0 { scheduleControlsHide() } } From b54d34ebfcf7ec7df2b3e5c7a9789ef24619a59d Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 26 Aug 2024 19:16:35 -0400 Subject: [PATCH 38/56] Fix video controls overlay being positioned incorrectly on macOS with Reduce Motion enabled Closes #535 --- .../Sources/GalleryVC/GalleryItemViewController.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift index 36ac26d5..4a0369cb 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift @@ -81,10 +81,10 @@ class GalleryItemViewController: UIViewController { overlayVC.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(overlayVC.view) NSLayoutConstraint.activate([ - overlayVC.view.leadingAnchor.constraint(equalTo: content.view.leadingAnchor), - overlayVC.view.trailingAnchor.constraint(equalTo: content.view.trailingAnchor), - overlayVC.view.topAnchor.constraint(equalTo: content.view.topAnchor), - overlayVC.view.bottomAnchor.constraint(equalTo: content.view.bottomAnchor), + overlayVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + overlayVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + overlayVC.view.topAnchor.constraint(equalTo: view.topAnchor), + overlayVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } From 1bd4d144a39850a9cbaab5a75796bb2b48267925 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 27 Aug 2024 12:42:41 -0400 Subject: [PATCH 39/56] Fix crash on launch if there are somehow duplicate saved hashtags --- Tusker/Screens/Main/NewMainTabBarViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index a0c99a97..44335353 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -285,7 +285,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { var tabs: [UITab] = [] let savedReq = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!) let saved = (try? mastodonController.persistentContainer.viewContext.fetch(savedReq)) ?? [] - for hashtag in saved { + for hashtag in saved where !seenTags.contains(hashtag.name) { seenTags.insert(hashtag.name) tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider)) } @@ -293,6 +293,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { let followedReq = FollowedHashtag.fetchRequest() let followed = (try? mastodonController.persistentContainer.viewContext.fetch(followedReq)) ?? [] for hashtag in followed where !seenTags.contains(hashtag.name) { + seenTags.insert(hashtag.name) tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider)) } From 59fb69525bb6fffecf17a5369ac8ee9ff5e3e146 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 27 Aug 2024 20:41:39 -0400 Subject: [PATCH 40/56] Custom emoji in push notifications, behind a feature flag --- .../NotificationService.swift | 31 ++++++++++++++++++- .../Supporting Types/FeatureFlag.swift | 1 + Tusker.xcodeproj/project.pbxproj | 7 +++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/NotificationExtension/NotificationService.swift b/NotificationExtension/NotificationService.swift index c33f7256..5acec490 100644 --- a/NotificationExtension/NotificationService.swift +++ b/NotificationExtension/NotificationService.swift @@ -15,9 +15,13 @@ import Pachyderm import Intents import HTMLStreamer import WebURL +import UIKit +import TuskerPreferences private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationService") +private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: []) + class NotificationService: UNNotificationServiceExtension { private static let textConverter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLCallbacks.self) @@ -225,8 +229,33 @@ class NotificationService: UNNotificationServiceExtension { } let updatedContent: UNMutableNotificationContent + + let contentProviding: any UNNotificationContentProviding + if #available(iOS 18.0, *), + await Preferences.shared.hasFeatureFlag(.pushNotifCustomEmoji) { + let attributedString = NSMutableAttributedString(string: content.body) + + for match in emojiRegex.matches(in: content.body, range: NSRange(location: 0, length: content.body.utf16.count)).reversed() { + let emojiName = (content.body as NSString).substring(with: match.range(at: 1)) + guard let emoji = notification.status?.emojis.first(where: { $0.shortcode == emojiName }), + let url = URL(emoji.url), + let (data, _) = try? await URLSession.shared.data(from: url), + let image = UIImage(data: data) else { + continue + } + let attachment = NSTextAttachment(image: image) + let attachmentStr = NSAttributedString(attachment: attachment) + attributedString.replaceCharacters(in: match.range, with: attachmentStr) + } + + let attributedCtx = UNNotificationAttributedMessageContext(sendMessageIntent: intent, attributedContent: attributedString) + contentProviding = attributedCtx + } else { + contentProviding = intent + } + do { - let newContent = try content.updating(from: intent) + let newContent = try content.updating(from: contentProviding) if let newMutableContent = newContent.mutableCopy() as? UNMutableNotificationContent { pendingRequest?.0 = newMutableContent updatedContent = newMutableContent diff --git a/Packages/TuskerPreferences/Sources/TuskerPreferences/Supporting Types/FeatureFlag.swift b/Packages/TuskerPreferences/Sources/TuskerPreferences/Supporting Types/FeatureFlag.swift index e8ed874e..e6bfe672 100644 --- a/Packages/TuskerPreferences/Sources/TuskerPreferences/Supporting Types/FeatureFlag.swift +++ b/Packages/TuskerPreferences/Sources/TuskerPreferences/Supporting Types/FeatureFlag.swift @@ -9,4 +9,5 @@ import Foundation public enum FeatureFlag: String, Codable { case iPadBrowserNavigation = "ipad-browser-navigation" + case pushNotifCustomEmoji = "push-notif-custom-emoji" } diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index e65fce05..e9b86e8a 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -77,6 +77,7 @@ D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; }; D6210D762C0B924F009BB569 /* RemoveProfileSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6210D752C0B924F009BB569 /* RemoveProfileSuggestionService.swift */; }; D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621733228F1D5ED004C7DB1 /* ReblogService.swift */; }; + D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */ = {isa = PBXBuildFile; productRef = D62220462C7EA8DF003E43B7 /* TuskerPreferences */; }; D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; }; D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53C2635F5590095BD04 /* StatusPollView.swift */; }; D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; }; @@ -828,6 +829,7 @@ buildActionMask = 2147483647; files = ( D630C4252BC7845800208903 /* WebURL in Frameworks */, + D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */, D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */, D630C3E52BC6313400208903 /* Pachyderm in Frameworks */, D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */, @@ -1795,6 +1797,7 @@ D630C3E42BC6313400208903 /* Pachyderm */, D630C4222BC7842C00208903 /* HTMLStreamer */, D630C4242BC7845800208903 /* WebURL */, + D62220462C7EA8DF003E43B7 /* TuskerPreferences */, ); productName = NotificationExtension; productReference = D630C3D12BC61B6000208903 /* NotificationExtension.appex */; @@ -3299,6 +3302,10 @@ isa = XCSwiftPackageProductDependency; productName = Pachyderm; }; + D62220462C7EA8DF003E43B7 /* TuskerPreferences */ = { + isa = XCSwiftPackageProductDependency; + productName = TuskerPreferences; + }; D630C3C72BC43AFD00208903 /* PushNotifications */ = { isa = XCSwiftPackageProductDependency; productName = PushNotifications; From 59af29ff6413c88cb6282be464a94351d3f9c5a4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 27 Aug 2024 20:42:51 -0400 Subject: [PATCH 41/56] Fix incorrect background color on feature flag prefs section --- Tusker/Screens/Preferences/AdvancedPrefsView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tusker/Screens/Preferences/AdvancedPrefsView.swift b/Tusker/Screens/Preferences/AdvancedPrefsView.swift index 05021cd5..2435cb59 100644 --- a/Tusker/Screens/Preferences/AdvancedPrefsView.swift +++ b/Tusker/Screens/Preferences/AdvancedPrefsView.swift @@ -199,6 +199,7 @@ struct AdvancedPrefsView : View { } header: { Text("Feature Flags") } + .appGroupedListRowBackground() } } From cc696e58fc8e88a739104616aa04c0e9360ce9ac Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 31 Aug 2024 10:49:21 -0400 Subject: [PATCH 42/56] Change how profile header collection view cell is sized Fixes crash due to collection view layout loop in some circumstances Closes #537 --- .../ProfileHeaderCollectionViewCell.swift | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Tusker/Screens/Profile/ProfileHeaderCollectionViewCell.swift b/Tusker/Screens/Profile/ProfileHeaderCollectionViewCell.swift index e74141b6..abf36ddc 100644 --- a/Tusker/Screens/Profile/ProfileHeaderCollectionViewCell.swift +++ b/Tusker/Screens/Profile/ProfileHeaderCollectionViewCell.swift @@ -63,11 +63,18 @@ class ProfileHeaderCollectionViewCell: UICollectionViewCell { } } - // overrides an internal method - // when the super impl is used, preferredLayoutAttributesFitting(_:) isn't called while the view is offscreen (i.e., window == nil) - // and so the collection view imposes a height of 44pts which breaks the layout - @objc func _preferredLayoutAttributesFittingAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { - return preferredLayoutAttributesFitting(attributes) + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + switch state { + case .unloaded: + return super.preferredLayoutAttributesFitting(layoutAttributes) + case .placeholder(let heightConstraint): + layoutAttributes.size.height = heightConstraint.constant + return layoutAttributes + case .view(let profileHeaderView): + let size = profileHeaderView.systemLayoutSizeFitting(layoutAttributes.size, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel) + layoutAttributes.size = size + return layoutAttributes + } } enum State { From 57c023c9731bee5f11e9ac31667e46fb7630d65e Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 31 Aug 2024 11:09:17 -0400 Subject: [PATCH 43/56] Fix profile tab switching animation ending in bad state Caused by fda0c187949d0e7c8d792d616bb90f1bd90c3d10, old/new.view is no longer the same as .collectionView, so the transform wasn't being properly reset. Closes #536 --- Tusker/Screens/Profile/ProfileViewController.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Tusker/Screens/Profile/ProfileViewController.swift b/Tusker/Screens/Profile/ProfileViewController.swift index 7711e66d..70633e57 100644 --- a/Tusker/Screens/Profile/ProfileViewController.swift +++ b/Tusker/Screens/Profile/ProfileViewController.swift @@ -213,7 +213,7 @@ class ProfileViewController: UIViewController, StateRestorableViewController { // old header cell must have the header view let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)! - // Set the outgoing VC's header view mode to placeholder, so that it does steal the header view back + // Set the outgoing VC's header view mode to placeholder, so that it does not steal the header view back // in case it updates the cell in the background. old.headerViewMode = .placeholder(height: oldHeaderCell.bounds.height) @@ -224,12 +224,13 @@ class ProfileViewController: UIViewController, StateRestorableViewController { } // disable user interaction during animation, to avoid any potential weird race conditions - headerView.isUserInteractionEnabled = false + view.isUserInteractionEnabled = false + headerView.layer.zPosition = 100 view.addSubview(headerView) let oldHeaderCellTop = oldHeaderCell.convert(CGPoint.zero, to: view).y let headerTopOffset = oldHeaderCellTop - view.safeAreaInsets.top - let headerBottomOffset = oldHeaderCell.convert(CGPoint(x: 0, y: oldHeaderCell.bounds.maxY), to: view).y// - view.safeAreaInsets.top + let headerBottomOffset = oldHeaderCell.convert(CGPoint(x: 0, y: oldHeaderCell.bounds.maxY), to: view).y NSLayoutConstraint.activate([ headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: headerTopOffset), headerView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: headerBottomOffset), @@ -272,16 +273,17 @@ class ProfileViewController: UIViewController, StateRestorableViewController { } animator.addCompletion { _ in old.removeViewAndController() - old.collectionView.transform = .identity + old.view.transform = .identity - new.collectionView.transform = .identity + new.view.transform = .identity new.collectionView.contentOffset = origOldContentOffset // reenable scroll indicators after the switching animation is done old.collectionView.showsVerticalScrollIndicator = true new.collectionView.showsVerticalScrollIndicator = true - headerView.isUserInteractionEnabled = true + self.view.isUserInteractionEnabled = true + headerView.transform = .identity headerView.layer.zPosition = 0 // move the header view into the new page controller's cell From 3e28c012d7350718006d00c8453fff1a4a748174 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 31 Aug 2024 11:10:59 -0400 Subject: [PATCH 44/56] Shhh --- Tusker/Screens/Timeline/TimelineViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tusker/Screens/Timeline/TimelineViewController.swift b/Tusker/Screens/Timeline/TimelineViewController.swift index df2f0893..663a4c49 100644 --- a/Tusker/Screens/Timeline/TimelineViewController.swift +++ b/Tusker/Screens/Timeline/TimelineViewController.swift @@ -1295,7 +1295,7 @@ extension TimelineViewController { // if we didn't add any items, that implies the gap was removed, and we want to to make clear what's happening if !addedItems { - var config = ToastConfiguration(title: "There's nothing in between!") + var config = ToastConfiguration(title: "That's all, folks!") config.dismissAutomaticallyAfter = 2 showToast(configuration: config, animated: true) } From 68bd9e0bed6704663ad1144f17f814a7f56feb56 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 31 Aug 2024 11:20:09 -0400 Subject: [PATCH 45/56] Tweak mute button symbol animation --- Tusker/Screens/Gallery/VideoControlsViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tusker/Screens/Gallery/VideoControlsViewController.swift b/Tusker/Screens/Gallery/VideoControlsViewController.swift index 7b2160d5..d2c582e6 100644 --- a/Tusker/Screens/Gallery/VideoControlsViewController.swift +++ b/Tusker/Screens/Gallery/VideoControlsViewController.swift @@ -408,7 +408,7 @@ private class MuteButton: UIControl { let image = UIImage(systemName: muted ? "speaker.slash.fill" : "speaker.wave.3.fill")! if animated, #available(iOS 17.0, *) { - imageView.setSymbolImage(image, contentTransition: .replace.wholeSymbol, options: .speed(5)) + imageView.setSymbolImage(image, contentTransition: .replace.byLayer) } else { imageView.image = image } From 40230c54786004e85e6bd8b91b6e6f959562bb81 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 31 Aug 2024 11:37:39 -0400 Subject: [PATCH 46/56] Add dark mode app icons, optimize pngs --- .../AppIcon.appiconset/1024x1024-dark@1x.png | Bin 0 -> 23187 bytes .../AppIcon.appiconset/1024x1024@1x.png | Bin 125788 -> 52180 bytes .../AppIcon.appiconset/20x20-dark@2x.png | Bin 0 -> 723 bytes .../AppIcon.appiconset/20x20-dark@3x.png | Bin 0 -> 1063 bytes .../AppIcon.appiconset/20x20@1x.png | Bin 580 -> 0 bytes .../AppIcon.appiconset/20x20@2x-1.png | Bin 1328 -> 0 bytes .../AppIcon.appiconset/20x20@2x.png | Bin 1328 -> 968 bytes .../AppIcon.appiconset/20x20@3x.png | Bin 2154 -> 1606 bytes .../AppIcon.appiconset/29x29-dark@2x.png | Bin 0 -> 1022 bytes .../AppIcon.appiconset/29x29-dark@3x.png | Bin 0 -> 1494 bytes .../AppIcon.appiconset/29x29@1x.png | Bin 917 -> 0 bytes .../AppIcon.appiconset/29x29@2x-1.png | Bin 2078 -> 0 bytes .../AppIcon.appiconset/29x29@2x.png | Bin 2078 -> 1552 bytes .../AppIcon.appiconset/29x29@3x.png | Bin 3430 -> 2609 bytes .../AppIcon.appiconset/38x38-dark@2x.png | Bin 0 -> 1342 bytes .../AppIcon.appiconset/38x38-dark@3x.png | Bin 0 -> 1971 bytes .../AppIcon.appiconset/38x38@2x.png | Bin 0 -> 2212 bytes .../AppIcon.appiconset/38x38@3x.png | Bin 0 -> 3657 bytes .../AppIcon.appiconset/40x40-dark@2x.png | Bin 0 -> 1402 bytes .../AppIcon.appiconset/40x40-dark@3x.png | Bin 0 -> 2038 bytes .../AppIcon.appiconset/40x40@1x.png | Bin 1328 -> 0 bytes .../AppIcon.appiconset/40x40@2x-1.png | Bin 3117 -> 0 bytes .../AppIcon.appiconset/40x40@2x.png | Bin 3117 -> 2368 bytes .../AppIcon.appiconset/40x40@3x 1.png | Bin 0 -> 3911 bytes .../AppIcon.appiconset/40x40@3x.png | Bin 5010 -> 3911 bytes .../AppIcon.appiconset/60x60-dark@2x.png | Bin 0 -> 2038 bytes .../AppIcon.appiconset/60x60-dark@3x.png | Bin 0 -> 3207 bytes .../AppIcon.appiconset/60x60@2x.png | Bin 5010 -> 0 bytes .../AppIcon.appiconset/60x60@3x.png | Bin 8371 -> 6251 bytes .../AppIcon.appiconset/64x64-dark@2x.png | Bin 0 -> 2216 bytes .../AppIcon.appiconset/64x64-dark@3x.png | Bin 0 -> 3310 bytes .../AppIcon.appiconset/64x64@2x.png | Bin 0 -> 4225 bytes .../AppIcon.appiconset/64x64@3x.png | Bin 0 -> 6596 bytes .../AppIcon.appiconset/68x68-dark@2x.png | Bin 0 -> 2351 bytes .../AppIcon.appiconset/68x68@2x.png | Bin 0 -> 4444 bytes .../AppIcon.appiconset/76x76-dark@2x.png | Bin 0 -> 2752 bytes .../AppIcon.appiconset/76x76@2x.png | Bin 6791 -> 5016 bytes .../AppIcon.appiconset/83.5x83.5-dark@2x.png | Bin 0 -> 2949 bytes .../AppIcon.appiconset/83.5x83.5@2x.png | Bin 7480 -> 5589 bytes .../AppIcon.appiconset/Contents.json | 494 ++++++++++++++++-- 40 files changed, 448 insertions(+), 46 deletions(-) create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/1024x1024-dark@1x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/20x20-dark@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/20x20-dark@3x.png delete mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/20x20@1x.png delete mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/20x20@2x-1.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/29x29-dark@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/29x29-dark@3x.png delete mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/29x29@1x.png delete mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/29x29@2x-1.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/38x38-dark@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/38x38-dark@3x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/38x38@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/38x38@3x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/40x40-dark@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/40x40-dark@3x.png delete mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/40x40@1x.png delete mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/40x40@2x-1.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/40x40@3x 1.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/60x60-dark@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/60x60-dark@3x.png delete mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/60x60@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/64x64-dark@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/64x64-dark@3x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/64x64@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/64x64@3x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/68x68-dark@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/68x68@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/76x76-dark@2x.png create mode 100644 Tusker/Assets.xcassets/AppIcon.appiconset/83.5x83.5-dark@2x.png diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/1024x1024-dark@1x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/1024x1024-dark@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..eb755315717747fc97f9c553f70b3e9676d19304 GIT binary patch literal 23187 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 UC>5X6lDP{lO)s02|Lz(0A6oqgF8}}l literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/1024x1024@1x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/1024x1024@1x.png index adf6a10e79bdf0a46be1e52ed2e3f10b870a2540..2366e8ac798b293fb199f39f331b95ab1e672cdf 100644 GIT binary patch literal 52180 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 literal 125788 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�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 F{~wH|V{rfg diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/20x20-dark@2x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/20x20-dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..58bd8159cc4b903a71171bef207f28366cf569c5 GIT binary patch literal 723 zcmV;^0xbQBP) zQ4fN6D4tZ%i>RPIwNez?WZvnA{X*+W6s&6ob4b>L#u`Ime=vt6ggklonb~EbP$(1% zg;o!5G`^8l8fxAo2?lZtJ^yE*1c^p8d@v6@}E|adIlQll^a!`03(+@I@NY;*Lk^(eUxWXTwVK z!xfk!zgZkaPQ_&?Sm_{wGT$+XhtORoQ#1=Qh+fj>Wiuk;1#3A-4Y`>7i%Rq=1sX3@& zIlVf%Pa3#|1YZLa{bqZtjMFE_xJbWm+uLg*+dbV}mH61QO=X}#l88y-X(T>~1{U&N ze`9OJN?|7^37!{+vEKcd(WH4f{o9;C^6|i-qT; z)j1lkmL_^eW??{SF=8jVY$zi>*!s8{uG`HQ_v7o>N_oDwR%s_`H$+iT4X@>R>=;`{ zDho9@6|L*lPIH1F;+ojeq~-@bzj-|?mjp!`QHxiBseG3j9@(PjcJgLcf*un+js$m8 zbL}D?98xqrzMoG>QndPTYgF-C^ z(bm9-+hw_Rc8zsylElyDC%RPX=8z;7rG`FRA69OdarUE*UQk(n4+(~lU?vcHNfDn_ z8b<8Vl9=E!SBF|wNuBfYYJ2|KWv3KAtcF6NP$;xI{Qy|tGyo9UO6dRq002ovPDHLk FV1l5QOg;br literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/20x20-dark@3x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/20x20-dark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..66fdd1295bce6f7c6b3bbd4684af59cc24003016 GIT binary patch literal 1063 zcmV+?1laqDP)zCU?$kBC99}7EFj%QnUA**=^d$LPZoU zDn=@T4-rHPK8gBLrG6KowNEXf&E7e)yNNZaU{WhFZC!U#HZ<&B(==mw)L9M(wb$+a!*W~lbWHg!A4l#g9iSp6$n{= zEXsPh{l<{x-k8vCMht`_^14O}{YG4v&<2}DS$4E}IkNmHHZ;Rv+O$3@7Lgu?+e22* ziTP?SHD5+XepWk(nxMRTpwc$Y3X$b_t+txsZ!*C^YOHPb{~ng1ovb?MI$D&(C1Sp| z6xXdl1HZ(CE@T**W*(MnS^J+;oaH!Lcq=muO`^`W4y%=D;8n8IpW*4y%4ZfdMaas- zwL+kQsK?4XDs8jo*(-=)8yWRmd~i5bu5UsEn|mX;UYw`yT(C+T=t2vh6VI$SQ^<0w zSg3TahU49k}T}h7LHfi#Z31JWY`@>upL=GojLMYlOBYtMMZg9 zA}4RnW-q2t%y8myQ7`T!6&f%xEAkkzta?~a0d7Nv9pxI0 z4PITRk>QUsrH-FN`w23zGo6z+i+bxW1gnr?U(9GtVg!BeoLwC}c`cd5^Tg^avM}Y^ zFR&)3KMR(@_;Yot*tQxOUPJ?*5W_)YHD?DP!+5#ACx-r% zg^r9#+#(i}J89{>e1$Ab#vD%mqzK;JRBT%@_ovcwLpp*7Q${@*nxMVVW z$jFC89k}~9F>EK6zmk=oiVGpMZTF*vZ|C?P|8ZMN$ias?4_(Oe;VtJl`tIzGxz_c> z63NIzmD^e6u6|*-ebdS(km1L9thiH};h1JMq#5?78NQxg6eHM8EPv1pd&xq+df%15 zlaWt}r9#_a9T_!nW{-wa9qXiy z0000>0i1Vw#Q*>R8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10ntfB zK~y-6?bE$$Q(+Xx@$d88C->(4hNOv7Qc+L~3JMNUDL6=N5wzkU2!f6ZT|{sfQNcgJ zq3+J&;3gCoT?9o?f)ra@kZ6S_ZOwh@z4tgs;|nc9$)Mnw&-o0$!%tnGn-Kaqx}Vs8 zu%DnnO@_!OMXV@ALyeJ5+^vIA5Obk)jmgSwT))PN@;N3ex2Q^eYoG{B&p*bs%bXs# z#MGO+Xbe}XH)$}AlFgHmL?|US3Q`8Z8o4@rLujB_)PrFGuWg_Sgq`z4i>Wpw^p`L_+sVz^URzMt7Z|*?Gx>g?p&b zVx%(0fl7nz`8o;(z{*;indP^<=se|J{0gUN0Dge$sJ9fdPY!c__%cQtrWYUay#0jb zU``C3n)-pVP!Q=$^!N4>BivCwpN(5^{2Jo{3~7lo563*X{jxn SbLqSQ0000NS&-rrh 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 delta 2152 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 literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/29x29-dark@3x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/29x29-dark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..cf0b249c2c5e7564356099fe50eee1997651d574 GIT binary patch literal 1494 zcmV;{1u6Q8P)gZ=V;^1=&T=X=5jd0nfpI;_F8-2 zYm`z-DW#NBN-3q3Qc5YMlu}A5rIb=isiQG+MSKN$n3!<9&loMQK%Y~cF9yY}{!topd|3U({b>VKIcr6L8X*DR0zu8;h!cXgQ zTAqL%<0>lSW2UPjiJ#3Al=abA6J!C8J~nt^=x8C1r( zt%k(VXzQ@h{Kga>CV_^W(?m_lW6^YC!^&8)9)3*(CYrV~q!zCxfx6a1irGow`ep%h z?^~S5?{Wkj#&#<>J@FMcm`)`0=fv>(-ePS9CYiS6eAJyhtj<%YMlFRaO=e%4Mjply zLu0=GV%T6kP9C~4+^CL4-1}lHIAh=RhxOIV`dww(nk^R|aEzOB1iYxFR?F4nJx<`K za8ACEvC#_5ptMj7^SEgz;aa6KqLpZJ2y@@x1pkna9x}&D;&E`eNbBLi0$bbX!WQT7 zG#W)dUJ=gAKVUI2OmKqtm}9^;^gJu!%1Z0uMG{zYaKVl@I*Et0L;s79li=poTQry# zHh)YDMlLU2Mhvf6!NwyC8FyF-MFrnX7#~d-t}{ods;1&}$8e|RXb^Gq5;4pr#&w5Y zwqxxu!9u%MP7>UjXNq{DuHQB05Qa;|tt_(x0z$`2HcRL2UGn>~Xp6Jr( z4%10wU4Ruxb9Dd=vyzqla?O*;O1RE3jwZpco#4i{4-`T-vVa)IRWo$UZhU8y`1l6$ z@MF$5)!+n^BWbTfVYC8|kQ63ZkF&{xSi!A%gUrtLxQqlprxeB~DGaa;JufuJiR_Uk z4^wjPDKt5W=O+Rc|7XwH*7vzL3o3LlDOktRX!0;7ytWdRLSO29W%qEdIS~-UK;ov%dL5V(8YPn^e9~tY*C5batUYWS>qj`20mC-bw;< zofMMxe3YqN+21n$wrA0>ZP+Tf(HsY|)tD8Sx_4EVcUTW=N$}%@xX(kf}@DxE$gHC#LaKG^SEe)^u5F!uWT7&+C3j{Z?m4& z!%h-hPlA6E$7S-cBHOwQ$9%r)IL3L|cIk-U`mNbx~>L^5`=C zG9W``SZ!t7i44=SqR6n&2`(lH?Gr2sBYVyGJ$d}rGJKYGC?209kIr#4$T8fh-&a#g wDW#NBN-3q3Qc5YMlu}A5rIb=iDb*JK1&^Z6y4hI6!~g&Q07*qoM6N<$f>izBbpQYW literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/29x29@1x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/29x29@1x.png deleted file mode 100644 index 92da03337f594aa3c70464d3757535322c23b00f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 917 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 IM6N<$g2X57=l}o! diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/29x29@2x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/29x29@2x.png index 68a6e1bead5f118a8c75d2059be3e79b3f5e31cd..3debd78e9bd91dd15dcab2b45eea1df355cb6f9e 100644 GIT binary patch delta 1546 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 AqyPW_ literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/38x38-dark@3x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/38x38-dark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ba067a6d8b7ba939c62aec64296984a002ea418 GIT binary patch literal 1971 zcmaJ?`#;l-AJxSrCB5=UHjzvSC5$ev4>gLB+%~3CBV;o%Vkp+3NTuMsg zOGPtcK1_V&e%UOy+9z4rp85x#^Lm|gUg!1x<^9t+cU_SP1$l_Pl$4Z$ql3NMZx8=N zIho&@^*JR@N=i1y(cacQdURq|4`3`Ef>G?Yq3#KxKlPzRz2;Zt~{;jW;8C#tTiodQ;}@`=5(fhoYVZ?%68Y`3l#1 zr2{?!Vm4c9tT1Ar2= zEIgf_`Bj{lT22t?TgszdxSc679d+W*lk@{sk-4ncof@J#0;fy)xhDl7FaS8xvH-^g zd(+f%&d4d2z1_q&;axP-*;XKQ0xh~4#Ibj6)Xt+4E;s`VV45LNK9{I!9ROXsoye`X zYHb=6@`PGkPD&%UyM1*$XO4=BIdVnxaX!}E+NFJZXfw5O7hwPD@<8i((%f8dK@ae; zhcly{qz8Z{gK2-sqI1qucHhoQcoM*ZaAUqvlS&f z-inlTsHPVlf)NQy?H2o%TV5?!<2^%M3lLBfT5K!^#0Kku;Y&-ZIr`wLrKLBXw_b^Y z?dS3@vEa@b7IV>T^aim&HaO6mfv*?yzt~U?z8G|);@++$oU479un~b|l-xvo5k55N zQZ!#*8f3kEt2G2-sszTN6rW>Yai1v`=5!#HKR(}DEOKaG z*V#N8&p=~m?e-@|PxPodO}wU?I6tN_STDQ z+O@d6`u@MvmnbsBjII;7{Bfp!uxrIz%-shl7CLC-e&+i~5;sQujFs6hoXO=}K;t6A zI#B1eU-@Wq&sojrhiIxMt8wAeR9a{9_!G~L$wAQqgbF=xeS?P2h9!(2XrS57RB}i* zb5s3SvpycM5B!zCh%ai@%wQ6?ywA!2{Y=!DmtJgp zbi_H@p(5!~NRV!ucd@0ifBI@ILqV| zFPqmB*B}v@Q~7gZ4`cGV*6T&%hc_C7ql^tJtXf;Y(7cGDdKb||OoJLGn5kY+Pj2GK z-HCNDOAb=U`{w0UzT?bMW)SSAnq5F&K~+UOm9s%f2FbK%ur(!pH5--AqXK=O^g|zl zbr5DSeyD%VG`;j?<>8SlaX1PPSd)dLz;UWu*Dn*ySe(2^&pd~(Nle)-R_pRj{JGKqwAii`uBaaO(+ELr_FiKt2o{Wtm5nL`yjCc&4pQiZc^!G=e;x`^0)C>+$0DQ*Q%1=B1H zS>-op@#ugOwG0LZSO`#30sp_}{(Dbk$B&Eqy&O+F5T6B0f9Qj2KJG<+AzEZ_@JKJX zs>>$<_DCy<`{c(g|Z!p`CX1iEA?7a*%PH(uE&HwlM6p5Teb1 zm$VK*yC2ARo6Nj0neE$a07P;D^|ukxC{YGo5k*^}JT4^jko2VUVPA~Ct#uk?(P5Kk z^x1rCZ^nw^)iLAV32;@!n&~>FekwjIS82uQsQ7~1*ico(g`(PNi9FiYEo?<&nKaZC w=0qUgdoYh#V6DaR!yNo7|7+6z+$b!`mLX)ft;Xn4pFi304AQ>#@5_n*0;yc(yZ`_I literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/38x38@2x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/38x38@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2bd5d75a7d3bce7fd0253cf94f2e0bc1d65cccb1 GIT binary patch literal 2212 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 bV#TT#yIw`7*u)uo00000NkvXXu0mjfgC;`0 literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/40x40-dark@2x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/40x40-dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..446031195959da2752fe366879a4e01c0a6097a9 GIT binary patch literal 1402 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 literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/40x40@1x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/40x40@1x.png deleted file mode 100644 index f371b08b4b9fb975279fb5497a3380e89bd9c997..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1328 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 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/40x40@2x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/40x40@2x.png index 772f73bdacc742df744ff28cb61dc7ee67018c16..b0985b15f9583adb63d9db2d128c94bedff1331c 100644 GIT binary patch literal 2368 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 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/40x40@3x 1.png b/Tusker/Assets.xcassets/AppIcon.appiconset/40x40@3x 1.png new file mode 100644 index 0000000000000000000000000000000000000000..ab34ae95ce057118a41f17f8b90c6f9d56dd5cbc GIT binary patch literal 3911 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 literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/60x60-dark@3x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/60x60-dark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a13c22797b4dbe0ea869210b4a85bf11f1bc7e11 GIT binary patch literal 3207 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 C-z&lZ literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/60x60@2x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/60x60@2x.png deleted file mode 100644 index b266c20e6445d70f4362c49ec667872bf1eff73b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5010 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 E1t>JvjsO4v literal 8371 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 Uu9&;YKmAtb%7 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/64x64-dark@2x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/64x64-dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..08816920a4d1866c2034c5cc58e8fe2876be8557 GIT binary patch literal 2216 zcmb7G`6Cky10I@k=V-Fo)Yl~Ut2s7TgoGp#k}L8q5#`z(b4>L5G2_mdKUn2 zD8$Cn+!;H%JdS(dapeer&)HHjcf_X(TPdG22c{B5o`6a}bBz{P&Ym>4Nvn5zI69P% zVUE3V_S8}JmD7lmldf);u?4FgN)=Ei)W(_H5igb2?{XUvW48ky%H335???4tsN!f% zMDm8wQIH#UO z_QMu#YsF^9hF<~usQGW#7F(FSiljS#<1}XqkC+Y=P91a|#t=`z1-d zRTuedP{Itx8<<5!;|Zku4k(2FDViq1Kru++%gulWBXp}~bn|LL6&%@@-{&eWUBgaH zc^A?eF&tFFcfs6)CskhcaVSnpfO8jHuul-r?ZRR~Kd%*^vlG;S-f$Yp5pAhrgtci^ z!-if@ks5>p}$+5)Frfbmz!j3=<09P47Yt zEFNg|wNsNeLKv>6pHQ-ES%vT*RUU=+$ZMT+KYNb@Pa?@~eDUI$cIipec~3dwJFquw z@TKY%-e{rcmiZ<&3DQ>8LdMi&xD4HYY-}#Le*qUeh$MBzX3f9nsFi8#WJByp469Kh z>idrg#hCyL2i*uNAaaf?A;|S>jj_X{l^A;Yff3ZXj)ojoc_%s}+Qkg79tw)s=@-`y zwRp^%T|?6KqJw2?lLRN5YK(sfF?RQ?qafMFe@`>L&qd2kc$qNFF&%z699rf6O`Dl%5%5k(R94sB+GG|zJdcn}jaN?%#}0hz3q;a3ni_ftS*7uxVq~SPB)0>5ZlvosUIf0WYpZj=8yRh`w-c)0 z;YY9jraX8((ZuvI!e4PGUB<)ObkBe0d1KwK7OuBLSHHRLl%p%S=DzVb6E78(;3*Ny zRH=By5gdM*Z9mv9;>H{_*~OlCG}|kVIai4NDJrs87I$26_KOt^MrxmE$|2ycVu}wj{L&<#8GE4EE#-l#KMK}VubOmG8ay<&@PnT zrpBveJbD*-wk1qZESPEFn6}^J@5TnV`9IDT_IQfP-z$70HDSurSwZPeH;|zLdgMKE zv-B^$usnyUL=}qgYX4;>tLpeFrN#8=tpYw}_fCkg>oB-wqGzp$=a=|i|E|1oE=E3+ zD}M^80mpY$w1|;_NyaVb@qw(vnrbOs%sY$%?&OD5IX-tB^PEw%{9K7Ssry4h>qS1b z#7IZ_LVb{};K)&f9rL^|Ic!_#H`Zzm=PK`vgCkELhS}|P#KwjSDufW+>4P4w$67OK!XG_cv#Tbg!+E+h!oVxe-PWRE1 z+kTsr>;prY41(ct=mOe^CJ>oP69&2U2;}A5=Vga8k5vyA2o%^Ja51?0kz;S%4f$%M ziW7DC+_YSIoHj9cO5#+;fzKl-IBdP z>CWFAfckz;EJ`0n@>ZL;r{jixm({a?jSr4Fj@Q2116v&C$%wJz7QO}v4Z;N=WdNZ?RwMH ziT|0qwxBetJlv>I?4)7Q4O_^vd>fi@uw$0_bh7>#5P{yqB%O*{`a6+fU8b+y6n_{W z>~vhJvf-~>tQ$E*_k`4$>UMiWN2AmAOLRel%L>7EXREA;Q*|`@{dmdc@@Y^`?7CRj zjTB;_z|MGrsg<)5IK6;hb9st5)6KViTUPZ%2fGc83EFSizcdB{q56>%JO>yk5aUVs zwKBk;Gk4<~8MdLq#EYMF`p5vOM}44!X|~n!2P0ZfNl6PqMMPXh}ZngZ~%R CJ3~kS literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/64x64-dark@3x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/64x64-dark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5c23d5bc91cf526907982d75d4ca89cd5e216daa GIT binary patch literal 3310 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 Gfc+mOrQ+@Y literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/64x64@3x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/64x64@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a504cff97fc075a606756b60163a88a387dc9c12 GIT binary patch literal 6596 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 literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/68x68-dark@2x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/68x68-dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..160250db0c7a2cc92287bf01e6ce05a58793b6dd GIT binary patch literal 2351 zcmb7G`!^E`9Pj3misa33DWp8}SdTR;uPtiLn~<^+7M7TegfJAFSGdT=mdC1D8PYsD z*IQv8o4WG2nHrmzLW2R(cblS;%9|d8KHcl?Wz~ND1sMwOqEz;aht?9 z*D67C>zy(wjr@d>v;w-f0
Yc6Y0XAYL{36(Q?Gds81)@EsSQ072) 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 O!qHOGSFKUDkNzK+iz6NY diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/83.5x83.5-dark@2x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/83.5x83.5-dark@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1b3843ec1b95c0c3b509fff07e37fbc7b9f7919b GIT binary patch literal 2949 zcmbVOXIByo1GV(Y6`6@UwOpAqQ?qjBz|;h_lp;r|C9W)O2slD>;6_Cpj~lmG&K%HG z($orP5}@V6REWZbPx|)!g7>`dJ?Gy0<(_lD+z%Jy3U&a9DToOO2ml-}+PVMn+J7l> z;E&#yxwI-EAb!cw?!uKQ+Uk@=2+$2k;kp5v+7C;bDJr%+lWH2pu>UqIP6VE8%Db;{ zk3HSRP=(hf5;Hnr3*tgTl9gH_`dg>FmnbP(hW5I;URUM&Y(5A0)`@`-+_;(+divF&`}erV*S6MImstC|m!8}F3OJp_zOndNf*rHX{EV5dfjvASufFqH z><9y1>XAEgPkerayb}H4gN7Qtoq{l#GMFhm6{B)%oAxd(c^W*jeJo2;-!-pPX1P8< z7D0vM+Qult*T^p{QAvP_f+Ccm2fC898ak}ZSWi-(hn~BBJQ4KuC=41rTFmF%ZXE>} zB`1}g>)rXX=!1{+_%;$lJ@zZKA&FESt}U(geyl|Q6e4?P;nKnrUT^a)=(OhuJZ~X& zVHp*u(#abogX$SP$zP`-Bi}%@gN}yJ}dp^>C?%Jo96?A=lwD9c$9PPO%P;9 z-LS!)gUOd8a+(6lC`zGzw7oC8XEJzryWS3Uk}pfdg2=gA-PJb{T#o8@&C=p2o~FnGSC(hW_VObaGQPhlX2>Mt-g4%Qj@Yx%ivzdn^ z9$AmGWDJo`jid{zBjH+50xM{{N#UK{z9H~(RfzC8uvdOE4Pm;Vo$ix8F!SVFQj*nh zFt0#C3DFqm&2#B4Zy`bEierSG+H=E{Rt0-B2q1|NLIUP5q-%QF4^`j*XZSwvwu+k> zv81co^9J+AMM7D%7)<>qts*LCw+L^YkCc_3i>?2$Kl}n32)R|mh-#55t9TI~>l17p ze8CxqKfzNkJNo|e*)f>GL&HWccU;w@s#J9}I;9;1)tV`%d;$AHh72HS#3h^pPHKY1 z`SdCzCuIyeU#-s{J=T>XK36s{7+W*1gG*jNP6kzs@G!Cna=aK zVn>|kM4uOk&9T+}M53=YOvjI&D(GlivoVC}V>c(92`?gaHXFIxCOKT>nYHgVgTXw1 z6C^0zNAX*rl|CAZoLb$|eVR7?;7(EP5x8=)3SiN~+(R%-uFvse1jus;Y4XVlv%oR= znb}aWV0$E}Ms5$5UWPyGrI@`DGLH%|TRdCCTBuLtHmh3(%-Z@*$A2x|41UUQx}0Hp z0ysarH1;ZP@>Lq0ZqWG)<-8=YM=tTagSCF-IPX9Px~2Ol^=r1+>h^YE_?fJgB2f8@ z%q=OaoGFay)OMF*3vFMVF1}#q3QP~Qxd3VleDZTa`LEj48Z#8kvUCTSKUDf&+-jGr7s_I)pGCw~7O%;oa5H(>8m1S)mZR?SGWDW^`X{gLzZ&keUik?1iLStJw*lxQj?J<>N zHHl#&E~!uf3^nsfM_~y07xFN{$7Xa-vL1nnP)6aMwKByL_^Wc~&dLwFL z`0BUot+x8rrnfQ}v%p#df%pU_p->z=1fKd2UspF*WlB}_(h{Yc<+_C1b#u;Mn+c=SNpU_N{#UjlE zpQ2w(>aM37>Gitrw?O5~H%3-G0~S^XBYJio`)9|T(*zkHDxNB?F^A7sE-7-~w3RRz z!Ec3FeX@*?PRcP^H;bP6su3qIrbSd;kB)V%zfeepz-KR8u9MuXuC^^)8AKSn!z2_ld zes^O15%rL)#dt&}tKo--&yW@<1`ac;*7J|VhU#$t4wXmj2; zCGy&mtuz2`?TmZbfQ2?tn=xva&R3#KmqmE?b+6=w)fD@?j7(u3a+_yrwT6;O?`G1v zxh83IK+{Q+p9c+nnkhK075w;$^iaMu&Xd9jQ>7PQ8G@;gMz2?<3^0yt!Tr;)hxp~t zO;d6Ie*9yX^7!0q7divtk=b8 z9MwLNYZ|?kpJo^QCgy#=4u!dI+FNcD6_;IW-k)5ED9gre@_)un?PgsP+#5rs2K$R4 zN$u<0lF1Y7Pk UfN=sP@BR%t+Jo(yZEnQ>2U#SiE&u=k literal 0 HcmV?d00001 diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/83.5x83.5@2x.png b/Tusker/Assets.xcassets/AppIcon.appiconset/83.5x83.5@2x.png index 3dbad7c19d26e8487b50062c86eeb7fbc53cb5d8..d3eff17384578917f353994e45299e59ddd49a02 100644 GIT binary patch literal 5589 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 literal 7480 zcmdT}^;Z<$)Al1GEiI)Y2{Ok)c7h{hJqL&*VuLnZ8cRBIX5=^`9Lqi{U6oXTkF8{_~LKYhd}2=lApI755)=kwIyzy1ES zKhI0;nE{=;cd7F(eBsh6Ea25OOfpyDZEw_<@3J*yhN>#&V=ne&5lLF{`nAxs?wpND zrk5OrELFC`{9!W4Vg=CuLzk*AZdlX7&An@@&9#!Q8456H=5NK~sSS$55qyU527VI) z0lDEyc`MxRKnG-QxMZ_h;Ue-%i-4rF-Q>IaA-3Uo1wkUBI45=}nKk@F_NQ1FvzoGU zK80el+Tv$HSM5b58y}G`zzK70d<(GTgAddF2UvP>K0>}fFRn8hL z2d#o10!JT1m6;5Cxt%@JB=Zn~3n%%1cO-(_I_%3)v&bU`S8?@tl7-Um63B}KPQ>;lEbZRw%A4{@1e{{HEA%qIEZrV%>vkfieh3qO|< zp0ObfV1A<#dWcs{q97Z_do99J+pwS0YDI+u#hDYXPBvMPuo*Lqlpb$P8P_%K%HCWn z4X3fLyc8-wTk!HdrE1CYAtubv-BuaAo)NY%S2P+=b6BaW-%{#{q#vvmYFSHf`F(Bl z9zWwSWt>j$d2+N%){I6i)%+Z~Ish`{;IP=y;CU-HGCyKAr@P+eHpI-TlA{lY-anHe zm1lAcvtW-lyc|PozJK#6tOI?6QN19x>lc*mIS;`d*XR$*n=WRy(kZ`@$uFagCcgbn z6`5>1-0;@w1nUoWtGB+%I-&JwrgXjJCUb_)!i?W?$Dyycz#XHV$ZP3=tM;%?Cgfa` zyM$ol+Mviy<}!HqKJ)d@K>6V5AoQy;m0|tK1mj7!cZGSq`U@#s!x;-NVz`nE!$>8iL8LH1#S9Wv}L|+7? z7h+z>J2jFR{^$G(LB$Ku`?P4n-dgU9FlZWY!((iJ0s8~=~@SAzp$kD`DON5O`KxfD8%C8i|(e-Lh;l%ds z!@D!=3`2AybXnE(&5m~_*5Zrt1=x9i5up>rKT(oFYmEDeA>EBv!Cv1h1d4J8UH1oJ zxyI^R(ztbZavM4b{#0JX5Oo0&Mcp&Lu7!Kzl7#%_dKLin3vCd;@$Ny1d~o$69yxF6 z#%c;xkABIANFlyWZhdf9k)YYf)qoZ+9G3ZT!vCt4#-Qsv!f7Y;XFj5;dl36sv6`|h zT!mYzH7a%E3vKWmiznLo4s&x}IQ+cMfi?2k$N#i1%>9D&lc6MW6DC}jOqAZCQeu7F zLjx`f&9t6oBZDqzag#;3o~cskh>dEMO6@qRzVtCnifs66l^eTsCsSo(77I&UqhjXP zlQM{^a|7Mo7xkd(2)M8bM+}dt+r)&ffhBp!M;mwITmqX^mx4Rr|74~SuJ*f3jvgEf z^PX~ACju@Yvr$w_y1(!D#ZyteDE8Pq3Xbh4D@T`W@Az14felCg>jvgCrJ6;wPw?*U zbvUh#)|7laqzjQBX?|2y7QeQJATCD8UyJ-Rr%o))6PWhk(RT%0xUzDF6C858 zQ$R#cx}A?q@|)@$qM@-Af=*)NSq3+IaeBPud~>WGp7b!Gk~;o#4Stj5+PkK7g58D> z-+!#G?<+tt*w^Wv@v1YrZlEW~l7b4%lZ9lF6r?Jzh4LQIheKPt|5#Dg(xq77-+g(F zQH*hOltvGCkv23#Oet%-$m2@QgoO9~ctH&0iS2Wy_-f^*xb`{a1uLEk*^mRE;l zPD5}tyg%CMG-qZE;p_~#RYTJ)22xH#1PsT$<}HOt`{Q1Y2#uVc*VEVJUcAj964~Ku z#&}_3W4?H8zW*(h|b(qOT6`6|}9lDAK~N1W=6wwv;U_X6GZoRMEjL6be~y@h4eemRF-ksC|qs=m$4 z3G_m5k@pw^Dt&*IxUw@Ml#O=R@|;P%YHj->W}AmgOGj^XmF@3_!S8j67K4Pc8ynj@ zEq~vnrEsw9s`@q3wXZe8; zY9OQ}rCV9EHyjRM$lOFta^SZ)i_4|J5;`OPbiM`lQ6nOc=dSwr)(L&)JuRrMW%@3P zE9`74H-I8%SlTvPktJh^%m;x;%Vb+^A-7v2E9!_V}@m-R3K zu1kQG@TR(0UhPYAf6{x940cZQjM`9U4hoiC;Tj;r>%~A=FL;#ell_-w3u)@hOTtT` zki5YIV3A-F6jAVX$02feq!a&AOB9rJ{pNXkOf~P4+K@{c9>40x$1!>nHLUIgG-GI8 zV58u_vt@PsSxNL}nY3-{PU`yoA<(-de~_=e>AIbUR~FSCzfg$tpB2aFG9zLVW~Att z9$vI2G)=fiV1C=K-El3n6Y}TCVLp@_K8QW+?GnORbPPtlghGV+c1r2BlBv#X59Vuu zyNdHjQzZ94gkSBJVgE+;AZ<<0$nFrCiXKVatYsfn4f_(}u2)KQ>`Lr#FCgi9ew?^% zZQXnAR?S=Pj^Pi5M{E5XWWppJo}CNP8ds9@OAm!p7ru2RgpIyEExcV?tl$C#nl+g}4=ChFp6EI%xZ8Lg08k)lebz5zFgyY^`_mjVU4V{y zL-quzSd(i8w)c-^;9h@GkBvKV^d9JOgSOeYUBwn}J{8~8Od7s^Q&J+3)Y@*It~uq# ze`szpf!cR{F`9Uk!PsKW45i&jI*p*NN&dWb|I2|~Hl#LAwV_5Gj`S{?K^ z-n#+X?X+~?+$uVD$rtJ=A*}gc0t_7frlp6ha|bqo$>tsMhIfT z_*KE_vealMJQS&TxmxX>?&RzgVRbV4{Kfn7gU?QjQmz(ic0Y7YzkZEd%6bqqMz(}& zFmq`=x1Rr7xwv<7iyNt(oJ!ad|jS%kH^G4jy`?a`jON>oa73N4`SyrF5 z_+zp#KfZN2;{5p>=0>5)FqsrYxVw^{t1hs(#ItIijC-R zlC5D#L#AXn)2J#{oO$JgWzHYfkCZ)w2zhBSeNPby(JSI}H|-=W z{NG^E*>rx^Ny!;LK8-Y%#1|#L3?WpRzHswB0oa2%O(R)yXWRVcR&2B&n&)1N1`l5w{LjK$EM!20=>A^NYZx=aw93T}?9tMU0zxH$?2Rrs3f#Np4(7k+pix z!1`sMK5(#dc`!B>xjsqe4Uo$eRs8tTs+epF_?DyO$;ZUh-oH7+C5374?W26`wW9cR z8#EvjkyiPFt*u>p5?iRTrPG_AOYcC8=GU6dYbuQ7({%3u!Ox;HN%Ms6LGNr2#Qoil zCB_$;8cw^ELSr~vJCT^ISl;T6M#TXhNGoLj)Gshmn|geG3`%DJ_r z&zE5GWRoq=i3%R@OD;`VaA_Eg@um?mCCoL6EVbiiL8rq4O}X&^pdL}-67|k(OKS@! zmkH+DcBIJ@oavDEPr$YiFOrhP4Yqq$bA3!}=hdgc(9|f}Eux1ubGuc7ZZN0W8w&b) z1k`Yg2oB&-nXGM5Jphh4TjkJ$76m9obfSPH1he@IFP*~%NYm8{LesR4z?b0Sv01CP z-CO5|8MO`PI*Y&l_4+^WcBDw!qT%foJ;8mx5n}L(`lYPAZhU{vz3n0XBfl@7Z_8P3 z0;*ogXi4=ISc;WpFJ`?<@^y2Evu?LE%gcj55?Do zLidCw*D*KoEqttpPVkt81>%~9E$X2mvGnm_rtAcB`%N;NJ~bD!djfQ-Z*ogN0|;xv zNSnC%FaaL21Ey?>&x(rSUi%yct@QXPwWen9d$OO_xcLCE=({X(h!JCV3~{^qcJ6pz zd5fQ1mMwpy8>cPI!&d(8M~_{*Xg)itL(K8g#c09*-}#&THdx(iJsOy|=Ip);lA5q- z67mfp-jp!S^lm6Tp&FCYOiRMGq_i#q%u92dv2O%ZV(Ub+r7|HTxx;CrJ0ccLmdAd8 zVVFhejJdU0FmbQRhRVr%?0tE~bZA)*SpmXIc8QqX@iNyqDaK?m{=9+tLDl1Q0K zRuPC1J0O9QPdp-KWI3Zndj42@Ywg?3aChiUQI&jo{|Paa$UM?m`|kusoSumuhms*v&Hr`uoq} znj9_h$(>lh(C|U1K)G-YK`xX!AWG01g@l3Y!M252QSWq^*a4z1iPp~pRA086b|lv! zY)vc8exv7&-`o4zR_x`KHf0qwzbJFNC{TqZXwLp5)T-%H21WC-!p)X#X7=z1Tp=$B+Q+xB0ZL2e~>Y96Y4W>-b5(M$q2lAY9Qr1cOm;aqu9e| z&&O7oUn}cN9w+L9pUykh-0%dmIG)l|{)bA&w^@&9aPNJ0mP`9ukUK-Q1GNJ5Mb>+6P)?+3 zSwcXT-Z9m!F%wo1&|&?6^(bK}B2w6zA!kzg zHx=va^(d#n5Oo=|cYv-$4TjvY$p9nm)XzDLvHz|xwf7h5_NLqw{4B=X^%A|^?Gj9u z+NX5ytd4mq;81n8BDUzQps%)#5nwMnC*!AbrE>!WSYYRo`yu`lF;h6}aUWZCiEjtIP;(a>j`z*Vbs!%WrLuXX6O-YLWw9-e-tyA? zxaO#s0ZK)GJSBY>__}BKh}_Q9ck|&wTnMp(%yu(48+efRPQ;tJ*e9VMuc{wYmrTwW zl!M!L(P=`!Q2;OQm-K5)KdP7?uEBZ;%=OLeMXQ{dXNE)s~-k zA*^A+RovFcUi(`!AnPkxA;Vs$P{xuQ_OWeRhNLgay{kp8f(xadn)&wBHQ+P3J)%5T zCkA0Z-E`pGm^|&yrdU`ahgbC)_ZymTNe7Ud`n<5m!ay_bf4k1bf3x>n&Fo-&)zJwC zaF$-uc6BxrSMUr+E|H3;Gr9BzvjTGHF*$tzvN3Gu!KTpMC}J0*zA95IX@yWd@B_J1 zp-DSzjF)DC`l&h^fbv}>&#-Oq3mbnof0vY%j&f^9>*Tj6C^JchbRE5?B`1xI zqa!8N`>WRf@4+4U<&COX&q{`xc_QYpu*~rES=$t2;RxH>=v6Q)^^c@U+>Ta`KCjv% zyMIUDOedygkQ)79UOo(77bb#Ggi6RFGyCnS75v`eYdL1LWTb9-tw@uVqi>Z?v>TP7 zHa4euzwS^Nvb1c1|A3VmH|99Nk;G@YV=Y@)OZ1f29-9{Tljk2K$>@Z8OC3+4cKwvD_>#R&xlmT7N%azOH)M^ z>!u9}H1A&T*v`=?JddfVo7T#qo(An{mD2(DJEzd_$`ewf2Df9)5CvouaH6c>KyU+4NOX`=$F11e9*Tm4dWB zWI$DIgK9NTo%yFkM?Ih-A~96~%i{JWv|pA9TD~feyf|AqKEedlwC9*owgmAvh!B^s zSrle2+JE7E?7d$(E9Z5qsbacG+L%;~W+2(CB2*s)a#>lihbxxh3tPZ@`GqNU@o5%e zYsVgW#c?7nD}E}Qm_y%1WSVo$z>b!+EPXK%zMrSBzNQ2g8+Anp&wPGv(Mz3IcA|sm zB&_}2Svix-*OXZq!giKPTSEN*-W)|g1dh05F(_*=7Q~WK7ibtZdMI&rIt9l8Y4>(@ zit{=QK6K_&)t@s(ZQ2y*q~#8U&P_CDWt9K;d^D>z#d0JL0q40A`txRQb6JV4P$K__U+?}XNip|@T~(qAi# z1Ih5Ma1$ti*(3#>xHi>BZIz78q8ka}C3&mgRv5QfICEfZ!93d)bG_)^JGV)S?fro%@?C)-Mg&)&LP8D`@nD;tgUcj3lu{;|BuzyNOjdv*Mk zj6w*te^T@ZngJut&Vzq`G+lK&;lGPk$Rby%1+Qe`^5$Rd&;^p3A6`m)uzFT`8x`p6 zw2X>Q&#UfXE<{?26+ZP$ z#$n1rLpYO?-aMb8$t9MDQ^{`fJRej}91e|rXYAj(?&V=E40&x&8O z2Rsmr&x~v*X`3<=N20*wbz*R|n3Lw13rx0Uj~KOeZOHcdM2_Ms)-*3Vk}tPR-YUnw zW$47D1uvK|cjFWLi9{LO0c}KQ5(c|2&XP{l|0+8%^NJ;&J37fVw`{wMh-8B$+4+$T q(}!lX6X*hADx?2z_iY;wSZ}esjKa#K7azBtPZVTSrD2k$LH`GN;Jo1g diff --git a/Tusker/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tusker/Assets.xcassets/AppIcon.appiconset/Contents.json index 6624667a..36a8e7aa 100644 --- a/Tusker/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Tusker/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -2,109 +2,511 @@ "images" : [ { "filename" : "20x20@2x.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "20x20" }, { "filename" : "20x20@3x.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "20x20" }, { "filename" : "29x29@2x.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "29x29" }, { "filename" : "29x29@3x.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "29x29" }, + { + "filename" : "38x38@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "38x38" + }, + { + "filename" : "38x38@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "38x38" + }, { "filename" : "40x40@2x.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "40x40" }, { "filename" : "40x40@3x.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "40x40" }, { - "filename" : "60x60@2x.png", - "idiom" : "iphone", + "filename" : "40x40@3x 1.png", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "60x60" }, { "filename" : "60x60@3x.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "60x60" }, { - "filename" : "20x20@1x.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "filename" : "20x20@2x-1.png", - "idiom" : "ipad", + "filename" : "64x64@2x.png", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", - "size" : "20x20" + "size" : "64x64" }, { - "filename" : "29x29@1x.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" + "filename" : "64x64@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "64x64" }, { - "filename" : "29x29@2x-1.png", - "idiom" : "ipad", + "filename" : "68x68@2x.png", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "40x40@1x.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "filename" : "40x40@2x-1.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" + "size" : "68x68" }, { "filename" : "76x76@2x.png", - "idiom" : "ipad", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "76x76" }, { "filename" : "83.5x83.5@2x.png", - "idiom" : "ipad", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "83.5x83.5" }, { "filename" : "1024x1024@1x.png", - "idiom" : "ios-marketing", - "scale" : "1x", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "20x20-dark@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "20x20" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "20x20-dark@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "20x20" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "29x29-dark@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "29x29" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "29x29-dark@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "29x29" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "38x38-dark@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "38x38" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "38x38-dark@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "38x38" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "40x40-dark@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "40x40" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "40x40-dark@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "40x40" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "60x60-dark@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "60x60" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "60x60-dark@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "60x60" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "64x64-dark@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "64x64" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "64x64-dark@3x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "64x64" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "68x68-dark@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "68x68" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "76x76-dark@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "76x76" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "83.5x83.5-dark@2x.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "1024x1024-dark@1x.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "20x20" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "20x20" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "29x29" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "29x29" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "38x38" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "38x38" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "40x40" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "40x40" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "60x60" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "60x60" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "64x64" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "64x64" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "68x68" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "76x76" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], From b7166771cf775087f0a72721296b3e1cbe4ed452 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 31 Aug 2024 11:42:48 -0400 Subject: [PATCH 47/56] Include SVGs in repo --- Artwork/Tusker no shadow.svg | 157 ++++++++++++++++++++++++++++++++ Artwork/Tusker transparent.svg | 153 +++++++++++++++++++++++++++++++ Artwork/Tusker.svg | 162 +++++++++++++++++++++++++++++++++ 3 files changed, 472 insertions(+) create mode 100644 Artwork/Tusker no shadow.svg create mode 100644 Artwork/Tusker transparent.svg create mode 100644 Artwork/Tusker.svg diff --git a/Artwork/Tusker no shadow.svg b/Artwork/Tusker no shadow.svg new file mode 100644 index 00000000..3bca8d5f --- /dev/null +++ b/Artwork/Tusker no shadow.svg @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/Artwork/Tusker transparent.svg b/Artwork/Tusker transparent.svg new file mode 100644 index 00000000..bf092516 --- /dev/null +++ b/Artwork/Tusker transparent.svg @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/Artwork/Tusker.svg b/Artwork/Tusker.svg new file mode 100644 index 00000000..dd4c3f12 --- /dev/null +++ b/Artwork/Tusker.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + From 3f4917931b83b970073e0c65e66b82c688c847c5 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 9 Sep 2024 19:13:57 -0400 Subject: [PATCH 48/56] Poll own_votes is a nullable array of nullable ints, at least on pleroma I do not understand why Closes #540 --- Packages/Pachyderm/Sources/Pachyderm/Model/Poll.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Packages/Pachyderm/Sources/Pachyderm/Model/Poll.swift b/Packages/Pachyderm/Sources/Pachyderm/Model/Poll.swift index d1c7a663..29d46bd5 100644 --- a/Packages/Pachyderm/Sources/Pachyderm/Model/Poll.swift +++ b/Packages/Pachyderm/Sources/Pachyderm/Model/Poll.swift @@ -16,7 +16,7 @@ public struct Poll: Codable, Sendable { public let votesCount: Int public let votersCount: Int? public let voted: Bool? - public let ownVotes: [Int]? + public let ownVotes: [Int?]? public let options: [Option] public let emojis: [Emoji] @@ -33,7 +33,7 @@ public struct Poll: Codable, Sendable { self.votesCount = try container.decode(Int.self, forKey: .votesCount) self.votersCount = try container.decodeIfPresent(Int.self, forKey: .votersCount) self.voted = try container.decodeIfPresent(Bool.self, forKey: .voted) - self.ownVotes = try container.decodeIfPresent([Int].self, forKey: .ownVotes) + self.ownVotes = try container.decodeIfPresent([Int?].self, forKey: .ownVotes) self.options = try container.decode([Poll.Option].self, forKey: .options) self.emojis = try container.decodeIfPresent([Emoji].self, forKey: .emojis) ?? [] } From f9c0506590eb75584e9ba27eaeecf5f36f02e68b Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 9 Sep 2024 19:18:12 -0400 Subject: [PATCH 49/56] Add tab-switching shortcuts to new tab bar Closes #541 --- .../Main/NewMainTabBarViewController.swift | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Tusker/Screens/Main/NewMainTabBarViewController.swift b/Tusker/Screens/Main/NewMainTabBarViewController.swift index 44335353..3b7c32a0 100644 --- a/Tusker/Screens/Main/NewMainTabBarViewController.swift +++ b/Tusker/Screens/Main/NewMainTabBarViewController.swift @@ -314,10 +314,6 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { } } - @objc func handleComposeKeyCommand() { - compose(editing: nil) - } - @objc private func sidebarTapped() { #if !os(visionOS) fastAccountSwitcher?.hide() @@ -408,6 +404,33 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController { return true } #endif + + // MARK: Keyboard shortcuts + + @objc func handleSidebarCommandTimelines() { + selectedTab = homeTab + } + + @objc func handleSidebarCommandNotifications() { + selectedTab = notificationsTab + } + + @objc func handleSidebarCommandExplore() { + selectedTab = exploreTab + } + + @objc func handleSidebarCommandBookmarks() { + selectedTab = bookmarksTab + } + + @objc func handleSidebarCommandMyProfile() { + selectedTab = myProfileTab + } + + @objc func handleComposeKeyCommand() { + compose(editing: nil) + } + } @available(iOS 18.0, *) From 506d2ad8a96ba9e00c312b294bcb21fa8f42a3f6 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 9 Sep 2024 19:35:15 -0400 Subject: [PATCH 50/56] Actually fix multi-column nav scrolling animations this time (hopefully) Closes #539 --- .../Screens/Utilities/MultiColumnNavigationController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tusker/Screens/Utilities/MultiColumnNavigationController.swift b/Tusker/Screens/Utilities/MultiColumnNavigationController.swift index 7292a553..2be2af35 100644 --- a/Tusker/Screens/Utilities/MultiColumnNavigationController.swift +++ b/Tusker/Screens/Utilities/MultiColumnNavigationController.swift @@ -152,10 +152,10 @@ class MultiColumnNavigationController: UIViewController { let column = stackView.arrangedSubviews[columnIndex] let columnFrame = column.convert(column.bounds, to: scrollView) let offset: CGFloat - if columnFrame.maxX < scrollView.bounds.width - scrollView.adjustedTrailingContentInset { + if columnFrame.maxX <= view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right - scrollView.adjustedTrailingContentInset { offset = -scrollView.adjustedLeadingContentInset } else { - offset = scrollView.contentSize.width - scrollView.bounds.width + scrollView.adjustedTrailingContentInset + offset = columnFrame.maxX - scrollView.bounds.width + scrollView.adjustedTrailingContentInset } scrollView.setContentOffset(CGPoint(x: offset, y: -scrollView.adjustedContentInset.top), animated: animated) } From 263210ac3c156a965a8cb1cddf882445e9a227be Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 9 Sep 2024 19:39:30 -0400 Subject: [PATCH 51/56] Fix gallery controls insets on iPhone 16 And change the default to the dynamic island metrics, so I hopefully don't have to touch this every year --- .../GalleryVC/GalleryItemViewController.swift | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift index 4a0369cb..b41f7397 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift @@ -378,9 +378,6 @@ class GalleryItemViewController: UIViewController { 47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus 50, // iPhone 12 mini, 13 mini ] - let islandDeviceTopInsets: [CGFloat] = [ - 59, // iPhone 14 Pro, 14 Pro Max, 15 Pro, 15 Pro Max - ] if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) { // the notch width is not the same for the iPhones 13, // but what we actually want is the same offset from the edges @@ -390,16 +387,18 @@ class GalleryItemViewController: UIViewController { let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2 shareButtonLeadingConstraint.constant = offset closeButtonTrailingConstraint.constant = offset - } else if islandDeviceTopInsets.contains(view.safeAreaInsets.top) { - shareButtonLeadingConstraint.constant = 24 - shareButtonTopConstraint.constant = 24 - closeButtonTrailingConstraint.constant = 24 - closeButtonTopConstraint.constant = 24 - } else { + } else if view.safeAreaInsets.top == 0 { + // square corner devices shareButtonLeadingConstraint.constant = 8 shareButtonTopConstraint.constant = 8 closeButtonTrailingConstraint.constant = 8 closeButtonTopConstraint.constant = 8 + } else { + // dynamic island devices + shareButtonLeadingConstraint.constant = 24 + shareButtonTopConstraint.constant = 24 + closeButtonTrailingConstraint.constant = 24 + closeButtonTopConstraint.constant = 24 } } From 522e7830e5d9dfb7fcf10842b5586df7ad1d2af8 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 9 Sep 2024 19:42:55 -0400 Subject: [PATCH 52/56] Fix scroll-to-top not working in in-app Safari Closes #538 --- Tusker/Screens/Main/BaseMainTabBarViewController.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Tusker/Screens/Main/BaseMainTabBarViewController.swift b/Tusker/Screens/Main/BaseMainTabBarViewController.swift index 990ef8c0..3019d6a4 100644 --- a/Tusker/Screens/Main/BaseMainTabBarViewController.swift +++ b/Tusker/Screens/Main/BaseMainTabBarViewController.swift @@ -184,10 +184,8 @@ extension BaseMainTabBarViewController: BackgroundableViewController { extension BaseMainTabBarViewController: StatusBarTappableViewController { func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult { - guard presentedViewController == nil else { - return .stop - } - guard let vc = selectedViewController as? StatusBarTappableViewController else { + guard presentedViewController == nil, + let vc = selectedViewController as? StatusBarTappableViewController else { return .continue } return vc.handleStatusBarTapped(xPosition: xPosition) From 93e72e1cb617f15d2b7cc2a928472b2433485993 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 9 Sep 2024 23:53:37 -0400 Subject: [PATCH 53/56] Fix add saved hashtag search results selection not being cleared --- .../Explore/AddSavedHashtagViewController.swift | 10 ++++++++-- Tusker/Screens/Explore/ExploreViewController.swift | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Tusker/Screens/Explore/AddSavedHashtagViewController.swift b/Tusker/Screens/Explore/AddSavedHashtagViewController.swift index 1eb7134a..a0bcee2f 100644 --- a/Tusker/Screens/Explore/AddSavedHashtagViewController.swift +++ b/Tusker/Screens/Explore/AddSavedHashtagViewController.swift @@ -9,14 +9,14 @@ import UIKit import Pachyderm -class AddSavedHashtagViewController: UIViewController { +class AddSavedHashtagViewController: UIViewController, CollectionViewController { weak var mastodonController: MastodonController! var resultsController: SearchResultsViewController! var searchController: UISearchController! - private var collectionView: UICollectionView! + private(set) var collectionView: UICollectionView! private var dataSource: UICollectionViewDiffableDataSource! init(mastodonController: MastodonController) { @@ -91,6 +91,12 @@ class AddSavedHashtagViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + if searchController.isActive { + resultsController.clearSelectionOnAppear(animated: animated) + } + + clearSelectionOnAppear(animated: animated) + let request = Client.getTrendingHashtags(limit: 10) mastodonController.run(request) { (response) in var snapshot = NSDiffableDataSourceSnapshot() diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index b9ee9a09..44f76d95 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -96,7 +96,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect // so we manually propagate this down to the results controller // so that it can deselect on appear if searchController.isActive { - resultsController.viewWillAppear(animated) + resultsController.clearSelectionOnAppear(animated: animated) } clearSelectionOnAppear(animated: animated) From 3a3af77907dfb49b8bfe251b256e1e94b4565d4d Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 10 Sep 2024 10:18:08 -0400 Subject: [PATCH 54/56] Fix swipe action completion handler not being called --- Tusker/Screens/Explore/ExploreViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tusker/Screens/Explore/ExploreViewController.swift b/Tusker/Screens/Explore/ExploreViewController.swift index 44f76d95..1534baa7 100644 --- a/Tusker/Screens/Explore/ExploreViewController.swift +++ b/Tusker/Screens/Explore/ExploreViewController.swift @@ -308,6 +308,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect actions.append(UIContextualAction(style: .destructive, title: "Unsave", handler: { _, _, completion in context.delete(existing) try! context.save() + completion(true) })) } if mastodonController.instanceFeatures.canFollowHashtags, From 814f64b3e2e06acec26e35fef474f557c686d8f0 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Tue, 10 Sep 2024 10:20:27 -0400 Subject: [PATCH 55/56] Simplify add saved hashtag toolbar buttons Closes #522 --- .../AddSavedHashtagViewController.swift | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/Tusker/Screens/Explore/AddSavedHashtagViewController.swift b/Tusker/Screens/Explore/AddSavedHashtagViewController.swift index a0bcee2f..5439e63d 100644 --- a/Tusker/Screens/Explore/AddSavedHashtagViewController.swift +++ b/Tusker/Screens/Explore/AddSavedHashtagViewController.swift @@ -114,7 +114,63 @@ class AddSavedHashtagViewController: UIViewController, CollectionViewController } private func selectHashtag(_ hashtag: Hashtag) { - show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil) + let vc = HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController) + vc.loadViewIfNeeded() + + let mastodonController = mastodonController! + let context = mastodonController.persistentContainer.viewContext + let existingSaved = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)).first + let saveItem = UIBarButtonItem() + func updateSaveItem(saved: Bool) { + saveItem.title = saved ? "Unsave Hashag" : "Save Hashtag" + saveItem.image = UIImage(systemName: saved ? "minus" : "plus") + } + saveItem.primaryAction = UIAction(handler: { [unowned self] _ in + // re-fetch this in case the button's been tapped before and the captured var would be out of date + let existingSaved = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)).first + if let existingSaved { + context.delete(existingSaved) + } else { + _ = SavedHashtag(hashtag: hashtag, account: mastodonController.accountInfo!, context: context) + } + mastodonController.persistentContainer.save(context: context) + updateSaveItem(saved: existingSaved == nil) + if existingSaved == nil { + self.presentingViewController?.dismiss(animated: true) + } + }) + // setting primaryAction replace's the bar button's title/image with the action, so do this after + updateSaveItem(saved: existingSaved != nil) + + vc.navigationItem.rightBarButtonItems = [ + saveItem, + ] + + if mastodonController.instanceFeatures.canFollowHashtags { + let existingFollowed = mastodonController.followedHashtags.first(where: { $0.name.lowercased() == hashtag.name }) + let followItem = UIBarButtonItem() + func updateFollowItem(followed: Bool) { + followItem.title = followed ? "Unfollow Hashtag" : "Follow Hashtag" + followItem.image = UIImage(systemName: "person.badge.\(followed ? "minus" : "plus")") + } + followItem.primaryAction = UIAction(handler: { [unowned self] _ in + Task { + let success = await ToggleFollowHashtagService(hashtagName: hashtag.name, presenter: self).toggleFollow() + if success { + let followed = mastodonController.followedHashtags.contains(where: { $0.name.lowercased() == hashtag.name }) + updateFollowItem(followed: followed) + if followed { + self.presentingViewController?.dismiss(animated: true) + } + } + } + }) + updateFollowItem(followed: existingFollowed != nil) + + vc.navigationItem.rightBarButtonItems!.append(followItem) + } + + show(vc, sender: self) } // MARK: - Interaction @@ -150,3 +206,7 @@ extension AddSavedHashtagViewController: SearchResultsViewControllerDelegate { selectHashtag(hashtag) } } + +extension AddSavedHashtagViewController: TuskerNavigationDelegate { + var apiController: MastodonController! { mastodonController } +} From c99c397cf6d4ba2ac36bf40a72874dc8576e67db Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Wed, 11 Sep 2024 18:26:20 -0400 Subject: [PATCH 56/56] Bump build number and update changelog --- CHANGELOG-release.md | 15 +++++++++++++++ CHANGELOG.md | 15 +++++++++++++++ NotificationExtension/NotificationService.swift | 2 +- Version.xcconfig | 2 +- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CHANGELOG-release.md b/CHANGELOG-release.md index ff7729b1..e5a02a67 100644 --- a/CHANGELOG-release.md +++ b/CHANGELOG-release.md @@ -1,3 +1,18 @@ +## 2024.4 +This release introduces support for iOS 18, including a new sidebar/tab bar on iPad, as well as bugfixes and improvements. + +Features/Improvements: +- Import image description when adding attachments from Photos if possible +- iPadOS 18: New floating sidebar/tab bar + +Bugfixes: +- Fix crash when viewing profiles in certain circumstances +- Fix video controls in attachment gallery not auto-hiding +- Fix crash if hashtag search results includes duplicates +- Fix "no content" text not being removed from list timeline after refreshing +- macOS: Fix video controls overlay being positioned incorrectly when Reduce Motion is on +- macOS: Fix reselecting current item not navigating back + ## 2024.3 This update includes a number of bugfixes and performance improvements. See below for a list of fixes. diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a91274..7e2b99ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 2024.4 (136) +Features/Improvements: +- Import image description when adding attachments from Photos if possible +- Reorganize toolbar buttons when adding saved hashtag +- Show errors when loading video in attachment gallery fails + +Bugfixes: +- Fix crash when viewing profiles in certain circumstances +- Fix profile tab switching animation getting stuck +- Fix video controls in attachment gallery not auto-hiding +- Pleroma: Fix error when loading polls in some circumstances +- iPadOS 18: Fix incorrect two-column layout when closing sidebar +- macOS: Fix video controls overlay being positioned incorrectly when Reduce Motion is on +- macOS: Fix reselecting current item not navigating back + ## 2024.4 (135) Features/Improvements: - iOS 18: New floating sidebar/tab bar diff --git a/NotificationExtension/NotificationService.swift b/NotificationExtension/NotificationService.swift index 5acec490..6270a335 100644 --- a/NotificationExtension/NotificationService.swift +++ b/NotificationExtension/NotificationService.swift @@ -231,7 +231,7 @@ class NotificationService: UNNotificationServiceExtension { let updatedContent: UNMutableNotificationContent let contentProviding: any UNNotificationContentProviding - if #available(iOS 18.0, *), + if #available(iOS 18.0, visionOS 2.0, *), await Preferences.shared.hasFeatureFlag(.pushNotifCustomEmoji) { let attributedString = NSMutableAttributedString(string: content.body) diff --git a/Version.xcconfig b/Version.xcconfig index 05fd3047..abcfb85d 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -10,7 +10,7 @@ // https://help.apple.com/xcode/#/dev745c5c974 MARKETING_VERSION = 2024.4 -CURRENT_PROJECT_VERSION = 135 +CURRENT_PROJECT_VERSION = 136 CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION)) CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev