2018-08-16 02:27:48 +00:00
//
// A p p D e l e g a t e . s w i f t
// T u s k e r
//
// C r e a t e d b y S h a d o w f a c t s o n 8 / 1 5 / 1 8 .
// C o p y r i g h t © 2 0 1 8 S h a d o w f a c t s . A l l r i g h t s r e s e r v e d .
//
import UIKit
2022-05-11 02:57:46 +00:00
import CoreData
2022-05-13 21:10:18 +00:00
import OSLog
2023-10-20 02:50:20 +00:00
#if canImport ( Sentry )
2022-10-30 21:10:58 +00:00
import Sentry
2023-10-20 02:50:20 +00:00
#endif
2023-03-05 19:35:25 +00:00
import UserAccounts
2023-04-16 17:31:10 +00:00
import ComposeUI
2023-04-18 23:47:49 +00:00
import TuskerPreferences
2024-04-08 13:48:40 +00:00
import PushNotifications
2023-04-18 23:47:49 +00:00
typealias Preferences = TuskerPreferences . Preferences
2022-05-13 21:10:18 +00:00
let stateRestorationLogger = Logger ( subsystem : Bundle . main . bundleIdentifier ! , category : " StateRestoration " )
2023-04-18 14:10:15 +00:00
private let logger = Logger ( subsystem : Bundle . main . bundleIdentifier ! , category : " AppDelegate " )
2018-08-16 02:27:48 +00:00
2024-01-26 16:02:40 +00:00
@ main
2018-08-16 02:27:48 +00:00
class AppDelegate : UIResponder , UIApplicationDelegate {
2020-06-21 03:11:35 +00:00
2018-08-16 02:27:48 +00:00
func application ( _ application : UIApplication , didFinishLaunchingWithOptions launchOptions : [ UIApplication . LaunchOptionsKey : Any ] ? ) -> Bool {
2023-10-20 02:50:20 +00:00
#if canImport ( Sentry )
2022-10-30 21:10:58 +00:00
configureSentry ( )
2023-10-20 02:50:20 +00:00
#endif
#if ! os ( visionOS )
2022-11-02 00:49:07 +00:00
swizzleStatusBar ( )
2023-02-03 04:02:11 +00:00
swizzlePresentationController ( )
2023-10-20 02:50:20 +00:00
#endif
2020-06-21 19:38:51 +00:00
2019-09-19 20:55:15 +00:00
AppShortcutItem . createItems ( for : application )
2020-06-21 19:38:51 +00:00
2022-05-11 02:57:46 +00:00
if let oldSavedData = SavedDataManager . load ( ) {
do {
for account in oldSavedData . accountIDs {
2023-03-05 19:35:25 +00:00
guard let account = UserAccountsManager . shared . getAccount ( id : account ) else {
2022-05-11 02:57:46 +00:00
continue
}
let controller = MastodonController . getForAccount ( account )
try oldSavedData . migrateToCoreData ( accountID : account . id , context : controller . persistentContainer . viewContext )
if controller . persistentContainer . viewContext . hasChanges {
try controller . persistentContainer . viewContext . save ( )
}
}
try SavedDataManager . destroy ( )
} catch {
// n o - o p
}
}
2023-04-16 17:31:10 +00:00
2023-05-04 22:33:06 +00:00
// m a k e s u r e t h e p e r s i s t e n t c o n t a i n e r i s i n i t i a l i z e d o n t h e m a i n t h r e a d
// o t h e r w i s e i n i t i a l i z i n g i t o n t h e b a c k g r o u n d t h r e a d c a n d e a d l o c k w i t h a c c e s s i n g i t o n t h e m a i n t h r e a d e l s e w h e r e
_ = DraftsPersistentContainer . shared
2023-04-16 17:31:10 +00:00
DispatchQueue . global ( qos : . userInitiated ) . async {
2024-04-13 22:44:43 +00:00
let documentsDirectory = FileManager . default . urls ( for : . documentDirectory , in : . userDomainMask ) . first !
2023-04-16 17:31:10 +00:00
let oldDraftsFile = documentsDirectory . appendingPathComponent ( " drafts " ) . appendingPathExtension ( " plist " )
2023-04-23 01:16:30 +00:00
let appGroupDraftsFile = FileManager . default . containerURL ( forSecurityApplicationGroupIdentifier : " group.space.vaccor.Tusker " ) ! . appendingPathComponent ( " drafts " ) . appendingPathExtension ( " plist " )
for url in [ oldDraftsFile , appGroupDraftsFile ] where FileManager . default . fileExists ( atPath : url . path ) {
DraftsPersistentContainer . shared . migrate ( from : url ) {
if case . failure ( let error ) = $0 {
2023-10-20 02:50:20 +00:00
#if canImport ( Sentry )
2023-04-23 01:16:30 +00:00
SentrySDK . capture ( error : error )
2023-10-20 02:50:20 +00:00
#endif
2023-04-23 01:16:30 +00:00
}
2023-04-16 17:31:10 +00:00
}
}
}
2023-04-18 23:47:49 +00:00
2023-05-16 03:26:03 +00:00
BackgroundManager . shared . registerHandlers ( )
2024-04-11 22:26:58 +00:00
initializePushNotifications ( )
2024-04-07 18:04:42 +00:00
2018-08-16 02:27:48 +00:00
return true
}
2020-06-21 03:11:35 +00:00
2023-10-20 02:50:20 +00:00
#if canImport ( Sentry )
2022-10-30 21:10:58 +00:00
private func configureSentry ( ) {
2024-04-07 18:04:42 +00:00
guard let info = Bundle . main . object ( forInfoDictionaryKey : " TuskerInfo " ) as ? [ String : Any ] ,
let dsn = info [ " SentryDSN " ] as ? String ,
2022-10-30 21:10:58 +00:00
! dsn . isEmpty else {
return
}
SentrySDK . start { options in
#if DEBUG
options . debug = true
options . environment = " dev "
#endif
// t h e ' / / ' i n t h e f u l l u r l c a n ' t b e e s c a p e d , s o w e h a v e t o a d d t h e s c h e m e b a c k
options . dsn = " https:// \( dsn ) "
options . enableSwizzling = false
2022-10-31 18:23:28 +00:00
// r e q u i r e d t o s u p p o r t r e l e a s e s / r e l e a s e h e a l t h
options . enableAutoSessionTracking = true
2023-01-28 03:39:15 +00:00
options . enableWatchdogTerminationTracking = false
options . enableAutoPerformanceTracing = false
2022-10-30 21:10:58 +00:00
options . enableNetworkTracking = false
options . enableAppHangTracking = false
2023-01-28 03:39:15 +00:00
options . enableCoreDataTracing = false
2022-10-31 18:23:28 +00:00
// w e d o n ' t c a r e a b o u t e v e n t s l i k e b a t t e r y , k e y b o a r d s h o w / h i d e
options . enableAutoBreadcrumbTracking = false
2023-01-28 03:39:15 +00:00
options . enableUserInteractionTracing = false
2022-10-30 22:17:53 +00:00
options . beforeSend = { event in
2022-10-31 18:23:28 +00:00
// j u s t n o , w h y w o u l d a n y o n e n e e d t h i s i n f o r m a t i o n
event . context ? . removeValue ( forKey : " culture " )
return Preferences . shared . reportErrorsAutomatically ? event : nil
2022-10-30 22:17:53 +00:00
}
2024-03-31 16:52:56 +00:00
if let clazz = NSClassFromString ( " SentryInstallation " ) ,
let objClazz = clazz as AnyObject as ? NSObject ,
let id = objClazz . perform ( Selector ( ( " idWithCacheDirectoryPath: " ) ) , with : options . cacheDirectoryPath ) . takeUnretainedValue ( ) as ? String {
logger . info ( " Initialized Sentry with installation/user ID: \( id , privacy : . public ) " )
}
2023-04-18 14:10:15 +00:00
}
2020-06-21 03:11:35 +00:00
}
2023-10-20 02:50:20 +00:00
#endif
2020-11-14 16:10:20 +00:00
override func buildMenu ( with builder : UIMenuBuilder ) {
if builder . system = = . main {
MenuController . buildMainMenu ( builder : builder )
}
}
2020-12-14 03:37:37 +00:00
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 {
2021-06-06 18:55:04 +00:00
case . mainScene :
return " main-scene "
2022-05-13 21:10:18 +00:00
2020-12-14 03:37:37 +00:00
case . showConversation ,
. showTimeline ,
. checkNotifications ,
. search ,
. bookmarks ,
. myProfile ,
2024-04-13 02:47:11 +00:00
. showProfile ,
. showNotification :
2022-05-13 21:10:18 +00:00
if activity . displaysAuxiliaryScene {
stateRestorationLogger . info ( " Using auxiliary scene for \( type . rawValue , privacy : . public ) " )
return " auxiliary "
} else {
return " main-scene "
}
2020-12-14 03:37:37 +00:00
case . newPost :
return " compose "
}
}
2022-05-15 14:34:24 +00:00
2024-04-07 18:04:42 +00:00
func application ( _ application : UIApplication , didRegisterForRemoteNotificationsWithDeviceToken deviceToken : Data ) {
PushManager . shared . didRegisterForRemoteNotifications ( deviceToken : deviceToken )
}
func application ( _ application : UIApplication , didFailToRegisterForRemoteNotificationsWithError error : any Error ) {
PushManager . shared . didFailToRegisterForRemoteNotifications ( error : error )
}
2024-04-11 22:26:58 +00:00
private func initializePushNotifications ( ) {
UNUserNotificationCenter . current ( ) . delegate = self
2024-04-09 16:35:00 +00:00
Task {
2024-04-15 13:49:42 +00:00
#if canImport ( Sentry )
2024-04-09 16:35:00 +00:00
PushManager . captureError = { SentrySDK . capture ( error : $0 ) }
2024-04-15 13:49:42 +00:00
#endif
2024-04-09 16:35:00 +00:00
await PushManager . shared . updateIfNecessary ( updateSubscription : {
guard let account = UserAccountsManager . shared . getAccount ( id : $0 . accountID ) else {
return false
}
let mastodonController = MastodonController . getForAccount ( account )
do {
let result = try await mastodonController . updatePushSubscription ( subscription : $0 )
2024-05-27 20:33:00 +00:00
PushManager . logger . info ( " Updated push subscription \( result . id , privacy : . public ) on \( mastodonController . instanceURL ) with endpoint \( $0 . endpoint , privacy : . public ) " )
PushManager . logger . debug ( " New push subscription: \( String ( describing : result ) ) " )
2024-04-09 16:35:00 +00:00
return true
} catch {
PushManager . logger . error ( " Error updating push subscription: \( String ( describing : error ) ) " )
return false
}
} )
}
}
2023-10-20 02:50:20 +00:00
#if ! os ( visionOS )
2022-11-02 00:49:07 +00:00
private func swizzleStatusBar ( ) {
let selector = Selector ( ( " handleTapAction: " ) )
var originalIMP : IMP ?
let imp = imp_implementationWithBlock ( { ( self : UIStatusBarManager , sender : AnyObject ) in
let original = unsafeBitCast ( originalIMP ! , to : ( @ convention ( c ) ( UIStatusBarManager , Selector , AnyObject ) -> Void ) . self )
2023-02-16 00:34:18 +00:00
let exception = catchNSException {
guard let windowScene = self . perform ( Selector ( ( " windowScene " ) ) ) . takeUnretainedValue ( ) as ? UIWindowScene ,
let xPosition = sender . value ( forKey : " xPosition " ) as ? CGFloat ,
let delegate = windowScene . delegate as ? TuskerSceneDelegate else {
original ( self , selector , sender )
return
}
switch delegate . handleStatusBarTapped ( xPosition : xPosition ) {
case . stop :
return
case . continue :
original ( self , selector , sender )
}
2022-11-02 00:49:07 +00:00
}
2023-02-19 19:19:39 +00:00
if let exception {
SentrySDK . capture ( exception : exception )
}
2022-11-02 00:49:07 +00:00
} as @ convention ( block ) ( UIStatusBarManager , AnyObject ) -> Void )
originalIMP = class_replaceMethod ( UIStatusBarManager . self , selector , imp , " v@:@ " )
if originalIMP = = nil {
Logging . general . error ( " Unable to swizzle status bar manager " )
}
}
2023-02-03 04:02:11 +00:00
2023-09-09 15:45:54 +00:00
@ available ( iOS , obsoleted : 17.0 )
2023-02-03 04:02:11 +00:00
private func swizzlePresentationController ( ) {
2023-09-09 15:45:54 +00:00
guard # unavailable ( iOS 17.0 ) else {
return
}
2023-02-03 04:02:11 +00:00
var originalIMP : IMP ?
let imp = imp_implementationWithBlock ( { ( self : UIPresentationController ) in
let new = UITraitCollection ( pureBlackDarkMode : self . presentingViewController . traitCollection . pureBlackDarkMode )
if let existing = self . overrideTraitCollection {
self . overrideTraitCollection = UITraitCollection ( traitsFrom : [ existing , new ] )
} else {
self . overrideTraitCollection = new
}
let original = unsafeBitCast ( originalIMP ! , to : ( @ convention ( c ) ( UIPresentationController ) -> Void ) . self )
original ( self )
} as ( @ convention ( block ) ( UIPresentationController ) -> Void ) )
let sel = Selector ( [ " Necessary " , " If " , " Traits " , " update " , " _ " ] . reversed ( ) . joined ( ) )
originalIMP = class_replaceMethod ( UIPresentationController . self , sel , imp , " v@: " )
if originalIMP = = nil {
Logging . general . error ( " Unable to swizzle presentation controller " )
}
}
2023-10-20 02:50:20 +00:00
#endif
2023-02-03 04:02:11 +00:00
2018-08-16 02:27:48 +00:00
}
2024-04-11 22:26:58 +00:00
extension AppDelegate : UNUserNotificationCenterDelegate {
func userNotificationCenter ( _ center : UNUserNotificationCenter , openSettingsFor notification : UNNotification ? ) {
let mainSceneDelegate : MainSceneDelegate
if let delegate = UIApplication . shared . activeScene ? . delegate as ? MainSceneDelegate {
mainSceneDelegate = delegate
} else if let scene = UIApplication . shared . connectedScenes . first ( where : { $0 . delegate is MainSceneDelegate } ) {
mainSceneDelegate = scene . delegate as ! MainSceneDelegate
} else if let accountID = UserAccountsManager . shared . mostRecentAccountID {
let activity = UserActivityManager . mainSceneActivity ( accountID : accountID )
activity . addUserInfoEntries ( from : [ " showNotificationsPreferences " : true ] )
UIApplication . shared . requestSceneSessionActivation ( nil , userActivity : activity , options : nil )
return
} else {
// w i t h o u t a n a c c o u n t , w e c a n ' t d o a n y t h i n g
return
}
mainSceneDelegate . showNotificationsPreferences ( )
}
2024-04-13 02:47:11 +00:00
func userNotificationCenter ( _ center : UNUserNotificationCenter , willPresent notification : UNNotification , withCompletionHandler completionHandler : @ escaping ( UNNotificationPresentationOptions ) -> Void ) {
completionHandler ( . banner )
}
func userNotificationCenter ( _ center : UNUserNotificationCenter , didReceive response : UNNotificationResponse , withCompletionHandler completionHandler : @ escaping ( ) -> Void ) {
let userInfo = response . notification . request . content . userInfo
guard let notificationID = userInfo [ " notificationID " ] as ? String ,
let accountID = userInfo [ " accountID " ] as ? String ,
let account = UserAccountsManager . shared . getAccount ( id : accountID ) else {
return
}
if let scene = response . targetScene ,
let delegate = scene . delegate as ? MainSceneDelegate ,
let rootViewController = delegate . rootViewController {
let mastodonController = MastodonController . getForAccount ( account )
// i f t h e s c e n e i s a l r e a d y a c t i v e , t h e n w e a n i m a t e t h e a c c o u n t s w i t c h i n g i f n e c e s s a r y
delegate . activateAccount ( account , animated : scene . activationState = = . foregroundActive )
rootViewController . select ( route : . notifications , animated : false )
let vc = NotificationLoadingViewController ( notificationID : notificationID , mastodonController : mastodonController )
rootViewController . getNavigationController ( ) . pushViewController ( vc , animated : false )
} else {
let activity = UserActivityManager . showNotificationActivity ( id : notificationID , accountID : accountID )
UIApplication . shared . requestSceneSessionActivation ( nil , userActivity : activity , options : nil )
}
completionHandler ( )
}
2024-04-11 22:26:58 +00:00
}