How to create live chat project in django using django channels

Feb. 26, 2024


0
26 min read
1.74K

In today's article, we are going to create a chatting web application which will also have a user authentication system. The user will create his account and after logging in, he will see all the users on it and can chat with anyone. If you want, you can change it as per your choice. so let's start it:

Prerequisites

Before diving into the development process, you should have a basic understanding of Python and some experience with web development. Additionally, you should have some understanding of Python and Django and have Docker installed on your system and an IDE on which you can write your code.

We assume that you have Django installed already. You can tell which Django is installed and which version by running the following command in a shell prompt or terminal:

django-admin --version 

If Django is not installed, install it and you can also install Channels and Daphne:

pip install django channels daphne

Once all the requirements are installed, we start the first step of our project. If anything else is required, we will install it there.

Step 1: Create a Django Project & App

Now let us start our project which we will name chat_project.

django-admin startproject chat_project

This will create a new directory called  chat_project that contains the basic structure of your Django project. A Django project can consist of multiple apps, each of which can handle a specific functionality of the website. To create a new app, navigate to the project directory and run the following command:

python manage.py startapp chat_app

This will create a new directory with the same name as your app, containing all the necessary files to run a Django app. Your structure will look like this:

├── chat_app
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── chatbot
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-310.pyc
│   │   └── settings.cpython-310.pyc
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py

Step 2: Add your App and Daphne to the project's settings

Your project directory will have a settings.py file, open it and mention the app & daphne like this:

# Application definition

INSTALLED_APPS = [
    "daphne",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "chat_app",
]

You’ll also need to point Daphne at the root routing configuration. Edit the chat_project/settings.py file again and add the following to the bottom of it:

# WSGI_APPLICATION = "chat_project.wsgi.application"
# Daphne
ASGI_APPLICATION = "chat_project.asgi.application"

With Daphne now in the installed apps, it will take control of the runserver command, replacing the standard Django development server with the ASGI-compatible version.

Let’s ensure that the Channels development server is working correctly. Run the following command:

python manage.py runserver

You will see output like this:

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
February 26, 2024 - 07:18:00
Django version 5.0.2, using settings 'chat_project.settings'
Starting ASGI/Daphne version 4.1.0 development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

Note the line starting with Starting ASGI/Daphne…. This indicates that the Daphne development server has taken over from the Django development server.

We close our server and move on to the next step. In this step, we create our authentication system

Step 3: Making an authentication system

For this, I will create a new app whose name will be user_app.

python manage.py startapp user_app

This will create our app and we will mention it in the installed apps of our project's settings.

# Application definition

INSTALLED_APPS = [
    "daphne",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "chat_app",
    "user_app", # new
]

Now we will create the form required for user authentication. For this, we have to create a file in our user_app named forms.py:

# forms.py

from django import forms
from django.contrib.auth.forms import (
    UserCreationForm,
    AuthenticationForm,
    UsernameField,
)
from django.contrib.auth.models import User
from django.utils.translation import gettext, gettext_lazy as _


# Default user creation form
class SignUpForm(UserCreationForm):
    password1 = forms.CharField(
        label="Password", widget=forms.PasswordInput(attrs={"class": "form-control"})
    )
    password2 = forms.CharField(
        label="Confirm Password (again)",
        widget=forms.PasswordInput(attrs={"class": "form-control"}),
    )

    class Meta:
        model = User
        fields = ["username", "first_name", "last_name", "email"]
        labels = {
            "first_name": "First Name",
            "last_name": "Last Name",
            "email": "Email",
        }
        widgets = {
            "username": forms.TextInput(attrs={"class": "form-control"}),
            "first_name": forms.TextInput(attrs={"class": "form-control"}),
            "last_name": forms.TextInput(attrs={"class": "form-control"}),
            "email": forms.EmailInput(attrs={"class": "form-control"}),
        }


# Default authentication form
class LoginForm(AuthenticationForm):
    username = UsernameField(
        widget=forms.TextInput(attrs={"autofocus": True, "class": "form-control"})
    )
    password = forms.CharField(
        label=_("Password"),
        strip=False,
        widget=forms.PasswordInput(
            attrs={"autocomplete": "current-password", "class": "form-control"}
        ),
    )

To handle these two forms, we will write our views. But we will write three extra views, one for our home page, one for the user's dashboard and one to log out of the user. To write the logic of the views, we will have a file named views.py inside our user_app, and we have to write this code in it:

from django.shortcuts import render, HttpResponseRedirect
from .forms import SignUpForm, LoginForm
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib import messages

# Create your views here.

# Sigup
def user_signup(request):
    if not request.user.is_authenticated:
        if request.method == "POST":
            form = SignUpForm(request.POST)
            if form.is_valid():
                form.save()
                messages.success(
                    request, "Congratulations!! Your account has been created"
                )
                HttpResponseRedirect("/login/")
        else:
            form = SignUpForm()
        return render(request, "signup.html", {"form": form})
    else:
        return HttpResponseRedirect("/")


# Login
def user_login(request):
    if not request.user.is_authenticated:
        if request.method == "POST":
            form = LoginForm(request=request, data=request.POST)
            if form.is_valid():
                uname = form.cleaned_data["username"]
                upass = form.cleaned_data["password"]
                user = authenticate(username=uname, password=upass)
                if user is not None:
                    login(request, user)
                    return HttpResponseRedirect("/")
        else:
            form = LoginForm()
        return render(request, "login.html", {"form": form})
    else:
        return HttpResponseRedirect("/")


# Logout
def user_logout(request):
    logout(request)
    return HttpResponseRedirect("/login/")

In this we have created a function for user login, sign up and log out. We want the user to log in only then he/she can visit our home page.

We have mentioned 2 templates in our views, we will write them all. For this, we have to create a directory called templates within our user_app. And you have to create your template inside it like this tempales/base.html.

base.html

<!DOCTYPE html>
<html lang="en">
{% load static %}
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Django Chat</title>
    <!-- Font Awesome -->
    <link
    href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
    rel="stylesheet"
    />
    <!-- Google Fonts -->
    <link
    href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
    rel="stylesheet"
    />
    <!-- MDB -->
    <link
    href="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/6.2.0/mdb.min.css"
    rel="stylesheet"
    />
</head>
<body>
<style>
    ::-webkit-scrollbar {
    width: 0px;
    height: 0px;
    }
    .avatar_nav{
        border-radius: 50%;
        width: 30px;
        height: 30px;
        color: rgb(255, 255, 255);
        background-color: black;
        display: flex;
        justify-content: center; /* Center horizontally */
        align-items: center; /* Center vertically */
        text-align: center; /* Optional: Center text horizontally if needed */
    }
    .avatar_nav p {
        margin-top: 15px; /* Optionally reset margin to remove any default margin */
    }
</style>

    <!-- Navbar -->
<nav style="background-color: #415abc;" class="navbar navbar-expand-lg navbar-dark">
    <!-- Container wrapper -->
    <div class="container">
      <!-- Navbar brand -->
      <a class="navbar-brand" href="{% url 'home' %}">Chat App</a>
  
      <!-- Toggle button -->
      <button
        class="navbar-toggler"
        type="button"
        data-mdb-toggle="collapse"
        data-mdb-target="#navbarSupportedContent"
        aria-controls="navbarSupportedContent"
        aria-expanded="false"
        aria-label="Toggle navigation"
      >
        <i class="fas fa-bars"></i>
      </button>
  
      <!-- Collapsible wrapper -->
      <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <!-- Left links -->
        <ul class="navbar-nav me-auto mb-2 mb-lg-0">
          <li class="nav-item">
            <a class="nav-link active" aria-current="page" href="{% url 'home' %}">Home</a>
          </li>
          {% if request.user.is_authenticated %}
          <li class="nav-item">
            <a class="nav-link" href="{% url 'logout' %}">logout</a>
          </li>
          {% else %}
          <li class="nav-item">
              <a class="nav-link" href="{% url 'signup' %}">Signup</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="{% url 'login' %}">Login</a>
            </li>
            {% endif %}
        </ul>
        <!-- Left links -->

      </div>

      <div class="d-flex align-items-center">
        <!-- Icon -->
        {% if request.user.is_authenticated %}
        <a class="link-secondary me-3" href="#">
            Hi, {{user.username}}
        </a>  
    </div>
    {% endif %}
      <!-- Right elements -->
       

    </div>
    <!-- Container wrapper -->
  </nav>
  <!-- Navbar -->

  {% if messages %}
  {% for message in messages %}
  <p class="col-3 alert alert-dark mx-4 mt-3">{{message}}</p>
  {% endfor %}
  {% endif %}

  {% block content %}


  {% endblock content %}


 <!-- MDB -->
<!-- MDB -->
<script
  type="text/javascript"
  src="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/7.1.0/mdb.umd.min.js"
></script>
</body>
</html>

This is our base HTML file, we will change it a bit later when we work on the chat.

signup.html

{% extends "base.html" %}
{% load static %}

{% block content %}


<div class="row">
    <div class="col-4"></div>
    <div class="col-4 mt-4 card">
        <h3 style="font-family: Georgia, serif; font-size: 40px;" class="text-center my-5">Signup Page</h3> 
        <form action="" method="post" novalidate>
          {% csrf_token %}
          {% for fm in form %}
           <div class="form-group mb-2">
           {{fm.label_tag}} {{fm}} <small class="text-warning">{{fm.errors|striptags}}</small>
           </div>
          {% endfor %}
          {% if form.non_field_errors %}
          {% for error in form.non_field_errors %}
          <p class="alert alert-danger my-3">{{error}}</p>
          {% endfor %}
          {% endif %}
          <button type="submit" class="btn btn-primary btn-block mt-4">Signup</button>
         </form>

         <!-- Register buttons -->
        <div class="text-center mt-4 mb-4">
            <p>Already registered? <a class="text-primary" href="{% url 'login' %}"><b>Login</b></a></p>
        </div>
    </div>
    <div class="col-4"></div>
</div>
  
{% endblock content %}

This is our signup HTML file contains the signup form

login.html

{% extends "base.html" %}
{% load static %}

{% block content %}


<div class="row">
    <div class="col-4"></div>
    <div class="col-4 mt-4 card">
        <h3 style="font-family: Georgia, serif; font-size: 40px;" class="text-center my-5">Login Page</h3> 
        <form action="" method="post" novalidate>
          {% csrf_token %}
          {% for fm in form %}
           <div class="form-group mb-2">
           {{fm.label_tag}} {{fm}} <small class="text-warning">{{fm.errors|striptags}}</small>
           </div>
          {% endfor %}
          {% if form.non_field_errors %}
          {% for error in form.non_field_errors %}
          <p class="alert alert-danger my-3">{{error}}</p>
          {% endfor %}
          {% endif %}
          <button type="submit" class="btn btn-primary btn-block mt-4">login</button>
         </form>

         <!-- Register buttons -->
        <div class="text-center mt-4 mb-4">
            <p>Not a member? <a class="text-primary" href="{% url 'signup' %}"><b>Register</b></a></p>
        </div>
    </div>
    <div class="col-4"></div>
</div>
  
{% endblock content %}

This is our login HTML file contains the login form.

Now we will create URLs for views For that, we have to create a file named urls.py inside our user_app.

from django.urls import path
from . import views

urlpatterns = [
    path("signup/", views.user_signup, name="signup"),
    path("login", views.user_login, name="login"),
    path("logout", views.user_logout, name="logout"),
]

We will later include these URLs in the URL of our project. Our authentication system is complete, now we move towards our next step.

Step 4: Define Models for chat

Our work starts from here. Now we will define our models. I will keep some basic things in it, you can customize it as per your convenience.

import string
import random
from django.db import models
from django.contrib.auth.models import User

# Create your models here.


def generate_random_string(length):
    random_string = "".join(random.choice(string.ascii_letters) for _ in range(length))
    return random_string


class Room(models.Model):
    token = models.CharField(max_length=255, unique=True)
    users = models.ManyToManyField(User)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    def save(self, *args, **kwargs):
        if not self.token:
            self.token = generate_random_string(20)

        return super(Room, self).save(*args, **kwargs)


class Message(models.Model):
    room = models.ForeignKey(Room, on_delete=models.CASCADE)
    sender = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="sender_user"
    )
    message = models.TextField()
    timestamp = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.sender.username

Here we have created a token for the room and have taken some basic things, you can change it as per your convenience.

Step 5: Migrate the Database

Once you have defined your models, you need to create the database tables to store the data. You can do this by running the following command:

python manage.py makemigrations
python manage.py migrate

You should also create a superuser because we will only allow logged-in users to do this.

python manage.py createsuperuser

Step 6: Create Consumers

This is most important for Django channels. For this, we will create a file named consumers.py inside our chat_app and write the code given below inside it.

import json
from django.shortcuts import get_object_or_404
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
from django.db.models import Q

from .models import Room, Message
from django.contrib.auth.models import User


class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.room_name = self.scope["url_route"]["kwargs"]["room_name"]

        try:
            target_user = get_object_or_404(User, username=self.room_name)
        except:
            target_user = ""

        if target_user is not None:
            users = [target_user, self.scope["user"]]
            room_qs = Room.objects.filter(users=target_user).filter(
                users=self.scope["user"]
            )

            if not room_qs.exists():
                self.room = Room.objects.create()
                self.room.users.set(users)
            else:
                self.room = room_qs.first()

            self.room_group_name = self.room.token

            async_to_sync(self.channel_layer.group_add)(
                self.room_group_name, self.channel_name
            )

            self.accept()

    def disconnect(self, close_code):
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name, self.channel_name
        )

    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json["message"]

        msg = Message.objects.create(
            room=self.room, sender=self.scope["user"], message=message
        )

        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                "type": "chat_message",
                "message": msg.message,
                "sender": msg.sender.username,
                "sender_full_name": msg.sender.get_full_name(),
                "timestamp": msg.timestamp.isoformat(),
            },
        )

    def chat_message(self, event):
        message = event["message"]
        sender = event["sender"]
        sender_full_name = event["sender_full_name"]  # Get sender's full name
        timestamp = event["timestamp"]
        self.send(
            text_data=json.dumps(
                {
                    "message": message,
                    "sender": sender,
                    "sender_full_name": sender_full_name,  # Include sender's full name in the message payload
                    "timestamp": timestamp,
                }
            )
        )

Let me explain you a little about this code. This code is a WebSocket consumer implemented using Django Channels, which enables real-time communication between clients and the server.

Imports: It imports necessary modules and classes from Django and Channels, as well as models from the application.

ChatConsumer Class: This class inherits from WebsocketConsumer, which is provided by Channels for handling WebSocket connections.

Connect Method: This method is called when a client initially connects to the WebSocket. It first extracts the room_name from the URL route. Then it attempts to fetch an User object corresponding to the room_name. If it succeeds, it proceeds to create a room if it doesn't exist already and adds the users to it. Finally, it adds the consumer to a group named after the room's token and accepts the WebSocket connection.

Disconnect Method: This method is called when a WebSocket connection is closed. It removes the consumer from the group associated with the room.

Receive Method: This method is called when the consumer receives a message from the WebSocket. It parses the received JSON data to extract the message content. Then it creates a Message object associated with the room and sender and sends the message to all users in the group associated with the room.

Chat Message Method: This method is responsible for sending chat messages to the connected clients. It receives an event containing message data (such as message content, sender, timestamp, etc.) and sends this data to the client.

Step 7: Create  routing

Routing is necessary to direct incoming requests to the appropriate code handler, allowing the application to effectively respond to client requests and manage communication between different parts of the application. For this, we will create a file named routing.py inside our chat_app. Here's the routing code:

from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi())
]

WebSocket URL Patterns List: This is a list named websocket_urlpatterns, which will contain URL patterns for WebSocket connections.

re_path Function: It is used to define a URL pattern for WebSocket connections. The pattern specified here is ws/chat/(?P<room_name>\w+)/$. Let's break it down:

ws/chat/: This part of the pattern represents the base URL for WebSocket connections.

(?P<room_name>\w+): This is a named capturing group pattern. It captures a room name from the URL and assigns it to the variable room_name. \w+ matches one or more word characters (alphanumeric characters plus underscore).

/$: This indicates the end of the URL.

Consumers.ChatConsumer.as_asgi(): This specifies the consumer class that will handle WebSocket connections for this URL pattern. The .as_asgi() method is used to convert the consumer class into an ASGI application instance, which is required for integration with Django Channels.

Step 8: Add routing to asgi

In ASGI (Asynchronous Server Gateway Interface), routing is typically implemented using a protocol server, such as Daphne for Django Channels. Routing in ASGI involves mapping specific routes or patterns to different ASGI applications or consumers. For this, we have to add a code like this named asgi.py in the directory of our project.

"""
ASGI config for chat_project project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""

import os
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application

import chat_app.routing

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

django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter(
    {
        "http": django_asgi_app,
        "websocket": AllowedHostsOriginValidator(
            AuthMiddlewareStack(URLRouter(chat_app.routing.websocket_urlpatterns))
        ),
    }
)

This ASGI (Asynchronous Server Gateway Interface) configuration file sets up routing for both HTTP and WebSocket protocols using Django Channels. Let's break down what each part does:

Import Statements: Import necessary modules and classes from Django Channels, including ProtocolTypeRouter, URLRouter, AuthMiddlewareStack, and AllowedHostsOriginValidator. Also import get_asgi_application from django.core.asgi and the routing module from the chat_app.

Environment Setup: Set the DJANGO_SETTINGS_MODULE environment variable to point to the Django project's settings module (chat_project.settings in this case).

Get Django ASGI Application: Retrieve the ASGI application for the Django project using get_asgi_application(). This is the application responsible for handling HTTP requests.

ProtocolTypeRouter Configuration: Define the ASGI application routing based on the protocol used:

For HTTP requests ("http"), use the Django ASGI application obtained earlier.

For WebSocket requests ("websocket"), use the AllowedHostsOriginValidator to validate the origin of WebSocket connections. Then, wrap the AuthMiddlewareStack around the URLRouter with WebSocket URL patterns obtained from chat_app.routing.websocket_urlpatterns.

Application Variable: Finally, assign the application variable to the configured ProtocolTypeRouter. This variable represents the overall ASGI application instance that will be used by the ASGI server (such as Daphne) to handle incoming requests.

The work on our channels is almost finished. 

Step 9: Add channel Layers in settings.py

For this, first of all, we have to install the channels_redis package.

pip install channels_redis

 This has to be added in settings.py

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}

Step 10: Create views for chat_app

Before we write the views, let us add the models we have created in our admin.py 

from django.contrib import admin
from .models import Message, Room

# Register your models here.


class MessageAdmin(admin.ModelAdmin):
    list_display = (
        "message",
        "timestamp",
    )
    list_filter = ("room",)
    search_fields = ("room",)


admin.site.register(Room)
admin.site.register(Message, MessageAdmin)

We write our views, For writing our views we will write them in a file named views.py inside our chat_app.

from django.shortcuts import render, get_object_or_404
from django.contrib.auth.models import User
from .models import Message, Room
from django.contrib.auth.decorators import login_required
from django.db.models import Q

# Create your views here.


@login_required(login_url="/login/")
def home(request):
    users = User.objects.exclude(username=request.user)
    context = {
        "users": users,
    }
    return render(request, "home.html", context)


@login_required(login_url="/login/")
def chat(request, room_name):
    user = get_object_or_404(User, username=room_name)
    room = Room.objects.filter(users=user.pk).filter(users=request.user.pk)
    chats = []
    if room.exists():
        chats = Message.objects.filter(room=room.first()).order_by("timestamp")

    context = {
        "chats": chats,
        "room_name": room_name,
        "users": User.objects.exclude(username=request.user),
    }

    return render(request, "chat.html", context)

There is nothing much to explain in this, one is for our home page and the other one is for showing the store chat to the user.

Step 11: Configure URLs for chat_app

URLs define the mapping between the web address and the view that should be displayed. To configure URLs, you have to create a file named urls.py in chat_app and add your URLs inside it.

from django.urls import path
from . import views


urlpatterns = [
    path("", views.home, name="home"),
    path("chat/<str:room_name>/", views.chat, name="chat"),
]

You have to include this urls file in your base urls file which you will find in your project's directory and we will also include the URLs of the user_app

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("user_app.urls")),
    path("", include("chat_app.urls")),
]

Step 12: Create Templates for chat_app

We will create templates. To create this, we will create a folder named templates inside the directory of our chat_app. We will name it home.html and chat.html which we have also mentioned in our views.

home.html

{% extends 'base.html' %}
{% load last_message %}

{% block content %}
<style>
    .scroll-chat{
        overflow: auto;
        height: 69vh;
    }
    .scroll-list{
        overflow: auto;
        height: 76vh;
    }
    .avatar{
        border-radius: 50%;
        width: 60px;
        height: 60px;
        color: rgb(255, 255, 255);
        background-color: black;
        display: flex;
        justify-content: center; /* Center horizontally */
        align-items: center; /* Center vertically */
        text-align: center; /* Optional: Center text horizontally if needed */
    }
    .avatar.black {
        background-color: black; /* Background color for black avatar */
        color: white; /* Text color for black avatar */
    }

    .avatar.white {
        background-color: white; /* Background color for white avatar */
        color: black; /* Text color for white avatar */
    }
    .avatar h2 {
        margin: 0; /* Optionally reset margin to remove any default margin */
    }
    .active_user{
        background-color: #eee;
    }
    .message-box{
        border: 2px solid rgb(11, 90, 175);
    }
    #chat-message-submit {
        /* Hide the button text */
        font-size: 0; /* Set font size to 0 */
        padding: 0; /* Optionally remove padding */
        border: none; /* Optionally remove border */
        background: none; /* Optionally remove background */
    }

    /* Set a non-zero font size for the icon */
    #chat-message-submit i {
        font-size: 16px; /* Adjust the font size as needed */
        margin-right: 8px;
    }
</style>
<section style="background-color: #eee;">
    <div class="container py-5">
  
      <div class="row">
  
        <div class="col-md-6 col-lg-5 col-xl-4 mb-4 mb-md-0">
  
          <h5 class="font-weight-bold mb-3 text-center text-lg-start">Member</h5>
  
          <div class="card">
            <div class="card-body scroll-list">
  
              <ul class="list-unstyled mb-0">
                {% for user in users %}
                <li class="p-2 border-bottom {% if user.username in request.path %}active_user{% endif %}">
                  <a href="{% url 'chat' user %}" class="d-flex justify-content-between">
                    <div class="d-flex flex-row">
                        {% if forloop.counter|divisibleby:2 %}
                            <div class="avatar white me-3 shadow-1-strong">
                                <h2>{{ user.username|first|upper }}</h2>
                            </div>
                        {% else %}
                            <div class="avatar black me-3 shadow-1-strong">
                                <h2>{{ user.username|first|upper }}</h2>
                            </div>
                        {% endif %}                     
                      <div class="pt-1">
                        <p class="fw-bold mb-0">{% if user.get_full_name %}
                            {{ user.get_full_name }}
                        {% else %}
                            {{ user.username }}
                        {% endif %}</p>
                        <p class="small text-muted">{{user|last_message:request.user}}</p>
                      </div>
                    </div>
                  </a>
                </li>
                {% endfor %}
              </ul>
  
            </div>
          </div>
  
        </div>
  
        <div class="col-md-6 col-lg-7 col-xl-8 mt-3">
  
          <ul class="list-unstyled scroll-chat">
           <li>
            <p align="center" class="mt-5 text-primary">
                Please select a User
              </p>
           </li>
        </ul>
        <div class="bg-white mt-5 mb-3">
          <div class="form-outline">
            <form method="post" class="message-box d-flex w-100">
                {% csrf_token %}
                <input type="text" id="chat-message-input" class="form-control" name="" id="">
                <button id="chat-message-submit" type="submit"><i class="fas fa-paper-plane"></i></button>
            </form>
          </div>
        </div>
  
        </div>
  
      </div>
  
    </div>
  </section>

{% endblock content %}

chat.html

{% extends 'base.html' %}
{% load last_message %}
{% block content %}
<style>
    .scroll-chat{
        overflow: auto;
        height: 69vh;
    }
    .scroll-list{
        overflow: auto;
        height: 76vh;
    }
    .avatar{
        border-radius: 50%;
        width: 60px;
        height: 60px;
        color: rgb(255, 255, 255);
        background-color: black;
        display: flex;
        justify-content: center; /* Center horizontally */
        align-items: center; /* Center vertically */
        text-align: center; /* Optional: Center text horizontally if needed */
    }
    .avatar.black {
        background-color: black; /* Background color for black avatar */
        color: white; /* Text color for black avatar */
    }

    .avatar.white {
        background-color: white; /* Background color for white avatar */
        color: black; /* Text color for white avatar */
    }
    .avatar h2 {
        margin: 0; /* Optionally reset margin to remove any default margin */
    }
    .active_user{
        background-color: #eee;
    }
    .message-box{
        border: 2px solid rgb(11, 90, 175);
    }
    #chat-message-submit {
        /* Hide the button text */
        font-size: 0; /* Set font size to 0 */
        padding: 0; /* Optionally remove padding */
        border: none; /* Optionally remove border */
        background: none; /* Optionally remove background */
    }

    /* Set a non-zero font size for the icon */
    #chat-message-submit i {
        font-size: 16px; /* Adjust the font size as needed */
        margin-right: 8px;
    }
    
</style>
<section style="background-color: #eee;">
    <div class="container py-5">
  
      <div class="row">
  
        <div class="col-md-6 col-lg-5 col-xl-4 mb-4 mb-md-0">
  
          <h5 class="font-weight-bold mb-3 text-center text-lg-start">Member</h5>
  
          <div class="card">
            <div class="card-body scroll-list">
  
              <ul class="list-unstyled mb-0">
                {% for user in users %}
                <li class="p-2 border-bottom {% if user.username in request.path %}active_user{% endif %}">
                  <a href="{% url 'chat' user %}" class="d-flex justify-content-between">
                    <div class="d-flex flex-row">
                        {% if forloop.counter|divisibleby:2 %}
                            <div class="avatar white me-3 shadow-1-strong">
                                <h2>{{ user.username|first|upper }}</h2>
                            </div>
                        {% else %}
                            <div class="avatar black me-3 shadow-1-strong">
                                <h2>{{ user.username|first|upper }}</h2>
                            </div>
                        {% endif %}                     
                      <div class="pt-1">
                        <p class="fw-bold mb-0">{% if user.get_full_name %}
                            {{ user.get_full_name }}
                        {% else %}
                            {{ user.username }}
                        {% endif %}</p>
                        <p class="small text-muted">{{user|last_message:request.user}}</p>
                      </div>
                    </div>
                  </a>
                </li>
                {% endfor %}
              </ul>
  
            </div>
          </div>
  
        </div>
  
        <div class="col-md-6 col-lg-7 col-xl-8 mt-3">
  
          <ul id="chat-log" class="list-unstyled scroll-chat">
            {% for chat in chats %}
            {% if chat.sender == request.user %}
            <div class="d-flex justify-content-between">
                <p class="small mb-1 text-muted"></p>
                <p class="small mb-1">{{chat.sender.get_full_name}}</p>
              </div>
              <div class="d-flex flex-row justify-content-end mb-4 pt-1">
                <div>
                  <p class="small p-2 me-3 mb-3 text-white rounded-3 bg-warning">
                    {{chat.message}} <br>
                    <small style="font-size: 9px;">{{chat.timestamp}}</small>
                  </p>
                </div>
                <img src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava6-bg.webp"
                  alt="avatar 1" style="width: 45px; height: 100%;">
              </div>
              {% else %}
              <div class="d-flex justify-content-between">
                <p class="small mb-1">{{chat.sender.get_full_name}}</p>
              </div>
              <div class="d-flex flex-row justify-content-start">
                <img src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava5-bg.webp"
                  alt="avatar 1" style="width: 45px; height: 100%;">
                <div>
                  <p title="{{chat.timestamp}}" class="small p-2 ms-3 mb-3 rounded-3" style="background-color: #f5f6f7;">
                    {{chat.message}} <br>
                    <small style="font-size: 9px;">{{chat.timestamp}}</small>
                </p>
                </div>
              </div>
              {% endif %}
              {% endfor %}
        </ul>
        <div class="bg-white mt-5 mb-3">
          <div class="form-outline">
            <form method="post" class="message-box d-flex w-100">
                {% csrf_token %}
                <input type="text" id="chat-message-input" class="form-control" name="" id="">
                <button id="chat-message-submit" type="submit"><i class="fas fa-paper-plane"></i></button>
            </form>
          </div>
        </div>
  
        </div>
  
      </div>
  
    </div>
  </section>


  <script>

    let chatLog = document.getElementById('chat-log');
    let messageInput = document.getElementById('chat-message-input');
    let messageSubmit = document.getElementById('chat-message-submit');
    let lastMessage = document.getElementById('last-message');
    let roomName = "{{room_name}}";
    let user = "{{request.user.username}}"
  
  
    const chatSocket = new WebSocket(
      `ws://${window.location.host}/ws/chat/${roomName}/`
    )
  
    chatSocket.onmessage = e => {
  const data = JSON.parse(e.data);
  var html = '';
 
  function formatTimestamp(timestamp) {
    const months = ['Jan.', 'Feb.', 'Mar.', 'Apr.', 'May', 'Jun.', 'Jul.', 'Aug.', 'Sep.', 'Oct.', 'Nov.', 'Dec.'];
    
    const date = new Date(timestamp);
    const month = months[date.getMonth()];
    const day = date.getDate();
    const year = date.getFullYear();
    let hour = date.getHours();
    const minute = date.getMinutes();
    const period = hour < 12 ? 'a.m.' : 'p.m.';

    // Convert hour to 12-hour format
    if (hour > 12) {
        hour -= 12;
    }

    // Pad single digit minute with leading zero
    const formattedMinute = minute < 10 ? '0' + minute : minute;

    return `${month} ${day}, ${year}, ${hour}:${formattedMinute} ${period}`;
}
const timestamp = data.timestamp; // Example timestamp
const formattedTimestamp = formatTimestamp(timestamp);
console.log(formattedTimestamp);

  
  if (data.sender === user) {
    // Message from the requested user
    console.log(e.data)
    html = `
    <div class="d-flex justify-content-between">
                <p class="small mb-1 text-muted"></p>
                <p class="small mb-1">${data.sender_full_name}</p>
              </div>
              <div class="d-flex flex-row justify-content-end mb-4 pt-1">
                <div>
                  <p class="small p-2 me-3 mb-3 text-white rounded-3 bg-warning">
                    ${data.message} ${data}<br>
                    <small style="font-size: 9px;">${data.timestamp}</small>
                  </p>
                </div>
            <img src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava6-bg.webp"
                  alt="avatar 1" style="width: 45px; height: 100%;">
    </div>
    `;
  } else {
    // Message from other users
    html = `
    <div class="d-flex justify-content-between">
                <p class="small mb-1">${data.sender_full_name}</p>
              </div>
              <div class="d-flex flex-row justify-content-start">
                <img src="https://mdbcdn.b-cdn.net/img/Photos/new-templates/bootstrap-chat/ava5-bg.webp"
                  alt="avatar 1" style="width: 45px; height: 100%;">
                <div>
                  <p title="${data.timestamp}" class="small p-2 ms-3 mb-3 rounded-3" style="background-color: #f5f6f7;">
                    ${data.message} <br>
                    <small style="font-size: 9px;">${formattedTimestamp}</small>
                </p>
                </div>
              </div>
    `;
  }
  
  chatLog.insertAdjacentHTML("beforeend", html);
  messageBody.scrollTop = messageBody.scrollHeight; // Scroll to the bottom
  lastMessage.textContent = data.message;
};
  
    chatSocket.onclose = e => {
      console.error('Chat socket closed unexpectedly');
    }
  
    messageSubmit.disabled = true;
    messageInput.addEventListener('input', function() {
      if (messageInput.value.trim() !== '') {
        messageSubmit.disabled = false;
      } else {
        messageSubmit.disabled = true;
      }
    })
  
    messageInput.focus();
    messageSubmit.onclick = e => {
      const message = messageInput.value;
      chatSocket.send(JSON.stringify({
        'message': message
      }));
      messageInput.value = '';
    }
  
  
    var messageBody = document.querySelector('.scroll-chat');
        messageBody.scrollTop = messageBody.scrollHeight - messageBody.clientHeight;
        document.body.scrollTop = document.documentElement.scrollTop = 0;
  </script>

{% endblock content %}

You must understand the code of HTML, I will tell you a little about the code of JavaScript. This code is a JavaScript snippet designed to handle WebSocket communication and manipulate the DOM (Document Object Model) for a chat application. Let's break it down step by step:

Element Selection: The code starts by selecting various DOM elements using getElementById. These elements include the chat log (chat-log), the message input field (chat-message-input), the message submit button (chat-message-submit), a placeholder for the last message (last-message), and variables for the current room name and user.

WebSocket Connection: It establishes a WebSocket connection to the server using the WebSocket constructor. The URL for the connection is dynamically generated based on the current window's host and the room name obtained from the Django template.

WebSocket Event Handlers:

  • onmessage: This event handler is triggered when the WebSocket receives a message from the server. It parses the received JSON data and formats it into HTML for display in the chat log. It distinguishes between messages from the current user and messages from other users, displaying them differently. The formatted HTML is then appended to the chat log.
  • onclose: This event handler is triggered if the WebSocket connection is closed unexpectedly. It logs an error message to the console.

Message Input Handling: It disables the message submit button if the message input field is empty. It enables the button as soon as the user starts typing a message.

Message Submission: When the message submit button is clicked, it retrieves the message from the input field, sends it to the server as a JSON string via the WebSocket connection, and then clears the input field.

Scrolling: It ensures that the chat log and the document body are scrolled to the bottom to display the latest messages.

Step 13: Create a template tag to get the last messages

We will create a template tag for the last message. First of all, we have to create a directory called templatetags. IInside this, we have to create two files, one __init__.py and the second one is last_message.py __init__.py is empty. It says it's a package. So we write our code in our last_message.py

from django import template
from chat_app.models import Room, Message

register = template.Library()


@register.filter(name="last_message")
def get_last_message(user, recipient):
    room = Room.objects.filter(users=user).filter(users=recipient)
    msg = Message.objects.filter(room=room.first())
    if msg.exists():
        return msg.last().message
    else:
        return "Last message goes here"

All our work is complete, now we start our server, before that, we will create a Docker container in the docker we had installed. 

Step 14: Enable a channel layer

A channel layer is a kind of communication system. It allows multiple consumer instances to talk with each other, and with other parts of Django.

A channel layer provides the following abstractions:

  • channel is a mailbox where messages can be sent. Each channel has a name. Anyone who has the name of a channel can send a message to the channel.
  • group is a group of related channels. A group has a name. Anyone who has the name of a group can add/remove a channel to the group by name and send a message to all channels in the group. It is not possible to enumerate what channels are in a particular group.

Every consumer instance has an automatically generated unique channel name, and so can be communicated with via a channel layer.

In our chat application, we want to have multiple instances  ChatConsumer in the same room communicate with each other. To do that we will have each ChatConsumer add its channel to a group whose name is based on the room name. That will allow ChatConsumers to transmit messages to all other ChatConsumers in the same room.

We will use a channel layer that uses Redis as its backing store. To start a Redis server on port 6379, run the following command:

docker run -p 6379:6379 -d redis:5

You'll see something like this.

Our project is now ready. To start the Channels development server, run the following command:

python manage.py runserver

If you are facing any problems regarding this project, then you can comment or message me directly. 

Conclusion

Building a real-time chat application with Django Channels and WebSocket opens up a world of possibilities for creating engaging and interactive web applications. With the power of asynchronous communication, users can communicate seamlessly in real time, making the application more dynamic and responsive.

As you continue to work on your chat application, consider adding additional features such as user authentication, message moderation, file sharing, and notifications to enhance the user experience further. Additionally, ensure to optimize the application for scalability and performance, especially as the user base grows.

With the knowledge gained from this tutorial, you're well-equipped to develop real-time applications and explore the exciting realm of WebSocket-based communication. Happy coding!

django Project chat django-channels websocket live-chat Appreciate you stopping by my post! 😊

Add a comment


Note: If you use these tags, write your text inside the HTML tag.
Login Required
Author's profile
Profile Image

Abdulla Fajal

Django Developer

With 'espere.in' under my care, I, Abdulla Fajal, graciously invite your insights and suggestions, as we endeavour to craft an exquisite online experience together.