From 106556812c59864a643eeafcb9df5a21e397a60e Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:33:49 +0400 Subject: [PATCH] android, desktop: block member for all (#3711) * android: block member for all * old buttons (revert this) * blocked by admin text * revise notification --- .../chat/simplex/common/model/ChatModel.kt | 74 ++++++++---- .../chat/simplex/common/model/SimpleXAPI.kt | 24 ++++ .../views/chat/group/GroupChatInfoView.kt | 61 +++++++++- .../views/chat/group/GroupMemberInfoView.kt | 106 ++++++++++++++++++ .../common/views/chat/item/ChatItemView.kt | 13 ++- .../common/views/chat/item/FramedItemView.kt | 5 +- .../views/chat/item/MarkedDeletedItemView.kt | 12 +- .../commonMain/resources/MR/base/strings.xml | 13 +++ 8 files changed, 270 insertions(+), 38 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index be0e6ce72..d44c80e92 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1279,6 +1279,7 @@ data class GroupMember ( val memberCategory: GroupMemberCategory, val memberStatus: GroupMemberStatus, val memberSettings: GroupMemberSettings, + val blockedByAdmin: Boolean, val invitedBy: InvitedBy, val localDisplayName: String, val memberProfile: LocalProfile, @@ -1297,6 +1298,7 @@ data class GroupMember ( val image: String? get() = memberProfile.image val contactLink: String? = memberProfile.contactLink val verified get() = activeConn?.connectionCode != null + val blocked get() = blockedByAdmin || !memberSettings.showMessages val chatViewName: String get() { @@ -1345,7 +1347,7 @@ data class GroupMember ( fun canBeRemoved(groupInfo: GroupInfo): Boolean { val userRole = groupInfo.membership.memberRole return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft - && userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberCurrent + && userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive } fun canChangeRoleTo(groupInfo: GroupInfo): List? = @@ -1354,6 +1356,12 @@ data class GroupMember ( GroupMemberRole.values().filter { it <= userRole && it != GroupMemberRole.Author } } + fun canBlockForAll(groupInfo: GroupInfo): Boolean { + val userRole = groupInfo.membership.memberRole + return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Admin + && userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive + } + val memberIncognito = memberProfile.profileId != memberContactProfileId companion object { @@ -1365,6 +1373,7 @@ data class GroupMember ( memberCategory = GroupMemberCategory.InviteeMember, memberStatus = GroupMemberStatus.MemComplete, memberSettings = GroupMemberSettings(showMessages = true), + blockedByAdmin = false, invitedBy = InvitedBy.IBUser(), localDisplayName = "alice", memberProfile = LocalProfile.sampleData, @@ -1734,6 +1743,7 @@ data class ChatItem ( is CIContent.RcvDeleted -> true is CIContent.SndModerated -> true is CIContent.RcvModerated -> true + is CIContent.RcvBlocked -> true else -> false } @@ -1785,20 +1795,18 @@ data class ChatItem ( } } - private val showNtfDir: Boolean get() = !chatDir.sent - val showNotification: Boolean get() = when (content) { - is CIContent.SndMsgContent -> showNtfDir - is CIContent.RcvMsgContent -> showNtfDir - is CIContent.SndDeleted -> showNtfDir - is CIContent.RcvDeleted -> showNtfDir - is CIContent.SndCall -> showNtfDir + is CIContent.SndMsgContent -> false + is CIContent.RcvMsgContent -> meta.itemDeleted == null + is CIContent.SndDeleted -> false + is CIContent.RcvDeleted -> false + is CIContent.SndCall -> false is CIContent.RcvCall -> false // notification is shown on CallInvitation instead - is CIContent.RcvIntegrityError -> showNtfDir - is CIContent.RcvDecryptionError -> showNtfDir - is CIContent.RcvGroupInvitation -> showNtfDir - is CIContent.SndGroupInvitation -> showNtfDir + is CIContent.RcvIntegrityError -> false + is CIContent.RcvDecryptionError -> false + is CIContent.RcvGroupInvitation -> true + is CIContent.SndGroupInvitation -> false is CIContent.RcvDirectEventContent -> when (content.rcvDirectEvent) { is RcvDirectEvent.ContactDeleted -> false is RcvDirectEvent.ProfileUpdated -> true @@ -1808,28 +1816,30 @@ data class ChatItem ( is RcvGroupEvent.MemberConnected -> false is RcvGroupEvent.MemberLeft -> false is RcvGroupEvent.MemberRole -> false - is RcvGroupEvent.UserRole -> showNtfDir + is RcvGroupEvent.MemberBlocked -> false + is RcvGroupEvent.UserRole -> true is RcvGroupEvent.MemberDeleted -> false - is RcvGroupEvent.UserDeleted -> showNtfDir - is RcvGroupEvent.GroupDeleted -> showNtfDir + is RcvGroupEvent.UserDeleted -> true + is RcvGroupEvent.GroupDeleted -> true is RcvGroupEvent.GroupUpdated -> false is RcvGroupEvent.InvitedViaGroupLink -> false is RcvGroupEvent.MemberCreatedContact -> false is RcvGroupEvent.MemberProfileUpdated -> false } - is CIContent.SndGroupEventContent -> showNtfDir + is CIContent.SndGroupEventContent -> false is CIContent.RcvConnEventContent -> false - is CIContent.SndConnEventContent -> showNtfDir + is CIContent.SndConnEventContent -> false is CIContent.RcvChatFeature -> false - is CIContent.SndChatFeature -> showNtfDir + is CIContent.SndChatFeature -> false is CIContent.RcvChatPreference -> false - is CIContent.SndChatPreference -> showNtfDir + is CIContent.SndChatPreference -> false is CIContent.RcvGroupFeature -> false - is CIContent.SndGroupFeature -> showNtfDir - is CIContent.RcvChatFeatureRejected -> showNtfDir - is CIContent.RcvGroupFeatureRejected -> showNtfDir - is CIContent.SndModerated -> true - is CIContent.RcvModerated -> true + is CIContent.SndGroupFeature -> false + is CIContent.RcvChatFeatureRejected -> true + is CIContent.RcvGroupFeatureRejected -> false + is CIContent.SndModerated -> false + is CIContent.RcvModerated -> false + is CIContent.RcvBlocked -> false is CIContent.InvalidJSON -> false } @@ -2174,6 +2184,7 @@ enum class SndCIStatusProgress { sealed class CIDeleted { @Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted() @Serializable @SerialName("blocked") class Blocked(val deletedTs: Instant?): CIDeleted() + @Serializable @SerialName("blockedByAdmin") class BlockedByAdmin(val deletedTs: Instant?): CIDeleted() @Serializable @SerialName("moderated") class Moderated(val deletedTs: Instant?, val byGroupMember: GroupMember): CIDeleted() } @@ -2218,6 +2229,7 @@ sealed class CIContent: ItemContent { @Serializable @SerialName("rcvGroupFeatureRejected") class RcvGroupFeatureRejected(val groupFeature: GroupFeature): CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("sndModerated") object SndModerated: CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("rcvModerated") object RcvModerated: CIContent() { override val msgContent: MsgContent? get() = null } + @Serializable @SerialName("rcvBlocked") object RcvBlocked: CIContent() { override val msgContent: MsgContent? get() = null } @Serializable @SerialName("invalidJSON") data class InvalidJSON(val json: String): CIContent() { override val msgContent: MsgContent? get() = null } override val text: String get() = when (this) { @@ -2246,6 +2258,7 @@ sealed class CIContent: ItemContent { is RcvGroupFeatureRejected -> "${groupFeature.text}: ${generalGetString(MR.strings.feature_received_prohibited)}" is SndModerated -> generalGetString(MR.strings.moderated_description) is RcvModerated -> generalGetString(MR.strings.moderated_description) + is RcvBlocked -> generalGetString(MR.strings.blocked_by_admin_item_description) is InvalidJSON -> "invalid data" } @@ -2258,6 +2271,7 @@ sealed class CIContent: ItemContent { is RcvDecryptionError -> true is RcvGroupInvitation -> true is RcvModerated -> true + is RcvBlocked -> true is InvalidJSON -> true else -> false } @@ -2958,6 +2972,7 @@ sealed class RcvGroupEvent() { @Serializable @SerialName("memberConnected") class MemberConnected(): RcvGroupEvent() @Serializable @SerialName("memberLeft") class MemberLeft(): RcvGroupEvent() @Serializable @SerialName("memberRole") class MemberRole(val groupMemberId: Long, val profile: Profile, val role: GroupMemberRole): RcvGroupEvent() + @Serializable @SerialName("memberBlocked") class MemberBlocked(val groupMemberId: Long, val profile: Profile, val blocked: Boolean): RcvGroupEvent() @Serializable @SerialName("userRole") class UserRole(val role: GroupMemberRole): RcvGroupEvent() @Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): RcvGroupEvent() @Serializable @SerialName("userDeleted") class UserDeleted(): RcvGroupEvent() @@ -2972,6 +2987,11 @@ sealed class RcvGroupEvent() { is MemberConnected -> generalGetString(MR.strings.rcv_group_event_member_connected) is MemberLeft -> generalGetString(MR.strings.rcv_group_event_member_left) is MemberRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_member_role), profile.profileViewName, role.text) + is MemberBlocked -> if (blocked) { + String.format(generalGetString(MR.strings.rcv_group_event_member_blocked), profile.profileViewName) + } else { + String.format(generalGetString(MR.strings.rcv_group_event_member_unblocked), profile.profileViewName) + } is UserRole -> String.format(generalGetString(MR.strings.rcv_group_event_changed_your_role), role.text) is MemberDeleted -> String.format(generalGetString(MR.strings.rcv_group_event_member_deleted), profile.profileViewName) is UserDeleted -> generalGetString(MR.strings.rcv_group_event_user_deleted) @@ -3000,6 +3020,7 @@ sealed class RcvGroupEvent() { sealed class SndGroupEvent() { @Serializable @SerialName("memberRole") class MemberRole(val groupMemberId: Long, val profile: Profile, val role: GroupMemberRole): SndGroupEvent() @Serializable @SerialName("userRole") class UserRole(val role: GroupMemberRole): SndGroupEvent() + @Serializable @SerialName("memberBlocked") class MemberBlocked(val groupMemberId: Long, val profile: Profile, val blocked: Boolean): SndGroupEvent() @Serializable @SerialName("memberDeleted") class MemberDeleted(val groupMemberId: Long, val profile: Profile): SndGroupEvent() @Serializable @SerialName("userLeft") class UserLeft(): SndGroupEvent() @Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): SndGroupEvent() @@ -3007,6 +3028,11 @@ sealed class SndGroupEvent() { val text: String get() = when (this) { is MemberRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_member_role), profile.profileViewName, role.text) is UserRole -> String.format(generalGetString(MR.strings.snd_group_event_changed_role_for_yourself), role.text) + is MemberBlocked -> if (blocked) { + String.format(generalGetString(MR.strings.snd_group_event_member_blocked), profile.profileViewName) + } else { + String.format(generalGetString(MR.strings.snd_group_event_member_unblocked), profile.profileViewName) + } is MemberDeleted -> String.format(generalGetString(MR.strings.snd_group_event_member_deleted), profile.profileViewName) is UserLeft -> generalGetString(MR.strings.snd_group_event_user_left) is GroupUpdated -> generalGetString(MR.strings.snd_group_event_group_profile_updated) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index ab172e61f..0f068e96a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1332,6 +1332,17 @@ object ChatController { } } + suspend fun apiBlockMemberForAll(rh: Long?, groupId: Long, memberId: Long, blocked: Boolean): GroupMember = + when (val r = sendCmd(rh, CC.ApiBlockMemberForAll(groupId, memberId, blocked))) { + is CR.MemberBlockedForAllUser -> r.member + else -> { + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiBlockMemberForAll", generalGetString(MR.strings.error_blocking_member_for_all), r) + } + throw Exception("failed to block member for all: ${r.responseType} ${r.details}") + } + } + suspend fun apiLeaveGroup(rh: Long?, groupId: Long): GroupInfo? { val r = sendCmd(rh, CC.ApiLeaveGroup(groupId)) if (r is CR.LeftMemberUser) return r.groupInfo @@ -1786,6 +1797,10 @@ object ChatController { if (active(r.user)) { chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) } + is CR.MemberBlockedForAll -> + if (active(r.user)) { + chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + } is CR.GroupDeleted -> // TODO update user member if (active(r.user)) { chatModel.updateGroup(rhId, r.groupInfo) @@ -2275,6 +2290,7 @@ sealed class CC { class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC() class ApiJoinGroup(val groupId: Long): CC() class ApiMemberRole(val groupId: Long, val memberId: Long, val memberRole: GroupMemberRole): CC() + class ApiBlockMemberForAll(val groupId: Long, val memberId: Long, val blocked: Boolean): CC() class ApiRemoveMember(val groupId: Long, val memberId: Long): CC() class ApiLeaveGroup(val groupId: Long): CC() class ApiListMembers(val groupId: Long): CC() @@ -2409,6 +2425,7 @@ sealed class CC { is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}" is ApiJoinGroup -> "/_join #$groupId" is ApiMemberRole -> "/_member role #$groupId $memberId ${memberRole.memberRole}" + is ApiBlockMemberForAll -> "/_block #$groupId $memberId blocked=${onOff(blocked)}" is ApiRemoveMember -> "/_remove #$groupId $memberId" is ApiLeaveGroup -> "/_leave #$groupId" is ApiListMembers -> "/_members #$groupId" @@ -2538,6 +2555,7 @@ sealed class CC { is ApiAddMember -> "apiAddMember" is ApiJoinGroup -> "apiJoinGroup" is ApiMemberRole -> "apiMemberRole" + is ApiBlockMemberForAll -> "apiBlockMemberForAll" is ApiRemoveMember -> "apiRemoveMember" is ApiLeaveGroup -> "apiLeaveGroup" is ApiListMembers -> "apiListMembers" @@ -3928,6 +3946,8 @@ sealed class CR { @Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: UserRef, val groupInfo: GroupInfo, val hostMember: 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("memberRoleUser") class MemberRoleUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() + @Serializable @SerialName("memberBlockedForAll") class MemberBlockedForAll(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val blocked: Boolean): CR() + @Serializable @SerialName("memberBlockedForAllUser") class MemberBlockedForAllUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val blocked: Boolean): CR() @Serializable @SerialName("deletedMemberUser") class DeletedMemberUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("deletedMember") class DeletedMember(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember): CR() @Serializable @SerialName("leftMember") class LeftMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @@ -4080,6 +4100,8 @@ sealed class CR { is JoinedGroupMemberConnecting -> "joinedGroupMemberConnecting" is MemberRole -> "memberRole" is MemberRoleUser -> "memberRoleUser" + is MemberBlockedForAll -> "memberBlockedForAll" + is MemberBlockedForAllUser -> "memberBlockedForAllUser" is DeletedMemberUser -> "deletedMemberUser" is DeletedMember -> "deletedMember" is LeftMember -> "leftMember" @@ -4227,6 +4249,8 @@ sealed class CR { is JoinedGroupMemberConnecting -> withUser(user, "groupInfo: $groupInfo\nhostMember: $hostMember\nmember: $member") is MemberRole -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole") is MemberRoleUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nfromRole: $fromRole\ntoRole: $toRole") + is MemberBlockedForAll -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\nmember: $member\nblocked: $blocked") + is MemberBlockedForAllUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nblocked: $blocked") is DeletedMemberUser -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is DeletedMember -> withUser(user, "groupInfo: $groupInfo\nbyMember: $byMember\ndeletedMember: $deletedMember") is LeftMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 4d76afcbb..d602d78d8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -368,6 +368,18 @@ private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick @Composable private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() -> Unit)? = null) { + @Composable + fun MemberInfo() { + if (member.blocked) { + Text(stringResource(MR.strings.member_info_member_blocked), color = MaterialTheme.colors.secondary) + } else { + val role = member.memberRole + if (role in listOf(GroupMemberRole.Owner, GroupMemberRole.Admin, GroupMemberRole.Observer)) { + Text(role.text, color = MaterialTheme.colors.secondary) + } + } + } + Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -401,10 +413,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() - ) } } - val role = member.memberRole - if (role in listOf(GroupMemberRole.Owner, GroupMemberRole.Admin, GroupMemberRole.Observer)) { - Text(role.text, color = MaterialTheme.colors.secondary) - } + MemberInfo() } } @@ -415,6 +424,7 @@ private fun MemberVerifiedShield() { @Composable private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: GroupInfo, showMenu: MutableState) { + // revert from this: DefaultDropdownMenu(showMenu) { if (member.canBeRemoved(groupInfo)) { ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = { @@ -434,6 +444,49 @@ private fun DropDownMenuForMember(rhId: Long?, member: GroupMember, groupInfo: G }) } } + // revert to this: vvv +// if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) { +// val canBlockForAll = member.canBlockForAll(groupInfo) +// val canRemove = member.canBeRemoved(groupInfo) +// if (canBlockForAll || canRemove) { +// DefaultDropdownMenu(showMenu) { +// if (canBlockForAll) { +// if (member.blockedByAdmin) { +// ItemAction(stringResource(MR.strings.unblock_for_all), painterResource(MR.images.ic_do_not_touch), onClick = { +// unblockForAllAlert(rhId, groupInfo, member) +// showMenu.value = false +// }) +// } else { +// ItemAction(stringResource(MR.strings.block_for_all), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = { +// blockForAllAlert(rhId, groupInfo, member) +// showMenu.value = false +// }) +// } +// } +// if (canRemove) { +// ItemAction(stringResource(MR.strings.remove_member_button), painterResource(MR.images.ic_delete), color = MaterialTheme.colors.error, onClick = { +// removeMemberAlert(rhId, groupInfo, member) +// showMenu.value = false +// }) +// } +// } +// } +// } else if (!member.blockedByAdmin) { +// DefaultDropdownMenu(showMenu) { +// if (member.memberSettings.showMessages) { +// ItemAction(stringResource(MR.strings.block_member_button), painterResource(MR.images.ic_back_hand), color = MaterialTheme.colors.error, onClick = { +// blockMemberAlert(rhId, groupInfo, member) +// showMenu.value = false +// }) +// } else { +// ItemAction(stringResource(MR.strings.unblock_member_button), painterResource(MR.images.ic_do_not_touch), onClick = { +// unblockMemberAlert(rhId, groupInfo, member) +// showMenu.value = false +// }) +// } +// } +// } + // ^^^ } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index 84d7afaae..54614d022 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -3,9 +3,11 @@ package chat.simplex.common.views.chat.group import InfoRow import SectionBottomSpacer import SectionDividerSpaced +import SectionItemView import SectionSpacer import SectionTextFooter import SectionView +import TextIconSpaced import androidx.compose.desktop.ui.tooling.preview.Preview import java.net.URI import androidx.compose.foundation.* @@ -99,6 +101,8 @@ fun GroupMemberInfoView( }, blockMember = { blockMemberAlert(rhId, groupInfo, member) }, unblockMember = { unblockMemberAlert(rhId, groupInfo, member) }, + blockForAll = { blockForAllAlert(rhId, groupInfo, member) }, + unblockForAll = { unblockForAllAlert(rhId, groupInfo, member) }, removeMember = { removeMemberDialog(rhId, groupInfo, member, chatModel, close) }, onRoleSelected = { if (it == newRole.value) return@GroupMemberInfoLayout @@ -230,6 +234,8 @@ fun GroupMemberInfoLayout( connectViaAddress: (String) -> Unit, blockMember: () -> Unit, unblockMember: () -> Unit, + blockForAll: () -> Unit, + unblockForAll: () -> Unit, removeMember: () -> Unit, onRoleSelected: (GroupMemberRole) -> Unit, switchMemberAddress: () -> Unit, @@ -248,6 +254,46 @@ fun GroupMemberInfoLayout( } } + @Composable + fun AdminDestructiveSection() { + val canBlockForAll = member.canBlockForAll(groupInfo) + val canRemove = member.canBeRemoved(groupInfo) + if (canBlockForAll || canRemove) { + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + if (canBlockForAll) { + if (member.blockedByAdmin) { + UnblockForAllButton(unblockForAll) + } else { + BlockForAllButton(blockForAll) + } + } + if (canRemove) { + RemoveMemberButton(removeMember) + } + } + } + } + + @Composable + fun NonAdminBlockSection() { + SectionDividerSpaced(maxBottomPadding = false) + SectionView { + if (member.blockedByAdmin) { + SettingsActionItem( + painterResource(MR.images.ic_back_hand), + stringResource(MR.strings.member_blocked_by_admin), + click = null, + disabled = true + ) + } else if (member.memberSettings.showMessages) { + BlockMemberButton(blockMember) + } else { + UnblockMemberButton(unblockMember) + } + } + } + Column( Modifier .fillMaxWidth() @@ -344,6 +390,7 @@ fun GroupMemberInfoLayout( } } + // revert from this: SectionDividerSpaced(maxBottomPadding = false) SectionView { if (member.memberSettings.showMessages) { @@ -355,6 +402,13 @@ fun GroupMemberInfoLayout( RemoveMemberButton(removeMember) } } + // revert to this: vvv +// if (groupInfo.membership.memberRole >= GroupMemberRole.Admin) { +// AdminDestructiveSection() +// } else { +// NonAdminBlockSection() +// } + // ^^^ if (developerTools) { SectionDividerSpaced() @@ -427,6 +481,26 @@ fun UnblockMemberButton(onClick: () -> Unit) { ) } +@Composable +fun BlockForAllButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_back_hand), + stringResource(MR.strings.block_for_all), + click = onClick, + textColor = Color.Red, + iconColor = Color.Red, + ) +} + +@Composable +fun UnblockForAllButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_do_not_touch), + stringResource(MR.strings.unblock_for_all), + click = onClick + ) +} + @Composable fun RemoveMemberButton(onClick: () -> Unit) { SettingsActionItem( @@ -553,6 +627,36 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem } } +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 = { + blockMemberForAll(rhId, gInfo, mem, true) + }, + destructive = true, + ) +} + +fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.unblock_for_all_question), + text = generalGetString(MR.strings.unblock_member_desc).format(mem.chatViewName), + confirmText = generalGetString(MR.strings.unblock_for_all), + onConfirm = { + blockMemberForAll(rhId, gInfo, mem, false) + }, + ) +} + +fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) { + withBGApi { + val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked) + chatModel.upsertGroupMember(rhId, gInfo, updatedMember) + } +} + @Preview @Composable fun PreviewGroupMemberInfoLayout() { @@ -570,6 +674,8 @@ fun PreviewGroupMemberInfoLayout() { connectViaAddress = {}, blockMember = {}, unblockMember = {}, + blockForAll = {}, + unblockForAll = {}, removeMember = {}, onRoleSelected = {}, switchMemberAddress = {}, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 8f49ce92c..549f5f2f5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -319,7 +319,7 @@ fun ChatItemView( } } - @Composable fun DeletedItem() { + @Composable fun LegacyDeletedItem() { DeletedItemView(cItem, cInfo.timedMessagesTTL) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) @@ -371,7 +371,7 @@ fun ChatItemView( } @Composable - fun ModeratedItem() { + fun DeletedItem() { MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) @@ -382,8 +382,8 @@ fun ChatItemView( when (val c = cItem.content) { is CIContent.SndMsgContent -> ContentItem() is CIContent.RcvMsgContent -> ContentItem() - is CIContent.SndDeleted -> DeletedItem() - is CIContent.RcvDeleted -> DeletedItem() + is CIContent.SndDeleted -> LegacyDeletedItem() + is CIContent.RcvDeleted -> LegacyDeletedItem() is CIContent.SndCall -> CallItem(c.status, c.duration) is CIContent.RcvCall -> CallItem(c.status, c.duration) is CIContent.RcvIntegrityError -> if (developerTools) { @@ -449,8 +449,9 @@ fun ChatItemView( CIChatFeatureView(cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu) MsgContentItemDropdownMenu() } - is CIContent.SndModerated -> ModeratedItem() - is CIContent.RcvModerated -> ModeratedItem() + is CIContent.SndModerated -> DeletedItem() + is CIContent.RcvModerated -> DeletedItem() + is CIContent.RcvBlocked -> DeletedItem() is CIContent.InvalidJSON -> CIInvalidJSONView(c.json) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 83ec134d8..641e6affa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -209,7 +209,10 @@ fun FramedItemView( is CIDeleted.Blocked -> { FramedItemHeader(stringResource(MR.strings.blocked_item_description), true, painterResource(MR.images.ic_back_hand)) } - else -> { + is CIDeleted.BlockedByAdmin -> { + FramedItemHeader(stringResource(MR.strings.blocked_by_admin_item_description), true, painterResource(MR.images.ic_back_hand)) + } + is CIDeleted.Deleted -> { FramedItemHeader(stringResource(MR.strings.marked_deleted_description), true, painterResource(MR.images.ic_delete)) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index f39795499..f7783d682 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt @@ -48,6 +48,7 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState = mutableSetOf() while (i < reversedChatItems.size) { @@ -59,16 +60,19 @@ private fun MergedMarkedDeletedText(chatItem: ChatItem, revealed: MutableState blocked += 1 + is CIDeleted.BlockedByAdmin -> blockedByAdmin +=1 is CIDeleted.Deleted -> deleted += 1 } i++ } - val total = moderated + blocked + deleted + val total = moderated + blocked + blockedByAdmin + deleted if (total <= 1) markedDeletedText(chatItem.meta) else if (total == moderated) stringResource(MR.strings.moderated_items_description).format(total, moderatedBy.joinToString(", ")) - else if (total == blocked) + else if (total == blockedByAdmin) + stringResource(MR.strings.blocked_by_admin_items_description).format(total) + else if (total == blocked + blockedByAdmin) stringResource(MR.strings.blocked_items_description).format(total) else stringResource(MR.strings.marked_deleted_items_description).format(total) @@ -93,7 +97,9 @@ private fun markedDeletedText(meta: CIMeta): String = String.format(generalGetString(MR.strings.moderated_item_description), meta.itemDeleted.byGroupMember.displayName) is CIDeleted.Blocked -> generalGetString(MR.strings.blocked_item_description) - else -> + is CIDeleted.BlockedByAdmin -> + generalGetString(MR.strings.blocked_by_admin_item_description) + is CIDeleted.Deleted, null -> generalGetString(MR.strings.marked_deleted_description) } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index aae9d54e9..d36eca273 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -35,7 +35,9 @@ moderated by %s %1$d messages moderated by %2$s blocked + blocked by admin %d messages blocked + %d messages blocked by admin sending files is not supported yet receiving files is not supported yet you @@ -1176,6 +1178,8 @@ connected left changed role of %s to %s + blocked %s + unblocked %s changed your role to %s removed %1$s removed you @@ -1185,6 +1189,8 @@ connected directly you changed role of %s to %s you changed role for yourself to %s + you blocked %s + you unblocked %s you removed %1$s you left group profile updated @@ -1339,11 +1345,17 @@ Block member? Block member Block + Block member for all? + Block for all All new messages from %s will be hidden! Unblock member? Unblock member Unblock + Unblock member for all? + Unblock for all Messages from %s will be shown! + Blocked by admin + blocked MEMBER Role Change role @@ -1356,6 +1368,7 @@ Сonnection request will be sent to this group member. Error removing member Error changing role + Error blocking member for all Group Connection direct