User avatar
Site Admin
Roblox TextChatService: Setting Up In-Experience Chat and Filtering Text Correctly
Roblox TextChatService: Setting Up In-Experience Chat and Filtering Text Correctly

TextChatService replaced the old Lua chat system, and it changes how you write chat code in two big ways. Filtering is automatic for anything that travels through a text channel, and message appearance is handled by callbacks instead of chat modules. If you still reach for Players:Chat or Player.Chatted for player-facing text, this is the API that replaces both.

The pieces
  • TextChatService: the singleton service. Text channels and chat commands have to be parented to it or they do nothing.
  • TextChannel: moves messages from a client to the server, which then delivers them based on permissions. Set CreateDefaultTextChannels to true and you get RBXGeneral and RBXSystem for free.
  • TextSource: one user inside one channel, created when you call TextChannel:AddUserAsync(). A player in three channels has three text sources.
  • TextChatMessage: the message itself, including the original text, the filtered text, the sender, and a timestamp.
  • TextChatCommand: a slash command. CreateDefaultCommands gives you the standard set, and you can add your own.
One warning straight from the docs: two text channels with the same name can break the default chat window. Name them carefully.

How a message travels

A player hits enter. TextChannel:SendAsync() runs on the client and decides whether the input is a command or a message. Commands fire TextChatCommand.Triggered and stop there. Everything else fires TextChatService.SendingMessage for the sender, and the message goes to the server.

On the server, TextChannel.ShouldDeliverCallback runs once per text source in the channel and returns a boolean. That is your permission hook, and it is the right place for proximity chat or for blocking a player from messaging people who should not receive it.

If the callback allows delivery, the server filters the message and fires TextChannel.OnIncomingMessage twice. The first pass is on the sending client, so the sender sees the processed version of their own message. The second pass is on every receiving client.

Keep these callbacks non-yielding. The docs are explicit about it. Waiting inside a callback blocks the chat system.

Filtering: what is automatic and what is not

Messages sent through a TextChannel are filtered by Roblox automatically, based on the account of the player who wrote them. You do not call FilterStringAsync for chat.

You do have to filter everything else. Anything a player can type or generate that you display outside chat is your responsibility. The docs list the usual cases: TextBox input, custom keyboards and keypads, words generated from random characters, text pulled from an external web server, and stored text such as a pet name that you read back from a data store.

The pattern is TextService:FilterStringAsync(text, fromUserId), which returns a TextFilterResult, then one of two methods:
  • GetNonChatStringForBroadcastAsync() for text everyone in the server will see, such as a sign in the world.
  • GetNonChatStringForUserAsync() for text aimed at one player, filtered to their age and settings.
Do not filter on every keystroke. Filter after the player submits, or you are yielding the filter for text only the typist can see.

This is not a soft requirement. If Roblox finds a game showing unfiltered text, the game gets removed until filtering is added. Treat it as a hard gate rather than a nice-to-have.

A working example

Client side, on a TextBox:

Code: Select all

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local textBox = script.Parent
local inputRemoteEvent = ReplicatedStorage:FindFirstChild("InputRemoteEvent")

local function onFocusLost(enterPressed, inputObject)
    if enterPressed and inputRemoteEvent then
        inputRemoteEvent:FireServer(textBox.Text)
    end
end

textBox.FocusLost:Connect(onFocusLost)
Server side, filtering before the text is displayed:

Code: Select all

local TextService = game:GetService("TextService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local inputRemoteEvent = ReplicatedStorage:FindFirstChild("InputRemoteEvent")

local function onInputReceived(player, text)
    if text == "" then return end

    local ok, filterResult = pcall(function()
        return TextService:FilterStringAsync(text, player.UserId)
    end)
    if not ok or not filterResult then
        warn("Could not generate TextFilterResult")
        return
    end

    local ok2, filtered = pcall(function()
        return filterResult:GetNonChatStringForBroadcastAsync()
    end)
    if ok2 then
        print("FILTERED:", filtered)
    end
end

inputRemoteEvent.OnServerEvent:Connect(onInputReceived)
Customizing how messages look

The old chat system used SetExtraData on a Speaker to set name colors and tags. TextChatService has no direct equivalent. The replacement is OnChatWindowAdded, which returns a ChatWindowMessageProperties object. Set PrefixText and TextColor3 on it and return it. Player attributes work well as the source of those colors, since they replicate and you can read them from inside the callback.

The related callbacks, all client side:
  • TextChatService.OnIncomingMessage runs first, for every message from every channel.
  • TextChannel.OnIncomingMessage runs per channel. The default channels already define it, and you can overwrite it.
  • OnBubbleAdded formats the bubble above a character, independent of the chat window.
  • OnChatWindowAdded formats the chat window, independent of bubbles.
Sources
[1] Roblox Creator Docs: Text chat overview
[2] Roblox Creator Docs: Text filtering
[3] Roblox Creator Docs: TextChatService API reference
[4] Roblox Creator Docs: TextChannel API reference