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