This commit is contained in:
Avently
2025-01-01 21:31:15 +07:00
parent e27f8a8d6a
commit 7e76fa43b6
32 changed files with 288 additions and 232 deletions
@@ -292,7 +292,7 @@ fun AndroidWrapInCallLayout(content: @Composable () -> Unit) {
@Composable
fun AndroidScreen(userPickerState: MutableStateFlow<AnimatedViewState>) {
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
@@ -48,71 +48,71 @@ import kotlin.time.*
@Stable
object ChatModel {
val controller: ChatController = ChatController
val setDeliveryReceipts = mutableStateOf(false)
val currentUser = mutableStateOf<User?>(null)
val users = mutableStateListOf<UserInfo>()
val localUserCreated = mutableStateOf<Boolean?>(null)
val chatRunning = mutableStateOf<Boolean?>(null)
val chatDbChanged = mutableStateOf<Boolean>(false)
val chatDbEncrypted = mutableStateOf<Boolean?>(false)
val chatDbStatus = mutableStateOf<DBMigrationResult?>(null)
val ctrlInitInProgress = mutableStateOf(false)
val dbMigrationInProgress = mutableStateOf(false)
val incompleteInitializedDbRemoved = mutableStateOf(false)
private val _chats = mutableStateOf(SnapshotStateList<Chat>())
val chats: State<List<Chat>> = _chats
val setDeliveryReceipts = MutableStateFlow(false)
val currentUser = MutableStateFlow<User?>(null)
val users = MutableStateFlow<List<UserInfo>>(emptyList())
val localUserCreated = MutableStateFlow<Boolean?>(null)
val chatRunning = MutableStateFlow<Boolean?>(null)
val chatDbChanged = MutableStateFlow<Boolean>(false)
val chatDbEncrypted = MutableStateFlow<Boolean?>(false)
val chatDbStatus = MutableStateFlow<DBMigrationResult?>(null)
val ctrlInitInProgress = MutableStateFlow(false)
val dbMigrationInProgress = MutableStateFlow(false)
val incompleteInitializedDbRemoved = MutableStateFlow(false)
private val _chats = MutableStateFlow(SnapshotStateList<Chat>())
val chats: StateFlow<List<Chat>> = _chats
private val chatsContext = ChatsContext()
// map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
val switchingUsersAndHosts = mutableStateOf(false)
val networkStatuses = MutableStateFlow<Map<String, NetworkStatus>>(emptyMap())
val switchingUsersAndHosts = MutableStateFlow(false)
// current chat
val chatId = mutableStateOf<String?>(null)
val chatId = MutableStateFlow<String?>(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<ChatItem>())
val chatItems = MutableStateFlow(SnapshotStateList<ChatItem>())
// 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<List<Pair<Long?, String>>>(emptyList())
val deletedChats = MutableStateFlow<List<Pair<Long?, String>>>(emptyList())
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
val groupMembers = mutableStateListOf<GroupMember>()
val groupMembersIndexes = mutableStateMapOf<Long, Int>()
val groupMembers = MutableStateFlow<List<GroupMember>>(emptyList())
val groupMembersIndexes = MutableStateFlow<Map<Long, Int>>(emptyMap())
// Chat Tags
val userTags = mutableStateOf(emptyList<ChatTag>())
val activeChatTagFilter = mutableStateOf<ActiveFilter?>(null)
val presetTags = mutableStateMapOf<PresetTagKind, Int>()
val unreadTags = mutableStateMapOf<Long, Int>()
val userTags = MutableStateFlow(emptyList<ChatTag>())
val activeChatTagFilter = MutableStateFlow<ActiveFilter?>(null)
val presetTags = MutableStateFlow<Map<PresetTagKind, Int>>(emptyMap())
val unreadTags = MutableStateFlow<Map<Long, Int>>(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<Boolean>()
val terminalItems = mutableStateOf<List<TerminalItem>>(listOf())
val userAddress = mutableStateOf<UserContactLinkRec?>(null)
val chatItemTTL = mutableStateOf<ChatItemTTL>(ChatItemTTL.None)
val terminalItems = MutableStateFlow<List<TerminalItem>>(emptyList())
val userAddress = MutableStateFlow<UserContactLinkRec?>(null)
val chatItemTTL = MutableStateFlow<ChatItemTTL>(ChatItemTTL.None)
// set when app opened from external intent
val clearOverlays = mutableStateOf<Boolean>(false)
val clearOverlays = MutableStateFlow<Boolean>(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<Pair<Long?, String>?>(null)
val appOpenUrl = MutableStateFlow<Pair<Long?, String>?>(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<String, RcvCallInvitation>()
val activeCallInvitation = mutableStateOf<RcvCallInvitation?>(null)
val activeCall = mutableStateOf<Call?>(null)
val activeCallViewIsVisible = mutableStateOf<Boolean>(false)
val activeCallViewIsCollapsed = mutableStateOf<Boolean>(false)
val callCommand = mutableStateListOf<WCallCommand>()
val showCallView = mutableStateOf(false)
val switchingCall = mutableStateOf(false)
val callInvitations = MutableStateFlow<Map<String, RcvCallInvitation>>(emptyMap())
val activeCallInvitation = MutableStateFlow<RcvCallInvitation?>(null)
val activeCall = MutableStateFlow<Call?>(null)
val activeCallViewIsVisible = MutableStateFlow<Boolean>(false)
val activeCallViewIsCollapsed = MutableStateFlow<Boolean>(false)
val callCommand = MutableStateFlow<List<WCallCommand>>(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<MigrationToState?> by lazy { mutableStateOf(MigrationToDeviceState.makeMigrationState()) }
val migrationState: MutableStateFlow<MigrationToState?> 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<File>()
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<RemoteHostInfo>()
val currentRemoteHost = mutableStateOf<RemoteHostInfo?>(null)
val remoteHosts = MutableStateFlow<List<RemoteHostInfo>>(emptyList())
val currentRemoteHost = MutableStateFlow<RemoteHostInfo?>(null)
val remoteHostId: Long? @Composable get() = remember { currentRemoteHost }.value?.remoteHostId
fun remoteHostId(): Long? = currentRemoteHost.value?.remoteHostId
val remoteHostPairing = mutableStateOf<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null)
val remoteCtrlSession = mutableStateOf<RemoteCtrlSession?>(null)
val remoteHostPairing = MutableStateFlow<Pair<RemoteHostInfo?, RemoteHostSessionState>?>(null)
val remoteCtrlSession = MutableStateFlow<RemoteCtrlSession?>(null)
val processedCriticalError: ProcessedErrors<AgentErrorType.CRITICAL> = ProcessedErrors(60_000)
val processedInternalError: ProcessedErrors<AgentErrorType.INTERNAL> = 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<Long>?, newTags: List<Long>) {
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<Long>) {
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<Long, Int>()
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<SnapshotStateList<Chat>>.add(index: Int, elem: Chat) {
fun MutableStateFlow<SnapshotStateList<Chat>>.add(index: Int, elem: Chat) {
value = SnapshotStateList<Chat>().apply { addAll(value); add(index, elem) }
}
fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(index: Int, elem: ChatItem) {
fun MutableStateFlow<SnapshotStateList<ChatItem>>.addAndNotify(index: Int, elem: ChatItem) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) }
}
fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) {
fun MutableStateFlow<SnapshotStateList<Chat>>.add(elem: Chat) {
value = SnapshotStateList<Chat>().apply { addAll(value); add(elem) }
}
@@ -2504,24 +2521,24 @@ fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) {
fun <T> MutableList<T>.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<SnapshotStateList<ChatItem>>.addAndNotify(elem: ChatItem) {
fun MutableStateFlow<SnapshotStateList<ChatItem>>.addAndNotify(elem: ChatItem) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) }
}
fun <T> MutableState<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) {
fun <T> MutableStateFlow<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) {
value = SnapshotStateList<T>().apply { addAll(value); addAll(index, elems) }
}
fun <T> MutableState<SnapshotStateList<T>>.addAll(elems: List<T>) {
fun <T> MutableStateFlow<SnapshotStateList<T>>.addAll(elems: List<T>) {
value = SnapshotStateList<T>().apply { addAll(value); addAll(elems) }
}
fun MutableState<SnapshotStateList<Chat>>.removeAll(block: (Chat) -> Boolean) {
fun MutableStateFlow<SnapshotStateList<Chat>>.removeAll(block: (Chat) -> Boolean) {
value = SnapshotStateList<Chat>().apply { addAll(value); removeAll(block) }
}
// Removes item(s) from chatItems and notifies a listener about removed item(s)
fun MutableState<SnapshotStateList<ChatItem>>.removeAllAndNotify(block: (ChatItem) -> Boolean) {
fun MutableStateFlow<SnapshotStateList<ChatItem>>.removeAllAndNotify(block: (ChatItem) -> Boolean) {
val toRemove = ArrayList<Triple<Long, Int, Boolean>>()
value = SnapshotStateList<ChatItem>().apply {
addAll(value)
@@ -2538,7 +2555,7 @@ fun MutableState<SnapshotStateList<ChatItem>>.removeAllAndNotify(block: (ChatIte
}
}
fun MutableState<SnapshotStateList<Chat>>.removeAt(index: Int): Chat {
fun MutableStateFlow<SnapshotStateList<Chat>>.removeAt(index: Int): Chat {
val new = SnapshotStateList<Chat>()
new.addAll(value)
val res = new.removeAt(index)
@@ -2546,7 +2563,14 @@ fun MutableState<SnapshotStateList<Chat>>.removeAt(index: Int): Chat {
return res
}
fun MutableState<SnapshotStateList<ChatItem>>.removeLastAndNotify() {
fun <T> MutableStateFlow<List<T>>.removeAt(index: Int): T {
val l = value.toMutableList()
val removed = l.removeAt(index)
value = l
return removed
}
fun MutableStateFlow<SnapshotStateList<ChatItem>>.removeLastAndNotify() {
val removed: Triple<Long, Int, Boolean>
value = SnapshotStateList<ChatItem>().apply {
addAll(value)
@@ -2557,29 +2581,53 @@ fun MutableState<SnapshotStateList<ChatItem>>.removeLastAndNotify() {
chatItemsChangesListener?.removed(listOf(removed), value)
}
fun <T> MutableState<SnapshotStateList<T>>.replaceAll(elems: List<T>) {
fun <T> MutableStateFlow<SnapshotStateList<T>>.replaceAll(elems: List<T>) {
value = SnapshotStateList<T>().apply { addAll(elems) }
}
fun MutableState<SnapshotStateList<Chat>>.clear() {
fun MutableStateFlow<SnapshotStateList<Chat>>.clear() {
value = SnapshotStateList()
}
// Removes all chatItems and notifies a listener about it
fun MutableState<SnapshotStateList<ChatItem>>.clearAndNotify() {
fun MutableStateFlow<SnapshotStateList<ChatItem>>.clearAndNotify() {
value = SnapshotStateList()
chatItemsChangesListener?.cleared()
}
fun <T> State<SnapshotStateList<T>>.asReversed(): MutableList<T> = value.asReversed()
fun <T> StateFlow<SnapshotStateList<T>>.asReversed(): MutableList<T> = value.asReversed()
fun <T> State<SnapshotStateList<T>>.toList(): List<T> = value.toList()
fun <T> StateFlow<SnapshotStateList<T>>.toList(): List<T> = value.toList()
operator fun <T> State<SnapshotStateList<T>>.get(i: Int): T = value[i]
operator fun <K, V> StateFlow<Map<K, V>>.get(key: K): V? = value[key]
operator fun <T> State<SnapshotStateList<T>>.set(index: Int, elem: T) { value[index] = elem }
operator fun <T> StateFlow<List<T>>.get(i: Int): T = value[i]
operator fun <T> StateFlow<SnapshotStateList<T>>.set(index: Int, elem: T) { value[index] = elem }
operator fun <T> MutableStateFlow<List<T>>.set(index: Int, elem: T) {
val l = value.toMutableList()
l[index] = elem
value = l
}
fun StateFlow<List<Any>>.isEmpty(): Boolean = value.isEmpty()
operator fun <K, V> MutableStateFlow<Map<K, V>>.set(key: K, elem: V) {
val m = value.toMutableMap()
m[key] = elem
value = m
}
fun <K, V> MutableStateFlow<Map<K, V>>.remove(key: K): V? {
val m = value.toMutableMap()
val removed = m.remove(key)
value = m
return removed
}
val State<List<Any>>.size: Int get() = value.size
val StateFlow<List<Any>>.size: Int get() = value.size
enum class CIMergeCategory {
MemberConnected,
@@ -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<ContactRef>, 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)
}
@@ -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
@@ -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 {
@@ -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
@@ -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
@@ -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<String?>, onComposed: suspend (chatId: String) -> Unit) {
fun ChatView(staleChatId: StateFlow<String?>, 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<String?>, 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<String?>, onComposed: suspend (chatId: String) -
if (chatInfo is ChatInfo.Direct) {
var contactInfo: Pair<ConnectionStats?, Profile?>? 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<String?>, 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<String?>, 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<Boolean>, remoteHostId:
}
@Composable
private fun SmallScrollOnNewMessage(listState: State<LazyListState>, chatItems: State<List<ChatItem>>) {
private fun SmallScrollOnNewMessage(listState: State<LazyListState>, chatItems: StateFlow<List<ChatItem>>) {
val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() }
LaunchedEffect(Unit) {
var lastTotalItems = listState.value.layoutInfo.totalItemsCount
@@ -83,7 +83,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
val s = search.trim().lowercase()
val memberContactIds = chatModel.groupMembers
val memberContactIds = chatModel.groupMembers.value
.filter { it.memberCurrent }
.mapNotNull { it.memberContactId }
return chatModel.chats.value
@@ -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,
@@ -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()
}
@@ -448,7 +448,7 @@ private fun ChatListToolbar(userPickerState: MutableStateFlow<AnimatedViewState>
}
}
} 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)
}
@@ -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 {
@@ -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 }
@@ -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()}")
@@ -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<DBMigrationResult?>,
chatDbStatus: StateFlow<DBMigrationResult?>,
appPreferences: AppPreferences,
) {
val progressIndicator = remember { mutableStateOf(false) }
@@ -200,7 +202,7 @@ fun DatabaseErrorView(
private fun runChat(
dbKey: String? = null,
confirmMigrations: MigrationConfirmation? = null,
chatDbStatus: State<DBMigrationResult?>,
chatDbStatus: StateFlow<DBMigrationResult?>,
progressIndicator: MutableState<Boolean>,
) = 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()
)
}
@@ -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<Instant?>,
chatDbChanged: MutableState<Boolean>,
chatDbChanged: MutableStateFlow<Boolean>,
progressIndicator: MutableState<Boolean>? = null
) {
withLongRunningApi {
@@ -535,7 +536,7 @@ fun deleteChatDatabaseFilesAndState() {
popChatCollector.clear()
}
}
chatModel.users.clear()
chatModel.users.value = emptyList()
ntfManager.cancelAllNotifications()
}
@@ -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<Long?, String>
}
}
fun hostDevice(rhId: Long?): Pair<Long?, String>? = if (rhId == null && chatModel.remoteHosts.isNotEmpty()) {
fun hostDevice(rhId: Long?): Pair<Long?, String>? = 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
@@ -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
@@ -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<MigrationToState?>.state: MigrationToState?
private var MutableStateFlow<MigrationToState?>.state: MigrationToState?
get() = value
set(v) { value = v }
@@ -159,7 +160,7 @@ fun ModalData.MigrateToDeviceView(close: () -> Unit) {
@Composable
private fun ModalData.MigrateToDeviceLayout(
migrationState: MutableState<MigrationToState?>,
migrationState: MutableStateFlow<MigrationToState?>,
chatReceiver: MutableState<MigrationToChatReceiver?>,
close: () -> Unit,
) {
@@ -174,7 +175,7 @@ private fun ModalData.MigrateToDeviceLayout(
@Composable
private fun ModalData.SectionByState(
migrationState: MutableState<MigrationToState?>,
migrationState: MutableStateFlow<MigrationToState?>,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationToChatReceiver?>,
close: () -> Unit
@@ -196,7 +197,7 @@ private fun ModalData.SectionByState(
}
@Composable
private fun MutableState<MigrationToState?>.PasteOrScanLinkView(close: () -> Unit) {
private fun MutableStateFlow<MigrationToState?>.PasteOrScanLinkView(close: () -> Unit) {
Box {
val progressIndicator = remember { mutableStateOf(false) }
Column {
@@ -224,7 +225,7 @@ private fun MutableState<MigrationToState?>.PasteOrScanLinkView(close: () -> Uni
}
@Composable
private fun MutableState<MigrationToState?>.PasteLinkView() {
private fun MutableStateFlow<MigrationToState?>.PasteLinkView() {
val clipboard = LocalClipboardManager.current
SectionItemView({
val str = clipboard.getText()?.text ?: return@SectionItemView
@@ -260,7 +261,7 @@ private fun ArchiveImportView(progressIndicator: MutableState<Boolean>, close: (
}
@Composable
private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableStateFlow<MigrationToState?>) {
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<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg, networkProxy: NetworkProxy?) {
private fun MutableStateFlow<MigrationToState?>.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<MigrationToState?>.DatabaseInitView(link: String, tempD
}
@Composable
private fun MutableState<MigrationToState?>.LinkDownloadingView(
private fun MutableStateFlow<MigrationToState?>.LinkDownloadingView(
link: String,
ctrl: ChatCtrl,
user: User,
@@ -364,7 +365,7 @@ private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) {
}
@Composable
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
private fun MutableStateFlow<MigrationToState?>.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<MigrationToState?>.DownloadFailedView(link: String, cha
}
@Composable
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
private fun MutableStateFlow<MigrationToState?>.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<MigrationToState?>.ArchiveImportView(archivePath: Strin
}
@Composable
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
private fun MutableStateFlow<MigrationToState?>.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<MigrationToState?>.ArchiveImportFailedView(archivePath:
}
@Composable
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
private fun MutableStateFlow<MigrationToState?>.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<MigrationToState?>.PassphraseEnteringView(currentKey: S
}
@Composable
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?) {
private fun MutableStateFlow<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?) {
data class Tuple4<A,B,C,D>(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<MigrationToState?>.checkUserLink(link: String): Boolean {
private suspend fun MutableStateFlow<MigrationToState?>.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<MigrationToState?>.checkUserLink(link: String):
}
}
private fun MutableState<MigrationToState?>.prepareDatabase(
private fun MutableStateFlow<MigrationToState?>.prepareDatabase(
link: String,
tempDatabaseFile: File,
netCfg: NetCfg,
@@ -567,7 +568,7 @@ private fun MutableState<MigrationToState?>.prepareDatabase(
}
}
private fun MutableState<MigrationToState?>.startDownloading(
private fun MutableStateFlow<MigrationToState?>.startDownloading(
totalBytes: Long,
ctrl: ChatCtrl,
user: User,
@@ -629,7 +630,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
}
}
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
private fun MutableStateFlow<MigrationToState?>.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<MigrationToState?>.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) {
private suspend fun MutableStateFlow<MigrationToState?>.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
@@ -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) }
@@ -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)
@@ -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
@@ -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
}
}
}
@@ -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
}
@@ -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)
}
}
},
@@ -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
})
@@ -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()}")
}
@@ -38,7 +38,7 @@ import kotlinx.coroutines.*
@Composable
fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: MutableState<Boolean>, 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<UserInfo> {
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<UserInfo> {
}
}
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
@@ -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
@@ -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<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
@@ -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<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
fun WebRTCController(callCommand: MutableStateFlow<List<WCallCommand>>, onResponse: (WVAPIMessage) -> Unit) {
val uriHandler = LocalUriHandler.current
val endCall = {
val call = chatModel.activeCall.value
@@ -187,15 +189,17 @@ fun WebRTCController(callCommand: SnapshotStateList<WCallCommand>, 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)
}