Frontend: One-on-one (Duologue) chatting application with Django channels and SvelteKit

sirneij

John Owolabi Idogun

Posted on January 7, 2023

Frontend: One-on-one (Duologue) chatting application with Django channels and SvelteKit

Introduction

This is the second part of a series of tutorials on building a one-on-one (duologue) chatting application with Django channels and SvelteKit. We will focus on building the app's frontend in this part.

NOTE: I won't delve much into the nitty-gritty of SvelteKit as I only intend to show how one can interact with WebSocket in the browser in SvelteKit. I wrote some other tutorials that talk more about it.

Source code

This tutorial's source code can be accessed here:

GitHub logo Sirneij / chatting

Full-stack private chatting application built using Django, Django Channels, and SvelteKit

chatting

chatting is a full-stack private chatting application which uses modern technologies such as PythonDjango and Django channels — and TypeScript/JavaScriptSvelteKit. Its real-time feature utilizes WebSocket.

recording.mp4

chatting has backend and frontend directories. Contrary to its name, backend is a classic full-fledged application, not only backend code. Though not refined yet, you can chat and enjoy real-time conversations there as well. frontend does what it implies. It houses all user-facing codes, written using SvelteKit and TypeScript.

Run locally

To locally run the app, clone this repository and then open two terminals. In one terminal, change directory to backend and in the other, to frontend. For the frontend terminal, you can run the development server using npm run dev:

╭─[Johns-MacBook-Pro] as sirneij in ~/Documents/Devs/chatting/frontend using node v18.11.0                                21:37:36
╰──➤ npm run dev
Enter fullscreen mode Exit fullscreen mode

In the backend terminal, create and activate a virtual…

Implementation

Step 1: Setup a SvelteKit project

In our chatting folder, create a SvelteKit project by issuing the following command in your terminal:

╭─ sirneij in ~/Documents/Devs/chatting on (main)
╰─(ノ˚Д˚)ノ npm create svelte@latest frontend
Enter fullscreen mode Exit fullscreen mode

From the prompts, I chose a skeleton project and added TypeScript support. Follow the instructions given by the command after your project has successfully been created. I added bootstrap (v5) and fontawesome (v6.2.0) to the frontend. I also added routes/+layout.svelte and modified routes/+page.svelte. routes/chat/[username]/+page.svelte was created as well to house the WebSocket logic. Before then, lib/store/message.store.ts has the following content:

import type { Message } from '$lib/types/message.interface';
import { writable, type Writable } from 'svelte/store';

const newMessages = () => {
 const { subscribe, update, set }: Writable<Array<Message>> = writable([]);

 return { subscribe, update, set };
};

const messages = newMessages();

const sendMessage = (message: string, senderUsername: string, socket: WebSocket) => {
 if (socket.readyState <= 1) {
  socket.send(
   JSON.stringify({
    message: message,
    senderUsername: senderUsername
   })
  );
 }
};

export { messages, sendMessage };
Enter fullscreen mode Exit fullscreen mode

This is a custom writable store that exposes a function sendMessage which does exactly what its name implies. It was used in routes/chat/[username]/+page.svelte to send messages to the backend. Let's look at the content of routes/chat/[username]/+page.svelte:

<script lang="ts">
 import type { PageData } from './$types';
 import Contact from '$lib/components/Contacts/Contact.svelte';
 import { session } from '$lib/store/user.store';
 import You from '$lib/components/Message/You.svelte';
 import Other from '$lib/components/Message/Other.svelte';
 import { messages, sendMessage } from '$lib/store/message.store';
 import { browser } from '$app/environment';
 import { page } from '$app/stores';
 import { BASE_URI_DOMAIN } from '$lib/constants';
 import type { Message } from '$lib/types/message.interface';
 export let data: PageData;
 const fullName = `${JSON.parse(data.context.user_object)[0].fields.first_name} ${
  JSON.parse(data.context.user_object)[0].fields.last_name
 }`;
 let messageInput: string, socket: WebSocket;
 if (browser) {
  const websocketUrl = `${
   $page.url.protocol.split(':')[0] === 'http' ? 'ws' : 'wss'
  }://${BASE_URI_DOMAIN}/ws/chat/${JSON.parse(data.context.user_object)[0].pk}/?${
   $session.user.pk
  }`;
  socket = new WebSocket(websocketUrl);
  socket.addEventListener('open', () => {
   console.log('Connection established!');
  });
  socket.addEventListener('message', (event) => {
   const data = JSON.parse(event.data);
   const messageList: Array<Message> = JSON.parse(data.messages).map((message: any) => {
    return {
     message: message.fields.message,
     thread_name: message.fields.thread_name,
     timestamp: message.fields.timestamp,
     sender__pk: message.fields.sender__pk,
     sender__username: message.fields.sender__username,
     sender__last_name: message.fields.sender__last_name,
     sender__first_name: message.fields.sender__first_name,
     sender__email: message.fields.sender__email,
     sender__is_staff: message.fields.sender__is_staff,
     sender__is_active: message.fields.sender__is_active,
     sender__is_superuser: message.fields.sender__is_superuser
    };
   });
   $messages = messageList;
   messageInput = '';
  });
 }
 const handleSendMessage = (event: MouseEvent) => {
  event.preventDefault();
  sendMessage(messageInput, $session.user.username as string, socket);
 };
</script>

<div class="container py-5">
 <div class="row">
  <div class="col-md-6 col-lg-5 col-xl-4 mb-4 mb-md-0 scrollable">
   <h5 class="font-weight-bold mb-3 text-center text-lg-start">
    {$session.user.username}'s contacts
   </h5>
   <Contact contacts={JSON.parse(data.context.users)} />
  </div>
  <div class="col-md-6 col-lg-7 col-xl-8 scrollable" id="message-wrapper">
   <h5 class="font-weight-bold mb-3 text-center text-lg-start">
    {fullName}
   </h5>
   <ul class="list-unstyled" id="chat-body">
    {#each $messages as message, id}
     {#if message.sender__pk === $session.user.pk}
      <You {message} />
     {:else}
      <Other {message} />
     {/if}
    {/each}
   </ul>
   <div
    class="text-muted d-flex justify-content-start align-items-center pe-3 pt-3 mt-2 message-control"
   >
    <img
     src="https://mdbcdn.b-cdn.net/img/Photos/Avatars/avatar-{$session.user.pk}.webp"
     alt="You"
     title="You"
     style="width: 40px; height: 100%"
    />
    <textarea
     placeholder="Type message"
     class="form-control form-control-lg"
     id="message-body"
     rows="1"
     bind:value={messageInput}
    />

    <a
     class="ms-3"
     id="send-message-btn"
     title="Send"
     href={null}
     on:click={(event) => handleSendMessage(event)}
    >
     <i class="fas fa-paper-plane" />
    </a>
   </div>
  </div>
 </div>
</div>
Enter fullscreen mode Exit fullscreen mode

handleSendMessage gets fired whenever a user sends a message. The only thing it does is use the exposed sendMessage function to send the message to the backend. sendMessage takes, among others, the WebSocket. It was initialized with let socket: WebSocket; which was populated with:

...
if (browser) {
  const websocketUrl = `${
   $page.url.protocol.split(':')[0] === 'http' ? 'ws' : 'wss'
  }://${BASE_URI_DOMAIN}/ws/chat/${JSON.parse(data.context.user_object)[0].pk}/?${
   $session.user.pk
  }`;
  socket = new WebSocket(websocketUrl);
  socket.addEventListener('open', () => {
   console.log('Connection established!');
  });
  socket.addEventListener('message', (event) => {
   const data = JSON.parse(event.data);
   const messageList: Array<Message> = JSON.parse(data.messages).map((message: any) => {
    return {
     message: message.fields.message,
     thread_name: message.fields.thread_name,
     timestamp: message.fields.timestamp,
     sender__pk: message.fields.sender__pk,
     sender__username: message.fields.sender__username,
     sender__last_name: message.fields.sender__last_name,
     sender__first_name: message.fields.sender__first_name,
     sender__email: message.fields.sender__email,
     sender__is_staff: message.fields.sender__is_staff,
     sender__is_active: message.fields.sender__is_active,
     sender__is_superuser: message.fields.sender__is_superuser
    };
   });
   $messages = messageList;
   messageInput = '';
  });
 }
...
Enter fullscreen mode Exit fullscreen mode

It must be in the browser block since SvelteKit does Server-side rendering by default which can be turned off but since we ain't turning it off, we must ensure WebSocket is initialized only in the browser since it's a browser-based API.

With this, we are done! Ensure you take a look at the complete code on GitHub.

Outro

Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, healthcare, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn and Twitter.

If you found this article valuable, consider sharing it with your network to help spread the knowledge!

💖 💪 🙅 🚩
sirneij
John Owolabi Idogun

Posted on January 7, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related