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 5c7eb76307..08314ab22b 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 @@ -68,10 +68,6 @@ object ChatModel { val reportsChatsContext = ChatsContext(MsgContentTag.Report) // declaration of chatsContext should be before any other variable that is taken from ChatsContext class and used in the model, otherwise, strange crash with NullPointerException for "this" parameter in random functions val chats: State> = chatsContext.chats - /** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on. - * If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages]. - * If you use api call to get the items, use just [add] instead of [addAndNotify]. - * Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */ // rhId, chatId val deletedChats = mutableStateOf>>(emptyList()) val chatItemStatuses = mutableMapOf() @@ -172,6 +168,12 @@ object ChatModel { // return true if you handled the click var centerPanelBackgroundClickHandler: (() -> Boolean)? = null + fun chatsForContent(contentTag: MsgContentTag?): State> = when(contentTag) { + null -> chatsContext.chats + MsgContentTag.Report -> reportsChatsContext.chats + else -> TODO() + } + fun chatItemsForContent(contentTag: MsgContentTag?): State> = when(contentTag) { null -> chatsContext.chatItems MsgContentTag.Report -> reportsChatsContext.chatItems @@ -184,6 +186,12 @@ object ChatModel { else -> TODO() } + fun chatItemsChangesListenerForContent(contentTag: MsgContentTag?): ChatItemsChangesListener? = when(contentTag) { + null -> chatsContext.chatItemsChangesListener + MsgContentTag.Report -> reportsChatsContext.chatItemsChangesListener + else -> TODO() + } + fun setChatItemsChangeListenerForContent(listener: ChatItemsChangesListener?, contentTag: MsgContentTag?) = when(contentTag) { null -> chatsContext.chatItemsChangesListener = listener MsgContentTag.Report -> reportsChatsContext.chatItemsChangesListener = listener @@ -281,27 +289,6 @@ object ChatModel { } } - fun markChatTagRead(chat: Chat) { - if (chat.unreadTag) { - chat.chatInfo.chatTags?.let { tags -> - markChatTagRead_(chat, tags) - } - } - } - - fun updateChatTagRead(chat: Chat, wasUnread: Boolean) { - val tags = chat.chatInfo.chatTags ?: return - val nowUnread = chat.unreadTag - - if (nowUnread && !wasUnread) { - tags.forEach { tag -> - unreadTags[tag] = (unreadTags[tag] ?: 0) + 1 - } - } else if (!nowUnread && wasUnread) { - markChatTagRead_(chat, tags) - } - } - fun moveChatTagUnread(chat: Chat, oldTags: List?, newTags: List) { if (chat.unreadTag) { oldTags?.forEach { t -> @@ -316,18 +303,6 @@ object ChatModel { } } } - - private fun markChatTagRead_(chat: Chat, tags: List) { - for (tag in tags) { - val count = unreadTags[tag] - if (count != null) { - unreadTags[tag] = maxOf(0, count - 1) - } - } - } - - // toList() here is to prevent ConcurrentModificationException that is rarely happens but happens - fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null // TODO pass rhId? fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id } fun getContactChat(contactId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } @@ -368,11 +343,16 @@ object ChatModel { class ChatsContext(private val contentTag: MsgContentTag?) { val chats = mutableStateOf(SnapshotStateList()) + /** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on. + * If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages]. + * If you use api call to get the items, use just [add] instead of [addAndNotify]. + * Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */ val chatItems = mutableStateOf(SnapshotStateList()) // set listener here that will be notified on every add/delete of a chat item var chatItemsChangesListener: ChatItemsChangesListener? = null val chatState = ActiveChatState() + fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id } private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId } @@ -491,7 +471,7 @@ object ChatModel { else chat.chatStats ) - updateChatTagRead(chats[i], wasUnread) + updateChatTagReadNoContentTag(chats[i], wasUnread) if (appPlatform.isDesktop && cItem.chatDir.sent) { reorderChat(chats[i], 0) @@ -507,9 +487,9 @@ object ChatModel { // Prevent situation when chat item already in the list received from backend if (chatItems.value.none { it.id == cItem.id }) { if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - chatItems.addAndNotify(kotlin.math.max(0, chatItems.value.lastIndex), cItem) + chatItems.addAndNotify(kotlin.math.max(0, chatItems.value.lastIndex), cItem, contentTag) } else { - chatItems.addAndNotify(cItem) + chatItems.addAndNotify(cItem, contentTag) } } } @@ -528,7 +508,7 @@ object ChatModel { chats[i] = chat.copy(chatItems = arrayListOf(cItem)) if (pItem.isRcvNew && !cItem.isRcvNew) { // status changed from New to Read, update counter - decreaseCounterInChat(rhId, cInfo.id) + decreaseCounterInChatNoContentTag(rhId, cInfo.id) } } res = false @@ -554,7 +534,7 @@ object ChatModel { } else { cItem } - chatItems.addAndNotify(ci) + chatItems.addAndNotify(ci, contentTag) true } } else { @@ -579,7 +559,7 @@ object ChatModel { fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { if (cItem.isRcvNew) { - decreaseCounterInChat(rhId, cInfo.id) + decreaseCounterInChatNoContentTag(rhId, cInfo.id) } // update previews val i = getChatIndex(rhId, cInfo.id) @@ -668,12 +648,11 @@ object ChatModel { } } - fun markChatItemsRead(remoteHostId: Long?, chatInfo: ChatInfo, itemIds: List? = null) { - val cInfo = chatInfo - val markedRead = markItemsReadInCurrentChat(chatInfo, itemIds) + fun markChatItemsRead(remoteHostId: Long?, id: ChatId, itemIds: List? = null) { + val markedRead = markItemsReadInCurrentChat(id, itemIds) // update preview - val chatIdx = getChatIndex(remoteHostId, cInfo.id) - if (chatIdx >= 0 && contentTag == null) { + val chatIdx = getChatIndex(remoteHostId, id) + if (chatIdx >= 0) { val chat = chats[chatIdx] val lastId = chat.chatItems.lastOrNull()?.id if (lastId != null) { @@ -683,15 +662,14 @@ object ChatModel { chats[chatIdx] = chat.copy( chatStats = chat.chatStats.copy(unreadCount = unreadCount) ) - updateChatTagRead(chats[chatIdx], wasUnread) + updateChatTagReadNoContentTag(chats[chatIdx], wasUnread) } } } - private fun markItemsReadInCurrentChat(chatInfo: ChatInfo, itemIds: List? = null): Int { - val cInfo = chatInfo + private fun markItemsReadInCurrentChat(id: ChatId, itemIds: List? = null): Int { var markedRead = 0 - if (chatId.value == cInfo.id) { + if (chatId.value == id) { val items = chatItems.value var i = items.lastIndex val itemIdsFromRange = itemIds?.toMutableSet() ?: mutableSetOf() @@ -721,7 +699,10 @@ object ChatModel { return markedRead } - private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) { + private fun decreaseCounterInChatNoContentTag(rhId: Long?, chatId: ChatId) { + // updates anything only in main ChatView, not GroupReportsView or anything else from the future + if (contentTag != null) return + val chatIndex = getChatIndex(rhId, chatId) if (chatIndex == -1) return @@ -734,7 +715,7 @@ object ChatModel { unreadCount = unreadCount, ) ) - updateChatTagRead(chats[chatIndex], wasUnread) + updateChatTagReadNoContentTag(chats[chatIndex], wasUnread) } fun removeChat(rhId: Long?, id: String) { @@ -802,6 +783,60 @@ object ChatModel { upsertGroupMember(rhId, groupInfo, updatedMember) } } + + fun increaseUnreadCounter(rhId: Long?, user: UserLike) { + changeUnreadCounterNoContentTag(rhId, user, 1) + } + + fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) { + changeUnreadCounterNoContentTag(rhId, user, -by) + } + + private fun changeUnreadCounterNoContentTag(rhId: Long?, user: UserLike, by: Int) { + // updates anything only in main ChatView, not GroupReportsView or anything else from the future + if (contentTag != null) return + + val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId } + if (i != -1) { + users[i] = users[i].copy(unreadCount = users[i].unreadCount + by) + } + } + + fun updateChatTagReadNoContentTag(chat: Chat, wasUnread: Boolean) { + // updates anything only in main ChatView, not GroupReportsView or anything else from the future + if (contentTag != null) return + + val tags = chat.chatInfo.chatTags ?: return + val nowUnread = chat.unreadTag + + if (nowUnread && !wasUnread) { + tags.forEach { tag -> + unreadTags[tag] = (unreadTags[tag] ?: 0) + 1 + } + } else if (!nowUnread && wasUnread) { + markChatTagReadNoContentTag_(chat, tags) + } + } + + fun markChatTagRead(chat: Chat) { + if (chat.unreadTag) { + chat.chatInfo.chatTags?.let { tags -> + markChatTagReadNoContentTag_(chat, tags) + } + } + } + + private fun markChatTagReadNoContentTag_(chat: Chat, tags: List) { + // updates anything only in main ChatView, not GroupReportsView or anything else from the future + if (contentTag != null) return + + for (tag in tags) { + val count = unreadTags[tag] + if (count != null) { + unreadTags[tag] = maxOf(0, count - 1) + } + } + } } fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) { @@ -832,7 +867,7 @@ object ChatModel { suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem { val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct) withChats { - chatItems.addAndNotify(cItem) + chatItems.addAndNotify(cItem, contentTag = null) } return cItem } @@ -841,27 +876,12 @@ object ChatModel { if (chatItemsForContent(null).value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { withApi { withChats { - chatItems.removeLastAndNotify() + chatItems.removeLastAndNotify(contentTag = null) } } } } - fun increaseUnreadCounter(rhId: Long?, user: UserLike) { - changeUnreadCounter(rhId, user, 1) - } - - fun decreaseUnreadCounter(rhId: Long?, user: UserLike, by: Int = 1) { - changeUnreadCounter(rhId, user, -by) - } - - private fun changeUnreadCounter(rhId: Long?, user: UserLike, by: Int) { - val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId } - if (i != -1) { - users[i] = users[i].copy(unreadCount = users[i].unreadCount + by) - } - } - fun getChatItemIndexOrNull(cItem: ChatItem, reversedChatItems: List): Int? { val index = reversedChatItems.indexOfFirst { it.id == cItem.id } return if (index != -1) index else null @@ -2548,8 +2568,8 @@ fun MutableState>.add(index: Int, elem: Chat) { value = SnapshotStateList().apply { addAll(value); add(index, elem) } } -fun MutableState>.addAndNotify(index: Int, elem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(index, elem); chatModel.chatsContext.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) } +fun MutableState>.addAndNotify(index: Int, elem: ChatItem, contentTag: MsgContentTag?) { + value = SnapshotStateList().apply { addAll(value); add(index, elem); chatModel.chatItemsChangesListenerForContent(contentTag)?.added(elem.id to elem.isRcvNew, index) } } fun MutableState>.add(elem: Chat) { @@ -2560,8 +2580,8 @@ fun MutableState>.add(elem: Chat) { fun MutableList.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmpty()) false else remAll(predicate) // Adds item to chatItems and notifies a listener about newly added item -fun MutableState>.addAndNotify(elem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(elem); chatModel.chatsContext.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) } +fun MutableState>.addAndNotify(elem: ChatItem, contentTag: MsgContentTag?) { + value = SnapshotStateList().apply { addAll(value); add(elem); chatModel.chatItemsChangesListenerForContent(contentTag)?.added(elem.id to elem.isRcvNew, lastIndex) } } fun MutableState>.addAll(index: Int, elems: List) { @@ -2603,7 +2623,7 @@ fun MutableState>.removeAt(index: Int): Chat { return res } -fun MutableState>.removeLastAndNotify() { +fun MutableState>.removeLastAndNotify(contentTag: MsgContentTag?) { val removed: Triple value = SnapshotStateList().apply { addAll(value) @@ -2611,7 +2631,7 @@ fun MutableState>.removeLastAndNotify() { val rem = removeLast() removed = Triple(rem.id, remIndex, rem.isRcvNew) } - chatModel.chatsContext.chatItemsChangesListener?.removed(listOf(removed), value) + chatModel.chatItemsChangesListenerForContent(contentTag)?.removed(listOf(removed), value) } fun MutableState>.replaceAll(elems: List) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index fc6fc0631e..07aa89e805 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -2409,7 +2409,7 @@ object ChatController { val cInfo = ChatInfo.ContactRequest(contactRequest) if (active(r.user)) { withChats { - if (chatModel.hasChat(rhId, contactRequest.id)) { + if (hasChat(rhId, contactRequest.id)) { updateChatInfo(rhId, cInfo) } else { addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) @@ -2419,7 +2419,7 @@ object ChatController { ntfManager.notifyContactRequestReceived(r.user, cInfo) } is CR.ContactUpdated -> { - if (active(r.user) && chatModel.hasChat(rhId, r.toContact.id)) { + if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.toContact.id)) { val cInfo = ChatInfo.Direct(r.toContact) withChats { updateChatInfo(rhId, cInfo) @@ -2437,7 +2437,7 @@ object ChatController { } } is CR.ContactsMerged -> { - if (active(r.user) && chatModel.hasChat(rhId, r.mergedContact.id)) { + if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.mergedContact.id)) { if (chatModel.chatId.value == r.mergedContact.id) { chatModel.chatId.value = r.intoContact.id } @@ -2489,7 +2489,9 @@ object ChatController { } } } else if (cItem.isRcvNew && cInfo.ntfsEnabled) { - chatModel.increaseUnreadCounter(rhId, r.user) + withChats { + increaseUnreadCounter(rhId, r.user) + } } val file = cItem.file val mc = cItem.content.msgContent @@ -2537,7 +2539,9 @@ object ChatController { if (!active(r.user)) { r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) -> if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled) { - chatModel.decreaseUnreadCounter(rhId, r.user) + withChats { + decreaseUnreadCounter(rhId, r.user) + } } } return 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 d5b3845db4..836fd6ac79 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 @@ -31,7 +31,6 @@ import chat.simplex.common.model.CIDirection.GroupRcv import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.activeCall import chat.simplex.common.model.ChatModel.controller -import chat.simplex.common.model.ChatModel.markChatTagRead import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.ui.theme.* @@ -108,7 +107,9 @@ fun ChatView( .distinctUntilChanged() .filterNotNull() .collect { chatId -> - markUnreadChatAsRead(chatId) + if (!groupReports.value.reportsView) { + markUnreadChatAsRead(chatId) + } showSearch.value = false searchText.value = "" selectedChatItems.value = null @@ -121,7 +122,7 @@ fun ChatView( // Having activeChat reloaded on every change in it is inefficient (UI lags) val unreadCount = remember { derivedStateOf { - chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == activeChatInfo.value?.id }?.chatStats?.unreadCount ?: 0 + chatModel.chatsForContent(if (reportsView) MsgContentTag.Report else null).value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0 } } val clipboard = LocalClipboardManager.current @@ -575,7 +576,7 @@ fun ChatView( withChats { // It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace withContext(Dispatchers.Main) { - markChatItemsRead(chatRh, chatInfo, itemsIds) + markChatItemsRead(chatRh, chatInfo.id, itemsIds) } ntfManager.cancelNotificationsForChat(chatInfo.id) chatModel.controller.apiChatItemsRead( @@ -586,7 +587,7 @@ fun ChatView( ) } withReportsChatsIfOpen { - markChatItemsRead(chatRh, chatInfo, itemsIds) + markChatItemsRead(chatRh, chatInfo.id, itemsIds) } } }, @@ -595,7 +596,7 @@ fun ChatView( withChats { // It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace withContext(Dispatchers.Main) { - markChatItemsRead(chatRh, chatInfo) + markChatItemsRead(chatRh, chatInfo.id) } ntfManager.cancelNotificationsForChat(chatInfo.id) chatModel.controller.apiChatRead( @@ -605,7 +606,7 @@ fun ChatView( ) } withReportsChatsIfOpen { - markChatItemsRead(chatRh, chatInfo) + markChatItemsRead(chatRh, chatInfo.id) } } }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 38e1d53647..831e71e4a0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -19,8 +19,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* -import chat.simplex.common.model.ChatModel.markChatTagRead -import chat.simplex.common.model.ChatModel.updateChatTagRead import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.platform.* @@ -569,10 +567,10 @@ fun markChatRead(c: Chat) { withApi { if (chat.chatStats.unreadCount > 0) { withChats { - markChatItemsRead(chat.remoteHostId, chat.chatInfo) + markChatItemsRead(chat.remoteHostId, chat.chatInfo.id) } withReportsChatsIfOpen { - markChatItemsRead(chat.remoteHostId, chat.chatInfo) + markChatItemsRead(chat.remoteHostId, chat.chatInfo.id) } chatModel.controller.apiChatRead( chat.remoteHostId, @@ -613,7 +611,7 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) { if (success) { withChats { replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) - updateChatTagRead(chat, wasUnread) + updateChatTagReadNoContentTag(chat, wasUnread) } } } @@ -875,7 +873,9 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch } val updatedChat = chatModel.getChat(chatInfo.id) if (updatedChat != null) { - chatModel.updateChatTagRead(updatedChat, wasUnread) + withChats { + updateChatTagReadNoContentTag(updatedChat, wasUnread) + } } val current = currentState?.value if (current != null) {