android, desktop: chat info shows another chat's mute, receipts and search; contact edits revert (#7927)

* desktop: show the open chat's mute, receipts and search in chat info

* android, desktop: show the open chat's mute state in the chat menu

* android, desktop: keep both contact info changes to mute and receipts

* android, desktop: list all members when the member search is hidden

* android, desktop: remove the unused chat menu mute state
This commit is contained in:
Narasimha-sc authored and GitHub committed 2026-10-31 20:19:58 +00:00
1 parent 0735bd2c6e
commit d1b3da8410
9 files changed
+120 -33

No files matched your search

@@ -73,7 +73,7 @@ fun ChatInfoView(
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null && currentUser != null) {
val chatRh = chat.remoteHostId
val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
val sendReceipts = remember(contact.id, contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) }
val chatItemTTL = remember(contact.id) { mutableStateOf(if (contact.chatItemTTL != null) ChatItemTTL.fromSeconds(contact.chatItemTTL) else null) }
val deletingItems = rememberSaveable(contact.id) { mutableStateOf(false) }
@@ -83,8 +83,9 @@ fun ChatInfoView(
currentUser,
sendReceipts = sendReceipts,
setSendReceipts = { sendRcpts ->
val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool)
updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel)
val chatInfo = (chatModel.getChat(chat.id) ?: chat).chatInfo
val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool)
updateChatSettings(chat.remoteHostId, chatInfo, chatSettings, chatModel)
sendReceipts.value = sendRcpts
},
chatItemTTL = chatItemTTL,
@@ -909,8 +910,7 @@ fun MuteButton(
chat: Chat,
contact: Contact
) {
val enableNtfs = remember { mutableStateOf(contact.chatSettings.enableNtfs ) }
val nextNtfMode by remember { derivedStateOf { enableNtfs.value.nextMode(false) } }
val nextNtfMode = contact.chatSettings.enableNtfs.nextMode(false)
val disabled = !contact.ready || !contact.active
InfoViewActionButton(
@@ -920,7 +920,7 @@ fun MuteButton(
disabled = disabled,
disabledLook = disabled,
onClick = {
toggleNotifications(chat.remoteHostId, chat.chatInfo, nextNtfMode, chatModel, enableNtfs)
toggleNotifications(chat.remoteHostId, (chatModel.getChat(chat.id) ?: chat).chatInfo, nextNtfMode, chatModel)
}
)
}
@@ -1229,7 +1229,7 @@ private fun ContactPreferencesButton(onClick: () -> Unit) {
@Composable
private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, onSelected: (SendReceipts) -> Unit) {
val values = remember {
val values = remember(currentUser.sendRcptsContacts) {
mutableListOf(SendReceipts.Yes, SendReceipts.No, SendReceipts.UserDefault(currentUser.sendRcptsContacts)).map { it to it.text }
}
ExposedDropDownSettingRow(
@@ -812,7 +812,7 @@ fun ChatView(
}
}
},
changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) },
changeNtfsState = { enabled -> toggleNotifications(chatRh, chatInfo, enabled, chatModel) },
onSearchValueChanged = onSearchValueChanged,
closeSearch = {
if (chatModel.openAroundItemId.value == null) {
@@ -976,7 +976,7 @@ fun ChatLayout(
openGroupLink: (GroupInfo) -> Unit,
markItemsRead: (List<Long>) -> Unit,
markChatRead: () -> Unit,
changeNtfsState: (MsgFilter, currentValue: MutableState<MsgFilter>) -> Unit,
changeNtfsState: (MsgFilter) -> Unit,
onSearchValueChanged: (String) -> Unit,
closeSearch: () -> Unit,
onComposed: suspend (chatId: String) -> Unit,
@@ -1217,7 +1217,7 @@ fun BoxScope.ChatInfoToolbar(
endCall: () -> Unit,
addMembers: (GroupInfo) -> Unit,
openGroupLink: (GroupInfo) -> Unit,
changeNtfsState: (MsgFilter, currentValue: MutableState<MsgFilter>) -> Unit,
changeNtfsState: (MsgFilter) -> Unit,
onSearchValueChanged: (String) -> Unit,
showSearch: MutableState<Boolean>,
contentFilter: MutableState<ContentFilter?>,
@@ -1372,8 +1372,7 @@ fun BoxScope.ChatInfoToolbar(
val enableNtfs = chatInfo.chatSettings?.enableNtfs
if (((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) && enableNtfs != null) {
val ntfMode = remember { mutableStateOf(enableNtfs) }
val nextNtfMode by remember { derivedStateOf { ntfMode.value.nextMode(chatInfo.hasMentions) } }
val nextNtfMode = enableNtfs.nextMode(chatInfo.hasMentions)
menuItems.add {
ItemAction(
stringResource(nextNtfMode.text(chatInfo.hasMentions)),
@@ -1383,7 +1382,7 @@ fun BoxScope.ChatInfoToolbar(
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
scope.launch {
delay(200)
changeNtfsState(nextNtfMode, ntfMode)
changeNtfsState(nextNtfMode)
}
}
)
@@ -3933,7 +3932,7 @@ fun PreviewChatLayout() = BubblePreview {
openGroupLink = {},
markItemsRead = { _ -> },
markChatRead = {},
changeNtfsState = { _, _ -> },
changeNtfsState = { _ -> },
onSearchValueChanged = {},
closeSearch = {},
onComposed = {},
@@ -4016,7 +4015,7 @@ fun PreviewGroupChatLayout() = BubblePreview {
openGroupLink = {},
markItemsRead = { _ -> },
markChatRead = {},
changeNtfsState = { _, _ -> },
changeNtfsState = { _ -> },
onSearchValueChanged = {},
closeSearch = {},
onComposed = {},
@@ -72,7 +72,7 @@ fun ModalData.ChannelMembersView(
}
}
}
val filtered = if (s.isEmpty()) members else members.filter { it.anyNameContains(s) }
val filtered = if (!showSearch || s.isEmpty()) members else members.filter { it.anyNameContains(s) }
channelMemberItems(filtered, groupInfo, GroupMemberRole.Member, dividerAboveFirst = true, showMemberInfo)
} else {
val contributors = members.filter { it.memberRole >= GroupMemberRole.Member && it.memberStatus != GroupMemberStatus.MemUnknown }
@@ -96,7 +96,7 @@ fun ModalData.ChannelMembersView(
}
}
}
val filtered = if (s.isEmpty()) contributors else contributors.filter { it.anyNameContains(s) }
val filtered = if (!showSearch || s.isEmpty()) contributors else contributors.filter { it.anyNameContains(s) }
channelMemberItems(filtered, groupInfo, GroupMemberRole.Moderator, dividerAboveFirst = showSearch || showUserRow, showMemberInfo)
}
item {
@@ -78,7 +78,7 @@ fun ModalData.GroupChatInfoView(
val developerTools = chatModel.controller.appPrefs.developerTools.get()
if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) {
val groupInfo = chat.chatInfo.groupInfo
val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, currentUser.sendRcptsSmallGroups)) }
val sendReceipts = remember(groupInfo.id, groupInfo.chatSettings.sendRcpts, currentUser.sendRcptsSmallGroups) { mutableStateOf(SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, currentUser.sendRcptsSmallGroups)) }
val chatItemTTL = remember(groupInfo.id) { mutableStateOf(if (groupInfo.chatItemTTL != null) ChatItemTTL.fromSeconds(groupInfo.chatItemTTL) else null) }
val deletingItems = rememberSaveable(groupInfo.id) { mutableStateOf(false) }
val scope = rememberCoroutineScope()
@@ -423,8 +423,7 @@ fun MuteButton(
chat: Chat,
groupInfo: GroupInfo
) {
val notificationMode = remember { mutableStateOf(groupInfo.chatSettings.enableNtfs) }
val nextNotificationMode by remember { derivedStateOf { notificationMode.value.nextMode(true) } }
val nextNotificationMode = groupInfo.chatSettings.enableNtfs.nextMode(true)
InfoViewActionButton(
modifier = modifier,
@@ -433,7 +432,7 @@ fun MuteButton(
disabled = !groupInfo.ready,
disabledLook = !groupInfo.ready,
onClick = {
toggleNotifications(chat.remoteHostId, chat.chatInfo, nextNotificationMode, chatModel, notificationMode)
toggleNotifications(chat.remoteHostId, chat.chatInfo, nextNotificationMode, chatModel)
}
)
}
@@ -546,14 +545,15 @@ fun ModalData.GroupChatInfoLayout(
) {
val listState = remember { appBarHandler.listState }
val scope = rememberCoroutineScope()
val showMemberSearch = activeSortedMembers.size > 8
KeyChangeEffect(chat.id) {
scope.launch { listState.scrollToItem(0) }
}
val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } }
val filteredMembers = remember(activeSortedMembers) {
val searchText = remember(chat.id) { stateGetOrPut("searchText:${chat.id}") { TextFieldValue() } }
val filteredMembers = remember(activeSortedMembers, searchText) {
derivedStateOf {
val s = searchText.value.text.trim().lowercase()
if (s.isEmpty()) activeSortedMembers else activeSortedMembers.filter { m -> m.anyNameContains(s) }
if (!showMemberSearch || s.isEmpty()) activeSortedMembers else activeSortedMembers.filter { m -> m.anyNameContains(s) }
}
}
Box {
@@ -767,7 +767,7 @@ fun ModalData.GroupChatInfoLayout(
}
AddMembersButton(addMembersTitleId, tint, onAddMembersClick)
}
if (activeSortedMembers.size > 8) {
if (showMemberSearch) {
SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) {
MemberListSearchRowView(searchText)
}
@@ -1059,7 +1059,7 @@ private fun GroupReportsButton(chat: Chat, onClick: () -> Unit) {
@Composable
private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, onSelected: (SendReceipts) -> Unit) {
val values = remember {
val values = remember(currentUser.sendRcptsSmallGroups) {
mutableListOf(SendReceipts.Yes, SendReceipts.No, SendReceipts.UserDefault(currentUser.sendRcptsSmallGroups)).map { it to it.text }
}
ExposedDropDownSettingRow(
@@ -949,9 +949,9 @@ fun groupInvitationAcceptedAlert(rhId: Long?) {
)
}
fun toggleNotifications(remoteHostId: Long?, chatInfo: ChatInfo, filter: MsgFilter, chatModel: ChatModel, currentState: MutableState<MsgFilter>? = null) {
fun toggleNotifications(remoteHostId: Long?, chatInfo: ChatInfo, filter: MsgFilter, chatModel: ChatModel) {
val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = filter)
updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel, currentState)
updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel)
}
fun toggleChatFavorite(remoteHostId: Long?, chatInfo: ChatInfo, favorite: Boolean, chatModel: ChatModel) {
@@ -959,7 +959,7 @@ fun toggleChatFavorite(remoteHostId: Long?, chatInfo: ChatInfo, favorite: Boolea
updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel)
}
fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState<MsgFilter>? = null) {
fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: ChatSettings, chatModel: ChatModel) {
val newChatInfo = when(chatInfo) {
is ChatInfo.Direct -> with (chatInfo) {
ChatInfo.Direct(contact.copy(chatSettings = chatSettings))
@@ -996,10 +996,6 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch
chatModel.chatsContext.updateChatTagReadInPrimaryContext(updatedChat, wasUnread)
}
}
val current = currentState?.value
if (current != null) {
currentState.value = chatSettings.enableNtfs
}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
# Chat menu offers another chat's mute mode
## Problem
The mute item of the chat header ⋮ menu shows the next mode of the chat it was first shown for:
- Desktop: after clicking another chat in the list.
- Android and desktop: after opening a member's direct chat from a group, or opening a chat from a notification while another chat is open.
- Android and desktop: after muting from the chat info; desktop: after muting from the chat list menu. The item still offers "Mute".
A tap applies the remembered chat's next mode to the open chat. In a group opened after a contact, the item cycles through two modes, as for a contact.
## Cause
`ChatInfoToolbar` stores the mode in `remember { mutableStateOf(enableNtfs) }` without a key (#1026) and reads the next mode through `remember { derivedStateOf { ... } }` (#5574), which also captures the first chat's `hasMentions`. `ChatView` is not recreated per chat.
## Fix
`nextNtfMode = enableNtfs.nextMode(chatInfo.hasMentions)`, computed from the open chat on every composition.
`ntfMode` is removed, with the second parameter of `changeNtfsState` and the `currentState` parameter of `toggleNotifications` and `updateChatSettings` (`ChatListNavLinkView.kt`), which only set it.
## Scope
Android and desktop (common `ChatView`). The chat list menu already reads the list item's chat.
@@ -0,0 +1,22 @@
# Contact info changes undo each other
## Problem
In contact info, a settings change reverts earlier changes to the contact:
- Mute, then change "Send receipts": the contact is unmuted again.
- Change "Send receipts", then mute: the receipts change is lost, so receipts can be sent to a contact they were turned off for.
- Desktop: favorite the contact from the chat list while its info is open, then mute from the info: the favorite is removed.
- An alias set in the info reverts in the chat list and the chat header after a mute or receipts change from the same info, until the chats are reloaded.
## Cause
`ChatInfoView` stores `chat = remember(contact.id) { ... }`, a copy taken when contact info opened (#2898). The receipts setter and `MuteButton` build the saved `ChatSettings` from `chat.chatInfo`. `updateChatSettings` sends all settings to the core and writes that copy, with the new settings, into the model.
## Fix
The receipts setter and `MuteButton` pass `(chatModel.getChat(chat.id) ?: chat).chatInfo`, as the wallpaper save does.
## Scope
Android and desktop. In group info, the chat is read from the model on every composition. The header and the delete and clear dialogs still use the copy.
@@ -0,0 +1,28 @@
# Desktop chat info shows another chat's mute, receipts and member search
## Problem
On desktop the chat info pane stays open when another chat is clicked in the list, and then shows the new chat with some state of the previous chat:
- Mute: open muted contact A's info, click unmuted contact B. B's button says "Unmute"; a tap applies A's next mode, so B stays unmuted. Groups behave the same.
- Group "Send receipts": "no" in group A, then click group B: B shows "no" although its setting is the default.
- Member search: a search typed in group A's members filters group B's members. In a group of 8 members or fewer besides you, the search field is hidden, so the members of B that do not match stay hidden.
Changes made outside the pane while it is open are not shown: muting the chat from the chat list menu, and changing receipts in Settings → Your privacy.
## Cause
- Contact and group `MuteButton` store the mode in `remember { mutableStateOf(...) }` without a key, read through `derivedStateOf`. The pane is not recreated per chat, and only `updateChatSettings` called from the button updates that state.
- The group `sendReceipts` is `remember { ... }` without a key; the contact one is keyed by contact id only (#2898). Neither is keyed by the setting.
- The member search text is `stateGetOrPut("searchText")` in the pane's `ModalData`, one value for every chat the pane shows (#4817).
## Fix
- `MuteButton` (contact and group): `nextMode` of the chat's own `chatSettings.enableNtfs`; the local state and its `toggleNotifications` argument are removed.
- `sendReceipts` (contact and group): `remember` keyed by the chat id, the chat's `chatSettings.sendRcpts` and the user's default for that chat type. The setter still sets it before the API call returns. The option list of `SendReceiptsOption` is keyed by the user's default.
- Member search: `remember(chat.id) { stateGetOrPut("searchText:${chat.id}") { ... } }`; `filteredMembers` is keyed by `searchText` too. The text remains in the pane's `ModalData` after Back from a page opened over the pane.
## Scope
- The info screen closes on a chat switch on Android, and the info sheet is dismissed on iOS (`ChatView.swift`). Android contact info now also shows the saved mute and receipts values after the other setting is changed.
- Member info, message info, "Invite members" and "Chats with members" close on a chat switch (`KeyChangeEffect(chatModel.chatId.value)`); Group link opened from the chat menu stays open and acts on its own group. Not changed.
@@ -0,0 +1,17 @@
# Group and channel members stay filtered after the search field disappears
## Problem
With a search typed in a group of 9 members besides you, when a member leaves, the search field is hidden, and members that do not match stay hidden until the info is closed. The same applies to the channel members page ("Subscribers" for moderators, "Owners & contributors" for others) when a listed member is removed or leaves.
## Cause
The search field is shown only above 8 members (`GroupChatInfoLayout`, `ChannelMembersView`), while the filter applies whenever the text is not empty. The text is stored in the page's `ModalData` (#4817 group info, #7264 channel members), so it remains after the field leaves composition.
## Fix
The filter applies only while the field is shown: `!showMemberSearch || s.isEmpty()` in group info, `!showSearch || s.isEmpty()` for both lists of `ChannelMembersView`. The stored text filters the list again once the field is shown.
## Scope
Android and desktop.