diff --git a/Assets/Plugins/StreamChat/Changelog.txt b/Assets/Plugins/StreamChat/Changelog.txt
index 4fc10064..3a66b3e1 100644
--- a/Assets/Plugins/StreamChat/Changelog.txt
+++ b/Assets/Plugins/StreamChat/Changelog.txt
@@ -1,3 +1,8 @@
+Unreleased:
+Features:
+
+* Add IStreamClientConfig.OptimisticMessageInsert (default true). When true (the existing behavior), a message you send is inserted into the local channel state and raised via IStreamChannel.MessageReceived immediately, before the server's message.new echo arrives. Set it to false to skip the optimistic local insert and wait for the server echo instead, so every participant - including the sender - observes messages in the same server-defined order. Useful when consistent cross-client ordering matters more than instant local feedback (e.g. a shared, broadcast-ordered feed).
+
v5.5.0:
Features:
diff --git a/Assets/Plugins/StreamChat/Core/Configs/IStreamClientConfig.cs b/Assets/Plugins/StreamChat/Core/Configs/IStreamClientConfig.cs
index 612e1cff..a8d1628d 100644
--- a/Assets/Plugins/StreamChat/Core/Configs/IStreamClientConfig.cs
+++ b/Assets/Plugins/StreamChat/Core/Configs/IStreamClientConfig.cs
@@ -15,5 +15,18 @@ public interface IStreamClientConfig
/// Disabled - no logs will be emitted. Not recommended in general - this could be only viable if you're capturing all of the thrown exceptions and handling the logging on your own.
///
StreamLogLevel LogLevel { get; set; }
+
+ ///
+ /// Whether a message you send is optimistically inserted into the local channel state and
+ /// raised via immediately, before
+ /// the server's `message.new` WebSocket echo arrives. Defaults to true.
+ ///
+ /// When true (default), the sender sees their own message right away and the later
+ /// WebSocket echo is de-duplicated. When false, the locally sent message is not added
+ /// to the channel until its server echo arrives, so every participant - including the sender -
+ /// observes messages in the same server-defined order. Disable this when consistent cross-client
+ /// ordering matters more than instant local feedback (e.g. a shared, broadcast-ordered feed).
+ ///
+ bool OptimisticMessageInsert { get; set; }
}
}
\ No newline at end of file
diff --git a/Assets/Plugins/StreamChat/Core/Configs/StreamClientConfig.cs b/Assets/Plugins/StreamChat/Core/Configs/StreamClientConfig.cs
index f82d1821..5e1da8bf 100644
--- a/Assets/Plugins/StreamChat/Core/Configs/StreamClientConfig.cs
+++ b/Assets/Plugins/StreamChat/Core/Configs/StreamClientConfig.cs
@@ -8,5 +8,7 @@ public class StreamClientConfig : IStreamClientConfig
public static IStreamClientConfig Default { get; set; } = new StreamClientConfig();
public StreamLogLevel LogLevel { get; set; } = StreamLogLevel.FailureOnly;
+
+ public bool OptimisticMessageInsert { get; set; } = true;
}
}
\ No newline at end of file
diff --git a/Assets/Plugins/StreamChat/Core/LowLevelClient/StreamChatLowLevelClient.cs b/Assets/Plugins/StreamChat/Core/LowLevelClient/StreamChatLowLevelClient.cs
index ec3d1924..051799ef 100644
--- a/Assets/Plugins/StreamChat/Core/LowLevelClient/StreamChatLowLevelClient.cs
+++ b/Assets/Plugins/StreamChat/Core/LowLevelClient/StreamChatLowLevelClient.cs
@@ -505,6 +505,8 @@ public void Dispose()
internal IInternalThreadsApi InternalThreadsApi { get; }
+ internal IStreamClientConfig Config => _config;
+
internal async Task ConnectUserAsync(string apiKey, string userId,
ITokenProvider tokenProvider, CancellationToken cancellationToken = default)
{
diff --git a/Assets/Plugins/StreamChat/Core/StatefulModels/StreamChannel.cs b/Assets/Plugins/StreamChat/Core/StatefulModels/StreamChannel.cs
index ca08e1f1..1bcd1419 100644
--- a/Assets/Plugins/StreamChat/Core/StatefulModels/StreamChannel.cs
+++ b/Assets/Plugins/StreamChat/Core/StatefulModels/StreamChannel.cs
@@ -205,9 +205,23 @@ public async Task SendNewMessageAsync(StreamSendMessageRequest s
var replyAlreadyInCache = !string.IsNullOrEmpty(responseMessageId)
&& Cache.Messages.TryGet(responseMessageId, out _);
- //StreamTodo: we update internal cache message without server confirmation that message got accepted. e.g. message could be rejected
- //It's ok to update the cache "in good faith" to not introduce update delay but we should handle if message got rejected
- InternalAppendOrUpdateMessage(response.Message, out var streamMessage);
+ StreamMessage streamMessage;
+ if (LowLevelClient.Config.OptimisticMessageInsert)
+ {
+ //StreamTodo: we update internal cache message without server confirmation that message got accepted. e.g. message could be rejected
+ //It's ok to update the cache "in good faith" to not introduce update delay but we should handle if message got rejected
+ InternalAppendOrUpdateMessage(response.Message, out streamMessage);
+ }
+ else
+ {
+ // Optimistic insert disabled (see IStreamClientConfig.OptimisticMessageInsert): don't add
+ // the sent message to the local timeline or raise MessageReceived here. The server's
+ // message.new echo delivers it instead, so every participant - including the sender -
+ // observes messages in the same server-defined order. Still update the cache so the
+ // returned message is tracked and the WS echo resolves to (and dedups against) the same
+ // instance.
+ streamMessage = Cache.TryCreateOrUpdate(response.Message, out _);
+ }
// Optimistic parent.ReplyCount bump for thread replies on the local sender. Without
// this, after InternalAppendOrUpdateMessage the reply is in cache and any subsequent