Using the UI framework (web only)

Mitter's standard component library (scl) makes it even easier to get started building your messaging apps. Unlike the platform and the SDK, the SCL takes a very opinionated approach to the various (often repeated) components of a messaging application.

Currently the SCL is in beta and is distributed for web platforms only. Since the SCL is primarily a UI framework, each edition of the framework is tied to a UI library. Currently one edition of the SCL is available, for React projects.

Installation

To add the scl to your project, simply use yarn or npm

yarn add @mitter-io/react-scl

or

npm install --save @mitter-io/react-scl

Before you begin make sure that your base project is setup as described here

Adding a managed Message list

In the getting started application we had the ability to send and receive messages. We were using a simple listener that would listen on any new messages coming to the channel and displaying them as a list of UI components. However, there are a few issues with the same:

  1. All previous messages are lost if the page is reloaded

  2. If we were to make a network call and get the previous messages that would be a sub-optimal experience as we would want to fetch only the messages that the user is interested in (preferably when they scroll up)

  3. If were to do that as well, we still don't want to generate a DOM element for every single message that was loaded - a channel could have upto thousands of messages and it could make your application extremely slow and bloated.

For this purpose, the SCL bundles in a component MessageListManager that takes care of all of the above along with some more nifty features for fine-grained control over message rendering.

In the following example, we will continue modifying the mitter-web-starter and use a message list from recat-scl instead.

You can checkout the final completed app:

git clone git@github.com:mitterio/mitter-web-starter --branch with-scl

Creating a message view producer

Before we can start using the MessageListManager we need to be able to tell the manager how to render individual messages. To do so, we'll start with creating a message component:

src/ChannelComponent.js
import { createMessageViewProducer } from '@mitter-io/react-scl'
this.messageViewProducer = createMessageViewProducer(
(message) => true,
(message) => {
const isSelfMessage =
this.props.selfUserId === message.senderId.identifier
return (
<div className={ 'message' + (isSelfMessage ? ' self' : '')}>
<div className='message-block'>
<span className='sender' ></span>
<div className='message-content'>
{message.textPayload}
</div>
</div>
</div>
)
}
)

A message view producer takes two lambdas - the first one is a predicate which for a given message dictates if the current view will be used to render the message and the second returns the rendered component. In our case we are setting our predicate to always return true since we want to use the same producer for all messages.

NOTE The above is code adapted from the mitter-web-starter package which you can use as a starting point to test out SCL.

Displaying messages

With a message view producer in place, we can now use the MessageListManager component. An example is as shown below:

src/ChannelComponent.js
renderMessages() {
if (this.state.activeChannel === null) {
return <div></div>
}
const activeChannelMessages =
this.props.channelMessages[this.state.activeChannel]
return (
<MessageListManager
messages={activeChannelMessages}
defaultView={this.messageViewProducer.produceView}
producers={[this.messageViewProducer]}
onEndCallback={() => {}}
/>
)
}

In the above example we are basically re-writing the renderMessages function. The various props passed to the component are:

messages This is a list of messages that are to be rendered. This can be a reference to the entire message list if required, or only a subset that is available. The MessageListManager uses a virtualized list that will create DOM elements for only the messages that are visible.

defaultView This is the view producer used when there is no other view available. Do note that we don't directly use the view producer, but instead use .produceView function on it. This is because defaultView does not perform any predicate checking and expects only rendering to be performed.

producers are a list of producers used to render messages. To render a particular message all producers are run (in insertion order) till one of their predicates returns true. If no such producer is found, then the defaultView is used.

An onEndCallback is a function called when new messages are to be fetched. We will shortly look into how to use this callback.

Till this point, everything would be working exactly like the mitter-web-starter. In terms of functionality we haven't really added anything else. Let's now add a PaginationManager to handle various events where the messages would be populated.

Adding a Pagination Manager

A PaginationManager is a construct available in the @mitter-io/core module which makes it easy to consume paginated APIs (like /v1/channels/{}/messages , /v1/channels) etc.

For our message list we need to do the following:

  1. Get the first page of messages whenever the component for a list of messages belonging to a particular channel is loaded.

  2. Get the previous page from the scroll point that the user scrolls up to.

Since we are maintaining a list of messages in our parent component, we will initiate the process of fetching the first list of messages in it's setChannels method (which in turn is called by componentDidMount).

Let's first modify the state to store our PaginationManager (this is required because pagination managers maintain state regarding which page they are on and hence should be persisted across the component lifecycle):

src/App.js
this.state = {
channelMessages: {},
channelMessageManagers: {}
}

Now when we get a list of channels, we want to create a pagination manager for the messages of each channel:

src/App.js
participatedChannels.forEach((participatedChannel) => {
channelMessages[participatedChannel.channel.identifier] = []
// Add the code below
channelMessageManagers[participatedChannel.channel.identifier] =
this.props.mitter.clients().messages()
.getPaginatedMessagesManager(participatedChannel.channel.identifier)
});

And then we'll write a function to fetch the first page of messages for a given channel:

src/App.js
// Make sure you've done the following in the constructor:
// this.fetchPreviousPage = this.fetchPreviousPage.bind(this)
fetchPreviousPage(channelId) {
let { channelMessageManagers } = this.state
channelMessageManagers[channelId].prevPage().then(messageList => {
const _list = this.state.channelMessages[channelId].slice()
for (let i=0; i < messageList.length; i++) {
_list.unshift(messageList[i]);
}
this.setState({
channelMessages: {
[channelId]: _list
}
})
});
}

And then we'll call it on the first page load:

src/App.js
Object.keys(channelMessageManagers).forEach((channelId) => {
this.fetchPreviousPage(channelId);
})

Try reloading the page now and you'll see that the latest few messages (by default 45) are automatically loaded in the message window.

However, when we scroll to the top, no new messages are loaded. For that we need to make simply make sure that this.fetchPreviousPage is called when the user scrolls to the top. To do so, pass on the method as a prop down to the MessageListManager :

In App.js,

src/App.js
<ChannelComponent
mitter={this.props.mitter}
channelMessages={this.state.channelMessages}
selfUserId={this.props.loggedUser}
fetchPreviousPage={this.fetchPreviousPage} />

Check the last prop that was added. And then pass this function to the onEndCallback prop in MessageListManager

src/ChannelComponent.js
<MessageListManager
messages={activeChannelMessages}
defaultView={this.messageViewProducer.produceView}
producers={[this.messageViewProducer]}
onEndCallback={() => {
this.props.fetchPreviousPage(this.state.activeChannel)
}}
/>

And that's all you have to do. Try scrolling up the window:

Adding a loader

If you tried out the application, you'd notice that when the user scrolls up there is a small delay before the previous messages are loaded. While this is happening, you might want to show a small loading icon/progress bar to the user. To do so, you have to set two props on the MessageListManager

src/ChannelComponent.js
<MessageListManager
messages={activeChannelMessages}
defaultView={this.messageViewProducer.produceView}
producers={[this.messageViewProducer]}
onEndCallback={() => {
this.props.fetchPreviousPage(this.state.activeChannel)
}}
isLoading={this.state.isLoading}
loader={() => <Loader />}
/>

Whenever isLoading is true, the message list will display the component returned by loader in the top area where the new messages would be appended.