A real-time multi room webchat that accepts Markdown syntax in messages sent by users.
- 1. Motivations
- 2. Main Features
- 3. Architectural Detailing
- 3.1. Back-End
- 3.2. Front-End
- 3.2.1. How to Run
- 3.2.2. Main Technologies
- 3.2.3. Details About the Code
- 3.2.4. Short Captures of Key Features
- 3.2.4.1. Real-time communication using WebSockets
- 3.2.4.2. It's necessary to inform a username and the name of the room
- 3.2.4.3. Avoid user enter with username in use
- 3.2.4.4. Display a list of all logged users at room
- 3.2.4.5. Show message when user leaves or enters room
- 3.2.4.6. Users list can be hidden/unhidden
- 3.2.4.7. Format text with Markdown
- 3.2.4.8. Messages with multiple lines
- 3.2.4.9. Ctrl+Enter or button Send to send messages
- 3.2.4.10. Responsive "mobile-first" design
The main motivation of this project was to understand how WebSocket could add value to a web application with Django by creating a real-time communication channel with the presentation layer.
As a result, the presentation layer was separated from Django and implemented as a SPA (Single Page Application) to demonstrate how this kind of real-time communication takes place in this architectural model.
Developed with Python + Django, it basically provides the WebSocket connection service and a REST endpoint for queries. The WebSocket service is implemented through the "Channels" library for Django and the REST endpoint is provided by native Django features.
An SQLite3 database was used as a means to persist and manage the list of connected users.
In general terms, it is a Javascript SPA (Single Page Application) that uses the React JS library.
Instead of starting with a Javascript application "from scratch", the presentation layer was built using the so-called "create-react-app" boilerplate, which adds advanced features to the project and some enablers that allow speed gain of development.
- Real-time communication using WebSockets.
- To access a room it is necessary to inform a username and the name of the room.
- A room cannot contain more than one user with the same name.
- The room should display a list of all users who are logged in there.
- When a new user enters or leaves the room, a message is displayed on the timeline.
- The room user list can be hidden and unhidden.
- Users can format text using Markdown syntax.
- Text input of messages allows writing on multiple lines.
- Users can send the message Ctrl+Enter or by clicking the Send button.
- Responsive design following the "mobile-first" paradigm.
First of all you have to install python3 and pip to be able to run this project and install all of the dependencies.
You'll find instructions to install python3 on your system at https://www.python.org/download/releases/3.0/ and pip at https://pip.pypa.io/en/stable/installation/.
I'm using venv to virtualize my Python environment, but you're free to make your choice.
To install all of the dependencies you just have to run the following command using pip and the requirements.txt file that contains all libs used by this project.
# pip install -r /path/to/requirements.txtFor security reasons I'm using .env files to the save secret key during programming time and the library that provides access to environment variables is python-decouple. You can rename file .env.template to .env and then put your security key inside and your Django app will be able to read it.
The next step is create the SQLite database and its entities using Django Migrations.
To do this, just run the following commands on your terminal inside the folder markdown-chat/back/markdownchat/:
# python3 manage.py makemigrations chat
# python3 manage.py migrateRight now you'll be able to run the server to get your app ready to receive new connections.
# python3 manage.py runserverAfter that you'll see some outputs on your terminal similar to this:
System check identified no issues (0 silenced).
August 21, 2021 - 23:11:22
Django version 3.2.5, using settings 'markdownchat.settings'
Starting ASGI/Channels version 3.0.4 development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.Basically, the back-end layer depends of three main technologies to implement the needed services: HTTP Requests, WebSocket and a Database.
The first reason I adopted Django as a framework to develop this project was productivity. It's amazing how in just a few minutes you can have a working MVC application with very little code as soon as you install Django.
The second reason was the possibility to use the Channels library which is quite mature, is simple to implement on top of Django, has excellent documentation and an active community of developers who use it.
And finally, being able to use the MVC model at the beginning of the project made it easier to carry out tests by creating simple views that allowed us to better understand how Channels works at development time.
At another time I intend to replace Django with its REST version, since the project doesn't use the MVC model in production.
You can learn more about Django at www.djangoproject.com.
Channels description from Channels documentation.
Channels is a project that takes Django and extends its abilities beyond HTTP - to handle WebSockets, chat protocols, IoT protocols, and more. It’s built on a Python specification called ASGI.
Channels allow us to implement the protocol that provides all communication between server and clients in real-time through the Consumer layer that could be compared to the Django Views layer.
To make it easier to understand how Channels works, let's take a look at the diagram below:
Source: Heroku Blog Post
You can learn more about Channels at channels.readthedocs.io.
There are some reasons to use a database in this project.
The first is that you need to maintain an up-to-date list of logged-in users and the best way to do this consistently is to persist in a database. SQLite is very easy to use, offers all the necessary features and requires no installation as it is file system based.
One of the improvements I can see for this project is the introduction of the native authentication system provided by Django, in which case the database will be an important part of making this mechanism work.
Using a database system, in the near future we may save other types of information to help users get a better experience with this chat, such as favorite rooms, profile information, usage statistics, message history, advertisements, warnings and more.
You can learn more about SQLite at sqlite.org.
Overall, the consumer is responsible for providing a simplified way to handle the Channel's low-level ASGI implementation.
Its main objective is to structure the code as a package of methods that will be activated whenever an event related to the WebSocket occurs, avoiding that we have to write an event loop.
In addition, we have the option of working with synchronous code, as is Django's default, or asynchronous, whichever is more convenient for your project.
Below I describe the main consumer methods and their purpose.
get_users_list: Returns do banco de dados the list of users logged into a specific room.get_user: Checks no banco de dados if a specific user is logged into a specific room.add_user: Add a new user to a specific room no banco de dados.remove_user: Removes a specific user from a specific room in the database.
-
connect: When socket connection is created.- Get route data to identify room name and username.
- Checks if the user is already logged in to a specific room and rejects if true.
- Adds the user to the list of logged in users for the room.
- Adds the new connection to the room group.
- Sends a message to the room informing you that a new user has joined.
-
disconnect: Triggered when socked connection is closed.- Remove disconnected user from the users list.
- Sends a message to the room that a user has left.
- Leave room group
-
receive: Triggered when receives a new message from WebSocket.- Get data and redirect to the specific room according the message type.
-
chat_message: Send data to a specific room.- Get the list of connected users at a specific room.
- Send data to WebSocket.
class SignedUser: Defines the model for the table of connected users which is intended to keep an up-to-date list of all active users and rooms on the server.
websocket_urlpatterns: Defines the patterns for the WebSocket request routes and their corresponding views.
urlpatterns: Defines the patterns for the HTTP request routes and their corresponding views.
def get_signed_user: Returnstrueif the requested user is already logged into a specific room andfalseif not. This endpoint is used so that the presentation layer can avoid two users with the sameusernamein the same room.
This file simulates the use of environment variables that would be created in a production environment. There is a file named .env.template that the developers could use to fill .env file according their development environment.
From its use in a development environment, the developer can avoid sensitive information such as keys, credentials, etc. published in code versioning.
In order for the application to be able to retrieve the environment variables it is necessary to use the python-decouple library.
This project was created using the create-react-app script to gain some time and productivity in its construction.
According to the official React JS website:
Create React App is a comfortable environment for learning React, and is the best way to start building a new single-page application in React.
It sets up your development environment so that you can use the latest JavaScript features, provides a nice developer experience, and optimizes your app for production. You’ll need to have Node >= 10.16 and npm >= 5.6 on your machine.
You don't have to use again create-react-app script on this project but, as you can see above, you'll need Node and npm to run in development mode and to build the files.
If you already have Node on your operating system, you probably also have npm because it's part of the Node package.
If you don't have Node yet and are using Linux or macOS, I strongly recommend installing it via nvm to be able to use different versions of Node very easily without creating confusion in your OS. You'll find detailed info about how to install nvm and how to use it to install your preferred Node version at github.com/nvm-sh/nvm. In case you are a windows user, there is a similar project called windows-nvm about which you'll find more information at github.com/coreybutler/nvm-windows.
But if don't want to use a version manager for Node, you can install it using the official source at nodejs.org.
And the last recommendation is to don't use Current version of Node, LTS is the best choice to avoid problems with incompatibilities and other kind of issues that we usually have to deal with non stable versions.
Now you are able to install all Javascript dependencies for your project.
# npm installOne of the advantages of using the create-react-app script is that it allows us to custom inject variables configured in the server environment. In this project I'm using an environment variable to store the base url for HTTP requests and also for WebSocket connections.
If you are running this project locally, the environment variables can be set via the .env.development.local file. You will notice that this file is not available in the project, but I have kept a template called .env.template that contains the variable names and you will only have to change its contents.
If you are in a production environment, just create the variables in the service that will provide access to the project, build the files and the variables will be automatically injected into the Javascript files.
You're ready to go.
# npm startAfter running npm start, you will see something like the image below in the terminal and your browser should automatically start on the project's home page.
Compiled successfully!
You can now view markdownchat in the browser.
Local: http://localhost:3000
On Your Network: http://192.168.0.104:3000
Note that the development build is not optimized.
To create a production build, use npm run build.After five years of the project, the React.js team realized that the use of classes brought some types of problems that hindered productivity, the reuse of components and even the learning curve for new developers (since Javascript this can have a concept somewhat different from other languages).
With the creation of version 16.8 a new paradigm called Hooks was introduced that, in general terms, allows you to use the state and other features of React without writing a class.
You can still program in React by classes, but there is a global trend of projects moving to the hook-based model and, after all, who am I to question the motives of the team that maintains the React.js project 😃
In general terms, we can say that the main feature of the Context API is to offer the possibility to share states globally within the component tree.
Let's take a quick look at the definition I got from the official website at reactjs.org/docs/context.html:
In a typical React application, data is passed top-down (parent to child) via props, but such usage can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.
In this project, I chose to use a context to centralize the states related to the WebSocket connection because this allowed me to easily access the necessary methods and states without having to pass props between components.
Adopting centralized state management with the Context API allowed route control with react-router-dom to perform user redirection automatically according to the state of the WebSocket. In addition, the home and room pages can transparently access shared methods and states as if they were implemented within those pages.
Well, in my original idea I imagined using some kind of framework, market design system or some React library for functional styles in order to save time, but as soon as I started working I realized that one of the most fun parts would be building the styles a from total zero.
From there, the design of this project was built using exclusively pure CSS without the help of other tools and according to the "mobile first" paradigm.
Unfortunately time didn't allow me to work on prototype screens before starting development, so the design was thought out and implemented directly in the code with the idea of prioritizing ease of use and minimalism of the UI.
Through the WebSocket object, Javascript offers us a complete API to communicate with the server with all the attributes, methods and events that we could possibly need. Implementing a client application with WebSocket is not complex and relies on a few flows, to understand it better you might want to take a look at the documentation provided by Mozilla here.
In this project, I chose to use a library called react-use-websocket which uses Javascript's WebSocket object and offers some extra facilities when implementing the methods using a React hook. For more information about this library, you can access the project link here on Github: github.com/robtaussig/react-use-websocket.
Next, in the code detail, I'll make a more detailed explanation on how I used this library to integrate WebSocket's status, methods and events with the chat system.
Originally the idea was that messages would be exchanged using only plain text without formatting, the main objective would be just to demonstrate communication using WebSocket with a web application. While building and testing the web client I realized that it could be a lot more fun to make and use if you could use text formatting and other cool features in the messages. After a quick search, Markdown was the best answer I could find for this need.
The library chosen for this purpose was react-markdown, which implements some improvements using the remark parser as a base. In fact, remark was considered by www.npmtrends.com as the most popular Markdown parser in the world, so using that combination seemed like a really good choice.
But what really made me choose this combination were two concerns that I know react-markdown is already taking care of very well: security and flexibility. I'll leave the link to the projects below so that you can get to know them better, but I want to make a clipping of a part of the react-markdown documentation that supports what I just said:
There are other ways for markdown in React out there so why use this one? The two main reasons are that they often rely on dangerouslySetInnerHTML or have bugs with how they handle markdown. react-markdown uses a syntax tree to build the virtual dom which allows for updating only the changing DOM instead of completely overwriting. react-markdown is 100% CommonMark (optionally GFM) compliant and has extensions to support custom syntax.
If you want to better understand what you can do with these two libraries, I recommend visiting their projects here on Github:
A React Context that is responsible for providing the methods, states and status that allow managing the WebSocket in a centralized way from anywhere in the application. From this context we can also make HTTP requests to retrieve important information about users, rooms, etc.
canConnect: Lets you check if the username is available for the requested room. If the query returns a valid user to the room being consulted, or an error occurs, the user will be prevented from connecting.onMessageHandler: Handler responsible for handling the data package sent by the server and storing it in the room's message history, in addition to keeping the list of connected users updated.handleSendMessage: Handler that sends messages to the server.handleConnect: Handler to keep states in case connection was successfully.
Responsible for automatically redirecting the user between home and chat room screens. If the user has just arrived and does not yet have a valid connection to a room, he will be directed to the Home page. If he has already entered a valid user and a valid room and successfully obtained a WebSocket connection, he will be directed to the respective room page.
Create an instance of axios so that it can be shared with the application. axios is responsible for providing an abstraction of the Fetch API, adding improvements and extra functionality.
Implements the user input screen where he can enter a username and the name of the room he wants to connect to. The method of connecting to the room is provided by the context described above.
handleOnChange: Updates the states that hold the username and the chosen room as the user types the information on the screen.
The styles that color and beautify this page are available in a styles.css file which is present in the same folder as the Home.page.jsx page.
Implements the chat room where a logged in user can see who the other users are and send messages to the room using plain text or text with Markdown syntax (and emojis 😄).
The styles that color and beautify this page are available in a styles.css file which is present in the same folder as the Room.page.jsx page.
useEffect: This React hook is used in two specific situations with different goals.- Automatic scroll: we monitor incoming messages so the screen can be scrolled automatically each time there is a new message.
- User list auto hide: when the screen has less than 600px available for rendering room content, the user list is automatically hidden.
handleOnChange: Updates the state that hold the message.handleMessageInputKeyDown: User can send a message using the combination ofCtrl+Enterkeys.RoomContext: Shares some states and methods that Room page need to work properly.isConnected: return true if user is connected and false if not.handleSendMessage: handler to send messages.messageHistory: message history to show on the screen.handleConnect: used to disconnect from the room.signedRoom: room name where the user is connected.signedUser: username who is connected.usersList: users name list.
|
|
|
|
|
|
|
|
|
|










