mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 15:48:54 +00:00
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. Completes #5909 for messages the invitee sends, edits and deletes. Chat list: - previewItem picks the row's item: for a pending invitee a message is not replaced by a content-less event, and otherwise the later item wins. - ChatInfo.inMainChatList replaces the repeated scope/memberPending tests in addChatItem, upsertChatItem and removeChatItem, so edits, status changes and deletions of a support item reach the row that shows it. - addChatItem is split into addItemToChatList, previewItem and addItemToScope. - upsertChatItem tests "chat is in the list" and "chat may be added to the list" as separate branches - only a main scope chat can be added. Sent items reach only the active chats context, so ChatModel.addSentChatItem and upsertSentChatItem update the primary context as well when the active context is a support chat shown in the main list. Cross context dispatch stays with the caller, as in processReceivedMsg; no ChatsContext method touches the other context. Chat preview row shows the message instead of the "reviewed by admins" status when there is text, or content that is rendered without text (media without a caption), which needs previews to be enabled.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+78
-68
@@ -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<Chat>())
|
||||
@@ -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<ChatItem> = 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
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user