kotlin: refactor chat contexts 1 (remove functions creating indirection) (#5827)

* kotlin: refactor chat contexts 1

* remove withChats

* comment

* remove withReportChatsIfOpen

* remove comment

* fix desktop
This commit is contained in:
spaced4ndy 2025-04-14 16:01:22 +00:00 committed by GitHub
parent 14d9240995
commit 38c2529d8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 598 additions and 580 deletions

View file

@ -24,7 +24,6 @@ import chat.simplex.app.views.call.CallActivity
import chat.simplex.common.helpers.* import chat.simplex.common.helpers.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
@ -33,7 +32,6 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.OnboardingStage
import com.jakewharton.processphoenix.ProcessPhoenix import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.map
import java.io.* import java.io.*
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -94,7 +92,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
Lifecycle.Event.ON_START -> { Lifecycle.Event.ON_START -> {
isAppOnForeground = true isAppOnForeground = true
if (chatModel.chatRunning.value == true) { if (chatModel.chatRunning.value == true) {
withChats { withContext(Dispatchers.Main) {
kotlin.runCatching { kotlin.runCatching {
val currentUserId = chatModel.currentUser.value?.userId val currentUserId = chatModel.currentUser.value?.userId
val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId())) val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId()))
@ -107,7 +105,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
/** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */ /** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats) if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
} }
updateChats(chats) chatModel.chatsContext.updateChats(chats)
} }
}.onFailure { Log.e(TAG, it.stackTraceToString()) } }.onFailure { Log.e(TAG, it.stackTraceToString()) }
} }

View file

@ -14,12 +14,11 @@ import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import chat.simplex.common.AppScreen import chat.simplex.common.AppScreen
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.clear
import chat.simplex.common.model.clearAndNotify import chat.simplex.common.model.clearAndNotify
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import androidx.compose.ui.platform.LocalContext as LocalContext1 import androidx.compose.ui.platform.LocalContext as LocalContext1
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.*
actual fun showToast(text: String, timeout: Long) = Toast.makeText(androidAppContext, text, Toast.LENGTH_SHORT).show() actual fun showToast(text: String, timeout: Long) = Toast.makeText(androidAppContext, text, Toast.LENGTH_SHORT).show()
@ -76,13 +75,13 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
ModalManager.start.closeModal() ModalManager.start.closeModal()
} else if (chatModel.chatId.value != null) { } else if (chatModel.chatId.value != null) {
withApi { withApi {
withChats { withContext(Dispatchers.Main) {
// Since no modals are open, the problem is probably in ChatView // Since no modals are open, the problem is probably in ChatView
chatModel.chatId.value = null chatModel.chatId.value = null
chatItems.clearAndNotify() chatModel.chatsContext.chatItems.clearAndNotify()
} }
withChats { withContext(Dispatchers.Main) {
chatItems.clearAndNotify() chatModel.chatsContext.chatItems.clearAndNotify()
} }
} }
} else { } else {

View file

@ -67,7 +67,7 @@ object ChatModel {
val chatId = mutableStateOf<String?>(null) val chatId = mutableStateOf<String?>(null)
val openAroundItemId: MutableState<Long?> = mutableStateOf(null) val openAroundItemId: MutableState<Long?> = mutableStateOf(null)
val chatsContext = ChatsContext(null) val chatsContext = ChatsContext(null)
val reportsChatsContext = ChatsContext(MsgContentTag.Report) val secondaryChatsContext = ChatsContext(MsgContentTag.Report)
// declaration of chatsContext should be before any other variable that is taken from ChatsContext class and used in the model, otherwise, strange crash with NullPointerException for "this" parameter in random functions // declaration of chatsContext should be before any other variable that is taken from ChatsContext class and used in the model, otherwise, strange crash with NullPointerException for "this" parameter in random functions
val chats: State<List<Chat>> = chatsContext.chats val chats: State<List<Chat>> = chatsContext.chats
// rhId, chatId // rhId, chatId
@ -170,36 +170,6 @@ object ChatModel {
// return true if you handled the click // return true if you handled the click
var centerPanelBackgroundClickHandler: (() -> Boolean)? = null var centerPanelBackgroundClickHandler: (() -> Boolean)? = null
fun chatsForContent(contentTag: MsgContentTag?): State<SnapshotStateList<Chat>> = when(contentTag) {
null -> chatsContext.chats
MsgContentTag.Report -> reportsChatsContext.chats
else -> TODO()
}
fun chatItemsForContent(contentTag: MsgContentTag?): State<SnapshotStateList<ChatItem>> = when(contentTag) {
null -> chatsContext.chatItems
MsgContentTag.Report -> reportsChatsContext.chatItems
else -> TODO()
}
fun chatStateForContent(contentTag: MsgContentTag?): ActiveChatState = when(contentTag) {
null -> chatsContext.chatState
MsgContentTag.Report -> reportsChatsContext.chatState
else -> TODO()
}
fun chatItemsChangesListenerForContent(contentTag: MsgContentTag?): ChatItemsChangesListener? = when(contentTag) {
null -> chatsContext.chatItemsChangesListener
MsgContentTag.Report -> reportsChatsContext.chatItemsChangesListener
else -> TODO()
}
fun setChatItemsChangeListenerForContent(listener: ChatItemsChangesListener?, contentTag: MsgContentTag?) = when(contentTag) {
null -> chatsContext.chatItemsChangesListener = listener
MsgContentTag.Report -> reportsChatsContext.chatItemsChangesListener = listener
else -> TODO()
}
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value currentUser.value
} else { } else {
@ -324,21 +294,6 @@ object ChatModel {
} }
} }
// running everything inside the block on main thread. Make sure any heavy computation is moved to a background thread
suspend fun <T> withChats(contentTag: MsgContentTag? = null, action: suspend ChatsContext.() -> T): T = withContext(Dispatchers.Main) {
when {
contentTag == null -> chatsContext.action()
contentTag == MsgContentTag.Report -> reportsChatsContext.action()
else -> TODO()
}
}
suspend fun <T> withReportsChatsIfOpen(action: suspend ChatsContext.() -> T) = withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) {
reportsChatsContext.action()
}
}
class ChatsContext(private val contentTag: MsgContentTag?) { class ChatsContext(private val contentTag: MsgContentTag?) {
val chats = mutableStateOf(SnapshotStateList<Chat>()) val chats = mutableStateOf(SnapshotStateList<Chat>())
/** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on. /** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on.
@ -659,8 +614,9 @@ object ChatModel {
subject subject
.throttleLatest(2000) .throttleLatest(2000)
.collect { .collect {
withChats(contentTag) { withContext(Dispatchers.Main) {
chats.replaceAll(popCollectedChats()) val chatsCtx = if (contentTag == null) chatsContext else secondaryChatsContext
chatsCtx.chats.replaceAll(popCollectedChats())
} }
} }
} }
@ -960,17 +916,17 @@ object ChatModel {
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem { suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct) val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
withChats { withContext(Dispatchers.Main) {
chatItems.addAndNotify(cItem, contentTag = null) chatsContext.chatItems.addAndNotify(cItem, contentTag = null)
} }
return cItem return cItem
} }
fun removeLiveDummy() { fun removeLiveDummy() {
if (chatItemsForContent(null).value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { if (chatsContext.chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
withApi { withApi {
withChats { withContext(Dispatchers.Main) {
chatItems.removeLastAndNotify(contentTag = null) chatsContext.chatItems.removeLastAndNotify(contentTag = null)
} }
} }
} }
@ -1042,9 +998,11 @@ object ChatModel {
fun replaceConnReqView(id: String, withId: String) { fun replaceConnReqView(id: String, withId: String) {
if (id == showingInvitation.value?.connId) { if (id == showingInvitation.value?.connId) {
withApi { withApi {
withChats { withContext(Dispatchers.Main) {
showingInvitation.value = null showingInvitation.value = null
chatItems.clearAndNotify() // TODO [contexts] - why does clearAndNotify operates with listeners for both contexts?
// TODO - should it be called for both contexts here instead?
chatsContext.chatItems.clearAndNotify()
chatModel.chatId.value = withId chatModel.chatId.value = withId
} }
} }
@ -1055,9 +1013,10 @@ object ChatModel {
fun dismissConnReqView(id: String) = withApi { fun dismissConnReqView(id: String) = withApi {
if (id == showingInvitation.value?.connId) { if (id == showingInvitation.value?.connId) {
withChats { withContext(Dispatchers.Main) {
showingInvitation.value = null showingInvitation.value = null
chatItems.clearAndNotify() // TODO [contexts] see replaceConnReqView
chatsContext.chatItems.clearAndNotify()
chatModel.chatId.value = null chatModel.chatId.value = null
} }
// Close NewChatView // Close NewChatView
@ -2739,7 +2698,8 @@ fun MutableState<SnapshotStateList<Chat>>.add(index: Int, elem: Chat) {
} }
fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(index: Int, elem: ChatItem, contentTag: MsgContentTag?) { fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(index: Int, elem: ChatItem, contentTag: MsgContentTag?) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, elem); chatModel.chatItemsChangesListenerForContent(contentTag)?.added(elem.id to elem.isRcvNew, index) } val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(index, elem); chatsCtx.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) }
} }
fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) { fun MutableState<SnapshotStateList<Chat>>.add(elem: Chat) {
@ -2751,7 +2711,8 @@ fun <T> MutableList<T>.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmp
// Adds item to chatItems and notifies a listener about newly added item // Adds item to chatItems and notifies a listener about newly added item
fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(elem: ChatItem, contentTag: MsgContentTag?) { fun MutableState<SnapshotStateList<ChatItem>>.addAndNotify(elem: ChatItem, contentTag: MsgContentTag?) {
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(elem); chatModel.chatItemsChangesListenerForContent(contentTag)?.added(elem.id to elem.isRcvNew, lastIndex) } val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
value = SnapshotStateList<ChatItem>().apply { addAll(value); add(elem); chatsCtx.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) }
} }
fun <T> MutableState<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) { fun <T> MutableState<SnapshotStateList<T>>.addAll(index: Int, elems: List<T>) {
@ -2781,7 +2742,7 @@ fun MutableState<SnapshotStateList<ChatItem>>.removeAllAndNotify(block: (ChatIte
} }
if (toRemove.isNotEmpty()) { if (toRemove.isNotEmpty()) {
chatModel.chatsContext.chatItemsChangesListener?.removed(toRemove, value) chatModel.chatsContext.chatItemsChangesListener?.removed(toRemove, value)
chatModel.reportsChatsContext.chatItemsChangesListener?.removed(toRemove, value) chatModel.secondaryChatsContext.chatItemsChangesListener?.removed(toRemove, value)
} }
} }
@ -2801,7 +2762,8 @@ fun MutableState<SnapshotStateList<ChatItem>>.removeLastAndNotify(contentTag: Ms
val rem = removeLast() val rem = removeLast()
removed = Triple(rem.id, remIndex, rem.isRcvNew) removed = Triple(rem.id, remIndex, rem.isRcvNew)
} }
chatModel.chatItemsChangesListenerForContent(contentTag)?.removed(listOf(removed), value) val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
chatsCtx.chatItemsChangesListener?.removed(listOf(removed), value)
} }
fun <T> MutableState<SnapshotStateList<T>>.replaceAll(elems: List<T>) { fun <T> MutableState<SnapshotStateList<T>>.replaceAll(elems: List<T>) {
@ -2816,7 +2778,7 @@ fun MutableState<SnapshotStateList<Chat>>.clear() {
fun MutableState<SnapshotStateList<ChatItem>>.clearAndNotify() { fun MutableState<SnapshotStateList<ChatItem>>.clearAndNotify() {
value = SnapshotStateList() value = SnapshotStateList()
chatModel.chatsContext.chatItemsChangesListener?.cleared() chatModel.chatsContext.chatItemsChangesListener?.cleared()
chatModel.reportsChatsContext.chatItemsChangesListener?.cleared() chatModel.secondaryChatsContext.chatItemsChangesListener?.cleared()
} }
fun <T> State<SnapshotStateList<T>>.asReversed(): MutableList<T> = value.asReversed() fun <T> State<SnapshotStateList<T>>.asReversed(): MutableList<T> = value.asReversed()

View file

@ -17,8 +17,6 @@ import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.getNetCfg
import chat.simplex.common.model.ChatController.setNetCfg import chat.simplex.common.model.ChatController.setNetCfg
import chat.simplex.common.model.ChatModel.changingActiveUserMutex import chat.simplex.common.model.ChatModel.changingActiveUserMutex
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.model.MsgContent.MCUnknown import chat.simplex.common.model.MsgContent.MCUnknown
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
@ -544,9 +542,9 @@ object ChatController {
} }
Log.d(TAG, "startChat: started") Log.d(TAG, "startChat: started")
} else { } else {
withChats { withContext(Dispatchers.Main) {
val chats = apiGetChats(null) val chats = apiGetChats(null)
updateChats(chats) chatModel.chatsContext.updateChats(chats)
} }
Log.d(TAG, "startChat: running") Log.d(TAG, "startChat: running")
} }
@ -627,9 +625,9 @@ object ChatController {
val hasUser = chatModel.currentUser.value != null val hasUser = chatModel.currentUser.value != null
chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null
chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None
withChats { withContext(Dispatchers.Main) {
val chats = apiGetChats(rhId) val chats = apiGetChats(rhId)
updateChats(chats) chatModel.chatsContext.updateChats(chats)
} }
chatModel.userTags.value = apiGetChatTags(rhId).takeIf { hasUser } ?: emptyList() chatModel.userTags.value = apiGetChatTags(rhId).takeIf { hasUser } ?: emptyList()
chatModel.activeChatTagFilter.value = null chatModel.activeChatTagFilter.value = null
@ -1490,8 +1488,8 @@ object ChatController {
suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) { suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) { if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) {
withChats { withContext(Dispatchers.Main) {
removeChat(chat.remoteHostId, cInfo.id) chatModel.chatsContext.removeChat(chat.remoteHostId, cInfo.id)
} }
} }
} }
@ -1539,11 +1537,11 @@ object ChatController {
withBGApi { withBGApi {
val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId) val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId)
if (updatedChatInfo != null) { if (updatedChatInfo != null) {
withChats { withContext(Dispatchers.Main) {
clearChat(chat.remoteHostId, updatedChatInfo) chatModel.chatsContext.clearChat(chat.remoteHostId, updatedChatInfo)
} }
withChats(MsgContentTag.Report) { withContext(Dispatchers.Main) {
clearChat(chat.remoteHostId, updatedChatInfo) chatModel.secondaryChatsContext.clearChat(chat.remoteHostId, updatedChatInfo)
} }
ntfManager.cancelNotificationsForChat(chat.chatInfo.id) ntfManager.cancelNotificationsForChat(chat.chatInfo.id)
close?.invoke() close?.invoke()
@ -1975,12 +1973,14 @@ object ChatController {
val r = sendCmd(rh, CC.ApiJoinGroup(groupId)) val r = sendCmd(rh, CC.ApiJoinGroup(groupId))
when (r) { when (r) {
is CR.UserAcceptedGroupSent -> is CR.UserAcceptedGroupSent ->
withChats { withContext(Dispatchers.Main) {
updateGroup(rh, r.groupInfo) chatModel.chatsContext.updateGroup(rh, r.groupInfo)
} }
is CR.ChatCmdError -> { is CR.ChatCmdError -> {
val e = r.chatError val e = r.chatError
suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { withChats { removeChat(rh, "#$groupId") } } } suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) {
withContext(Dispatchers.Main) { chatModel.chatsContext.removeChat(rh, "#$groupId") } }
}
if (e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH) { if (e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH) {
deleteGroup() deleteGroup()
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.alert_title_group_invitation_expired), generalGetString(MR.strings.alert_message_group_invitation_expired)) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.alert_title_group_invitation_expired), generalGetString(MR.strings.alert_message_group_invitation_expired))
@ -2134,8 +2134,8 @@ object ChatController {
val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param) val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param)
val toContact = apiSetContactPrefs(rh, contact.contactId, prefs) val toContact = apiSetContactPrefs(rh, contact.contactId, prefs)
if (toContact != null) { if (toContact != null) {
withChats { withContext(Dispatchers.Main) {
updateContact(rh, toContact) chatModel.chatsContext.updateContact(rh, toContact)
} }
} }
} }
@ -2406,19 +2406,19 @@ object ChatController {
when (r) { when (r) {
is CR.ContactDeletedByContact -> { is CR.ContactDeletedByContact -> {
if (active(r.user) && r.contact.directOrUsed) { if (active(r.user) && r.contact.directOrUsed) {
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, r.contact) chatModel.chatsContext.updateContact(rhId, r.contact)
} }
} }
} }
is CR.ContactConnected -> { is CR.ContactConnected -> {
if (active(r.user) && r.contact.directOrUsed) { if (active(r.user) && r.contact.directOrUsed) {
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, r.contact) chatModel.chatsContext.updateContact(rhId, r.contact)
val conn = r.contact.activeConn val conn = r.contact.activeConn
if (conn != null) { if (conn != null) {
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
removeChat(rhId, conn.id) chatModel.chatsContext.removeChat(rhId, conn.id)
} }
} }
} }
@ -2429,24 +2429,24 @@ object ChatController {
} }
is CR.ContactConnecting -> { is CR.ContactConnecting -> {
if (active(r.user) && r.contact.directOrUsed) { if (active(r.user) && r.contact.directOrUsed) {
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, r.contact) chatModel.chatsContext.updateContact(rhId, r.contact)
val conn = r.contact.activeConn val conn = r.contact.activeConn
if (conn != null) { if (conn != null) {
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
removeChat(rhId, conn.id) chatModel.chatsContext.removeChat(rhId, conn.id)
} }
} }
} }
} }
is CR.ContactSndReady -> { is CR.ContactSndReady -> {
if (active(r.user) && r.contact.directOrUsed) { if (active(r.user) && r.contact.directOrUsed) {
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, r.contact) chatModel.chatsContext.updateContact(rhId, r.contact)
val conn = r.contact.activeConn val conn = r.contact.activeConn
if (conn != null) { if (conn != null) {
chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}")
removeChat(rhId, conn.id) chatModel.chatsContext.removeChat(rhId, conn.id)
} }
} }
} }
@ -2456,11 +2456,11 @@ object ChatController {
val contactRequest = r.contactRequest val contactRequest = r.contactRequest
val cInfo = ChatInfo.ContactRequest(contactRequest) val cInfo = ChatInfo.ContactRequest(contactRequest)
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
if (hasChat(rhId, contactRequest.id)) { if (chatModel.chatsContext.hasChat(rhId, contactRequest.id)) {
updateChatInfo(rhId, cInfo) chatModel.chatsContext.updateChatInfo(rhId, cInfo)
} else { } else {
addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf()))
} }
} }
} }
@ -2469,18 +2469,20 @@ object ChatController {
is CR.ContactUpdated -> { is CR.ContactUpdated -> {
if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.toContact.id)) { if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.toContact.id)) {
val cInfo = ChatInfo.Direct(r.toContact) val cInfo = ChatInfo.Direct(r.toContact)
withChats { withContext(Dispatchers.Main) {
updateChatInfo(rhId, cInfo) chatModel.chatsContext.updateChatInfo(rhId, cInfo)
} }
} }
} }
is CR.GroupMemberUpdated -> { is CR.GroupMemberUpdated -> {
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.toMember) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.toMember)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.toMember) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, r.toMember)
}
} }
} }
} }
@ -2489,8 +2491,8 @@ object ChatController {
if (chatModel.chatId.value == r.mergedContact.id) { if (chatModel.chatId.value == r.mergedContact.id) {
chatModel.chatId.value = r.intoContact.id chatModel.chatId.value = r.intoContact.id
} }
withChats { withContext(Dispatchers.Main) {
removeChat(rhId, r.mergedContact.id) chatModel.chatsContext.removeChat(rhId, r.mergedContact.id)
} }
} }
} }
@ -2501,8 +2503,8 @@ object ChatController {
is CR.ContactSubSummary -> { is CR.ContactSubSummary -> {
for (sub in r.contactSubscriptions) { for (sub in r.contactSubscriptions) {
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, sub.contact) chatModel.chatsContext.updateContact(rhId, sub.contact)
} }
} }
val err = sub.contactError val err = sub.contactError
@ -2528,20 +2530,22 @@ object ChatController {
val cInfo = chatItem.chatInfo val cInfo = chatItem.chatInfo
val cItem = chatItem.chatItem val cItem = chatItem.chatItem
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
addChatItem(rhId, cInfo, cItem) chatModel.chatsContext.addChatItem(rhId, cInfo, cItem)
if (cItem.isActiveReport) { if (cItem.isActiveReport) {
increaseGroupReportsCounter(rhId, cInfo.id) chatModel.chatsContext.increaseGroupReportsCounter(rhId, cInfo.id)
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
if (cItem.isReport) { if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
addChatItem(rhId, cInfo, cItem) if (cItem.isReport) {
chatModel.secondaryChatsContext.addChatItem(rhId, cInfo, cItem)
}
} }
} }
} else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) { } else if (cItem.isRcvNew && cInfo.ntfsEnabled(cItem)) {
withChats { withContext(Dispatchers.Main) {
increaseUnreadCounter(rhId, r.user) chatModel.chatsContext.increaseUnreadCounter(rhId, r.user)
} }
} }
val file = cItem.file val file = cItem.file
@ -2562,12 +2566,14 @@ object ChatController {
val cInfo = chatItem.chatInfo val cInfo = chatItem.chatInfo
val cItem = chatItem.chatItem val cItem = chatItem.chatItem
if (!cItem.isDeletedContent && active(r.user)) { if (!cItem.isDeletedContent && active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) chatModel.chatsContext.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
if (cItem.isReport) { if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) if (cItem.isReport) {
chatModel.secondaryChatsContext.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus)
}
} }
} }
} }
@ -2576,12 +2582,14 @@ object ChatController {
chatItemUpdateNotify(rhId, r.user, r.chatItem) chatItemUpdateNotify(rhId, r.user, r.chatItem)
is CR.ChatItemReaction -> { is CR.ChatItemReaction -> {
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) chatModel.chatsContext.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
if (r.reaction.chatReaction.chatItem.isReport) { if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) if (r.reaction.chatReaction.chatItem.isReport) {
chatModel.secondaryChatsContext.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem)
}
} }
} }
} }
@ -2590,8 +2598,8 @@ object ChatController {
if (!active(r.user)) { if (!active(r.user)) {
r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) -> r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) ->
if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled(deletedChatItem.chatItem)) { if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled(deletedChatItem.chatItem)) {
withChats { withContext(Dispatchers.Main) {
decreaseUnreadCounter(rhId, r.user) chatModel.chatsContext.decreaseUnreadCounter(rhId, r.user)
} }
} }
} }
@ -2614,22 +2622,24 @@ object ChatController {
generalGetString(if (toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description) generalGetString(if (toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description)
) )
} }
withChats { withContext(Dispatchers.Main) {
if (toChatItem == null) { if (toChatItem == null) {
removeChatItem(rhId, cInfo, cItem) chatModel.chatsContext.removeChatItem(rhId, cInfo, cItem)
} else { } else {
upsertChatItem(rhId, cInfo, toChatItem.chatItem) chatModel.chatsContext.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
} }
if (cItem.isActiveReport) { if (cItem.isActiveReport) {
decreaseGroupReportsCounter(rhId, cInfo.id) chatModel.chatsContext.decreaseGroupReportsCounter(rhId, cInfo.id)
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
if (cItem.isReport) { if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (toChatItem == null) { if (cItem.isReport) {
removeChatItem(rhId, cInfo, cItem) if (toChatItem == null) {
} else { chatModel.secondaryChatsContext.removeChatItem(rhId, cInfo, cItem)
upsertChatItem(rhId, cInfo, toChatItem.chatItem) } else {
chatModel.secondaryChatsContext.upsertChatItem(rhId, cInfo, toChatItem.chatItem)
}
} }
} }
} }
@ -2640,9 +2650,9 @@ object ChatController {
} }
is CR.ReceivedGroupInvitation -> { is CR.ReceivedGroupInvitation -> {
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
// update so that repeat group invitations are not duplicated // update so that repeat group invitations are not duplicated
updateGroup(rhId, r.groupInfo) chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
} }
// TODO NtfManager.shared.notifyGroupInvitation // TODO NtfManager.shared.notifyGroupInvitation
} }
@ -2650,137 +2660,149 @@ object ChatController {
is CR.UserAcceptedGroupSent -> { is CR.UserAcceptedGroupSent -> {
if (!active(r.user)) return if (!active(r.user)) return
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, r.groupInfo) chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
val conn = r.hostContact?.activeConn val conn = r.hostContact?.activeConn
if (conn != null) { if (conn != null) {
chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}") chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}")
removeChat(rhId, conn.id) chatModel.chatsContext.removeChat(rhId, conn.id)
} }
} }
} }
is CR.GroupLinkConnecting -> { is CR.GroupLinkConnecting -> {
if (!active(r.user)) return if (!active(r.user)) return
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, r.groupInfo) chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
val hostConn = r.hostMember.activeConn val hostConn = r.hostMember.activeConn
if (hostConn != null) { if (hostConn != null) {
chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}") chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}")
removeChat(rhId, hostConn.id) chatModel.chatsContext.removeChat(rhId, hostConn.id)
} }
} }
} }
is CR.BusinessLinkConnecting -> { is CR.BusinessLinkConnecting -> {
if (!active(r.user)) return if (!active(r.user)) return
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, r.groupInfo) chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
} }
if (chatModel.chatId.value == r.fromContact.id) { if (chatModel.chatId.value == r.fromContact.id) {
openGroupChat(rhId, r.groupInfo.groupId) openGroupChat(rhId, r.groupInfo.groupId)
} }
withChats { withContext(Dispatchers.Main) {
removeChat(rhId, r.fromContact.id) chatModel.chatsContext.removeChat(rhId, r.fromContact.id)
} }
} }
is CR.JoinedGroupMemberConnecting -> is CR.JoinedGroupMemberConnecting ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.member) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
} }
} }
is CR.DeletedMemberUser -> // TODO update user member is CR.DeletedMemberUser -> // TODO update user member
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, r.groupInfo) chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
if (r.withMessages) { if (r.withMessages) {
removeMemberItems(rhId, r.groupInfo.membership, byMember = r.member, r.groupInfo) chatModel.chatsContext.removeMemberItems(rhId, r.groupInfo.membership, byMember = r.member, r.groupInfo)
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
if (r.withMessages) { if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
removeMemberItems(rhId, r.groupInfo.membership, byMember = r.member, r.groupInfo) if (r.withMessages) {
chatModel.secondaryChatsContext.removeMemberItems(rhId, r.groupInfo.membership, byMember = r.member, r.groupInfo)
}
} }
} }
} }
is CR.DeletedMember -> is CR.DeletedMember ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.deletedMember) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
if (r.withMessages) { if (r.withMessages) {
removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo) chatModel.chatsContext.removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo)
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.deletedMember) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (r.withMessages) { chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, r.deletedMember)
removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo) if (r.withMessages) {
chatModel.secondaryChatsContext.removeMemberItems(rhId, r.deletedMember, byMember = r.byMember, r.groupInfo)
}
} }
} }
} }
is CR.LeftMember -> is CR.LeftMember ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.member) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.member) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
} }
} }
is CR.MemberRole -> is CR.MemberRole ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.member) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.member) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
} }
} }
is CR.MembersRoleUser -> is CR.MembersRoleUser ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
r.members.forEach { member -> r.members.forEach { member ->
upsertGroupMember(rhId, r.groupInfo, member) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, member)
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
r.members.forEach { member -> if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
upsertGroupMember(rhId, r.groupInfo, member) r.members.forEach { member ->
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, member)
}
} }
} }
} }
is CR.MemberBlockedForAll -> is CR.MemberBlockedForAll ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.member) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.member) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
}
} }
} }
is CR.GroupDeleted -> // TODO update user member is CR.GroupDeleted -> // TODO update user member
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, r.groupInfo) chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
} }
} }
is CR.UserJoinedGroup -> is CR.UserJoinedGroup ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, r.groupInfo) chatModel.chatsContext.updateGroup(rhId, r.groupInfo)
} }
} }
is CR.JoinedGroupMember -> is CR.JoinedGroupMember ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.member) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
} }
} }
is CR.ConnectedToGroupMember -> { is CR.ConnectedToGroupMember -> {
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, r.groupInfo, r.member) chatModel.chatsContext.upsertGroupMember(rhId, r.groupInfo, r.member)
} }
} }
if (r.memberContact != null) { if (r.memberContact != null) {
@ -2789,14 +2811,14 @@ object ChatController {
} }
is CR.GroupUpdated -> is CR.GroupUpdated ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, r.toGroup) chatModel.chatsContext.updateGroup(rhId, r.toGroup)
} }
} }
is CR.NewMemberContactReceivedInv -> is CR.NewMemberContactReceivedInv ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, r.contact) chatModel.chatsContext.updateContact(rhId, r.contact)
} }
} }
is CR.RcvFileStart -> is CR.RcvFileStart ->
@ -2897,26 +2919,26 @@ object ChatController {
} }
is CR.ContactSwitch -> is CR.ContactSwitch ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) chatModel.chatsContext.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats)
} }
} }
is CR.GroupMemberSwitch -> is CR.GroupMemberSwitch ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats)
} }
} }
is CR.ContactRatchetSync -> is CR.ContactRatchetSync ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) chatModel.chatsContext.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats)
} }
} }
is CR.GroupMemberRatchetSync -> is CR.GroupMemberRatchetSync ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats)
} }
} }
is CR.RemoteHostSessionCode -> { is CR.RemoteHostSessionCode -> {
@ -2930,8 +2952,8 @@ object ChatController {
} }
is CR.ContactDisabled -> { is CR.ContactDisabled -> {
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, r.contact) chatModel.chatsContext.updateContact(rhId, r.contact)
} }
} }
} }
@ -3055,8 +3077,8 @@ object ChatController {
} }
is CR.ContactPQEnabled -> is CR.ContactPQEnabled ->
if (active(r.user)) { if (active(r.user)) {
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, r.contact) chatModel.chatsContext.updateContact(rhId, r.contact)
} }
} }
is CR.ChatRespError -> when { is CR.ChatRespError -> when {
@ -3120,8 +3142,8 @@ object ChatController {
suspend fun leaveGroup(rh: Long?, groupId: Long) { suspend fun leaveGroup(rh: Long?, groupId: Long) {
val groupInfo = apiLeaveGroup(rh, groupId) val groupInfo = apiLeaveGroup(rh, groupId)
if (groupInfo != null) { if (groupInfo != null) {
withChats { withContext(Dispatchers.Main) {
updateGroup(rh, groupInfo) chatModel.chatsContext.updateGroup(rh, groupInfo)
} }
} }
} }
@ -3130,10 +3152,12 @@ object ChatController {
if (activeUser(rh, user)) { if (activeUser(rh, user)) {
val cInfo = aChatItem.chatInfo val cInfo = aChatItem.chatInfo
val cItem = aChatItem.chatItem val cItem = aChatItem.chatItem
withChats { upsertChatItem(rh, cInfo, cItem) } withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
if (cItem.isReport) { if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
upsertChatItem(rh, cInfo, cItem) if (cItem.isReport) {
chatModel.secondaryChatsContext.upsertChatItem(rh, cInfo, cItem)
}
} }
} }
} }
@ -3147,15 +3171,16 @@ object ChatController {
return return
} }
val cInfo = ChatInfo.Group(r.groupInfo) val cInfo = ChatInfo.Group(r.groupInfo)
withChats { withContext(Dispatchers.Main) {
val chatsCtx = chatModel.chatsContext
r.chatItemIDs.forEach { itemId -> r.chatItemIDs.forEach { itemId ->
decreaseGroupReportsCounter(rhId, cInfo.id) chatsCtx.decreaseGroupReportsCounter(rhId, cInfo.id)
val cItem = chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach val cItem = chatsCtx.chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach
if (chatModel.chatId.value != null) { if (chatModel.chatId.value != null) {
// Stop voice playback only inside a chat, allow to play in a chat list // Stop voice playback only inside a chat, allow to play in a chat list
AudioPlayer.stop(cItem) AudioPlayer.stop(cItem)
} }
val isLastChatItem = getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id val isLastChatItem = chatsCtx.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id
if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) { if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) {
ntfManager.cancelNotificationsForChat(cInfo.id) ntfManager.cancelNotificationsForChat(cInfo.id)
ntfManager.displayNotification( ntfManager.displayNotification(
@ -3170,22 +3195,25 @@ object ChatController {
} else { } else {
CIDeleted.Deleted(Clock.System.now()) CIDeleted.Deleted(Clock.System.now())
} }
upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted))) chatsCtx.upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted)))
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
r.chatItemIDs.forEach { itemId -> if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
val cItem = chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach val chatsCtx = chatModel.secondaryChatsContext
if (chatModel.chatId.value != null) { r.chatItemIDs.forEach { itemId ->
// Stop voice playback only inside a chat, allow to play in a chat list val cItem = chatsCtx.chatItems.value.lastOrNull { it.id == itemId } ?: return@forEach
AudioPlayer.stop(cItem) if (chatModel.chatId.value != null) {
// Stop voice playback only inside a chat, allow to play in a chat list
AudioPlayer.stop(cItem)
}
val deleted = if (r.member_ != null && (cItem.chatDir as CIDirection.GroupRcv?)?.groupMember?.groupMemberId != r.member_.groupMemberId) {
CIDeleted.Moderated(Clock.System.now(), r.member_)
} else {
CIDeleted.Deleted(Clock.System.now())
}
chatsCtx.upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted)))
} }
val deleted = if (r.member_ != null && (cItem.chatDir as CIDirection.GroupRcv?)?.groupMember?.groupMemberId != r.member_.groupMemberId) {
CIDeleted.Moderated(Clock.System.now(), r.member_)
} else {
CIDeleted.Deleted(Clock.System.now())
}
upsertChatItem(rhId, cInfo, cItem.copy(meta = cItem.meta.copy(itemDeleted = deleted)))
} }
} }
} }
@ -3197,8 +3225,14 @@ object ChatController {
if (!activeUser(rh, user)) { if (!activeUser(rh, user)) {
notify() notify()
} else { } else {
val createdChat = withChats { upsertChatItem(rh, cInfo, cItem) } val createdChat = withContext(Dispatchers.Main) { chatModel.chatsContext.upsertChatItem(rh, cInfo, cItem) }
withReportsChatsIfOpen { if (cItem.content.msgContent is MsgContent.MCReport) { upsertChatItem(rh, cInfo, cItem) } } withContext(Dispatchers.Main) {
if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (cItem.content.msgContent is MsgContent.MCReport) {
chatModel.secondaryChatsContext.upsertChatItem(rh, cInfo, cItem)
}
}
}
if (createdChat) { if (createdChat) {
notify() notify()
} else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) { } else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) {
@ -3243,15 +3277,17 @@ object ChatController {
chatModel.users.addAll(users) chatModel.users.addAll(users)
chatModel.currentUser.value = user chatModel.currentUser.value = user
if (user == null) { if (user == null) {
withChats { withContext(Dispatchers.Main) {
chatItems.clearAndNotify() chatModel.chatsContext.chatItems.clearAndNotify()
chats.clear() chatModel.chatsContext.chats.clear()
popChatCollector.clear() chatModel.chatsContext.popChatCollector.clear()
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
chatItems.clearAndNotify() if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chats.clear() chatModel.secondaryChatsContext.chatItems.clearAndNotify()
popChatCollector.clear() chatModel.secondaryChatsContext.chats.clear()
chatModel.secondaryChatsContext.popChatCollector.clear()
}
} }
} }
val statuses = apiGetNetworkStatuses(rhId) val statuses = apiGetNetworkStatuses(rhId)

View file

@ -19,7 +19,6 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
@ -37,17 +36,16 @@ import androidx.compose.ui.unit.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.group.ChatTTLSection import chat.simplex.common.views.chat.group.ChatTTLSection
import chat.simplex.common.views.chat.group.ProgressIndicator
import chat.simplex.common.views.chatlist.updateChatSettings import chat.simplex.common.views.chatlist.updateChatSettings
import chat.simplex.common.views.database.* import chat.simplex.common.views.database.*
import chat.simplex.common.views.newchat.* import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -126,8 +124,8 @@ fun ChatInfoView(
val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId) val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId)
connStats.value = cStats connStats.value = cStats
if (cStats != null) { if (cStats != null) {
withChats { withContext(Dispatchers.Main) {
updateContactConnectionStats(chatRh, contact, cStats) chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, cStats)
} }
} }
close.invoke() close.invoke()
@ -140,8 +138,8 @@ fun ChatInfoView(
val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId) val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId)
connStats.value = cStats connStats.value = cStats
if (cStats != null) { if (cStats != null) {
withChats { withContext(Dispatchers.Main) {
updateContactConnectionStats(chatRh, contact, cStats) chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, cStats)
} }
} }
} }
@ -171,8 +169,8 @@ fun ChatInfoView(
verify = { code -> verify = { code ->
chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r -> chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r ->
val (verified, existingCode) = r val (verified, existingCode) = r
withChats { withContext(Dispatchers.Main) {
updateContact( chatModel.chatsContext.updateContact(
chatRh, chatRh,
ct.copy( ct.copy(
activeConn = ct.activeConn?.copy( activeConn = ct.activeConn?.copy(
@ -200,8 +198,8 @@ suspend fun syncContactConnection(rhId: Long?, contact: Contact, connectionStats
val cStats = chatModel.controller.apiSyncContactRatchet(rhId, contact.contactId, force = force) val cStats = chatModel.controller.apiSyncContactRatchet(rhId, contact.contactId, force = force)
connectionStats.value = cStats connectionStats.value = cStats
if (cStats != null) { if (cStats != null) {
withChats { withContext(Dispatchers.Main) {
updateContactConnectionStats(rhId, contact, cStats) chatModel.chatsContext.updateContactConnectionStats(rhId, contact, cStats)
} }
} }
} }
@ -475,14 +473,14 @@ fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDe
val chatRh = chat.remoteHostId val chatRh = chat.remoteHostId
val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode) val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode)
if (ct != null) { if (ct != null) {
withChats { withContext(Dispatchers.Main) {
when (chatDeleteMode) { when (chatDeleteMode) {
is ChatDeleteMode.Full -> is ChatDeleteMode.Full ->
removeChat(chatRh, chatInfo.id) chatModel.chatsContext.removeChat(chatRh, chatInfo.id)
is ChatDeleteMode.Entity -> is ChatDeleteMode.Entity ->
updateContact(chatRh, ct) chatModel.chatsContext.updateContact(chatRh, ct)
is ChatDeleteMode.Messages -> is ChatDeleteMode.Messages ->
clearChat(chatRh, ChatInfo.Direct(ct)) chatModel.chatsContext.clearChat(chatRh, ChatInfo.Direct(ct))
} }
} }
if (chatModel.chatId.value == chatInfo.id) { if (chatModel.chatId.value == chatInfo.id) {
@ -1270,11 +1268,11 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c
wallpaperFilesToDelete.forEach(::removeWallpaperFile) wallpaperFilesToDelete.forEach(::removeWallpaperFile)
if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) { if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) {
withChats { withContext(Dispatchers.Main) {
if (chat.chatInfo is ChatInfo.Direct) { if (chat.chatInfo is ChatInfo.Direct) {
updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) chatModel.chatsContext.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes)))
} else if (chat.chatInfo is ChatInfo.Group) { } else if (chat.chatInfo is ChatInfo.Group) {
updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) chatModel.chatsContext.updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes)))
} }
} }
} }
@ -1283,8 +1281,8 @@ suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, c
private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi { private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi {
val chatRh = chat.remoteHostId val chatRh = chat.remoteHostId
chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
withChats { withContext(Dispatchers.Main) {
updateContact(chatRh, it) chatModel.chatsContext.updateContact(chatRh, it)
} }
} }
} }
@ -1386,10 +1384,10 @@ private suspend fun afterSetChatTTL(rhId: Long?, chatInfo: ChatInfo, progressInd
val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, null, pagination) ?: return val (chat, navInfo) = controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId, null, pagination) ?: return
if (chat.chatItems.isEmpty()) { if (chat.chatItems.isEmpty()) {
// replacing old chat with the same old chat but without items. Less intrusive way of clearing a preview // replacing old chat with the same old chat but without items. Less intrusive way of clearing a preview
withChats { withContext(Dispatchers.Main) {
val oldChat = getChat(chat.id) val oldChat = chatModel.chatsContext.getChat(chat.id)
if (oldChat != null) { if (oldChat != null) {
replaceChat(oldChat.remoteHostId, oldChat.id, oldChat.copy(chatItems = emptyList())) chatModel.chatsContext.replaceChat(oldChat.remoteHostId, oldChat.id, oldChat.copy(chatItems = emptyList()))
} }
} }
} }

View file

@ -2,7 +2,6 @@ package chat.simplex.common.views.chat
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.chatModel import chat.simplex.common.platform.chatModel
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -48,29 +47,30 @@ suspend fun processLoadedChat(
openAroundItemId: Long?, openAroundItemId: Long?,
visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 } visibleItemIndexesNonReversed: () -> IntRange = { 0 .. 0 }
) { ) {
val chatState = chatModel.chatStateForContent(contentTag) val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val chatState = chatsCtx.chatState
val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState
val oldItems = chatModel.chatItemsForContent(contentTag).value val oldItems = chatsCtx.chatItems.value
val newItems = SnapshotStateList<ChatItem>() val newItems = SnapshotStateList<ChatItem>()
when (pagination) { when (pagination) {
is ChatPagination.Initial -> { is ChatPagination.Initial -> {
val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList() val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList()
if (contentTag == null) { if (contentTag == null) {
// update main chats, not content tagged // update main chats, not content tagged
withChats { withContext(Dispatchers.Main) {
val oldChat = getChat(chat.id) val oldChat = chatModel.chatsContext.getChat(chat.id)
if (oldChat == null) { if (oldChat == null) {
addChat(chat) chatModel.chatsContext.addChat(chat)
} else { } else {
updateChatInfo(chat.remoteHostId, chat.chatInfo) chatModel.chatsContext.updateChatInfo(chat.remoteHostId, chat.chatInfo)
// unreadChat is currently not actual in getChat query (always false) // unreadChat is currently not actual in getChat query (always false)
updateChatStats(chat.remoteHostId, chat.id, chat.chatStats.copy(unreadChat = oldChat.chatStats.unreadChat)) chatModel.chatsContext.updateChatStats(chat.remoteHostId, chat.id, chat.chatStats.copy(unreadChat = oldChat.chatStats.unreadChat))
} }
} }
} }
withChats(contentTag) { withContext(Dispatchers.Main) {
chatItemStatuses.clear() chatsCtx.chatItemStatuses.clear()
chatItems.replaceAll(chat.chatItems) chatsCtx.chatItems.replaceAll(chat.chatItems)
chatModel.chatId.value = chat.id chatModel.chatId.value = chat.id
splits.value = newSplits splits.value = newSplits
if (chat.chatItems.isNotEmpty()) { if (chat.chatItems.isNotEmpty()) {
@ -93,8 +93,8 @@ suspend fun processLoadedChat(
) )
val insertAt = (indexInCurrentItems - (wasSize - newItems.size) + trimmedIds.size).coerceAtLeast(0) val insertAt = (indexInCurrentItems - (wasSize - newItems.size) + trimmedIds.size).coerceAtLeast(0)
newItems.addAll(insertAt, chat.chatItems) newItems.addAll(insertAt, chat.chatItems)
withChats(contentTag) { withContext(Dispatchers.Main) {
chatItems.replaceAll(newItems) chatsCtx.chatItems.replaceAll(newItems)
splits.value = newSplits splits.value = newSplits
chatState.moveUnreadAfterItem(oldUnreadSplitIndex, newUnreadSplitIndex, oldItems) chatState.moveUnreadAfterItem(oldUnreadSplitIndex, newUnreadSplitIndex, oldItems)
} }
@ -112,8 +112,8 @@ suspend fun processLoadedChat(
val indexToAdd = min(indexInCurrentItems + 1, newItems.size) val indexToAdd = min(indexInCurrentItems + 1, newItems.size)
val indexToAddIsLast = indexToAdd == newItems.size val indexToAddIsLast = indexToAdd == newItems.size
newItems.addAll(indexToAdd, chat.chatItems) newItems.addAll(indexToAdd, chat.chatItems)
withChats(contentTag) { withContext(Dispatchers.Main) {
chatItems.replaceAll(newItems) chatsCtx.chatItems.replaceAll(newItems)
splits.value = newSplits splits.value = newSplits
chatState.moveUnreadAfterItem(splits.value.firstOrNull() ?: newItems.last().id, newItems) chatState.moveUnreadAfterItem(splits.value.firstOrNull() ?: newItems.last().id, newItems)
// loading clear bottom area, updating number of unread items after the newest loaded item // loading clear bottom area, updating number of unread items after the newest loaded item
@ -134,8 +134,8 @@ suspend fun processLoadedChat(
newItems.addAll(itemIndex, chat.chatItems) newItems.addAll(itemIndex, chat.chatItems)
newSplits.add(splitIndex, chat.chatItems.last().id) newSplits.add(splitIndex, chat.chatItems.last().id)
withChats(contentTag) { withContext(Dispatchers.Main) {
chatItems.replaceAll(newItems) chatsCtx.chatItems.replaceAll(newItems)
splits.value = newSplits splits.value = newSplits
unreadAfterItemId.value = chat.chatItems.last().id unreadAfterItemId.value = chat.chatItems.last().id
totalAfter.value = navInfo.afterTotal totalAfter.value = navInfo.afterTotal
@ -157,8 +157,8 @@ suspend fun processLoadedChat(
val newSplits = removeDuplicatesAndUnusedSplits(newItems, chat, chatState.splits.value) val newSplits = removeDuplicatesAndUnusedSplits(newItems, chat, chatState.splits.value)
removeDuplicates(newItems, chat) removeDuplicates(newItems, chat)
newItems.addAll(chat.chatItems) newItems.addAll(chat.chatItems)
withChats(contentTag) { withContext(Dispatchers.Main) {
chatItems.replaceAll(newItems) chatsCtx.chatItems.replaceAll(newItems)
chatState.splits.value = newSplits chatState.splits.value = newSplits
unreadAfterNewestLoaded.value = 0 unreadAfterNewestLoaded.value = 0
} }

View file

@ -32,8 +32,6 @@ import chat.simplex.common.model.CIDirection.GroupRcv
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.activeCall import chat.simplex.common.model.ChatModel.activeCall
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.group.* import chat.simplex.common.views.chat.group.*
@ -114,9 +112,10 @@ fun ChatView(
val chatRh = remoteHostId.value val chatRh = remoteHostId.value
// We need to have real unreadCount value for displaying it inside top right button // We need to have real unreadCount value for displaying it inside top right button
// Having activeChat reloaded on every change in it is inefficient (UI lags) // Having activeChat reloaded on every change in it is inefficient (UI lags)
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val unreadCount = remember { val unreadCount = remember {
derivedStateOf { derivedStateOf {
chatModel.chatsForContent(contentTag).value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0 chatsCtx.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats?.unreadCount ?: 0
} }
} }
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
@ -389,23 +388,25 @@ fun ChatView(
if (deleted != null) { if (deleted != null) {
deletedChatItem = deleted.deletedChatItem.chatItem deletedChatItem = deleted.deletedChatItem.chatItem
toChatItem = deleted.toChatItem?.chatItem toChatItem = deleted.toChatItem?.chatItem
withChats { withContext(Dispatchers.Main) {
if (toChatItem != null) { if (toChatItem != null) {
upsertChatItem(chatRh, chatInfo, toChatItem) chatModel.chatsContext.upsertChatItem(chatRh, chatInfo, toChatItem)
} else { } else {
removeChatItem(chatRh, chatInfo, deletedChatItem) chatModel.chatsContext.removeChatItem(chatRh, chatInfo, deletedChatItem)
} }
val deletedItem = deleted.deletedChatItem.chatItem val deletedItem = deleted.deletedChatItem.chatItem
if (deletedItem.isActiveReport) { if (deletedItem.isActiveReport) {
decreaseGroupReportsCounter(chatRh, chatInfo.id) chatModel.chatsContext.decreaseGroupReportsCounter(chatRh, chatInfo.id)
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
if (deletedChatItem.isReport) { if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (toChatItem != null) { if (deletedChatItem.isReport) {
upsertChatItem(chatRh, chatInfo, toChatItem) if (toChatItem != null) {
} else { chatModel.secondaryChatsContext.upsertChatItem(chatRh, chatInfo, toChatItem)
removeChatItem(chatRh, chatInfo, deletedChatItem) } else {
chatModel.secondaryChatsContext.removeChatItem(chatRh, chatInfo, deletedChatItem)
}
} }
} }
} }
@ -463,8 +464,8 @@ fun ChatView(
if (r != null) { if (r != null) {
val contactStats = r.first val contactStats = r.first
if (contactStats != null) if (contactStats != null)
withChats { withContext(Dispatchers.Main) {
updateContactConnectionStats(chatRh, contact, contactStats) chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, contactStats)
} }
} }
} }
@ -475,8 +476,8 @@ fun ChatView(
if (r != null) { if (r != null) {
val memStats = r.second val memStats = r.second
if (memStats != null) { if (memStats != null) {
withChats { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) chatModel.chatsContext.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats)
} }
} }
} }
@ -486,8 +487,8 @@ fun ChatView(
withBGApi { withBGApi {
val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false)
if (cStats != null) { if (cStats != null) {
withChats { withContext(Dispatchers.Main) {
updateContactConnectionStats(chatRh, contact, cStats) chatModel.chatsContext.updateContactConnectionStats(chatRh, contact, cStats)
} }
} }
} }
@ -496,8 +497,8 @@ fun ChatView(
withBGApi { withBGApi {
val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) { if (r != null) {
withChats { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) chatModel.chatsContext.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second)
} }
} }
} }
@ -519,12 +520,14 @@ fun ChatView(
reaction = reaction reaction = reaction
) )
if (updatedCI != null) { if (updatedCI != null) {
withChats { withContext(Dispatchers.Main) {
updateChatItem(cInfo, updatedCI) chatModel.chatsContext.updateChatItem(cInfo, updatedCI)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
if (cItem.isReport) { if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
updateChatItem(cInfo, updatedCI) if (cItem.isReport) {
chatModel.secondaryChatsContext.updateChatItem(cInfo, updatedCI)
}
} }
} }
} }
@ -544,7 +547,7 @@ fun ChatView(
groupMembersJob.cancel() groupMembersJob.cancel()
groupMembersJob = scope.launch(Dispatchers.Default) { groupMembersJob = scope.launch(Dispatchers.Default) {
var initialCiInfo = loadChatItemInfo() ?: return@launch var initialCiInfo = loadChatItemInfo() ?: return@launch
if (!ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) { if (!ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
ModalManager.end.closeModals() ModalManager.end.closeModals()
} }
ModalManager.end.showModalCloseable(endButtons = { ModalManager.end.showModalCloseable(endButtons = {
@ -578,11 +581,8 @@ fun ChatView(
openGroupLink = { groupInfo -> openGroupLink(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) }, openGroupLink = { groupInfo -> openGroupLink(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) },
markItemsRead = { itemsIds -> markItemsRead = { itemsIds ->
withBGApi { withBGApi {
withChats { withContext(Dispatchers.Main) {
// It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace chatModel.chatsContext.markChatItemsRead(chatRh, chatInfo.id, itemsIds)
withContext(Dispatchers.Main) {
markChatItemsRead(chatRh, chatInfo.id, itemsIds)
}
ntfManager.cancelNotificationsForChat(chatInfo.id) ntfManager.cancelNotificationsForChat(chatInfo.id)
chatModel.controller.apiChatItemsRead( chatModel.controller.apiChatItemsRead(
chatRh, chatRh,
@ -591,18 +591,17 @@ fun ChatView(
itemsIds itemsIds
) )
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
markChatItemsRead(chatRh, chatInfo.id, itemsIds) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.markChatItemsRead(chatRh, chatInfo.id, itemsIds)
}
} }
} }
}, },
markChatRead = { markChatRead = {
withBGApi { withBGApi {
withChats { withContext(Dispatchers.Main) {
// It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace chatModel.chatsContext.markChatItemsRead(chatRh, chatInfo.id)
withContext(Dispatchers.Main) {
markChatItemsRead(chatRh, chatInfo.id)
}
ntfManager.cancelNotificationsForChat(chatInfo.id) ntfManager.cancelNotificationsForChat(chatInfo.id)
chatModel.controller.apiChatRead( chatModel.controller.apiChatRead(
chatRh, chatRh,
@ -610,8 +609,10 @@ fun ChatView(
chatInfo.apiId chatInfo.apiId
) )
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
markChatItemsRead(chatRh, chatInfo.id) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.markChatItemsRead(chatRh, chatInfo.id)
}
} }
} }
}, },
@ -636,8 +637,8 @@ fun ChatView(
LaunchedEffect(chatInfo.id) { LaunchedEffect(chatInfo.id) {
onComposed(chatInfo.id) onComposed(chatInfo.id)
ModalManager.end.closeModals() ModalManager.end.closeModals()
withChats { withContext(Dispatchers.Main) {
chatItems.clearAndNotify() chatModel.chatsContext.chatItems.clearAndNotify()
} }
} }
} }
@ -649,8 +650,8 @@ fun ChatView(
LaunchedEffect(chatInfo.id) { LaunchedEffect(chatInfo.id) {
onComposed(chatInfo.id) onComposed(chatInfo.id)
ModalManager.end.closeModals() ModalManager.end.closeModals()
withChats { withContext(Dispatchers.Main) {
chatItems.clearAndNotify() chatModel.chatsContext.chatItems.clearAndNotify()
} }
} }
} }
@ -1206,12 +1207,13 @@ fun BoxScope.ChatItemsList(
val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf<Long>()) } val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf<Long>()) }
val contentTag = LocalContentTag.current val contentTag = LocalContentTag.current
// not using reversedChatItems inside to prevent possible derivedState bug in Compose when one derived state access can cause crash asking another derived state // not using reversedChatItems inside to prevent possible derivedState bug in Compose when one derived state access can cause crash asking another derived state
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val mergedItems = remember { val mergedItems = remember {
derivedStateOf { derivedStateOf {
MergedItems.create(chatModel.chatItemsForContent(contentTag).value.asReversed(), unreadCount, revealedItems.value, chatModel.chatStateForContent(contentTag)) MergedItems.create(chatsCtx.chatItems.value.asReversed(), unreadCount, revealedItems.value, chatsCtx.chatState)
} }
} }
val reversedChatItems = remember { derivedStateOf { chatModel.chatItemsForContent(contentTag).value.asReversed() } } val reversedChatItems = remember { derivedStateOf { chatsCtx.chatItems.value.asReversed() } }
val reportsCount = reportsCount(chatInfo.id) val reportsCount = reportsCount(chatInfo.id)
val topPaddingToContent = topPaddingToContent(chatView = contentTag == null, contentTag == null && reportsCount > 0) val topPaddingToContent = topPaddingToContent(chatView = contentTag == null, contentTag == null && reportsCount > 0)
val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() }) val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent.roundToPx() })
@ -1297,11 +1299,11 @@ fun BoxScope.ChatItemsList(
DisposableEffectOnGone( DisposableEffectOnGone(
always = { always = {
chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(contentTag)), contentTag) chatsCtx.chatItemsChangesListener = recalculateChatStatePositions(chatsCtx.chatState)
}, },
whenGone = { whenGone = {
VideoPlayerHolder.releaseAll() VideoPlayerHolder.releaseAll()
chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(contentTag)), contentTag) chatsCtx.chatItemsChangesListener = recalculateChatStatePositions(chatsCtx.chatState)
} }
) )
@ -1648,8 +1650,9 @@ private suspend fun loadLastItems(chatId: State<ChatId>, contentTag: MsgContentT
} }
private fun lastItemsLoaded(contentTag: MsgContentTag?): Boolean { private fun lastItemsLoaded(contentTag: MsgContentTag?): Boolean {
val chatState = chatModel.chatStateForContent(contentTag) val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
return chatState.splits.value.isEmpty() || chatState.splits.value.firstOrNull() != chatModel.chatItemsForContent(contentTag).value.lastOrNull()?.id val chatState = chatsCtx.chatState
return chatState.splits.value.isEmpty() || chatState.splits.value.firstOrNull() != chatsCtx.chatItems.value.lastOrNull()?.id
} }
// TODO: in extra rare case when after loading last items only 1 item is loaded, the view will jump like when receiving new message // TODO: in extra rare case when after loading last items only 1 item is loaded, the view will jump like when receiving new message
@ -1740,7 +1743,8 @@ fun BoxScope.FloatingButtons(
fun scrollToTopUnread() { fun scrollToTopUnread() {
scope.launch { scope.launch {
tryBlockAndSetLoadingMore(loadingMoreItems) { tryBlockAndSetLoadingMore(loadingMoreItems) {
if (chatModel.chatStateForContent(contentTag).splits.value.isNotEmpty()) { val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
if (chatsCtx.chatState.splits.value.isNotEmpty()) {
val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT) val pagination = ChatPagination.Initial(ChatPagination.INITIAL_COUNT)
val oldSize = reversedChatItems.value.size val oldSize = reversedChatItems.value.size
loadMessages(chatInfo.value.id, pagination) { loadMessages(chatInfo.value.id, pagination) {
@ -2129,7 +2133,7 @@ private fun SaveReportsStateOnDispose(listState: State<LazyListState>) {
val contentTag = LocalContentTag.current val contentTag = LocalContentTag.current
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
reportsListState = if (contentTag == MsgContentTag.Report && ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) listState.value else null reportsListState = if (contentTag == MsgContentTag.Report && ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) listState.value else null
} }
} }
} }
@ -2240,8 +2244,10 @@ fun reportsCount(staleChatId: String?): Int {
} }
} }
private fun reversedChatItemsStatic(contentTag: MsgContentTag?): List<ChatItem> = private fun reversedChatItemsStatic(contentTag: MsgContentTag?): List<ChatItem> {
chatModel.chatItemsForContent(contentTag).value.asReversed() val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
return chatsCtx.chatItems.value.asReversed()
}
private fun oldestPartiallyVisibleListItemInListStateOrNull(topPaddingToContentPx: State<Int>, mergedItems: State<MergedItems>, listState: State<LazyListState>): ListItem? { private fun oldestPartiallyVisibleListItemInListStateOrNull(topPaddingToContentPx: State<Int>, mergedItems: State<MergedItems>, listState: State<LazyListState>): ListItem? {
val lastFullyVisibleOffset = listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value val lastFullyVisibleOffset = listState.value.layoutInfo.viewportEndOffset - topPaddingToContentPx.value
@ -2326,11 +2332,13 @@ private fun findQuotedItemFromItem(
scope.launch(Dispatchers.Default) { scope.launch(Dispatchers.Default) {
val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId, contentTag) val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId, contentTag)
if (item != null) { if (item != null) {
withChats { withContext(Dispatchers.Main) {
updateChatItem(chatInfo.value, item) chatModel.chatsContext.updateChatItem(chatInfo.value, item)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
updateChatItem(chatInfo.value, item) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.updateChatItem(chatInfo.value, item)
}
} }
if (item.quotedItem?.itemId != null) { if (item.quotedItem?.itemId != null) {
scrollToItem(item.quotedItem.itemId) scrollToItem(item.quotedItem.itemId)
@ -2510,28 +2518,30 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long
) )
} }
if (deleted != null) { if (deleted != null) {
withChats { withContext(Dispatchers.Main) {
for (di in deleted) { for (di in deleted) {
val toChatItem = di.toChatItem?.chatItem val toChatItem = di.toChatItem?.chatItem
if (toChatItem != null) { if (toChatItem != null) {
upsertChatItem(chatRh, chatInfo, toChatItem) chatModel.chatsContext.upsertChatItem(chatRh, chatInfo, toChatItem)
} else { } else {
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) chatModel.chatsContext.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
} }
val deletedItem = di.deletedChatItem.chatItem val deletedItem = di.deletedChatItem.chatItem
if (deletedItem.isActiveReport) { if (deletedItem.isActiveReport) {
decreaseGroupReportsCounter(chatRh, chatInfo.id) chatModel.chatsContext.decreaseGroupReportsCounter(chatRh, chatInfo.id)
} }
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
for (di in deleted) { if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (di.deletedChatItem.chatItem.isReport) { for (di in deleted) {
val toChatItem = di.toChatItem?.chatItem if (di.deletedChatItem.chatItem.isReport) {
if (toChatItem != null) { val toChatItem = di.toChatItem?.chatItem
upsertChatItem(chatRh, chatInfo, toChatItem) if (toChatItem != null) {
} else { chatModel.secondaryChatsContext.upsertChatItem(chatRh, chatInfo, toChatItem)
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) } else {
chatModel.secondaryChatsContext.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
}
} }
} }
} }
@ -2552,28 +2562,30 @@ private fun archiveReports(chatRh: Long?, chatInfo: ChatInfo, itemIds: List<Long
mode = if (forAll) CIDeleteMode.cidmBroadcast else CIDeleteMode.cidmInternalMark mode = if (forAll) CIDeleteMode.cidmBroadcast else CIDeleteMode.cidmInternalMark
) )
if (deleted != null) { if (deleted != null) {
withChats { withContext(Dispatchers.Main) {
for (di in deleted) { for (di in deleted) {
val toChatItem = di.toChatItem?.chatItem val toChatItem = di.toChatItem?.chatItem
if (toChatItem != null) { if (toChatItem != null) {
upsertChatItem(chatRh, chatInfo, toChatItem) chatModel.chatsContext.upsertChatItem(chatRh, chatInfo, toChatItem)
} else { } else {
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) chatModel.chatsContext.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
} }
val deletedItem = di.deletedChatItem.chatItem val deletedItem = di.deletedChatItem.chatItem
if (deletedItem.isActiveReport) { if (deletedItem.isActiveReport) {
decreaseGroupReportsCounter(chatRh, chatInfo.id) chatModel.chatsContext.decreaseGroupReportsCounter(chatRh, chatInfo.id)
} }
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
for (di in deleted) { if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
if (di.deletedChatItem.chatItem.isReport) { for (di in deleted) {
val toChatItem = di.toChatItem?.chatItem if (di.deletedChatItem.chatItem.isReport) {
if (toChatItem != null) { val toChatItem = di.toChatItem?.chatItem
upsertChatItem(chatRh, chatInfo, toChatItem) if (toChatItem != null) {
} else { chatModel.secondaryChatsContext.upsertChatItem(chatRh, chatInfo, toChatItem)
removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) } else {
chatModel.secondaryChatsContext.removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem)
}
} }
} }
} }
@ -2607,9 +2619,9 @@ private fun markUnreadChatAsRead(chatId: String) {
false false
) )
if (success) { if (success) {
withChats { withContext(Dispatchers.Main) {
replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) chatModel.chatsContext.replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
markChatTagRead(chat) chatModel.chatsContext.markChatTagRead(chat)
} }
} }
} }
@ -2791,7 +2803,7 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf
private fun forwardContent(chatItemsIds: List<Long>, chatInfo: ChatInfo) { private fun forwardContent(chatItemsIds: List<Long>, chatInfo: ChatInfo) {
chatModel.chatId.value = null chatModel.chatId.value = null
chatModel.sharedContent.value = SharedContent.Forward( chatModel.sharedContent.value = SharedContent.Forward(
chatModel.chatItemsForContent(null).value.filter { chatItemsIds.contains(it.id) }, chatModel.chatsContext.chatItems.value.filter { chatItemsIds.contains(it.id) },
chatInfo chatInfo
) )
} }

View file

@ -25,7 +25,6 @@ import androidx.compose.ui.util.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.filesToDelete import chat.simplex.common.model.ChatModel.filesToDelete
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.* import chat.simplex.common.views.chat.item.*
@ -473,8 +472,8 @@ fun ComposeView(
) )
if (!chatItems.isNullOrEmpty()) { if (!chatItems.isNullOrEmpty()) {
chatItems.forEach { aChatItem -> chatItems.forEach { aChatItem ->
withChats { withContext(Dispatchers.Main) {
addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) chatModel.chatsContext.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem)
} }
} }
return chatItems.first().chatItem return chatItems.first().chatItem
@ -505,9 +504,9 @@ fun ComposeView(
ttl = ttl ttl = ttl
) )
withChats { withContext(Dispatchers.Main) {
chatItems?.forEach { chatItem -> chatItems?.forEach { chatItem ->
addChatItem(rhId, chat.chatInfo, chatItem) chatModel.chatsContext.addChatItem(rhId, chat.chatInfo, chatItem)
} }
} }
@ -567,9 +566,9 @@ fun ComposeView(
suspend fun sendReport(reportReason: ReportReason, chatItemId: Long): List<ChatItem>? { suspend fun sendReport(reportReason: ReportReason, chatItemId: Long): List<ChatItem>? {
val cItems = chatModel.controller.apiReportMessage(chat.remoteHostId, chat.chatInfo.apiId, chatItemId, reportReason, msgText) val cItems = chatModel.controller.apiReportMessage(chat.remoteHostId, chat.chatInfo.apiId, chatItemId, reportReason, msgText)
if (cItems != null) { if (cItems != null) {
withChats { withContext(Dispatchers.Main) {
cItems.forEach { chatItem -> cItems.forEach { chatItem ->
addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem) chatModel.chatsContext.addChatItem(chat.remoteHostId, chat.chatInfo, chatItem.chatItem)
} }
} }
} }
@ -581,8 +580,8 @@ fun ComposeView(
val mc = checkLinkPreview() val mc = checkLinkPreview()
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
if (contact != null) { if (contact != null) {
withChats { withContext(Dispatchers.Main) {
updateContact(chat.remoteHostId, contact) chatModel.chatsContext.updateContact(chat.remoteHostId, contact)
} }
} }
} }
@ -599,8 +598,10 @@ fun ComposeView(
updatedMessage = UpdatedMessage(updateMsgContent(oldMsgContent), cs.memberMentions), updatedMessage = UpdatedMessage(updateMsgContent(oldMsgContent), cs.memberMentions),
live = live live = live
) )
if (updatedItem != null) withChats { if (updatedItem != null) {
upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) withContext(Dispatchers.Main) {
chatModel.chatsContext.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem)
}
} }
return updatedItem?.chatItem return updatedItem?.chatItem
} }
@ -890,7 +891,7 @@ fun ComposeView(
fun editPrevMessage() { fun editPrevMessage() {
if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return
val lastEditable = chatModel.chatItemsForContent(null).value.findLast { it.meta.editable } val lastEditable = chatModel.chatsContext.chatItems.value.findLast { it.meta.editable }
if (lastEditable != null) { if (lastEditable != null) {
composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews) composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews)
} }

View file

@ -12,16 +12,16 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.common.views.usersettings.PreferenceToggle
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatModel
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.*
@Composable @Composable
fun ContactPreferencesView( fun ContactPreferencesView(
@ -41,8 +41,8 @@ fun ContactPreferencesView(
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed) val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs) val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs)
if (toContact != null) { if (toContact != null) {
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, toContact) chatModel.chatsContext.updateContact(rhId, toContact)
currentFeaturesAllowed = featuresAllowed currentFeaturesAllowed = featuresAllowed
} }
} }

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.unit.dp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.BackHandler import chat.simplex.common.platform.BackHandler
import chat.simplex.common.platform.chatModel import chat.simplex.common.platform.chatModel
import chat.simplex.common.views.chat.group.LocalContentTag
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.res.MR import chat.simplex.res.MR
@ -121,7 +122,8 @@ fun SelectedItemsButtonsToolbar(
} }
Divider(Modifier.align(Alignment.TopStart)) Divider(Modifier.align(Alignment.TopStart))
} }
val chatItems = remember { derivedStateOf { chatModel.chatItemsForContent(contentTag).value } } val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val chatItems = remember { derivedStateOf { chatsCtx.chatItems.value } }
LaunchedEffect(chatInfo, chatItems.value, selectedChatItems.value) { LaunchedEffect(chatInfo, chatItems.value, selectedChatItems.value) {
recheckItems(chatInfo, chatItems.value, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canArchiveReports, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited) recheckItems(chatInfo, chatItems.value, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canArchiveReports, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited)
} }

View file

@ -25,8 +25,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.ChatInfoToolbarTitle import chat.simplex.common.views.chat.ChatInfoToolbarTitle
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@ -35,6 +33,7 @@ import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.*
@Composable @Composable
fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) { fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolean = false, chatModel: ChatModel, close: () -> Unit) {
@ -62,11 +61,13 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
for (contactId in selectedContacts) { for (contactId in selectedContacts) {
val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value)
if (member != null) { if (member != null) {
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, groupInfo, member) chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, member)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, groupInfo, member) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, groupInfo, member)
}
} }
} else { } else {
break break

View file

@ -32,8 +32,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
@ -182,8 +180,8 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl
withBGApi { withBGApi {
val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId) val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId)
if (r) { if (r) {
withChats { withContext(Dispatchers.Main) {
removeChat(chat.remoteHostId, chatInfo.id) chatModel.chatsContext.removeChat(chat.remoteHostId, chatInfo.id)
if (chatModel.chatId.value == chatInfo.id) { if (chatModel.chatId.value == chatInfo.id) {
chatModel.chatId.value = null chatModel.chatId.value = null
ModalManager.end.closeModals() ModalManager.end.closeModals()
@ -957,8 +955,8 @@ private fun SearchRowView(
private fun setGroupAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi { private fun setGroupAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi {
val chatRh = chat.remoteHostId val chatRh = chat.remoteHostId
chatModel.controller.apiSetGroupAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { chatModel.controller.apiSetGroupAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let {
withChats { withContext(Dispatchers.Main) {
updateGroup(chatRh, it) chatModel.chatsContext.updateGroup(chatRh, it)
} }
} }
} }
@ -967,14 +965,16 @@ fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onSu
withBGApi { withBGApi {
val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds) val updatedMembers = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds)
if (updatedMembers != null) { if (updatedMembers != null) {
withChats { withContext(Dispatchers.Main) {
updatedMembers.forEach { updatedMember -> updatedMembers.forEach { updatedMember ->
upsertGroupMember(rhId, groupInfo, updatedMember) chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, updatedMember)
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
updatedMembers.forEach { updatedMember -> if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
upsertGroupMember(rhId, groupInfo, updatedMember) updatedMembers.forEach { updatedMember ->
chatModel.secondaryChatsContext.upsertGroupMember(rhId, groupInfo, updatedMember)
}
} }
} }
onSuccess() onSuccess()

View file

@ -27,8 +27,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@ -40,6 +38,7 @@ import chat.simplex.common.views.chatlist.openLoadedChat
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.coroutines.*
@Composable @Composable
fun GroupMemberInfoView( fun GroupMemberInfoView(
@ -63,11 +62,13 @@ fun GroupMemberInfoView(
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false) val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false)
if (r != null) { if (r != null) {
connStats.value = r.second connStats.value = r.second
withChats { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
} }
close.invoke() close.invoke()
} }
@ -100,8 +101,8 @@ fun GroupMemberInfoView(
val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId) val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId)
if (memberContact != null) { if (memberContact != null) {
val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf()) val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf())
withChats { withContext(Dispatchers.Main) {
addChat(memberChat) chatModel.chatsContext.addChat(memberChat)
} }
openLoadedChat(memberChat) openLoadedChat(memberChat)
closeAll() closeAll()
@ -149,11 +150,13 @@ fun GroupMemberInfoView(
val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
if (r != null) { if (r != null) {
connStats.value = r.second connStats.value = r.second
withChats { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
} }
close.invoke() close.invoke()
} }
@ -166,11 +169,13 @@ fun GroupMemberInfoView(
val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId)
if (r != null) { if (r != null) {
connStats.value = r.second connStats.value = r.second
withChats { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
} }
close.invoke() close.invoke()
} }
@ -186,11 +191,13 @@ fun GroupMemberInfoView(
val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true) val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true)
if (r != null) { if (r != null) {
connStats.value = r.second connStats.value = r.second
withChats { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) chatModel.chatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second)
}
} }
close.invoke() close.invoke()
} }
@ -212,11 +219,13 @@ fun GroupMemberInfoView(
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
) )
) )
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, groupInfo, copy) chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, copy)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, groupInfo, copy) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, groupInfo, copy)
}
} }
r r
} }
@ -247,14 +256,16 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c
withBGApi { withBGApi {
val removedMembers = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId)) val removedMembers = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId))
if (removedMembers != null) { if (removedMembers != null) {
withChats { withContext(Dispatchers.Main) {
removedMembers.forEach { removedMember -> removedMembers.forEach { removedMember ->
upsertGroupMember(rhId, groupInfo, removedMember) chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, removedMember)
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
removedMembers.forEach { removedMember -> if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
upsertGroupMember(rhId, groupInfo, removedMember) removedMembers.forEach { removedMember ->
chatModel.secondaryChatsContext.upsertGroupMember(rhId, groupInfo, removedMember)
}
} }
} }
} }
@ -697,14 +708,16 @@ fun updateMembersRole(newRole: GroupMemberRole, rhId: Long?, groupInfo: GroupInf
withBGApi { withBGApi {
kotlin.runCatching { kotlin.runCatching {
val members = chatModel.controller.apiMembersRole(rhId, groupInfo.groupId, memberIds, newRole) val members = chatModel.controller.apiMembersRole(rhId, groupInfo.groupId, memberIds, newRole)
withChats { withContext(Dispatchers.Main) {
members.forEach { member -> members.forEach { member ->
upsertGroupMember(rhId, groupInfo, member) chatModel.chatsContext.upsertGroupMember(rhId, groupInfo, member)
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
members.forEach { member -> if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
upsertGroupMember(rhId, groupInfo, member) members.forEach { member ->
chatModel.secondaryChatsContext.upsertGroupMember(rhId, groupInfo, member)
}
} }
} }
onSuccess() onSuccess()
@ -798,11 +811,13 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem
withBGApi { withBGApi {
val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings) val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings)
if (success) { if (success) {
withChats { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) chatModel.chatsContext.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings))
}
} }
} }
} }
@ -857,14 +872,16 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, onS
fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, blocked: Boolean, onSuccess: () -> Unit = {}) { fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, memberIds: List<Long>, blocked: Boolean, onSuccess: () -> Unit = {}) {
withBGApi { withBGApi {
val updatedMembers = ChatController.apiBlockMembersForAll(rhId, gInfo.groupId, memberIds, blocked) val updatedMembers = ChatController.apiBlockMembersForAll(rhId, gInfo.groupId, memberIds, blocked)
withChats { withContext(Dispatchers.Main) {
updatedMembers.forEach { updatedMember -> updatedMembers.forEach { updatedMember ->
upsertGroupMember(rhId, gInfo, updatedMember) chatModel.chatsContext.upsertGroupMember(rhId, gInfo, updatedMember)
} }
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
updatedMembers.forEach { updatedMember -> if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
upsertGroupMember(rhId, gInfo, updatedMember) updatedMembers.forEach { updatedMember ->
chatModel.secondaryChatsContext.upsertGroupMember(rhId, gInfo, updatedMember)
}
} }
} }
onSuccess() onSuccess()

View file

@ -15,9 +15,10 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatModel
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.*
private val featureRoles: List<Pair<GroupMemberRole?, String>> = listOf( private val featureRoles: List<Pair<GroupMemberRole?, String>> = listOf(
null to generalGetString(MR.strings.feature_roles_all_members), null to generalGetString(MR.strings.feature_roles_all_members),
@ -42,12 +43,12 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () ->
val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences()) val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences())
val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp)
if (g != null) { if (g != null) {
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, g) chatModel.chatsContext.updateGroup(rhId, g)
currentPreferences = preferences currentPreferences = preferences
} }
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, g) chatModel.chatsContext.updateGroup(rhId, g)
} }
} }
afterSave() afterSave()

View file

@ -17,8 +17,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.* import chat.simplex.common.views.*
@ -27,8 +25,7 @@ import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import kotlinx.coroutines.delay import kotlinx.coroutines.*
import kotlinx.coroutines.launch
import java.net.URI import java.net.URI
@Composable @Composable
@ -40,8 +37,8 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
withBGApi { withBGApi {
val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p) val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p)
if (gInfo != null) { if (gInfo != null) {
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, gInfo) chatModel.chatsContext.updateGroup(rhId, gInfo)
} }
close.invoke() close.invoke()
} }

View file

@ -72,7 +72,7 @@ private fun ItemsReload(contentTag: MsgContentTag?) {
suspend fun showGroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo) { suspend fun showGroupReportsView(staleChatId: State<String?>, scrollToItemId: MutableState<Long?>, chatInfo: ChatInfo) {
openChat(chatModel.remoteHostId(), chatInfo, MsgContentTag.Report) openChat(chatModel.remoteHostId(), chatInfo, MsgContentTag.Report)
ModalManager.end.showCustomModal(true, id = ModalViewId.GROUP_REPORTS) { close -> ModalManager.end.showCustomModal(true, id = ModalViewId.SECONDARY_CHAT) { close ->
ModalView({}, showAppBar = false) { ModalView({}, showAppBar = false) {
val chatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chatModel.chatId.value }?.chatInfo } }.value val chatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chatModel.chatId.value }?.chatInfo } }.value
if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate) { if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canModerate) {

View file

@ -26,13 +26,10 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING
import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.model.GroupInfo import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.*
import chat.simplex.common.platform.chatJsonLength
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.*
private const val maxByteCount = 1200 private const val maxByteCount = 1200
@ -51,8 +48,8 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: ()
val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated) val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated)
if (res != null) { if (res != null) {
gInfo = res gInfo = res
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, res) chatModel.chatsContext.updateGroup(rhId, res)
} }
welcomeText.value = welcome ?: "" welcomeText.value = welcome ?: ""
} }

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.getChatItemIndexOrNull import chat.simplex.common.model.ChatModel.getChatItemIndexOrNull
import chat.simplex.common.platform.chatModel
import chat.simplex.common.platform.onRightClick import chat.simplex.common.platform.onRightClick
import chat.simplex.common.views.chat.group.LocalContentTag import chat.simplex.common.views.chat.group.LocalContentTag
@ -76,7 +77,8 @@ private fun mergedFeatures(chatItem: ChatItem, chatInfo: ChatInfo): List<Feature
val m = ChatModel val m = ChatModel
val fs: ArrayList<FeatureInfo> = arrayListOf() val fs: ArrayList<FeatureInfo> = arrayListOf()
val icons: MutableSet<PainterBox> = mutableSetOf() val icons: MutableSet<PainterBox> = mutableSetOf()
val reversedChatItems = m.chatItemsForContent(LocalContentTag.current).value.asReversed() val chatsCtx = if (LocalContentTag.current == null) m.chatsContext else m.secondaryChatsContext
val reversedChatItems = chatsCtx.chatItems.value.asReversed()
var i = getChatItemIndexOrNull(chatItem, reversedChatItems) var i = getChatItemIndexOrNull(chatItem, reversedChatItems)
if (i != null) { if (i != null) {
while (i < reversedChatItems.size) { while (i < reversedChatItems.size) {

View file

@ -10,7 +10,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.* import androidx.compose.ui.geometry.*
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
@ -632,7 +631,8 @@ fun ChatItemView(
} }
@Composable fun EventItemView() { @Composable fun EventItemView() {
val reversedChatItems = chatModel.chatItemsForContent(LocalContentTag.current).value.asReversed() val chatsCtx = if (LocalContentTag.current == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val reversedChatItems = chatsCtx.chatItems.value.asReversed()
CIEventView(eventItemViewText(reversedChatItems)) CIEventView(eventItemViewText(reversedChatItems))
} }
@ -839,13 +839,14 @@ fun DeleteItemAction(
buttonText: String = stringResource(MR.strings.delete_verb), buttonText: String = stringResource(MR.strings.delete_verb),
) { ) {
val contentTag = LocalContentTag.current val contentTag = LocalContentTag.current
val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
ItemAction( ItemAction(
buttonText, buttonText,
painterResource(MR.images.ic_delete), painterResource(MR.images.ic_delete),
onClick = { onClick = {
showMenu.value = false showMenu.value = false
if (!revealed.value) { if (!revealed.value) {
val reversedChatItems = chatModel.chatItemsForContent(contentTag).value.asReversed() val reversedChatItems = chatsCtx.chatItems.value.asReversed()
val currIndex = chatModel.getChatItemIndexOrNull(cItem, reversedChatItems) val currIndex = chatModel.getChatItemIndexOrNull(cItem, reversedChatItems)
val ciCategory = cItem.mergeCategory val ciCategory = cItem.mergeCategory
if (currIndex != null && ciCategory != null) { if (currIndex != null && ciCategory != null) {
@ -1314,7 +1315,7 @@ fun shapeStyle(chatItem: ChatItem? = null, tailEnabled: Boolean, tailVisible: Bo
} }
private fun closeReportsIfNeeded() { private fun closeReportsIfNeeded() {
if (appPlatform.isAndroid && ModalManager.end.isLastModalOpen(ModalViewId.GROUP_REPORTS)) { if (appPlatform.isAndroid && ModalManager.end.isLastModalOpen(ModalViewId.SECONDARY_CHAT)) {
ModalManager.end.closeModals() ModalManager.end.closeModals()
} }
} }

View file

@ -44,7 +44,8 @@ fun MarkedDeletedItemView(ci: ChatItem, chatInfo: ChatInfo, timedMessagesTTL: In
@Composable @Composable
private fun MergedMarkedDeletedText(chatItem: ChatItem, chatInfo: ChatInfo, revealed: State<Boolean>) { private fun MergedMarkedDeletedText(chatItem: ChatItem, chatInfo: ChatInfo, revealed: State<Boolean>) {
val reversedChatItems = chatModel.chatItemsForContent(LocalContentTag.current).value.asReversed() val chatsCtx = if (LocalContentTag.current == null) chatModel.chatsContext else chatModel.secondaryChatsContext
val reversedChatItems = chatsCtx.chatItems.value.asReversed()
var i = getChatItemIndexOrNull(chatItem, reversedChatItems) var i = getChatItemIndexOrNull(chatItem, reversedChatItems)
val ciCategory = chatItem.mergeCategory val ciCategory = chatItem.mergeCategory
val text = if (!revealed.value && ciCategory != null && i != null) { val text = if (!revealed.value && ciCategory != null && i != null) {

View file

@ -20,8 +20,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.*
@ -233,17 +231,19 @@ suspend fun openChat(
) )
suspend fun openLoadedChat(chat: Chat, contentTag: MsgContentTag? = null) { suspend fun openLoadedChat(chat: Chat, contentTag: MsgContentTag? = null) {
withChats(contentTag) { withContext(Dispatchers.Main) {
chatItemStatuses.clear() val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
chatItems.replaceAll(chat.chatItems) chatsCtx.chatItemStatuses.clear()
chatsCtx.chatItems.replaceAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id chatModel.chatId.value = chat.chatInfo.id
chatModel.chatStateForContent(contentTag).clear() chatsCtx.chatState.clear()
} }
} }
suspend fun apiFindMessages(ch: Chat, search: String, contentTag: MsgContentTag?) { suspend fun apiFindMessages(ch: Chat, search: String, contentTag: MsgContentTag?) {
withChats(contentTag) { withContext(Dispatchers.Main) {
chatItems.clearAndNotify() val chatsCtx = if (contentTag == null) chatModel.chatsContext else chatModel.secondaryChatsContext
chatsCtx.chatItems.clearAndNotify()
} }
apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, contentTag, pagination = if (search.isNotEmpty()) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT), search = search) apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, contentTag, pagination = if (search.isNotEmpty()) ChatPagination.Last(ChatPagination.INITIAL_COUNT) else ChatPagination.Initial(ChatPagination.INITIAL_COUNT), search = search)
} }
@ -604,11 +604,13 @@ fun markChatRead(c: Chat) {
var chat = c var chat = c
withApi { withApi {
if (chat.chatStats.unreadCount > 0) { if (chat.chatStats.unreadCount > 0) {
withChats { withContext(Dispatchers.Main) {
markChatItemsRead(chat.remoteHostId, chat.chatInfo.id) chatModel.chatsContext.markChatItemsRead(chat.remoteHostId, chat.chatInfo.id)
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
markChatItemsRead(chat.remoteHostId, chat.chatInfo.id) if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.markChatItemsRead(chat.remoteHostId, chat.chatInfo.id)
}
} }
chatModel.controller.apiChatRead( chatModel.controller.apiChatRead(
chat.remoteHostId, chat.remoteHostId,
@ -625,9 +627,9 @@ fun markChatRead(c: Chat) {
false false
) )
if (success) { if (success) {
withChats { withContext(Dispatchers.Main) {
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) chatModel.chatsContext.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)))
markChatTagRead(chat) chatModel.chatsContext.markChatTagRead(chat)
} }
} }
} }
@ -647,9 +649,9 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) {
true true
) )
if (success) { if (success) {
withChats { withContext(Dispatchers.Main) {
replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) chatModel.chatsContext.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true)))
updateChatTagReadNoContentTag(chat, wasUnread) chatModel.chatsContext.updateChatTagReadNoContentTag(chat, wasUnread)
} }
} }
} }
@ -690,8 +692,8 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe
val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId) val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId)
if (contact != null && isCurrentUser && contactRequest != null) { if (contact != null && isCurrentUser && contactRequest != null) {
val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf())
withChats { withContext(Dispatchers.Main) {
replaceChat(rhId, contactRequest.id, chat) chatModel.chatsContext.replaceChat(rhId, contactRequest.id, chat)
} }
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
close?.invoke(chat) close?.invoke(chat)
@ -702,8 +704,8 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe
fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) {
withBGApi { withBGApi {
chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId) chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId)
withChats { withContext(Dispatchers.Main) {
removeChat(rhId, contactRequest.id) chatModel.chatsContext.removeChat(rhId, contactRequest.id)
} }
} }
} }
@ -720,8 +722,8 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti
withBGApi { withBGApi {
AlertManager.shared.hideAlert() AlertManager.shared.hideAlert()
if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) { if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) {
withChats { withContext(Dispatchers.Main) {
removeChat(rhId, connection.id) chatModel.chatsContext.removeChat(rhId, connection.id)
} }
onSuccess() onSuccess()
} }
@ -741,8 +743,8 @@ fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatMo
withBGApi { withBGApi {
val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId) val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId)
if (r) { if (r) {
withChats { withContext(Dispatchers.Main) {
removeChat(rhId, chatInfo.id) chatModel.chatsContext.removeChat(rhId, chatInfo.id)
} }
if (chatModel.chatId.value == chatInfo.id) { if (chatModel.chatId.value == chatInfo.id) {
chatModel.chatId.value = null chatModel.chatId.value = null
@ -805,8 +807,8 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress(
suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactId: Long, incognito: Boolean): Boolean { suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactId: Long, incognito: Boolean): Boolean {
val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId) val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId)
if (contact != null) { if (contact != null) {
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, contact) chatModel.chatsContext.updateContact(rhId, contact)
} }
AlertManager.privacySensitive.showAlertMsg( AlertManager.privacySensitive.showAlertMsg(
title = generalGetString(MR.strings.connection_request_sent), title = generalGetString(MR.strings.connection_request_sent),
@ -848,8 +850,8 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
withBGApi { withBGApi {
val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId) val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId)
if (r) { if (r) {
withChats { withContext(Dispatchers.Main) {
removeChat(rhId, groupInfo.id) chatModel.chatsContext.removeChat(rhId, groupInfo.id)
} }
if (chatModel.chatId.value == groupInfo.id) { if (chatModel.chatId.value == groupInfo.id) {
chatModel.chatId.value = null chatModel.chatId.value = null
@ -903,16 +905,16 @@ fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: Ch
val wasUnread = chat?.unreadTag ?: false val wasUnread = chat?.unreadTag ?: false
val wasFavorite = chatInfo.chatSettings?.favorite ?: false val wasFavorite = chatInfo.chatSettings?.favorite ?: false
chatModel.updateChatFavorite(favorite = chatSettings.favorite, wasFavorite) chatModel.updateChatFavorite(favorite = chatSettings.favorite, wasFavorite)
withChats { withContext(Dispatchers.Main) {
updateChatInfo(remoteHostId, newChatInfo) chatModel.chatsContext.updateChatInfo(remoteHostId, newChatInfo)
} }
if (chatSettings.enableNtfs == MsgFilter.None) { if (chatSettings.enableNtfs == MsgFilter.None) {
ntfManager.cancelNotificationsForChat(chatInfo.id) ntfManager.cancelNotificationsForChat(chatInfo.id)
} }
val updatedChat = chatModel.getChat(chatInfo.id) val updatedChat = chatModel.getChat(chatInfo.id)
if (updatedChat != null) { if (updatedChat != null) {
withChats { withContext(Dispatchers.Main) {
updateChatTagReadNoContentTag(updatedChat, wasUnread) chatModel.chatsContext.updateChatTagReadNoContentTag(updatedChat, wasUnread)
} }
} }
val current = currentState?.value val current = currentState?.value

View file

@ -27,7 +27,6 @@ import androidx.compose.ui.unit.*
import chat.simplex.common.AppLock import chat.simplex.common.AppLock
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatController.setConditionsNotified
import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
@ -38,8 +37,6 @@ import chat.simplex.common.views.chat.topPaddingToContent
import chat.simplex.common.views.newchat.* import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.onboarding.* import chat.simplex.common.views.onboarding.*
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
import chat.simplex.common.views.usersettings.networkAndServers.ConditionsLinkButton
import chat.simplex.common.views.usersettings.networkAndServers.UsageConditionsView
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
@ -139,7 +136,7 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow<Animate
if (appPlatform.isDesktop) { if (appPlatform.isDesktop) {
KeyChangeEffect(chatModel.chatId.value) { KeyChangeEffect(chatModel.chatId.value) {
if (chatModel.chatId.value != null && !ModalManager.end.isLastModalOpen(ModalViewId.GROUP_REPORTS)) { if (chatModel.chatId.value != null && !ModalManager.end.isLastModalOpen(ModalViewId.SECONDARY_CHAT)) {
ModalManager.end.closeModalsExceptFirst() ModalManager.end.closeModalsExceptFirst()
} }
AudioPlayer.stop() AudioPlayer.stop()

View file

@ -32,8 +32,6 @@ import chat.simplex.common.model.ChatController.apiDeleteChatTag
import chat.simplex.common.model.ChatController.apiSetChatTags import chat.simplex.common.model.ChatController.apiSetChatTags
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.clearActiveChatFilterIfNeeded import chat.simplex.common.model.ChatModel.clearActiveChatFilterIfNeeded
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chat.item.ItemAction
@ -43,6 +41,7 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
@Composable @Composable
fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: Boolean) { fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: Boolean) {
@ -417,15 +416,15 @@ private fun setTag(rhId: Long?, tagId: Long?, chat: Chat, close: () -> Unit) {
when (val cInfo = chat.chatInfo) { when (val cInfo = chat.chatInfo) {
is ChatInfo.Direct -> { is ChatInfo.Direct -> {
val contact = cInfo.contact.copy(chatTags = result.second) val contact = cInfo.contact.copy(chatTags = result.second)
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, contact) chatModel.chatsContext.updateContact(rhId, contact)
} }
} }
is ChatInfo.Group -> { is ChatInfo.Group -> {
val group = cInfo.groupInfo.copy(chatTags = result.second) val group = cInfo.groupInfo.copy(chatTags = result.second)
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, group) chatModel.chatsContext.updateGroup(rhId, group)
} }
} }
@ -453,14 +452,14 @@ private fun deleteTag(rhId: Long?, tag: ChatTag, saving: MutableState<Boolean>)
when (val cInfo = c.chatInfo) { when (val cInfo = c.chatInfo) {
is ChatInfo.Direct -> { is ChatInfo.Direct -> {
val contact = cInfo.contact.copy(chatTags = cInfo.contact.chatTags.filter { it != tagId }) val contact = cInfo.contact.copy(chatTags = cInfo.contact.chatTags.filter { it != tagId })
withChats { withContext(Dispatchers.Main) {
updateContact(rhId, contact) chatModel.chatsContext.updateContact(rhId, contact)
} }
} }
is ChatInfo.Group -> { is ChatInfo.Group -> {
val group = cInfo.groupInfo.copy(chatTags = cInfo.groupInfo.chatTags.filter { it != tagId }) val group = cInfo.groupInfo.copy(chatTags = cInfo.groupInfo.chatTags.filter { it != tagId })
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId, group) chatModel.chatsContext.updateGroup(rhId, group)
} }
} }
else -> {} else -> {}

View file

@ -5,7 +5,6 @@ import androidx.compose.ui.graphics.Color
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.*
import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chat.item.ItemAction

View file

@ -21,8 +21,6 @@ import androidx.compose.ui.unit.dp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
@ -36,6 +34,7 @@ import java.nio.file.StandardCopyOption
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlinx.coroutines.*
@Composable @Composable
fun DatabaseView() { fun DatabaseView() {
@ -538,15 +537,17 @@ fun deleteChatDatabaseFilesAndState() {
// Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself // 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.chatId.value = null
withLongRunningApi { withLongRunningApi {
withChats { withContext(Dispatchers.Main) {
chatItems.clearAndNotify() chatModel.chatsContext.chatItems.clearAndNotify()
chats.clear() chatModel.chatsContext.chats.clear()
popChatCollector.clear() chatModel.chatsContext.popChatCollector.clear()
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
chatItems.clearAndNotify() if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chats.clear() chatModel.secondaryChatsContext.chatItems.clearAndNotify()
popChatCollector.clear() chatModel.secondaryChatsContext.chats.clear()
chatModel.secondaryChatsContext.popChatCollector.clear()
}
} }
} }
chatModel.users.clear() chatModel.users.clear()
@ -785,10 +786,10 @@ private fun afterSetCiTTL(
appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath)
withApi { withApi {
try { try {
withChats { withContext(Dispatchers.Main) {
// this is using current remote host on purpose - if it changes during update, it will load correct chats // 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()) val chats = m.controller.apiGetChats(m.remoteHostId())
updateChats(chats) chatModel.chatsContext.updateChats(chats)
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "apiGetChats error: ${e.message}") Log.e(TAG, "apiGetChats error: ${e.message}")

View file

@ -85,7 +85,7 @@ class ModalData(val keyboardCoversBar: Boolean = true) {
} }
enum class ModalViewId { enum class ModalViewId {
GROUP_REPORTS SECONDARY_CHAT
} }
class ModalManager(private val placement: ModalPlacement? = null) { class ModalManager(private val placement: ModalPlacement? = null) {

View file

@ -18,7 +18,6 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.group.AddGroupMembersView import chat.simplex.common.views.chat.group.AddGroupMembersView
import chat.simplex.common.views.chatlist.setGroupMembers import chat.simplex.common.views.chatlist.setGroupMembers
@ -30,6 +29,7 @@ import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import java.net.URI import java.net.URI
@Composable @Composable
@ -42,10 +42,10 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c
withBGApi { withBGApi {
val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile) val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile)
if (groupInfo != null) { if (groupInfo != null) {
withChats { withContext(Dispatchers.Main) {
updateGroup(rhId = rhId, groupInfo) chatModel.chatsContext.updateGroup(rhId = rhId, groupInfo)
chatItems.clearAndNotify() chatModel.chatsContext.chatItems.clearAndNotify()
chatItemStatuses.clear() chatModel.chatsContext.chatItemStatuses.clear()
chatModel.chatId.value = groupInfo.id chatModel.chatId.value = groupInfo.id
} }
setGroupMembers(rhId, groupInfo, chatModel) setGroupMembers(rhId, groupInfo, chatModel)

View file

@ -7,13 +7,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.net.URI
enum class ConnectionLinkType { enum class ConnectionLinkType {
INVITATION, CONTACT, GROUP INVITATION, CONTACT, GROUP
@ -359,8 +357,8 @@ suspend fun connectViaUri(
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri) val pcc = chatModel.controller.apiConnect(rhId, incognito, uri)
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION
if (pcc != null) { if (pcc != null) {
withChats { withContext(Dispatchers.Main) {
updateContactConnection(rhId, pcc) chatModel.chatsContext.updateContactConnection(rhId, pcc)
} }
close?.invoke() close?.invoke()
AlertManager.privacySensitive.showAlertMsg( AlertManager.privacySensitive.showAlertMsg(

View file

@ -5,7 +5,6 @@ import SectionDividerSpaced
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -22,11 +21,11 @@ import chat.simplex.common.views.chat.LocalAliasEditor
import chat.simplex.common.views.chatlist.deleteContactConnectionAlert import chat.simplex.common.views.chatlist.deleteContactConnectionAlert
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.PendingContactConnection import chat.simplex.common.model.PendingContactConnection
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.*
@Composable @Composable
fun ContactConnectionInfoView( fun ContactConnectionInfoView(
@ -185,8 +184,8 @@ fun DeleteButton(onClick: () -> Unit) {
private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withBGApi { private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withBGApi {
chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let { chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let {
withChats { withContext(Dispatchers.Main) {
updateContactConnection(rhId, it) chatModel.chatsContext.updateContactConnection(rhId, it)
} }
} }
} }

View file

@ -31,7 +31,6 @@ import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.topPaddingToContent import chat.simplex.common.views.chat.topPaddingToContent
@ -39,7 +38,6 @@ import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.net.URI
enum class NewChatOption { enum class NewChatOption {
INVITE, CONNECT INVITE, CONNECT
@ -315,8 +313,8 @@ fun ActiveProfilePicker(
if (contactConnection != null) { if (contactConnection != null) {
updatedConn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId) updatedConn = controller.apiChangeConnectionUser(rhId, contactConnection.pccConnId, user.userId)
if (updatedConn != null) { if (updatedConn != null) {
withChats { withContext(Dispatchers.Main) {
updateContactConnection(rhId, updatedConn) chatModel.chatsContext.updateContactConnection(rhId, updatedConn)
updateShownConnection(updatedConn) updateShownConnection(updatedConn)
} }
} }
@ -338,8 +336,8 @@ fun ActiveProfilePicker(
} }
if (updatedConn != null) { if (updatedConn != null) {
withChats { withContext(Dispatchers.Main) {
updateContactConnection(user.remoteHostId, updatedConn) chatModel.chatsContext.updateContactConnection(user.remoteHostId, updatedConn)
} }
} }
@ -368,8 +366,8 @@ fun ActiveProfilePicker(
appPreferences.incognito.set(true) appPreferences.incognito.set(true)
val conn = controller.apiSetConnectionIncognito(rhId, contactConnection.pccConnId, true) val conn = controller.apiSetConnectionIncognito(rhId, contactConnection.pccConnId, true)
if (conn != null) { if (conn != null) {
withChats { withContext(Dispatchers.Main) {
updateContactConnection(rhId, conn) chatModel.chatsContext.updateContactConnection(rhId, conn)
updateShownConnection(conn) updateShownConnection(conn)
} }
close() close()
@ -685,8 +683,8 @@ private fun createInvitation(
withBGApi { withBGApi {
val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get()) val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get())
if (r != null) { if (r != null) {
withChats { withContext(Dispatchers.Main) {
updateContactConnection(rhId, r.second) chatModel.chatsContext.updateContactConnection(rhId, r.second)
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false, conn = r.second) chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false, conn = r.second)
contactConnection.value = r.second contactConnection.value = r.second
} }

View file

@ -11,13 +11,13 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.ColumnWithScrollBar
import chat.simplex.common.platform.chatModel
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.*
@Composable @Composable
fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
@ -34,8 +34,8 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) {
if (updated != null) { if (updated != null) {
val (updatedProfile, updatedContacts) = updated val (updatedProfile, updatedContacts) = updated
m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences) m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences)
withChats { withContext(Dispatchers.Main) {
updatedContacts.forEach { updateContact(user.remoteHostId, it) } updatedContacts.forEach { chatModel.chatsContext.updateContact(user.remoteHostId, it) }
} }
currentPreferences = preferences currentPreferences = preferences
} }

View file

@ -30,8 +30,8 @@ import chat.simplex.common.views.isValidDisplayName
import chat.simplex.common.views.localauth.SetAppPasscodeView import chat.simplex.common.views.localauth.SetAppPasscodeView
import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.views.onboarding.ReadableText
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import kotlinx.coroutines.*
enum class LAMode { enum class LAMode {
SYSTEM, SYSTEM,
@ -119,15 +119,15 @@ fun PrivacySettingsView(
chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable) chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable)
if (clearOverrides) { if (clearOverrides) {
// For loop here is to prevent ConcurrentModificationException that happens with forEach // For loop here is to prevent ConcurrentModificationException that happens with forEach
withChats { withContext(Dispatchers.Main) {
for (i in 0 until chats.size) { for (i in 0 until chatModel.chatsContext.chats.size) {
val chat = chats[i] val chat = chatModel.chatsContext.chats[i]
if (chat.chatInfo is ChatInfo.Direct) { if (chat.chatInfo is ChatInfo.Direct) {
var contact = chat.chatInfo.contact var contact = chat.chatInfo.contact
val sendRcpts = contact.chatSettings.sendRcpts val sendRcpts = contact.chatSettings.sendRcpts
if (sendRcpts != null && sendRcpts != enable) { if (sendRcpts != null && sendRcpts != enable) {
contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null)) contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null))
updateContact(currentUser.remoteHostId, contact) chatModel.chatsContext.updateContact(currentUser.remoteHostId, contact)
} }
} }
} }
@ -143,16 +143,16 @@ fun PrivacySettingsView(
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable) chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable)
if (clearOverrides) { if (clearOverrides) {
withChats { withContext(Dispatchers.Main) {
// For loop here is to prevent ConcurrentModificationException that happens with forEach // For loop here is to prevent ConcurrentModificationException that happens with forEach
for (i in 0 until chats.size) { for (i in 0 until chatModel.chatsContext.chats.size) {
val chat = chats[i] val chat = chatModel.chatsContext.chats[i]
if (chat.chatInfo is ChatInfo.Group) { if (chat.chatInfo is ChatInfo.Group) {
var groupInfo = chat.chatInfo.groupInfo var groupInfo = chat.chatInfo.groupInfo
val sendRcpts = groupInfo.chatSettings.sendRcpts val sendRcpts = groupInfo.chatSettings.sendRcpts
if (sendRcpts != null && sendRcpts != enable) { if (sendRcpts != null && sendRcpts != enable) {
groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null)) groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null))
updateGroup(currentUser.remoteHostId, groupInfo) chatModel.chatsContext.updateGroup(currentUser.remoteHostId, groupInfo)
} }
} }
} }

View file

@ -14,8 +14,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.* import androidx.compose.ui.window.*
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.common.ui.theme.SimpleXTheme
@ -58,12 +56,14 @@ fun showApp() {
} else { } else {
// The last possible cause that can be closed // The last possible cause that can be closed
withApi { withApi {
withChats { withContext(Dispatchers.Main) {
chatModel.chatId.value = null chatModel.chatId.value = null
chatItems.clearAndNotify() chatModel.chatsContext.chatItems.clearAndNotify()
} }
withReportsChatsIfOpen { withContext(Dispatchers.Main) {
chatItems.clearAndNotify() if (ModalManager.end.hasModalOpen(ModalViewId.SECONDARY_CHAT)) {
chatModel.secondaryChatsContext.chatItems.clearAndNotify()
}
} }
} }
} }