├── .gitignore
├── README.md
├── chat
├── __init__.py
├── admin.py
├── apps.py
├── consumers.py
├── migrations
│ ├── 0001_initial.py
│ └── __init__.py
├── models.py
├── routing.py
├── templates
│ ├── messages.html
│ └── start_point_messages.html
├── tests.py
├── urls.py
└── views.py
├── db.sqlite3
├── manage.py
├── myproject
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
├── requirements.txt
└── static
├── css
└── messages.css
└── js
└── messages.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | __pycache__/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chat-app-tutorial
2 | ## Real-Time Chatting app in Django with Channels. (Whatsapp Web Clone)
3 | You can chat with multiple persons by staying on the same page.
4 |
5 | Explanation for this repository : https://youtu.be/205tbCUl4Uk
6 |
--------------------------------------------------------------------------------
/chat/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Omkar0231/Chat-app-tutorial/a12a7705b7ff248bb9707fe0f5c39653489d7705/chat/__init__.py
--------------------------------------------------------------------------------
/chat/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django import forms
3 | from django.core.exceptions import ValidationError
4 | from django.db.models import Q
5 | from .models import Thread, ChatMessage
6 |
7 | admin.site.register(ChatMessage)
8 |
9 |
10 | class ChatMessage(admin.TabularInline):
11 | model = ChatMessage
12 |
13 |
14 | # class ThreadForm(forms.ModelForm):
15 | # def clean(self):
16 | # """
17 | # This is the function that can be used to
18 | # validate your model data from admin
19 | # """
20 | # super(ThreadForm, self).clean()
21 | # first_person = self.cleaned_data.get('first_person')
22 | # second_person = self.cleaned_data.get('second_person')
23 | #
24 | # lookup1 = Q(first_person=first_person) & Q(second_person=second_person)
25 | # lookup2 = Q(first_person=second_person) & Q(second_person=first_person)
26 | # lookup = Q(lookup1 | lookup2)
27 | # qs = Thread.objects.filter(lookup)
28 | # if qs.exists():
29 | # raise ValidationError(f'Thread between {first_person} and {second_person} already exists.')
30 | #
31 |
32 | class ThreadAdmin(admin.ModelAdmin):
33 | inlines = [ChatMessage]
34 | class Meta:
35 | model = Thread
36 |
37 |
38 | admin.site.register(Thread, ThreadAdmin)
--------------------------------------------------------------------------------
/chat/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ChatConfig(AppConfig):
5 | default_auto_field = 'django.db.models.BigAutoField'
6 | name = 'chat'
7 |
--------------------------------------------------------------------------------
/chat/consumers.py:
--------------------------------------------------------------------------------
1 | import json
2 | from channels.consumer import AsyncConsumer
3 | from channels.db import database_sync_to_async
4 | from django.contrib.auth import get_user_model
5 |
6 | from chat.models import Thread, ChatMessage
7 |
8 | User = get_user_model()
9 |
10 |
11 | class ChatConsumer(AsyncConsumer):
12 | async def websocket_connect(self, event):
13 | print('connected', event)
14 | user = self.scope['user']
15 | chat_room = f'user_chatroom_{user.id}'
16 | self.chat_room = chat_room
17 | await self.channel_layer.group_add(
18 | chat_room,
19 | self.channel_name
20 | )
21 | await self.send({
22 | 'type': 'websocket.accept'
23 | })
24 |
25 | async def websocket_receive(self, event):
26 | print('receive', event)
27 | received_data = json.loads(event['text'])
28 | msg = received_data.get('message')
29 | sent_by_id = received_data.get('sent_by')
30 | send_to_id = received_data.get('send_to')
31 | thread_id = received_data.get('thread_id')
32 |
33 | if not msg:
34 | print('Error:: empty message')
35 | return False
36 |
37 | sent_by_user = await self.get_user_object(sent_by_id)
38 | send_to_user = await self.get_user_object(send_to_id)
39 | thread_obj = await self.get_thread(thread_id)
40 | if not sent_by_user:
41 | print('Error:: sent by user is incorrect')
42 | if not send_to_user:
43 | print('Error:: send to user is incorrect')
44 | if not thread_obj:
45 | print('Error:: Thread id is incorrect')
46 |
47 | await self.create_chat_message(thread_obj, sent_by_user, msg)
48 |
49 | other_user_chat_room = f'user_chatroom_{send_to_id}'
50 | self_user = self.scope['user']
51 | response = {
52 | 'message': msg,
53 | 'sent_by': self_user.id,
54 | 'thread_id': thread_id
55 | }
56 |
57 | await self.channel_layer.group_send(
58 | other_user_chat_room,
59 | {
60 | 'type': 'chat_message',
61 | 'text': json.dumps(response)
62 | }
63 | )
64 |
65 | await self.channel_layer.group_send(
66 | self.chat_room,
67 | {
68 | 'type': 'chat_message',
69 | 'text': json.dumps(response)
70 | }
71 | )
72 |
73 |
74 |
75 | async def websocket_disconnect(self, event):
76 | print('disconnect', event)
77 |
78 | async def chat_message(self, event):
79 | print('chat_message', event)
80 | await self.send({
81 | 'type': 'websocket.send',
82 | 'text': event['text']
83 | })
84 |
85 | @database_sync_to_async
86 | def get_user_object(self, user_id):
87 | qs = User.objects.filter(id=user_id)
88 | if qs.exists():
89 | obj = qs.first()
90 | else:
91 | obj = None
92 | return obj
93 |
94 | @database_sync_to_async
95 | def get_thread(self, thread_id):
96 | qs = Thread.objects.filter(id=thread_id)
97 | if qs.exists():
98 | obj = qs.first()
99 | else:
100 | obj = None
101 | return obj
102 |
103 | @database_sync_to_async
104 | def create_chat_message(self, thread, user, msg):
105 | ChatMessage.objects.create(thread=thread, user=user, message=msg)
106 |
--------------------------------------------------------------------------------
/chat/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.2.4 on 2021-06-30 17:48
2 |
3 | from django.conf import settings
4 | from django.db import migrations, models
5 | import django.db.models.deletion
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14 | ]
15 |
16 | operations = [
17 | migrations.CreateModel(
18 | name='Thread',
19 | fields=[
20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21 | ('updated', models.DateTimeField(auto_now=True)),
22 | ('timestamp', models.DateTimeField(auto_now_add=True)),
23 | ('first_person', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='thread_first_person', to=settings.AUTH_USER_MODEL)),
24 | ('second_person', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='thread_second_person', to=settings.AUTH_USER_MODEL)),
25 | ],
26 | options={
27 | 'unique_together': {('first_person', 'second_person')},
28 | },
29 | ),
30 | migrations.CreateModel(
31 | name='ChatMessage',
32 | fields=[
33 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34 | ('message', models.TextField()),
35 | ('timestamp', models.DateTimeField(auto_now_add=True)),
36 | ('thread', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='chatmessage_thread', to='chat.thread')),
37 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
38 | ],
39 | ),
40 | ]
41 |
--------------------------------------------------------------------------------
/chat/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Omkar0231/Chat-app-tutorial/a12a7705b7ff248bb9707fe0f5c39653489d7705/chat/migrations/__init__.py
--------------------------------------------------------------------------------
/chat/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 | from django.contrib.auth import get_user_model
3 | from django.db.models import Q
4 |
5 | User = get_user_model()
6 |
7 | # Create your models here.
8 |
9 | class ThreadManager(models.Manager):
10 | def by_user(self, **kwargs):
11 | user = kwargs.get('user')
12 | lookup = Q(first_person=user) | Q(second_person=user)
13 | qs = self.get_queryset().filter(lookup).distinct()
14 | return qs
15 |
16 |
17 | class Thread(models.Model):
18 | first_person = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name='thread_first_person')
19 | second_person = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True,
20 | related_name='thread_second_person')
21 | updated = models.DateTimeField(auto_now=True)
22 | timestamp = models.DateTimeField(auto_now_add=True)
23 |
24 | objects = ThreadManager()
25 | class Meta:
26 | unique_together = ['first_person', 'second_person']
27 |
28 |
29 | class ChatMessage(models.Model):
30 | thread = models.ForeignKey(Thread, null=True, blank=True, on_delete=models.CASCADE, related_name='chatmessage_thread')
31 | user = models.ForeignKey(User, on_delete=models.CASCADE)
32 | message = models.TextField()
33 | timestamp = models.DateTimeField(auto_now_add=True)
--------------------------------------------------------------------------------
/chat/routing.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import consumers
3 |
4 |
5 | websocket_urlpatterns = [
6 | path('chat/', consumers.ChatConsumer.as_asgi()),
7 | ]
--------------------------------------------------------------------------------
/chat/templates/messages.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 | Chat
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | {% if user.is_authenticated %}
18 | Logged in as : {{ user.username }}
19 |
20 | {% endif %}
21 |
22 |
23 |
58 |
59 |
60 | {% for thread in Threads %}
61 |
68 |
98 |
99 |
100 |
101 | {% for chat in thread.chatmessage_thread.all %}
102 | {% if chat.user == user %}
103 |
104 |
105 | {{ chat.message }}
106 | {{ chat.timestamp|date:"d D" }}, {{ chat.timestamp|time:"H:i" }}
107 |
108 |
109 |

110 |
111 |
112 | {% else %}
113 |
114 |
115 |

116 |
117 |
118 | {{ chat.message }}
119 | {{ chat.timestamp|date:"d D" }}, {{ chat.timestamp|time:"H:i" }}
120 |
121 |
122 | {% endif %}
123 | {% endfor %}
124 |
125 |
126 |
127 |
128 |
129 |
130 | {% endfor %}
131 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/chat/templates/start_point_messages.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 |
3 |
4 |
5 | Chat
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
95 |
96 |
97 |
122 |
123 |
124 |
125 |
126 |

127 |
128 |
129 | Hi, how are you samim?
130 | 8:40 AM, Today
131 |
132 |
133 |
134 |
135 | Hi Khalid i am good tnx how about you?
136 | 8:55 AM, Today
137 |
138 |
139 |

140 |
141 |
142 |
143 |
144 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
--------------------------------------------------------------------------------
/chat/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/chat/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 | from . import views
3 | urlpatterns = [
4 | path('', views.messages_page),
5 | ]
6 |
--------------------------------------------------------------------------------
/chat/views.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth.decorators import login_required
2 | from django.shortcuts import render
3 |
4 | # Create your views here.
5 | from chat.models import Thread
6 |
7 |
8 | @login_required
9 | def messages_page(request):
10 | threads = Thread.objects.by_user(user=request.user).prefetch_related('chatmessage_thread').order_by('timestamp')
11 | context = {
12 | 'Threads': threads
13 | }
14 | return render(request, 'messages.html', context)
15 |
--------------------------------------------------------------------------------
/db.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Omkar0231/Chat-app-tutorial/a12a7705b7ff248bb9707fe0f5c39653489d7705/db.sqlite3
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | """Run administrative tasks."""
9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
10 | try:
11 | from django.core.management import execute_from_command_line
12 | except ImportError as exc:
13 | raise ImportError(
14 | "Couldn't import Django. Are you sure it's installed and "
15 | "available on your PYTHONPATH environment variable? Did you "
16 | "forget to activate a virtual environment?"
17 | ) from exc
18 | execute_from_command_line(sys.argv)
19 |
20 |
21 | if __name__ == '__main__':
22 | main()
23 |
--------------------------------------------------------------------------------
/myproject/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Omkar0231/Chat-app-tutorial/a12a7705b7ff248bb9707fe0f5c39653489d7705/myproject/__init__.py
--------------------------------------------------------------------------------
/myproject/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI config for myproject project.
3 |
4 | It exposes the ASGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
8 | """
9 |
10 | import os
11 | import chat.routing
12 | from django.core.asgi import get_asgi_application
13 | from channels.routing import ProtocolTypeRouter, URLRouter
14 | from channels.auth import AuthMiddlewareStack
15 |
16 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
17 |
18 | application = ProtocolTypeRouter({
19 | 'http': get_asgi_application(),
20 | 'websocket': AuthMiddlewareStack(
21 | URLRouter(
22 | chat.routing.websocket_urlpatterns
23 | )
24 | )
25 | })
26 |
--------------------------------------------------------------------------------
/myproject/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for myproject project.
3 |
4 | Generated by 'django-admin startproject' using Django 3.2.4.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.2/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/3.2/ref/settings/
11 | """
12 | import os.path
13 | from pathlib import Path
14 |
15 | # Build paths inside the project like this: BASE_DIR / 'subdir'.
16 | BASE_DIR = Path(__file__).resolve().parent.parent
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = 'django-insecure-il2i31l!d$kn*!=ro18w*5-)(^pttckg0*-gp!u7g+=o4z$(+j'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 | 'chat',
41 | 'channels',
42 | ]
43 |
44 | MIDDLEWARE = [
45 | 'django.middleware.security.SecurityMiddleware',
46 | 'django.contrib.sessions.middleware.SessionMiddleware',
47 | 'django.middleware.common.CommonMiddleware',
48 | 'django.middleware.csrf.CsrfViewMiddleware',
49 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
50 | 'django.contrib.messages.middleware.MessageMiddleware',
51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
52 | ]
53 |
54 | ROOT_URLCONF = 'myproject.urls'
55 |
56 | TEMPLATES = [
57 | {
58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
59 | 'DIRS': [BASE_DIR / 'templates']
60 | ,
61 | 'APP_DIRS': True,
62 | 'OPTIONS': {
63 | 'context_processors': [
64 | 'django.template.context_processors.debug',
65 | 'django.template.context_processors.request',
66 | 'django.contrib.auth.context_processors.auth',
67 | 'django.contrib.messages.context_processors.messages',
68 | ],
69 | },
70 | },
71 | ]
72 |
73 | # WSGI_APPLICATION = 'myproject.wsgi.application'
74 | ASGI_APPLICATION = 'myproject.asgi.application'
75 |
76 | # Database
77 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases
78 |
79 | # DATABASES = {
80 | # 'default': {
81 | # 'ENGINE': 'django.db.backends.sqlite3',
82 | # 'NAME': BASE_DIR / 'db.sqlite3',
83 | # }
84 | # }
85 |
86 | DATABASES = {
87 | 'default': {
88 | 'ENGINE': 'django.db.backends.postgresql_psycopg2',
89 | 'NAME': 'channelsdb',
90 | 'USER': 'omkar',
91 | 'PASSWORD': '2311',
92 | 'HOST': 'localhost',
93 | 'PORT': '',
94 | }
95 | }
96 |
97 |
98 | # Password validation
99 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
100 |
101 | AUTH_PASSWORD_VALIDATORS = [
102 | {
103 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
104 | },
105 | {
106 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
107 | },
108 | {
109 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
110 | },
111 | {
112 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
113 | },
114 | ]
115 |
116 |
117 | # Internationalization
118 | # https://docs.djangoproject.com/en/3.2/topics/i18n/
119 |
120 | LANGUAGE_CODE = 'en-us'
121 |
122 | TIME_ZONE = 'UTC'
123 |
124 | USE_I18N = True
125 |
126 | USE_L10N = True
127 |
128 | USE_TZ = True
129 |
130 |
131 | # Static files (CSS, JavaScript, Images)
132 | # https://docs.djangoproject.com/en/3.2/howto/static-files/
133 |
134 | STATIC_URL = '/static/'
135 | STATICFILES_DIRS = [
136 | os.path.join(BASE_DIR, 'static')
137 | ]
138 |
139 | # Default primary key field type
140 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
141 |
142 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
143 |
144 | CHANNEL_LAYERS = {
145 | 'default': {
146 | 'BACKEND': 'channels.layers.InMemoryChannelLayer',
147 | # 'CONFIG': {
148 | # 'hosts': [('127.0.0.1', 6379)],
149 | # }
150 | }
151 | }
--------------------------------------------------------------------------------
/myproject/urls.py:
--------------------------------------------------------------------------------
1 | """myproject URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/3.2/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import path, include
18 |
19 | urlpatterns = [
20 | path('admin/', admin.site.urls),
21 | path('chat/', include('chat.urls'))
22 | ]
23 |
--------------------------------------------------------------------------------
/myproject/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for myproject project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | asgiref==3.4.0
2 | attrs==21.2.0
3 | autobahn==21.3.1
4 | Automat==20.2.0
5 | cffi==1.14.5
6 | channels==3.0.3
7 | constantly==15.1.0
8 | cryptography==3.4.7
9 | daphne==3.0.2
10 | Django==3.2.4
11 | hyperlink==21.0.0
12 | idna==3.2
13 | incremental==21.3.0
14 | pyasn1==0.4.8
15 | pyasn1-modules==0.2.8
16 | pycparser==2.20
17 | pyOpenSSL==20.0.1
18 | pytz==2021.1
19 | service-identity==21.1.0
20 | six==1.16.0
21 | sqlparse==0.4.1
22 | typing-extensions==3.10.0.0
23 | zope.interface==5.4.0
24 |
--------------------------------------------------------------------------------
/static/css/messages.css:
--------------------------------------------------------------------------------
1 | body,
2 | html {
3 | height: 100%;
4 | margin: 0;
5 | background: #7f7fd5;
6 | background: -webkit-linear-gradient(to right, #91eae4, #86a8e7, #7f7fd5);
7 | background: linear-gradient(to right, #91eae4, #86a8e7, #7f7fd5);
8 | }
9 |
10 | .chat {
11 | margin-top: auto;
12 | margin-bottom: auto;
13 | }
14 | .card {
15 | height: 90%;
16 | border-radius: 15px !important;
17 | background-color: rgba(0, 0, 0, 0.4) !important;
18 | }
19 | .contacts_body {
20 | padding: 0.75rem 0 !important;
21 | overflow-y: auto;
22 | white-space: nowrap;
23 | }
24 | .msg_card_body {
25 | overflow-y: auto;
26 | max-height: 65%;
27 | padding-bottom: 20px;
28 | }
29 | .card-header {
30 | border-radius: 15px 15px 0 0 !important;
31 | border-bottom: 0 !important;
32 | }
33 | .card-footer {
34 | border-radius: 0 0 15px 15px !important;
35 | border-top: 0 !important;
36 | bottom: 0;
37 | position: absolute;
38 | width: 100%;
39 | }
40 | .container {
41 | align-content: center;
42 | }
43 | .search {
44 | border-radius: 15px 0 0 15px !important;
45 | background-color: rgba(0, 0, 0, 0.3) !important;
46 | border: 0 !important;
47 | color: white !important;
48 | }
49 | .search:focus {
50 | box-shadow: none !important;
51 | outline: 0px !important;
52 | }
53 | .type_msg {
54 | background-color: rgba(0, 0, 0, 0.3) !important;
55 | border: 0 !important;
56 | color: white !important;
57 | height: 60px !important;
58 | overflow-y: auto;
59 | }
60 | .type_msg:focus {
61 | box-shadow: none !important;
62 | outline: 0px !important;
63 | }
64 | .attach_btn {
65 | border-radius: 15px 0 0 15px !important;
66 | background-color: rgba(0, 0, 0, 0.3) !important;
67 | border: 0 !important;
68 | color: white !important;
69 | cursor: pointer;
70 | }
71 | .send_btn {
72 | border-radius: 0 15px 15px 0 !important;
73 | background-color: rgba(0, 0, 0, 0.3) !important;
74 | border: 0 !important;
75 | color: white !important;
76 | cursor: pointer;
77 | }
78 | .search_btn {
79 | border-radius: 0 15px 15px 0 !important;
80 | background-color: rgba(0, 0, 0, 0.3) !important;
81 | border: 0 !important;
82 | color: white !important;
83 | cursor: pointer;
84 | }
85 | .contacts {
86 | list-style: none;
87 | padding: 0;
88 | }
89 | .contacts li {
90 | width: 100% !important;
91 | padding: 5px 10px;
92 | margin-bottom: 15px !important;
93 | }
94 | .active {
95 | background-color: rgba(0, 0, 0, 0.3);
96 | }
97 | .user_img {
98 | height: 70px;
99 | width: 70px;
100 | border: 1.5px solid #f5f6fa;
101 | }
102 | .user_img_msg {
103 | height: 40px;
104 | width: 40px;
105 | border: 1.5px solid #f5f6fa;
106 | }
107 | .img_cont {
108 | position: relative;
109 | height: 70px;
110 | width: 70px;
111 | }
112 | .img_cont_msg {
113 | height: 40px;
114 | width: 40px;
115 | }
116 | .online_icon {
117 | position: absolute;
118 | height: 15px;
119 | width: 15px;
120 | background-color: #4cd137;
121 | border-radius: 50%;
122 | bottom: 0.2em;
123 | right: 0.4em;
124 | border: 1.5px solid white;
125 | }
126 | .offline {
127 | background-color: #c23616 !important;
128 | }
129 | .user_info {
130 | margin-top: auto;
131 | margin-bottom: auto;
132 | margin-left: 15px;
133 | }
134 | .user_info span {
135 | font-size: 20px;
136 | color: white;
137 | }
138 | .user_info p {
139 | font-size: 10px;
140 | color: rgba(255, 255, 255, 0.6);
141 | }
142 | .video_cam {
143 | margin-left: 50px;
144 | margin-top: 5px;
145 | }
146 | .video_cam span {
147 | color: white;
148 | font-size: 20px;
149 | cursor: pointer;
150 | margin-right: 20px;
151 | }
152 | .msg_cotainer {
153 | margin-top: auto;
154 | margin-bottom: auto;
155 | margin-left: 10px;
156 | border-radius: 25px;
157 | background-color: #82ccdd;
158 | padding: 10px;
159 | position: relative;
160 | min-width: 70px;
161 | text-align: center;
162 | }
163 | .msg_cotainer_send {
164 | min-width: 70px;
165 | text-align: center;
166 | margin-top: auto;
167 | margin-bottom: auto;
168 | margin-right: 10px;
169 | border-radius: 25px;
170 | background-color: #78e08f;
171 | padding: 10px;
172 | position: relative;
173 | }
174 | .msg_time {
175 | position: absolute;
176 | left: 0;
177 | bottom: -15px;
178 | color: rgba(255, 255, 255, 0.5);
179 | font-size: 10px;
180 | }
181 | .msg_time_send {
182 | position: absolute;
183 | right: 0;
184 | bottom: -15px;
185 | color: rgba(255, 255, 255, 0.5);
186 | font-size: 10px;
187 | }
188 | .msg_head {
189 | position: relative;
190 | }
191 | #action_menu_btn {
192 | position: absolute;
193 | right: 10px;
194 | top: 10px;
195 | color: white;
196 | cursor: pointer;
197 | font-size: 20px;
198 | }
199 | .action_menu {
200 | z-index: 1;
201 | position: absolute;
202 | padding: 15px 0;
203 | background-color: rgba(0, 0, 0, 0.5);
204 | color: white;
205 | border-radius: 15px;
206 | top: 30px;
207 | right: 15px;
208 | display: none;
209 | }
210 | .action_menu ul {
211 | list-style: none;
212 | padding: 0;
213 | margin: 0;
214 | }
215 | .action_menu ul li {
216 | width: 100%;
217 | padding: 10px 15px;
218 | margin-bottom: 5px;
219 | }
220 | .action_menu ul li i {
221 | padding-right: 10px;
222 | }
223 | .action_menu ul li:hover {
224 | cursor: pointer;
225 | background-color: rgba(0, 0, 0, 0.2);
226 | }
227 | @media (max-width: 576px) {
228 | .contacts_card {
229 | margin-bottom: 15px !important;
230 | }
231 | }
232 |
233 | .received {
234 | justify-content: flex-start;
235 | }
236 |
237 | .replied {
238 | justify-content: flex-end;
239 | }
240 |
241 | .is_active{
242 | display: block !important;
243 | }
244 |
245 | .hide{
246 | display: none;
247 | }
248 |
249 | /* Hide scrollbar for Chrome, Safari and Opera */
250 | .msg_card_body::-webkit-scrollbar {
251 | display: none;
252 | }
253 |
254 | /* Hide scrollbar for IE, Edge and Firefox */
255 | .msg_card_body {
256 | -ms-overflow-style: none; /* IE and Edge */
257 | scrollbar-width: none; /* Firefox */
258 | }
259 |
260 | .messages-wrapper{
261 | height: 100%;
262 | }
--------------------------------------------------------------------------------
/static/js/messages.js:
--------------------------------------------------------------------------------
1 | let input_message = $('#input-message')
2 | let message_body = $('.msg_card_body')
3 | let send_message_form = $('#send-message-form')
4 | const USER_ID = $('#logged-in-user').val()
5 |
6 | let loc = window.location
7 | let wsStart = 'ws://'
8 |
9 | if(loc.protocol === 'https') {
10 | wsStart = 'wss://'
11 | }
12 | let endpoint = wsStart + loc.host + loc.pathname
13 |
14 | var socket = new WebSocket(endpoint)
15 |
16 | socket.onopen = async function(e){
17 | console.log('open', e)
18 | send_message_form.on('submit', function (e){
19 | e.preventDefault()
20 | let message = input_message.val()
21 | let send_to = get_active_other_user_id()
22 | let thread_id = get_active_thread_id()
23 |
24 | let data = {
25 | 'message': message,
26 | 'sent_by': USER_ID,
27 | 'send_to': send_to,
28 | 'thread_id': thread_id
29 | }
30 | data = JSON.stringify(data)
31 | socket.send(data)
32 | $(this)[0].reset()
33 | })
34 | }
35 |
36 | socket.onmessage = async function(e){
37 | console.log('message', e)
38 | let data = JSON.parse(e.data)
39 | let message = data['message']
40 | let sent_by_id = data['sent_by']
41 | let thread_id = data['thread_id']
42 | newMessage(message, sent_by_id, thread_id)
43 | }
44 |
45 | socket.onerror = async function(e){
46 | console.log('error', e)
47 | }
48 |
49 | socket.onclose = async function(e){
50 | console.log('close', e)
51 | }
52 |
53 |
54 | function newMessage(message, sent_by_id, thread_id) {
55 | if ($.trim(message) === '') {
56 | return false;
57 | }
58 | let message_element;
59 | let chat_id = 'chat_' + thread_id
60 | if(sent_by_id == USER_ID){
61 | message_element = `
62 |
63 |
64 | ${message}
65 | 8:55 AM, Today
66 |
67 |
68 |

69 |
70 |
71 | `
72 | }
73 | else{
74 | message_element = `
75 |
76 |
77 |

78 |
79 |
80 | ${message}
81 | 8:40 AM, Today
82 |
83 |
84 | `
85 |
86 | }
87 |
88 | let message_body = $('.messages-wrapper[chat-id="' + chat_id + '"] .msg_card_body')
89 | message_body.append($(message_element))
90 | message_body.animate({
91 | scrollTop: $(document).height()
92 | }, 100);
93 | input_message.val(null);
94 | }
95 |
96 |
97 | $('.contact-li').on('click', function (){
98 | $('.contacts .actiive').removeClass('active')
99 | $(this).addClass('active')
100 |
101 | // message wrappers
102 | let chat_id = $(this).attr('chat-id')
103 | $('.messages-wrapper.is_active').removeClass('is_active')
104 | $('.messages-wrapper[chat-id="' + chat_id +'"]').addClass('is_active')
105 |
106 | })
107 |
108 | function get_active_other_user_id(){
109 | let other_user_id = $('.messages-wrapper.is_active').attr('other-user-id')
110 | other_user_id = $.trim(other_user_id)
111 | return other_user_id
112 | }
113 |
114 | function get_active_thread_id(){
115 | let chat_id = $('.messages-wrapper.is_active').attr('chat-id')
116 | let thread_id = chat_id.replace('chat_', '')
117 | return thread_id
118 | }
--------------------------------------------------------------------------------