mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 15:07:32 +00:00
ios, android, desktop: add updates only when the newest items are loaded, keep statuses of sent items until they are added
This commit is contained in:
1 parent
3bcdd12743
commit
44eb0baf7f
3 files changed
+55
-14
No files matched your search
@@ -85,6 +85,8 @@ class ItemsModel: ObservableObject {
|
||||
}
|
||||
|
||||
let chatState = ActiveChatState()
|
||||
var sndStatusesChatId: ChatId? = nil
|
||||
var sndStatusesBeforeAdd: [Int64: CIStatus] = [:]
|
||||
|
||||
// Publishes directly to `objectWillChange` publisher,
|
||||
// this will cause reversedChatItems to be rendered without throttling
|
||||
@@ -744,7 +746,12 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
// add to current scope
|
||||
if let ciIM = getCIItemsModel(cInfo, cItem) {
|
||||
_ = _upsertChatItem(ciIM, cInfo, cItem, add: true)
|
||||
var ci = cItem
|
||||
if let keptStatus = ciIM.sndStatusesBeforeAdd.removeValue(forKey: cItem.id),
|
||||
shouldKeepOldSndCIStatus(oldStatus: keptStatus, newStatus: cItem.meta.itemStatus) {
|
||||
ci.meta.itemStatus = keptStatus
|
||||
}
|
||||
_ = _upsertChatItem(ciIM, cInfo, ci, add: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -779,7 +786,7 @@ final class ChatModel: ObservableObject {
|
||||
if let chat = getChat(cInfo.id) {
|
||||
if let pItem = chat.chatItems.last {
|
||||
lastItemId = pItem.id
|
||||
if pItem.id == cItem.id || (chatId == cInfo.id && cItem.id > max(pItem.id, im.reversedChatItems.lazy.map(\.id).max() ?? 0)) {
|
||||
if pItem.id == cItem.id || (chatId == cInfo.id && im.lastItemsLoaded && cItem.id > max(pItem.id, im.reversedChatItems.lazy.map(\.id).max() ?? 0)) {
|
||||
chat.chatItems = [cItem]
|
||||
}
|
||||
} else {
|
||||
@@ -795,8 +802,7 @@ final class ChatModel: ObservableObject {
|
||||
}
|
||||
// update current scope
|
||||
if let ciIM = getCIItemsModel(cInfo, cItem) {
|
||||
// an update of an earlier item that is not loaded is not added, it is shown when loaded
|
||||
let add = cItem.id > max(lastItemId, ciIM.reversedChatItems.lazy.map(\.id).max() ?? 0)
|
||||
let add = ciIM.lastItemsLoaded && cItem.id > max(lastItemId, ciIM.reversedChatItems.lazy.map(\.id).max() ?? 0)
|
||||
itemAdded = _upsertChatItem(ciIM, cInfo, cItem, add: add)
|
||||
}
|
||||
return itemAdded
|
||||
@@ -820,6 +826,13 @@ final class ChatModel: ObservableObject {
|
||||
ChatItemDummyModel.shared.sendUpdate()
|
||||
return true
|
||||
} else {
|
||||
if cItem.chatDir.sent && !cItem.meta.itemStatus.isSndRcvd {
|
||||
if ciIM.sndStatusesChatId != cInfo.id {
|
||||
ciIM.sndStatusesChatId = cInfo.id
|
||||
ciIM.sndStatusesBeforeAdd.removeAll()
|
||||
}
|
||||
ciIM.sndStatusesBeforeAdd[cItem.id] = cItem.meta.itemStatus
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
+20
-4
@@ -424,6 +424,8 @@ object ChatModel {
|
||||
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
|
||||
// set listener here that will be notified on every add/delete of a chat item
|
||||
val chatState = ActiveChatState()
|
||||
private var sndStatusesChatId: String? = null
|
||||
private val sndStatusesBeforeAdd = mutableMapOf<Long, CIStatus>()
|
||||
|
||||
fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null
|
||||
fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id }
|
||||
@@ -631,12 +633,18 @@ object ChatModel {
|
||||
// add to current scope
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatItemBelongsToScope(cInfo, cItem)) {
|
||||
val keptStatus = sndStatusesBeforeAdd.remove(cItem.id)
|
||||
val ci = if (keptStatus != null && shouldKeepOldSndCIStatus(keptStatus, cItem.meta.itemStatus)) {
|
||||
cItem.copy(meta = cItem.meta.copy(itemStatus = keptStatus))
|
||||
} else {
|
||||
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)
|
||||
addToChatItems(kotlin.math.max(0, chatItems.value.lastIndex), ci)
|
||||
} else {
|
||||
addToChatItems(cItem)
|
||||
addToChatItems(ci)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -699,10 +707,18 @@ object ChatModel {
|
||||
cItem
|
||||
}
|
||||
items[itemIndex] = ci
|
||||
} else if (cItem.id > maxOf(lastItemId ?: 0, items.maxOfOrNull { it.id } ?: 0)) {
|
||||
// an update of an earlier item that is not loaded is not added, it is shown when loaded
|
||||
} else if (
|
||||
(chatState.splits.value.isEmpty() || chatState.splits.value.first() != items.lastOrNull()?.id)
|
||||
&& cItem.id > maxOf(lastItemId ?: 0, items.maxOfOrNull { it.id } ?: 0)
|
||||
) {
|
||||
addToChatItems(cItem)
|
||||
itemAdded = true
|
||||
} else if (cItem.chatDir.sent && cItem.meta.itemStatus !is CIStatus.SndRcvd) {
|
||||
if (sndStatusesChatId != cInfo.id) {
|
||||
sndStatusesChatId = cInfo.id
|
||||
sndStatusesBeforeAdd.clear()
|
||||
}
|
||||
sndStatusesBeforeAdd[cItem.id] = cItem.meta.itemStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
When the core reports an update of a chat item (`ChatItemUpdated`, file status events, delivery statuses, deletions that keep the item), both apps pass it to `upsertChatItem`. If the item is not among the items loaded in the open chat, `upsertChatItem` adds it at the bottom of the chat and reports it as added:
|
||||
|
||||
- Kotlin `ChatModel.upsertChatItem`: `addToChatItems(cItem)`, `itemAdded = true`; `chatItemUpdateNotify` then shows a notification (`ntfManager.notifyMessageReceived`), subject to `allowedToShowNotification()` or the chat not being open.
|
||||
- iOS `ChatModel.upsertChatItem` / `_upsertChatItem`: inserts the item at the bottom, and for the open chat also replaces the chat preview with it; `chatItemSimpleUpdate` then adds a notification.
|
||||
- Kotlin `ChatModel.ChatsContext.upsertChatItem`: `addToChatItems(cItem)`, `itemAdded = true`; for `ChatItemUpdated`, `chatItemUpdateNotify` then shows a notification (`ntfManager.notifyMessageReceived`), subject to `allowedToShowNotification()` or the chat not being open.
|
||||
- iOS `ChatModel.upsertChatItem` / `_upsertChatItem`: inserts the item at the bottom, and for the open chat also replaces the chat preview with it; for `ChatItemUpdated` and file events, `chatItemSimpleUpdate` then adds a notification.
|
||||
|
||||
So an update of an old message, for example an edit, a moderation, a file status change, or the "file was sent before you connected to the sender" marker from #7638, makes that old message appear at the bottom of the open chat (until it is reopened), become the chat preview on iOS, and show a notification with its text.
|
||||
So an update of an old message, for example an edit, a moderation, a file status change, a delivery status change, or the "file was sent before you connected to the sender" marker from #7638, makes that old message appear at the bottom of the open chat (until it is reopened) and become the chat preview on iOS. Edits and the #7638 marker (`ChatItemUpdated`) also show a notification with the item's text on both platforms; file status changes do so only on iOS (Kotlin `chatItemSimpleUpdate` never notifies); moderation, deletions and delivery statuses never notify.
|
||||
|
||||
## Why the item is added at all
|
||||
|
||||
@@ -15,13 +15,25 @@ So an update of an old message, for example an edit, a moderation, a file status
|
||||
|
||||
## Change
|
||||
|
||||
Chat item IDs only grow, so an item created by an update is newer than every other item in the chat, and an update of an earlier message is not. `upsertChatItem` on both platforms adds an item that is not loaded only if its ID is greater than both the chat preview's item ID and every loaded item's ID; otherwise the update is not applied to the loaded items, and the item is shown with its current state when that part of the chat is loaded. On iOS the open chat's preview is replaced by an item that is not loaded only by the same rule: its ID must be greater than the preview's and every loaded item's (the preview of a chat whose last item was deleted is a placeholder with ID -1).
|
||||
Chat item IDs only grow, so an item created by an update is newer than every other item in the chat, and an update of an earlier message is not. `upsertChatItem` on both platforms adds an item that is not loaded only if:
|
||||
|
||||
`addChatItem` (new items) is unchanged: on iOS it calls `_upsertChatItem` with `add: true`.
|
||||
- the newest items of the chat are loaded (iOS `ItemsModel.lastItemsLoaded`; Kotlin the same check of `chatState.splits`, written inline because `lastItemsLoaded` in `ChatView.kt` is private), and
|
||||
- its ID is greater than both the chat preview's item ID and every loaded item's ID.
|
||||
|
||||
Otherwise the update is not applied to the loaded items, and the item is shown with its current state when that part of the chat is loaded. On iOS the open chat's preview is replaced by an item that is not loaded only by the same rule.
|
||||
|
||||
While the newest items are not loaded (the chat was opened at the first unread item, or after a jump to a quoted or found item), neither the loaded items nor the preview bound the IDs of the items that are not loaded: in groups the preview is the item with the latest timestamp, not the highest ID, and after the last item is deleted the preview is a placeholder with ID -1. Once a new item is added at the bottom, the newest items count as loaded again; that is safe, because every earlier item has a lower ID than the new item. The same check applies to member support chats and reports, which use their own loaded state.
|
||||
|
||||
Status of a sent message before the send response: the core can report a status change of a message the user has just sent (`ChatItemsStatusesUpdated`) before the app adds that message from the send response, and a message received in between (`NewChatItems`) has a higher ID; while the newest items are not loaded, no such message is needed. The rule above drops that status update. So when `upsertChatItem` drops an update of an own sent item and its status is not a delivery receipt (`SndRcvd`), it keeps the status (Kotlin `ChatsContext.sndStatusesBeforeAdd`, iOS `ItemsModel.sndStatusesBeforeAdd`), and `addChatItem` applies the kept status to the item it adds when `shouldKeepOldSndCIStatus` prefers it, the same rule `upsertChatItem` uses for a loaded item. The kept statuses are of one chat only: they are cleared when a status of another chat is kept, and each is removed when its item is added. Other kept statuses come from updates of own messages that are not loaded, for example an error or a warning after a retry, or a group message reaching more members; they stay until a status of another chat is kept. Receipts, the usual status change of an old message, are not kept.
|
||||
|
||||
`addChatItem` otherwise is unchanged: on iOS it calls `_upsertChatItem` with `add: true`.
|
||||
|
||||
Not changed:
|
||||
|
||||
- A chat that is not in the chat list is still added with the item, as before: without its last item the app cannot tell whether the item is new.
|
||||
- Android and desktop still notify for every `ChatItemUpdated` of a user that is not active (iOS does not).
|
||||
- Live message placeholders have negative IDs (`-2`) and do not affect the comparison.
|
||||
- If the user sends a message after the core created an item from an update but before the app received that update, the created item is no longer newer than every loaded item, so it is shown only when the chat is reloaded, without a notification. Only the core reporting such items as new items (`NewChatItems`) would close this; that is a core change.
|
||||
- If the user sends a message after the core created an item from an update but before the app received that update, the created item is no longer newer than every loaded item, so it is shown only when the chat is reloaded, without a notification. An item created from an update while the newest items of the chat are not loaded is likewise shown when they are loaded, without a notification. Only the core reporting such items as new items (`NewChatItems`) would close this; that is a core change.
|
||||
- In groups, items are loaded in the order of their timestamps (`item_ts`), not their IDs. A message received late, for example while catching up after being offline, when members' messages arrive in any order, can be placed above more than a page of messages with later timestamps, and still have a higher ID than every loaded item and the preview. An update of it is still added at the bottom while the newest items are loaded. Only the core change above would remove this.
|
||||
- While the user writes a live message, its placeholder is the last item, so the newest items count as loaded, as they do for `ChatView`, and only the ID rule applies.
|
||||
- In the chat list, the preview of a sent message shows the status from the send response, as before, until the next status change.
|
||||
Reference in new issue
Block a user