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.
You need to have the following prerequisites to work with this tutorial:
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.
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;
}
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.
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' },
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
}
];
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 thengOnInit()
hook because we are using theawait
keyword in the body.
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 ofChatService
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.
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:
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.
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 thengOnDestroy()
method will be called when we navigate to the login page using thenavigateByUrl()
method.The
logout()
method ofAuthService
is already defined in the previous tutorials.
This is a screenshot of the UI after adding the logout 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>
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
☞ 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