ui: add swipe/menu actions to member contact requests (#6138)

* ios: add swipe actions to member contact requests

* kotlin

* padding
This commit is contained in:
spaced4ndy
2025-08-01 15:08:54 +00:00
committed by GitHub
parent f42a6751b1
commit 1fb8e63680
6 changed files with 149 additions and 65 deletions

View File

@@ -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<Bool>? = nil) {
Task {
await acceptMemberContact(contactId: contact.contactId, inProgress: inProgress)
}
}

View File

@@ -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 {

View File

@@ -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(

View File

@@ -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<Boolean>? = null
) {
withBGApi {

View File

@@ -271,8 +271,14 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo
@Composable
fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState<Boolean>, 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<Boolean>, 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<Boolean>) {
ItemAction(

View File

@@ -73,14 +73,25 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, 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)
}