From 1fb8e6368090698ab5380b102e8778dd3f28ecf2 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:08:54 +0000 Subject: [PATCH] ui: add swipe/menu actions to member contact requests (#6138) * ios: add swipe actions to member contact requests * kotlin * padding --- .../ContextMemberContactActionsView.swift | 68 +++++++++---------- .../Views/ChatList/ChatListNavLink.swift | 62 ++++++++++++----- .../Views/Contacts/ContactListNavLink.swift | 19 +++++- ...ContextGroupDirectInvitationActionsView.kt | 6 +- .../views/chatlist/ChatListNavLinkView.kt | 32 ++++++++- .../views/contacts/ContactListNavView.kt | 27 +++++--- 6 files changed, 149 insertions(+), 65 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextMemberContactActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextMemberContactActionsView.swift index 9e90575af4..9a73b2b5d4 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextMemberContactActionsView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextMemberContactActionsView.swift @@ -27,13 +27,13 @@ struct ContextMemberContactActionsView: View { .frame(maxWidth: .infinity, minHeight: 60) } else { HStack(spacing: 0) { - Button(role: .destructive, action: showRejectRequestAlert) { + Button(role: .destructive, action: { showRejectMemberContactRequestAlert(contact) }) { Label("Reject", systemImage: "multiply") } .frame(maxWidth: .infinity, minHeight: 60) Button { - acceptRequest() + acceptMemberContactRequest(contact, inProgress: $inProgress) } label: { Label("Accept", systemImage: "checkmark") } @@ -61,44 +61,44 @@ struct ContextMemberContactActionsView: View { } } } +} - private func showRejectRequestAlert() { - showAlert( - NSLocalizedString("Reject contact request", comment: "alert title"), - message: NSLocalizedString("The sender will NOT be notified", comment: "alert message"), - actions: {[ - UIAlertAction(title: NSLocalizedString("Reject", comment: "alert action"), style: .destructive) { _ in - deleteContact() - }, - cancelAlertAction - ]} - ) - } +func showRejectMemberContactRequestAlert(_ contact: Contact) { + showAlert( + NSLocalizedString("Reject contact request", comment: "alert title"), + message: NSLocalizedString("The sender will NOT be notified", comment: "alert message"), + actions: {[ + UIAlertAction(title: NSLocalizedString("Reject", comment: "alert action"), style: .destructive) { _ in + deleteContact(contact) + }, + cancelAlertAction + ]} + ) +} - func deleteContact() { - Task { - do { - let _ct = try await apiDeleteContact(id: contact.contactId, chatDeleteMode: .full(notify: false)) - await MainActor.run { - ChatModel.shared.removeChat(contact.id) - ChatModel.shared.chatId = nil - } - } catch let error { - logger.error("apiDeleteContact: \(responseError(error))") - await MainActor.run { - showAlert( - NSLocalizedString("Error deleting chat!", comment: "alert title"), - message: responseError(error) - ) - } +private func deleteContact(_ contact: Contact) { + Task { + do { + _ = try await apiDeleteContact(id: contact.contactId, chatDeleteMode: .full(notify: false)) + await MainActor.run { + ChatModel.shared.removeChat(contact.id) + ChatModel.shared.chatId = nil + } + } catch let error { + logger.error("apiDeleteContact: \(responseError(error))") + await MainActor.run { + showAlert( + NSLocalizedString("Error deleting chat!", comment: "alert title"), + message: responseError(error) + ) } } } +} - private func acceptRequest() { - Task { - await acceptMemberContact(contactId: contact.contactId, inProgress: $inProgress) - } +func acceptMemberContactRequest(_ contact: Contact, inProgress: Binding? = nil) { + Task { + await acceptMemberContact(contactId: contact.contactId, inProgress: inProgress) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index fc151d4889..4937bca20e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -130,26 +130,54 @@ struct ChatListNavLink: View { } } .swipeActions(edge: .trailing, allowsFullSwipe: true) { - if contact.nextAcceptContactRequest, - let contactRequestId = contact.contactRequestId { - Button { - Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) } - } label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } - .tint(theme.colors.primary) - if !ChatModel.shared.addressShortLinkDataSet { + if contact.nextAcceptContactRequest { + if let contactRequestId = contact.contactRequestId { Button { - Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) } - } label: { - SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) + Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) } + } label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } + .tint(theme.colors.primary) + if !ChatModel.shared.addressShortLinkDataSet { + Button { + Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) } + } label: { + SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) + } + .tint(.indigo) } - .tint(.indigo) + Button { + AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequestId)) + } label: { + SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply", inverted: oneHandUI) + } + .tint(.red) + } else if let groupDirectInv = contact.groupDirectInv, !groupDirectInv.memberRemoved { + Button { + acceptMemberContactRequest(contact) + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) + Button { + showRejectMemberContactRequestAlert(contact) + } label: { + Label("Reject", systemImage: "multiply") + } + .tint(.red) + } else { + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + deleteLabel + } + .tint(.red) } - Button { - AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequestId)) - } label: { - SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply", inverted: oneHandUI) - } - .tint(.red) } else { tagChatButton(chat) if !chat.chatItems.isEmpty { diff --git a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift index 1c5a3bfaac..fcfcde2c07 100644 --- a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift +++ b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift @@ -87,8 +87,10 @@ struct ContactListNavLink: View { if let contactRequestId = contact.contactRequestId { Button { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) } - } label: { Label("Accept", systemImage: "checkmark") } - .tint(theme.colors.primary) + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) if !ChatModel.shared.addressShortLinkDataSet { Button { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) } @@ -103,6 +105,19 @@ struct ContactListNavLink: View { Label("Reject", systemImage: "multiply") } .tint(.red) + } else if let groupDirectInv = contact.groupDirectInv, !groupDirectInv.memberRemoved { + Button { + acceptMemberContactRequest(contact) + } label: { + Label("Accept", systemImage: "checkmark") + } + .tint(theme.colors.primary) + Button { + showRejectMemberContactRequestAlert(contact) + } label: { + Label("Reject", systemImage: "multiply") + } + .tint(.red) } else { Button { deleteContactDialog( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt index 78a5407e00..5c8ac3bc5d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @@ -59,7 +60,8 @@ fun ComposeContextMemberContactActionsView( if (groupDirectInv.memberRemoved) { Row( Modifier - .fillMaxSize(), + .fillMaxSize() + .padding(horizontal = DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) ) { @@ -149,7 +151,7 @@ private fun deleteMemberContact(rhId: Long?, contact: Contact) { fun acceptMemberContact( rhId: Long?, contactId: Long, - close: ((chat: Chat) -> Unit)? = null, // currently unused, can pass function to open chat if reused in other views (e.g. see onRequestAccepted) + close: ((chat: Chat) -> Unit)? = null, inProgress: MutableState? = null ) { withBGApi { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 325f6181c9..2128d1991b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -271,8 +271,14 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo @Composable fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState, showMarkRead: Boolean) { - if (contact.nextAcceptContactRequest && contact.contactRequestId != null) { - ContactRequestMenuItems(chat.remoteHostId, contactRequestId = contact.contactRequestId, chatModel, showMenu) + if (contact.nextAcceptContactRequest) { + if (contact.contactRequestId != null) { + ContactRequestMenuItems(chat.remoteHostId, contactRequestId = contact.contactRequestId, chatModel, showMenu) + } else if (contact.groupDirectInv != null && !contact.groupDirectInv.memberRemoved) { + MemberContactRequestMenuItems(chat.remoteHostId, contact, showMenu) + } else { + DeleteContactAction(chat, chatModel, showMenu) + } } else { if (contact.activeConn != null) { if (showMarkRead) { @@ -545,6 +551,28 @@ fun ContactRequestMenuItems(rhId: Long?, contactRequestId: Long, chatModel: Chat ) } +@Composable +fun MemberContactRequestMenuItems(rhId: Long?, contact: Contact, showMenu: MutableState, onSuccess: ((chat: Chat) -> Unit)? = null) { + ItemAction( + stringResource(MR.strings.accept_contact_button), + painterResource(MR.images.ic_check), + color = MaterialTheme.colors.onBackground, + onClick = { + acceptMemberContact(rhId, contact.contactId, onSuccess) + showMenu.value = false + } + ) + ItemAction( + stringResource(MR.strings.reject_contact_button), + painterResource(MR.images.ic_close), + onClick = { + showRejectMemberContactRequestAlert(rhId, contact) + showMenu.value = false + }, + color = Color.Red + ) +} + @Composable fun ContactConnectionMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactConnection, chatModel: ChatModel, showMenu: MutableState) { ItemAction( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt index 83df302064..371e9072ea 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt @@ -73,14 +73,25 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDel }, dropdownMenuItems = { tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { - if (contactType == ContactType.CONTACT_WITH_REQUEST && chat.chatInfo.contact.contactRequestId != null) { - ContactRequestMenuItems( - rhId = chat.remoteHostId, - contactRequestId = chat.chatInfo.contact.contactRequestId, - chatModel = chatModel, - showMenu = showMenu, - onSuccess = { onRequestAccepted(it) } - ) + if (contactType == ContactType.CONTACT_WITH_REQUEST) { + if (chat.chatInfo.contact.contactRequestId != null) { + ContactRequestMenuItems( + rhId = chat.remoteHostId, + contactRequestId = chat.chatInfo.contact.contactRequestId, + chatModel = chatModel, + showMenu = showMenu, + onSuccess = { onRequestAccepted(it) } + ) + } else if (chat.chatInfo.contact.groupDirectInv != null && !chat.chatInfo.contact.groupDirectInv.memberRemoved) { + MemberContactRequestMenuItems( + rhId = chat.remoteHostId, + contact = chat.chatInfo.contact, + showMenu = showMenu, + onSuccess = { onRequestAccepted(it) } + ) + } else { + DeleteContactAction(chat, chatModel, showMenu) + } } else { DeleteContactAction(chat, chatModel, showMenu) }