Setting Up Your App

Setting up the project

In this tutorial, we will start with creating a basic chat application where users can chat with each other and then incrementally add more functionality to the app. For this app, we will be using React and generating the project using create-react-app. However, to use the Mitter.io SDKs, you do not need to use React or even the create-react-app utility. You can use any UI framework of your choice, and you can change the UI components accordingly.

You can also simply clone the mitter-web-starter project from GitHub, which has all of this already setup for you.

Firstly, get the create-react-app utility using npm or yarn

yarn global add create-react-app​

Or using npm

npm install -g create-react-app​

Then create your app and install the dependencies

create-react-app my-mitter-app
cd my-mitter-app​

# Install the dependencies
yarn install

​# Install the mitter.io SDKs
yarn add axios @mitter-io/web @mitter-io/models​

Now, you can start the app

yarn start

This will open up the application in your default browser and you will see the standard React application with the placeholder content.

Let us first strip out all the placeholder stuff and just add a generic message. To do so, simply edit App.js to look something like this:

App.js
import React, { Component } from 'react'

class App extends Component {
    render() {
        return (
            <div className='App'>
                Hello!
          </div>
        );
    }
}

Setting up users

In this example application, we will be adding three users with the usernames @john, @amy, @candice. Among themselves, they will have two channels: #john-amy which will be a direct channel be between @john and @amy, and a channel #vacation which will be a channel between @john, @amy and @candice. To recognize which user is which, we will use a pattern from the URL. So, when a user visits http://localhost:3000/user/@john, it will recognize the user as @john. To do so, open up index.js and add the following code after the import statements:

index.js
const regex = /^\/user\/(@[a-zA-Z0-9-]+)/
const loggedUser = (new URL(document.location.href).pathname.match(regex)[1])

Also, we need to pass this property to the App component. Further down in the same file, pass this as a prop to App

App.js
<App
    loggedUser={loggedUser}
/>

Any call that is made to Mitter.io requires the caller to identify itself as an authenticated entity. There are a few anonymous calls that are allowed, but they offer little to no functionality and are present to facilitate the fetching of authorization for the user. In a real world scenario, you would get the user authorization from your application backend which acts as a source of truth. There is no way to anonymously fetch user authorization or to create a new user, unless the application has enabled federated authentication (in which case, the user still has to identify themselves via an authentication provider like Google OAuth).

One way to get user authorization tokens for development purposes (and development purposes ONLY) is to use the Mitter.io Dashboard to get user authorization tokens.

In the same Dashboard, we are going to create our users as well for the purposes of setting up this app. Do note that in a production environment, user management should also be done from a trusted source like an application backend.

Go to the Mitter.io Dashboard and navigate to the application that you created. In this application, navigate to the Users Panel and create three new users, setting their screen names to @john, @amy and @candice. Do note that this is not the same as the user ID itself. When you create a user on Mitter.io, it generates a user ID automatically. Once you've done that, you can generate user tokens for each of these users. The token itself encodes the generated user ID for the user. You can inspect the contents of the token by decoding it, or online at https://jwt.io.

Put these user tokens in your index.js file, mapping it to their user IDs:

index.js
const userAuth = {
    '@john': ' ... johns user token ...',
    '@amy': ' ... amys user token ...',
    '@candice': ' ... candices user token ... '
}

We'll be using this information shortly. Meanwhile, let us build the basic UI for our application.

Creating a basic Chat UI

Let us add a few basic elements to this application, generic for any app

App.js
import React, { Component } from 'react'

class App extends Component {
    render() {
        return (
            <div className='App'>
                <h2 className='application-title'>
                  My Chat App
                  
                  <div className='user-label'>
                      Welcome, <strong>{this.props.loggedUser}</strong>
                  </div>
              </h2>
          </div>
        );
    }
}

And the associated styling for this component to be added in App.css (feel free to remove any content that is already there)

App.css
.App {
    position: absolute;
    width: 100%;
    bottom: 0;
    top: 0;
}

.application-title {
    margin: 0;
    padding: 15pt;
    border-bottom: 1pt solid black;
    color: white;
    background-color: #741090;
}

.user-label {
    font-size: 10pt;
    float: right;
    position: relative;
    font-weight: normal;
}

At this point, your app will look something like this

NOTE If you are using the mitter-react-starter project as your base, then this is the state you will start at.

So far it doesn't do much, and we need to add something that makes it more of a chat application. To do so, we will create two panels - one with a list of channels a user is a part of, and another panel that shows the messages in a selected channel. Here, we will introduce a component of an activeChannel which is simply the channel that the user has currently selected.

Create two new files, ChannelComponent.js and Channel.css

ChannelComponent.js
import React, { Component } from 'react'
import './Channel.css'

export default class ChannelComponent extends Component {
    constructor() {
        super();

        this.state = {
            activeChannel: null
        }
    }

    componentDidUpdate() {
        if (Object.keys(this.props.channelMessages).length > 0) {
            this.setActiveChannel(Object.keys(this.props.channelMessages)[0])()
        }
    }

    renderChannelList() {
        return Object.keys(this.props.channelMessages).map(channelId => {
            const isChannelActive = this.state.activeChannel === channelId

            return (
                <div
                    key={channelId}
                    className={ 'channel-tile' +
                        ((isChannelActive) ? ' active' : '') }
                    onClick={this.setActiveChannel(channelId)}
                >
                    { channelId }
                </div>
            )
        })
    }

    renderMessages() {
        if (this.state.activeChannel === null) {
            return <div></div>
        }

        const activeChannelMessages =
            this.props.channelMessages[this.state.activeChannel]

        return activeChannelMessages.map(message => {
            return (
                <div key={message.messageId}>
                    {message.textPayload}
                </div>
            )
        })
    }

    render() {
        return (
            <div className='chat-parent chat-panel'>
                <div className='channel-list'>
                    { this.renderChannelList() }
                </div>

                <div className='chat-window chat-panel'>
                    { this.renderMessages() }
                    <div className='message-input-box'>
                        <input className='message-input' type='text' />
                        &nbsp;
                        <input className='send-message' type='submit' value='Send' />
                    </div>
                </div>
            </div>
        );
    }

    setActiveChannel(channelId) {
        return () => {
            this.setState((prevState) => Object.assign({}, prevState, {
                activeChannel: channelId
            }))
        }
    }
}
Channel.css
.chat-parent {
    background-color: green;
    display: flex;
    top: 69px;
    position: absolute;
    width: 100%;
    bottom: 0;
}

.chat-parent > .channel-list {
    width: 350px;
    background-color: #D3E2FE;
    border-right: 1pt solid black;
}

.chat-parent > .chat-window {
    flex: 1;
    background-color: white;
}

.chat-parent > .channel-list > .channel-tile {
    padding: 20pt;
    border-bottom: 1pt solid black;
}

.chat-parent > .channel-list > .active {
    border-bottom: 2pt solid black;
    font-weight: bold;
    background-color: #082289;
    color: white;
}

.chat-parent > .chat-window > .message-input-box {
    display: flex;
    position: absolute;
    bottom: 0;
    right: 0;
    left: 350px;
    padding: 5pt;
}

.message-input-box > .message-input {
    flex: 1;
    font-size: 12pt;
    padding: 5pt;
}

.message-input-box > .send-message {
    font-size: 12pt;
    padding: 5pt;
}

This is a pretty standard React component, but let's walk through the shapes of the component's state and props. In the constructor you can see the state shape:

ChannelComponent.js
this.state = {
    activeChannel: null
}

What we are doing is storing the currently active channel in the state, and a null basically signifies that there is no channel currently selected. What we will be storing here is simply the identifier of the channel that is currently selected.

The props that it expects are of the shape:

{
    channelMessages: {
        channelId: [ array of messages ],
        channelId: [ array of messages ]
    }
}

We are storing all the messages in a channel in an array and are associating it in a dictionary with the channelId as the key of the map. Once you have this, we need to render this component. Also we would really like to test out how this looks, so let us first create a mock of the data we expect:

App.js
const channelMessages = {
    'channel-a': [{
      messageId: 'message-001',
      textPayload: 'hello world!',
      senderId: {
        identifier: '@john'
      }
    }, {
      messageId: 'message-002',
      textPayload: 'hello back!',
      senderId: {
        identifier: '@amy'
      }
    }],
    'channel-b': []
}

class App extends Component {
    render() {
        return (
            <div className='App'>
                <h2 className='application-title'>
                  My Chat App

                  <div className='user-label'>
                      Welcome, <strong>{this.props.loggedUser}</strong>
                  </div>
              </h2>

              <ChannelComponent
                  channelMessages={channelMessages}
              />
          </div>
        );
    }
}

export default App;

Take a look at the channelMessages object we created at the beginning of the file. This contains a list of messages in a map, mapped by the channel ID. Do note that all of this is simply dummy data which we will later wire to actual data from Mitter.io. The shape of the message object is what the Mitter.io platform uses in all of its request/response objects. The ones that are shown here are:

  1. messageId A globally unique ID for the message. Do note that this ID is namespaced by your application, not by the channel it is in, and hence is unique across all messages in your application.

  2. textPayload A text representation of your message. Any message, be it a file, image, multimedia is always accompanied by a text representation of it. In this example app, we will be using this to render our messages.

  3. senderId The identifier of the user who sent this message.

NOTE Mitter.io always sends out identifiers in an encapsulated object. Some IDs are also sent with specific names in top-level objects. For example, here messageId is directly a string, but senderId is an encapsulated object of the shape {identifier: '...'}. The actual message object received from the server will have both messageId and an identifier object as well, both pointing to the same ID. Refer to the docs on Mitter API modelling for more information.

Coming back to our ChannelComponent.js file, take a look at the componentDidMount() function:

ChannelComponent.js
    componentDidUpdate() {
        if (Object.keys(this.props.channelMessages).length > 0) {
            this.setActiveChannel(Object.keys(this.props.channelMessages)[0])()
        }
    }

Nothing fancy, but all that we are doing is setting the first channel as active if there are any channels that were passed to this object. And the render function for the same:

ChannelComponent.js
    renderChannelList() {
        return Object.keys(this.props.channelMessages).map(channelId => {
            const isChannelActive = this.state.activeChannel === channelId

            return (
                <div
                    key={channelId}
                    className={ 'channel-tile' +
                        ((isChannelActive) ? ' active' : '') }
                    onClick={this.setActiveChannel(channelId)}
                >
                    { channelId }
                </div>
            )
        })
    }

We iterate through the channel objects, and simply add a div for each channel that the user is a part of. We also add another class called active to the tile of the channel that is currently active. The exact styles are as shown in Channel.css a little above in the documentation.

Clicking on a channel tile sets that tiles channel to be the active channel, as can be seen in the function setActiveChannel.

For rendering our messages, we are currently just stacking it over one another in the renderMessages() function:

ChannelComponent.js
    renderMessages() {
        if (this.state.activeChannel === null) {
            return <div></div>
        }

        const activeChannelMessages =
            this.props.channelMessages[this.state.activeChannel]

        return activeChannelMessages.map(message => {
            return (
                <div key={message.messageId}>
                    {message.textPayload}
                </div>
            )
        })
    }
    

This doesn't look all that good, but we will get to styling it in a bit. So far, the application should look something like this:

What we would like to now do is align the messages that were sent by the logged in user to the right, and other messages to the left. We would also like to show the sender ID for messages not sent by us. To do so, we have to modify the renderMessages function a bit:

ChannelComponent.js
    renderMessages() {
        if (this.state.activeChannel === null) {
            return <div></div>
        }

        const activeChannelMessages =
            this.props.channelMessages[this.state.activeChannel]


        return activeChannelMessages.map(message => {
            const isSelfMessage =
                this.props.selfUserId === message.senderId

            return (
                <div key={message.messageId}
                     className={ 'message' + (isSelfMessage ? ' self' : '') }
                >
                    <div className='message-block'>
                        <span className='sender'>{message.senderId}</span>

                        <div className='message-content'>
                            {message.textPayload}
                        </div>
                    </div>
                </div>
            )
        })
    }
    

What we are doing here is rendering the senderId in the message block and we have also added some hierarchy to the overall message structure that will allow us to align it. Also, for messages sent by the current user (as checked on line no. 12, in the variable isSelfMessage) we will attach another class self to the top-level message element.

Add these following style definitions to the Channel.css fileChannel.css

Channel.css
.message-input-box > .message-input {
    flex: 1;
    font-size: 12pt;
    padding: 5pt;
}

.message-input-box > .send-message {
    font-size: 12pt;
    padding: 5pt;
}

.chat-parent > .chat-window > .message {
    margin: 10pt;
}

.message > .message-block {
    display: inline-block;
}

.message > .message-block > .sender {
    font-size: 10pt;
}

.message > .message-block > .message-content {
    background-color: #D3E2FE;
    padding: 10pt;
    margin-top: 2pt;
}

.message.self > .message-block > .sender {
    display: none;
}

.message.self {
    text-align: right;
}

With this done, your app should now look something like:

Great going so far! Now what we need to do is wire up our application to consume the actual data from Mitter.io. To do this, let's head on over to the next section.

Last updated