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 <stefanc@matrix.org> * removing unneeded task * code improvement * changelog --------- Co-authored-by: Stefan Ceriu <stefanc@matrix.org>
This commit is contained in:
45
ElementX/Sources/Other/Extensions/Alert.swift
Normal file
45
ElementX/Sources/Other/Extensions/Alert.swift
Normal file
@@ -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<I, V>(item: Binding<I?>, actions: (I) -> V, message: (I) -> V) -> some View where I: AlertItem, V: View {
|
||||
let binding = Binding<Bool>(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<I, V>(item: Binding<I?>, actions: (I) -> V) -> some View where I: AlertItem, V: View {
|
||||
let binding = Binding<Bool>(get: {
|
||||
item.wrappedValue != nil
|
||||
}, set: { newValue in
|
||||
if !newValue {
|
||||
item.wrappedValue = nil
|
||||
}
|
||||
})
|
||||
return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions)
|
||||
}
|
||||
}
|
||||
@@ -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<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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,10 @@ struct MockRoomProxy: RoomProxyProtocol {
|
||||
.failure(.failedRedactingEvent)
|
||||
}
|
||||
|
||||
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError> {
|
||||
.failure(.failedReportingContent)
|
||||
}
|
||||
|
||||
func members() async -> Result<[RoomMemberProxy], RoomProxyError> {
|
||||
if let members {
|
||||
return .success(members)
|
||||
|
||||
@@ -253,6 +253,22 @@ class RoomProxy: RoomProxyProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reportContent(_ eventID: String, reason: 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.reportContent(eventId: eventID, score: nil, reason: reason)
|
||||
return .success(())
|
||||
} catch {
|
||||
return .failure(.failedReportingContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func members() async -> Result<[RoomMemberProxy], RoomProxyError> {
|
||||
await Task.dispatch(on: .global()) {
|
||||
|
||||
@@ -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<Void, RoomProxyError>
|
||||
|
||||
func reportContent(_ eventID: String, reason: String?) async -> Result<Void, RoomProxyError>
|
||||
|
||||
func members() async -> Result<[RoomMemberProxy], RoomProxyError>
|
||||
|
||||
func retryDecryption(for sessionID: String) async
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
1
changelog.d/115.feature
Normal file
1
changelog.d/115.feature
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user