Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## Upcoming

🐞 Fixed

- Fixed `message.updated` and soft `message.deleted` events being incorrectly upserted into `ChannelState.messages` (and thread reply lists) when they targeted a message outside the currently loaded window.

## 10.1.0

✅ Added
Expand Down
34 changes: 29 additions & 5 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3194,7 +3194,7 @@ class ChannelClientState {
final message = event.message;
if (message == null) return;

return updateMessage(message);
return _updateMessages([message], upsert: false);
}),
);
}
Expand All @@ -3209,7 +3209,9 @@ class ChannelClientState {
deletedForMe: event.deletedForMe,
);

return deleteMessage(message, hardDelete: hardDelete);
if (hardDelete) return deleteMessage(message, hardDelete: true);
// Soft delete, update the message only if loaded (upsert: false)
return _updateMessages([message], upsert: false);
}),
);
}
Expand Down Expand Up @@ -3930,18 +3932,20 @@ class ChannelClientState {
void _updateMessages(
Iterable<Message> messages, {
Message Function(Message original, Message updated) update = _mergeUpdate,
bool upsert = true,
}) {
if (messages.isEmpty) return;

_updateThreadMessages(messages, update: update);
_updateChannelMessages(messages, update: update);
_updateThreadMessages(messages, update: update, upsert: upsert);
_updateChannelMessages(messages, update: update, upsert: upsert);
_updatePinnedMessages(messages, update: update);
_updateActiveLiveLocations(messages);
}

void _updateThreadMessages(
Iterable<Message> messages, {
Message Function(Message original, Message updated) update = _mergeUpdate,
bool upsert = true,
}) {
if (messages.isEmpty) return;

Expand All @@ -3963,6 +3967,7 @@ class ChannelClientState {
existing: threadMessages,
toMerge: value,
update: update,
upsert: upsert,
);

// Update the thread with the modified message list.
Expand All @@ -3976,6 +3981,7 @@ class ChannelClientState {
void _updateChannelMessages(
Iterable<Message> messages, {
Message Function(Message original, Message updated) update = _mergeUpdate,
bool upsert = true,
}) {
if (messages.isEmpty) return;

Expand All @@ -3996,6 +4002,7 @@ class ChannelClientState {
existing: channelMessages,
toMerge: affectedMessages,
update: update,
upsert: upsert,
);

// Calculate the new last message at time.
Expand Down Expand Up @@ -4092,14 +4099,20 @@ class ChannelClientState {
required Iterable<Message> existing,
required Iterable<Message> toMerge,
Message Function(Message original, Message updated) update = _mergeUpdate,
bool upsert = true,
}) {
if (toMerge.isEmpty) return existing;

// [update] decides whether each pair is reconciled (default — see
// `_mergeUpdate`) or replaced (`_replaceUpdate`, used by local rollback
// paths that don't want enrichment fallback to keep optimistic values).
//
// [upsert] controls whether ids not already in [existing] are inserted.
// Event-driven paths (`message.updated`, `message.deleted` soft) pass
// `upsert: false` so an out-of-window message isn't dropped into a gap
// between the loaded slice and history the client hasn't paged in yet.
final existingList = existing is List<Message> ? existing : existing.toList();
final toMergeList = toMerge is List<Message> ? toMerge : toMerge.toList();
var toMergeList = toMerge is List<Message> ? toMerge : toMerge.toList();

// Single-message fast path. The hot ingest path (server echoes, edits,
// reactions, read receipts) always lands here, and `lastIndexWhere` +
Expand All @@ -4108,6 +4121,10 @@ class ChannelClientState {
if (toMergeList.length == 1) {
final message = toMergeList.first;
final oldIndex = existingList.lastIndexWhere((it) => it.id == message.id);

// upsert: false — skip update if message is not loaded
if (oldIndex == -1 && !upsert) return existingList;

final resolved = oldIndex == -1 ? message : update(existingList[oldIndex], message);

final mergedMessages = existingList.sortedUpsertAt(
Expand All @@ -4127,6 +4144,13 @@ class ChannelClientState {
);
}

// upsert: false - skip messages not loaded in the window
if (!upsert) {
final existingIds = {for (final m in existingList) m.id};
toMergeList = toMergeList.where((m) => existingIds.contains(m.id)).toList();
if (toMergeList.isEmpty) return existingList;
}

// Batch path: receiver (`existingList`) is maintained sorted as a
// state invariant; `mergeSorted` sorts `toMergeList` internally and
// returns a sorted result.
Expand Down
Loading
Loading