From c090b68bdda6073aeca7ac94dfde8b654eec5aba Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Thu, 19 Oct 2023 19:52:59 +0400 Subject: [PATCH] ios, android: ask to notify contact or not on contact deletion (#3247) --- apps/ios/Shared/Model/SimpleXAPI.swift | 8 +-- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 65 +++++++++-------- .../Views/ChatList/ChatListNavLink.swift | 42 ++++++----- apps/ios/SimpleXChat/APITypes.swift | 8 ++- .../chat/simplex/common/model/SimpleXAPI.kt | 12 ++-- .../simplex/common/views/chat/ChatInfoView.kt | 69 +++++++++++++++---- .../commonMain/resources/MR/base/strings.xml | 1 + 7 files changed, 137 insertions(+), 68 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index bad15ad52..99e8c0284 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -671,18 +671,18 @@ private func connectionErrorAlert(_ r: ChatResponse) -> Alert { } } -func apiDeleteChat(type: ChatType, id: Int64) async throws { - let r = await chatSendCmd(.apiDeleteChat(type: type, id: id), bgTask: false) +func apiDeleteChat(type: ChatType, id: Int64, notify: Bool? = nil) async throws { + let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, notify: notify), bgTask: false) if case .direct = type, case .contactDeleted = r { return } if case .contactConnection = type, case .contactConnectionDeleted = r { return } if case .group = type, case .groupDeletedUser = r { return } throw r } -func deleteChat(_ chat: Chat) async { +func deleteChat(_ chat: Chat, notify: Bool? = nil) async { do { let cInfo = chat.chatInfo - try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId) + try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId, notify: notify) DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) } } catch let error { logger.error("deleteChat apiDeleteChat error: \(responseError(error))") diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 5438eb13b..ec4cc0fc4 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -99,12 +99,12 @@ struct ChatInfoView: View { @Binding var connectionCode: String? @FocusState private var aliasTextFieldFocused: Bool @State private var alert: ChatInfoViewAlert? = nil + @State private var showDeleteContactActionSheet = false @State private var sendReceipts = SendReceipts.userDefault(true) @State private var sendReceiptsUserDefault = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false enum ChatInfoViewAlert: Identifiable { - case deleteContactAlert case clearChatAlert case networkStatusAlert case switchAddressAlert @@ -114,7 +114,6 @@ struct ChatInfoView: View { var id: String { switch self { - case .deleteContactAlert: return "deleteContactAlert" case .clearChatAlert: return "clearChatAlert" case .networkStatusAlert: return "networkStatusAlert" case .switchAddressAlert: return "switchAddressAlert" @@ -233,7 +232,6 @@ struct ChatInfoView: View { } .alert(item: $alert) { alertItem in switch(alertItem) { - case .deleteContactAlert: return deleteContactAlert() case .clearChatAlert: return clearChatAlert() case .networkStatusAlert: return networkStatusAlert() case .switchAddressAlert: return switchAddressAlert(switchContactAddress) @@ -242,6 +240,26 @@ struct ChatInfoView: View { case let .error(title, error): return mkAlert(title: title, message: error) } } + .actionSheet(isPresented: $showDeleteContactActionSheet) { + if contact.ready && contact.active { + ActionSheet( + title: Text("Delete contact?\nThis cannot be undone!"), + buttons: [ + .destructive(Text("Delete and notify contact")) { deleteContact(notify: true) }, + .destructive(Text("Delete")) { deleteContact(notify: false) }, + .cancel() + ] + ) + } else { + ActionSheet( + title: Text("Delete contact?\nThis cannot be undone!"), + buttons: [ + .destructive(Text("Delete")) { deleteContact() }, + .cancel() + ] + ) + } + } } private func contactInfoHeader() -> some View { @@ -414,7 +432,7 @@ struct ChatInfoView: View { private func deleteContactButton() -> some View { Button(role: .destructive) { - alert = .deleteContactAlert + showDeleteContactActionSheet = true } label: { Label("Delete contact", systemImage: "trash") .foregroundColor(Color.red) @@ -430,30 +448,23 @@ struct ChatInfoView: View { } } - private func deleteContactAlert() -> Alert { - Alert( - title: Text("Delete contact?"), - message: Text("Contact and all messages will be deleted - this cannot be undone!"), - primaryButton: .destructive(Text("Delete")) { - Task { - do { - try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) - await MainActor.run { - dismiss() - chatModel.chatId = nil - chatModel.removeChat(chat.chatInfo.id) - } - } catch let error { - logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))") - let a = getErrorAlert(error, "Error deleting contact") - await MainActor.run { - alert = .error(title: a.title, error: a.message) - } - } + private func deleteContact(notify: Bool? = nil) { + Task { + do { + try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, notify: notify) + await MainActor.run { + dismiss() + chatModel.chatId = nil + chatModel.removeChat(chat.chatInfo.id) } - }, - secondaryButton: .cancel() - ) + } catch let error { + logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))") + let a = getErrorAlert(error, "Error deleting contact") + await MainActor.run { + alert = .error(title: a.title, error: a.message) + } + } + } } private func clearChatAlert() -> Alert { diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index f445ae4b5..be912d666 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -32,6 +32,7 @@ struct ChatListNavLink: View { @State private var showJoinGroupDialog = false @State private var showContactConnectionInfo = false @State private var showInvalidJSON = false + @State private var showDeleteContactActionSheet = false var body: some View { switch chat.chatInfo { @@ -64,17 +65,37 @@ struct ChatListNavLink: View { clearChatButton() } Button { - AlertManager.shared.showAlert( - contact.ready || !contact.active - ? deleteContactAlert(chat.chatInfo) - : deletePendingContactAlert(chat, contact) - ) + if contact.ready || !contact.active { + showDeleteContactActionSheet = true + } else { + AlertManager.shared.showAlert(deletePendingContactAlert(chat, contact)) + } } label: { Label("Delete", systemImage: "trash") } .tint(.red) } .frame(height: rowHeights[dynamicTypeSize]) + .actionSheet(isPresented: $showDeleteContactActionSheet) { + if contact.ready && contact.active { + ActionSheet( + title: Text("Delete contact?\nThis cannot be undone!"), + buttons: [ + .destructive(Text("Delete and notify contact")) { Task { await deleteChat(chat, notify: true) } }, + .destructive(Text("Delete")) { Task { await deleteChat(chat, notify: false) } }, + .cancel() + ] + ) + } else { + ActionSheet( + title: Text("Delete contact?\nThis cannot be undone!"), + buttons: [ + .destructive(Text("Delete")) { Task { await deleteChat(chat) } }, + .cancel() + ] + ) + } + } } @ViewBuilder private func groupNavLink(_ groupInfo: GroupInfo) -> some View { @@ -269,17 +290,6 @@ struct ChatListNavLink: View { } } - private func deleteContactAlert(_ chatInfo: ChatInfo) -> Alert { - Alert( - title: Text("Delete contact?"), - message: Text("Contact and all messages will be deleted - this cannot be undone!"), - primaryButton: .destructive(Text("Delete")) { - Task { await deleteChat(chat) } - }, - secondaryButton: .cancel() - ) - } - private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert { Alert( title: Text("Delete group?"), diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 53da91b04..5c7220f37 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -89,7 +89,7 @@ public enum ChatCommand { case apiSetConnectionIncognito(connId: Int64, incognito: Bool) case apiConnectPlan(userId: Int64, connReq: String) case apiConnect(userId: Int64, incognito: Bool, connReq: String) - case apiDeleteChat(type: ChatType, id: Int64) + case apiDeleteChat(type: ChatType, id: Int64, notify: Bool?) case apiClearChat(type: ChatType, id: Int64) case apiListContacts(userId: Int64) case apiUpdateProfile(userId: Int64, profile: Profile) @@ -224,7 +224,11 @@ public enum ChatCommand { case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)" case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)" - case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))" + case let .apiDeleteChat(type, id, notify): if let notify = notify { + return "/_delete \(ref(type, id)) notify=\(onOff(notify))" + } else { + return "/_delete \(ref(type, id))" + } case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" case let .apiListContacts(userId): return "/_contacts \(userId)" case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))" 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 8397d2edb..da09ea132 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 @@ -904,8 +904,8 @@ object ChatController { } } - suspend fun apiDeleteChat(type: ChatType, id: Long): Boolean { - val r = sendCmd(CC.ApiDeleteChat(type, id)) + suspend fun apiDeleteChat(type: ChatType, id: Long, notify: Boolean? = null): Boolean { + val r = sendCmd(CC.ApiDeleteChat(type, id, notify)) when { r is CR.ContactDeleted && type == ChatType.Direct -> return true r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> return true @@ -1924,7 +1924,7 @@ sealed class CC { class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() class APIConnectPlan(val userId: Long, val connReq: String): CC() class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC() - class ApiDeleteChat(val type: ChatType, val id: Long): CC() + class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC() class ApiClearChat(val type: ChatType, val id: Long): CC() class ApiListContacts(val userId: Long): CC() class ApiUpdateProfile(val userId: Long, val profile: Profile): CC() @@ -2034,7 +2034,11 @@ sealed class CC { is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" is APIConnectPlan -> "/_connect plan $userId $connReq" is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq" - is ApiDeleteChat -> "/_delete ${chatRef(type, id)}" + is ApiDeleteChat -> if (notify != null) { + "/_delete ${chatRef(type, id)} notify=${onOff(notify)}" + } else { + "/_delete ${chatRef(type, id)}" + } is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" is ApiListContacts -> "/_contacts $userId" is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}" diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 01173157a..4564a4a6e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -196,28 +196,67 @@ sealed class SendReceipts { } fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { - AlertManager.shared.showAlertDialog( + AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.delete_contact_question), - text = generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning), - confirmText = generalGetString(MR.strings.delete_verb), - onConfirm = { - withApi { - val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId) - if (r) { - chatModel.removeChat(chatInfo.id) - if (chatModel.chatId.value == chatInfo.id) { - chatModel.chatId.value = null - ModalManager.end.closeModals() + text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)), + buttons = { + Column { + if (chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) { + // Delete and notify contact + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + deleteContact(chatInfo, chatModel, close, notify = true) + } + }) { + Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) } - ntfManager.cancelNotificationsForChat(chatInfo.id) - close?.invoke() + // Delete + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + deleteContact(chatInfo, chatModel, close, notify = false) + } + }) { + Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + } else { + // Delete + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + deleteContact(chatInfo, chatModel, close) + } + }) { + Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } } - }, - destructive = true, + } ) } +fun deleteContact(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) { + withApi { + val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId, notify) + if (r) { + chatModel.removeChat(chatInfo.id) + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.end.closeModals() + } + ntfManager.cancelNotificationsForChat(chatInfo.id) + close?.invoke() + } + } +} + fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.clear_chat_question), diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index bd59d236d..1ae85cd05 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -350,6 +350,7 @@ Delete contact? Contact and all messages will be deleted - this cannot be undone! + Delete and notify contact Delete contact Set contact name… Connected