Add multi-window support and auxiliary windows

This commit is contained in:
Shadowfacts 2020-12-13 22:37:37 -05:00
parent 67a029180e
commit 522c9b2b03
24 changed files with 533 additions and 89 deletions

View File

@ -186,6 +186,7 @@
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; }; D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; }; D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
D686BBE324FBF8110068E6AA /* WrappedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D686BBE224FBF8110068E6AA /* WrappedProgressView.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 */; }; D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */; };
D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.swift */; }; D68E525D24A3E8F00054355A /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E525C24A3E8F00054355A /* SearchViewController.swift */; };
D68E6F59253C9969001A1B4C /* MultiSourceEmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.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 */; }; D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */; };
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; }; D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3723AC739F005C403C /* InstanceTimelineViewController.swift */; };
D6945C3A23AC75E2005C403C /* FindInstanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6945C3923AC75E2005C403C /* FindInstanceViewController.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 */; }; D6969E9E240C81B9002843CE /* NSTextAttachment+Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */; };
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; }; D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6969E9F240C8384002843CE /* EmojiLabel.swift */; };
D6969EA4240DD28D002843CE /* UnknownNotificationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */; }; 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 */; }; D6A5BB2D23BBA9C4003BF21D /* MyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */; };
D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; }; D6A5BB2F23BBAC97003BF21D /* DelegatingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */; };
D6A5BB3123BBAD87003BF21D /* JSONResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A5BB3023BBAD87003BF21D /* JSONResponse.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 */; }; D6ACE1AC240C3BAD004EA8E2 /* Ambassador.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F613023AE99E000F3CFD3 /* Ambassador.framework */; };
D6ACE1AD240C3BAD004EA8E2 /* Embassy.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D65F612D23AE990C00F3CFD3 /* Embassy.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 */; }; 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 = "<group>"; }; D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; }; D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; }; D686BBE224FBF8110068E6AA /* WrappedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedProgressView.swift; sourceTree = "<group>"; };
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; }; D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; }; D68E525C24A3E8F00054355A /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSourceEmojiLabel.swift; sourceTree = "<group>"; }; D68E6F58253C9969001A1B4C /* MultiSourceEmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiSourceEmojiLabel.swift; sourceTree = "<group>"; };
@ -554,6 +558,8 @@
D6945C3323AC6431005C403C /* AddSavedHashtagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSavedHashtagViewController.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>"; }; 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; }; 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 = "<group>"; };
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeSceneDelegate.swift; sourceTree = "<group>"; };
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = "<group>"; }; D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextAttachment+Emoji.swift"; sourceTree = "<group>"; };
D6969E9F240C8384002843CE /* EmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLabel.swift; sourceTree = "<group>"; }; D6969E9F240C8384002843CE /* EmojiLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiLabel.swift; sourceTree = "<group>"; };
D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UnknownNotificationTableViewCell.xib; sourceTree = "<group>"; }; D6969EA2240DD28D002843CE /* UnknownNotificationTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UnknownNotificationTableViewCell.xib; sourceTree = "<group>"; };
@ -575,7 +581,7 @@
D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; }; D6A5BB2C23BBA9C4003BF21D /* MyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileTests.swift; sourceTree = "<group>"; };
D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; }; D6A5BB2E23BBAC97003BF21D /* DelegatingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatingResponse.swift; sourceTree = "<group>"; };
D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; }; D6A5BB3023BBAD87003BF21D /* JSONResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONResponse.swift; sourceTree = "<group>"; };
D6AC956623C4347E008C9946 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = "<group>"; };
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; }; D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; }; D6AEBB402321642700E5038B /* SendMesasgeActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMesasgeActivity.swift; sourceTree = "<group>"; };
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; }; D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
@ -1177,6 +1183,7 @@
D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */, D6D3F4C324FDB6B700EC4A6A /* View+ConditionalModifier.swift */,
D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */, D6D4CC90250D2C3100FCCF8D /* UIAccessibility.swift */,
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */, D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */,
D69693F32585941A00F4E116 /* UIWindowSceneDelegate+Close.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1421,7 +1428,9 @@
children = ( children = (
D6E4885C24A2890C0011C13E /* Tusker.entitlements */, D6E4885C24A2890C0011C13E /* Tusker.entitlements */,
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */, D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
D6AC956623C4347E008C9946 /* SceneDelegate.swift */, D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */,
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */,
D69693F925859A8000F4E116 /* ComposeSceneDelegate.swift */,
D64D0AAC2128D88B005A6F37 /* LocalData.swift */, D64D0AAC2128D88B005A6F37 /* LocalData.swift */,
D6945C2E23AC47C3005C403C /* SavedDataManager.swift */, D6945C2E23AC47C3005C403C /* SavedDataManager.swift */,
D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */, D6C693EE216192C2007D6A6D /* TuskerNavigationDelegate.swift */,
@ -1854,6 +1863,7 @@
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */, D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */,
D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */, D64BC18F23C18B9D000D0238 /* FollowRequestNotificationTableViewCell.swift in Sources */,
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */, D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */,
D69693FA25859A8000F4E116 /* ComposeSceneDelegate.swift in Sources */,
D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */, D65234C9256189D0001AF9CF /* TimelineLikeTableViewController.swift in Sources */,
D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */, D6A3BC8A2321F79B00FD64D5 /* AccountTableViewCell.swift in Sources */,
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */, D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */,
@ -1875,7 +1885,7 @@
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */, D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */, D627943723A552C200D38C68 /* BookmarkStatusActivity.swift in Sources */,
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */, D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
D6AC956723C4347E008C9946 /* SceneDelegate.swift in Sources */, D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */, D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */, D60E2F2E244248BF005F8713 /* MastodonCachePersistentStore.swift in Sources */,
D620483623D38075008A63EF /* ContentTextView.swift in Sources */, D620483623D38075008A63EF /* ContentTextView.swift in Sources */,
@ -1946,6 +1956,7 @@
D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */, D622757424EDF1CD00B82A16 /* ComposeAttachmentsList.swift in Sources */,
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */, D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */, D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */, D6C143DA253510F4007DC240 /* ComposeContentWarningTextField.swift in Sources */,
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */, 0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */,
D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */, D627943923A553B600D38C68 /* UnbookmarkStatusActivity.swift in Sources */,
@ -1983,6 +1994,7 @@
D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */, D6D3F4C424FDB6B700EC4A6A /* View+ConditionalModifier.swift in Sources */,
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */, D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */,
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */, D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */,
D68C2AE325869BAB00548EFF /* AuxiliarySceneDelegate.swift in Sources */,
D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */, D6AEBB432321685E00E5038B /* OpenInSafariActivity.swift in Sources */,
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */, D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */, D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,

View File

@ -52,4 +52,33 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
MenuController.buildMainMenu(builder: builder) 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)")
}
}
} }

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -51,21 +51,24 @@
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>Post videos from the camera.</string> <string>Post videos from the camera.</string>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>Save photos directly from other people's posts.</string> <string>Save photos directly from other people&apos;s posts.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Post photos from the photo library.</string> <string>Post photos from the photo library.</string>
<key>NSUserActivityTypes</key> <key>NSUserActivityTypes</key>
<array> <array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-conversation</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-timeline</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-notifications</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.check-mentions</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.new-post</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.new-post</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.search</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.search</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.bookmarks</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.my-profile</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).activity.show-profile</string>
</array> </array>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>
<false/> <true/>
<key>UISceneConfigurations</key> <key>UISceneConfigurations</key>
<dict> <dict>
<key>UIWindowSceneSessionRoleApplication</key> <key>UIWindowSceneSessionRoleApplication</key>
@ -76,7 +79,23 @@
<key>UISceneConfigurationName</key> <key>UISceneConfigurationName</key>
<string>main-scene</string> <string>main-scene</string>
<key>UISceneDelegateClassName</key> <key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string> <string>$(PRODUCT_MODULE_NAME).MainSceneDelegate</string>
</dict>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).AuxiliarySceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>auxiliary</string>
</dict>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).ComposeSceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>compose</string>
</dict> </dict>
</array> </array>
</dict> </dict>

View File

@ -1,5 +1,5 @@
// //
// SceneDelegate.swift // MainSceneDelegate.swift
// Tusker // Tusker
// //
// Created by Shadowfacts on 1/6/20. // Created by Shadowfacts on 1/6/20.
@ -11,7 +11,7 @@ import Pachyderm
import CrashReporter import CrashReporter
import MessageUI import MessageUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate { class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow? var window: UIWindow?
@ -195,13 +195,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
} }
extension SceneDelegate: OnboardingViewControllerDelegate { extension MainSceneDelegate: OnboardingViewControllerDelegate {
func didFinishOnboarding(account: LocalData.UserAccountInfo) { func didFinishOnboarding(account: LocalData.UserAccountInfo) {
activateAccount(account, animated: false) activateAccount(account, animated: false)
} }
} }
extension SceneDelegate: MFMailComposeViewControllerDelegate { extension MainSceneDelegate: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true) { controller.dismiss(animated: true) {
self.showAppOrOnboardingUI() self.showAppOrOnboardingUI()

View File

@ -47,4 +47,8 @@ class DraftsManager: Codable {
drafts.removeAll { $0 == draft } drafts.removeAll { $0 == draft }
} }
func getBy(id: UUID) -> Draft? {
return drafts.first { $0.id == id }
}
} }

View File

@ -11,8 +11,14 @@ import Combine
import Pachyderm import Pachyderm
import PencilKit import PencilKit
protocol ComposeHostingControllerDelegate: class {
func dismissCompose(mode: ComposeUIState.DismissMode) -> Bool
}
class ComposeHostingController: UIHostingController<ComposeContainerView> { class ComposeHostingController: UIHostingController<ComposeContainerView> {
weak var delegate: ComposeHostingControllerDelegate?
let mastodonController: MastodonController let mastodonController: MastodonController
let uiState: ComposeUIState let uiState: ComposeUIState
@ -61,7 +67,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self) pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
userActivity = UserActivityManager.newPostActivity() userActivity = UserActivityManager.newPostActivity(accountID: mastodonController.accountInfo!.id)
self.uiState.$draft self.uiState.$draft
.flatMap(\.$visibility) .flatMap(\.$visibility)
@ -81,21 +87,6 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
fatalError("init(coder:) has not been implemented") 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?) { override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent) super.didMove(toParent: parent)
@ -287,9 +278,12 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
extension ComposeHostingController: ComposeUIStateDelegate { extension ComposeHostingController: ComposeUIStateDelegate {
var assetPickerDelegate: AssetPickerViewControllerDelegate? { self } var assetPickerDelegate: AssetPickerViewControllerDelegate? { self }
func dismissCompose() { func dismissCompose(mode: ComposeUIState.DismissMode) {
let dismissed = delegate?.dismissCompose(mode: mode) ?? false
if !dismissed {
self.dismiss(animated: true) self.dismiss(animated: true)
} }
}
func presentAssetPickerSheet() { func presentAssetPickerSheet() {
let sheetContainer = AssetPickerSheetContainerViewController() let sheetContainer = AssetPickerSheetContainerViewController()

View File

@ -11,7 +11,7 @@ import SwiftUI
protocol ComposeUIStateDelegate: class { protocol ComposeUIStateDelegate: class {
var assetPickerDelegate: AssetPickerViewControllerDelegate? { get } var assetPickerDelegate: AssetPickerViewControllerDelegate? { get }
func dismissCompose() func dismissCompose(mode: ComposeUIState.DismissMode)
func presentAssetPickerSheet() func presentAssetPickerSheet()
func presentComposeDrawing() func presentComposeDrawing()
@ -54,6 +54,12 @@ extension ComposeUIState {
} }
} }
extension ComposeUIState {
enum DismissMode {
case cancel, post
}
}
protocol ComposeAutocompleteHandler: class { protocol ComposeAutocompleteHandler: class {
func autocomplete(with string: String) func autocomplete(with string: String)
} }

View File

@ -168,14 +168,14 @@ struct ComposeView: View {
private func cancel() { private func cancel() {
if Preferences.shared.automaticallySaveDrafts { if Preferences.shared.automaticallySaveDrafts {
// draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear // draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear
uiState.delegate?.dismissCompose() uiState.delegate?.dismissCompose(mode: .cancel)
} else { } else {
// if the draft doesn't have content, it doesn't need to be saved // if the draft doesn't have content, it doesn't need to be saved
if draft.hasContent { if draft.hasContent {
uiState.isShowingSaveDraftSheet = true uiState.isShowingSaveDraftSheet = true
} else { } else {
DraftsManager.shared.remove(draft) DraftsManager.shared.remove(draft)
uiState.delegate?.dismissCompose() uiState.delegate?.dismissCompose(mode: .cancel)
} }
} }
} }
@ -185,12 +185,12 @@ struct ComposeView: View {
.default(Text("Save Draft"), action: { .default(Text("Save Draft"), action: {
// draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear // draft is already stored in drafts manager, drafts manager is saved by ComposeHostingController.viewWillDisappear
uiState.isShowingSaveDraftSheet = false uiState.isShowingSaveDraftSheet = false
uiState.delegate?.dismissCompose() uiState.delegate?.dismissCompose(mode: .cancel)
}), }),
.destructive(Text("Delete Draft"), action: { .destructive(Text("Delete Draft"), action: {
DraftsManager.shared.remove(draft) DraftsManager.shared.remove(draft)
uiState.isShowingSaveDraftSheet = false uiState.isShowingSaveDraftSheet = false
uiState.delegate?.dismissCompose() uiState.delegate?.dismissCompose(mode: .cancel)
}), }),
.cancel(), .cancel(),
]) ])
@ -242,7 +242,7 @@ struct ComposeView: View {
// wait .25 seconds so the user can see the progress bar has completed // wait .25 seconds so the user can see the progress bar has completed
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) { DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
self.uiState.delegate?.dismissCompose() self.uiState.delegate?.dismissCompose(mode: .post)
} }
} }
} }

View File

@ -119,7 +119,7 @@ class CrashReporterViewController: UIViewController {
} }
@IBAction func cancelPressed(_ sender: Any) { @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 { extension CrashReporterViewController: MFMailComposeViewControllerDelegate {
func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) {
controller.dismiss(animated: true) { controller.dismiss(animated: true) {
(self.view.window!.windowScene!.delegate as! SceneDelegate).showAppOrOnboardingUI() (self.view.window!.windowScene!.delegate as! MainSceneDelegate).showAppOrOnboardingUI()
} }
} }
} }

View File

@ -131,7 +131,7 @@ class FastAccountSwitcherViewController: UIViewController {
selectionChangedFeedbackGenerator = nil selectionChangedFeedbackGenerator = nil
hide() { hide() {
(self.view.window!.windowScene!.delegate as! SceneDelegate).activateAccount(account, animated: true) (self.view.window!.windowScene!.delegate as! MainSceneDelegate).activateAccount(account, animated: true)
} }
} else { } else {
hide() hide()

View File

@ -80,6 +80,7 @@ class MainSidebarViewController: UIViewController {
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.backgroundColor = .clear collectionView.backgroundColor = .clear
collectionView.delegate = self collectionView.delegate = self
collectionView.dragDelegate = self
view.addSubview(collectionView) view.addSubview(collectionView)
dataSource = createDataSource() dataSource = createDataSource()
@ -220,6 +221,32 @@ class MainSidebarViewController: UIViewController {
present(navController, animated: true) 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, *) @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, *) @available(iOS 14.0, *)
extension MainSidebarViewController: InstanceTimelineViewControllerDelegate { extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
func didSaveInstance(url: URL) { func didSaveInstance(url: URL) {

View File

@ -16,16 +16,19 @@ class NotificationsPageViewController: SegmentedPageViewController {
weak var mastodonController: MastodonController! weak var mastodonController: MastodonController!
init(mastodonController: MastodonController) { var initialMode: NotificationsMode?
init(initialMode: NotificationsMode? = nil, mastodonController: MastodonController) {
self.initialMode = initialMode
self.mastodonController = mastodonController self.mastodonController = mastodonController
let notifications = NotificationsTableViewController(allowedTypes: Pachyderm.Notification.Kind.allCases, mastodonController: mastodonController) let notifications = NotificationsTableViewController(allowedTypes: Pachyderm.Notification.Kind.allCases, mastodonController: mastodonController)
notifications.title = notificationsTitle notifications.title = notificationsTitle
notifications.userActivity = UserActivityManager.checkNotificationsActivity() notifications.userActivity = UserActivityManager.checkNotificationsActivity(mode: .allNotifications)
let mentions = NotificationsTableViewController(allowedTypes: [.mention], mastodonController: mastodonController) let mentions = NotificationsTableViewController(allowedTypes: [.mention], mastodonController: mastodonController)
mentions.title = mentionsTitle mentions.title = mentionsTitle
mentions.userActivity = UserActivityManager.checkMentionsActivity() mentions.userActivity = UserActivityManager.checkNotificationsActivity(mode: .mentionsOnly)
super.init(titles: [ super.init(titles: [
notificationsTitle, notificationsTitle,
@ -46,7 +49,7 @@ class NotificationsPageViewController: SegmentedPageViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
selectMode(Preferences.shared.defaultNotificationsMode) selectMode(initialMode ?? Preferences.shared.defaultNotificationsMode)
} }
func selectMode(_ mode: NotificationsMode) { func selectMode(_ mode: NotificationsMode) {

View File

@ -63,7 +63,8 @@ class PreferencesNavigationController: UINavigationController {
// the propper fix would be to figure out what's leaking instances of this class // the propper fix would be to figure out what's leaking instances of this class
guard let window = self.view.window, guard let window = self.view.window,
let windowScene = window.windowScene, 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 return
} }
let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo let account = notification.userInfo!["account"] as! LocalData.UserAccountInfo
@ -76,7 +77,8 @@ class PreferencesNavigationController: UINavigationController {
@objc func userLoggedOut() { @objc func userLoggedOut() {
guard let window = self.view.window, guard let window = self.view.window,
let windowScene = window.windowScene, 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 return
} }
isSwitchingAccounts = true isSwitchingAccounts = true
@ -90,7 +92,8 @@ class PreferencesNavigationController: UINavigationController {
extension PreferencesNavigationController: OnboardingViewControllerDelegate { extension PreferencesNavigationController: OnboardingViewControllerDelegate {
func didFinishOnboarding(account: LocalData.UserAccountInfo) { func didFinishOnboarding(account: LocalData.UserAccountInfo) {
DispatchQueue.main.async { 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 instance selector
self.dismiss(animated: true) { // dismiss preferences self.dismiss(animated: true) { // dismiss preferences
sceneDelegate.activateAccount(account, animated: false) sceneDelegate.activateAccount(account, animated: false)

View File

@ -37,6 +37,8 @@ class MyProfileViewController: ProfileViewController {
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Preferences", style: .plain, target: self, action: #selector(preferencesPressed)) navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Preferences", style: .plain, target: self, action: #selector(preferencesPressed))
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
userActivity = UserActivityManager.myProfileActivity()
} }
private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) { private func setAvatarTabBarImage<Account: AccountProtocol>(account: Account) {

View File

@ -78,6 +78,8 @@ class ProfileViewController: UIPageViewController {
} }
navigationItem.rightBarButtonItem = composeButton navigationItem.rightBarButtonItem = composeButton
userActivity = UserActivityManager.showProfileActivity(id: accountID!, accountID: mastodonController.accountInfo!.id)
headerView = ProfileHeaderView.create() headerView = ProfileHeaderView.create()
headerView.delegate = self headerView.delegate = self

View File

@ -32,6 +32,8 @@ class SearchViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
view.backgroundColor = .systemBackground
resultsController = SearchResultsViewController(mastodonController: mastodonController) resultsController = SearchResultsViewController(mastodonController: mastodonController)
resultsController.exploreNavigationController = self.navigationController resultsController.exploreNavigationController = self.navigationController
searchController = UISearchController(searchResultsController: resultsController) searchController = UISearchController(searchResultsController: resultsController)

View File

@ -28,7 +28,9 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
title = timeline.title title = timeline.title
tabBarItem.image = timeline.tabBarImage tabBarItem.image = timeline.tabBarImage
userActivity = UserActivityManager.showTimelineActivity(timeline: timeline) if let id = mastodonController.accountInfo?.id {
userActivity = UserActivityManager.showTimelineActivity(timeline: timeline, accountID: id)
}
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -51,6 +53,8 @@ class TimelineTableViewController: TimelineLikeTableViewController<TimelineEntry
super.viewDidLoad() super.viewDidLoad()
tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell") tableView.register(UINib(nibName: "TimelineStatusTableViewCell", bundle: .main), forCellReuseIdentifier: "statusCell")
tableView.dragDelegate = self
} }
override class func refreshCommandTitle() -> String { override class func refreshCommandTitle() -> 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]
}
}

View File

@ -189,7 +189,7 @@ extension MenuPreviewProvider {
})) }))
} }
let shareSection = [ var shareSection = [
openInSafariAction(url: status.url!), openInSafariAction(url: status.url!),
createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in createAction(identifier: "share", title: "Share...", systemImageName: "square.and.arrow.up", handler: { [weak self] (_) in
guard let self = self else { return } 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 [ return [
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection), UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: shareSection),
UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection), UIMenu(title: "", image: nil, identifier: nil, options: [.displayInline], children: actionsSection),
] ]
} }
private func createAction(identifier: String, title: String, systemImageName: String, handler: @escaping UIActionHandler) -> UIAction { 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) 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 { private func openInSafariAction(url: URL) -> UIAction {

View File

@ -31,17 +31,26 @@ class UserActivityManager {
getMainViewController().present(vc, animated: animated) 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 // 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 // todo: update to use managed objects
let activity = NSUserActivity(type: .newPost) let activity = NSUserActivity(type: .newPost)
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.userInfo = [
"accountID": accountID,
]
if let mentioning = mentioning { if let mentioning = mentioning {
activity.userInfo = ["mentioning": mentioning.acct] activity.userInfo!["mentioning"] = mentioning.acct
activity.title = "Send a message to \(mentioning.displayName)" activity.title = "Send a message to \(mentioning.displayName)"
activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayName)" activity.suggestedInvocationPhrase = "Send a message to \(mentioning.displayName)"
} else { } else {
activity.userInfo = [:]
activity.title = "New Post" activity.title = "New Post"
activity.suggestedInvocationPhrase = "Post in Tusker" activity.suggestedInvocationPhrase = "Post in Tusker"
} }
@ -56,12 +65,39 @@ class UserActivityManager {
present(UINavigationController(rootViewController: composeVC)) 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 // MARK: - Check Notifications
static func checkNotificationsActivity() -> NSUserActivity { static func checkNotificationsActivity(mode: NotificationsMode = .allNotifications) -> NSUserActivity {
let activity = NSUserActivity(type: .checkNotifications) let activity = NSUserActivity(type: .checkNotifications)
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.addUserInfoEntries(from: [
"notificationsMode": mode.rawValue
])
switch mode {
case .allNotifications:
activity.title = NSLocalizedString("Check Notifications", comment: "check notifications shortcut title") activity.title = NSLocalizedString("Check Notifications", comment: "check notifications shortcut title")
activity.suggestedInvocationPhrase = NSLocalizedString("Check my Tusker notifications", comment: "check notifications shortcut invocation phrase") 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 return activity
} }
@ -72,37 +108,27 @@ class UserActivityManager {
let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController { let notificationsPageController = navigationController.viewControllers.first as? NotificationsPageViewController {
navigationController.popToRootViewController(animated: false) navigationController.popToRootViewController(animated: false)
notificationsPageController.loadViewIfNeeded() notificationsPageController.loadViewIfNeeded()
notificationsPageController.selectMode(.allNotifications) notificationsPageController.selectMode(getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
} }
} }
// MARK: - Check Mentions static func getNotificationsMode(from activity: NSUserActivity) -> NotificationsMode? {
static func checkMentionsActivity() -> NSUserActivity { guard let str = activity.userInfo?["notificationsMode"] as? String else {
let activity = NSUserActivity(type: .checkMentions) return nil
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)
} }
return NotificationsMode(rawValue: str)
} }
// MARK: - Show Timeline // 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 } guard let timelineData = try? encoder.encode(timeline) else { return nil }
let activity = NSUserActivity(type: .showTimeline) let activity = NSUserActivity(type: .showTimeline)
activity.isEligibleForPrediction = true activity.isEligibleForPrediction = true
activity.userInfo = ["timelineData": timelineData] activity.userInfo = [
"timelineData": timelineData,
"accountID": accountID,
]
switch timeline { switch timeline {
case .home: case .home:
activity.title = NSLocalizedString("Show Home Timeline", comment: "home timeline shortcut title") activity.title = NSLocalizedString("Show Home Timeline", comment: "home timeline shortcut title")
@ -127,11 +153,16 @@ class UserActivityManager {
return activity return activity
} }
static func handleShowTimeline(activity: NSUserActivity) { static func getTimeline(from activity: NSUserActivity) -> Timeline? {
guard let timelineData = activity.userInfo?["timelineData"] as? Data, guard activity.activityType == UserActivityType.showTimeline.rawValue,
let timeline = try? decoder.decode(Timeline.self, from: timelineData) else { let data = activity.userInfo?["timelineData"] as? Data else {
return 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() let mainViewController = getMainViewController()
mainViewController.select(tab: .timelines) 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 // MARK: - Explore
static func searchActivity() -> NSUserActivity { 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
}
} }

View File

@ -9,12 +9,14 @@
import Foundation import Foundation
enum UserActivityType: String { enum UserActivityType: String {
case newPost = "net.shadowfacts.Tusker.activity.new-post" case newPost = "space.vaccor.Tusker.activity.new-post"
case checkNotifications = "net.shadowfacts.Tusker.activity.check-notifications" case checkNotifications = "space.vaccor.Tusker.activity.check-notifications"
case checkMentions = "net.shadowfacts.Tusker.activity.check-mentions" case showTimeline = "space.vaccor.Tusker.activity.show-timeline"
case showTimeline = "net.shadowfacts.Tusker.activity.show-timeline" case search = "space.vaccor.Tusker.activity.search"
case search = "net.shadowfacts.Tusker.activity.search" case bookmarks = "space.vaccor.Tusker.activity.bookmarks"
case bookmarks = "net.shadowfacts.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 { extension UserActivityType {
@ -24,14 +26,18 @@ extension UserActivityType {
return UserActivityManager.handleNewPost return UserActivityManager.handleNewPost
case .checkNotifications: case .checkNotifications:
return UserActivityManager.handleCheckNotifications return UserActivityManager.handleCheckNotifications
case .checkMentions:
return UserActivityManager.handleCheckMentions
case .showTimeline: case .showTimeline:
return UserActivityManager.handleShowTimeline return UserActivityManager.handleShowTimeline
case .search: case .search:
return UserActivityManager.handleSearch return UserActivityManager.handleSearch
case .bookmarks: case .bookmarks:
return UserActivityManager.handleBookmarks return UserActivityManager.handleBookmarks
case .showConversation:
fatalError("cannot handle show conversation activity")
case .myProfile:
return UserActivityManager.handleMyProfile
case .showProfile:
fatalError("cannot handle show profile activity")
} }
} }
} }

View File

@ -81,9 +81,10 @@ class BaseStatusTableViewCell: UITableViewCell {
displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) displayNameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
usernameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed))) usernameLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
avatarImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
avatarImageView.layer.masksToBounds = true avatarImageView.layer.masksToBounds = true
avatarImageView.addInteraction(UIDragInteraction(delegate: self))
attachmentsView.delegate = self attachmentsView.delegate = self
@ -479,3 +480,13 @@ extension BaseStatusTableViewCell: MenuPreviewProvider {
return self.getStatusCellPreviewProviders(for: location, sourceViewController: sourceViewController) 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)]
}
}