From f1a9814faa7ea3bfd50e4d3923a22a7ff6f09976 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 6 Mar 2023 21:57:58 +0000 Subject: [PATCH] ios: UI to moderate messages of other members (#1980) * ios: UI to moderate messages of other members * split moderate action * do not show moderate button on moderated, show alert --- apps/ios/Shared/Model/SimpleXAPI.swift | 6 +++ .../Views/Chat/ChatItem/FramedItemView.swift | 8 ++- .../Chat/ChatItem/MarkedDeletedItemView.swift | 17 ++++-- apps/ios/Shared/Views/Chat/ChatView.swift | 53 ++++++++++++++++--- apps/ios/SimpleXChat/APITypes.swift | 3 ++ apps/ios/SimpleXChat/ChatTypes.swift | 13 +++++ 6 files changed, 86 insertions(+), 14 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index b90ed752c3..2dcf79922c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -288,6 +288,12 @@ func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteM throw r } +func apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) async throws -> (ChatItem, ChatItem?) { + let r = await chatSendCmd(.apiDeleteMemberChatItem(groupId: groupId, groupMemberId: groupMemberId, itemId: itemId), bgDelay: msgDelay) + if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) } + throw r +} + func apiGetNtfToken() -> (DeviceToken?, NtfTknStatus?, NotificationsMode) { let r = chatSendCmdSync(.apiGetNtfToken) switch r { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 4b51a7f44e..1fe89a24cb 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -29,8 +29,12 @@ struct FramedItemView: View { var body: some View { let v = ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading, spacing: 0) { - if chatItem.meta.itemDeleted != nil { - framedItemHeader(icon: "trash", caption: Text("marked deleted").italic()) + if let di = chatItem.meta.itemDeleted { + if case let .moderated(byGroupMember) = di { + framedItemHeader(icon: "flag", caption: Text("moderated by \(byGroupMember.chatViewName)").italic()) + } else { + framedItemHeader(icon: "trash", caption: Text("marked deleted").italic()) + } } else if chatItem.meta.isLive { framedItemHeader(caption: Text("LIVE")) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index 2b40227f8d..63a54fcb7e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -19,10 +19,11 @@ struct MarkedDeletedItemView: View { if showMember, let member = chatItem.memberDisplayName { Text(member).font(.caption).fontWeight(.medium) + Text(": ").font(.caption) } - Text("marked deleted") - .font(.caption) - .foregroundColor(.secondary) - .italic() + if case let .moderated(byGroupMember) = chatItem.meta.itemDeleted { + markedDeletedText("moderated by \(byGroupMember.chatViewName)") + } else { + markedDeletedText("marked deleted") + } CIMetaView(chatItem: chatItem) .padding(.horizontal, 12) } @@ -32,6 +33,14 @@ struct MarkedDeletedItemView: View { .cornerRadius(18) .textSelection(.disabled) } + + func markedDeletedText(_ s: LocalizedStringKey) -> some View { + Text(s) + .font(.caption) + .foregroundColor(.secondary) + .italic() + .lineLimit(1) + } } struct MarkedDeletedItemView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index d17a82321d..21fcb66ddd 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -492,8 +492,13 @@ struct ChatView: View { if !live || !ci.meta.isLive { menu.append(deleteUIAction()) } + if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) { + menu.append(moderateUIAction(groupInfo)) + } } else if ci.meta.itemDeleted != nil { - menu.append(revealUIAction()) + if !ci.isDeletedContent { + menu.append(revealUIAction()) + } menu.append(deleteUIAction()) } else if ci.isDeletedContent { menu.append(deleteUIAction()) @@ -595,7 +600,29 @@ struct ChatView: View { deletingItem = ci } } - + + private func moderateUIAction(_ groupInfo: GroupInfo) -> UIAction { + UIAction( + title: NSLocalizedString("Moderate", comment: "chat item action"), + image: UIImage(systemName: "flag"), + attributes: [.destructive] + ) { _ in + AlertManager.shared.showAlert(Alert( + title: Text("Delete member message?"), + message: Text( + groupInfo.fullGroupPreferences.fullDelete.on + ? "The message will be deleted for all members." + : "The message will be marked as moderated for all members." + ), + primaryButton: .destructive(Text("Delete")) { + deletingItem = ci + deleteMessage(.cidmBroadcast) + }, + secondaryButton: .cancel() + )) + } + } + private func revealUIAction() -> UIAction { UIAction( title: NSLocalizedString("Reveal", comment: "chat item action"), @@ -638,12 +665,22 @@ struct ChatView: View { logger.debug("ChatView deleteMessage: in Task") do { if let di = deletingItem { - let (deletedItem, toItem) = try await apiDeleteChatItem( - type: chat.chatInfo.chatType, - id: chat.chatInfo.apiId, - itemId: di.id, - mode: mode - ) + var deletedItem: ChatItem + var toItem: ChatItem? + if let (groupInfo, groupMember) = di.memberToModerate(chat.chatInfo) { + (deletedItem, toItem) = try await apiDeleteMemberChatItem( + groupId: groupInfo.apiId, + groupMemberId: groupMember.groupMemberId, + itemId: di.id + ) + } else { + (deletedItem, toItem) = try await apiDeleteChatItem( + type: chat.chatInfo.chatType, + id: chat.chatInfo.apiId, + itemId: di.id, + mode: mode + ) + } DispatchQueue.main.async { deletingItem = nil if let toItem = toItem { diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index d7b14ac7ec..6cd1a5a67f 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -33,6 +33,7 @@ public enum ChatCommand { case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) + case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) case apiGetNtfToken case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) @@ -122,6 +123,7 @@ public enum ChatCommand { return "/_send \(ref(type, id)) live=\(onOff(live)) json \(msg)" case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)" case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" + case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)" case .apiGetNtfToken: return "/_ntf get " case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" @@ -216,6 +218,7 @@ public enum ChatCommand { case .apiSendMessage: return "apiSendMessage" case .apiUpdateChatItem: return "apiUpdateChatItem" case .apiDeleteChatItem: return "apiDeleteChatItem" + case .apiDeleteMemberChatItem: return "apiDeleteMemberChatItem" case .apiGetNtfToken: return "apiGetNtfToken" case .apiRegisterToken: return "apiRegisterToken" case .apiVerifyToken: return "apiVerifyToken" diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 49ad79449c..71850a5ee0 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1722,6 +1722,8 @@ public struct ChatItem: Identifiable, Decodable { switch content { case .sndDeleted: return true case .rcvDeleted: return true + case .sndModerated: return true + case .rcvModerated: return true default: return false } } @@ -1789,6 +1791,17 @@ public struct ChatItem: Identifiable, Decodable { } } + public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? { + switch (chatInfo, chatDir) { + case let (.group(groupInfo), .groupRcv(groupMember)): + let m = groupInfo.membership + return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil + ? (groupInfo, groupMember) + : nil + default: return nil + } + } + public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> ChatItem { ChatItem( chatDir: dir,