We're hiring
Products

Channels

Beams

Chatkit

DocsTutorialsSupportCareersPusher Blog
Sign InSign Up
Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Sign InSign Up

Add profile photos and read cursors to your Ionic 4 chat app

  • Ahmed Bouchefra
April 15th, 2019
You will need Ionic 4 and Node installed on your machine.

In this tutorial, we'll learn to use Chatkit read cursors and customize user profile photos.

This is based on the Ionic 4 application we’ve built in the previous series. You can get the source code of this part from this GitHub repository.

In the previous tutorials, we’ve started building a mobile application with Ionic and Angular on the frontend and Nest.js on the backend. For chat features, we’ve used Chatkit which provides out of the box chat features commonly used in most popular chat apps. We’ve added features like typing indicators and file attachments. Now, we’ll proceed with our demo application by implementing other functionalities such as read cursors and profile photos.

Note: You can read the previous tutorials where we’ve built our demo chat application from these links:

Building a mobile app with Nest.js, Ionic 4 and Chatkit - Part 1: Build the backend Building a mobile app with Nest.js, Ionic 4 and Chatkit - Part 2: Build the frontend Adding authentication, typing indicators and file attachments to your Ionic 4 chat app

We are not going to reinvent the wheel, instead we’ll be using the demo application we’ve built in the previous tutorials so if you don’t want to follow from the start, you can simply clone the project from GitHub. Follow these instructions to set up and run your application.

Start by cloning the latest version of our frontend project using the following command:

    $ git clone https://github.com/techiediaries/chatkit-ionic-demo.git

Next, navigate inside the frontend folder and install the dependencies using:

    $ cd chatkit-ionic-demo/frontend
    $ npm install

Before starting your application, you need to open the frontend/src/app/chat.service.ts file and update YOUR_INSTANCE_LOCATOR and YOUR_ROOM_ID with your own values which you can get from your Pusher dashboard after creating a Chatkit instance.

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.

Now, you can start the development server of the frontend project using:

    $ ionic serve

Your Ionic application will be running from 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-ionic-demo/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.

That’s it, you can now start the backend application using:

    $ npm run start:dev

This will start a live-reload development server which will be running from the http://localhost:3000 address.

Adding improvements

Before implementing the features of this tutorial, let’s first add some improvements to our application.

First, open the src/app/chat.service.ts file, initialize the currentUser variable with a null value and add the isConnectedToChatkit() method which checks if we are connected to Chatkit:

    // src/app/chat.service.ts

    currentUser = null;

    // [...]

      isConnectedToChatkit(){
        return this.currentUser !== null;
      }

This simply checks if currentUser is different than null.

Also, you need to change the connectToChatkit() method to use a local variable for storing messages:

    // src/app/chat.service.ts

    async connectToChatkit(userId: string) {
        let messages = [];
        this.chatManager = new ChatManager({
          instanceLocator: this.INSTANCE_LOCATOR,
          userId: userId,
          tokenProvider: new TokenProvider({ url: this.AUTH_URL })
        })
        this.currentUser = await this.chatManager.connect();
        await this.currentUser.subscribeToRoom({
          roomId: this.GENERAL_ROOM_ID,
          hooks: {
            onMessage: message => {
              messages.push(message);
              this.messagesSubject.next(messages);
            },
            onUserStartedTyping: user => {
              this.typingUsers.push(user.name);
            },
            onUserStoppedTyping: user => {
              this.typingUsers = this.typingUsers.filter(username => username !== user.name);
            }        
          },
          messageLimit: 20
        });

        const users = this.currentUser.rooms[this.GENERAL_ROOM_INDEX].users;
        this.usersSubject.next(users);
      }

On the line 4, we define a messages array and on line 15, we push the received messages to this array instead of this.messages (member variable of the service).

Next, open the src/app/chat/chat.page.ts file and import then inject Ionic Storage and ChangeDetectorRef:

    // src/app/chat/chat.page.ts
    // [...]
    import { ChangeDetectorRef } from '@angular/core';
    import { Storage } from '@ionic/storage';

    export class ChatPage implements OnInit {
      // [...]
      constructor(private router: Router, private chatService: ChatService, private authService: AuthService, private storage: Storage, private cdRef : ChangeDetectorRef) { }

Next, in the ngOnInit() method check if we are connected to Chatkit, if not call the connectToChatkit() and pass the user ID:

    // src/app/chat/chat.page.ts
    // [...]

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

This will allow us to connect to Chatkit from the chat page if we are not connected yet.

Note: Make sure to add the async keyword before the ngOnInit() method.

Next, import the OnDestroy and AfterViewChecked interfaces and implement them:

    // src/app/chat/chat.page.ts

    export class ChatPage implements OnInit, AfterViewChecked, OnDestroy {}

Next, define the ngOnDestroy() and ngAfterViewChecked() methods:

      // src/app/chat/chat.page.ts

      ngOnDestroy(){
        if(this.getMessagesSubscription){
          this.getMessagesSubscription.unsubscribe();
        }
      }
      ngAfterViewChecked(){
        this.cdRef.detectChanges();
      }

Also define getMessagesSubscription variable we used in the ngOnDestroy() method:

    // src/app/chat/chat.page.ts

    export class ChatPage implements OnInit, AfterViewChecked, OnDestroy {
      // [...]
      getMessagesSubscription;

And change the ngOnInit() method to assign the returned Subscription from the this.chatService.getMessages().subscribe() method to this.getMessagesSubscription:

       // src/app/chat/chat.page.ts

       async ngOnInit() {
        const userId = await this.storage.get("USER_ID");
        if(!this.chatService.isConnectedToChatkit()){
          await this.chatService.connectToChatkit(userId);
        }
        this.getMessagesSubscription = this.chatService.getMessages().subscribe(messages => {
          this.messageList = messages;
          this.scrollToBottom();   
        });
      }

Next, change the logout() method to unsubscribe from getMessagesSubscription:

     // src/app/chat/chat.page.ts

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

Now, open the src/app/home/home.page.ts file and add the getUsersSubscription variable:

    // src/app/home/home.page.ts

    export class HomePage implements OnInit {
      // [...]
      getUsersSubscription;

Next, import the OnDestroy interface and implement it:

    // src/app/home/home.page.ts
    export class HomePage implements OnInit, OnDestroy {}

Next, change the ngOnInit() method to assign the Subscription returned from the getUsers().subscribe() method to getUsersSubscription variable:

      // src/app/home/home.page.ts

      async ngOnInit() {
        this.userId = this.route.snapshot.params.id || await this.storage.get("USER_ID");
        this.chatService.connectToChatkit(this.userId);
        this.getUsersSubscription = this.chatService.getUsers().subscribe((users) => {
          this.userList = users;
        });
      }

Finally add the ngOnDestroy() method and unsubscribe from getUsersSubscription:

      // src/app/home/home.page.ts

      ngOnDestroy(){
        this.getUsersSubscription.unsubscribe();
      }

The previous changes will allow us to unsubscribe from the usersSubject and messagesSubject subjects defined in the src/app/chat.service.ts file and solve issues related to Angular Change Detection in development (The ExpressionChangedAfterItHasBeenCheckedError error) and duplicate messages displayed in the chat UI.

Let’s now implement our features!

Adding profile photos

Most chat applications provide a way for users to add a profile photo. Chatkit allows you to assign profile photos to users but doesn’t provide storage so you need to upload images to your server.

Implementing the backend

Nest.js uses the multer middleware for supporting file uploading. The middleware can be configured and adjusted depending on your requirements.

Note: Multer is a middleware that works only with the multipart/form-data encoding type, which is primarily used for uploading files.

You can upload a single file by using the FileInterceptor() and @UploadedFile() decorators, and you can then access the file from the file property in the request object. Let’s see this by example.

Open the src/auth/auth.service.ts file and add the following method:

       // src/auth/auth.service.ts

       public async updateUserAvatar(userData: any): Promise<any>{
          const userId = userData.userId;
          const avatarURL = userData.avatarURL;
          return this.chatkit.updateUser({id:userId, avatarURL:avatarURL});
        }

This method will be used to update the user avatar. It simply calls the updateUser() available in the chatkit instance. You can read the docs for more information about this method.

The method is passed an object that provides the ID of the user and the avatar URL.

Next, open the src/app.controller.ts file and add the imports for the Get, Res and Param symbols from the @nestjs/common package:

    // src/app.controller.ts

    import { Get, Post, Body,Request, Res, Param, Controller} from '@nestjs/common';

Note: the other symbols are already imported from the previous tutorials.

Next, add the following imports which are necessary for file uploading:

    // src/app.controller.ts

    import { UseInterceptors, FileInterceptor, UploadedFile } from '@nestjs/common';

Next, import diskStorage from the multer package and extname from the path module:

    // src/app.controller.ts

    import { diskStorage } from 'multer';
    import { extname } from 'path';

Next, add the following route:

      // src/app.controller.ts

      @Get('uploads/:imgId')
      async uploads(@Param('imgId') imgId, @Res() res): Promise<any> {
        res.sendFile(imgId, { root: 'uploads'});
      }

The @Get() decorator before the uploads() method tells Nest to create an endpoint for this particular route path and map every corresponding request to this handler.

The @Param decorator allows us to get the value of the imgId parameter passed through the URL.

The @Res() decorator allows us to inject a library-specific response object. You can find more details about it from the Express docs.

The injected response object provides the sendFile() method (which is supported in Express v4.8.0+) that transfers a file at the given path (passed as the first argument) and sets the Content-Type response HTTP header field based on the filename’s extension.

Since we specified the root option in the options object (passed as the second argument), the path argument can be a relative path to the file (In our case it’s just the name of the file). In our example, the file will be transferred from the uploads folder.

This route will allow our Nest.js application to serve static files from an uploads folder.

Now, let’s test if our application is serving files correctly. In the root folder of your server project, create an uploads folder:

    mkdir uploads 

Note: If you don’t manually create this folder it will automatically be created by Nest.js but in our case we need to add our user default avatar to this folder that’s why we are manually creating it.

Next, add the default user avatar that we used before from the https://image.flaticon.com/icons/png/128/149/149071.png link in the uploads folder (save it as avatar.png).

Next, open the src/app/auth/auth.service.ts file, locate the createUser() method and change the default value of the avatarURL variable to point to our avatar.png image in our server:

    // src/app/auth/auth.service.ts

    const avatarURL = "http://127.0.0.1:3000/uploads/avatar.png";

Now, we are serving the default avatar from our server.

At this point, if you test both your frontend and backend apps, you should be able to see the default avatar assigned to new users in your application:

Now that we are able to serve static image files from our backend application, let’s add the route for allowing users to update their profile photos.

Open the src/app.controller.ts file and add the following route:

  // src/app.controller.ts

  @Post('avatar')
  @UseInterceptors(FileInterceptor('file',
    {
      storage: diskStorage({
        destination: './uploads',
        filename: (req, file, cb) => {
          const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('')
          return cb(null, `${randomName}${extname(file.originalname)}`)
        }
      })
    }
  )
  )
  uploadAvatar(@Body() userData, @UploadedFile() file) {
    let userId = userData.userId;
    this.authService.updateUserAvatar({
      userId: userId,
      avatarURL: `${this.SERVER_URL}${file.path}`
    });
    return {
      avatarURL:  `${this.SERVER_URL}${file.path}`
    };
  }

This allows you to send a POST request to the 127.0.0.1:3000/avatar endpoint from the frontend application to update the profile photo.

The uploadAvatar() method is mapped to the /avatar endpoint and accepts POST requests which contain the user identifier and the uploaded file. In the body of the method we simply call the updateUserAvatar() method defined in AuthService to update the user avatar with the URL of the uploaded image.

Also in your controller add the SERVER_URL variable which holds the address of your server:

  // src/app.controller.ts

  SERVER_URL = 'http://localhost:3000/';  

Adding the profile page in the frontend

After implementing file uploading in the backend, let’s now add a profile page in our Ionic application which allows users to upload their profile avatars to the server.

Open a new terminal and navigate to your frontend folder using:

  $ cd chatkit-ionic-demo/frontend

Next, generate a new Ionic page using the following command:

    $ ionic generate page profile

This will create a src/app/profile folder with the necessary files and add a profile route to the src/app/app-routing.module.ts file:

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

  { path: 'profile', loadChildren: './profile/profile.module#ProfilePageModule' },

Next, open the src/app/home/home.page.html file and add a button that will take us to the profile page just below the START CHATTING button:

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

  <ion-button color="light" outline size="large" [routerLink]="'/profile'">
    <ion-icon name="settings"></ion-icon>
    Profile settings
  </ion-button> 

This is a screenshot of the UI:

Next, open the src/app/profile/profile.page.html file and update its content with the following code:

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

    <ion-header>
      <ion-toolbar color="primary">
        <ion-title>
          Chatkit Demo
        </ion-title>

      </ion-toolbar>
    </ion-header>
    <ion-content padding>
      <form #f="ngForm" (ngSubmit)="uploadAvatar(f)">
        <ion-grid>
          <ion-row justify-content-center>
            <ion-col align-self-center size-md="6" size-lg="5" size-xs="12">
              <div text-center>
                <h3>Update your profile photo</h3>
              </div>
              <div padding>
                <ion-item>
                    <img *ngIf="avatarURL" [src]="avatarURL"
                    />
                </ion-item>
                <ion-item>
                  <input name="file" type="file" accept="image/x-png,image/jpeg" (change)="attachFile($event)" ngModel required />
                </ion-item>
              </div>
              <div padding>
                <ion-button size="large" type="submit" [disabled]="f.invalid" expand="block">Update photo</ion-button>
              </div>
            </ion-col>
          </ion-row>
        </ion-grid>
      </form>
    </ion-content> 

We create a form and we bind the ngSubmit event to an uploadAvatar() method that will be called when the users click on the submit button. The uploadAvatar() method takes a reference to the form created using a template reference variable (#f="ngForm").

We also bind an attachFile() method to the change event of the file input tag which will be called when the user clicks the Choose File button and select a file.

Next, we need to define the uploadAvatar() and attachFile() methods. Open the src/app/profile/profile.page.ts file and start by adding the following imports:

    // src/app/profile/profile.page.ts

    import { HttpClient } from '@angular/common/http';
    import { Storage } from '@ionic/storage';

We import HttpClient for sending POST requests to the server and the Ionic Storage service for working with local storage.

Next, define SERVER_URL, fileToUpload, userId and avatarURL variables:

    // src/app/profile/profile.page.ts

    @Component({
      selector: 'app-profile',
      templateUrl: './profile.page.html',
      styleUrls: ['./profile.page.scss'],
    })
    export class ProfilePage implements OnInit {
      SERVER_URL = 'http://localhost:3000/avatar';
      fileToUpload: File = null;
      userId = null;
      avatarURL;

The SERVER_URL variable simply holds the server endpoint for uploading user profiles.

The fileToUpload variable will hold the selected image file that will be uploaded to the server.

Next, inject the HttpClient and Storage services via the constructor:

  // src/app/profile/profile.page.ts

  constructor(private httpClient: HttpClient, private storage: Storage) { }

Next, we need to retrieve the ID of the currently logged in user from localStorage in the ngOnInit() life-cycle method of the page:

  // src/app/profile/profile.page.ts

  async ngOnInit() {
    this.userId =  await this.storage.get("USER_ID");
  } 

We simply call the get() method of the Storage service to retrieve the USER_ID and assign it to the userId variable we defined earlier.

Note: Make sure to add the async keyword before the ngOnInit() method to be able to use the await keyword in the body of the method.

Next, we define the attachFile() method:

  // src/app/profile/profile.page.ts

  attachFile(e){
    if (e.target.files.length == 0) {
      console.log("No file selected!");
      return
    }
    let file: File = e.target.files[0];
    this.fileToUpload = file;
  }

This method will be called when the user selects a file. If a file is selected it will be stored in the fileToUpload variable we defined earlier.

Now that we have added the code to retrieve the user identifier from the local storage and select the file from the user drive, let’s add the method that actually uploads the selected file to the server along with the currently logged in user:

  // src/app/profile/profile.page.ts

  uploadAvatar(f){
    let formData = new FormData(); 
    formData.append('file', this.fileToUpload, this.fileToUpload.name); 
    formData.append('userId', this.userId);
    this.httpClient.post(this.SERVER_URL, formData).subscribe((res) => {

    console.log(res);
    this.avatarURL = res['avatarURL'];
    });
    return false;     
  }

We use the FormData interface to create a form object and we use the append() method to add fields to the form (the file field which contains the file to upload and the userId field which contains the user identifier). Finally we send the form data with a POST request using the post() method of HttpClient.

Here is the definition of FormData from Mozilla docs:

The FormData interface provides a way to easily construct a set of key/value pairs representing form fields and their values, which can then be easily sent using the XMLHttpRequest.send() method. It uses the same format a form would use if the encoding type were set to "multipart/form-data".

That’s it we are finished with this part dealing with updating the user avatar. This is a screenshot of the profile page after we uploaded a new avatar for the currently logged in user:

Adding read cursors

Read cursors allow you to let users know how far they or other users have read the conversation. This means you can keep track of the most recently read message ID for each user of a room.

Let’s start with the chat service of the frontend project. Open the src/app/chat.service.ts file and add the following two methods:

  // src/app/chat.service.ts

  setReadCursor(messageId: number , roomId = this.GENERAL_ROOM_ID){
    this.currentUser.setReadCursor({
      roomId: roomId,
      position: messageId
    })
  }

  getReadCursor(roomId = this.GENERAL_ROOM_ID) {    
    const cursor = this.currentUser.readCursor({
      roomId: roomId
    })
    if (cursor) {
      return cursor.position;
    } else {
      return -1;
    }
  }

The setReadCursor() calls the setReadCursor() of currentUser to set the position of the read cursor of the current user in the room.

The getReadCursor() method calls the readCursor() method of currentUser to get the position of the read cursor of the current user in the room. If the read cursor is undefined, we return -1.

In both methods, if you don’t specify the room ID, the ID of our general room will be used.

Next, open the src/app/chat/chat.page.ts and start by defining these variables:

  // src/app/chat/chat.page.ts

  export class ChatPage implements OnInit, AfterViewChecked, OnDestroy {
    // [...]
    readPosition: number;
    userTyped = false; 
    unreadCount = 0;

The readPosition variable will be used to store the position of the read cursor of the current user. The userTyped variable is a boolean which will be used to track if the user has typed something in the message input area and the unreadCount variable will store the number of the unread messages.

Next, add the getReadMessageId() which returns the index in the messageList array of the message that was most recently read by the user:

  // src/app/chat/chat.page.ts

  getReadMessageId(){

    let i = 0, l = this.messageList.length;
    for(i; i < l; i++) {
      if(this.messageList[i].id == this.readPosition)
      {
        return i;
      } 
    }
    return l;
  }

We simply loop through the array and we compare the ID of the current message with the read cursor position that was previously stored in the readPosition variable.

Next, update the sendMessage() method to set the read cursor position after the user sends a message:

  // src/app/chat/chat.page.ts

  sendMessage() {
    this.chatService.sendMessage({ text: this.chatMessage, attachment: this.attachment }).then((messageId) => {
      this.chatMessage = "";
      this.attachment = null;
      this.scrollToBottom();
      this.chatService.setReadCursor(messageId);
    });
  }

Also, update the onKeydown() method (that gets called when the keydown event is fired in the message textarea) to set the userTyped variable to true.

  //src/app/chat/chat.page.ts

  onKeydown(e){
    this.chatService.sendTypingEvent();
    this.userTyped = true;
  }

Next, define the onFocus() method which gets called when the focusin event of the message textarea is fired:

  // src/app/chat/chat.page.ts

  onFocus(e){    
    const messageListLength = this.messageList.length;
    let messageId = this.messageList[messageListLength - 1].id;
    this.chatService.setReadCursor(messageId);
    this.scrollToBottom();
  }

In this method, we set the position of the read cursor of the current user to the latest message that was received and we also call the scrollToBottom() method to scroll down the chat UI.

Note: When the message textarea gets focus we consider that the user has read the latest messages in the room.

Next, add the isMostRecentReadMessage() method which returns whether a chat message is the latest read message by the user:

  // src/app/chat/chat.page.ts

  isMostRecentReadMessage(messageDom, msg){
    let lastMessage = this.messageList[this.messageList.length - 1];
    let messageId = Number(messageDom.getAttribute('data-message-id'));

    return messageId == this.readPosition && !this.userTyped && messageId !== lastMessage.id;
  }

We first get the last message in the messageList array, next we get the data-message-id attribute from the message <div> element in the chat UI. Finally, we we return true if:

  • The message ID equals the read cursor position,
  • The user hasn’t typed something yet,
  • And the message is not the last message in the array.

This method will be applied on each message DOM element of the chat UI and will be used to determine if the message is the latest one read by the user.

Note: We’ll use the isMostRecentReadMessage() to determine whether we can display the Un-Read Messages DOM element on the chat UI that’s why we also check if the user has typed something on the message input field. This way, once the user has started typing, the Un-Read Messages element will disappear.

Now, open the src/app/chat/chat.page.html file and start by binding the onFocus() method to the focusin event of the message textarea:

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

  <textarea #messageInput placeholder="Enter your message!" [(ngModel)]="chatMessage" (keyup.enter)="sendMessage()" (keydown)="onKeydown($event)" (keyup)="onKeyup($event)" (focusin)="onFocus()">   

Note: The focusin event was already bound to the scrollToBottom() method that’s why we moved the call of this method to the onFocus() method.

Next add the #messageId template variable to the <div> element that will contain each message and also add the data-message-id data attribute which holds the ID of the message contained in the <div> element:

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

  <ion-content #scrollArea padding>
    <div class="container">
      <div  #messageId *ngFor="let msg of messageList" class="message left " [attr.data-message-id]="msg.id">

We’ll use the messageId template reference to pass the DOM element containing the message as the first argument of the isMostRecentReadMessage() method we defined earlier.

If you now inspect the chat UI with the browser dev tools, you will see that each <div> element contains a data-message-id attribute which holds the Chatkit ID of the corresponding message:

Next, below the <div> element with the msg-detail class add the following code:

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

  <div class="msg-unread" *ngIf="isMostRecentReadMessage(messageId, msg)">
    <p>Un-Read Messages: ({{unreadCount}})</p>
  </div>

We use the ngIf directive to display this <div> element after the most recent message read by the user except if it’s the last received message. This element shows the Un-Read Massages string with the number of unread messages and will disappear if the user starts typing in the message input area.

This is the full content of the chat.page.html file at this point:

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

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

        </ion-buttons>
      </ion-toolbar>
    </ion-header>

    <ion-content #scrollArea padding>
      <div class="container">
        <div  #messageId *ngFor="let msg of messageList" class="message left " [attr.data-message-id]="msg.id">
          <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>
              <img *ngIf="msg.attachment" [src]="msg.attachment.link"
              />
              <p class="line-breaker">{{msg.text}}</p>
            </div>

          </div>
          <div class="msg-unread" *ngIf="isMostRecentReadMessage(messageId, msg)">
            <p>Un-Read Messages: ({{unreadCount}})</p>

          </div>
        </div>
      </div>
    </ion-content>

    <ion-footer no-border>
      <div  *ngIf="typingUsers.length > 0">
            {{ typingUsers[0] }} is typing
      </div>
      <div class="input-wrap">

        <textarea #messageInput placeholder="Enter your message!" [(ngModel)]="chatMessage" (keyup.enter)="sendMessage()" (keydown)="onKeydown($event)" (keyup)="onKeyup($event)" (focusin)="onFocus()">
        </textarea>
        <input #messageAttachment type="file" accept="image/x-png,image/gif,image/jpeg"
         name="myAttachment" (change)="attachFile($event)" style = "display: none;"/>

        <ion-button  shape="round" fill="outline" icon-only item-right (click)="messageAttachment.click()">
            <ion-icon name="folder"></ion-icon>
        </ion-button>

        <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/chat/chat.page.scss and add some styling for the msg-unread class:

    // src/app/chat/chat.page.scss

    .container {

      .message {
     // [...]
        .msg-unread{
            width: 100%;
            padding-left: 60px;
            display: inline-block;
        }
      }
    }

Here is a screenshot of the page with four unread messages:

Now, try to send a few messages in the group then logout and login with another account, you should get Un-Read Messages displayed with the count of the unread messages.

If you register a new user, their read cursor will be undefined so you will not see the Un-Read Messages message but once they send their first message, their read cursor will be set to that message and you’ll be able to see Un-Read Messages in the next time they login provided that someone has sent a message in the group.

Note: For better testing results, try to use a clean browsing session. You can either use a browser which was not used before for testing the application, clear your browser history and local storage or use the incognito mode in Chrome or the private mode in Firefox.

Conclusion

In this tutorial, we’ve seen how to upload and serve user avatars in our Ionic 4 and Nest.js chat application and we also used Chatkit to implement read cursors that show users the position of the latest message they have read and the count of their unread messages in the room. You can get the source code from this GitHub repository.

Clone the project repository
  • Angular
  • Chat
  • chatroom
  • JavaScript
  • TypeScript
  • Chatkit

Products

  • Channels
  • Beams
  • Chatkit

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.