mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-24 15:15:35 +00:00
Merge branch 'master' into av/multiplatform-reports-screen
This commit is contained in:
@@ -1284,20 +1284,11 @@ struct ChatView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func menu(_ ci: ChatItem, _ range: ClosedRange<Int>?, live: Bool) -> some View {
|
||||
if let groupInfo = chat.chatInfo.groupInfo, ci.isReport, ci.meta.itemDeleted == nil {
|
||||
if ci.chatDir == .groupSnd {
|
||||
deleteButton(ci)
|
||||
} else {
|
||||
if case let .group(gInfo) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil {
|
||||
if ci.chatDir != .groupSnd, gInfo.membership.memberRole >= .moderator {
|
||||
archiveReportButton(ci)
|
||||
if let qi = ci.quotedItem {
|
||||
moderateReportedButton(qi, ci, groupInfo)
|
||||
if let rMember = qi.memberToModerate(chat.chatInfo) {
|
||||
if !rMember.blockedByAdmin, rMember.canBlockForAll(groupInfo: groupInfo) {
|
||||
blockMemberButton(rMember, groupInfo, qi, ci)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
deleteButton(ci, label: "Delete report")
|
||||
} else if let mc = ci.content.msgContent, !ci.isReport, ci.meta.itemDeleted == nil || revealed {
|
||||
if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction,
|
||||
availableReactions.count > 0 {
|
||||
@@ -1351,7 +1342,7 @@ struct ChatView: View {
|
||||
if ci.chatDir != .groupSnd {
|
||||
if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) {
|
||||
moderateButton(ci, groupInfo)
|
||||
} else if ci.meta.itemDeleted == nil, case let .group(gInfo) = chat.chatInfo, gInfo.membership.memberRole < .moderator, !live, composeState.voiceMessageRecordingState == .noRecording {
|
||||
} else if ci.meta.itemDeleted == nil, case let .group(gInfo) = chat.chatInfo, gInfo.membership.memberRole == .member, !live, composeState.voiceMessageRecordingState == .noRecording {
|
||||
reportButton(ci)
|
||||
}
|
||||
}
|
||||
@@ -1627,7 +1618,7 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteButton(_ ci: ChatItem) -> Button<some View> {
|
||||
private func deleteButton(_ ci: ChatItem, label: LocalizedStringKey = "Delete") -> Button<some View> {
|
||||
Button(role: .destructive) {
|
||||
if !revealed,
|
||||
let currIndex = m.getChatItemIndex(ci),
|
||||
@@ -1649,10 +1640,7 @@ struct ChatView: View {
|
||||
deletingItem = ci
|
||||
}
|
||||
} label: {
|
||||
Label(
|
||||
NSLocalizedString("Delete", comment: "chat item action"),
|
||||
systemImage: "trash"
|
||||
)
|
||||
Label(label, systemImage: "trash")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1668,31 +1656,19 @@ struct ChatView: View {
|
||||
|
||||
private func moderateButton(_ ci: ChatItem, _ groupInfo: GroupInfo) -> Button<some View> {
|
||||
Button(role: .destructive) {
|
||||
showModerateMessageAlert(groupInfo) {
|
||||
deletingItem = ci
|
||||
deleteMessage(.cidmBroadcast, moderate: true)
|
||||
}
|
||||
} label: {
|
||||
Label(
|
||||
NSLocalizedString("Moderate", comment: "chat item action"),
|
||||
systemImage: "flag"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func moderateReportedButton(_ rItem: CIQuote, _ reportItem: ChatItem, _ groupInfo: GroupInfo) -> Button<some View> {
|
||||
Button(role: .destructive) {
|
||||
showModerateMessageAlert(groupInfo) {
|
||||
Task {
|
||||
let deleted = await deleteReportedMessage(rItem, reportItem.id, groupInfo)
|
||||
if deleted != nil {
|
||||
await MainActor.run {
|
||||
deletingItem = reportItem
|
||||
deleteMessage(.cidmInternalMark, moderate: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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, moderate: true)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
} label: {
|
||||
Label(
|
||||
NSLocalizedString("Moderate", comment: "chat item action"),
|
||||
@@ -1715,74 +1691,7 @@ struct ChatView: View {
|
||||
)
|
||||
)
|
||||
} label: {
|
||||
Label(
|
||||
NSLocalizedString("Archive", comment: "chat item action"),
|
||||
systemImage: "archivebox"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func blockMemberButton(_ member: GroupMember, _ groupInfo: GroupInfo, _ rItem: CIQuote, _ report: ChatItem) -> Button<some View> {
|
||||
Button(role: .destructive) {
|
||||
actionSheet = SomeActionSheet(
|
||||
actionSheet: ActionSheet(
|
||||
title: Text("Block and moderate?"),
|
||||
buttons: [
|
||||
.destructive(Text("Block and moderate")) {
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text("Delete member message and block?"),
|
||||
message: Text(
|
||||
NSLocalizedString(
|
||||
groupInfo.fullGroupPreferences.fullDelete.on
|
||||
? "The message will be deleted for all members.\nAll new messages from \(member.chatViewName) will be hidden!"
|
||||
: "The message will be marked as moderated for all members.\n All new messages from \(member.chatViewName) will be hidden!"
|
||||
, comment: "block and moderate action"
|
||||
)
|
||||
),
|
||||
primaryButton: .destructive(Text("Delete and block")) {
|
||||
Task {
|
||||
let deleted = await deleteReportedMessage(rItem, report.id, groupInfo)
|
||||
if deleted != nil {
|
||||
let blocked = await blockMemberForAll(groupInfo, member, true)
|
||||
|
||||
if blocked != nil {
|
||||
await MainActor.run {
|
||||
deletingItem = report
|
||||
deleteMessage(.cidmInternalMark, moderate: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
)
|
||||
},
|
||||
.destructive(Text("Only block")) {
|
||||
Task {
|
||||
if (await getLocalIdForReportedMessage(rItem, report.id, groupInfo)) != nil {
|
||||
AlertManager.shared.showAlert(
|
||||
blockForAllAlert(groupInfo, member) {
|
||||
deletingItem = report
|
||||
deleteMessage(.cidmInternalMark, moderate: false)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
showNoMessageMessageAlert()
|
||||
}
|
||||
}
|
||||
},
|
||||
.cancel()
|
||||
]
|
||||
),
|
||||
id: "blockMember"
|
||||
)
|
||||
} label: {
|
||||
Label(
|
||||
NSLocalizedString("Block member", comment: "chat item action"),
|
||||
systemImage: "hand.raised"
|
||||
)
|
||||
Label("Archive report", systemImage: "archivebox")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1886,60 +1795,6 @@ struct ChatView: View {
|
||||
itemIds.forEach { selectedChatItems?.remove($0) }
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteReportedMessage(_ rItem: CIQuote, _ reportId: Int64, _ groupInfo: GroupInfo) async -> ChatItemDeletion? {
|
||||
do {
|
||||
let itemId = await getLocalIdForReportedMessage(rItem, reportId, groupInfo)
|
||||
|
||||
if let itemId = itemId {
|
||||
let deletedItem = try await apiDeleteMemberChatItems(
|
||||
groupId: groupInfo.apiId,
|
||||
itemIds: [itemId]
|
||||
).first
|
||||
|
||||
if let di = deletedItem {
|
||||
await MainActor.run {
|
||||
if let toItem = di.toChatItem {
|
||||
_ = m.upsertChatItem(chat.chatInfo, toItem.chatItem)
|
||||
} else {
|
||||
m.removeChatItem(chat.chatInfo, di.deletedChatItem.chatItem)
|
||||
}
|
||||
}
|
||||
|
||||
return di
|
||||
}
|
||||
} else {
|
||||
showNoMessageMessageAlert()
|
||||
}
|
||||
} catch {
|
||||
logger.error("ChatView.deleteReportedMessage error: \(error)")
|
||||
AlertManager.shared.showAlertMsg(title: LocalizedStringKey("Error"), message: LocalizedStringKey("Failed to delete reported message"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func getLocalIdForReportedMessage(_ rItem: CIQuote, _ reportId: Int64, _ groupInfo: GroupInfo) async -> Int64? {
|
||||
do {
|
||||
if let itemId = rItem.itemId {
|
||||
return itemId
|
||||
} else {
|
||||
let reportItem = try await apiGetChatItems(
|
||||
type: chat.chatInfo.chatType,
|
||||
id: chat.chatInfo.apiId,
|
||||
pagination: .around(chatItemId: reportId, count: 0)
|
||||
).first
|
||||
|
||||
if let itemId = reportItem?.quotedItem?.itemId {
|
||||
return itemId
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
logger.error("ChatView.getLocalIdForReportedMessage error: \(error)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func deleteMessage(_ mode: CIDeleteMode, moderate: Bool) {
|
||||
logger.debug("ChatView deleteMessage")
|
||||
@@ -2014,26 +1869,6 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func showModerateMessageAlert(_ groupInfo: GroupInfo, _ onModerate: @escaping () -> Void) {
|
||||
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"), action: onModerate),
|
||||
secondaryButton: .cancel()
|
||||
))
|
||||
}
|
||||
|
||||
private func showNoMessageMessageAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: LocalizedStringKey("No message"),
|
||||
message: LocalizedStringKey("This message was deleted or not received yet.")
|
||||
)
|
||||
}
|
||||
|
||||
private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey {
|
||||
chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone"
|
||||
}
|
||||
|
||||
@@ -781,18 +781,12 @@ func updateMemberSettings(_ gInfo: GroupInfo, _ member: GroupMember, _ memberSet
|
||||
}
|
||||
}
|
||||
|
||||
func blockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember, _ onBlocked: (() -> Void)? = nil) -> Alert {
|
||||
func blockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert {
|
||||
Alert(
|
||||
title: Text("Block member for all?"),
|
||||
message: Text("All new messages from \(mem.chatViewName) will be hidden!"),
|
||||
primaryButton: .destructive(Text("Block for all")) {
|
||||
Task {
|
||||
let uMember = await blockMemberForAll(gInfo, mem, true)
|
||||
|
||||
if uMember != nil {
|
||||
onBlocked?()
|
||||
}
|
||||
}
|
||||
blockMemberForAll(gInfo, mem, true)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
@@ -803,25 +797,23 @@ func unblockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert {
|
||||
title: Text("Unblock member for all?"),
|
||||
message: Text("Messages from \(mem.chatViewName) will be shown!"),
|
||||
primaryButton: .default(Text("Unblock for all")) {
|
||||
Task {
|
||||
await blockMemberForAll(gInfo, mem, false)
|
||||
}
|
||||
blockMemberForAll(gInfo, mem, false)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
|
||||
func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) async -> GroupMember? {
|
||||
do {
|
||||
let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked)
|
||||
await MainActor.run {
|
||||
_ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember)
|
||||
func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) {
|
||||
Task {
|
||||
do {
|
||||
let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked)
|
||||
await MainActor.run {
|
||||
_ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember)
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("apiBlockMemberForAll error: \(responseError(error))")
|
||||
}
|
||||
return updatedMember
|
||||
} catch let error {
|
||||
logger.error("apiBlockMemberForAll error: \(responseError(error))")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
struct GroupMemberInfoView_Previews: PreviewProvider {
|
||||
|
||||
@@ -277,7 +277,7 @@ struct ChatPreviewView: View {
|
||||
|
||||
func prefix() -> Text {
|
||||
switch cItem.content.msgContent {
|
||||
case let .report(text, reason): return Text(!text.isEmpty ? "\(reason.text): " : reason.text).italic().foregroundColor(Color.red)
|
||||
case let .report(_, reason): return Text(!itemText.isEmpty ? "\(reason.text): " : reason.text).italic().foregroundColor(Color.red)
|
||||
default: return Text("")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2105,7 +2105,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
|
||||
public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? {
|
||||
if !canBeRemoved(groupInfo: groupInfo) { return nil }
|
||||
let userRole = groupInfo.membership.memberRole
|
||||
return GroupMemberRole.allCases.filter { $0 <= userRole && $0 != .author }
|
||||
return GroupMemberRole.supportedRoles.filter { $0 <= userRole }
|
||||
}
|
||||
|
||||
public func canBlockForAll(groupInfo: GroupInfo) -> Bool {
|
||||
@@ -3364,17 +3364,6 @@ public struct CIQuote: Decodable, ItemContent, Hashable {
|
||||
}
|
||||
return CIQuote(chatDir: chatDir, itemId: itemId, sentAt: sentAt, content: mc)
|
||||
}
|
||||
|
||||
public func memberToModerate(_ chatInfo: ChatInfo) -> GroupMember? {
|
||||
switch (chatInfo, chatDir) {
|
||||
case let (.group(groupInfo), .groupRcv(groupMember)):
|
||||
let m = groupInfo.membership
|
||||
return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole
|
||||
? groupMember
|
||||
: nil
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct CIReactionCount: Decodable, Hashable {
|
||||
|
||||
-13
@@ -3192,19 +3192,6 @@ class CIQuote (
|
||||
null -> null
|
||||
}
|
||||
|
||||
fun memberToModerate(chatInfo: ChatInfo): GroupMember? {
|
||||
return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) {
|
||||
val m = chatInfo.groupInfo.membership
|
||||
if (m.memberRole >= GroupMemberRole.Moderator && m.memberRole >= chatDir.groupMember.memberRole) {
|
||||
chatDir.groupMember
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getSample(itemId: Long?, sentAt: Instant, text: String, chatDir: CIDirection?): CIQuote =
|
||||
CIQuote(chatDir = chatDir, itemId = itemId, sentAt = sentAt, content = MsgContent.MCText(text))
|
||||
|
||||
+39
-39
@@ -365,50 +365,50 @@ fun ChatView(
|
||||
}
|
||||
},
|
||||
deleteMessage = { itemId, mode ->
|
||||
val toDeleteItem = reversedChatItems.value.lastOrNull { it.id == itemId }
|
||||
val toModerate = toDeleteItem?.memberToModerate(chatInfo)
|
||||
val groupInfo = toModerate?.first
|
||||
val groupMember = toModerate?.second
|
||||
val deletedChatItem: ChatItem?
|
||||
val toChatItem: ChatItem?
|
||||
val r = if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
||||
chatModel.controller.apiDeleteMemberChatItems(
|
||||
chatRh,
|
||||
groupId = groupInfo.groupId,
|
||||
itemIds = listOf(itemId)
|
||||
)
|
||||
} else {
|
||||
chatModel.controller.apiDeleteChatItems(
|
||||
chatRh,
|
||||
type = chatInfo.chatType,
|
||||
id = chatInfo.apiId,
|
||||
itemIds = listOf(itemId),
|
||||
mode = mode
|
||||
)
|
||||
}
|
||||
val deleted = r?.firstOrNull()
|
||||
if (deleted != null) {
|
||||
deletedChatItem = deleted.deletedChatItem.chatItem
|
||||
toChatItem = deleted.toChatItem?.chatItem
|
||||
withChats {
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, deletedChatItem)
|
||||
}
|
||||
withBGApi {
|
||||
val toDeleteItem = reversedChatItems.value.lastOrNull { it.id == itemId }
|
||||
val toModerate = toDeleteItem?.memberToModerate(chatInfo)
|
||||
val groupInfo = toModerate?.first
|
||||
val groupMember = toModerate?.second
|
||||
val deletedChatItem: ChatItem?
|
||||
val toChatItem: ChatItem?
|
||||
val r = if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
|
||||
chatModel.controller.apiDeleteMemberChatItems(
|
||||
chatRh,
|
||||
groupId = groupInfo.groupId,
|
||||
itemIds = listOf(itemId)
|
||||
)
|
||||
} else {
|
||||
chatModel.controller.apiDeleteChatItems(
|
||||
chatRh,
|
||||
type = chatInfo.chatType,
|
||||
id = chatInfo.apiId,
|
||||
itemIds = listOf(itemId),
|
||||
mode = mode
|
||||
)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (deletedChatItem.content.msgContent is MsgContent.MCReport) {
|
||||
val deleted = r?.firstOrNull()
|
||||
if (deleted != null) {
|
||||
deletedChatItem = deleted.deletedChatItem.chatItem
|
||||
toChatItem = deleted.toChatItem?.chatItem
|
||||
withChats {
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, deletedChatItem)
|
||||
}
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
if (deletedChatItem.content.msgContent is MsgContent.MCReport) {
|
||||
if (toChatItem != null) {
|
||||
upsertChatItem(chatRh, chatInfo, toChatItem)
|
||||
} else {
|
||||
removeChatItem(chatRh, chatInfo, deletedChatItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleted
|
||||
},
|
||||
deleteMessages = { itemIds -> deleteMessages(chatRh, chatInfo, itemIds, false, moderate = false) },
|
||||
receiveFile = { fileId ->
|
||||
@@ -688,7 +688,7 @@ fun ChatLayout(
|
||||
showGroupReports: () -> Unit,
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
@@ -1140,7 +1140,7 @@ fun BoxScope.ChatItemsList(
|
||||
showMemberInfo: (GroupInfo, GroupMember) -> Unit,
|
||||
showChatInfo: () -> Unit,
|
||||
loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
|
||||
deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
@@ -2697,7 +2697,7 @@ fun PreviewChatLayout() {
|
||||
showGroupReports = {},
|
||||
showMemberInfo = { _, _ -> },
|
||||
loadMessages = { _, _, _ -> },
|
||||
deleteMessage = { _, _ -> null },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _ -> },
|
||||
cancelFile = {},
|
||||
@@ -2775,7 +2775,7 @@ fun PreviewGroupChatLayout() {
|
||||
showGroupReports = {},
|
||||
showMemberInfo = { _, _ -> },
|
||||
loadMessages = { _, _, _ -> },
|
||||
deleteMessage = { _, _ -> null },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = {},
|
||||
receiveFile = { _ -> },
|
||||
cancelFile = {},
|
||||
|
||||
+12
-10
@@ -779,13 +779,13 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem
|
||||
}
|
||||
}
|
||||
|
||||
fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember, blockMember: () -> Unit = { withBGApi { blockMemberForAll(rhId, gInfo, mem, true) } }) {
|
||||
fun blockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.block_for_all_question),
|
||||
text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.block_for_all),
|
||||
onConfirm = {
|
||||
blockMember()
|
||||
blockMemberForAll(rhId, gInfo, mem, true)
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
@@ -797,18 +797,20 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
|
||||
text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.unblock_for_all),
|
||||
onConfirm = {
|
||||
withBGApi { blockMemberForAll(rhId, gInfo, mem, false) }
|
||||
blockMemberForAll(rhId, gInfo, mem, false)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) {
|
||||
val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) {
|
||||
withBGApi {
|
||||
val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked)
|
||||
withChats {
|
||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
}
|
||||
withReportsChatsIfOpen {
|
||||
upsertGroupMember(rhId, gInfo, updatedMember)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+25
-213
@@ -27,12 +27,9 @@ import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.currentUser
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.*
|
||||
import chat.simplex.common.views.chat.group.blockForAllAlert
|
||||
import chat.simplex.common.views.chat.group.blockMemberForAll
|
||||
import chat.simplex.common.views.chat.group.LocalContentTag
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -78,7 +75,7 @@ fun ChatItemView(
|
||||
selectedChatItems: MutableState<Set<Long>?>,
|
||||
fillMaxWidth: Boolean = true,
|
||||
selectChatItem: () -> Unit,
|
||||
deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
cancelFile: (Long) -> Unit,
|
||||
@@ -114,12 +111,6 @@ fun ChatItemView(
|
||||
val onLinkLongClick = { _: String -> showMenu.value = true }
|
||||
val live = remember { derivedStateOf { composeState.value.liveMessage != null } }.value
|
||||
|
||||
val deleteMessageAsync: (Long, CIDeleteMode) -> Unit = { id, mode ->
|
||||
withBGApi {
|
||||
deleteMessage(id, mode)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier,
|
||||
contentAlignment = alignment,
|
||||
@@ -294,7 +285,7 @@ fun ChatItemView(
|
||||
@Composable
|
||||
fun DeleteItemMenu() {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
@@ -309,31 +300,12 @@ fun ChatItemView(
|
||||
// cItem.id check is a special case for live message chat item which has negative ID while not sent yet
|
||||
cItem.isReport && cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group -> {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
if (cItem.chatDir is CIDirection.GroupSnd) {
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages)
|
||||
} else {
|
||||
ArchiveReportItemAction(cItem, showMenu, deleteMessageAsync)
|
||||
val qItem = cItem.quotedItem
|
||||
if (qItem != null) {
|
||||
ModerateReportItemAction(rhId, cInfo, cItem, qItem, showMenu, deleteMessage)
|
||||
val rMember = qItem.memberToModerate(cInfo)
|
||||
if (rMember != null && !rMember.blockedByAdmin && rMember.canBlockForAll(cInfo.groupInfo)) {
|
||||
BlockMemberAction(
|
||||
rhId,
|
||||
chatInfo = cInfo,
|
||||
groupInfo = cInfo.groupInfo,
|
||||
cItem = cItem,
|
||||
reportedItem = qItem,
|
||||
member = rMember,
|
||||
showMenu = showMenu,
|
||||
deleteMessage = deleteMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
if (cItem.chatDir !is CIDirection.GroupSnd && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
|
||||
ArchiveReportItemAction(cItem, showMenu, deleteMessage)
|
||||
}
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages, buttonText = stringResource(MR.strings.delete_report))
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
}
|
||||
}
|
||||
cItem.content.msgContent != null && cItem.id >= 0 && !cItem.isReport -> {
|
||||
@@ -422,13 +394,13 @@ fun ChatItemView(
|
||||
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
|
||||
}
|
||||
if (!(live && cItem.meta.isLive) && !preview) {
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
}
|
||||
if (cItem.chatDir !is CIDirection.GroupSnd) {
|
||||
val groupInfo = cItem.memberToModerate(cInfo)?.first
|
||||
if (groupInfo != null) {
|
||||
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessageAsync)
|
||||
} else if (cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group && cInfo.groupInfo.membership.memberRole < GroupMemberRole.Moderator && !live) {
|
||||
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessage)
|
||||
} else if (cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group && cInfo.groupInfo.membership.memberRole == GroupMemberRole.Member && !live) {
|
||||
ReportItemAction(cItem, composeState, showMenu)
|
||||
}
|
||||
}
|
||||
@@ -448,7 +420,7 @@ fun ChatItemView(
|
||||
ExpandItemAction(revealed, showMenu, reveal)
|
||||
}
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
@@ -458,7 +430,7 @@ fun ChatItemView(
|
||||
cItem.isDeletedContent -> {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
@@ -472,7 +444,7 @@ fun ChatItemView(
|
||||
} else {
|
||||
ExpandItemAction(revealed, showMenu, reveal)
|
||||
}
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
@@ -481,7 +453,7 @@ fun ChatItemView(
|
||||
}
|
||||
else -> {
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (selectedChatItems.value == null) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
@@ -498,7 +470,7 @@ fun ChatItemView(
|
||||
RevealItemAction(revealed, showMenu, reveal)
|
||||
}
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
@@ -532,7 +504,7 @@ fun ChatItemView(
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
@@ -590,7 +562,7 @@ fun ChatItemView(
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessageAsync, deleteMessages)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
|
||||
if (cItem.canBeDeletedForSelf) {
|
||||
Divider()
|
||||
SelectItemAction(showMenu, selectChatItem)
|
||||
@@ -774,10 +746,11 @@ fun DeleteItemAction(
|
||||
questionText: String,
|
||||
deleteMessage: (Long, CIDeleteMode) -> Unit,
|
||||
deleteMessages: (List<Long>) -> Unit,
|
||||
buttonText: String = stringResource(MR.strings.delete_verb),
|
||||
) {
|
||||
val contentTag = LocalContentTag.current
|
||||
ItemAction(
|
||||
stringResource(MR.strings.delete_verb),
|
||||
buttonText,
|
||||
painterResource(MR.images.ic_delete),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
@@ -825,7 +798,7 @@ fun ModerateItemAction(
|
||||
painterResource(MR.images.ic_flag),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
moderateMessageAlertDialog(cItem.id, questionText, deleteMessage = deleteMessage)
|
||||
moderateMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
@@ -940,120 +913,10 @@ private fun ReportItemAction(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModerateReportItemAction(
|
||||
rhId: Long?,
|
||||
chatInfo: ChatInfo,
|
||||
cItem: ChatItem,
|
||||
reportedItem: CIQuote,
|
||||
showMenu: MutableState<Boolean>,
|
||||
deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.moderate_verb),
|
||||
painterResource(MR.images.ic_flag),
|
||||
onClick = {
|
||||
withBGApi {
|
||||
val reportedMessageId = getLocalIdForReportedMessage(rhId, chatInfo, reportedItem, cItem.id)
|
||||
if (reportedMessageId != null) {
|
||||
moderateMessageAlertDialog(
|
||||
reportedMessageId,
|
||||
questionText = moderateMessageQuestionText(chatInfo.featureEnabled(ChatFeature.FullDelete), 1),
|
||||
deleteMessage = { id, m ->
|
||||
withApi {
|
||||
val deleted = deleteMessage(id, m)
|
||||
if (deleted != null) {
|
||||
deleteMessage(cItem.id, CIDeleteMode.cidmInternalMark)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BlockMemberAction(
|
||||
rhId: Long?,
|
||||
chatInfo: ChatInfo,
|
||||
groupInfo: GroupInfo,
|
||||
cItem: ChatItem,
|
||||
reportedItem: CIQuote,
|
||||
member: GroupMember,
|
||||
showMenu: MutableState<Boolean>,
|
||||
deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?
|
||||
) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.block_member_button),
|
||||
painterResource(MR.images.ic_back_hand),
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertDialogButtonsColumn(
|
||||
title = generalGetString(MR.strings.report_block_and_moderate_title),
|
||||
buttons = {
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withBGApi {
|
||||
val reportedMessageId = getLocalIdForReportedMessage(rhId, chatInfo, reportedItem, cItem.id)
|
||||
if (reportedMessageId != null) {
|
||||
blockAndModerateAlertDialog(
|
||||
rhId,
|
||||
reportedMessageId = reportedMessageId,
|
||||
reportId = cItem.id,
|
||||
gInfo = groupInfo,
|
||||
mem = member,
|
||||
deleteMessage = deleteMessage,
|
||||
)
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.report_block_and_moderate_block_and_moderate_action), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
withBGApi {
|
||||
val reportedMessageId = getLocalIdForReportedMessage(rhId, chatInfo, reportedItem, cItem.id)
|
||||
if (reportedMessageId != null) {
|
||||
blockForAllAlert(rhId, gInfo = groupInfo, mem = member, blockMember = {
|
||||
withBGApi {
|
||||
try {
|
||||
blockMemberForAll(
|
||||
rhId,
|
||||
gInfo = groupInfo,
|
||||
member = member,
|
||||
blocked = true
|
||||
)
|
||||
deleteMessage(reportedMessageId, CIDeleteMode.cidmInternalMark)
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "BlockMemberAction block and moderate ${ex.message}")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.report_block_and_moderate_only_block_action), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
|
||||
}
|
||||
SectionItemView({
|
||||
AlertManager.shared.hideAlert()
|
||||
}) {
|
||||
Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
|
||||
}
|
||||
}
|
||||
)
|
||||
showMenu.value = false
|
||||
},
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveReportItemAction(cItem: ChatItem, showMenu: MutableState<Boolean>, deleteMessage: (Long, CIDeleteMode) -> Unit) {
|
||||
ItemAction(
|
||||
stringResource(MR.strings.archive_verb),
|
||||
stringResource(MR.strings.archive_report),
|
||||
painterResource(MR.images.ic_inventory_2),
|
||||
onClick = {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
@@ -1404,14 +1267,14 @@ fun moderateMessageQuestionText(fullDeleteAllowed: Boolean, count: Int): String
|
||||
}
|
||||
}
|
||||
|
||||
fun moderateMessageAlertDialog(chatItemId: Long, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
|
||||
fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.delete_member_message__question),
|
||||
text = questionText,
|
||||
confirmText = generalGetString(MR.strings.delete_verb),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
deleteMessage(chatItemId, CIDeleteMode.cidmBroadcast)
|
||||
deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1426,59 +1289,8 @@ fun moderateMessagesAlertDialog(itemIds: List<Long>, questionText: String, delet
|
||||
)
|
||||
}
|
||||
|
||||
private fun blockAndModerateAlertDialog(
|
||||
rhId: Long?,
|
||||
reportedMessageId: Long,
|
||||
reportId: Long,
|
||||
gInfo: GroupInfo,
|
||||
mem: GroupMember,
|
||||
deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?
|
||||
) {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.report_block_and_moderate_confirmation_title),
|
||||
text = generalGetString(
|
||||
if (gInfo.fullGroupPreferences.fullDelete.on) MR.strings.report_block_and_moderate_confirmation_desc_full_delete else MR.strings.report_block_and_moderate_confirmation_desc_full_delete).format(mem.chatViewName),
|
||||
confirmText = generalGetString(MR.strings.report_block_and_moderate_confirmation_ok),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
try {
|
||||
val deleted = deleteMessage(reportedMessageId, CIDeleteMode.cidmBroadcast)
|
||||
if (deleted != null) {
|
||||
blockMemberForAll(rhId, gInfo, mem, true)
|
||||
deleteMessage(reportId, CIDeleteMode.cidmInternalMark)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Log.e(TAG, "blockAndModerateAlertDialog block and moderate ${ex.message}")
|
||||
}
|
||||
}
|
||||
},
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
|
||||
expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager)
|
||||
|
||||
private suspend fun getLocalIdForReportedMessage(
|
||||
rhId: Long?,
|
||||
chatInfo: ChatInfo,
|
||||
reportedItem: CIQuote,
|
||||
itemId: Long): Long? {
|
||||
if (reportedItem.itemId != null) {
|
||||
return reportedItem.itemId
|
||||
}
|
||||
val item = apiLoadSingleMessage(rhId, chatInfo.chatType, chatInfo.apiId, itemId, null)
|
||||
|
||||
if (item?.quotedItem?.itemId != null) {
|
||||
withChats {
|
||||
updateChatItem(chatInfo, item)
|
||||
}
|
||||
return item.quotedItem.itemId
|
||||
} else {
|
||||
showQuotedItemDoesNotExistAlert()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatItemView(
|
||||
@@ -1496,7 +1308,7 @@ fun PreviewChatItemView(
|
||||
range = remember { mutableStateOf(0..1) },
|
||||
selectedChatItems = remember { mutableStateOf(setOf()) },
|
||||
selectChatItem = {},
|
||||
deleteMessage = { _, _ -> null },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _ -> },
|
||||
cancelFile = {},
|
||||
@@ -1542,7 +1354,7 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
range = remember { mutableStateOf(0..1) },
|
||||
selectedChatItems = remember { mutableStateOf(setOf()) },
|
||||
selectChatItem = {},
|
||||
deleteMessage = { _, _ -> null },
|
||||
deleteMessage = { _, _ -> },
|
||||
deleteMessages = { _ -> },
|
||||
receiveFile = { _ -> },
|
||||
cancelFile = {},
|
||||
|
||||
@@ -308,13 +308,6 @@
|
||||
<string name="report_reason_alert_title">Report reason?</string>
|
||||
<string name="report_archive_alert_title">Archive report?</string>
|
||||
<string name="report_archive_alert_desc">The report will be archived for you.</string>
|
||||
<string name="report_block_and_moderate_title">Block and moderate?</string>
|
||||
<string name="report_block_and_moderate_block_and_moderate_action">Block and moderate</string>
|
||||
<string name="report_block_and_moderate_only_block_action">Only block</string>
|
||||
<string name="report_block_and_moderate_confirmation_title">Delete member message and block?</string>
|
||||
<string name="report_block_and_moderate_confirmation_desc_full_delete">The message will be deleted for all members.\nAll new messages from %1$s will be hidden!</string>
|
||||
<string name="report_block_and_moderate_confirmation_desc_mark_delete">The message will be marked as moderated for all members.\nAll new messages from %1$s will be hidden!</string>
|
||||
<string name="report_block_and_moderate_confirmation_ok">Delete and block</string>
|
||||
|
||||
<!-- CIStatus errors -->
|
||||
<string name="ci_status_other_error">Error: %1$s</string>
|
||||
@@ -341,6 +334,8 @@
|
||||
<string name="info_menu">Info</string>
|
||||
<string name="search_verb">Search</string>
|
||||
<string name="archive_verb">Archive</string>
|
||||
<string name="archive_report">Archive report</string>
|
||||
<string name="delete_report">Delete report</string>
|
||||
<string name="sent_message">Sent message</string>
|
||||
<string name="received_message">Received message</string>
|
||||
<string name="edit_history">History</string>
|
||||
|
||||
@@ -10,6 +10,8 @@ import Simplex.Chat.Terminal (terminalChatConfig)
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
opts@DirectoryOpts {directoryLog} <- welcomeGetOpts
|
||||
opts@DirectoryOpts {directoryLog, runCLI} <- welcomeGetOpts
|
||||
st <- restoreDirectoryStore directoryLog
|
||||
simplexChatCore terminalChatConfig (mkChatOpts opts) $ directoryService st opts
|
||||
if runCLI
|
||||
then directoryServiceCLI st opts
|
||||
else simplexChatCore terminalChatConfig (mkChatOpts opts) $ directoryService st opts
|
||||
|
||||
@@ -23,6 +23,7 @@ data DirectoryOpts = DirectoryOpts
|
||||
superUsers :: [KnownContact],
|
||||
directoryLog :: Maybe FilePath,
|
||||
serviceName :: T.Text,
|
||||
runCLI :: Bool,
|
||||
searchResults :: Int,
|
||||
testing :: Bool
|
||||
}
|
||||
@@ -58,6 +59,11 @@ directoryOpts appDir defaultDbFileName = do
|
||||
<> help "The display name of the directory service bot, without *'s and spaces (SimpleX-Directory)"
|
||||
<> value "SimpleX-Directory"
|
||||
)
|
||||
runCLI <-
|
||||
switch
|
||||
( long "run-cli"
|
||||
<> help "Run directory service as CLI"
|
||||
)
|
||||
pure
|
||||
DirectoryOpts
|
||||
{ coreOptions,
|
||||
@@ -65,6 +71,7 @@ directoryOpts appDir defaultDbFileName = do
|
||||
superUsers,
|
||||
directoryLog,
|
||||
serviceName = T.pack serviceName,
|
||||
runCLI,
|
||||
searchResults = 10,
|
||||
testing = False
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
module Directory.Service
|
||||
( welcomeGetOpts,
|
||||
directoryService,
|
||||
directoryServiceCLI,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -36,6 +37,8 @@ import Simplex.Chat.Messages
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Chat.Protocol (MsgContent (..))
|
||||
import Simplex.Chat.Store.Shared (StoreError (..))
|
||||
import Simplex.Chat.Terminal (terminalChatConfig)
|
||||
import Simplex.Chat.Terminal.Main (simplexChatCLI')
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Types.Shared
|
||||
import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
|
||||
@@ -77,33 +80,51 @@ welcomeGetOpts = do
|
||||
putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db"
|
||||
pure opts
|
||||
|
||||
directoryServiceCLI :: DirectoryStore -> DirectoryOpts -> IO ()
|
||||
directoryServiceCLI st opts = do
|
||||
env <- newServiceState
|
||||
eventQ <- newTQueueIO
|
||||
let eventHook cc resp = atomically $ resp <$ writeTQueue eventQ (cc, resp)
|
||||
race_
|
||||
(simplexChatCLI' terminalChatConfig {chatHooks = defaultChatHooks {eventHook}} (mkChatOpts opts) Nothing)
|
||||
(processEvents eventQ env)
|
||||
where
|
||||
processEvents eventQ env = forever $ do
|
||||
(cc, resp) <- atomically $ readTQueue eventQ
|
||||
u_ <- readTVarIO (currentUser cc)
|
||||
forM_ u_ $ \user -> directoryServiceEvent st opts env user cc resp
|
||||
|
||||
directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO ()
|
||||
directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchResults, testing} user@User {userId} cc = do
|
||||
directoryService st opts@DirectoryOpts {testing} user cc = do
|
||||
initializeBotAddress' (not testing) cc
|
||||
env <- newServiceState
|
||||
race_ (forever $ void getLine) . forever $ do
|
||||
(_, _, resp) <- atomically . readTBQueue $ outputQ cc
|
||||
forM_ (crDirectoryEvent resp) $ \case
|
||||
DEContactConnected ct -> deContactConnected ct
|
||||
DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole
|
||||
DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner
|
||||
DEGroupUpdated {contactId, fromGroup, toGroup} -> deGroupUpdated contactId fromGroup toGroup
|
||||
DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role
|
||||
DEServiceRoleChanged g role -> deServiceRoleChanged g role
|
||||
DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g
|
||||
DEContactLeftGroup ctId g -> deContactLeftGroup ctId g
|
||||
DEServiceRemovedFromGroup g -> deServiceRemovedFromGroup g
|
||||
DEGroupDeleted _g -> pure ()
|
||||
DEUnsupportedMessage _ct _ciId -> pure ()
|
||||
DEItemEditIgnored _ct -> pure ()
|
||||
DEItemDeleteIgnored _ct -> pure ()
|
||||
DEContactCommand ct ciId (ADC sUser cmd) -> do
|
||||
logInfo $ "command received " <> directoryCmdTag cmd
|
||||
case sUser of
|
||||
SDRUser -> deUserCommand env ct ciId cmd
|
||||
SDRAdmin -> deAdminCommand ct ciId cmd
|
||||
SDRSuperUser -> deSuperUserCommand ct ciId cmd
|
||||
DELogChatResponse r -> logInfo r
|
||||
directoryServiceEvent st opts env user cc resp
|
||||
|
||||
directoryServiceEvent :: DirectoryStore -> DirectoryOpts -> ServiceState -> User -> ChatController -> ChatResponse -> IO ()
|
||||
directoryServiceEvent st DirectoryOpts {adminUsers, superUsers, serviceName, searchResults} ServiceState {searchRequests} user@User {userId} cc event =
|
||||
forM_ (crDirectoryEvent event) $ \case
|
||||
DEContactConnected ct -> deContactConnected ct
|
||||
DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole
|
||||
DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner
|
||||
DEGroupUpdated {contactId, fromGroup, toGroup} -> deGroupUpdated contactId fromGroup toGroup
|
||||
DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role
|
||||
DEServiceRoleChanged g role -> deServiceRoleChanged g role
|
||||
DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g
|
||||
DEContactLeftGroup ctId g -> deContactLeftGroup ctId g
|
||||
DEServiceRemovedFromGroup g -> deServiceRemovedFromGroup g
|
||||
DEGroupDeleted _g -> pure ()
|
||||
DEUnsupportedMessage _ct _ciId -> pure ()
|
||||
DEItemEditIgnored _ct -> pure ()
|
||||
DEItemDeleteIgnored _ct -> pure ()
|
||||
DEContactCommand ct ciId (ADC sUser cmd) -> do
|
||||
logInfo $ "command received " <> directoryCmdTag cmd
|
||||
case sUser of
|
||||
SDRUser -> deUserCommand ct ciId cmd
|
||||
SDRAdmin -> deAdminCommand ct ciId cmd
|
||||
SDRSuperUser -> deSuperUserCommand ct ciId cmd
|
||||
DELogChatResponse r -> logInfo r
|
||||
where
|
||||
withAdminUsers action = void . forkIO $ do
|
||||
forM_ superUsers $ \KnownContact {contactId} -> action contactId
|
||||
@@ -153,7 +174,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
||||
processInvitation :: Contact -> GroupInfo -> IO ()
|
||||
processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = do
|
||||
void $ addGroupReg st ct g GRSProposed
|
||||
r <- sendChatCmd cc $ APIJoinGroup groupId
|
||||
r <- sendChatCmd cc $ APIJoinGroup groupId MFNone
|
||||
sendMessage cc ct $ case r of
|
||||
CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…"
|
||||
_ -> "Error joining group " <> displayName <> ", please re-send the invitation!"
|
||||
@@ -417,8 +438,8 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
||||
notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory."
|
||||
notifyAdminUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)."
|
||||
|
||||
deUserCommand :: ServiceState -> Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO ()
|
||||
deUserCommand env@ServiceState {searchRequests} ct ciId = \case
|
||||
deUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO ()
|
||||
deUserCommand ct ciId = \case
|
||||
DCHelp ->
|
||||
sendMessage cc ct $
|
||||
"You must be the owner to add the group to the directory:\n\
|
||||
@@ -446,7 +467,7 @@ directoryService st DirectoryOpts {adminUsers, superUsers, serviceName, searchRe
|
||||
STRecent -> withFoundListedGroups Nothing $ sendNextSearchResults takeRecent search
|
||||
Nothing -> showAllGroups
|
||||
where
|
||||
showAllGroups = deUserCommand env ct ciId DCAllGroups
|
||||
showAllGroups = deUserCommand ct ciId DCAllGroups
|
||||
DCAllGroups -> withFoundListedGroups Nothing $ sendAllGroups takeTop "top" STAll
|
||||
DCRecentGroups -> withFoundListedGroups Nothing $ sendAllGroups takeRecent "the most recent" STRecent
|
||||
DCSubmitGroup _link -> pure ()
|
||||
|
||||
@@ -349,7 +349,7 @@ data ChatCommand
|
||||
| APIGetNtfConns {nonce :: C.CbNonce, encNtfInfo :: ByteString}
|
||||
| ApiGetConnNtfMessages {connIds :: NonEmpty AgentConnId}
|
||||
| APIAddMember GroupId ContactId GroupMemberRole
|
||||
| APIJoinGroup GroupId
|
||||
| APIJoinGroup {groupId :: GroupId, enableNtfs :: MsgFilter}
|
||||
| APIMemberRole GroupId GroupMemberId GroupMemberRole
|
||||
| APIBlockMemberForAll GroupId GroupMemberId Bool
|
||||
| APIRemoveMember GroupId GroupMemberId
|
||||
@@ -467,7 +467,7 @@ data ChatCommand
|
||||
| APINewGroup UserId IncognitoEnabled GroupProfile
|
||||
| NewGroup IncognitoEnabled GroupProfile
|
||||
| AddMember GroupName ContactName GroupMemberRole
|
||||
| JoinGroup GroupName
|
||||
| JoinGroup {groupName :: GroupName, enableNtfs :: MsgFilter}
|
||||
| MemberRole GroupName ContactName GroupMemberRole
|
||||
| BlockForAll GroupName ContactName Bool
|
||||
| RemoveMember GroupName ContactName
|
||||
|
||||
@@ -1924,12 +1924,12 @@ processChatCommand' vr = \case
|
||||
pure $ CRSentGroupInvitation user gInfo contact member {memberRole = memRole}
|
||||
Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName
|
||||
| otherwise -> throwChatError $ CEGroupDuplicateMember cName
|
||||
APIJoinGroup groupId -> withUser $ \user@User {userId} -> do
|
||||
APIJoinGroup groupId enableNtfs -> withUser $ \user@User {userId} -> do
|
||||
withGroupLock "joinGroup" groupId . procCmd $ do
|
||||
(invitation, ct) <- withFastStore $ \db -> do
|
||||
inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db vr user groupId
|
||||
(inv,) <$> getContactViaMember db vr user fromMember
|
||||
let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation
|
||||
let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership, chatSettings}} = invitation
|
||||
GroupMember {memberId = membershipMemId} = membership
|
||||
Contact {activeConn} = ct
|
||||
case activeConn of
|
||||
@@ -1946,7 +1946,9 @@ processChatCommand' vr = \case
|
||||
withFastStore' $ \db -> do
|
||||
updateGroupMemberStatus db userId fromMember GSMemAccepted
|
||||
updateGroupMemberStatus db userId membership GSMemAccepted
|
||||
void (withAgent $ \a -> joinConnection a (aUserId user) agentConnId True connRequest dm PQSupportOff subMode)
|
||||
-- MFAll is default for new groups
|
||||
unless (enableNtfs == MFAll) $ updateGroupSettings db user groupId chatSettings {enableNtfs}
|
||||
void (withAgent $ \a -> joinConnection a (aUserId user) agentConnId (enableNtfs /= MFNone) connRequest dm PQSupportOff subMode)
|
||||
`catchChatError` \e -> do
|
||||
withFastStore' $ \db -> do
|
||||
updateGroupMemberStatus db userId fromMember GSMemInvited
|
||||
@@ -2043,9 +2045,9 @@ processChatCommand' vr = \case
|
||||
AddMember gName cName memRole -> withUser $ \user -> do
|
||||
(groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName
|
||||
processChatCommand $ APIAddMember groupId contactId memRole
|
||||
JoinGroup gName -> withUser $ \user -> do
|
||||
JoinGroup gName enableNtfs -> withUser $ \user -> do
|
||||
groupId <- withFastStore $ \db -> getGroupIdByName db user gName
|
||||
processChatCommand $ APIJoinGroup groupId
|
||||
processChatCommand $ APIJoinGroup groupId enableNtfs
|
||||
MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMemberRole gId gMemberId memRole
|
||||
BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMemberForAll gId gMemberId blocked
|
||||
RemoveMember gName gMemberName -> withMemberName gName gMemberName APIRemoveMember
|
||||
@@ -3630,7 +3632,7 @@ chatCommandP =
|
||||
"/_ntf conns " *> (APIGetNtfConns <$> strP <* A.space <*> strP),
|
||||
"/_ntf conn messages " *> (ApiGetConnNtfMessages <$> strP),
|
||||
"/_add #" *> (APIAddMember <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
|
||||
"/_join #" *> (APIJoinGroup <$> A.decimal),
|
||||
"/_join #" *> (APIJoinGroup <$> A.decimal <*> pure MFAll), -- needs to be changed to support in UI
|
||||
"/_member role #" *> (APIMemberRole <$> A.decimal <* A.space <*> A.decimal <*> memberRole),
|
||||
"/_block #" *> (APIBlockMemberForAll <$> A.decimal <* A.space <*> A.decimal <* A.space <* "blocked=" <*> onOffP),
|
||||
"/_remove #" *> (APIRemoveMember <$> A.decimal <* A.space <*> A.decimal),
|
||||
@@ -3712,7 +3714,7 @@ chatCommandP =
|
||||
("/group" <|> "/g") *> (NewGroup <$> incognitoP <* A.space <* char_ '#' <*> groupProfile),
|
||||
"/_group " *> (APINewGroup <$> A.decimal <*> incognitoOnOffP <* A.space <*> jsonP),
|
||||
("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRMember)),
|
||||
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName),
|
||||
("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName <*> (" mute" $> MFNone <|> pure MFAll)),
|
||||
("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole),
|
||||
"/block for all #" *> (BlockForAll <$> displayName <* A.space <*> (char_ '@' *> displayName) <*> pure True),
|
||||
"/unblock for all #" *> (BlockForAll <$> displayName <* A.space <*> (char_ '@' *> displayName) <*> pure False),
|
||||
|
||||
@@ -72,6 +72,7 @@ mkDirectoryOpts tmp superUsers =
|
||||
superUsers,
|
||||
directoryLog = Just $ tmp </> "directory_service.log",
|
||||
serviceName = "SimpleX-Directory",
|
||||
runCLI = False,
|
||||
searchResults = 3,
|
||||
testing = True
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user