diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
index 103cb88d9..d0b79ce67 100644
--- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -114,7 +114,7 @@
MastodonIntent.xcscheme_^#shared#^_
orderHint
- 31
+ 20
MastodonIntents.xcscheme_^#shared#^_
@@ -129,12 +129,12 @@
NotificationService.xcscheme_^#shared#^_
orderHint
- 30
+ 21
ShareActionExtension.xcscheme_^#shared#^_
orderHint
- 32
+ 22
SuppressBuildableAutocreation
diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift
index 4491d383a..c6d81c9c2 100644
--- a/Mastodon/Coordinator/SceneCoordinator.swift
+++ b/Mastodon/Coordinator/SceneCoordinator.swift
@@ -19,7 +19,7 @@ final public class SceneCoordinator {
private weak var scene: UIScene!
private weak var sceneDelegate: SceneDelegate!
- private weak var appContext: AppContext!
+ private(set) weak var appContext: AppContext!
let id = UUID().uuidString
diff --git a/Mastodon/Info.plist b/Mastodon/Info.plist
index ce2875109..cfdbb923d 100644
--- a/Mastodon/Info.plist
+++ b/Mastodon/Info.plist
@@ -2,19 +2,6 @@
- NSAppTransportSecurity
-
- NSExceptionDomains
-
- onion
-
- NSExceptionAllowsInsecureHTTPLoads
-
- NSIncludesSubdomains
-
-
-
-
CADisableMinimumFrameDurationOnPhone
CFBundleDevelopmentRegion
@@ -59,6 +46,19 @@
LSRequiresIPhoneOS
+ NSAppTransportSecurity
+
+ NSExceptionDomains
+
+ onion
+
+ NSExceptionAllowsInsecureHTTPLoads
+
+ NSIncludesSubdomains
+
+
+
+
NSUserActivityTypes
SendPostIntent
@@ -103,6 +103,10 @@
UIApplicationSupportsIndirectInputEvents
+ UIBackgroundModes
+
+ remote-notification
+
UILaunchStoryboardName
Main
UIMainStoryboardFile
diff --git a/Mastodon/Service/NotificationService.swift b/Mastodon/Service/NotificationService.swift
index e4e7508a3..0d12d54ab 100644
--- a/Mastodon/Service/NotificationService.swift
+++ b/Mastodon/Service/NotificationService.swift
@@ -12,9 +12,12 @@ import CoreData
import CoreDataStack
import MastodonSDK
import AppShared
+import MastodonLocalization
final class NotificationService {
+ public static let unreadShortcutItemIdentifier = "org.joinmastodon.app.NotificationService.unread-shortcut"
+
var disposeBag = Set()
let workingQueue = DispatchQueue(label: "org.joinmastodon.app.NotificationService.working-queue")
@@ -74,6 +77,9 @@ final class NotificationService {
UserDefaults.shared.notificationBadgeCount = count
UIApplication.shared.applicationIconBadgeNumber = count
+ Task { @MainActor in
+ UIApplication.shared.shortcutItems = try? await self.unreadApplicationShortcutItems()
+ }
self.unreadNotificationCountDidUpdate.send()
}
@@ -100,6 +106,38 @@ extension NotificationService {
}
}
+extension NotificationService {
+ public func unreadApplicationShortcutItems() async throws -> [UIApplicationShortcutItem] {
+ guard let authenticationService = self.authenticationService else { return [] }
+ let managedObjectContext = authenticationService.managedObjectContext
+ return try await managedObjectContext.perform {
+ var items: [UIApplicationShortcutItem] = []
+ for object in authenticationService.mastodonAuthentications.value {
+ guard let authentication = managedObjectContext.object(with: object.objectID) as? MastodonAuthentication else { continue }
+
+ let accessToken = authentication.userAccessToken
+ let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken)
+ guard count > 0 else { continue }
+
+ let title = "@\(authentication.user.acctWithDomain)"
+ let subtitle = L10n.A11y.Plural.Count.Unread.notification(count)
+
+ let item = UIApplicationShortcutItem(
+ type: NotificationService.unreadShortcutItemIdentifier,
+ localizedTitle: title,
+ localizedSubtitle: subtitle,
+ icon: nil,
+ userInfo: [
+ "accessToken": accessToken as NSSecureCoding
+ ]
+ )
+ items.append(item)
+ }
+ return items
+ }
+ }
+}
+
extension NotificationService {
func dequeueNotificationViewModel(
diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift
index 7b1185f84..7b750a481 100644
--- a/Mastodon/Supporting Files/AppDelegate.swift
+++ b/Mastodon/Supporting Files/AppDelegate.swift
@@ -106,6 +106,14 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
completionHandler([.sound])
}
+
+ // notification present in the background (or resume from background)
+ func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult {
+ let shortcutItems = try? await appContext.notificationService.unreadApplicationShortcutItems()
+ UIApplication.shared.shortcutItems = shortcutItems
+ return .noData
+ }
+
// response to user action for notification (e.g. redirect to post)
func userNotificationCenter(
_ center: UNUserNotificationCenter,
diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift
index 54b1fd57e..ee60c14e9 100644
--- a/Mastodon/Supporting Files/SceneDelegate.swift
+++ b/Mastodon/Supporting Files/SceneDelegate.swift
@@ -110,7 +110,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
AppContext.shared.statusFilterService.filterUpdatePublisher.send()
if let shortcutItem = savedShortCutItem {
- _ = handler(shortcutItem: shortcutItem)
+ Task {
+ _ = await handler(shortcutItem: shortcutItem)
+ }
savedShortCutItem = nil
}
}
@@ -134,14 +136,45 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
extension SceneDelegate {
- func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
- completionHandler(handler(shortcutItem: shortcutItem))
+
+ func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem) async -> Bool {
+ return await handler(shortcutItem: shortcutItem)
}
- private func handler(shortcutItem: UIApplicationShortcutItem) -> Bool {
+ @MainActor
+ private func handler(shortcutItem: UIApplicationShortcutItem) async -> Bool {
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(shortcutItem.type)")
switch shortcutItem.type {
+ case NotificationService.unreadShortcutItemIdentifier:
+ guard let coordinator = self.coordinator else { return false }
+
+ guard let accessToken = shortcutItem.userInfo?["accessToken"] as? String else {
+ assertionFailure()
+ return false
+ }
+ let request = MastodonAuthentication.sortedFetchRequest
+ request.predicate = MastodonAuthentication.predicate(userAccessToken: accessToken)
+ request.fetchLimit = 1
+
+ guard let authentication = try? coordinator.appContext.managedObjectContext.fetch(request).first else {
+ assertionFailure()
+ return false
+ }
+
+ let _isActive = try? await coordinator.appContext.authenticationService.activeMastodonUser(
+ domain: authentication.domain,
+ userID: authentication.userID
+ )
+ .singleOutput()
+ .get()
+
+ guard _isActive == true else {
+ return false
+ }
+
+ coordinator.switchToTabBar(tab: .notification)
+
case "org.joinmastodon.app.new-post":
if coordinator?.tabBarController.topMost is ComposeViewController {
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): composing…")
@@ -158,6 +191,7 @@ extension SceneDelegate {
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): not authenticated")
}
}
+
case "org.joinmastodon.app.search":
coordinator?.switchToTabBar(tab: .search)
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): select search tab")
@@ -166,6 +200,7 @@ extension SceneDelegate {
searchViewController.searchBarTapPublisher.send()
logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): trigger search")
}
+
default:
assertionFailure()
break