From 683434625e8963ecdc46f56764891933159dc665 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Thu, 20 Aug 2026 11:32:42 +0000 Subject: [PATCH] android, desktop, ios: extract progress indicator timeout The effect that turns on the progress indicator after a delay was copied into 5 Kotlin and 4 Swift views. 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. Timing is unchanged. --- .../Chat/ChatItem/CIGroupInvitationView.swift | 10 +------- .../Chat/ComposeMessage/ComposeView.swift | 10 +------- .../ContextContactRequestActionsView.swift | 10 +------- .../Views/ChatList/ChatListNavLink.swift | 10 +------- .../Shared/Views/Helpers/ViewModifiers.swift | 15 +++++++++++ ...ComposeContextContactRequestActionsView.kt | 13 +--------- ...ContextGroupDirectInvitationActionsView.kt | 12 +-------- .../simplex/common/views/chat/ComposeView.kt | 10 ++------ .../views/chat/item/CIGroupInvitationView.kt | 12 +-------- .../views/chatlist/ChatListNavLinkView.kt | 11 +------- .../common/views/helpers/ProgressByTimeout.kt | 25 +++++++++++++++++++ .../common/views/newchat/NewChatView.kt | 11 +------- 12 files changed, 51 insertions(+), 98 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProgressByTimeout.kt diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index ddb58fdfd1..b34076eafd 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -75,15 +75,7 @@ struct CIGroupInvitationView: View { .background { chatItemFrameColor(chatItem, theme).modifier(ChatTailPadding()) } .textSelection(.disabled) .onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 } - .onChange(of: inProgress) { inProgress in - if inProgress { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - progressByTimeout = inProgress - } - } else { - progressByTimeout = false - } - } + .progressByTimeout(inProgress, $progressByTimeout) if action { v.simultaneousGesture(TapGesture().onEnded { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 734ccc083c..ce809dedf6 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -584,15 +584,7 @@ struct ComposeView: View { clearState() } } - .onChange(of: composeState.inProgress) { inProgress in - if inProgress { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - composeState.progressByTimeout = composeState.inProgress - } - } else { - composeState.progressByTimeout = false - } - } + .progressByTimeout(composeState.inProgress, $composeState.progressByTimeout) .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 82c89cd43d..41794f2dad 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextContactRequestActionsView.swift @@ -44,15 +44,7 @@ struct ContextContactRequestActionsView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } } - .onChange(of: inProgress) { inPrgrs in - if inPrgrs { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - progressByTimeout = inProgress - } - } else { - progressByTimeout = false - } - } + .progressByTimeout(inProgress, $progressByTimeout) } private func showRejectRequestAlert() { diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 0ed78401b0..e7e871a20e 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -79,15 +79,7 @@ struct ChatListNavLink: View { invalidJSONPreview(json) } } - .onChange(of: inProgress) { inProgress in - if inProgress { - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - progressByTimeout = inProgress - } - } else { - progressByTimeout = false - } - } + .progressByTimeout(inProgress, $progressByTimeout) .actionSheet(item: $actionSheet) { $0.actionSheet } } diff --git a/apps/ios/Shared/Views/Helpers/ViewModifiers.swift b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift index 902a3f95d7..cedd75d561 100644 --- a/apps/ios/Shared/Views/Helpers/ViewModifiers.swift +++ b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift @@ -26,8 +26,23 @@ 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 = 1_000_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 de558a894e..86b3f7e14d 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 @@ -21,7 +21,6 @@ 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( @@ -31,21 +30,11 @@ fun ComposeContextContactRequestActionsView( 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 } } - var progressByTimeout by rememberSaveable { mutableStateOf(false) } + val progressByTimeout by rememberProgressByTimeout(inProgress) KeyChangeEffect(chatModel.chatId.value) { if (inProgressLocal.value) { inProgressLocal.value = false - progressByTimeout = false - } - } - - LaunchedEffect(inProgress.value) { - progressByTimeout = if (inProgress.value) { - delay(1000) - inProgress.value - } else { - false } } 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 21799ff820..d6092bfa69 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,21 +28,11 @@ fun ComposeContextMemberContactActionsView( groupDirectInv: GroupDirectInvitation ) { val inProgress = rememberSaveable { mutableStateOf(false) } - var progressByTimeout by rememberSaveable { mutableStateOf(false) } + val progressByTimeout by rememberProgressByTimeout(inProgress) KeyChangeEffect(chatModel.chatId.value) { if (inProgress.value) { inProgress.value = false - progressByTimeout = false - } - } - - LaunchedEffect(inProgress.value) { - progressByTimeout = if (inProgress.value) { - delay(1000) - inProgress.value - } else { - 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 b0dad99452..20776957af 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 @@ -1568,14 +1568,8 @@ fun ComposeView( chatModel.sharedContent.value = null } - LaunchedEffect(composeState.value.inProgress) { - val newProgressByTimeout = if (composeState.value.inProgress) { - delay(1000) - composeState.value.inProgress - } else { - false - } - composeState.value = composeState.value.copy(progressByTimeout = newProgressByTimeout) + ProgressByTimeoutEffect(composeState.value.inProgress) { + composeState.value = composeState.value.copy(progressByTimeout = it) } 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 9b8393f66a..86f03f15ac 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,7 +6,6 @@ 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,7 +19,6 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.res.MR -import kotlinx.coroutines.delay @Composable fun CIGroupInvitationView( @@ -35,15 +33,7 @@ fun CIGroupInvitationView( val sent = ci.chatDir.sent val action = !sent && groupInvitation.status == CIGroupInvitationStatus.Pending val inProgress = remember { mutableStateOf(false) } - var progressByTimeout by rememberSaveable { mutableStateOf(false) } - LaunchedEffect(inProgress.value) { - progressByTimeout = if (inProgress.value) { - delay(1000) - inProgress.value - } else { - false - } - } + val progressByTimeout by rememberProgressByTimeout(inProgress) @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 8401f138fe..d594881014 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 @@ -48,16 +48,7 @@ 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) } - var progressByTimeout by rememberSaveable { mutableStateOf(false) } - - LaunchedEffect(inProgress.value) { - progressByTimeout = if (inProgress.value) { - delay(1000) - inProgress.value - } else { - false - } - } + val progressByTimeout by rememberProgressByTimeout(inProgress) val scope = rememberCoroutineScope() 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 new file mode 100644 index 0000000000..a5e0b79ab4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ProgressByTimeout.kt @@ -0,0 +1,25 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.runtime.* +import kotlinx.coroutines.delay + +private const val PROGRESS_TIMEOUT_MS = 1000L + +@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 d3bca178aa..3c67cd1aea 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,16 +303,7 @@ fun ActiveProfilePicker( filteredProfiles(chatModel.users.map { it.user }.sortedBy { !it.activeUser }, searchTextOrPassword.value) } - var progressByTimeout by rememberSaveable { mutableStateOf(false) } - - LaunchedEffect(switchingProfile.value) { - progressByTimeout = if (switchingProfile.value) { - delay(500) - switchingProfile.value - } else { - false - } - } + val progressByTimeout by rememberProgressByTimeout(switchingProfile) @Composable fun ProfilePickerUserOption(user: User) {