From 106dceabfc39f72aca7924b441f398e4c31c04b5 Mon Sep 17 00:00:00 2001 From: JRoberts <8711996+jr-simplex@users.noreply.github.com> Date: Tue, 17 May 2022 22:48:54 +0400 Subject: [PATCH] mobile: clear chat; allow to delete items deleted by sender (#660) * ios: clear chat * android: clear chat * fix chat stats * fixes * check if deleted * delete from files for groups * android - fixes * Update apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> * Update apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 12 ++++ .../java/chat/simplex/app/model/SimpleXAPI.kt | 13 ++++ .../app/views/chat/item/ChatItemView.kt | 18 ++++- .../app/views/chatlist/ChatListNavLinkView.kt | 69 +++++++++++++++---- .../app/src/main/res/values-ru/strings.xml | 5 ++ .../app/src/main/res/values/strings.xml | 5 ++ apps/ios/Shared/Model/ChatModel.swift | 12 ++++ apps/ios/Shared/Model/Shared/APITypes.swift | 6 ++ apps/ios/Shared/Model/SimpleXAPI.swift | 16 +++++ apps/ios/Shared/Views/Chat/ChatView.swift | 9 ++- .../Views/ChatList/ChatListNavLink.swift | 26 +++++++ apps/ios/SimpleX.xcodeproj/project.pbxproj | 50 ++++++-------- src/Simplex/Chat.hs | 11 +-- src/Simplex/Chat/Store.hs | 26 ++++--- 14 files changed, 215 insertions(+), 63 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 8dfab64c3..f8820cd93 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -172,6 +172,18 @@ class ChatModel(val controller: ChatController) { } } + fun clearChat(cInfo: ChatInfo) { + // clear preview + val i = getChatIndex(cInfo.id) + if (i >= 0) { + chats[i] = chats[i]?.copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats()) + } + // clear current chat + if (chatId.value == cInfo.id) { + chatItems.clear() + } + } + fun markChatItemsRead(cInfo: ChatInfo) { val chatIdx = getChatIndex(cInfo.id) // update current chat diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 3078cb4d7..a2ab13f6c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -274,6 +274,13 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt return false } + suspend fun apiClearChat(type: ChatType, id: Long): Boolean { + val r = sendCmd(CC.ApiClearChat(type, id)) + if (r is CR.ChatCleared) return true + Log.e(TAG, "apiClearChat bad response: ${r.responseType} ${r.details}") + return false + } + suspend fun apiUpdateProfile(profile: Profile): Profile? { val r = sendCmd(CC.ApiUpdateProfile(profile)) if (r is CR.UserProfileNoChange) return profile @@ -717,6 +724,7 @@ sealed class CC { class AddContact: CC() class Connect(val connReq: String): CC() class ApiDeleteChat(val type: ChatType, val id: Long): CC() + class ApiClearChat(val type: ChatType, val id: Long): CC() class ApiUpdateProfile(val profile: Profile): CC() class ApiParseMarkdown(val text: String): CC() class CreateMyAddress: CC() @@ -750,6 +758,7 @@ sealed class CC { is AddContact -> "/connect" is Connect -> "/connect $connReq" is ApiDeleteChat -> "/_delete ${chatRef(type, id)}" + is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" is ApiUpdateProfile -> "/_profile ${json.encodeToString(profile)}" is ApiParseMarkdown -> "/_parse $text" is CreateMyAddress -> "/address" @@ -784,6 +793,7 @@ sealed class CC { is AddContact -> "addContact" is Connect -> "connect" is ApiDeleteChat -> "apiDeleteChat" + is ApiClearChat -> "apiClearChat" is ApiUpdateProfile -> "updateProfile" is ApiParseMarkdown -> "apiParseMarkdown" is CreateMyAddress -> "createMyAddress" @@ -855,6 +865,7 @@ sealed class CR { @Serializable @SerialName("sentInvitation") class SentInvitation: CR() @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val contact: Contact): CR() @Serializable @SerialName("contactDeleted") class ContactDeleted(val contact: Contact): CR() + @Serializable @SerialName("chatCleared") class ChatCleared(val chatInfo: ChatInfo): CR() @Serializable @SerialName("userProfileNoChange") class UserProfileNoChange: CR() @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR() @Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List? = null): CR() @@ -912,6 +923,7 @@ sealed class CR { is SentInvitation -> "sentInvitation" is ContactAlreadyExists -> "contactAlreadyExists" is ContactDeleted -> "contactDeleted" + is ChatCleared -> "chatCleared" is UserProfileNoChange -> "userProfileNoChange" is UserProfileUpdated -> "userProfileUpdated" is ParsedMarkdown -> "apiParsedMarkdown" @@ -970,6 +982,7 @@ sealed class CR { is SentInvitation -> noDetails() is ContactAlreadyExists -> json.encodeToString(contact) is ContactDeleted -> json.encodeToString(contact) + is ChatCleared -> json.encodeToString(chatInfo) is UserProfileNoChange -> noDetails() is UserProfileUpdated -> json.encodeToString(toProfile) is ParsedMarkdown -> json.encodeToString(formattedText) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 21fc8280d..b8e45484c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -100,7 +100,7 @@ fun ChatItemView( }) } } - if (cItem.chatDir.sent && cItem.meta.editable) { + if (cItem.meta.editable) { ItemAction(stringResource(R.string.edit_verb), Icons.Filled.Edit, onClick = { composeState.value = ComposeState(editingItem = cItem) showMenu.value = false @@ -116,6 +116,22 @@ fun ChatItemView( color = Color.Red ) } + } else if (cItem.isDeletedContent) { + DropdownMenu( + expanded = showMenu.value, + onDismissRequest = { showMenu.value = false }, + Modifier.width(220.dp) + ) { + ItemAction( + stringResource(R.string.delete_verb), + Icons.Outlined.Delete, + onClick = { + showMenu.value = false + deleteMessageAlertDialog(cItem, deleteMessage = deleteMessage) + }, + color = Color.Red + ) + } } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt index 888447965..72724bf71 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Restore import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -53,6 +54,22 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } ) }, + clearChat = { + AlertManager.shared.showAlertMsg( + title = generalGetString(R.string.clear_chat_question), + text = generalGetString(R.string.clear_chat_warning), + confirmText = generalGetString(R.string.clear_verb), + onConfirm = { + val cInfo = chat.chatInfo + withApi { + val r = chatModel.controller.apiClearChat(cInfo.chatType, cInfo.apiId) + if (r) { + chatModel.clearChat(cInfo) + } + } + } + ) + }, ) } @@ -159,14 +176,22 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) { } @Composable -fun ChatListNavLinkLayout(chat: Chat, click: () -> Unit, deleteContact: () -> Unit) { +fun ChatListNavLinkLayout( + chat: Chat, + click: () -> Unit, + deleteContact: () -> Unit, + clearChat: () -> Unit +) { val showMenu = remember { mutableStateOf(false) } Surface( modifier = Modifier .fillMaxWidth() .combinedClickable( onClick = click, - onLongClick = { if (chat.chatInfo is ChatInfo.Direct) showMenu.value = true } + onLongClick = { + if (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) + showMenu.value = true + } ) .height(88.dp) ) { @@ -185,21 +210,32 @@ fun ChatListNavLinkLayout(chat: Chat, click: () -> Unit, deleteContact: () -> Un is ChatInfo.ContactConnection -> ContactConnectionView(chat.chatInfo.contactConnection) } } - if (chat.chatInfo is ChatInfo.Direct) { + Box(Modifier.padding(horizontal = 16.dp)) { DropdownMenu( expanded = showMenu.value, onDismissRequest = { showMenu.value = false }, Modifier.width(220.dp) ) { - ItemAction( - stringResource(R.string.delete_verb), - Icons.Outlined.Delete, - onClick = { - deleteContact() - showMenu.value = false - }, - color = Color.Red - ) + if (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) + ItemAction( + stringResource(R.string.clear_verb), + Icons.Outlined.Restore, + onClick = { + clearChat() + showMenu.value = false + } + ) + if (chat.chatInfo is ChatInfo.Direct) { + ItemAction( + stringResource(R.string.delete_verb), + Icons.Outlined.Delete, + onClick = { + deleteContact() + showMenu.value = false + }, + color = Color.Red + ) + } } } } @@ -229,7 +265,8 @@ fun PreviewChatListNavLinkDirect() { chatStats = Chat.ChatStats() ), click = {}, - deleteContact = {} + deleteContact = {}, + clearChat = {} ) } } @@ -257,7 +294,8 @@ fun PreviewChatListNavLinkGroup() { chatStats = Chat.ChatStats() ), click = {}, - deleteContact = {} + deleteContact = {}, + clearChat = {} ) } } @@ -278,7 +316,8 @@ fun PreviewChatListNavLinkContactRequest() { chatStats = Chat.ChatStats() ), click = {}, - deleteContact = {} + deleteContact = {}, + clearChat = {} ) } } diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 9667b2411..6116d4f8d 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -168,6 +168,11 @@ Принять Отклонить + + Очистить чат? + Все сообщения будут удалены - это действие нельзя отменить! Сообщения будут удалены только для вас. + Очистить + Вы пригласили ваш контакт Вы приняли приглашение соединиться diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index 7143c0e31..119fe2247 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -169,6 +169,11 @@ Accept Reject + + Clear chat? + All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. + Clear + You invited your contact You accepted connection diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 578552a99..797e5051a 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -185,6 +185,18 @@ final class ChatModel: ObservableObject { } } + func clearChat(_ cInfo: ChatInfo) { + // clear preview + if let chat = getChat(cInfo.id) { + chat.chatItems = [] + chat.chatStats = ChatStats() + } + // clear current chat + if chatId == cInfo.id { + chatItems = [] + } + } + func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) { // update preview if let i = getChatIndex(cInfo.id) { diff --git a/apps/ios/Shared/Model/Shared/APITypes.swift b/apps/ios/Shared/Model/Shared/APITypes.swift index 6873c8bb4..2a651222e 100644 --- a/apps/ios/Shared/Model/Shared/APITypes.swift +++ b/apps/ios/Shared/Model/Shared/APITypes.swift @@ -30,6 +30,7 @@ enum ChatCommand { case addContact case connect(connReq: String) case apiDeleteChat(type: ChatType, id: Int64) + case apiClearChat(type: ChatType, id: Int64) case apiUpdateProfile(profile: Profile) case apiParseMarkdown(text: String) case createMyAddress @@ -72,6 +73,7 @@ enum ChatCommand { case .addContact: return "/connect" case let .connect(connReq): return "/connect \(connReq)" case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))" + case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" case let .apiUpdateProfile(profile): return "/_profile \(encodeJSON(profile))" case let .apiParseMarkdown(text): return "/_parse \(text)" case .createMyAddress: return "/address" @@ -114,6 +116,7 @@ enum ChatCommand { case .addContact: return "addContact" case .connect: return "connect" case .apiDeleteChat: return "apiDeleteChat" + case .apiClearChat: return "apiClearChat" case .apiUpdateProfile: return "apiUpdateProfile" case .apiParseMarkdown: return "apiParseMarkdown" case .createMyAddress: return "createMyAddress" @@ -161,6 +164,7 @@ enum ChatResponse: Decodable, Error { case sentInvitation case contactAlreadyExists(contact: Contact) case contactDeleted(contact: Contact) + case chatCleared(chatInfo: ChatInfo) case userProfileNoChange case userProfileUpdated(fromProfile: Profile, toProfile: Profile) case apiParsedMarkdown(formattedText: [FormattedText]?) @@ -222,6 +226,7 @@ enum ChatResponse: Decodable, Error { case .sentInvitation: return "sentInvitation" case .contactAlreadyExists: return "contactAlreadyExists" case .contactDeleted: return "contactDeleted" + case .chatCleared: return "chatCleared" case .userProfileNoChange: return "userProfileNoChange" case .userProfileUpdated: return "userProfileUpdated" case .apiParsedMarkdown: return "apiParsedMarkdown" @@ -284,6 +289,7 @@ enum ChatResponse: Decodable, Error { case .sentInvitation: return noDetails case let .contactAlreadyExists(contact): return String(describing: contact) case let .contactDeleted(contact): return String(describing: contact) + case let .chatCleared(chatInfo): return String(describing: chatInfo) case .userProfileNoChange: return noDetails case let .userProfileUpdated(_, toProfile): return String(describing: toProfile) case let .apiParsedMarkdown(formattedText): return String(describing: formattedText) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 8a1ad8e92..1ff932818 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -286,6 +286,22 @@ func apiDeleteChat(type: ChatType, id: Int64) async throws { throw r } +func apiClearChat(type: ChatType, id: Int64) async throws { + let r = await chatSendCmd(.apiClearChat(type: type, id: id), bgTask: false) + if case .chatCleared = r { return } + throw r +} + +func clearChat(_ chat: Chat) async { + do { + let cInfo = chat.chatInfo + try await apiClearChat(type: cInfo.chatType, id: cInfo.apiId) + DispatchQueue.main.async { ChatModel.shared.clearChat(cInfo) } + } catch { + logger.error("clearChat apiClearChat error: \(responseError(error))") + } +} + func apiUpdateProfile(profile: Profile) async throws -> Profile? { let r = await chatSendCmd(.apiUpdateProfile(profile: profile)) switch r { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 10e21774c..4f0df6d90 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -178,9 +178,12 @@ struct ChatView: View { Button(role: .destructive) { showDeleteMessage = true deletingItem = ci - } label: { - Label("Delete", systemImage: "trash") - } + } label: { Label("Delete", systemImage: "trash") } + } else if ci.isDeletedContent() { + Button(role: .destructive) { + showDeleteMessage = true + deletingItem = ci + } label: { Label("Delete", systemImage: "trash") } } } .confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) { diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index dd2a16cf9..ae70f343b 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -54,6 +54,9 @@ struct ChatListNavLink: View { markReadButton() } } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + clearChatButton() + } .swipeActions(edge: .trailing) { Button(role: .destructive) { AlertManager.shared.showAlert( @@ -90,6 +93,9 @@ struct ChatListNavLink: View { } } .swipeActions(edge: .trailing, allowsFullSwipe: true) { + clearChatButton() + } + .swipeActions(edge: .trailing) { Button(role: .destructive) { AlertManager.shared.showAlert(deleteGroupAlert(groupInfo)) } label: { @@ -108,6 +114,15 @@ struct ChatListNavLink: View { .tint(Color.accentColor) } + private func clearChatButton() -> some View { + Button { + AlertManager.shared.showAlert(clearChatAlert()) + } label: { + Label("Clear", systemImage: "gobackward") + } + .tint(Color.orange) + } + private func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View { ContactRequestView(contactRequest: contactRequest, chat: chat) .swipeActions(edge: .trailing, allowsFullSwipe: true) { @@ -173,6 +188,17 @@ struct ChatListNavLink: View { ) } + private func clearChatAlert() -> Alert { + Alert( + title: Text("Clear chat?"), + message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), + primaryButton: .destructive(Text("Clear")) { + Task { await clearChat(chat) } + }, + secondaryButton: .cancel() + ) + } + private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert { Alert( title: Text("Delete group"), diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index d2debf35a..8e7c1bd2b 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -90,19 +90,14 @@ 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; }; 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; 5CEACCED27DEA495000BD591 /* MsgContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */; }; - 5CFE0916282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0911282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a */; }; - 5CFE0917282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0911282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a */; }; - 5CFE0918282EE05E0002594B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0912282EE05E0002594B /* libffi.a */; }; - 5CFE0919282EE05E0002594B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0912282EE05E0002594B /* libffi.a */; }; - 5CFE091A282EE05E0002594B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0913282EE05E0002594B /* libgmpxx.a */; }; - 5CFE091B282EE05E0002594B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0913282EE05E0002594B /* libgmpxx.a */; }; - 5CFE091C282EE05E0002594B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0914282EE05E0002594B /* libgmp.a */; }; - 5CFE091D282EE05E0002594B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0914282EE05E0002594B /* libgmp.a */; }; - 5CFE091E282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0915282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a */; }; - 5CFE091F282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CFE0915282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a */; }; 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 640F50E327CF991C001E05C2 /* SMPServers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F50E227CF991C001E05C2 /* SMPServers.swift */; }; + 644320022833BF0800CA19E0 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64431FFD2833BF0800CA19E0 /* libgmp.a */; }; + 644320032833BF0800CA19E0 /* libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64431FFE2833BF0800CA19E0 /* libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L.a */; }; + 644320042833BF0800CA19E0 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64431FFF2833BF0800CA19E0 /* libgmpxx.a */; }; + 644320052833BF0800CA19E0 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644320002833BF0800CA19E0 /* libffi.a */; }; + 644320062833BF0800CA19E0 /* libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 644320012833BF0800CA19E0 /* libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L-ghc8.10.7.a */; }; 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6454036E2822A9750090DDFF /* ComposeFileView.swift */; }; 648010AB281ADD15009009B9 /* CIFileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648010AA281ADD15009009B9 /* CIFileView.swift */; }; 649BCDA0280460FD00C3A862 /* ComposeImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCD9F280460FD00C3A862 /* ComposeImageView.swift */; }; @@ -206,13 +201,13 @@ 5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = ""; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; - 5CFE0911282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a"; sourceTree = ""; }; - 5CFE0912282EE05E0002594B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5CFE0913282EE05E0002594B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5CFE0914282EE05E0002594B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5CFE0915282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a"; sourceTree = ""; }; 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 640F50E227CF991C001E05C2 /* SMPServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServers.swift; sourceTree = ""; }; + 64431FFD2833BF0800CA19E0 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 64431FFE2833BF0800CA19E0 /* libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L.a"; sourceTree = ""; }; + 64431FFF2833BF0800CA19E0 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 644320002833BF0800CA19E0 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 644320012833BF0800CA19E0 /* libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L-ghc8.10.7.a"; sourceTree = ""; }; 6454036E2822A9750090DDFF /* ComposeFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeFileView.swift; sourceTree = ""; }; 648010AA281ADD15009009B9 /* CIFileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIFileView.swift; sourceTree = ""; }; 6493D667280ED77F007A76FB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -228,13 +223,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CFE091A282EE05E0002594B /* libgmpxx.a in Frameworks */, - 5CFE091E282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a in Frameworks */, - 5CFE091C282EE05E0002594B /* libgmp.a in Frameworks */, - 5CFE0916282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a in Frameworks */, + 644320042833BF0800CA19E0 /* libgmpxx.a in Frameworks */, + 644320032833BF0800CA19E0 /* libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L.a in Frameworks */, 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */, 5C764E83279C748B000C6508 /* libz.tbd in Frameworks */, - 5CFE0918282EE05E0002594B /* libffi.a in Frameworks */, + 644320052833BF0800CA19E0 /* libffi.a in Frameworks */, + 644320022833BF0800CA19E0 /* libgmp.a in Frameworks */, + 644320062833BF0800CA19E0 /* libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L-ghc8.10.7.a in Frameworks */, 5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -250,13 +245,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CFE091F282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a in Frameworks */, 5CDCAD5F28187D6900503DA2 /* libiconv.tbd in Frameworks */, - 5CFE091B282EE05E0002594B /* libgmpxx.a in Frameworks */, 5CDCAD6128187D8000503DA2 /* libz.tbd in Frameworks */, - 5CFE0917282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a in Frameworks */, - 5CFE091D282EE05E0002594B /* libgmp.a in Frameworks */, - 5CFE0919282EE05E0002594B /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -305,11 +295,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CFE0912282EE05E0002594B /* libffi.a */, - 5CFE0914282EE05E0002594B /* libgmp.a */, - 5CFE0913282EE05E0002594B /* libgmpxx.a */, - 5CFE0915282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ-ghc8.10.7.a */, - 5CFE0911282EE05E0002594B /* libHSsimplex-chat-2.0.1-4e10XUnKa54A7I6rlOZBZ.a */, + 644320002833BF0800CA19E0 /* libffi.a */, + 64431FFD2833BF0800CA19E0 /* libgmp.a */, + 64431FFF2833BF0800CA19E0 /* libgmpxx.a */, + 644320012833BF0800CA19E0 /* libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L-ghc8.10.7.a */, + 64431FFE2833BF0800CA19E0 /* libHSsimplex-chat-2.0.1-IL4OVvclaJm1EwZEKXfP4L.a */, ); path = Libraries; sourceTree = ""; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index f53cd517c..fe3eadd7e 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -411,11 +411,12 @@ processChatCommand = \case CTGroup -> do gInfo <- withStore $ \st -> getGroupInfo st user chatId ciIdsAndFileInfo <- withStore $ \st -> getGroupChatItemIdsAndFileInfo st userId chatId - forM_ ciIdsAndFileInfo $ \(itemId, fileInfo_) -> do - forM_ fileInfo_ $ \fileInfo -> do - cancelFile user fileInfo - withFilesFolder $ \filesFolder -> deleteFile filesFolder fileInfo - void $ withStore $ \st -> deleteGroupChatItemInternal st user gInfo itemId + forM_ ciIdsAndFileInfo $ \(itemId, itemDeleted, fileInfo_) -> + unless itemDeleted $ do + forM_ fileInfo_ $ \fileInfo -> do + cancelFile user fileInfo + withFilesFolder $ \filesFolder -> deleteFile filesFolder fileInfo + void $ withStore $ \st -> deleteGroupChatItemInternal st user gInfo itemId pure $ CRChatCleared (AChatInfo SCTGroup (GroupChat gInfo)) CTContactConnection -> pure $ chatCmdError "not supported" CTContactRequest -> pure $ chatCmdError "not supported" diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index 288635670..df205f996 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -2312,25 +2312,31 @@ getContactChatItemIdsAndFileInfo st userId contactId = |] (userId, contactId) -getGroupChatItemIdsAndFileInfo :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> m [(ChatItemId, Maybe CIFileInfo)] +toItemIdAndFileInfo :: (ChatItemId, Maybe Int64, Maybe ACIFileStatus, Maybe FilePath) -> (ChatItemId, Maybe CIFileInfo) +toItemIdAndFileInfo (chatItemId, fileId_, fileStatus_, filePath) = + case (fileId_, fileStatus_) of + (Just fileId, Just fileStatus) -> (chatItemId, Just CIFileInfo {fileId, fileStatus, filePath}) + _ -> (chatItemId, Nothing) + +getGroupChatItemIdsAndFileInfo :: MonadUnliftIO m => SQLiteStore -> UserId -> Int64 -> m [(ChatItemId, Bool, Maybe CIFileInfo)] getGroupChatItemIdsAndFileInfo st userId groupId = liftIO . withTransaction st $ \db -> - map toItemIdAndFileInfo + map toItemIdDeletedAndFileInfo <$> DB.query db [sql| - SELECT i.chat_item_id, f.file_id, f.ci_file_status, f.file_path + SELECT i.chat_item_id, i.item_deleted, f.file_id, f.ci_file_status, f.file_path FROM chat_items i LEFT JOIN files f ON f.chat_item_id = i.chat_item_id WHERE i.user_id = ? AND i.group_id = ? |] (userId, groupId) -toItemIdAndFileInfo :: (ChatItemId, Maybe Int64, Maybe ACIFileStatus, Maybe FilePath) -> (ChatItemId, Maybe CIFileInfo) -toItemIdAndFileInfo (chatItemId, fileId_, fileStatus_, filePath) = +toItemIdDeletedAndFileInfo :: (ChatItemId, Bool, Maybe Int64, Maybe ACIFileStatus, Maybe FilePath) -> (ChatItemId, Bool, Maybe CIFileInfo) +toItemIdDeletedAndFileInfo (chatItemId, itemDeleted, fileId_, fileStatus_, filePath) = case (fileId_, fileStatus_) of - (Just fileId, Just fileStatus) -> (chatItemId, Just CIFileInfo {fileId, fileStatus, filePath}) - _ -> (chatItemId, Nothing) + (Just fileId, Just fileStatus) -> (chatItemId, itemDeleted, Just CIFileInfo {fileId, fileStatus, filePath}) + _ -> (chatItemId, itemDeleted, Nothing) createNewSndMessage :: StoreMonad m => SQLiteStore -> TVar ChaChaDRG -> ConnOrGroupId -> (SharedMsgId -> NewMessage) -> m SndMessage createNewSndMessage st gVar connOrGroupId mkMessage = @@ -2658,7 +2664,7 @@ getDirectChatPreviews_ db User {userId} = do LEFT JOIN ( SELECT contact_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE item_status = ? + WHERE item_status = ? AND item_deleted != 1 GROUP BY contact_id ) ChatStats ON ChatStats.contact_id = ct.contact_id LEFT JOIN chat_items ri ON i.quoted_shared_msg_id = ri.shared_msg_id @@ -2732,7 +2738,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do LEFT JOIN ( SELECT group_id, COUNT(1) AS UnreadCount, MIN(chat_item_id) AS MinUnread FROM chat_items - WHERE item_status = ? + WHERE item_status = ? AND item_deleted != 1 GROUP BY group_id ) ChatStats ON ChatStats.group_id = g.group_id LEFT JOIN group_members m ON m.group_member_id = i.group_member_id @@ -3452,6 +3458,7 @@ deleteGroupChatItemInternal st user gInfo itemId = currentTs <- liftIO getCurrentTime ci <- deleteGroupChatItem_ db user gInfo itemId CIDMInternal True currentTs setChatItemMessagesDeleted_ db itemId + DB.execute db "DELETE FROM files WHERE chat_item_id = ?" (Only itemId) pure ci deleteGroupChatItemRcvBroadcast :: StoreMonad m => SQLiteStore -> User -> GroupInfo -> ChatItemId -> MessageId -> m AChatItem @@ -3463,6 +3470,7 @@ deleteGroupChatItemSndBroadcast st user gInfo itemId msgId = liftIOEither . withTransaction st $ \db -> do ci <- deleteGroupChatItemBroadcast_ db user gInfo itemId True msgId setChatItemMessagesDeleted_ db itemId + DB.execute db "DELETE FROM files WHERE chat_item_id = ?" (Only itemId) pure ci deleteGroupChatItemBroadcast_ :: DB.Connection -> User -> GroupInfo -> ChatItemId -> Bool -> MessageId -> IO (Either StoreError AChatItem)