Merge branch 'stable'

This commit is contained in:
Evgeny Poberezkin 2025-01-09 22:29:31 +00:00
commit 13fae855fc
No known key found for this signature in database
GPG key ID: 494BDDD9A28B577D
9 changed files with 106 additions and 494 deletions

View file

@ -1284,20 +1284,11 @@ struct ChatView: View {
@ViewBuilder @ViewBuilder
private func menu(_ ci: ChatItem, _ range: ClosedRange<Int>?, live: Bool) -> some View { 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 case let .group(gInfo) = chat.chatInfo, ci.isReport, ci.meta.itemDeleted == nil {
if ci.chatDir == .groupSnd { if ci.chatDir != .groupSnd, gInfo.membership.memberRole >= .moderator {
deleteButton(ci)
} else {
archiveReportButton(ci) 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 { } else if let mc = ci.content.msgContent, !ci.isReport, ci.meta.itemDeleted == nil || revealed {
if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction, if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction,
availableReactions.count > 0 { availableReactions.count > 0 {
@ -1351,7 +1342,7 @@ struct ChatView: View {
if ci.chatDir != .groupSnd { if ci.chatDir != .groupSnd {
if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) { if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) {
moderateButton(ci, groupInfo) 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) 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) { Button(role: .destructive) {
if !revealed, if !revealed,
let currIndex = m.getChatItemIndex(ci), let currIndex = m.getChatItemIndex(ci),
@ -1649,10 +1640,7 @@ struct ChatView: View {
deletingItem = ci deletingItem = ci
} }
} label: { } label: {
Label( Label(label, systemImage: "trash")
NSLocalizedString("Delete", comment: "chat item action"),
systemImage: "trash"
)
} }
} }
@ -1668,31 +1656,19 @@ struct ChatView: View {
private func moderateButton(_ ci: ChatItem, _ groupInfo: GroupInfo) -> Button<some View> { private func moderateButton(_ ci: ChatItem, _ groupInfo: GroupInfo) -> Button<some View> {
Button(role: .destructive) { Button(role: .destructive) {
showModerateMessageAlert(groupInfo) { AlertManager.shared.showAlert(Alert(
deletingItem = ci title: Text("Delete member message?"),
deleteMessage(.cidmBroadcast, moderate: true) message: Text(
} groupInfo.fullGroupPreferences.fullDelete.on
} label: { ? "The message will be deleted for all members."
Label( : "The message will be marked as moderated for all members."
NSLocalizedString("Moderate", comment: "chat item action"), ),
systemImage: "flag" primaryButton: .destructive(Text("Delete")) {
) deletingItem = ci
} deleteMessage(.cidmBroadcast, moderate: true)
} },
secondaryButton: .cancel()
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)
}
}
}
}
} label: { } label: {
Label( Label(
NSLocalizedString("Moderate", comment: "chat item action"), NSLocalizedString("Moderate", comment: "chat item action"),
@ -1715,74 +1691,7 @@ struct ChatView: View {
) )
) )
} label: { } label: {
Label( Label("Archive report", systemImage: "archivebox")
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"
)
} }
} }
@ -1886,60 +1795,6 @@ struct ChatView: View {
itemIds.forEach { selectedChatItems?.remove($0) } 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) { private func deleteMessage(_ mode: CIDeleteMode, moderate: Bool) {
logger.debug("ChatView deleteMessage") 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 { private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey {
chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone"
} }

View file

@ -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( Alert(
title: Text("Block member for all?"), title: Text("Block member for all?"),
message: Text("All new messages from \(mem.chatViewName) will be hidden!"), message: Text("All new messages from \(mem.chatViewName) will be hidden!"),
primaryButton: .destructive(Text("Block for all")) { primaryButton: .destructive(Text("Block for all")) {
Task { blockMemberForAll(gInfo, mem, true)
let uMember = await blockMemberForAll(gInfo, mem, true)
if uMember != nil {
onBlocked?()
}
}
}, },
secondaryButton: .cancel() secondaryButton: .cancel()
) )
@ -803,25 +797,23 @@ func unblockForAllAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert {
title: Text("Unblock member for all?"), title: Text("Unblock member for all?"),
message: Text("Messages from \(mem.chatViewName) will be shown!"), message: Text("Messages from \(mem.chatViewName) will be shown!"),
primaryButton: .default(Text("Unblock for all")) { primaryButton: .default(Text("Unblock for all")) {
Task { blockMemberForAll(gInfo, mem, false)
await blockMemberForAll(gInfo, mem, false)
}
}, },
secondaryButton: .cancel() secondaryButton: .cancel()
) )
} }
func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) async -> GroupMember? { func blockMemberForAll(_ gInfo: GroupInfo, _ member: GroupMember, _ blocked: Bool) {
do { Task {
let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked) do {
await MainActor.run { let updatedMember = try await apiBlockMemberForAll(gInfo.groupId, member.groupMemberId, blocked)
_ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember) 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 { struct GroupMemberInfoView_Previews: PreviewProvider {

View file

@ -277,7 +277,7 @@ struct ChatPreviewView: View {
func prefix() -> Text { func prefix() -> Text {
switch cItem.content.msgContent { 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("") default: return Text("")
} }
} }

View file

@ -2105,7 +2105,7 @@ public struct GroupMember: Identifiable, Decodable, Hashable {
public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? { public func canChangeRoleTo(groupInfo: GroupInfo) -> [GroupMemberRole]? {
if !canBeRemoved(groupInfo: groupInfo) { return nil } if !canBeRemoved(groupInfo: groupInfo) { return nil }
let userRole = groupInfo.membership.memberRole 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 { 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) 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 { public struct CIReactionCount: Decodable, Hashable {

View file

@ -3140,19 +3140,6 @@ class CIQuote (
null -> null 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 { companion object {
fun getSample(itemId: Long?, sentAt: Instant, text: String, chatDir: CIDirection?): CIQuote = fun getSample(itemId: Long?, sentAt: Instant, text: String, chatDir: CIDirection?): CIQuote =
CIQuote(chatDir = chatDir, itemId = itemId, sentAt = sentAt, content = MsgContent.MCText(text)) CIQuote(chatDir = chatDir, itemId = itemId, sentAt = sentAt, content = MsgContent.MCText(text))

View file

@ -309,41 +309,41 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
} }
}, },
deleteMessage = { itemId, mode -> deleteMessage = { itemId, mode ->
val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } withBGApi {
val toModerate = toDeleteItem?.memberToModerate(chatInfo) val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId }
val groupInfo = toModerate?.first val toModerate = toDeleteItem?.memberToModerate(chatInfo)
val groupMember = toModerate?.second val groupInfo = toModerate?.first
val deletedChatItem: ChatItem? val groupMember = toModerate?.second
val toChatItem: ChatItem? val deletedChatItem: ChatItem?
val r = if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { val toChatItem: ChatItem?
chatModel.controller.apiDeleteMemberChatItems( val r = if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) {
chatRh, chatModel.controller.apiDeleteMemberChatItems(
groupId = groupInfo.groupId, chatRh,
itemIds = listOf(itemId) groupId = groupInfo.groupId,
) itemIds = listOf(itemId)
} else { )
chatModel.controller.apiDeleteChatItems( } else {
chatRh, chatModel.controller.apiDeleteChatItems(
type = chatInfo.chatType, chatRh,
id = chatInfo.apiId, type = chatInfo.chatType,
itemIds = listOf(itemId), id = chatInfo.apiId,
mode = mode itemIds = listOf(itemId),
) mode = mode
} )
val deleted = r?.firstOrNull() }
if (deleted != null) { val deleted = r?.firstOrNull()
deletedChatItem = deleted.deletedChatItem.chatItem if (deleted != null) {
toChatItem = deleted.toChatItem?.chatItem deletedChatItem = deleted.deletedChatItem.chatItem
withChats { toChatItem = deleted.toChatItem?.chatItem
if (toChatItem != null) { withChats {
upsertChatItem(chatRh, chatInfo, toChatItem) if (toChatItem != null) {
} else { upsertChatItem(chatRh, chatInfo, toChatItem)
removeChatItem(chatRh, chatInfo, deletedChatItem) } else {
removeChatItem(chatRh, chatInfo, deletedChatItem)
}
} }
} }
} }
deleted
}, },
deleteMessages = { itemIds -> deleteMessages(chatRh, chatInfo, itemIds, false, moderate = false) }, deleteMessages = { itemIds -> deleteMessages(chatRh, chatInfo, itemIds, false, moderate = false) },
receiveFile = { fileId -> receiveFile = { fileId ->
@ -613,7 +613,7 @@ fun ChatLayout(
info: () -> Unit, info: () -> Unit,
showMemberInfo: (GroupInfo, GroupMember) -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit,
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?, deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit, deleteMessages: (List<Long>) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit, cancelFile: (Long) -> Unit,
@ -960,7 +960,7 @@ fun BoxScope.ChatItemsList(
showMemberInfo: (GroupInfo, GroupMember) -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit,
showChatInfo: () -> Unit, showChatInfo: () -> Unit,
loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit,
deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?, deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit, deleteMessages: (List<Long>) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit, cancelFile: (Long) -> Unit,
@ -2453,7 +2453,7 @@ fun PreviewChatLayout() {
info = {}, info = {},
showMemberInfo = { _, _ -> }, showMemberInfo = { _, _ -> },
loadMessages = { _, _, _, _ -> }, loadMessages = { _, _, _, _ -> },
deleteMessage = { _, _ -> null }, deleteMessage = { _, _ -> },
deleteMessages = { _ -> }, deleteMessages = { _ -> },
receiveFile = { _ -> }, receiveFile = { _ -> },
cancelFile = {}, cancelFile = {},
@ -2526,7 +2526,7 @@ fun PreviewGroupChatLayout() {
info = {}, info = {},
showMemberInfo = { _, _ -> }, showMemberInfo = { _, _ -> },
loadMessages = { _, _, _, _ -> }, loadMessages = { _, _, _, _ -> },
deleteMessage = { _, _ -> null }, deleteMessage = { _, _ -> },
deleteMessages = {}, deleteMessages = {},
receiveFile = { _ -> }, receiveFile = { _ -> },
cancelFile = {}, cancelFile = {},

View file

@ -757,13 +757,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( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.block_for_all_question), title = generalGetString(MR.strings.block_for_all_question),
text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName), text = generalGetString(MR.strings.block_member_desc).format(mem.chatViewName),
confirmText = generalGetString(MR.strings.block_for_all), confirmText = generalGetString(MR.strings.block_for_all),
onConfirm = { onConfirm = {
blockMember() blockMemberForAll(rhId, gInfo, mem, true)
}, },
destructive = true, destructive = true,
) )
@ -775,15 +775,17 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) {
text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName), text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName),
confirmText = generalGetString(MR.strings.unblock_for_all), confirmText = generalGetString(MR.strings.unblock_for_all),
onConfirm = { onConfirm = {
withBGApi { blockMemberForAll(rhId, gInfo, mem, false) } blockMemberForAll(rhId, gInfo, mem, false)
}, },
) )
} }
suspend fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) { fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) {
val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked) withBGApi {
withChats { val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked)
upsertGroupMember(rhId, gInfo, updatedMember) withChats {
upsertGroupMember(rhId, gInfo, updatedMember)
}
} }
} }

View file

@ -27,12 +27,9 @@ import androidx.compose.ui.unit.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.currentUser import chat.simplex.common.model.ChatModel.currentUser
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.* 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.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@ -77,7 +74,7 @@ fun ChatItemView(
selectedChatItems: MutableState<Set<Long>?>, selectedChatItems: MutableState<Set<Long>?>,
fillMaxWidth: Boolean = true, fillMaxWidth: Boolean = true,
selectChatItem: () -> Unit, selectChatItem: () -> Unit,
deleteMessage: suspend (Long, CIDeleteMode) -> ChatItemDeletion?, deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit, deleteMessages: (List<Long>) -> Unit,
receiveFile: (Long) -> Unit, receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit, cancelFile: (Long) -> Unit,
@ -113,12 +110,6 @@ fun ChatItemView(
val onLinkLongClick = { _: String -> showMenu.value = true } val onLinkLongClick = { _: String -> showMenu.value = true }
val live = remember { derivedStateOf { composeState.value.liveMessage != null } }.value val live = remember { derivedStateOf { composeState.value.liveMessage != null } }.value
val deleteMessageAsync: (Long, CIDeleteMode) -> Unit = { id, mode ->
withBGApi {
deleteMessage(id, mode)
}
}
Box( Box(
modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier, modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier,
contentAlignment = alignment, contentAlignment = alignment,
@ -293,7 +284,7 @@ fun ChatItemView(
@Composable @Composable
fun DeleteItemMenu() { fun DeleteItemMenu() {
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) { if (cItem.canBeDeletedForSelf) {
Divider() Divider()
SelectItemAction(showMenu, selectChatItem) SelectItemAction(showMenu, selectChatItem)
@ -308,31 +299,12 @@ fun ChatItemView(
// cItem.id check is a special case for live message chat item which has negative ID while not sent yet // 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 -> { cItem.isReport && cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group -> {
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
if (cItem.chatDir is CIDirection.GroupSnd) { if (cItem.chatDir !is CIDirection.GroupSnd && cInfo.groupInfo.membership.memberRole >= GroupMemberRole.Moderator) {
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) ArchiveReportItemAction(cItem, showMenu, deleteMessage)
} 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)
} }
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 -> { cItem.content.msgContent != null && cItem.id >= 0 && !cItem.isReport -> {
@ -421,13 +393,13 @@ fun ChatItemView(
CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction)
} }
if (!(live && cItem.meta.isLive) && !preview) { 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) { if (cItem.chatDir !is CIDirection.GroupSnd) {
val groupInfo = cItem.memberToModerate(cInfo)?.first val groupInfo = cItem.memberToModerate(cInfo)?.first
if (groupInfo != null) { if (groupInfo != null) {
ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessageAsync) 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.Moderator && !live) { } else if (cItem.meta.itemDeleted == null && cInfo is ChatInfo.Group && cInfo.groupInfo.membership.memberRole == GroupMemberRole.Member && !live) {
ReportItemAction(cItem, composeState, showMenu) ReportItemAction(cItem, composeState, showMenu)
} }
} }
@ -447,7 +419,7 @@ fun ChatItemView(
ExpandItemAction(revealed, showMenu, reveal) ExpandItemAction(revealed, showMenu, reveal)
} }
ItemInfoAction(cInfo, cItem, showItemDetails, 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) { if (cItem.canBeDeletedForSelf) {
Divider() Divider()
SelectItemAction(showMenu, selectChatItem) SelectItemAction(showMenu, selectChatItem)
@ -457,7 +429,7 @@ fun ChatItemView(
cItem.isDeletedContent -> { cItem.isDeletedContent -> {
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, 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) { if (cItem.canBeDeletedForSelf) {
Divider() Divider()
SelectItemAction(showMenu, selectChatItem) SelectItemAction(showMenu, selectChatItem)
@ -471,7 +443,7 @@ fun ChatItemView(
} else { } else {
ExpandItemAction(revealed, showMenu, reveal) ExpandItemAction(revealed, showMenu, reveal)
} }
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (cItem.canBeDeletedForSelf) { if (cItem.canBeDeletedForSelf) {
Divider() Divider()
SelectItemAction(showMenu, selectChatItem) SelectItemAction(showMenu, selectChatItem)
@ -480,7 +452,7 @@ fun ChatItemView(
} }
else -> { else -> {
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessageAsync, deleteMessages) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
if (selectedChatItems.value == null) { if (selectedChatItems.value == null) {
Divider() Divider()
SelectItemAction(showMenu, selectChatItem) SelectItemAction(showMenu, selectChatItem)
@ -497,7 +469,7 @@ fun ChatItemView(
RevealItemAction(revealed, showMenu, reveal) RevealItemAction(revealed, showMenu, reveal)
} }
ItemInfoAction(cInfo, cItem, showItemDetails, 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) { if (cItem.canBeDeletedForSelf) {
Divider() Divider()
SelectItemAction(showMenu, selectChatItem) SelectItemAction(showMenu, selectChatItem)
@ -531,7 +503,7 @@ fun ChatItemView(
DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp) DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, 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) { if (cItem.canBeDeletedForSelf) {
Divider() Divider()
SelectItemAction(showMenu, selectChatItem) SelectItemAction(showMenu, selectChatItem)
@ -588,7 +560,7 @@ fun ChatItemView(
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp) MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
DefaultDropdownMenu(showMenu) { DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, 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) { if (cItem.canBeDeletedForSelf) {
Divider() Divider()
SelectItemAction(showMenu, selectChatItem) SelectItemAction(showMenu, selectChatItem)
@ -772,9 +744,10 @@ fun DeleteItemAction(
questionText: String, questionText: String,
deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit,
deleteMessages: (List<Long>) -> Unit, deleteMessages: (List<Long>) -> Unit,
buttonText: String = stringResource(MR.strings.delete_verb),
) { ) {
ItemAction( ItemAction(
stringResource(MR.strings.delete_verb), buttonText,
painterResource(MR.images.ic_delete), painterResource(MR.images.ic_delete),
onClick = { onClick = {
showMenu.value = false showMenu.value = false
@ -822,7 +795,7 @@ fun ModerateItemAction(
painterResource(MR.images.ic_flag), painterResource(MR.images.ic_flag),
onClick = { onClick = {
showMenu.value = false showMenu.value = false
moderateMessageAlertDialog(cItem.id, questionText, deleteMessage = deleteMessage) moderateMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage)
}, },
color = Color.Red color = Color.Red
) )
@ -937,120 +910,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 @Composable
private fun ArchiveReportItemAction(cItem: ChatItem, showMenu: MutableState<Boolean>, deleteMessage: (Long, CIDeleteMode) -> Unit) { private fun ArchiveReportItemAction(cItem: ChatItem, showMenu: MutableState<Boolean>, deleteMessage: (Long, CIDeleteMode) -> Unit) {
ItemAction( ItemAction(
stringResource(MR.strings.archive_verb), stringResource(MR.strings.archive_report),
painterResource(MR.images.ic_inventory_2), painterResource(MR.images.ic_inventory_2),
onClick = { onClick = {
AlertManager.shared.showAlertDialog( AlertManager.shared.showAlertDialog(
@ -1401,14 +1264,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( AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_member_message__question), title = generalGetString(MR.strings.delete_member_message__question),
text = questionText, text = questionText,
confirmText = generalGetString(MR.strings.delete_verb), confirmText = generalGetString(MR.strings.delete_verb),
destructive = true, destructive = true,
onConfirm = { onConfirm = {
deleteMessage(chatItemId, CIDeleteMode.cidmBroadcast) deleteMessage(chatItem.id, CIDeleteMode.cidmBroadcast)
} }
) )
} }
@ -1423,59 +1286,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) 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)
if (item?.quotedItem?.itemId != null) {
withChats {
updateChatItem(chatInfo, item)
}
return item.quotedItem.itemId
} else {
showQuotedItemDoesNotExistAlert()
return null
}
}
@Preview @Preview
@Composable @Composable
fun PreviewChatItemView( fun PreviewChatItemView(
@ -1493,7 +1305,7 @@ fun PreviewChatItemView(
range = remember { mutableStateOf(0..1) }, range = remember { mutableStateOf(0..1) },
selectedChatItems = remember { mutableStateOf(setOf()) }, selectedChatItems = remember { mutableStateOf(setOf()) },
selectChatItem = {}, selectChatItem = {},
deleteMessage = { _, _ -> null }, deleteMessage = { _, _ -> },
deleteMessages = { _ -> }, deleteMessages = { _ -> },
receiveFile = { _ -> }, receiveFile = { _ -> },
cancelFile = {}, cancelFile = {},
@ -1539,7 +1351,7 @@ fun PreviewChatItemViewDeletedContent() {
range = remember { mutableStateOf(0..1) }, range = remember { mutableStateOf(0..1) },
selectedChatItems = remember { mutableStateOf(setOf()) }, selectedChatItems = remember { mutableStateOf(setOf()) },
selectChatItem = {}, selectChatItem = {},
deleteMessage = { _, _ -> null }, deleteMessage = { _, _ -> },
deleteMessages = { _ -> }, deleteMessages = { _ -> },
receiveFile = { _ -> }, receiveFile = { _ -> },
cancelFile = {}, cancelFile = {},

View file

@ -308,13 +308,6 @@
<string name="report_reason_alert_title">Report reason?</string> <string name="report_reason_alert_title">Report reason?</string>
<string name="report_archive_alert_title">Archive report?</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_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 --> <!-- CIStatus errors -->
<string name="ci_status_other_error">Error: %1$s</string> <string name="ci_status_other_error">Error: %1$s</string>
@ -341,6 +334,8 @@
<string name="info_menu">Info</string> <string name="info_menu">Info</string>
<string name="search_verb">Search</string> <string name="search_verb">Search</string>
<string name="archive_verb">Archive</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="sent_message">Sent message</string>
<string name="received_message">Received message</string> <string name="received_message">Received message</string>
<string name="edit_history">History</string> <string name="edit_history">History</string>