From a92c74d3e51b146816b0771f7226b683a16804a8 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 16 May 2025 08:26:28 +0000 Subject: [PATCH 1/4] android, desktop: better Chat with admins indications for members; unread badges (#5917) --- .../views/chatlist/UserPicker.android.kt | 2 +- .../simplex/common/views/chat/ChatView.kt | 9 +++-- .../views/chat/group/GroupChatInfoView.kt | 40 ++++++++++++++----- .../views/chat/group/MemberSupportView.kt | 26 +++++------- .../common/views/chatlist/ChatPreviewView.kt | 35 +++++++++++----- .../common/views/chatlist/UserPicker.kt | 2 +- .../views/chatlist/UserPicker.desktop.kt | 2 +- 7 files changed, 72 insertions(+), 44 deletions(-) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt index 54e3061d25..a09ca2792b 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.android.kt @@ -91,7 +91,7 @@ fun UserPickerUserBox( ProfileImageForActiveCall(size = USER_PICKER_IMAGE_SIZE, image = userInfo.user.profile.image, color = MaterialTheme.colors.secondaryVariant) if (userInfo.unreadCount > 0 && !userInfo.user.activeUser) { - unreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false) + userUnreadBadge(userInfo.unreadCount, userInfo.user.showNtfs, false) } } val user = userInfo.user diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index ca9f2983d4..fc26a9c8a3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -898,7 +898,6 @@ fun ChatLayout( if (oneHandUI.value && chatBottomBar.value) { if ( chatInfo is ChatInfo.Group - && chatInfo.groupInfo.canModerate && chatsCtx.secondaryContextFilter == null && (reportsCount > 0 || supportUnreadCount > 0) ) { @@ -960,8 +959,6 @@ fun ChatLayout( } if ( chatInfo is ChatInfo.Group - && chatInfo.groupInfo.canModerate - && chatsCtx.contentTag == null && (reportsCount > 0 || supportUnreadCount > 0) && (!oneHandUI.value || !chatBottomBar.value) ) { @@ -1212,7 +1209,11 @@ private fun SupportChatsCountToolbar( .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly, ) { - if (reportsCount > 0) { + if ( + chatInfo is ChatInfo.Group + && chatInfo.groupInfo.canModerate + && reportsCount > 0 + ) { Row( Modifier .fillMaxWidth() 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 ee0d0e33cd..cb1794b568 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 @@ -331,13 +331,14 @@ fun AddGroupMembersButton( @Composable fun UserSupportChatButton( + chat: Chat, groupInfo: GroupInfo, scrollToItemId: MutableState ) { val scope = rememberCoroutineScope() - SettingsActionItem( - painterResource(MR.images.ic_flag), + SettingsActionItemWithContent( + painterResource(if (chat.supportUnreadCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag), stringResource(MR.strings.button_support_chat), click = { val scopeInfo = GroupChatScopeInfo.MemberSupport(groupMember_ = null) @@ -351,8 +352,15 @@ fun UserSupportChatButton( ) } }, - iconColor = MaterialTheme.colors.secondary, - ) + iconColor = (if (chat.supportUnreadCount > 0) MaterialTheme.colors.primary else MaterialTheme.colors.secondary), + ) { + if (chat.supportUnreadCount > 0) { + UnreadBadge( + text = unreadCountStr(chat.supportUnreadCount), + backgroundColor = MaterialTheme.colors.primary + ) + } + } } @Composable @@ -488,7 +496,7 @@ fun ModalData.GroupChatInfoLayout( (groupInfo.membership.memberRole < GroupMemberRole.Moderator || groupInfo.membership.supportChat != null) ) { anyTopSectionRowShow = true - UserSupportChatButton(groupInfo, scrollToItemId) + UserSupportChatButton(chat, groupInfo, scrollToItemId) } } if (anyTopSectionRowShow) { @@ -743,12 +751,19 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) { @Composable private fun MemberSupportButton(chat: Chat, onClick: () -> Unit) { - SettingsActionItem( + SettingsActionItemWithContent( painterResource(if (chat.supportUnreadCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag), stringResource(MR.strings.member_support), click = onClick, iconColor = (if (chat.supportUnreadCount > 0) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) - ) + ) { + if (chat.supportUnreadCount > 0) { + UnreadBadge( + text = unreadCountStr(chat.supportUnreadCount), + backgroundColor = MaterialTheme.colors.primary + ) + } + } } @Composable @@ -762,12 +777,19 @@ private fun GroupPreferencesButton(titleId: StringResource, onClick: () -> Unit) @Composable private fun GroupReportsButton(chat: Chat, onClick: () -> Unit) { - SettingsActionItem( + SettingsActionItemWithContent( painterResource(if (chat.chatStats.reportsCount > 0) MR.images.ic_flag_filled else MR.images.ic_flag), stringResource(MR.strings.group_reports_member_reports), click = onClick, iconColor = (if (chat.chatStats.reportsCount > 0) Color.Red else MaterialTheme.colors.secondary) - ) + ) { + if (chat.chatStats.reportsCount > 0) { + UnreadBadge( + text = unreadCountStr(chat.chatStats.reportsCount), + backgroundColor = Color.Red + ) + } + } } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt index 46b0c05a31..0ef63a2a11 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportView.kt @@ -25,8 +25,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.item.ItemAction -import chat.simplex.common.views.chatlist.setGroupMembers -import chat.simplex.common.views.chatlist.unreadCountStr +import chat.simplex.common.views.chatlist.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.launch @@ -179,12 +178,12 @@ fun SupportChatRow(member: GroupMember) { Box(Modifier.widthIn(min = 34.sp.toDp()), contentAlignment = Alignment.TopEnd) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.sp.toDp())) { if (supportChat.unread > 0 || supportChat.mentions > 0 || supportChat.memberAttention > 0) { - val indicatorTint = when { + val unreadBadgeColor = when { supportChat.mentions > 0 || supportChat.memberAttention > 0 -> MaterialTheme.colors.primaryVariant else -> MaterialTheme.colors.secondary } if (supportChat.mentions == 1 && supportChat.unread == 1) { - Box(modifier = Modifier.offset(y = 2.sp.toDp()).size(15.sp.toDp()).background(indicatorTint, shape = CircleShape), contentAlignment = Alignment.Center) { + Box(modifier = Modifier.offset(y = 2.sp.toDp()).size(15.sp.toDp()).background(unreadBadgeColor, shape = CircleShape), contentAlignment = Alignment.Center) { Icon( painterResource(MR.images.ic_alternate_email), contentDescription = generalGetString(MR.strings.notifications), @@ -197,22 +196,15 @@ fun SupportChatRow(member: GroupMember) { Icon( painterResource(MR.images.ic_alternate_email), contentDescription = generalGetString(MR.strings.notifications), - tint = indicatorTint, - modifier = Modifier.size(12.sp.toDp()).offset(y = 3.sp.toDp()) + tint = unreadBadgeColor, + modifier = Modifier.size(12.sp.toDp()).offset(y = 2.sp.toDp()) ) } - Text( - unreadCountStr(supportChat.unread), - color = Color.White, - fontSize = 10.sp, - style = TextStyle(textAlign = TextAlign.Center), - modifier = Modifier - .offset(y = 3.sp.toDp()) - .background(indicatorTint, shape = CircleShape) - .badgeLayout() - .padding(horizontal = 2.sp.toDp()) - .padding(vertical = 1.sp.toDp()) + UnreadBadge( + text = unreadCountStr(supportChat.unread), + backgroundColor = unreadBadgeColor, + yOffset = 2.dp ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 2d375389dc..65a9d175dd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -471,17 +471,10 @@ fun ChatPreviewView( ) } } else { - Text( - if (n > 0) unreadCountStr(n) else "", - color = Color.White, - fontSize = 10.sp, - style = TextStyle(textAlign = TextAlign.Center), - modifier = Modifier - .offset(y = 3.sp.toDp()) - .background(if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape) - .badgeLayout() - .padding(horizontal = 2.sp.toDp()) - .padding(vertical = 1.sp.toDp()) + UnreadBadge( + text = if (n > 0) unreadCountStr(n) else "", + backgroundColor = if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, + yOffset = 3.dp ) } } @@ -573,6 +566,26 @@ private fun groupInvitationPreviewText(currentUserProfileDisplayName: String?, g stringResource(MR.strings.group_preview_you_are_invited) } +@Composable +fun UnreadBadge( + text: String, + backgroundColor: Color, + yOffset: Dp? = null +) { + Text( + text, + color = Color.White, + fontSize = 10.sp, + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier + .offset(y = yOffset ?: 0.dp) + .background(backgroundColor, shape = CircleShape) + .badgeLayout() + .padding(horizontal = 2.sp.toDp()) + .padding(vertical = 1.sp.toDp()) + ) +} + @Composable fun unreadCountStr(n: Int): String { return if (n < 1000) "$n" else "${n / 1000}" + stringResource(MR.strings.thousand_abbreviation) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 185ec3925f..13351a2111 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -634,7 +634,7 @@ fun HostDisconnectButton(onClick: (() -> Unit)?) { } @Composable -fun BoxScope.unreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) { +fun BoxScope.userUnreadBadge(unreadCount: Int, userMuted: Boolean, hasPadding: Boolean) { Text( if (unreadCount > 0) unreadCountStr(unreadCount) else "", color = Color.White, diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt index 3855835ab6..52e845b422 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.desktop.kt @@ -63,7 +63,7 @@ actual fun UserPickerUsersSection( ProfileImage(size = 55.dp, image = user.profile.image, color = iconColor) if (u.unreadCount > 0 && !user.activeUser) { - unreadBadge(u.unreadCount, user.showNtfs, true) + userUnreadBadge(u.unreadCount, user.showNtfs, true) } } From 5e7f77d1d16010e6635c2a2e4aec4e8285efadda Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 16 May 2025 10:54:13 +0000 Subject: [PATCH 2/4] android: go back to chat list after closing support chat when invitee is pending (#5918) --- .../chat/simplex/common/model/ChatModel.kt | 6 ++++++ .../views/chat/group/MemberSupportChatView.kt | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) 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 f50b4a99ca..2a47e22be8 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 @@ -323,6 +323,12 @@ object ChatModel { is SecondaryContextFilter.MsgContentTagContext -> null } + val isUserSupportChat: Boolean = + when (groupScopeInfo) { + null -> false + is GroupChatScopeInfo.MemberSupport -> groupScopeInfo.groupMember_ == null + } + suspend fun addChat(chat: Chat) { chats.add(index = 0, chat) popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = 0) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt index 3196cae15c..99e2e3198e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt @@ -22,6 +22,7 @@ import dev.icerock.moko.resources.compose.stringResource @Composable private fun MemberSupportChatView( + chatInfo: ChatInfo, memberSupportChatsCtx: ChatModel.ChatsContext, staleChatId: State, scrollToItemId: MutableState @@ -29,6 +30,21 @@ private fun MemberSupportChatView( KeyChangeEffect(chatModel.chatId.value) { ModalManager.end.closeModals() } + if (appPlatform.isAndroid) { + DisposableEffect(Unit) { + onDispose { + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatInfo.id } + if ( + memberSupportChatsCtx.isUserSupportChat + && chat?.chatInfo?.groupInfo_?.membership?.memberPending == true + ) { + withBGApi { + chatModel.chatId.value = null + } + } + } + } + } ChatView(memberSupportChatsCtx, staleChatId, scrollToItemId, onComposed = {}) } @@ -118,7 +134,7 @@ suspend fun showMemberSupportChatView(staleChatId: State, scrollToItemI ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close -> ModalView({}, showAppBar = false) { if (chatInfo is ChatInfo.Group && chatInfo.groupChatScope != null) { - MemberSupportChatView(memberSupportChatsCtx, staleChatId, scrollToItemId) + MemberSupportChatView(chatInfo, memberSupportChatsCtx, staleChatId, scrollToItemId) } else { LaunchedEffect(Unit) { close() From 041807c8700801a8e39c85799bb729c7a088fac4 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 16 May 2025 15:03:15 +0000 Subject: [PATCH 3/4] core: decrease membersRequireAttention counter when member is deleted or leaves (#5919) --- apps/ios/Shared/Model/SimpleXAPI.swift | 6 +++-- .../Views/Chat/Group/GroupChatInfoView.swift | 7 ++--- .../Chat/Group/GroupMemberInfoView.swift | 5 ++-- .../Views/ChatList/ChatListNavLink.swift | 4 +-- apps/ios/SimpleX.xcodeproj/project.pbxproj | 16 +++++------ apps/ios/SimpleXChat/ChatTypes.swift | 6 ++++- .../chat/simplex/common/model/ChatModel.kt | 5 +++- .../chat/simplex/common/model/SimpleXAPI.kt | 6 +++-- .../views/chat/group/GroupChatInfoView.kt | 12 +++++---- .../views/chat/group/GroupMemberInfoView.kt | 10 ++++--- .../views/chatlist/ChatListNavLinkView.kt | 4 +-- src/Simplex/Chat/Library/Commands.hs | 24 ++++++++++++----- src/Simplex/Chat/Library/Internal.hs | 15 +++++++---- src/Simplex/Chat/Library/Subscriber.hs | 16 ++++++----- src/Simplex/Chat/Store/Groups.hs | 27 +++++++++++-------- .../SQLite/Migrations/chat_query_plans.txt | 16 +++++------ 16 files changed, 111 insertions(+), 68 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index c625a5a043..844f888e29 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1646,9 +1646,9 @@ func apiAcceptMember(_ groupId: Int64, _ groupMemberId: Int64, _ memberRole: Gro throw r.unexpected } -func apiRemoveMembers(_ groupId: Int64, _ memberIds: [Int64], _ withMessages: Bool = false) async throws -> [GroupMember] { +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(_, _, members, withMessages) = r { return members } + if case let .userDeletedMembers(_, updatedGroupInfo, members, _withMessages) = r { return (updatedGroupInfo, members) } throw r.unexpected } @@ -2267,6 +2267,7 @@ func processReceivedMsg(_ res: ChatEvent) async { case let .deletedMember(user, groupInfo, byMember, deletedMember, withMessages): if active(user) { await MainActor.run { + m.updateGroup(groupInfo) _ = m.upsertGroupMember(groupInfo, deletedMember) if withMessages { m.removeMemberItems(deletedMember, byMember: byMember, groupInfo) @@ -2276,6 +2277,7 @@ func processReceivedMsg(_ res: ChatEvent) async { case let .leftMember(user, groupInfo, member): if active(user) { await MainActor.run { + m.updateGroup(groupInfo) _ = m.upsertGroupMember(groupInfo, member) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 2336207b4a..4218e94224 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -175,7 +175,7 @@ struct GroupChatInfoView: View { if groupInfo.canDelete { deleteGroupButton() } - if groupInfo.membership.memberCurrent { + if groupInfo.membership.memberCurrentOrPending { leaveGroupButton() } } @@ -797,10 +797,11 @@ struct GroupChatInfoView: View { func removeMember(_ groupInfo: GroupInfo, _ mem: GroupMember, dismiss: DismissAction? = nil) { Task { do { - let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) + let (updatedGroupInfo, updatedMembers) = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) await MainActor.run { + ChatModel.shared.updateGroup(updatedGroupInfo) updatedMembers.forEach { updatedMember in - _ = ChatModel.shared.upsertGroupMember(groupInfo, updatedMember) + _ = ChatModel.shared.upsertGroupMember(updatedGroupInfo, updatedMember) } dismiss?() } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index a36632116b..fa7fc7cae4 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -640,10 +640,11 @@ struct GroupMemberInfoView: View { primaryButton: .destructive(Text("Remove")) { Task { do { - let updatedMembers = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) + let (updatedGroupInfo, updatedMembers) = try await apiRemoveMembers(groupInfo.groupId, [mem.groupMemberId]) await MainActor.run { + chatModel.updateGroup(updatedGroupInfo) updatedMembers.forEach { updatedMember in - _ = chatModel.upsertGroupMember(groupInfo, updatedMember) + _ = chatModel.upsertGroupMember(updatedGroupInfo, updatedMember) } dismiss() } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 6d590fcc52..1e747b8019 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -189,7 +189,7 @@ struct ChatListNavLink: View { } .swipeActions(edge: .trailing) { tagChatButton(chat) - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { leaveGroupChatButton(groupInfo) } if groupInfo.canDelete { @@ -214,7 +214,7 @@ struct ChatListNavLink: View { let showReportsButton = chat.chatStats.reportsCount > 0 && groupInfo.membership.memberRole >= .moderator let showClearButton = !chat.chatItems.isEmpty let showDeleteGroup = groupInfo.canDelete - let showLeaveGroup = groupInfo.membership.memberCurrent + let showLeaveGroup = groupInfo.membership.memberCurrentOrPending let totalNumberOfButtons = 1 + (showReportsButton ? 1 : 0) + (showClearButton ? 1 : 0) + (showDeleteGroup ? 1 : 0) + (showLeaveGroup ? 1 : 0) if showClearButton && totalNumberOfButtons <= 3 { diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 59f018e9fe..77b4ef39a8 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -179,8 +179,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -543,8 +543,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -702,8 +702,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -788,8 +788,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.3.4.1-Cm6JGiMgJjo4088oWn41JO.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.0-Adp18CY8iMhDelg0G0VSjh.a */, ); path = Libraries; sourceTree = ""; diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 81fc500072..d068b50412 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2043,7 +2043,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { } public var canDelete: Bool { - return membership.memberRole == .owner || !membership.memberCurrent + return membership.memberRole == .owner || !membership.memberCurrentOrPending } public var canAddMembers: Bool { @@ -2275,6 +2275,10 @@ public struct GroupMember: Identifiable, Decodable, Hashable { } } + public var memberCurrentOrPending: Bool { + memberCurrent || memberPending + } + public func canBeRemoved(groupInfo: GroupInfo) -> Bool { let userRole = groupInfo.membership.memberRole return memberStatus != .memRemoved && memberStatus != .memLeft 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 2a47e22be8..9329fe5dda 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 @@ -1883,7 +1883,7 @@ data class GroupInfo ( get() = membership.memberRole == GroupMemberRole.Owner && membership.memberCurrent val canDelete: Boolean - get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrent + get() = membership.memberRole == GroupMemberRole.Owner || !membership.memberCurrentOrPending val canAddMembers: Boolean get() = membership.memberRole >= GroupMemberRole.Admin && membership.memberActive @@ -2090,6 +2090,9 @@ data class GroupMember ( else -> false } + val memberCurrentOrPending: Boolean get() = + memberCurrent || memberPending + fun canBeRemoved(groupInfo: GroupInfo): Boolean { val userRole = groupInfo.membership.memberRole return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft 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 ebcf502105..12c93888ee 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 @@ -1911,9 +1911,9 @@ object ChatController { return null } - suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean = false): List? { + suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List, withMessages: Boolean = false): Pair>? { val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds, withMessages)) - if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.members + if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.groupInfo to r.res.members if (!(networkErrorAlert(r))) { apiErrorAlert("apiRemoveMembers", generalGetString(MR.strings.error_removing_member), r) } @@ -2603,6 +2603,7 @@ object ChatController { is CR.DeletedMember -> if (active(r.user)) { withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.deletedMember) if (r.withMessages) { chatModel.chatsContext.removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo) @@ -2618,6 +2619,7 @@ object ChatController { is CR.LeftMember -> if (active(r.user)) { withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, r.groupInfo) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member) } withContext(Dispatchers.Main) { 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 cb1794b568..db6eff562e 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 @@ -596,7 +596,7 @@ fun ModalData.GroupChatInfoLayout( val titleId = if (groupInfo.businessChat == null) MR.strings.button_delete_group else MR.strings.button_delete_chat DeleteGroupButton(titleId, deleteGroup) } - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { val titleId = if (groupInfo.businessChat == null) MR.strings.button_leave_group else MR.strings.button_leave_chat LeaveGroupButton(titleId, leaveGroup) } @@ -1055,16 +1055,18 @@ private fun setGroupAlias(chat: Chat, localAlias: String, chatModel: ChatModel) fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List, onSuccess: () -> Unit = {}) { withBGApi { - val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds) - if (updatedMembers != null) { + val r = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds) + if (r != null) { + val (updatedGroupInfo, updatedMembers) = r withContext(Dispatchers.Main) { + chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo) updatedMembers.forEach { updatedMember -> - chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, updatedMember) + chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, updatedMember) } } withContext(Dispatchers.Main) { updatedMembers.forEach { updatedMember -> - chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, updatedMember) + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, updatedGroupInfo, updatedMember) } } onSuccess() 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 5db1ba93f7..0ce0f8fa3c 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 @@ -246,16 +246,18 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c confirmText = generalGetString(MR.strings.remove_member_confirmation), onConfirm = { withBGApi { - val removedMembers = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId)) - if (removedMembers != null) { + 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, groupInfo, removedMember) + chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, removedMember) } } withContext(Dispatchers.Main) { removedMembers.forEach { removedMember -> - chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, groupInfo, removedMember) + chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, updatedGroupInfo, removedMember) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index b70a4e00d1..52b4059eef 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -304,7 +304,7 @@ fun GroupMenuItems( } } GroupMemberStatus.MemAccepted -> { - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu) } if (groupInfo.canDelete) { @@ -326,7 +326,7 @@ fun GroupMenuItems( } } ClearChatAction(chat, showMenu) - if (groupInfo.membership.memberCurrent) { + if (groupInfo.membership.memberCurrentOrPending) { LeaveGroupAction(chat.remoteHostId, groupInfo, chatModel, showMenu) } if (groupInfo.canDelete) { diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 656191d0b5..f1ef1f369f 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2077,8 +2077,8 @@ processChatCommand' vr = \case void $ sendGroupMessage user gInfo scope ([m] <> rcpModMs') msg when (maxVersion (memberChatVRange m) < groupKnockingVersion) $ forM_ (memberConn m) $ \mConn -> do - let msg = XMsgNew $ MCSimple $ extMsgContent (MCText acceptedToGroupMessage) Nothing - void $ sendDirectMemberMessage mConn msg groupId + let msg2 = XMsgNew $ MCSimple $ extMsgContent (MCText acceptedToGroupMessage) Nothing + void $ sendDirectMemberMessage mConn msg2 groupId (m', gInfo') <- withFastStore' $ \db -> do m' <- updateGroupMemberAccepted db user m newMemberStatus role gInfo' <- updateGroupMembersRequireAttention db user gInfo m m' @@ -2227,10 +2227,13 @@ processChatCommand' vr = \case let acis = acis2 <> acis3 <> acis4 errs = errs1 <> errs2 <> errs3 <> errs4 deleted = deleted1 <> deleted2 <> deleted3 <> deleted4 - unless (null acis) $ toView $ CEvtNewChatItems user acis + -- Read group info with updated membersRequireAttention + gInfo' <- withFastStore $ \db -> getGroupInfo db vr user groupId + let acis' = map (updateCIGroupInfo gInfo') acis + unless (null acis') $ toView $ CEvtNewChatItems user acis' unless (null errs) $ toView $ CEvtChatErrors errs - when withMessages $ deleteMessages user gInfo deleted - pure $ CRUserDeletedMembers user gInfo deleted withMessages -- same order is not guaranteed + when withMessages $ deleteMessages user gInfo' deleted + pure $ CRUserDeletedMembers user gInfo' deleted withMessages -- same order is not guaranteed where selectMembers :: [GroupMember] -> (Int, [GroupMember], [GroupMember], [GroupMember], [GroupMember], GroupMemberRole, Bool) selectMembers = foldl' addMember (0, [], [], [], [], GRObserver, False) @@ -2280,8 +2283,17 @@ processChatCommand' vr = \case ts = ciContentTexts content in NewSndChatItemData msg content ts M.empty Nothing Nothing Nothing delMember db m = do - deleteOrUpdateMemberRecordIO db user m + -- We're in a function used in batch member deletion, and since we're passing same gInfo for each member, + -- voided result (updated group info) may have incorrect state of membersRequireAttention. + -- To avoid complicating code by chaining group info updates, + -- instead we re-read it once after deleting all members before response. + void $ deleteOrUpdateMemberRecordIO db user gInfo m pure m {memberStatus = GSMemRemoved} + updateCIGroupInfo :: GroupInfo -> AChatItem -> AChatItem + updateCIGroupInfo gInfo' = \case + AChatItem SCTGroup SMDSnd (GroupChat _gInfo chatScopeInfo) ci -> + AChatItem SCTGroup SMDSnd (GroupChat gInfo' chatScopeInfo) ci + aci -> aci deleteMessages user gInfo@GroupInfo {membership} ms | groupFeatureMemberAllowed SGFFullDelete membership gInfo = deleteGroupMembersCIs user gInfo ms membership | otherwise = markGroupMembersCIsDeleted user gInfo ms membership diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index 60be94ccc9..6200fb2435 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -1564,15 +1564,20 @@ deleteMemberConnection' GroupMember {activeConn} waitDelivery = do deleteAgentConnectionAsync' (aConnId conn) waitDelivery withStore' $ \db -> updateConnectionStatus db conn ConnDeleted -deleteOrUpdateMemberRecord :: User -> GroupMember -> CM () -deleteOrUpdateMemberRecord user member = - withStore' $ \db -> deleteOrUpdateMemberRecordIO db user member +deleteOrUpdateMemberRecord :: User -> GroupInfo -> GroupMember -> CM GroupInfo +deleteOrUpdateMemberRecord user gInfo member = + withStore' $ \db -> deleteOrUpdateMemberRecordIO db user gInfo member -deleteOrUpdateMemberRecordIO :: DB.Connection -> User -> GroupMember -> IO () -deleteOrUpdateMemberRecordIO db user@User {userId} member = +deleteOrUpdateMemberRecordIO :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO GroupInfo +deleteOrUpdateMemberRecordIO db user@User {userId} gInfo member = do + gInfo' <- + if gmRequiresAttention member + then decreaseGroupMembersRequireAttention db user gInfo + else pure gInfo checkGroupMemberHasItems db user member >>= \case Just _ -> updateGroupMemberStatus db userId member GSMemRemoved Nothing -> deleteGroupMember db user member + pure gInfo' sendDirectContactMessages :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM [Either ChatError SndMessage] sendDirectContactMessages user ct events = do diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 2c4946a07a..12e6263ba0 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -2697,10 +2697,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- ? prohibit deleting member if it's the sender - sender should use x.grp.leave deleteMemberConnection member -- undeleted "member connected" chat item will prevent deletion of member record - deleteOrUpdateMemberRecord user member + gInfo' <- deleteOrUpdateMemberRecord user gInfo member when withMessages $ deleteMessages member SMDRcv deleteMemberItem $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) - toView $ CEvtDeletedMember user gInfo m member {memberStatus = GSMemRemoved} withMessages + toView $ CEvtDeletedMember user gInfo' m member {memberStatus = GSMemRemoved} withMessages where checkRole GroupMember {memberRole} a | senderRole < GRAdmin || senderRole < memberRole = @@ -2719,11 +2719,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xGrpLeave gInfo m msg brokerTs = do deleteMemberConnection m -- member record is not deleted to allow creation of "member left" chat item - withStore' $ \db -> updateGroupMemberStatus db userId m GSMemLeft - (gInfo', m', scopeInfo) <- mkGroupChatScope gInfo m - (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEMemberLeft) + gInfo' <- withStore' $ \db -> do + updateGroupMemberStatus db userId m GSMemLeft + if gmRequiresAttention m + then decreaseGroupMembersRequireAttention db user gInfo + else pure gInfo + (gInfo'', m', scopeInfo) <- mkGroupChatScope gInfo' m + (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gInfo'' scopeInfo m') msg brokerTs (CIRcvGroupEvent RGEMemberLeft) groupMsgToView cInfo ci - toView $ CEvtLeftMember user gInfo' m' {memberStatus = GSMemLeft} + toView $ CEvtLeftMember user gInfo'' m' {memberStatus = GSMemLeft} xGrpDel :: GroupInfo -> GroupMember -> RcvMessage -> UTCTime -> CM () xGrpDel gInfo@GroupInfo {membership} m@GroupMember {memberRole} msg brokerTs = do diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 35e7fe96a9..68fdb5c0be 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -81,6 +81,7 @@ module Simplex.Chat.Store.Groups updateGroupMemberStatusById, updateGroupMemberAccepted, updateGroupMembersRequireAttention, + decreaseGroupMembersRequireAttention, increaseGroupMembersRequireAttention, createNewGroupMember, checkGroupMemberHasItems, @@ -1231,24 +1232,28 @@ updateGroupMemberAccepted db User {userId} m@GroupMember {groupMemberId} status pure m {memberStatus = status, memberRole = role, updatedAt = currentTs} updateGroupMembersRequireAttention :: DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> IO GroupInfo -updateGroupMembersRequireAttention db user@User {userId} g@GroupInfo {groupId, membersRequireAttention} member member' +updateGroupMembersRequireAttention db user g member member' | nowRequires && not didRequire = increaseGroupMembersRequireAttention db user g - | not nowRequires && didRequire = do - DB.execute - db - [sql| - UPDATE groups - SET members_require_attention = members_require_attention - 1 - WHERE user_id = ? AND group_id = ? - |] - (userId, groupId) - pure g {membersRequireAttention = membersRequireAttention - 1} + | not nowRequires && didRequire = + decreaseGroupMembersRequireAttention db user g | otherwise = pure g where didRequire = gmRequiresAttention member nowRequires = gmRequiresAttention member' +decreaseGroupMembersRequireAttention :: DB.Connection -> User -> GroupInfo -> IO GroupInfo +decreaseGroupMembersRequireAttention db User {userId} g@GroupInfo {groupId, membersRequireAttention} = do + DB.execute + db + [sql| + UPDATE groups + SET members_require_attention = members_require_attention - 1 + WHERE user_id = ? AND group_id = ? + |] + (userId, groupId) + pure g {membersRequireAttention = membersRequireAttention - 1} + increaseGroupMembersRequireAttention :: DB.Connection -> User -> GroupInfo -> IO GroupInfo increaseGroupMembersRequireAttention db User {userId} g@GroupInfo {groupId, membersRequireAttention} = do DB.execute diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index 76068e61e5..44b915a8fe 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -1335,14 +1335,6 @@ SEARCH group_profiles USING INTEGER PRIMARY KEY (rowid=?) LIST SUBQUERY 1 SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) -Query: - UPDATE groups - SET members_require_attention = members_require_attention - 1 - WHERE user_id = ? AND group_id = ? - -Plan: -SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE user_contact_links SET auto_accept = ?, business_address = ?, auto_accept_incognito = ?, auto_reply_msg_content = ? @@ -4451,6 +4443,14 @@ Query: Plan: SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) +Query: + UPDATE groups + SET members_require_attention = members_require_attention - 1 + WHERE user_id = ? AND group_id = ? + +Plan: +SEARCH groups USING INTEGER PRIMARY KEY (rowid=?) + Query: UPDATE groups SET via_group_link_uri_hash = (SELECT via_contact_uri_hash FROM connections WHERE connection_id = ?) From 507feb92a57023de09e9b519e11c6ed3ecf618b5 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin Date: Fri, 16 May 2025 19:26:46 +0100 Subject: [PATCH 4/4] core: 6.4.0.1 --- simplex-chat.cabal | 2 +- src/Simplex/Chat/Remote.hs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index b220b066af..7b09e9c926 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 6.4.0.0 +version: 6.4.0.1 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index b69d703fcd..b71b103919 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -75,11 +75,11 @@ remoteFilesFolder = "simplex_v1_files" -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [6, 4, 0, 0] +minRemoteCtrlVersion = AppVersion [6, 4, 0, 1] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [6, 4, 0, 0] +minRemoteHostVersion = AppVersion [6, 4, 0, 1] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version