Start a Basic Chat

Last updated 2 months ago

Make sure that you have followed the steps for creating the users/channels in the previous section. We will be using this data now to integrate your app with the Mitter.io platform

Setting up the Mitter.io SDK

Before you consume any Mitter.io APIs, you need to setup the Mitter object. In your index.js file, add the following lines:

index.js
import { Mitter } from '@mitter-io/web'
const mitter = Mitter.forWeb('... your application id ...')

Your application ID can be fetched from the Mitter.io Dashboard. Once you have this set up, you now need to pass in the user authorization. You can pick the correct user authorization for the user authorization map we had created earlier.

index.js
const userAuth = {
'@john': ' ... johns user token ...',
'@amy': ' ... amys user token ...',
'@candice': ' ... candices user token ... '
}
mitter.setUserAuthorization(userAuth[loggedUser])

We will also need to pass the mitter object we created to the App component so that it can fetch the user data and render it. At the end of this, your index.js file should look something like:

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { Mitter } from '@mitter-io/web'
const regex = /^\/user\/(@[a-zA-Z0-9-]+)/
const loggedUser = (new URL(document.location.href).pathname.match(regex)[1])
const userAuth = {
'@john': ' ... johns user token ...',
'@amy': ' ... amys user token ...',
'@candice': ' ... candices user token ... '
}
const mitter = Mitter.forWeb('... your application id ...')
mitter.setUserAuthorization(userAuth[loggedUser])
ReactDOM.render(
<App
mitter={mitter}
loggedUser={loggedUser}
/>,
document.getElementById('root')
);
registerServiceWorker();

Getting the list of participated channels

We now want to get the list of channels for a user and then render them in our app. To do so, we'll have to make a couple of changes in our application. First, we'll have to move the channel messages object that we created to a variable that can be changed and propagated to the ChannelComponent. We'll move it to the state for the App component. Also, we'll pass on the Mitter object to the the ChannelComponent as we will need to send messages later on.

App.js
class App extends Component {
constructor() {
super();
this.state = {
channelMessages: {}
}
}
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
mitter={this.props.mitter}
channelMessages={this.state.channelMessages}
selfUserId={this.props.loggedUser}
/>
</div>
);
}
}

Do note that we have even modified the render() function to now pass the channelMessages prop from App via a state rather than the hard-coded variable. If you reload your application, you'll see a blank page. In your App component, we will now fetch a list of channels for the user. We will do this in the componentDidMount() method:

NOTE Do note that for a messaging-based application, the architecture of this application is not a recommended one. Ideally, you should be using a state management system like redux or flux, but for the sake of simplicity we are not using this in the application so that we can focus on introducing Mitter.io concepts. There is also a package mitter-redux currently in alpha that builds atop redux and handles all intricacies of state management which should be used in production apps.

App.js
class App extends Component {
construtor() {
super()
this.state = {
channelMessages: {}
}
this.setChannels = this.setChannels.bind(this)
}
setChannels(participatedChannels) {
const activeChannels = {}
Objects.forEach(participatedChannels, (participatedChannel) => {
activeChannels[participatedChannel] = []
this.setState((prevState) => {
return Object.assign({}, prevState, {
activeChannels
})
})
})
}
componentDidMount() {
const mitter = this.props.mitter
mitter.clients().channels().participatedChannels()
.then(participatedChannels => this.setChannels(participatedChannels))
}
}

In the code above, we are transforming a response we get from Mitter.io, of the form:

[
{
participantId: '...',
channel: {
channelId: 'channel-a'
},
participationStatus: 'Active'
},
{
partipantId: '...',
channel: {
channelId: 'channel-b'
},
participationStatus: 'Active'
}
]
{
'channel-a': [],
'channel-b': []
}

Which is basically a map of channel IDs to an empty array of messages. We will be adding messages to this object as we get them from the Mitter.io pipeline.

Reload the page and you should see the channels listed for the current selected user.

The chat window with the user's channels loaded from mitter.io

Change the url from http://localhost:3000/user/@john to http://localhost:3000/user/@candice and you should now see only one channel (#roadtrip) as opposed to two channels earlier.

We are now loading the channels for each user from Mitter.io

Listening to messages and populating them in a channel

Now, we will move on to setup the next phase of the project, where we listen to incoming events (in this case specifically, messages) and render them in our application. To listen to pipeline payloads (that's what Mitter calls events sent on different front-end mechanisms), you need to subscribe to them. So, add the following lines in the componentDidMount() function (also pay attention to the additional import isNewMessagePayload at the top of the file):

App.js
import { Mitter, isNewMessagePayload } from '@mitter-io/core'
// your other code and imports
class App extends React.Component {
constructor() {
// Previous code in constructor
this.newMessage = this.newMessage.bind(this)
}
// other functions in the App
newMessage(messagePayload) {
// currently does nothing
}
comoponentDidMount() {
mitter.subscribeToPayload(payload => {
if (isNewMessagePayload(payload)) {
this.newMessage(messagePayload)
}
})
}
// ... rest of the file

Adding this new message to our state is quite straightforward now. This is how the newMessage method should now look:

App.js
newMessage(messagePayload) {
this.setState((prevState) => {
const channelId = messagePayload.channelId.identifier // [1]
if (
prevState.channelMessages[channelId]
.find(x => x.messageId === messagePayload.message.messageId)
!== undefined
) { // [2]
return prevState
}
return Object.assign({}, prevState, { // [3]
channelMessages: Object.assign({}, prevState.channelMessages, {
[messagePayload.channelId.identifier]:
prevState.channelMessages[messagePayload.channelId.identifier]
.concat(messagePayload.message)
})
})
})
}

A quick description of what's going on here (follow the numbered labels in the code):

  1. We extract the channel ID from the payload. This is the channel that the message was sent in.

  2. We are checking if the message already exists for the same ID in our prevState. While you may not encounter this frequently, Mitter.io might occasionally send duplicate messages on a payload. This usually happens when Mitter.io cannot confidently determine that a message delivery has occurred, but it might still have propagated. Also, the current implementation performs an entire iteration of the messages in a channel, which might not be very efficient. As an exercise to the reader, modify this to a store backed by a hashing algorithm.

  3. We now concat this message on to the list of messages for the given channel.

NOTE There are certain caveats with this approach, notably that you might get receive payloads for messages for which you do not have a channels object yet. This could happen if a user was added to a channel after the participated channels were fetched. While such a situation will not arise in our setup, production apps need to always be resilient to partial state and must reconstruct the state in whatever form they can from the available events.

This is pretty much it! However, these changes will not result in you seeing anything, because no messages are being sent. In the next section lets wire it up to send messages.

Sending messages

To send messages, we'll have to wire up the Send button in our ChannelComponent. We'll add a few methods, namely sendMessage() and updateTypedMessage to ChannelComponent. Also, we'll set up the handlers on the input fields as we usually do for any React App. The input components will now look like this:

ChannelComponent.js
<div className='message-input-box'>
<input
ref={(input) => { this.messageInput = input }}
onChange={this.updateTypedMessage}
value={this.state.typedMessage}
className='message-input'
type='text'
/>
&nbsp;
<input onClick={this.sendMessage} className='send-message'
type='submit' value='Send' />
</div>

We'll modify our state to accommodate changes for the input field and also make the appropriate function binds so that we can use them as callbacks:

ChannelComponent.js
constructor() {
this.state = {
activeChannel: null,
typedMessage: ''
}
this.updateTypedMessage = this.updateTypedMessage.bind(this)
this.sendMessage = this.sendMessage.bind(this)
}

And the functions to now send the messages:

ChannelComponent.js
sendMessage() {
const mitter = this.props.mitter
this.setState((prevState) => Object.assign({}, prevState, { // [1]
typedMessage: ''
}))
this.messageInput.focus() // [2]
mitter.clients().messages() // [3]
.sendMessage(this.state.activeChannel, {
senderId: mitter.me(),
textPayload: this.state.typedMessage,
timelineEvents: [
{
type: "mitter.mtet.SentTime",
eventTimeMs: new Date().getTime(),
subject: mitter.me()
}
]
})
}
updateTypedMessage(evt) {
const value = evt.target.value
this.setState((prevState) => {
return Object.assign({}, prevState, {
typedMessage: value
})
})
}

The updateTypedMessage is your standard message to store the state of an input field, and have a way to control it. Let us look into what we are doing in the sendMessage function. Pay attention to the numbered labels in the code:

  1. When we send a message, we would like to clear the input field so that the user can type their next message

  2. We would also like to re-focus the messageInput field (this property is set in the ref callback of the <input> field)

  3. We now use the message client to send a message. This message contains the basic minimum fields required to send a message. While the senderId, textPayload have been discussed before, timelineEvents are something new. Let's discuss them for a while.

A TimelineEvent is used to record events that occur for a given entity. Mitter.io supports timeline events for Channels and Messages. For example, this is what is used to store and transmit read/delivered receipts. You are free to use any type of timeline events and interpret them as you wish, with the exception that they may not start with mitter. or io.mitter.. Also, any message that is sent must have a mitter.mtet.SentTime timeline event attached to it. The server then attaches another timeline event recording the server receive time, synchronized to the servers clock.

Once you've done this, open up two browser windows and go to http://localhost:3000/user/@john and http://localhost:3000/user/@candice. Try exchanging a few messages between them and you'll notice that you have a working chat app!

A basic working chat app with multiple users

You might be wondering how your own messages got rendered. This is because every message to a channel is sent out to all participants of the channel and hence every user always gets an echo back of their own message. Do note that on slower networks there will be a significant delay in this occurring, so you might want to populate your message state when the user hits Send and then let the network call take its time.

Let's now add a slightly more complex behavior in our application. In the next section, we will explore ACLs and see how we can use them to implement selective deliveries.