Improve the TextChatService API so we can ensure messages appear in the correct order

I am trying to create a script that shows the recent chat history to a user from before they joined the experience (like the legacy Lua Chat System does).

DisplaySystemMessage seems to be my only option here. I’ve been trying to use it to show all of the recent messages as soon as a user joins, and have those messages displayed in order.

In this thread, I am going to simplify the problem by just using numbers rather than message logs.

Consider this code:


local defaultChannel = game:GetService("TextChatService"):WaitForChild("TextChannels"):WaitForChild("RBXGeneral")

for i = 1, 10 do
	defaultChannel:DisplaySystemMessage("Test " .. tostring(i))

As an aside, I don’t know an alternative to putting task.wait(1) up there. There seems to be no way to yield for the chat box being ready, so without that task.wait, the code runs too fast for the messages to appear in chat.

Here is how the chat looks after running that code:




Even tossing in a task.wait() doesn’t do the trick to keep the chat in proper order:

for i = 1, 10 do
	defaultChannel:DisplaySystemMessage("Test " .. tostring(i))

<!video controls>

I tried an alternative approach to force the messages into appearing in the right order. The idea is that the TextChatService.MessageReceived or TextChannel.MessageReceived events would be fired as soon as the message appears in chat, and so my loop would just yield for the latest message to have appeared before moving to display the next message.

Here is the new code:

local logSignature = game:GetService("HttpService"):GenerateGUID(false)
-- attach logSignature to joining messages so they're distinct from regular ones
local logIndex = 0
-- logIndex keeps track of the latest sent message
local latestIndex = logIndex
-- latestIndex keeps track of the latest received message
local loggedMessageLoaded ="BindableEvent")
-- the loop uses this to yield for the latest message to be received

	local logData = string.split(TextChatMessage.Metadata, " ")
	local signature, index = logData[1], tonumber(logData[2])
	if signature == logSignature and index == logIndex then
		print("MESSAGE RECEIVED:", index)
		latestIndex = logIndex


for i = 1, 10 do
	logIndex += 1
	local metadata = string.format("%s %s", logSignature, logIndex)
	defaultChannel:DisplaySystemMessage("Test " .. tostring(i), metadata)
	print("MESSAGE SENT:", logIndex)
	if latestIndex ~= logIndex then
		print("Yielding for message", logIndex, "to be received...")
		-- don't go to the next message until the last one has loaded

This does not work. Sure, the output looks as if everything is in order and nothing even needs to yield:


Everything is still out of order in the chat, though!


As an alternative to TextChannel.MessageReceived, there is TextChatService.OnIncomingMessage — this could be used to intercept chat messages and ensure the correct order before returning to allow those messages to be displayed in the chat. However, this is not ideal:

I wouldn’t want to override any other modules in the experience which might be using that callback function. Regardless, when I tried this it still had the same problem MessageReceived did — the chat did not match the output.

This behavior suggests a few things:

  • TextChannel:DisplaySystemMessage() is asynchronous, not yielding for the message to appear in chat before allowing the code to move on

  • TextChannel.MessageReceived does not fire when messages actually appear in the chat

  • When TextChatService.OnIncomingMessage returns a TextChatMessageProperties instance, it takes some time before it’s displayed it the chat

There doesn’t seem to be any remaining ways to detect when a message actually appears in chat, and there is simply no way to ensure message order. I could use artificial yields by putting a task.wait(0.1) at the bottom of the loop, but there could be chat messages by other users in-game while our system messages are still yielding. That messes up the intended effect of all the logged messages being side-by-side. Additionally, using artificial waits to ensure order is just bad practice — it creates a race condition, and may not always work, as seen with the task.wait() example at the top of this thread.

It would make more sense to me if DisplaySystemMessage yielded until the message appeared in the chat window. More events and also more explicit naming of events would help here as well.

Repro: dump many chats on join.rbxm (2.2 KB)

  • Place the folder in game.ReplicatedStorage.

  • Ensure game.TextChatService.ChatVersion is set to TextChatService.

  • When testing any of the scripts, ensure only one is enabled at a time.

  • Ensure game.Players.CharacterAutoLoads is disabled for this test just in case it slows down the loading time.

  • If nothing shows in the chat, just increase the yielding time from right before where the loop begins. Try task.wait(5), for example.


Support. This is very odd behavior. However, I just thought of a very quick (and temporary) solution that could help you while we wait for a fix.

local textChatService = game:GetService("TextChatService")

local defaultChannel = textChatService.TextChannels.RBXGeneral

local messages = {
    "Message 1",
    "Message 2",
    "Message 3"

local fullString = ""
for _, message in ipairs(messages) do
    fullString = fullString .. "\n" .. message


That is very interesting, I don’t know why I didn’t think of it. I will give it a try, and if it all works out I will edit this post with a Here is my Community Resources topic link. This does what I set out to do in the first sentence of the OP.

Thank you for the suggestion!


I wonder if this behavior is unpredictable enough to make a bug report. It doesn’t seem like it’s intended, but also I feel staff would just say “oh, DisplaySystemMessage isn’t meant to yield, this isn’t a bug” or something.

@SubtleShuttle — I see that you managed the topic and replies for the TextChatService release. Was this behavior known about during development? Thanks in advance.

1 Like