Build a Chat App With Ionic 4

Ayooluwa I.
Ayooluwa I.
Published January 27, 2020 Updated June 9, 2021

Ionic is an open-source framework that allows you to build and deploy apps that work across multiple platforms, such as iOS, Android, desktop, and the web as a Progressive Web App – all with the same code base.

At first, Ionic was built to work with Angular, but with the release of Ionic 4, you can now use any framework to build your application. The Ionic team has provided official integrations for Angular and React, with full support for Vue on the way.

This article will explore how a Chat application can be built using Ionic 4 and React. The application you will be building is a group messaging app that will allow users to register, login, and then chat with other users who are on the platform.

Prerequisites

Before you proceed with this tutorial, make sure you have Node.js, npm and MongoDB installed on your machine. You also need to have a basic familiarity with Typescript, Node.js, and React. TypeScript is a superset of JavaScript that adds static types to the language, and Ionic 4 requires its use.

Sign up for Stream

Go here to create a free Stream account or login to your existing account. Once you’re logged in, you can save the application access keys from the app that's automatically created for you. Or, if you prefer, go to the dashboard, hit the blue “Create App” to create an app. Once you have created your application, you will be presented with your application access keys, which we’ll be making use of soon.

Image shows access keys in the stream dashboard

Set up the server

We’ll be making use of the Open-Source Stream Chat API to set up our server. Built with Express and MongoDB, it supports user authentication and storage through a MongoDB database and token generation for new and existing users on a Stream Chat instance, amongst other things.

The first thing to do is to clone the repo using the command below:

bash
1
$ git clone https://github.com/astrotars/stream-chat-api

Following that, cd into the newly created directory and run npm install to download the necessary dependencies. Next, rename the .env.example file to .env and add your Stream app credentials and MongoDB database URL, which should be mongodb://localhost:27017/stream-chat if you’re running MongoDB locally and can access it on localhost:27017. If this is not the case, then change the URL as needed.

At this point, you can start the server in development mode using the command below:

bash
1
$ npm run dev

Create the Ionic Application

The first step is to install the tooling we need to bootstrap the application:

bash
1
$ npm install -g ionic

Next, create an Ionic React app that uses the “blank” starter template and adds Capacitor for native functionality:

bash
1
$ ionic start chat-app blank --type=react --capacitor

It might take a while to pull in all the dependencies required to build the application. Once it’s finally done, cd into the new chat-app directory and run the command below to install Stream's Chat JavaScript SDK, React components, axios for making requests in the browser, and react-bulma-components to provide some styling for the app.

bash
1
$ npm install stream-chat stream-chat-react axios react-bulma-components
Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Finally, run ionic serve to start the development server on http://localhost:8100.

Set up Authentication

Before a user is allowed access to the chatroom, they need to be authenticated against the application. The server we set up in the previous step supports user authentication and storage, so all we need to do now is provide a way for users to register and login into the user interface.

Open up your text editor in the root of your Ionic app, and create a new Auth.tsx file in the src/pages directory:

// src/pages/Auth.tsx

import React from 'react';
import 'react-bulma-components/dist/react-bulma-components.min.css';
import './Auth.css';

function Auth(props: any) {
  const {
    auth,
    setAuth,
    login,
    firstNameInput,
    lastNameInput,
    emailInput,
    passwordInput,
  } = props;

  return (
    <div className="auth">
      <div className="card">
        <div className="tabs is-centered">
          <ul>
            <li className={auth === 'login' ? 'is-active' : ''}>
              <a
                href="#"
                onClick={e => {
                  e.preventDefault();
                  setAuth('login');
                }}
              >
                <span>Login</span>
              </a>
            </li>
            <li className={auth === 'signup' ? 'is-active' : ''}>
              <a
                href="#"
                onClick={e => {
                  e.preventDefault();
                  setAuth('signup');
                }}
              >
                <span>Sign Up</span>
              </a>
            </li>
          </ul>
        </div>
        <div className="content">
          <div className="signup">
            <form action="">
              <div className={`field ${auth === 'login' ? 'hidden' : ''}`}>
                <label className="label" htmlFor="first_name">
                  First name:
                </label>
                <div className="control">{firstNameInput}</div>
              </div>
              <div className={`field ${auth === 'login' ? 'hidden' : ''}`}>
                <label className="label" htmlFor="last_name">
                  Last name:
                </label>
                <div className="control">{lastNameInput}</div>
              </div>
              <div className="field">
                <label className="label" htmlFor="email">
                  Email:
                </label>
                <div className="control">{emailInput}</div>
              </div>
              <div className="field">
                <label className="label" htmlFor="password">
                  Password:
                </label>
                <div className="control">{passwordInput}</div>
              </div>
            </form>
            <button
              onClick={() => login()}
              className="button is-link is-fullwidth"
            >
              Submit
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

export default Auth;

Auth.tsx is a component that contains a basic form for signing up or logging into the application. Add the following styles for this component in src/pages/Auth.css:

/* src/pages/Auth.css*/

.auth {
  width: 100%;
  max-width: 500px;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.content {
  padding: 0px 20px 20px;
}

.button {
  margin-top: 20px;
}

.hidden {
  display: none;
}

We can render the Auth component by placing it in Home.tsx as shown below:

// src/pages/Home.tsx

import React, { useState } from 'react';
import axios from 'axios';
import Auth from './Auth';

function useInput(type: string) {
  const [value, setValue] = useState('');
  const input = (
    <input
      className="input"
      value={value}
      onChange={e => setValue(e.target.value)}
      type={type}
    />
  );
  return [value, input];
}

function App() {
  const [auth, setAuth] = useState('login');
  const [firstName, firstNameInput] = useInput('text');
  const [lastName, lastNameInput] = useInput('text');
  const [email, emailInput] = useInput('text');
  const [password, passwordInput] = useInput('password');

  async function login() {
    const payload = {
      name: {
        first: firstName,
        last: lastName,
      },
      email: email,
      password: password,
    };

    try {
      const response = await axios.post(
        'http://localhost:8080/v1/auth/init',
        payload
      );

      console.log(response.data)
    } catch (err) {
      console.log(err);
    }
  }
 
  return (
    <Auth
      auth={auth}
      setAuth={setAuth}
      firstNameInput={firstNameInput}
      lastNameInput={lastNameInput}
      emailInput={emailInput}
      passwordInput={passwordInput}
      login={login}
    />
  );
}

export default App;

Here, we’ve set up the Auth component and passed down the necessary props that are required to display the input fields. Also, the login function is passed down to Auth so that once the Submit button is clicked, the user’s details are sent to the server for authentication. In this demo app, we’re using the same function for both signs up and log in to reduce the amount of code we have to write.

On the server-side, the user’s email is checked against the MongoDB database to verify if it already exists. If not, a new user is created in the database and also on our Stream instance. Otherwise, the user with the email is retrieved from the database, and the password is checked against the hashed password stored in the database to confirm that it matches. If so, a new token is generated for the user. This token is what will be used to authenticate the user on the client-side. You can find the code for this process in src/controllers/v1/auth/init.action.js. It’s heavily commented, so it should be easy to figure out.

Try signing up as a new user. You will see the generated token for the user in your browser console if all goes well.

Set up the Chat Interface

Now that we can generate and retrieve user tokens from the server, we have everything we need to initiate the user on our Stream chat instance and create a UI for the chatroom. We’ll make use of the ready-made Stream React components, which makes the complicated task of setting up a feature-rich chat interface trivial, as you’ll see.

Update your Home.tsx component to look like this:

// src/pages/Home.tsx

import React, { useState } from 'react';
import {
  Chat,
  Channel,
  ChannelHeader,
  Thread,
  Window,
  ChannelList,
  ChannelListTeam,
  MessageList,
  MessageTeam,
  MessageInput,
} from 'stream-chat-react';
import { StreamChat } from 'stream-chat';
import axios from 'axios';
import Auth from './Auth';

import 'stream-chat-react/dist/css/index.css';

let chatClient: any;

function useInput(type: string) {
  const [value, setValue] = useState('');
  const input = (
    <input
      className="input"
      value={value}
      onChange={e => setValue(e.target.value)}
      type={type}
    />
  );
  return [value, input];
}

function App() {
  const [channel, setChannel] = useState(undefined);
  const [auth, setAuth] = useState('login');
  const [firstName, firstNameInput] = useInput('text');
  const [lastName, lastNameInput] = useInput('text');
  const [email, emailInput] = useInput('text');
  const [password, passwordInput] = useInput('password');

  async function login() {
    const payload = {
      name: {
        first: firstName,
        last: lastName,
      },
      email: email,
      password: password,
    };

    try {
      const response = await axios.post(
        'http://localhost:8080/v1/auth/init',
        payload
      );

      const { apiKey, user, token } = response.data;
      chatClient = new StreamChat(apiKey);
      await chatClient.setUser(
        {
          id: user._id,
          name: user.name.first,
          role: 'admin',
        },
        token
      );

      const channel = chatClient.channel('messaging', 'General');
      await channel.watch();

      setChannel(channel);
    } catch (err) {
      console.log(err);
    }
  }

  if (channel) {
    return (
      <Chat client={chatClient} theme="messaging light">
        <ChannelList
          options={{
            subscribe: true,
            state: true,
          }}
          filters={{
            type: 'messaging',
          }}
          List={ChannelListTeam}
        />
        <Channel channel={channel}>
          <Window>
            <ChannelHeader />
            <MessageList Message={MessageTeam} />
            <MessageInput focus />
          </Window>
          <Thread Message={MessageTeam} />
        </Channel>
      </Chat>
    );
  }

  return (
    <Auth
      auth={auth}
      setAuth={setAuth}
      firstNameInput={firstNameInput}
      lastNameInput={lastNameInput}
      emailInput={emailInput}
      passwordInput={passwordInput}
      login={login}
    />
  );
}

export default App;

Here, we’ve imported a few components from the stream-chat-react package and used it to set up a functional chat interface that is presented to the user once they’ve been authorized on the server. You can view the documentation for each component here.

Once the token is received from the server, the user is set on our chat client instance, and a General channel is initialized before the chat interface rendered. By utilizing the Stream components, you get a lot of basic and advanced features for free such as typing indicators, emoji, reactions, file support, rich link preview, user presence (online or offline), and more. It helps you save much time instead of rolling out your interface.

You can test it out by logging in as the user you created earlier. It should work exactly, as shown in the GIF below:

Wrap Up

In this tutorial, we’ve described how to set up a fully functional chat application using Ionic 4 and Stream Chat. Ionic apps are genuinely cross-platform, so you can quickly deploy this application to Android or iOS without making any changes to the code.

The complete source code used in this tutorial can be found in this GitHub repo. We also have a follow up to this tutorial covering how to build an Ionic real-time chat application if you are looking for more to do. Thanks for reading!

Happy chatting! 👩‍💻

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