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).
This commit is contained in:
Narasimha-sc committed 2026-10-19 08:39:55 +00:00
1 parent c571762b00
commit 6525367493
6 files changed
+63 -41

No files matched your search

+22 -19
View File
@@ -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)
}
@@ -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)
+4
View File
@@ -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 {
@@ -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<Chat>())
@@ -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)
}
@@ -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
@@ -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 {