Chat Read Cursors with Angular 9/Ionic 5 Chat App: Working with TypeScript Variables/Methods & Textarea Keydown/Focusin Events

Chat Read Cursors with Angular 9/Ionic 5 Chat App: Working with TypeScript Variables/Methods & Textarea Keydown/Focusin Events

Angular 9 and Ionic 5 Chat App

In this tutorial we'll implement chat read cursors in our Angular 9 and Ionic 5 Chat App using TypeScript basics like variables and methods, HTML textarea keydown/focusin events and HTML data attributes.

We'll also see how to work with template reference variables in Angular and how to bind TypeScript methods to DOM events in Angular templates.

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.

We'll learn about:

  • Adding Chat Read Cursors to our Angular 9/Ionic 5 App,
  • Using Angular template variables and HTML data attributes to track unread messages in the chat UI.
  • Defining TypeScript methods and bind them to DOM events in Angular templates.
  • Defining TypeScript variables such as the readPosition variable that will be used to store the position of the read cursor of the current user. The userTyped variable which is a boolean that will be used to track if the user has typed something in the message input area and the unreadCount variable which stores the number of the unread messages.
  • Defining the TypeScript getReadMessageId() method which returns the index in the messageList array of the message that was most recently read by the user.
  • Updating the onKeydown() method (that gets called when the keydown event is fired in the HTML textarea) to send the typing event and set a userTyped boolean variable accordingly.
  • Defining the TypeScript onFocus() method which gets called when the focusin event of the HTML textarea is fired. 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 TypeScript scrollToBottom() method to scroll down the chat UI.

These are all the tutorial parts:

Adding TypeScript Methods for Setting and Reading Chat Cursors

Let’s start with the chat service of the Angular 9/Ionic 5 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 TypeScript setReadCursor() method of our Angular service calls the setReadCursor() method of currentUser to set the position of the read cursor of the current user in the room.

The TypeScript 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 our both defined TypeScript 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 file and start by defining these TypeScript 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() method 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 TypeScript 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);
    });
  }

Updating the Textarea' keydown Handler Method

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;
  }

Defining the Handler Method for the Textarea'focusin event

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 be removed.

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: ()</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>

          </p>
        </div>
        <div class="msg-content">
          <span class="triangle"></span>
          <img *ngIf="msg.attachment" [src]="msg.attachment.link"
          />
          <p class="line-breaker"></p>
        </div>

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

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

<ion-footer no-border>
  <div  *ngIf="typingUsers.length > 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 implemented chat 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.

We have learned about:

  • Using Angular template variables and HTML data attributes for getting and displaying unread messages in our UI.
  • Defining and binding TypeScript methods to events in Angular templates supercharged with Ionic UI components.
  • Defining TypeScript number, boolean. and string TypeScript.
  • Defining the TypeScript onFocus() method which gets called when the focusin event of the HTML textarea is fired.


Create Angular 17 Project
Building a Password Strength Meter in Angular 17
Angular 17 Password Show/Hide with Eye Icon
Angular 17 tutoriel
Angular Select Change Event
Angular iframe
Angular FormArray setValue() and patchValue()
Angular Find Substring in String
Send File to API in Angular 17
EventEmitter Parent to Child Communication
Create an Angular Material button with an icon and text
Input change event in Angular 17
Find an element by ID in Angular 17
Find an element by ID from another component in Angular 17
Find duplicate objects in an array in JavaScript and Angular
What is new with Angular 17
Style binding to text-decoration in Angular
Remove an item from an array in Angular
Remove a component in Angular
Delete a component in Angular
Use TypeScript enums in Angular templates
Set the value of an individual reactive form fields
Signal-based components in Angular 17
Angular libraries for Markdown: A comprehensive guide
Angular libraries for cookies: A comprehensive guide
Build an Angular 14 CRUD Example & Tutorial
Angular 9 Components: Input and Output
Angular 13 selectors
Picture-in-Picture with JavaScript and Angular 10
Jasmine Unit Testing for Angular 12
Angular 9 Tutorial By Example: REST CRUD APIs & HTTP GET Requests with HttpClient
Angular 10/9 Elements Tutorial by Example: Building Web Components
Angular 10/9 Router Tutorial: Learn Routing & Navigation by Example
Angular 10/9 Router CanActivate Guards and UrlTree Parsed Routes
Angular 10/9 JWT Authentication Tutorial with Example
Style Angular 10/9 Components with CSS and ngStyle/ngClass Directives
Upload Images In TypeScript/Node & Angular 9/Ionic 5: Working with Imports, Decorators, Async/Await and FormData
Angular 9/Ionic 5 Chat App: Unsubscribe from RxJS Subjects, OnDestroy/OnInit and ChangeDetectorRef
Adding UI Guards, Auto-Scrolling, Auth State, Typing Indicators and File Attachments with FileReader to your Angular 9/Ionic 5 Chat App
Private Chat Rooms in Angular 9/Ionic 5: Working with TypeScript Strings, Arrays, Promises, and RxJS Behavior/Replay Subjects
Building a Chat App with TypeScript/Node.js, Ionic 5/Angular 9 & PubNub/Chatkit
Chat Read Cursors with Angular 9/Ionic 5 Chat App: Working with TypeScript Variables/Methods & Textarea Keydown/Focusin Events
Add JWT REST API Authentication to Your Node.js/TypeScript Backend with TypeORM and SQLite3 Database
Building Chat App Frontend UI with JWT Auth Using Ionic 5/Angular 9
Install Angular 10 CLI with NPM and Create a New Example App with Routing
Styling An Angular 10 Example App with Bootstrap 4 Navbar, Jumbotron, Tables, Forms and Cards
Integrate Bootstrap 4/jQuery with Angular 10 and Styling the UI With Navbar and Table CSS Classes
Angular 10/9 Tutorial and Example: Build your First Angular App
Angular 9/8 ngIf Tutorial & Example
Angular 10 New Features
Create New Angular 9 Workspace and Application: Using Build and Serve
Angular 10 Release Date: Angular 10 Will Focus on Ivy Artifacts and Libraries Support
HTML5 Download Attribute with TypeScript and Angular 9
Angular 9.1+ Local Direction Query API: getLocaleDirection Example
Angular 9.1 displayBlock CLI Component Generator Option by Example
Angular 15 Basics Tutorial by Example
Angular 9/8 ngFor Directive: Render Arrays with ngFor by Example
Responsive Image Breakpoints Example with CDK's BreakpointObserver in Angular 9/8
Angular 9/8 DOM Queries: ViewChild and ViewChildren Example
The Angular 9/8 Router: Route Parameters with Snapshot and ParamMap by Example
Angular 9/8 Nested Routing and Child Routes by Example
Angular 9 Examples: 2 Ways To Display A Component (Selector & Router)
Angular 9/8 Tutorial: Http POST to Node/Express.js Example
Angular 9/8 Feature and Root Modules by Example
Angular 9/8 with PHP: Consuming a RESTful CRUD API with HttpClient and Forms
Angular 9/8 with PHP and MySQL Database: REST CRUD Example & Tutorial
Unit Testing Angular 9/8 Apps Tutorial with Jasmine & Karma by Example
Angular 9 Web Components: Custom Elements & Shadow DOM
Angular 9 Renderer2 with Directives Tutorial by Example
Build Progressive Web Apps (PWA) with Angular 9/8 Tutorial and Example
Angular 9 Internationalization/Localization with ngx-translate Tutorial and Example
Create Angular 9 Calendar with ngx-bootstrap datepicker Example and Tutorial
Multiple File Upload with Angular 9 FormData and PHP by Example
Angular 9/8 Reactive Forms with Validation Tutorial by Example
Angular 9/8 Template Forms Tutorial: Example Authentication Form (ngModel/ngForm/ngSubmit)
Angular 9/8 JAMStack By Example
Angular HttpClient v9/8 — Building a Service for Sending API Calls and Fetching Data
Styling An Angular 9/8/7 Example App with Bootstrap 4 Navbar, Jumbotron, Tables, Forms and Cards

✋If you have any questions about this article, ask them in our GitHub Discussions 👈 community. You can also Gitter

✋ Want to master Angular 14? Read our angular tutorial and join our #DailyAngularChallenge where we learn to build components, directives, services, pipes and complete web, mobile, and desktop applications with latest Angular version.

✋ Make sure to join our Angular 14 Dev Community 👈 to discuss anything related to Angular development.

❤️ Like our page and subscribe to our feed for updates!

Find a list of emojis to copy and paste