A quick guide on how to use and migrate to TextChatService

TLDR cheat sheet

Description Legacy Example TextChatService Example
Sending Messages RemoteEvent:FireServer(chatMessage) TextChannel:SendAsync(chatMessage)
Receiving Messages RemoteEvent.OnClientEvent TextChannel.MessageReceived or TextChatService.MessageReceived
Display System Messages - TextChannel:DisplaySystemMessage(message, metadata)
Customizing Messages Speaker:SetExtraData TextChannel.OnIncomingMessage or TextChatService.OnIncomingMessage
Display Chat Bubble Chat:Chat(part, message) TextChatService:DisplayBubble(part, message)

Start of Guide

In light of the recent announcement, I’ve noticed that there’s a bit of confusion on how to use the TextChatService. Having spent some time with it, I’m happy to report migrating your experience to the new service is relatively straightforward once you know how it works.

I’ve found this system to be pretty flexible and have yet to run into a situation where I couldn’t replicate the functionality of the old chat system.

Here are a few quick-start guides depending on your current setup. I’ll probably be editing this post as I go along.

Rendering non-chat messages

System Messages

Sending system messages, only works on client. For server-side, use RemoteEvent to raise an event to the client, and then use TextChannel to display it.

Tip: If you are using the default text channels, you can use RBXSystem to render system messages. If you pass in “Error” as part of the second metadata argument, it will render as red!

These messages won’t be filtered or localized. The first argument is the text to be displayed. The second argument is the “metadata” argument which you can use to identify different types of messages in OnIncomingMessage callbacks.

-- client
SystemTextChannel:DisplaySystemMessage(message, "System.Error")

In this example, we detect if the message is a system message and check it’s metadata. You won’t need to do this for RBXSystem, the default TextChannel.

-- client
SystemTextChannel.OnIncomingMessage = function(message)
  if message.TextSource == nil then
  -- its a system message
  if message.Metadata == "System.Error" then
      -- make it red
      local properties = Instance.new("TextChatMessageProperties")
      properties.Text = `<font color="#FF0000">{message.Text}</font>`
      return properties
    end
  end

  return nil
end

TextChannels

In addition to the default TextChannels created from TextChatService.CreateDefaultTextChannels, you can create your own TextChannels for your own purposes.

There doesnt seem to be any limitation on how many TextChannels you can have. You’ll just want to make sure they are parented to TextChatService. You can determine who is part of a TextChannel by looking at which TextSource children they have. A TextSource represents a player in a TextChannel. A player can be in many TextChannels at a time.

To add a TextSource to a TextChannel, call TextChannel:AddUserAsync(player.UserId) from the server.

To remove a TextSource from a TextChannel, just destroy it from the server.

When a message is sent through a TextChannel, only players with a TextSource for that TextChannel will receive it.

TextChannels can be a great way to organize different chat streams. Since each TextChannel can have its own OnIncomingMessage callback, you can customize the appearance of the messages in each channel independent of others.

ShouldDeliverCallback

One neat thing you can do with TextChannels is to implement a custom TextChannel.ShouldDeliverCallback on the server. You can use this to implement all sorts of custom message filtering logic which is very cool.

The wiki already gives a good example on using this to implement some sort of proximity based chat, but you could also use this to create some interesting asymmetrical chat mechanics.

OnIncomingMessage callbacks

If you’re rendering your own UI, you can probably ignore OnIncomingMessage and just use the MessageReceived event. If you’re using the default TextChatService UI, you’ll want to use the OnIncomingMessage callback to customize the appearance of the chat messages.

When a message is received, there are a few callbacks you can hook into the TextChatService system to customize the message. These callbacks all expect a TextChatMessageProperties or nil as a return value. Each callback is called sequentially. Its a good idea to write OnIncomingMessage in TextChannels first, as they will be the most local and have the most context. TextChatService itself has an OnIncomingMessage callback which will run for ALL TextChannels in your experience.

DO NOT YIELD OR THROW IN THESE CALLBACKS. Weird things can happen as indicated by the documentation. If you must yield to determine stuff like asset ownership or group membership, do that query ahead of time and store an attribute or something with the result. (You’ll probably want to do that anyway since then you can reuse that state for other non-chat usecase)

The default TextChannels each have their OnIncomingMessage callbacks defined. The neat thing is you can overwrite these callbacks to redefine how the messages are displayed in the default channels. Custom TextChannels you create will render messages based on the OnIncomingMessage callback you define. If none are defined, the default UI will render the messages plainly

Below is a quick reference table of how the default TextChannels use the OnIncomingMessage callback.

TextChannel OnIncomingMessage Description
RBXGeneral Usernames are colored either by the Player’s TeamColor property or by using the name color algorithm from 2005
RBXSystem When DisplaySystemMessage is used on TextChannels, you can pass in a “metadata” value as the second argument. In this channel, it will render all messages grey unless “Error” is found in the metadata string where it will render as red instead.
RBXTeam Each team channel will be associated with a team color. Each name will be rendered with [Team] as a prefix in the team’s color
RBXWhisper Similar to RBXGeneral, except it will be prefixed with the other player’s name as [To OtherName]

Migrating to TextChatService

Here are a few quick-start guides depending on your current setup. I won’t be able to cover every edge case, but I’ll try to cover the most common scenarios.

If you have specific follow up questions, feel free to ask in this thread!

You're using Legacy Chat and it's not very integrated in your experience

If you’re using Legacy Lua Chat and not doing anything fancy or forked or using chat tags: You’re in luck, this is likely the easiest type of experience to migrate.

Set TextChatService.ChatVersion to Enum.ChatVersion.TextChatService and everything should continue to work out of the box.

You're using Legacy Chat and use SetExtraData to add chat tags

If you want to continue using chat tags, you’ll need to learn how to use OnIncomingMessage callbacks to customize the chat messages in the new chat ui.

Set TextChatService.ChatVersion to Enum.ChatVersion.TextChatService and get halfway there.

The OnIncomingMessage callback is where you can customize the chat messages. You can use this to add chat tags and customize the appearance of the chat messages. The default TextChatService UI relies on rich text tags to format the messages. This is nice because it gives flexibility on how you can render the messages and formatting is all done in a centralized place.

The easiest way to replicate the old SetExtraData method is to add attributes to the Player instance. You can then use these attributes in the OnIncomingMessage callback to customize the chat messages.

local Players = game:GetService("Players")

local OWNER = 12345 -- your userId here

Players.OnPlayerAdded:Connect(function(player)
  -- you could check for group membership or asset ownership here as well
  if player.UserId = OWNER then
    player:SetAttribute("ChatTags", "<font color='rgb(255, 255, 0)'>[Owner]</font>")
    player:SetAttribute("NameColor", Color3.fromRGB(255, 0, 0))
    player:SetAttribute("ChatColor", Color3.fromRGB(255, 0, 0))
  end
end)

IMO I would probably store stuff like “isOwner” or “isVIP” as an attribute instead and have all of my text-ui-formatting code live in the OnIncomingMessage callback but you do you.

local TextChatService = game:GetService("TextChatService")
local Players = game:GetService("Players")

TextChatService.OnIncomingMessage = function(textChatMessage)
	local textSource = textChatMessage.TextSource
	if textSource then
		local player = Players:GetPlayerByUserId(textSource.UserId)
		if player then
			local properties = Instance.new("TextChatMessageProperties")
			properties.PrefixText = textChatMessage.PrefixText
      properties.Text = textChatMessage.Text

      local tags = player:GetAttribute("ChatTags")
      if tags and typeof(tags) == "string" then
        properties.PrefixText = `{tags} {properties.PrefixText}`
      end

			local nameColor = player:GetAttribute("NameColor")
			if nameColor and typeof(nameColor) == "Color3" then
				properties.PrefixText = `<font color='#{nameColor:ToHex()}'>{properties.PrefixText}</font>`
			end

			local chatColor = player:GetAttribute("ChatColor")
			if chatColor and typeof(chatColor) == "Color3" then
				properties.Text = `<font color='#{chatColor:ToHex()}'>{properties.Text}</font>`
			end

			return properties
		end
	end

  return nil
end
You've made a totally custom chat system

The main thing you’ll need to do is to replace your RemoteEvents with TextChannels. TextChannels are the new way to send messages to the chat service.

You have a few options when it comes to TextChannels. You can use the default TextChannels that come with the TextChatService (toggled with TextChatService.CreateDefaultTextChannels), or you can create your own custom TextChannels.

Since your UI is already handling how messages are displayed, you can likely ignore the OnIncomingMessage callback and just use the MessageReceived events to render the messages.

The main thing you’ll have to do is replace your RemoteEvents with TextChannels. You can use the SendAsync method to send messages to a TextChannel instead of firing the RemoteEvent. You can use the MessageReceived event to receive messages from a TextChannel.

Bonus: Your UI has some special code to render some message in special ways

If your RemoteEvent is firing something like:
RemoteEvent:FireServer(chatMessage, { color = Color3.fromRGB(255, 0, 0) }) or some other additional data alongside the message, you may have to do a little more work, but it should be relatively straightforward.

Just like TextChannel:DisplaySystemMessage, you can also append additional metadata to TextChannel:SendAsync with the second argument.

-- in your TextBox code:
TextChannel:SendAsync(chatMessage, "Color.Red")

-- in your UI code:
local function createChatTextLabel(textChatMessage: TextChatMessage)
  local label = Instance.new("TextLabel")
  label.Text = textChatMessage.Text

  if textChatMessage.Metadata == "Color.Red" then
    label.TextColor3 = Color3.fromRGB(255, 0, 0)
  else
    label.TextColor3 = Color3.fromRGB(255, 255, 255)
  end

  return label
end
16 Likes

Here’s a pretty simple .rbxl of a custom UI that’s powered with TextChatService. In the Explorer, you can quickly find all the scripts by searching for Is:Script

Simple CustomChat Example.rbxl (60.3 KB)

The whole thing is less than 50 lines of code. I’ve opted to create a custom chat channel but it would be even shorter if I reused RBXGeneral when TextChatService.CreateDefaultTextChannels is true

Expand to see source code from rbxl

To keep things accessible, here’s the TextChatService-specific source code in the three scripts:

TextBox Script
--!strict
local TextChatService = game:GetService("TextChatService")
local TextBox = script.Parent
local myTextChannel = TextChatService:FindFirstChild("MyGameChat")

-- this is the bit of code you'd probably wanna replace with your
-- RemoteEvent:FireServer code
-- this would be your UI chat input bar that would actually
-- send messages to TextChannels
TextBox.FocusLost:Connect(function(enterPressed)
	if enterPressed then
		-- btw this can yield and can technically fail so handle
		-- with care
		myTextChannel:SendAsync(TextBox.Text)
		TextBox.Text = ""
	end
end)
ScreenGui Script
--!strict
local TextChatService = game:GetService("TextChatService")

local addMessageGui = require(script.addMessageGui)

-- this is the bit of code on the client that you'll probably wanna
-- replace with your RemoteEvent.OnClientEvent code.
-- addMessageGui is just some custom ui glue code I made for demo purposes
TextChatService.MessageReceived:Connect(function(textChatMessage)
	addMessageGui(textChatMessage)	
end)
TextChannel Script

This one exists just to add everyone to the custom TextChannel

--!strict
local Players = game:GetService("Players")
local TextChatService = game:GetService("TextChatService")
local myTextChannel = script.Parent

-- this is how we determine text channel participants
-- there could be cases you only want a subset of users to
-- be able to send and receive messages (eg party chat)
-- but today we'll throw everyone in here
Players.PlayerAdded:Connect(function(player)
	-- caution, this could yield someday and could throw in real life
	myTextChannel:AddUserAsync(player.UserId)
end)
4 Likes

Thank you this helped me understand it better!