ui: include support chats that need attention in the chat list unread filter

A group whose support chats need attention - for a member, unread replies in
their own "Chat with admins"; for a moderator, members waiting on them - is
flagged on its chat list row, but the unread filter did not list it.
chatStats.unreadCount is filled by a query scoped to the main conversation
(group_scope_tag IS NULL), so, except for a pending member's own support chat,
unreadTag cannot see support scopes at all.

Match on Chat.hasUnread, which adds supportUnreadCount to unreadTag, gated on
the chat not being muted to "mute all" - so a support chat waiting in a fully
muted group does not put it in the filter, matching what the profile's unread
count does with that group. Marking such a chat unread by hand still lists it,
through unreadTag's unreadChat branch. On iOS, updateChatInfo changes the chat
in place, so it now publishes when hasUnread changes, for the filter to update.

supportUnreadCount is the same value the row's flag shows. For a moderator it is
membersRequireAttention, which counts members: pending approval or review, or
with unread messages nobody has answered or unread mentions. Reading does not
clear a pending member - approving (once nothing else in their support chat
needs attention), rejecting, or the member leaving does - so the group stays
listed until then. Mentions-only chats still count support chats, on the view
that mentions-only is a statement about group chatter, and support chats (the
user's own line to the admins, or members waiting on a moderator) are not
chatter.
This commit is contained in:
Narasimha-sc committed 2026-10-20 09:13:27 +00:00
1 parent ef6c913f78
commit 43ad806482
8 files changed
+12 -6

No files matched your search

+4
View File
@@ -623,12 +623,14 @@ final class ChatModel: ObservableObject {
// Spec: spec/state.md#updateChatInfo
func updateChatInfo(_ cInfo: ChatInfo) {
if let i = getChatIndex(cInfo.id) {
let wasUnread = chats[i].hasUnread
if case let .group(groupInfo, groupChatScope) = cInfo, groupChatScope != nil {
chats[i].chatInfo = .group(groupInfo: groupInfo, groupChatScope: nil)
} else {
chats[i].chatInfo = cInfo
}
chats[i].created = Date.now
if chats[i].hasUnread != wasUnread { objectWillChange.send() }
}
}
@@ -1470,6 +1472,8 @@ final class Chat: ObservableObject, Identifiable, ChatLike {
}
}
var hasUnread: Bool { unreadTag || (chatInfo.chatSettings?.enableNtfs != MsgFilter.none && supportUnreadCount > 0) }
public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
}
@@ -673,7 +673,7 @@ struct ChatListView: View {
switch chatTagsModel.activeFilter {
case let .presetTag(tag): presetTagMatchesChat(tag, chat.chatInfo, chat.chatStats)
case let .userTag(tag): chat.chatInfo.chatTags?.contains(tag.chatTagId) == true
case .unread: chat.unreadTag
case .unread: chat.hasUnread
case .none: true
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ Managed by `ChatTagsModel` and `TagListView`:
| Filter | PresetTag | Description |
|---|---|---|
| All | (none) | No filter, shows all chats |
| Unread | `.unread` | Chats with unread messages |
| Unread | `.unread` | Chats with unread messages, or support chats that need attention (as flagged on the row: unread messages or mentions, or, for moderators, members pending, with unread messages nobody has answered, or with unread mentions) unless muted to "mute all" |
| Favorites | `.favorites` | User-favorited chats |
| Groups | `.groups` | Group conversations only |
| Contacts | `.contacts` | Direct contacts only |
+1 -1
View File
@@ -164,7 +164,7 @@ Horizontal scrolling tab bar below the navigation bar. Tabs:
| Tab | Filter | Shows |
|-----|--------|-------|
| All | `nil` | All conversations |
| Unread | `.unread` | Conversations with unread messages |
| Unread | `.unread` | Conversations with unread messages, or support chats that need attention (as flagged on the row: unread messages or mentions, or, for moderators, members pending, with unread messages nobody has answered, or with unread mentions) unless muted to "mute all" |
| Favorites | `.presetTag(.favorites)` | Favorited conversations |
| Groups | `.presetTag(.groups)` | Group conversations |
| Contacts | `.presetTag(.contacts)` | Direct conversations |
@@ -1507,6 +1507,8 @@ data class Chat(
else -> 0
}
val hasUnread: Boolean get() = unreadTag || (chatInfo.chatSettings?.enableNtfs != None && supportUnreadCount > 0)
fun groupFeatureEnabled(feature: GroupFeature): Boolean =
if (chatInfo is ChatInfo.Group) {
chatInfo.groupInfo.groupFeatureEnabled(feature)
@@ -1601,7 +1601,7 @@ private fun filtered(chat: Chat, activeFilter: ActiveFilter?): Boolean =
when (activeFilter) {
is ActiveFilter.PresetTag -> presetTagMatchesChat(activeFilter.tag, chat.chatInfo, chat.chatStats)
is ActiveFilter.UserTag -> chat.chatInfo.chatTags?.contains(activeFilter.tag.chatTagId) ?: false
is ActiveFilter.Unread -> chat.unreadTag
is ActiveFilter.Unread -> chat.hasUnread
else -> true
}
@@ -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 (toggle via filter button) |
| Unread | `ActiveFilter.Unread` | Filter list icon | Chats with unread messages, or support chats that need attention (as flagged on the row: unread messages or mentions, or, for moderators, members pending, with unread messages nobody has answered, or with unread mentions) 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
+1 -1
View File
@@ -144,7 +144,7 @@ The `filteredChats` function (line ~1474) 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 chats with `unreadCount > 0` or `unreadChat == true`.
- `Unread`: Matches `Chat.hasUnread` — a chat marked unread by hand, unread messages as the chat's notification setting counts them, or support chats that need attention (as flagged on the row: unread messages or mentions, or, for moderators, members pending, with unread messages nobody has answered, or with unread mentions) unless the chat is muted to "mute all".
### Search Bar