ios: reports inline (#5466)

* initial types

* changes types

* decode

* possible mock for inline report

* remove avatar

* diff

* updates

* parser and display message

* send messages and support placeholder

* profile reports and all reports working

* new api

* check member support for receiving reports

* report chat item text

* moderator role

* placeholder on text compose for report

* rename method

* remove need to have reported item in memory to action

* archived reports

* changes/fix

* fix block member

* delete and moderate

* archive

* report reason

* context menu/moderation fixes

* typo

* not needed

* report reason as caption, and change text

* remove auto archive

* move placeholder to match text

* prefix red italic report

* archive

* apply mark deleted fix

* Revert "apply mark deleted fix"

This reverts commit b12f14c0f5.

* remove extra space

* context menu rework

* strings, icons

* recheck items extra check on reports

* simplify

* simpler

* reports: never show for own messages, disable attachments, hide when recording or live

* style, allow local deletion

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
Diogo 2025-01-08 18:28:45 +00:00 committed by GitHub
parent 8dc29082d5
commit 7e344b3ee8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 617 additions and 99 deletions

View file

@ -24,6 +24,7 @@ enum ComposeContextItem {
case quotedItem(chatItem: ChatItem)
case editingItem(chatItem: ChatItem)
case forwardingItems(chatItems: [ChatItem], fromChatInfo: ChatInfo)
case reportedItem(chatItem: ChatItem, reason: ReportReason)
}
enum VoiceMessageRecordingState {
@ -116,13 +117,31 @@ struct ComposeState {
default: return false
}
}
var reporting: Bool {
switch contextItem {
case .reportedItem: return true
default: return false
}
}
var submittingValidReport: Bool {
switch contextItem {
case let .reportedItem(_, reason):
switch reason {
case .other: return !message.isEmpty
default: return true
}
default: return false
}
}
var sendEnabled: Bool {
switch preview {
case let .mediaPreviews(media): return !media.isEmpty
case .voicePreview: return voiceMessageRecordingState == .finished
case .filePreview: return true
default: return !message.isEmpty || forwarding || liveMessage != nil
default: return !message.isEmpty || forwarding || liveMessage != nil || submittingValidReport
}
}
@ -175,7 +194,7 @@ struct ComposeState {
}
var attachmentDisabled: Bool {
if editing || forwarding || liveMessage != nil || inProgress { return true }
if editing || forwarding || liveMessage != nil || inProgress || reporting { return true }
switch preview {
case .noPreview: return false
case .linkPreview: return false
@ -193,6 +212,15 @@ struct ComposeState {
}
}
var placeholder: String? {
switch contextItem {
case let .reportedItem(_, reason):
return reason.text
default:
return nil
}
}
var empty: Bool {
message == "" && noPreview
}
@ -297,6 +325,11 @@ struct ComposeView: View {
ContextInvitingContactMemberView()
Divider()
}
if case let .reportedItem(_, reason) = composeState.contextItem {
reportReasonView(reason)
Divider()
}
// preference checks should match checks in forwarding list
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
@ -686,6 +719,27 @@ struct ComposeView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial)
}
private func reportReasonView(_ reason: ReportReason) -> some View {
let reportText = switch reason {
case .spam: NSLocalizedString("Report spam: only group moderators will see it.", comment: "report reason")
case .profile: NSLocalizedString("Report member profile: only group moderators will see it.", comment: "report reason")
case .community: NSLocalizedString("Report violation: only group moderators will see it.", comment: "report reason")
case .illegal: NSLocalizedString("Report content: only group moderators will see it.", comment: "report reason")
case .other: NSLocalizedString("Report other: only group moderators will see it.", comment: "report reason")
case .unknown: "" // Should never happen
}
return Text(reportText)
.italic()
.font(.caption)
.padding(12)
.frame(minHeight: 44)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.thinMaterial)
}
@ViewBuilder private func contextItemView() -> some View {
switch composeState.contextItem {
@ -715,6 +769,15 @@ struct ComposeView: View {
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
)
Divider()
case let .reportedItem(chatItem: reportedItem, _):
ContextItemView(
chat: chat,
contextItems: [reportedItem],
contextIcon: "flag",
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) },
contextIconForeground: Color.red
)
Divider()
}
}
@ -746,6 +809,8 @@ struct ComposeView: View {
sent = await updateMessage(ci, live: live)
} else if let liveMessage = liveMessage, liveMessage.sentMsg != nil {
sent = await updateMessage(liveMessage.chatItem, live: live)
} else if case let .reportedItem(chatItem, reason) = composeState.contextItem {
sent = await send(reason, chatItemId: chatItem.id)
} else {
var quoted: Int64? = nil
if case let .quotedItem(chatItem: quotedItem) = composeState.contextItem {
@ -872,6 +937,8 @@ struct ComposeView: View {
return .voice(text: msgText, duration: duration)
case .file:
return .file(msgText)
case .report(_, let reason):
return .report(text: msgText, reason: reason)
case .unknown(let type, _):
return .unknown(type: type, text: msgText)
}
@ -891,7 +958,25 @@ struct ComposeView: View {
return nil
}
}
func send(_ reportReason: ReportReason, chatItemId: Int64) async -> ChatItem? {
if let chatItems = await apiReportMessage(
groupId: chat.chatInfo.apiId,
chatItemId: chatItemId,
reportReason: reportReason,
reportText: msgText
) {
await MainActor.run {
for chatItem in chatItems {
chatModel.addChatItem(chat.chatInfo, chatItem)
}
}
return chatItems.first
}
return nil
}
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? {
await send(
[ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc)],