android, desktop, ios: change only the progress indicator timeout

Reverts the shared helper the timeout change was extracted into, leaving the
effect where each view already had it. NewChatView and ViewModifiers are no
longer touched by this branch at all - the first was already at 0.5s.
This commit is contained in:
Narasimha-sc
2026-08-20 15:37:29 +00:00
parent b2b4ff4d4d
commit 1462b8e012
13 changed files with 99 additions and 55 deletions
@@ -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) })
@@ -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
@@ -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() {
@@ -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 }
}
@@ -26,23 +26,8 @@ extension View {
self
}
}
func progressByTimeout(_ inProgress: Bool, _ progressByTimeout: Binding<Bool>) -> 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")
}
@@ -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),
@@ -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
}
}
@@ -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) }
@@ -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() {
@@ -73,7 +73,15 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
}
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 = {
@@ -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<Boolean>): State<Boolean> {
val progressByTimeout = remember { mutableStateOf(false) }
ProgressByTimeoutEffect(inProgress.value) { progressByTimeout.value = it }
return progressByTimeout
}
@@ -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) {
@@ -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