From 19cab39ee8c49cb82684495cf64ccc02990bfeea Mon Sep 17 00:00:00 2001 From: Stanislav Dmitrenko <7953703+avently@users.noreply.github.com> Date: Thu, 1 Aug 2024 02:43:31 +0900 Subject: [PATCH] android, desktop: refactoring to use mutex when updating chats (#4541) * moving to mutablestate + snapshotstatelist from snapshotstatelist * android, desktop: refactoring to use mutex when updating chats * wrapped into class instead of object * fix --------- Co-authored-by: Evgeny Poberezkin --- .../main/java/chat/simplex/app/SimplexApp.kt | 7 +- .../chat/simplex/common/model/ChatModel.kt | 585 +++++++++--------- .../chat/simplex/common/model/SimpleXAPI.kt | 231 ++++--- .../simplex/common/views/chat/ChatInfoView.kt | 53 +- .../simplex/common/views/chat/ChatView.kt | 71 ++- .../simplex/common/views/chat/ComposeView.kt | 17 +- .../common/views/chat/ContactPreferences.kt | 7 +- .../views/chat/group/AddGroupMembersView.kt | 7 +- .../views/chat/group/GroupChatInfoView.kt | 23 +- .../views/chat/group/GroupMemberInfoView.kt | 71 ++- .../views/chat/group/GroupPreferences.kt | 7 +- .../views/chat/group/GroupProfileView.kt | 5 +- .../views/chat/group/WelcomeMessageView.kt | 5 +- .../views/chatlist/ChatListNavLinkView.kt | 41 +- .../common/views/chatlist/ChatListView.kt | 8 +- .../common/views/chatlist/ShareListView.kt | 6 +- .../common/views/database/DatabaseView.kt | 12 +- .../common/views/newchat/AddGroupView.kt | 11 +- .../common/views/newchat/ConnectPlan.kt | 5 +- .../newchat/ContactConnectionInfoView.kt | 5 +- .../common/views/newchat/NewChatView.kt | 15 +- .../common/views/usersettings/Preferences.kt | 5 +- .../views/usersettings/PrivacySettings.kt | 43 +- .../kotlin/chat/simplex/desktop/Main.kt | 1 + 24 files changed, 737 insertions(+), 504 deletions(-) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 7d0312a43a..c203c3bd78 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -18,7 +18,7 @@ import chat.simplex.app.views.call.CallActivity import chat.simplex.common.helpers.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs -import chat.simplex.common.model.ChatModel.updatingChatsMutex +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.ui.theme.DefaultTheme @@ -27,7 +27,6 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage import com.jakewharton.processphoenix.ProcessPhoenix import kotlinx.coroutines.* -import kotlinx.coroutines.sync.withLock import java.io.* import java.util.* import java.util.concurrent.TimeUnit @@ -86,7 +85,7 @@ class SimplexApp: Application(), LifecycleEventObserver { Lifecycle.Event.ON_START -> { isAppOnForeground = true if (chatModel.chatRunning.value == true) { - updatingChatsMutex.withLock { + withChats { kotlin.runCatching { val currentUserId = chatModel.currentUser.value?.userId val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId())) @@ -99,7 +98,7 @@ class SimplexApp: Application(), LifecycleEventObserver { /** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */ if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats) } - chatModel.updateChats(chats) + updateChats(chats) } }.onFailure { Log.e(TAG, it.stackTraceToString()) } } 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 16a61da6b8..a717a8cd0c 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 @@ -54,7 +54,9 @@ object ChatModel { val ctrlInitInProgress = mutableStateOf(false) val dbMigrationInProgress = mutableStateOf(false) val incompleteInitializedDbRemoved = mutableStateOf(false) - val chats = mutableStateListOf() + private val _chats = mutableStateOf(SnapshotStateList()) + val chats: State> = _chats + private val chatsContext = ChatsContext() // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() val switchingUsersAndHosts = mutableStateOf(false) @@ -126,7 +128,7 @@ object ChatModel { val updatingProgress = mutableStateOf(null as Float?) var updatingRequest: Closeable? = null - val updatingChatsMutex: Mutex = Mutex() + private val updatingChatsMutex: Mutex = Mutex() val changingActiveUserMutex: Mutex = Mutex() val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null @@ -170,11 +172,11 @@ object ChatModel { } // toList() here is to prevent ConcurrentModificationException that is rarely happens but happens - fun hasChat(rhId: Long?, id: String): Boolean = chats.toList().firstOrNull { it.id == id && it.remoteHostId == rhId } != null + 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.toList().firstOrNull { it.id == id } - fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } - fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } + 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 } + fun getGroupChat(groupId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } fun populateGroupMembersIndexes() { groupMembersIndexes.clear() @@ -192,97 +194,102 @@ object ChatModel { } } - private fun getChatIndex(rhId: Long?, id: String): Int = chats.toList().indexOfFirst { it.id == id && it.remoteHostId == rhId } - fun addChat(chat: Chat) = chats.add(index = 0, chat) + suspend fun withChats(action: suspend ChatsContext.() -> T): T = updatingChatsMutex.withLock { + chatsContext.action() + } - fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) { - val i = getChatIndex(rhId, cInfo.id) - if (i >= 0) { - val currentCInfo = chats[i].chatInfo - var newCInfo = cInfo - if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) { - val currentStats = currentCInfo.contact.activeConn?.connectionStats - val newConn = newCInfo.contact.activeConn - val newStats = newConn?.connectionStats - if (currentStats != null && newConn != null && newStats == null) { - newCInfo = newCInfo.copy( - contact = newCInfo.contact.copy( - activeConn = newConn.copy( - connectionStats = currentStats + class ChatsContext { + val chats = _chats + + fun addChat(chat: Chat) = chats.add(index = 0, chat) + + fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) { + val i = getChatIndex(rhId, cInfo.id) + if (i >= 0) { + val currentCInfo = chats[i].chatInfo + var newCInfo = cInfo + if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) { + val currentStats = currentCInfo.contact.activeConn?.connectionStats + val newConn = newCInfo.contact.activeConn + val newStats = newConn?.connectionStats + if (currentStats != null && newConn != null && newStats == null) { + newCInfo = newCInfo.copy( + contact = newCInfo.contact.copy( + activeConn = newConn.copy( + connectionStats = currentStats + ) ) ) - ) - } - } - chats[i] = chats[i].copy(chatInfo = newCInfo) - } - } - - fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection)) - - fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed) - - fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) { - val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats) - val updatedContact = contact.copy(activeConn = updatedConn) - updateContact(rhId, updatedContact) - } - - fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo)) - - private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) { - if (hasChat(rhId, cInfo.id)) { - updateChatInfo(rhId, cInfo) - } else if (addMissing) { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf())) - } - } - - fun updateChats(newChats: List) { - chats.clear() - chats.addAll(newChats) - - val cId = chatId.value - // If chat is null, it was deleted in background after apiGetChats call - if (cId != null && getChat(cId) == null) { - chatId.value = null - } - } - - fun replaceChat(rhId: Long?, id: String, chat: Chat) { - val i = getChatIndex(rhId, id) - if (i >= 0) { - chats[i] = chat - } else { - // invalid state, correcting - chats.add(index = 0, chat) - } - } - - suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock { - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - if (i >= 0) { - chat = chats[i] - val newPreviewItem = when (cInfo) { - is ChatInfo.Group -> { - val currentPreviewItem = chat.chatItems.firstOrNull() - if (currentPreviewItem != null) { - if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) { - cItem - } else { - currentPreviewItem - } - } else { - cItem } } - else -> cItem + chats[i] = chats[i].copy(chatInfo = newCInfo) } - chats[i] = chat.copy( - chatItems = arrayListOf(newPreviewItem), - chatStats = + } + + fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection)) + + fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed) + + fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) { + val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats) + val updatedContact = contact.copy(activeConn = updatedConn) + updateContact(rhId, updatedContact) + } + + fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo)) + + private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) { + if (hasChat(rhId, cInfo.id)) { + updateChatInfo(rhId, cInfo) + } else if (addMissing) { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf())) + } + } + + fun updateChats(newChats: List) { + chats.clear() + chats.addAll(newChats) + + val cId = chatId.value + // If chat is null, it was deleted in background after apiGetChats call + if (cId != null && getChat(cId) == null) { + chatId.value = null + } + } + + fun replaceChat(rhId: Long?, id: String, chat: Chat) { + val i = getChatIndex(rhId, id) + if (i >= 0) { + chats[i] = chat + } else { + // invalid state, correcting + chats.add(index = 0, chat) + } + } + suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + if (i >= 0) { + chat = chats[i] + val newPreviewItem = when (cInfo) { + is ChatInfo.Group -> { + val currentPreviewItem = chat.chatItems.firstOrNull() + if (currentPreviewItem != null) { + if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) { + cItem + } else { + currentPreviewItem + } + } else { + cItem + } + } + else -> cItem + } + chats[i] = chat.copy( + chatItems = arrayListOf(newPreviewItem), + chatStats = if (cItem.meta.itemStatus is CIStatus.RcvNew) { val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId increaseUnreadCounter(rhId, currentUser.value!!) @@ -290,123 +297,197 @@ object ChatModel { } else chat.chatStats - ) - if (i > 0) { - popChat_(i) + ) + if (i > 0) { + chats.add(index = 0, chats.removeAt(i)) + } + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) } - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - } - withContext(Dispatchers.Main) { - // add to current chat - if (chatId.value == cInfo.id) { - // 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.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem) - } else { - chatItems.add(cItem) + withContext(Dispatchers.Main) { + // add to current chat + if (chatId.value == cInfo.id) { + // 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.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem) + } else { + chatItems.add(cItem) + } } } } } - } - suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock { - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - val res: Boolean - if (i >= 0) { - chat = chats[i] - val pItem = chat.chatItems.lastOrNull() - if (pItem?.id == cItem.id) { - chats[i] = chat.copy(chatItems = arrayListOf(cItem)) - if (pItem.isRcvNew && !cItem.isRcvNew) { - // status changed from New to Read, update counter - decreaseCounterInChat(rhId, cInfo.id) + suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean { + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + val res: Boolean + if (i >= 0) { + chat = chats[i] + val pItem = chat.chatItems.lastOrNull() + if (pItem?.id == cItem.id) { + chats[i] = chat.copy(chatItems = arrayListOf(cItem)) + if (pItem.isRcvNew && !cItem.isRcvNew) { + // status changed from New to Read, update counter + decreaseCounterInChat(rhId, cInfo.id) + } + } + res = false + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + res = true + } + return withContext(Dispatchers.Main) { + // update current chat + if (chatId.value == cInfo.id) { + val items = chatItems.value + val itemIndex = items.indexOfFirst { it.id == cItem.id } + if (itemIndex >= 0) { + items[itemIndex] = cItem + false + } else { + val status = chatItemStatuses.remove(cItem.id) + val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) { + cItem.copy(meta = cItem.meta.copy(itemStatus = status)) + } else { + cItem + } + chatItems.add(ci) + true + } + } else { + res } } - res = false - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - res = true } - return withContext(Dispatchers.Main) { - // update current chat + + suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { + withContext(Dispatchers.Main) { + if (chatId.value == cInfo.id) { + val items = chatItems.value + val itemIndex = items.indexOfFirst { it.id == cItem.id } + if (itemIndex >= 0) { + items[itemIndex] = cItem + } + } else if (status != null) { + chatItemStatuses[cItem.id] = status + } + } + } + + fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + if (cItem.isRcvNew) { + decreaseCounterInChat(rhId, cInfo.id) + } + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + if (i >= 0) { + chat = chats[i] + val pItem = chat.chatItems.lastOrNull() + if (pItem?.id == cItem.id) { + chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy)) + } + } + // remove from current chat if (chatId.value == cInfo.id) { - val items = chatItems.value - val itemIndex = items.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - items[itemIndex] = cItem + chatItems.removeAll { + val remove = it.id == cItem.id + if (remove) { AudioPlayer.stop(it) } + remove + } + } + } + + fun clearChat(rhId: Long?, cInfo: ChatInfo) { + // clear preview + val i = getChatIndex(rhId, cInfo.id) + if (i >= 0) { + decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount) + chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo) + } + // clear current chat + if (chatId.value == cInfo.id) { + chatItemStatuses.clear() + chatItems.clear() + } + } + + fun markChatItemsRead(chat: Chat, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) { + val cInfo = chat.chatInfo + val markedRead = markItemsReadInCurrentChat(chat, range) + // update preview + val chatIdx = getChatIndex(chat.remoteHostId, cInfo.id) + if (chatIdx >= 0) { + val chat = chats[chatIdx] + val lastId = chat.chatItems.lastOrNull()?.id + if (lastId != null) { + val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0 + decreaseUnreadCounter(chat.remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) + chats[chatIdx] = chat.copy( + chatStats = chat.chatStats.copy( + unreadCount = unreadCount, + // Can't use minUnreadItemId currently since chat items can have unread items between read items + //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 + ) + ) + } + } + } + + private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) { + val chatIndex = getChatIndex(rhId, chatId) + if (chatIndex == -1) return + + val chat = chats[chatIndex] + val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0) + decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) + chats[chatIndex] = chat.copy( + chatStats = chat.chatStats.copy( + unreadCount = unreadCount, + ) + ) + } + + fun removeChat(rhId: Long?, id: String) { + chats.removeAll { it.id == id && it.remoteHostId == rhId } + } + + fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean { + // user member was updated + if (groupInfo.membership.groupMemberId == member.groupMemberId) { + updateGroup(rhId, groupInfo) + return false + } + // update current chat + return if (chatId.value == groupInfo.id) { + val memberIndex = groupMembersIndexes[member.groupMemberId] + if (memberIndex != null) { + groupMembers[memberIndex] = member false } else { - val status = chatItemStatuses.remove(cItem.id) - val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) { - cItem.copy(meta = cItem.meta.copy(itemStatus = status)) - } else { - cItem - } - chatItems.add(ci) + groupMembers.add(member) + groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1 true } } else { - res + false + } + } + + fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) { + val memberConn = member.activeConn + if (memberConn != null) { + val updatedConn = memberConn.copy(connectionStats = connectionStats) + val updatedMember = member.copy(activeConn = updatedConn) + upsertGroupMember(rhId, groupInfo, updatedMember) } } } - suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { - withContext(Dispatchers.Main) { - if (chatId.value == cInfo.id) { - val items = chatItems.value - val itemIndex = items.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - items[itemIndex] = cItem - } - } else if (status != null) { - chatItemStatuses[cItem.id] = status - } - } - } - - fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { - if (cItem.isRcvNew) { - decreaseCounterInChat(rhId, cInfo.id) - } - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - if (i >= 0) { - chat = chats[i] - val pItem = chat.chatItems.lastOrNull() - if (pItem?.id == cItem.id) { - chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy)) - } - } - // remove from current chat - if (chatId.value == cInfo.id) { - chatItems.removeAll { - val remove = it.id == cItem.id - if (remove) { AudioPlayer.stop(it) } - remove - } - } - } - - fun clearChat(rhId: Long?, cInfo: ChatInfo) { - // clear preview - val i = getChatIndex(rhId, cInfo.id) - if (i >= 0) { - decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount) - chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo) - } - // clear current chat - if (chatId.value == cInfo.id) { - chatItemStatuses.clear() - chatItems.clear() - } - } + private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId } fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) { val current = currentUser.value ?: return @@ -447,28 +528,6 @@ object ChatModel { } } - fun markChatItemsRead(chat: Chat, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) { - val cInfo = chat.chatInfo - val markedRead = markItemsReadInCurrentChat(chat, range) - // update preview - val chatIdx = getChatIndex(chat.remoteHostId, cInfo.id) - if (chatIdx >= 0) { - val chat = chats[chatIdx] - val lastId = chat.chatItems.lastOrNull()?.id - if (lastId != null) { - val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0 - decreaseUnreadCounter(chat.remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) - chats[chatIdx] = chat.copy( - chatStats = chat.chatStats.copy( - unreadCount = unreadCount, - // Can't use minUnreadItemId currently since chat items can have unread items between read items - //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 - ) - ) - } - } - } - private fun markItemsReadInCurrentChat(chat: Chat, range: CC.ItemRange? = null): Int { val cInfo = chat.chatInfo var markedRead = 0 @@ -493,20 +552,6 @@ object ChatModel { return markedRead } - private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) { - val chatIndex = getChatIndex(rhId, chatId) - if (chatIndex == -1) return - - val chat = chats[chatIndex] - val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0) - decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) - chats[chatIndex] = chat.copy( - chatStats = chat.chatStats.copy( - unreadCount = unreadCount, - ) - ) - } - fun increaseUnreadCounter(rhId: Long?, user: UserLike) { changeUnreadCounter(rhId, user, 1) } @@ -600,11 +645,6 @@ object ChatModel { // } // } - private fun popChat_(i: Int) { - val chat = chats.removeAt(i) - chats.add(index = 0, chat) - } - fun replaceConnReqView(id: String, withId: String) { if (id == showingInvitation.value?.connId) { showingInvitation.value = null @@ -629,41 +669,6 @@ object ChatModel { showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true) } - fun removeChat(rhId: Long?, id: String) { - chats.removeAll { it.id == id && it.remoteHostId == rhId } - } - - fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean { - // user member was updated - if (groupInfo.membership.groupMemberId == member.groupMemberId) { - updateGroup(rhId, groupInfo) - return false - } - // update current chat - return if (chatId.value == groupInfo.id) { - val memberIndex = groupMembersIndexes[member.groupMemberId] - if (memberIndex != null) { - groupMembers[memberIndex] = member - false - } else { - groupMembers.add(member) - groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1 - true - } - } else { - false - } - } - - fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) { - val memberConn = member.activeConn - if (memberConn != null) { - val updatedConn = memberConn.copy(connectionStats = connectionStats) - val updatedMember = member.copy(activeConn = updatedConn) - upsertGroupMember(rhId, groupInfo, updatedMember) - } - } - fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) { val conn = contact.activeConn if (conn != null) { @@ -2108,45 +2113,55 @@ data class ChatItem ( } } -fun MutableState>.add(index: Int, chatItem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(index, chatItem) } +fun MutableState>.add(index: Int, elem: T) { + value = SnapshotStateList().apply { addAll(value); add(index, elem) } } -fun MutableState>.add(chatItem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(chatItem) } +fun MutableState>.add(elem: T) { + value = SnapshotStateList().apply { addAll(value); add(elem) } } -fun MutableState>.addAll(index: Int, chatItems: List) { - value = SnapshotStateList().apply { addAll(value); addAll(index, chatItems) } +fun MutableState>.addAll(index: Int, elems: List) { + value = SnapshotStateList().apply { addAll(value); addAll(index, elems) } } -fun MutableState>.addAll(chatItems: List) { - value = SnapshotStateList().apply { addAll(value); addAll(chatItems) } +fun MutableState>.addAll(elems: List) { + value = SnapshotStateList().apply { addAll(value); addAll(elems) } } -fun MutableState>.removeAll(block: (ChatItem) -> Boolean) { - value = SnapshotStateList().apply { addAll(value); removeAll(block) } +fun MutableState>.removeAll(block: (T) -> Boolean) { + value = SnapshotStateList().apply { addAll(value); removeAll(block) } } -fun MutableState>.removeAt(index: Int) { - value = SnapshotStateList().apply { addAll(value); removeAt(index) } +fun MutableState>.removeAt(index: Int): T { + val new = SnapshotStateList() + new.addAll(value) + val res = new.removeAt(index) + value = new + return res } -fun MutableState>.removeLast() { - value = SnapshotStateList().apply { addAll(value); removeLast() } +fun MutableState>.removeLast() { + value = SnapshotStateList().apply { addAll(value); removeLast() } } -fun MutableState>.replaceAll(chatItems: List) { - value = SnapshotStateList().apply { addAll(chatItems) } +fun MutableState>.replaceAll(elems: List) { + value = SnapshotStateList().apply { addAll(elems) } } -fun MutableState>.clear() { - value = SnapshotStateList() +fun MutableState>.clear() { + value = SnapshotStateList() } -fun State>.asReversed(): MutableList = value.asReversed() +fun State>.asReversed(): MutableList = value.asReversed() -val State>.size: Int get() = value.size +fun State>.toList(): List = value.toList() + +operator fun State>.get(i: Int): T = value[i] + +operator fun State>.set(index: Int, elem: T) { value[index] = elem } + +val State>.size: Int get() = value.size enum class CIMergeCategory { MemberConnected, 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 87bf26aea5..27bad500bb 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 @@ -15,8 +15,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.setNetCfg -import chat.simplex.common.model.ChatModel.updatingChatsMutex import chat.simplex.common.model.ChatModel.changingActiveUserMutex +import chat.simplex.common.model.ChatModel.withChats import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* @@ -478,9 +478,9 @@ object ChatController { } Log.d(TAG, "startChat: started") } else { - updatingChatsMutex.withLock { + withChats { val chats = apiGetChats(null) - chatModel.updateChats(chats) + updateChats(chats) } Log.d(TAG, "startChat: running") } @@ -558,9 +558,9 @@ object ChatController { val hasUser = chatModel.currentUser.value != null chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None - updatingChatsMutex.withLock { + withChats { val chats = apiGetChats(rhId) - chatModel.updateChats(chats) + updateChats(chats) } } @@ -1180,7 +1180,9 @@ object ChatController { suspend fun deleteChat(chat: Chat, notify: Boolean? = null) { val cInfo = chat.chatInfo if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) { - chatModel.removeChat(chat.remoteHostId, cInfo.id) + withChats { + removeChat(chat.remoteHostId, cInfo.id) + } } } @@ -1211,7 +1213,9 @@ object ChatController { withBGApi { val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId) if (updatedChatInfo != null) { - chatModel.clearChat(chat.remoteHostId, updatedChatInfo) + withChats { + clearChat(chat.remoteHostId, updatedChatInfo) + } ntfManager.cancelNotificationsForChat(chat.chatInfo.id) close?.invoke() } @@ -1546,10 +1550,12 @@ object ChatController { val r = sendCmd(rh, CC.ApiJoinGroup(groupId)) when (r) { is CR.UserAcceptedGroupSent -> - chatModel.updateGroup(rh, r.groupInfo) + withChats { + updateGroup(rh, r.groupInfo) + } is CR.ChatCmdError -> { val e = r.chatError - suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { chatModel.removeChat(rh, "#$groupId") } } + suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { withChats { removeChat(rh, "#$groupId") } } } if (e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH) { deleteGroup() AlertManager.shared.showAlertMsg(generalGetString(MR.strings.alert_title_group_invitation_expired), generalGetString(MR.strings.alert_message_group_invitation_expired)) @@ -1703,7 +1709,9 @@ object ChatController { val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param) val toContact = apiSetContactPrefs(rh, contact.contactId, prefs) if (toContact != null) { - chatModel.updateContact(rh, toContact) + withChats { + updateContact(rh, toContact) + } } } @@ -1973,16 +1981,20 @@ object ChatController { when (r) { is CR.ContactDeletedByContact -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } } is CR.ContactConnected -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) - val conn = r.contact.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + removeChat(rhId, conn.id) + } } } if (r.contact.directOrUsed) { @@ -1992,21 +2004,25 @@ object ChatController { } is CR.ContactConnecting -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) - val conn = r.contact.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + removeChat(rhId, conn.id) + } } } } is CR.ContactSndReady -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) - val conn = r.contact.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + removeChat(rhId, conn.id) + } } } chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected()) @@ -2015,10 +2031,12 @@ object ChatController { val contactRequest = r.contactRequest val cInfo = ChatInfo.ContactRequest(contactRequest) if (active(r.user)) { - if (chatModel.hasChat(rhId, contactRequest.id)) { - chatModel.updateChatInfo(rhId, cInfo) - } else { - chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + withChats { + if (chatModel.hasChat(rhId, contactRequest.id)) { + updateChatInfo(rhId, cInfo) + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + } } } ntfManager.notifyContactRequestReceived(r.user, cInfo) @@ -2026,12 +2044,16 @@ object ChatController { is CR.ContactUpdated -> { if (active(r.user) && chatModel.hasChat(rhId, r.toContact.id)) { val cInfo = ChatInfo.Direct(r.toContact) - chatModel.updateChatInfo(rhId, cInfo) + withChats { + updateChatInfo(rhId, cInfo) + } } } is CR.GroupMemberUpdated -> { if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.toMember) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.toMember) + } } } is CR.ContactsMerged -> { @@ -2039,7 +2061,9 @@ object ChatController { if (chatModel.chatId.value == r.mergedContact.id) { chatModel.chatId.value = r.intoContact.id } - chatModel.removeChat(rhId, r.mergedContact.id) + withChats { + removeChat(rhId, r.mergedContact.id) + } } } // ContactsSubscribed, ContactsDisconnected and ContactSubSummary are only used in CLI, @@ -2049,7 +2073,9 @@ object ChatController { is CR.ContactSubSummary -> { for (sub in r.contactSubscriptions) { if (active(r.user)) { - chatModel.updateContact(rhId, sub.contact) + withChats { + updateContact(rhId, sub.contact) + } } val err = sub.contactError if (err == null) { @@ -2073,7 +2099,9 @@ object ChatController { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem if (active(r.user)) { - chatModel.addChatItem(rhId, cInfo, cItem) + withChats { + addChatItem(rhId, cInfo, cItem) + } } else if (cItem.isRcvNew && cInfo.ntfsEnabled) { chatModel.increaseUnreadCounter(rhId, r.user) } @@ -2094,14 +2122,18 @@ object ChatController { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem if (!cItem.isDeletedContent && active(r.user)) { - chatModel.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + withChats { + updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + } } } is CR.ChatItemUpdated -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) is CR.ChatItemReaction -> { if (active(r.user)) { - chatModel.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + withChats { + updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + } } } is CR.ChatItemsDeleted -> { @@ -2127,82 +2159,113 @@ object ChatController { generalGetString(if (toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description) ) } - if (toChatItem == null) { - chatModel.removeChatItem(rhId, cInfo, cItem) - } else { - chatModel.upsertChatItem(rhId, cInfo, toChatItem.chatItem) + withChats { + if (toChatItem == null) { + removeChatItem(rhId, cInfo, cItem) + } else { + upsertChatItem(rhId, cInfo, toChatItem.chatItem) + } } } } is CR.ReceivedGroupInvitation -> { if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) // update so that repeat group invitations are not duplicated + withChats { + // update so that repeat group invitations are not duplicated + updateGroup(rhId, r.groupInfo) + } // TODO NtfManager.shared.notifyGroupInvitation } } is CR.UserAcceptedGroupSent -> { if (!active(r.user)) return - chatModel.updateGroup(rhId, r.groupInfo) - val conn = r.hostContact?.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateGroup(rhId, r.groupInfo) + val conn = r.hostContact?.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}") + removeChat(rhId, conn.id) + } } } is CR.GroupLinkConnecting -> { if (!active(r.user)) return - chatModel.updateGroup(rhId, r.groupInfo) - val hostConn = r.hostMember.activeConn - if (hostConn != null) { - chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}") - chatModel.removeChat(rhId, hostConn.id) + withChats { + updateGroup(rhId, r.groupInfo) + val hostConn = r.hostMember.activeConn + if (hostConn != null) { + chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}") + removeChat(rhId, hostConn.id) + } } } is CR.JoinedGroupMemberConnecting -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.DeletedMemberUser -> // TODO update user member if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withChats { + updateGroup(rhId, r.groupInfo) + } } is CR.DeletedMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + } } is CR.LeftMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberRole -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberRoleUser -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberBlockedForAll -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.GroupDeleted -> // TODO update user member if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withChats { + updateGroup(rhId, r.groupInfo) + } } is CR.UserJoinedGroup -> if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withChats { + updateGroup(rhId, r.groupInfo) + } } is CR.JoinedGroupMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.ConnectedToGroupMember -> { if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } if (r.memberContact != null) { chatModel.setContactNetworkStatus(r.memberContact, NetworkStatus.Connected()) @@ -2210,11 +2273,15 @@ object ChatController { } is CR.GroupUpdated -> if (active(r.user)) { - chatModel.updateGroup(rhId, r.toGroup) + withChats { + updateGroup(rhId, r.toGroup) + } } is CR.NewMemberContactReceivedInv -> if (active(r.user)) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } is CR.RcvFileStart -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) @@ -2314,19 +2381,27 @@ object ChatController { } is CR.ContactSwitch -> if (active(r.user)) { - chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) + withChats { + updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) + } } is CR.GroupMemberSwitch -> if (active(r.user)) { - chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) + withChats { + updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) + } } is CR.ContactRatchetSync -> if (active(r.user)) { - chatModel.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) + withChats { + updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) + } } is CR.GroupMemberRatchetSync -> if (active(r.user)) { - chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) + withChats { + updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) + } } is CR.RemoteHostSessionCode -> { chatModel.remoteHostPairing.value = r.remoteHost_ to RemoteHostSessionState.PendingConfirmation(r.sessionCode) @@ -2338,7 +2413,9 @@ object ChatController { } is CR.ContactDisabled -> { if (active(r.user)) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } } is CR.RemoteHostStopped -> { @@ -2459,7 +2536,9 @@ object ChatController { } is CR.ContactPQEnabled -> if (active(r.user)) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } is CR.ChatRespError -> when { r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> { @@ -2535,7 +2614,9 @@ object ChatController { suspend fun leaveGroup(rh: Long?, groupId: Long) { val groupInfo = apiLeaveGroup(rh, groupId) if (groupInfo != null) { - chatModel.updateGroup(rh, groupInfo) + withChats { + updateGroup(rh, groupInfo) + } } } @@ -2545,7 +2626,7 @@ object ChatController { val notify = { ntfManager.notifyMessageReceived(user, cInfo, cItem) } if (!activeUser(rh, user)) { notify() - } else if (chatModel.upsertChatItem(rh, cInfo, cItem)) { + } else if (withChats { upsertChatItem(rh, cInfo, cItem) }) { notify() } else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) { notify() @@ -2589,7 +2670,9 @@ object ChatController { chatModel.currentUser.value = user if (user == null) { chatModel.chatItems.clear() - chatModel.chats.clear() + withChats { + chats.clear() + } } val statuses = apiGetNetworkStatuses(rhId) if (statuses != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 838225afb8..87094a9395 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -57,7 +58,7 @@ fun ChatInfoView( ) { BackHandler(onBack = close) val contact = rememberUpdatedState(contact).value - val chat = remember(contact.id) { chatModel.chats.firstOrNull { it.id == contact.id } } + val chat = remember(contact.id) { chatModel.chats.value.firstOrNull { it.id == contact.id } } val currentUser = remember { chatModel.currentUser }.value val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() @@ -102,7 +103,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } close.invoke() } @@ -114,7 +117,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } } }) @@ -124,7 +129,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } close.invoke() } @@ -135,7 +142,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } close.invoke() } @@ -151,14 +160,16 @@ fun ChatInfoView( verify = { code -> chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r -> val (verified, existingCode) = r - chatModel.updateContact( - chatRh, - ct.copy( - activeConn = ct.activeConn?.copy( - connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + withChats { + updateContact( + chatRh, + ct.copy( + activeConn = ct.activeConn?.copy( + connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + ) ) ) - ) + } r } }, @@ -247,7 +258,9 @@ fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify val chatRh = chat.remoteHostId val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify) if (r) { - chatModel.removeChat(chatRh, chatInfo.id) + withChats { + removeChat(chatRh, chatInfo.id) + } if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -347,7 +360,7 @@ fun ChatInfoLayout( WallpaperButton { ModalManager.end.showModal { - val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } } + val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } val c = chat.value if (c != null) { ChatWallpaperEditorModal(c) @@ -768,10 +781,12 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c wallpaperFilesToDelete.forEach(::removeWallpaperFile) if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) { - if (chat.chatInfo is ChatInfo.Direct) { - chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) - } else if (chat.chatInfo is ChatInfo.Group) { - chatModel.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) + withChats { + if (chat.chatInfo is ChatInfo.Direct) { + updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) + } else if (chat.chatInfo is ChatInfo.Group) { + updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) + } } } } @@ -779,7 +794,9 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi { val chatRh = chat.remoteHostId chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { - chatModel.updateContact(chatRh, it) + withChats { + updateContact(chatRh, it) + } } } 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 d293bfe1fb..c743136dcb 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 @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* import chat.simplex.common.views.chat.group.* @@ -45,7 +46,7 @@ import kotlin.math.sign @Composable fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: String) -> Unit) { - val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }) } + val activeChat = remember { mutableStateOf(chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == chatId }) } val searchText = rememberSaveable { mutableStateOf("") } val user = chatModel.currentUser.value val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() @@ -87,7 +88,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: * TODO: Re-write [ChatModel.chats] logic to a new list assignment instead of changing content of mutableList to prevent that * */ try { - chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } + chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } } catch (e: ConcurrentModificationException) { Log.e(TAG, e.stackTraceToString()) null @@ -112,7 +113,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: // Having activeChat reloaded on every change in it is inefficient (UI lags) val unreadCount = remember { derivedStateOf { - chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0 + chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0 } } val clipboard = LocalClipboardManager.current @@ -268,10 +269,12 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: if (deleted != null) { deletedChatItem = deleted.deletedChatItem.chatItem toChatItem = deleted.toChatItem?.chatItem - if (toChatItem != null) { - chatModel.upsertChatItem(chatRh, cInfo, toChatItem) - } else { - chatModel.removeChatItem(chatRh, cInfo, deletedChatItem) + withChats { + if (toChatItem != null) { + upsertChatItem(chatRh, cInfo, toChatItem) + } else { + removeChatItem(chatRh, cInfo, deletedChatItem) + } } } } @@ -284,8 +287,10 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: chatRh, chatInfo.chatType, chatInfo.apiId, itemIds, CIDeleteMode.cidmInternal ) if (deleted != null) { - for (di in deleted) { - chatModel.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) + withChats { + for (di in deleted) { + removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) + } } } } @@ -351,7 +356,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: if (r != null) { val contactStats = r.first if (contactStats != null) - chatModel.updateContactConnectionStats(chatRh, contact, contactStats) + withChats { + updateContactConnectionStats(chatRh, contact, contactStats) + } } } }, @@ -361,7 +368,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: if (r != null) { val memStats = r.second if (memStats != null) { - chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) + withChats { + updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) + } } } } @@ -370,7 +379,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: withBGApi { val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } } }, @@ -378,7 +389,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: withBGApi { val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) if (r != null) { - chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) + } } } }, @@ -399,7 +412,9 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: reaction = reaction ) if (updatedCI != null) { - chatModel.updateChatItem(cInfo, updatedCI) + withChats { + updateChatItem(cInfo, updatedCI) + } } } }, @@ -464,15 +479,17 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: } }, markRead = { range, unreadCountAfter -> - chatModel.markChatItemsRead(chat, range, unreadCountAfter) - ntfManager.cancelNotificationsForChat(chat.id) withBGApi { - chatModel.controller.apiChatRead( - chatRh, - chat.chatInfo.chatType, - chat.chatInfo.apiId, - range - ) + withChats { + markChatItemsRead(chat, range, unreadCountAfter) + ntfManager.cancelNotificationsForChat(chat.id) + chatModel.controller.apiChatRead( + chatRh, + chat.chatInfo.chatType, + chat.chatInfo.apiId, + range + ) + } } }, changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) }, @@ -1141,7 +1158,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: .filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it } .collect { try { - if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.size)) { + if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.value.size)) { if (appPlatform.isAndroid) listState.animateScrollToItem(0) else listState.scrollToItem(0) } else { if (appPlatform.isAndroid) listState.animateScrollBy(scrollDistance) else listState.scrollBy(scrollDistance) @@ -1245,7 +1262,7 @@ fun BoxWithConstraintsScope.FloatingButtons( painterResource(MR.images.ic_check), onClick = { markRead( - CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), + CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), bottomUnreadCount ) showDropDown.value = false @@ -1388,8 +1405,10 @@ private fun markUnreadChatAsRead(activeChat: MutableState, chatModel: Cha false ) if (success && chat.id == activeChat.value?.id) { - activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)) - chatModel.replaceChat(chatRh, chat.id, activeChat.value!!) + withChats { + activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)) + replaceChat(chatRh, chat.id, activeChat.value!!) + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index a0e1bf8107..404a8636ef 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.filesToDelete +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.* @@ -393,7 +394,9 @@ fun ComposeView( ttl = ttl ) if (aChatItem != null) { - chatModel.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + withChats { + addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + } return aChatItem.chatItem } if (file != null) removeFile(file.filePath) @@ -421,7 +424,9 @@ fun ComposeView( ttl = ttl ) if (chatItem != null) { - chatModel.addChatItem(rhId, chat.chatInfo, chatItem) + withChats { + addChatItem(rhId, chat.chatInfo, chatItem) + } } return chatItem } @@ -458,7 +463,9 @@ fun ComposeView( val mc = checkLinkPreview() val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) if (contact != null) { - chatModel.updateContact(chat.remoteHostId, contact) + withChats { + updateContact(chat.remoteHostId, contact) + } } } @@ -474,7 +481,9 @@ fun ComposeView( mc = updateMsgContent(oldMsgContent), live = live ) - if (updatedItem != null) chatModel.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + if (updatedItem != null) withChats { + upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + } return updatedItem?.chatItem } return null diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt index 502074d629..f7f0a55372 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt @@ -19,6 +19,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR @@ -40,8 +41,10 @@ fun ContactPreferencesView( val prefs = contactFeaturesAllowedToPrefs(featuresAllowed) val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs) if (toContact != null) { - m.updateContact(rhId, toContact) - currentFeaturesAllowed = featuresAllowed + withChats { + updateContact(rhId, toContact) + currentFeaturesAllowed = featuresAllowed + } } afterSave() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index 931cc17872..42fbec35cd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -24,6 +24,7 @@ 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.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.ChatInfoToolbarTitle import chat.simplex.common.views.helpers.* @@ -58,7 +59,9 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea for (contactId in selectedContacts) { val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) if (member != null) { - chatModel.upsertGroupMember(rhId, groupInfo, member) + withChats { + upsertGroupMember(rhId, groupInfo, member) + } } else { break } @@ -81,7 +84,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List { val memberContactIds = chatModel.groupMembers .filter { it.memberCurrent } .mapNotNull { it.memberContactId } - return chatModel.chats + return chatModel.chats.value .asSequence() .map { it.chatInfo } .filterIsInstance() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index c91bc3bcfc..754fe5faf0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -43,7 +44,7 @@ const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair?) -> Unit, close: () -> Unit) { BackHandler(onBack = close) // TODO derivedStateOf? - val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId } + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId } val currentUser = chatModel.currentUser.value val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) { @@ -131,13 +132,15 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl withBGApi { val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId) if (r) { - chatModel.removeChat(chat.remoteHostId, chatInfo.id) - if (chatModel.chatId.value == chatInfo.id) { - chatModel.chatId.value = null - ModalManager.end.closeModals() + withChats { + removeChat(chat.remoteHostId, chatInfo.id) + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.end.closeModals() + } + ntfManager.cancelNotificationsForChat(chatInfo.id) + close?.invoke() } - ntfManager.cancelNotificationsForChat(chatInfo.id) - close?.invoke() } } }, @@ -169,7 +172,9 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe withBGApi { val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId) if (updatedMember != null) { - chatModel.upsertGroupMember(rhId, groupInfo, updatedMember) + withChats { + upsertGroupMember(rhId, groupInfo, updatedMember) + } } } }, @@ -235,7 +240,7 @@ fun GroupChatInfoLayout( WallpaperButton { ModalManager.end.showModal { - val chat = remember { derivedStateOf { chatModel.chats.firstOrNull { it.id == chat.id } } } + val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } val c = chat.value if (c != null) { ChatWallpaperEditorModal(c) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index 98b9ca729f..9ef680b3a8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* @@ -52,7 +53,7 @@ fun GroupMemberInfoView( closeAll: () -> Unit, // Close all open windows up to ChatView ) { BackHandler(onBack = close) - val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId } + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId } val connStats = remember { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() var progressIndicator by remember { mutableStateOf(false) } @@ -72,13 +73,15 @@ fun GroupMemberInfoView( withBGApi { val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it) if (c != null) { - if (chatModel.getContactChat(it) == null) { - chatModel.addChat(c) + withChats { + if (chatModel.getContactChat(it) == null) { + addChat(c) + } + chatModel.chatItemStatuses.clear() + chatModel.chatItems.replaceAll(c.chatItems) + chatModel.chatId.value = c.id + closeAll() } - chatModel.chatItemStatuses.clear() - chatModel.chatItems.replaceAll(c.chatItems) - chatModel.chatId.value = c.id - closeAll() } } }, @@ -88,8 +91,10 @@ fun GroupMemberInfoView( val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId) if (memberContact != null) { val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf()) - chatModel.addChat(memberChat) - openLoadedChat(memberChat, chatModel) + withChats { + addChat(memberChat) + openLoadedChat(memberChat, chatModel) + } closeAll() chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) } @@ -114,7 +119,9 @@ fun GroupMemberInfoView( withBGApi { kotlin.runCatching { val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it) - chatModel.upsertGroupMember(rhId, groupInfo, mem) + withChats { + upsertGroupMember(rhId, groupInfo, mem) + } }.onFailure { newRole.value = prevValue } @@ -127,7 +134,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -139,7 +148,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -150,7 +161,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -161,7 +174,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -177,15 +192,17 @@ fun GroupMemberInfoView( verify = { code -> chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r -> val (verified, existingCode) = r - chatModel.upsertGroupMember( - rhId, - groupInfo, - mem.copy( - activeConn = mem.activeConn?.copy( - connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + withChats { + upsertGroupMember( + rhId, + groupInfo, + mem.copy( + activeConn = mem.activeConn?.copy( + connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + ) ) ) - ) + } r } }, @@ -211,7 +228,9 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c withBGApi { val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId) if (removedMember != null) { - chatModel.upsertGroupMember(rhId, groupInfo, removedMember) + withChats { + upsertGroupMember(rhId, groupInfo, removedMember) + } } close?.invoke() } @@ -621,7 +640,9 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem withBGApi { val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings) if (success) { - ChatModel.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + withChats { + upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + } } } } @@ -652,7 +673,9 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) { withBGApi { val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked) - chatModel.upsertGroupMember(rhId, gInfo, updatedMember) + withChats { + upsertGroupMember(rhId, gInfo, updatedMember) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index 82646a99c5..b7d66dd4f6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -17,6 +17,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @@ -43,8 +44,10 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences()) val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) if (g != null) { - m.updateGroup(rhId, g) - currentPreferences = preferences + withChats { + updateGroup(rhId, g) + currentPreferences = preferences + } } afterSave() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index 7975a298d1..6375ef1a20 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -17,6 +17,7 @@ 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.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.* @@ -38,7 +39,9 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl withBGApi { val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p) if (gInfo != null) { - chatModel.updateGroup(rhId, gInfo) + withChats { + updateGroup(rhId, gInfo) + } close.invoke() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index 3bbeceb03c..794d6d4a20 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -28,6 +28,7 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.chatJsonLength @@ -52,7 +53,9 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated) if (res != null) { gInfo = res - m.updateGroup(rhId, res) + withChats { + updateGroup(rhId, res) + } welcomeText.value = welcome ?: "" } afterSave() 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 f6d23c020f..071f363bdc 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,6 +19,7 @@ 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.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* @@ -556,7 +557,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) { withApi { if (chat.chatStats.unreadCount > 0) { val minUnreadItemId = chat.chatStats.minUnreadItemId - chatModel.markChatItemsRead(chat) + withChats { + markChatItemsRead(chat) + } chatModel.controller.apiChatRead( chat.remoteHostId, chat.chatInfo.chatType, @@ -573,7 +576,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) { false ) if (success) { - chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + withChats { + replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + } } } } @@ -591,7 +596,9 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) { true ) if (success) { - chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) + withChats { + replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) + } } } } @@ -631,7 +638,9 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId) if (contact != null && isCurrentUser && contactRequest != null) { val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) - chatModel.replaceChat(rhId, contactRequest.id, chat) + withChats { + replaceChat(rhId, contactRequest.id, chat) + } chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) } } @@ -640,7 +649,9 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { withBGApi { chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId) - chatModel.removeChat(rhId, contactRequest.id) + withChats { + removeChat(rhId, contactRequest.id) + } } } @@ -656,7 +667,9 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti withBGApi { AlertManager.shared.hideAlert() if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) { - chatModel.removeChat(rhId, connection.id) + withChats { + removeChat(rhId, connection.id) + } onSuccess() } } @@ -675,7 +688,9 @@ fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatMo withBGApi { val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId) if (r) { - chatModel.removeChat(rhId, chatInfo.id) + withChats { + removeChat(rhId, chatInfo.id) + } if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -737,7 +752,9 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactId: Long, incognito: Boolean): Boolean { val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId) if (contact != null) { - chatModel.updateContact(rhId, contact) + withChats { + updateContact(rhId, contact) + } AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted), @@ -778,7 +795,9 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { withBGApi { val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId) if (r) { - chatModel.removeChat(rhId, groupInfo.id) + withChats { + removeChat(rhId, groupInfo.id) + } if (chatModel.chatId.value == groupInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -827,7 +846,9 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo else -> false } if (res && newChatInfo != null) { - chatModel.updateChatInfo(chat.remoteHostId, newChatInfo) + withChats { + updateChatInfo(chat.remoteHostId, newChatInfo) + } if (chatSettings.enableNtfs != MsgFilter.All) { ntfManager.cancelNotificationsForChat(chat.id) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 43f0b7ef9b..1032b59474 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -121,7 +121,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf if (!chatModel.desktopNoUserNoRemote) { ChatList(chatModel, searchText = searchText) } - if (chatModel.chats.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { + if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { Text(stringResource( if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) if (!stopped && !newChatSheetState.collectAsState().value.isVisible() && chatModel.chatRunning.value == true && searchText.value.text.isEmpty()) { @@ -415,7 +415,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState } } else { val padding = if (appPlatform.isDesktop) 0.dp else 7.dp - if (chatModel.chats.size > 0) { + if (chatModel.chats.value.isNotEmpty()) { ToggleFilterEnabledButton() } Spacer(Modifier.width(padding)) @@ -494,7 +494,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState(null) } - val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.toList()) + val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList()) LazyColumnWithScrollBar( Modifier.fillMaxWidth(), listState @@ -523,7 +523,7 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState= 8) { + if (chatModel.chats.value.size >= 8) { barButtons.add { IconButton({ showSearch = true }) { Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) @@ -186,7 +186,7 @@ private fun ShareList( ) { val chats by remember(search) { derivedStateOf { - val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local } + val sorted = chatModel.chats.value.toList().sortedByDescending { it.chatInfo is ChatInfo.Local } if (search.isEmpty()) { sorted.filter { it.chatInfo.ready } } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index fa43048e9e..3ade14bdce 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -20,7 +20,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller -import chat.simplex.common.model.ChatModel.updatingChatsMutex +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -502,7 +502,11 @@ fun deleteChatDatabaseFilesAndState() { // Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself chatModel.chatId.value = null chatModel.chatItems.clear() - chatModel.chats.clear() + withLongRunningApi { + withChats { + chats.clear() + } + } chatModel.users.clear() ntfManager.cancelAllNotifications() } @@ -714,10 +718,10 @@ private fun afterSetCiTTL( appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) withApi { try { - updatingChatsMutex.withLock { + withChats { // this is using current remote host on purpose - if it changes during update, it will load correct chats val chats = m.controller.apiGetChats(m.remoteHostId()) - m.updateChats(chats) + updateChats(chats) } } catch (e: Exception) { Log.e(TAG, "apiGetChats error: ${e.message}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 807b3a09c0..b69fc039fc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -18,6 +18,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.group.AddGroupMembersView import chat.simplex.common.views.chatlist.setGroupMembers @@ -39,10 +40,12 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { withBGApi { val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile) if (groupInfo != null) { - chatModel.updateGroup(rhId = rhId, groupInfo) - chatModel.chatItems.clear() - chatModel.chatItemStatuses.clear() - chatModel.chatId.value = groupInfo.id + withChats { + updateGroup(rhId = rhId, groupInfo) + chatModel.chatItems.clear() + chatModel.chatItemStatuses.clear() + chatModel.chatId.value = groupInfo.id + } setGroupMembers(rhId, groupInfo, chatModel) close.invoke() if (!groupInfo.incognito) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 5b56fe5e39..e49fbcf1e6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* @@ -331,7 +332,9 @@ suspend fun connectViaUri( val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString()) val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION if (pcc != null) { - chatModel.updateContactConnection(rhId, pcc) + withChats { + updateContactConnection(rhId, pcc) + } close?.invoke() AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index 31623e4a61..09a6c447d5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -22,6 +22,7 @@ import chat.simplex.common.views.chat.LocalAliasEditor import chat.simplex.common.views.chatlist.deleteContactConnectionAlert import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.PendingContactConnection import chat.simplex.common.platform.* import chat.simplex.common.views.usersettings.* @@ -186,7 +187,9 @@ fun DeleteButton(onClick: () -> Unit) { private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withBGApi { chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let { - chatModel.updateContactConnection(rhId, it) + withChats { + updateContactConnection(rhId, it) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index a877e123b3..4c3e83ef58 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -25,6 +25,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -218,8 +219,10 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection withBGApi { val contactConn = contactConnection.value ?: return@withBGApi val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi - contactConnection.value = conn - chatModel.updateContactConnection(rhId, conn) + withChats { + contactConnection.value = conn + updateContactConnection(rhId, conn) + } } chatModel.markShowingInvitationUsed() } @@ -367,9 +370,11 @@ private fun createInvitation( withBGApi { val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get()) if (r != null) { - chatModel.updateContactConnection(rhId, r.second) - chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false) - contactConnection.value = r.second + withChats { + updateContactConnection(rhId, r.second) + chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false) + contactConnection.value = r.second + } } else { creatingConnReq.value = false if (alert != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt index cd0e40f5d0..96a0bdcda3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR @@ -33,7 +34,9 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { if (updated != null) { val (updatedProfile, updatedContacts) = updated m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences) - updatedContacts.forEach { m.updateContact(user.remoteHostId, it) } + withChats { + updatedContacts.forEach { updateContact(user.remoteHostId, it) } + } currentPreferences = preferences } afterSave() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 88b14b6a66..578a505e0c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -32,6 +32,7 @@ import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.localauth.SetAppPasscodeView import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import kotlin.math.min import kotlin.math.roundToInt @@ -121,14 +122,16 @@ fun PrivacySettingsView( chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable) if (clearOverrides) { // For loop here is to prevent ConcurrentModificationException that happens with forEach - for (i in 0 until chatModel.chats.size) { - val chat = chatModel.chats[i] - if (chat.chatInfo is ChatInfo.Direct) { - var contact = chat.chatInfo.contact - val sendRcpts = contact.chatSettings.sendRcpts - if (sendRcpts != null && sendRcpts != enable) { - contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null)) - chatModel.updateContact(currentUser.remoteHostId, contact) + withChats { + for (i in 0 until chats.size) { + val chat = chats[i] + if (chat.chatInfo is ChatInfo.Direct) { + var contact = chat.chatInfo.contact + val sendRcpts = contact.chatSettings.sendRcpts + if (sendRcpts != null && sendRcpts != enable) { + contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null)) + updateContact(currentUser.remoteHostId, contact) + } } } } @@ -143,15 +146,17 @@ fun PrivacySettingsView( chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable) if (clearOverrides) { - // For loop here is to prevent ConcurrentModificationException that happens with forEach - for (i in 0 until chatModel.chats.size) { - val chat = chatModel.chats[i] - if (chat.chatInfo is ChatInfo.Group) { - var groupInfo = chat.chatInfo.groupInfo - val sendRcpts = groupInfo.chatSettings.sendRcpts - if (sendRcpts != null && sendRcpts != enable) { - groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null)) - chatModel.updateGroup(currentUser.remoteHostId, groupInfo) + withChats { + // For loop here is to prevent ConcurrentModificationException that happens with forEach + for (i in 0 until chats.size) { + val chat = chats[i] + if (chat.chatInfo is ChatInfo.Group) { + var groupInfo = chat.chatInfo.groupInfo + val sendRcpts = groupInfo.chatSettings.sendRcpts + if (sendRcpts != null && sendRcpts != enable) { + groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null)) + updateGroup(currentUser.remoteHostId, groupInfo) + } } } } @@ -164,7 +169,7 @@ fun PrivacySettingsView( DeliveryReceiptsSection( currentUser = currentUser, setOrAskSendReceiptsContacts = { enable -> - val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> if (chat.chatInfo is ChatInfo.Direct) { val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) @@ -179,7 +184,7 @@ fun PrivacySettingsView( } }, setOrAskSendReceiptsGroups = { enable -> - val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> if (chat.chatInfo is ChatInfo.Group) { val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index d6ee659dbf..8e1643571b 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.* import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.size import chat.simplex.common.platform.* import chat.simplex.common.platform.DesktopPlatform import chat.simplex.common.showApp