core, ui: full delete member items from DB when fullDelete is on

This commit is contained in:
shum committed 2026-04-21 12:45:41 +00:00
1 parent 27b06dfb38
commit 7f4e90c5f5
5 files changed
+67 -67

No files matched your search

@@ -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])
+1 -1
View File
@@ -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
+12 -14
View File
@@ -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
+5 -5
View File
@@ -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 =
+12 -32
View File
@@ -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