---
source_url: https://www.pubnub.com/docs/chat/unity-chat-sdk/build/sample-chat-ui
title: Sample chat UI
updated_at: 2026-06-05T11:10:05.554Z
---

> Documentation Index
> For a curated overview of PubNub documentation, see: https://www.pubnub.com/docs/llms.txt
> For the full list of all documentation pages, see: https://www.pubnub.com/docs/llms-full.txt


# Sample chat UI

The Unity Chat SDK ships with a ready-made chat UI prefab so you can bring up a working chat screen without writing any UI code.

:::note Starter sample
This prefab is a starter sample you copy into your project and customize. Treat it as a reference implementation, not a production UI component.
:::

:::warning Key security
The controller exposes `publishKey`, `subscribeKey`, and `secretKey` as serialized inspector fields, which means they are saved into the scene file. For production, load keys from secure storage or use an [Access Manager](https://www.pubnub.com/docs/chat/unity-chat-sdk/build/features/users/permissions) token issued by your server, and never commit secret keys to source control.
:::

## What is included

The sample lives under `Assets/PubnubChat/Samples~/UIChatPrefab/` in the [Unity Chat SDK repository](https://github.com/pubnub/unity-chat) and contains three files:

| File | Type | Purpose |
| --- | --- | --- |
| `PubnubChatUI.prefab` | Unity prefab | Canvas with a scroll view for messages, a text input, and a send button. Drop it into a scene to get a full chat layout. |
| `PubnubChatUIController.cs` | MonoBehaviour | Initializes the Chat SDK, creates a public conversation, optionally fetches recent message history, listens for incoming messages, and sends new messages from the input field. |
| `PubnubMessageUIController.cs` | MonoBehaviour | Renders a single message as a left- or right-aligned bubble with a metadata line (user ID and timestamp). |

## Prerequisites

Install and initialize the Unity Chat SDK before using the sample. Refer to [Initial configuration](https://www.pubnub.com/docs/chat/unity-chat-sdk/build/configuration).

## Import the sample

The prefab is distributed as a UPM package sample:

1. In Unity Editor, open **Window -> Package Manager**.
2. Select the **Pubnub Chat** package from the list.
3. Expand the **Samples** section and click **Import** next to **Pubnub Chat - Example UI Prefab**.
4. Unity copies the sample into `Assets/Samples/Pubnub Chat/<version>/Pubnub Chat - Example UI Prefab/`.

## Use the sample

1. Open the scene where you want the chat to appear. Make sure it contains an `EventSystem` (Unity adds one automatically when you drop the prefab into an empty scene).
2. Drag `PubnubChatUI.prefab` from the imported sample folder into the scene hierarchy.
3. Select the prefab root object and fill in the inspector fields under **Pubnub settings** with your PubNub keys, a user ID, and the channel ID you want to join. For the field reference, see [Inspector reference](#inspector-reference).
4. Press **Play**. The controller creates a public conversation, wires up the send button and input field, and starts receiving messages on the channel.

## Inspector reference

### PubnubChatUIController

| Parameter | Description |
| --- | --- |
| `publishKey` *Type: `string`Default: n/a | [Publish Key](https://www.pubnub.com/docs/chat/unity-chat-sdk/build/configuration#prerequisites) from your PubNub keyset. |
| `subscribeKey` *Type: `string`Default: n/a | [Subscribe Key](https://www.pubnub.com/docs/chat/unity-chat-sdk/build/configuration#prerequisites) from your PubNub keyset. |
| `secretKey`Type: `string`Default: n/a | Secret Key used for server-side operations. Leave empty for client builds. |
| `userId` *Type: `string`Default: n/a | [Unique User ID](https://www.pubnub.com/docs/general/setup/users-and-devices) that identifies the current user. |
| `channelId` *Type: `string`Default: n/a | Identifier of the public channel the sample joins on start. If the channel already exists, the controller reuses it. |
| `fetchChannelHistoryOnStart`Type: `bool`Default: `false` | When `true`, the controller loads the last five hours of messages (up to `displayedMessagesLimit`) into the scroll view on start. |
| `displayedMessagesLimit`Type: `int`Default: `50` | Maximum number of messages shown at once. Older messages are removed from the UI when this limit is reached. |
| `messageUIPrefab` *Type: `PubnubMessageUIController`Default: n/a | Prefab used to render each message bubble. Pre-wired in the sample to a bubble prefab under the same folder. |
| `scrollView` *Type: `ScrollRect`Default: n/a | Scroll view that contains the message list. |
| `scrollViewContent` *Type: `RectTransform`Default: n/a | Content transform under the scroll view where message instances are parented. |
| `information` *Type: `TextMeshProUGUI`Default: n/a | Header label that displays the active channel ID. |
| `messageInput` *Type: `TMP_InputField`Default: n/a | Input field where the user types new messages. |
| `sendButton` *Type: `Button`Default: n/a | Button that publishes the text currently in `messageInput`. |

### PubnubMessageUIController

| Parameter | Description |
| --- | --- |
| `TheirBubble` *Type: `GameObject`Default: n/a | Container shown when the message was sent by another user. |
| `TheirText` *Type: `TextMeshProUGUI`Default: n/a | Text component inside `TheirBubble` that displays the message body. |
| `TheirMeta` *Type: `TextMeshProUGUI`Default: n/a | Metadata line (user ID and timestamp) for messages received from others. |
| `MyBubble` *Type: `GameObject`Default: n/a | Container shown when the current user sent the message. |
| `MyText` *Type: `TextMeshProUGUI`Default: n/a | Text component inside `MyBubble` that displays the message body. |
| `MyMeta` *Type: `TextMeshProUGUI`Default: n/a | Metadata line for messages sent by the current user. |

## Controller source

The controller handles the full lifecycle: it creates a [Chat](https://www.pubnub.com/docs/chat/unity-chat-sdk/learn/chat-entities/chat) instance, creates or reuses a public [Channel](https://www.pubnub.com/docs/chat/unity-chat-sdk/learn/chat-entities/channel) with the configured ID, optionally loads history, binds `OnMessageReceived`, and calls `Connect()` to start receiving live messages. On destroy, it removes the button, input, and message listeners so scene transitions leave no dangling subscriptions.

```csharp
using System;
using System.Collections.Generic;
using PubnubApi;
using PubnubChatApi;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Button = UnityEngine.UI.Button;
using Channel = PubnubChatApi.Channel;

/// <summary>
/// A simple UI implementation of a Pubnub Chat.
/// </summary>
public class PubnubChatUIController : MonoBehaviour
{
    [Header("Pubnub settings")]
    [SerializeField] private string publishKey;
    [SerializeField] private string subscribeKey;
    [SerializeField] private string secretKey;
    [SerializeField] private string userId;
    [SerializeField] private string channelId;
    [SerializeField] private bool fetchChannelHistoryOnStart;
    [SerializeField] private int displayedMessagesLimit = 50;
    [Header("UI references")]
    [SerializeField] private PubnubMessageUIController messageUIPrefab;
    [SerializeField] private ScrollRect scrollView;
    [SerializeField] private RectTransform scrollViewContent;
    [SerializeField] private TextMeshProUGUI information;
    [SerializeField] private TMP_InputField messageInput;
    [SerializeField] private Button sendButton;

    private Stack<PubnubMessageUIController> displayedMessages = new ();
    private Channel channel;
    
    private async void Awake()
    {
        //Displaying the ID, alternatively you can use channel.Name after it has been fetched later
        information.text = $"Channel: {channelId}";
        
        //Create a chat and abort if there was an error
        var createChat = await UnityChat.CreateInstance(
            new PubnubChatConfig(), 
            new PNConfiguration(new UserId(userId))
            {
                PublishKey = publishKey, 
                SubscribeKey = subscribeKey, 
                SecretKey = secretKey
            }, false);
        if (createChat.Error)
        {
            Debug.LogError($"Failed to initialize chat! Error message: {createChat.Exception.Message}");
            return;
        }
        var chat = createChat.Result;
        
        //Create a channel and abort if there was an error
        //Note that if a channel with the given ID already exists the result of this operation will simply return it
        var createChanel = await chat.CreatePublicConversation(channelId);
        if (createChanel.Error)
        {
            Debug.LogError($"Failed to create channel! Error message: {createChanel.Exception.Message}");
            return;
        }
        channel = createChanel.Result;
        
        //Assign listeners for sending message
        sendButton.onClick.AddListener(SendMessage);
        messageInput.onSubmit.AddListener(delegate{SendMessage();});
        
        //If the setting is enabled messages will be fetched from the Channel history
        if (fetchChannelHistoryOnStart)
        {
            //Hardcoded for 5h for demonstration purpose
            var getHistory = await channel.GetMessageHistory(ChatUtils.TimeTokenNow(),
                ChatUtils.TimeToken(DateTime.UtcNow.Subtract(new TimeSpan(5, 0, 0))), displayedMessagesLimit);
            if (getHistory.Error)
            {
                Debug.LogError($"Failed to fetch history! Error message: {getHistory.Error}");
            }
            else
            {
                var history = getHistory.Result;
                foreach (var message in history)
                {
                    DisplayMessage(message);
                }
            }
        }
        //Setting up the callback for incoming messages
        channel.OnMessageReceived += OnMessageReceived;
        //Connecting to the channel (OnMessageReceived won't start triggering without this)
        //Conversely, to stop receiving OnMessageReceived you can call channel.Disconnect()
        channel.Connect();
    }

    private void SendMessage()
    {
        var text = messageInput.text;
        if (string.IsNullOrEmpty(text))
        {
            return;
        }
        messageInput.text = string.Empty;
        //Not awaiting to not block UI
        channel.SendText(text);
    }

    private void OnMessageReceived(Message message)
    {
        DisplayMessage(message);
    }

    private void DisplayMessage(Message message)
    {
        //Destroying messages if limit is reached (can be useful for performance)
        if (displayedMessages.Count >= displayedMessagesLimit)
        {
            var poppedMessage = displayedMessages.Pop();
            Destroy(poppedMessage.gameObject); 
        }
        var messageUi = Instantiate(messageUIPrefab, scrollViewContent);
        messageUi.gameObject.SetActive(true);
        //Pubnub timetokens are in UTC so to display human-readable time the ToLocalTime() call is needed
        var messageSendDate = Pubnub.TranslatePubnubUnixNanoSecondsToDateTime(message.TimeToken).ToLocalTime();
        messageUi.Initialize(message.MessageText, $"<b>{message.UserId}</b>, {messageSendDate:yyyy-MM-dd HH:mm:ss}",message.UserId == userId);
        displayedMessages.Push(messageUi);
        scrollView.normalizedPosition = new Vector2(0, 0);
    }

    //For listeners cleanup
    private void OnDestroy()
    {
        sendButton.onClick.RemoveListener(SendMessage);
        messageInput.onSubmit.RemoveAllListeners();
        if (channel != null)
        {
            channel.OnMessageReceived -= OnMessageReceived;   
        }
    }
}
```

## Customize

Use the prefab as a starting point and adapt it to your game:

| Customization | Description |
| --- | --- |
| Theming | Create [prefab variants](https://docs.unity3d.com/Manual/PrefabVariants.html) of `PubnubChatUI.prefab` to swap backgrounds, fonts, and colors without losing upstream updates. |
| Message bubbles | Edit the `PubnubMessageUIController` prefab to restyle `MyBubble` and `TheirBubble`, change TextMeshPro fonts, or add avatar slots. Extend `Initialize()` to accept the data you need. |
| Channel types | Replace `chat.CreatePublicConversation(channelId)` with [CreateGroupConversation()](https://www.pubnub.com/docs/chat/unity-chat-sdk/build/features/channels/create#group-conversation) or [CreateDirectConversation()](https://www.pubnub.com/docs/chat/unity-chat-sdk/build/features/channels/create#direct-conversation) to target private rooms. |
| More real-time events | Subscribe to additional channel events like [typing indicators](https://www.pubnub.com/docs/chat/unity-chat-sdk/build/features/channels/typing-indicator), [presence](https://www.pubnub.com/docs/chat/unity-chat-sdk/build/features/users/presence), and [read receipts](https://www.pubnub.com/docs/chat/unity-chat-sdk/build/features/messages/read-receipts). |
| Secure keys | Move `publishKey`, `subscribeKey`, and `secretKey` out of the inspector and into a runtime source such as a server-issued [Access Manager](https://www.pubnub.com/docs/chat/unity-chat-sdk/build/features/users/permissions) token. |