mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 20:39:24 +00:00
Avoid race and duplication conditions between local and remote notifications (#848)
* added timestamp control of the local notification * improved the code, and implemented the solution, just needs some testing * sdk bump * UserPreference init with initialValue instead of defaultValue * pr suggestions * changelog * notifications sliding sync view added
This commit is contained in:
@@ -101,12 +101,19 @@ class NotificationManager: NSObject, NotificationManagerProtocol {
|
||||
}
|
||||
|
||||
private func showLocalNotification(_ notification: NotificationItemProxyProtocol) async {
|
||||
guard let userSession else { return }
|
||||
guard let userSession,
|
||||
notification.event.timestamp > ServiceLocator.shared.settings.lastLaunchDate else { return }
|
||||
do {
|
||||
guard let content = try await notification.process(receiverId: userSession.userID, roomId: notification.roomID, mediaProvider: userSession.mediaProvider) else {
|
||||
guard let content = try await notification.process(mediaProvider: userSession.mediaProvider),
|
||||
let identifier = notification.id else {
|
||||
return
|
||||
}
|
||||
let request = UNNotificationRequest(identifier: ProcessInfo.processInfo.globallyUniqueString, content: content, trigger: nil)
|
||||
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
|
||||
guard !ServiceLocator.shared.settings.servedNotificationIdentifiers.contains(identifier) else {
|
||||
MXLog.info("NotificationManager] local notification discarded because it has already been served")
|
||||
return
|
||||
}
|
||||
ServiceLocator.shared.settings.servedNotificationIdentifiers.insert(identifier)
|
||||
try await notificationCenter.add(request)
|
||||
} catch {
|
||||
MXLog.error("[NotificationManager] show local notification item failed: \(error)")
|
||||
|
||||
@@ -23,6 +23,7 @@ enum NotificationConstants {
|
||||
static let unreadCount = "unread_count"
|
||||
static let pusherNotificationClientIdentifier = "pusher_notification_client_identifier"
|
||||
static let receiverIdentifier = "receiver_id"
|
||||
static let notificationIdentifier = "notification_identifier"
|
||||
}
|
||||
|
||||
enum Category {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
import UserNotifications
|
||||
|
||||
@@ -24,6 +25,8 @@ protocol NotificationItemProxyProtocol {
|
||||
|
||||
var roomID: String { get }
|
||||
|
||||
var receiverID: String { get }
|
||||
|
||||
var senderDisplayName: String? { get }
|
||||
|
||||
var senderAvatarMediaSource: MediaSourceProxy? { get }
|
||||
@@ -39,8 +42,20 @@ protocol NotificationItemProxyProtocol {
|
||||
var isEncrypted: Bool { get }
|
||||
}
|
||||
|
||||
extension NotificationItemProxyProtocol {
|
||||
var id: String? {
|
||||
let identifiers = receiverID + roomID + event.eventID
|
||||
guard let data = identifiers.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
let digest = SHA256.hash(data: data)
|
||||
return digest.compactMap { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationItemProxy: NotificationItemProxyProtocol {
|
||||
let notificationItem: NotificationItem
|
||||
let receiverID: String
|
||||
|
||||
var event: TimelineEventProxyProtocol {
|
||||
TimelineEventProxy(timelineEvent: notificationItem.event)
|
||||
@@ -99,6 +114,8 @@ struct MockNotificationItemProxy: NotificationItemProxyProtocol {
|
||||
|
||||
let roomID: String
|
||||
|
||||
let receiverID: String
|
||||
|
||||
var senderDisplayName: String? { nil }
|
||||
|
||||
var senderAvatarURL: String? { nil }
|
||||
@@ -116,6 +133,8 @@ struct MockNotificationItemProxy: NotificationItemProxyProtocol {
|
||||
var senderAvatarMediaSource: MediaSourceProxy? { nil }
|
||||
|
||||
var roomAvatarMediaSource: MediaSourceProxy? { nil }
|
||||
|
||||
var notificationIdentifier: String { "" }
|
||||
}
|
||||
|
||||
extension NotificationItemProxyProtocol {
|
||||
@@ -148,11 +167,9 @@ extension NotificationItemProxyProtocol {
|
||||
/// - roomId: Room identifier
|
||||
/// - mediaProvider: Media provider to process also media. May be passed nil to ignore media operations.
|
||||
/// - Returns: A notification content object if the notification should be displayed. Otherwise nil.
|
||||
func process(receiverId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? {
|
||||
func process(mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent? {
|
||||
if self is MockNotificationItemProxy {
|
||||
return processMock(receiverId: receiverId, roomId: roomId)
|
||||
return processMock()
|
||||
} else {
|
||||
switch event.type {
|
||||
case .none, .state:
|
||||
@@ -162,19 +179,19 @@ extension NotificationItemProxyProtocol {
|
||||
case .roomMessage(messageType: let messageType):
|
||||
switch messageType {
|
||||
case .emote(content: let content):
|
||||
return try await processEmote(content: content, receiverId: receiverId, senderId: event.senderID, roomId: roomId, mediaProvider: mediaProvider)
|
||||
return try await processEmote(content: content, mediaProvider: mediaProvider)
|
||||
case .image(content: let content):
|
||||
return try await processImage(content: content, receiverId: receiverId, senderId: event.senderID, roomId: roomId, mediaProvider: mediaProvider)
|
||||
return try await processImage(content: content, mediaProvider: mediaProvider)
|
||||
case .audio(content: let content):
|
||||
return try await processAudio(content: content, receiverId: receiverId, senderId: event.senderID, roomId: roomId, mediaProvider: mediaProvider)
|
||||
return try await processAudio(content: content, mediaProvider: mediaProvider)
|
||||
case .video(content: let content):
|
||||
return try await processVideo(content: content, receiverId: receiverId, senderId: event.senderID, roomId: roomId, mediaProvider: mediaProvider)
|
||||
return try await processVideo(content: content, mediaProvider: mediaProvider)
|
||||
case .file(content: let content):
|
||||
return try await processFile(content: content, receiverId: receiverId, senderId: event.senderID, roomId: roomId, mediaProvider: mediaProvider)
|
||||
return try await processFile(content: content, mediaProvider: mediaProvider)
|
||||
case .notice(content: let content):
|
||||
return try await processNotice(content: content, receiverId: receiverId, senderId: event.senderID, roomId: roomId, mediaProvider: mediaProvider)
|
||||
return try await processNotice(content: content, mediaProvider: mediaProvider)
|
||||
case .text(content: let content):
|
||||
return try await processText(content: content, receiverId: receiverId, senderId: event.senderID, roomId: roomId, mediaProvider: mediaProvider)
|
||||
return try await processText(content: content, mediaProvider: mediaProvider)
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
@@ -188,32 +205,33 @@ extension NotificationItemProxyProtocol {
|
||||
// MARK: - Private
|
||||
|
||||
// To be removed once we don't need the mock anymore
|
||||
private func processMock(receiverId: String,
|
||||
roomId: String) -> UNMutableNotificationContent {
|
||||
private func processMock() -> UNMutableNotificationContent {
|
||||
let notification = UNMutableNotificationContent()
|
||||
notification.receiverID = receiverId
|
||||
notification.receiverID = receiverID
|
||||
notification.roomID = roomID
|
||||
notification.eventID = event.eventID
|
||||
notification.notificationID = id
|
||||
notification.title = InfoPlistReader(bundle: .app).bundleDisplayName
|
||||
notification.body = L10n.notification
|
||||
notification.threadIdentifier = roomId
|
||||
notification.threadIdentifier = roomID
|
||||
notification.categoryIdentifier = NotificationConstants.Category.reply
|
||||
notification.sound = isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil
|
||||
return notification
|
||||
}
|
||||
|
||||
private func processCommon(receiverId: String,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
private func processCommon(mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
var notification = UNMutableNotificationContent()
|
||||
notification.receiverID = receiverId
|
||||
// These are fallbacks since the senderIcon also sets the title and the subtitle
|
||||
notification.receiverID = receiverID
|
||||
notification.roomID = roomID
|
||||
notification.eventID = event.eventID
|
||||
notification.notificationID = id
|
||||
notification.title = senderDisplayName ?? roomDisplayName
|
||||
if notification.title != roomDisplayName {
|
||||
notification.subtitle = roomDisplayName
|
||||
}
|
||||
// We can store the room identifier into the thread identifier since it's used for notifications
|
||||
// that belong to the same group
|
||||
notification.threadIdentifier = roomId
|
||||
notification.threadIdentifier = roomID
|
||||
notification.categoryIdentifier = NotificationConstants.Category.reply
|
||||
notification.sound = isNoisy ? UNNotificationSound(named: UNNotificationSoundName(rawValue: "message.caf")) : nil
|
||||
|
||||
@@ -228,40 +246,28 @@ extension NotificationItemProxyProtocol {
|
||||
}
|
||||
|
||||
notification = try await notification.addSenderIcon(using: mediaProvider,
|
||||
senderId: senderId,
|
||||
receiverId: receiverId,
|
||||
senderId: event.senderID,
|
||||
receiverId: receiverID,
|
||||
senderName: senderName,
|
||||
groupName: groupName,
|
||||
mediaSource: mediaSource,
|
||||
roomId: roomId)
|
||||
roomId: roomID)
|
||||
return notification
|
||||
}
|
||||
|
||||
// MARK: Message Types
|
||||
|
||||
private func processText(content: TextMessageContent,
|
||||
receiverId: String,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
let notification = try await processCommon(receiverId: receiverId,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
let notification = try await processCommon(mediaProvider: mediaProvider)
|
||||
notification.body = content.body
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
private func processImage(content: ImageMessageContent,
|
||||
receiverId: String,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
var notification = try await processCommon(receiverId: receiverId,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
var notification = try await processCommon(mediaProvider: mediaProvider)
|
||||
notification.body = "📷 " + content.body
|
||||
|
||||
notification = await notification.addMediaAttachment(using: mediaProvider,
|
||||
@@ -272,14 +278,8 @@ extension NotificationItemProxyProtocol {
|
||||
}
|
||||
|
||||
private func processVideo(content: VideoMessageContent,
|
||||
receiverId: String,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
var notification = try await processCommon(receiverId: receiverId,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
var notification = try await processCommon(mediaProvider: mediaProvider)
|
||||
notification.body = "📹 " + content.body
|
||||
|
||||
notification = await notification.addMediaAttachment(using: mediaProvider,
|
||||
@@ -290,56 +290,32 @@ extension NotificationItemProxyProtocol {
|
||||
}
|
||||
|
||||
private func processFile(content: FileMessageContent,
|
||||
receiverId: String,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
let notification = try await processCommon(receiverId: receiverId,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
let notification = try await processCommon(mediaProvider: mediaProvider)
|
||||
notification.body = "📄 " + content.body
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
private func processNotice(content: NoticeMessageContent,
|
||||
receiverId: String,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
let notification = try await processCommon(receiverId: receiverId,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
let notification = try await processCommon(mediaProvider: mediaProvider)
|
||||
notification.body = "❕ " + content.body
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
private func processEmote(content: EmoteMessageContent,
|
||||
receiverId: String,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
let notification = try await processCommon(receiverId: receiverId,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
let notification = try await processCommon(mediaProvider: mediaProvider)
|
||||
notification.body = "🫥 " + content.body
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
private func processAudio(content: AudioMessageContent,
|
||||
receiverId: String,
|
||||
senderId: String,
|
||||
roomId: String,
|
||||
mediaProvider: MediaProviderProtocol?) async throws -> UNMutableNotificationContent {
|
||||
var notification = try await processCommon(receiverId: receiverId,
|
||||
senderId: senderId,
|
||||
roomId: roomId,
|
||||
mediaProvider: mediaProvider)
|
||||
var notification = try await processCommon(mediaProvider: mediaProvider)
|
||||
notification.body = "🔊 " + content.body
|
||||
|
||||
notification = await notification.addMediaAttachment(using: mediaProvider,
|
||||
|
||||
@@ -18,14 +18,16 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
class NotificationServiceProxy: NotificationServiceProxyProtocol {
|
||||
private let userID: String
|
||||
// private let service: NotificationServiceProtocol
|
||||
|
||||
init(basePath: String,
|
||||
userId: String) {
|
||||
userID: String) {
|
||||
self.userID = userID
|
||||
// service = NotificationService(basePath: basePath, userId: userId)
|
||||
}
|
||||
|
||||
func notificationItem(roomId: String, eventId: String) async throws -> NotificationItemProxyProtocol? {
|
||||
MockNotificationItemProxy(eventID: eventId, roomID: roomId)
|
||||
MockNotificationItemProxy(eventID: eventId, roomID: roomId, receiverID: userID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user