From 5d5f788a771e0672b9d2a4749be13942f83adfda Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Wed, 22 Feb 2023 18:14:43 +0100 Subject: [PATCH] Report Content (#587) * ui for reporting content has been implemented * implementation done, will only work withe a mx rust sdk that supports report content * fix * code improvements * code improvement * fix typo Co-authored-by: Stefan Ceriu * removing unneeded task * code improvement * changelog --------- Co-authored-by: Stefan Ceriu --- ElementX/Sources/Other/Extensions/Alert.swift | 45 +++++++++++++++++++ .../Screens/RoomScreen/RoomScreenModels.swift | 23 +++++++++- .../RoomScreen/RoomScreenViewModel.swift | 6 +++ .../Screens/RoomScreen/View/RoomScreen.swift | 11 +++++ .../View/TimelineItemContextMenu.swift | 5 +++ .../Sources/Services/Room/MockRoomProxy.swift | 4 ++ .../Sources/Services/Room/RoomProxy.swift | 16 +++++++ .../Services/Room/RoomProxyProtocol.swift | 3 ++ .../MockRoomTimelineController.swift | 2 + .../RoomTimelineController.swift | 10 +++++ .../RoomTimelineControllerProtocol.swift | 2 + changelog.d/115.feature | 1 + 12 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 ElementX/Sources/Other/Extensions/Alert.swift create mode 100644 changelog.d/115.feature diff --git a/ElementX/Sources/Other/Extensions/Alert.swift b/ElementX/Sources/Other/Extensions/Alert.swift new file mode 100644 index 000000000..cc5a96537 --- /dev/null +++ b/ElementX/Sources/Other/Extensions/Alert.swift @@ -0,0 +1,45 @@ +// +// 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 + +protocol AlertItem { + var title: String { get } +} + +extension View { + func alert(item: Binding, actions: (I) -> V, message: (I) -> V) -> some View where I: AlertItem, V: View { + let binding = Binding(get: { + item.wrappedValue != nil + }, set: { newValue in + if !newValue { + item.wrappedValue = nil + } + }) + return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions, message: message) + } + + func alert(item: Binding, actions: (I) -> V) -> some View where I: AlertItem, V: View { + let binding = Binding(get: { + item.wrappedValue != nil + }, set: { newValue in + if !newValue { + item.wrappedValue = nil + } + }) + return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions) + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 4fedfe12c..330242e5b 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -15,6 +15,7 @@ // import Combine +import SwiftUI import UIKit enum RoomScreenViewModelAction { @@ -54,6 +55,7 @@ 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 { @@ -87,8 +89,27 @@ struct RoomScreenViewStateBindings { /// Information describing the currently displayed alert. var alertInfo: AlertInfo? - + var debugInfo: TimelineItemDebugView.DebugInfo? + + // Report + var report: ReportAlertItem? +} + +final class ReportAlertItem: AlertItem { + init(itemID: String) { + self.itemID = itemID + } + + let title = ElementL10n.reportContentCustomHint + let itemID: String + + private(set) var reason = "" + lazy var reasonBinding = Binding(get: { [unowned self] in + self.reason + }, set: { [unowned self] newValue in + self.reason = newValue + }) } enum RoomScreenErrorType: Hashable { diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 74574ee8d..05ac88d01 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -120,6 +120,8 @@ 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) } } @@ -227,6 +229,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol if item.isOutgoing { actions.append(.redact) + } else { + actions.append(.report) } var debugActions: [TimelineItemContextMenuAction] = ServiceLocator.shared.settings.canShowDeveloperOptions ? [.viewSource] : [] @@ -280,6 +284,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol Task { await timelineController.retryDecryption(for: sessionID) } + case .report: + state.bindings.report = ReportAlertItem(itemID: itemID) } if action.switchToDefaultComposer { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 38a6d1a0a..b249c81ee 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -30,6 +30,8 @@ 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. @@ -39,6 +41,15 @@ struct RoomScreen: View { context.send(viewAction: .markRoomAsRead) } } + + @ViewBuilder + func reportAlertActions(_ report: ReportAlertItem) -> some View { + TextField("", text: report.reasonBinding) + Button(ElementL10n.actionSend, action: { + context.send(viewAction: .report(itemID: report.itemID, reason: report.reason)) + }) + Button(ElementL10n.actionCancel, role: .cancel, action: { }) + } var timeline: some View { TimelineView() diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift index 13bceb7e3..8573cbe9d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemContextMenu.swift @@ -40,6 +40,7 @@ enum TimelineItemContextMenuAction: Identifiable, Hashable { case reply case viewSource case retryDecryption(sessionID: String) + case report var id: Self { self } @@ -107,6 +108,10 @@ public struct TimelineItemContextMenu: View { Button { send(action) } label: { Label(ElementL10n.roomTimelineContextMenuRetryDecryption, systemImage: "arrow.down.message") } + case .report: + Button(role: .destructive) { send(action) } label: { + Label(ElementL10n.reportContent, systemImage: "exclamationmark.bubble") + } } } } diff --git a/ElementX/Sources/Services/Room/MockRoomProxy.swift b/ElementX/Sources/Services/Room/MockRoomProxy.swift index 24134910c..916282a52 100644 --- a/ElementX/Sources/Services/Room/MockRoomProxy.swift +++ b/ElementX/Sources/Services/Room/MockRoomProxy.swift @@ -74,6 +74,10 @@ struct MockRoomProxy: RoomProxyProtocol { .failure(.failedRedactingEvent) } + func reportContent(_ eventID: String, reason: String?) async -> Result { + .failure(.failedReportingContent) + } + func members() async -> Result<[RoomMemberProxy], RoomProxyError> { if let members { return .success(members) diff --git a/ElementX/Sources/Services/Room/RoomProxy.swift b/ElementX/Sources/Services/Room/RoomProxy.swift index 9e036fb0c..3a3f81a5e 100644 --- a/ElementX/Sources/Services/Room/RoomProxy.swift +++ b/ElementX/Sources/Services/Room/RoomProxy.swift @@ -253,6 +253,22 @@ class RoomProxy: RoomProxyProtocol { } } } + + func reportContent(_ eventID: String, reason: String?) async -> Result { + sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true) + defer { + sendMessageBackgroundTask?.stop() + } + + return await Task.dispatch(on: userInitiatedDispatchQueue) { + do { + try self.room.reportContent(eventId: eventID, score: nil, reason: reason) + return .success(()) + } catch { + return .failure(.failedReportingContent) + } + } + } func members() async -> Result<[RoomMemberProxy], RoomProxyError> { await Task.dispatch(on: .global()) { diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 821496d78..a4f0bd78c 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -28,6 +28,7 @@ enum RoomProxyError: Error { case failedSendingReaction case failedEditingMessage case failedRedactingEvent + case failedReportingContent case failedAddingTimelineListener case failedRetrievingMembers } @@ -71,6 +72,8 @@ protocol RoomProxyProtocol { func redact(_ eventID: String) async -> Result + func reportContent(_ eventID: String, reason: String?) async -> Result + func members() async -> Result<[RoomMemberProxy], RoomProxyError> func retryDecryption(for sessionID: String) async diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index 20ab044cc..58cc5774b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -64,6 +64,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { func editMessage(_ newMessage: String, original itemID: String) async { } func redact(_ itemID: String) async { } + + func reportContent(_ itemID: String, reason: String?) async { } func debugDescription(for itemID: String) -> String { "Mock debug description" diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index d7c0b16b4..fd678c61e 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -187,6 +187,16 @@ class RoomTimelineController: RoomTimelineControllerProtocol { MXLog.error("Failed redacting message with error: \(error)") } } + + func reportContent(_ itemID: String, reason: String?) async { + MXLog.info("Send report content in \(roomID)") + switch await roomProxy.reportContent(itemID, reason: reason) { + case .success: + MXLog.info("Finished reporting content") + case .failure(let error): + MXLog.error("Failed reporting content with error: \(error)") + } + } // Handle this parallel to the timeline items so we're not forced // to bundle the Rust side objects within them diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 404becf36..7ffe4ff66 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -59,6 +59,8 @@ protocol RoomTimelineControllerProtocol { func sendReaction(_ reaction: String, to itemID: String) async func redact(_ itemID: String) async + + func reportContent(_ itemID: String, reason: String?) async func debugDescription(for itemID: String) -> String diff --git a/changelog.d/115.feature b/changelog.d/115.feature new file mode 100644 index 000000000..9305a6c6b --- /dev/null +++ b/changelog.d/115.feature @@ -0,0 +1 @@ +Added a feature that allows a user to report content posted by another user by opening the context menu and provide a reason. \ No newline at end of file