├── .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 |
24 |
25 |
26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 |
34 | 35 | {% for thread in Threads %} 36 |
  • 37 |
    38 |
    39 | 40 | 41 |
    42 | 51 |
    52 |
  • 53 | {% endfor %} 54 |
    55 |
    56 | 57 |
    58 |
    59 |
    60 | {% for thread in Threads %} 61 |
    68 |
    69 |
    70 |
    71 | 72 | 73 |
    74 | 83 |
    84 | 85 | 86 |
    87 |
    88 | 89 |
    90 |
      91 |
    • View profile
    • 92 |
    • Add to close friends
    • 93 |
    • Add to group
    • 94 |
    • Block
    • 95 |
    96 |
    97 |
    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 |
    20 |
    21 |
    22 |
    23 | 24 |
    25 | 26 |
    27 |
    28 |
    29 |
    30 | 31 |
  • 32 |
    33 |
    34 | 35 | 36 |
    37 | 41 |
    42 |
  • 43 |
  • 44 |
    45 |
    46 | 47 | 48 |
    49 | 53 |
    54 |
  • 55 |
  • 56 |
    57 |
    58 | 59 | 60 |
    61 | 65 |
    66 |
  • 67 |
  • 68 |
    69 |
    70 | 71 | 72 |
    73 | 77 |
    78 |
  • 79 |
  • 80 |
    81 |
    82 | 83 | 84 |
    85 | 89 |
    90 |
  • 91 |
    92 |
    93 | 94 |
    95 |
    96 |
    97 |
    98 |
    99 |
    100 | 101 | 102 |
    103 | 107 |
    108 | 109 | 110 |
    111 |
    112 | 113 |
    114 |
      115 |
    • View profile
    • 116 |
    • Add to close friends
    • 117 |
    • Add to group
    • 118 |
    • Block
    • 119 |
    120 |
    121 |
    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 | } --------------------------------------------------------------------------------