feat: add notification badge in AccountList scene
This commit is contained in:
parent
7dea01da1e
commit
5a746ef881
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = "<group>"; };
|
||||
DB4932B026F1FB5300EF46D4 /* WizardCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardCardView.swift; sourceTree = "<group>"; };
|
||||
DB4932B226F2054200EF46D4 /* CircleAvatarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleAvatarButton.swift; sourceTree = "<group>"; };
|
||||
DB4932B826F31AD300EF46D4 /* BadgeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeButton.swift; sourceTree = "<group>"; };
|
||||
DB49A61325FF2C5600B98345 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = "<group>"; };
|
||||
DB49A61E25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel.swift"; sourceTree = "<group>"; };
|
||||
DB49A62425FF334C00B98345 /* EmojiService+CustomEmojiViewModel+LoadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EmojiService+CustomEmojiViewModel+LoadState.swift"; sourceTree = "<group>"; };
|
||||
|
@ -2817,6 +2820,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DBA5A53026F08EF000CACBAA /* DragIndicatorView.swift */,
|
||||
DB4932B826F31AD300EF46D4 /* BadgeButton.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 */,
|
||||
);
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<NSManagedObjectID?, Never>
|
||||
) {
|
||||
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: " ")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 { }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,11 +24,12 @@ final class NotificationService {
|
|||
weak var authenticationService: AuthenticationService?
|
||||
let isNotificationPermissionGranted = CurrentValueSubject<Bool, Never>(false)
|
||||
let deviceToken = CurrentValueSubject<Data?, Never>(nil)
|
||||
let applicationIconBadgeNeedsUpdate = CurrentValueSubject<Void, Never>(Void())
|
||||
|
||||
// output
|
||||
/// [Token: UserID]
|
||||
let notificationSubscriptionDict: [String: NotificationViewModel] = [:]
|
||||
let hasUnreadPushNotification = CurrentValueSubject<Bool, Never>(false)
|
||||
let unreadNotificationCountDidUpdate = CurrentValueSubject<Void, Never>(Void())
|
||||
let requestRevealNotificationPublisher = PassthroughSubject<Mastodon.Entity.Notification.ID, Never>()
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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])
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue