From b6ade2d4a9e8438f1572aef4951b3e71d744fee0 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 29 Apr 2025 17:43:56 +0200 Subject: [PATCH] updated SDK and improved report flow the report flow is now based on the matrix version and the new one will only be used if the SDK checks if the server supports it. --- ElementX.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- ElementX/Sources/Application/AppSettings.swift | 7 ------- ElementX/Sources/Mocks/ClientProxyMock.swift | 2 ++ .../Mocks/Generated/GeneratedMocks.swift | 17 +++++++++++++++++ .../HomeScreen/HomeScreenViewModel.swift | 12 ++++++------ .../JoinRoomScreenViewModel.swift | 6 +++--- .../RoomDetailsScreenViewModel.swift | 6 +++--- .../DeveloperOptionsScreenModels.swift | 2 -- .../View/DeveloperOptionsScreen.swift | 11 ----------- .../OIDCConfigurationProxy.swift | 1 - .../Sources/Services/Client/ClientProxy.swift | 13 ++++++++++++- .../Services/Client/ClientProxyProtocol.swift | 2 ++ .../Services/Timeline/TimelineProxy.swift | 10 +++++----- .../Sources/HomeScreenViewModelTests.swift | 18 ++++++++++++++++-- .../Sources/JoinRoomScreenViewModelTests.swift | 15 ++++++++++++--- project.yml | 2 +- 17 files changed, 82 insertions(+), 48 deletions(-) diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 1dce2aefe..57fc215b6 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -8659,7 +8659,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 25.04.16; + version = 25.04.29; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0a5e7eaf5..970b0d62e 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -158,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "895ba54c171859cace965e77b6fe2f64919ecb27", - "version" : "25.4.16" + "revision" : "cd4509acd56641c4d110b29f692ecafe5438698f", + "version" : "25.4.29" } }, { diff --git a/ElementX/Sources/Application/AppSettings.swift b/ElementX/Sources/Application/AppSettings.swift index 24c43e707..d33da3139 100644 --- a/ElementX/Sources/Application/AppSettings.swift +++ b/ElementX/Sources/Application/AppSettings.swift @@ -57,8 +57,6 @@ final class AppSettings { case fuzzyRoomListSearchEnabled case enableOnlySignedDeviceIsolationMode case knockingEnabled - case reportRoomEnabled - case reportInviteEnabled case threadsEnabled } @@ -327,11 +325,6 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.knockingEnabled, defaultValue: false, storageType: .userDefaults(store)) var knockingEnabled - @UserPreference(key: UserDefaultsKeys.reportRoomEnabled, defaultValue: false, storageType: .userDefaults(store)) var reportRoomEnabled - - @UserPreference(key: UserDefaultsKeys.reportInviteEnabled, defaultValue: false, storageType: .userDefaults(store)) - var reportInviteEnabled - @UserPreference(key: UserDefaultsKeys.threadsEnabled, defaultValue: false, storageType: .userDefaults(store)) var threadsEnabled diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index 31bd7bad5..aa1c97912 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -92,5 +92,7 @@ extension ClientProxyMock { } userIdentityForReturnValue = .success(UserIdentityProxyMock(configuration: .init())) + + underlyingIsReportRoomSupported = true } } diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index e59c9275f..11a049ebc 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -2303,6 +2303,23 @@ class ClientProxyMock: ClientProxyProtocol, @unchecked Sendable { } var underlyingSecureBackupController: SecureBackupControllerProtocol! var sessionVerificationController: SessionVerificationControllerProxyProtocol? + var isReportRoomSupportedCallsCount = 0 + var isReportRoomSupportedCalled: Bool { + return isReportRoomSupportedCallsCount > 0 + } + + var isReportRoomSupported: Bool { + get async { + isReportRoomSupportedCallsCount += 1 + if let isReportRoomSupportedClosure = isReportRoomSupportedClosure { + return await isReportRoomSupportedClosure() + } else { + return underlyingIsReportRoomSupported + } + } + } + var underlyingIsReportRoomSupported: Bool! + var isReportRoomSupportedClosure: (() async -> Bool)? //MARK: - isOnlyDeviceLeft diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 4281c87c3..cd5aac873 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -108,9 +108,9 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol .weakAssign(to: \.state.hideInviteAvatars, on: self) .store(in: &cancellables) - appSettings.$reportRoomEnabled - .weakAssign(to: \.state.reportRoomEnabled, on: self) - .store(in: &cancellables) + Task { + state.reportRoomEnabled = await userSession.clientProxy.isReportRoomSupported + } let isSearchFieldFocused = context.$viewState.map(\.bindings.isSearchFieldFocused) let searchQuery = context.$viewState.map(\.bindings.searchQuery) @@ -208,7 +208,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol await acceptInvite(roomID: roomIdentifier) } case .declineInvite(let roomIdentifier): - showDeclineInviteConfirmationAlert(roomID: roomIdentifier) + Task { await showDeclineInviteConfirmationAlert(roomID: roomIdentifier) } } } @@ -419,7 +419,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } } - private func showDeclineInviteConfirmationAlert(roomID: String) { + private func showDeclineInviteConfirmationAlert(roomID: String) async { guard let room = state.rooms.first(where: { $0.id == roomID }) else { displayError() return @@ -429,7 +429,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol let title = room.isDirect ? L10n.screenInvitesDeclineDirectChatTitle : L10n.screenInvitesDeclineChatTitle let message = room.isDirect ? L10n.screenInvitesDeclineDirectChatMessage(roomPlaceholder) : L10n.screenInvitesDeclineChatMessage(roomPlaceholder) - if appSettings.reportInviteEnabled, let userID = room.inviter?.id { + if await userSession.clientProxy.isReportRoomSupported, let userID = room.inviter?.id { state.bindings.alertInfo = .init(id: UUID(), title: title, message: message, diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift index caf401169..38f82d13e 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift @@ -83,7 +83,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo case .dismiss: actionsSubject.send(.dismiss) case .declineInviteAndBlock(let userID): - showDeclineAndBlockConfirmationAlert(userID: userID) + Task { await showDeclineAndBlockConfirmationAlert(userID: userID) } } } @@ -322,8 +322,8 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo secondaryButton: .init(title: L10n.screenJoinRoomCancelKnockAlertConfirmation, role: .destructive) { Task { await self.cancelKnock() } }) } - private func showDeclineAndBlockConfirmationAlert(userID: String) { - if appSettings.reportInviteEnabled { + private func showDeclineAndBlockConfirmationAlert(userID: String) async { + if await clientProxy.isReportRoomSupported { actionsSubject.send(.presentDeclineAndBlock(userID: userID)) } else { state.bindings.alertInfo = .init(id: .declineInviteAndBlock, diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 698d18106..4fdd0cadf 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -78,9 +78,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr .weakAssign(to: \.state.knockingEnabled, on: self) .store(in: &cancellables) - appSettings.$reportRoomEnabled - .weakAssign(to: \.state.reportRoomEnabled, on: self) - .store(in: &cancellables) + Task { + state.reportRoomEnabled = await clientProxy.isReportRoomSupported + } appMediator.networkMonitor.reachabilityPublisher .filter { $0 == .reachable } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index ea89e82b8..2f086e04c 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -44,8 +44,6 @@ protocol DeveloperOptionsProtocol: AnyObject { var enableOnlySignedDeviceIsolationMode: Bool { get set } var elementCallBaseURLOverride: URL? { get set } var knockingEnabled: Bool { get set } - var reportRoomEnabled: Bool { get set } - var reportInviteEnabled: Bool { get set } var threadsEnabled: Bool { get set } var isNewBloomEnabled: Bool { get set } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index ab92a2fd1..de89245e0 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -73,17 +73,6 @@ struct DeveloperOptionsScreen: View { } footer: { Text("This setting controls how end-to-end encryption (E2EE) keys are exchanged. Enabling it will prevent the inclusion of devices that have not been explicitly verified by their owners.") } - - Section("Reporting") { - Toggle(isOn: $context.reportRoomEnabled) { - Text("Report rooms") - Text("Report API might not work properly") - } - Toggle(isOn: $context.reportInviteEnabled) { - Text("Report invites") - Text("Report API might not work properly") - } - } Section { TextField("Leave empty to use EC locally", text: $elementCallURLOverrideString) diff --git a/ElementX/Sources/Services/Authentication/OIDCConfigurationProxy.swift b/ElementX/Sources/Services/Authentication/OIDCConfigurationProxy.swift index 17acfcb45..b36b2b815 100644 --- a/ElementX/Sources/Services/Authentication/OIDCConfigurationProxy.swift +++ b/ElementX/Sources/Services/Authentication/OIDCConfigurationProxy.swift @@ -28,7 +28,6 @@ extension OIDCConfigurationProxy { logoUri: logoURI.absoluteString, tosUri: tosURI.absoluteString, policyUri: policyURI.absoluteString, - contacts: nil, staticRegistrations: staticRegistrations) } } diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index ffa1e9f90..b3b570138 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -168,7 +168,7 @@ class ClientProxy: ClientProxyProtocol { syncServiceStateUpdateTaskHandle = createSyncServiceStateObserver(syncService) roomListStateUpdateTaskHandle = createRoomListServiceObserver(roomListService) roomListStateLoadingStateUpdateTaskHandle = createRoomListLoadingStateUpdateObserver(roomListService) - + delegateHandle = client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in self?.hasEncounteredAuthError = true self?.actionsSubject.send(.receivedAuthError(isSoftLogout: isSoftLogout)) @@ -269,6 +269,17 @@ class ClientProxy: ClientProxyProtocol { return nil } } + + var isReportRoomSupported: Bool { + get async { + do { + return try await client.isReportRoomApiSupported() + } catch { + MXLog.error("Failed checking report room support with error: \(error)") + return false + } + } + } private(set) lazy var pusherNotificationClientIdentifier: String? = { // NOTE: The result is stored as part of the restoration token. Any changes diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 97450350e..bc979ed1d 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -113,6 +113,8 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol { var sessionVerificationController: SessionVerificationControllerProxyProtocol? { get } + var isReportRoomSupported: Bool { get async } + func isOnlyDeviceLeft() async -> Result func startSync() diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 494bdfde5..a35fbef57 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -233,7 +233,7 @@ final class TimelineProxy: TimelineProxyProtocol { MXLog.info("Sending audio") do { - let handle = try timeline.sendAudio(params: .init(filename: url.path(percentEncoded: false), + let handle = try timeline.sendAudio(params: .init(source: .file(filename: url.path(percentEncoded: false)), caption: caption, formattedCaption: nil, // Rust will build this from the caption's markdown. mentions: nil, @@ -261,7 +261,7 @@ final class TimelineProxy: TimelineProxyProtocol { MXLog.info("Sending file") do { - let handle = try timeline.sendFile(params: .init(filename: url.path(percentEncoded: false), + let handle = try timeline.sendFile(params: .init(source: .file(filename: url.path(percentEncoded: false)), caption: caption, formattedCaption: nil, // Rust will build this from the caption's markdown. mentions: nil, @@ -290,7 +290,7 @@ final class TimelineProxy: TimelineProxyProtocol { MXLog.info("Sending image") do { - let handle = try timeline.sendImage(params: .init(filename: url.path(percentEncoded: false), + let handle = try timeline.sendImage(params: .init(source: .file(filename: url.path(percentEncoded: false)), caption: caption, formattedCaption: nil, // Rust will build this from the caption's markdown. mentions: nil, @@ -338,7 +338,7 @@ final class TimelineProxy: TimelineProxyProtocol { MXLog.info("Sending video") do { - let handle = try timeline.sendVideo(params: .init(filename: url.path(percentEncoded: false), + let handle = try timeline.sendVideo(params: .init(source: .file(filename: url.path(percentEncoded: false)), caption: caption, formattedCaption: nil, mentions: nil, @@ -367,7 +367,7 @@ final class TimelineProxy: TimelineProxyProtocol { MXLog.info("Sending voice message") do { - let handle = try timeline.sendVoiceMessage(params: .init(filename: url.path(percentEncoded: false), + let handle = try timeline.sendVoiceMessage(params: .init(source: .file(filename: url.path(percentEncoded: false)), caption: nil, formattedCaption: nil, mentions: nil, diff --git a/UnitTests/Sources/HomeScreenViewModelTests.swift b/UnitTests/Sources/HomeScreenViewModelTests.swift index 6513c4eb8..1565e0f66 100644 --- a/UnitTests/Sources/HomeScreenViewModelTests.swift +++ b/UnitTests/Sources/HomeScreenViewModelTests.swift @@ -304,7 +304,6 @@ class HomeScreenViewModelTests: XCTestCase { func testDeclineInvite() async throws { setupViewModel(withInvites: true) - let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID) appSettings.seenInvites = Set(invitedRoomIDs) XCTAssertEqual(invitedRoomIDs.count, 2) @@ -323,7 +322,7 @@ class HomeScreenViewModelTests: XCTestCase { return .invited(roomProxy) } - context.viewState.bindings.alertInfo?.secondaryButton?.action?() + context.viewState.bindings.alertInfo?.verticalButtons?[0].action?() await fulfillment(of: [rejectExpectation], timeout: 1.0) XCTAssertEqual(appSettings.seenInvites, [invitedRoomIDs[1]]) @@ -331,6 +330,21 @@ class HomeScreenViewModelTests: XCTestCase { XCTAssertEqual(notificationManager.removeDeliveredMessageNotificationsForReceivedInvocations, [invitedRoomIDs[0]]) } + func testDeclineAndBlockInvite() async throws { + setupViewModel(withInvites: true) + let invitedRoomIDs = context.viewState.rooms.invites.compactMap(\.roomID) + appSettings.seenInvites = Set(invitedRoomIDs) + XCTAssertEqual(invitedRoomIDs.count, 2) + + let deferred = deferFulfillment(context.$viewState) { $0.bindings.alertInfo != nil } + context.send(viewAction: .declineInvite(roomIdentifier: invitedRoomIDs[0])) + try await deferred.fulfill() + + let deferredAction = deferFulfillment(viewModel.actions) { $0 == .presentDeclineAndBlock(userID: RoomMemberProxyMock.mockCharlie.userID, roomID: invitedRoomIDs[0]) } + context.viewState.bindings.alertInfo?.secondaryButton?.action?() + try await deferredAction.fulfill() + } + // MARK: - Helpers private func setupViewModel(securityStatePublisher: CurrentValuePublisher? = nil, withInvites: Bool = false) { diff --git a/UnitTests/Sources/JoinRoomScreenViewModelTests.swift b/UnitTests/Sources/JoinRoomScreenViewModelTests.swift index 2abfab8ee..95b4de388 100644 --- a/UnitTests/Sources/JoinRoomScreenViewModelTests.swift +++ b/UnitTests/Sources/JoinRoomScreenViewModelTests.swift @@ -111,8 +111,9 @@ class JoinRoomScreenViewModelTests: XCTestCase { try await deferred.fulfill() } - func testDeclineAndBlockInviteInteraction() async throws { + func testDeclineAndBlockInviteLegacyInteraction() async throws { setupViewModel(mode: .invited) + clientProxy.underlyingIsReportRoomSupported = false let expectation = expectation(description: "Wait for the user to be ignored") clientProxy.ignoreUserClosure = { userID in defer { expectation.fulfill() } @@ -124,15 +125,23 @@ class JoinRoomScreenViewModelTests: XCTestCase { context.send(viewAction: .declineInviteAndBlock(userID: "@test:matrix.org")) + try await deferFulfillment(viewModel.context.$viewState) { $0.bindings.alertInfo != nil }.fulfill() XCTAssertEqual(viewModel.context.alertInfo?.id, .declineInviteAndBlock) let deferred = deferFulfillment(viewModel.actionsPublisher) { action in action == .dismiss } context.alertInfo?.secondaryButton?.action?() - try await deferred.fulfill() - await fulfillment(of: [expectation], timeout: 10) + try await deferred.fulfill() + } + + func testDeclineAndBlockInviteInteraction() async throws { + setupViewModel(mode: .invited) + try await deferFulfillment(viewModel.context.$viewState) { $0.roomDetails != nil }.fulfill() + let deferredAction = deferFulfillment(viewModel.actionsPublisher) { $0 == .presentDeclineAndBlock(userID: "@test:matrix.org") } + context.send(viewAction: .declineInviteAndBlock(userID: "@test:matrix.org")) + try await deferredAction.fulfill() } func testForgetRoom() async throws { diff --git a/project.yml b/project.yml index 563d7e319..4b9512010 100644 --- a/project.yml +++ b/project.yml @@ -59,7 +59,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 25.04.16 + exactVersion: 25.04.29 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios