Compare commits
28 Commits
18f6445a7c
...
230696f456
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 230696f456 | |
Shadowfacts | c113903980 | |
Shadowfacts | 0e95cd0adf | |
Shadowfacts | 494708a362 | |
Shadowfacts | 3a21983b98 | |
Shadowfacts | 1817247077 | |
Shadowfacts | 0d9eed73dd | |
Shadowfacts | 59d43fd3f6 | |
Shadowfacts | d321c31776 | |
Shadowfacts | ce10c7d6e2 | |
Shadowfacts | 37b9673b12 | |
Shadowfacts | 7c7af945e4 | |
Shadowfacts | cb32c66a59 | |
Shadowfacts | 4249ab30ca | |
Shadowfacts | 67e9c1245e | |
Shadowfacts | 3d9a1086b6 | |
Shadowfacts | fda0c18794 | |
Shadowfacts | dffa5d8f75 | |
Shadowfacts | 9891b601a8 | |
Shadowfacts | a8f6aa6ed7 | |
Shadowfacts | 348dcc558c | |
Shadowfacts | 703f6f695b | |
Shadowfacts | fdbfe49a7c | |
Shadowfacts | 3f0dd599b3 | |
Shadowfacts | 07b6bf33cb | |
Shadowfacts | d0758dc73c | |
Shadowfacts | b85c0eb95d | |
Shadowfacts | eea0ef258c |
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -1,5 +1,19 @@
|
|||
# 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
|
||||
|
||||
## 2024.3 (132)
|
||||
- Add ToS nag before signing in
|
||||
|
||||
## 2024.3 (131)
|
||||
Bugfixes:
|
||||
- Fix Cmd+3 not correctly switching to Explore tab
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -129,6 +129,10 @@
|
|||
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 */; };
|
||||
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 */; };
|
||||
|
@ -215,15 +219,12 @@
|
|||
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 */; };
|
||||
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 */; };
|
||||
|
@ -560,6 +561,10 @@
|
|||
D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D64A50452C739DC0009D7193 /* NewMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewMainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||
D64A50472C739DEA009D7193 /* BaseMainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||
D64A50BB2C74F8F4009D7193 /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInstanceViewController.swift; sourceTree = "<group>"; };
|
||||
D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptableNavigationController.swift; sourceTree = "<group>"; };
|
||||
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
||||
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||
|
@ -649,15 +654,12 @@
|
|||
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
|
||||
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.swift; sourceTree = "<group>"; };
|
||||
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeaturedProfileCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
||||
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedDataManager.swift; sourceTree = "<group>"; };
|
||||
D6945C3123AC4D36005C403C /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = "<group>"; };
|
||||
D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowSceneDelegate+Close.swift"; sourceTree = "<group>"; };
|
||||
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; };
|
||||
|
@ -977,7 +979,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 */,
|
||||
|
@ -987,8 +989,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 */,
|
||||
|
@ -1124,7 +1124,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 */,
|
||||
|
@ -1556,6 +1558,7 @@
|
|||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */,
|
||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */,
|
||||
D61F759129365C6C00C0B37F /* CollectionViewController.swift */,
|
||||
D64A50BD2C752247009D7193 /* AdaptableNavigationController.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2014,7 +2017,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 */,
|
||||
|
@ -2141,6 +2143,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 +2196,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 */,
|
||||
|
@ -2233,6 +2237,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 */,
|
||||
|
@ -2252,7 +2257,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 */,
|
||||
|
@ -2272,7 +2276,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 */,
|
||||
|
@ -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 */,
|
||||
|
@ -3268,7 +3272,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 +3287,8 @@
|
|||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/karwa/swift-url";
|
||||
requirement = {
|
||||
branch = main;
|
||||
kind = branch;
|
||||
kind = exactVersion;
|
||||
version = 0.4.2;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,10 +9,14 @@
|
|||
import Foundation
|
||||
|
||||
@propertyWrapper
|
||||
class Box<Value> {
|
||||
final class Box<Value> {
|
||||
var wrappedValue: Value
|
||||
|
||||
init(wrappedValue: Value) {
|
||||
self.wrappedValue = wrappedValue
|
||||
}
|
||||
|
||||
var projectedValue: Box<Value> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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<Section, Item>!
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Section, Item>!
|
||||
|
||||
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<Section, Item>()
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems(accountIDs)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
|
||||
|
@ -57,15 +73,6 @@ class AccountListViewController: UIViewController, CollectionViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
snapshot.appendSections([.accounts])
|
||||
snapshot.appendItems(accountIDs)
|
||||
dataSource.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
|
|
|
@ -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<Section, Item>!
|
||||
|
||||
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,12 +66,18 @@ 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()
|
||||
|
|
|
@ -48,11 +48,17 @@ 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()
|
||||
|
|
|
@ -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<Void, Never>?
|
||||
|
||||
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 {}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="gTV-IL-0wX" customClass="FeaturedProfileCollectionViewCell" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="YkJ-rV-f3C">
|
||||
<rect key="frame" x="0.0" y="0.0" width="400" height="200"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="bo4-Sd-caI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="400" height="66"/>
|
||||
<color key="backgroundColor" systemColor="systemGray5Color"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="66" id="9Aa-Up-chJ"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RQe-uE-TEv">
|
||||
<rect key="frame" x="8" y="34" width="64" height="64"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="4wd-wq-Sh2">
|
||||
<rect key="frame" x="2" y="2" width="60" height="60"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="60" id="Xyl-Ry-J3r"/>
|
||||
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="height" multiplier="1:1" id="YEc-fT-FRB"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="RQe-uE-TEv" secondAttribute="height" multiplier="1:1" id="4vR-IF-yS8"/>
|
||||
<constraint firstAttribute="width" secondItem="4wd-wq-Sh2" secondAttribute="width" constant="4" id="52Q-zq-k28"/>
|
||||
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerY" secondItem="RQe-uE-TEv" secondAttribute="centerY" id="Ped-H7-QtP"/>
|
||||
<constraint firstItem="4wd-wq-Sh2" firstAttribute="centerX" secondItem="RQe-uE-TEv" secondAttribute="centerX" id="bRk-uJ-JGg"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display name" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="10" adjustsLetterSpacingToFitWidth="YES" translatesAutoresizingMaskIntoConstraints="NO" id="voW-Is-1b2" customClass="AccountDisplayNameLabel" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="76" y="72" width="316" height="24"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="20"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" verticalCompressionResistancePriority="749" scrollEnabled="NO" editable="NO" textAlignment="natural" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bvj-F0-ggC" customClass="StatusContentTextView" customModule="Tusker" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="102" width="384" height="98"/>
|
||||
<string key="text">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.</string>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="bvj-F0-ggC" secondAttribute="trailing" constant="8" id="1sd-Df-jR1"/>
|
||||
<constraint firstItem="bvj-F0-ggC" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" constant="8" id="35h-Wh-fvk"/>
|
||||
<constraint firstItem="voW-Is-1b2" firstAttribute="top" relation="greaterThanOrEqual" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="39l-yo-g8V"/>
|
||||
<constraint firstItem="bo4-Sd-caI" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" id="3lQ-uN-93N"/>
|
||||
<constraint firstAttribute="trailing" secondItem="voW-Is-1b2" secondAttribute="trailing" constant="8" id="Ckp-Bq-lB5"/>
|
||||
<constraint firstItem="bo4-Sd-caI" firstAttribute="top" secondItem="YkJ-rV-f3C" secondAttribute="top" id="DWh-S5-PLQ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="bvj-F0-ggC" secondAttribute="bottom" id="MH3-7E-THx"/>
|
||||
<constraint firstItem="RQe-uE-TEv" firstAttribute="leading" secondItem="YkJ-rV-f3C" secondAttribute="leading" constant="8" id="Tzo-aN-Bxq"/>
|
||||
<constraint firstItem="voW-Is-1b2" firstAttribute="bottom" secondItem="4wd-wq-Sh2" secondAttribute="bottom" id="Wk6-u2-azz"/>
|
||||
<constraint firstItem="RQe-uE-TEv" firstAttribute="centerY" secondItem="bo4-Sd-caI" secondAttribute="bottom" id="bon-bj-qnk"/>
|
||||
<constraint firstItem="bvj-F0-ggC" firstAttribute="top" secondItem="RQe-uE-TEv" secondAttribute="bottom" constant="4" id="dyg-LN-BDn"/>
|
||||
<constraint firstItem="voW-Is-1b2" firstAttribute="leading" secondItem="RQe-uE-TEv" secondAttribute="trailing" constant="4" id="shC-67-vC2"/>
|
||||
<constraint firstAttribute="trailing" secondItem="bo4-Sd-caI" secondAttribute="trailing" id="wZn-gO-zue"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
</view>
|
||||
<viewLayoutGuide key="safeArea" id="ZTg-uK-7eu"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="YkJ-rV-f3C" secondAttribute="trailing" id="Dy3-h1-zfM"/>
|
||||
<constraint firstItem="YkJ-rV-f3C" firstAttribute="leading" secondItem="gTV-IL-0wX" secondAttribute="leading" id="Gld-3x-oE0"/>
|
||||
<constraint firstItem="YkJ-rV-f3C" firstAttribute="top" secondItem="gTV-IL-0wX" secondAttribute="top" id="NIV-n8-4Rl"/>
|
||||
<constraint firstAttribute="bottom" secondItem="YkJ-rV-f3C" secondAttribute="bottom" id="zNw-2z-Hlx"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="avatarContainerView" destination="RQe-uE-TEv" id="tBI-fT-26P"/>
|
||||
<outlet property="avatarImageView" destination="4wd-wq-Sh2" id="rba-cv-8fb"/>
|
||||
<outlet property="clippingView" destination="YkJ-rV-f3C" id="hLI-4z-yIc"/>
|
||||
<outlet property="displayNameLabel" destination="voW-Is-1b2" id="XVS-4d-PKx"/>
|
||||
<outlet property="headerImageView" destination="bo4-Sd-caI" id="YkL-Wi-BXb"/>
|
||||
<outlet property="noteTextView" destination="bvj-F0-ggC" id="Bbm-ai-bu1"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="535" y="428"/>
|
||||
</collectionViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="labelColor">
|
||||
<color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemGray5Color">
|
||||
<color red="0.89803921568627454" green="0.89803921568627454" blue="0.91764705882352937" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
|
@ -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),
|
||||
])
|
||||
|
|
|
@ -53,11 +53,17 @@ 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<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
|
||||
cell.indicator.startAnimating()
|
||||
|
|
|
@ -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),
|
||||
])
|
||||
|
|
|
@ -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<Section, Item>!
|
||||
|
||||
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<Section, Item> {
|
||||
|
@ -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)
|
||||
|
||||
|
|
|
@ -45,6 +45,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]
|
||||
switch sectionIdentifier {
|
||||
|
@ -114,12 +116,18 @@ 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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -19,9 +19,7 @@ class LocalPredicateStatusesViewController: UIViewController, CollectionViewCont
|
|||
private let predicateTitle: String
|
||||
private let request: (RequestRange) -> Request<[TryDecode<Status>]>
|
||||
|
||||
var collectionView: UICollectionView! {
|
||||
view as? UICollectionView
|
||||
}
|
||||
private(set) var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
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<Section, Item> {
|
||||
|
@ -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)
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
@ -147,9 +156,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 +166,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()
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
//
|
||||
// BaseMainTabBarViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/19/24.
|
||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewControllerDelegate {
|
||||
|
||||
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(),
|
||||
myProfileButton.window != nil,
|
||||
let fastSwitcherIndicator else {
|
||||
fastSwitcherIndicator?.isHidden = true
|
||||
return
|
||||
}
|
||||
fastSwitcherIndicator.isHidden = false
|
||||
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)
|
||||
|
||||
// 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([
|
||||
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),
|
||||
])
|
||||
#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)
|
||||
}
|
||||
}
|
||||
|
||||
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? any NavigationControllerProtocol)?.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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
@ -370,7 +371,7 @@ extension MainSidebarViewController {
|
|||
case let .savedInstance(url):
|
||||
return url.host!
|
||||
case .addSavedInstance:
|
||||
return "Find An Instance..."
|
||||
return "Find an Instance..."
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import Combine
|
||||
import TuskerPreferences
|
||||
|
||||
@available(iOS, obsoleted: 18.0)
|
||||
class MainSplitViewController: UISplitViewController {
|
||||
|
||||
private let mastodonController: MastodonController
|
||||
|
@ -92,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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -578,20 +578,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()
|
||||
|
@ -677,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)!
|
||||
|
@ -690,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 {
|
||||
|
|
|
@ -9,19 +9,12 @@
|
|||
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 selectedTab: Tab {
|
||||
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()
|
||||
|
||||
|
@ -63,43 +46,14 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
|||
]
|
||||
|
||||
#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)
|
||||
}
|
||||
setupFastAccountSwitcher()
|
||||
#endif
|
||||
|
||||
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)
|
||||
|
@ -117,53 +71,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)
|
||||
}
|
||||
|
@ -177,22 +84,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
|
||||
}
|
||||
|
@ -227,7 +118,7 @@ extension MainTabBarViewController {
|
|||
}
|
||||
}
|
||||
|
||||
func getTabController(tab: Tab) -> UIViewController? {
|
||||
private func getTabController(tab: Tab) -> UIViewController? {
|
||||
if tab == .compose {
|
||||
return nil
|
||||
} else {
|
||||
|
@ -238,53 +129,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)
|
||||
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
|
||||
}
|
||||
}
|
||||
#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 activity == nil {
|
||||
stateRestorationLogger.fault("MainTabBarViewController: Unable to create state restoration activity, couldn't find StateRestorableViewController")
|
||||
}
|
||||
return activity
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -348,24 +207,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: selectedTab) 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 {
|
||||
|
|
|
@ -0,0 +1,835 @@
|
|||
//
|
||||
// NewMainTabBarViewController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/19/24.
|
||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import Pachyderm
|
||||
import TuskerPreferences
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
final 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 hashtagsGroup: UITabGroup!
|
||||
private var instancesGroup: UITabGroup!
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var navigationStacks = [String: [UIViewController]]()
|
||||
private var isCompact: Bool?
|
||||
@Box fileprivate var myProfileCell: UIView?
|
||||
private var sidebarTapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
mode = .tabSidebar
|
||||
delegate = self
|
||||
sidebar.delegate = self
|
||||
tabBar.isSpringLoaded = true
|
||||
view.backgroundColor = .appBackground
|
||||
|
||||
let viewControllerProvider = { [unowned self] (tab: UITab) -> UIViewController in
|
||||
self.makeViewController(for: tab)
|
||||
}
|
||||
|
||||
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
|
||||
favoritesTab = UITab(title: "Favorites", image: UIImage(systemName: "star"), identifier: Tab.favorites.rawValue, viewControllerProvider: viewControllerProvider)
|
||||
favoritesTab.preferredPlacement = .optional
|
||||
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)
|
||||
return AdaptableNavigationController()
|
||||
}
|
||||
listsGroup.preferredPlacement = .sidebarOnly
|
||||
listsGroup.sidebarActions = [
|
||||
UIAction(title: "New List…", image: UIImage(systemName: "plus"), handler: { [unowned self] _ in
|
||||
self.showAddList()
|
||||
})
|
||||
]
|
||||
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()
|
||||
|
||||
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 || UIDevice.current.userInterfaceIdiom == .vision {
|
||||
self.tabs = [
|
||||
homeTab,
|
||||
notificationsTab,
|
||||
composeTab,
|
||||
exploreTab,
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
|
||||
}
|
||||
|
||||
#if !os(visionOS)
|
||||
setupFastAccountSwitcher()
|
||||
#endif
|
||||
}
|
||||
|
||||
private func updatePadTabs() {
|
||||
let wasCompact = isCompact
|
||||
|
||||
if self.traitCollection.horizontalSizeClass == .compact {
|
||||
isCompact = true
|
||||
|
||||
var exploreNavStack: [UIViewController]? = nil
|
||||
if let parent = selectedTab?.parent,
|
||||
parent === listsGroup || parent === hashtagsGroup || parent === instancesGroup {
|
||||
let nav = parent.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 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
|
||||
}
|
||||
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
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.tabs = [
|
||||
homeTab,
|
||||
notificationsTab,
|
||||
exploreTab,
|
||||
bookmarksTab,
|
||||
favoritesTab,
|
||||
myProfileTab,
|
||||
composeTab,
|
||||
listsGroup,
|
||||
hashtagsGroup,
|
||||
instancesGroup,
|
||||
]
|
||||
|
||||
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
|
||||
// to the size class change. So wait until it finishes to do so.
|
||||
DispatchQueue.main.async {
|
||||
self.selectedTab = tab
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
root = ExploreViewController(mastodonController: mastodonController)
|
||||
} else {
|
||||
let nav = AdaptableNavigationController(viewControllersToPrependInCompact: [
|
||||
ExploreViewController(mastodonController: mastodonController)
|
||||
])
|
||||
nav.viewControllers = [InlineTrendsViewController(mastodonController: mastodonController)]
|
||||
return nav
|
||||
}
|
||||
case .bookmarks:
|
||||
root = BookmarksViewController(mastodonController: mastodonController)
|
||||
case .favorites:
|
||||
root = FavoritesViewController(mastodonController: mastodonController)
|
||||
case .myProfile:
|
||||
root = MyProfileViewController(mastodonController: mastodonController)
|
||||
case .lists, .hashtags, .instances:
|
||||
fatalError("unreachable")
|
||||
}
|
||||
return embedInNavigationController(root)
|
||||
}
|
||||
|
||||
private func embedInNavigationController(_ vc: UIViewController) -> UIViewController {
|
||||
let nav = AdaptableNavigationController()
|
||||
nav.viewControllers = [vc]
|
||||
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]) {
|
||||
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<String> = []
|
||||
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 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)
|
||||
}
|
||||
|
||||
@objc private func sidebarTapped() {
|
||||
#if !os(visionOS)
|
||||
fastAccountSwitcher?.hide()
|
||||
#endif
|
||||
}
|
||||
|
||||
private func showAddList() {
|
||||
let service = CreateListService(mastodonController: mastodonController, present: {
|
||||
self.present($0, animated: true)
|
||||
}) { list in
|
||||
let tab = self.listsGroup.tab(forIdentifier: ListTab.identifier(for: list))!
|
||||
let listVC = tab.viewController as! ListTimelineViewController
|
||||
listVC.presentEditOnAppear = true
|
||||
self.selectedTab = tab
|
||||
}
|
||||
service.run()
|
||||
}
|
||||
|
||||
private func showAddSavedHashtag() {
|
||||
let addController = AddSavedHashtagViewController(mastodonController: mastodonController)
|
||||
let nav = EnhancedNavigationViewController(rootViewController: addController)
|
||||
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
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
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, *)
|
||||
extension NewMainTabBarViewController {
|
||||
enum Tab: String, Hashable, CaseIterable {
|
||||
case home
|
||||
case notifications
|
||||
case compose
|
||||
case explore
|
||||
case bookmarks
|
||||
case favorites
|
||||
case myProfile
|
||||
|
||||
case lists
|
||||
case hashtags
|
||||
case instances
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
extension NewMainTabBarViewController: UITabBarControllerDelegate {
|
||||
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
|
||||
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
|
||||
|
||||
compose(editing: nil)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func tabBarController(_ tabBarController: UITabBarController, didSelectTab newTab: UITab, previousTab: UITab?) {
|
||||
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.
|
||||
// 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 === listsGroup || group === hashtagsGroup || group === instancesGroup,
|
||||
let nav = group.viewController as? any NavigationControllerProtocol {
|
||||
updateViewControllerSafeAreaInsets(nav)
|
||||
|
||||
if let previousTab {
|
||||
navigationStacks[previousTab.identifier] = nav.viewControllers
|
||||
}
|
||||
|
||||
if let existing = navigationStacks[newTab.identifier] {
|
||||
nav.viewControllers = existing
|
||||
} else if let newVC = newTab.viewController {
|
||||
nav.viewControllers = [newVC]
|
||||
} else {
|
||||
fatalError("unreachable")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
let vc = selectedTab!.parent?.viewController ?? selectedTab!.viewController!
|
||||
animator.addAnimations {
|
||||
self.updateViewControllerSafeAreaInsets(vc)
|
||||
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,
|
||||
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 os(visionOS)
|
||||
item.contentConfiguration = config
|
||||
#else
|
||||
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
|
||||
}
|
||||
#endif
|
||||
}
|
||||
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 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 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:
|
||||
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, .hashtags, .instances:
|
||||
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, *)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@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?
|
||||
let configureView: (UIView) -> Void
|
||||
|
||||
init(wrapped: any UIContentConfiguration, view: Box<UIView?>, 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)
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)"
|
||||
}
|
||||
}
|
||||
|
||||
@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)"
|
||||
}
|
||||
}
|
||||
|
||||
@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!)"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
])
|
||||
|
|
|
@ -245,10 +245,17 @@ extension OnboardingViewController {
|
|||
|
||||
extension OnboardingViewController: InstanceSelectorTableViewControllerDelegate {
|
||||
func didSelectInstance(url instanceURL: URL) {
|
||||
let alert = UIAlertController(title: "Terms of Service", message: "By logging in to '\(instanceURL.host!)', you agree to follow all applicable rules and terms of service for that instance.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { [unowned self] _ in
|
||||
self.instanceSelector.tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Continue", style: .default, handler: { [unowned self] _ in
|
||||
Task {
|
||||
await self.login(to: instanceURL)
|
||||
instanceSelector.tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
self.instanceSelector.tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
}
|
||||
}))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,12 +32,12 @@ struct TipJarView: View {
|
|||
@StateObject private var observer = UbiquitousKeyValueStoreObserver()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.appGroupedBackground
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
productsView
|
||||
|
||||
List {
|
||||
productsOrLoading
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.appGroupedListBackground(container: PreferencesNavigationController.self)
|
||||
.overlay {
|
||||
if showConfetti {
|
||||
ConfettiView()
|
||||
.transition(.opacity.animation(.default))
|
||||
|
@ -75,46 +75,66 @@ struct TipJarView: View {
|
|||
.onDisappear {
|
||||
updatesObserver?.cancel()
|
||||
}
|
||||
.onReceive(Just(showConfetti).filter { $0 }.delay(for: .seconds(5), scheduler: DispatchQueue.main)) { _ in
|
||||
.onChange(of: showConfetti) { newValue in
|
||||
if newValue {
|
||||
Task {
|
||||
try await Task.sleep(nanoseconds: 5 * NSEC_PER_SEC)
|
||||
showConfetti = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var productsView: some View {
|
||||
private var productsOrLoading: some View {
|
||||
if isLoaded {
|
||||
VStack {
|
||||
if !supporterProducts.isEmpty {
|
||||
supporterSubscriptions
|
||||
}
|
||||
|
||||
tipPurchases
|
||||
|
||||
if let tipStatus {
|
||||
tipStatus
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 16)
|
||||
Text("Thank you!")
|
||||
}
|
||||
}
|
||||
productsSections
|
||||
} else {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var supporterSubscriptions: some View {
|
||||
Text("If you want to contribute Tusker's continued development, you can become a supporter. Supporting Tusker is an auto-renewable monthly subscription.")
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
private var productsSections: some View {
|
||||
if let tipStatus {
|
||||
Section {
|
||||
VStack(alignment: .leading) {
|
||||
tipStatus
|
||||
.multilineTextAlignment(.leading)
|
||||
Text("Thank you!")
|
||||
}
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
}
|
||||
|
||||
if !supporterProducts.isEmpty {
|
||||
supporterSection
|
||||
}
|
||||
|
||||
tipSection
|
||||
}
|
||||
|
||||
private var supporterSection: some View {
|
||||
Section {
|
||||
Text("If you want to contribute Tusker's continued development, you can become a supporter and give a montly tip.")
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
VStack(alignment: .myAlignment) {
|
||||
ForEach($supporterProducts, id: \.0.id) { $productAndPurchasing in
|
||||
TipRow(product: productAndPurchasing.0, buttonWidth: supporterButtonWidth, isPurchasing: $productAndPurchasing.1, showConfetti: $showConfetti)
|
||||
}
|
||||
} footer: {
|
||||
VStack {
|
||||
var privacyPolicy: AttributedString = "Privacy Policy"
|
||||
let _ = privacyPolicy.link = URL(string: "https://vaccor.space/tusker#privacy")!
|
||||
var eula: AttributedString = "EULA"
|
||||
let _ = eula.link = URL(string: "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/")!
|
||||
Text(AttributedString("Supporting Tusker is an auto-renewable monthly subscription. Subscribing does not provide additional features or capabilities.\n") + privacyPolicy + AttributedString(" and ") + eula)
|
||||
}
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
.onPreferenceChange(ButtonWidthKey.self) { newValue in
|
||||
if let supporterButtonWidth {
|
||||
self.supporterButtonWidth = max(supporterButtonWidth, newValue)
|
||||
|
@ -124,18 +144,16 @@ struct TipJarView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var tipPurchases: some View {
|
||||
private var tipSection: some View {
|
||||
Section {
|
||||
Text("Or, you can choose to make a one-time tip to show your gratitutde or help support the app's development. It is greatly appreciated!")
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 16)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
VStack(alignment: .myAlignment) {
|
||||
ForEach($tipProducts, id: \.0.id) { $productAndPurchasing in
|
||||
TipRow(product: productAndPurchasing.0, buttonWidth: tipButtonWidth, isPurchasing: $productAndPurchasing.1, showConfetti: $showConfetti)
|
||||
}
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
.onPreferenceChange(ButtonWidthKey.self) { newValue in
|
||||
if let tipButtonWidth {
|
||||
self.tipButtonWidth = max(tipButtonWidth, newValue)
|
||||
|
@ -231,6 +249,8 @@ private struct TipRow: View {
|
|||
Text(product.displayName)
|
||||
.alignmentGuide(.myAlignment, computeValue: { context in context[.trailing] })
|
||||
|
||||
Spacer()
|
||||
|
||||
if let subscription = product.subscription {
|
||||
SubscriptionButton(product: product, subscriptionInfo: subscription, isPurchasing: $isPurchasing, buttonWidth: buttonWidth, purchase: purchase)
|
||||
} else {
|
||||
|
|
|
@ -38,10 +38,12 @@ class ProfileHeaderCollectionViewCell: UICollectionViewCell {
|
|||
header.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.embedSubview(header)
|
||||
self.state = .view(header)
|
||||
case .view(_):
|
||||
case .view(let existing):
|
||||
if existing !== header {
|
||||
fatalError("profile header collection view cell already has view")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addConstraint(height: CGFloat) -> ProfileHeaderView? {
|
||||
switch state {
|
||||
|
|
|
@ -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<TimelineItem>!
|
||||
|
@ -26,11 +26,11 @@ class ProfileStatusesViewController: UIViewController, TimelineLikeCollectionVie
|
|||
private var older: RequestRange?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
var collectionView: UICollectionView! {
|
||||
view as? UICollectionView
|
||||
}
|
||||
private(set) var collectionView: UICollectionView!
|
||||
private(set) var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
private(set) var headerCell: ProfileHeaderCollectionViewCell?
|
||||
var headerCell: ProfileHeaderCollectionViewCell? {
|
||||
collectionView.cellForItem(at: IndexPath(item: 0, section: 0)) as? ProfileHeaderCollectionViewCell
|
||||
}
|
||||
|
||||
var reconfigureVisibleItemsOnEndDecelerating: Bool = false
|
||||
|
||||
|
@ -54,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
|
||||
|
@ -101,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()
|
||||
|
@ -113,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)
|
||||
|
@ -173,29 +179,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 {
|
||||
switch self.headerViewMode {
|
||||
case nil:
|
||||
fatalError("missing initialHeaderMode")
|
||||
case .createView:
|
||||
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)
|
||||
self.headerViewMode = .useExistingView(view)
|
||||
}
|
||||
case .useExistingView(let view):
|
||||
view.updateUI(for: id)
|
||||
cell.addHeader(view)
|
||||
case .placeholder(height: let height):
|
||||
_ = cell.addConstraint(height: height)
|
||||
}
|
||||
self.headerCell = cell
|
||||
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 +417,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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Section, Item>!
|
||||
|
||||
/// 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)
|
||||
|
@ -295,7 +301,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])
|
||||
|
|
|
@ -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<Section, Item>!
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
])
|
||||
|
|
|
@ -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),
|
||||
])
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
//
|
||||
// AdaptableNavigationController.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/20/24.
|
||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import TuskerPreferences
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
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)?
|
||||
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()
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||
}
|
||||
|
||||
private func updateNavigationController() {
|
||||
let isTransferring: Bool
|
||||
var stack: [UIViewController]
|
||||
if let _current {
|
||||
_current.removeViewAndController()
|
||||
stack = _current.viewControllers
|
||||
_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
|
||||
self.currentWidescreenNavigationMode = Preferences.shared.widescreenNavigationMode
|
||||
switch Preferences.shared.widescreenNavigationMode {
|
||||
case .stack:
|
||||
return EnhancedNavigationViewController()
|
||||
case .splitScreen:
|
||||
return SplitNavigationController()
|
||||
case .multiColumn:
|
||||
return MultiColumnNavigationController()
|
||||
}
|
||||
}
|
||||
|
||||
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, *)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 = 131
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue