Day 8 : Searching users, creating / displaying chats

Create a chat application with Ionic 5/ Angular 12/ Capacitor 3 and Django 3 – Series – Part height

Ok no that we learned how to develop and deploy on application on a device, it’s time to build our main application goal : Chatting with users.

Day 8 : Searching users, creating / displaying chats

Search user

It is obvious that to chat with other Users we need to be able to find them. So we will implement a searchbar functionality on our application main page a.k.a HomePage .

Let’s edit our src/app/pages/home-page.page.html file to add the search bar:

<ion-header>
  <ion-toolbar>
    <ion-title>ChatTuto</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
   <ion-searchbar placeholder="Search users" inputmode="text" type="text" 
    (ionChange)="onSearchChange($event)" 
    [debounce]="250">
  </ion-searchbar>

</ion-content>

Each time the user will enter a caracter the event ionChange will be launched and the method onSearchChange will be called with the event value. The debounce instruction will wait a little (250 milliseconds) before calling our method. This will avoid intempestive calls while the user is typing.

In the src/app/pages/home-page.page.ts file, we can write the method and display the value entered:

onSearchChange(event){
    console.log(event.detail.value)
}

For rapid and simple development, i usually develop and test using ionic serve command, and used Ionic hot live deploy when i really want to test on a simulator or a device. It’s up to you. Both methods are valid.

Now we will ask our backend to send us all the users existing in the system, having first or last name containing the text searched. Let’s continue to modify our HomePage:

import { Component, OnInit } from '@angular/core';
import { SplashScreen } from '@capacitor/splash-screen';
import { User } from 'src/app/models/user';
import { ApiserviceService } from 'src/app/services/api-service.service';
@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.page.html',
  styleUrls: ['./home-page.page.scss'],
})
export class HomePagePage implements OnInit {

  listOfUser : User[] = []
  isSearching = false
  constructor(public apiService:ApiserviceService) { }

  ngOnInit() {
  }

  ngAfterViewInit() {
    SplashScreen.hide()
  }

  onSearchChange(event){
    console.log(event.detail.value)
    if (this.isSearching==false){
      if (this.apiService.networkConnected){
        this.isSearching = true
      }
      else{
        this.apiService.showNoNetwork()
      }
    }
  }
}

First we add a isSearching variable to know that we already asked the backend for searching users and avoid launching several requests… Then we check if the network is available to search, otherwise we display a no network message.

Now let’s show a loader and launch a search on our backend:

 onSearchChange(event){
    console.log(event.detail.value)
    if (this.isSearching==false){
      if (this.apiService.networkConnected){
        this.isSearching = true
        this.apiService.showLoading().then(()=>{
          this.apiService.searchUser(event.detail.value).subscribe((results)=>{
            this.apiService.stopLoading()
            this.isSearching = false
          })
        })
      }
      else{
        this.apiService.showNoNetwork()
      }
    }
  }

The apiService.searchUser method doesn’t exist yet and we need to create it.

We have a findUserWithQuery methods in our ApiService but please remember that for security issues (avoiding showing other users data), this method is setup in Django to only returns your current user application. Remember the code we did:

# Filter for connected user
def get_queryset(self):
  user = self.request.user
  queryset = User.objects.filter(pk=user.id)
  return queryset

So we need to create a new searchUser method on backend, that will search on first and last names and will return filtered data (only the name). Then we will be able to use it into our Ionic application.

SearchUser API in Django

First we will edit our api/urls.py file to add the new route pattern:

url(r'^searchUser/$', SearchUserListView.as_view()),

Then we will edit the api/views.py to create the SearchUserListView method:

class SearchUserListView(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticated]
    queryset = User.objects.all()
    serializer_class = SearchUserSerializer
    search_fields = ['first_name','last_name']
    filter_backends = [DjangoFilterBackend,filters.SearchFilter,filters.OrderingFilter]

and create the SearchUserSerializer in the api/serializers.py file:

class SearchUserSerializer(ModelSerializer):
    class Meta:
        ref_name = "SearchUser"
        model = User
        fields = ["id","first_name","last_name"]

In the serializer we only return id, first_name, last_name to avoid security issues and keep compliant with RGPD issues.

Now we can test our new API with a curl method:

curl --location --request GET 'http://localhost:8000/api/searchUser?search=Christ' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjI4NDkzNjI4LCJqdGkiOiI3MDAzMDU0ZWIyY2U0Y2Q0OTUzMDM4Mjk1OTQyOGMyOSIsInVzZXJfaWQiOiIzY2RlM2Y3ZS0yNjFlLTRlYmItODQzNy1mYTRjMjdkMzViZjAifQ.sciuIti7Ce8Bci9lnJ5k7aGWkk67Q66UvPVL292m1qI'

Please remember we need to be authenticated with a JWt access token.

And see the response:

{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
        {
            "first_name": "Christophe",
            "last_name": "Surbier",
            "id":"3cde3f7e-261e-4ebb-8437-fa4c27d35bf0"
        }
    ]
}

Ok nice the API is working and sending back results, except at this time, there is only one user in the backend who is myself and we are not going to chat with ourselves ! So we need to modify our code to filter and exclude our own user from the result list.

Let’s modify our api/views.py file:

class SearchUserListView(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticated]
    queryset = User.objects.all()
    serializer_class = SearchUserSerializer
    search_fields = ['first_name','last_name']
    filter_backends = [DjangoFilterBackend,filters.SearchFilter,filters.OrderingFilter]
    # Filter for connected user
    def get_queryset(self):
        user = self.request.user
        queryset = User.objects.exclude(pk=user.id)
        return queryset

If we call again the API, then the answer will have no data:

{
    "count": 0,
    "next": null,
    "previous": null,
    "results": []
}

This is perfect, except the fact that we would like some tests data for our application.

Let’s fix this.

Generate Fake user data using Django

To generate fake data, we will install the ModelBakery and Faker libraries:

pip install model_bakery
pip install Faker

Then we will create a createFakeData.py file at the root of our chatttuo project:

import os

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chattuto.settings")

import django

django.setup()

from faker import factory, Faker
from chat.models import *
from model_bakery.recipe import Recipe, foreign_key

fake = Faker()

for k in range(100):
    user = Recipe(User,
                  first_name=fake.first_name(),
                  last_name=fake.last_name(),
                  email=fake.email(),
                  is_active=True,
                    createdAt=fake.future_datetime(end_date="+30d", tzinfo=None),
                    updatedAt=fake.future_datetime(end_date="+30d", tzinfo=None), )
    user.make()

The script is quite simple, we ask the Faker library to create 100 users, and we specify which type of fields are our first_name, last_name, … To generate realistic values.

To run the script just launch the command:

python createFakeData.py

Remember to have your environment variables set (database connexion, …)

And voilà, you should have 100 new users in your backend, which you can verify easily with the Django Admin

Search : display user list results

Ok now we go back to our Ionic application and implement the searchUser method.

Let’s add it to our src/app/services/api-service.service.ts file.

First we need to declare our new url :

 this.getSearchUserUrl = this.virtualHostName + this.apiPrefix + "/searchUser/"

and then implement the method:

 searchUser(searchTerm) {
    const options = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      })
    };

    return Observable.create(observer => {
      this.http.get(this.getSearchUserUrl+"?search="+searchTerm, options)
        .pipe(retry(1))
        .subscribe(res => {
          observer.next(res);
          observer.complete();
        }, error => {
          observer.next();
          observer.complete();
          console.log(error);// Error getting the data
        });
    });
  }

Now we can modify our method onSearchChange on HomePage to display the results in the console log:

onSearchChange(event){
    console.log(event.detail.value)
    if (this.isSearching==false){
      if (this.apiService.networkConnected){
        this.isSearching = true
        this.apiService.showLoading().then(()=>{
          this.apiService.searchUser(event.detail.value).subscribe((results)=>{
            this.apiService.stopLoading()
            this.isSearching = false
            console.log(results)
          })
        })
      }
      else{
        this.apiService.showNoNetwork()
      }
    }
  }

In which i could see (for my fake data generated):

{
    "count": 10,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": "60e40b98-527e-409e-89ce-d517af15a0eb",
            "first_name": "David",
            "last_name": "Medina"
        },
        {
            "id": "e326b45e-2560-4e05-a942-6d26319fba7a",
            "first_name": "Amanda",
            "last_name": "Hill"
        },
        {
            "id": "de7b3dde-ab8a-42ee-8b27-7618262fd84a",
            "first_name": "Wendy",
            "last_name": "Davis"
        },
        {
            "id": "4250b946-e0f3-4f5e-b653-2fb96c46fab2",
            "first_name": "Amanda",
            "last_name": "Long"
        },
        {
            "id": "447b8a41-d17d-44d2-b781-1f34b0abb164",
            "first_name": "Brian",
            "last_name": "Adams"
        },
        {
            "id": "a1db0d0a-210b-4b7d-bcc8-9e6d09ebcaed",
            "first_name": "Joseph",
            "last_name": "Daniel"
        },
        {
            "id": "9e311652-7cf6-473d-bf43-4fef39514d81",
            "first_name": "Yolanda",
            "last_name": "Norris"
        },
        {
            "id": "098c54ec-61d3-48c9-abd5-af7f4bbe6460",
            "first_name": "Stephanie",
            "last_name": "Miranda"
        },
        {
            "id": "6e72d0f0-12c2-4001-8f13-c5c450b1c639",
            "first_name": "Michelle",
            "last_name": "Davis"
        },
        {
            "id": "2cf8bcce-0c83-4af9-a27a-146a3e1b50df",
            "first_name": "Caroline",
            "last_name": "Davis"
        }
    ]
}

Obviously values should be different for you since they are randomly generated.

Now to avoid launching a request to the API without something to search (if the user click on the searchbar cancel button), we can check there is a value by verifying the length of the search term.

if (this.isSearching==false && event.detail.value.length>0){
      if (this.apiService.networkConnected){
        this.isSearching = true
        this.apiService.showLoading().then(()=>{
          this.apiService.searchUser(event.detail.value).subscribe((results)=>{
            this.apiService.stopLoading()
            this.isSearching = false
            console.log(results)
          })
        })
      }
      else{
        this.apiService.showNoNetwork()
      }
    }

Finally, we will iterate the result list sended back by the API, and will add each user to our own listOfUser variable:

import { Component, OnInit } from '@angular/core';
import { SplashScreen } from '@capacitor/splash-screen';
import { User } from 'src/app/models/user';
import { ApiserviceService } from 'src/app/services/api-service.service';
@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.page.html',
  styleUrls: ['./home-page.page.scss'],
})
export class HomePagePage implements OnInit {

  listOfUser : User[] = []
  isSearching = false

  constructor(public apiService:ApiserviceService) { }

  ngOnInit() {
  }

  ngAfterViewInit() {
    SplashScreen.hide()
  }

  onSearchChange(event){
    if (this.isSearching==false && event.detail.value.length>0){
      if (this.apiService.networkConnected){
        this.isSearching = true
        this.apiService.showLoading().then(()=>{
          this.apiService.searchUser(event.detail.value).subscribe((results)=>{
            this.apiService.stopLoading()
            this.isSearching = false
            console.log(results)
            let count = results["count"]
            if (count>0){
              let data = results["results"]
              this.listOfUser = []
              for (let item of data){
                this.listOfUser.push(item)
              }
            }
          })
        })
      }
      else{
        this.apiService.showNoNetwork()
      }
    }
    else{
      this.listOfUser = []
    }
  }
}

If nothing is searched, we reset the list to an empty one.

Let’s edit the src/app/pages/home-page/home-page.page.html to display the value of our list:

<ion-header>
  <ion-toolbar>
    <ion-title>ChatTuto</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
   <ion-searchbar placeholder="Search users" inputmode="text" type="text" 
    (ionChange)="onSearchChange($event)" 
    [debounce]="250">
  </ion-searchbar>
  <ion-list mode="ios" no-lines message-list> 
      <ion-item  *ngFor="let item of listOfUser">
        <ion-label>{{item.first_name}} {{item.last_name}}</ion-label>
      </ion-item>
   </ion-list>
</ion-content>

You can try the application and search for something, you should see your results :

SearchResult

Let’s modify the code again to add some style :

<ion-header>
  <ion-toolbar>
    <ion-title>ChatTuto</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
   <ion-searchbar placeholder="Search users" inputmode="text" type="text" 
    (ionChange)="onSearchChange($event)" 
    [debounce]="250">
  </ion-searchbar>
  <ion-list mode="ios" no-lines message-list> 
      <ion-item  *ngFor="let item of listOfUser">
        <div class="image-profile" [ngStyle]="{'background':item.backgroundColor}">

        </div>
        <ion-label class="username">{{item.first_name}} {{item.last_name}}</ion-label>
      </ion-item>
   </ion-list>
</ion-content>

Then edit the src/app/pages/home-page/home-page.page.scss to add our custom css:

.image-profile {
    width: 2rem;
    height: 2rem;
    display: block;
    border-radius: 50%;
    border: 3px solid green;
    margin-right: 10px;
 }

 .username{
     font-weight: bold;
 }

and finally we modify our result list, to add a random color for our user:

 import { Component, OnInit } from '@angular/core';
import { SplashScreen } from '@capacitor/splash-screen';
import { User } from 'src/app/models/user';
import { ApiserviceService } from 'src/app/services/api-service.service';
@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.page.html',
  styleUrls: ['./home-page.page.scss'],
})
export class HomePagePage implements OnInit {

  listOfUser : User[] = []
  isSearching = false
  chatList : any; 

  constructor(public apiService:ApiserviceService) { }

  ngOnInit() {
  }

  ngAfterViewInit() {
    SplashScreen.hide()
  }

  getRandomColor() {
    var color = Math.floor(0x1000000 * Math.random()).toString(16);
    return "#" + ("000000" + color).slice(-6);
  }

  onSearchChange(event){
    if (this.isSearching==false && event.detail.value.length>0){
      if (this.apiService.networkConnected){
        this.isSearching = true
        this.apiService.showLoading().then(()=>{
          this.apiService.searchUser(event.detail.value).subscribe((results)=>{
            this.apiService.stopLoading()
            this.isSearching = false
            console.log(results)
            let count = results["count"]
            if (count>0){
              let data = results["results"]
              this.listOfUser = []
              for (let item of data){
                item["backgroundColor"]= this.getRandomColor()
                this.listOfUser.push(item)
              }
            }
          })
        })
      }
      else{
        this.apiService.showNoNetwork()
      }
    }
    else{
      this.listOfUser = []
    }
  }
}

Now list of results should look like this:

ColorResult

As improvment we could use the avatar image of our Django User model, which means we will need to modify our RegisterPage to let the User choose or take a picture. Let’s keep things simple for now !

Add a button to create a chat

Since we want to chat with a user in the list, we should add a button to do it, let’s add it to our html:

<ion-header>
  <ion-toolbar>
    <ion-title>ChatTuto</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
   <ion-searchbar placeholder="Search users" inputmode="text" type="text" 
    (ionChange)="onSearchChange($event)" 
    [debounce]="250">
  </ion-searchbar>
  <ion-list mode="ios" no-lines message-list> 
      <ion-item  *ngFor="let item of listOfUser">
        <div class="image-profile" [ngStyle]="{'background':item.backgroundColor}">

        </div>
        <ion-label class="username">{{item.first_name}} {{item.last_name}}</ion-label>
        <ion-button slot="end" (click)="createChat(item)" expand="block" fill="clear" shape="round">
          <ion-icon name="chatbubbles-outline"></ion-icon>
        </ion-button>
      </ion-item>
   </ion-list>
</ion-content>

Create a Chat

The method createChat(item) will call our API to create a chat between our current user and the user selected in the list.

Because a chat can already exists between the two users, we will need to verify if it’s already exists and send it back or create a new chat and send it back.

Let’s create this new API endpoint

Create a Chat with Django

First add a new url endpoint into our api/urls.py file:

 url(r'^createChat/$', CreateChatView.as_view()),

Then create the method into our api/views.py file:

class CreateChatView(APIView):
   def post(self, request, format=None):
       try:
           refUser = request.data["refUser"]
           chatWithUser = request.data["chatWithUser"]
           chat, created = Chat.objects.filter(Q(fromUser_id=refUser, toUser_id=chatWithUser) | Q(fromUser_id=chatWithUser, toUser_id=refUser)).get_or_create(fromUser_id=refUser, toUser_id=chatWithUser)
           print("Chat %s created %d"%(chat,created))
           serializer = ChatSerializer(chat)
           return Response(serializer.data, status=status.HTTP_201_CREATED)
       except Exception as e:
           logger.error(e)
           return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)

Don’t forget the correct libraries import:

from django.contrib.auth.models import User
from django.db.models import Q
from rest_framework import generics, permissions
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status

First the method will expect two parameters in a POST request : refUser and chatWithUser

Then the method will check if a chat already exists, otherwise will create it:

chat, created = Chat.objects.filter(Q(fromUser_id=refUser, toUser_id=chatWithUser) | Q(fromUser_id=chatWithUser, toUser_id=refUser)).get_or_create(fromUser_id=refUser, toUser_id=chatWithUser)

Don’t forget the chat can have been created by yourself (refUser) or by the other user (chatWithUser), so we need to make a request with both possibilities (the Q parameters )

Then with our Chat objet, we can create a ChatSerializer and send it back as our API response.

Implement the chat creation from Ionic

As usual, we need to declare the new url:

this.getCreateChatUrl = this.virtualHostName + this.apiPrefix + "/createChat/"

Then we can create the method createChat in our ApiService :

 createChat(refUser,withUser) {
    const options = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      })
    };

    console.log("URL " + this.getCreateChatUrl)

    let params = {
      "refUser":refUser,
      "chatWithUser":withUser
    }

    console.log(params)
    return Observable.create(observer => {
      this.http.post(this.getCreateChatUrl, params, options)
        .pipe(retry(1))
        .subscribe(res => {
          this.networkConnected = true
          observer.next(res);
          observer.complete();
        }, error => {
          observer.next(false);
          observer.complete();
          console.log(error);// Error getting the data
        });
    });

  }

And then we can create our createChat method in our HomePage:

createChat(userToChat){
    if (this.apiService.networkConnected){
      this.apiService.showLoading().then(()=>{
        this.apiService.createChat(this.userManager.currentUser.id,userToChat.id).subscribe((response)=>{
          this.apiService.stopLoading()
          console.log(response)
        })
      })
    }
    else{
      this.apiService.showNoNetwork()
    }
  }

The userManager is injected into our HomePage constructor and contains the current user value :

constructor(public apiService:ApiserviceService,
    public userManager:UserManagerServiceService) { }

And we should see the chat created or retrieved if already existing in our javascript console log:

{
    "id": "26fd40d1-fdf1-42bc-a7d9-d6b11f5d3654",
    "createdAt": "2021-08-09T14:20:44.765814Z",
    "updatedAt": "2021-08-09T14:20:44.765882Z",
    "fromUser": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
    "toUser": "fffee82d-924a-4da4-b407-4784dbaf1005"
}

As this step, we should go to a chat with user page which doesn’t exist yet. We will see this soon.

Displaying chat list

But first let’s focus on chat list. Now that we are able to search for a user and create a chat with this user, it should be useful to display current chat list on our HomePage !

To have more than one chat in the list, we can use our application to create multiple chat with multiple users.

Managing Chat API in Django backend

In the API, we already have an endpoint to get existing chats:

curl --location --request GET 'http://localhost:8000/api/chat/' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjI4NTgwNTEwLCJqdGkiOiI0YjRhYzBmNGUxNGI0MzZlYjA5NWJkMzFkODg5NDUyZiIsInVzZXJfaWQiOiIzY2RlM2Y3ZS0yNjFlLTRlYmItODQzNy1mYTRjMjdkMzViZjAifQ.nE7fkZw0rt1xw0o945wxYo8XWoDvULKhMFhZjgmtDEs'

Response:

{
    "count": 6,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": "53713a91-e936-4169-9fef-4bc870a0babc",
            "createdAt": "2021-03-08T08:11:30.677821Z",
            "updatedAt": "2021-03-08T08:11:30.677921Z",
            "fromUser": "9052a080-24d8-4a0e-8d9a-742213c0bf91",
            "toUser": "b4db6ca7-21e0-4c83-b56b-f5c86c4cbc43"
        },
        {
            "id": "26fd40d1-fdf1-42bc-a7d9-d6b11f5d3654",
            "createdAt": "2021-08-09T14:20:44.765814Z",
            "updatedAt": "2021-08-09T14:20:44.765882Z",
            "fromUser": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
            "toUser": "fffee82d-924a-4da4-b407-4784dbaf1005"
        },
        {
            "id": "467229f1-2033-4879-adab-d9272da8b43a",
            "createdAt": "2021-08-09T14:37:07.916113Z",
            "updatedAt": "2021-08-09T14:37:07.916162Z",
            "fromUser": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
            "toUser": "60e40b98-527e-409e-89ce-d517af15a0eb"
        },
        {
            "id": "71fd852a-d8eb-4b2a-b5d1-d95f19762852",
            "createdAt": "2021-08-10T06:58:41.563718Z",
            "updatedAt": "2021-08-10T06:58:41.563831Z",
            "fromUser": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
            "toUser": "aa9ea63b-c3e9-41d3-9d54-68d09b87f634"
        },
        {
            "id": "2a43948a-3842-4db3-9692-978c22b3d6ff",
            "createdAt": "2021-08-10T06:58:44.301552Z",
            "updatedAt": "2021-08-10T06:58:44.301660Z",
            "fromUser": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
            "toUser": "a1db0d0a-210b-4b7d-bcc8-9e6d09ebcaed"
        },
        {
            "id": "33e21813-e5ed-47c3-a1a7-056251fa3ddf",
            "createdAt": "2021-08-10T06:58:46.357281Z",
            "updatedAt": "2021-08-10T06:58:46.357388Z",
            "fromUser": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
            "toUser": "be61aeae-768e-47e2-b99b-9e11c730e633"
        }
    ]
}

But this API returns all existing chats not only those involving our current user application. We need to modify the code to filter data.

Let’s edit the api/views.py file :

class ChatListView(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticated]
    queryset = Chat.objects.all()
    serializer_class = ChatSerializer
    filterset_fields = ['id']
    filter_backends = [DjangoFilterBackend,filters.SearchFilter,filters.OrderingFilter]

    def get_queryset(self):
        user = self.request.user
        queryset = Chat.objects.filter(Q(fromUser_id=user.id)|Q(toUser_id=user.id)).select_related("fromUser").select_related("toUser")
        return queryset

We added the get_queryset method, to override the default queryset, and we filter the list with the fromUser or toUser fields having our current user id.

Now the list is filtered for our user. But the information returns in the JSON are not so useful. We just have user identifiers. Let’s modify the ChatSerializer to add more relevant information about the users.

Edit the api/serializers.py file:

class ChatUserSerializer(ModelSerializer):
    class Meta:
        model = User
        fields = ['id','first_name','last_name']

class ChatSerializer(ModelSerializer):
    fromUser = ChatUserSerializer()
    toUser = ChatUserSerializer()
    class Meta:
        model = Chat
        fields = '__all__'

First we create a new ChatUserSerializer with restricted information (we just want id, first name and last name).

We don’t use the existing UserSerializer to avoid returns all user fields information, which could be a security issue.

Then we modify the ChatSerializer, to specify that the fields fromUser, toUser needs to use the new ChatUserSerializer.
Now if we look at our API call, it is much more better:

{
    "count": 5,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": "26fd40d1-fdf1-42bc-a7d9-d6b11f5d3654",
            "fromUser": {
                "id": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
                "first_name": "Christophe",
                "last_name": "Surbier"
            },
            "toUser": {
                "id": "fffee82d-924a-4da4-b407-4784dbaf1005",
                "first_name": "Julie",
                "last_name": "Wright"
            },
            "createdAt": "2021-08-09T14:20:44.765814Z",
            "updatedAt": "2021-08-09T14:20:44.765882Z"
        },
        {
            "id": "467229f1-2033-4879-adab-d9272da8b43a",
            "fromUser": {
                "id": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
                "first_name": "Christophe",
                "last_name": "Surbier"
            },
            "toUser": {
                "id": "60e40b98-527e-409e-89ce-d517af15a0eb",
                "first_name": "David",
                "last_name": "Medina"
            },
            "createdAt": "2021-08-09T14:37:07.916113Z",
            "updatedAt": "2021-08-09T14:37:07.916162Z"
        },
        {
            "id": "71fd852a-d8eb-4b2a-b5d1-d95f19762852",
            "fromUser": {
                "id": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
                "first_name": "Christophe",
                "last_name": "Surbier"
            },
            "toUser": {
                "id": "aa9ea63b-c3e9-41d3-9d54-68d09b87f634",
                "first_name": "Paul",
                "last_name": "Walker"
            },
            "createdAt": "2021-08-10T06:58:41.563718Z",
            "updatedAt": "2021-08-10T06:58:41.563831Z"
        },
        {
            "id": "2a43948a-3842-4db3-9692-978c22b3d6ff",
            "fromUser": {
                "id": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
                "first_name": "Christophe",
                "last_name": "Surbier"
            },
            "toUser": {
                "id": "a1db0d0a-210b-4b7d-bcc8-9e6d09ebcaed",
                "first_name": "Joseph",
                "last_name": "Daniel"
            },
            "createdAt": "2021-08-10T06:58:44.301552Z",
            "updatedAt": "2021-08-10T06:58:44.301660Z"
        },
        {
            "id": "33e21813-e5ed-47c3-a1a7-056251fa3ddf",
            "fromUser": {
                "id": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
                "first_name": "Christophe",
                "last_name": "Surbier"
            },
            "toUser": {
                "id": "be61aeae-768e-47e2-b99b-9e11c730e633",
                "first_name": "Jennifer",
                "last_name": "Patrick"
            },
            "createdAt": "2021-08-10T06:58:46.357281Z",
            "updatedAt": "2021-08-10T06:58:46.357388Z"
        }
    ]
}

We have our user information in our toUser or fromUser dictionnaries.

Final step, usually in Chat application, it is usual to display the last message sent in the chat.
We need to find and add this information into our JSON.

Let’s again modify our ChatSerializer to include this last message information:

class ChatSerializer(ModelSerializer):
    fromUser = ChatUserSerializer()
    toUser = ChatUserSerializer()
    lastMessage = serializers.SerializerMethodField()

    class Meta:
        model = Chat
        fields = '__all__'

    def get_lastMessage(self, obj):
        # get last message between users
        try:
            messages = Message.objects.filter(refChat=obj.id).select_related("refChat").select_related("author").order_by('-createdAt')[:1]
            return MessageSerializer(messages,many=True).data
        except Exception as e:
            print(e)
            return None

We add a lastMessage field:

lastMessage = serializers.SerializerMethodField()

and we write the method to get the last message for this chat:

def get_lastMessage(self, obj):
       # get last message between users
       try:
           messages = Message.objects.filter(refChat=obj.id).select_related("refChat").select_related("author").order_by('-createdAt')[:1]
           return MessageSerializer(messages,many=True).data
       except Exception as e:
           print(e)
           return None

To test our code, we need to add a message to an existing chat. Because we can’t do this yet with our Ionic application, we can use the Django Admin and then check in our return API json that we have the lastMessage information:

 {
            "id": "33e21813-e5ed-47c3-a1a7-056251fa3ddf",
            "fromUser": {
                "id": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
                "first_name": "Christophe",
                "last_name": "Surbier"
            },
            "toUser": {
                "id": "be61aeae-768e-47e2-b99b-9e11c730e633",
                "first_name": "Jennifer",
                "last_name": "Patrick"
            },
            "lastMessage": [
                {
                    "id": "c6c14089-b8ae-40c6-b5a1-eec11eb6bc7d",
                    "message": "my last message",
                    "type": 0,
                    "extraData": null,
                    "isRead": false,
                    "createdAt": "2021-08-10T07:26:56.960389Z",
                    "updatedAt": "2021-08-10T07:26:56.960470Z",
                    "refChat": "33e21813-e5ed-47c3-a1a7-056251fa3ddf",
                    "author": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0"
                }
            ],
            "createdAt": "2021-08-10T06:58:46.357281Z",
            "updatedAt": "2021-08-10T06:58:46.357388Z"
        }

Ok now with all that backend code ready, we can focus on the Ionic application to display the existing chat list.

Managing chat list in Ionic

Let’s first add our new API in our api-service.service.ts file

 this.getChatUrl = this.virtualHostName + this.apiPrefix + "/chat/"

and create a getChat method:

getChats() {
    const options = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json'
      })
    };
    return Observable.create(observer => {
      this.http.get(this.getChatUrl, options)
        .pipe(retry(1))
        .subscribe(res => {
          this.networkConnected = true
          observer.next(res);
          observer.complete();
        }, error => {
          observer.next(false);
          observer.complete();
          console.log(error);// Error getting the data
        });
    });
  }

Then we will edit our HomePage to load our chat list

export class HomePagePage implements OnInit {

  listOfUser : User[] = []
  isSearching = false
  chatList : any; 

  constructor(public apiService:ApiserviceService,
    public userManager:UserManagerServiceService) {
      this.loadExistingChat()
  }

The method for loading chat will check the network and then call our new API method:

loadExistingChat(){
    if (this.apiService.networkConnected){
      this.isSearching = true

        this.apiService.getChats().subscribe((list)=>{
          console.log(list)
        })
    }
  }

The list variable should contains our list (if exists). We can check the count value in the returned JSON and iterate it if we have values, to put chats in our chatList array:

 loadExistingChat(){
    if (this.apiService.networkConnected){
      this.isSearching = true

        this.chatList = []
        this.apiService.getChats().subscribe((list)=>{

          if (list){
            let count = list["count"]
            if (count>0){
              //Iterate existing chat
              for (let aChat of list["results"]){
                console.log(aChat)
                this.chatList.push(aChat)
              }
            }
          }

      })
    }
  }

And see in our javascript console logs:

{id: "467229f1-2033-4879-adab-d9272da8b43a", fromUser: {…}, toUser: {…}, lastMessage: Array(0), createdAt: "2021-08-09T14:37:07.916113Z", …}
{id: "71fd852a-d8eb-4b2a-b5d1-d95f19762852", fromUser: {…}, toUser: {…}, lastMessage: Array(0), createdAt: "2021-08-10T06:58:41.563718Z", …}
{id: "2a43948a-3842-4db3-9692-978c22b3d6ff", fromUser: {…}, toUser: {…}, lastMessage: Array(0), createdAt: "2021-08-10T06:58:44.301552Z", …}
{id: "26fd40d1-fdf1-42bc-a7d9-d6b11f5d3654", fromUser: {…}, toUser: {…}, lastMessage: Array(0), createdAt: "2021-08-09T14:20:44.765814Z", …}
{id: "33e21813-e5ed-47c3-a1a7-056251fa3ddf", fromUser: {…}, toUser: {…}, lastMessage: Array(1), createdAt: "2021-08-10T06:58:46.357281Z", …}

Now we need to display the chat but be careful, our current user could be in the fromUser OR toUser variable depending on who created the chat. So we don’t want to display our own user in the list but instead we want to display the other user of the chat. So we need to do modify the code above to take care of this.

And before we will create a new class chat.ts in our models directory to deal with typescript object instead of json:

import { User } from "./user";

export class Chat {
    id:string;
    fromUser:User;
    toUser:User;
    lastMessage : any;
    createdAt : Date;
    updateAt : Date;

    constructor() {

    }

    initWithJSON(json) : Chat{
      for (var key in json) {
          if (key=="fromUser" || key=="toUser"){
            let aUser = new User().initWithJSON(json[key])
            this[key]=aUser
          }
          else {
            this[key] = json[key];
          }
      }
      return this;
    }
}

The initWithJSON method will iterate the json and for keys fromUser, toUser will create a User object too.

We can modifiy our method loadExistingChat to use this class:

loadExistingChat(){
    if (this.apiService.networkConnected){
      this.isSearching = true

        this.chatList = []
        this.apiService.getChats().subscribe((list)=>{
          if (list){
            let count = list["count"]
            if (count>0){
              //Iterate existing chat
              for (let aChat of list["results"]){
                let theChat = new Chat().initWithJSON(aChat)
                console.log(theChat)
                theChat["backgroundColor"]= this.getRandomColor()
                this.chatList.push(theChat)
              }
            }
          }
      })
    }
  }

We also added an extra variable backgroundColor to have same behaviour as our search and display a random color for the user.

Now it’s time to modify our home-page.page.html file to display list:

  <!-- Chat list -->
   <ion-list-header>
    <ion-label>Chats</ion-label>
  </ion-list-header>
   <ion-list mode="ios" no-lines message-list> 
    <ion-item  *ngFor="let chat of chatList">
      <div class="image-profile" [ngStyle]="{'background':chat.backgroundColor}">

      </div>
      <ion-label class="username" *ngIf="chat.fromUser.id!=userManager.currentUser.id">{{chat.fromUser.first_name}} {{chat.fromUser.last_name}}</ion-label>
      <ion-label class="username" *ngIf="chat.toUser.id!=userManager.currentUser.id">{{chat.toUser.first_name}} {{chat.toUser.last_name}}</ion-label>
    </ion-item>
 </ion-list>

We iterate the chatList variable and to know which user to display, we need to check the identifier between our Ionic current user and the fromUser or toUser.

The chat list should look like this:

ChatList

Now we need to display the lastMessage received and the date of the message. Before modifying the Html again, a little remember of the JSON structure for a Chat:

 {
            "id": "33e21813-e5ed-47c3-a1a7-056251fa3ddf",
            "fromUser": {
                "id": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0",
                "first_name": "Christophe",
                "last_name": "Surbier"
            },
            "toUser": {
                "id": "be61aeae-768e-47e2-b99b-9e11c730e633",
                "first_name": "Jennifer",
                "last_name": "Patrick"
            },
            "lastMessage": [
                {
                    "id": "c6c14089-b8ae-40c6-b5a1-eec11eb6bc7d",
                    "message": "mon dernier message",
                    "type": 0,
                    "extraData": null,
                    "isRead": false,
                    "createdAt": "2021-08-10T07:26:56.960389Z",
                    "updatedAt": "2021-08-10T07:26:56.960470Z",
                    "refChat": "33e21813-e5ed-47c3-a1a7-056251fa3ddf",
                    "author": "3cde3f7e-261e-4ebb-8437-fa4c27d35bf0"
                }
            ],
            "createdAt": "2021-08-10T06:58:46.357281Z",
            "updatedAt": "2021-08-10T06:58:46.357388Z"
        }

The lastMessage is an array which contains only one element: the last message received. If there is no message yet the array is empty.

The isRead parameter is used to know if the message has been read or not.

The author parameter is the user identifier of who sent the message.

Based on that here is the modified HTML:

 <!-- Chat list -->
  <ion-list-header>
    <ion-label>Chats</ion-label>
  </ion-list-header>
   <ion-list mode="ios" no-lines message-list> 
    <ion-item  *ngFor="let chat of chatList">
      <div class="image-profile" [ngStyle]="{'background':chat.backgroundColor}">

      </div>
      <ion-label class="username" *ngIf="chat.fromUser.id!=userManager.currentUser.id">
        <ion-row>
          <ion-col size="10">
            <h2>{{chat.fromUser.first_name}} {{chat.fromUser.last_name}}</h2>
            <p *ngIf="chat.lastMessage.length>0">
              {{chat.lastMessage[0].message}} 
            </p>
          </ion-col>
          <ion-col size="2" *ngIf="chat.lastMessage.length>0">
              <span class="timestamp" *ngIf="chat.lastMessage[0].isRead">{{ chat.lastMessage[0].updatedAt | date: 'HH:mm' }}</span>
              <ion-badge *ngIf="!chat.lastMessage[0].isRead" color="primary">
              {{ chat.lastMessage[0].updatedAt | date: 'HH:mm' }}
              </ion-badge>
          </ion-col>
        </ion-row>
      </ion-label>
      <ion-label class="username" *ngIf="chat.toUser.id!=userManager.currentUser.id">
        <ion-row>
          <ion-col size="10">
            <h2>{{chat.toUser.first_name}} {{chat.toUser.last_name}}</h2>
            <p *ngIf="chat.lastMessage.length>0">
              {{chat.lastMessage[0].message}} 
            </p>
          </ion-col>
          <ion-col size="2" *ngIf="chat.lastMessage.length>0">
            <span class="timestamp" *ngIf="chat.lastMessage[0].isRead">{{ chat.lastMessage[0].updatedAt | date: 'HH:mm' }}</span>
            <ion-badge *ngIf="!chat.lastMessage[0].isRead" color="primary">
              {{ chat.lastMessage[0].updatedAt | date: 'HH:mm' }}
            </ion-badge>
          </ion-col>
        </ion-row>
      </ion-label>
    </ion-item>
 </ion-list>

And the result:

ChatWtithMessage

Ok we can improve again the view by showing the last message at the top of the list. To do that, we can order our chat list queryset using an order_by instruction:

class ChatListView(generics.ListCreateAPIView):
    permission_classes = [permissions.IsAuthenticated]
    queryset = Chat.objects.all()
    serializer_class = ChatSerializer
    filterset_fields = ['id']
    filter_backends = [DjangoFilterBackend,filters.SearchFilter,filters.OrderingFilter]

    def get_queryset(self):
        user = self.request.user
        queryset = Chat.objects.filter(Q(fromUser_id=user.id)|Q(toUser_id=user.id)).order_by("-updatedAt").select_related("fromUser").select_related("toUser")
        return queryset

and display the last message in bold and red if not already read. Let’s add our home-page.page.scss to add two variables:

 .messageUnread{
     p{
        font-weight: bold;
        color: red;
     }
 }

 .messageread{
    p{
       font-weight: normal;
       color: black;
    }

Then we modify our loadExistingChat method to check the status of the isRead variable:

loadExistingChat(){
    if (this.apiService.networkConnected){
      this.isSearching = true

        this.chatList = []
        this.apiService.getChats().subscribe((list)=>{
          if (list){
            let count = list["count"]
            if (count>0){
              //Iterate existing chat
              for (let aChat of list["results"]){
                let theChat = new Chat().initWithJSON(aChat)
                console.log(theChat)
                theChat["backgroundColor"]= this.getRandomColor()
                if (theChat.lastMessage.length>0){
                  let lastmessage = theChat.lastMessage[0]
                  let classMessage = 'messageUnread'
                  if (lastmessage.isRead){
                    classMessage="messageread"
                  }
                  theChat["classMessage"]=classMessage
                }
                this.chatList.push(theChat)
              }
            }
          }
      })
    }
  }

Based on the isRead value with set the appropriate CSS class. And then we modify our css to set the correct class to our ion-row element:

  <!-- Chat list -->
   <ion-list-header>
    <ion-label>Chats</ion-label>
  </ion-list-header>
   <ion-list mode="ios" no-lines message-list> 
    <ion-item  *ngFor="let chat of chatList">
      <div class="image-profile" [ngStyle]="{'background':chat.backgroundColor}">

      </div>
      <ion-label class="username" *ngIf="chat.fromUser.id!=userManager.currentUser.id">
        <ion-row [ngClass]="chat.classMessage" >
          <ion-col size="10">
            <h2>{{chat.fromUser.first_name}} {{chat.fromUser.last_name}}</h2>
            <p *ngIf="chat.lastMessage.length>0">
              {{chat.lastMessage[0].message}} 
            </p>
          </ion-col>
           <ion-col size="2" *ngIf="chat.lastMessage.length>0">
              <span class="timestamp" *ngIf="chat.lastMessage[0].isRead">{{ chat.lastMessage[0].updatedAt | date: 'HH:mm' }}</span>
              <ion-badge *ngIf="!chat.lastMessage[0].isRead" color="primary">
              {{ chat.lastMessage[0].updatedAt | date: 'HH:mm' }}
              </ion-badge>
          </ion-col>
        </ion-row>
      </ion-label>
      <ion-label class="username" *ngIf="chat.toUser.id!=userManager.currentUser.id">
        <ion-row [ngClass]="chat.classMessage" >
          <ion-col size="10">
            <h2>{{chat.toUser.first_name}} {{chat.toUser.last_name}}</h2>
            <p *ngIf="chat.lastMessage.length>0">
              {{chat.lastMessage[0].message}} 
            </p>
          </ion-col>
          <ion-col size="2" *ngIf="chat.lastMessage.length>0">
            <span class="timestamp" *ngIf="chat.lastMessage[0].isRead">{{ chat.lastMessage[0].updatedAt | date: 'HH:mm' }}</span>
            <ion-badge *ngIf="!chat.lastMessage[0].isRead" color="primary">
              {{ chat.lastMessage[0].updatedAt | date: 'HH:mm' }}
            </ion-badge>
           </ion-col>
        </ion-row>
      </ion-label>
    </ion-item>
 </ion-list>

And voila, the message and date should be bold and red. To check our code is working, you can use the Django Admin again, find the message and set the isRead value to True. Then you can reload your Ionic page and verify that bold and red color has disappeared.

Of course you can still use the search bar to search users.

Before to conclude, you should have notice that when searching a user and displaying the result list, the search bar is loosing focus and we need to click on it again. This is quite annoying. Let’s fix this:

 <ion-searchbar #searchbar placeholder="Search users" inputmode="text" type="text" 
    (ionChange)="onSearchChange($event)" 
    [debounce]="250">
  </ion-searchbar>

First we modify our searchBar html to tag it : #searchbar

Then from our typescript code, we will get a reference to this searchbar element and at the end of the search, we will set the focus on it again:

export class HomePagePage implements OnInit {
  @ViewChild('searchbar', { static: true }) searchbarElement;

and

 onSearchChange(event) {
    if (this.isSearching == false && event.detail.value.length > 0) {
      if (this.apiService.networkConnected) {
        this.isSearching = true
        this.apiService.showLoading().then(() => {
          console.log("===Call api with ", event.detail.value)
          this.apiService.searchUser(event.detail.value).subscribe((results) => {
            this.apiService.stopLoading()
            this.isSearching = false
            console.log(results)
            let count = results["count"]
            if (count > 0) {
              let data = results["results"]
              this.listOfUser = []
              for (let item of data) {
                item["backgroundColor"] = this.getRandomColor()
                this.listOfUser.push(item)
              }
              //Focus on searchbar again
              this.searchbarElement.setFocus();
            }
          })
        })
      }
      else {
        this.apiService.showNoNetwork()
      }
    }
    else {
      this.listOfUser = []
      this.isSearching = false
    }
  }

The main instruction is :

this.searchbarElement.setFocus();

We will stop here and in next day tutorial, we will learn how to chat with a User.

The source code for this tutorial can be found on my Github

Questions/Answers

  1. In a Django Rest framework serializer, how to specify which fields to include in the response

    Use the fields instruction and list the fields.
    fields = ["id","first_name","last_name"]

  2. How to generate fake data with Django

    Use the Faker library

  3. Which Ionic file should we modify to modify the stylesheet of a page ?

    The scss file of the page

  4. How to add custom fields to a serializers ?

    Add a new field of type serializers.SerializerMethodField() and write the method get_

    myCustomField  = serializers.SerializerMethodField()

    def get_myCustomField(self,obj):
        return "MyValue"
  1. How to get an HTML element reference from typescript ?

    Tag the element in the Html and use the @ViewChild instruction in the typescript code.

Christophe Surbier