Add Block user to Report Content screen. (#742)

This commit is contained in:
Doug
2023-04-03 10:21:24 +01:00
committed by GitHub
parent 7d1ab997ce
commit 220138d492
33 changed files with 220 additions and 130 deletions
@@ -114,7 +114,7 @@
"preference_rageshake" = "Rageshake to report bug";
"rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
"rageshake_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
"report_content_explanation" = "Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.";
"report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages.";
"report_content_hint" = "Reason for reporting this content";
"rich_text_editor_bullet_list" = "Toggle bullet list";
"rich_text_editor_code_block" = "Toggle code block";
@@ -166,6 +166,8 @@
"screen_login_username_hint" = "Username";
"screen_onboarding_welcome_subtitle" = "Welcome to the %1$@ Beta. Supercharged, for speed and simplicity.";
"screen_onboarding_welcome_title" = "Be in your Element";
"screen_report_content_block_user" = "Block user";
"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user";
"screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.";
"screen_room_details_encryption_enabled_title" = "Message encryption enabled";
"screen_room_details_invite_people_title" = "Invite people";
@@ -114,7 +114,7 @@
"preference_rageshake" = "Rageshake to report bug";
"rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
"rageshake_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
"report_content_explanation" = "Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.";
"report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages.";
"report_content_hint" = "Reason for reporting this content";
"rich_text_editor_bullet_list" = "Toggle bullet list";
"rich_text_editor_code_block" = "Toggle code block";
@@ -166,6 +166,8 @@
"screen_login_username_hint" = "Username";
"screen_onboarding_welcome_subtitle" = "Welcome to the %1$@ Beta. Supercharged, for speed and simplicity.";
"screen_onboarding_welcome_title" = "Be in your Element";
"screen_report_content_block_user" = "Block user";
"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user";
"screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.";
"screen_room_details_encryption_enabled_title" = "Message encryption enabled";
"screen_room_details_invite_people_title" = "Invite people";
@@ -114,7 +114,7 @@
"preference_rageshake" = "Rageshake to report bug";
"rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
"rageshake_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
"report_content_explanation" = "Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.";
"report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages.";
"report_content_hint" = "Reason for reporting this content";
"rich_text_editor_bullet_list" = "Toggle bullet list";
"rich_text_editor_code_block" = "Toggle code block";
@@ -166,6 +166,8 @@
"screen_login_username_hint" = "Username";
"screen_onboarding_welcome_subtitle" = "Welcome to the %1$@ Beta. Supercharged, for speed and simplicity.";
"screen_onboarding_welcome_title" = "Be in your Element";
"screen_report_content_block_user" = "Block user";
"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user";
"screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.";
"screen_room_details_encryption_enabled_title" = "Message encryption enabled";
"screen_room_details_invite_people_title" = "Invite people";
+5 -1
View File
@@ -256,7 +256,7 @@ public enum L10n {
public static var rageshakeDetectionDialogContent: String { return L10n.tr("Localizable", "rageshake_detection_dialog_content") }
/// You seem to be shaking the phone in frustration. Would you like to open the bug report screen?
public static var rageshakeDialogContent: String { return L10n.tr("Localizable", "rageshake_dialog_content") }
/// Reporting this message will send it’s unique ‘event ID’ to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.
/// This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages.
public static var reportContentExplanation: String { return L10n.tr("Localizable", "report_content_explanation") }
/// Reason for reporting this content
public static var reportContentHint: String { return L10n.tr("Localizable", "report_content_hint") }
@@ -372,6 +372,10 @@ public enum L10n {
}
/// Be in your Element
public static var screenOnboardingWelcomeTitle: String { return L10n.tr("Localizable", "screen_onboarding_welcome_title") }
/// Block user
public static var screenReportContentBlockUser: String { return L10n.tr("Localizable", "screen_report_content_block_user") }
/// Check if you want to hide all current and future messages from this user
public static var screenReportContentBlockUserHint: String { return L10n.tr("Localizable", "screen_report_content_block_user_hint") }
/// Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.
public static var screenRoomDetailsEncryptionEnabledSubtitle: String { return L10n.tr("Localizable", "screen_room_details_encryption_enabled_subtitle") }
/// Message encryption enabled
@@ -427,6 +427,27 @@ class RoomProxyMock: RoomProxyProtocol {
return membersReturnValue
}
}
//MARK: - ignoreUser
var ignoreUserCallsCount = 0
var ignoreUserCalled: Bool {
return ignoreUserCallsCount > 0
}
var ignoreUserReceivedUserID: String?
var ignoreUserReceivedInvocations: [String] = []
var ignoreUserReturnValue: Result<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
var retryDecryptionForCallsCount = 0
@@ -22,6 +22,7 @@ struct A11yIdentifiers {
static let homeScreen = HomeScreen()
static let loginScreen = LoginScreen()
static let onboardingScreen = OnboardingScreen()
static let reportContent = ReportContent()
static let roomScreen = RoomScreen()
static let roomDetailsScreen = RoomDetailsScreen()
static let sessionVerificationScreen = SessionVerificationScreen()
@@ -66,6 +67,10 @@ struct A11yIdentifiers {
let signIn = "onboarding-sign_in"
let hidden = "onboarding-hidden"
}
struct ReportContent {
let ignoreUser = "report_content-ignore_user"
}
struct RoomScreen {
let name = "room-name"
@@ -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 {
let itemID: String
let senderID: String
let roomProxy: RoomProxyProtocol
weak var userIndicatorController: UserIndicatorControllerProtocol?
}
@@ -36,7 +37,7 @@ final class ReportContentCoordinator: CoordinatorProtocol {
init(parameters: ReportContentCoordinatorParameters) {
self.parameters = parameters
viewModel = ReportContentViewModel(itemID: parameters.itemID, roomProxy: parameters.roomProxy)
viewModel = ReportContentViewModel(itemID: parameters.itemID, senderID: parameters.senderID, roomProxy: parameters.roomProxy)
}
// MARK: - Public
@@ -29,6 +29,7 @@ struct ReportContentViewState: BindableState {
struct ReportContentViewStateBindings {
var reasonText: String
var ignoreUser: Bool
}
enum ReportContentViewAction {
@@ -22,12 +22,15 @@ class ReportContentViewModel: ReportContentViewModelType, ReportContentViewModel
var callback: ((ReportContentViewModelAction) -> Void)?
private let itemID: String
private let senderID: String
private let roomProxy: RoomProxyProtocol
init(itemID: String, roomProxy: RoomProxyProtocol) {
init(itemID: String, senderID: String, roomProxy: RoomProxyProtocol) {
self.itemID = itemID
self.senderID = senderID
self.roomProxy = roomProxy
super.init(initialViewState: ReportContentViewState(bindings: ReportContentViewStateBindings(reasonText: "")))
super.init(initialViewState: ReportContentViewState(bindings: ReportContentViewStateBindings(reasonText: "", ignoreUser: false)))
}
// MARK: - Public
@@ -45,13 +48,21 @@ class ReportContentViewModel: ReportContentViewModelType, ReportContentViewModel
private func submitReport() async {
callback?(.submitStarted)
switch await roomProxy.reportContent(itemID, reason: state.bindings.reasonText) {
case .success:
MXLog.info("Submit Report Content succeeded")
callback?(.submitFinished)
case let .failure(error):
if case let .failure(error) = await roomProxy.reportContent(itemID, reason: state.bindings.reasonText) {
MXLog.error("Submit Report Content failed: \(error)")
callback?(.submitFailed(error: error))
return
}
// Ignore the sender if the user wants to.
if state.bindings.ignoreUser, case let .failure(error) = await roomProxy.ignoreUser(senderID) {
MXLog.error("Ignore user failed: \(error)")
callback?(.submitFailed(error: error))
return
}
MXLog.info("Submit Report Content succeeded")
callback?(.submitFinished)
}
}
@@ -26,35 +26,43 @@ struct ReportContentScreen: View {
}
var body: some View {
ScrollView {
mainContent
.padding(.top, 50)
.padding(.horizontal, horizontalPadding)
Form {
reasonSection
ignoreUserSection
}
.scrollDismissesKeyboard(.immediately)
.background(Color.element.formBackground.ignoresSafeArea())
.compoundForm()
.navigationTitle(L10n.actionReportContent)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.interactiveDismissDisabled()
}
/// The main content of the view to be shown in a scroll view.
var mainContent: some View {
VStack(alignment: .leading, spacing: 24) {
infoText
reasonTextEditor
private var reasonSection: some View {
Section {
TextField(L10n.reportContentHint,
text: $context.reasonText,
prompt: Text(L10n.reportContentHint).compoundFormTextFieldPlaceholder(),
axis: .vertical)
.lineLimit(4, reservesSpace: true)
.textFieldStyle(.compoundForm)
} footer: {
Text(L10n.reportContentExplanation)
.compoundFormSectionFooter()
}
.compoundFormSection()
}
private var infoText: some View {
Text(L10n.reportContentExplanation)
.font(.element.body)
.foregroundColor(Color.element.primaryContent)
}
private var reasonTextEditor: some View {
FormTextEditor(text: $context.reasonText, placeholder: L10n.reportContentHint)
private var ignoreUserSection: some View {
Section {
Toggle(L10n.screenReportContentBlockUser, isOn: $context.ignoreUser)
.toggleStyle(.compoundForm)
.accessibilityIdentifier(A11yIdentifiers.reportContent.ignoreUser)
} footer: {
Text(L10n.screenReportContentBlockUserHint)
.compoundFormSectionFooter()
}
}
@ToolbarContentBuilder
@@ -76,9 +84,13 @@ struct ReportContentScreen: View {
// MARK: - Previews
struct ReportContent_Previews: PreviewProvider {
static let viewModel = ReportContentViewModel(itemID: "", roomProxy: RoomProxyMock(with: .init(displayName: nil)))
static let viewModel = ReportContentViewModel(itemID: "",
senderID: "",
roomProxy: RoomProxyMock(with: .init(displayName: nil)))
static var previews: some View {
ReportContentScreen(context: viewModel.context)
NavigationStack {
ReportContentScreen(context: viewModel.context)
}
}
}
@@ -58,10 +58,10 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
self.displayRoomDetails()
case .displayMediaFile(let file, let title):
self.displayFilePreview(for: file, with: title)
case .displayEmojiPicker(let itemId):
self.displayEmojiPickerScreen(for: itemId)
case .displayReportContent(let itemId):
self.displayReportContent(for: itemId)
case .displayEmojiPicker(let itemID):
self.displayEmojiPickerScreen(for: itemID)
case .displayReportContent(let itemID, let senderID):
self.displayReportContent(for: itemID, from: senderID)
case .displayCameraPicker:
self.displayMediaPickerWithSource(.camera)
case .displayMediaPicker:
@@ -159,10 +159,11 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
navigationStackCoordinator.push(coordinator)
}
private func displayReportContent(for itemId: String) {
private func displayReportContent(for itemID: String, from senderID: String) {
let navigationCoordinator = NavigationStackCoordinator()
let userIndicatorController = UserIndicatorController(rootCoordinator: NavigationStackCoordinator())
let parameters = ReportContentCoordinatorParameters(itemID: itemId,
let userIndicatorController = UserIndicatorController(rootCoordinator: navigationCoordinator)
let parameters = ReportContentCoordinatorParameters(itemID: itemID,
senderID: senderID,
roomProxy: parameters.roomProxy,
userIndicatorController: userIndicatorController)
let coordinator = ReportContentCoordinator(parameters: parameters)
@@ -21,8 +21,8 @@ import UIKit
enum RoomScreenViewModelAction {
case displayRoomDetails
case displayMediaFile(file: MediaFileHandleProxy, title: String?)
case displayEmojiPicker(itemId: String)
case displayReportContent(itemId: String)
case displayEmojiPicker(itemID: String)
case displayReportContent(itemID: String, senderID: String)
case displayCameraPicker
case displayMediaPicker
case displayDocumentPicker
@@ -155,7 +155,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private func itemDoubleTapped(with itemId: String) {
guard let item = state.items.first(where: { $0.id == itemId }), item.isReactable else { return }
callback?(.displayEmojiPicker(itemId: itemId))
callback?(.displayEmojiPicker(itemID: itemId))
}
private func buildTimelineViews() {
@@ -294,7 +294,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
switch action {
case .react:
callback?(.displayEmojiPicker(itemId: item.id))
callback?(.displayEmojiPicker(itemID: item.id))
case .copy:
UIPasteboard.general.string = item.body
case .edit:
@@ -327,7 +327,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
await timelineController.retryDecryption(for: sessionID)
}
case .report:
callback?(.displayReportContent(itemId: itemID))
callback?(.displayReportContent(itemID: itemID, senderID: item.sender.id))
}
if action.switchToDefaultComposer {
@@ -288,6 +288,22 @@ class RoomProxy: RoomProxyProtocol {
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
private func buildRoomMemberProxies(members: [RoomMember]) -> [RoomMemberProxy] {
@@ -76,10 +76,12 @@ protocol RoomProxyProtocol {
func editMessage(_ newMessage: String, original 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 members() async -> Result<[RoomMemberProxyProtocol], RoomProxyError>
func ignoreUser(_ userID: String) async -> Result<Void, RoomProxyError>
func retryDecryption(for sessionID: String) async
@@ -305,7 +305,9 @@ class MockScreen: Identifiable {
return navigationStackCoordinator
case .reportContent:
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test", roomProxy: RoomProxyMock(with: .init(displayName: "test"))))
let coordinator = ReportContentCoordinator(parameters: .init(itemID: "test",
senderID: RoomMemberProxyMock.mockAlice.userID,
roomProxy: RoomProxyMock(with: .init(displayName: "test"))))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .startChat: