diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index f733ed6a16..861784e513 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -743,6 +743,22 @@ func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = . throw r } +func apiDeleteContact(id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws -> Contact { + let type: ChatType = .direct + let chatId = type.rawValue + id.description + if case .full = chatDeleteMode { + DispatchQueue.main.async { ChatModel.shared.deletedChats.insert(chatId) } + } + defer { + if case .full = chatDeleteMode { + DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) } + } + } + let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, chatDeleteMode: chatDeleteMode), bgTask: false) + if case let .contactDeleted(_, contact) = r { return contact } + throw r +} + func deleteChat(_ chat: Chat, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async { do { let cInfo = chat.chatInfo @@ -1607,6 +1623,11 @@ func processReceivedMsg(_ res: ChatResponse) async { let cItem = aChatItem.chatItem await MainActor.run { if active(user) { + if case let .direct(contact) = cInfo, contact.chatDeleted { + var updatedContact = contact + updatedContact.chatDeleted = false + m.updateContact(updatedContact) + } m.addChatItem(cInfo, cItem) } else if cItem.isRcvNew && cInfo.ntfsEnabled { m.increaseUnreadCounter(user: user) diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 855f5193d7..4c3f579c0d 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -88,6 +88,18 @@ enum SendReceipts: Identifiable, Hashable { } } +enum ContactDeleteMode { + case full + case entity + + public func toChatDeleteMode(notify: Bool) -> ChatDeleteMode { + switch self { + case .full: .full(notify: notify) + case .entity: .entity(notify: notify) + } + } +} + struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss: DismissAction @@ -99,7 +111,7 @@ struct ChatInfoView: View { @State private var connectionCode: String? = nil @FocusState private var aliasTextFieldFocused: Bool @State private var alert: ChatInfoViewAlert? = nil - @State private var showDeleteContactActionSheet = false + @State private var actionSheet: ChatInfoViewActionSheet? = nil @State private var sendReceipts = SendReceipts.userDefault(true) @State private var sendReceiptsUserDefault = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -124,6 +136,18 @@ struct ChatInfoView: View { } } + enum ChatInfoViewActionSheet: Identifiable { + case deleteContactActionSheet + case notifyDeleteContactActionSheet(contactDeleteMode: ContactDeleteMode) + + var id: String { + switch self { + case .deleteContactActionSheet: return "deleteContactActionSheet" + case .notifyDeleteContactActionSheet: return "notifyDeleteContactActionSheet" + } + } + } + var body: some View { NavigationView { List { @@ -263,24 +287,37 @@ struct ChatInfoView: View { case let .error(title, error): return mkAlert(title: title, message: error) } } - .actionSheet(isPresented: $showDeleteContactActionSheet) { - if contact.ready && contact.active { + .actionSheet(item: $actionSheet) { sheet in + switch(sheet) { + case .deleteContactActionSheet: return ActionSheet( - title: Text("Delete contact?\nThis cannot be undone!"), + title: Text("Delete contact or conversation?"), buttons: [ - .destructive(Text("Delete and notify contact")) { deleteContact(chatDeleteMode: .full(notify: true)) }, - .destructive(Text("Delete")) { deleteContact(chatDeleteMode: .full(notify: false)) }, - .cancel() - ] - ) - } else { - return ActionSheet( - title: Text("Delete contact?\nThis cannot be undone!"), - buttons: [ - .destructive(Text("Delete")) { deleteContact(chatDeleteMode: .full(notify: false)) }, // just a default + .destructive(Text("Only delete conversation")) { deleteContact(chatDeleteMode: .messages) }, + .destructive(Text("Delete contact, keep conversation")) { actionSheet = .notifyDeleteContactActionSheet(contactDeleteMode: .entity) }, + .destructive(Text("Delete contact and conversation")) { actionSheet = .notifyDeleteContactActionSheet(contactDeleteMode: .full) }, .cancel() ] ) + case let .notifyDeleteContactActionSheet(contactDeleteMode): + if contact.ready && contact.active { + return ActionSheet( + title: Text("Notify contact?\nThis cannot be undone!"), + buttons: [ + .destructive(Text("Delete and notify contact")) { deleteContact(chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: true)) }, + .destructive(Text("Delete without notification")) { deleteContact(chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: false)) }, + .cancel() + ] + ) + } else { + return ActionSheet( + title: Text("Confirm contact deletion.\nThis cannot be undone!"), + buttons: [ + .destructive(Text("Delete")) { deleteContact(chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: false)) }, + .cancel() + ] + ) + } } } } @@ -454,9 +491,9 @@ struct ChatInfoView: View { private func deleteContactButton() -> some View { Button(role: .destructive) { - showDeleteContactActionSheet = true + actionSheet = .deleteContactActionSheet } label: { - Label("Delete contact", systemImage: "trash") + Label("Delete", systemImage: "trash") .foregroundColor(Color.red) } } @@ -473,11 +510,15 @@ struct ChatInfoView: View { private func deleteContact(chatDeleteMode: ChatDeleteMode) { Task { do { - try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, chatDeleteMode: chatDeleteMode) + let ct = try await apiDeleteContact(id: chat.chatInfo.apiId, chatDeleteMode: chatDeleteMode) await MainActor.run { dismiss() chatModel.chatId = nil - chatModel.removeChat(chat.chatInfo.id) + if case .full = chatDeleteMode { + chatModel.removeChat(chat.chatInfo.id) + } else { + chatModel.updateContact(ct) + } } } catch let error { logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))") diff --git a/apps/ios/Shared/Views/ChatList/ChatsView.swift b/apps/ios/Shared/Views/ChatList/ChatsView.swift index 5ef4328db2..9167239a96 100644 --- a/apps/ios/Shared/Views/ChatList/ChatsView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatsView.swift @@ -105,16 +105,18 @@ struct ChatsView: View { } else { let s = searchString() return s == "" && !showUnreadAndFavorites - ? chatModel.chats + ? chatModel.chats.filter { chat in !chat.chatInfo.chatDeleted } : chatModel.chats.filter { chat in let cInfo = chat.chatInfo switch cInfo { case let .direct(contact): - return s == "" - ? filtered(chat) - : (viewNameContains(cInfo, s) || - contact.profile.displayName.localizedLowercase.contains(s) || - contact.fullName.localizedLowercase.contains(s)) + return !contact.chatDeleted && ( + s == "" + ? filtered(chat) + : (viewNameContains(cInfo, s) || + contact.profile.displayName.localizedLowercase.contains(s) || + contact.fullName.localizedLowercase.contains(s)) + ) case let .group(gInfo): return s == "" ? (filtered(chat) || gInfo.membership.memberStatus == .memInvited) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index cd26d9707e..13b17faf50 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1287,6 +1287,15 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } + public var chatDeleted: Bool { + get { + switch self { + case let .direct(contact): return contact.chatDeleted + default: return false + } + } + } + public var sendMsgEnabled: Bool { get { switch self { @@ -1504,7 +1513,7 @@ public struct Contact: Identifiable, Decodable, NamedChat { var chatTs: Date? var contactGroupMemberId: Int64? var contactGrpInvSent: Bool - var chatDeleted: Bool + public var chatDeleted: Bool public var id: ChatId { get { "@\(contactId)" } } public var apiId: Int64 { get { contactId } }