From 6525367493142ef80dfed498740966a69345e863 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:29:39 +0000 Subject: [PATCH] android, desktop, ios: show pending invitee's support messages in chat list preview A member pending review has no main scope to write in, so their "chat with admins" messages are what the group's row in the chat list should show. New support items reach the row since #5909 and #7072; this covers edits, status changes and deletions of those items, the invitee's own edits, and which item the row shows. Chat list: - upsertChatItem and removeChatItem use ChatInfo.inMainChatList (added on iOS here, as in Kotlin) instead of the main-scope test, so edits, status changes and deletions of a support item reach the row that shows it; only a main scope chat can still be added to the list. On iOS, an item not loaded in the open chat replaces the preview only for main-scope items. - previewItem, extracted from addChatItem on both platforms, picks the row's item: for a pending invitee the onboarding events (pending review, "end-to-end encrypted" info, member connected) do not replace a message, and a message replaces them, whatever their timestamps; otherwise the later item wins. Sent edits reach only the active chats context, so ChatModel.upsertSentChatItem updates the primary context as well when the active context is a support chat shown in the main list, as addSentChatItem does for new items. Cross context dispatch stays with the caller, as in processReceivedMsg; no ChatsContext method touches the other context. The chat preview row shows the message instead of the "reviewed by admins" status when there is text or, with previews enabled, content that is rendered without text (a media item without a caption). --- apps/ios/Shared/Model/ChatModel.swift | 41 ++++++++-------- .../Views/ChatList/ChatPreviewView.swift | 5 +- apps/ios/SimpleXChat/ChatTypes.swift | 4 ++ .../chat/simplex/common/model/ChatModel.kt | 47 +++++++++++-------- .../simplex/common/views/chat/ComposeView.kt | 2 +- .../common/views/chatlist/ChatPreviewView.kt | 5 +- 6 files changed, 63 insertions(+), 41 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 1ee79e1c4f..51c934abbc 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -714,21 +714,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) } @@ -748,6 +735,22 @@ 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 { + if isOnboardingEvent(newItem) && currentItem.content.msgContent != nil { return currentItem } + if isOnboardingEvent(currentItem) && newItem.content.msgContent != nil { return newItem } + } + return newItem.meta.itemTs < currentItem.meta.itemTs ? currentItem : newItem + } + + private func isOnboardingEvent(_ item: ChatItem) -> Bool { + switch item.content { + case .sndGroupEvent(.userPendingReview), .rcvGroupEvent(.memberConnected), .rcvGroupE2EEInfo: true + default: false + } + } + func getCIItemsModel(_ cInfo: ChatInfo, _ ci: ChatItem) -> ItemsModel? { let cInfoScope = cInfo.groupChatScope() return if let cInfoScope = cInfoScope { @@ -774,16 +777,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 } @@ -845,7 +848,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 8156ea81c7..c5d895c20d 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 a4d65a6d10..c6ab9c5dba 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2170,6 +2170,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 0df6d5e78c..5be305049f 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 @@ -421,6 +421,13 @@ object ChatModel { } } + 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()) @@ -594,22 +601,7 @@ object ChatModel { chat = chats[i] // update preview (for chat from main scope to show new items for invitee in pending status) if (cInfo.inMainChatList) { - val newPreviewItem = when (cInfo) { - is ChatInfo.Group -> { - val currentPreviewItem = chat.chatItems.firstOrNull() - if (currentPreviewItem != null) { - if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) { - cItem - } else { - currentPreviewItem - } - } else { - cItem - } - } - - else -> cItem - } + val newPreviewItem = previewItem(cInfo, chat.chatItems.firstOrNull(), cItem) val wasUnread = chat.unreadTag chats[i] = chat.copy( chatItems = arrayListOf(newPreviewItem), @@ -650,6 +642,23 @@ object ChatModel { } } + private fun previewItem(cInfo: ChatInfo, currentItem: ChatItem?, newItem: ChatItem): ChatItem { + if (cInfo !is ChatInfo.Group || currentItem == null) return newItem + if (cInfo.groupInfo.membership.memberPending) { + if (isOnboardingEvent(newItem) && currentItem.content.msgContent != null) return currentItem + if (isOnboardingEvent(currentItem) && newItem.content.msgContent != null) return newItem + } + return if (newItem.meta.itemTs < currentItem.meta.itemTs) currentItem else newItem + } + + private fun isOnboardingEvent(item: ChatItem): Boolean = + when (val c = item.content) { + is CIContent.SndGroupEventContent -> c.sndGroupEvent is SndGroupEvent.UserPendingReview + is CIContent.RcvGroupEventContent -> c.rcvGroupEvent is RcvGroupEvent.MemberConnected + is CIContent.RcvGroupE2EEInfo -> true + else -> false + } + private fun chatItemBelongsToScope(cInfo: ChatInfo, cItem: ChatItem): Boolean = when (secondaryContextFilter) { null -> @@ -669,7 +678,7 @@ object ChatModel { suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean { var itemAdded = false // update chat list - if (cInfo.groupChatScope() == null) { + if (cInfo.inMainChatList) { val i = getChatIndex(rhId, cInfo.id) val chat: Chat if (i >= 0) { @@ -682,7 +691,7 @@ object ChatModel { decreaseCounterInPrimaryContext(rhId, cInfo.id) } } - } else { + } else if (cInfo.groupChatScope() == null) { addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) itemAdded = true } @@ -727,7 +736,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) } 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 a15fe26e30..dd5fe87f97 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 @@ -819,7 +819,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/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 7e11fdfb97..13a635b444 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 {