mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-14 07:10:19 +00:00
core, ui: don't count muted chats in profile unread counter, count chats marked unread
Unread counter above the profiles counted unread mentions in groups with mentions-only notifications, but nothing in muted chats, so two profiles with chats that both show as muted could show different counters. getUsersInfo now also counts chats marked unread - 1 per chat, unless the chat has unread items counted already. Muted chats are not counted at all, including when they are marked unread. Clients use the same rule in Chat.userUnreadCount and update the counter by the difference between what the chat contributed before and after, so that muting a chat or marking it unread updates the counter without reloading.
This commit is contained in:
@@ -567,12 +567,15 @@ final class ChatModel: ObservableObject {
|
||||
// Spec: spec/state.md#updateChatInfo
|
||||
func updateChatInfo(_ cInfo: ChatInfo) {
|
||||
if let i = getChatIndex(cInfo.id) {
|
||||
let prevUserUnread = chats[i].userUnreadCount
|
||||
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
|
||||
// muted chat does not count towards user unread counter, so it is updated when notifications change
|
||||
if let user = currentUser { changeUnreadCounter(user: user, by: chats[i].userUnreadCount - prevUserUnread) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -959,8 +962,10 @@ final class ChatModel: ObservableObject {
|
||||
func markChatUnread(_ cInfo: ChatInfo, unreadChat: Bool = true) {
|
||||
_updateChat(cInfo.id) { chat in
|
||||
let wasUnread = chat.unreadTag
|
||||
let prevUserUnread = chat.userUnreadCount
|
||||
chat.chatStats.unreadChat = unreadChat
|
||||
ChatTagsModel.shared.updateChatTagRead(chat, wasUnread: wasUnread)
|
||||
self.changeUnreadCounter(user: self.currentUser!, by: chat.userUnreadCount - prevUserUnread)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1103,11 +1108,12 @@ final class ChatModel: ObservableObject {
|
||||
|
||||
func changeUnreadCounter(_ chatIndex: Int, by count: Int, unreadMentions: Int) {
|
||||
let wasUnread = chats[chatIndex].unreadTag
|
||||
let prevUserUnread = chats[chatIndex].userUnreadCount
|
||||
let stats = chats[chatIndex].chatStats
|
||||
chats[chatIndex].chatStats.unreadCount = stats.unreadCount + count
|
||||
chats[chatIndex].chatStats.unreadMentions = stats.unreadMentions + unreadMentions
|
||||
ChatTagsModel.shared.updateChatTagRead(chats[chatIndex], wasUnread: wasUnread)
|
||||
changeUnreadCounter(user: currentUser!, by: count)
|
||||
changeUnreadCounter(user: currentUser!, by: chats[chatIndex].userUnreadCount - prevUserUnread)
|
||||
}
|
||||
|
||||
func increaseUnreadCounter(user: any UserLike) {
|
||||
@@ -1115,10 +1121,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
func decreaseUnreadCounter(user: any UserLike, chat: Chat) {
|
||||
let by = chat.chatInfo.chatSettings?.enableNtfs == .mentions
|
||||
? chat.chatStats.unreadMentions
|
||||
: chat.chatStats.unreadCount
|
||||
decreaseUnreadCounter(user: user, by: by)
|
||||
decreaseUnreadCounter(user: user, by: chat.userUnreadCount)
|
||||
}
|
||||
|
||||
func decreaseUnreadCounter(user: any UserLike, by: Int = 1) {
|
||||
@@ -1126,6 +1129,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func changeUnreadCounter(user: any UserLike, by: Int) {
|
||||
if by == 0 { return }
|
||||
if let i = users.firstIndex(where: { $0.user.userId == user.userId }) {
|
||||
users[i].unreadCount += by
|
||||
}
|
||||
@@ -1136,11 +1140,7 @@ final class ChatModel: ObservableObject {
|
||||
func totalUnreadCountForAllUsers() -> Int {
|
||||
var unread: Int = 0
|
||||
for chat in chats {
|
||||
switch chat.chatInfo.chatSettings?.enableNtfs {
|
||||
case .all: unread += chat.chatStats.unreadCount
|
||||
case .mentions: unread += chat.chatStats.unreadMentions
|
||||
default: ()
|
||||
}
|
||||
unread += chat.userUnreadCount
|
||||
}
|
||||
for u in users {
|
||||
if !u.user.activeUser {
|
||||
@@ -1391,6 +1391,15 @@ final class Chat: ObservableObject, Identifiable, ChatLike {
|
||||
}
|
||||
}
|
||||
|
||||
// what this chat adds to unread counter of its user profile - has to match getUsersInfo in Profiles.hs
|
||||
var userUnreadCount: Int {
|
||||
switch chatInfo.chatSettings?.enableNtfs {
|
||||
case .all: chatStats.unreadCount > 0 ? chatStats.unreadCount : (chatStats.unreadChat ? 1 : 0)
|
||||
case .mentions: chatStats.unreadMentions > 0 ? chatStats.unreadMentions : (chatStats.unreadChat ? 1 : 0)
|
||||
default: 0
|
||||
}
|
||||
}
|
||||
|
||||
var id: ChatId { get { chatInfo.id } }
|
||||
|
||||
var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } }
|
||||
|
||||
@@ -1869,6 +1869,8 @@ func apiCallStatus(_ contact: Contact, _ status: String) async throws {
|
||||
|
||||
func markChatRead(_ im: ItemsModel, _ chat: Chat) async {
|
||||
do {
|
||||
// markAllChatItemsRead resets chat stats of the same chat object, so it has to be read before
|
||||
let unreadChat = chat.chatStats.unreadChat
|
||||
if chat.chatStats.unreadCount > 0 {
|
||||
let cInfo = chat.chatInfo
|
||||
try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId)
|
||||
@@ -1876,7 +1878,7 @@ func markChatRead(_ im: ItemsModel, _ chat: Chat) async {
|
||||
withAnimation { ChatModel.shared.markAllChatItemsRead(im, cInfo) }
|
||||
}
|
||||
}
|
||||
if chat.chatStats.unreadChat {
|
||||
if unreadChat {
|
||||
await markChatUnread(chat, unreadChat: false)
|
||||
}
|
||||
} catch {
|
||||
|
||||
+27
-9
@@ -433,7 +433,10 @@ object ChatModel {
|
||||
} else if (currentCInfo is ChatInfo.Group && newCInfo is ChatInfo.Group && newCInfo.groupChatScope != null) {
|
||||
newCInfo = newCInfo.copy(groupInfo = newCInfo.groupInfo, groupChatScope = null)
|
||||
}
|
||||
chats[i] = chats[i].copy(chatInfo = newCInfo)
|
||||
val updatedChat = chats[i].copy(chatInfo = newCInfo)
|
||||
// muted chat does not count towards user unread counter, so it is updated when notifications change
|
||||
currentUser.value?.let { changeUserUnreadCounter(rhId, it, chats[i], updatedChat) }
|
||||
chats[i] = updatedChat
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,6 +493,7 @@ object ChatModel {
|
||||
suspend fun replaceChat(rhId: Long?, id: String, chat: Chat) {
|
||||
val i = getChatIndex(rhId, id)
|
||||
if (i >= 0) {
|
||||
currentUser.value?.let { changeUserUnreadCounter(rhId, it, chats[i], chat) }
|
||||
chats[i] = chat
|
||||
} else {
|
||||
// invalid state, correcting
|
||||
@@ -557,11 +561,11 @@ object ChatModel {
|
||||
chatItems = arrayListOf(newPreviewItem),
|
||||
chatStats =
|
||||
if (cItem.meta.itemStatus is CIStatus.RcvNew) {
|
||||
increaseUnreadCounter(rhId, currentUser.value!!)
|
||||
chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions)
|
||||
} else
|
||||
chat.chatStats
|
||||
)
|
||||
changeUserUnreadCounter(rhId, currentUser.value!!, chat, chatsContext.chats[i])
|
||||
updateChatTagReadInPrimaryContext(chatsContext.chats[i], wasUnread)
|
||||
}
|
||||
// pop chat
|
||||
@@ -621,7 +625,7 @@ object ChatModel {
|
||||
chats[i] = chat.copy(chatItems = arrayListOf(cItem))
|
||||
if (pItem.isRcvNew && !cItem.isRcvNew) {
|
||||
// status changed from New to Read, update counter
|
||||
decreaseCounterInPrimaryContext(rhId, cInfo.id)
|
||||
decreaseCounterInPrimaryContext(rhId, cInfo.id, cItem.meta.userMention)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -671,7 +675,7 @@ object ChatModel {
|
||||
// update chat list
|
||||
if (cInfo.groupChatScope() == null) {
|
||||
if (cItem.isRcvNew) {
|
||||
decreaseCounterInPrimaryContext(rhId, cInfo.id)
|
||||
decreaseCounterInPrimaryContext(rhId, cInfo.id, cItem.meta.userMention)
|
||||
}
|
||||
// update preview
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
@@ -723,7 +727,7 @@ object ChatModel {
|
||||
for (item in chatItems.value) {
|
||||
if (isRemovedMemberItem(item)) {
|
||||
if (item.isRcvNew) {
|
||||
decreaseCounterInPrimaryContext(rhId, groupInfo.id)
|
||||
decreaseCounterInPrimaryContext(rhId, groupInfo.id, item.meta.userMention)
|
||||
}
|
||||
if (item.isActiveReport) {
|
||||
decreaseGroupReportsCounter(rhId, groupInfo.id)
|
||||
@@ -768,9 +772,9 @@ object ChatModel {
|
||||
// clear preview
|
||||
val i = getChatIndex(rhId, cInfo.id)
|
||||
if (i >= 0) {
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount)
|
||||
val chatBefore = chats[i]
|
||||
chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo)
|
||||
changeUserUnreadCounter(rhId, currentUser.value!!, chatBefore, chats[i])
|
||||
markChatTagRead(chatBefore)
|
||||
}
|
||||
// clear current chat
|
||||
@@ -841,10 +845,10 @@ object ChatModel {
|
||||
val wasUnread = chat.unreadTag
|
||||
val unreadCount = if (itemIds != null) chat.chatStats.unreadCount - markedRead else 0
|
||||
val unreadMentions = if (itemIds != null) chat.chatStats.unreadMentions - mentionsMarkedRead else 0
|
||||
decreaseUnreadCounter(remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIdx] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(unreadCount = unreadCount, unreadMentions = unreadMentions)
|
||||
)
|
||||
changeUserUnreadCounter(remoteHostId, currentUser.value!!, chat, chats[chatIdx])
|
||||
updateChatTagReadInPrimaryContext(chats[chatIdx], wasUnread)
|
||||
}
|
||||
}
|
||||
@@ -886,7 +890,7 @@ object ChatModel {
|
||||
return markedRead to mentionsMarkedRead
|
||||
}
|
||||
|
||||
private fun decreaseCounterInPrimaryContext(rhId: Long?, chatId: ChatId) {
|
||||
private fun decreaseCounterInPrimaryContext(rhId: Long?, chatId: ChatId, userMention: Boolean) {
|
||||
// updates anything only in main ChatView, not GroupReportsView or anything else from the future
|
||||
if (secondaryContextFilter != null) return
|
||||
|
||||
@@ -896,12 +900,13 @@ object ChatModel {
|
||||
val chat = chats[chatIndex]
|
||||
val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0)
|
||||
val wasUnread = chat.unreadTag
|
||||
decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount)
|
||||
chats[chatIndex] = chat.copy(
|
||||
chatStats = chat.chatStats.copy(
|
||||
unreadCount = unreadCount,
|
||||
unreadMentions = if (userMention) kotlin.math.max(chat.chatStats.unreadMentions - 1, 0) else chat.chatStats.unreadMentions,
|
||||
)
|
||||
)
|
||||
changeUserUnreadCounter(rhId, currentUser.value!!, chat, chats[chatIndex])
|
||||
updateChatTagReadInPrimaryContext(chats[chatIndex], wasUnread)
|
||||
}
|
||||
|
||||
@@ -984,6 +989,12 @@ object ChatModel {
|
||||
changeUnreadCounterInPrimaryContext(rhId, user, 1)
|
||||
}
|
||||
|
||||
// updates unread counter of the user profile by the difference of what the chat contributes to it - see Chat.userUnreadCount
|
||||
private fun changeUserUnreadCounter(rhId: Long?, user: UserLike, prevChat: Chat, updatedChat: Chat) {
|
||||
val by = updatedChat.userUnreadCount - prevChat.userUnreadCount
|
||||
if (by != 0) changeUnreadCounterInPrimaryContext(rhId, user, by)
|
||||
}
|
||||
|
||||
fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) {
|
||||
changeUnreadCounterInPrimaryContext(rhId, user, -by)
|
||||
}
|
||||
@@ -1425,6 +1436,13 @@ data class Chat(
|
||||
else -> chatStats.unreadChat
|
||||
}
|
||||
|
||||
// what this chat adds to unread counter of its user profile - has to match getUsersInfo in Profiles.hs
|
||||
val userUnreadCount: Int get() = when (chatInfo.chatSettings?.enableNtfs) {
|
||||
All -> if (chatStats.unreadCount > 0) chatStats.unreadCount else if (chatStats.unreadChat) 1 else 0
|
||||
Mentions -> if (chatStats.unreadMentions > 0) chatStats.unreadMentions else if (chatStats.unreadChat) 1 else 0
|
||||
else -> 0
|
||||
}
|
||||
|
||||
val id: String get() = chatInfo.id
|
||||
|
||||
val supportUnreadCount: Int get() = when (chatInfo) {
|
||||
|
||||
@@ -194,7 +194,31 @@ getUsersInfo db = getUsers db >>= mapM getUserInfo
|
||||
AND (g.enable_ntfs = 1 OR g.enable_ntfs IS NULL OR (g.enable_ntfs = 2 AND i.user_mention = 1))
|
||||
|]
|
||||
(userId, CISRcvNew)
|
||||
pure UserInfo {user, unreadCount = fromMaybe 0 ctCount + fromMaybe 0 gCount}
|
||||
-- chat marked unread counts as 1, unless it has unread items counted above
|
||||
unreadChatCount <-
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT
|
||||
(SELECT COUNT(1) FROM contacts ct
|
||||
WHERE ct.user_id = ? AND ct.unread_chat = 1 AND ct.is_user = 0 AND ct.deleted = 0
|
||||
AND (ct.enable_ntfs = 1 OR ct.enable_ntfs IS NULL)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM chat_items i
|
||||
WHERE i.user_id = ct.user_id AND i.contact_id = ct.contact_id AND i.item_status = ?
|
||||
))
|
||||
+ (SELECT COUNT(1) FROM groups g
|
||||
WHERE g.user_id = ? AND g.unread_chat = 1
|
||||
AND (g.enable_ntfs = 1 OR g.enable_ntfs IS NULL OR g.enable_ntfs = 2)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM chat_items i
|
||||
WHERE i.user_id = g.user_id AND i.group_id = g.group_id AND i.item_status = ?
|
||||
AND (COALESCE(g.enable_ntfs, 1) <> 2 OR i.user_mention = 1)
|
||||
))
|
||||
|]
|
||||
(userId, CISRcvNew, userId, CISRcvNew)
|
||||
pure UserInfo {user, unreadCount = fromMaybe 0 ctCount + fromMaybe 0 gCount + fromMaybe 0 unreadChatCount}
|
||||
|
||||
getUsers :: DB.Connection -> IO [User]
|
||||
getUsers db = do
|
||||
|
||||
Reference in New Issue
Block a user