diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index e57ca35fe..8b0ad2a19 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -6779,7 +6779,7 @@ repositoryURL = "https://github.com/matrix-org/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.1.38; + version = 1.1.39; }; }; 821C67C9A7F8CC3FD41B28B4 /* XCRemoteSwiftPackageReference "emojibase-bindings" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index af774a8fd..adad58435 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -129,8 +129,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/matrix-org/matrix-rust-components-swift", "state" : { - "revision" : "691d8b0f0994d9669fadbd2452bef7270f3713ad", - "version" : "1.1.38" + "revision" : "8a5813a3cdf541bee3ceb4776c362d1f6b767581", + "version" : "1.1.39" } }, { @@ -262,7 +262,7 @@ { "identity" : "swiftui-introspect", "kind" : "remoteSourceControl", - "location" : "https://github.com/siteline/SwiftUI-Introspect", + "location" : "https://github.com/siteline/SwiftUI-Introspect.git", "state" : { "revision" : "b94da693e57eaf79d16464b8b7c90d09cba4e290", "version" : "0.9.2" diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index f8eed30f8..02c53f028 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -409,9 +409,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { self.roomProxy = roomProxy Task { - // Mark the room as read on entering but don't send read receipts - // as those will be handled by the timeline - await roomProxy.markAsRead(sendReadReceipts: false, receiptType: appSettings.sharePresence ? .read : .readPrivate) + // Flag the room as read on entering, the timeline will take care of the read receipts + await roomProxy.flagAsRead() } let userID = userSession.clientProxy.userID diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 2eaf2ffa7..c4d23f69c 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2232,42 +2232,59 @@ class RoomProxyMock: RoomProxyProtocol { return canUserTriggerRoomNotificationUserIDReturnValue } } - //MARK: - markAsUnread + //MARK: - flagAsUnread - var markAsUnreadCallsCount = 0 - var markAsUnreadCalled: Bool { - return markAsUnreadCallsCount > 0 + var flagAsUnreadCallsCount = 0 + var flagAsUnreadCalled: Bool { + return flagAsUnreadCallsCount > 0 } - var markAsUnreadReturnValue: Result! - var markAsUnreadClosure: (() async -> Result)? + var flagAsUnreadReturnValue: Result! + var flagAsUnreadClosure: (() async -> Result)? - func markAsUnread() async -> Result { - markAsUnreadCallsCount += 1 - if let markAsUnreadClosure = markAsUnreadClosure { - return await markAsUnreadClosure() + func flagAsUnread() async -> Result { + flagAsUnreadCallsCount += 1 + if let flagAsUnreadClosure = flagAsUnreadClosure { + return await flagAsUnreadClosure() } else { - return markAsUnreadReturnValue + return flagAsUnreadReturnValue + } + } + //MARK: - flagAsRead + + var flagAsReadCallsCount = 0 + var flagAsReadCalled: Bool { + return flagAsReadCallsCount > 0 + } + var flagAsReadReturnValue: Result! + var flagAsReadClosure: (() async -> Result)? + + func flagAsRead() async -> Result { + flagAsReadCallsCount += 1 + if let flagAsReadClosure = flagAsReadClosure { + return await flagAsReadClosure() + } else { + return flagAsReadReturnValue } } //MARK: - markAsRead - var markAsReadSendReadReceiptsReceiptTypeCallsCount = 0 - var markAsReadSendReadReceiptsReceiptTypeCalled: Bool { - return markAsReadSendReadReceiptsReceiptTypeCallsCount > 0 + var markAsReadReceiptTypeCallsCount = 0 + var markAsReadReceiptTypeCalled: Bool { + return markAsReadReceiptTypeCallsCount > 0 } - var markAsReadSendReadReceiptsReceiptTypeReceivedArguments: (sendReadReceipts: Bool, receiptType: ReceiptType)? - var markAsReadSendReadReceiptsReceiptTypeReceivedInvocations: [(sendReadReceipts: Bool, receiptType: ReceiptType)] = [] - var markAsReadSendReadReceiptsReceiptTypeReturnValue: Result! - var markAsReadSendReadReceiptsReceiptTypeClosure: ((Bool, ReceiptType) async -> Result)? + var markAsReadReceiptTypeReceivedReceiptType: ReceiptType? + var markAsReadReceiptTypeReceivedInvocations: [ReceiptType] = [] + var markAsReadReceiptTypeReturnValue: Result! + var markAsReadReceiptTypeClosure: ((ReceiptType) async -> Result)? - func markAsRead(sendReadReceipts: Bool, receiptType: ReceiptType) async -> Result { - markAsReadSendReadReceiptsReceiptTypeCallsCount += 1 - markAsReadSendReadReceiptsReceiptTypeReceivedArguments = (sendReadReceipts: sendReadReceipts, receiptType: receiptType) - markAsReadSendReadReceiptsReceiptTypeReceivedInvocations.append((sendReadReceipts: sendReadReceipts, receiptType: receiptType)) - if let markAsReadSendReadReceiptsReceiptTypeClosure = markAsReadSendReadReceiptsReceiptTypeClosure { - return await markAsReadSendReadReceiptsReceiptTypeClosure(sendReadReceipts, receiptType) + func markAsRead(receiptType: ReceiptType) async -> Result { + markAsReadReceiptTypeCallsCount += 1 + markAsReadReceiptTypeReceivedReceiptType = receiptType + markAsReadReceiptTypeReceivedInvocations.append(receiptType) + if let markAsReadReceiptTypeClosure = markAsReadReceiptTypeClosure { + return await markAsReadReceiptTypeClosure(receiptType) } else { - return markAsReadSendReadReceiptsReceiptTypeReturnValue + return markAsReadReceiptTypeReturnValue } } //MARK: - sendTypingNotification diff --git a/ElementX/Sources/Mocks/RoomProxyMock.swift b/ElementX/Sources/Mocks/RoomProxyMock.swift index b8bbc9312..fbe226dd0 100644 --- a/ElementX/Sources/Mocks/RoomProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomProxyMock.swift @@ -80,7 +80,10 @@ extension RoomProxyMock { canUserRedactOtherUserIDReturnValue = .success(false) canUserTriggerRoomNotificationUserIDReturnValue = .success(configuration.canUserTriggerRoomNotification) canUserJoinCallUserIDReturnValue = .success(configuration.canUserJoinCall) - markAsReadSendReadReceiptsReceiptTypeReturnValue = .success(()) + + flagAsReadReturnValue = .success(()) + flagAsUnreadReturnValue = .success(()) + markAsReadReceiptTypeReturnValue = .success(()) let widgetDriver = ElementCallWidgetDriverMock() widgetDriver.underlyingMessagePublisher = .init() diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 20e8b931e..ca9f78911 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -167,7 +167,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol return } - switch await roomProxy.markAsUnread() { + switch await roomProxy.flagAsUnread() { case .success: ServiceLocator.shared.analytics.trackInteraction(name: .MobileRoomListRoomContextMenuUnreadToggle) case .failure(let error): @@ -181,11 +181,15 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol return } - switch await roomProxy.markAsRead(sendReadReceipts: true, receiptType: appSettings.sharePresence ? .read : .readPrivate) { + switch await roomProxy.flagAsRead() { case .success: ServiceLocator.shared.analytics.trackInteraction(name: .MobileRoomListRoomContextMenuUnreadToggle) + + if case .failure(let error) = await roomProxy.markAsRead(receiptType: appSettings.sharePresence ? .read : .readPrivate) { + MXLog.error("Failed marking room \(roomIdentifier) as read with error: \(error)") + } case .failure(let error): - MXLog.error("Failed marking room \(roomIdentifier) as read with error: \(error)") + MXLog.error("Failed flagging room \(roomIdentifier) as read with error: \(error)") } } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 933feacfa..6d35b1240 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -418,12 +418,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol notificationCenter.post(name: .roomMarkedAsRead, object: roomProxy.id) } - switch await timelineController.sendReadReceipt(for: lastVisibleItemID) { - case .success: - break - case let .failure(error): - MXLog.error("[TimelineViewController] Failed to send read receipt: \(error)") - } + await timelineController.sendReadReceipt(for: lastVisibleItemID) } private func handleItemTapped(with itemID: TimelineItemIdentifier) async { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift index 7cd818559..4f56b4af9 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TimelineTableViewController.swift @@ -64,7 +64,7 @@ class TimelineTableViewController: UIViewController { paginateBackwardsPublisher.send() } - sendReadReceiptIfNeeded() + sendLastVisibleItemReadReceipt() } } @@ -144,6 +144,12 @@ class TimelineTableViewController: UIViewController { } .store(in: &cancellables) + NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification) + .sink { [weak self] _ in + self?.sendLastVisibleItemReadReceipt() + } + .store(in: &cancellables) + configureDataSource() } @@ -153,6 +159,8 @@ class TimelineTableViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + sendLastVisibleItemReadReceipt() + guard !hasAppearedOnce else { return } tableView.contentOffset.y = -1 hasAppearedOnce = true @@ -287,13 +295,20 @@ class TimelineTableViewController: UIViewController { coordinator.send(viewAction: .paginateBackwards) } - private func sendReadReceiptIfNeeded() { - guard let lastVisibleItemIndexPath = tableView.indexPathsForVisibleRows?.first, - let lastVisibleItemTimelineID = dataSource?.itemIdentifier(for: lastVisibleItemIndexPath), - let lastVisibleItemID = timelineItemsDictionary[lastVisibleItemTimelineID]?.identifier - else { return } + private func sendLastVisibleItemReadReceipt() { + // Find the last visible timeline item and send a read receipt for it + guard let visibleIndexPaths = tableView.indexPathsForVisibleRows else { + return + } - coordinator.send(viewAction: .sendReadReceiptIfNeeded(lastVisibleItemID)) + // These are already in reverse order because the table view is flipped + for indexPath in visibleIndexPaths { + if let visibleItemTimelineID = dataSource?.itemIdentifier(for: indexPath), + let visibleItemID = timelineItemsDictionary[visibleItemTimelineID]?.identifier { + coordinator.send(viewAction: .sendReadReceiptIfNeeded(visibleItemID)) + return + } + } } } @@ -327,15 +342,15 @@ extension TimelineTableViewController: UITableViewDelegate { } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - sendReadReceiptIfNeeded() + sendLastVisibleItemReadReceipt() } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - sendReadReceiptIfNeeded() + sendLastVisibleItemReadReceipt() } func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { - sendReadReceiptIfNeeded() + sendLastVisibleItemReadReceipt() } } diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 43147cd33..261891542 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -368,28 +368,33 @@ class RoomProxy: RoomProxyProtocol { } } - func markAsUnread() async -> Result { - MXLog.info("Marking room \(id) as unread") + func flagAsUnread() async -> Result { + MXLog.info("Flagging room \(id) as unread") do { - try await room.markAsUnread() + try await room.setUnreadFlag(newValue: true) return .success(()) } catch { MXLog.error("Failed marking room \(id) as unread with error: \(error)") - return .failure(.failedMarkingAsUnread) + return .failure(.failedFlaggingAsUnread) } } - func markAsRead(sendReadReceipts: Bool, receiptType: ReceiptType) async -> Result { - MXLog.info("Marking room \(id) as read, sending read receipts: \(sendReadReceipts)") + func flagAsRead() async -> Result { + MXLog.info("Flagging room \(id) as read") do { - if sendReadReceipts { - try await room.markAsReadAndSendReadReceipt(receiptType: receiptType) - } else { - try await room.markAsRead() - } - + try await room.setUnreadFlag(newValue: false) + return .success(()) + } catch { + MXLog.error("Failed marking room \(id) as read with error: \(error)") + return .failure(.failedFlaggingAsRead) + } + } + + func markAsRead(receiptType: ReceiptType) async -> Result { + do { + try await room.markAsRead(receiptType: receiptType) return .success(()) } catch { MXLog.error("Failed marking room \(id) as read with error: \(error)") diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index a23dfa697..a6000d4bb 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -31,8 +31,9 @@ enum RoomProxyError: Error, Equatable { case failedRemovingAvatar case failedUploadingAvatar case failedCheckingPermission + case failedFlaggingAsUnread + case failedFlaggingAsRead case failedMarkingAsRead - case failedMarkingAsUnread case failedSendingTypingNotice } @@ -108,9 +109,11 @@ protocol RoomProxyProtocol { func canUserTriggerRoomNotification(userID: String) async -> Result - func markAsUnread() async -> Result + func flagAsUnread() async -> Result - func markAsRead(sendReadReceipts: Bool, receiptType: ReceiptType) async -> Result + func flagAsRead() async -> Result + + func markAsRead(receiptType: ReceiptType) async -> Result /// https://spec.matrix.org/v1.9/client-server-api/#typing-notifications @discardableResult func sendTypingNotification(isTyping: Bool) async -> Result diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index df749860f..844c7e7d9 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -55,14 +55,9 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { return .success(()) } - func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result { - guard let roomProxy, let eventID = itemID.eventID else { return .failure(.generic) } - switch await roomProxy.timeline.sendReadReceipt(for: eventID, type: .read) { - case .success: - return .success(()) - case .failure: - return .failure(.generic) - } + func sendReadReceipt(for itemID: TimelineItemIdentifier) async { + guard let roomProxy, let eventID = itemID.eventID else { return } + _ = await roomProxy.timeline.sendReadReceipt(for: eventID, type: .read) } func processItemAppearance(_ itemID: TimelineItemIdentifier) async { } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index ad162359f..752bf0fba 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -16,6 +16,7 @@ import Combine import Foundation +import MatrixRustSDK import UIKit class RoomTimelineController: RoomTimelineControllerProtocol { @@ -91,17 +92,18 @@ class RoomTimelineController: RoomTimelineControllerProtocol { } } - func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result { - guard let eventID = itemID.eventID else { - return .failure(.generic) - } + func sendReadReceipt(for itemID: TimelineItemIdentifier) async { + let receiptType: MatrixRustSDK.ReceiptType = appSettings.sharePresence ? .read : .readPrivate - switch await roomProxy.timeline.sendReadReceipt(for: eventID, - type: appSettings.sharePresence ? .read : .readPrivate) { - case .success: - return .success(()) - case .failure: - return .failure(.generic) + // Mark the whole room as read if it's the last timeline item + if timelineItems.last?.id == itemID { + _ = await roomProxy.markAsRead(receiptType: receiptType) + } else { + guard let eventID = itemID.eventID else { + return + } + + _ = await roomProxy.timeline.sendReadReceipt(for: eventID, type: receiptType) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 9338e64c9..2671696cc 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -49,7 +49,7 @@ protocol RoomTimelineControllerProtocol { func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result - func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result + func sendReadReceipt(for itemID: TimelineItemIdentifier) async func sendMessage(_ message: String, html: String?, diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 5c8e8cfbf..1c26a39dc 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -454,7 +454,7 @@ final class TimelineProxy: TimelineProxyProtocol { } func sendReadReceipt(for eventID: String, type: ReceiptType) async -> Result { - MXLog.info("Sending read receipt for eventID: \(eventID)") + MXLog.verbose("Sending read receipt for eventID: \(eventID)") sendMessageBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) defer { diff --git a/project.yml b/project.yml index 1bd11baf7..b22317647 100644 --- a/project.yml +++ b/project.yml @@ -47,7 +47,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/matrix-org/matrix-rust-components-swift - exactVersion: 1.1.38 + exactVersion: 1.1.39 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios