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 591dde89cd..1eb5c201e0 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 @@ -696,23 +696,45 @@ object ChatModel { } suspend fun removeMemberItems(rhId: Long?, removedMember: GroupMember, byMember: GroupMember, groupInfo: GroupInfo) { - fun removedUpdatedItem(item: ChatItem): ChatItem? { - val newContent = when { - item.chatDir is CIDirection.GroupSnd && removedMember.groupMemberId == groupInfo.membership.groupMemberId -> CIContent.SndModerated - item.chatDir is CIDirection.GroupRcv && item.chatDir.groupMember.groupMemberId == removedMember.groupMemberId -> CIContent.RcvModerated - else -> return null - } - val updatedItem = item.copy( - meta = item.meta.copy(itemDeleted = CIDeleted.Moderated(Clock.System.now(), byGroupMember = byMember)), - content = if (groupInfo.fullGroupPreferences.fullDelete.on) newContent else item.content - ) - if (item.isActiveReport) { - decreaseGroupReportsCounter(rhId, groupInfo.id) - } - return updatedItem - } + fun isMemberItem(item: ChatItem): Boolean = + (item.chatDir is CIDirection.GroupSnd && removedMember.groupMemberId == groupInfo.membership.groupMemberId) || + (item.chatDir is CIDirection.GroupRcv && item.chatDir.groupMember.groupMemberId == removedMember.groupMemberId) val cInfo = ChatInfo.Group(groupInfo, groupChatScope = null) // TODO [knocking] review + // When fullDelete is on, items are fully deleted from DB by backend — + // remove them from UI immediately so no "moderated" bubbles are shown. + if (groupInfo.fullGroupPreferences.fullDelete.on) { + // Update chat list preview + val i = getChatIndex(rhId, groupInfo.id) + if (i >= 0) { + val chat = chats[i] + if (chat.chatItems.isNotEmpty() && isMemberItem(chat.chatItems[0])) { + chats.value[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy)) + } + } + // Remove from open chat view + if (chatId.value == groupInfo.id) { + chatItems.removeAllAndNotify { item -> + val remove = isMemberItem(item) + if (remove) { + AudioPlayer.stop(item) // stop voice message if playing + if (item.isRcvNew) decreaseCounterInPrimaryContext(rhId, cInfo.id) + if (item.isActiveReport) decreaseGroupReportsCounter(rhId, groupInfo.id) + } + remove + } + } + return + } + // When fullDelete is off, mark items as moderated (original behavior) + fun removedUpdatedItem(item: ChatItem): ChatItem? { + if (!isMemberItem(item)) return null + val updatedItem = item.copy( + meta = item.meta.copy(itemDeleted = CIDeleted.Moderated(Clock.System.now(), byGroupMember = byMember)), + ) + if (item.isActiveReport) decreaseGroupReportsCounter(rhId, groupInfo.id) + return updatedItem + } if (chatId.value == groupInfo.id) { for (i in 0 until chatItems.value.size) { val updatedItem = removedUpdatedItem(chatItems.value[i]) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 042280e062..b34116d9ad 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -2890,7 +2890,7 @@ processChatCommand vr nm = \case void $ deleteOrUpdateMemberRecordIO db user gInfo m pure m {memberStatus = GSMemRemoved} deleteMessages user gInfo@GroupInfo {membership} ms - | groupFeatureUserAllowed SGFFullDelete gInfo = deleteGroupMembersCIs user gInfo ms membership + | groupFeatureUserAllowed SGFFullDelete gInfo = fullDeleteGroupMembersCIs user gInfo ms | otherwise = markGroupMembersCIsDeleted user gInfo ms membership APILeaveGroup groupId -> withUser $ \user@User {userId} -> do gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db vr user groupId diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index f82b6884b2..e236bbfb20 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -510,23 +510,21 @@ updateACIGroupInfo gInfo' = \case AChatItem SCTGroup dir (GroupChat gInfo' chatScopeInfo) ci aci -> aci -deleteGroupMemberCIs :: MsgDirectionI d => User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> CM () -deleteGroupMemberCIs user gInfo member byGroupMember msgDir = do - deletedTs <- liftIO getCurrentTime - filesInfo <- withStore' $ \db -> deleteGroupMemberCIs_ db user gInfo member byGroupMember msgDir deletedTs +fullDeleteGroupMemberCIs :: User -> GroupInfo -> GroupMember -> CM () +fullDeleteGroupMemberCIs user gInfo member = do + filesInfo <- withStore' $ \db -> getGroupMemberFileInfo db user gInfo member deleteCIFiles user filesInfo + withStore' $ \db -> do + fullDeleteMemberCIs db user gInfo member + deleteGroupMember db user member -deleteGroupMembersCIs :: User -> GroupInfo -> [GroupMember] -> GroupMember -> CM () -deleteGroupMembersCIs user gInfo members byGroupMember = do - deletedTs <- liftIO getCurrentTime - filesInfo <- withStore' $ \db -> fmap concat $ forM members $ \m -> deleteGroupMemberCIs_ db user gInfo m byGroupMember SMDRcv deletedTs +fullDeleteGroupMembersCIs :: User -> GroupInfo -> [GroupMember] -> CM () +fullDeleteGroupMembersCIs user gInfo members = do + filesInfo <- withStore' $ \db -> fmap concat $ forM members $ \m -> getGroupMemberFileInfo db user gInfo m deleteCIFiles user filesInfo - -deleteGroupMemberCIs_ :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> UTCTime -> IO [CIFileInfo] -deleteGroupMemberCIs_ db user gInfo member byGroupMember msgDir deletedTs = do - fs <- getGroupMemberFileInfo db user gInfo member - updateMemberCIsModerated db user gInfo member byGroupMember msgDir deletedTs - pure fs + withStore' $ \db -> forM_ members $ \m -> do + fullDeleteMemberCIs db user gInfo m + deleteGroupMember db user m deleteLocalCIs :: User -> NoteFolder -> [CChatItem 'CTLocal] -> Bool -> Bool -> CM ChatResponse deleteLocalCIs user nf items byUser timed = do diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index e92622b60c..722d3e99b8 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -3072,7 +3072,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = unless (isUserGrpFwdRelay gInfo) $ deleteGroupConnections user gInfo False withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemRemoved let membership' = membership {memberStatus = GSMemRemoved} - when withMessages $ deleteMessages gInfo membership' SMDSnd + when withMessages $ deleteMessages gInfo membership' deleteMemberItem gInfo RGEUserDeleted toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned pure $ Just DJSGroup {jobSpec = DJRelayRemoved} @@ -3100,7 +3100,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = gInfo'' <- updatePublicGroupData user gInfo' let wasDeleted = memberStatus == GSMemRemoved || memberStatus == GSMemLeft deletedMember' = deletedMember {memberStatus = GSMemRemoved} - when withMessages $ deleteMessages gInfo'' deletedMember' SMDRcv + when withMessages $ deleteMessages gInfo'' deletedMember' unless wasDeleted $ deleteMemberItem gInfo'' $ RGEMemberDeleted groupMemberId (fromLocalProfile memberProfile) toView $ CEvtDeletedMember user gInfo'' m deletedMember' withMessages msgSigned pure deliveryScope @@ -3113,9 +3113,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (gi', m', scopeInfo) <- mkGroupChatScope gi m (ci, cInfo) <- saveRcvChatItemNoParse user (CDGroupRcv gi' scopeInfo m') msg brokerTs (CIRcvGroupEvent gEvent) groupMsgToView cInfo ci - deleteMessages :: MsgDirectionI d => GroupInfo -> GroupMember -> SMsgDirection d -> CM () - deleteMessages gInfo' delMem msgDir - | groupFeatureMemberAllowed SGFFullDelete m gInfo' = deleteGroupMemberCIs user gInfo' delMem m msgDir + deleteMessages :: GroupInfo -> GroupMember -> CM () + deleteMessages gInfo' delMem + | groupFeatureMemberAllowed SGFFullDelete m gInfo' = fullDeleteGroupMemberCIs user gInfo' delMem | otherwise = markGroupMemberCIsDeleted user gInfo' delMem m forwardToMember :: GroupMember -> CM () forwardToMember member = diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 5d433088a4..8f6d5ba039 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -65,7 +65,7 @@ module Simplex.Chat.Store.Messages updateGroupCIMentions, deleteGroupChatItem, updateGroupChatItemModerated, - updateMemberCIsModerated, + fullDeleteMemberCIs, updateGroupCIBlockedByAdmin, markGroupChatItemDeleted, markMemberCIsDeleted, @@ -2814,39 +2814,19 @@ updateGroupChatItemModerated db User {userId} GroupInfo {groupId} ci m@GroupMemb (deletedTs, groupMemberId, toContent, toText, currentTs, userId, groupId, itemId) pure ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just deletedTs) m), editable = False, deletable = False}, formattedText = Nothing} -updateMemberCIsModerated :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> GroupMember -> GroupMember -> SMsgDirection d -> UTCTime -> IO () -updateMemberCIsModerated db User {userId} GroupInfo {groupId, membership} member byGroupMember md deletedTs = do - itemIds <- updateCIs =<< getCurrentTime -#if defined(dbPostgres) - let inItemIds = Only $ In (map fromOnly itemIds) - DB.execute db "DELETE FROM messages WHERE message_id IN (SELECT message_id FROM chat_item_messages WHERE chat_item_id IN ?)" inItemIds - DB.execute db "DELETE FROM chat_item_versions WHERE chat_item_id IN ?" inItemIds -#else - DB.executeMany db deleteChatItemMessagesQuery itemIds - DB.executeMany db "DELETE FROM chat_item_versions WHERE chat_item_id = ?" itemIds -#endif +fullDeleteMemberCIs :: DB.Connection -> User -> GroupInfo -> GroupMember -> IO () +fullDeleteMemberCIs db User {userId} GroupInfo {groupId, membership} member + | memId == groupMemberId' membership = deleteWithCond " AND group_member_id IS NULL AND item_sent = 1 " (userId, groupId) + | otherwise = deleteWithCond " AND group_member_id = ? " (userId, groupId, memId) where memId = groupMemberId' member - updateQuery = - [sql| - UPDATE chat_items - SET item_deleted = 1, item_deleted_ts = ?, item_deleted_by_group_member_id = ?, item_content = ?, item_text = ?, updated_at = ? - WHERE user_id = ? AND group_id = ? - |] - updateCIs :: UTCTime -> IO [Only Int64] - updateCIs currentTs - | memId == groupMemberId' membership = - DB.query - db - (updateQuery <> " AND group_member_id IS NULL AND item_sent = 1 RETURNING chat_item_id") - (columns :. (userId, groupId)) - | otherwise = - DB.query - db - (updateQuery <> " AND group_member_id = ? RETURNING chat_item_id") - (columns :. (userId, groupId, memId)) - where - columns = (deletedTs, groupMemberId' byGroupMember, msgDirToModeratedContent_ md, ciModeratedText, currentTs) + deleteWithCond :: ToRow p => Query -> p -> IO () + deleteWithCond cond params = do + let ciQuery = "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? " <> cond + -- delete messages first (they don't cascade from chat_items), using chat_item_messages join + DB.execute db ("DELETE FROM messages WHERE message_id IN (SELECT message_id FROM chat_item_messages WHERE chat_item_id IN (" <> ciQuery <> "))") params + -- delete chat_items; chat_item_messages, chat_item_versions, chat_item_mentions, files, calls, group_snd_item_statuses all cascade + DB.execute db ("DELETE FROM chat_items WHERE user_id = ? AND group_id = ? " <> cond) params updateGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> UTCTime -> IO (ChatItem 'CTGroup d) updateGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci deletedTs = do