From 192974ff3a2ed42c4d2da2e1eb60546e56c8c82b Mon Sep 17 00:00:00 2001 From: Doug <6060466+pixlwave@users.noreply.github.com> Date: Mon, 3 Apr 2023 10:21:24 +0100 Subject: [PATCH] Add Block user to Report Content screen. (#742) --- .../de.lproj/Localizable.strings | 4 +- .../en.lproj/Localizable.strings | 4 +- .../fr.lproj/Localizable.strings | 4 +- ElementX/Sources/Generated/Strings.swift | 6 +- .../Mocks/Generated/GeneratedMocks.swift | 21 ++++++ .../Other/AccessibilityIdentifiers.swift | 5 ++ .../Other/SwiftUI/Views/FormTextEditor.swift | 64 ------------------- .../ReportContentCoordinator.swift | 3 +- .../ReportContent/ReportContentModels.swift | 1 + .../ReportContentViewModel.swift | 25 ++++++-- .../View/ReportContentScreen.swift | 54 ++++++++++------ .../RoomScreen/RoomScreenCoordinator.swift | 15 +++-- .../Screens/RoomScreen/RoomScreenModels.swift | 4 +- .../RoomScreen/RoomScreenViewModel.swift | 6 +- .../Sources/Services/Room/RoomProxy.swift | 16 +++++ .../Services/Room/RoomProxyProtocol.swift | 6 +- .../UITests/UITestsAppCoordinator.swift | 4 +- .../Sources/ReportContentScreenUITests.swift | 17 ++++- ...GB-iPad-9th-generation.reportContent-0.png | 3 + ...GB-iPad-9th-generation.reportContent-1.png | 3 + ...n-GB-iPad-9th-generation.reportContent.png | 3 - .../en-GB-iPhone-14.reportContent-0.png | 3 + .../en-GB-iPhone-14.reportContent-1.png | 3 + .../en-GB-iPhone-14.reportContent.png | 3 - ...do-iPad-9th-generation.reportContent-0.png | 3 + ...do-iPad-9th-generation.reportContent-1.png | 3 + ...eudo-iPad-9th-generation.reportContent.png | 3 - .../pseudo-iPhone-14.reportContent-0.png | 3 + .../pseudo-iPhone-14.reportContent-1.png | 3 + .../pseudo-iPhone-14.reportContent.png | 3 - .../MediaProvider/MediaLoaderTests.swift | 2 + .../Sources/ReportContentViewModelTests.swift | 52 +++++++++++++-- changelog.d/115.change | 1 + 33 files changed, 220 insertions(+), 130 deletions(-) delete mode 100644 ElementX/Sources/Other/SwiftUI/Views/FormTextEditor.swift create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-0.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-1.png delete mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-0.png create mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-1.png delete mode 100644 UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-0.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-1.png delete mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-0.png create mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-1.png delete mode 100644 UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent.png create mode 100644 changelog.d/115.change diff --git a/ElementX/Resources/Localizations/de.lproj/Localizable.strings b/ElementX/Resources/Localizations/de.lproj/Localizable.strings index f028b0342..d2f709aee 100644 --- a/ElementX/Resources/Localizations/de.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/de.lproj/Localizable.strings @@ -114,7 +114,7 @@ "preference_rageshake" = "Rageshake to report bug"; "rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"; "rageshake_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"; -"report_content_explanation" = "Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images."; +"report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."; "report_content_hint" = "Reason for reporting this content"; "rich_text_editor_bullet_list" = "Toggle bullet list"; "rich_text_editor_code_block" = "Toggle code block"; @@ -166,6 +166,8 @@ "screen_login_username_hint" = "Username"; "screen_onboarding_welcome_subtitle" = "Welcome to the %1$@ Beta. Supercharged, for speed and simplicity."; "screen_onboarding_welcome_title" = "Be in your Element"; +"screen_report_content_block_user" = "Block user"; +"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; "screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."; "screen_room_details_encryption_enabled_title" = "Message encryption enabled"; "screen_room_details_invite_people_title" = "Invite people"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index b30ceff83..5218dd8ac 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -114,7 +114,7 @@ "preference_rageshake" = "Rageshake to report bug"; "rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"; "rageshake_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"; -"report_content_explanation" = "Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images."; +"report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."; "report_content_hint" = "Reason for reporting this content"; "rich_text_editor_bullet_list" = "Toggle bullet list"; "rich_text_editor_code_block" = "Toggle code block"; @@ -166,6 +166,8 @@ "screen_login_username_hint" = "Username"; "screen_onboarding_welcome_subtitle" = "Welcome to the %1$@ Beta. Supercharged, for speed and simplicity."; "screen_onboarding_welcome_title" = "Be in your Element"; +"screen_report_content_block_user" = "Block user"; +"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; "screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."; "screen_room_details_encryption_enabled_title" = "Message encryption enabled"; "screen_room_details_invite_people_title" = "Invite people"; diff --git a/ElementX/Resources/Localizations/fr.lproj/Localizable.strings b/ElementX/Resources/Localizations/fr.lproj/Localizable.strings index 618a3808a..b02c5160b 100644 --- a/ElementX/Resources/Localizations/fr.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/fr.lproj/Localizable.strings @@ -114,7 +114,7 @@ "preference_rageshake" = "Rageshake to report bug"; "rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"; "rageshake_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"; -"report_content_explanation" = "Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images."; +"report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages."; "report_content_hint" = "Reason for reporting this content"; "rich_text_editor_bullet_list" = "Toggle bullet list"; "rich_text_editor_code_block" = "Toggle code block"; @@ -166,6 +166,8 @@ "screen_login_username_hint" = "Username"; "screen_onboarding_welcome_subtitle" = "Welcome to the %1$@ Beta. Supercharged, for speed and simplicity."; "screen_onboarding_welcome_title" = "Be in your Element"; +"screen_report_content_block_user" = "Block user"; +"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user"; "screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."; "screen_room_details_encryption_enabled_title" = "Message encryption enabled"; "screen_room_details_invite_people_title" = "Invite people"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index fb51b0494..5822e8f8a 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -256,7 +256,7 @@ public enum L10n { public static var rageshakeDetectionDialogContent: String { return L10n.tr("Localizable", "rageshake_detection_dialog_content") } /// You seem to be shaking the phone in frustration. Would you like to open the bug report screen? public static var rageshakeDialogContent: String { return L10n.tr("Localizable", "rageshake_dialog_content") } - /// Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images. + /// This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages. public static var reportContentExplanation: String { return L10n.tr("Localizable", "report_content_explanation") } /// Reason for reporting this content public static var reportContentHint: String { return L10n.tr("Localizable", "report_content_hint") } @@ -372,6 +372,10 @@ public enum L10n { } /// Be in your Element public static var screenOnboardingWelcomeTitle: String { return L10n.tr("Localizable", "screen_onboarding_welcome_title") } + /// Block user + public static var screenReportContentBlockUser: String { return L10n.tr("Localizable", "screen_report_content_block_user") } + /// Check if you want to hide all current and future messages from this user + public static var screenReportContentBlockUserHint: String { return L10n.tr("Localizable", "screen_report_content_block_user_hint") } /// Messages are secured with locks. Only you and the recipients have the unique keys to unlock them. public static var screenRoomDetailsEncryptionEnabledSubtitle: String { return L10n.tr("Localizable", "screen_room_details_encryption_enabled_subtitle") } /// Message encryption enabled diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 873ca684d..c42453dfc 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -427,6 +427,27 @@ class RoomProxyMock: RoomProxyProtocol { return membersReturnValue } } + //MARK: - ignoreUser + + var ignoreUserCallsCount = 0 + var ignoreUserCalled: Bool { + return ignoreUserCallsCount > 0 + } + var ignoreUserReceivedUserID: String? + var ignoreUserReceivedInvocations: [String] = [] + var ignoreUserReturnValue: Result! + var ignoreUserClosure: ((String) async -> Result)? + + func ignoreUser(_ userID: String) async -> Result { + ignoreUserCallsCount += 1 + ignoreUserReceivedUserID = userID + ignoreUserReceivedInvocations.append(userID) + if let ignoreUserClosure = ignoreUserClosure { + return await ignoreUserClosure(userID) + } else { + return ignoreUserReturnValue + } + } //MARK: - retryDecryption var retryDecryptionForCallsCount = 0 diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index c8d5b0b9f..d7c298609 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -22,6 +22,7 @@ struct A11yIdentifiers { static let homeScreen = HomeScreen() static let loginScreen = LoginScreen() static let onboardingScreen = OnboardingScreen() + static let reportContent = ReportContent() static let roomScreen = RoomScreen() static let roomDetailsScreen = RoomDetailsScreen() static let sessionVerificationScreen = SessionVerificationScreen() @@ -66,6 +67,10 @@ struct A11yIdentifiers { let signIn = "onboarding-sign_in" let hidden = "onboarding-hidden" } + + struct ReportContent { + let ignoreUser = "report_content-ignore_user" + } struct RoomScreen { let name = "room-name" diff --git a/ElementX/Sources/Other/SwiftUI/Views/FormTextEditor.swift b/ElementX/Sources/Other/SwiftUI/Views/FormTextEditor.swift deleted file mode 100644 index e59670431..000000000 --- a/ElementX/Sources/Other/SwiftUI/Views/FormTextEditor.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// Copyright 2023 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -struct FormTextEditor: View { - @Binding var text: String - let placeholder: String - var editorAccessibilityIdentifier: String? - - var body: some View { - ZStack(alignment: .topLeading) { - RoundedRectangle(cornerRadius: 14, style: .continuous) - .fill(Color.element.formRowBackground) - - let textEditor = TextEditor(text: $text) - .tint(.element.brand) - .padding(.horizontal, 10) - .padding(.vertical, 4) - .cornerRadius(14) - .scrollContentBackground(.hidden) - if let editorAccessibilityIdentifier { - textEditor - .accessibilityIdentifier(editorAccessibilityIdentifier) - } else { - textEditor - } - - if text.isEmpty { - Text(placeholder) - .font(.element.body) - .foregroundColor(Color.element.secondaryContent) - .padding(.horizontal, 16) - .padding(.vertical, 12) - .allowsHitTesting(false) - } - - RoundedRectangle(cornerRadius: 14, style: .continuous) - .stroke(Color.element.quaternaryContent) - } - .frame(maxWidth: .infinity) - .frame(height: 220) - .font(.body) - } -} - -struct FormTextEditor_Previews: PreviewProvider { - static var previews: some View { - FormTextEditor(text: .constant(""), placeholder: "test", editorAccessibilityIdentifier: nil) - } -} diff --git a/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift b/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift index 10eef0b99..70f389bb0 100644 --- a/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift +++ b/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift @@ -18,6 +18,7 @@ import SwiftUI struct ReportContentCoordinatorParameters { let itemID: String + let senderID: String let roomProxy: RoomProxyProtocol weak var userIndicatorController: UserIndicatorControllerProtocol? } @@ -36,7 +37,7 @@ final class ReportContentCoordinator: CoordinatorProtocol { init(parameters: ReportContentCoordinatorParameters) { self.parameters = parameters - viewModel = ReportContentViewModel(itemID: parameters.itemID, roomProxy: parameters.roomProxy) + viewModel = ReportContentViewModel(itemID: parameters.itemID, senderID: parameters.senderID, roomProxy: parameters.roomProxy) } // MARK: - Public diff --git a/ElementX/Sources/Screens/ReportContent/ReportContentModels.swift b/ElementX/Sources/Screens/ReportContent/ReportContentModels.swift index 57da3657c..44c04c188 100644 --- a/ElementX/Sources/Screens/ReportContent/ReportContentModels.swift +++ b/ElementX/Sources/Screens/ReportContent/ReportContentModels.swift @@ -29,6 +29,7 @@ struct ReportContentViewState: BindableState { struct ReportContentViewStateBindings { var reasonText: String + var ignoreUser: Bool } enum ReportContentViewAction { diff --git a/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift b/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift index 6af981915..d039c06f4 100644 --- a/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift +++ b/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift @@ -22,12 +22,15 @@ class ReportContentViewModel: ReportContentViewModelType, ReportContentViewModel var callback: ((ReportContentViewModelAction) -> Void)? private let itemID: String + private let senderID: String private let roomProxy: RoomProxyProtocol - init(itemID: String, roomProxy: RoomProxyProtocol) { + init(itemID: String, senderID: String, roomProxy: RoomProxyProtocol) { self.itemID = itemID + self.senderID = senderID self.roomProxy = roomProxy - super.init(initialViewState: ReportContentViewState(bindings: ReportContentViewStateBindings(reasonText: ""))) + + super.init(initialViewState: ReportContentViewState(bindings: ReportContentViewStateBindings(reasonText: "", ignoreUser: false))) } // MARK: - Public @@ -45,13 +48,21 @@ class ReportContentViewModel: ReportContentViewModelType, ReportContentViewModel private func submitReport() async { callback?(.submitStarted) - switch await roomProxy.reportContent(itemID, reason: state.bindings.reasonText) { - case .success: - MXLog.info("Submit Report Content succeeded") - callback?(.submitFinished) - case let .failure(error): + + if case let .failure(error) = await roomProxy.reportContent(itemID, reason: state.bindings.reasonText) { MXLog.error("Submit Report Content failed: \(error)") callback?(.submitFailed(error: error)) + return } + + // Ignore the sender if the user wants to. + if state.bindings.ignoreUser, case let .failure(error) = await roomProxy.ignoreUser(senderID) { + MXLog.error("Ignore user failed: \(error)") + callback?(.submitFailed(error: error)) + return + } + + MXLog.info("Submit Report Content succeeded") + callback?(.submitFinished) } } diff --git a/ElementX/Sources/Screens/ReportContent/View/ReportContentScreen.swift b/ElementX/Sources/Screens/ReportContent/View/ReportContentScreen.swift index 506b77631..8e1eaf750 100644 --- a/ElementX/Sources/Screens/ReportContent/View/ReportContentScreen.swift +++ b/ElementX/Sources/Screens/ReportContent/View/ReportContentScreen.swift @@ -26,35 +26,43 @@ struct ReportContentScreen: View { } var body: some View { - ScrollView { - mainContent - .padding(.top, 50) - .padding(.horizontal, horizontalPadding) + Form { + reasonSection + + ignoreUserSection } .scrollDismissesKeyboard(.immediately) - .background(Color.element.formBackground.ignoresSafeArea()) + .compoundForm() .navigationTitle(L10n.actionReportContent) .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } .interactiveDismissDisabled() } - /// The main content of the view to be shown in a scroll view. - var mainContent: some View { - VStack(alignment: .leading, spacing: 24) { - infoText - reasonTextEditor + private var reasonSection: some View { + Section { + TextField(L10n.reportContentHint, + text: $context.reasonText, + prompt: Text(L10n.reportContentHint).compoundFormTextFieldPlaceholder(), + axis: .vertical) + .lineLimit(4, reservesSpace: true) + .textFieldStyle(.compoundForm) + } footer: { + Text(L10n.reportContentExplanation) + .compoundFormSectionFooter() } + .compoundFormSection() } - - private var infoText: some View { - Text(L10n.reportContentExplanation) - .font(.element.body) - .foregroundColor(Color.element.primaryContent) - } - - private var reasonTextEditor: some View { - FormTextEditor(text: $context.reasonText, placeholder: L10n.reportContentHint) + + private var ignoreUserSection: some View { + Section { + Toggle(L10n.screenReportContentBlockUser, isOn: $context.ignoreUser) + .toggleStyle(.compoundForm) + .accessibilityIdentifier(A11yIdentifiers.reportContent.ignoreUser) + } footer: { + Text(L10n.screenReportContentBlockUserHint) + .compoundFormSectionFooter() + } } @ToolbarContentBuilder @@ -76,9 +84,13 @@ struct ReportContentScreen: View { // MARK: - Previews struct ReportContent_Previews: PreviewProvider { - static let viewModel = ReportContentViewModel(itemID: "", roomProxy: RoomProxyMock(with: .init(displayName: nil))) + static let viewModel = ReportContentViewModel(itemID: "", + senderID: "", + roomProxy: RoomProxyMock(with: .init(displayName: nil))) static var previews: some View { - ReportContentScreen(context: viewModel.context) + NavigationStack { + ReportContentScreen(context: viewModel.context) + } } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index efc405c7d..7bdd86c3c 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -58,10 +58,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol { self.displayRoomDetails() case .displayMediaFile(let file, let title): self.displayFilePreview(for: file, with: title) - case .displayEmojiPicker(let itemId): - self.displayEmojiPickerScreen(for: itemId) - case .displayReportContent(let itemId): - self.displayReportContent(for: itemId) + case .displayEmojiPicker(let itemID): + self.displayEmojiPickerScreen(for: itemID) + case .displayReportContent(let itemID, let senderID): + self.displayReportContent(for: itemID, from: senderID) case .displayCameraPicker: self.displayMediaPickerWithSource(.camera) case .displayMediaPicker: @@ -159,10 +159,11 @@ final class RoomScreenCoordinator: CoordinatorProtocol { navigationStackCoordinator.push(coordinator) } - private func displayReportContent(for itemId: String) { + private func displayReportContent(for itemID: String, from senderID: String) { let navigationCoordinator = NavigationStackCoordinator() - let userIndicatorController = UserIndicatorController(rootCoordinator: NavigationStackCoordinator()) - let parameters = ReportContentCoordinatorParameters(itemID: itemId, + let userIndicatorController = UserIndicatorController(rootCoordinator: navigationCoordinator) + let parameters = ReportContentCoordinatorParameters(itemID: itemID, + senderID: senderID, roomProxy: parameters.roomProxy, userIndicatorController: userIndicatorController) let coordinator = ReportContentCoordinator(parameters: parameters) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 4639fc2af..6da653920 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -21,8 +21,8 @@ import UIKit enum RoomScreenViewModelAction { case displayRoomDetails case displayMediaFile(file: MediaFileHandleProxy, title: String?) - case displayEmojiPicker(itemId: String) - case displayReportContent(itemId: String) + case displayEmojiPicker(itemID: String) + case displayReportContent(itemID: String, senderID: String) case displayCameraPicker case displayMediaPicker case displayDocumentPicker diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index e5aeb9888..ed621e354 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -155,7 +155,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol private func itemDoubleTapped(with itemId: String) { guard let item = state.items.first(where: { $0.id == itemId }), item.isReactable else { return } - callback?(.displayEmojiPicker(itemId: itemId)) + callback?(.displayEmojiPicker(itemID: itemId)) } private func buildTimelineViews() { @@ -294,7 +294,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol switch action { case .react: - callback?(.displayEmojiPicker(itemId: item.id)) + callback?(.displayEmojiPicker(itemID: item.id)) case .copy: UIPasteboard.general.string = item.body case .edit: @@ -327,7 +327,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol await timelineController.retryDecryption(for: sessionID) } case .report: - callback?(.displayReportContent(itemId: itemID)) + callback?(.displayReportContent(itemID: itemID, senderID: item.sender.id)) } if action.switchToDefaultComposer { diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index a6b071982..3ba0677a5 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -288,6 +288,22 @@ class RoomProxy: RoomProxyProtocol { return .failure(.failedRetrievingMembers) } } + + func ignoreUser(_ userID: String) async -> Result { + sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + return await Task.dispatch(on: userInitiatedDispatchQueue) { + do { + try self.room.ignoreUser(userId: userID) + return .success(()) + } catch { + return .failure(.failedReportingContent) + } + } + } @MainActor private func buildRoomMemberProxies(members: [RoomMember]) -> [RoomMemberProxy] { diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 25d0d52df..2d037df67 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -76,10 +76,12 @@ protocol RoomProxyProtocol { func editMessage(_ newMessage: String, original eventID: String) async -> Result func redact(_ eventID: String) async -> Result - + func reportContent(_ eventID: String, reason: String?) async -> Result - + func members() async -> Result<[RoomMemberProxyProtocol], RoomProxyError> + + func ignoreUser(_ userID: String) async -> Result func retryDecryption(for sessionID: String) async diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index 612f7d34f..7ed214ee0 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -305,7 +305,9 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .reportContent: let navigationStackCoordinator = NavigationStackCoordinator() - let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test", roomProxy: RoomProxyMock(with: .init(displayName: "test")))) + let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test", + senderID: RoomMemberProxyMock.mockAlice.userID, + roomProxy: RoomProxyMock(with: .init(displayName: "test")))) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .startChat: diff --git a/UITests/Sources/ReportContentScreenUITests.swift b/UITests/Sources/ReportContentScreenUITests.swift index ffe073424..8a89e210a 100644 --- a/UITests/Sources/ReportContentScreenUITests.swift +++ b/UITests/Sources/ReportContentScreenUITests.swift @@ -20,6 +20,21 @@ import XCTest class ReportContentScreenUITests: XCTestCase { func testInitialStateComponents() { let app = Application.launch(.reportContent) - app.assertScreenshot(.reportContent) + app.assertScreenshot(.reportContent, step: 0) + } + + func testToggleIgnoreUser() { + let app = Application.launch(.reportContent) + + // Don't know why, but there's an issue on CI where the toggle is tapped but doesn't respond. Waiting for + // it fixes this (even it it already exists). Reproducible by running the test after quitting the simulator. + let sendingLogsToggle = app.switches[A11yIdentifiers.reportContent.ignoreUser] + XCTAssertTrue(sendingLogsToggle.waitForExistence(timeout: 1)) + XCTAssertFalse(sendingLogsToggle.isOn) + + sendingLogsToggle.tap() + + XCTAssertTrue(sendingLogsToggle.isOn) + app.assertScreenshot(.reportContent, step: 1) } } diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-0.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-0.png new file mode 100644 index 000000000..87eeae64c --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5e16b7b56132cb5c7c30d502911dcb29b41efe3228c10053576fe4b34e5178e +size 94846 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-1.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-1.png new file mode 100644 index 000000000..ecb2d81f9 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06d95ccd192c2c9550e4a405b76352d8f75d86a1d72cbc42dfab15743bcac2de +size 96766 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent.png deleted file mode 100644 index 4ba9b0c38..000000000 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:71b413b0ec66b086cbcb077defbf5890332f56d2c865fb0ddc2ec251224a7000 -size 102265 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-0.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-0.png new file mode 100644 index 000000000..d843e7dc6 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a330b8acd8f2a4ebdda45d125c29c17e21f6cd8fb2397443d7527988ff5a943 +size 117146 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-1.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-1.png new file mode 100644 index 000000000..adeb41a6c --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63627788babeceb6940989acf8e07c7244d50b4ab84c97bc74b4e19b9ee6ce47 +size 120785 diff --git a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent.png deleted file mode 100644 index 3f085df41..000000000 --- a/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c816fa74f027b401c84513b2c4d8f912fb91e3229e4b4fa1cdda17db2d88be4 -size 131307 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-0.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-0.png new file mode 100644 index 000000000..00d349948 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c253c4c823104c55bf916c9d112ad87e4de3db24fd324a61350a834584edf275 +size 116667 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-1.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-1.png new file mode 100644 index 000000000..3f5b5df37 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:324756300a6ca0abca19892b5ff4fea9bcd1718fb9375ed87de8e9e981a06908 +size 118557 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent.png deleted file mode 100644 index 476008a5a..000000000 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPad-9th-generation.reportContent.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a5f69a58df0557ef1e1adb04a1dbe9b245a003684f0b96228a7a438da168ec25 -size 133827 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-0.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-0.png new file mode 100644 index 000000000..a36bcac9b --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4781fa334f9c08adf7ba7e3b0885e9b04a7f28c7e1e407553a74171570775a41 +size 162791 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-1.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-1.png new file mode 100644 index 000000000..77848b368 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4120dca6e5eb55094f9e301bb3fa6e538b6f9a1226f29a2ab4832886661b429 +size 163825 diff --git a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent.png b/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent.png deleted file mode 100644 index dd1582e3c..000000000 --- a/UITests/Sources/__Snapshots__/Application/pseudo-iPhone-14.reportContent.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c47eb8afbde353f625bb0d70712a62df83e769284a4c1a7d0918f27369414d65 -size 193082 diff --git a/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift b/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift index 1d088cd68..ffa8a1153 100644 --- a/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift +++ b/UnitTests/Sources/MediaProvider/MediaLoaderTests.swift @@ -97,6 +97,8 @@ private class MockMediaLoadingClient: ClientProtocol { func getMediaFile(source: MatrixRustSDK.MediaSource, mimeType: String) throws -> MatrixRustSDK.MediaFileHandle { fatalError() } + func getProfile(userId: String) throws -> MatrixRustSDK.UserProfile { fatalError() } + func getSessionVerificationController() throws -> MatrixRustSDK.SessionVerificationController { fatalError() } func fullSlidingSync() throws -> MatrixRustSDK.SlidingSync { fatalError() } diff --git a/UnitTests/Sources/ReportContentViewModelTests.swift b/UnitTests/Sources/ReportContentViewModelTests.swift index 1c57e8031..9aa0c8d23 100644 --- a/UnitTests/Sources/ReportContentViewModelTests.swift +++ b/UnitTests/Sources/ReportContentViewModelTests.swift @@ -20,10 +20,52 @@ import XCTest @MainActor class ReportContentScreenViewModelTests: XCTestCase { - func testInitialState() { - let viewModel = ReportContentViewModel(itemID: "test-id", roomProxy: RoomProxyMock(with: .init(displayName: "test"))) - let context = viewModel.context - - XCTAssertEqual(context.reasonText, "") + let itemID = "test-id" + let senderID = "@meany:server.com" + let reportReason = "I don't like it." + + func testReportContent() async { + // Given the report content view for some content. + let roomProxy = RoomProxyMock(with: .init(displayName: "test")) + roomProxy.reportContentReasonReturnValue = .success(()) + let viewModel = ReportContentViewModel(itemID: itemID, + senderID: senderID, + roomProxy: roomProxy) + + // When reporting the content without ignoring the user. + viewModel.state.bindings.reasonText = reportReason + viewModel.state.bindings.ignoreUser = false + viewModel.context.send(viewAction: .submit) + await Task.yield() + + // Then the content should be reported, but the user should not be included. + XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") + XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, itemID, "The event ID should match the content being reported.") + XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.reason, reportReason, "The reason should match the user input.") + XCTAssertEqual(roomProxy.ignoreUserCallsCount, 0, "A call to ignore a user should not have been made.") + XCTAssertNil(roomProxy.ignoreUserReceivedUserID, "The sender shouldn't have been ignored.") + } + + func testReportIgnoringSender() async { + // Given the report content view for some content. + let roomProxy = RoomProxyMock(with: .init(displayName: "test")) + roomProxy.reportContentReasonReturnValue = .success(()) + roomProxy.ignoreUserReturnValue = .success(()) + let viewModel = ReportContentViewModel(itemID: itemID, + senderID: senderID, + roomProxy: roomProxy) + + // When reporting the content and also ignoring the user. + viewModel.state.bindings.reasonText = reportReason + viewModel.state.bindings.ignoreUser = true + viewModel.context.send(viewAction: .submit) + await Task.yield() + + // Then the content should be reported, and the user should be ignored. + XCTAssertEqual(roomProxy.reportContentReasonCallsCount, 1, "The content should always be reported.") + XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.eventID, itemID, "The event ID should match the content being reported.") + XCTAssertEqual(roomProxy.reportContentReasonReceivedArguments?.reason, reportReason, "The reason should match the user input.") + XCTAssertEqual(roomProxy.ignoreUserCallsCount, 1, "A call should have been made to ignore the sender.") + XCTAssertEqual(roomProxy.ignoreUserReceivedUserID, senderID, "The ignored user ID should match the sender.") } } diff --git a/changelog.d/115.change b/changelog.d/115.change new file mode 100644 index 000000000..d77ba2296 --- /dev/null +++ b/changelog.d/115.change @@ -0,0 +1 @@ +Add Block user toggle to Report Content screen. \ No newline at end of file