From fa257d948ba373368a5f6ebd0667f18553827880 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:21:20 +0000 Subject: [PATCH] ui: don't match support chats in the unread filter when muted to "mute all" supportUnreadCount never reads chatSettings, so the filter listed a group with an unread support chat even when the user had muted it entirely - while the profile picker's counter, which the button now reads, drops that group. Gate the support half on enableNtfs != None so the two agree on a fully muted chat. Mentions-only is deliberately left counting support unread. The counter takes a mentions-only group's item only when it mentions the user; this predicate takes any support unread, on the view that mentions-only is a statement about group chatter and the user's own line to the admins is not chatter. "Mute all" is a statement about the whole chat, so it applies. --- apps/ios/Shared/Model/ChatModel.swift | 2 +- apps/ios/spec/client/chat-list.md | 2 +- apps/ios/spec/state.md | 2 +- .../commonMain/kotlin/chat/simplex/common/model/ChatModel.kt | 2 +- apps/multiplatform/product/views/chat-list.md | 2 +- apps/multiplatform/spec/client/chat-list.md | 2 +- apps/multiplatform/spec/state.md | 2 +- plans/2026-08-03-unread-filter-button-highlight.md | 4 ++-- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 1564b18843..edee5e69c2 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -1407,7 +1407,7 @@ final class Chat: ObservableObject, Identifiable, ChatLike { } } - var hasUnread: Bool { unreadTag || supportUnreadCount > 0 } + var hasUnread: Bool { unreadTag || (chatInfo.chatSettings?.enableNtfs != MsgFilter.none && supportUnreadCount > 0) } public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) } diff --git a/apps/ios/spec/client/chat-list.md b/apps/ios/spec/client/chat-list.md index 13a26789c5..a0d07bd0e0 100644 --- a/apps/ios/spec/client/chat-list.md +++ b/apps/ios/spec/client/chat-list.md @@ -163,7 +163,7 @@ Horizontal scrolling tab bar below the navigation bar. Tabs: | Tab | Filter | Shows | |-----|--------|-------| | All | `nil` | All conversations | -| Unread | `.unread` | Conversations with unread messages or unread support chats | +| Unread | `.unread` | Conversations with unread messages, or unread support chats unless muted to "mute all" | | Favorites | `.presetTag(.favorites)` | Favorited conversations | | Groups | `.presetTag(.groups)` | Group conversations | | Contacts | `.presetTag(.contacts)` | Direct conversations | diff --git a/apps/ios/spec/state.md b/apps/ios/spec/state.md index 07c82e2728..9ac0ed34de 100644 --- a/apps/ios/spec/state.md +++ b/apps/ios/spec/state.md @@ -330,7 +330,7 @@ struct ChatStats: Decodable, Hashable { | `viewId` | Unique view identity including creation time | [L1338](../Shared/Model/ChatModel.swift#L1338) | | `unreadTag` | Whether chat counts as "unread" based on notification settings | [L1328](../Shared/Model/ChatModel.swift#L1328) | | `supportUnreadCount` | Unread count for group support scope | [L1340](../Shared/Model/ChatModel.swift#L1340) | -| `hasUnread` | Whether chat matches the unread filter: `unreadTag` or any support unread | [L1410](../Shared/Model/ChatModel.swift#L1410) | +| `hasUnread` | Whether chat matches the unread filter: `unreadTag`, or support unread when not muted to `.none` | [L1410](../Shared/Model/ChatModel.swift#L1410) | --- diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index b1344403ab..92873076ba 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1437,7 +1437,7 @@ data class Chat( else -> 0 } - val hasUnread: Boolean get() = unreadTag || supportUnreadCount > 0 + val hasUnread: Boolean get() = unreadTag || (chatInfo.chatSettings?.enableNtfs != None && supportUnreadCount > 0) fun groupFeatureEnabled(feature: GroupFeature): Boolean = if (chatInfo is ChatInfo.Group) { diff --git a/apps/multiplatform/product/views/chat-list.md b/apps/multiplatform/product/views/chat-list.md index 7297e9f8ab..9746cbfcc6 100644 --- a/apps/multiplatform/product/views/chat-list.md +++ b/apps/multiplatform/product/views/chat-list.md @@ -65,7 +65,7 @@ Managed by `chatModel.userTags`, `chatModel.presetTags`, and `chatModel.activeCh | Business | `BUSINESS` | Work | Business chat conversations | | Notes | `NOTES` | Folder | Notes to self | | Custom tags | `UserTag(ChatTag)` | Label/emoji | User-created tags with custom emoji and name | -| Unread | `ActiveFilter.Unread` | Filter list icon | Chats with unread messages or unread support chats (toggle via filter button) | +| Unread | `ActiveFilter.Unread` | Filter list icon | Chats with unread messages, or unread support chats unless muted to "mute all" (toggle via filter button) | Display logic: - When collapsible preset tags exceed 3 total (with user tags), they collapse into a `CollapsedTagsFilterView` dropdown menu diff --git a/apps/multiplatform/spec/client/chat-list.md b/apps/multiplatform/spec/client/chat-list.md index 92bdd6dd87..7d749e4aee 100644 --- a/apps/multiplatform/spec/client/chat-list.md +++ b/apps/multiplatform/spec/client/chat-list.md @@ -143,7 +143,7 @@ The `filteredChats` function (line ~1188) applies filters in this order: 3. **Active filter:** - `PresetTag`: Matches chat type and characteristics (e.g., `CONTACTS` filters `ChatInfo.Direct`, `GROUPS` filters `ChatInfo.Group`). - `UserTag`: Matches chats whose `chatTags` contain the tag ID. - - `Unread`: Matches `Chat.hasUnread` — unread messages, as the chat's notification setting counts them, or unread support chats. + - `Unread`: Matches `Chat.hasUnread` — unread messages, as the chat's notification setting counts them, or unread support chats unless the chat is muted to "mute all". ### Search Bar diff --git a/apps/multiplatform/spec/state.md b/apps/multiplatform/spec/state.md index d6ca04178a..956ab41b3a 100644 --- a/apps/multiplatform/spec/state.md +++ b/apps/multiplatform/spec/state.md @@ -282,7 +282,7 @@ data class ChatStats( | `id` | 1349 | Chat ID derived from `chatInfo.id` | | `unreadTag` | 1343 | Whether chat counts as "unread" for tag filtering (considers notification settings) | | `supportUnreadCount` | 1351 | Unread count in support/moderation context | -| `hasUnread` | 1440 | Whether chat matches the unread filter: `unreadTag` or any support unread | +| `hasUnread` | 1440 | Whether chat matches the unread filter: `unreadTag`, or support unread when not muted to `None` | | `nextSendGrpInv` | 1337 | Whether next message should send group invitation | diff --git a/plans/2026-08-03-unread-filter-button-highlight.md b/plans/2026-08-03-unread-filter-button-highlight.md index eb81c3c826..26c47cade3 100644 --- a/plans/2026-08-03-unread-filter-button-highlight.md +++ b/plans/2026-08-03-unread-filter-button-highlight.md @@ -28,7 +28,7 @@ Two consequences of taking `supportUnreadCount` whole are deliberate. *Pending members count, and that signal is sticky.* For a moderator `supportUnreadCount` is `membersRequireAttention`, and the core computes `gmRequiresAttention` as `memberPending m || memberAttention > 0 || mentions > 0` (`Types.hs`) — so a member awaiting approval or review counts even with nothing to read. `updateSupportChatItemsRead` (`Store/Messages.hs`) zeroes the support counters but decrements the group's count only when the member stops requiring attention, which a pending member never does; approving or rejecting is what clears it. A moderator with a pending join request therefore sees the button tinted and the group listed under Unread with no unread badge until they act on the request. That is accepted: the member is genuinely waiting on the moderator, and the filter is where they will look for who is waiting. It does disagree with the chat list row, which renders `memberPending` as a grey flag rather than a call to action. -*Mute does not suppress the support half.* `unreadTag` gates on `enableNtfs`; `supportUnreadCount` never reads `chatSettings`, and this predicate does not add that gate. Muting a group silences its conversation but not its support chats — a message to or from admins still lights the button. That is intentional: mute is about group chatter, not about the user's own line to the people running the group. +*Mute all suppresses the support half; mentions-only does not.* `supportUnreadCount` never reads `chatSettings`, so the predicate adds the gate: `enableNtfs != None && supportUnreadCount > 0`. A group muted to *mute all* therefore drops out of the filter even with a support chat waiting, matching what the profile picker's counter does with it. Under *mentions only* the support half still counts, which is where the filter and the counter part company — the counter takes a mentions-only group's item only when it mentions the user, and this predicate takes any support unread. That is deliberate: mentions-only is a statement about group chatter, and the user's own line to the admins is not chatter; *mute all* is a statement about the whole chat. Support chats belong in this predicate because `unreadTag` alone cannot see them. `chatStats.unreadCount` is filled by a query scoped to the main conversation (`group_scope_tag IS NULL AND group_scope_group_member_id IS NULL`, `Store/Messages.hs`), so a group whose only unread sits in a member support scope satisfies neither `unreadTag` nor the filter. There was no way to reach the waiting member from the chat list. @@ -41,7 +41,7 @@ The tint is `users.any { it.user.activeUser && it.unreadCount > 0 }` — the sam This is deliberately *not* the predicate the filter matches on, so the two can disagree. Where they do: - **Marked unread.** `unread_chat` is a column on `contacts`/`groups`, and `getUsersInfo` never reads it. A chat you marked unread by hand lists under the filter with the button grey. -- **Muted chats.** The counter's group clause is `enable_ntfs = 1 OR IS NULL OR (enable_ntfs = 2 AND user_mention = 1)` (`MFNone = 0`, `MFAll = 1`, `MFMentions = 2`, `Types.hs`). So under *mute all* no support item counts, and under *mentions only* a support item counts only if it mentions the user — an ordinary support message does not. Either way the group lists under the filter, which ignores mute for the support half, without lighting the button. The contact clause is stricter still: `enable_ntfs = 1 OR IS NULL`, with no mentions term. +- **Mentions-only chats.** The counter's group clause is `enable_ntfs = 1 OR IS NULL OR (enable_ntfs = 2 AND user_mention = 1)` (`MFNone = 0`, `MFAll = 1`, `MFMentions = 2`, `Types.hs`). The two now agree on *mute all* — neither counts it — but under *mentions only* the counter takes a support item only when it mentions the user, while the filter takes any support unread. So an ordinary support message in a mentions-only group lists under the filter without lighting the button. The contact clause is stricter still: `enable_ntfs = 1 OR IS NULL`, with no mentions term. - **Pending members.** The counter counts messages, so a member awaiting approval with nothing written lists under the filter without lighting the button. The stickiness described above therefore never reaches the tint. Accepted: the button is a cue that something is worth a tap, matching the count the user already recognises from the picker, and the filter is the authority on what is actually listed.