Merge pull request #471 from mastodon/feature-unread-application-shortcut
Add unread notification application shortcut
This commit is contained in:
commit
603c348b64
|
@ -114,7 +114,7 @@
|
|||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>31</integer>
|
||||
<integer>20</integer>
|
||||
</dict>
|
||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -129,12 +129,12 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>30</integer>
|
||||
<integer>21</integer>
|
||||
</dict>
|
||||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>32</integer>
|
||||
<integer>22</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -2,19 +2,6 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>onion</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
|
@ -59,6 +46,19 @@
|
|||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>onion</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>SendPostIntent</string>
|
||||
|
@ -103,6 +103,10 @@
|
|||
</array>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>Main</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue