ui: ignore a second click while its page is still loading

These buttons open their page only after a load, and nothing covered or
disabled the button meanwhile, so a second click during the load opened
a second page: the reports bar, the support bar as a member, group info
"Chat with admins", "Member reports", "Invite" and "Invite members",
"Chats with members" rows, member info "Chat with member", and a
channel's subscribers/owners and relays rows.

Each opener now ignores a click while its load is in progress, on
desktop and Android. The support and reports chats share one flag, as
they share one secondary chat context, so the support chat a pending
member's chat opens by itself is skipped while another of them loads.
Group info's "Invite" and "Invite members" share one flag with the chat
menu's "Invite members", as they open the same page.
This commit is contained in:
Narasimha-sc committed 2026-10-18 11:25:02 +00:00
1 parent 813f914d3a
commit 306bbabe0d
4 files changed
+31 -6

No files matched your search

@@ -52,6 +52,7 @@ import kotlinx.coroutines.flow.*
import kotlinx.datetime.*
import java.io.File
import java.net.URI
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.*
@Stable
@@ -3233,7 +3234,10 @@ private fun findQuotedItemFromItem(
val chatViewScrollState = MutableStateFlow(false)
val addGroupMembersOpening = AtomicBoolean(false)
fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
if (!addGroupMembersOpening.compareAndSet(false, true)) return
hideKeyboard(view)
withBGApi {
setGroupMembers(rhId, groupInfo, chatModel)
@@ -3241,7 +3245,7 @@ fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close:
ModalManager.end.showModalCloseable(showClose = true) { close ->
AddGroupMembersView(rhId, groupInfo, false, chatModel, close)
}
}
}.invokeOnCompletion { addGroupMembersOpening.set(false) }
}
fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
@@ -52,6 +52,7 @@ import chat.simplex.common.views.newchat.SimpleXLinkQRCode
import chat.simplex.res.*
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
import java.util.concurrent.atomic.AtomicBoolean
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
val MEMBER_ROW_AVATAR_SIZE = 42.dp
@@ -113,6 +114,7 @@ fun ModalData.GroupChatInfoView(
appBar,
scrollToItemId,
addMembers = {
if (!addGroupMembersOpening.compareAndSet(false, true)) return@GroupChatInfoLayout
scope.launch(Dispatchers.Default) {
setGroupMembers(rhId, groupInfo, chatModel)
if (!isActive) return@launch
@@ -120,7 +122,7 @@ fun ModalData.GroupChatInfoView(
ModalManager.end.showModalCloseable(showClose = true) { close ->
AddGroupMembersView(rhId, groupInfo, false, chatModel, close)
}
}
}.invokeOnCompletion { addGroupMembersOpening.set(false) }
},
showMemberInfo = { member, groupRelay ->
val connStats = mutableStateOf<ConnectionStats?>(null)
@@ -1293,6 +1295,8 @@ private fun ShareViaChatButton(onClick: () -> Unit) {
}
}
private val channelMembersOpening = AtomicBoolean(false)
@Composable
private fun ChannelMembersButton(rhId: Long?, groupInfo: GroupInfo, showMemberInfo: (GroupMember, GroupRelay?) -> Unit) {
val title = if (groupInfo.isOwner) {
@@ -1304,29 +1308,33 @@ private fun ChannelMembersButton(rhId: Long?, groupInfo: GroupInfo, showMemberIn
painterResource(MR.images.ic_group),
title,
click = {
if (!channelMembersOpening.compareAndSet(false, true)) return@SettingsActionItem
withBGApi {
setGroupMembers(rhId, groupInfo, chatModel)
ModalManager.end.showModalCloseable(true) { close ->
ChannelMembersView(rhId, groupInfo, chatModel, close) { member -> showMemberInfo(member, null) }
}
}
}.invokeOnCompletion { channelMembersOpening.set(false) }
},
iconColor = MaterialTheme.colors.secondary
)
}
private val channelRelaysOpening = AtomicBoolean(false)
@Composable
private fun ChannelRelaysButton(rhId: Long?, groupInfo: GroupInfo, showMemberInfo: (GroupMember, GroupRelay?) -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_wifi_tethering),
stringResource(MR.strings.button_channel_relays),
click = {
if (!channelRelaysOpening.compareAndSet(false, true)) return@SettingsActionItem
withBGApi {
setGroupMembers(rhId, groupInfo, chatModel)
ModalManager.end.showModalCloseable(true) { close ->
ChannelRelaysView(rhId, groupInfo, chatModel, close, showMemberInfo)
}
}
}.invokeOnCompletion { channelRelaysOpening.set(false) }
},
iconColor = MaterialTheme.colors.secondary
)
@@ -77,8 +77,13 @@ fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) {
}
suspend fun showGroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo) {
if (!secondaryChatOpening.compareAndSet(false, true)) return
val reportsChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.MsgContentTagContext(MsgContentTag.Report))
openChat(secondaryChatsCtx = reportsChatsCtx, chatModel.remoteHostId(), chatInfo)
try {
openChat(secondaryChatsCtx = reportsChatsCtx, chatModel.remoteHostId(), chatInfo)
} finally {
secondaryChatOpening.set(false)
}
ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close ->
ModalView({}, showAppBar = false) {
val chatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chatModel.chatId.value }?.chatInfo } }.value
@@ -19,6 +19,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.res.*
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import java.util.concurrent.atomic.AtomicBoolean
@Composable
private fun MemberSupportChatView(
@@ -156,9 +157,16 @@ fun MemberSupportChatToolbarTitle(member: GroupMember, imageSize: Dp = 40.dp, ic
}
}
val secondaryChatOpening = AtomicBoolean(false)
suspend fun showMemberSupportChatView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo, scopeInfo: GroupChatScopeInfo) {
if (!secondaryChatOpening.compareAndSet(false, true)) return
val memberSupportChatsCtx = ChatModel.ChatsContext(secondaryContextFilter = SecondaryContextFilter.GroupChatScopeContext(scopeInfo))
openChat(secondaryChatsCtx = memberSupportChatsCtx, chatModel.remoteHostId(), chatInfo)
try {
openChat(secondaryChatsCtx = memberSupportChatsCtx, chatModel.remoteHostId(), chatInfo)
} finally {
secondaryChatOpening.set(false)
}
ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close ->
ModalView({}, showAppBar = false) {
if (chatInfo is ChatInfo.Group && chatInfo.groupChatScope != null) {