Merge pull request #471 from mastodon/feature-unread-application-shortcut

Add unread notification application shortcut
This commit is contained in:
CMK 2022-07-27 17:53:45 +08:00 committed by GitHub
commit 603c348b64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 106 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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