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";
|
||||
|
||||
"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
|
||||
/*
|
||||
|
@ -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))
|
||||
|
@ -32,7 +32,6 @@ struct A11yIdentifiers {
|
||||
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"
|
||||
}
|
||||
|
||||
|
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:
|
||||
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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
@ -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<RoomScreenErrorType>?
|
||||
|
||||
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 {
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
@ -42,15 +40,6 @@ struct RoomScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
.id(context.viewState.roomId)
|
||||
|
@ -65,8 +65,6 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
||||
|
||||
func redact(_ itemID: String) async { }
|
||||
|
||||
func reportContent(_ itemID: String, reason: String?) async { }
|
||||
|
||||
func debugDescription(for itemID: String) -> String {
|
||||
"Mock debug description"
|
||||
}
|
||||
|
@ -190,16 +190,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
func debugDescription(for itemID: String) -> String {
|
||||
@ -297,8 +287,6 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
||||
case .unknown:
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func isItemCollapsible(_ item: TimelineItemProxy) -> Bool {
|
||||
|
@ -59,8 +59,6 @@ protocol RoomTimelineControllerProtocol {
|
||||
|
||||
func redact(_ itemID: String) async
|
||||
|
||||
func reportContent(_ itemID: String, reason: String?) async
|
||||
|
||||
func debugDescription(for itemID: String) -> String
|
||||
|
||||
func retryDecryption(for sessionID: String) async
|
||||
|
@ -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
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
@ -43,6 +43,7 @@ enum UITestsScreenIdentifier: String {
|
||||
case roomDetailsScreen
|
||||
case roomDetailsScreenWithRoomAvatar
|
||||
case roomMemberDetailsScreen
|
||||
case reportContent
|
||||
}
|
||||
|
||||
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