diff --git a/AppShared/UserDefaults.swift b/AppShared/UserDefaults.swift index 9cecdcf60..67a3cf685 100644 --- a/AppShared/UserDefaults.swift +++ b/AppShared/UserDefaults.swift @@ -6,7 +6,39 @@ // import UIKit +import CryptoKit extension UserDefaults { public static let shared = UserDefaults(suiteName: AppName.groupID)! } + +extension UserDefaults { + // always use hash value (SHA256) from accessToken as key + private static func deriveKey(from accessToken: String, prefix: String) -> String { + let digest = SHA256.hash(data: Data(accessToken.utf8)) + let bytes = [UInt8](digest) + let hex = bytes.toHexString() + let key = prefix + "@" + hex + return key + } + + private static let notificationCountKeyPrefix = "notification_count" + + public func getNotificationCountWithAccessToken(accessToken: String) -> Int { + let prefix = UserDefaults.notificationCountKeyPrefix + let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix) + return integer(forKey: key) + } + + public func setNotificationCountWithAccessToken(accessToken: String, value: Int) { + let prefix = UserDefaults.notificationCountKeyPrefix + let key = UserDefaults.deriveKey(from: accessToken, prefix: prefix) + setValue(value, forKey: key) + } + + public func increaseNotificationCount(accessToken: String) { + let count = getNotificationCountWithAccessToken(accessToken: accessToken) + setNotificationCountWithAccessToken(accessToken: accessToken, value: count + 1) + } + +} diff --git a/Localization/app.json b/Localization/app.json index ce307a2e0..f4c3ffb5a 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -541,7 +541,8 @@ "add_account": "Add Account", }, "wizard": { - "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button." + "multiple_account_switch_intro_description": "Switch between multiple accounts by holding the profile button.", + "accessibility_hint": "Double tap to dismiss this wizard" } } } \ No newline at end of file diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 28d55c93f..c67b879ea 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -265,6 +265,8 @@ DB4924E226312AB200E9DB22 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4924E126312AB200E9DB22 /* NotificationService.swift */; }; DB4932B126F1FB5300EF46D4 /* WizardCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */; }; DB4932B326F2054200EF46D4 /* CircleAvatarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B226F2054200EF46D4 /* CircleAvatarButton.swift */; }; + DB4932B726F30F0700EF46D4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20223826146553000C64BF /* Array.swift */; }; + DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4932B826F31AD300EF46D4 /* BadgeButton.swift */; }; DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61325FF2C5600B98345 /* EmojiService.swift */; }; DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */; }; DB49A62525FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */; }; @@ -1024,6 +1026,7 @@ DB4924E126312AB200E9DB22 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardCardView.swift; sourceTree = ""; }; DB4932B226F2054200EF46D4 /* CircleAvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleAvatarButton.swift; sourceTree = ""; }; + DB4932B826F31AD300EF46D4 /* BadgeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeButton.swift; sourceTree = ""; }; DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = ""; }; DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = ""; }; DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = ""; }; @@ -2817,6 +2820,7 @@ isa = PBXGroup; children = ( DBA5A53026F08EF000CACBAA /* DragIndicatorView.swift */, + DB4932B826F31AD300EF46D4 /* BadgeButton.swift */, ); path = View; sourceTree = ""; @@ -4155,6 +4159,7 @@ 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */, DB482A4B261340A7008AE74C /* APIService+UserTimeline.swift in Sources */, DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */, + DB4932B926F31AD300EF46D4 /* BadgeButton.swift in Sources */, 0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */, DBCC3B30261440A50045B23D /* UITabBarController.swift in Sources */, DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */, @@ -4236,6 +4241,7 @@ buildActionMask = 2147483647; files = ( DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */, + DB4932B726F30F0700EF46D4 /* Array.swift in Sources */, DB6804922637CD8700430867 /* AppName.swift in Sources */, DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */, ); diff --git a/Mastodon/Generated/Assets.swift b/Mastodon/Generated/Assets.swift index f3ef1088e..36edb7bcd 100644 --- a/Mastodon/Generated/Assets.swift +++ b/Mastodon/Generated/Assets.swift @@ -70,6 +70,7 @@ internal enum Asset { internal static let valid = ColorAsset(name: "Colors/TextField/valid") } internal static let alertYellow = ColorAsset(name: "Colors/alert.yellow") + internal static let badgeBackground = ColorAsset(name: "Colors/badge.background") internal static let battleshipGrey = ColorAsset(name: "Colors/battleshipGrey") internal static let brandBlue = ColorAsset(name: "Colors/brand.blue") internal static let brandBlueDarken20 = ColorAsset(name: "Colors/brand.blue.darken.20") diff --git a/Mastodon/Resources/Assets.xcassets/Colors/badge.background.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/badge.background.colorset/Contents.json new file mode 100644 index 000000000..f58a604a1 --- /dev/null +++ b/Mastodon/Resources/Assets.xcassets/Colors/badge.background.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "48", + "green" : "59", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Resources/Assets.xcassets/Colors/danger.colorset/Contents.json b/Mastodon/Resources/Assets.xcassets/Colors/danger.colorset/Contents.json index 8ea3105e6..b77cb3c75 100644 --- a/Mastodon/Resources/Assets.xcassets/Colors/danger.colorset/Contents.json +++ b/Mastodon/Resources/Assets.xcassets/Colors/danger.colorset/Contents.json @@ -1,20 +1,20 @@ { - "info" : { - "version" : 1, - "author" : "xcode" - }, "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", - "red" : "0.875", - "blue" : "0.353", - "green" : "0.251" + "blue" : "90", + "green" : "64", + "red" : "223" } }, "idiom" : "universal" } - ] -} \ No newline at end of file + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mastodon/Scene/Account/AccountListViewModel.swift b/Mastodon/Scene/Account/AccountListViewModel.swift index a70209eeb..1977b90ec 100644 --- a/Mastodon/Scene/Account/AccountListViewModel.swift +++ b/Mastodon/Scene/Account/AccountListViewModel.swift @@ -86,11 +86,10 @@ extension AccountListViewModel { switch item { case .authentication(let objectID): let authentication = managedObjectContext.object(with: objectID) as! MastodonAuthentication - let user = authentication.user let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: AccountListTableViewCell.self), for: indexPath) as! AccountListTableViewCell AccountListViewModel.configure( cell: cell, - user: user, + authentication: authentication, activeMastodonUserObjectID: self.activeMastodonUserObjectID.eraseToAnyPublisher() ) return cell @@ -107,9 +106,11 @@ extension AccountListViewModel { static func configure( cell: AccountListTableViewCell, - user: MastodonUser, + authentication: MastodonAuthentication, activeMastodonUserObjectID: AnyPublisher ) { + let user = authentication.user + // avatar cell.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: user.avatarImageURL())) @@ -127,14 +128,32 @@ extension AccountListViewModel { let usernameMetaContent = PlaintextMetaContent(string: "@" + user.acctWithDomain) cell.usernameLabel.configure(content: usernameMetaContent) + // badge + let accessToken = authentication.userAccessToken + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: accessToken) + cell.badgeButton.setBadge(number: count) + // checkmark activeMastodonUserObjectID .receive(on: DispatchQueue.main) .sink { objectID in let isCurrentUser = user.objectID == objectID cell.tintColor = .label - cell.accessoryType = isCurrentUser ? .checkmark : .none + cell.checkmarkImageView.isHidden = !isCurrentUser + if isCurrentUser { + cell.accessibilityTraits.insert(.selected) + } else { + cell.accessibilityTraits.remove(.selected) + } } .store(in: &cell.disposeBag) + + cell.accessibilityLabel = [ + cell.nameLabel.text, + cell.usernameLabel.text, + cell.badgeButton.accessibilityLabel + ] + .compactMap { $0 } + .joined(separator: " ") } } diff --git a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift index aec182798..4c9977f37 100644 --- a/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift +++ b/Mastodon/Scene/Account/Cell/AccountListTableViewCell.swift @@ -17,6 +17,13 @@ final class AccountListTableViewCell: UITableViewCell { let avatarButton = CircleAvatarButton(frame: .zero) let nameLabel = MetaLabel(style: .accountListName) let usernameLabel = MetaLabel(style: .accountListUsername) + let badgeButton = BadgeButton() + let checkmarkImageView: UIImageView = { + let image = UIImage(systemName: "checkmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 17, weight: .semibold)) + let imageView = UIImageView(image: image) + imageView.tintColor = .label + return imageView + }() let separatorLine = UIView.separatorLine override func prepareForReuse() { @@ -63,15 +70,36 @@ extension AccountListTableViewCell { labelContainerStackView.leadingAnchor.constraint(equalTo: avatarButton.trailingAnchor, constant: 10), contentView.bottomAnchor.constraint(equalTo: labelContainerStackView.bottomAnchor, constant: 10), avatarButton.heightAnchor.constraint(equalTo: labelContainerStackView.heightAnchor, multiplier: 0.8).priority(.required - 10), - labelContainerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), ]) labelContainerStackView.addArrangedSubview(nameLabel) labelContainerStackView.addArrangedSubview(usernameLabel) - + + badgeButton.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(badgeButton) + NSLayoutConstraint.activate([ + badgeButton.leadingAnchor.constraint(equalTo: labelContainerStackView.trailingAnchor, constant: 4), + badgeButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + badgeButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 16).priority(.required - 1), + badgeButton.widthAnchor.constraint(equalTo: badgeButton.heightAnchor, multiplier: 1.0).priority(.required - 1), + ]) + badgeButton.setContentHuggingPriority(.required - 10, for: .horizontal) + badgeButton.setContentCompressionResistancePriority(.required - 10, for: .horizontal) + + checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(checkmarkImageView) + NSLayoutConstraint.activate([ + checkmarkImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + checkmarkImageView.leadingAnchor.constraint(equalTo: badgeButton.trailingAnchor, constant: 12), + checkmarkImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + ]) + checkmarkImageView.setContentHuggingPriority(.required - 9, for: .horizontal) + checkmarkImageView.setContentCompressionResistancePriority(.required - 9, for: .horizontal) + avatarButton.isUserInteractionEnabled = false nameLabel.isUserInteractionEnabled = false usernameLabel.isUserInteractionEnabled = false + badgeButton.isUserInteractionEnabled = false separatorLine.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(separatorLine) diff --git a/Mastodon/Scene/Account/View/BadgeButton.swift b/Mastodon/Scene/Account/View/BadgeButton.swift new file mode 100644 index 000000000..c55448ae4 --- /dev/null +++ b/Mastodon/Scene/Account/View/BadgeButton.swift @@ -0,0 +1,46 @@ +// +// BadgeButton.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-9-16. +// + +import UIKit + +final class BadgeButton: UIButton { + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension BadgeButton { + private func _init() { + titleLabel?.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .medium)) + setBackgroundColor(Asset.Colors.badgeBackground.color, for: .normal) + setTitleColor(.white, for: .normal) + + contentEdgeInsets = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) + } + + override func layoutSubviews() { + super.layoutSubviews() + + layer.masksToBounds = true + layer.cornerRadius = frame.height * 0.5 + } + + func setBadge(number: Int) { + let number = min(99, max(0, number)) + setTitle("\(number)", for: .normal) + self.isHidden = number == 0 + accessibilityLabel = "\(number) unread notification" + } +} diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 4d9bb470f..b0fddb445 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -26,14 +26,7 @@ extension HomeTimelineViewController { showMenu, moveMenu, dropMenu, - UIAction(title: "Toggle EmptyView", image: UIImage(systemName: "clear"), attributes: []) { [weak self] action in - guard let self = self else { return } - if self.emptyView.superview != nil { - self.emptyView.removeFromSuperview() - } else { - self.showEmptyView() - } - }, + miscMenu, UIAction(title: "Settings", image: UIImage(systemName: "gear"), attributes: []) { [weak self] action in guard let self = self else { return } self.showSettings(action) @@ -139,6 +132,39 @@ extension HomeTimelineViewController { } ) } + + var miscMenu: UIMenu { + return UIMenu( + title: "Debug…", + image: UIImage(systemName: "switch.2"), + identifier: nil, + options: [], + children: [ + UIAction(title: "Toggle EmptyView", image: UIImage(systemName: "clear"), attributes: []) { [weak self] action in + guard let self = self else { return } + if self.emptyView.superview != nil { + self.emptyView.removeFromSuperview() + } else { + self.showEmptyView() + } + }, + UIAction( + title: "notification badge +1", + image: UIImage(systemName: "1.circle.fill"), + identifier: nil, + attributes: [], + state: .off, + handler: { [weak self] _ in + guard let self = self else { return } + guard let accessToken = self.context.authenticationService.activeMastodonAuthentication.value?.userAccessToken else { return } + UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) + self.context.notificationService.applicationIconBadgeNeedsUpdate.send() + } + ) + ] + ) + } + } extension HomeTimelineViewController { diff --git a/Mastodon/Scene/MainTab/MainTabBarController.swift b/Mastodon/Scene/MainTab/MainTabBarController.swift index ca978a5a2..f07a1f905 100644 --- a/Mastodon/Scene/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/MainTab/MainTabBarController.swift @@ -194,17 +194,25 @@ extension MainTabBarController { .store(in: &disposeBag) // handle push notification. toggle entry when finish fetch latest notification - context.notificationService.hasUnreadPushNotification - .receive(on: DispatchQueue.main) - .sink { [weak self] hasUnreadPushNotification in - guard let self = self else { return } - guard let notificationViewController = self.notificationViewController else { return } - - let image = hasUnreadPushNotification ? UIImage(systemName: "bell.badge.fill")! : UIImage(systemName: "bell.fill")! - notificationViewController.tabBarItem.image = image - notificationViewController.navigationController?.tabBarItem.image = image - } - .store(in: &disposeBag) + Publishers.CombineLatest( + context.authenticationService.activeMastodonAuthentication, + context.notificationService.unreadNotificationCountDidUpdate + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] authentication, _ in + guard let self = self else { return } + guard let notificationViewController = self.notificationViewController else { return } + + let hasUnreadPushNotification: Bool = authentication.flatMap { authentication in + let count = UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.userAccessToken) + return count > 0 + } ?? false + + let image = hasUnreadPushNotification ? UIImage(systemName: "bell.badge.fill")! : UIImage(systemName: "bell.fill")! + notificationViewController.tabBarItem.image = image + notificationViewController.navigationController?.tabBarItem.image = image + } + .store(in: &disposeBag) context.notificationService.requestRevealNotificationPublisher .receive(on: DispatchQueue.main) diff --git a/Mastodon/Scene/Notification/NotificationViewController.swift b/Mastodon/Scene/Notification/NotificationViewController.swift index 100cd3d82..e00749657 100644 --- a/Mastodon/Scene/Notification/NotificationViewController.swift +++ b/Mastodon/Scene/Notification/NotificationViewController.swift @@ -166,6 +166,16 @@ extension NotificationViewController { self.viewModel.loadLatestStateMachine.enter(NotificationViewModel.LoadLatestState.Loading.self) } } + + // reset notification count + context.notificationService.clearNotificationCountForActiveUser() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + // reset notification count + context.notificationService.clearNotificationCountForActiveUser() } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { diff --git a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift index 04d33202f..dac7bb7d3 100644 --- a/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift +++ b/Mastodon/Scene/Notification/NotificationViewModel+LoadLatestState.swift @@ -67,8 +67,6 @@ extension NotificationViewModel.LoadLatestState { viewModel.isFetchingLatestNotification.value = false os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch notification failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription) case .finished: - // toggle unread state - viewModel.context.notificationService.hasUnreadPushNotification.value = false // handle isFetchingLatestTimeline in fetch controller delegate break } diff --git a/Mastodon/Scene/Onboarding/Welcome/View/WizardCardView.swift b/Mastodon/Scene/Onboarding/Welcome/View/WizardCardView.swift index 7855d76d6..defe6f4ee 100644 --- a/Mastodon/Scene/Onboarding/Welcome/View/WizardCardView.swift +++ b/Mastodon/Scene/Onboarding/Welcome/View/WizardCardView.swift @@ -107,4 +107,28 @@ extension WizardCardView { backgroundShapeLayer.fillColor = UIColor.white.cgColor backgroundShapeLayer.path = path.cgPath } + + override var isAccessibilityElement: Bool { + get { true } + set { } + } + + override var accessibilityLabel: String? { + get { + return [ + titleLabel.text, + descriptionLabel.text + ] + .compactMap { $0 } + .joined(separator: " ") + } + set { } + } + + override var accessibilityHint: String? { + get { + return "Wizard for account switcher on the Profile tab. Double tap to dismiss this wizard" + } + set { } + } } diff --git a/Mastodon/Service/NotificationService.swift b/Mastodon/Service/NotificationService.swift index ffe4c9916..6437e1b66 100644 --- a/Mastodon/Service/NotificationService.swift +++ b/Mastodon/Service/NotificationService.swift @@ -24,11 +24,12 @@ final class NotificationService { weak var authenticationService: AuthenticationService? let isNotificationPermissionGranted = CurrentValueSubject(false) let deviceToken = CurrentValueSubject(nil) + let applicationIconBadgeNeedsUpdate = CurrentValueSubject(Void()) // output /// [Token: UserID] let notificationSubscriptionDict: [String: NotificationViewModel] = [:] - let hasUnreadPushNotification = CurrentValueSubject(false) + let unreadNotificationCountDidUpdate = CurrentValueSubject(Void()) let requestRevealNotificationPublisher = PassthroughSubject() init( @@ -57,6 +58,26 @@ final class NotificationService { os_log(.info, log: .api, "%{public}s[%{public}ld], %{public}s: deviceToken: %s", ((#file as NSString).lastPathComponent), #line, #function, token) } .store(in: &disposeBag) + + Publishers.CombineLatest( + authenticationService.mastodonAuthentications, + applicationIconBadgeNeedsUpdate + ) + .receive(on: DispatchQueue.main) + .sink { [weak self] mastodonAuthentications, _ in + guard let self = self else { return } + + var count = 0 + for authentication in mastodonAuthentications { + count += UserDefaults.shared.getNotificationCountWithAccessToken(accessToken: authentication.userAccessToken) + } + + UserDefaults.shared.notificationBadgeCount = count + UIApplication.shared.applicationIconBadgeNumber = count + + self.unreadNotificationCountDidUpdate.send() + } + .store(in: &disposeBag) } } @@ -101,7 +122,9 @@ extension NotificationService { } func handle(mastodonPushNotification: MastodonPushNotification) { - hasUnreadPushNotification.value = true + defer { + unreadNotificationCountDidUpdate.send() + } // Subscription maybe failed to cancel when sign-out // Try cancel again if receive that kind push notification @@ -154,6 +177,17 @@ extension NotificationService { } +extension NotificationService { + func clearNotificationCountForActiveUser() { + guard let authenticationService = self.authenticationService else { return } + if let accessToken = authenticationService.activeMastodonAuthentication.value?.userAccessToken { + UserDefaults.shared.setNotificationCountWithAccessToken(accessToken: accessToken, value: 0) + } + + applicationIconBadgeNeedsUpdate.send() + } +} + // MARK: - NotificationViewModel extension NotificationService { diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index 56382babf..192f201d1 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -100,6 +100,11 @@ extension AppDelegate: UNUserNotificationCenterDelegate { let notificationID = String(mastodonPushNotification.notificationID) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Push Notification] notification %s", ((#file as NSString).lastPathComponent), #line, #function, notificationID) + + let accessToken = mastodonPushNotification.accessToken + UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) + appContext.notificationService.applicationIconBadgeNeedsUpdate.send() + appContext.notificationService.handle(mastodonPushNotification: mastodonPushNotification) completionHandler([.sound]) } diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index d0067129b..f5cc269b0 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -87,9 +87,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Called when the scene has moved from an inactive state to an active state. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - // reset notification badge - UserDefaults.shared.notificationBadgeCount = 0 - UIApplication.shared.applicationIconBadgeNumber = 0 + // update application badge + AppContext.shared.notificationService.applicationIconBadgeNeedsUpdate.send() // trigger status filter update AppContext.shared.statusFilterService.filterUpdatePublisher.send() diff --git a/NotificationService/NotificationService.swift b/NotificationService/NotificationService.swift index e65ea9aca..c3d02933b 100644 --- a/NotificationService/NotificationService.swift +++ b/NotificationService/NotificationService.swift @@ -60,6 +60,9 @@ class NotificationService: UNNotificationServiceExtension { bestAttemptContent.sound = UNNotificationSound.init(named: UNNotificationSoundName(rawValue: "BoopSound.caf")) bestAttemptContent.userInfo["plaintext"] = plaintextData + let accessToken = notification.accessToken + UserDefaults.shared.increaseNotificationCount(accessToken: accessToken) + UserDefaults.shared.notificationBadgeCount += 1 bestAttemptContent.badge = NSNumber(integerLiteral: UserDefaults.shared.notificationBadgeCount)