From f70182d09aa14511eca2f5b98c19f9afb4a991e3 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 13 May 2026 19:46:52 +0300 Subject: [PATCH] Add a developer option button for marking every single room as read (#5582) This will use an underlying SDK method and send a private read receipt plus a fully read marker in every single room the user is part of. It will most likely be a long operation that will get rate limited so the task is set to run detached for as long as it takes. --- .../Mocks/Generated/GeneratedMocks.swift | 65 +++++++++++++++++++ .../DeveloperOptionsScreenModels.swift | 1 + .../DeveloperOptionsScreenViewModel.swift | 7 ++ .../View/DeveloperOptionsScreen.swift | 21 ++++++ .../Sources/Services/Client/ClientProxy.swift | 9 +++ .../Services/Client/ClientProxyProtocol.swift | 2 + 6 files changed, 105 insertions(+) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index ee3128987..4d46d5124 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -5200,6 +5200,71 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable { return optimizeStoresReturnValue } } + //MARK: - markAllRoomsAsRead + + var markAllRoomsAsReadUnderlyingCallsCount = 0 + var markAllRoomsAsReadCallsCount: Int { + get { + if Thread.isMainThread { + return markAllRoomsAsReadUnderlyingCallsCount + } else { + var returnValue: Int? = nil + DispatchQueue.main.sync { + returnValue = markAllRoomsAsReadUnderlyingCallsCount + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + markAllRoomsAsReadUnderlyingCallsCount = newValue + } else { + DispatchQueue.main.sync { + markAllRoomsAsReadUnderlyingCallsCount = newValue + } + } + } + } + var markAllRoomsAsReadCalled: Bool { + return markAllRoomsAsReadCallsCount > 0 + } + + var markAllRoomsAsReadUnderlyingReturnValue: Result! + var markAllRoomsAsReadReturnValue: Result! { + get { + if Thread.isMainThread { + return markAllRoomsAsReadUnderlyingReturnValue + } else { + var returnValue: Result? = nil + DispatchQueue.main.sync { + returnValue = markAllRoomsAsReadUnderlyingReturnValue + } + + return returnValue! + } + } + set { + if Thread.isMainThread { + markAllRoomsAsReadUnderlyingReturnValue = newValue + } else { + DispatchQueue.main.sync { + markAllRoomsAsReadUnderlyingReturnValue = newValue + } + } + } + } + var markAllRoomsAsReadClosure: (() async -> Result)? + + @discardableResult + func markAllRoomsAsRead() async -> Result { + markAllRoomsAsReadCallsCount += 1 + if let markAllRoomsAsReadClosure = markAllRoomsAsReadClosure { + return await markAllRoomsAsReadClosure() + } else { + return markAllRoomsAsReadReturnValue + } + } //MARK: - storeSizes var storeSizesUnderlyingCallsCount = 0 diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 7d5693f29..9beb6bee5 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -48,6 +48,7 @@ struct DeveloperOptionsScreenViewStateBindings { enum DeveloperOptionsScreenViewAction { case clearCache + case markAllRoomsAsRead } protocol DeveloperOptionsProtocol: AnyObject { diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index 9ad05242e..0c096148f 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -18,7 +18,10 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve actionsSubject.eraseToAnyPublisher() } + private let clientProxy: ClientProxyProtocol? + init(developerOptions: DeveloperOptionsProtocol, elementCallBaseURL: URL, appHooks: AppHooks, clientProxy: ClientProxyProtocol?) { + self.clientProxy = clientProxy super.init(initialViewState: .init(elementCallBaseURL: elementCallBaseURL, appHooks: appHooks, shouldShowClearCache: clientProxy != nil, @@ -55,6 +58,10 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve switch viewAction { case .clearCache: actionsSubject.send(.clearCache) + case .markAllRoomsAsRead: + Task.detached { + await self.clientProxy?.markAllRoomsAsRead() + } } } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index d952d69ab..4c84345b3 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -10,6 +10,7 @@ import SwiftUI struct DeveloperOptionsScreen: View { @Environment(\.dismiss) private var dismiss + @State private var showMarkAllRoomsAsReadAlert = false @Bindable var context: DeveloperOptionsScreenViewModel.Context @@ -130,6 +131,26 @@ struct DeveloperOptionsScreen: View { } } + Section { + Button { + showMarkAllRoomsAsReadAlert = true + } label: { + Text("Mark all rooms as read") + }.alert("Are you sure you want to mark all the rooms as read?", isPresented: $showMarkAllRoomsAsReadAlert) { + Button("Cancel", role: .cancel) { } + + Button("Yes") { + context.send(viewAction: .markAllRoomsAsRead) + } + } + } footer: { + Text(""" + This will send a private read receipt and a read marker in every room you are part of. \ + It's a long running operation that might get rate limited. \ + It will run in the background but the app must be alive for it to finish. + """) + } + Section { Button { showConfetti = true diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 80edd14f0..1ff29d450 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -857,6 +857,15 @@ class ClientProxy: ClientProxyProtocol { return .failure(.sdkError(error)) } } + + func markAllRoomsAsRead() async -> Result { + do { + return try await .success(client.markAllRoomsAsRead()) + } catch { + MXLog.error("Failed marking all rooms as read with error: \(error)") + return .failure(.sdkError(error)) + } + } func storeSizes() async -> Result { do { diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 575f8fd7b..d7fd837c2 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -245,6 +245,8 @@ protocol ClientProxyProtocol: AnyObject { @discardableResult func clearCaches() async -> Result @discardableResult func optimizeStores() async -> Result + + @discardableResult func markAllRoomsAsRead() async -> Result func storeSizes() async -> Result