core, ui: allow to delete member support chat; rename reject action (#5927)

This commit is contained in:
spaced4ndy
2025-05-20 09:07:44 +00:00
committed by GitHub
parent ca9f6267e4
commit 1a80ecfc29
15 changed files with 212 additions and 48 deletions
+6
View File
@@ -69,6 +69,7 @@ enum ChatCommand: ChatCmdProtocol {
case apiAddMember(groupId: Int64, contactId: Int64, memberRole: GroupMemberRole)
case apiJoinGroup(groupId: Int64)
case apiAcceptMember(groupId: Int64, groupMemberId: Int64, memberRole: GroupMemberRole)
case apiDeleteMemberSupportChat(groupId: Int64, groupMemberId: Int64)
case apiMembersRole(groupId: Int64, memberIds: [Int64], memberRole: GroupMemberRole)
case apiBlockMembersForAll(groupId: Int64, memberIds: [Int64], blocked: Bool)
case apiRemoveMembers(groupId: Int64, memberIds: [Int64], withMessages: Bool)
@@ -250,6 +251,7 @@ enum ChatCommand: ChatCmdProtocol {
case let .apiAddMember(groupId, contactId, memberRole): return "/_add #\(groupId) \(contactId) \(memberRole)"
case let .apiJoinGroup(groupId): return "/_join #\(groupId)"
case let .apiAcceptMember(groupId, groupMemberId, memberRole): return "/_accept member #\(groupId) \(groupMemberId) \(memberRole.rawValue)"
case let .apiDeleteMemberSupportChat(groupId, groupMemberId): return "/_delete member chat #\(groupId) \(groupMemberId)"
case let .apiMembersRole(groupId, memberIds, memberRole): return "/_member role #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) \(memberRole.rawValue)"
case let .apiBlockMembersForAll(groupId, memberIds, blocked): return "/_block #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) blocked=\(onOff(blocked))"
case let .apiRemoveMembers(groupId, memberIds, withMessages): return "/_remove #\(groupId) \(memberIds.map({ "\($0)" }).joined(separator: ",")) messages=\(onOff(withMessages))"
@@ -425,6 +427,7 @@ enum ChatCommand: ChatCmdProtocol {
case .apiAddMember: return "apiAddMember"
case .apiJoinGroup: return "apiJoinGroup"
case .apiAcceptMember: return "apiAcceptMember"
case .apiDeleteMemberSupportChat: return "apiDeleteMemberSupportChat"
case .apiMembersRole: return "apiMembersRole"
case .apiBlockMembersForAll: return "apiBlockMembersForAll"
case .apiRemoveMembers: return "apiRemoveMembers"
@@ -851,6 +854,7 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case leftMemberUser(user: UserRef, groupInfo: GroupInfo)
case groupMembers(user: UserRef, group: SimpleXChat.Group)
case memberAccepted(user: UserRef, groupInfo: GroupInfo, member: GroupMember)
case memberSupportChatDeleted(user: UserRef, groupInfo: GroupInfo, member: GroupMember)
case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole)
case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool)
case groupUpdated(user: UserRef, toGroup: GroupInfo)
@@ -900,6 +904,7 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case .leftMemberUser: "leftMemberUser"
case .groupMembers: "groupMembers"
case .memberAccepted: "memberAccepted"
case .memberSupportChatDeleted: "memberSupportChatDeleted"
case .membersRoleUser: "membersRoleUser"
case .membersBlockedForAllUser: "membersBlockedForAllUser"
case .groupUpdated: "groupUpdated"
@@ -945,6 +950,7 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case let .leftMemberUser(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .groupMembers(u, group): return withUser(u, String(describing: group))
case let .memberAccepted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
case let .memberSupportChatDeleted(u, groupInfo, member): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)")
case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)")
case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)")
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
+6
View File
@@ -1646,6 +1646,12 @@ func apiAcceptMember(_ groupId: Int64, _ groupMemberId: Int64, _ memberRole: Gro
throw r.unexpected
}
func apiDeleteMemberSupportChat(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (GroupInfo, GroupMember) {
let r: ChatResponse2 = try await chatSendCmd(.apiDeleteMemberSupportChat(groupId: groupId, groupMemberId: groupMemberId))
if case let .memberSupportChatDeleted(_, groupInfo, member) = r { return (groupInfo, member) }
throw r.unexpected
}
func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> (GroupInfo, [GroupMember]) {
let r: ChatResponse2 = try await chatSendCmd(.apiRemoveMembers(groupId: groupId, memberIds: memberIds, withMessages: withMessages), bgTask: false)
if case let .userDeletedMembers(_, updatedGroupInfo, members, _withMessages) = r { return (updatedGroupInfo, members) }
@@ -18,13 +18,13 @@ struct ContextPendingMemberActionsView: View {
var body: some View {
HStack(spacing: 0) {
ZStack {
Text("Remove")
Text("Reject")
.foregroundColor(.red)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.onTapGesture {
showRemoveMemberAlert(groupInfo, member, dismiss: dismiss)
showRejectMemberAlert(groupInfo, member, dismiss: dismiss)
}
ZStack {
@@ -43,6 +43,15 @@ struct ContextPendingMemberActionsView: View {
}
}
func showRejectMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismiss: DismissAction? = nil) {
showAlert(
title: NSLocalizedString("Reject member?", comment: "alert title"),
buttonTitle: "Reject",
buttonAction: { removeMember(groupInfo, member, dismiss: dismiss) },
cancelButton: true
)
}
func showAcceptMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismiss: DismissAction? = nil) {
showAlert(
NSLocalizedString("Accept member", comment: "alert title"),
@@ -75,7 +84,7 @@ func acceptMember(_ groupInfo: GroupInfo, _ member: GroupMember, _ role: GroupMe
do {
let (gInfo, acceptedMember) = try await apiAcceptMember(groupInfo.groupId, member.groupMemberId, role)
await MainActor.run {
_ = ChatModel.shared.upsertGroupMember(groupInfo, acceptedMember)
_ = ChatModel.shared.upsertGroupMember(gInfo, acceptedMember)
ChatModel.shared.updateGroup(gInfo)
dismiss?()
}
@@ -100,14 +100,14 @@ struct MemberSupportView: View {
Label("Accept", systemImage: "checkmark")
}
.tint(theme.colors.primary)
} else {
Button {
showDeleteMemberSupportChatAlert(groupInfo, memberWithChat.wrapped)
} label: {
Label("Delete", systemImage: "trash")
}
.tint(.red)
}
Button {
showRemoveMemberAlert(groupInfo, memberWithChat.wrapped)
} label: {
Label("Remove", systemImage: "trash")
}
.tint(.red)
}
}
}
@@ -248,15 +248,37 @@ struct MemberSupportView: View {
}
}
func showRemoveMemberAlert(_ groupInfo: GroupInfo, _ member: GroupMember, dismiss: DismissAction? = nil) {
func showDeleteMemberSupportChatAlert(_ groupInfo: GroupInfo, _ member: GroupMember) {
showAlert(
title: NSLocalizedString("Remove member?", comment: "alert title"),
buttonTitle: "Remove",
buttonAction: { removeMember(groupInfo, member, dismiss: dismiss) },
title: NSLocalizedString("Delete chat with member?", comment: "alert title"),
buttonTitle: "Delete",
buttonAction: { deleteMemberSupportChat(groupInfo, member) },
cancelButton: true
)
}
func deleteMemberSupportChat(_ groupInfo: GroupInfo, _ member: GroupMember) {
Task {
do {
let (gInfo, updatedMember) = try await apiDeleteMemberSupportChat(groupInfo.groupId, member.groupMemberId)
await MainActor.run {
_ = ChatModel.shared.upsertGroupMember(gInfo, updatedMember)
ChatModel.shared.updateGroup(gInfo)
}
// TODO member row doesn't get removed from list (upsertGroupMember correctly sets supportChat to nil) - this repopulates list to fix it
await ChatModel.shared.loadGroupMembers(gInfo)
} catch let error {
logger.error("apiDeleteMemberSupportChat error: \(responseError(error))")
await MainActor.run {
showAlert(
NSLocalizedString("Error deleting member support chat", comment: "alert title"),
message: responseError(error)
)
}
}
}
}
#Preview {
MemberSupportView(
groupInfo: GroupInfo.sampleData,
@@ -1914,6 +1914,13 @@ object ChatController {
return null
}
suspend fun apiDeleteMemberSupportChat(rh: Long?, groupId: Long, groupMemberId: Long): Pair<GroupInfo, GroupMember>? {
val r = sendCmd(rh, CC.ApiDeleteMemberSupportChat(groupId, groupMemberId))
if (r is API.Result && r.res is CR.MemberSupportChatDeleted) return r.res.groupInfo to r.res.member
apiErrorAlert("apiDeleteMemberSupportChat", generalGetString(MR.strings.error_deleting_member_support_chat), r)
return null
}
suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List<Long>, withMessages: Boolean = false): Pair<GroupInfo, List<GroupMember>>? {
val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds, withMessages))
if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.groupInfo to r.res.members
@@ -3345,6 +3352,7 @@ sealed class CC {
class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC()
class ApiJoinGroup(val groupId: Long): CC()
class ApiAcceptMember(val groupId: Long, val groupMemberId: Long, val memberRole: GroupMemberRole): CC()
class ApiDeleteMemberSupportChat(val groupId: Long, val groupMemberId: Long): CC()
class ApiMembersRole(val groupId: Long, val memberIds: List<Long>, val memberRole: GroupMemberRole): CC()
class ApiBlockMembersForAll(val groupId: Long, val memberIds: List<Long>, val blocked: Boolean): CC()
class ApiRemoveMembers(val groupId: Long, val memberIds: List<Long>, val withMessages: Boolean): CC()
@@ -3531,6 +3539,7 @@ sealed class CC {
is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}"
is ApiJoinGroup -> "/_join #$groupId"
is ApiAcceptMember -> "/_accept member #$groupId $groupMemberId ${memberRole.memberRole}"
is ApiDeleteMemberSupportChat -> "/_delete member chat #$groupId $groupMemberId"
is ApiMembersRole -> "/_member role #$groupId ${memberIds.joinToString(",")} ${memberRole.memberRole}"
is ApiBlockMembersForAll -> "/_block #$groupId ${memberIds.joinToString(",")} blocked=${onOff(blocked)}"
is ApiRemoveMembers -> "/_remove #$groupId ${memberIds.joinToString(",")} messages=${onOff(withMessages)}"
@@ -3695,6 +3704,7 @@ sealed class CC {
is ApiAddMember -> "apiAddMember"
is ApiJoinGroup -> "apiJoinGroup"
is ApiAcceptMember -> "apiAcceptMember"
is ApiDeleteMemberSupportChat -> "apiDeleteMemberSupportChat"
is ApiMembersRole -> "apiMembersRole"
is ApiBlockMembersForAll -> "apiBlockMembersForAll"
is ApiRemoveMembers -> "apiRemoveMembers"
@@ -5849,6 +5859,7 @@ sealed class CR {
@Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: UserRef, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR()
@Serializable @SerialName("memberAccepted") class MemberAccepted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("memberSupportChatDeleted") class MemberSupportChatDeleted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("memberAcceptedByOther") class MemberAcceptedByOther(val user: UserRef, val groupInfo: GroupInfo, val acceptingMember: GroupMember, val member: GroupMember): CR()
@Serializable @SerialName("memberRole") class MemberRole(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR()
@Serializable @SerialName("membersRoleUser") class MembersRoleUser(val user: UserRef, val groupInfo: GroupInfo, val members: List<GroupMember>, val toRole: GroupMemberRole): CR()
@@ -6027,6 +6038,7 @@ sealed class CR {
is GroupDeletedUser -> "groupDeletedUser"
is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting"
is MemberAccepted -> "memberAccepted"
is MemberSupportChatDeleted -> "memberSupportChatDeleted"
is MemberAcceptedByOther -> "memberAcceptedByOther"
is MemberRole -> "memberRole"
is MembersRoleUser -> "membersRoleUser"
@@ -6198,6 +6210,7 @@ sealed class CR {
is GroupDeletedUser -> withUser(user, json.encodeToString(groupInfo))
is JoinedGroupMemberConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member")
is MemberAccepted -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
is MemberSupportChatDeleted -> withUser(user, "groupInfo: $groupInfo\nmember: $member")
is MemberAcceptedByOther -> withUser(user, "groupInfo: $groupInfo\nacceptingMember: $acceptingMember\nmember: $member")
is MemberRole -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole")
is MembersRoleUser -> withUser(user, "groupInfo: $groupInfo\nmembers: $members\ntoRole: $toRole")
@@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.group.removeMember
import chat.simplex.common.views.chat.group.removeMemberDialog
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
@@ -44,12 +45,12 @@ fun ComposeContextPendingMemberActionsView(
.fillMaxHeight()
.weight(1F)
.clickable {
removeMemberDialog(rhId, groupInfo, member, chatModel, close = { ModalManager.end.closeModal() })
rejectMemberDialog(rhId, member, chatModel, close = { ModalManager.end.closeModal() })
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(stringResource(MR.strings.remove_pending_member_button), color = Color.Red)
Text(stringResource(MR.strings.reject_pending_member_button), color = Color.Red)
}
Column(
@@ -69,6 +70,17 @@ fun ComposeContextPendingMemberActionsView(
}
}
fun rejectMemberDialog(rhId: Long?, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.reject_pending_member_alert_title),
confirmText = generalGetString(MR.strings.reject_pending_member_button),
onConfirm = {
removeMember(rhId, member, chatModel, close)
},
destructive = true,
)
}
fun acceptMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.accept_pending_member_alert_title),
@@ -105,12 +117,9 @@ private fun acceptMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember,
val r = chatModel.controller.apiAcceptMember(rhId, groupInfo.groupId, member.groupMemberId, role)
if (r != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, r.second)
chatModel.chatsContext.upsertGroupMember(rhId, r.first, r.second)
chatModel.chatsContext.updateGroup(rhId, r.first)
}
withContext(Dispatchers.Main) {
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, r.second)
}
}
close?.invoke()
}
@@ -245,29 +245,28 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c
text = generalGetString(messageId),
confirmText = generalGetString(MR.strings.remove_member_confirmation),
onConfirm = {
withBGApi {
val r = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId))
if (r != null) {
val (updatedGroupInfo, removedMembers) = r
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo)
removedMembers.forEach { removedMember ->
chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, removedMember)
}
}
withContext(Dispatchers.Main) {
removedMembers.forEach { removedMember ->
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, updatedGroupInfo, removedMember)
}
}
}
close?.invoke()
}
removeMember(rhId, member, chatModel, close)
},
destructive = true,
)
}
fun removeMember(rhId: Long?, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
withBGApi {
val r = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId))
if (r != null) {
val (updatedGroupInfo, removedMembers) = r
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo)
removedMembers.forEach { removedMember ->
chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, removedMember)
}
}
}
close?.invoke()
}
}
@Composable
fun GroupMemberInfoLayout(
rhId: Long?,
@@ -28,7 +28,7 @@ import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
@Composable
fun ModalData.MemberSupportView(
@@ -269,14 +269,34 @@ private fun DropDownMenuForSupportChat(rhId: Long?, member: GroupMember, groupIn
acceptMemberDialog(rhId, groupInfo, member)
showMenu.value = false
})
} else {
ItemAction(stringResource(MR.strings.delete_member_support_chat_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
deleteMemberSupportChatDialog(rhId, groupInfo, member)
showMenu.value = false
})
}
}
}
fun deleteMemberSupportChatDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_member_support_chat_alert_title),
confirmText = generalGetString(MR.strings.delete_member_support_chat_button),
onConfirm = {
deleteMemberSupportChat(rhId, groupInfo, member)
},
destructive = true,
)
}
private fun deleteMemberSupportChat(rhId: Long?, groupInfo: GroupInfo, member: GroupMember) {
withBGApi {
val r = chatModel.controller.apiDeleteMemberSupportChat(rhId, groupInfo.groupId, member.groupMemberId)
if (r != null) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.upsertGroupMember(rhId, r.first, r.second)
chatModel.chatsContext.updateGroup(rhId, r.first)
}
}
ItemAction(stringResource(MR.strings.remove_pending_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = {
removeMemberDialog(rhId, groupInfo, member, chatModel)
showMenu.value = false
})
// TODO [knocking] mark read, mark unread
// ItemAction(stringResource(MR.strings.mark_unread), painterResource(MR.images.ic_mark_chat_unread), onClick = {
// showMenu.value = false
// })
}
}
@@ -159,6 +159,7 @@
<string name="error_adding_members">Error adding member(s)</string>
<string name="error_joining_group">Error joining group</string>
<string name="error_accepting_member">Error accepting member</string>
<string name="error_deleting_member_support_chat">Error deleting member support chat</string>
<string name="cannot_receive_file">Cannot receive file</string>
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
<string name="file_not_approved_title">Unknown servers!</string>
@@ -2188,10 +2189,13 @@
<!-- MemberSupportView.kt -->
<string name="member_support">Chats with members</string>
<string name="no_support_chats">No chats with members</string>
<string name="delete_member_support_chat_button">Delete chat</string>
<string name="delete_member_support_chat_alert_title">Delete chat with member?</string>
<!-- MemberSupportChatView.kt -->
<string name="support_chat">Chat with admins</string>
<string name="remove_pending_member_button">Remove</string>
<string name="reject_pending_member_button">Reject</string>
<string name="reject_pending_member_alert_title">Reject member?</string>
<string name="accept_pending_member_button">Accept</string>
<string name="accept_pending_member_alert_title">Accept member</string>
<string name="accept_pending_member_alert_question">Member will join the group, accept member?</string>