mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Report Content v2 (#659)
* created the empty files * set up the view content * connected the Room Coordinator to the ReportContent Coordinator * added the loading indicators and the dismiss behaviour * almost completed but I need to display the success indicator when the report is sent succesfully * completed * added an untranslated string * tests * Update ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> * Update ElementX/Sources/Screens/ReportContent/View/ReportContentScreen.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> * pr comment * pr suggestion * removing unused identifiers * fixing compilation error * added a form text editor view * changelog --------- Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
parent
1c09a7eace
commit
eed031c9cc
@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
"message" = "Message";
|
"message" = "Message";
|
||||||
|
|
||||||
|
"sending" = "Sending...";
|
||||||
|
|
||||||
"screenshot_detected_title" = "You took a screenshot";
|
"screenshot_detected_title" = "You took a screenshot";
|
||||||
"screenshot_detected_message" = "Would you like to submit a bug report?";
|
"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_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_attach_screenshot" = "Attach Screenshot";
|
||||||
"bug_report_screen_edit_screenshot" = "Edit 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
|
// Session verification
|
||||||
/*
|
/*
|
||||||
|
@ -26,8 +26,6 @@ extension ElementL10n {
|
|||||||
public static let bugReportScreenIncludeLogs = ElementL10n.tr("Untranslated", "bug_report_screen_include_logs")
|
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.
|
/// 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")
|
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
|
/// Report a bug
|
||||||
public static let bugReportScreenTitle = ElementL10n.tr("Untranslated", "bug_report_screen_title")
|
public static let bugReportScreenTitle = ElementL10n.tr("Untranslated", "bug_report_screen_title")
|
||||||
/// %@ iOS
|
/// %@ iOS
|
||||||
@ -96,6 +94,10 @@ extension ElementL10n {
|
|||||||
}
|
}
|
||||||
/// Notification
|
/// Notification
|
||||||
public static let notification = ElementL10n.tr("Untranslated", "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
|
/// About
|
||||||
public static let roomDetailsAboutSectionTitle = ElementL10n.tr("Untranslated", "room_details_about_section_title")
|
public static let roomDetailsAboutSectionTitle = ElementL10n.tr("Untranslated", "room_details_about_section_title")
|
||||||
/// Copy Link
|
/// Copy Link
|
||||||
@ -136,6 +138,8 @@ extension ElementL10n {
|
|||||||
public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message")
|
public static let screenshotDetectedMessage = ElementL10n.tr("Untranslated", "screenshot_detected_message")
|
||||||
/// You took a screenshot
|
/// You took a screenshot
|
||||||
public static let screenshotDetectedTitle = ElementL10n.tr("Untranslated", "screenshot_detected_title")
|
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. %@
|
/// 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 {
|
public static func serverSelectionServerFooter(_ p1: Any) -> String {
|
||||||
return ElementL10n.tr("Untranslated", "server_selection_server_footer", String(describing: p1))
|
return ElementL10n.tr("Untranslated", "server_selection_server_footer", String(describing: p1))
|
||||||
|
@ -26,13 +26,12 @@ struct A11yIdentifiers {
|
|||||||
static let roomDetailsScreen = RoomDetailsScreen()
|
static let roomDetailsScreen = RoomDetailsScreen()
|
||||||
static let sessionVerificationScreen = SessionVerificationScreen()
|
static let sessionVerificationScreen = SessionVerificationScreen()
|
||||||
static let softLogoutScreen = SoftLogoutScreen()
|
static let softLogoutScreen = SoftLogoutScreen()
|
||||||
|
|
||||||
struct BugReportScreen {
|
struct BugReportScreen {
|
||||||
let report = "bug_report-report"
|
let report = "bug_report-report"
|
||||||
let sendLogs = "bug_report-send_logs"
|
let sendLogs = "bug_report-send_logs"
|
||||||
let screenshot = "bug_report-screenshot"
|
let screenshot = "bug_report-screenshot"
|
||||||
let removeScreenshot = "bug_report-remove_screenshot"
|
let removeScreenshot = "bug_report-remove_screenshot"
|
||||||
let send = "bug_report-send"
|
|
||||||
let attachScreenshot = "bug-report-attach_screenshot"
|
let attachScreenshot = "bug-report-attach_screenshot"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
64
ElementX/Sources/Other/SwiftUI/Views/FormTextEditor.swift
Normal file
64
ElementX/Sources/Other/SwiftUI/Views/FormTextEditor.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -57,7 +57,7 @@ final class BugReportCoordinator: CoordinatorProtocol {
|
|||||||
case .cancel:
|
case .cancel:
|
||||||
self.completion?(.cancel)
|
self.completion?(.cancel)
|
||||||
case let .submitStarted(progressTracker):
|
case let .submitStarted(progressTracker):
|
||||||
self.startLoading(label: ElementL10n.bugReportScreenSending, progressPublisher: progressTracker)
|
self.startLoading(label: ElementL10n.sending, progressPublisher: progressTracker)
|
||||||
case .submitFinished:
|
case .submitFinished:
|
||||||
self.stopLoading()
|
self.stopLoading()
|
||||||
self.completion?(.finish)
|
self.completion?(.finish)
|
||||||
|
@ -61,35 +61,10 @@ struct BugReportScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private var descriptionTextEditor: some View {
|
private var descriptionTextEditor: some View {
|
||||||
ZStack(alignment: .topLeading) {
|
FormTextEditor(text: $context.reportText,
|
||||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
placeholder: ElementL10n.bugReportScreenDescription,
|
||||||
.fill(Color.element.formRowBackground)
|
editorAccessibilityIdentifier: A11yIdentifiers.bugReportScreen.report)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@ -164,7 +139,6 @@ struct BugReportScreen: View {
|
|||||||
context.send(viewAction: .submit)
|
context.send(viewAction: .submit)
|
||||||
}
|
}
|
||||||
.disabled(context.reportText.count < 5)
|
.disabled(context.reportText.count < 5)
|
||||||
.accessibilityIdentifier(A11yIdentifiers.bugReportScreen.send)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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<ReportContentViewState, ReportContentViewAction>
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 }
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -54,6 +54,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.displayFile(for: fileURL, with: title)
|
self.displayFile(for: fileURL, with: title)
|
||||||
case .displayEmojiPicker(let itemId):
|
case .displayEmojiPicker(let itemId):
|
||||||
self.displayEmojiPickerScreen(for: itemId)
|
self.displayEmojiPickerScreen(for: itemId)
|
||||||
|
case .displayReportContent(let itemId):
|
||||||
|
self.displayReportContent(for: itemId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,4 +117,27 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
navigationStackCoordinator.push(coordinator)
|
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"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ enum RoomScreenViewModelAction {
|
|||||||
case displayVideo(videoURL: URL, title: String?)
|
case displayVideo(videoURL: URL, title: String?)
|
||||||
case displayFile(fileURL: URL, title: String?)
|
case displayFile(fileURL: URL, title: String?)
|
||||||
case displayEmojiPicker(itemId: String)
|
case displayEmojiPicker(itemId: String)
|
||||||
|
case displayReportContent(itemId: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomScreenComposerMode: Equatable {
|
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.
|
/// Mark the entire room as read - this is heavy handed as a starting point for now.
|
||||||
case markRoomAsRead
|
case markRoomAsRead
|
||||||
case contextMenuAction(itemID: String, action: TimelineItemContextMenuAction)
|
case contextMenuAction(itemID: String, action: TimelineItemContextMenuAction)
|
||||||
case report(itemID: String, reason: String)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomScreenViewState: BindableState {
|
struct RoomScreenViewState: BindableState {
|
||||||
@ -91,25 +91,6 @@ struct RoomScreenViewStateBindings {
|
|||||||
var alertInfo: AlertInfo<RoomScreenErrorType>?
|
var alertInfo: AlertInfo<RoomScreenErrorType>?
|
||||||
|
|
||||||
var debugInfo: TimelineItemDebugView.DebugInfo?
|
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<String>(get: { [unowned self] in
|
|
||||||
self.reason
|
|
||||||
}, set: { [unowned self] newValue in
|
|
||||||
self.reason = newValue
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomScreenErrorType: Hashable {
|
enum RoomScreenErrorType: Hashable {
|
||||||
|
@ -111,8 +111,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
await markRoomAsRead()
|
await markRoomAsRead()
|
||||||
case .contextMenuAction(let itemID, let action):
|
case .contextMenuAction(let itemID, let action):
|
||||||
processContentMenuAction(action, itemID: itemID)
|
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)
|
await timelineController.retryDecryption(for: sessionID)
|
||||||
}
|
}
|
||||||
case .report:
|
case .report:
|
||||||
state.bindings.report = ReportAlertItem(itemID: itemID)
|
callback?(.displayReportContent(itemId: itemID))
|
||||||
}
|
}
|
||||||
|
|
||||||
if action.switchToDefaultComposer {
|
if action.switchToDefaultComposer {
|
||||||
|
@ -30,8 +30,6 @@ struct RoomScreen: View {
|
|||||||
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
|
.toolbarBackground(.visible, for: .navigationBar) // Fix the toolbar's background.
|
||||||
.overlay { loadingIndicator }
|
.overlay { loadingIndicator }
|
||||||
.alert(item: $context.alertInfo) { $0.alert }
|
.alert(item: $context.alertInfo) { $0.alert }
|
||||||
.alert(item: $context.report,
|
|
||||||
actions: { reportAlertActions($0) })
|
|
||||||
.sheet(item: $context.debugInfo) { TimelineItemDebugView(info: $0) }
|
.sheet(item: $context.debugInfo) { TimelineItemDebugView(info: $0) }
|
||||||
.task(id: context.viewState.roomId) {
|
.task(id: context.viewState.roomId) {
|
||||||
// Give a couple of seconds for items to load and to see them.
|
// Give a couple of seconds for items to load and to see them.
|
||||||
@ -41,15 +39,6 @@ struct RoomScreen: View {
|
|||||||
context.send(viewAction: .markRoomAsRead)
|
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 {
|
var timeline: some View {
|
||||||
TimelineView()
|
TimelineView()
|
||||||
|
@ -64,8 +64,6 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
func editMessage(_ newMessage: String, original itemID: String) async { }
|
func editMessage(_ newMessage: String, original itemID: String) async { }
|
||||||
|
|
||||||
func redact(_ itemID: String) async { }
|
func redact(_ itemID: String) async { }
|
||||||
|
|
||||||
func reportContent(_ itemID: String, reason: String?) async { }
|
|
||||||
|
|
||||||
func debugDescription(for itemID: String) -> String {
|
func debugDescription(for itemID: String) -> String {
|
||||||
"Mock debug description"
|
"Mock debug description"
|
||||||
|
@ -189,16 +189,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
MXLog.error("Failed redacting message with error: \(error)")
|
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
|
// Handle this parallel to the timeline items so we're not forced
|
||||||
// to bundle the Rust side objects within them
|
// to bundle the Rust side objects within them
|
||||||
@ -297,8 +287,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
case .unknown:
|
case .unknown:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isItemCollapsible(_ item: TimelineItemProxy) -> Bool {
|
private func isItemCollapsible(_ item: TimelineItemProxy) -> Bool {
|
||||||
|
@ -58,8 +58,6 @@ protocol RoomTimelineControllerProtocol {
|
|||||||
func sendReaction(_ reaction: String, to itemID: String) async
|
func sendReaction(_ reaction: String, to itemID: String) async
|
||||||
|
|
||||||
func redact(_ itemID: String) async
|
func redact(_ itemID: String) async
|
||||||
|
|
||||||
func reportContent(_ itemID: String, reason: String?) async
|
|
||||||
|
|
||||||
func debugDescription(for itemID: String) -> String
|
func debugDescription(for itemID: String) -> String
|
||||||
|
|
||||||
|
@ -300,6 +300,11 @@ class MockScreen: Identifiable {
|
|||||||
members: [.mockAlice, .mockBob, .mockCharlie]))
|
members: [.mockAlice, .mockBob, .mockCharlie]))
|
||||||
navigationStackCoordinator.setRootCoordinator(coordinator)
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
return navigationStackCoordinator
|
return navigationStackCoordinator
|
||||||
|
case .reportContent:
|
||||||
|
let navigationStackCoordinator = NavigationStackCoordinator()
|
||||||
|
let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test", roomProxy: MockRoomProxy(displayName: "test")))
|
||||||
|
navigationStackCoordinator.setRootCoordinator(coordinator)
|
||||||
|
return navigationStackCoordinator
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ enum UITestsScreenIdentifier: String {
|
|||||||
case roomDetailsScreen
|
case roomDetailsScreen
|
||||||
case roomDetailsScreenWithRoomAvatar
|
case roomDetailsScreenWithRoomAvatar
|
||||||
case roomMemberDetailsScreen
|
case roomMemberDetailsScreen
|
||||||
|
case reportContent
|
||||||
}
|
}
|
||||||
|
|
||||||
extension UITestsScreenIdentifier: CustomStringConvertible {
|
extension UITestsScreenIdentifier: CustomStringConvertible {
|
||||||
|
25
UITests/Sources/ReportContentScreenUITests.swift
Normal file
25
UITests/Sources/ReportContentScreenUITests.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.reportContent.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPad-9th-generation.reportContent.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.reportContent.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/de-DE-iPhone-14.reportContent.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPad-9th-generation.reportContent.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent.png
(Stored with Git LFS)
Normal file
BIN
UITests/Sources/__Snapshots__/Application/en-GB-iPhone-14.reportContent.png
(Stored with Git LFS)
Normal file
Binary file not shown.
29
UnitTests/Sources/ReportContentViewModelTests.swift
Normal file
29
UnitTests/Sources/ReportContentViewModelTests.swift
Normal file
@ -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, "")
|
||||||
|
}
|
||||||
|
}
|
1
changelog.d/115.change
Normal file
1
changelog.d/115.change
Normal file
@ -0,0 +1 @@
|
|||||||
|
Improved report content UI.
|
Loading…
x
Reference in New Issue
Block a user