mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-27 22:34:51 +00:00
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:
@@ -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?
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+3
-1
@@ -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)
|
||||
|
||||
+5
-10
@@ -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
-15
@@ -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)
|
||||
}
|
||||
|
||||
+12
-7
@@ -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(
|
||||
|
||||
+7
-9
@@ -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,
|
||||
)
|
||||
|
||||
+3
-3
@@ -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) },
|
||||
|
||||
+52
-50
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user