android, desktop: refactoring to use mutex when updating chats (#4541)

* moving to mutablestate + snapshotstatelist from snapshotstatelist

* android, desktop: refactoring to use mutex when updating chats

* wrapped into class instead of object

* fix

---------

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