From 7e76fa43b60e73e88603e96b76f839008f659705 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Wed, 1 Jan 2025 21:31:15 +0700 Subject: [PATCH] tmp --- .../kotlin/chat/simplex/common/App.kt | 2 +- .../chat/simplex/common/model/ChatModel.kt | 260 +++++++++++------- .../chat/simplex/common/model/SimpleXAPI.kt | 53 ++-- .../chat/simplex/common/platform/Core.kt | 2 +- .../chat/simplex/common/views/WelcomeView.kt | 7 +- .../simplex/common/views/call/CallManager.kt | 4 +- .../simplex/common/views/chat/ChatInfoView.kt | 2 +- .../simplex/common/views/chat/ChatView.kt | 20 +- .../views/chat/group/AddGroupMembersView.kt | 2 +- .../views/chat/group/GroupChatInfoView.kt | 2 +- .../views/chatlist/ChatListNavLinkView.kt | 7 +- .../common/views/chatlist/ChatListView.kt | 8 +- .../views/chatlist/ServersSummaryView.kt | 2 +- .../common/views/chatlist/ShareListView.kt | 4 +- .../common/views/chatlist/UserPicker.kt | 10 +- .../views/database/DatabaseErrorView.kt | 8 +- .../common/views/database/DatabaseView.kt | 11 +- .../common/views/helpers/AlertManager.kt | 6 +- .../views/migration/MigrateFromDevice.kt | 1 + .../common/views/migration/MigrateToDevice.kt | 37 +-- .../common/views/newchat/NewChatView.kt | 4 +- .../views/onboarding/LinkAMobileView.kt | 4 +- .../views/onboarding/SetNotificationsMode.kt | 2 +- .../common/views/remote/ConnectMobileView.kt | 8 +- .../common/views/usersettings/Appearance.kt | 10 +- .../usersettings/NotificationsSettingsView.kt | 4 +- .../views/usersettings/PrivacySettings.kt | 2 +- .../usersettings/SetDeliveryReceiptsView.kt | 4 +- .../views/usersettings/UserProfilesView.kt | 6 +- .../AdvancedNetworkSettings.kt | 2 +- .../networkAndServers/NetworkAndServers.kt | 2 +- .../common/views/call/CallView.desktop.kt | 24 +- 32 files changed, 288 insertions(+), 232 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index fc17c49c7e..3310e0aeb8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -292,7 +292,7 @@ fun AndroidWrapInCallLayout(content: @Composable () -> Unit) { @Composable fun AndroidScreen(userPickerState: MutableStateFlow) { BoxWithConstraints { - val currentChatId = remember { mutableStateOf(chatModel.chatId.value) } + val currentChatId = remember { MutableStateFlow(chatModel.chatId.value) } val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } val cutout = WindowInsets.displayCutout.only(WindowInsetsSides.Horizontal).asPaddingValues() val direction = LocalLayoutDirection.current 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 e2fe96e178..9439412518 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 @@ -48,71 +48,71 @@ import kotlin.time.* @Stable object ChatModel { val controller: ChatController = ChatController - val setDeliveryReceipts = mutableStateOf(false) - val currentUser = mutableStateOf(null) - val users = mutableStateListOf() - val localUserCreated = mutableStateOf(null) - val chatRunning = mutableStateOf(null) - val chatDbChanged = mutableStateOf(false) - val chatDbEncrypted = mutableStateOf(false) - val chatDbStatus = mutableStateOf(null) - val ctrlInitInProgress = mutableStateOf(false) - val dbMigrationInProgress = mutableStateOf(false) - val incompleteInitializedDbRemoved = mutableStateOf(false) - private val _chats = mutableStateOf(SnapshotStateList()) - val chats: State> = _chats + val setDeliveryReceipts = MutableStateFlow(false) + val currentUser = MutableStateFlow(null) + val users = MutableStateFlow>(emptyList()) + val localUserCreated = MutableStateFlow(null) + val chatRunning = MutableStateFlow(null) + val chatDbChanged = MutableStateFlow(false) + val chatDbEncrypted = MutableStateFlow(false) + val chatDbStatus = MutableStateFlow(null) + val ctrlInitInProgress = MutableStateFlow(false) + val dbMigrationInProgress = MutableStateFlow(false) + val incompleteInitializedDbRemoved = MutableStateFlow(false) + private val _chats = MutableStateFlow(SnapshotStateList()) + val chats: StateFlow> = _chats private val chatsContext = ChatsContext() // map of connections network statuses, key is agent connection id - val networkStatuses = mutableStateMapOf() - val switchingUsersAndHosts = mutableStateOf(false) + val networkStatuses = MutableStateFlow>(emptyMap()) + val switchingUsersAndHosts = MutableStateFlow(false) // current chat - val chatId = mutableStateOf(null) + val chatId = MutableStateFlow(null) /** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on. * If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages]. * If you use api call to get the items, use just [add] instead of [addAndNotify]. * Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */ - val chatItems = mutableStateOf(SnapshotStateList()) + val chatItems = MutableStateFlow(SnapshotStateList()) // set listener here that will be notified on every add/delete of a chat item var chatItemsChangesListener: ChatItemsChangesListener? = null val chatState = ActiveChatState() // rhId, chatId - val deletedChats = mutableStateOf>>(emptyList()) + val deletedChats = MutableStateFlow>>(emptyList()) val chatItemStatuses = mutableMapOf() - val groupMembers = mutableStateListOf() - val groupMembersIndexes = mutableStateMapOf() + val groupMembers = MutableStateFlow>(emptyList()) + val groupMembersIndexes = MutableStateFlow>(emptyMap()) // Chat Tags - val userTags = mutableStateOf(emptyList()) - val activeChatTagFilter = mutableStateOf(null) - val presetTags = mutableStateMapOf() - val unreadTags = mutableStateMapOf() + val userTags = MutableStateFlow(emptyList()) + val activeChatTagFilter = MutableStateFlow(null) + val presetTags = MutableStateFlow>(emptyMap()) + val unreadTags = MutableStateFlow>(emptyMap()) // false: default placement, true: floating window. // Used for deciding to add terminal items on main thread or not. Floating means appPrefs.terminalAlwaysVisible var terminalsVisible = setOf() - val terminalItems = mutableStateOf>(listOf()) - val userAddress = mutableStateOf(null) - val chatItemTTL = mutableStateOf(ChatItemTTL.None) + val terminalItems = MutableStateFlow>(emptyList()) + val userAddress = MutableStateFlow(null) + val chatItemTTL = MutableStateFlow(ChatItemTTL.None) // set when app opened from external intent - val clearOverlays = mutableStateOf(false) + val clearOverlays = MutableStateFlow(false) // Only needed during onboarding when user skipped password setup (left as random password) - val desktopOnboardingRandomPassword = mutableStateOf(false) + val desktopOnboardingRandomPassword = MutableStateFlow(false) // set when app is opened via contact or invitation URI (rhId, uri) - val appOpenUrl = mutableStateOf?>(null) + val appOpenUrl = MutableStateFlow?>(null) // Needed to check for bottom nav bar and to apply or not navigation bar color on Android - val newChatSheetVisible = mutableStateOf(false) + val newChatSheetVisible = MutableStateFlow(false) // Needed to apply black color to left/right cutout area on Android - val fullscreenGalleryVisible = mutableStateOf(false) + val fullscreenGalleryVisible = MutableStateFlow(false) // preferences val notificationPreviewMode by lazy { - mutableStateOf( + MutableStateFlow( try { NotificationPreviewMode.valueOf(controller.appPrefs.notificationPreviewMode.get()!!) } catch (e: Exception) { @@ -120,41 +120,41 @@ object ChatModel { } ) } - val showAuthScreen by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) } - val showAdvertiseLAUnavailableAlert = mutableStateOf(false) - val showChatPreviews by lazy { mutableStateOf(ChatController.appPrefs.privacyShowChatPreviews.get()) } + val showAuthScreen by lazy { MutableStateFlow(ChatController.appPrefs.performLA.get()) } + val showAdvertiseLAUnavailableAlert = MutableStateFlow(false) + val showChatPreviews by lazy { MutableStateFlow(ChatController.appPrefs.privacyShowChatPreviews.get()) } // current WebRTC call val callManager = CallManager(this) - val callInvitations = mutableStateMapOf() - val activeCallInvitation = mutableStateOf(null) - val activeCall = mutableStateOf(null) - val activeCallViewIsVisible = mutableStateOf(false) - val activeCallViewIsCollapsed = mutableStateOf(false) - val callCommand = mutableStateListOf() - val showCallView = mutableStateOf(false) - val switchingCall = mutableStateOf(false) + val callInvitations = MutableStateFlow>(emptyMap()) + val activeCallInvitation = MutableStateFlow(null) + val activeCall = MutableStateFlow(null) + val activeCallViewIsVisible = MutableStateFlow(false) + val activeCallViewIsCollapsed = MutableStateFlow(false) + val callCommand = MutableStateFlow>(emptyList()) + val showCallView = MutableStateFlow(false) + val switchingCall = MutableStateFlow(false) // currently showing invitation - val showingInvitation = mutableStateOf(null as ShowingInvitation?) + val showingInvitation = MutableStateFlow(null as ShowingInvitation?) - val migrationState: MutableState by lazy { mutableStateOf(MigrationToDeviceState.makeMigrationState()) } + val migrationState: MutableStateFlow by lazy { MutableStateFlow(MigrationToDeviceState.makeMigrationState()) } - var draft = mutableStateOf(null as ComposeState?) - var draftChatId = mutableStateOf(null as String?) + var draft = MutableStateFlow(null as ComposeState?) + var draftChatId = MutableStateFlow(null as String?) // working with external intents or internal forwarding of chat items - val sharedContent = mutableStateOf(null as SharedContent?) + val sharedContent = MutableStateFlow(null as SharedContent?) val filesToDelete = mutableSetOf() - val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) } + val simplexLinkMode by lazy { MutableStateFlow(ChatController.appPrefs.simplexLinkMode.get()) } - val clipboardHasText = mutableStateOf(false) - val networkInfo = mutableStateOf(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true)) + val clipboardHasText = MutableStateFlow(false) + val networkInfo = MutableStateFlow(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true)) - val conditions = mutableStateOf(ServerOperatorConditionsDetail.empty) + val conditions = MutableStateFlow(ServerOperatorConditionsDetail.empty) - val updatingProgress = mutableStateOf(null as Float?) + val updatingProgress = MutableStateFlow(null as Float?) var updatingRequest: Closeable? = null private val updatingChatsMutex: Mutex = Mutex() @@ -164,12 +164,12 @@ object ChatModel { fun desktopNoUserNoRemote(): Boolean = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null // remote controller - val remoteHosts = mutableStateListOf() - val currentRemoteHost = mutableStateOf(null) + val remoteHosts = MutableStateFlow>(emptyList()) + val currentRemoteHost = MutableStateFlow(null) val remoteHostId: Long? @Composable get() = remember { currentRemoteHost }.value?.remoteHostId fun remoteHostId(): Long? = currentRemoteHost.value?.remoteHostId - val remoteHostPairing = mutableStateOf?>(null) - val remoteCtrlSession = mutableStateOf(null) + val remoteHostPairing = MutableStateFlow?>(null) + val remoteCtrlSession = MutableStateFlow(null) val processedCriticalError: ProcessedErrors = ProcessedErrors(60_000) val processedInternalError: ProcessedErrors = ProcessedErrors(20_000) @@ -180,11 +180,11 @@ object ChatModel { fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { currentUser.value } else { - users.firstOrNull { it.user.userId == userId }?.user + users.value.firstOrNull { it.user.userId == userId }?.user } private fun getUserIndex(user: User): Int = - users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == user.remoteHostId } + users.value.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == user.remoteHostId } fun updateUser(user: User) { val i = getUserIndex(user) @@ -230,42 +230,47 @@ object ChatModel { activeChatTagFilter.value = null } - presetTags.clear() - presetTags.putAll(newPresetTags) - unreadTags.clear() - unreadTags.putAll(newUnreadTags) + presetTags.value = newPresetTags + unreadTags.value = newUnreadTags } fun updateChatFavorite(favorite: Boolean, wasFavorite: Boolean) { - val count = presetTags[PresetTagKind.FAVORITES] + val pTags = presetTags.value.toMutableMap() + val count = pTags[PresetTagKind.FAVORITES] if (favorite && !wasFavorite) { - presetTags[PresetTagKind.FAVORITES] = (count ?: 0) + 1 + pTags[PresetTagKind.FAVORITES] = (count ?: 0) + 1 + presetTags.value = pTags } else if (!favorite && wasFavorite && count != null) { - presetTags[PresetTagKind.FAVORITES] = maxOf(0, count - 1) - if (activeChatTagFilter.value == ActiveFilter.PresetTag(PresetTagKind.FAVORITES) && (presetTags[PresetTagKind.FAVORITES] ?: 0) == 0) { + pTags[PresetTagKind.FAVORITES] = maxOf(0, count - 1) + if (activeChatTagFilter.value == ActiveFilter.PresetTag(PresetTagKind.FAVORITES) && (pTags[PresetTagKind.FAVORITES] ?: 0) == 0) { activeChatTagFilter.value = null } + presetTags.value = pTags } } fun addPresetChatTags(chatInfo: ChatInfo) { + val pTags = presetTags.value.toMutableMap() for (tag in PresetTagKind.entries) { if (presetTagMatchesChat(tag, chatInfo)) { - presetTags[tag] = (presetTags[tag] ?: 0) + 1 + pTags[tag] = (pTags[tag] ?: 0) + 1 } } + presetTags.value = pTags } fun removePresetChatTags(chatInfo: ChatInfo) { + val pTags = presetTags.value.toMutableMap() for (tag in PresetTagKind.entries) { if (presetTagMatchesChat(tag, chatInfo)) { - val count = presetTags[tag] + val count = pTags[tag] if (count != null) { - presetTags[tag] = maxOf(0, count - 1) + pTags[tag] = maxOf(0, count - 1) } } } + presetTags.value = pTags } fun markChatTagRead(chat: Chat) { @@ -281,9 +286,11 @@ object ChatModel { val nowUnread = chat.unreadTag if (nowUnread && !wasUnread) { + val uTags = unreadTags.value.toMutableMap() tags.forEach { tag -> - unreadTags[tag] = (unreadTags[tag] ?: 0) + 1 + uTags[tag] = (uTags[tag] ?: 0) + 1 } + unreadTags.value = uTags } else if (!nowUnread && wasUnread) { markChatTagRead_(chat, tags) } @@ -291,26 +298,30 @@ object ChatModel { fun moveChatTagUnread(chat: Chat, oldTags: List?, newTags: List) { if (chat.unreadTag) { + val uTags = unreadTags.value.toMutableMap() oldTags?.forEach { t -> - val oldCount = unreadTags[t] + val oldCount = uTags[t] if (oldCount != null) { - unreadTags[t] = maxOf(0, oldCount - 1) + uTags[t] = maxOf(0, oldCount - 1) } } newTags.forEach { t -> - unreadTags[t] = (unreadTags[t] ?: 0) + 1 + uTags[t] = (uTags[t] ?: 0) + 1 } + unreadTags.value = uTags } } private fun markChatTagRead_(chat: Chat, tags: List) { + val uTags = unreadTags.value.toMutableMap() for (tag in tags) { - val count = unreadTags[tag] + val count = uTags[tag] if (count != null) { - unreadTags[tag] = maxOf(0, count - 1) + uTags[tag] = maxOf(0, count - 1) } } + unreadTags.value = uTags } // toList() here is to prevent ConcurrentModificationException that is rarely happens but happens @@ -321,16 +332,17 @@ object ChatModel { fun getGroupChat(groupId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } fun populateGroupMembersIndexes() { - groupMembersIndexes.clear() - groupMembers.forEachIndexed { i, member -> - groupMembersIndexes[member.groupMemberId] = i + val indexes = mutableMapOf() + groupMembers.value.forEachIndexed { i, member -> + indexes[member.groupMemberId] = i } + groupMembersIndexes.value = indexes } fun getGroupMember(groupMemberId: Long): GroupMember? { - val memberIndex = groupMembersIndexes[groupMemberId] + val memberIndex = groupMembersIndexes.value[groupMemberId] return if (memberIndex != null) { - groupMembers[memberIndex] + groupMembers.value[memberIndex] } else { null } @@ -694,7 +706,7 @@ object ChatModel { } // update current chat return if (chatId.value == groupInfo.id) { - val memberIndex = groupMembersIndexes[member.groupMemberId] + val memberIndex = groupMembersIndexes.value[member.groupMemberId] val updated = chatItems.value.map { // Take into account only specific changes, not all. Other member updates are not important and can be skipped if (it.chatDir is CIDirection.GroupRcv && it.chatDir.groupMember.groupMemberId == member.groupMemberId && @@ -710,11 +722,14 @@ object ChatModel { if (updated != chatItems.value) { chatItems.replaceAll(updated) } + val gMembers = groupMembers.value.toMutableList() if (memberIndex != null) { - groupMembers[memberIndex] = member + gMembers[memberIndex] = member + groupMembers.value = gMembers false } else { - groupMembers.add(member) + gMembers.add(member) + groupMembers.value = gMembers groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1 true } @@ -741,7 +756,7 @@ object ChatModel { profile = newProfile.toLocalProfile(current.profile.profileId), fullPreferences = preferences ?: current.fullPreferences ) - val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId } + val i = users.value.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId } if (i != -1) { users[i] = users[i].copy(user = updated) } @@ -753,7 +768,7 @@ object ChatModel { val updated = current.copy( uiThemes = uiThemes ) - val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId } + val i = users.value.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId } if (i != -1) { users[i] = users[i].copy(user = updated) } @@ -816,9 +831,11 @@ object ChatModel { } private fun changeUnreadCounter(rhId: Long?, user: UserLike, by: Int) { - val i = users.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId } + val i = users.value.indexOfFirst { it.user.userId == user.userId && it.user.remoteHostId == rhId } if (i != -1) { - users[i] = users[i].copy(unreadCount = users[i].unreadCount + by) + val usrs = users.value.toMutableList() + usrs[i] = usrs[i].copy(unreadCount = usrs[i].unreadCount + by) + users.value = usrs } } @@ -2488,15 +2505,15 @@ data class ChatItem ( } } -fun MutableState>.add(index: Int, elem: Chat) { +fun MutableStateFlow>.add(index: Int, elem: Chat) { value = SnapshotStateList().apply { addAll(value); add(index, elem) } } -fun MutableState>.addAndNotify(index: Int, elem: ChatItem) { +fun MutableStateFlow>.addAndNotify(index: Int, elem: ChatItem) { value = SnapshotStateList().apply { addAll(value); add(index, elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) } } -fun MutableState>.add(elem: Chat) { +fun MutableStateFlow>.add(elem: Chat) { value = SnapshotStateList().apply { addAll(value); add(elem) } } @@ -2504,24 +2521,24 @@ fun MutableState>.add(elem: Chat) { fun MutableList.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmpty()) false else remAll(predicate) // Adds item to chatItems and notifies a listener about newly added item -fun MutableState>.addAndNotify(elem: ChatItem) { +fun MutableStateFlow>.addAndNotify(elem: ChatItem) { value = SnapshotStateList().apply { addAll(value); add(elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) } } -fun MutableState>.addAll(index: Int, elems: List) { +fun MutableStateFlow>.addAll(index: Int, elems: List) { value = SnapshotStateList().apply { addAll(value); addAll(index, elems) } } -fun MutableState>.addAll(elems: List) { +fun MutableStateFlow>.addAll(elems: List) { value = SnapshotStateList().apply { addAll(value); addAll(elems) } } -fun MutableState>.removeAll(block: (Chat) -> Boolean) { +fun MutableStateFlow>.removeAll(block: (Chat) -> Boolean) { value = SnapshotStateList().apply { addAll(value); removeAll(block) } } // Removes item(s) from chatItems and notifies a listener about removed item(s) -fun MutableState>.removeAllAndNotify(block: (ChatItem) -> Boolean) { +fun MutableStateFlow>.removeAllAndNotify(block: (ChatItem) -> Boolean) { val toRemove = ArrayList>() value = SnapshotStateList().apply { addAll(value) @@ -2538,7 +2555,7 @@ fun MutableState>.removeAllAndNotify(block: (ChatIte } } -fun MutableState>.removeAt(index: Int): Chat { +fun MutableStateFlow>.removeAt(index: Int): Chat { val new = SnapshotStateList() new.addAll(value) val res = new.removeAt(index) @@ -2546,7 +2563,14 @@ fun MutableState>.removeAt(index: Int): Chat { return res } -fun MutableState>.removeLastAndNotify() { +fun MutableStateFlow>.removeAt(index: Int): T { + val l = value.toMutableList() + val removed = l.removeAt(index) + value = l + return removed +} + +fun MutableStateFlow>.removeLastAndNotify() { val removed: Triple value = SnapshotStateList().apply { addAll(value) @@ -2557,29 +2581,53 @@ fun MutableState>.removeLastAndNotify() { chatItemsChangesListener?.removed(listOf(removed), value) } -fun MutableState>.replaceAll(elems: List) { +fun MutableStateFlow>.replaceAll(elems: List) { value = SnapshotStateList().apply { addAll(elems) } } -fun MutableState>.clear() { +fun MutableStateFlow>.clear() { value = SnapshotStateList() } // Removes all chatItems and notifies a listener about it -fun MutableState>.clearAndNotify() { +fun MutableStateFlow>.clearAndNotify() { value = SnapshotStateList() chatItemsChangesListener?.cleared() } -fun State>.asReversed(): MutableList = value.asReversed() +fun StateFlow>.asReversed(): MutableList = value.asReversed() -fun State>.toList(): List = value.toList() +fun StateFlow>.toList(): List = value.toList() -operator fun State>.get(i: Int): T = value[i] +operator fun StateFlow>.get(key: K): V? = value[key] -operator fun State>.set(index: Int, elem: T) { value[index] = elem } +operator fun StateFlow>.get(i: Int): T = value[i] + +operator fun StateFlow>.set(index: Int, elem: T) { value[index] = elem } + +operator fun MutableStateFlow>.set(index: Int, elem: T) { + val l = value.toMutableList() + l[index] = elem + value = l +} + +fun StateFlow>.isEmpty(): Boolean = value.isEmpty() + +operator fun MutableStateFlow>.set(key: K, elem: V) { + val m = value.toMutableMap() + m[key] = elem + value = m +} + +fun MutableStateFlow>.remove(key: K): V? { + val m = value.toMutableMap() + val removed = m.remove(key) + value = m + return removed +} val State>.size: Int get() = value.size +val StateFlow>.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 a86be622b9..abfe51e4f9 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 @@ -524,8 +524,7 @@ object ChatController { apiSetNetworkConfig(getNetCfg()) val chatRunning = apiCheckChatRunning() val users = listUsers(null) - chatModel.users.clear() - chatModel.users.addAll(users) + chatModel.users.value = users if (!chatRunning) { chatModel.currentUser.value = user chatModel.localUserCreated.value = true @@ -557,7 +556,7 @@ object ChatController { Log.d(TAG, "user: null") try { if (chatModel.chatRunning.value == true) return - chatModel.users.clear() + chatModel.users.value = emptyList() chatModel.currentUser.value = null chatModel.localUserCreated.value = false appPrefs.chatLastStart.set(Clock.System.now()) @@ -609,10 +608,9 @@ object ChatController { ntfManager.cancelNotificationsForUser(prevActiveUser.userId) } val users = listUsers(rhId) - chatModel.users.clear() - chatModel.users.addAll(users) + chatModel.users.value = users getUserChatData(rhId) - val invitation = chatModel.callInvitations.values.firstOrNull { inv -> inv.user.userId == toUserId } + val invitation = chatModel.callInvitations.value.values.firstOrNull { inv -> inv.user.userId == toUserId } if (invitation != null && currentUser != null) { chatModel.callManager.reportNewIncomingCall(invitation.copy(user = currentUser)) } @@ -2086,8 +2084,7 @@ object ChatController { suspend fun reloadRemoteHosts() { val hosts = listRemoteHosts() ?: return - chatModel.remoteHosts.clear() - chatModel.remoteHosts.addAll(hosts) + chatModel.remoteHosts.value = hosts } suspend fun startRemoteHost(rhId: Long?, multicast: Boolean = true, address: RemoteCtrlAddress?, port: Int?): CR.RemoteHostStarted? { @@ -2446,14 +2443,18 @@ object ChatController { } } is CR.NetworkStatusResp -> { + val nStatuses = chatModel.networkStatuses.value.toMutableMap() for (cId in r.connections) { - chatModel.networkStatuses[cId] = r.networkStatus + nStatuses[cId] = r.networkStatus } + chatModel.networkStatuses.value = nStatuses } is CR.NetworkStatuses -> { + val nStatuses = chatModel.networkStatuses.value.toMutableMap() for (s in r.networkStatuses) { - chatModel.networkStatuses[s.agentConnId] = s.networkStatus + nStatuses[s.agentConnId] = s.networkStatus } + chatModel.networkStatuses.value = nStatuses } is CR.NewChatItems -> withBGApi { r.chatItems.forEach { chatItem -> @@ -2727,31 +2728,33 @@ object ChatController { val useRelay = appPrefs.webrtcPolicyRelay.get() val iceServers = getIceServers() Log.d(TAG, ".callOffer iceServers $iceServers") - chatModel.callCommand.add(WCallCommand.Offer( + chatModel.callCommand.value += WCallCommand.Offer( offer = r.offer.rtcSession, iceCandidates = r.offer.rtcIceCandidates, media = r.callType.media, aesKey = r.sharedKey, iceServers = iceServers, relay = useRelay - )) + ) } } is CR.CallAnswer -> { withCall(r, r.contact) { call -> chatModel.activeCall.value = call.copy(callState = CallState.AnswerReceived) - chatModel.callCommand.add(WCallCommand.Answer(answer = r.answer.rtcSession, iceCandidates = r.answer.rtcIceCandidates)) + chatModel.callCommand.value += WCallCommand.Answer(answer = r.answer.rtcSession, iceCandidates = r.answer.rtcIceCandidates) } } is CR.CallExtraInfo -> { withCall(r, r.contact) { _ -> - chatModel.callCommand.add(WCallCommand.Ice(iceCandidates = r.extraInfo.rtcIceCandidates)) + chatModel.callCommand.value += WCallCommand.Ice(iceCandidates = r.extraInfo.rtcIceCandidates) } } is CR.CallEnded -> { - val invitation = chatModel.callInvitations.remove(r.contact.id) + val invits = chatModel.callInvitations.value.toMutableMap() + val invitation = invits.remove(r.contact.id) if (invitation != null) { chatModel.callManager.reportCallRemoteEnded(invitation = invitation) + chatModel.callInvitations.value = invits } withCall(r, r.contact) { call -> withBGApi { chatModel.callManager.endCall(call) } @@ -2797,7 +2800,7 @@ object ChatController { } } is CR.RemoteHostStopped -> { - val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ } + val disconnectedHost = chatModel.remoteHosts.value.firstOrNull { it.remoteHostId == r.remoteHostId_ } chatModel.remoteHostPairing.value = null if (disconnectedHost != null) { val deviceName = disconnectedHost.hostDeviceName.ifEmpty { disconnectedHost.remoteHostId.toString() } @@ -2955,14 +2958,11 @@ object ChatController { val m = chatModel m.remoteCtrlSession.value = null val users = listUsers(null) - m.users.clear() - m.users.addAll(users) + m.users.value = users getUserChatData(null) val statuses = apiGetNetworkStatuses(null) if (statuses != null) { - chatModel.networkStatuses.clear() - val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap() - chatModel.networkStatuses.putAll(ss) + chatModel.networkStatuses.value = statuses.associate { it.agentConnId to it.networkStatus }.toMap() } } @@ -3009,9 +3009,11 @@ object ChatController { } private fun updateContactsStatus(contactRefs: List, status: NetworkStatus) { + val nStatuses = chatModel.networkStatuses.value.toMutableMap() for (c in contactRefs) { - chatModel.networkStatuses[c.agentConnId] = status + nStatuses[c.agentConnId] = status } + chatModel.networkStatuses.value = nStatuses } private fun processContactSubError(contact: Contact, chatError: ChatError) { @@ -3040,8 +3042,7 @@ object ChatController { reloadRemoteHosts() val user = apiGetActiveUser(rhId) val users = listUsers(rhId) - chatModel.users.clear() - chatModel.users.addAll(users) + chatModel.users.value = users chatModel.currentUser.value = user if (user == null) { chatModel.chatItems.clearAndNotify() @@ -3052,9 +3053,7 @@ object ChatController { } val statuses = apiGetNetworkStatuses(rhId) if (statuses != null) { - chatModel.networkStatuses.clear() - val ss = statuses.associate { it.agentConnId to it.networkStatus }.toMap() - chatModel.networkStatuses.putAll(ss) + chatModel.networkStatuses.value = statuses.associate { it.agentConnId to it.networkStatus }.toMap() } getUserChatData(rhId) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 5262714099..f502713925 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -131,7 +131,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat if (user == null) { chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = null - chatModel.users.clear() + chatModel.users.value = emptyList() if (appPlatform.isDesktop) { /** * Setting it here to null because otherwise the screen will flash in [MainScreen] after the first start diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 8317c6cf6c..0395e30a91 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -108,7 +108,7 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { var savedKeyboardState by remember { mutableStateOf(keyboardState) } CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler()) { ModalView({ - if (chatModel.users.none { !it.user.hidden }) { + if (chatModel.users.value.none { !it.user.hidden }) { appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } else { close() @@ -185,8 +185,7 @@ fun createProfileInProfiles(chatModel: ChatModel, displayName: String, close: () chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) } else { val users = chatModel.controller.listUsers(rhId) - chatModel.users.clear() - chatModel.users.addAll(users) + chatModel.users.value = users chatModel.controller.getUserChatData(rhId) close() } @@ -201,7 +200,7 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () chatModel.localUserCreated.value = true val onboardingStage = chatModel.controller.appPrefs.onboardingStage // No users or no visible users - if (chatModel.users.none { u -> !u.user.hidden }) { + if (chatModel.users.value.none { u -> !u.user.hidden }) { onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) { OnboardingStage.Step2_5_SetupDatabasePassphrase } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt index d6ab57a70d..1b696a6762 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt @@ -58,12 +58,12 @@ class CallManager(val chatModel: ChatModel) { val useRelay = controller.appPrefs.webrtcPolicyRelay.get() val iceServers = getIceServers() Log.d(TAG, "answerIncomingCall iceServers: $iceServers") - callCommand.add(WCallCommand.Start( + callCommand.value += WCallCommand.Start( media = invitation.callType.media, aesKey = invitation.sharedKey, iceServers = iceServers, relay = useRelay - )) + ) callInvitations.remove(invitation.contact.id) if (invitation.contact.id == activeCallInvitation.value?.contact?.id) { activeCallInvitation.value = 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 9b580edb62..5770ab2afa 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 @@ -68,7 +68,7 @@ fun ChatInfoView( val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null && currentUser != null) { - val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) { + val contactNetworkStatus = remember(chatModel.networkStatuses.value, contact) { mutableStateOf(chatModel.contactNetworkStatus(contact)) } val chatRh = chat.remoteHostId 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 c58561718e..81a505d041 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 @@ -57,7 +57,7 @@ data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val dat @Composable // staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat // to chat list smooth. Otherwise, chat view will become blank right before the transition starts -fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) -> Unit) { +fun ChatView(staleChatId: StateFlow, onComposed: suspend (chatId: String) -> Unit) { val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } val showSearch = rememberSaveable { mutableStateOf(false) } val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } } @@ -220,8 +220,8 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - hideKeyboard(view) AudioPlayer.stop() chatModel.chatId.value = null - chatModel.groupMembers.clear() - chatModel.groupMembersIndexes.clear() + chatModel.groupMembers.value = emptyList() + chatModel.groupMembersIndexes.value = emptyMap() }, info = { if (ModalManager.end.hasModalsOpen()) { @@ -246,7 +246,7 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - if (chatInfo is ChatInfo.Direct) { var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } var code: String? by remember { mutableStateOf(preloadedCode) } - KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) { + KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.value) { contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) preloadedContactInfo = contactInfo code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second @@ -359,11 +359,13 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - acceptCall = { contact -> hideKeyboard(view) withBGApi { - val invitation = chatModel.callInvitations.remove(contact.id) + val invts = chatModel.callInvitations.value.toMutableMap() + val invitation = invts.remove(contact.id) ?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id } if (invitation == null) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) } else { + chatModel.callInvitations.value = invts chatModel.callManager.acceptIncomingCall(invitation = invitation) } } @@ -431,7 +433,7 @@ fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) - chatModel.getChat(chatId) }, findModelMember = { memberId -> - chatModel.groupMembers.find { it.id == memberId } + chatModel.groupMembers.value.find { it.id == memberId } }, setReaction = { cInfo, cItem, add, reaction -> withBGApi { @@ -578,7 +580,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) activeCall.value?.androidCallState?.close() chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile, androidCallState = platform.androidCreateActiveCallState()) chatModel.showCallView.value = true - chatModel.callCommand.add(WCallCommand.Capabilities(media)) + chatModel.callCommand.value += WCallCommand.Capabilities(media) } } } @@ -739,7 +741,7 @@ fun BoxScope.ChatInfoToolbar( } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val menuItems = arrayListOf<@Composable () -> Unit>() - val activeCall by remember { chatModel.activeCall } + val activeCall by chatModel.activeCall.collectAsState() if (chatInfo is ChatInfo.Local) { barButtons.add { IconButton( @@ -1359,7 +1361,7 @@ private fun LoadLastItems(loadingMoreItems: MutableState, remoteHostId: } @Composable -private fun SmallScrollOnNewMessage(listState: State, chatItems: State>) { +private fun SmallScrollOnNewMessage(listState: State, chatItems: StateFlow>) { val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() } LaunchedEffect(Unit) { var lastTotalItems = listState.value.layoutInfo.totalItemsCount 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 25661f00a0..6072abfc36 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 @@ -83,7 +83,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea fun getContactsToAdd(chatModel: ChatModel, search: String): List { val s = search.trim().lowercase() - val memberContactIds = chatModel.groupMembers + val memberContactIds = chatModel.groupMembers.value .filter { it.memberCurrent } .mapNotNull { it.memberContactId } return chatModel.chats.value 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 c92ac2ddc3..dd1f10c042 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 @@ -64,7 +64,7 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel) sendReceipts.value = sendRcpts }, - members = chatModel.groupMembers + members = chatModel.groupMembers.value .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } .sortedByDescending { it.memberRole }, developerTools, 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 2f0311b087..a4e857f188 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 @@ -230,7 +230,7 @@ suspend fun apiFindMessages(ch: Chat, search: String) { suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { val groupMembers = chatModel.controller.apiListMembers(rhId, groupInfo.groupId) - val currentMembers = chatModel.groupMembers + val currentMembers = chatModel.groupMembers.value val newMembers = groupMembers.map { newMember -> val currentMember = currentMembers.find { it.id == newMember.id } val currentMemberStats = currentMember?.activeConn?.connectionStats @@ -241,9 +241,8 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo newMember } } - chatModel.groupMembers.clear() - chatModel.groupMembersIndexes.clear() - chatModel.groupMembers.addAll(newMembers) + chatModel.groupMembersIndexes.value = emptyMap() + chatModel.groupMembers.value = newMembers chatModel.populateGroupMembersIndexes() } 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 4648ac5037..d368e746c2 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 @@ -448,7 +448,7 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow } } } else { - val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } + val users by remember { derivedStateOf { chatModel.users.value.filter { u -> u.user.activeUser || !u.user.hidden } } } val allRead = users .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } @@ -544,7 +544,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U } } if (appPlatform.isDesktop) { - val h by remember { chatModel.currentRemoteHost } + val h by chatModel.currentRemoteHost.collectAsState() if (h != null) { Spacer(Modifier.width(12.dp)) HostDisconnectButton { @@ -947,8 +947,8 @@ private fun TagsView() { val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) TagsRow { - if (presetTags.size > 1) { - if (presetTags.size + userTags.value.size <= 3) { + if (presetTags.value.size > 1) { + if (presetTags.value.size + userTags.value.size <= 3) { PresetTagKind.entries.filter { t -> (presetTags[t] ?: 0) > 0 }.forEach { tag -> ExpandedTagFilterView(tag) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt index acbc72ff48..6af68d4813 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -709,7 +709,7 @@ fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableSta } LaunchedEffect(Unit) { - if (chatModel.users.count { u -> u.user.activeUser || !u.user.hidden } == 1 + if (chatModel.users.value.count { u -> u.user.activeUser || !u.user.hidden } == 1 ) { selectedUserCategory.value = PresentedUserCategory.CURRENT_USER } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index aa9847c98a..b13543148a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -94,11 +94,11 @@ private fun ShareListToolbar(chatModel: ChatModel, stopped: Boolean, onSearchVal if (showSearch) { BackHandler(onBack = hideSearchOnBack) } - val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } + val users by remember { derivedStateOf { chatModel.users.value.filter { u -> u.user.activeUser || !u.user.hidden } } } val navButton: @Composable RowScope.() -> Unit = { when { showSearch -> NavigationButtonBack(hideSearchOnBack) - (users.size > 1 || chatModel.remoteHosts.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward -> { + (users.size > 1 || chatModel.remoteHosts.value.isNotEmpty()) && remember { chatModel.sharedContent }.value !is SharedContent.Forward -> { val allRead = users .filter { u -> !u.user.activeUser && !u.user.hidden } .all { u -> u.unreadCount == 0 } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 185ec3925f..ced070d9aa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -55,14 +55,14 @@ fun UserPicker( } val users by remember { derivedStateOf { - chatModel.users + chatModel.users.value .filter { u -> u.user.activeUser || !u.user.hidden } .sortedByDescending { it.user.activeOrder } } } val remoteHosts by remember { derivedStateOf { - chatModel.remoteHosts + chatModel.remoteHosts.value .sortedBy { it.hostDeviceName } } } @@ -111,8 +111,7 @@ fun UserPicker( } } if (!same) { - chatModel.users.clear() - chatModel.users.addAll(updatedUsers) + chatModel.users.value = updatedUsers } } catch (e: Exception) { Log.e(TAG, "Error updating users ${e.stackTraceToString()}") @@ -121,8 +120,7 @@ fun UserPicker( try { val updatedHosts = chatModel.controller.listRemoteHosts()?.sortedBy { it.hostDeviceName } ?: emptyList() if (remoteHosts != updatedHosts) { - chatModel.remoteHosts.clear() - chatModel.remoteHosts.addAll(updatedHosts) + chatModel.remoteHosts.value = updatedHosts } } catch (e: Exception) { Log.e(TAG, "Error updating remote hosts ${e.stackTraceToString()}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt index 9264ca69af..af94e4b367 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt @@ -26,6 +26,8 @@ import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.datetime.Clock import java.io.File import java.nio.file.Files @@ -34,7 +36,7 @@ import kotlin.io.path.Path @Composable fun DatabaseErrorView( - chatDbStatus: State, + chatDbStatus: StateFlow, appPreferences: AppPreferences, ) { val progressIndicator = remember { mutableStateOf(false) } @@ -200,7 +202,7 @@ fun DatabaseErrorView( private fun runChat( dbKey: String? = null, confirmMigrations: MigrationConfirmation? = null, - chatDbStatus: State, + chatDbStatus: StateFlow, progressIndicator: MutableState, ) = CoroutineScope(Dispatchers.Default).launch { // Don't do things concurrently. Shouldn't be here concurrently, just in case @@ -337,7 +339,7 @@ private fun ColumnScope.RestoreDbButton(onClick: () -> Unit) { fun PreviewChatInfoLayout() { SimpleXTheme { DatabaseErrorView( - remember { mutableStateOf(DBMigrationResult.ErrorNotADatabase("simplex_v1_chat.db")) }, + remember { MutableStateFlow(DBMigrationResult.ErrorNotADatabase("simplex_v1_chat.db")) }, AppPreferences() ) } 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 28772f01d3..a005c2d7a5 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 @@ -26,6 +26,7 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.common.platform.* import chat.simplex.res.MR +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.datetime.* import java.io.* import java.net.URI @@ -82,7 +83,7 @@ fun DatabaseView() { appFilesCountAndSize, chatItemTTL, user, - m.users, + m.users.value, startChat = { startChat(m, chatLastStart, m.chatDbChanged, progressIndicator) }, stopChatAlert = { stopChatAlert(m, progressIndicator) }, exportArchive = { @@ -116,7 +117,7 @@ fun DatabaseView() { } }, disconnectAllHosts = { - val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected } + val connected = chatModel.remoteHosts.value.filter { it.sessionState is RemoteHostSessionState.Connected } connected.forEachIndexed { index, h -> controller.stopRemoteHostAndReloadHosts(h, index == connected.lastIndex && chatModel.connectedToRemote()) } @@ -182,7 +183,7 @@ fun DatabaseLayout( ) SectionDividerSpaced(maxTopPadding = true) } - val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected } + val toggleEnabled = chatModel.remoteHosts.collectAsState().value.none { it.sessionState is RemoteHostSessionState.Connected } if (chatModel.localUserCreated.value == true) { // still show the toggle in case database was stopped when the user opened this screen because it can be in the following situations: // - database was stopped after migration and the app relaunched @@ -355,7 +356,7 @@ fun RunChatSetting( fun startChat( m: ChatModel, chatLastStart: MutableState, - chatDbChanged: MutableState, + chatDbChanged: MutableStateFlow, progressIndicator: MutableState? = null ) { withLongRunningApi { @@ -535,7 +536,7 @@ fun deleteChatDatabaseFilesAndState() { popChatCollector.clear() } } - chatModel.users.clear() + chatModel.users.value = emptyList() ntfManager.cancelAllNotifications() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 6bfcf2809f..d6c58715f4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -277,7 +277,7 @@ class AlertManager { } } -private fun alertTitle(title: String): (@Composable () -> Unit)? { +private fun alertTitle(title: String): (@Composable () -> Unit) { return { Text( title, @@ -368,12 +368,12 @@ private fun AlertContent(text: AnnotatedString?, hostDevice: Pair } } -fun hostDevice(rhId: Long?): Pair? = if (rhId == null && chatModel.remoteHosts.isNotEmpty()) { +fun hostDevice(rhId: Long?): Pair? = if (rhId == null && chatModel.remoteHosts.value.isNotEmpty()) { null to ChatModel.controller.appPrefs.deviceNameForRemoteAccess.get()!! } else if (rhId == null) { null } else { - rhId to (chatModel.remoteHosts.firstOrNull { it.remoteHostId == rhId }?.hostDeviceName?.ifEmpty { rhId.toString() } ?: rhId.toString()) + rhId to (chatModel.remoteHosts.value.firstOrNull { it.remoteHostId == rhId }?.hostDeviceName?.ifEmpty { rhId.toString() } ?: rhId.toString()) } @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt index 8588e0e981..89a3befe68 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt @@ -33,6 +33,7 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.datetime.* import kotlinx.serialization.* import java.io.File diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index 1a28bbf589..d6f42f4f68 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -31,6 +31,7 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.datetime.Clock import kotlinx.datetime.toJavaInstant import kotlinx.serialization.* @@ -112,7 +113,7 @@ sealed class MigrationToState { @Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState() } -private var MutableState.state: MigrationToState? +private var MutableStateFlow.state: MigrationToState? get() = value set(v) { value = v } @@ -159,7 +160,7 @@ fun ModalData.MigrateToDeviceView(close: () -> Unit) { @Composable private fun ModalData.MigrateToDeviceLayout( - migrationState: MutableState, + migrationState: MutableStateFlow, chatReceiver: MutableState, close: () -> Unit, ) { @@ -174,7 +175,7 @@ private fun ModalData.MigrateToDeviceLayout( @Composable private fun ModalData.SectionByState( - migrationState: MutableState, + migrationState: MutableStateFlow, tempDatabaseFile: File, chatReceiver: MutableState, close: () -> Unit @@ -196,7 +197,7 @@ private fun ModalData.SectionByState( } @Composable -private fun MutableState.PasteOrScanLinkView(close: () -> Unit) { +private fun MutableStateFlow.PasteOrScanLinkView(close: () -> Unit) { Box { val progressIndicator = remember { mutableStateOf(false) } Column { @@ -224,7 +225,7 @@ private fun MutableState.PasteOrScanLinkView(close: () -> Uni } @Composable -private fun MutableState.PasteLinkView() { +private fun MutableStateFlow.PasteLinkView() { val clipboard = LocalClipboardManager.current SectionItemView({ val str = clipboard.getText()?.text ?: return@SectionItemView @@ -260,7 +261,7 @@ private fun ArchiveImportView(progressIndicator: MutableState, close: ( } @Composable -private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState) { +private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableStateFlow) { val onionHosts = remember { stateGetOrPut("onionHosts") { getNetCfg().copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts } } @@ -323,7 +324,7 @@ private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, lin } @Composable -private fun MutableState.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg, networkProxy: NetworkProxy?) { +private fun MutableStateFlow.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg, networkProxy: NetworkProxy?) { Box { SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {} ProgressView() @@ -334,7 +335,7 @@ private fun MutableState.DatabaseInitView(link: String, tempD } @Composable -private fun MutableState.LinkDownloadingView( +private fun MutableStateFlow.LinkDownloadingView( link: String, ctrl: ChatCtrl, user: User, @@ -364,7 +365,7 @@ private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) { } @Composable -private fun MutableState.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { +private fun MutableStateFlow.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_download), @@ -384,7 +385,7 @@ private fun MutableState.DownloadFailedView(link: String, cha } @Composable -private fun MutableState.ArchiveImportView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { +private fun MutableStateFlow.ArchiveImportView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { Box { SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {} ProgressView() @@ -395,7 +396,7 @@ private fun MutableState.ArchiveImportView(archivePath: Strin } @Composable -private fun MutableState.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { +private fun MutableStateFlow.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) { SettingsActionItemWithContent( icon = painterResource(MR.images.ic_download), @@ -410,7 +411,7 @@ private fun MutableState.ArchiveImportFailedView(archivePath: } @Composable -private fun MutableState.PassphraseEnteringView(currentKey: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { +private fun MutableStateFlow.PassphraseEnteringView(currentKey: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { val currentKey = rememberSaveable { mutableStateOf(currentKey) } val verifyingPassphrase = rememberSaveable { mutableStateOf(false) } val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) } @@ -459,7 +460,7 @@ private fun MutableState.PassphraseEnteringView(currentKey: S } @Composable -private fun MutableState.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?) { +private fun MutableStateFlow.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?) { data class Tuple4(val a: A, val b: B, val c: C, val d: D) val (header: String, button: String?, footer: String, confirmation: MigrationConfirmation?) = when (status) { is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) { @@ -518,7 +519,7 @@ private fun ProgressView() { DefaultProgressView(null) } -private suspend fun MutableState.checkUserLink(link: String): Boolean { +private suspend fun MutableStateFlow.checkUserLink(link: String): Boolean { return if (strHasSimplexFileLink(link.trim())) { val data = MigrationFileLinkData.readFromLink(link) val hasProxyConfigured = data?.networkConfig?.hasProxyConfigured() ?: false @@ -547,7 +548,7 @@ private suspend fun MutableState.checkUserLink(link: String): } } -private fun MutableState.prepareDatabase( +private fun MutableStateFlow.prepareDatabase( link: String, tempDatabaseFile: File, netCfg: NetCfg, @@ -567,7 +568,7 @@ private fun MutableState.prepareDatabase( } } -private fun MutableState.startDownloading( +private fun MutableStateFlow.startDownloading( totalBytes: Long, ctrl: ChatCtrl, user: User, @@ -629,7 +630,7 @@ private fun MutableState.startDownloading( } } -private fun MutableState.importArchive(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { +private fun MutableStateFlow.importArchive(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) { withLongRunningApi { try { if (ChatController.ctrl == null || ChatController.ctrl == -1L) { @@ -706,7 +707,7 @@ private fun hideView(close: () -> Unit) { close() } -private suspend fun MutableState.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) { +private suspend fun MutableStateFlow.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) { val state = state if (state is MigrationToState.ArchiveImportFailed) { // Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state 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 923c0256a8..6eb353dc96 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 @@ -279,11 +279,11 @@ fun ActiveProfilePicker( val incognito = remember { chatModel.showingInvitation.value?.conn?.incognito ?: controller.appPrefs.incognito.get() } - val selectedProfile by remember { chatModel.currentUser } + val selectedProfile by chatModel.currentUser.collectAsState() val searchTextOrPassword = rememberSaveable { search } // Intentionally don't use derivedStateOf in order to NOT change an order after user was selected val filteredProfiles = remember(searchTextOrPassword.value) { - filteredProfiles(chatModel.users.map { it.user }.sortedBy { !it.activeUser }, searchTextOrPassword.value) + filteredProfiles(chatModel.users.value.map { it.user }.sortedBy { !it.activeUser }, searchTextOrPassword.value) } var progressByTimeout by rememberSaveable { mutableStateOf(false) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt index 9e48f4b2bd..0c450005eb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt @@ -57,7 +57,7 @@ private fun LinkAMobileLayout( ModalView({ appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }) { Column(Modifier.fillMaxSize().padding(top = AppBarHeight * fontSizeSqrtMultiplier)) { Box(Modifier.align(Alignment.CenterHorizontally)) { - AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) + AppBarTitle(stringResource(if (chatModel.remoteHosts.collectAsState().value.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) } Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { Column( @@ -75,7 +75,7 @@ private fun LinkAMobileLayout( Box(Modifier.weight(0.7f)) { AddingMobileDevice(false, staleQrCode, connecting) { // currentRemoteHost will be set instantly but remoteHosts may be delayed - if (chatModel.remoteHosts.isEmpty() && chatModel.currentRemoteHost.value == null) { + if (chatModel.remoteHosts.value.isEmpty() && chatModel.currentRemoteHost.value == null) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } else { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index 84f473067f..12441bac34 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -121,7 +121,7 @@ private fun NotificationBatteryUsageInfo() { fun prepareChatBeforeFinishingOnboarding() { // No visible users but may have hidden. In this case chat should be started anyway because it's stopped on this stage with hidden users - if (chatModel.users.any { u -> !u.user.hidden }) return + if (chatModel.users.value.any { u -> !u.user.hidden }) return withBGApi { val user = chatModel.controller.apiGetActiveUser(null) ?: return@withBGApi chatModel.currentUser.value = user diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index 1d01ab11ff..444d2d25cb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -53,9 +53,9 @@ fun ConnectMobileView() { } ConnectMobileLayout( deviceName = remember { deviceName.state }, - remoteHosts = remoteHosts, + remoteHosts = remoteHosts.value, connecting, - connectedHost = remember { chatModel.currentRemoteHost }, + connectedHost = chatModel.currentRemoteHost.collectAsState(), updateDeviceName = { withBGApi { if (it != "") { @@ -71,7 +71,9 @@ fun ConnectMobileView() { withBGApi { val success = controller.deleteRemoteHost(host.remoteHostId) if (success) { - chatModel.remoteHosts.removeAll { it.remoteHostId == host.remoteHostId } + val rHosts = chatModel.remoteHosts.value.toMutableList() + rHosts.removeAll { it.remoteHostId == host.remoteHostId } + chatModel.remoteHosts.value = rHosts } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt index b4fead6692..15f9c0f014 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -890,7 +890,7 @@ object AppearanceScope { // Otherwise, it would be needed to make global variable and to use it everywhere for making a decision to include these overrides into active theme constructing or not chatModel.currentUser.value = chatModel.currentUser.value?.copy(uiThemes = null) } else { - chatModel.updateCurrentUserUiThemes(chatModel.remoteHostId(), chatModel.users.firstOrNull { it.user.userId == chatModel.currentUser.value?.userId }?.user?.uiThemes) + chatModel.updateCurrentUserUiThemes(chatModel.remoteHostId(), chatModel.users.value.firstOrNull { it.user.userId == chatModel.currentUser.value?.userId }?.user?.uiThemes) } } DisposableEffect(Unit) { @@ -898,15 +898,15 @@ object AppearanceScope { // Skip when Appearance screen is not hidden yet if (ModalManager.start.hasModalsOpen()) return@onDispose // Restore user overrides from stored list of users - chatModel.updateCurrentUserUiThemes(chatModel.remoteHostId(), chatModel.users.firstOrNull { it.user.userId == chatModel.currentUser.value?.userId }?.user?.uiThemes) + chatModel.updateCurrentUserUiThemes(chatModel.remoteHostId(), chatModel.users.value.firstOrNull { it.user.userId == chatModel.currentUser.value?.userId }?.user?.uiThemes) themeUserDestination.value = if (chatModel.currentUser.value?.uiThemes == null) null else chatModel.currentUser.value?.userId!! to chatModel.currentUser.value?.uiThemes } } - val values by remember(chatModel.users.toList()) { mutableStateOf( + val values by remember(chatModel.users.value) { mutableStateOf( listOf(null as Long? to generalGetString(MR.strings.theme_destination_app_theme)) + - chatModel.users.filter { it.user.activeUser }.map { + chatModel.users.value.filter { it.user.activeUser }.map { it.user.userId to it.user.chatViewName }, ) @@ -921,7 +921,7 @@ object AppearanceScope { onSelected = { userId -> themeUserDest.value = userId if (userId != null) { - themeUserDestination.value = userId to chatModel.users.firstOrNull { it.user.userId == userId }?.user?.uiThemes + themeUserDestination.value = userId to chatModel.users.value.firstOrNull { it.user.userId == userId }?.user?.uiThemes } else { themeUserDestination.value = null } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index 5af5d5fb90..c677092d61 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt @@ -31,12 +31,12 @@ fun NotificationsSettingsView( NotificationsSettingsLayout( notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state }, - notificationPreviewMode = chatModel.notificationPreviewMode, + notificationPreviewMode = chatModel.notificationPreviewMode.collectAsState(), showPage = { page -> ModalManager.start.showModalCloseable(true) { when (page) { CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) } - CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected) + CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode.collectAsState(), onNotificationPreviewModeSelected) } } }, 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 9ec2d29843..2162f369ed 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 @@ -81,7 +81,7 @@ fun PrivacySettingsView( chatModel.draftChatId.value = null } }) - SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { + SimpleXLinkOptions(chatModel.simplexLinkMode.collectAsState(), onSelected = { simplexLinkMode.set(it) chatModel.simplexLinkMode.value = it }) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt index ef4acdeac6..4fada5f16b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt @@ -13,6 +13,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.size import chat.simplex.common.platform.* import chat.simplex.res.MR import chat.simplex.common.ui.theme.DEFAULT_PADDING @@ -32,8 +33,7 @@ fun SetDeliveryReceiptsView(m: ChatModel) { m.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) try { val users = m.controller.listUsers(currentUser.remoteHostId) - m.users.clear() - m.users.addAll(users) + m.users.value = users } catch (e: Exception) { Log.e(TAG, "listUsers error: ${e.stackTraceToString()}") } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index ad732cd699..9abd1fb96b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.* @Composable fun UserProfilesView(m: ChatModel, search: MutableState, profileHidden: MutableState, withAuth: (block: () -> Unit) -> Unit) { val searchTextOrPassword = rememberSaveable { search } - val users by remember { derivedStateOf { m.users.map { it.user } } } + val users by remember { derivedStateOf { m.users.value.map { it.user } } } val filteredUsers by remember { derivedStateOf { filteredUsers(m, searchTextOrPassword.value) } } UserProfilesLayout( users = users, @@ -306,7 +306,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: ( fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List { val s = searchTextOrPassword.trim() val lower = s.lowercase() - return m.users.filter { u -> + return m.users.value.filter { u -> if ((u.user.activeUser || !u.user.hidden) && (s == "" || u.user.anyNameContains(lower))) { true } else { @@ -315,7 +315,7 @@ fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List { } } -private fun visibleUsersCount(m: ChatModel): Int = m.users.filter { u -> !u.user.hidden }.size +private fun visibleUsersCount(m: ChatModel): Int = m.users.value.filter { u -> !u.user.hidden }.size fun correctPassword(user: User, pwd: String): Boolean { val ph = user.viewPwdHash diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt index fc042cc46c..224971e765 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/AdvancedNetworkSettings.kt @@ -32,7 +32,7 @@ import java.text.DecimalFormat @Composable fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> Unit, close: () -> Unit) { - val currentRemoteHost by remember { chatModel.currentRemoteHost } + val currentRemoteHost by chatModel.currentRemoteHost.collectAsState() val developerTools = remember { appPrefs.developerTools.get() } // Will be actual once the screen is re-opened diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt index 835e01ec27..017d4a0d0c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/networkAndServers/NetworkAndServers.kt @@ -43,7 +43,7 @@ import kotlinx.coroutines.* @Composable fun ModalData.NetworkAndServersView(closeNetworkAndServers: () -> Unit) { - val currentRemoteHost by remember { chatModel.currentRemoteHost } + val currentRemoteHost by chatModel.currentRemoteHost.collectAsState() // It's not a state, just a one-time value. Shouldn't be used in any state-related situations val netCfg = remember { chatModel.controller.getNetCfg() } val networkUseSocksProxy: MutableState = remember { mutableStateOf(netCfg.useSocksProxy) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt index e3b0642547..c0b0c04b76 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -97,7 +97,7 @@ actual fun ActiveCallView() { is WCallCommand.Camera -> { chatModel.activeCall.value = call.copy(localCamera = cmd.camera) if (!call.localMediaSources.mic) { - chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false)) + chatModel.callCommand.value += WCallCommand.Media(CallMediaSource.Mic, enable = false) } } is WCallCommand.End -> @@ -106,11 +106,11 @@ actual fun ActiveCallView() { } is WCallResponse.Error -> { when (apiMsg.command) { - is WCallCommand.Capabilities -> chatModel.callCommand.add(WCallCommand.Permission( + is WCallCommand.Capabilities -> chatModel.callCommand.value += WCallCommand.Permission( title = generalGetString(MR.strings.call_desktop_permission_denied_title), chrome = generalGetString(MR.strings.call_desktop_permission_denied_chrome), safari = generalGetString(MR.strings.call_desktop_permission_denied_safari) - )) + ) else -> {} } Log.e(TAG, "ActiveCallView: command error ${r.message}") @@ -123,11 +123,13 @@ actual fun ActiveCallView() { DisposableEffect(Unit) { chatModel.activeCallViewIsVisible.value = true // After the first call, End command gets added to the list which prevents making another calls - chatModel.callCommand.removeAll { it is WCallCommand.End } + val cmds = chatModel.callCommand.value.toMutableList() + cmds.removeAll { it is WCallCommand.End } + chatModel.callCommand.value = cmds onDispose { CallSoundsPlayer.stop() chatModel.activeCallViewIsVisible.value = false - chatModel.callCommand.clear() + chatModel.callCommand.value = emptyList() } } } @@ -143,13 +145,13 @@ private fun SendStateUpdates() { val connInfo = call.connectionInfo val connInfoText = if (connInfo == null) "" else " (${connInfo.text})" val description = call.encryptionStatus + connInfoText - chatModel.callCommand.add(WCallCommand.Description(state, description)) + chatModel.callCommand.value += WCallCommand.Description(state, description) } } } @Composable -fun WebRTCController(callCommand: SnapshotStateList, onResponse: (WVAPIMessage) -> Unit) { +fun WebRTCController(callCommand: MutableStateFlow>, onResponse: (WVAPIMessage) -> Unit) { val uriHandler = LocalUriHandler.current val endCall = { val call = chatModel.activeCall.value @@ -187,15 +189,17 @@ fun WebRTCController(callCommand: SnapshotStateList, onResponse: ( } } LaunchedEffect(Unit) { - snapshotFlow { callCommand.firstOrNull() } + snapshotFlow { callCommand.value.firstOrNull() } .distinctUntilChanged() .filterNotNull() .collect { while (connections.isEmpty()) { delay(100) } - while (callCommand.isNotEmpty()) { - val cmd = callCommand.removeFirst() + while (callCommand.value.isNotEmpty()) { + val cmds = callCommand.value.toMutableList() + val cmd = cmds.removeFirst() + callCommand.value = cmds Log.d(TAG, "WebRTCController LaunchedEffect executing $cmd") processCommand(cmd) }