android, desktop, ios: do not offer to accept or join twice

An invitation being accepted is a fact about the invitation, not about the
view that shows it, but each view kept its own flag. The chat list row, the
long-press menu, the accept alert, the composer banner, the chat item
"tap to join" and the notification action therefore disagreed: accepting
from one left the others fully enabled, and the second accept reached the
core with an invitation the first one had already used.

ChatModel now records which contact requests are being accepted and which
groups are being joined. The shared acceptContactRequest and the new
joinGroup maintain it, so every caller participates without changing its
call site, and every view derives from it instead of owning a flag. The
record is taken before the request is sent, so a second tap in the same
view is blocked as well.

The connect composer no longer renders in a group's support scope: on
desktop that is a second view of the same group next to the main one, and
it offered its own Join button.
This commit is contained in:
Narasimha-sc
2026-08-20 11:43:11 +00:00
parent ce6650a87a
commit 58aee54ad6
12 changed files with 151 additions and 139 deletions
+4
View File
@@ -383,6 +383,10 @@ final class ChatModel: ObservableObject {
// list of chat "previews"
@Published private(set) var chats: [Chat] = []
@Published var deletedChats: Set<String> = []
// 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<Int64> = []
@Published var joiningGroups: Set<Int64> = []
// current chat
@Published var chatId: String?
@Published var chatAgentConnId: String?
+4 -4
View File
@@ -1770,8 +1770,8 @@ func networkErrorAlert<R>(_ res: APIResult<R>) -> (title: String, message: Strin
}
}
func acceptContactRequest(incognito: Bool, contactRequestId: Int64, inProgress: Binding<Bool>? = 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) }
}
}
@@ -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
@@ -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)
}
}
}
@@ -57,11 +57,26 @@ struct ChatListNavLink: View {
@State private var actionSheet: SomeActionSheet? = nil
@State private var sheet: SomeSheet<AnyView>? = 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 {
@@ -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<Long>()
val joiningGroups = mutableStateListOf<Long>()
// current chat
val chatId = mutableStateOf<String?>(null)
@@ -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<Long>, 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<Long>, 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 = { _ -> },
@@ -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<Boolean>) {
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)
}
@@ -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(
@@ -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,
)
@@ -88,7 +88,7 @@ fun ChatItemView(
archiveReports: (List<Long>, Boolean) -> Unit,
receiveFile: (Long) -> Unit,
cancelFile: (Long) -> Unit,
joinGroup: (Long, () -> Unit) -> Unit,
joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit,
scrollToItemId: MutableState<Long?>,
@@ -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) },
@@ -47,8 +47,6 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
}
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<Boolean>) {
)
}
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<Boolean>) {
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<Boolean>) {
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<Boolean>) {
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<Boolean>? = 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<Boolean>,
inProgress: MutableState<Boolean>,
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<Boolean>,
inProgress: MutableState<Boolean>
showMenu: MutableState<Boolean>
) {
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<Boolean>, 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<Boolean>? = 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<Boolean> =
remember(contactRequestId) { derivedStateOf { contactRequestId in chatModel.acceptingContactRequests } }
@Composable
fun rememberJoiningGroup(groupId: Long): State<Boolean> =
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<Boolean>? = 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),