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 45fc894b6e..0f1c603d80 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 @@ -413,6 +413,7 @@ fun ChatView( var preloadedContactInfo: Pair? = null var preloadedCode: String? = null var preloadedLink: GroupLink? = null + var preloadedChatId: ChatId? = chatInfo.id if (chatInfo is ChatInfo.Direct) { preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second @@ -428,27 +429,35 @@ fun ChatView( ModalView(close, cardScreen = true, appBar = appBar.value) { val chatInfo = remember { activeChat }.value?.chatInfo if (chatInfo is ChatInfo.Direct) { - var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } - var code: String? by remember { mutableStateOf(preloadedCode) } - KeyChangeEffect(chatInfo.id) { + var contactInfo: Pair? by remember(chatInfo.id) { mutableStateOf(if (chatInfo.id == preloadedChatId) preloadedContactInfo else null) } + var code: String? by remember(chatInfo.id) { mutableStateOf(if (chatInfo.id == preloadedChatId) preloadedCode else null) } + LaunchedEffect(chatInfo.id) { + if (chatInfo.id == preloadedChatId) return@LaunchedEffect + preloadedChatId = null contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) preloadedContactInfo = contactInfo code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second preloadedCode = code + preloadedChatId = chatInfo.id } ChatInfoView(chatsCtx, chatModel, chatInfo.contact, contactInfo?.first, contactInfo?.second, chatInfo.localAlias, code, close) { showSearch.value = true } } else if (chatInfo is ChatInfo.Group) { - var link: GroupLink? by remember(chatInfo.id) { mutableStateOf(preloadedLink) } - KeyChangeEffect(chatInfo.id) { + var link: GroupLink? by remember(chatInfo.id) { mutableStateOf(if (chatInfo.id == preloadedChatId) preloadedLink else null) } + var linkLoaded by remember(chatInfo.id) { mutableStateOf(chatInfo.id == preloadedChatId) } + LaunchedEffect(chatInfo.id) { + if (chatInfo.id == preloadedChatId && chatModel.membersLoaded.value) return@LaunchedEffect + preloadedChatId = null setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId) + linkLoaded = true preloadedLink = link + preloadedChatId = chatInfo.id } - GroupChatInfoView(chatsCtx, chatRh, chatInfo.id, link, selectedItems, appBar, scrollToItemId, { + GroupChatInfoView(chatsCtx, chatRh, chatInfo.id, link, linkLoaded, selectedItems, appBar, scrollToItemId, { link = it - preloadedLink = it + if (chatInfo.id == preloadedChatId) preloadedLink = it }, close, { showSearch.value = true }) } else { LaunchedEffect(Unit) { 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 8cbbeb18c7..82e5901a58 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 @@ -63,6 +63,7 @@ fun ModalData.GroupChatInfoView( rhId: Long?, chatId: String, groupLink: GroupLink?, + groupLinkLoaded: Boolean, selectedItems: MutableState?>, appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, scrollToItemId: MutableState, @@ -109,6 +110,7 @@ fun ModalData.GroupChatInfoView( developerTools, onLocalAliasChanged = { setGroupAlias(chat, it, chatModel) }, groupLink, + groupLinkLoaded, selectedItems, appBar, scrollToItemId, @@ -462,14 +464,15 @@ fun AddGroupMembersButton( fun ChannelLinkActionButton( modifier: Modifier, groupInfo: GroupInfo, + groupLinkLoaded: Boolean, manageGroupLink: () -> Unit ) { InfoViewActionButton( modifier = modifier, icon = painterResource(MR.images.ic_link), title = stringResource(MR.strings.action_button_channel_link), - disabled = !groupInfo.ready, - disabledLook = !groupInfo.ready, + disabled = !groupInfo.ready || !groupLinkLoaded, + disabledLook = !groupInfo.ready || !groupLinkLoaded, onClick = manageGroupLink ) } @@ -521,6 +524,7 @@ fun ModalData.GroupChatInfoLayout( developerTools: Boolean, onLocalAliasChanged: (String) -> Unit, groupLink: GroupLink?, + groupLinkLoaded: Boolean, selectedItems: MutableState?>, appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, scrollToItemId: MutableState, @@ -606,7 +610,7 @@ fun ModalData.GroupChatInfoLayout( ) { if (groupInfo.useRelays && groupInfo.isOwner) { SearchButton(modifier = Modifier.fillMaxWidth(0.33f), chat, groupInfo, close, onSearchClicked) - ChannelLinkActionButton(modifier = Modifier.fillMaxWidth(0.5f), groupInfo, manageGroupLink) + ChannelLinkActionButton(modifier = Modifier.fillMaxWidth(0.5f), groupInfo, groupLinkLoaded, manageGroupLink) MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo) } else if (!groupInfo.useRelays && groupInfo.canAddMembers) { SearchButton(modifier = Modifier.fillMaxWidth(0.33f), chat, groupInfo, close, onSearchClicked) @@ -642,7 +646,7 @@ fun ModalData.GroupChatInfoLayout( if (groupInfo.isOwner && groupLink != null) { anyTopSectionRowShow = true ChannelLinkButton(manageGroupLink) - } else if (channelLink != null) { + } else if (channelLink != null && (groupLinkLoaded || !groupInfo.isOwner)) { anyTopSectionRowShow = true ChannelLinkQRCodeSection(channelLink) ShareViaChatButton { @@ -681,7 +685,7 @@ fun ModalData.GroupChatInfoLayout( } } else { SectionView { - if (groupInfo.canAddMembers && groupInfo.businessChat == null) { + if (groupInfo.canAddMembers && groupInfo.businessChat == null && groupLinkLoaded) { anyTopSectionRowShow = true if (groupLink == null) { CreateGroupLinkButton(manageGroupLink) @@ -1460,6 +1464,7 @@ fun PreviewGroupChatInfoLayout() { developerTools = false, onLocalAliasChanged = {}, groupLink = null, + groupLinkLoaded = true, selectedItems = remember { mutableStateOf(null) }, appBar = remember { mutableStateOf(null) }, scrollToItemId = remember { mutableStateOf(null) }, diff --git a/plans/2026-10-15-desktop-chat-info-stale-after-chat-switch.md b/plans/2026-10-15-desktop-chat-info-stale-after-chat-switch.md new file mode 100644 index 0000000000..88c55746da --- /dev/null +++ b/plans/2026-10-15-desktop-chat-info-stale-after-chat-switch.md @@ -0,0 +1,43 @@ +# Desktop: chat info shows the previous chat's data after switching chats + +Fixes #5135. + +## Problem + +With chat info open in the right pane on desktop, clicking another chat can leave the pane showing the previous chat's data: +- Contact A's info → "Verify security code" → click contact B → "Verify security code": A's code is shown under "B is not verified", and "Mark verified" fails with "Incorrect security code!". +- Group A's info → "Group preferences" → click group B: B's info shows "1 members" and A's group link. +- Switching between a contact and a group with the info shown: the same, without any page on top. +- Contact A → contact B, or group A → group B, with the info shown: until B's info is reloaded, "Verify security code" opens A's code, and "Group link" opens and shares A's link as B's; "Create group link" for a group that has a link fails with "Error creating group link". +- Info on group A → another chat → back to A before the info has loaded: A's info shows only the user as a member. + +## Cause + +Before the app opens the pane, `ChatView` loads the contact info, security code, group link and group members into preloaded values, and the pane's state is initialised from them. They are reloaded in `KeyChangeEffect(chatInfo.id)` only when the key changes after the first composition of the pane's branch. + +The branch is composed again for another chat in two cases, and then nothing is reloaded: +- On a chat switch the app calls `ModalManager.end.closeModalsExceptFirst()` (`ChatListView.kt`), so a page on top of the pane ("Verify security code", "Group preferences", "Group link") is closed and the pane is composed for the new chat. +- The contact and group cases are separate branches, so after a switch between a contact and a group the app composes the other branch. + +When the key does change, the contact branch's state is remembered without a key and the group link is initialised from the previous group's link, so the previous chat's values are shown until the reload returns. + +The app clears the group members on every chat switch, and members are stored by `setGroupMembers` only while their group is the open chat. After info on group A → another chat → A, the members list is empty, and the pane opens for A, the chat its values were preloaded for, so nothing is reloaded. + +## Fix + +`preloadedChatId` stores the id of the chat the preloaded values belong to. The app reloads them in `LaunchedEffect(chatInfo.id)` when the shown chat's id differs from `preloadedChatId`, or when the shown chat is a group whose members are not loaded (`!chatModel.membersLoaded.value`). `preloadedChatId` is cleared while a load runs and set when it completes, so a cancelled load is redone. + +Until the reload completes, none of the previous chat's values are shown: +- The contact info, security code and group link are initialised empty for another chat (`remember(chatInfo.id)`); without a code the "Verify security code" row is hidden. +- While `linkLoaded` (passed to `GroupChatInfoView` as `groupLinkLoaded`) is false, the "Create group link" / "Group link" row and a channel owner's channel link rows are hidden, and the channel "Link" button is disabled. + +A link created, changed or deleted on the "Group link" page is stored in the preloaded values only while they belong to the group of that page. + +The app skips the reload on the first open and on Back for the same chat, as before, unless the group's members were cleared. + +## Scope + +- `ChatView.kt`: the info pane in `ChatView`. +- `GroupChatInfoView.kt`: the `groupLinkLoaded` parameter of `GroupChatInfoView`, `GroupChatInfoLayout` and `ChannelLinkActionButton`. +- Desktop. Android uses the same code; its info is a full-screen modal over the chat list, and the app closes it on a notification tap. +- iOS is unchanged: the contact info, security code and group link are loaded when its info views appear, and the group members before its `ChatView` shows the group info.