diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 089741f42..294f194c8 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -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) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index b76bacb05..fb731de91 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -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 { diff --git a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift index d4bc0a4e0..4c2961d2d 100644 --- a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift @@ -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, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift index f3a209a5f..8a1230996 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift @@ -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, diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift index 24d7bb8a2..1bbc05a5b 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift @@ -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, diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index 2abf06f0b..ee9086ff0 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -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 { diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift index 5162deb68..90406af9f 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManagerProtocol.swift @@ -33,4 +33,6 @@ protocol NotificationManagerProtocol: AnyObject { func requestAuthorization() func removeDeliveredMessageNotifications(for roomID: String) async + + func removeDeliveredNotificationsForFullyReadRooms(_ rooms: [RoomSummary]) async } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift index 0baa9a16f..287829ae9 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift @@ -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 diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index 7559bbf96..5f8747575 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -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), diff --git a/UnitTests/Sources/HomeScreenRoomTests.swift b/UnitTests/Sources/HomeScreenRoomTests.swift index d711f456f..00bef4aac 100644 --- a/UnitTests/Sources/HomeScreenRoomTests.swift +++ b/UnitTests/Sources/HomeScreenRoomTests.swift @@ -28,6 +28,7 @@ class HomeScreenRoomTests: XCTestCase { avatarURL: nil, heroes: [], lastMessage: nil, + lastMessageDate: nil, lastMessageFormattedTimestamp: nil, unreadMessagesCount: unreadMessagesCount, unreadMentionsCount: unreadMentionsCount, diff --git a/UnitTests/Sources/LoggingTests.swift b/UnitTests/Sources/LoggingTests.swift index ebbe75a14..8d0291861 100644 --- a/UnitTests/Sources/LoggingTests.swift +++ b/UnitTests/Sources/LoggingTests.swift @@ -86,6 +86,7 @@ class LoggingTests: XCTestCase { avatarURL: nil, heroes: [.init(userID: "", displayName: heroName)], lastMessage: AttributedString(lastMessage), + lastMessageDate: nil, lastMessageFormattedTimestamp: "Now", unreadMessagesCount: 0, unreadMentionsCount: 0, diff --git a/UnitTests/Sources/RoomSummaryTests.swift b/UnitTests/Sources/RoomSummaryTests.swift index 74d8e1b05..7d17d0c01 100644 --- a/UnitTests/Sources/RoomSummaryTests.swift +++ b/UnitTests/Sources/RoomSummaryTests.swift @@ -62,6 +62,7 @@ class RoomSummaryTests: XCTestCase { avatarURL: hasRoomAvatar ? roomDetails.avatarURL : nil, heroes: heroes, lastMessage: nil, + lastMessageDate: nil, lastMessageFormattedTimestamp: nil, unreadMessagesCount: 0, unreadMentionsCount: 0,