From fffab507bee64bad2658caea9d28c992f64bf185 Mon Sep 17 00:00:00 2001 From: Avently <7953703+avently@users.noreply.github.com> Date: Tue, 7 Jan 2025 23:17:10 +0700 Subject: [PATCH] changes --- .../kotlin/chat/simplex/common/App.kt | 2 +- .../chat/simplex/common/model/ChatModel.kt | 97 ++++++-- .../chat/simplex/common/model/SimpleXAPI.kt | 97 +++++++- .../chat/simplex/common/views/TerminalView.kt | 5 + .../common/views/chat/ChatItemsLoader.kt | 30 ++- .../simplex/common/views/chat/ChatView.kt | 228 ++++++++++++------ .../views/chat/SelectableChatItemToolbars.kt | 6 +- .../views/chat/group/GroupMemberInfoView.kt | 2 +- .../views/chat/group/GroupReportsView.kt | 56 ++++- .../views/chatlist/ChatListNavLinkView.kt | 31 ++- .../common/views/database/DatabaseView.kt | 6 + .../simplex/common/views/helpers/ModalView.kt | 39 ++- .../commonMain/resources/MR/base/strings.xml | 2 +- .../kotlin/chat/simplex/common/DesktopApp.kt | 4 + 14 files changed, 440 insertions(+), 165 deletions(-) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index 28f91226c7..437d9e8179 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -339,7 +339,7 @@ fun AndroidScreen(userPickerState: MutableStateFlow) { .graphicsLayer { translationX = maxWidth.toPx() - minOf(offset.value.dp, maxWidth).toPx() } ) Box2@{ currentChatId.value?.let { - ChatView(currentChatId, reportsView = false, onComposed) + ChatView(currentChatId, reportsView = false, onComposed = onComposed) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 0c99d5f42b..72cb1d2f47 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -8,7 +8,6 @@ import androidx.compose.ui.graphics.* import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.style.TextDecoration -import chat.simplex.common.model.ChatModel.chatItemsChangesListener import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* @@ -59,25 +58,21 @@ object ChatModel { val ctrlInitInProgress = mutableStateOf(false) val dbMigrationInProgress = mutableStateOf(false) val incompleteInitializedDbRemoved = mutableStateOf(false) - private val _chats = mutableStateOf(SnapshotStateList()) - val chats: State> = _chats // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() val switchingUsersAndHosts = mutableStateOf(false) // current chat val chatId = mutableStateOf(null) + val chatsContext = ChatsContext(null) + val reportsChatsContext = ChatsContext(MsgContentTag.Report) + val chats: State> = chatsContext.chats /** if you modify the items by adding/removing them, use helpers methods like [addAndNotify], [removeLastAndNotify], [removeAllAndNotify], [clearAndNotify] and so on. * If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages]. * If you use api call to get the items, use just [add] instead of [addAndNotify]. * Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */ - private val _chatItems = mutableStateOf(SnapshotStateList()) - val chatItems: State> = _chatItems // declaration of chatsContext should be after any other variable that is directly attached to ChatsContext class, otherwise, strange crash with NullPointerException for "this" parameter in random functions - private val chatsContext = ChatsContext() - // set listener here that will be notified on every add/delete of a chat item - var chatItemsChangesListener: ChatItemsChangesListener? = null - val chatState = ActiveChatState() + val chatItems: State> = chatsContext.chatItems // rhId, chatId val deletedChats = mutableStateOf>>(emptyList()) val chatItemStatuses = mutableMapOf() @@ -178,6 +173,24 @@ object ChatModel { // return true if you handled the click var centerPanelBackgroundClickHandler: (() -> Boolean)? = null + fun chatItemsForContent(contentTag: MsgContentTag?): State> = 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 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) { currentUser.value } else { @@ -250,7 +263,7 @@ object ChatModel { } } - fun addPresetChatTags(chatInfo: ChatInfo) { + private fun addPresetChatTags(chatInfo: ChatInfo) { for (tag in PresetTagKind.entries) { if (presetTagMatchesChat(tag, chatInfo)) { presetTags[tag] = (presetTags[tag] ?: 0) + 1 @@ -340,13 +353,29 @@ object ChatModel { } // running everything inside the block on main thread. Make sure any heavy computation is moved to a background thread - suspend fun withChats(action: suspend ChatsContext.() -> T): T = withContext(Dispatchers.Main) { - chatsContext.action() + suspend fun withChats(contentTag: MsgContentTag? = null, action: suspend ChatsContext.() -> T): T = withContext(Dispatchers.Main) { + when { + contentTag == null -> chatsContext.action() + contentTag == MsgContentTag.Report -> reportsChatsContext.action() + else -> TODO() + } } - class ChatsContext { - val chats = _chats - val chatItems = _chatItems + suspend fun withReportsChatsIfOpen(action: suspend ChatsContext.() -> T) = withContext(Dispatchers.Main) { + if (ModalManager.end.hasModalOpen(ModalViewId.GROUP_REPORTS)) { + reportsChatsContext.action() + } + } + + class ChatsContext(private val contentTag: MsgContentTag?) { + val chats = mutableStateOf(SnapshotStateList()) + val chatItems = mutableStateOf(SnapshotStateList()) + // set listener here that will be notified on every add/delete of a chat item + var chatItemsChangesListener: ChatItemsChangesListener? = null + val chatState = ActiveChatState() + + fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id } + private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId } suspend fun addChat(chat: Chat) { chats.add(index = 0, chat) @@ -743,8 +772,6 @@ object ChatModel { } } - private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId } - fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) { val current = currentUser.value ?: return val updated = current.copy( @@ -816,7 +843,8 @@ object ChatModel { } i-- } - chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items) + // TODO LALAL + chatsContext.chatItemsChangesListener?.read(if (itemIds != null) markedReadIds else null, items) } return markedRead } @@ -1157,7 +1185,15 @@ data class Chat( } @Serializable - data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0, val unreadChat: Boolean = false) + data class ChatStats( + val unreadCount: Int = 0, + // actual only via getChats() and getChat(.initial), otherwise, zero + val reportsCount: Int = 0, + // actual only via getChat(.initial), otherwise, zero + val archivedReportsCount: Int = 0, + val minUnreadItemId: Long = 0, + val unreadChat: Boolean = false + ) companion object { val sampleData = Chat( @@ -2519,7 +2555,7 @@ fun MutableState>.add(index: Int, elem: Chat) { } fun MutableState>.addAndNotify(index: Int, elem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(index, elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) } + value = SnapshotStateList().apply { addAll(value); add(index, elem); chatModel.chatsContext.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, index) } } fun MutableState>.add(elem: Chat) { @@ -2531,7 +2567,7 @@ fun MutableList.removeAll(predicate: (T) -> Boolean): Boolean = if (isEmp // Adds item to chatItems and notifies a listener about newly added item fun MutableState>.addAndNotify(elem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(elem); chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) } + value = SnapshotStateList().apply { addAll(value); add(elem); chatModel.chatsContext.chatItemsChangesListener?.added(elem.id to elem.isRcvNew, lastIndex) } } fun MutableState>.addAll(index: Int, elems: List) { @@ -2560,7 +2596,8 @@ fun MutableState>.removeAllAndNotify(block: (ChatIte } } if (toRemove.isNotEmpty()) { - chatItemsChangesListener?.removed(toRemove, value) + chatModel.chatsContext.chatItemsChangesListener?.removed(toRemove, value) + chatModel.reportsChatsContext.chatItemsChangesListener?.removed(toRemove, value) } } @@ -2580,7 +2617,7 @@ fun MutableState>.removeLastAndNotify() { val rem = removeLast() removed = Triple(rem.id, remIndex, rem.isRcvNew) } - chatItemsChangesListener?.removed(listOf(removed), value) + chatModel.chatsContext.chatItemsChangesListener?.removed(listOf(removed), value) } fun MutableState>.replaceAll(elems: List) { @@ -2594,7 +2631,8 @@ fun MutableState>.clear() { // Removes all chatItems and notifies a listener about it fun MutableState>.clearAndNotify() { value = SnapshotStateList() - chatItemsChangesListener?.cleared() + chatModel.chatsContext.chatItemsChangesListener?.cleared() + chatModel.reportsChatsContext.chatItemsChangesListener?.cleared() } fun State>.asReversed(): MutableList = value.asReversed() @@ -3678,6 +3716,17 @@ object MsgContentSerializer : KSerializer { } } +@Serializable +enum class MsgContentTag { + @SerialName("text") Text, + @SerialName("link") Link, + @SerialName("image") Image, + @SerialName("video") Video, + @SerialName("voice") Voice, + @SerialName("file") File, + @SerialName("report") Report, +} + @Serializable class FormattedText(val text: String, val format: Format? = null) { // TODO make it dependent on simplexLinkMode preference diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index e2ff76f53f..03812bbbf9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -18,6 +18,7 @@ import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.setNetCfg import chat.simplex.common.model.ChatModel.changingActiveUserMutex import chat.simplex.common.model.ChatModel.withChats +import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* @@ -896,8 +897,8 @@ object ChatController { return null } - suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, pagination: ChatPagination, search: String = ""): Pair? { - val r = sendCmd(rh, CC.ApiGetChat(type, id, pagination, search)) + suspend fun apiGetChat(rh: Long?, type: ChatType, id: Long, contentFilter: ContentFilter? = null, pagination: ChatPagination, search: String = ""): Pair? { + val r = sendCmd(rh, CC.ApiGetChat(type, id, contentFilter, pagination, search)) if (r is CR.ApiChat) return if (rh == null) r.chat to r.navInfo else r.chat.copy(remoteHostId = rh) to r.navInfo Log.e(TAG, "apiGetChat bad response: ${r.responseType} ${r.details}") if (pagination is ChatPagination.Around && r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.ChatItemNotFound) { @@ -1500,6 +1501,9 @@ object ChatController { withChats { clearChat(chat.remoteHostId, updatedChatInfo) } + withChats(MsgContentTag.Report) { + clearChat(chat.remoteHostId, updatedChatInfo) + } ntfManager.cancelNotificationsForChat(chat.chatInfo.id) close?.invoke() } @@ -2427,6 +2431,9 @@ object ChatController { withChats { upsertGroupMember(rhId, r.groupInfo, r.toMember) } + withReportsChatsIfOpen { + upsertGroupMember(rhId, r.groupInfo, r.toMember) + } } } is CR.ContactsMerged -> { @@ -2476,6 +2483,11 @@ object ChatController { withChats { addChatItem(rhId, cInfo, cItem) } + withReportsChatsIfOpen { + if (cItem.content.msgContent is MsgContent.MCReport) { + addChatItem(rhId, cInfo, cItem) + } + } } else if (cItem.isRcvNew && cInfo.ntfsEnabled) { chatModel.increaseUnreadCounter(rhId, r.user) } @@ -2500,6 +2512,11 @@ object ChatController { withChats { updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) } + withReportsChatsIfOpen { + if (cItem.content.msgContent is MsgContent.MCReport) { + updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + } + } } } is CR.ChatItemUpdated -> @@ -2509,6 +2526,11 @@ object ChatController { withChats { updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) } + withReportsChatsIfOpen { + if (r.reaction.chatReaction.chatItem.content.msgContent is MsgContent.MCReport) { + updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + } + } } } is CR.ChatItemsDeleted -> { @@ -2544,6 +2566,15 @@ object ChatController { upsertChatItem(rhId, cInfo, toChatItem.chatItem) } } + withReportsChatsIfOpen { + if (cItem.content.msgContent is MsgContent.MCReport) { + if (toChatItem == null) { + removeChatItem(rhId, cInfo, cItem) + } else { + upsertChatItem(rhId, cInfo, toChatItem.chatItem) + } + } + } } } is CR.ReceivedGroupInvitation -> { @@ -2603,42 +2634,63 @@ object ChatController { withChats { updateGroup(rhId, r.groupInfo) } + withReportsChatsIfOpen { + updateGroup(rhId, r.groupInfo) + } } is CR.DeletedMember -> if (active(r.user)) { withChats { upsertGroupMember(rhId, r.groupInfo, r.deletedMember) } + withReportsChatsIfOpen { + upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + } } is CR.LeftMember -> if (active(r.user)) { withChats { upsertGroupMember(rhId, r.groupInfo, r.member) } + withReportsChatsIfOpen { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberRole -> if (active(r.user)) { withChats { upsertGroupMember(rhId, r.groupInfo, r.member) } + withReportsChatsIfOpen { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberRoleUser -> if (active(r.user)) { withChats { upsertGroupMember(rhId, r.groupInfo, r.member) } + withReportsChatsIfOpen { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberBlockedForAll -> if (active(r.user)) { withChats { upsertGroupMember(rhId, r.groupInfo, r.member) } + withReportsChatsIfOpen { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.GroupDeleted -> // TODO update user member if (active(r.user)) { withChats { updateGroup(rhId, r.groupInfo) } + withReportsChatsIfOpen { + updateGroup(rhId, r.groupInfo) + } } is CR.UserJoinedGroup -> if (active(r.user)) { @@ -2667,6 +2719,9 @@ object ChatController { withChats { updateGroup(rhId, r.toGroup) } + withReportsChatsIfOpen { + updateGroup(rhId, r.toGroup) + } } is CR.NewMemberContactReceivedInv -> if (active(r.user)) { @@ -3005,6 +3060,11 @@ object ChatController { val cInfo = aChatItem.chatInfo val cItem = aChatItem.chatItem withChats { upsertChatItem(rh, cInfo, cItem) } + withReportsChatsIfOpen { + if (cItem.content.msgContent is MsgContent.MCReport) { + upsertChatItem(rh, cInfo, cItem) + } + } } } @@ -3014,10 +3074,14 @@ object ChatController { val notify = { ntfManager.notifyMessageReceived(rh, user, cInfo, cItem) } if (!activeUser(rh, user)) { notify() - } else if (withChats { upsertChatItem(rh, cInfo, cItem) }) { - notify() - } else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) { - notify() + } else { + val createdChat = withChats { upsertChatItem(rh, cInfo, cItem) } + withReportsChatsIfOpen { if (cItem.content.msgContent is MsgContent.MCReport) { upsertChatItem(rh, cInfo, cItem) } } + if (createdChat) { + notify() + } else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) { + notify() + } } } @@ -3062,6 +3126,11 @@ object ChatController { chats.clear() popChatCollector.clear() } + withReportsChatsIfOpen { + chatItems.clearAndNotify() + chats.clear() + popChatCollector.clear() + } } val statuses = apiGetNetworkStatuses(rhId) if (statuses != null) { @@ -3204,7 +3273,7 @@ sealed class CC { class ApiGetSettings(val settings: AppSettings): CC() class ApiGetChatTags(val userId: Long): CC() class ApiGetChats(val userId: Long): CC() - class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC() + class ApiGetChat(val type: ChatType, val id: Long, val contentFilter: ContentFilter?, val pagination: ChatPagination, val search: String = ""): CC() class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC() class ApiSendMessages(val type: ChatType, val id: Long, val live: Boolean, val ttl: Int?, val composedMessages: List): CC() class ApiCreateChatTag(val tag: ChatTagData): CC() @@ -3366,7 +3435,14 @@ sealed class CC { is ApiGetSettings -> "/_get app settings ${json.encodeToString(settings)}" is ApiGetChatTags -> "/_get tags $userId" is ApiGetChats -> "/_get chats $userId pcc=on" - is ApiGetChat -> "/_get chat ${chatRef(type, id)} ${pagination.cmdString}" + (if (search == "") "" else " search=$search") + is ApiGetChat -> { + val filter = if (contentFilter == null) { + "" + } else { + " content=${contentFilter.mcTag.name.lowercase()} deleted=${onOff(contentFilter.deleted ?: true)}" + } + "/_get chat ${chatRef(type, id)}$filter ${pagination.cmdString}" + (if (search == "") "" else " search=$search") + } is ApiGetChatItemInfo -> "/_get item info ${chatRef(type, id)} $itemId" is ApiSendMessages -> { val msgs = json.encodeToString(composedMessages) @@ -3703,6 +3779,11 @@ data class NewUser( val pastTimestamp: Boolean ) +data class ContentFilter( + val mcTag: MsgContentTag, + val deleted: Boolean? +) + sealed class ChatPagination { class Last(val count: Int): ChatPagination() class After(val chatItemId: Long, val count: Int): ChatPagination() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index 475ebfd3c1..02dab5f294 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -26,6 +26,7 @@ import chat.simplex.common.platform.* import chat.simplex.common.views.chat.item.CONSOLE_COMPOSE_LAYOUT_ID import chat.simplex.common.views.chat.item.AdaptingBottomPaddingLayout import chat.simplex.common.views.chatlist.NavigationBarBackground +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch @@ -54,6 +55,10 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState IntRange = { 0 .. 0 } ) = coroutineScope { - val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, pagination, search) ?: return@coroutineScope + val (chat, navInfo) = chatModel.controller.apiGetChat(rhId, chatType, apiId, contentFilter, pagination, search) ?: return@coroutineScope // For .initial allow the chatItems to be empty as well as chatModel.chatId to not match this chat because these values become set after .initial finishes if (((chatModel.chatId.value != chat.id || chat.chatItems.isEmpty()) && pagination !is ChatPagination.Initial && pagination !is ChatPagination.Last) || !isActive) return@coroutineScope + val chatState = chatModel.chatStateForContent(contentFilter?.mcTag) + val contentTag = contentFilter?.mcTag val (splits, unreadAfterItemId, totalAfter, unreadTotal, unreadAfter, unreadAfterNewestLoaded) = chatState - val oldItems = chatModel.chatItems.value + val oldItems = chatModel.chatItemsForContent(contentFilter?.mcTag).value val newItems = SnapshotStateList() when (pagination) { is ChatPagination.Initial -> { val newSplits = if (chat.chatItems.isNotEmpty() && navInfo.afterTotal > 0) listOf(chat.chatItems.last().id) else emptyList() - withChats { - if (chatModel.getChat(chat.id) == null) { + withChats(contentTag) { + if (getChat(chat.id) == null) { addChat(chat) + } else { + updateChatInfo(chat.remoteHostId, chat.chatInfo) } } - withChats { + withChats(contentTag) { chatModel.chatItemStatuses.clear() chatItems.replaceAll(chat.chatItems) chatModel.chatId.value = chat.chatInfo.id @@ -70,7 +76,7 @@ suspend fun apiLoadMessages( ) val insertAt = (indexInCurrentItems - (wasSize - newItems.size) + trimmedIds.size).coerceAtLeast(0) newItems.addAll(insertAt, chat.chatItems) - withChats { + withChats(contentTag) { chatItems.replaceAll(newItems) splits.value = newSplits chatState.moveUnreadAfterItem(oldUnreadSplitIndex, newUnreadSplitIndex, oldItems) @@ -89,7 +95,7 @@ suspend fun apiLoadMessages( val indexToAdd = min(indexInCurrentItems + 1, newItems.size) val indexToAddIsLast = indexToAdd == newItems.size newItems.addAll(indexToAdd, chat.chatItems) - withChats { + withChats(contentTag) { chatItems.replaceAll(newItems) splits.value = newSplits chatState.moveUnreadAfterItem(splits.value.firstOrNull() ?: newItems.last().id, newItems) @@ -104,7 +110,7 @@ suspend fun apiLoadMessages( val newSplits = removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed) // currently, items will always be added on top, which is index 0 newItems.addAll(0, chat.chatItems) - withChats { + withChats(contentTag) { chatItems.replaceAll(newItems) splits.value = listOf(chat.chatItems.last().id) + newSplits unreadAfterItemId.value = chat.chatItems.last().id @@ -119,7 +125,7 @@ suspend fun apiLoadMessages( newItems.addAll(oldItems) removeDuplicates(newItems, chat) newItems.addAll(chat.chatItems) - withChats { + withChats(contentTag) { chatItems.replaceAll(newItems) unreadAfterNewestLoaded.value = 0 } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index cf58267710..29a2c1e80a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -33,6 +33,7 @@ import chat.simplex.common.model.ChatModel.activeCall import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.markChatTagRead import chat.simplex.common.model.ChatModel.withChats +import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* import chat.simplex.common.views.chat.group.* @@ -57,10 +58,17 @@ data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val dat @Composable // staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat // to chat list smooth. Otherwise, chat view will become blank right before the transition starts -fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: suspend (chatId: String) -> Unit) { - val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } +fun ChatView( + staleChatId: State, + reportsView: Boolean, + scrollToItemId: MutableState = remember { mutableStateOf(null) }, + onComposed: suspend (chatId: String) -> Unit +) { val showSearch = rememberSaveable { mutableStateOf(false) } + // They have their own iterator inside for a reason to prevent crash "Reading a state that was created after the snapshot..." + val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } } + val activeChatStats = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatStats } } val user = chatModel.currentUser.value val chatInfo = activeChatInfo.value if (chatInfo == null || user == null) { @@ -70,7 +78,11 @@ fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: susp } } else { val showArchivedReports = remember { mutableStateOf(false) } - val groupReports = remember { derivedStateOf { GroupReports((activeChatInfo.value as? ChatInfo.Group)?.apiId?.toInt() ?: 0, reportsView, showArchivedReports.value) } } + val groupReports = remember { derivedStateOf { + val reportsCount = if (activeChatInfo.value is ChatInfo.Group) activeChatStats.value?.reportsCount ?: 0 else 0 + GroupReports(reportsCount, reportsView, showArchivedReports.value) } + } + val reversedChatItems = remember { derivedStateOf { chatModel.chatItemsForContent(groupReports.value.contentTag).value.asReversed() } } val searchText = rememberSaveable { mutableStateOf("") } val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() val composeState = rememberSaveable(saver = ComposeState.saver()) { @@ -126,13 +138,14 @@ fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: susp val c = chatModel.getChat(chatInfo.id) ?: return@onSearchValueChanged if (chatModel.chatId.value != chatInfo.id) return@onSearchValueChanged withBGApi { - apiFindMessages(c, value) + apiFindMessages(c, value, groupReports.value.toContentFilter()) searchText.value = value } } ChatLayout( remoteHostId = remoteHostId, chatInfo = activeChatInfo, + reversedChatItems = reversedChatItems, unreadCount, composeState, composeView = { @@ -161,7 +174,7 @@ fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: susp } } else { SelectedItemsBottomToolbar( - chatItems = remember { chatModel.chatItems }.value, + reversedChatItems = reversedChatItems, selectedChatItems = selectedChatItems, chatInfo = chatInfo, deleteItems = { canDeleteForAll -> @@ -223,6 +236,8 @@ fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: susp } }, groupReports, + showArchivedReports, + scrollToItemId, attachmentOption, attachmentBottomSheetState, searchText, @@ -291,28 +306,23 @@ fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: susp } }, showGroupReports = { + val info = activeChatInfo.value ?: return@ChatLayout if (ModalManager.end.hasModalsOpen()) { ModalManager.end.closeModals() return@ChatLayout } hideKeyboard(view) - ModalManager.end.showCustomModal(true) { close -> - ModalView({}, showAppBar = false) { - val oneHandUI = remember { appPrefs.oneHandUI.state } - val chatInfo = remember { activeChatInfo }.value - if (chatInfo is ChatInfo.Group) { - GroupReportsView(staleChatId) - if (oneHandUI.value) { - StatusBarBackground() - } - Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) { - GroupReportsAppBar(groupReports, close, showArchived = { showHide -> - showArchivedReports.value = showHide - }, onSearchValueChanged) - } - } else { - LaunchedEffect(Unit) { - close() + scope.launch { + openChat(chatModel.remoteHostId(), info, ContentFilter(MsgContentTag.Report, false)) + ModalManager.end.showCustomModal(true, id = ModalViewId.GROUP_REPORTS) { close -> + ModalView({}, showAppBar = false) { + val chatInfo = remember { activeChatInfo }.value + if (chatInfo is ChatInfo.Group) { + GroupReportsView(staleChatId, scrollToItemId) + } else { + LaunchedEffect(Unit) { + close() + } } } } @@ -341,16 +351,16 @@ fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: susp } } }, - loadMessages = { chatId, pagination, chatState, visibleItemIndexes -> + loadMessages = { chatId, pagination, visibleItemIndexes -> val c = chatModel.getChat(chatId) if (chatModel.chatId.value != chatId) return@ChatLayout if (c != null) { - apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, pagination, chatState, searchText.value, visibleItemIndexes) + apiLoadMessages(c.remoteHostId, c.chatInfo.chatType, c.chatInfo.apiId, groupReports.value.toContentFilter(), pagination, searchText.value, visibleItemIndexes) } }, deleteMessage = { itemId, mode -> withBGApi { - val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } + val toDeleteItem = reversedChatItems.value.lastOrNull { it.id == itemId } val toModerate = toDeleteItem?.memberToModerate(chatInfo) val groupInfo = toModerate?.first val groupMember = toModerate?.second @@ -382,6 +392,13 @@ fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: susp removeChatItem(chatRh, chatInfo, deletedChatItem) } } + withReportsChatsIfOpen { + if (toChatItem != null) { + upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + removeChatItem(chatRh, chatInfo, deletedChatItem) + } + } } } }, @@ -556,6 +573,12 @@ fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: susp itemsIds ) } + withReportsChatsIfOpen { + // It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace + withContext(Dispatchers.Main) { + markChatItemsRead(chatRh, chatInfo, itemsIds) + } + } } }, markChatRead = { @@ -572,6 +595,12 @@ fun ChatView(staleChatId: State, reportsView: Boolean, onComposed: susp chatInfo.apiId ) } + withReportsChatsIfOpen { + // It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace + withContext(Dispatchers.Main) { + markChatItemsRead(chatRh, chatInfo) + } + } } }, changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) }, @@ -632,10 +661,13 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) fun ChatLayout( remoteHostId: State, chatInfo: State, + reversedChatItems: State>, unreadCount: State, composeState: MutableState, composeView: (@Composable () -> Unit), groupReports: State, + showArchivedReports: MutableState, + scrollToItemId: MutableState, attachmentOption: MutableState, attachmentBottomSheetState: ModalBottomSheetState, searchValue: State, @@ -646,7 +678,7 @@ fun ChatLayout( info: () -> Unit, showGroupReports: () -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit, - loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, + loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, receiveFile: (Long) -> Unit, @@ -718,8 +750,8 @@ fun ChatLayout( override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float = 0f }) { ChatItemsList( - remoteHostId, chatInfo, unreadCount, composeState, composeViewHeight, searchValue, - useLinkPreviews, linkMode, groupReports, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, + remoteHostId, chatInfo, reversedChatItems, unreadCount, composeState, composeViewHeight, searchValue, + useLinkPreviews, linkMode, groupReports, scrollToItemId, selectedChatItems, showMemberInfo, showChatInfo = info, loadMessages, deleteMessage, deleteMessages, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, setReaction, showItemDetails, markItemsRead, markChatRead, remember { { onComposed(it) } }, developerTools, showViaProxy, @@ -773,6 +805,16 @@ fun ChatLayout( GroupReportsToolbar(groupReports, withStatusBar = false, showGroupReports) } } + if (groupReports.value.reportsView) { + if (oneHandUI.value) { + StatusBarBackground() + } + Box(Modifier.align(if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart)) { + GroupReportsAppBar(groupReports, { ModalManager.end.closeModal() }, showArchived = { showHide -> + showArchivedReports.value = showHide + }, onSearchValueChanged) + } + } } } } @@ -1017,7 +1059,7 @@ private fun GroupReportsToolbar( ) { Icon(painterResource(MR.images.ic_flag), null, Modifier.size(22.dp), tint = MaterialTheme.colors.error) Spacer(Modifier.width(4.dp)) - val reports = groupReports.value.activeReports + val reports = groupReports.value.reportsCount Text( if (reports == 1) { stringResource(MR.strings.group_reports_active_one) @@ -1040,6 +1082,7 @@ private fun ContactVerifiedShield() { fun BoxScope.ChatItemsList( remoteHostId: Long?, chatInfo: ChatInfo, + reversedChatItems: State>, unreadCount: State, composeState: MutableState, composeViewHeight: State, @@ -1047,10 +1090,11 @@ fun BoxScope.ChatItemsList( useLinkPreviews: Boolean, linkMode: SimplexLinkMode, groupReports: State, + scrollToItemId: MutableState, selectedChatItems: MutableState?>, showMemberInfo: (GroupInfo, GroupMember) -> Unit, showChatInfo: () -> Unit, - loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, + loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, receiveFile: (Long) -> Unit, @@ -1075,9 +1119,8 @@ fun BoxScope.ChatItemsList( showViaProxy: Boolean ) { val searchValueIsEmpty = remember { derivedStateOf { searchValue.value.isEmpty() } } - val reversedChatItems = remember { derivedStateOf { chatModel.chatItems.asReversed() } } val revealedItems = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(setOf()) } - val mergedItems = remember { derivedStateOf { MergedItems.create(reversedChatItems.value, unreadCount, revealedItems.value, chatModel.chatState) } } + val mergedItems = remember { derivedStateOf { MergedItems.create(reversedChatItems.value, unreadCount, revealedItems.value, chatModel.chatStateForContent(groupReports.value.contentTag)) } } val topPaddingToContentPx = rememberUpdatedState(with(LocalDensity.current) { topPaddingToContent(chatView = !groupReports.value.reportsView, groupReports.value.showBar).roundToPx() }) /** determines height based on window info and static height of two AppBars. It's needed because in the first graphic frame height of * [composeViewHeight] is unknown, but we need to set scroll position for unread messages already so it will be correct before the first frame appears @@ -1098,11 +1141,11 @@ fun BoxScope.ChatItemsList( val animatedScrollingInProgress = remember { mutableStateOf(false) } val ignoreLoadingRequests = remember(remoteHostId) { mutableSetOf() } if (!loadingMoreItems.value) { - PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination -> + PreloadItems(chatInfo.id, if (searchValueIsEmpty.value) ignoreLoadingRequests else mutableSetOf(), reversedChatItems, mergedItems, listState, ChatPagination.UNTIL_PRELOAD_COUNT) { chatId, pagination -> if (loadingMoreItems.value) return@PreloadItems false try { loadingMoreItems.value = true - loadMessages(chatId, pagination, chatModel.chatState) { + loadMessages(chatId, pagination) { visibleItemIndexesNonReversed(mergedItems, listState.value) } } finally { @@ -1116,21 +1159,27 @@ fun BoxScope.ChatItemsList( val chatInfoUpdated = rememberUpdatedState(chatInfo) val highlightedItems = remember { mutableStateOf(setOf()) } val scope = rememberCoroutineScope() - val scrollToItem: (Long) -> Unit = remember { scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) } - val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem) } - - LoadLastItems(loadingMoreItems, remoteHostId, chatInfo) - SmallScrollOnNewMessage(listState, chatModel.chatItems) + val scrollToItem: (Long) -> Unit = remember { + // In group reports just set the itemId to scroll to so the main ChatView will handle scrolling + if (groupReports.value.reportsView) return@remember { scrollToItemId.value = it } + scrollToItem(searchValue, loadingMoreItems, animatedScrollingInProgress, highlightedItems, chatInfoUpdated, maxHeight, scope, reversedChatItems, mergedItems, listState, loadMessages) + } + val scrollToQuotedItemFromItem: (Long) -> Unit = remember { findQuotedItemFromItem(remoteHostIdUpdated, chatInfoUpdated, scope, scrollToItem, groupReports.value.contentTag) } + if (!groupReports.value.reportsView) { + LaunchedEffect(Unit) { snapshotFlow { scrollToItemId.value }.filterNotNull().collect { scrollToItem(it); scrollToItemId.value = null } } + } + LoadLastItems(loadingMoreItems, remoteHostId, chatInfo, groupReports) + SmallScrollOnNewMessage(listState, reversedChatItems) val finishedInitialComposition = remember { mutableStateOf(false) } NotifyChatListOnFinishingComposition(finishedInitialComposition, chatInfo, revealedItems, listState, onComposed) DisposableEffectOnGone( always = { - chatModel.chatItemsChangesListener = recalculateChatStatePositions(chatModel.chatState) + chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(groupReports.value.contentTag)), groupReports.value.contentTag) }, whenGone = { VideoPlayerHolder.releaseAll() - chatModel.chatItemsChangesListener = null + chatModel.setChatItemsChangeListenerForContent(recalculateChatStatePositions(chatModel.chatStateForContent(groupReports.value.contentTag)), groupReports.value.contentTag) } ) @@ -1152,7 +1201,7 @@ fun BoxScope.ChatItemsList( LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() ) { val provider = { - providerForGallery(chatModel.chatItems.value, cItem.id) { indexInReversed -> + providerForGallery(reversedChatItems.value.asReversed(), cItem.id) { indexInReversed -> itemScope.launch { listState.value.scrollToItem( min(reversedChatItems.value.lastIndex, indexInReversed + 1), @@ -1177,7 +1226,7 @@ fun BoxScope.ChatItemsList( highlightedItems.value = setOf() } } - ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, highlighted = highlighted, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems, reversedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, scrollToQuotedItemFromItem = scrollToQuotedItemFromItem, setReaction = setReaction, showItemDetails = showItemDetails, reveal = reveal, showMemberInfo = showMemberInfo, showChatInfo = showChatInfo, developerTools = developerTools, showViaProxy = showViaProxy, itemSeparation = itemSeparation, showTimestamp = itemSeparation.timestamp) } } @@ -1363,7 +1412,7 @@ fun BoxScope.ChatItemsList( if (selectionVisible) { Box(Modifier.matchParentSize().clickable { val checked = selectedChatItems.value?.contains(cItem.id) == true - selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems) + selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems, reversedChatItems) }) } } @@ -1448,14 +1497,14 @@ fun BoxScope.ChatItemsList( } @Composable -private fun LoadLastItems(loadingMoreItems: MutableState, remoteHostId: Long?, chatInfo: ChatInfo) { +private fun LoadLastItems(loadingMoreItems: MutableState, remoteHostId: Long?, chatInfo: ChatInfo, groupReports: State) { LaunchedEffect(remoteHostId, chatInfo.id) { try { loadingMoreItems.value = true - if (chatModel.chatState.totalAfter.value <= 0) return@LaunchedEffect + if (chatModel.chatStateForContent(groupReports.value.contentTag).totalAfter.value <= 0) return@LaunchedEffect delay(500) withContext(Dispatchers.Default) { - apiLoadMessages(remoteHostId, chatInfo.chatType, chatInfo.apiId, ChatPagination.Last(ChatPagination.INITIAL_COUNT), chatModel.chatState) + apiLoadMessages(remoteHostId, chatInfo.chatType, chatInfo.apiId, groupReports.value.toContentFilter(), ChatPagination.Last(ChatPagination.INITIAL_COUNT)) } } finally { loadingMoreItems.value = false @@ -1464,20 +1513,20 @@ private fun LoadLastItems(loadingMoreItems: MutableState, remoteHostId: } @Composable -private fun SmallScrollOnNewMessage(listState: State, chatItems: State>) { +private fun SmallScrollOnNewMessage(listState: State, reversedChatItems: State>) { val scrollDistance = with(LocalDensity.current) { -39.dp.toPx() } LaunchedEffect(Unit) { - var lastTotalItems = listState.value.layoutInfo.totalItemsCount - var lastItemId = chatItems.value.lastOrNull()?.id + var prevTotalItems = listState.value.layoutInfo.totalItemsCount + var newestItemId = reversedChatItems.value.firstOrNull()?.id snapshotFlow { listState.value.layoutInfo.totalItemsCount } .distinctUntilChanged() .drop(1) .collect { - val diff = listState.value.layoutInfo.totalItemsCount - lastTotalItems - val sameLastItem = lastItemId == chatItems.value.lastOrNull()?.id - lastTotalItems = listState.value.layoutInfo.totalItemsCount - lastItemId = chatItems.value.lastOrNull()?.id - if (diff < 1 || diff > 2 || sameLastItem) { + val diff = listState.value.layoutInfo.totalItemsCount - prevTotalItems + val sameNewestItem = newestItemId == reversedChatItems.value.firstOrNull()?.id + prevTotalItems = listState.value.layoutInfo.totalItemsCount + newestItemId = reversedChatItems.value.firstOrNull()?.id + if (diff < 1 || diff > 2 || sameNewestItem) { return@collect } try { @@ -1488,7 +1537,7 @@ private fun SmallScrollOnNewMessage(listState: State, chatItems: } } catch (e: CancellationException) { /** - * When you tap and hold a finger on a lazy column with chatItems, and then you receive a message, + * When you tap and hold a finger on a lazy column with reversedChatItems, and then you receive a message, * this coroutine will be canceled with the message "Current mutation had a higher priority" because of animatedScroll. * Which breaks auto-scrolling to bottom. So just ignoring the exception * */ @@ -1618,6 +1667,7 @@ fun BoxScope.FloatingButtons( fun PreloadItems( chatId: String, ignoreLoadingRequests: MutableSet, + reversedChatItems: State>, mergedItems: State, listState: State, remaining: Int, @@ -1628,8 +1678,8 @@ fun PreloadItems( val chatId = rememberUpdatedState(chatId) val loadItems = rememberUpdatedState(loadItems) val ignoreLoadingRequests = rememberUpdatedState(ignoreLoadingRequests) - PreloadItemsBefore(allowLoad, chatId, ignoreLoadingRequests, mergedItems, listState, remaining, loadItems) - PreloadItemsAfter(allowLoad, chatId, mergedItems, listState, remaining, loadItems) + PreloadItemsBefore(allowLoad, chatId, ignoreLoadingRequests, reversedChatItems, mergedItems, listState, remaining, loadItems) + PreloadItemsAfter(allowLoad, chatId, reversedChatItems, mergedItems, listState, remaining, loadItems) } @Composable @@ -1637,6 +1687,7 @@ private fun PreloadItemsBefore( allowLoad: State, chatId: State, ignoreLoadingRequests: State>, + reversedChatItems: State>, mergedItems: State, listState: State, remaining: Int, @@ -1649,7 +1700,7 @@ private fun PreloadItemsBefore( val splits = mergedItems.value.splits val lastVisibleIndex = (listState.value.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0) var lastIndexToLoadFrom: Int? = findLastIndexToLoadFromInSplits(firstVisibleIndex, lastVisibleIndex, remaining, splits) - val items = chatModel.chatItems.value + val items = reversedChatItems.value if (splits.isEmpty() && items.isNotEmpty() && lastVisibleIndex > mergedItems.value.items.size - remaining && items.size >= ChatPagination.INITIAL_COUNT) { lastIndexToLoadFrom = items.lastIndex } @@ -1663,10 +1714,10 @@ private fun PreloadItemsBefore( .filter { !ignoreLoadingRequests.value.contains(it) } .collect { loadFromItemId -> withBGApi { - val sizeWas = chatModel.chatItems.value.size - val firstItemIdWas = chatModel.chatItems.value.firstOrNull()?.id + val sizeWas = reversedChatItems.value.size + val oldestItemIdWas = reversedChatItems.value.lastOrNull()?.id val triedToLoad = loadItems.value(chatId.value, ChatPagination.Before(loadFromItemId, ChatPagination.PRELOAD_COUNT)) - if (triedToLoad && sizeWas == chatModel.chatItems.value.size && firstItemIdWas == chatModel.chatItems.value.firstOrNull()?.id) { + if (triedToLoad && sizeWas == reversedChatItems.value.size && oldestItemIdWas == reversedChatItems.value.lastOrNull()?.id) { ignoreLoadingRequests.value.add(loadFromItemId) } } @@ -1678,6 +1729,7 @@ private fun PreloadItemsBefore( private fun PreloadItemsAfter( allowLoad: MutableState, chatId: State, + reversedChatItems: State>, mergedItems: State, listState: State, remaining: Int, @@ -1698,12 +1750,12 @@ private fun PreloadItemsAfter( snapshotFlow { listState.value.firstVisibleItemIndex } .distinctUntilChanged() .map { firstVisibleIndex -> - val items = chatModel.chatItems.value + val items = reversedChatItems.value val splits = mergedItems.value.splits val split = splits.lastOrNull { it.indexRangeInParentItems.contains(firstVisibleIndex) } // we're inside a splitRange (top --- [end of the splitRange --- we're here --- start of the splitRange] --- bottom) if (split != null && split.indexRangeInParentItems.first + remaining > firstVisibleIndex) { - items.getOrNull(items.lastIndex - split.indexRangeInReversed.first)?.id + items.getOrNull(split.indexRangeInReversed.first)?.id } else { null } @@ -1984,7 +2036,7 @@ private fun scrollToItem( reversedChatItems: State>, mergedItems: State, listState: State, - loadMessages: suspend (ChatId, ChatPagination, ActiveChatState, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, + loadMessages: suspend (ChatId, ChatPagination, visibleItemIndexesNonReversed: () -> IntRange) -> Unit, ): (Long) -> Unit = { itemId: Long -> withApi { try { @@ -1998,7 +2050,7 @@ private fun scrollToItem( val pagination = ChatPagination.Around(itemId, ChatPagination.PRELOAD_COUNT * 2) val oldSize = reversedChatItems.value.size withContext(Dispatchers.Default) { - loadMessages(chatInfo.value.id, pagination, chatModel.chatState) { + loadMessages(chatInfo.value.id, pagination) { visibleItemIndexesNonReversed(mergedItems, listState.value) } } @@ -2030,14 +2082,18 @@ private fun findQuotedItemFromItem( rhId: State, chatInfo: State, scope: CoroutineScope, - scrollToItem: (Long) -> Unit + scrollToItem: (Long) -> Unit, + contentTag: MsgContentTag? ): (Long) -> Unit = { itemId: Long -> scope.launch(Dispatchers.Default) { - val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId) + val item = apiLoadSingleMessage(rhId.value, chatInfo.value.chatType, chatInfo.value.apiId, itemId, contentTag) if (item != null) { withChats { updateChatItem(chatInfo.value, item) } + withReportsChatsIfOpen { + updateChatItem(chatInfo.value, item) + } if (item.quotedItem?.itemId != null) { scrollToItem(item.quotedItem.itemId) } else { @@ -2134,7 +2190,13 @@ private fun SelectedChatItem( ) } -private fun selectUnselectChatItem(select: Boolean, ci: ChatItem, revealed: State, selectedChatItems: MutableState?>) { +private fun selectUnselectChatItem( + select: Boolean, + ci: ChatItem, + revealed: State, + selectedChatItems: MutableState?>, + reversedChatItems: State> +) { val itemIds = mutableSetOf() if (!revealed.value) { val currIndex = chatModel.getChatItemIndexOrNull(ci) @@ -2143,9 +2205,9 @@ private fun selectUnselectChatItem(select: Boolean, ci: ChatItem, revealed: Stat val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory) val range = chatViewItemsRange(currIndex, prevHidden) if (range != null) { - val reversedChatItems = chatModel.chatItems.asReversed() + val reversed = reversedChatItems.value for (i in range) { - itemIds.add(reversedChatItems[i].id) + itemIds.add(reversed[i].id) } } else { itemIds.add(ci.id) @@ -2195,6 +2257,16 @@ private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List(null) }, attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), searchValue, @@ -2545,7 +2624,7 @@ fun PreviewChatLayout() { info = {}, showGroupReports = {}, showMemberInfo = { _, _ -> }, - loadMessages = { _, _, _, _ -> }, + loadMessages = { _, _, _ -> }, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, receiveFile = { _ -> }, @@ -2606,10 +2685,13 @@ fun PreviewGroupChatLayout() { ChatLayout( remoteHostId = remember { mutableStateOf(null) }, chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, + reversedChatItems = remember { mutableStateOf(emptyList()) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = {}, groupReports = remember { mutableStateOf(GroupReports(0, false)) }, + showArchivedReports = remember { mutableStateOf(false) }, + scrollToItemId = remember { mutableStateOf(null) }, attachmentOption = remember { mutableStateOf(null) }, attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), searchValue, @@ -2620,7 +2702,7 @@ fun PreviewGroupChatLayout() { info = {}, showGroupReports = {}, showMemberInfo = { _, _ -> }, - loadMessages = { _, _, _, _ -> }, + loadMessages = { _, _, _ -> }, deleteMessage = { _, _ -> }, deleteMessages = {}, receiveFile = { _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt index 838398c503..92c23ed662 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt @@ -49,7 +49,7 @@ fun BoxScope.SelectedItemsTopToolbar(selectedChatItems: MutableState?> @Composable fun SelectedItemsBottomToolbar( chatInfo: ChatInfo, - chatItems: List, + reversedChatItems: State>, selectedChatItems: MutableState?>, deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible moderateItems: () -> Unit, @@ -108,8 +108,8 @@ fun SelectedItemsBottomToolbar( } Divider(Modifier.align(Alignment.TopStart)) } - LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) { - recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited) + LaunchedEffect(chatInfo, reversedChatItems.value, selectedChatItems.value) { + recheckItems(chatInfo, reversedChatItems.value.asReversed(), selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, forwardEnabled, deleteCountProhibited, forwardCountProhibited) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index 657c0923a5..8fe72d761a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -83,7 +83,7 @@ fun GroupMemberInfoView( getContactChat = { chatModel.getContactChat(it) }, openDirectChat = { withBGApi { - apiLoadMessages(rhId, ChatType.Direct, it, ChatPagination.Initial(ChatPagination.INITIAL_COUNT), chatModel.chatState) + apiLoadMessages(rhId, ChatType.Direct, it, null, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) if (chatModel.getContactChat(it) != null) { closeAll() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt index bc9cb671b9..eb5d5815c9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupReportsView.kt @@ -1,34 +1,40 @@ package chat.simplex.common.views.chat.group -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.height import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.views.chat.ChatView +import chat.simplex.common.views.chat.apiLoadMessages import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch data class GroupReports( - val activeReports: Int, + val reportsCount: Int, val reportsView: Boolean, val showArchived: Boolean = false ) { - val showBar: Boolean = activeReports > 0 && !reportsView + val showBar: Boolean = reportsCount > 0 && !reportsView + + fun toContentFilter(): ContentFilter? { + if (!reportsView) return null + return ContentFilter(MsgContentTag.Report, deleted = showArchived) + } + + val contentTag: MsgContentTag? = if (!reportsView) null else MsgContentTag.Report } @Composable -fun GroupReportsView(staleChatId: State) { - ChatView(staleChatId, reportsView = true, onComposed = {}) +fun GroupReportsView(staleChatId: State, scrollToItemId: MutableState) { + ChatView(staleChatId, reportsView = true, scrollToItemId, onComposed = {}) } @Composable @@ -71,8 +77,8 @@ fun GroupReportsAppBar( showSearch.value = true }) ItemAction( - if (groupReports.value.showArchived) stringResource(MR.strings.group_reports_hide_archived) else stringResource(MR.strings.group_reports_show_archived), - painterResource(MR.images.ic_add), + if (groupReports.value.showArchived) stringResource(MR.strings.group_reports_show_active) else stringResource(MR.strings.group_reports_show_archived), + painterResource(if (groupReports.value.showArchived) MR.images.ic_flag else MR.images.ic_inventory_2), onClick = { onClosedAction.value = { showArchived(!groupReports.value.showArchived) @@ -84,4 +90,30 @@ fun GroupReportsAppBar( } } ) + ItemsReload(groupReports) } + +@Composable +private fun ItemsReload(groupReports: State) { + LaunchedEffect(Unit) { + snapshotFlow { groupReports.value.showArchived to chatModel.chatId.value } + .distinctUntilChanged() + .drop(1) + .map { it.second } + .filterNotNull() + .map { chatModel.getChat(it) } + .filterNotNull() + .filter { it.chatInfo is ChatInfo.Group } + .collect { chat -> + reloadItems(chat, groupReports) + } + } +} +private suspend fun reloadItems(chat: Chat, groupReports: State) { + val contentFilter = groupReports.value.toContentFilter() + if (chat.chatStats.reportsCount > 0 || contentFilter?.deleted == true) { + apiLoadMessages(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId, contentFilter, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) + } else { + openLoadedChat(chat.copy(chatItems = emptyList()), contentTag = contentFilter?.mcTag) + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 994d56d1fc..f825646849 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -10,17 +10,11 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material.MaterialTheme.colors import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* -import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -209,27 +203,30 @@ suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) = openChat suspend fun openDirectChat(rhId: Long?, contactId: Long) = openChat(rhId, ChatType.Direct, contactId) -suspend fun openGroupChat(rhId: Long?, groupId: Long) = openChat(rhId, ChatType.Group, groupId) +suspend fun openGroupChat(rhId: Long?, groupId: Long, contentFilter: ContentFilter? = null) = openChat(rhId, ChatType.Group, groupId, contentFilter) -suspend fun openChat(rhId: Long?, chatInfo: ChatInfo) = openChat(rhId, chatInfo.chatType, chatInfo.apiId) +suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, contentFilter: ContentFilter? = null) = openChat(rhId, chatInfo.chatType, chatInfo.apiId, contentFilter) -private suspend fun openChat(rhId: Long?, chatType: ChatType, apiId: Long) = - apiLoadMessages(rhId, chatType, apiId, ChatPagination.Initial(ChatPagination.INITIAL_COUNT), chatModel.chatState) +private suspend fun openChat(rhId: Long?, chatType: ChatType, apiId: Long, contentFilter: ContentFilter? = null) = + apiLoadMessages(rhId, chatType, apiId, contentFilter, ChatPagination.Initial(ChatPagination.INITIAL_COUNT)) -suspend fun openLoadedChat(chat: Chat) { - withChats { - chatModel.chatItemStatuses.clear() +suspend fun openLoadedChat(chat: Chat, contentTag: MsgContentTag? = null) { + withChats(contentTag) { + // LALAL TODO MOVE item statuses to chats context + if (contentTag == null) { + chatModel.chatItemStatuses.clear() + } chatItems.replaceAll(chat.chatItems) chatModel.chatId.value = chat.chatInfo.id - chatModel.chatState.clear() + chatModel.chatStateForContent(contentTag).clear() } } -suspend fun apiFindMessages(ch: Chat, search: String) { - withChats { +suspend fun apiFindMessages(ch: Chat, search: String, contentFilter: ContentFilter?) { + withChats(contentFilter?.mcTag) { chatItems.clearAndNotify() } - apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), chatModel.chatState, search = search) + apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, contentFilter, pagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), search = search) } suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 933bc0c93a..d951f1f812 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -21,6 +21,7 @@ import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.withChats +import chat.simplex.common.model.ChatModel.withReportsChatsIfOpen import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -534,6 +535,11 @@ fun deleteChatDatabaseFilesAndState() { chats.clear() popChatCollector.clear() } + withReportsChatsIfOpen { + chatItems.clearAndNotify() + chats.clear() + popChatCollector.clear() + } } chatModel.users.clear() ntfManager.cancelAllNotifications() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index 819efcdd9a..54ea1c27b5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -77,8 +77,19 @@ class ModalData(val keyboardCoversBar: Boolean = true) { val appBarHandler = AppBarHandler(null, null, keyboardCoversBar = keyboardCoversBar) } +enum class ModalViewId { + GROUP_REPORTS +} + class ModalManager(private val placement: ModalPlacement? = null) { - private val modalViews = arrayListOf Unit) -> Unit)>>() + data class ModalViewHolder( + val id: ModalViewId?, + val animated: Boolean, + val data: ModalData, + val modal: @Composable ModalData.(close: () -> Unit) -> Unit + ) + + private val modalViews = arrayListOf() private val _modalCount = mutableStateOf(0) val modalCount: State = _modalCount private val toRemove = mutableSetOf() @@ -88,19 +99,21 @@ class ModalManager(private val placement: ModalPlacement? = null) { private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) private var onTimePasscodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) - fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { - showCustomModal { close -> + fun hasModalOpen(id: ModalViewId): Boolean = modalViews.any { it.id == id } + + fun showModal(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { + showCustomModal(id = id) { close -> ModalView(close, showClose = showClose, endButtons = endButtons, content = { content() }) } } - fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { - showCustomModal { close -> + fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, id: ModalViewId? = null, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { + showCustomModal(id = id) { close -> ModalView(close, showClose = showClose, endButtons = endButtons, content = { content(close) }) } } - fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, modal: @Composable ModalData.(close: () -> Unit) -> Unit) { + fun showCustomModal(animated: Boolean = true, keyboardCoversBar: Boolean = true, id: ModalViewId? = null, modal: @Composable ModalData.(close: () -> Unit) -> Unit) { Log.d(TAG, "ModalManager.showCustomModal") val data = ModalData(keyboardCoversBar = keyboardCoversBar) // Means, animation is in progress or not started yet. Do not wait until animation finishes, just remove all from screen. @@ -111,7 +124,7 @@ class ModalManager(private val placement: ModalPlacement? = null) { // Make animated appearance only on Android (everytime) and on Desktop (when it's on the start part of the screen or modals > 0) // to prevent unneeded animation on different situations val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START) - modalViews.add(Triple(anim, data, modal)) + modalViews.add(ModalViewHolder(id, anim, data, modal)) _modalCount.value = modalViews.size - toRemove.size if (placement == ModalPlacement.CENTER) { @@ -139,7 +152,7 @@ class ModalManager(private val placement: ModalPlacement? = null) { fun closeModal() { if (modalViews.isNotEmpty()) { - if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex) + if (modalViews.lastOrNull()?.animated == false) modalViews.removeAt(modalViews.lastIndex) else runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) } } _modalCount.value = modalViews.size - toRemove.size @@ -161,10 +174,10 @@ class ModalManager(private val placement: ModalPlacement? = null) { @Composable fun showInView() { // Without animation - if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) { + if (modalCount.value > 0 && modalViews.lastOrNull()?.animated == false) { modalViews.lastOrNull()?.let { - CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) { - it.third(it.second, ::closeModal) + CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.data.appBarHandler)) { + it.modal(it.data, ::closeModal) } } return @@ -179,8 +192,8 @@ class ModalManager(private val placement: ModalPlacement? = null) { } ) { modalViews.getOrNull(it - 1)?.let { - CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.second.appBarHandler)) { - it.third(it.second, ::closeModal) + CompositionLocalProvider(LocalAppBarHandler provides adjustAppBarHandler(it.data.appBarHandler)) { + it.modal(it.data, ::closeModal) } } // This is needed because if we delete from modalViews immediately on request, animation will be bad diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index d23fa3f2cf..b15b6d8d75 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -429,7 +429,7 @@ %d reports Member reports Show archived - Hide archived + Show active Share message… diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 221f1a1291..9d747206ab 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* 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.ui.theme.DEFAULT_START_MODAL_WIDTH import chat.simplex.common.ui.theme.SimpleXTheme @@ -61,6 +62,9 @@ fun showApp() { chatModel.chatId.value = null chatItems.clearAndNotify() } + withReportsChatsIfOpen { + chatItems.clearAndNotify() + } } } chatModel.activeCall.value?.let {