mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 12:07:55 +00:00
Remove delivered notifications for rooms that have meantime become fully read and have messages newer than what's delivered
- this is a best effort solution to cleanup outdated notifications when using multiple clients and reading them elsewhere
This commit is contained in:
committed by
Stefan Ceriu
parent
e14672e1f6
commit
899c33a5ad
@@ -387,39 +387,22 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
userSession.clientProxy.actionsPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] action in
|
||||
guard let self, case let .receivedDecryptionError(info) = action else {
|
||||
return
|
||||
}
|
||||
guard let self else { return }
|
||||
|
||||
let timeToDecryptMs: Int
|
||||
if let unsignedTimeToDecryptMs = info.timeToDecryptMs {
|
||||
timeToDecryptMs = Int(unsignedTimeToDecryptMs)
|
||||
} else {
|
||||
timeToDecryptMs = -1
|
||||
switch action {
|
||||
case .receivedDecryptionError(let info):
|
||||
processDecryptionError(info)
|
||||
case .receivedSyncUpdate:
|
||||
Task {
|
||||
let roomSummaries = self.userSession.clientProxy.staticRoomSummaryProvider.roomListPublisher.value
|
||||
await self.notificationManager.removeDeliveredNotificationsForFullyReadRooms(roomSummaries)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let errorName: AnalyticsEvent.Error.Name = switch info.cause {
|
||||
case .unknown: .OlmKeysNotSentError
|
||||
case .unknownDevice, .unsignedDevice: .ExpectedSentByInsecureDevice
|
||||
case .verificationViolation: .ExpectedVerificationViolation
|
||||
case .sentBeforeWeJoined: .ExpectedDueToMembership
|
||||
case .historicalMessageAndBackupIsDisabled, .historicalMessageAndDeviceIsUnverified: .HistoricalMessage
|
||||
case .withheldForUnverifiedOrInsecureDevice: .RoomKeysWithheldForUnverifiedDevice
|
||||
case .withheldBySender: .OlmKeysNotSentError
|
||||
}
|
||||
|
||||
analytics.trackError(context: nil,
|
||||
domain: .E2EE,
|
||||
name: errorName,
|
||||
timeToDecryptMillis: timeToDecryptMs,
|
||||
eventLocalAgeMillis: Int(truncatingIfNeeded: info.eventLocalAgeMillis),
|
||||
isFederated: info.ownHomeserver != info.senderHomeserver,
|
||||
isMatrixDotOrg: info.ownHomeserver == "matrix.org",
|
||||
userTrustsOwnIdentity: info.userTrustsOwnIdentity,
|
||||
wasVisibleToUser: nil)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
elementCallService.actions
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] action in
|
||||
@@ -444,6 +427,35 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func processDecryptionError(_ info: UnableToDecryptInfo) {
|
||||
let timeToDecryptMs: Int
|
||||
if let unsignedTimeToDecryptMs = info.timeToDecryptMs {
|
||||
timeToDecryptMs = Int(unsignedTimeToDecryptMs)
|
||||
} else {
|
||||
timeToDecryptMs = -1
|
||||
}
|
||||
|
||||
let errorName: AnalyticsEvent.Error.Name = switch info.cause {
|
||||
case .unknown: .OlmKeysNotSentError
|
||||
case .unknownDevice, .unsignedDevice: .ExpectedSentByInsecureDevice
|
||||
case .verificationViolation: .ExpectedVerificationViolation
|
||||
case .sentBeforeWeJoined: .ExpectedDueToMembership
|
||||
case .historicalMessageAndBackupIsDisabled, .historicalMessageAndDeviceIsUnverified: .HistoricalMessage
|
||||
case .withheldForUnverifiedOrInsecureDevice: .RoomKeysWithheldForUnverifiedDevice
|
||||
case .withheldBySender: .OlmKeysNotSentError
|
||||
}
|
||||
|
||||
analytics.trackError(context: nil,
|
||||
domain: .E2EE,
|
||||
name: errorName,
|
||||
timeToDecryptMillis: timeToDecryptMs,
|
||||
eventLocalAgeMillis: Int(truncatingIfNeeded: info.eventLocalAgeMillis),
|
||||
isFederated: info.ownHomeserver != info.senderHomeserver,
|
||||
isMatrixDotOrg: info.ownHomeserver == "matrix.org",
|
||||
userTrustsOwnIdentity: info.userTrustsOwnIdentity,
|
||||
wasVisibleToUser: nil)
|
||||
}
|
||||
|
||||
private func setupSessionVerificationRequestsObserver() {
|
||||
userSession.clientProxy.sessionVerificationController?.actions
|
||||
.receive(on: DispatchQueue.main)
|
||||
|
||||
@@ -12026,6 +12026,47 @@ class NotificationManagerMock: NotificationManagerProtocol, @unchecked Sendable
|
||||
}
|
||||
await removeDeliveredMessageNotificationsForClosure?(roomID)
|
||||
}
|
||||
//MARK: - removeDeliveredNotificationsForFullyReadRooms
|
||||
|
||||
var removeDeliveredNotificationsForFullyReadRoomsUnderlyingCallsCount = 0
|
||||
var removeDeliveredNotificationsForFullyReadRoomsCallsCount: Int {
|
||||
get {
|
||||
if Thread.isMainThread {
|
||||
return removeDeliveredNotificationsForFullyReadRoomsUnderlyingCallsCount
|
||||
} else {
|
||||
var returnValue: Int? = nil
|
||||
DispatchQueue.main.sync {
|
||||
returnValue = removeDeliveredNotificationsForFullyReadRoomsUnderlyingCallsCount
|
||||
}
|
||||
|
||||
return returnValue!
|
||||
}
|
||||
}
|
||||
set {
|
||||
if Thread.isMainThread {
|
||||
removeDeliveredNotificationsForFullyReadRoomsUnderlyingCallsCount = newValue
|
||||
} else {
|
||||
DispatchQueue.main.sync {
|
||||
removeDeliveredNotificationsForFullyReadRoomsUnderlyingCallsCount = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var removeDeliveredNotificationsForFullyReadRoomsCalled: Bool {
|
||||
return removeDeliveredNotificationsForFullyReadRoomsCallsCount > 0
|
||||
}
|
||||
var removeDeliveredNotificationsForFullyReadRoomsReceivedRooms: [RoomSummary]?
|
||||
var removeDeliveredNotificationsForFullyReadRoomsReceivedInvocations: [[RoomSummary]] = []
|
||||
var removeDeliveredNotificationsForFullyReadRoomsClosure: (([RoomSummary]) async -> Void)?
|
||||
|
||||
func removeDeliveredNotificationsForFullyReadRooms(_ rooms: [RoomSummary]) async {
|
||||
removeDeliveredNotificationsForFullyReadRoomsCallsCount += 1
|
||||
removeDeliveredNotificationsForFullyReadRoomsReceivedRooms = rooms
|
||||
DispatchQueue.main.async {
|
||||
self.removeDeliveredNotificationsForFullyReadRoomsReceivedInvocations.append(rooms)
|
||||
}
|
||||
await removeDeliveredNotificationsForFullyReadRoomsClosure?(rooms)
|
||||
}
|
||||
}
|
||||
class NotificationSettingsProxyMock: NotificationSettingsProxyProtocol, @unchecked Sendable {
|
||||
var callbacks: PassthroughSubject<NotificationSettingsProxyCallback, Never> {
|
||||
|
||||
@@ -79,6 +79,7 @@ extension RoomSummary {
|
||||
avatarURL: nil,
|
||||
heroes: [],
|
||||
lastMessage: AttributedString("I do not wish to take the trouble to understand mysticism"),
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: "14:56",
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -102,6 +103,7 @@ extension Array where Element == RoomSummary {
|
||||
avatarURL: nil,
|
||||
heroes: [],
|
||||
lastMessage: AttributedString("I do not wish to take the trouble to understand mysticism"),
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: "14:56",
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -120,6 +122,7 @@ extension Array where Element == RoomSummary {
|
||||
avatarURL: .mockMXCAvatar,
|
||||
heroes: [],
|
||||
lastMessage: AttributedString("How do you see the Emperor then? You think he keeps office hours?"),
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: "2:56 PM",
|
||||
unreadMessagesCount: 2,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -138,6 +141,7 @@ extension Array where Element == RoomSummary {
|
||||
avatarURL: nil,
|
||||
heroes: [],
|
||||
lastMessage: try? AttributedString(markdown: "He certainly seemed no *mental genius* to me"),
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: "Some time ago",
|
||||
unreadMessagesCount: 3,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -156,6 +160,7 @@ extension Array where Element == RoomSummary {
|
||||
avatarURL: nil,
|
||||
heroes: [],
|
||||
lastMessage: AttributedString("There's an archaic measure of time that's called the month"),
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: "Just now",
|
||||
unreadMessagesCount: 2,
|
||||
unreadMentionsCount: 2,
|
||||
@@ -174,6 +179,7 @@ extension Array where Element == RoomSummary {
|
||||
avatarURL: nil,
|
||||
heroes: [],
|
||||
lastMessage: AttributedString("Clearly, if Earth is powerful enough to do that, it might also be capable of adjusting minds in order to force belief in its radioactivity"),
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: "1986",
|
||||
unreadMessagesCount: 1,
|
||||
unreadMentionsCount: 1,
|
||||
@@ -192,6 +198,7 @@ extension Array where Element == RoomSummary {
|
||||
avatarURL: nil,
|
||||
heroes: [],
|
||||
lastMessage: AttributedString("Are you groping for the word 'paranoia'?"),
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: "きょうはじゅういちがつじゅういちにちです",
|
||||
unreadMessagesCount: 6,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -210,6 +217,7 @@ extension Array where Element == RoomSummary {
|
||||
avatarURL: nil,
|
||||
heroes: [],
|
||||
lastMessage: nil,
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: nil,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -261,6 +269,7 @@ extension Array where Element == RoomSummary {
|
||||
avatarURL: .mockMXCAvatar,
|
||||
heroes: [],
|
||||
lastMessage: nil,
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: nil,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -279,6 +288,7 @@ extension Array where Element == RoomSummary {
|
||||
avatarURL: nil,
|
||||
heroes: [],
|
||||
lastMessage: nil,
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: nil,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
|
||||
@@ -207,6 +207,7 @@ private extension HomeScreenRoom {
|
||||
avatarURL: nil,
|
||||
heroes: [.init(userID: "@someone:somewhere.com")],
|
||||
lastMessage: nil,
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: nil,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -235,6 +236,7 @@ private extension HomeScreenRoom {
|
||||
avatarURL: avatarURL,
|
||||
heroes: [.init(userID: "@someone:somewhere.com")],
|
||||
lastMessage: nil,
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: nil,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
|
||||
@@ -152,6 +152,7 @@ private extension HomeScreenRoom {
|
||||
avatarURL: nil,
|
||||
heroes: [.init(userID: "@someone:somewhere.com")],
|
||||
lastMessage: nil,
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: nil,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
@@ -180,6 +181,7 @@ private extension HomeScreenRoom {
|
||||
avatarURL: avatarURL,
|
||||
heroes: [.init(userID: "@someone:somewhere.com")],
|
||||
lastMessage: nil,
|
||||
lastMessageDate: nil,
|
||||
lastMessageFormattedTimestamp: nil,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
|
||||
@@ -128,6 +128,28 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
|
||||
.map(\.request.identifier)
|
||||
notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationsIdentifiers)
|
||||
}
|
||||
|
||||
func removeDeliveredNotificationsForFullyReadRooms(_ rooms: [RoomSummary]) async {
|
||||
let roomsToLastMessageDates = rooms
|
||||
.filter { $0.hasUnreadMessages == false }
|
||||
.reduce(into: [:]) { partialResult, roomSummary in
|
||||
partialResult[roomSummary.id] = roomSummary.lastMessageDate
|
||||
}
|
||||
|
||||
let notificationsIdentifiers = await notificationCenter
|
||||
.deliveredNotifications()
|
||||
.filter { notification in
|
||||
guard let roomID = notification.request.content.roomID,
|
||||
let lastMessageDate = roomsToLastMessageDates[roomID] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return notification.date <= lastMessageDate
|
||||
}
|
||||
.map(\.request.identifier)
|
||||
|
||||
notificationCenter.removeDeliveredNotifications(withIdentifiers: notificationsIdentifiers)
|
||||
}
|
||||
|
||||
private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool {
|
||||
do {
|
||||
|
||||
@@ -33,4 +33,6 @@ protocol NotificationManagerProtocol: AnyObject {
|
||||
func requestAuthorization()
|
||||
|
||||
func removeDeliveredMessageNotifications(for roomID: String) async
|
||||
|
||||
func removeDeliveredNotificationsForFullyReadRooms(_ rooms: [RoomSummary]) async
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ struct RoomSummary {
|
||||
let avatarURL: URL?
|
||||
let heroes: [UserProfileProxy]
|
||||
let lastMessage: AttributedString?
|
||||
let lastMessageDate: Date?
|
||||
let lastMessageFormattedTimestamp: String?
|
||||
let unreadMessagesCount: UInt
|
||||
let unreadMentionsCount: UInt
|
||||
@@ -95,6 +96,7 @@ extension RoomSummary {
|
||||
avatarURL = nil
|
||||
heroes = []
|
||||
lastMessage = AttributedString(string)
|
||||
lastMessageDate = .now
|
||||
lastMessageFormattedTimestamp = "Now"
|
||||
unreadMessagesCount = hasUnreadMessages ? 1 : 0
|
||||
unreadMentionsCount = hasUnreadMentions ? 1 : 0
|
||||
|
||||
@@ -242,10 +242,12 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
}
|
||||
|
||||
var attributedLastMessage: AttributedString?
|
||||
var lastMessageDate: Date?
|
||||
var lastMessageFormattedTimestamp: String?
|
||||
|
||||
if let latestRoomMessage = roomDetails.latestEvent {
|
||||
let lastMessage = EventTimelineItemProxy(item: latestRoomMessage, uniqueID: .init("0"))
|
||||
lastMessageDate = lastMessage.timestamp
|
||||
lastMessageFormattedTimestamp = lastMessage.timestamp.formattedMinimal()
|
||||
attributedLastMessage = eventStringBuilder.buildAttributedString(for: lastMessage)
|
||||
}
|
||||
@@ -271,6 +273,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
|
||||
heroes: roomInfo.heroes.map(UserProfileProxy.init),
|
||||
lastMessage: attributedLastMessage,
|
||||
lastMessageDate: lastMessageDate,
|
||||
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
|
||||
unreadMessagesCount: UInt(roomInfo.numUnreadMessages),
|
||||
unreadMentionsCount: UInt(roomInfo.numUnreadMentions),
|
||||
|
||||
Reference in New Issue
Block a user