diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index dedb03b5aa..bff6b8d01d 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -383,6 +383,10 @@ final class ChatModel: ObservableObject { // list of chat "previews" @Published private(set) var chats: [Chat] = [] @Published var deletedChats: Set = [] + // ids of contact requests being accepted and of groups being joined, + // to not offer accepting the same invitation from another view + @Published var acceptingContactRequests: Set = [] + @Published var joiningGroups: Set = [] // current chat @Published var chatId: String? @Published var chatAgentConnId: String? diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 7a934fc746..b02632f937 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1770,8 +1770,8 @@ func networkErrorAlert(_ res: APIResult) -> (title: String, message: Strin } } -func acceptContactRequest(incognito: Bool, contactRequestId: Int64, inProgress: Binding? = nil) async { - await MainActor.run { inProgress?.wrappedValue = true } +func acceptContactRequest(incognito: Bool, contactRequestId: Int64) async { + await MainActor.run { _ = ChatModel.shared.acceptingContactRequests.insert(contactRequestId) } if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequestId) { let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: []) await MainActor.run { @@ -1780,7 +1780,7 @@ func acceptContactRequest(incognito: Bool, contactRequestId: Int64, inProgress: } else { ChatModel.shared.replaceChat(contactRequestChatId(contactRequestId), chat) } - inProgress?.wrappedValue = false + ChatModel.shared.acceptingContactRequests.remove(contactRequestId) } if contact.sndReady { let chatId = chat.id @@ -1791,7 +1791,7 @@ func acceptContactRequest(incognito: Bool, contactRequestId: Int64, inProgress: } } } else { - await MainActor.run { inProgress?.wrappedValue = false } + await MainActor.run { _ = ChatModel.shared.acceptingContactRequests.remove(contactRequestId) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index b34076eafd..af9b26317a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -21,9 +21,10 @@ struct CIGroupInvitationView: View { var memberRole: GroupMemberRole var chatIncognito: Bool = false @State private var frameWidth: CGFloat = 0 - @State private var inProgress = false @State private var progressByTimeout = false + private var inProgress: Bool { chatModel.joiningGroups.contains(groupInvitation.groupId) } + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false var body: some View { @@ -78,12 +79,7 @@ struct CIGroupInvitationView: View { .progressByTimeout(inProgress, $progressByTimeout) if action { - v.simultaneousGesture(TapGesture().onEnded { - inProgress = true - joinGroup(groupInvitation.groupId) { - await MainActor.run { inProgress = false } - } - }) + v.simultaneousGesture(TapGesture().onEnded { joinGroup(groupInvitation.groupId) }) .disabled(inProgress) } else { v diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift index 41794f2dad..b3a2c71fdb 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift @@ -10,12 +10,14 @@ import SwiftUI import SimpleXChat struct ContextContactRequestActionsView: View { + @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var theme: AppTheme var contactRequestId: Int64 @UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial - @State private var inProgress = false @State private var progressByTimeout = false + private var inProgress: Bool { chatModel.acceptingContactRequests.contains(contactRequestId) } + var body: some View { HStack(spacing: 0) { Button(role: .destructive, action: showRejectRequestAlert) { @@ -77,7 +79,7 @@ struct ContextContactRequestActionsView: View { private func acceptRequest(incognito: Bool = false) { Task { - await acceptContactRequest(incognito: incognito, contactRequestId: contactRequestId, inProgress: $inProgress) + await acceptContactRequest(incognito: incognito, contactRequestId: contactRequestId) } } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index e7e871a20e..296dba8857 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -57,11 +57,26 @@ struct ChatListNavLink: View { @State private var actionSheet: SomeActionSheet? = nil @State private var sheet: SomeSheet? = nil @State private var showConnectContactViaAddressDialog = false - @State private var inProgress = false @State private var progressByTimeout = false var dynamicRowHeight: CGFloat { dynamicSize(userFont).rowHeight } + private var inProgress: Bool { + switch chat.chatInfo { + case let .group(groupInfo, _): + return chatModel.joiningGroups.contains(groupInfo.groupId) + case let .direct(contact): + if let contactRequestId = contact.contactRequestId { + return chatModel.acceptingContactRequests.contains(contactRequestId) + } + return false + case let .contactRequest(contactRequest): + return chatModel.acceptingContactRequests.contains(contactRequest.apiId) + default: + return false + } + } + var body: some View { Group { switch chat.chatInfo { @@ -126,17 +141,19 @@ struct ChatListNavLink: View { .swipeActions(edge: .trailing, allowsFullSwipe: true) { if contact.nextAcceptContactRequest { if 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 !inProgress { 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)) @@ -212,7 +229,9 @@ struct ChatListNavLink: View { ChatPreviewView(chat: chat, progressByTimeout: $progressByTimeout) .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { - joinGroupButton() + if !inProgress { + joinGroupButton() + } if groupInfo.canDelete { deleteGroupChatButton(groupInfo) } @@ -220,10 +239,7 @@ struct ChatListNavLink: View { .onTapGesture { showJoinGroupDialog = true } .confirmationDialog("Group invitation", isPresented: $showJoinGroupDialog, titleVisibility: .visible) { Button(chat.chatInfo.incognito ? "Join incognito" : "Join group") { - inProgress = true - joinGroup(groupInfo.groupId) { - await MainActor.run { inProgress = false } - } + joinGroup(groupInfo.groupId) } Button("Delete invitation", role: .destructive) { Task { await deleteChat(chat) } } } @@ -311,10 +327,7 @@ struct ChatListNavLink: View { private func joinGroupButton() -> some View { Button { - inProgress = true - joinGroup(chat.chatInfo.apiId) { - await MainActor.run { inProgress = false } - } + joinGroup(chat.chatInfo.apiId) } label: { SwipeLabel(NSLocalizedString("Join", comment: "swipe action"), systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward", inverted: oneHandUI) } @@ -488,17 +501,19 @@ struct ChatListNavLink: View { ContactRequestView(contactRequest: contactRequest, chat: chat) .frameCompat(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { - Button { - Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } - } label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } - .tint(theme.colors.primary) - if !ChatModel.shared.addressShortLinkDataSet { + if !inProgress { Button { - Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } - } label: { - SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) + Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } + } 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: contactRequest.apiId) } + } label: { + SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) + } + .tint(.indigo) } - .tint(.indigo) } Button { AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest.apiId)) @@ -509,6 +524,7 @@ struct ChatListNavLink: View { } .contentShape(Rectangle()) .onTapGesture { showContactRequestDialog = true } + .disabled(inProgress) .confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) { Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } } if !ChatModel.shared.addressShortLinkDataSet { @@ -724,7 +740,8 @@ func connectContactViaAddress(_ contactId: Int64, _ incognito: Bool, showAlert: return false } -func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { +func joinGroup(_ groupId: Int64) { + ChatModel.shared.joiningGroups.insert(groupId) Task { logger.debug("joinGroup") do { @@ -740,13 +757,12 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { await deleteGroup() } } - await onComplete() } catch let error { - await onComplete() await MainActor.run { showErrorAlert(error, NSLocalizedString("Error joining group", comment: "")) } } + await MainActor.run { _ = ChatModel.shared.joiningGroups.remove(groupId) } func deleteGroup() async { do { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 7881d363bf..cafc7ea616 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -122,8 +122,10 @@ object ChatModel { val incompleteInitializedDbRemoved = mutableStateOf(false) // map of connections network statuses, key is agent connection id val switchingUsersAndHosts = mutableStateOf(false) - // ids of contact requests being accepted, to not offer accepting the same request from another view + // ids of contact requests being accepted and of groups being joined, + // to not offer accepting the same invitation from another view val acceptingContactRequests = mutableStateListOf() + val joiningGroups = mutableStateListOf() // current chat val chatId = mutableStateOf(null) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 288ad41d4a..203ab9ad04 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -586,12 +586,7 @@ fun ChatView( cancelFile = { fileId -> withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) } }, - joinGroup = { groupId, onComplete -> - withBGApi { - chatModel.controller.apiJoinGroup(chatRh, groupId) - onComplete.invoke() - } - }, + joinGroup = { groupId -> joinGroup(chatRh, groupId) }, startCall = out@{ media -> startChatCall(chatRh, chatInfo, media) }, endCall = { val call = chatModel.activeCall.value @@ -923,7 +918,7 @@ fun ChatLayout( archiveReports: (List, Boolean) -> Unit, receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, - joinGroup: (Long, () -> Unit) -> Unit, + joinGroup: (Long) -> Unit, startCall: (CallMediaType) -> Unit, endCall: () -> Unit, acceptCall: (Contact) -> Unit, @@ -1750,7 +1745,7 @@ fun BoxScope.ChatItemsList( archiveReports: (List, Boolean) -> Unit, receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, - joinGroup: (Long, () -> Unit) -> Unit, + joinGroup: (Long) -> Unit, acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, @@ -3835,7 +3830,7 @@ fun PreviewChatLayout() { archiveReports = { _, _ -> }, receiveFile = { _ -> }, cancelFile = {}, - joinGroup = { _, _ -> }, + joinGroup = {}, startCall = {}, endCall = {}, acceptCall = { _ -> }, @@ -3918,7 +3913,7 @@ fun PreviewGroupChatLayout() { archiveReports = { _, _ -> }, receiveFile = { _ -> }, cancelFile = {}, - joinGroup = { _, _ -> }, + joinGroup = {}, startCall = {}, endCall = {}, acceptCall = { _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextContactRequestActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextContactRequestActionsView.kt index 86b3f7e14d..40ac86add6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextContactRequestActionsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextContactRequestActionsView.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -16,6 +15,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import chat.simplex.common.platform.chatModel import chat.simplex.common.views.chatlist.acceptContactRequest +import chat.simplex.common.views.chatlist.rememberAcceptingContactRequest import chat.simplex.common.views.chatlist.rejectContactRequest import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @@ -27,17 +27,9 @@ fun ComposeContextContactRequestActionsView( rhId: Long?, contactRequestId: Long ) { - val inProgressLocal = rememberSaveable { mutableStateOf(false) } - // the request can also be accepted from another view, e.g. via notification - val inProgress = remember(contactRequestId) { derivedStateOf { inProgressLocal.value || contactRequestId in chatModel.acceptingContactRequests } } + val inProgress = rememberAcceptingContactRequest(contactRequestId) val progressByTimeout by rememberProgressByTimeout(inProgress) - KeyChangeEffect(chatModel.chatId.value) { - if (inProgressLocal.value) { - inProgressLocal.value = false - } - } - Box( Modifier.height(60.dp), contentAlignment = Alignment.Center @@ -80,9 +72,9 @@ fun ComposeContextContactRequestActionsView( else acceptButtonModifier.clickable { if (chatModel.addressShortLinkDataSet()) { - acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null, inProgress = inProgressLocal) + acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null) } else { - showAcceptRequestAlert(rhId, contactRequestId, inProgress = inProgressLocal) + showAcceptRequestAlert(rhId, contactRequestId) } } Row( @@ -124,7 +116,7 @@ fun showRejectRequestAlert(rhId: Long?, contactRequestId: Long) { ) } -fun showAcceptRequestAlert(rhId: Long?, contactRequestId: Long, inProgress: MutableState) { +fun showAcceptRequestAlert(rhId: Long?, contactRequestId: Long) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.accept_contact_request), buttons = { @@ -132,14 +124,14 @@ fun showAcceptRequestAlert(rhId: Long?, contactRequestId: Long, inProgress: Muta // Accept SectionItemView({ AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null, inProgress = inProgress) + acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null) }) { Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } // Accept incognito SectionItemView({ AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = true, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null, inProgress = inProgress) + acceptContactRequest(rhId, incognito = true, contactRequestId, isCurrentUser = true, chatModel = chatModel, close = null) }) { Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 20776957af..0b79b42185 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -654,12 +654,17 @@ fun ComposeView( val mc = checkLinkPreview() sending() val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get() - val result = chatModel.controller.apiConnectPreparedGroup( - rh = chat.remoteHostId, - groupId = chat.chatInfo.apiId, - incognito = incognito, - msg = mc - ) + chatModel.joiningGroups.add(chat.chatInfo.apiId) + val result = try { + chatModel.controller.apiConnectPreparedGroup( + rh = chat.remoteHostId, + groupId = chat.chatInfo.apiId, + incognito = incognito, + msg = mc + ) + } finally { + chatModel.joiningGroups.remove(chat.chatInfo.apiId) + } if (result != null) { val (groupInfo, relayResults) = result withContext(Dispatchers.Main) { @@ -1657,7 +1662,7 @@ fun ComposeView( Surface(color = MaterialTheme.colors.background, contentColor = MaterialTheme.colors.onBackground) { Divider() - if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared) { + if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared && chat.chatInfo.groupChatScope() == null) { if (chat.chatInfo.groupInfo.businessChat == null) { val isChannel = chat.chatInfo.groupInfo.useRelays ConnectButtonView( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt index 86f03f15ac..0ff8218e9e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.rememberJoiningGroup import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.res.MR @@ -27,12 +28,12 @@ fun CIGroupInvitationView( memberRole: GroupMemberRole, showTimestamp: Boolean, chatIncognito: Boolean = false, - joinGroup: (Long, () -> Unit) -> Unit, + joinGroup: (Long) -> Unit, timedMessagesTTL: Int? ) { val sent = ci.chatDir.sent val action = !sent && groupInvitation.status == CIGroupInvitationStatus.Pending - val inProgress = remember { mutableStateOf(false) } + val inProgress = rememberJoiningGroup(groupInvitation.groupId) val progressByTimeout by rememberProgressByTimeout(inProgress) @Composable @@ -75,10 +76,7 @@ fun CIGroupInvitationView( val sentColor = MaterialTheme.appColors.sentMessage val receivedColor = MaterialTheme.appColors.receivedMessage Surface( - modifier = if (action && !inProgress.value) Modifier.clickable(onClick = { - inProgress.value = true - joinGroup(groupInvitation.groupId) { inProgress.value = false } - }) else Modifier, + modifier = if (action && !inProgress.value) Modifier.clickable(onClick = { joinGroup(groupInvitation.groupId) }) else Modifier, shape = RoundedCornerShape(18.dp), color = if (sent) sentColor else receivedColor, contentColor = LocalContentColor.current @@ -151,7 +149,7 @@ fun PendingCIGroupInvitationViewPreview() { ci = ChatItem.getGroupInvitationSample(), groupInvitation = CIGroupInvitation.getSample(), memberRole = GroupMemberRole.Admin, - joinGroup = { _, _ -> }, + joinGroup = {}, timedMessagesTTL = null, showTimestamp = true, ) @@ -169,7 +167,7 @@ fun CIGroupInvitationViewAcceptedPreview() { ci = ChatItem.getGroupInvitationSample(), groupInvitation = CIGroupInvitation.getSample(status = CIGroupInvitationStatus.Accepted), memberRole = GroupMemberRole.Admin, - joinGroup = { _, _ -> }, + joinGroup = {}, timedMessagesTTL = null, showTimestamp = true, ) @@ -187,7 +185,7 @@ fun CIGroupInvitationViewLongNamePreview() { status = CIGroupInvitationStatus.Accepted ), memberRole = GroupMemberRole.Admin, - joinGroup = { _, _ -> }, + joinGroup = {}, timedMessagesTTL = null, showTimestamp = true, ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index bab6576646..ac10f36d79 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -88,7 +88,7 @@ fun ChatItemView( archiveReports: (List, Boolean) -> Unit, receiveFile: (Long) -> Unit, cancelFile: (Long) -> Unit, - joinGroup: (Long, () -> Unit) -> Unit, + joinGroup: (Long) -> Unit, acceptCall: (Contact) -> Unit, scrollToItem: (Long) -> Unit, scrollToItemId: MutableState, @@ -1507,7 +1507,7 @@ fun PreviewChatItemView( archiveReports = { _, _ -> }, receiveFile = { _ -> }, cancelFile = {}, - joinGroup = { _, _ -> }, + joinGroup = {}, acceptCall = { _ -> }, scrollToItem = {}, scrollToItemId = remember { mutableStateOf(null) }, @@ -1558,7 +1558,7 @@ fun PreviewChatItemViewDeletedContent() { archiveReports = { _, _ -> }, receiveFile = { _ -> }, cancelFile = {}, - joinGroup = { _, _ -> }, + joinGroup = {}, acceptCall = { _ -> }, scrollToItem = {}, scrollToItemId = remember { mutableStateOf(null) }, 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 d594881014..92f158f362 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 @@ -47,8 +47,6 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { } val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } } val showChatPreviews = chatModel.showChatPreviews.value - val inProgress = remember { mutableStateOf(false) } - val progressByTimeout by rememberProgressByTimeout(inProgress) val scope = rememberCoroutineScope() @@ -74,7 +72,9 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ) } is ChatInfo.Group -> { - val defaultClickAction = { if (!inProgress.value && chatModel.chatId.value != chat.id) scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) } } + val inProgress = rememberJoiningGroup(chat.chatInfo.groupInfo.groupId) + val progressByTimeout by rememberProgressByTimeout(inProgress) + val defaultClickAction = { if (!inProgress.value && chatModel.chatId.value != chat.id) scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) } } ChatListNavLinkLayout( chatLinkPreview = { tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { @@ -84,7 +84,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { click = defaultClickAction, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { - GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) + GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) } }, showMenu, @@ -113,14 +113,15 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { nextChatSelected, ) } - is ChatInfo.ContactRequest -> + is ChatInfo.ContactRequest -> { + val inProgress = rememberAcceptingContactRequest(chat.chatInfo.apiId) ChatListNavLinkLayout( chatLinkPreview = { tryOrShowError("${chat.id}ChatListNavLink", error = { ErrorChatListItem() }) { ContactRequestView(chat.chatInfo) } }, - click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) { onRequestAccepted(it) } }, + click = { if (!inProgress.value) contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) { onRequestAccepted(it) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { ContactRequestMenuItems(chat.remoteHostId, contactRequestId = chat.chatInfo.apiId, chatModel, showMenu) @@ -131,6 +132,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { selectedChat, nextChatSelected, ) + } is ChatInfo.ContactConnection -> ChatListNavLinkLayout( chatLinkPreview = { @@ -184,9 +186,9 @@ suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel } } -suspend fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState? = null) { +suspend fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { when (groupInfo.membership.memberStatus) { - GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress) + GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel) GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId) else -> openGroupChat(rhId, groupInfo.groupId) } @@ -294,13 +296,12 @@ fun GroupMenuItems( groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState, - inProgress: MutableState, showMarkRead: Boolean ) { when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> { - if (!inProgress.value) { - JoinGroupAction(chat, groupInfo, chatModel, showMenu, inProgress) + if (groupInfo.groupId !in chatModel.joiningGroups) { + JoinGroupAction(chat, groupInfo, showMenu) } if (groupInfo.canDelete) { DeleteGroupAction(chat, groupInfo, chatModel, showMenu) @@ -476,23 +477,14 @@ fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, sh fun JoinGroupAction( chat: Chat, groupInfo: GroupInfo, - chatModel: ChatModel, - showMenu: MutableState, - inProgress: MutableState + showMenu: MutableState ) { - val joinGroup: () -> Unit = { - withBGApi { - inProgress.value = true - chatModel.controller.apiJoinGroup(chat.remoteHostId, groupInfo.groupId) - inProgress.value = false - } - } ItemAction( if (chat.chatInfo.incognito) stringResource(MR.strings.join_group_incognito_button) else stringResource(MR.strings.join_group_button), if (chat.chatInfo.incognito) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_login), color = if (chat.chatInfo.incognito) Indigo else MaterialTheme.colors.onBackground, onClick = { - joinGroup() + joinGroup(chat.remoteHostId, groupInfo.groupId) showMenu.value = false } ) @@ -513,25 +505,27 @@ fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, sh @Composable fun ContactRequestMenuItems(rhId: Long?, contactRequestId: Long, chatModel: ChatModel, showMenu: MutableState, onSuccess: ((chat: Chat) -> Unit)? = null) { - ItemAction( - stringResource(MR.strings.accept_contact_button), - painterResource(MR.images.ic_check), - color = MaterialTheme.colors.onBackground, - onClick = { - acceptContactRequest(rhId, incognito = false, contactRequestId, true, chatModel, onSuccess) - showMenu.value = false - } - ) - if (!chatModel.addressShortLinkDataSet()) { + if (contactRequestId !in chatModel.acceptingContactRequests) { ItemAction( - stringResource(MR.strings.accept_contact_incognito_button), - painterResource(MR.images.ic_theater_comedy), + stringResource(MR.strings.accept_contact_button), + painterResource(MR.images.ic_check), color = MaterialTheme.colors.onBackground, onClick = { - acceptContactRequest(rhId, incognito = true, contactRequestId, true, chatModel, onSuccess) + acceptContactRequest(rhId, incognito = false, contactRequestId, true, chatModel, onSuccess) showMenu.value = false } ) + if (!chatModel.addressShortLinkDataSet()) { + ItemAction( + stringResource(MR.strings.accept_contact_incognito_button), + painterResource(MR.images.ic_theater_comedy), + color = MaterialTheme.colors.onBackground, + onClick = { + acceptContactRequest(rhId, incognito = true, contactRequestId, true, chatModel, onSuccess) + showMenu.value = false + } + ) + } } ItemAction( stringResource(MR.strings.reject_contact_button), @@ -721,12 +715,10 @@ fun acceptContactRequest( contactRequestId: Long, isCurrentUser: Boolean, chatModel: ChatModel, - close: ((chat: Chat) -> Unit)? = null, - inProgress: MutableState? = null + close: ((chat: Chat) -> Unit)? = null ) { + chatModel.acceptingContactRequests.add(contactRequestId) withBGApi { - inProgress?.value = true - chatModel.acceptingContactRequests.add(contactRequestId) try { val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, contactRequestId) if (contact != null && isCurrentUser) { @@ -737,11 +729,8 @@ fun acceptContactRequest( } else { chatModel.chatsContext.replaceChat(rhId, contactRequestChatId(contactRequestId), chat) } - inProgress?.value = false } close?.invoke(chat) - } else { - inProgress?.value = false } } finally { chatModel.acceptingContactRequests.remove(contactRequestId) @@ -749,6 +738,25 @@ fun acceptContactRequest( } } +fun joinGroup(rhId: Long?, groupId: Long) { + chatModel.joiningGroups.add(groupId) + withBGApi { + try { + chatModel.controller.apiJoinGroup(rhId, groupId) + } finally { + chatModel.joiningGroups.remove(groupId) + } + } +} + +@Composable +fun rememberAcceptingContactRequest(contactRequestId: Long): State = + remember(contactRequestId) { derivedStateOf { contactRequestId in chatModel.acceptingContactRequests } } + +@Composable +fun rememberJoiningGroup(groupId: Long): State = + remember(groupId) { derivedStateOf { groupId in chatModel.joiningGroups } } + fun rejectContactRequest(rhId: Long?, contactRequestId: Long, chatModel: ChatModel, dismissToChatList: Boolean = false) { withBGApi { val contact_ = chatModel.controller.apiRejectContactRequest(rhId, contactRequestId) @@ -892,18 +900,12 @@ suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactI return false } -fun acceptGroupInvitationAlertDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState? = null) { +fun acceptGroupInvitationAlertDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.join_group_question), text = generalGetString(MR.strings.you_are_invited_to_group_join_to_connect_with_group_members), confirmText = if (groupInfo.membership.memberIncognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - onConfirm = { - withBGApi { - inProgress?.value = true - chatModel.controller.apiJoinGroup(rhId, groupInfo.groupId) - inProgress?.value = false - } - }, + onConfirm = { joinGroup(rhId, groupInfo.groupId) }, dismissText = generalGetString(MR.strings.delete_verb), onDismiss = { deleteGroup(rhId, groupInfo, chatModel) }, hostDevice = hostDevice(rhId),