Ephemeral Chat Messages

Ayooluwa I.
Ayooluwa I.
Published February 18, 2020

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:

sh
1
npx create-react-app stream-chat-demo

If you do not have npx on your machine, install it first by running yarn 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:

sh
1
yarn 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!

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->