diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index e3a6ae30b9..5de2bfdb31 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -658,21 +658,8 @@ final class ChatModel: ObservableObject { // update chat list if let i = getChatIndex(cInfo.id) { // update preview - if cInfo.groupChatScope() == nil || cInfo.groupInfo?.membership.memberPending ?? false { - chats[i].chatItems = switch cInfo { - case .group: - if let currentPreviewItem = chats[i].chatItems.first { - if cItem.meta.itemTs >= currentPreviewItem.meta.itemTs { - [cItem] - } else { - [currentPreviewItem] - } - } else { - [cItem] - } - default: - [cItem] - } + if cInfo.inMainChatList { + chats[i].chatItems = [previewItem(cInfo, chats[i].chatItems.first, cItem)] if case .rcvNew = cItem.meta.itemStatus { unreadCollector.changeUnreadCounter(cInfo.id, by: 1, unreadMentions: cItem.meta.userMention ? 1 : 0) } @@ -692,6 +679,17 @@ final class ChatModel: ObservableObject { } } + private func previewItem(_ cInfo: ChatInfo, _ currentItem: ChatItem?, _ newItem: ChatItem) -> ChatItem { + guard case let .group(groupInfo, _) = cInfo, let currentItem else { return newItem } + if groupInfo.membership.memberPending { + let currentIsMessage = currentItem.content.msgContent != nil + if currentIsMessage != (newItem.content.msgContent != nil) { + return currentIsMessage ? currentItem : newItem + } + } + return newItem.meta.itemTs < currentItem.meta.itemTs ? currentItem : newItem + } + func getCIItemsModel(_ cInfo: ChatInfo, _ ci: ChatItem) -> ItemsModel? { let cInfoScope = cInfo.groupChatScope() return if let cInfoScope = cInfoScope { @@ -718,16 +716,16 @@ final class ChatModel: ObservableObject { func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { // update chat list var itemAdded: Bool = false - if cInfo.groupChatScope() == nil { + if cInfo.inMainChatList { if let chat = getChat(cInfo.id) { if let pItem = chat.chatItems.last { - if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) { + if pItem.id == cItem.id || (cInfo.groupChatScope() == nil && chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) { chat.chatItems = [cItem] } } else { chat.chatItems = [cItem] } - } else { + } else if cInfo.groupChatScope() == nil { addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) itemAdded = true } @@ -789,7 +787,7 @@ final class ChatModel: ObservableObject { func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) { // update chat list - if cInfo.groupChatScope() == nil { + if cInfo.inMainChatList { if cItem.isRcvNew { unreadCollector.changeUnreadCounter(cInfo.id, by: -1, unreadMentions: cItem.meta.userMention ? -1 : 0) } diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 59b92a265b..2d118e1aa7 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -349,10 +349,13 @@ struct ChatPreviewView: View { } @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?, _ hasFilePreview: Bool = false) -> some View { + let memberPending = chat.chatInfo.groupInfo?.membership.memberPending ?? false + let itemHasText = cItem?.content.hasMsgContent == true + let itemContentShown = showChatPreviews && memberPending && cItem?.content.msgContent != nil if chatModel.draftChatId == chat.id, let draft = chatModel.draft { let (t, hasSecrets) = messageDraft(draft) chatPreviewLayout(t, draft: true, hasFilePreview: hasFilePreview, hasSecrets: hasSecrets) - } else if cItem?.content.hasMsgContent != true, let previewText = chatPreviewInfoText() { + } else if !itemHasText, !itemContentShown, let previewText = chatPreviewInfoText() { chatPreviewInfoTextLayout(previewText) } else if let cItem = cItem { let (t, hasSecrets) = chatItemPreview(cItem) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 7f9f1a4fcc..82bd5810eb 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1886,6 +1886,10 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { groupInfo?.useRelays == true } + public var inMainChatList: Bool { + groupChatScope() == nil || groupInfo?.membership.memberPending ?? false + } + // this works for features that are common for contacts and groups public func featureEnabled(_ feature: ChatFeature) -> Bool { switch self { 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 b532246f48..dc7ca8ec30 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 @@ -363,6 +363,20 @@ object ChatModel { } } + suspend fun addSentChatItem(activeCtx: ChatsContext, rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + activeCtx.addChatItem(rhId, cInfo, cItem) + if (activeCtx.secondaryContextFilter != null && cInfo.inMainChatList) { + chatsContext.addChatItem(rhId, cInfo, cItem) + } + } + + suspend fun upsertSentChatItem(activeCtx: ChatsContext, rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + activeCtx.upsertChatItem(rhId, cInfo, cItem) + if (activeCtx.secondaryContextFilter != null && cInfo.inMainChatList) { + chatsContext.upsertChatItem(rhId, cInfo, cItem) + } + } + // Spec: spec/state.md#ChatsContext class ChatsContext(val secondaryContextFilter: SecondaryContextFilter?) { val chats = mutableStateOf(SnapshotStateList()) @@ -520,75 +534,69 @@ object ChatModel { } suspend fun addChatItem(rhId: Long?, chatInfo: ChatInfo, cItem: ChatItem) { - // updates membersRequireAttention val cInfo = if (chatInfo is ChatInfo.Direct && chatInfo.chatDeleted) { // mark chat non deleted - val updatedContact = chatInfo.contact.copy(chatDeleted = false) - ChatInfo.Direct(updatedContact) + ChatInfo.Direct(chatInfo.contact.copy(chatDeleted = false)) } else { chatInfo } + // updates membersRequireAttention updateChatInfo(rhId, cInfo) - // update chat list - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - if (i >= 0) { - chat = chats[i] - // update preview (for chat from main scope to show new items for invitee in pending status) - if (cInfo.groupChatScope() == null || cInfo.groupInfo_?.membership?.memberPending == true) { - val newPreviewItem = when (cInfo) { - is ChatInfo.Group -> { - val currentPreviewItem = chat.chatItems.firstOrNull() - if (currentPreviewItem != null) { - if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) { - cItem - } else { - currentPreviewItem - } - } else { - cItem - } - } + addItemToChatList(rhId, cInfo, cItem) + withContext(Dispatchers.Main) { + addItemToScope(cInfo, cItem) + } + } - else -> cItem - } - val wasUnread = chat.unreadTag - chats[i] = chat.copy( - chatItems = arrayListOf(newPreviewItem), - chatStats = - if (cItem.meta.itemStatus is CIStatus.RcvNew) { - increaseUnreadCounter(rhId, currentUser.value!!) - chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1, unreadMentions = if (cItem.meta.userMention) chat.chatStats.unreadMentions + 1 else chat.chatStats.unreadMentions) - } else - chat.chatStats - ) - updateChatTagReadInPrimaryContext(chats[i], wasUnread) - } - // pop chat - if (appPlatform.isDesktop && cItem.chatDir.sent) { - reorderChat(chats[i], 0) - } else { - popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i) - } + private suspend fun addItemToChatList(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + val i = getChatIndex(rhId, cInfo.id) + if (i < 0) { + val items: List = if (cInfo.groupChatScope() == null) arrayListOf(cItem) else emptyList() + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = items)) + return + } + val chat = chats[i] + if (cInfo.inMainChatList) { + val wasUnread = chat.unreadTag + val stats = chat.chatStats + chats[i] = chat.copy( + chatItems = arrayListOf(previewItem(cInfo, chat.chatItems.firstOrNull(), cItem)), + chatStats = if (cItem.meta.itemStatus is CIStatus.RcvNew) { + increaseUnreadCounter(rhId, currentUser.value!!) + stats.copy( + unreadCount = stats.unreadCount + 1, + unreadMentions = if (cItem.meta.userMention) stats.unreadMentions + 1 else stats.unreadMentions + ) + } else stats + ) + updateChatTagReadInPrimaryContext(chats[i], wasUnread) + } + if (appPlatform.isDesktop && cItem.chatDir.sent) { + reorderChat(chats[i], 0) } else { - if (cInfo.groupChatScope() == null) { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = emptyList())) + popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i) + } + } + + private fun previewItem(cInfo: ChatInfo, currentItem: ChatItem?, newItem: ChatItem): ChatItem { + if (cInfo !is ChatInfo.Group || currentItem == null) return newItem + if (cInfo.groupInfo.membership.memberPending) { + val currentIsMessage = currentItem.content.msgContent != null + if (currentIsMessage != (newItem.content.msgContent != null)) { + return if (currentIsMessage) currentItem else newItem } } - // add to current scope - withContext(Dispatchers.Main) { - if (chatItemBelongsToScope(cInfo, cItem)) { - // Prevent situation when chat item already in the list received from backend - if (chatItems.value.none { it.id == cItem.id }) { - if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - addToChatItems(kotlin.math.max(0, chatItems.value.lastIndex), cItem) - } else { - addToChatItems(cItem) - } - } - } + return if (newItem.meta.itemTs < currentItem.meta.itemTs) currentItem else newItem + } + + private fun addItemToScope(cInfo: ChatInfo, cItem: ChatItem) { + if (!chatItemBelongsToScope(cInfo, cItem)) return + // Prevent situation when chat item already in the list received from backend + if (chatItems.value.any { it.id == cItem.id }) return + if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + addToChatItems(kotlin.math.max(0, chatItems.value.lastIndex), cItem) + } else { + addToChatItems(cItem) } } @@ -611,11 +619,10 @@ object ChatModel { suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean { var itemAdded = false // update chat list - if (cInfo.groupChatScope() == null) { - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - if (i >= 0) { - chat = chats[i] + val i = getChatIndex(rhId, cInfo.id) + if (i >= 0) { + if (cInfo.inMainChatList) { + val chat = chats[i] val pItem = chat.chatItems.lastOrNull() if (pItem?.id == cItem.id) { chats[i] = chat.copy(chatItems = arrayListOf(cItem)) @@ -624,10 +631,10 @@ object ChatModel { decreaseCounterInPrimaryContext(rhId, cInfo.id) } } - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - itemAdded = true } + } else if (cInfo.groupChatScope() == null) { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + itemAdded = true } // update current scope withContext(Dispatchers.Main) { @@ -669,7 +676,7 @@ object ChatModel { fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { // update chat list - if (cInfo.groupChatScope() == null) { + if (cInfo.inMainChatList) { if (cItem.isRcvNew) { decreaseCounterInPrimaryContext(rhId, cInfo.id) } @@ -1821,6 +1828,9 @@ sealed class ChatInfo: SomeChat, NamedChat { val isChannel: Boolean get() = groupInfo_?.useRelays == true + + val inMainChatList: Boolean + get() = groupChatScope() == null || groupInfo_?.membership?.memberPending == true } @Serializable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 6bfbad52ef..0e43d2dc05 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -583,7 +583,7 @@ fun ComposeView( if (!chatItems.isNullOrEmpty()) { chatItems.forEach { aChatItem -> withContext(Dispatchers.Main) { - chatsCtx.addChatItem(chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem) + chatModel.addSentChatItem(chatsCtx, chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem) } } return chatItems.first().chatItem @@ -725,7 +725,7 @@ fun ComposeView( withContext(Dispatchers.Main) { chatItems?.forEach { chatItem -> - chatsCtx.addChatItem(rhId, chat.chatInfo, chatItem) + chatModel.addSentChatItem(chatsCtx, rhId, chat.chatInfo, chatItem) } } @@ -803,7 +803,7 @@ fun ComposeView( ) if (updatedItem != null) { withContext(Dispatchers.Main) { - chatsCtx.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + chatModel.upsertSentChatItem(chatsCtx, chat.remoteHostId, cInfo, updatedItem.chatItem) } } return updatedItem?.chatItem diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index cbd15aca67..50d9732cdc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -442,7 +442,7 @@ fun sendCommandMsg(chatsCtx: ChatModel.ChatsContext, chat: Chat, msg: String) { if (!chatItems.isNullOrEmpty()) { chatItems.forEach { aChatItem -> withContext(Dispatchers.Main) { - chatsCtx.addChatItem(chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem) + chatModel.addSentChatItem(chatsCtx, chat.remoteHostId, aChatItem.chatInfo, aChatItem.chatItem) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index fbc4c7e336..8240d129f0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -226,6 +226,9 @@ fun ChatPreviewView( fun chatPreviewText() { val previewText = chatPreviewInfoText() val ci = chat.chatItems.lastOrNull() + val memberPending = cInfo.groupInfo_?.membership?.memberPending == true + val itemHasText = ci?.content?.hasMsgContent == true + val itemContentShown = showChatPreviews && memberPending && ci?.content?.msgContent != null if (chatModelDraftChatId == chat.id && chatModelDraft != null) { val sp20 = with(LocalDensity.current) { 20.sp.toDp() } val (text: CharSequence, inlineTextContent) = remember(chatModelDraft) { chatModelDraft.message.text to messageDraft(chatModelDraft, sp20) } @@ -246,7 +249,7 @@ fun ChatPreviewView( inlineContent = inlineTextContent, modifier = Modifier.fillMaxWidth() ) - } else if (ci?.content?.hasMsgContent != true && previewText != null) { + } else if (!itemHasText && !itemContentShown && previewText != null) { Text(previewText.first, color = previewText.second) } else if (ci != null && showChatPreviews) { val (text: CharSequence, inlineTextContent) = when {