Adding one-to-one rooms to your Ionic 4 chat app

Adding one-to-one rooms to your Ionic 4 chat app
In this tutorial, we'll be learning how to add one-to-one chat rooms to a chat application built using Ionic 4 (with Angular), Nest.js and Chatkit.

You can get the source code of this demo from this GitHub repository.

We’ll not be building the application from scratch as we have already built one in the previous tutorial that you can check from the following links if you are interested to start the series from the beginning:

If you are only interested in one-to-one chat rooms, you can simply clone the starter project from GitHub and follow this tutorial instructions to learn how to implement the feature.

Prerequisites

You need to have the following prerequisites to work with this tutorial:

  • Knowledge of TypeScript (Required by both Nest.js and Ionic/Angular v4).
  • Recent versions of Node.js (v8.11.2) and NPM (v5.6.0).

Cloning and setting up the starter project

Let’s see how you can clone, set up and run the application step by step.

First, let’s start by cloning the latest version of the frontend project using the following command:

    $ git clone https://github.com/techiediaries/chatkit-profiles-read-cursors.git

Next, you need to navigate inside the frontend folder and install the dependencies by running the following commands:

    $ cd chatkit-profiles-read-cursors/frontend
    $ npm install

Next, if you didn’t do it yet, you need to create a Chatkit instance from Pusher Dashboard and take note of your instance locator and your room ID. Now, open the frontend/src/app/chat.service.ts file and update YOUR_INSTANCE_LOCATOR and YOUR_ROOM_ID with your own values.

Note: You can refer to the Configuring Chatkit section on the Building a mobile chat app with Nest.js and Ionic 4 - Part 1: Build the backend tutorial for instructions on how to create a Chatkit instance.

After adding these changes, you can run the development server of your frontend project using the following command:

    $ ionic serve

Your Ionic application will be running on the http://localhost:8100 address.

Next, open a new terminal and navigate to the server folder then install the dependencies of the server application using the following command:

    $ cd chatkit-profiles-read-cursors/server
    $ npm install

Next, open the server/src/auth/auth.service.ts file and change YOUR_INSTANCE_LOCATOR, YOUR_SECRET_KEY and YOUR_ROOM_ID with your own values.

After adding the changes, you can run a development server of the backend application using the following command:

    $ npm run start:dev

Your server will be running from the http://localhost:3000 address.

In our starter project we have added a public chat room where registered users can meet and start chatting with each other in group. This means everyone can see what others are saying in the chat room. Let’s now proceed to add one-to-one chat rooms, which allow us to implement chatting between two users and prevent the other users of the application from seeing what messages are sent between individual users.

Creating and joining 1-1 rooms

According to the docs:

Each user can be a member of many rooms. Rooms can be used for 1-1 chats or group chats; Chatkit does not differentiate between the two concepts at the room level.

You can create private or public rooms. By default, public rooms are visible to any user and can be joined by all users while private rooms are only visible to the members of the room and can’t be joined unless the user has the appropriate permissions or added by a member of the room.

At the room level, Chatkit doesn’t differentiate between 1-1 chats or group chats so we simply implement 1-1 chats between our users using private rooms that contain only two members.

You can create a room using the currentUser.createRoom() method. Let’s start by adding a getCurrentRoomId() method which creates a private room, if it doesn’t exist, of two users and returns its ID. Open the src/app/chat.service.ts file and add the following method:

       // src/app/chat.service.ts

       getCurrentRoomId(otherUserId){
        let returnObs = new BehaviorSubject(null);
        let userRooms: Array<any> = this.currentUser.rooms;
        const userId = this.currentUser.id;
        let name = `${userId}-${otherUserId}`;
        let altName = `${otherUserId}-${userId}`;

        let roomExists = userRooms.findIndex((room) =>{
          if(room['name'] === name || room['name'] === altName)
          {
            return true;
          }
          return false;

        });    
        if(roomExists !== -1) {
          returnObs.next(userRooms[roomExists].id)
          return returnObs;
        }
        this.currentUser.createRoom({
          name,
          private: true,
          addUserIds: [otherUserId]
        }).then(room => {
          returnObs.next(room.id);
        })
        .catch(err => {
          console.log(`Error creating room ${err}`)
        })
        return returnObs;
      }

We use the room name to store the identifiers of the two users of the room in the UserId-OtherUserId or OtherUserId-UserId formats. This way we make it very easy to check if a room is already created.

We get the rooms of the current user using this.currentUser.rooms and we use the findIndex() method of the array to check if a private room, of the current user and the other user, they are chatting with, already exists. In such case, we push the room ID to an RxJS Subject and return it. Otherwise, we call the createRoom() method to create the private room.

Next, we implement the connectToRoom() method which subscribes the current user to the specified private room:

      // src/app/chat.service.ts

       async connectToRoom(roomId){
        console.log("Subscribe to room: ", roomId);
        let messageSubject = new ReplaySubject();
        await this.currentUser.subscribeToRoom({
          roomId: roomId,
          hooks: {
            onMessage: message => {
              console.log("Got message: ", message);
              messageSubject.next(message);
            }        
          },
          messageLimit: 20
        }).then(currentRoom => {
          console.log("Subscribed to room: ", roomId);
        }); 

        return messageSubject;
      }

Accessing the private chat page

We already displayed our application users in the home page. Let’s now add a button to access the private chat before each user in the list. Open the src/app/home/home.page.html file, locate <ion-list> that displays the users and a <ion-button> with a chatbubbles icon:

    <!-- src/app/home/home.page.html -->

    <ion-list>
      <ion-item class="user-item" *ngFor="let user of userList">
          <div class="user-avatar">
              <img  [src]="user.avatarURL" alt="">
          </div>
          <ion-label class="user-name">
             {{ user.name }}
          </ion-label>
          <div class="user-presence">
              <ion-icon [class.user-online]="isOnline(user)" name="radio-button-on"></ion-icon>
          </div>  
          <div>
              <ion-button size="small" [routerLink]="['/private-chat',user.id]">
                <ion-icon name="chatbubbles" ></ion-icon>
              </ion-button>

          </div>  

      </ion-item>
    </ion-list>

We use the routerLink directive to create a /private-chat route that has a dynamic segment that contains the user ID which corresponds to the second user, in addition to the current user, in the private chat room.

Adding a private chat page

Let’s now add an Ionic page for private chat. Open a new terminal and run the following command to generate a page:

    $ ionic generate page private-chat  

This will add a route to the src/app/app-routing.module.ts file for accessing the private chat page:

    // src/app/app-routing.module.ts

    { path: 'private-chat', loadChildren: './private-chat/private-chat.module#PrivateChatPageModule' },

Passing the user ID to the route as argument

At this point, we can access the private chat page from /private-chat route but we also want to pass the ID of the user we are chatting with to the page so we’ll need to pass the ID using a route argument.

Open the src/private-chat/private-chat.module.ts file and update it accordingly:

    // src/private-chat/private-chat.module.ts

    const routes: Routes = [
      {
        path: ':id',
        component: PrivateChatPage
      }  
    ];

Injecting the necessary services

We’ll be using the Router, ActivatedRoute, Ionic Storage and ChatService services so we’ll need to inject them via the component constructor. Open the src/app/private-chat/private-chat.page.ts file and start by adding the following imports:

    // src/app/private-chat/private-chat.page.ts

    import { Router, ActivatedRoute } from '@angular/router';
    import { ChatService } from '../chat.service';
    import { Storage } from '@ionic/storage';

Next, inject the services as follows:

    // src/app/private-chat/private-chat.page.ts

    export class PrivateChatPage implements OnInit {
      constructor(private router: Router,private route: ActivatedRoute, private chatService: ChatService, private storage: Storage) { }

Next, define the following variables:

    // src/app/private-chat/private-chat.page.ts

      roomId;
      messageList: any[] = [];
      chatMessage: string = "";

      messageSubscription;
      roomSubscription;

Next, add the following initialization code:

    // src/app/private-chat/private-chat.page.ts

      async ngOnInit() {
        const userId = await this.storage.get("USER_ID");
        if(!this.chatService.isConnectedToChatkit()){
          await this.chatService.connectToChatkit(userId);
        }

        const otherUserId = this.route.snapshot.params.id;
        this.roomSubscription = this.chatService.getCurrentRoomId(otherUserId).subscribe(async (roomId)=>{
          this.roomId = roomId;
          this.messageSubscription = (await this.chatService.connectToRoom(roomId)).subscribe( message => {
            this.messageList.push(message);
          });
        });
      }

We retrieve the current user ID from the local storage using the Ionic Storage API, next we check if the user is connected to Chatkit using the isConnectedToChatkit() method. It it’s not connected, we call the connectToChatkit() method which takes the user ID as argument and connects the user to the Chatkit instance.

Next, we retrieve the other user ID from the route :id parameter and we use it to get the ID of the room that contains the two users (If it doesn’t exist, we create it) by calling the getCurrentRoomId() method of ChatService. Since this method is asynchronous, we need to subscribe to the returned RxJS subject to get the room ID.

We also store the retrieved private room ID in the roomId variable of the component. This way we can call it from the other methods of the component.

After that, we call the connectToRoom() method of ChatService to subscribe the current user to the room identified by the retrieved ID.

Note: We added the async keyword before the ngOnInit() hook because we are using the await keyword in the body.

Adding a method for sending messages

After adding the code for getting the room messages from the Chatkit instance, let’s now implement a method for sending a message to a private room. In the same src/app/private-chat/private-chat.page.ts file, add the following method to the component:

    // src/app/private-chat/private-chat.page.ts

      sendMessage() {
        this.chatService.sendMessage({ text: this.chatMessage , roomId: this.roomId}).then((messageId) => {
          this.chatMessage = "";
        });
      }

In the sendMessage() method of the component we call the sendMessage() method of ChatService, which takes an object containing the text of the message and the room ID. When the message is successfully sent we clear the message input area that’s bound to the chatMessage variable.

We’ll next bind the sendMessage() method to a button on our chat UI to enable users to send a message when clicked and also to the keyup event of the Enter key of the keyboard on the message input area.

Note: Please note that we have implemented the sendMessage() method of ChatService in the previous tutorials so we don’t need to re-implement it in this tutorial.

This is the implementation of the sendMessage() method:

    // src/app/chat.service.ts

      sendMessage(message) {
        if(message.attachment){
          return this.currentUser.sendMessage({
            text: message.text,
            attachment: { file: message.attachment, name: message.attachment.name },
            roomId: message.roomId || this.GENERAL_ROOM_ID
          });
        }
        else
        {
          return this.currentUser.sendMessage({
            text: message.text,
            roomId: message.roomId || this.GENERAL_ROOM_ID
          });
        }
      }

You can see that if the passed message object has a roomId key we call the sendMessage() of currentUser to send a message to the identified room. Otherwise we send the message to the general public room.

Building the UI

Let’s now add the code for creating a list that will hold the latest messages in the room and an input area we can users type their messages. Open the src/app/private-chat/private-chat.page.html file and add the following code:

    <!-- src/app/private-chat/private-chat.page.html -->

    <ion-header>
      <ion-toolbar color="primary">
        <ion-title>
          Private Chat Room
        </ion-title>
      </ion-toolbar>
    </ion-header>
    <ion-content padding>
      <div class="container">
        <div *ngFor="let msg of messageList" class="message left ">
          <img class="user-img" [src]="msg.sender.avatarURL" alt="" src="">
          <div class="msg-detail">
            <div class="msg-info">
              <p>
                {{msg.sender.name}}
              </p>
            </div>
            <div class="msg-content">
              <span class="triangle"></span>
              <p class="line-breaker">{{msg.text}}</p>
            </div>

          </div>
        </div>
      </div>
    </ion-content>
    <ion-footer no-border>
      <div class="input-wrap">
        <textarea #messageInput placeholder="Enter your message!" [(ngModel)]="chatMessage" (keyup.enter)="sendMessage()">
        </textarea>
        <button ion-button clear icon-only item-right (click)="sendMessage()">
          <ion-icon name="ios-send" ios="ios-send" md="md-send"></ion-icon>
        </button>
      </div>
    </ion-footer>

Next, open the src/app/private-chat/private-chat.page.scss file and add the following styles:

    // src/app/private-chat/private-chat.page.scss

    .input-wrap {
        padding: 5px;
        display: flex;
        textarea {
          flex: 3;
          border: 0;
          border-bottom: 1px #000;
          border-style: solid;
        }
        button {
            flex: 1;
        }
    }
    ion-footer {
        box-shadow: 0 0 4px rgba(0, 0, 0, 0.11);
        background-color: #fff;
    }
    ion-content .scroll-content {
        background-color: #f5f5f5;
    }
    .line-breaker {
        white-space: pre-line;
    }
    .container {

      .message {
        position: relative;
        padding: 7px 0;
        .msg-content {
            color: #343434;
            background-color: #ddd;
            float: left;        
        }
        .user-img {
            position: absolute;
            border-radius: 45px;
            width: 45px;
            height: 45px;
            box-shadow: 0 0 2px rgba(0, 0, 0, 0.36);
        }
        .msg-detail {
            width: 100%;
            padding-left: 60px;
            display: inline-block;
            p {
              margin: 0;
            }
            .msg-info {
              p {
                font-size: .8em;
                color: #888;
              }
            }
        }
      }
    }

This is a screenshot of our UI at this point:

This is our UI after sending a few messages:

Unsubscribing from the RxJS Observables

It’s always a good habit to unsubscribe from any Observables to avoid weird behaviors in your code. You can do that when the chat page is destroyed so go to the src/app/private-chat/private-chat.page.ts file and import the OnDestroy interface then implement it and override the ngOnDestroy() method as follows:

    // src/app/private-chat/private-chat.page.ts

    import { OnDestroy } from '@angular/core';
    /* ... */

    export class PrivateChatPage implements OnInit, OnDestroy {
      /* ... */
      ngOnDestroy(){
        if(this.roomSubscription){
          this.roomSubscription.unsubscribe();
        }
        if(this.messageSubscription){
          this.messageSubscription.unsubscribe();
        }
      }

    }

This way when the user is navigated away from the private chat page, they will be unsubscribed from the roomSubscription and messageSubscription subscriptions.

OnDestroy is an Angular interface that defines the ngOnDestroy() life-cycle hook which gets called when a component, directive, pipe, or service is destroyed.

Adding the logout method

Let’s also add a logout method to the private room. In the src/app/private-chat/private-chat.page.html file add a button to the toolbar area:

      <!-- src/app/private-chat/private-chat.page.html -->

      <ion-toolbar color="primary">
        <ion-title>
          Private Chat Room
        </ion-title>
        <ion-buttons slot="end">
          <ion-button (click)="logout()">
            Logout
          </ion-button>
        </ion-buttons>
      </ion-toolbar>

We bind the logout() method to the click event of the <ion-button> button.

Next, open the src/app/private-chat/private-chat.page.ts file and import AuthService:

    // src/app/private-chat/private-chat.page.ts

    import { AuthService } from '../auth.service';

Next, let’s inject it via the component constructor:

    // src/app/private-chat/private-chat.page.ts

      constructor(/* ... */, private authService: AuthService ) { }

Finally, define the logout() method as follows:

    // src/app/private-chat/private-chat.page.ts

      async logout(){
        await this.authService.logout();
        this.router.navigateByUrl('/login');
      }

We call the logout() method of AuthService and we navigate to the login page using the navigateByUrl() method of the Router.

Note: Please note that we don’t need to unsubscribe from the RxJS Observables in the logout() method since the ngOnDestroy() method will be called when we navigate to the login page using the navigateByUrl() method.

The logout() method of AuthService is already defined in the previous tutorials.

This is a screenshot of the UI after adding the logout button:

Adding a back button

Let’s also add a back button that allows us to go back to the previous home page from the private chat page. Go to the the src/app/private-chat/private-chat.page.html file and add:

    <!-- src/app/private-chat/private-chat.page.html -->

      <ion-toolbar color="primary">
        <!-- [...] -->
        <ion-buttons slot="start">
          <ion-back-button  defaultHref="home"></ion-back-button>
        </ion-buttons>
      </ion-toolbar> 

Conclusion

Throughout this tutorial we’ve implemented one-to-one private chats in our Ionic 4 chat application so now users can either chat in a group or access a private chat room by clicking on the button with the chat bubbles icon in the list of users in the home page. You can get the source code of this demo from this GitHub repository.

Further reading:

Ionic 2 Tutorial with Two Complete Apps
http://learnstartup.net/p/r1TOtH3Le

Ionic 2 Master Course - The New Generation of Mobile Apps
http://learnstartup.net/p/SJj9Yr3Ix

Rapid Prototyping with Ionic: Build a Data-Driven Mobile App
http://learnstartup.net/p/ryc75Sn8x

Ionic 3 - Learn How to Design Ionic Apps
http://learnstartup.net/p/HkYN-DreT9b

Ionic 3 - Tips & Tricks for Developing Ionic Apps
http://learnstartup.net/p/HkqMZVbnZ

Suggest:

Angular and Nodejs Integration Tutorial

Angular Tutorial - Learn Angular from Scratch

Build a Real Time Chat App With Node.js

Getting Started with Node.js - Full Tutorial

Intro to HTML & CSS - Tutorial

How To Create A Password Protected File Sharing Site With Node.js, MongoDB, and Express