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.
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
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 [email protected]:mitterio/mitter-web-starter --branch with-scl
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 themitter-web-starter
package which you can use as a starting point to test out SCL.
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.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:
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.Last modified 5yr ago