diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index af9b26317a..5bb957afe0 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -76,7 +76,15 @@ struct CIGroupInvitationView: View { .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } .textSelection(.disabled) .onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 } - .progressByTimeout(inProgress, $progressByTimeout) + .onChange(of: inProgress) { inPrgrs in + if inPrgrs { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + progressByTimeout = inProgress + } + } else { + progressByTimeout = false + } + } if action { v.simultaneousGesture(TapGesture().onEnded { joinGroup(groupInvitation.groupId) }) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index ce809dedf6..3a0769b71e 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -584,7 +584,15 @@ struct ComposeView: View { clearState() } } - .progressByTimeout(composeState.inProgress, $composeState.progressByTimeout) + .onChange(of: composeState.inProgress) { inProgress in + if inProgress { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + composeState.progressByTimeout = composeState.inProgress + } + } else { + composeState.progressByTimeout = false + } + } .confirmationDialog("Attach", isPresented: $showChooseSource, titleVisibility: .visible) { Button("Take picture") { showTakePhoto = true diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift index b3a2c71fdb..ec3173028a 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift @@ -46,7 +46,15 @@ struct ContextContactRequestActionsView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } } - .progressByTimeout(inProgress, $progressByTimeout) + .onChange(of: inProgress) { inPrgrs in + if inPrgrs { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + progressByTimeout = inProgress + } + } else { + progressByTimeout = false + } + } } private func showRejectRequestAlert() { diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 296dba8857..1cf9f9b786 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -94,7 +94,15 @@ struct ChatListNavLink: View { invalidJSONPreview(json) } } - .progressByTimeout(inProgress, $progressByTimeout) + .onChange(of: inProgress) { inPrgrs in + if inPrgrs { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + progressByTimeout = inProgress + } + } else { + progressByTimeout = false + } + } .actionSheet(item: $actionSheet) { $0.actionSheet } } diff --git a/apps/ios/Shared/Views/Helpers/ViewModifiers.swift b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift index cfcf0353ee..902a3f95d7 100644 --- a/apps/ios/Shared/Views/Helpers/ViewModifiers.swift +++ b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift @@ -26,23 +26,8 @@ extension View { self } } - - func progressByTimeout(_ inProgress: Bool, _ progressByTimeout: Binding) -> some View { - task(id: inProgress) { - if inProgress { - try? await Task.sleep(nanoseconds: progressTimeout) - if !Task.isCancelled { - progressByTimeout.wrappedValue = true - } - } else { - progressByTimeout.wrappedValue = false - } - } - } } -private let progressTimeout: UInt64 = 500_000000 - extension Notification.Name { static let chatViewWillBeginScrolling = Notification.Name("chatWillBeginScrolling") } 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 40ac86add6..1514f32323 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,6 +7,7 @@ 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 @@ -21,6 +22,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.delay @Composable fun ComposeContextContactRequestActionsView( @@ -28,7 +30,15 @@ fun ComposeContextContactRequestActionsView( contactRequestId: Long ) { val inProgress = rememberAcceptingContactRequest(contactRequestId) - val progressByTimeout by rememberProgressByTimeout(inProgress) + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(inProgress.value) { + progressByTimeout = if (inProgress.value) { + delay(500) + inProgress.value + } else { + false + } + } Box( Modifier.height(60.dp), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt index d6092bfa69..6a2d0f367b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextGroupDirectInvitationActionsView.kt @@ -28,11 +28,20 @@ fun ComposeContextMemberContactActionsView( groupDirectInv: GroupDirectInvitation ) { val inProgress = rememberSaveable { mutableStateOf(false) } - val progressByTimeout by rememberProgressByTimeout(inProgress) + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(inProgress.value) { + progressByTimeout = if (inProgress.value) { + delay(500) + inProgress.value + } else { + false + } + } KeyChangeEffect(chatModel.chatId.value) { if (inProgress.value) { inProgress.value = false + progressByTimeout = false } } 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 0b79b42185..157d10da36 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 @@ -1573,8 +1573,14 @@ fun ComposeView( chatModel.sharedContent.value = null } - ProgressByTimeoutEffect(composeState.value.inProgress) { - composeState.value = composeState.value.copy(progressByTimeout = it) + LaunchedEffect(composeState.value.inProgress) { + val newProgressByTimeout = if (composeState.value.inProgress) { + delay(500) + composeState.value.inProgress + } else { + false + } + composeState.value = composeState.value.copy(progressByTimeout = newProgressByTimeout) } val relayListExpanded = remember { mutableStateOf(false) } 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 0ff8218e9e..f873f2d14f 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 @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape 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.text.buildAnnotatedString @@ -20,6 +21,7 @@ import chat.simplex.common.views.chatlist.rememberJoiningGroup import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.res.MR +import kotlinx.coroutines.delay @Composable fun CIGroupInvitationView( @@ -34,7 +36,15 @@ fun CIGroupInvitationView( val sent = ci.chatDir.sent val action = !sent && groupInvitation.status == CIGroupInvitationStatus.Pending val inProgress = rememberJoiningGroup(groupInvitation.groupId) - val progressByTimeout by rememberProgressByTimeout(inProgress) + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(inProgress.value) { + progressByTimeout = if (inProgress.value) { + delay(500) + inProgress.value + } else { + false + } + } @Composable fun groupInfoView() { 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 92f158f362..25e3c16ddc 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 @@ -73,7 +73,15 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { } is ChatInfo.Group -> { val inProgress = rememberJoiningGroup(chat.chatInfo.groupInfo.groupId) - val progressByTimeout by rememberProgressByTimeout(inProgress) + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(inProgress.value) { + progressByTimeout = if (inProgress.value) { + delay(500) + inProgress.value + } else { + false + } + } val defaultClickAction = { if (!inProgress.value && chatModel.chatId.value != chat.id) scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) } } ChatListNavLinkLayout( chatLinkPreview = { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProgressByTimeout.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProgressByTimeout.kt deleted file mode 100644 index 4d6b97ef05..0000000000 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProgressByTimeout.kt +++ /dev/null @@ -1,25 +0,0 @@ -package chat.simplex.common.views.helpers - -import androidx.compose.runtime.* -import kotlinx.coroutines.delay - -private const val PROGRESS_TIMEOUT_MS = 500L - -@Composable -fun ProgressByTimeoutEffect(inProgress: Boolean, setProgressByTimeout: (Boolean) -> Unit) { - LaunchedEffect(inProgress) { - if (inProgress) { - delay(PROGRESS_TIMEOUT_MS) - setProgressByTimeout(true) - } else { - setProgressByTimeout(false) - } - } -} - -@Composable -fun rememberProgressByTimeout(inProgress: State): State { - val progressByTimeout = remember { mutableStateOf(false) } - ProgressByTimeoutEffect(inProgress.value) { progressByTimeout.value = it } - return progressByTimeout -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 3c67cd1aea..d3bca178aa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -303,7 +303,16 @@ fun ActiveProfilePicker( filteredProfiles(chatModel.users.map { it.user }.sortedBy { !it.activeUser }, searchTextOrPassword.value) } - val progressByTimeout by rememberProgressByTimeout(switchingProfile) + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(switchingProfile.value) { + progressByTimeout = if (switchingProfile.value) { + delay(500) + switchingProfile.value + } else { + false + } + } @Composable fun ProfilePickerUserOption(user: User) { diff --git a/plans/2026-08-20-do-not-accept-or-join-twice.md b/plans/2026-08-20-do-not-accept-or-join-twice.md index d843567ff7..f44f66fdf4 100644 --- a/plans/2026-08-20-do-not-accept-or-join-twice.md +++ b/plans/2026-08-20-do-not-accept-or-join-twice.md @@ -158,10 +158,10 @@ Join buttons. The connect composer belongs to the chat itself, not to a scope wi **5. Client: the progress indicator appears after 0.5s instead of 1s** (android, desktop, ios). -The effect behind it was copied into 5 Kotlin and 4 Swift views; it is now one helper per platform, -which is also what makes the threshold a single constant. Two of the Swift copies had drifted — the -deferred write captured the value that started the delay rather than reading the current one, so an -operation that finished before the delay elapsed left the indicator on with nothing to turn it off. +Accepting a request or joining a group usually takes longer than 1s, so the button looked inert for +a second before anything acknowledged the tap - which is what prompts a second tap. The threshold is +per view; extracting the effect the views share was considered and left out, to keep the change to +the timing. ## Alternatives considered and rejected