This tutorial describes how to use the RADChatRoom library to quickly and easily add a nice-looking, fully functional chat room to your Codename One application.

The finished product will look like the following:

ChatRoomView screenshot

You can download the full source of this tutorial’s project here.

You can also try it out yourself here.

The demo link uses the CodeName One Javascript port, which allows you to deploy Codename One java applications as native Javascript apps inside the browser without any plugins or extensions. When you build a Codename One project for iOS, Android, Desktop, or any of the other build targets, they will be compiled as native apps, with native performance, and will not use Javascript.

You may also try out an Android build of this project. For other platforms, you can download the sources and build it yourself.

About the ChatRoomView Component

The ChatRoomView component is the first in a new breed of Codename One UI components which go beyond the the fundamental building blocks of user interfaces to provide a rich, fully-functional user experience out of the box. It is built on top of the new CodeRAD library which enables a new level of code-reuse based on the tried-and-true MVC (Model-View-Controller) design pattern. As you’ll see, adding a chat feature to your app is not difficult. All of the minutiae of UI details come working and ready to go. You just need to bind it to your view model and controller.

Prerequisites

In order to create the project in this tutorial, you’ll need the following:

  1. IntelliJ, NetBeans, or Eclipse with the Codename One plugin installed.

For information about installing Codename One, see this page.

Project Setup

For this tutorial, we’ll create a basic Codename One project, and we’ll add the "CodeRAD" and "RADChatRoom" cn1libs as dependencies. I’ll use NetBeans in this tutorial, but you can use your preferred IDE (IntelliJ or Eclipse).

For the sake of this tutorial, I’m going to name the project CN1Chat, and my package will be "com.codename1.cn1chat". I’ll be using the "Hello World" bare-bones project template.

Create new project in NetBeans
Figure 1. New project dialog in NetBeans.
Create new project in NetBeans wizard page 2
Figure 2. Page 2 of new project wizard in NetBeans

Step 1: Create a New Codename One project

If you haven’t created a Codename One project before, you can refer to this tutorial, which walks you through the creation of your first Codename One project.

Step 2: Activate CSS

The CodeRAD and RADChatRoom libs require CSS to be activated in your project. See this tutorial for the steps on enabling CSS.

Step 3: Add Dependencies

In Codename One settings, add the following cn1libs:

  1. CodeRAD

  2. RADChatRoom

If you haven’t activated any cn1libs before in your Codename One projects, see this tutorial which explains the process.

Step 4: Create Application Controller

We’ll be using MVC for this app. The CodeRAD cn1lib includes a set of controller classes that help with the structure of such apps. We’ll begin by modifying our app’s main application class (CN1Chat.java) so that it extends ApplicationController, and we’ll replace the class contents with the following:

package com.codename1.cn1chat;


import com.codename1.rad.controllers.ApplicationController;
import com.codename1.rad.controllers.ControllerEvent;

public class CN1Chat extends ApplicationController {
     @Override
    public void actionPerformed(ControllerEvent evt) {
        if (evt instanceof StartEvent) {
            evt.consume();

            // The app has started
        }
    }

}

Step 5: Create A Form Controller

Next we’ll create a controller for the form that will contain the chat. This will create a basic view model, and use it to create a ChatRoomView object, which we will add to the form. The code for the first iteration of this controller is as follows:

package com.codename1.cn1chat;

// imports


public class ChatFormController extends FormController {

    public ChatFormController(Controller parent) {
        super(parent);
        Form f = new Form("My First Chat Room", new BorderLayout());
        ChatRoomView view = new ChatRoomView(createViewModel(), f);
        f.add(CENTER, view);
        setView(f);
    }

    /**
     * Creates a view model for the chat room.
     * @return
     */
    private Entity createViewModel() {
        ChatRoomView.ViewModel room = new ChatRoomView.ViewModel();

        ChatBubbleView.ViewModel message = new ChatBubbleView.ViewModel();
        message.messageText("Hello World");
        room.addMessages(message);
        return room;
    }

}

A couple of things to note with this code:

  1. The createViewModel() method creates a minimal view model for our chat room. It uses the ChatRoomView.ViewModel class for the view model. This class is only a reference implementation of a view model, and the ChatRoomView class doesn’t require you to use this class at all if you don’t want to. Later on, in this tutorial, I’ll show you how to use your own custom class for the view model.

  2. Similarly, the ChatBubbleView.ViewModel is a reference implementation of a view model to encapsulate a message in the chat room, but you can use your own custom classes for these models also.

Step 6: Show the Form

Finally, we need to show the Chat form when the app launches. Modify your Application controller class to create a new instance of ChatFormController() and show its form as follows:

public class CN1Chat extends ApplicationController {
     @Override
    public void actionPerformed(ControllerEvent evt) {
        if (evt instanceof StartEvent) {
            evt.consume();
            new ChatFormController(this).getView().show();
        }
    }
}

Step 7: Run the App

Now that we have the minimal foundation in place, let’s run the app in the simulator. If everything goes well, you should see something like the following.

Running basic app in simulator
Figure 3. First run of Chat app in simulator.

This looks good, but it’s not a fully functional chat app yet. You’ll notice that it is missing many of the features that are present in the screenshot I shared of the finished project. The finished project included a title component with avatars of the chat participants:

Chat participants title bar
Figure 4. Chat participants listed in title bar in finished version of the app.

This is absent because we didn’t add any participants to the chat model.

In addition, there is no "Send" button, in this version, so there is no apparent way to send messages in this chat.

We’ll correct both of these omissions, and add some other features over the course of this tutorial.

Adding a "Send" Button

A "Send" button is a pretty important part of any chat application. We’ll add a send button to our app by defining an action in our controller, and passing it to the ChatRoomView as follows. First we’ll define the action in our ChatFormController class:

// We're going to use a lot of static functions from the UI class for creating
// UI elements like actions declaratively, so we'll do a static import here.
import static com.codename1.rad.ui.UI.*;

// ...
public class ChatFormController extends FormController {

    // Define the "SEND" action for the chat room
    public static final ActionNode send = action(icon(FontImage.MATERIAL_SEND));

Then we’ll create a ViewNode to pass to the ChatRoomView constructor. This is can contain properties that the chat room uses to render itself, including which actions it should "embed" and where.

ViewNode viewNode = new ViewNode(
    actions(ChatRoomView.SEND_ACTION, send)
);

ChatRoomView view = new ChatRoomView(createViewModel(), viewNode, f);

If this is the first time you’ve seen a ViewNode definition, this may look a little bit foreign. All this does is register our "send" action with the "ChatRoomView.SEND_ACTION" category so that the chat room view knows to use it as the "send" action in the chat room. The full source of the ChatRoomController class after these changes is as follows:

package com.codename1.cn1chat;

import com.codename1.rad.controllers.Controller;
import com.codename1.rad.controllers.FormController;
import com.codename1.rad.models.Entity;
import com.codename1.rad.nodes.ActionNode;
import com.codename1.rad.nodes.ViewNode;
import com.codename1.rad.ui.chatroom.ChatBubbleView;
import com.codename1.rad.ui.chatroom.ChatRoomView;
import static com.codename1.ui.CN.CENTER;
import com.codename1.ui.FontImage;
import com.codename1.ui.Form;
import com.codename1.ui.layouts.BorderLayout;

// We're going to use a lot of static functions from the UI class for creating
// UI elements like actions declaratively, so we'll do a static import here.
import static com.codename1.rad.ui.UI.*;


public class ChatFormController extends FormController {

    // Define the "SEND" action for the chat room
    public static final ActionNode send = action(icon(FontImage.MATERIAL_SEND));

    public ChatFormController(Controller parent) {
        super(parent);
        Form f = new Form("My First Chat Room", new BorderLayout());

        // Create a "view node" as a UI descriptor for the chat room.
        // This allows us to customize and extend the chat room.
        ViewNode viewNode = new ViewNode(
            actions(ChatRoomView.SEND_ACTION, send)
        );

        // Add the viewNode as the 2nd parameter
        ChatRoomView view = new ChatRoomView(createViewModel(), viewNode, f);
        f.add(CENTER, view);
        setView(f);


    }

    /**
     * Creates a view model for the chat room.
     * @return
     */
    private Entity createViewModel() {
        ChatRoomView.ViewModel room = new ChatRoomView.ViewModel();

        ChatBubbleView.ViewModel message = new ChatBubbleView.ViewModel();
        message.messageText("Hello World");
        room.addMessages(message);
        return room;
    }

}

Now, let’s run the app in the simulator again.

ChatRoom after adding the send action

Notice that a "send" button has been added to the bototm-right of the form, next to the text entry box.

Send button and chat entry box

This is progress, but you may be disappointed, upon playing with the send button, to discover that it doesn’t do anything. In fact, when you click the "send" button, the view is sending an event to our controller. We just haven’t implemented a handler for it.

Let’s do that now.

Handling the "Send" Action Event

To handle the "send" event, we simply add the following inside the constructor of our form controller:

addActionListener(send, evt->{
    evt.consume();
    ChatRoomView.ViewModel room = (ChatRoomView.ViewModel)evt.getEntity();
    String textFieldContents = room.getInputBuffer();
    if (textFieldContents != null && !textFieldContents.isEmpty()) {
        ChatBubbleView.ViewModel message = new ChatBubbleView.ViewModel();
        message.messageText(textFieldContents);
        message.date(new Date());
        message.isOwn(true); // Indicates that this is sent by "this" user
                            // so bubble is on right side of room view.

        // Now add the message
        room.addMessages(message);

        // Clear the text field contents
        room.inputBuffer("");
    }

});

This listener will be called whenever the "send" action is fired. On mobile devices this will only occur when the user presses the "Send" button. But on desktop, it will also be fired when the user hits "Enter" while the text field is focused.

The event passed to this handler is an instance of ActionEventNode which includes all of the contextual information necessary to identify the source of the action, including the entity (the room), the UI component (the ChatRoomView) object, and the action (send), that triggered the event.

The logic in this handler should be pretty straight forward. It checks if the "input buffer" contains any text. Since the input buffer is bound to the text field, this is just checks if the text field contains any text. It then creates a new message with the input buffer contents, and clears the contents of the input buffer.

All of these property changes will fire PropertyChangeEvents to the view so that the view state will be updated automatically and instantly.

If you run the app in the simulator again, you should be able to enter text into the text field, and press send, to see a new chat bubble animated into place.

Posting a second message

Bonus Points: Disable Send Button When Input Empty

In out action handler, we include logic to prevent sending empty messages. But it would be nice if we game the user a cue in the user interface that "send" doesn’t work when the field is empty. We can do this using the enabledCondition attribute in our action definition:

public static final ActionNode send = action(
    enabledCondition(entity-> {
        return !entity.isEmpty(ChatRoom.inputBuffer);
    }),
    icon(FontImage.MATERIAL_SEND)
);

This says that the send action should only be enabled when the "entity" is non-empty. The "entity" in this case is the view model for the chat room.

Start the app again in the simulator and notice that the "send" button toggles between enabled and disabled depending on whether there is text in the input field.

Send is disabled
Figure 5. Send button is disabled because the input field is empty
Send is enabled
Figure 6. Send button is enabled because the input field in non-empty

Adding Text Messages from Other Users

Our current example only includes messages that the current user posted themself. I.e. We only have chat bubbles on the right-hand side of the view. Let’s add some more sample data to our view model to give us a feel for how a real chat will look. In the ChatFormController class, we’ll change the createViewModel() method as follows:

Creating more interesting sample data for the ChatRoom’s view model. We add messages from both the current user and other users.
// Create a view model for the chat room
private Entity createViewModel() {
    ChatRoomView.ViewModel room = new ChatRoomView.ViewModel();

    // Make up some dummy times for the chat messages.
    long SECOND = 1000l;
    long MINUTE = SECOND * 60;
    long HOUR = MINUTE * 60;
    long DAY = HOUR * 24;

    // Make first message 2 days ago.
    long t = System.currentTimeMillis() - 2 * DAY;

    // Some thumbnails for the avatars of the participants
    String georgeThumb = "https://weblite.ca/cn1tests/radchat/george.jpg";
    String kramerThumb = "https://weblite.ca/cn1tests/radchat/kramer.jpg";

    room.addMessages(createDemoMessage("Why couldn't you have made me an architect? You know I always wanted to pretend that I was an architect. "
            + "Well I'm supposed to see her tomorrow, I'm gonna tell her what's goin on. Maybe she likes me for me.",
            new Date(t), "George", georgeThumb));
    t += HOUR;
    room.addMessages(createDemoMessage("Hey", new Date(t), "Kramer", kramerThumb));
    t += MINUTE;
    room.addMessages(createDemoMessage("Hey", new Date(t), null,  null));

    return room;
}

// Create a single demo message
private Entity createDemoMessage(String text,
        Date datePosted,
        String participant,
        String iconUrl) {
    ChatBubbleView.ViewModel msg = new ChatBubbleView.ViewModel();
    msg.messageText(text)
            .date(datePosted)
            .iconUrl(iconUrl)
            .isOwn(participant == null);
    if (participant != null) {
        msg.postedBy(participant);
    }
    return msg;

}

To make things easier to read, I’ve broken out the code for creating a message into a separate method so we can call create new messages more easily. I’ve created a couple of pretend users, "George" and "Kramer", and I’ve provided some thumbnail URLs for them, which can be used as avatars in the chat room.

And the result:

Chat room with messages from both current user
Figure 7. Chat room now includes messages from two other users, George and Kramer.

Notice that it shows the time of the first chat message, but not the others. This is intentional. The chat room will only show the time of messages if there is a long delay between it and the previous message. You can see the time of each message by swiping to the left:

Image 220220 064242.291
Figure 8. Swipe to the left to reveal the date and time of each message.

Adding the "Participants" Title Component

Recall the screenshot of the finished app, in which the form title included a list of participants in the chat room with their avatars.

Image 220220 064702.972
Figure 9. Participants title component with avatars.

Let’s add this now by adding some participants to the view model. The ChatRoomView.ViewModel includes methods to directly add participants to the model via addParticpant(Entity…​ participants). Each participant entity should implement the Thing.name or Thing.thumbnailUrl tags, or both. If Only Thing.name is provided, then it will generate an avatar with the first letter of their name. If Thing.thumbnailUrl is provided, then it will use the image at this url as the avatar.

Let’s begin by creating a custom entity/view model named "ChatAccount" which will be used as participants in the chat. Create a new Java class named "ChatAccount" with the following contents:

The ChatAccount entity will be used to encapsulate profiles for participants in the chat room.
package com.codename1.cn1chat;
import com.codename1.rad.models.Entity;
import com.codename1.rad.models.EntityType;
import static com.codename1.rad.models.EntityType.tags;
import com.codename1.rad.models.StringProperty;
import com.codename1.rad.schemas.Thing;

/**
 * View model for an account profile.
 * @author shannah
 */
public class ChatAccount extends Entity {

    // The name property
    public static StringProperty name; (1)

    private static final EntityType TYPE = new EntityType() {{ (2)
        name = string(tags(Thing.name)); (3)
    }};
    {
        setEntityType(TYPE); (4)
    }

    public ChatAccount(String nm) {
        set(name, nm);
    }

}
1 The "name" property of our entity.
2 Define an entity type for the ChatAccount entity. The entity type defines which properties are supported by the ChatAccount entity.
3 Generating the "name" property as a string property. Notice that we assign the Thing.name tag to this property, which will allow views to bind to it.
4 Set the entity type inside the "instance" initializer so that all ChatAccount objects have the same entity type. This could have been placed inside the constructor, but placing it simply inside the initializer (i.e. inside {..}) makes for a little less typing, and also helps to signify the declarative nature of this call.

I’ve added some notes about the key lines of the code listing above which should help to get you up to speed if this is your first custom entity. This entity defines a single property, "name". If we were to define this entity as a POJO (Plain-Old Java object), the class might look something like:

What the ChatAccount entity would look like if implemented as a POJO (Plain old java object).
public class ChatAccount {
    private String name;
    public ChatAccount(String name) {
        this.name = name;
    }
}

So why not use a POJO for our entity?

The Entity class, together with EntityType provide lots of useful features such as bindable properties, property change events, data conversion, observability, and reflection. All of these features are necessary to enable the creation of loosely coupled components with clean separation between models, views, and controllers. As you’ll see, this loose coupling greatly enhances our ability to produce complex, reusable components, which results in better apps with less code.

Sidebar: Getting and Setting Properties on Entities

Before proceeding, its worth discussing the basics of how to use entities. The Entity class allows us to get and set properties without needing to define getter and setter methods. It also includes a rich set of convenience methods for handling data-conversion. Finally, one of the most powerful features of entities is its loose coupling. It is possible to get and set property values without any knowledge of which properties exist in the entity, via tags.

First things first: Getting and setting property values.

  1. Getting and setting property values using a direct property reference.

ChatAccount account = new ChatAccount("George");
String name = account.get(ChatAccount.name);  // "George"
account.set(ChatAccount.name, "Kramer");
name = account.get(ChatAccount.name);  // "Kramer"

This code is tightly coupled to the ChatAccount entity because it directly references the ChatAccount.name property. In some cases, this tight coupling is fine. In other cases, such as when you want to develop a reusable component that requires a "name" property, you may prefer to use "loose" coupling, as follows:

Getting and Setting properties using Tags instead of Properties allows for loose coupling.
Entity account = ...;  // Could be any entity, but happens to be a ChatAccount
account.setText(Thing.name, "George");
String name = account.getText(Thing.name); // "George"
The CodeRAD library includes a hierarchy of schemas which define tags that may be used to tag entity properties. These schemas were adapted from https://schema.org, which defines entities and properties for a large number of common object types. All schemas extend the base schema, Thing. Some common tags include name, identifier, and thumbnailUrl. When creating reusable components, you can use these schema "tags" to access property values of view models in loosely coupled way. The javadocs for View components should list the tags that it expects on its view models, so you can tag the properties on your entities accordingly. For a full list of schemas, check out https://schema.org/docs/full.html. Only a subset has been ported into the CodeRAD library. More will be added over time, and you may contribute your own with a pull request.

Finally…​ Adding the Participants

After a lengthy discussion of Entities, Entity types, Tags, and Properties, we can now go ahead and add some participants to the chat room. Add the following inside our createViewModel() method of the ChatFormController class:

room.addParticipants(new ChatAccount("George"), new ChatAccount("Kramer"));

This adds two profiles to the chat room as participants. Now, if we launch the app we’ll see the form title replaced with the following avatars.

Image 220220 074948.423
Figure 10. Title component with avatars generated from our participants

Now, let’s go a step further and add a "thumbnail url" property to our ChatAccount entity.

Adding a thumbnailUrl property to the ChatAccount entity
package com.codename1.cn1chat;
import com.codename1.rad.models.Entity;
import com.codename1.rad.models.EntityType;
import static com.codename1.rad.models.EntityType.tags;
import com.codename1.rad.models.StringProperty;
import com.codename1.rad.schemas.Thing;

/**
 * View model for an account profile.
 * @author shannah
 */
public class ChatAccount extends Entity {

    // The name property
    public static StringProperty name, thumbnailUrl;

    private static final EntityType TYPE = new EntityType() {{
        name = string(tags(Thing.name));
        thumbnailUrl = string(tags(Thing.thumbnailUrl));
    }};
    {
        setEntityType(TYPE);
    }

    public ChatAccount(String nm, String thumb) {
        set(name, nm);
        set(thumbnailUrl, thumb);
    }

}

And modify our ChatFormController to set the thumbnail URL on our entity.

room.addParticipants(
    new ChatAccount("George", georgeThumb),
    new ChatAccount("Kramer", kramerThumb)
);

And reload…​

Image 220220 075435.679
Figure 11. Title component after setting thumbnail URLs for our participants.

Adding More Actions

So far we’ve implemented the basic requirements of a chat room. It can display messages, show particpants, and it allows users to send new messages. Now let’s go a step further and add some more actions. CodeRAD views like ChatRoomView allow for customization in a variety of ways, but the two primary methods are:

  1. Actions

  2. View properties

We’ve already used one action to implement the "send" function. As a reminder, we defined the action in our controller, then we passed it as an attribute to the ViewNode when creating the view:

public static final ActionNode send = action( (1)
    enabledCondition(entity-> {
        return !entity.isEmpty(ChatRoom.inputBuffer);
    }),
    icon(FontImage.MATERIAL_SEND)
);
....
ViewNode viewNode = new ViewNode(
    actions(ChatRoomView.SEND_ACTION, send) (2)
);
....
ChatRoomView view = new ChatRoomView(createViewModel(), viewNode, f); (3)
1 Defining the "send" action.
2 Adding the "send" action to the view node, under the ChatRoomView.SEND_ACTION category. The category is a hint to the view about where and how the action should be incorporated into the View.
3 Creating new ChatRoomView, passing our ViewNode as a parameter
A ViewNode is a user interface descriptor that can be used to customize the behaviour of a View. It provides a declarative way to define complex user interfaces in a simple way. For the purpose of this tutorial, we will only use the node as a means to pass actions to the ChatRoomView.

The "Send" action was added to the ChatRoomView.SEND_ACTION category, but the ChatRoomView also supports some other categories:

  1. ChatBubbleView.CHAT_BUBBLE_CLICKED - An action that will be "fired" when the user clicks a chat bubble.

  2. ChatBubbleView.CHAT_BUBBLE_LONG_PRESS - An action that will be "fired" when the user long presses a chat bubble.

  3. ChatBubbleView.CHAT_BUBBLE_CLICKED_MENU - Actions that will be displayed in a popup-menu when the user clicks on a chat bubble. This category many include more than one action, and all of supplied actions will be included as menu items in the menu.

  4. ChatBubbleView.CHAT_BUBBLE_CLICKED_MENU - Actions that will be displayed in a popup-menu when the user long presses on a chat bubble.

  5. ChatBubbleView.CHAT_BUBBLE_LONG_PRESS_MENU - Actions that will be displayed in a popup-menu when the chat bubble is long pressed.

  6. ChatBubbleView.CHAT_BUBBLE_BADGES - Actions in this category will be rendered as "badge" icons next to the chat bubble. This is useful, for example, for displaying a "Like/Heart" badge on a chat bubble.

  7. ProfileAvatarView.PROFILE_AVATAR_CLICKED - An action that will be "fired" when the user clicks on one of the profile avatars next to a chat bubble, or in the title component.

  8. ProfileAvatarView.PROFILE_AVATAR_LONG_PRESS - An action that will be "fired" when the user long presses on one of the profile avatars.

  9. ProfileAvatarView.PROFILE_AVATAR_CLICKED_MENU - Actions in this category will be rendered in a popup menu when the user clicks on an avatar.

  10. ProfileAvatarView.PROFILE_AVATAR_LONG_PRESS_MENU - Actions in this category will be rendered in a popup menu when the user long presses on an avatar.

  11. ChatRoomView.TEXT_ACTIONS - Actions in this category will be rendered as buttons next to the text input field. This is an appropriate place to add "Photo" or "Video" capture capabilities.

Adding Phone and Video Conferencing

To get our feet wet with actions, let’s add some options to initiate a phone-call or video conference with one of the participants. When the user taps on a profile’s avatar, we’ll present the user with a menu to start a call or video conference.

In the ChatFormController, we’ll add a couple of new actions.

public static final ActionNode phone = action(
    icon(FontImage.MATERIAL_PHONE)
);

public static final ActionNode videoConference = action(
    icon(FontImage.MATERIAL_VIDEOCAM)
);

...

ViewNode viewNode = new ViewNode(
    actions(ChatRoomView.SEND_ACTION, send),
    actions(ProfileAvatarView.PROFILE_AVATAR_CLICKED_MENU, phone, videoConference) (1)
);
1 We add the phone and videoConference actions to the ViewNode in the ProfileAvatarView.PROFILE_AVATAR_CLICKED_MENU category so that they’ll be rendered in a popup-menu when the user presses on an avatar.

Now run the app and click on the title component:

Image 220220 084136.625
Figure 12. Menu when clicking on the title component

Or tap on an avatar next to one of the chat bubbles:

Image 220220 084244.673
Figure 13. Pop-up menu when tapping on an avatar

Currently, clicking on the "phone" or "camera" icon doesn’t do anything because we haven’t defined a handler. Let’s do that now:

addActionListener(phone, evt->{
    evt.consume();
    if (!CN.canDial()) {
        Dialog.show("Not supported", "Phone calls not supported on this device", "OK", null);
        return;
    }
    if (evt.getEntity().isEmpty(Person.telephone)) {
        Dialog.show("No Phone", "This user has no phone number", "OK", null);
        return;
    }

    String phoneNumber = evt.getEntity().getText(Person.telephone);
    CN.dial(phoneNumber);

});

In this handler we first check to see if the platform supports phone calls, and fail with a dialog if it doesn’t. Then we check if the entity in question has a phone number. This code makes use of loose-coupling as we using the Person.telephone tag to check for a phone number rather than a particular property. This will allow this code to work with any entity that has such a property. We also make use of the handy Entity.isEmpty(Tag) method, which will return true if this entity doesn’t have a matching property, or if the entity has the property, but has an "empty" value for it.

If you try the app out and attempt to phone any of the users, you’ll receive this dialog:

Image 220220 085620.325
Figure 14. Currently our ChatAccount entity doesn’t include any properties with the Person.telephone tag, so attempting to phone a user will yield this error dialog.

Let’s remedy this situation by adding a property to the ChatAccount entity type.

package com.codename1.cn1chat;
import com.codename1.rad.models.Entity;
import com.codename1.rad.models.EntityType;
import static com.codename1.rad.models.EntityType.tags;
import com.codename1.rad.models.StringProperty;
import com.codename1.rad.schemas.Person;
import com.codename1.rad.schemas.Thing;

/**
 * View model for an account profile.
 * @author shannah
 */
public class ChatAccount extends Entity {

    // The name property
    public static StringProperty name, thumbnailUrl, phone;

    private static final EntityType TYPE = new EntityType() {{
        name = string(tags(Thing.name));
        thumbnailUrl = string(tags(Thing.thumbnailUrl));
        phone = string(tags(Person.telephone)); (1)
    }};
    {
        setEntityType(TYPE);
    }

    public ChatAccount(String nm, String thumb, String phoneNum) {
        set(name, nm);
        set(thumbnailUrl, thumb);
        set(phone, phoneNum);
    }

}
1 Creating the phone property as a string property with the Person.telephone tag.

And we’ll update the code in our ChatFormController that creates our participants to add a phone number.

Adding a phone number to the George account in the view controller. We leave Kramer’s phone number null.
room.addParticipants(
    new ChatAccount("George", georgeThumb, "712-555-1234"),
    new ChatAccount("Kramer", kramerThumb, null)
);

Let’s start up the app again. There are a few things to notice here:

  1. If you press on either George or Kramer’s avatar next to one of their chat bubbles, and try to phone them, they’ll both give you the "This user has no phone number" message. Thats because the avatar that appears next to the chat bubble is actually the ChatMessage.ViewModel entity, and not our ChatAccount entity. The ChatMessage.ViewModel entity doesn’t include a telephone field. The ChatAccount entities are only used to render the title component of the form.

  2. If you try to phone Kramer via the title component, you’ll get the same "This user has no phone number" message. This is correct, because we didn’t give Kramer a phone number.

  3. If you try to phone George via the title component, it will dial the number that we registered with the George account. (If you’re running in the simulator, it won’t dial…​ it will just display a message in the console indicating that it is dialing the number).

This is progress, but why don’t we save the user the agony of having to click "phone" to find out if the app can actually make a phone call to that user. We have two options for this, we can either "disable" the phone action conditionally, like we did for the "send" action when the input field is empty. This will still show the phone button in the menu, but it will be greyed out and disabled. Alternatively we could actually remove the phone action in such cases so that it isn’t displayed at all for entities that don’t support it.

Let’s try it both ways:

Disabling the phone action for entities that don’t have a phone number
public static final ActionNode phone = action(
    icon(FontImage.MATERIAL_PHONE),
    enabledCondition(entity->{
        return CN.canDial() && !entity.isEmpty(Person.telephone);
    })
);

Result:

Image 220220 091214.259
Figure 15. Kramer’s phone button is disabled because we didn’t provide a phone number for him.

If we want to remove the action from menus where it isn’t supported, then we simply change enabledCondition() to condition().

Removing the phone action for entities that don’t have a phone number.
public static final ActionNode phone = action(
    icon(FontImage.MATERIAL_PHONE),
    condition(entity->{ (1)
        return CN.canDial() && !entity.isEmpty(Person.telephone);
    })
);
1 We use the condition(…​) attribute instead of enabledCondition(…​) to disable/hide the action

And the result:

Image 220220 091549.549
Figure 16. Kramer has no "phone" option now because he doesn’t have a phone number.

Adding a "Like" Badge

Most messaging apps provide a way to "like" a chat message. Let’s add this functionality to our app by using the ChatBubbleView.CHAT_BUBBLE_BADGES category to display the "liked" badge. We’ll use the ChatBubbleView.CHAT_BUBBLE_LONG_PRESS_MENU category to display the toggle button for the user to "like" and "unlike" the message.

public static final ActionNode likedBadge = UI.action(
    UI.uiid("ChatBubbleLikedBadge"), (1)
    icon(FontImage.MATERIAL_FAVORITE),
    condition(entity->{ (2)
        return !entity.isFalsey(ChatMessage.isFavorite); (3)
    })

);

public static final ActionNode likeAction = UI.action(
    icon(FontImage.MATERIAL_FAVORITE_OUTLINE),
    uiid("LikeButton"), (4)
    selected(icon(FontImage.MATERIAL_FAVORITE)), (5)
    selectedCondition(entity->{
        return !entity.isFalsey(ChatMessage.isFavorite); (6)
    })

);

...

ViewNode viewNode = new ViewNode(
    actions(ChatRoomView.SEND_ACTION, send),
    actions(ProfileAvatarView.PROFILE_AVATAR_CLICKED_MENU, phone, videoConference),
    actions(ChatBubbleView.CHAT_BUBBLE_LONG_PRESS_MENU, likeAction), (7)
    actions(ChatBubbleView.CHAT_BUBBLE_BADGES, likedBadge) (8)
);
1 We set the UIID of the badge to "ChatBubbleLikedBadge" which is a style defined in the RADChatRoom cn1lib’s stylesheet. It will make the badge small and red.
2 Use the condition() attribute to ensure that the "liked" badge only shows up if the message has been liked.
3 We are using the convenience method Entity.isFalsey(Tag) to determine if the chat message has been liked. This returns "true" if the value of this field is anything "falsey", like null, or "", or 0, or false. This allows for flexibility about how the view model wants to store whether the message is a favourite or not.
4 We define a UIID for the "Like" action so that we can make the button look how we like.
5 We use the selected(…​) attribute on the likeAction to define a different icon for the action when the action is "selected".
6 We use selectedCondition() on the like action to cause the action to be selected conditionally on whether the message is "liked". This works similar to the condition() and enabledCondition() attributes, except this will affect the selected state of the action’s button. The presence of this attribute causes the button to be rendered as a toggle button instead of a regular button.
7 We add the like action to the CHAT_BUBBLE_LONG_PRESS_MENU category.
8 We add the liked action to the CHAT_BUBBLE_BADGES category.

And, of course, we need to handle the "like" action to toggle the property on and off in the view model.

addActionListener(likeAction, evt->{
    evt.consume(); (1)
    Entity chatMessage = evt.getEntity();
    chatMessage.setBoolean( (2)
            ChatMessage.isFavorite, (3)
            chatMessage.isFalsey(ChatMessage.isFavorite) (4)
    );

});
1 We consume the event so that the view knows that we handled it. This prevents any default behaviour from conflicting.
2 We use the Entity.setBoolean(…​) method to signify that we are setting the value as a boolean. This will ensure that the value is converted to the correct type for the underlying property.
3 We use the ChatMessage.isFavorite tag to target the field for loose coupling. The ChatBubbleView.ViewModel class that we’re using does implement a property with this tag, but we are writing code in such a way that we don’t need to care about which property it is.
4 Again using isFalsey() to get the current value of the flag, and we toggle it to be opposite.

Finally, our "Like" button will be a heart icon. When selected it will be a filled heart icon. When unselected, it will be contour of a heart. We specified a UIID of "LikeButton" for this action in its definition. We just need to add this style to our stylesheet. Open the project’s stylesheet (at css/theme.css) and add the following:

The style for the Like button, which we add the the project’s stylesheet.
LikeButton {
    background-color:transparent;
    cn1-border-type: none;
    color: red;
}

And the test drive…​ Open up the app again, long press on a chat message, and click the "Like" action. Then it should display a red heart badge next to the chat bubble.

Image 230220 090032.786
Figure 17. Menu appears when you long-press on a chat bubble. Clicking on the button will fire the "Like" action.
Image 220220 094646.764
Figure 18. After we "like" George’s message, it displays the "liked" badge.

Adding A Photo Capture Feature

Most messaging applications include the ability to add photos to messages. Let’s add this feature to our chat app now.

First we’ll define a new action called "capturePhoto", and add to the the TEXT_ACTIONS category of our view node.

public static final ActionNode capturePhoto = action(
        icon(FontImage.MATERIAL_CAMERA)
);

...

ViewNode viewNode = new ViewNode(
    actions(ChatRoomView.SEND_ACTION, send),
    actions(ProfileAvatarView.PROFILE_AVATAR_CLICKED_MENU, phone, videoConference),
    actions(ChatBubbleView.CHAT_BUBBLE_LONG_PRESS_MENU, likeAction),
    actions(ChatBubbleView.CHAT_BUBBLE_BADGES, likedBadge),
    actions(ChatRoomView.TEXT_ACTIONS, capturePhoto) (1)
);
1 Added capturePhoto action to the TEXT_ACTIONS category so that it will appear as a button beside the text field.

And we’ll also add a handler for this action, which will capture a photo, and emed the photo in a message that we will add to the chat room’s view model.

addActionListener(capturePhoto, evt->{
    evt.consume();
    String photoPath = Capture.capturePhoto();
    if (photoPath == null) {
        // User canceled the photo capture
        return;
    }


    File photos = new File("photos"); (1)
    photos.mkdirs();
    Entity entity = evt.getEntity();
    File photo = new File(photos, System.currentTimeMillis()+".png");
    try (InputStream input = FileSystemStorage.getInstance().openInputStream(photoPath);
            OutputStream output = FileSystemStorage.getInstance().openOutputStream(photo.getAbsolutePath())) {
        Util.copy(input, output);

        ChatBubbleView.ViewModel message = new ChatBubbleView.ViewModel();
        message.attachmentImageUrl(photo.getAbsolutePath()); (2)
        message.isOwn(true);
        message.date(new Date());
        EntityList messages = entity.getEntityList(ChatRoom.messages); (3)
        if (messages == null) {
            throw new IllegalStateException("This chat room has no messages list set up");
        }
        messages.add(message); (4)

    } catch (IOException ex) {
        Log.e(ex);
        ToastBar.showErrorMessage(ex.getMessage());
    }
});
1 We will create a directory named "photos" where we store all of the photos for the app.
2 Set the path of this photo under attachmentImageUrl. The ChatBubbleView will accept http, https, and file URLs, as well as storage keys. It will render them correctly in the view according to the type of URL it is.
3 The "entity" of this event is the view model for the ChatRoomView. Here we use the ChatRoom.messages tag to access the messages list in a loosely coupled way. This code will work even if we change the class that we use for the ChatRoomView’s view model.
4 Adding the message to the messages entity list will trigger a list change event and it will be rendered automatically in the chat room.

Now, let’s fire the chat up again and take it for a test drive.

Image 230220 010814.955
Figure 19. The capturePhoto action is rendered as a button beside the input text field.

You should now be able to click on the "capture photo" button to capture an image. In the simulator, it will open a file dialog to select an image. On device, it will activate the devices camera so that you can take a photo. After capturing an image, it should be added to the chat inside a message bubble as shown below:

Image 230220 011106.570
Figure 20. Photo appears in chat after capture.

Linking to a Back-end Chat Server

In this tutorial we created a mock chat application in order to demostrate the ChatRoomView, which is a user interface component. It did not include any integration with a server so it doesn’t allow you to actually chat with other people. Linking to a server is not difficult, and the MVC architecture of this example should make it very clear how the integration should occur. I’ll leave this integration as an exercise for the reader. As a starting point, I recommend checking out the cn1-websockets library, and its chat demo.

Further Reading

This tutorial provides just a small glimpse into the capabilities of Codename One and the CodeRAD library. To learn more, please see https://www.codenameone.com.