From 522c9b2b030f97f187c70422650969f04c6152b4 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 13 Dec 2020 22:37:37 -0500 Subject: [PATCH] Add multi-window support and auxiliary windows --- Tusker.xcodeproj/project.pbxproj | 20 ++- Tusker/AppDelegate.swift | 29 ++++ Tusker/AuxiliarySceneDelegate.swift | 109 ++++++++++++++ Tusker/ComposeSceneDelegate.swift | 72 ++++++++++ .../UIWindowSceneDelegate+Close.swift | 20 +++ Tusker/Info.plist | 27 +++- ...Delegate.swift => MainSceneDelegate.swift} | 8 +- Tusker/Models/DraftsManager.swift | 4 + .../Compose/ComposeHostingController.swift | 30 ++-- Tusker/Screens/Compose/ComposeUIState.swift | 8 +- Tusker/Screens/Compose/ComposeView.swift | 10 +- .../CrashReporterViewController.swift | 4 +- .../FastAccountSwitcherViewController.swift | 2 +- .../Main/MainSidebarViewController.swift | 40 ++++++ .../NotificationsPageViewController.swift | 11 +- .../PreferencesNavigationController.swift | 9 +- .../Profile/MyProfileViewController.swift | 2 + .../Profile/ProfileViewController.swift | 2 + .../Screens/Search/SearchViewController.swift | 2 + .../TimelineTableViewController.swift | 21 ++- Tusker/Screens/Utilities/Previewing.swift | 22 ++- Tusker/Shortcuts/UserActivityManager.swift | 135 ++++++++++++++---- Tusker/Shortcuts/UserActivityType.swift | 22 +-- .../Status/BaseStatusTableViewCell.swift | 13 +- 24 files changed, 533 insertions(+), 89 deletions(-) create mode 100644 Tusker/AuxiliarySceneDelegate.swift create mode 100644 Tusker/ComposeSceneDelegate.swift create mode 100644 Tusker/Extensions/UIWindowSceneDelegate+Close.swift rename Tusker/{SceneDelegate.swift => MainSceneDelegate.swift} (97%) diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index dc9b3fe2..109eeeb4 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -186,6 +186,7 @@ D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; }; D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; }; D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */; }; + D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */; }; D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; }; D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.swift */; }; D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */; }; @@ -199,6 +200,8 @@ 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 */; }; + D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */; }; + D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */; }; D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; }; D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; }; D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */; }; @@ -221,7 +224,7 @@ D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; }; D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; }; D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */; }; - D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* SceneDelegate.swift */; }; + D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */; }; D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; }; D6ACE1AD240C3BAD004EA8E2 /* Embassy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F612D23AE990C00F3CFD3 /* Embassy.framework */; }; D6AEBB3E2321638100E5038B /* UIActivity+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */; }; @@ -541,6 +544,7 @@ D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = ""; }; D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = ""; }; D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = ""; }; + D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = ""; }; D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = ""; }; D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = ""; }; D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSourceEmojiLabel.swift; sourceTree = ""; }; @@ -554,6 +558,8 @@ D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.swift; sourceTree = ""; }; D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstanceTimelineViewController.swift; sourceTree = ""; }; D6945C3923AC75E2005C403C /* FindInstanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = FindInstanceViewController.swift; path = Tusker/Screens/FindInstanceViewController.swift; sourceTree = SOURCE_ROOT; }; + D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindowSceneDelegate+Close.swift"; sourceTree = ""; }; + D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = ""; }; D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = ""; }; D6969E9F240C8384002843CE /* EmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLabel.swift; sourceTree = ""; }; D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UnknownNotificationTableViewCell.xib; sourceTree = ""; }; @@ -575,7 +581,7 @@ D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = ""; }; D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = ""; }; D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = ""; }; - D6AC956623C4347E008C9946 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = ""; }; D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = ""; }; D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = ""; }; D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = ""; }; @@ -1177,6 +1183,7 @@ D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */, D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */, D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */, + D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */, ); path = Extensions; sourceTree = ""; @@ -1421,7 +1428,9 @@ children = ( D6E4885C24A2890C0011C13E /* Tusker.entitlements */, D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, - D6AC956623C4347E008C9946 /* SceneDelegate.swift */, + D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */, + D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */, + D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */, D6945C2E23AC47C3005C403C /* SavedDataManager.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, @@ -1854,6 +1863,7 @@ D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, + D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */, D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */, D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */, D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */, @@ -1875,7 +1885,7 @@ D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */, D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, - D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */, + D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */, @@ -1946,6 +1956,7 @@ D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */, + D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */, D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */, 0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */, D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */, @@ -1983,6 +1994,7 @@ D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */, D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */, D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */, + D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */, D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, diff --git a/Tusker/AppDelegate.swift b/Tusker/AppDelegate.swift index 69284e99..def7da56 100644 --- a/Tusker/AppDelegate.swift +++ b/Tusker/AppDelegate.swift @@ -52,4 +52,33 @@ class AppDelegate: UIResponder, UIApplicationDelegate { MenuController.buildMainMenu(builder: builder) } } + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + let name = getSceneNameForActivity(options.userActivities.first) + return UISceneConfiguration(name: name, sessionRole: connectingSceneSession.role) + } + + private func getSceneNameForActivity(_ activity: NSUserActivity?) -> String { + guard let activity = activity, + let type = UserActivityType(rawValue: activity.activityType) else { + return "main-scene" + } + + switch type { + case .showConversation, + .showTimeline, + .checkNotifications, + .search, + .bookmarks, + .myProfile, + .showProfile: + return "auxiliary" + + case .newPost: + return "compose" + + default: + fatalError("no scene for activity type \(type)") + } + } } diff --git a/Tusker/AuxiliarySceneDelegate.swift b/Tusker/AuxiliarySceneDelegate.swift new file mode 100644 index 00000000..a652bed1 --- /dev/null +++ b/Tusker/AuxiliarySceneDelegate.swift @@ -0,0 +1,109 @@ +// +// AuxiliarySceneDelegate.swift +// Tusker +// +// Created by Shadowfacts on 12/13/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit +import Pachyderm + +class AuxiliarySceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + private var launchActivity: NSUserActivity? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { + return + } + + guard let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity else { + // without an account, we don't know what type of auxiliary scene this is and can't do anything + UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil) + return + } + launchActivity = activity + + let account: LocalData.UserAccountInfo + + if let activityAccount = UserActivityManager.getAccount(from: activity) { + account = activityAccount + } else if let mostRecent = LocalData.shared.getMostRecentAccount() { + account = mostRecent + } else { + // without an account, we can't do anything so we just destroy the scene + // todo: this isn't really true for instance public timelines, how much do we care about that? + UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil) + return + } + + let controller = MastodonController.getForAccount(account) + session.mastodonController = controller + + guard let rootVC = viewController(for: activity, mastodonController: controller) else { + UIApplication.shared.requestSceneSessionDestruction(session, options: nil, errorHandler: nil) + return + } + rootVC.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(close)) + let nav = EnhancedNavigationViewController(rootViewController: rootVC) + + window = UIWindow(windowScene: windowScene) + window!.rootViewController = nav + window!.makeKeyAndVisible() + } + + func sceneWillResignActive(_ scene: UIScene) { + scene.userActivity = launchActivity + } + + func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { + return scene.userActivity + } + + private func viewController(for activity: NSUserActivity, mastodonController: MastodonController) -> UIViewController? { + switch UserActivityType(rawValue: activity.activityType) { + case .showTimeline: + guard let timeline = UserActivityManager.getTimeline(from: activity) else { return nil } + return timelineViewController(for: timeline, mastodonController: mastodonController) + + case .showConversation: + guard let id = UserActivityManager.getConversationStatus(from: activity) else { return nil } + return ConversationTableViewController(for: id, mastodonController: mastodonController) + + case .checkNotifications: + guard let mode = UserActivityManager.getNotificationsMode(from: activity) else { return nil } + return NotificationsPageViewController(initialMode: mode, mastodonController: mastodonController) + + case .search: + return SearchViewController(mastodonController: mastodonController) + + case .bookmarks: + return BookmarksTableViewController(mastodonController: mastodonController) + + case .myProfile: + return MyProfileViewController(mastodonController: mastodonController) + + case .showProfile: + guard let id = UserActivityManager.getProfile(from: activity) else { return nil } + return ProfileViewController(accountID: id, mastodonController: mastodonController) + + default: + fatalError("invalid activity type for auxiliary scene: \(activity.activityType)") + } + } + + private func timelineViewController(for timeline: Timeline, mastodonController: MastodonController) -> UIViewController { + switch timeline { + // todo: list/hashtag controllers need whole objects which must be fetched asynchronously + default: + return TimelineTableViewController(for: timeline, mastodonController: mastodonController) + } + } + + @objc private func close() { + closeWindow() + } +} diff --git a/Tusker/ComposeSceneDelegate.swift b/Tusker/ComposeSceneDelegate.swift new file mode 100644 index 00000000..934fb649 --- /dev/null +++ b/Tusker/ComposeSceneDelegate.swift @@ -0,0 +1,72 @@ +// +// ComposeSceneDelegate.swift +// Tusker +// +// Created by Shadowfacts on 12/12/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +class ComposeSceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { + return + } + + let account: LocalData.UserAccountInfo + let draft: Draft? + + if let activity = connectionOptions.userActivities.first ?? session.stateRestorationActivity, + let activityAccount = UserActivityManager.getAccount(from: activity) { + account = activityAccount + draft = UserActivityManager.getDraft(from: activity) + } else { + account = LocalData.shared.getMostRecentAccount()! + draft = nil + } + + let controller = MastodonController.getForAccount(account) + session.mastodonController = controller + + let composeVC = ComposeHostingController(draft: draft, mastodonController: controller) + composeVC.delegate = self + let nav = EnhancedNavigationViewController(rootViewController: composeVC) + + window = UIWindow(windowScene: windowScene) + window!.rootViewController = nav + window!.makeKeyAndVisible() + } + + func sceneWillResignActive(_ scene: UIScene) { + DraftsManager.save() + + if let window = window, + let nav = window.rootViewController as? UINavigationController, + let compose = nav.topViewController as? ComposeHostingController { + scene.userActivity = UserActivityManager.editDraftActivity(id: compose.draft.id, accountID: scene.session.mastodonController!.accountInfo!.id) + } + } + + func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { + return scene.userActivity + } + +} + +extension ComposeSceneDelegate: ComposeHostingControllerDelegate { + func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool { + let animation: UIWindowScene.DismissalAnimation + switch mode { + case .cancel: + animation = .decline + case .post: + animation = .commit + } + closeWindow(animation: animation) + return true + } +} diff --git a/Tusker/Extensions/UIWindowSceneDelegate+Close.swift b/Tusker/Extensions/UIWindowSceneDelegate+Close.swift new file mode 100644 index 00000000..75dde0a2 --- /dev/null +++ b/Tusker/Extensions/UIWindowSceneDelegate+Close.swift @@ -0,0 +1,20 @@ +// +// UIWindowSceneDelegate+Close.swift +// Tusker +// +// Created by Shadowfacts on 12/12/20. +// Copyright © 2020 Shadowfacts. All rights reserved. +// + +import UIKit + +extension UIWindowSceneDelegate { + + func closeWindow(animation: UIWindowScene.DismissalAnimation = .standard, errorHandler: ((Error) -> Void)? = nil) { + guard let session = self.window??.windowScene?.session else { return } + let options = UIWindowSceneDestructionRequestOptions() + options.windowDismissalAnimation = animation + UIApplication.shared.requestSceneSessionDestruction(session, options: options, errorHandler: errorHandler) + } + +} diff --git a/Tusker/Info.plist b/Tusker/Info.plist index a3ce1098..9af3905c 100644 --- a/Tusker/Info.plist +++ b/Tusker/Info.plist @@ -51,21 +51,24 @@ NSMicrophoneUsageDescription Post videos from the camera. NSPhotoLibraryAddUsageDescription - Save photos directly from other people's posts. + Save photos directly from other people's posts. NSPhotoLibraryUsageDescription Post photos from the photo library. NSUserActivityTypes + $(PRODUCT_BUNDLE_IDENTIFIER).activity.show-conversation $(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline $(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications - $(PRODUCT_BUNDLE_IDENTIFIER).activity.check-mentions $(PRODUCT_BUNDLE_IDENTIFIER).activity.new-post $(PRODUCT_BUNDLE_IDENTIFIER).activity.search + $(PRODUCT_BUNDLE_IDENTIFIER).activity.bookmarks + $(PRODUCT_BUNDLE_IDENTIFIER).activity.my-profile + $(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile UIApplicationSceneManifest UIApplicationSupportsMultipleScenes - + UISceneConfigurations UIWindowSceneSessionRoleApplication @@ -76,7 +79,23 @@ UISceneConfigurationName main-scene UISceneDelegateClassName - $(PRODUCT_MODULE_NAME).SceneDelegate + $(PRODUCT_MODULE_NAME).MainSceneDelegate + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).AuxiliarySceneDelegate + UISceneConfigurationName + auxiliary + + + UISceneClassName + UIWindowScene + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).ComposeSceneDelegate + UISceneConfigurationName + compose diff --git a/Tusker/SceneDelegate.swift b/Tusker/MainSceneDelegate.swift similarity index 97% rename from Tusker/SceneDelegate.swift rename to Tusker/MainSceneDelegate.swift index 036bc0fc..c4be6561 100644 --- a/Tusker/SceneDelegate.swift +++ b/Tusker/MainSceneDelegate.swift @@ -1,5 +1,5 @@ // -// SceneDelegate.swift +// MainSceneDelegate.swift // Tusker // // Created by Shadowfacts on 1/6/20. @@ -11,7 +11,7 @@ import Pachyderm import CrashReporter import MessageUI -class SceneDelegate: UIResponder, UIWindowSceneDelegate { +class MainSceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? @@ -195,13 +195,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } -extension SceneDelegate: OnboardingViewControllerDelegate { +extension MainSceneDelegate: OnboardingViewControllerDelegate { func didFinishOnboarding(account: LocalData.UserAccountInfo) { activateAccount(account, animated: false) } } -extension SceneDelegate: MFMailComposeViewControllerDelegate { +extension MainSceneDelegate: MFMailComposeViewControllerDelegate { func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { controller.dismiss(animated: true) { self.showAppOrOnboardingUI() diff --git a/Tusker/Models/DraftsManager.swift b/Tusker/Models/DraftsManager.swift index 39823107..8ce79395 100644 --- a/Tusker/Models/DraftsManager.swift +++ b/Tusker/Models/DraftsManager.swift @@ -47,4 +47,8 @@ class DraftsManager: Codable { drafts.removeAll { $0 == draft } } + func getBy(id: UUID) -> Draft? { + return drafts.first { $0.id == id } + } + } diff --git a/Tusker/Screens/Compose/ComposeHostingController.swift b/Tusker/Screens/Compose/ComposeHostingController.swift index 954da8eb..7d97053e 100644 --- a/Tusker/Screens/Compose/ComposeHostingController.swift +++ b/Tusker/Screens/Compose/ComposeHostingController.swift @@ -11,8 +11,14 @@ import Combine import Pachyderm import PencilKit +protocol ComposeHostingControllerDelegate: class { + func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool +} + class ComposeHostingController: UIHostingController { + weak var delegate: ComposeHostingControllerDelegate? + let mastodonController: MastodonController let uiState: ComposeUIState @@ -61,7 +67,7 @@ class ComposeHostingController: UIHostingController { pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self) - userActivity = UserActivityManager.newPostActivity() + userActivity = UserActivityManager.newPostActivity(accountID: mastodonController.accountInfo!.id) self.uiState.$draft .flatMap(\.$visibility) @@ -81,21 +87,6 @@ class ComposeHostingController: UIHostingController { fatalError("init(coder:) has not been implemented") } - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // can't do this in viewDidLoad because viewDidLoad isn't called for UIHostingController -// if mainToolbar.superview == nil { -// view.addSubview(mainToolbar) -// NSLayoutConstraint.activate([ -// mainToolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor), -// mainToolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor), -// // use the top anchor of the toolbar so our additionalSafeAreaInsets (which has the bottom as the toolbar height) don't affect it -// mainToolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), -// ]) -// } - } - override func didMove(toParent parent: UIViewController?) { super.didMove(toParent: parent) @@ -287,8 +278,11 @@ class ComposeHostingController: UIHostingController { extension ComposeHostingController: ComposeUIStateDelegate { var assetPickerDelegate: AssetPickerViewControllerDelegate? { self } - func dismissCompose() { - self.dismiss(animated: true) + func dismissCompose(mode: ComposeUIState.DismissMode) { + let dismissed = delegate?.dismissCompose(mode: mode) ?? false + if !dismissed { + self.dismiss(animated: true) + } } func presentAssetPickerSheet() { diff --git a/Tusker/Screens/Compose/ComposeUIState.swift b/Tusker/Screens/Compose/ComposeUIState.swift index 2c5b0a27..49462008 100644 --- a/Tusker/Screens/Compose/ComposeUIState.swift +++ b/Tusker/Screens/Compose/ComposeUIState.swift @@ -11,7 +11,7 @@ import SwiftUI protocol ComposeUIStateDelegate: class { var assetPickerDelegate: AssetPickerViewControllerDelegate? { get } - func dismissCompose() + func dismissCompose(mode: ComposeUIState.DismissMode) func presentAssetPickerSheet() func presentComposeDrawing() @@ -54,6 +54,12 @@ extension ComposeUIState { } } +extension ComposeUIState { + enum DismissMode { + case cancel, post + } +} + protocol ComposeAutocompleteHandler: class { func autocomplete(with string: String) } diff --git a/Tusker/Screens/Compose/ComposeView.swift b/Tusker/Screens/Compose/ComposeView.swift index 9b95f3ff..4fbdf3cf 100644 --- a/Tusker/Screens/Compose/ComposeView.swift +++ b/Tusker/Screens/Compose/ComposeView.swift @@ -168,14 +168,14 @@ struct ComposeView: View { private func cancel() { if Preferences.shared.automaticallySaveDrafts { // draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear - uiState.delegate?.dismissCompose() + uiState.delegate?.dismissCompose(mode: .cancel) } else { // if the draft doesn't have content, it doesn't need to be saved if draft.hasContent { uiState.isShowingSaveDraftSheet = true } else { DraftsManager.shared.remove(draft) - uiState.delegate?.dismissCompose() + uiState.delegate?.dismissCompose(mode: .cancel) } } } @@ -185,12 +185,12 @@ struct ComposeView: View { .default(Text("Save Draft"), action: { // draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear uiState.isShowingSaveDraftSheet = false - uiState.delegate?.dismissCompose() + uiState.delegate?.dismissCompose(mode: .cancel) }), .destructive(Text("Delete Draft"), action: { DraftsManager.shared.remove(draft) uiState.isShowingSaveDraftSheet = false - uiState.delegate?.dismissCompose() + uiState.delegate?.dismissCompose(mode: .cancel) }), .cancel(), ]) @@ -242,7 +242,7 @@ struct ComposeView: View { // wait .25 seconds so the user can see the progress bar has completed DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) { - self.uiState.delegate?.dismissCompose() + self.uiState.delegate?.dismissCompose(mode: .post) } } } diff --git a/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift b/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift index 25f9f658..971fa2ee 100644 --- a/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift +++ b/Tusker/Screens/Crash Reporter/CrashReporterViewController.swift @@ -119,7 +119,7 @@ class CrashReporterViewController: UIViewController { } @IBAction func cancelPressed(_ sender: Any) { - (view.window!.windowScene!.delegate as! SceneDelegate).showAppOrOnboardingUI() + (view.window!.windowScene!.delegate as! MainSceneDelegate).showAppOrOnboardingUI() } } @@ -127,7 +127,7 @@ class CrashReporterViewController: UIViewController { extension CrashReporterViewController: MFMailComposeViewControllerDelegate { func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { controller.dismiss(animated: true) { - (self.view.window!.windowScene!.delegate as! SceneDelegate).showAppOrOnboardingUI() + (self.view.window!.windowScene!.delegate as! MainSceneDelegate).showAppOrOnboardingUI() } } } diff --git a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift index a43fb8b2..5bc917c1 100644 --- a/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift +++ b/Tusker/Screens/Fast Account Switcher/FastAccountSwitcherViewController.swift @@ -131,7 +131,7 @@ class FastAccountSwitcherViewController: UIViewController { selectionChangedFeedbackGenerator = nil hide() { - (self.view.window!.windowScene!.delegate as! SceneDelegate).activateAccount(account, animated: true) + (self.view.window!.windowScene!.delegate as! MainSceneDelegate).activateAccount(account, animated: true) } } else { hide() diff --git a/Tusker/Screens/Main/MainSidebarViewController.swift b/Tusker/Screens/Main/MainSidebarViewController.swift index bd4c5805..db751304 100644 --- a/Tusker/Screens/Main/MainSidebarViewController.swift +++ b/Tusker/Screens/Main/MainSidebarViewController.swift @@ -80,6 +80,7 @@ class MainSidebarViewController: UIViewController { collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.backgroundColor = .clear collectionView.delegate = self + collectionView.dragDelegate = self view.addSubview(collectionView) dataSource = createDataSource() @@ -220,6 +221,32 @@ class MainSidebarViewController: UIViewController { present(navController, animated: true) } + private func userActivityForItem(_ item: Item) -> NSUserActivity? { + guard let id = mastodonController.accountInfo?.id else { return nil } + + switch item { + case .tab(.notifications): + return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode) + case .tab(.compose): + return UserActivityManager.newPostActivity(accountID: id) + case .search: + return UserActivityManager.searchActivity() + case .bookmarks: + return UserActivityManager.bookmarksActivity() + case .tab(.myProfile): + return UserActivityManager.myProfileActivity() + case let .list(list): + return UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: id) + case let .savedHashtag(tag): + return UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: tag.name), accountID: id) + case .savedInstance(_): + // todo: show timeline activity doesn't work for public timelines + return nil + default: + return nil + } + } + } @available(iOS 14.0, *) @@ -367,6 +394,19 @@ extension MainSidebarViewController: UICollectionViewDelegate { } } +@available(iOS 14.0, *) +extension MainSidebarViewController: UICollectionViewDragDelegate { + func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + guard let item = dataSource.itemIdentifier(for: indexPath), + let activity = userActivityForItem(item) else { + return [] + } + + let provider = NSItemProvider(object: activity) + return [UIDragItem(itemProvider: provider)] + } +} + @available(iOS 14.0, *) extension MainSidebarViewController: InstanceTimelineViewControllerDelegate { func didSaveInstance(url: URL) { diff --git a/Tusker/Screens/Notifications/NotificationsPageViewController.swift b/Tusker/Screens/Notifications/NotificationsPageViewController.swift index 6a23f5c6..8b5c38d5 100644 --- a/Tusker/Screens/Notifications/NotificationsPageViewController.swift +++ b/Tusker/Screens/Notifications/NotificationsPageViewController.swift @@ -16,16 +16,19 @@ class NotificationsPageViewController: SegmentedPageViewController { weak var mastodonController: MastodonController! - init(mastodonController: MastodonController) { + var initialMode: NotificationsMode? + + init(initialMode: NotificationsMode? = nil, mastodonController: MastodonController) { + self.initialMode = initialMode self.mastodonController = mastodonController let notifications = NotificationsTableViewController(allowedTypes: Pachyderm.Notification.Kind.allCases, mastodonController: mastodonController) notifications.title = notificationsTitle - notifications.userActivity = UserActivityManager.checkNotificationsActivity() + notifications.userActivity = UserActivityManager.checkNotificationsActivity(mode: .allNotifications) let mentions = NotificationsTableViewController(allowedTypes: [.mention], mastodonController: mastodonController) mentions.title = mentionsTitle - mentions.userActivity = UserActivityManager.checkMentionsActivity() + mentions.userActivity = UserActivityManager.checkNotificationsActivity(mode: .mentionsOnly) super.init(titles: [ notificationsTitle, @@ -46,7 +49,7 @@ class NotificationsPageViewController: SegmentedPageViewController { override func viewDidLoad() { super.viewDidLoad() - selectMode(Preferences.shared.defaultNotificationsMode) + selectMode(initialMode ?? Preferences.shared.defaultNotificationsMode) } func selectMode(_ mode: NotificationsMode) { diff --git a/Tusker/Screens/Preferences/PreferencesNavigationController.swift b/Tusker/Screens/Preferences/PreferencesNavigationController.swift index 35e1e3d5..c3446174 100644 --- a/Tusker/Screens/Preferences/PreferencesNavigationController.swift +++ b/Tusker/Screens/Preferences/PreferencesNavigationController.swift @@ -63,7 +63,8 @@ class PreferencesNavigationController: UINavigationController { // the propper fix would be to figure out what's leaking instances of this class guard let window = self.view.window, let windowScene = window.windowScene, - let sceneDelegate = windowScene.delegate as? SceneDelegate else { + // todo: my profile can be torn off into a separate window, this doesn't work + let sceneDelegate = windowScene.delegate as? MainSceneDelegate else { return } let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo @@ -76,7 +77,8 @@ class PreferencesNavigationController: UINavigationController { @objc func userLoggedOut() { guard let window = self.view.window, let windowScene = window.windowScene, - let sceneDelegate = windowScene.delegate as? SceneDelegate else { + // todo: my profile can be torn off into a separate window, this doesn't work + let sceneDelegate = windowScene.delegate as? MainSceneDelegate else { return } isSwitchingAccounts = true @@ -90,7 +92,8 @@ class PreferencesNavigationController: UINavigationController { extension PreferencesNavigationController: OnboardingViewControllerDelegate { func didFinishOnboarding(account: LocalData.UserAccountInfo) { DispatchQueue.main.async { - let sceneDelegate = self.view.window!.windowScene!.delegate as! SceneDelegate + // todo: my profile can be torn off into a separate window, this will crash + let sceneDelegate = self.view.window!.windowScene!.delegate as! MainSceneDelegate self.dismiss(animated: true) { // dismiss instance selector self.dismiss(animated: true) { // dismiss preferences sceneDelegate.activateAccount(account, animated: false) diff --git a/Tusker/Screens/Profile/MyProfileViewController.swift b/Tusker/Screens/Profile/MyProfileViewController.swift index 5860e12e..4219fd1e 100644 --- a/Tusker/Screens/Profile/MyProfileViewController.swift +++ b/Tusker/Screens/Profile/MyProfileViewController.swift @@ -37,6 +37,8 @@ class MyProfileViewController: ProfileViewController { navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Preferences", style: .plain, target: self, action: #selector(preferencesPressed)) NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) + + userActivity = UserActivityManager.myProfileActivity() } private func setAvatarTabBarImage(account: Account) { diff --git a/Tusker/Screens/Profile/ProfileViewController.swift b/Tusker/Screens/Profile/ProfileViewController.swift index e60c7aa9..0653acb7 100644 --- a/Tusker/Screens/Profile/ProfileViewController.swift +++ b/Tusker/Screens/Profile/ProfileViewController.swift @@ -78,6 +78,8 @@ class ProfileViewController: UIPageViewController { } navigationItem.rightBarButtonItem = composeButton + userActivity = UserActivityManager.showProfileActivity(id: accountID!, accountID: mastodonController.accountInfo!.id) + headerView = ProfileHeaderView.create() headerView.delegate = self diff --git a/Tusker/Screens/Search/SearchViewController.swift b/Tusker/Screens/Search/SearchViewController.swift index 71153eef..f5200518 100644 --- a/Tusker/Screens/Search/SearchViewController.swift +++ b/Tusker/Screens/Search/SearchViewController.swift @@ -32,6 +32,8 @@ class SearchViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .systemBackground + resultsController = SearchResultsViewController(mastodonController: mastodonController) resultsController.exploreNavigationController = self.navigationController searchController = UISearchController(searchResultsController: resultsController) diff --git a/Tusker/Screens/Timeline/TimelineTableViewController.swift b/Tusker/Screens/Timeline/TimelineTableViewController.swift index d350a221..c77c5d23 100644 --- a/Tusker/Screens/Timeline/TimelineTableViewController.swift +++ b/Tusker/Screens/Timeline/TimelineTableViewController.swift @@ -28,7 +28,9 @@ class TimelineTableViewController: TimelineLikeTableViewController String { @@ -171,3 +175,18 @@ extension TimelineTableViewController: UITableViewDataSourcePrefetching { } } } + +extension TimelineTableViewController: UITableViewDragDelegate { + func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { + let id = item(for: indexPath).id + guard let status = mastodonController.persistentContainer.status(for: id), + let accountId = mastodonController.accountInfo?.id else { + return [] + } + let activity = UserActivityManager.showConversationActivity(mainStatusID: id, accountID: accountId) + let itemProvider = NSItemProvider(object: status.url! as NSURL) + itemProvider.registerObject(activity, visibility: .all) + let dragItem = UIDragItem(itemProvider: itemProvider) + return [dragItem] + } +} diff --git a/Tusker/Screens/Utilities/Previewing.swift b/Tusker/Screens/Utilities/Previewing.swift index b6c0a82a..60177302 100644 --- a/Tusker/Screens/Utilities/Previewing.swift +++ b/Tusker/Screens/Utilities/Previewing.swift @@ -189,7 +189,7 @@ extension MenuPreviewProvider { })) } - let shareSection = [ + var shareSection = [ openInSafariAction(url: status.url!), createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in guard let self = self else { return } @@ -197,14 +197,30 @@ extension MenuPreviewProvider { }), ] + #if targetEnvironment(macCatalyst) + shareSection.append(createAction(identifier: "new_window", title: "Open in New Window", systemImageName: "", handler: { (_) in + guard let id = mastodonController.accountInfo?.id else { + return + } + // todo: this should try to find an existing session + UIApplication.shared.requestSceneSessionActivation(nil, userActivity: UserActivityManager.showConversationActivity(mainStatusID: statusID, accountID: id), options: nil, errorHandler: nil) + })) + #endif + return [ UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection), UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection), ] } - private func createAction(identifier: String, title: String, systemImageName: String, handler: @escaping UIActionHandler) -> UIAction { - return UIAction(title: title, image: UIImage(systemName: systemImageName), identifier: UIAction.Identifier(identifier), discoverabilityTitle: nil, attributes: [], state: .off, handler: handler) + private func createAction(identifier: String, title: String, systemImageName: String?, handler: @escaping UIActionHandler) -> UIAction { + let image: UIImage? + if let name = systemImageName { + image = UIImage(systemName: name) + } else { + image = nil + } + return UIAction(title: title, image: image, identifier: UIAction.Identifier(identifier), discoverabilityTitle: nil, attributes: [], state: .off, handler: handler) } private func openInSafariAction(url: URL) -> UIAction { diff --git a/Tusker/Shortcuts/UserActivityManager.swift b/Tusker/Shortcuts/UserActivityManager.swift index faf95ad5..37795a49 100644 --- a/Tusker/Shortcuts/UserActivityManager.swift +++ b/Tusker/Shortcuts/UserActivityManager.swift @@ -30,18 +30,27 @@ class UserActivityManager { private static func present(_ vc: UIViewController, animated: Bool = true) { getMainViewController().present(vc, animated: animated) } + + static func getAccount(from activity: NSUserActivity) -> LocalData.UserAccountInfo? { + guard let id = activity.userInfo?["accountID"] as? String else { + return nil + } + return LocalData.shared.getAccount(id: id) + } // MARK: - New Post - static func newPostActivity(mentioning: Account? = nil) -> NSUserActivity { + static func newPostActivity(mentioning: Account? = nil, accountID: String) -> NSUserActivity { // todo: update to use managed objects let activity = NSUserActivity(type: .newPost) activity.isEligibleForPrediction = true + activity.userInfo = [ + "accountID": accountID, + ] if let mentioning = mentioning { - activity.userInfo = ["mentioning": mentioning.acct] + activity.userInfo!["mentioning"] = mentioning.acct activity.title = "Send a message to \(mentioning.displayName)" activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayName)" } else { - activity.userInfo = [:] activity.title = "New Post" activity.suggestedInvocationPhrase = "Post in Tusker" } @@ -56,12 +65,39 @@ class UserActivityManager { present(UINavigationController(rootViewController: composeVC)) } + static func editDraftActivity(id: UUID, accountID: String) -> NSUserActivity { + let activity = NSUserActivity(type: .newPost) + activity.userInfo = [ + "accountID": accountID, + "draftID": id.uuidString, + ] + return activity + } + + static func getDraft(from activity: NSUserActivity) -> Draft? { + guard activity.activityType == UserActivityType.newPost.rawValue, + let str = activity.userInfo?["draftID"] as? String, + let uuid = UUID(uuidString: str) else { + return nil + } + return DraftsManager.shared.getBy(id: uuid) + } + // MARK: - Check Notifications - static func checkNotificationsActivity() -> NSUserActivity { + static func checkNotificationsActivity(mode: NotificationsMode = .allNotifications) -> NSUserActivity { let activity = NSUserActivity(type: .checkNotifications) activity.isEligibleForPrediction = true - activity.title = NSLocalizedString("Check Notifications", comment: "check notifications shortcut title") - activity.suggestedInvocationPhrase = NSLocalizedString("Check my Tusker notifications", comment: "check notifications shortcut invocation phrase") + activity.addUserInfoEntries(from: [ + "notificationsMode": mode.rawValue + ]) + switch mode { + case .allNotifications: + activity.title = NSLocalizedString("Check Notifications", comment: "check notifications shortcut title") + activity.suggestedInvocationPhrase = NSLocalizedString("Check my Tusker notifications", comment: "check notifications shortcut invocation phrase") + case .mentionsOnly: + activity.title = NSLocalizedString("Check Mentions", comment: "check mentions shortcut title") + activity.suggestedInvocationPhrase = NSLocalizedString("Check my mentions", comment: "check mentions shortcut invocation phrase") + } return activity } @@ -72,37 +108,27 @@ class UserActivityManager { let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController { navigationController.popToRootViewController(animated: false) notificationsPageController.loadViewIfNeeded() - notificationsPageController.selectMode(.allNotifications) + notificationsPageController.selectMode(getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode) } } - // MARK: - Check Mentions - static func checkMentionsActivity() -> NSUserActivity { - let activity = NSUserActivity(type: .checkMentions) - activity.isEligibleForPrediction = true - activity.title = NSLocalizedString("Check Mentions", comment: "check mentions shortcut title") - activity.suggestedInvocationPhrase = NSLocalizedString("Check my mentions", comment: "check mentions shortcut invocation phrase") - return activity - } - - static func handleCheckMentions(activity: NSUserActivity) { - let mainViewController = getMainViewController() - mainViewController.select(tab: .notifications) - if let navController = mainViewController.getTabController(tab: .notifications) as? UINavigationController, - let notificationsPageController = navController.viewControllers.first as? NotificationsPageViewController { - navController.popToRootViewController(animated: false) - notificationsPageController.loadViewIfNeeded() - notificationsPageController.selectMode(.mentionsOnly) + static func getNotificationsMode(from activity: NSUserActivity) -> NotificationsMode? { + guard let str = activity.userInfo?["notificationsMode"] as? String else { + return nil } + return NotificationsMode(rawValue: str) } // MARK: - Show Timeline - static func showTimelineActivity(timeline: Timeline) -> NSUserActivity? { + static func showTimelineActivity(timeline: Timeline, accountID: String) -> NSUserActivity? { guard let timelineData = try? encoder.encode(timeline) else { return nil } let activity = NSUserActivity(type: .showTimeline) activity.isEligibleForPrediction = true - activity.userInfo = ["timelineData": timelineData] + activity.userInfo = [ + "timelineData": timelineData, + "accountID": accountID, + ] switch timeline { case .home: activity.title = NSLocalizedString("Show Home Timeline", comment: "home timeline shortcut title") @@ -127,11 +153,16 @@ class UserActivityManager { return activity } - static func handleShowTimeline(activity: NSUserActivity) { - guard let timelineData = activity.userInfo?["timelineData"] as? Data, - let timeline = try? decoder.decode(Timeline.self, from: timelineData) else { - return + static func getTimeline(from activity: NSUserActivity) -> Timeline? { + guard activity.activityType == UserActivityType.showTimeline.rawValue, + let data = activity.userInfo?["timelineData"] as? Data else { + return nil } + return try? decoder.decode(Timeline.self, from: data) + } + + static func handleShowTimeline(activity: NSUserActivity) { + guard let timeline = getTimeline(from: activity) else { return } let mainViewController = getMainViewController() mainViewController.select(tab: .timelines) @@ -162,6 +193,21 @@ class UserActivityManager { } } + // MARK: - Show Conversation + static func showConversationActivity(mainStatusID: String, accountID: String, isEligibleForPrediction: Bool = false) -> NSUserActivity { + let activity = NSUserActivity(type: .showConversation) + activity.userInfo = [ + "mainStatusID": mainStatusID, + "accountID": accountID, + ] + activity.isEligibleForPrediction = isEligibleForPrediction + return activity + } + + static func getConversationStatus(from activity: NSUserActivity) -> String? { + return activity.userInfo?["mainStatusID"] as? String + } + // MARK: - Explore static func searchActivity() -> NSUserActivity { @@ -200,4 +246,33 @@ class UserActivityManager { } } + // MARK: - My Profile + static func myProfileActivity() -> NSUserActivity { + let activity = NSUserActivity(type: .myProfile) + activity.isEligibleForPrediction = true + activity.title = NSLocalizedString("My Profile", comment: "my profile shortcut title") + activity.suggestedInvocationPhrase = NSLocalizedString("Show my Mastodon profile", comment: "my profile shortuct invocation phrase") + return activity + } + + static func handleMyProfile(activity: NSUserActivity) { + let mainViewController = getMainViewController() + mainViewController.select(tab: .myProfile) + } + + // MARK: - Show Profile + static func showProfileActivity(id profileID: String, accountID: String) -> NSUserActivity { + let activity = NSUserActivity(type: .showProfile) + activity.userInfo = [ + "profileID": profileID, + "accountID": accountID, + ] + // todo: should this be eligible for prediction? + return activity + } + + static func getProfile(from activity: NSUserActivity) -> String? { + return activity.userInfo?["profileID"] as? String + } + } diff --git a/Tusker/Shortcuts/UserActivityType.swift b/Tusker/Shortcuts/UserActivityType.swift index b525fbc5..77c9323d 100644 --- a/Tusker/Shortcuts/UserActivityType.swift +++ b/Tusker/Shortcuts/UserActivityType.swift @@ -9,12 +9,14 @@ import Foundation enum UserActivityType: String { - case newPost = "net.shadowfacts.Tusker.activity.new-post" - case checkNotifications = "net.shadowfacts.Tusker.activity.check-notifications" - case checkMentions = "net.shadowfacts.Tusker.activity.check-mentions" - case showTimeline = "net.shadowfacts.Tusker.activity.show-timeline" - case search = "net.shadowfacts.Tusker.activity.search" - case bookmarks = "net.shadowfacts.Tusker.activity.bookmarks" + case newPost = "space.vaccor.Tusker.activity.new-post" + case checkNotifications = "space.vaccor.Tusker.activity.check-notifications" + case showTimeline = "space.vaccor.Tusker.activity.show-timeline" + case search = "space.vaccor.Tusker.activity.search" + case bookmarks = "space.vaccor.Tusker.activity.bookmarks" + case showConversation = "space.vaccor.Tusker.activity.show-conversation" + case myProfile = "space.vaccor.Tusker.activity.my-profile" + case showProfile = "space.vaccor.Tusker.activity.show-profile" } extension UserActivityType { @@ -24,14 +26,18 @@ extension UserActivityType { return UserActivityManager.handleNewPost case .checkNotifications: return UserActivityManager.handleCheckNotifications - case .checkMentions: - return UserActivityManager.handleCheckMentions case .showTimeline: return UserActivityManager.handleShowTimeline case .search: return UserActivityManager.handleSearch case .bookmarks: return UserActivityManager.handleBookmarks + case .showConversation: + fatalError("cannot handle show conversation activity") + case .myProfile: + return UserActivityManager.handleMyProfile + case .showProfile: + fatalError("cannot handle show profile activity") } } } diff --git a/Tusker/Views/Status/BaseStatusTableViewCell.swift b/Tusker/Views/Status/BaseStatusTableViewCell.swift index 0081a3f0..14d04e10 100644 --- a/Tusker/Views/Status/BaseStatusTableViewCell.swift +++ b/Tusker/Views/Status/BaseStatusTableViewCell.swift @@ -81,9 +81,10 @@ class BaseStatusTableViewCell: UITableViewCell { displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) usernameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) + avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) - avatarImageView.layer.masksToBounds = true + avatarImageView.addInteraction(UIDragInteraction(delegate: self)) attachmentsView.delegate = self @@ -479,3 +480,13 @@ extension BaseStatusTableViewCell: MenuPreviewProvider { return self.getStatusCellPreviewProviders(for: location, sourceViewController: sourceViewController) } } + +extension BaseStatusTableViewCell: UIDragInteractionDelegate { + func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] { + guard let currentAccountID = mastodonController.accountInfo?.id else { + return [] + } + let provider = NSItemProvider(object: UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)) + return [UIDragItem(itemProvider: provider)] + } +}