This is the third and final post in our series on how to build a custom React video chat app using Daily's real time video and audio APIs, and our new custom React hooks library, Daily React Hooks. In part one, we covered the basics of a custom video application. In part two, we added a prejoin UI.

In this post, we’ll be adding a new feature to our application: text chat. This will allow participants to send chat messages to each other during a call. At the end of this post, our app will look like this:

Screenshot of the final product

Tip: If you're wondering why the video feed of “Maude (You)” – the top left feed – is flipped, that's because it's the local participant! We tend to prefer to look at a "mirror" image of ourselves in video calls.

There are lots of reasons you might want to add chat to your video meetings. For example, when you want to allow meeting participants to share links or thoughts with each other without interrupting the meeting. And while one-on-one messaging is outside the scope of this post, private messaging would allow people to discreetly notify each other when someone’s got spinach stuck between their teeth :-) if you’re interesting in learning how, here is a previous post on building a React input that can switch between private and public messages.

To follow along with this blog post you’ll need some basic knowledge of React and JavaScript. If you’re not looking to build your own fully custom experience, but would like a low-code way to integrate Daily video calls in your website, take a look at the first part of this blog post on three ways to add chat to your video calls. With Daily Prebuilt, enabling chat in your app is just a matter of toggling a switch in the Daily dashboard!

What's the plan?

Before we dive in, let’s do a quick recap of this series so far (feel free to skip this part if you’ve just come from reading those, or if you’re solely interested in building a custom chat widget):

  • In part one, we went over the basics of a custom video application. We saw how we could create rooms using the Daily API, build a call user interface and control tray, and added basic error handling.
  • In part two, we added a prejoin UI, or “hair check”. This prejoin UI allows participants to preview their video before joining, as well as select their preferred devices (camera and microphone), and set their username.
Screenshot of the prejoin UI

Now we’ve arrived at the third and final iteration of our app. We’ll build on top everything we’ve done so far. At the end of this post, we’ll have a fully functional video calling application with chat. ✨

So how do we add chat messaging? We’re going to add a new component, <Chat/>, to our app. In this component we’ll be using the useAppMessage() hook. This nifty hook wraps around the sendAppMessage() instance method in daily-js. To simplify things a little, we won’t be implementing private chat; if a participant sends a message, it will be shown to all other participants in the call.

Just sending messages to the Daily API won’t be enough, however. We’ll also need to display the sent and received messages in the UI. Because messages are not stored by Daily, we’ll need to do a bit of extra coding to store the messages in the React state, so we can keep them around even when the chat UI isn’t active.

To follow along with the rest of this post, we’ll assume you’ve followed the set-up steps in part one, and that you’ve also implemented or pulled the code we wrote in part two​​. Everything we’ve done so far in the series, plus the chat functionality, can be found in the main branch of the GitHub repository. Make sure that your local copy of the repo is up-to-date, and that you’ve checked out the main branch before continuing.

Chat feature requirements

Before we start coding, we need to have a clear idea of what it is we’re trying to achieve. What should our chat do? Let’s break it down:

  1. There should be a form, where participants can enter and submit their message
  2. The messages must be ordered: the newest message sent should be last in the list of all messages
  3. The message should display who sent it
  4. When a message is sent, other participants should be notified somehow

It’s important to keep in mind that Daily does not store messages on its servers. That means that if someone joins the call after messages have already been sent, there is no way to retrieve those messages from Daily servers. It also means chat is built to be HIPAA-compliant by default! We can, however, fetch the chat history from other participants. We’ll go into more detail about that later.

Now that we have our requirements, we can start writing some code. Like in the other posts of this series, we’ll be using the Daily React Hooks library to interact with the call object.

Adjusting the <Tray/> component

First, we need to decide where to put the Chat component. It should be rendered on screen somehow, but how, and where?

We already have a <Tray/> component with control buttons. From the tray, users can mute their audio and video, toggle meeting information, et cetera. It makes sense to add a button there that toggles the Chat UI. Take a look at this line in Tray.js:

​​<button onClick={toggleChat}>
 {newChatMessage ? <ChatHighlighted /> : <ChatIcon />}
 {showChat ? 'Hide chat' : 'Show chat'}
</button>

We’ll come back to the newChatMessage variable later. Let’s focus on the showChat boolean first: if it’s true, we’ll render the Chat component:

<Chat showChat={showChat} />

And then, in the <Chat/> component, we use the showChat prop to decide what to render:

return showChat ? “a bunch of JSX” : null

If you’ve been paying close attention, you’ll see that this is slightly different from what we’re doing in another toggleable component in the <Tray/>: <MeetingInformation/>.

If showMeetingInformation is true — see line 77 in Tray.js — we render the meeting information component. If it’s set to false, we simply remove the component from the DOM. MeetingInformation is stateless - if the component is unmounted, we’ll lose the information, but as soon as we toggle it back on, the information we want to display will be retrieved again from the Daily API.

Chat messages will be slightly different. Messages aren’t saved to Daily — there is no method like getAllMessagesForThisSpecificMeeting(). Any message we send to Daily will also need to be saved to our React component’s state. The potential problem here is that if we remove the Chat component from the DOM, we’ll lose its state, and lose access to the messages.

So any functionality that does something with chat messages must do two things:

  1. Send the message to Daily, so Daily can notify other participants that there’s a new message
  2. Put that message in React state, so we can display it in the UI, and keep that state around even when the chat isn’t active

To avoid losing the messages, we’ll always render the <Chat /> component; we just don’t always render its HTML. We want to have access to its state at all times, or at least until the participant leaves a meeting or refreshes the page.

Note: you could always use your own database to store these messages and pull them whenever you want to display them. With a database, you would also be able to keep the messages around for longer than the duration of the call. In our demo app, we decided to go with a client-side code solution to keep things simple.

<Chat/> and useAppMessage()

Let’s take a look at the <Chat/> component. We’ll be using two Daily React hooks:

  1. useLocalParticipant() to retrieve usernames of senders
  2. useAppMessage() to deliver messages

We discussed useLocalParticipant() in part two and used this hook to set a participant’s user name in the prejoin UI. We’ll use it here to retrieve the user name of a message’s sender:

const localParticipant = useLocalParticipant();

const username = localParticipant?.user_name || 'Guest',

useAppMessage() is slightly different from the other hooks in the Daily React Hooks library. useLocalParticipant() allows you to retrieve a participant object and its properties. The useAppMessage() hook actually takes an object with an onAppMessage function, which is a callback for the app-message” event. That might sound complicated, so let’s break it down with code:

import { useAppMessage } from '@daily-co/daily-react-hooks';

const sendAppMessage = useAppMessage({
 onAppMessage: useCallback(event => console.log(event), []),
});

When sendAppMessage() is called, the ”app-message” event is fired for all participants, except the one doing the sending.

sendAppMessage(
 {
   msg: “hi”,
   name: “Maude”
 },
 '*', // we’ll return to this wildcard later!
);

If Maude and Jeff are in a call and Maude sends a message, Jeff’s browser console will log the following:

When an "app-message" event is fired, the onAppMessage callback in the hook will be executed. You’ll see that this callback has an event payload: event. This payload will contain a bunch of information about the message. Most importantly, it’ll include the msg and the name. We’ll use this payload to put the message in our <Chat/> component’s state.

Let’s change our sendAppMessage() function so that instead of logging data to the console, it saves messages to a messages array:

const sendAppMessage = useAppMessage({
 onAppMessage: useCallback(
   (event) =>
     setMessages((messages) => [
       ...messages,
       {
         msg: event.data.msg,
         name: event.data.name,
       },
     ]),
   [],
 ),
});

And we’ll call sendAppMessage() like this:

sendAppMessage(
 {
   msg: message,
   name: localParticipant?.user_name || 'Guest',
 },
 '*',
);

The second entry in the object that we’re sending to sendAppMessage() is a string, ’*’. This means that we are broadcasting the message to every participant in the meeting. If you want to send a private message, you would replace the wildcard with a participant’s session_id.

What is important to understand here is that broadcast messages (i.e. messages to *) are not delivered to the sender of the message. This means that when we render the messages array in our JSX, people will not see their own messages.

Let’s unpack that with an example. We have a meeting with three participants: Liza, Jeff, and Maude. When Liza sends a message to *, the ”app-message” event will fire for Jeff and Maude, but not for Liza, since she’s the broadcaster in this scenario. This means that for Liza, setMessages() is never called. She’ll never see her own message! This is pretty strange when you’re building a chat UI — you definitely want to see your own messages. On the other hand, Liza already knows what her message is (she wrote it!) so she doesn’t need an event with that information.

Jennifer Lawrence #Awards GIF by BAFTA

So we have to do some extra work to allow Liza to render her own messages in the chat UI. Luckily, it’s not a lot 😌. We’ll wrap sendAppMessage() in a new sendMessage() function that uses React’s useCallback() hook. Inside this new function, we’ll call sendAppMessage() for broadcasting to other participants, and we’ll also directly call setMessages() just for the sender:

const sendMessage = useCallback(
 (message) => {
   /* Send the message to all participants in the chat - this does not include ourselves!*/
   sendAppMessage(
     {
       msg: message,
       name: localParticipant?.user_name || 'Guest',
     },
     '*',
   );

   /* Since we don't receive our own messages, we will set our own message manually in the messages array. This way _we_ can also see what we wrote.*/
   setMessages([
     ...messages,
     {
       msg: message,
       name: localParticipant?.user_name || 'Guest',
     },
   ]);
 },
 [localParticipant, messages, sendAppMessage],
);

When Liza hits ‘Send’ in the form in <Chat/>, the useAppMessage() hook will broadcast her message to Maude and Jeff, and the function will save her own message in the messages array – allowing her to see what she just wrote.

We just need to add the form, and then we’re pretty much done building a working chat in our video app! 💃

The form

In our <Chat/> component we render a form with an <input> field. Anything the user fills out there, we’ll track in the inputValue component state:

const [inputValue, setInputValue] = useState('');

const onChange = (e) => {
 setInputValue(e.target.value);
};

<input
 type="text"
 placeholder="Type your message here.."
 value={inputValue}
 onChange={(e) => onChange(e)}
/>

When a participant sends a message, the handleSubmit() function is called. This broadcasts the message stored in inputValue to other participants on the call using the sendMessage() function we defined earlier. sendMessage() will also save the participant’s own message in the messages array, so they’re able to see their own messages.

Then in our JSX, we will render the messages array using the .map() function:

<ul>
 {messages?.map((message, index) => (
   <li key={index}>
     <span>{message?.name}</span>:{' '}
     <p>{message?.msg}</p>
   </li>
 ))}
</ul>

Here’s the result:

Notifications

Our chat UI is togglable, meaning not everyone will have it open at all times. We should notify participants who don’t have the chat opened when there is a new message.

One of the ways to do this is to turn the Chat icon in <Tray/> red when a new message is received by a participant, like so:

{newChatMessage ? <ChatIconHighlighted /> : <ChatIconNormal />}

We can use the useDailyEvent() hook for this. Remember that when a new message is sent, ”app-message” is triggered for all other participants. By listening for ”app-message” in our <Tray/> component, we can set the newChatMessage boolean accordingly.

// Tray.js
const [showChat, setShowChat] = useState(false);
const [newChatMessage, setNewChatMessage] = useState(false);

useDailyEvent(
 'app-message',
 useCallback(() => {
   /* Only light up the chat icon if the chat isn't already open. */
   if (!showChat) {
     setNewChatMessage(true);
   }
 }, [showChat]),
);

If a participant receives a new message and does not have the chat UI toggled open, newChatMessage will be set to true, and ChatIconHighlighted will be rendered in the control tray.

When the participant opens the chat UI, we’ll set newChatMessage back to false. Obviously, this approach to notifications is a bit naive. Just because someone has opened the chat, doesn’t necessarily mean they’ve read the new message. Here are some ideas to make notifications even better:

  • Only mark the message as read after 5 seconds
  • Only mark the message as read after the participant has hovered over it with their mouse
  • Show the amount of unread messages by counting the number of ”app-message” events received

But about chat history?

If a participant joins a meeting after others have already been chatting, they won’t be able to see the sent messages. After all, since they weren’t participating in the meeting, they never received any ”app-message” events. Dropping new participants into an empty chat with no context isn’t a great user experience.

We can solve this problem with client-side React code. No need for databases or servers! This blog post on adding chat history explains how you can use sendAppMessage() and shared local application state to allow new participants in the meeting to read older messages. Keep in mind though that this post was written before Daily React Hooks was launched, so you can update the code to use Daily hooks or use it as is. It’s up to you!

With that being said: we're done 🥳! Here is the chat in action:

0:00
/

Conclusion

This concludes the Daily React Hooks series! If you’ve followed along with all three posts (or if you’ve just cloned the repo, which is totally valid), you now have a fully functional custom video application ​that includes a prejoin UI and chat! 💃

There are tons of other things you can add to your app, such as highlighting the active speaker in a call, spatialization, audio and video recording or custom video backgrounds.

Not all of these tutorials use the Daily React Hooks library, but you by now should have a good enough understanding on how to use them to create new things. The sky's the limit! If you’ve built something cool, we’d love to see it: reach out to us anytime at help@daily.co or Twitter. 🥰