Real-Time Chat and Commenting Components

You can add a real-time chat to your application by using the CollaborationMessageList and CollaborationMessageInput components.

Example of a CollaborationMessageList and CollaborationMessageInput

They enhance the regular MessageList and MessageInput components by automatically synchronizing the messages for all users connected to the same topic. The message list only renders the submitted messages, while the message input provides controls for submitting new messages. Here’s an example of setting them up:

User userEntity = userService.getCurrentUser();
UserInfo userInfo = new UserInfo(userEntity.getId(),
        userEntity.getName(), userEntity.getImageUrl());
String topicId = "general";

CollaborationMessageList messageList = new CollaborationMessageList(
        userInfo, topicId);
CollaborationMessageInput messageInput = new CollaborationMessageInput(
        messageList);
Note
Example classes
User and UserService are application-specific example classes, and not part of the Collaboration Engine API.

The CollaborationMessageList constructor takes two arguments: the information about the end user associated with this session, and a topic id. The user info is used to render the user name and avatar in the messages submitted by this user. Refer to the CollaborationAvatarGroup documentation for more details on how the avatars are rendered, or how to load images from a backend with the setImageProvider method.

The topic id works in the same way as for other Collaboration Engine features. The data in Collaboration Engine is shared among those users who are connected to the same topic. With these components, you can consider the topic as a chat room. In the example above, the topic id is hard-coded as "general", which means that every user in this view sees each others' messages. You can dynamically change the topic with the setTopic method, for example when the user makes a selection in the UI.

CollaborationMessageInput takes the message list component as its constructor argument. It hooks up with the list, to submit messages to its current topic. When the list component has null topic id, no messages are displayed, and the connected message input will be automatically disabled.

After constructing the components, the last required step is to place them somewhere in your view. Here’s an example of placing the message list and input on top of each other in a layout with a defined size:

VerticalLayout chatLayout = new VerticalLayout(messageList,
        messageInput);
chatLayout.setHeight("500px");
chatLayout.setWidth("400px");
chatLayout.expand(messageList);
messageInput.setWidthFull();
add(chatLayout);

Persisting Messages

The topic data in Collaboration Engine is stored only in the application memory. This applies also to messages in a CollaborationMessageList, which means that all the submitted messages will be lost when restarting the server.

To solve this issue, you can configure CollaborationMessageList to store the messages in your own backend. This is done by implementing the CollaborationMessagePersister interface. It specifies two operations:

  • Fetch all messages in a given topic, which have been submitted during or after a given timestamp.

  • Store a new message.

In the following example implementation, you can see how the persister is mapping between the CollaborationMessage objects and the application’s own Message entities, and integrating with the service layer:

@SpringComponent
public class MyMessagePersister implements CollaborationMessagePersister {

    private final MessageService messageService;
    private final UserService userService;

    public MyMessagePersister(MessageService messageService,
            UserService userService) {
        this.messageService = messageService;
        this.userService = userService;
    }

    @Override
    public Stream<CollaborationMessage> fetchMessages(FetchQuery query) {
        return messageService
                .findAllByTopicSince(query.getTopicId(), query.getSince())
                .map(messageEntity -> {
                    User author = messageEntity.getAuthor();
                    UserInfo userInfo = new UserInfo(author.getId(),
                            author.getName(), author.getImageUrl());

                    return new CollaborationMessage(userInfo,
                            messageEntity.getText(), messageEntity.getTime());
                });
    }

    @Override
    public void persistMessage(PersistRequest request) {
        CollaborationMessage message = request.getMessage();

        Message messageEntity = new Message();
        messageEntity.setTopic(request.getTopicId());
        messageEntity.setText(message.getText());
        messageEntity
                .setAuthor(userService.findById(message.getUser().getId()));

        // Set the time from the message only as a fallback option if your
        // database can't automatically add an insertion timestamp:
        // messageEntity.setTime(message.getTime());

        messageService.save(messageEntity);
    }
}
Note
Example classes
MessageService, UserService, Message and User are application-specific example classes, and not part of the Collaboration Engine API.
Tip
Initialize a persister from lambdas
You can also create a message persister instance with lambdas by using the static CollaborationMessagePersister.fromCallbacks() method.

It’s best to configure your database to insert the creation/insertion timestamp of the entity. This ensures that the messages are ordered by their timestamps in the database. If you use the timestamp provided by CollaborationMessage::getTime (based on the JVM clock), you take a risk of skipping some inserted messages in rare edge cases.

For example, if you use Hibernate to create the database table, you can include an automatic insertion timestamp in your entity class with the following annotation:

@Column(columnDefinition="TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
private Instant time;

You can set the message persister in the message list’s constructor:

CollaborationMessageList messageList = new CollaborationMessageList(
        userInfo, "general", myMessagePersister);

When a persister is provided, CollaborationMessageList takes care of keeping the component state in sync with your backend. When the first message list connects to a topic after starting the application, it fetches all the messages from the persister (making a FetchQuery with Unix epoch as the getSince() timestamp). The fetched messages are stored in Collaboration Engine’s memory and shared among other message lists connected to the same topic, avoiding redundant backend queries.

When a new message is submitted, the data flows through your backend. First, the message list saves the message to your backend with the persister. Then, the message list makes a query to the persister to fetch all the messages with a timestamp that is equal or more recent than the last message in memory. The new messages are again stored in the Collaboration Engine’s memory, updating all the message list instances which are connected to the same topic.

When implementing the fetchMessages() method, it is important to note that the returned stream should include the messages sent exactly at the time that you can get with getSince() from the FetchQuery. This ensures that all messages are fetched even when multiple messages have the same timestamp. The component takes care of filtering out the duplicates, which are expected when returning the latest message again.

Using Custom Input Components

Instead of using the provided CollaborationMessageInput component, you can also configure your own component to submit messages. The component can be registered with the CollaborationMessageList::setSubmitter method. Here’s an example of submitting messages with TextField and Button components:

TextField field = new TextField("Message");
Button button = new Button("Submit");
button.setEnabled(false);

messageList.setSubmitter(activationContext -> {
    button.setEnabled(true);
    Registration registration = button.addClickListener(
            event -> activationContext.appendMessage(field.getValue()));
    return () -> {
        registration.remove();
        button.setEnabled(false);
    };
});

The setSubmitter method takes a callback that is run when a new topic connection is established (topic id is set to a non-null value). This callback should be used to set up any listeners for submitting new messages to CollaborationMessageList through the provided ActivationContext. The callback should return another callback for clean-up when the topic connection is deactivated (topic id is changed). Note how the submit button is enabled only when there’s an active topic connection.

Customizing Messages

CollaborationMessageList enables changing the properties of MessageListItem items after they are generated. You can do that by providing a MessageConfigurator.

The following example shows how to use a message configurator to add a custom style to the current user’s messages:

CollaborationMessageList collaborationMessageList =
    new CollaborationMessageList(userInfo, topicId);
collaborationMessageList.setMessageConfigurator((message, user) -> {
    if (user.equals(localUser)) {
        message.addThemeNames("current-user");
    }
});

Please refer to Styling in Message List documentation for details on how to style messages based on the theme.

The message configurator can also be used to change the text content of messages, for example, to censor certain words in messages, as is done in the following example.

CollaborationMessageList collaborationMessageList =
    new CollaborationMessageList(userInfo, topicId);
 collaborationMessageList.setMessageConfigurator((message, user) -> {
    // Example: Replace all occurrences of "badword" with "***"
    String censored = message.getText().replaceAll("badword", "***");
    message.setText(censored);
  });