From 306bbabe0db91ae8264d197285ac5a703602e0e2 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Sun, 18 Oct 2026 11:25:02 +0000 Subject: [PATCH] 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. --- .../chat/simplex/common/views/chat/ChatView.kt | 6 +++++- .../common/views/chat/group/GroupChatInfoView.kt | 14 +++++++++++--- .../common/views/chat/group/GroupReportsView.kt | 7 ++++++- .../views/chat/group/MemberSupportChatView.kt | 10 +++++++++- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index 960efd0350..84b2ee971c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -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) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 973b684d38..13265bb0e6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -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(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 ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt index 0c272ae98d..adb313fd70 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt @@ -77,8 +77,13 @@ fun ItemsReload(chatsCtx: ChatModel.ChatsContext,) { } suspend fun showGroupReportsView(staleChatId: State, scrollToItemId: MutableState, 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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt index 8e0b3321b5..260cf866f5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/MemberSupportChatView.kt @@ -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, scrollToItemId: MutableState, 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) {