One increasingly common feature in chat apps is the ability to send self-destructing messages, also known as "ephemeral" messages. When this feature is enabled, the messaging system automatically erases the content minutes or seconds after the message is sent. This deletion is effective on all the devices that received the message, as well as on system servers, to ensure that no lasting record of the conversation is kept.
This tutorial will demonstrate how you can implement ephemeral messages in your chat app by hooking into the Stream Chat API! A demo of the application we’ll be building is shown below; notice how the messages disappear after a short time:
Prerequisites
To follow along with this tutorial, you'll need to have Node.js and yarn installed on your computer. We'd also recommend that you have some experience with building React applications.
Signing Up for Stream Chat
Create a free Stream account or sign in to your existing account here. Once you’re logged in, create a new application and grab your app access keys, which we’ll be making use of shortly:
Bootstrapping the React Application
Use the create-react-app
package to bootstrap a new React application for this tutorial:
1npx create-react-app stream-chat-demo
If you do not have
npx
on your machine, install it first by runningyarn global add npx
.
Next, cd
into the stream-chat-demo
directory and run the command below, to install the additional dependencies we’ll be using throughout the process of building our React app and Node.js server:
1yarn add express cors dotenv body-parser random-username-generator stream-chat stream-chat-react axios -D
With our dependencies installed, let’s go ahead and move on to the next step of the setup!
Setting Up the Express Server
Create a new .env
file in the root of your project directory, and paste in the credentials from your Stream application dashboard:
PORT=5500 STREAM_API_KEY= STREAM_APP_SECRET=
Next, create a new server.js
file and open it in your favorite text editor. Populate the file with the following code:
require('dotenv').config(); const express = require('express'); const cors = require('cors'); const bodyParser = require('body-parser'); const { StreamChat } = require('stream-chat'); const app = express(); app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); // initialize Stream Chat SDK const serverSideClient = new StreamChat( process.env.STREAM_API_KEY, process.env.STREAM_APP_SECRET ); app.post('/delete-message', async (req, res) => { const { timeout, message_id: messageID } = req.body; setTimeout(async () => { try { await serverSideClient.deleteMessage(messageID); res.status(200); } catch (err) { console.log(err); res.status(500); } }, 1000 * timeout); }); app.post('/join', async (req, res) => { const { username } = req.body; const token = serverSideClient.createToken(username); try { await serverSideClient.updateUser( { id: username, name: username, }, token ); } catch (err) { console.log(err); } const admin = { id: 'admin' }; const channel = serverSideClient.channel('team', 'group-chat', { name: 'Talk to me', created_by: admin, }); try { await channel.create(); await channel.addMembers([username, 'admin']); } catch (err) { console.log(err); } return res .status(200) .json({ user: { username }, token, api_key: process.env.STREAM_API_KEY }); }); const server = app.listen(process.env.PORT || 5500, () => { const { port } = server.address(); console.log(`Server running on PORT ${port}`); });
We have two routes on the server. The /join
route creates or updates a user on our Stream chat instance and generates a token that enables authentication on the application frontend. Meanwhile, the /delete-message
route expects a message_id
and timeout
, and deletes the message from the stream chat instance after the expiration of the timeout.
We’ll take a look at how we can send a message_id
and timeout
in the next step. You can start the server now by running node server.js
.
Building the Application Frontend
Open up src/App.js
and swap the code out for the following:
import React, { useState, useEffect } from 'react'; import './App.css'; import { Chat, Channel, ChannelHeader, Thread, Window, ChannelList, ChannelListTeam, MessageList, MessageTeam, MessageInput, } from 'stream-chat-react'; import { StreamChat } from 'stream-chat'; import rug from 'random-username-generator'; import axios from 'axios'; import 'stream-chat-react/dist/css/index.css'; let chatClient; function App() { const [channel, setChannel] = useState(null); useEffect(() => { const username = rug.generate(); async function getToken() { try { const response = await axios.post('http://localhost:5500/join', { username, }); const { token } = response.data; const apiKey = response.data.api_key; chatClient = new StreamChat(apiKey); const user = await chatClient.setUser( { id: username, name: username, }, token ); const channel = chatClient.channel('team', 'group-chat'); await channel.watch(); setChannel(channel); } catch (err) { console.log(err); return; } } getToken(); }, []); if (channel) { return ( <Chat client={chatClient} theme="team light"> <ChannelList options={{ subscribe: true, state: true, }} List={ChannelListTeam} /> <Channel channel={channel}> <Window> <ChannelHeader /> <MessageList Message={MessageTeam} /> <MessageInput focus /> </Window> <Thread Message={MessageTeam} /> </Channel> </Chat> ); } return <div></div>; } export default App;
That’s all the code we need to have a fully functional chat application! You can now start the development server using yarn start
and send a few messages in the app. Make sure your server is running before opening up the application.
Automatically Deleting Messages for Everyone
Now, let’s implement self-destructing messages for everyone! Update your App.js
file as follows:
import React, { useState, useEffect } from 'react'; import './App.css'; import { Chat, Channel, ChannelHeader, Thread, Window, ChannelList, ChannelListTeam, MessageList, MessageTeam, MessageInput, } from 'stream-chat-react'; import { StreamChat } from 'stream-chat'; import rug from 'random-username-generator'; import axios from 'axios'; import 'stream-chat-react/dist/css/index.css'; let chatClient; function App() { const [channel, setChannel] = useState(null); useEffect(() => { const username = rug.generate(); async function getToken() { try { const response = await axios.post('http://localhost:5500/join', { username, }); const { token } = response.data; const apiKey = response.data.api_key; chatClient = new StreamChat(apiKey); const user = await chatClient.setUser( { id: username, name: username, }, token ); const channel = chatClient.channel('team', 'group-chat'); await channel.watch(); setChannel(channel); channel.on('message.new', async event => { if (user.me.id === event.user.id) { await axios.post('http://localhost:5500/delete-message', { timeout: 5, message_id: event.message.id, }); } }); } catch (err) { console.log(err); return; } } getToken(); }, []); if (channel) { return ( <Chat client={chatClient} theme="team light"> <ChannelList options={{ subscribe: true, state: true, }} List={ChannelListTeam} /> <Channel channel={channel}> <Window> <ChannelHeader /> <MessageList Message={MessageTeam} /> <MessageInput focus /> </Window> <Thread Message={MessageTeam} /> </Channel> </Chat> ); } return <div></div>; } export default App;
The relevant changes are on lines 50-57. Stream Chat has a neat feature that allows us to listen for events that occur on a channel. Here, we’re listening for the message.new
event which is triggered when a new message is sent in the channel. We then pass the message_id
of the new message and a timeout
of 5 seconds to the /delete-message
route on the server which has the effect of deleting the message after five seconds.
To prevent the request from being sent multiple times (from all connected clients), we’re checking if the current user is the user who sent the message before triggering the POST to /delete-message
.
Final Thoughts
You now should have the basic structure in place to build ephemeral messaging into your chat application! For real-world applications, you might consider making the timeout for when messages disappear more configurable for users through a settings menu.
If you ran into any issues while building the demo, you can always clone the repo from GitHub for a fresh start. Be sure to check out Stream's interactive API tour and API documentation to learn about other things you can do with the Stream platform!
Thanks for reading!