diff --git a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings index 1531907fd..a861c61f3 100644 --- a/ElementX/Resources/Localizations/en.lproj/Untranslated.strings +++ b/ElementX/Resources/Localizations/en.lproj/Untranslated.strings @@ -8,6 +8,8 @@ "message" = "Message"; +"sending" = "Sending..."; + "screenshot_detected_title" = "You took a screenshot"; "screenshot_detected_message" = "Would you like to submit a bug report?"; @@ -86,7 +88,10 @@ "bug_report_screen_logs_description" = "To check things work as intended, logs will be sent with your message. These will be private. To just send your message, turn off this setting."; "bug_report_screen_attach_screenshot" = "Attach Screenshot"; "bug_report_screen_edit_screenshot" = "Edit Screenshot"; -"bug_report_screen_sending" = "Sending..."; + +// Report Content +"report_content_info" = "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_submitted" = "Report Submitted"; // Session verification /* diff --git a/ElementX/Sources/Generated/Strings+Untranslated.swift b/ElementX/Sources/Generated/Strings+Untranslated.swift index a321b8d92..2b4176ea7 100644 --- a/ElementX/Sources/Generated/Strings+Untranslated.swift +++ b/ElementX/Sources/Generated/Strings+Untranslated.swift @@ -26,8 +26,6 @@ extension ElementL10n { public static let bugReportScreenIncludeLogs = ElementL10n.tr("Untranslated", "bug_report_screen_include_logs") /// To check things work as intended, logs will be sent with your message. These will be private. To just send your message, turn off this setting. public static let bugReportScreenLogsDescription = ElementL10n.tr("Untranslated", "bug_report_screen_logs_description") - /// Sending... - public static let bugReportScreenSending = ElementL10n.tr("Untranslated", "bug_report_screen_sending") /// Report a bug public static let bugReportScreenTitle = ElementL10n.tr("Untranslated", "bug_report_screen_title") /// %@ iOS @@ -96,6 +94,10 @@ extension ElementL10n { } /// Notification public static let notification = ElementL10n.tr("Untranslated", "Notification") + /// 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. + public static let reportContentInfo = ElementL10n.tr("Untranslated", "report_content_info") + /// Report Submitted + public static let reportContentSubmitted = ElementL10n.tr("Untranslated", "report_content_submitted") /// About public static let roomDetailsAboutSectionTitle = ElementL10n.tr("Untranslated", "room_details_about_section_title") /// Copy Link @@ -136,6 +138,8 @@ extension ElementL10n { public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message") /// You took a screenshot public static let screenshotDetectedTitle = ElementL10n.tr("Untranslated", "screenshot_detected_title") + /// Sending... + public static let sending = ElementL10n.tr("Untranslated", "sending") /// You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %@ public static func serverSelectionServerFooter(_ p1: Any) -> String { return ElementL10n.tr("Untranslated", "server_selection_server_footer", String(describing: p1)) diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index dd03369e5..3faf0461f 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -26,13 +26,12 @@ struct A11yIdentifiers { static let roomDetailsScreen = RoomDetailsScreen() static let sessionVerificationScreen = SessionVerificationScreen() static let softLogoutScreen = SoftLogoutScreen() - + struct BugReportScreen { let report = "bug_report-report" let sendLogs = "bug_report-send_logs" let screenshot = "bug_report-screenshot" let removeScreenshot = "bug_report-remove_screenshot" - let send = "bug_report-send" let attachScreenshot = "bug-report-attach_screenshot" } diff --git a/ElementX/Sources/Other/SwiftUI/Views/FormTextEditor.swift b/ElementX/Sources/Other/SwiftUI/Views/FormTextEditor.swift new file mode 100644 index 000000000..e59670431 --- /dev/null +++ b/ElementX/Sources/Other/SwiftUI/Views/FormTextEditor.swift @@ -0,0 +1,64 @@ +// +// 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/BugReport/BugReportCoordinator.swift b/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift index 1bcd9c22c..371399593 100644 --- a/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift +++ b/ElementX/Sources/Screens/BugReport/BugReportCoordinator.swift @@ -57,7 +57,7 @@ final class BugReportCoordinator: CoordinatorProtocol { case .cancel: self.completion?(.cancel) case let .submitStarted(progressTracker): - self.startLoading(label: ElementL10n.bugReportScreenSending, progressPublisher: progressTracker) + self.startLoading(label: ElementL10n.sending, progressPublisher: progressTracker) case .submitFinished: self.stopLoading() self.completion?(.finish) diff --git a/ElementX/Sources/Screens/BugReport/View/BugReportScreen.swift b/ElementX/Sources/Screens/BugReport/View/BugReportScreen.swift index 4e749af8a..67fd13f9e 100644 --- a/ElementX/Sources/Screens/BugReport/View/BugReportScreen.swift +++ b/ElementX/Sources/Screens/BugReport/View/BugReportScreen.swift @@ -61,35 +61,10 @@ struct BugReportScreen: View { } } - @ViewBuilder private var descriptionTextEditor: some View { - ZStack(alignment: .topLeading) { - RoundedRectangle(cornerRadius: 14, style: .continuous) - .fill(Color.element.formRowBackground) - - TextEditor(text: $context.reportText) - .tint(.element.brand) - .padding(.horizontal, 10) - .padding(.vertical, 4) - .cornerRadius(14) - .accessibilityIdentifier(A11yIdentifiers.bugReportScreen.report) - .scrollContentBackground(.hidden) - - if context.reportText.isEmpty { - Text(ElementL10n.bugReportScreenDescription) - .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) + FormTextEditor(text: $context.reportText, + placeholder: ElementL10n.bugReportScreenDescription, + editorAccessibilityIdentifier: A11yIdentifiers.bugReportScreen.report) } @ViewBuilder @@ -164,7 +139,6 @@ struct BugReportScreen: View { context.send(viewAction: .submit) } .disabled(context.reportText.count < 5) - .accessibilityIdentifier(A11yIdentifiers.bugReportScreen.send) } } } diff --git a/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift b/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift new file mode 100644 index 000000000..bfe73c52e --- /dev/null +++ b/ElementX/Sources/Screens/ReportContent/ReportContentCoordinator.swift @@ -0,0 +1,90 @@ +// +// Copyright 2022 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 ReportContentCoordinatorParameters { + let itemID: String + let roomProxy: RoomProxyProtocol + weak var userIndicatorController: UserIndicatorControllerProtocol? +} + +enum ReportContentCoordinatorAction { + case cancel + case finish +} + +final class ReportContentCoordinator: CoordinatorProtocol { + private let parameters: ReportContentCoordinatorParameters + private var viewModel: ReportContentViewModelProtocol + + var callback: ((ReportContentCoordinatorAction) -> Void)? + + init(parameters: ReportContentCoordinatorParameters) { + self.parameters = parameters + + viewModel = ReportContentViewModel(itemID: parameters.itemID, roomProxy: parameters.roomProxy) + } + + // MARK: - Public + + func start() { + viewModel.callback = { [weak self] action in + guard let self else { return } + switch action { + case .submitStarted: + self.startLoading() + case let .submitFailed(error): + self.stopLoading() + self.showError(description: error.localizedDescription) + case .submitFinished: + self.stopLoading() + self.callback?(.finish) + case .cancel: + self.callback?(.cancel) + } + } + } + + func stop() { + stopLoading() + } + + func toPresentable() -> AnyView { + AnyView(ReportContentScreen(context: viewModel.context)) + } + + // MARK: - Private + + private static let loadingIndicatorIdentifier = "ReportContentLoading" + + private func startLoading() { + parameters.userIndicatorController?.submitIndicator( + UserIndicator(id: Self.loadingIndicatorIdentifier, + type: .modal, + title: ElementL10n.sending, + persistent: true) + ) + } + + private func stopLoading() { + parameters.userIndicatorController?.retractIndicatorWithId(Self.loadingIndicatorIdentifier) + } + + private func showError(description: String) { + parameters.userIndicatorController?.submitIndicator(UserIndicator(title: description)) + } +} diff --git a/ElementX/Sources/Screens/ReportContent/ReportContentModels.swift b/ElementX/Sources/Screens/ReportContent/ReportContentModels.swift new file mode 100644 index 000000000..57da3657c --- /dev/null +++ b/ElementX/Sources/Screens/ReportContent/ReportContentModels.swift @@ -0,0 +1,37 @@ +// +// Copyright 2022 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 Foundation + +enum ReportContentViewModelAction { + case cancel + case submitStarted + case submitFinished + case submitFailed(error: Error) +} + +struct ReportContentViewState: BindableState { + var bindings: ReportContentViewStateBindings +} + +struct ReportContentViewStateBindings { + var reasonText: String +} + +enum ReportContentViewAction { + case cancel + case submit +} diff --git a/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift b/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift new file mode 100644 index 000000000..6af981915 --- /dev/null +++ b/ElementX/Sources/Screens/ReportContent/ReportContentViewModel.swift @@ -0,0 +1,57 @@ +// +// Copyright 2022 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 + +typealias ReportContentViewModelType = StateStoreViewModel + +class ReportContentViewModel: ReportContentViewModelType, ReportContentViewModelProtocol { + var callback: ((ReportContentViewModelAction) -> Void)? + + private let itemID: String + private let roomProxy: RoomProxyProtocol + + init(itemID: String, roomProxy: RoomProxyProtocol) { + self.itemID = itemID + self.roomProxy = roomProxy + super.init(initialViewState: ReportContentViewState(bindings: ReportContentViewStateBindings(reasonText: ""))) + } + + // MARK: - Public + + override func process(viewAction: ReportContentViewAction) async { + switch viewAction { + case .cancel: + callback?(.cancel) + case .submit: + await submitReport() + } + } + + // MARK: Private + + 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): + MXLog.error("Submit Report Content failed: \(error)") + callback?(.submitFailed(error: error)) + } + } +} diff --git a/ElementX/Sources/Screens/ReportContent/ReportContentViewModelProtocol.swift b/ElementX/Sources/Screens/ReportContent/ReportContentViewModelProtocol.swift new file mode 100644 index 000000000..6b04a6dc0 --- /dev/null +++ b/ElementX/Sources/Screens/ReportContent/ReportContentViewModelProtocol.swift @@ -0,0 +1,23 @@ +// +// Copyright 2022 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 Foundation + +@MainActor +protocol ReportContentViewModelProtocol { + var callback: ((ReportContentViewModelAction) -> Void)? { get set } + var context: ReportContentViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/ReportContent/View/ReportContentScreen.swift b/ElementX/Sources/Screens/ReportContent/View/ReportContentScreen.swift new file mode 100644 index 000000000..f7629c12a --- /dev/null +++ b/ElementX/Sources/Screens/ReportContent/View/ReportContentScreen.swift @@ -0,0 +1,84 @@ +// +// Copyright 2022 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 ReportContentScreen: View { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + + @ObservedObject var context: ReportContentViewModel.Context + + private var horizontalPadding: CGFloat { + horizontalSizeClass == .regular ? 50 : 16 + } + + var body: some View { + ScrollView { + mainContent + .padding(.top, 50) + .padding(.horizontal, horizontalPadding) + } + .scrollDismissesKeyboard(.immediately) + .background(Color.element.formBackground.ignoresSafeArea()) + .navigationTitle(ElementL10n.reportContent) + .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 infoText: some View { + Text(ElementL10n.reportContentInfo) + .font(.element.body) + .foregroundColor(Color.element.primaryContent) + } + + private var reasonTextEditor: some View { + FormTextEditor(text: $context.reasonText, placeholder: ElementL10n.reportContentCustomHint) + } + + @ToolbarContentBuilder + private var toolbar: some ToolbarContent { + ToolbarItem(placement: .cancellationAction) { + Button(ElementL10n.actionCancel) { + context.send(viewAction: .cancel) + } + } + + ToolbarItem(placement: .confirmationAction) { + Button(ElementL10n.actionSend) { + context.send(viewAction: .submit) + } + } + } +} + +// MARK: - Previews + +struct ReportContent_Previews: PreviewProvider { + static let viewModel = ReportContentViewModel(itemID: "", roomProxy: MockRoomProxy(displayName: nil)) + + static var previews: some View { + ReportContentScreen(context: viewModel.context) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index adf9f7211..4f89f65b1 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -54,6 +54,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol { self.displayFile(for: fileURL, with: title) case .displayEmojiPicker(let itemId): self.displayEmojiPickerScreen(for: itemId) + case .displayReportContent(let itemId): + self.displayReportContent(for: itemId) } } } @@ -115,4 +117,27 @@ final class RoomScreenCoordinator: CoordinatorProtocol { navigationStackCoordinator.push(coordinator) } + + private func displayReportContent(for itemId: String) { + let navigationCoordinator = NavigationStackCoordinator() + let userIndicatorController = UserIndicatorController(rootCoordinator: NavigationStackCoordinator()) + let parameters = ReportContentCoordinatorParameters(itemID: itemId, + roomProxy: parameters.roomProxy, + userIndicatorController: userIndicatorController) + let coordinator = ReportContentCoordinator(parameters: parameters) + coordinator.callback = { [weak self] completion in + self?.navigationStackCoordinator.setSheetCoordinator(nil) + switch completion { + case .cancel: break + case .finish: + self?.showSuccess(label: ElementL10n.reportContentSubmitted) + } + } + navigationCoordinator.setRootCoordinator(coordinator) + navigationStackCoordinator.setSheetCoordinator(userIndicatorController) + } + + private func showSuccess(label: String) { + ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: label, iconName: "checkmark")) + } } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 330242e5b..5e3928789 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -23,6 +23,7 @@ enum RoomScreenViewModelAction { case displayVideo(videoURL: URL, title: String?) case displayFile(fileURL: URL, title: String?) case displayEmojiPicker(itemId: String) + case displayReportContent(itemId: String) } enum RoomScreenComposerMode: Equatable { @@ -55,7 +56,6 @@ enum RoomScreenViewAction { /// Mark the entire room as read - this is heavy handed as a starting point for now. case markRoomAsRead case contextMenuAction(itemID: String, action: TimelineItemContextMenuAction) - case report(itemID: String, reason: String) } struct RoomScreenViewState: BindableState { @@ -91,25 +91,6 @@ struct RoomScreenViewStateBindings { var alertInfo: AlertInfo? var debugInfo: TimelineItemDebugView.DebugInfo? - - // Report - var report: ReportAlertItem? -} - -final class ReportAlertItem: AlertItem { - init(itemID: String) { - self.itemID = itemID - } - - let title = ElementL10n.reportContentCustomHint - let itemID: String - - private(set) var reason = "" - lazy var reasonBinding = Binding(get: { [unowned self] in - self.reason - }, set: { [unowned self] newValue in - self.reason = newValue - }) } enum RoomScreenErrorType: Hashable { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 354c24271..a9566c44f 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -111,8 +111,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol await markRoomAsRead() case .contextMenuAction(let itemID, let action): processContentMenuAction(action, itemID: itemID) - case let .report(itemID, reason): - await timelineController.reportContent(itemID, reason: reason) } } @@ -320,7 +318,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol await timelineController.retryDecryption(for: sessionID) } case .report: - state.bindings.report = ReportAlertItem(itemID: itemID) + callback?(.displayReportContent(itemId: itemID)) } if action.switchToDefaultComposer { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 5b49fcd03..0d91ceb17 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -30,8 +30,6 @@ struct RoomScreen: View { .toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background. .overlay { loadingIndicator } .alert(item: $context.alertInfo) { $0.alert } - .alert(item: $context.report, - actions: { reportAlertActions($0) }) .sheet(item: $context.debugInfo) { TimelineItemDebugView(info: $0) } .task(id: context.viewState.roomId) { // Give a couple of seconds for items to load and to see them. @@ -41,15 +39,6 @@ struct RoomScreen: View { context.send(viewAction: .markRoomAsRead) } } - - @ViewBuilder - func reportAlertActions(_ report: ReportAlertItem) -> some View { - TextField("", text: report.reasonBinding) - Button(ElementL10n.actionSend, action: { - context.send(viewAction: .report(itemID: report.itemID, reason: report.reason)) - }) - Button(ElementL10n.actionCancel, role: .cancel, action: { }) - } var timeline: some View { TimelineView() diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index 58cc5774b..20ab044cc 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -64,8 +64,6 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { func editMessage(_ newMessage: String, original itemID: String) async { } func redact(_ itemID: String) async { } - - func reportContent(_ itemID: String, reason: String?) async { } func debugDescription(for itemID: String) -> String { "Mock debug description" diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 056c6bdd9..59da99fad 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -189,16 +189,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol { MXLog.error("Failed redacting message with error: \(error)") } } - - func reportContent(_ itemID: String, reason: String?) async { - MXLog.info("Send report content in \(roomID)") - switch await roomProxy.reportContent(itemID, reason: reason) { - case .success: - MXLog.info("Finished reporting content") - case .failure(let error): - MXLog.error("Failed reporting content with error: \(error)") - } - } // Handle this parallel to the timeline items so we're not forced // to bundle the Rust side objects within them @@ -297,8 +287,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol { case .unknown: return nil } - - return nil } private func isItemCollapsible(_ item: TimelineItemProxy) -> Bool { diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 64165bf5b..9173e37aa 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -58,8 +58,6 @@ protocol RoomTimelineControllerProtocol { func sendReaction(_ reaction: String, to itemID: String) async func redact(_ itemID: String) async - - func reportContent(_ itemID: String, reason: String?) async func debugDescription(for itemID: String) -> String diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index a2a16e535..f46474636 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -300,6 +300,11 @@ class MockScreen: Identifiable { members: [.mockAlice, .mockBob, .mockCharlie])) navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator + case .reportContent: + let navigationStackCoordinator = NavigationStackCoordinator() + let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test", roomProxy: MockRoomProxy(displayName: "test"))) + navigationStackCoordinator.setRootCoordinator(coordinator) + return navigationStackCoordinator } }() } diff --git a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift index ee047eb9d..6f9ca28a3 100644 --- a/ElementX/Sources/UITests/UITestsScreenIdentifier.swift +++ b/ElementX/Sources/UITests/UITestsScreenIdentifier.swift @@ -43,6 +43,7 @@ enum UITestsScreenIdentifier: String { case roomDetailsScreen case roomDetailsScreenWithRoomAvatar case roomMemberDetailsScreen + case reportContent } extension UITestsScreenIdentifier: CustomStringConvertible { diff --git a/UITests/Sources/ReportContentScreenUITests.swift b/UITests/Sources/ReportContentScreenUITests.swift new file mode 100644 index 000000000..ffe073424 --- /dev/null +++ b/UITests/Sources/ReportContentScreenUITests.swift @@ -0,0 +1,25 @@ +// +// Copyright 2022 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 ElementX +import XCTest + +class ReportContentScreenUITests: XCTestCase { + func testInitialStateComponents() { + let app = Application.launch(.reportContent) + app.assertScreenshot(.reportContent) + } +} diff --git a/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.reportContent.png b/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.reportContent.png new file mode 100644 index 000000000..64302d289 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.reportContent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec76c4a7817c01035c828a72134bc0838f69bb1aa506bb36b5f2446b0e30cc0e +size 99569 diff --git a/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.reportContent.png b/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.reportContent.png new file mode 100644 index 000000000..7b5dd5053 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.reportContent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f4639f2a8e394ac19f489514ca8028c8b88933f794ecf4133c11bd5923a4ce7 +size 125839 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 new file mode 100644 index 000000000..4ba9b0c38 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent.png @@ -0,0 +1,3 @@ +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.png b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent.png new file mode 100644 index 000000000..3f085df41 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c816fa74f027b401c84513b2c4d8f912fb91e3229e4b4fa1cdda17db2d88be4 +size 131307 diff --git a/UnitTests/Sources/ReportContentViewModelTests.swift b/UnitTests/Sources/ReportContentViewModelTests.swift new file mode 100644 index 000000000..6f079eb1e --- /dev/null +++ b/UnitTests/Sources/ReportContentViewModelTests.swift @@ -0,0 +1,29 @@ +// +// Copyright 2022 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 XCTest + +@testable import ElementX + +@MainActor +class ReportContentScreenViewModelTests: XCTestCase { + func testInitialState() { + let viewModel = ReportContentViewModel(itemID: "test-id", roomProxy: MockRoomProxy(displayName: "test")) + let context = viewModel.context + + XCTAssertEqual(context.reasonText, "") + } +} diff --git a/changelog.d/115.change b/changelog.d/115.change new file mode 100644 index 000000000..021a31637 --- /dev/null +++ b/changelog.d/115.change @@ -0,0 +1 @@ +Improved report content UI. \ No newline at end of file