├── chat ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0003_auto_20200823_1119.py │ ├── 0001_initial.py │ └── 0002_auto_20200823_0905.py ├── templates │ └── chat │ │ ├── index.html │ │ ├── unauthorized.html │ │ ├── room_message_chat_header.html │ │ ├── room_preview_image.html │ │ ├── room_message.html │ │ └── room.html ├── tests.py ├── apps.py ├── static │ └── chat │ │ ├── default_group.png │ │ ├── default_profile.png │ │ └── style.css ├── routing.py ├── urls.py ├── admin.py ├── services.py ├── models.py ├── tortoise_models.py ├── views.py └── consumers.py ├── django_channel_tutorial ├── base.py ├── __init__.py ├── routing.py ├── wsgi.py ├── asgi.py ├── static │ └── base.css ├── templates │ └── base.html ├── urls.py ├── staging.py ├── production.py └── settings.py ├── runtime.txt ├── Procfile ├── .pylintrc ├── .github ├── FUNDING.yml └── workflows │ └── django.yml ├── .gitignore ├── Containerfile ├── manage.py ├── requirements.txt ├── Jenkinsfile.Staging ├── README.md └── Jenkinsfile.Deploy /chat/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chat/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chat/templates/chat/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_channel_tutorial/base.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.7.12 2 | -------------------------------------------------------------------------------- /django_channel_tutorial/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chat/templates/chat/unauthorized.html: -------------------------------------------------------------------------------- 1 | Unauthorized! -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn django_channel_tutorial.wsgi 2 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | disable= 3 | C0114, # missing-module-docstring 4 | -------------------------------------------------------------------------------- /chat/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: paypal.me/josnin417 4 | -------------------------------------------------------------------------------- /chat/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatConfig(AppConfig): 5 | name = 'chat' 6 | -------------------------------------------------------------------------------- /chat/static/chat/default_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingelle/django-whatsapp-web-clone/HEAD/chat/static/chat/default_group.png -------------------------------------------------------------------------------- /chat/static/chat/default_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingelle/django-whatsapp-web-clone/HEAD/chat/static/chat/default_profile.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__/ 3 | db.sqlite3 4 | db.sqlite3.tortoise 5 | env 6 | env2 7 | media 8 | .vscode 9 | .env 10 | .env.production 11 | .env.staging 12 | *.swp 13 | -------------------------------------------------------------------------------- /chat/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from . import consumers 4 | 5 | websocket_urlpatterns = [ 6 | re_path(r'ws/chat/(?P\w+)/$', consumers.ChatConsumer.as_asgi()), 7 | ] 8 | -------------------------------------------------------------------------------- /chat/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.urls import path 3 | 4 | from . import views 5 | 6 | app_name = 'chat' 7 | 8 | urlpatterns = [ 9 | path('', views.index, name='index'), 10 | path('history//', views.history, name='history'), 11 | path('unauthorized/', views.unauthorized, name='unauthorized'), 12 | path('/', views.room, name='room'), 13 | ] 14 | -------------------------------------------------------------------------------- /Containerfile: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi8/python-38 2 | 3 | # Add application sources with correct permissions for OpenShift 4 | USER 0 5 | ADD . . 6 | RUN chown -R 1001:0 ./ 7 | USER 1001 8 | 9 | # Install the dependencies 10 | RUN pip install -U pip && \ 11 | pip install -r requirements.txt 12 | 13 | # Run the application 14 | CMD python manage.py runserver 0.0.0.0:8080 15 | -------------------------------------------------------------------------------- /django_channel_tutorial/routing.py: -------------------------------------------------------------------------------- 1 | from channels.auth import AuthMiddlewareStack 2 | from channels.routing import ProtocolTypeRouter, URLRouter 3 | import chat.routing 4 | 5 | application = ProtocolTypeRouter({ 6 | # (http->django views is added by default) 7 | 'websocket': AuthMiddlewareStack( 8 | URLRouter( 9 | chat.routing.websocket_urlpatterns 10 | ) 11 | ), 12 | }) 13 | -------------------------------------------------------------------------------- /chat/migrations/0003_auto_20200823_1119.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-08-23 11:19 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('auth', '0011_update_proxy_permissions'), 10 | ('chat', '0002_auto_20200823_0905'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RenameModel( 15 | old_name='ChartGroup', 16 | new_name='ChatGroup', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /django_channel_tutorial/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_channel_tutorial 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.0/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', 'django_channel_tutorial.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /chat/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import ChatGroup 3 | 4 | # Register your models here. 5 | class ChatGroupAdmin(admin.ModelAdmin): 6 | """ enable Chart Group admin """ 7 | list_display = ('id', 'name', 'description', 'icon', 'mute_notifications', 'date_created', 'date_modified') 8 | list_filter = ('id', 'name', 'description', 'icon', 'mute_notifications', 'date_created', 'date_modified') 9 | list_display_links = ('name',) 10 | 11 | admin.site.register(ChatGroup, ChatGroupAdmin) 12 | 13 | -------------------------------------------------------------------------------- /django_channel_tutorial/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django_channel_tutorial 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.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | import django 12 | 13 | from channels.routing import get_default_application 14 | 15 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_channel_tutorial.settings') 16 | 17 | django.setup() 18 | 19 | application = get_default_application() 20 | -------------------------------------------------------------------------------- /django_channel_tutorial/static/base.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, body { 6 | margin:0; 7 | padding:0; 8 | font-family: Segoe UI,Helvetica Neue,Helvetica,Lucida Grande,Arial,Ubuntu,Cantarell,Fira Sans,sans-serif; 9 | /* font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 10 | font-weight: 300; */ 11 | color: #4a4a4a; 12 | } 13 | 14 | .container { 15 | width: 50vw; 16 | border:1px solid brown; 17 | } 18 | 19 | 20 | .header { 21 | height: 1rem; 22 | } 23 | -------------------------------------------------------------------------------- /django_channel_tutorial/templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% block title %} 11 | {% endblock title %} 12 | 13 | 14 | {% block app_css %} 15 | {% endblock app_css %} 16 | 17 | 18 | {% block content %} 19 | {% endblock content %} 20 | 21 | -------------------------------------------------------------------------------- /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 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_channel_tutorial.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /chat/services.py: -------------------------------------------------------------------------------- 1 | 2 | from tortoise import Tortoise, run_async 3 | from django.conf import settings 4 | from .tortoise_models import ChatMessage 5 | 6 | async def chat_save_message(username, room_id, message, message_type, image_caption): 7 | 8 | """ function to store chat message in sqlite """ 9 | 10 | await Tortoise.init(**settings.TORTOISE_INIT) 11 | await Tortoise.generate_schemas() 12 | 13 | await ChatMessage.create(room_id=room_id, 14 | username=username, 15 | message=message, 16 | message_type=message_type, 17 | image_caption=image_caption 18 | ) 19 | 20 | await Tortoise.close_connections() -------------------------------------------------------------------------------- /chat/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import Group 3 | 4 | 5 | 6 | # Create your models here. 7 | class ChatGroup(Group): 8 | """ extend Group model to add extra info""" 9 | description = models.TextField(blank=True, help_text="description of the group") 10 | mute_notifications = models.BooleanField(default=False, help_text="disable notification if true") 11 | icon = models.ImageField(help_text="Group icon", blank=True, upload_to="chartgroup") 12 | date_created = models.DateTimeField(auto_now_add=True) 13 | date_modified = models.DateTimeField(auto_now=True) 14 | 15 | def get_absolute_url(self): 16 | from django.urls import reverse 17 | return reverse('chat:room', args=[str(self.id)]) 18 | -------------------------------------------------------------------------------- /chat/tortoise_models.py: -------------------------------------------------------------------------------- 1 | 2 | from tortoise import fields 3 | from tortoise.models import Model 4 | 5 | class ChatMessage(Model): 6 | """ use to store chat history message 7 | make used of tortoise ORM since its support asyncio ORM 8 | """ 9 | id = fields.IntField(pk=True) 10 | room_id = fields.IntField(null=True) 11 | username = fields.CharField(max_length=50, null=True) 12 | message = fields.TextField() 13 | message_type = fields.CharField(max_length=50, null=True) 14 | image_caption = fields.CharField(max_length=50, null=True) 15 | date_created = fields.DatetimeField(null=True, auto_now_add=True) 16 | 17 | class Meta: 18 | table = 'chat_chatmessage' 19 | 20 | def __str__(self): 21 | return self.message 22 | -------------------------------------------------------------------------------- /django_channel_tutorial/urls.py: -------------------------------------------------------------------------------- 1 | """django_channel_tutorial URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.0/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 | from django.conf import settings 19 | from django.conf.urls.static import static 20 | 21 | urlpatterns = [ 22 | path('admin/', admin.site.urls), 23 | path('chat/', include('chat.urls', namespace='chat')), 24 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 25 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis==1.3.1 2 | aiosqlite==0.15.0 3 | anyio==3.7.1 4 | asgiref==3.4.1 5 | async-timeout==3.0.1 6 | asyncpg==0.25.0 7 | attrs==20.2.0 8 | autobahn==21.3.1 9 | Automat==20.2.0 10 | cffi==1.14.2 11 | channels==3.0.4 12 | channels-redis==3.2.0 13 | click==8.1.7 14 | constantly==15.1.0 15 | cryptography==35.0.0 16 | daphne==3.0.2 17 | Django==3.2.13 18 | django-sslserver==0.22 19 | exceptiongroup==1.1.3 20 | h11==0.14.0 21 | hiredis==1.1.0 22 | httptools==0.6.0 23 | hyperlink==21.0.0 24 | idna==2.10 25 | importlib-metadata==6.7.0 26 | incremental==21.3.0 27 | iso8601==0.1.13 28 | msgpack==1.0.0 29 | Pillow==9.0.1 30 | psycopg2-binary==2.9.2 31 | pyasn1==0.4.8 32 | pyasn1-modules==0.2.8 33 | pycparser==2.20 34 | PyHamcrest==2.0.2 35 | pyOpenSSL==19.1.0 36 | PyPika==0.42.1 37 | python-dotenv==0.19.2 38 | pytz==2020.1 39 | PyYAML==6.0.1 40 | service-identity==18.1.0 41 | six==1.15.0 42 | sniffio==1.3.0 43 | sqlparse==0.3.1 44 | tortoise-orm==0.16.16 45 | Twisted==22.4.0 46 | txaio==21.2.1 47 | typing-extensions==3.7.4.3 48 | uvicorn==0.22.0 49 | uvloop==0.18.0 50 | watchfiles==0.20.0 51 | websockets==11.0.3 52 | zipp==3.15.0 53 | zope.interface==5.1.0 54 | -------------------------------------------------------------------------------- /django_channel_tutorial/staging.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv('.env.staging') 5 | 6 | # Database 7 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 8 | 9 | DEBUG = False 10 | 11 | DATABASES = { 12 | 'default': { 13 | 'ENGINE': 'django.db.backends.postgresql', 14 | 'NAME': os.getenv('POSTGRESQL_DATABASE'), 15 | 'USER': os.getenv('POSTGRESQL_USER'), 16 | 'PASSWORD': os.getenv('POSTGRESQL_PASSWORD'), 17 | 'HOST': os.getenv('POSTGRESQL_HOST'), 18 | 'PORT': os.getenv('POSTGRESQL_PORT'), 19 | } 20 | } 21 | 22 | TORTOISE_INIT = { 23 | "db_url": f"postgres://{os.getenv('POSTGRESQL_USER')}:{os.getenv('POSTGRESQL_PASSWORD')}@{os.getenv('POSTGRESQL_HOST')}:{os.getenv('POSTGRESQL_PORT')}/{os.getenv('POSTGRESQL_DATABASE')}", 24 | "modules" : { 25 | "models": ["chat.tortoise_models"] 26 | } 27 | } 28 | 29 | CHANNEL_LAYERS = { 30 | 'default': { 31 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 32 | 'CONFIG': { 33 | "hosts": [(os.getenv('REDIS_HOST'), os.getenv('REDIS_PORT'))], 34 | }, 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /chat/templates/chat/room_message_chat_header.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 |
4 |
5 | {% if chatgroup.icon %} 6 | 7 | {% else %} 8 | Default Profile 9 | {% endif %} 10 |
11 |
12 |
{{chatgroup.name|title}}
13 |
{{participants}}
14 |
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /django_channel_tutorial/production.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | from dotenv import load_dotenv 3 | 4 | load_dotenv('.env.production') 5 | 6 | # Database 7 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 8 | 9 | DEBUG = False 10 | 11 | STATIC_URL = "/django-whatsapp-clone/static/" 12 | MEDIA_URL = "/django-whatsapp-clone/media/" 13 | 14 | DATABASES = { 15 | 'default': { 16 | 'ENGINE': 'django.db.backends.postgresql', 17 | 'NAME': os.getenv('POSTGRESQL_DATABASE'), 18 | 'USER': os.getenv('POSTGRESQL_USER'), 19 | 'PASSWORD': os.getenv('POSTGRESQL_PASSWORD'), 20 | 'HOST': os.getenv('POSTGRESQL_HOST'), 21 | 'PORT': os.getenv('POSTGRESQL_PORT'), 22 | } 23 | } 24 | 25 | TORTOISE_INIT = { 26 | "db_url": f"postgres://{os.getenv('POSTGRESQL_USER')}:{os.getenv('POSTGRESQL_PASSWORD')}@{os.getenv('POSTGRESQL_HOST')}:{os.getenv('POSTGRESQL_PORT')}/{os.getenv('POSTGRESQL_DATABASE')}", 27 | "modules" : { 28 | "models": ["chat.tortoise_models"] 29 | } 30 | } 31 | 32 | CHANNEL_LAYERS = { 33 | 'default': { 34 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 35 | 'CONFIG': { 36 | "hosts": [(os.getenv('REDIS_HOST'), os.getenv('REDIS_PORT'))], 37 | }, 38 | }, 39 | } 40 | 41 | WS_URL = "wss://demo.josnin.dev/django-whatsapp-clone/ws/chat" -------------------------------------------------------------------------------- /chat/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-08-23 08:56 2 | 3 | import django.contrib.auth.models 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 | ('auth', '0011_update_proxy_permissions'), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='ChatGroup', 19 | fields=[ 20 | ('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.Group')), 21 | ('description', models.TextField(blank=True, help_text='description of the group')), 22 | ('mute_notifications', models.BooleanField(default=False, help_text='disable notification if true')), 23 | ('icon', models.ImageField(blank=True, help_text='Group icon', upload_to='chatgroup')), 24 | ('date_created', models.DateTimeField(auto_now_add=True)), 25 | ('date_modified', models.DateTimeField(auto_now=True)), 26 | ], 27 | bases=('auth.group',), 28 | managers=[ 29 | ('objects', django.contrib.auth.models.GroupManager()), 30 | ], 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /chat/migrations/0002_auto_20200823_0905.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-08-23 09:05 2 | 3 | import django.contrib.auth.models 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('auth', '0011_update_proxy_permissions'), 12 | ('chat', '0001_initial'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='ChartGroup', 18 | fields=[ 19 | ('group_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.Group')), 20 | ('description', models.TextField(blank=True, help_text='description of the group')), 21 | ('mute_notifications', models.BooleanField(default=False, help_text='disable notification if true')), 22 | ('icon', models.ImageField(blank=True, help_text='Group icon', upload_to='chartgroup')), 23 | ('date_created', models.DateTimeField(auto_now_add=True)), 24 | ('date_modified', models.DateTimeField(auto_now=True)), 25 | ], 26 | bases=('auth.group',), 27 | managers=[ 28 | ('objects', django.contrib.auth.models.GroupManager()), 29 | ], 30 | ), 31 | migrations.DeleteModel( 32 | name='ChatGroup', 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /.github/workflows/django.yml: -------------------------------------------------------------------------------- 1 | name: Django CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | services: 14 | postgres: 15 | image: postgres 16 | # Provide the password for postgres 17 | env: 18 | POSTGRES_USER: postgres 19 | POSTGRES_PASSWORD: postgres 20 | POSTGRES_DB: chatdb 21 | # Set health checks to wait until postgres has started 22 | options: >- 23 | --health-cmd pg_isready 24 | --health-interval 10s 25 | --health-timeout 5s 26 | --health-retries 5 27 | ports: 28 | # Maps tcp port 5432 on service container to the host 29 | - 5432:5432 30 | strategy: 31 | max-parallel: 4 32 | matrix: 33 | python-version: [3.7] 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Set up Python ${{ matrix.python-version }} 38 | uses: actions/setup-python@v2 39 | with: 40 | python-version: ${{ matrix.python-version }} 41 | - name: Install Dependencies 42 | run: | 43 | python -m pip install --upgrade pip 44 | pip install -r requirements.txt 45 | - name: Run Tests 46 | env: 47 | SECRET_KEY: '*ht-$58ds7tp3+@ms0%*5d_a=a343$9ko&#pd+@34&3-3=dvkm' 48 | API_KEY: ${{ secrets.API_KEY }} 49 | DJANGO_SUPERUSER_PASSWORD: admin 50 | DJANGO_SUPERUSER_USERNAME: admin 51 | DJANGO_SUPERUSER_EMAIL: admin@gmail.com 52 | DJANGO_SETTINGS_MODULE: django_channel_tutorial.production 53 | POSTGRESQL_USER: postgres 54 | POSTGRESQL_PASSWORD: postgres 55 | POSTGRESQL_DATABASE: chatdb 56 | POSTGRESQL_HOST: 127.0.0.1 57 | POSTGRESQL_PORT: 5432 58 | run: | 59 | python manage.py migrate 60 | python manage.py createsuperuser --no-input 61 | python manage.py test 62 | -------------------------------------------------------------------------------- /chat/templates/chat/room_preview_image.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 |
3 | {% include 'chat/room_message_chat_header.html' %} 4 | 5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 38 |
39 | 40 |
41 | -------------------------------------------------------------------------------- /Jenkinsfile.Staging: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { label "sweetheart" } 3 | environment { 4 | MYHOME = "$HOME/Documents/django-whatsapp-web-clone" 5 | } 6 | stages { 7 | stage("Build") { 8 | steps { 9 | // 63791,54321 to avoid clash w. prod port 10 | sh """ 11 | git clone https://github.com/codingelle/django-whatsapp-web-clone.git 12 | podman pod create -n staging-pod -p 63791:6379 -p 54321:5432 13 | podman run --pod=staging-pod -d --name redis-staging-svc registry.redhat.io/rhel8/redis-5:1-160.1647451816 14 | podman run --pod=staging-pod -d --name psql-staging-svc --env-file=$MYHOME/.env.staging registry.redhat.io/rhel8/postgresql-12:1-99.1647451835 15 | """ 16 | //podman run --pod=staging-pod -d --name app-svc --env-file=$MYHOME/.env.staging localhost/django-app:latest 17 | } 18 | } 19 | 20 | stage("Test") { 21 | steps { 22 | sh 'podman run -u root --pod staging-pod --name app-staging-svc --env-file=$MYHOME/.env.staging -v $MYHOME/:/opt/app-root/src/ registry.access.redhat.com/ubi8/python-38 bash -c "pip install -U pip && pip install -r requirements.txt && python manage.py migrate && python manage.py createsuperuser --no-input && python manage.py test"' 23 | //sh 'podman run -u root --pod staging-pod --name app-svc --env-file=$MYHOME/.env.staging -v $MYHOME/:/opt/app-root/src/ registry.access.redhat.com/ubi8/python-38 bash -c "pip install -U pip && pip install -r requirements.txt && python manage.py migrate && python manage.py createsuperuser --no-input && daphne -b 0.0.0.0 -p 8080 django_channel_tutorial.asgi:application && python manage.py test"' 24 | } 25 | } 26 | 27 | stage("Deploy") { 28 | steps { 29 | echo "Deploy here" 30 | dir("$MYHOME") { 31 | //sh "git pull origin master" 32 | git credentialsId: 'GithubPassphrase', url: 'https://github.com/codingelle/django-whatsapp-web-clone' 33 | // restart all services? 34 | } 35 | } 36 | } 37 | } 38 | post { 39 | // Clean after build 40 | always { 41 | cleanWs() 42 | sh "podman pod rm staging-pod -f" 43 | } 44 | success { 45 | echo "Build success" 46 | } 47 | failure { 48 | echo "Build failed" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /chat/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.contrib.auth.decorators import login_required 3 | from django.urls import reverse 4 | from django.http import HttpResponseRedirect, JsonResponse 5 | from asgiref.sync import sync_to_async 6 | from tortoise import Tortoise 7 | from django.conf import settings 8 | 9 | # Create your views here. 10 | 11 | # chat/views.py 12 | from django.shortcuts import render 13 | from .models import ChatGroup 14 | from .tortoise_models import ChatMessage 15 | 16 | @login_required 17 | def index(request): 18 | return render(request, 'chat/index.html', {}) 19 | 20 | def get_participants(group_id=None, group_obj=None, user=None): 21 | """ function to get all participants that belong the specific group """ 22 | 23 | if group_id: 24 | chatgroup = ChatGroup.objects.get(id=id) 25 | else: 26 | chatgroup = group_obj 27 | 28 | temp_participants = [] 29 | for participants in chatgroup.user_set.values_list('username', flat=True): 30 | if participants != user: 31 | temp_participants.append(participants.title()) 32 | temp_participants.append('You') 33 | return ', '.join(temp_participants) 34 | 35 | 36 | @login_required 37 | def room(request, group_id): 38 | if request.user.groups.filter(id=group_id).exists(): 39 | chatgroup = ChatGroup.objects.get(id=group_id) 40 | #TODO: make sure user assigned to existing group 41 | assigned_groups = list(request.user.groups.values_list('id', flat=True)) 42 | groups_participated = ChatGroup.objects.filter(id__in=assigned_groups) 43 | return render(request, 'chat/room.html', { 44 | 'chatgroup': chatgroup, 45 | 'participants': get_participants(group_obj=chatgroup, user=request.user.username), 46 | 'groups_participated': groups_participated, 47 | 'GIPHY_URL': settings.GIPHY_URL, 48 | 'API_KEY': settings.API_KEY, 49 | 'WS_URL': settings.WS_URL 50 | }) 51 | else: 52 | return HttpResponseRedirect(reverse("chat:unauthorized")) 53 | 54 | @login_required 55 | def unauthorized(request): 56 | return render(request, 'chat/unauthorized.html', {}) 57 | 58 | 59 | async def history(request, room_id): 60 | 61 | await Tortoise.init(**settings.TORTOISE_INIT) 62 | chat_message = await ChatMessage.filter(room_id=room_id).order_by('date_created').values() 63 | await Tortoise.close_connections() 64 | 65 | return await sync_to_async(JsonResponse)(chat_message, safe=False) 66 | -------------------------------------------------------------------------------- /chat/consumers.py: -------------------------------------------------------------------------------- 1 | import json 2 | from channels.generic.websocket import AsyncWebsocketConsumer 3 | #from asgiref.sync import await_to_sync 4 | from chat.services import chat_save_message 5 | 6 | 7 | 8 | class ChatConsumer(AsyncWebsocketConsumer): 9 | """ handshake websocket front end """ 10 | 11 | room_name = None 12 | room_group_name = None 13 | 14 | async def connect(self): 15 | self.room_name = self.scope['url_route']['kwargs']['room_name'] 16 | self.room_group_name = 'chat_%s' % self.room_name 17 | 18 | # Join room group 19 | await self.channel_layer.group_add( 20 | self.room_group_name, 21 | self.channel_name 22 | ) 23 | 24 | await self.accept() 25 | 26 | 27 | async def disconnect(self, code): 28 | # Leave room group 29 | if self.room_group_name and self.channel_name: 30 | await self.channel_layer.group_discard( 31 | self.room_group_name, 32 | self.channel_name 33 | ) 34 | 35 | # Receive message from WebSocket 36 | async def receive(self, text_data=None, bytes_data=None): 37 | text_data_json = json.loads(text_data) 38 | message = text_data_json['message'] 39 | image_caption = text_data_json['image_caption'] 40 | message_type = text_data_json['message_type'] 41 | 42 | # Send message to room group 43 | await self.channel_layer.group_send( 44 | self.room_group_name, 45 | { 46 | 'type': 'chat_message', 47 | 'username': self.scope['user'].username.title(), 48 | 'message': message, 49 | 'message_type': message_type, 50 | 'image_caption': image_caption 51 | } 52 | ) 53 | 54 | await chat_save_message( 55 | username=self.scope['user'].username.title(), 56 | room_id=self.room_name, 57 | message=message, 58 | message_type=message_type, 59 | image_caption=image_caption 60 | ) 61 | 62 | 63 | # Receive message from room group 64 | async def chat_message(self, event): 65 | """ exhange message here """ 66 | message = event['message'] 67 | username = event['username'] 68 | image_caption = event['image_caption'] 69 | message_type = event['message_type'] 70 | 71 | # Send message to WebSocket 72 | await self.send(text_data=json.dumps({ 73 | 'message': message, 74 | 'username': username, 75 | 'image_caption': image_caption, 76 | 'message_type': message_type 77 | })) 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A WhatsApp Web Clone Chat Application for those developers that like to use Django Channel for handling WebSocket request 2 | 3 | [![Django CI](https://github.com/codingelle/django-whatsapp-web-clone/actions/workflows/django.yml/badge.svg)](https://github.com/codingelle/django-whatsapp-web-clone/actions/workflows/django.yml) 4 | 5 | ## Demo 6 | 7 | #### Login User1 8 | * Url: https://demo.josnin.dev/django-whatsapp-clone/admin/login/ (Use Chrome Browser) 9 | * Login: johnny2020 10 | * Pass: johnny2020 11 | 12 | #### Login User2 13 | * Url: https://demo.josnin.dev/django-whatsapp-clone/admin/login/ (Use Microsoft Edge or any browser except Chrome) 14 | * User: jay1234 15 | * Pass: jay1234 16 | 17 | #### Start Chat 18 | ##### Make sure to login using User1 or User2 19 | https://demo.josnin.dev/django-whatsapp-clone/chat/2/ 20 | 21 | 22 | 23 | ## Send GIFs by GIPHY 24 | 25 | ![ezgif-7-8f0423e40e28](https://user-images.githubusercontent.com/3206118/141478058-df2f4ebb-f7f1-4666-b084-a14bcb98634e.gif) 26 | 27 | 28 | ## Screenshot of 2 users exchanging message 29 | 30 | ![image](https://user-images.githubusercontent.com/3206118/91178093-3e144000-e717-11ea-9e3b-ef16b0c40ef0.png) 31 | 32 | 33 | ## Screenshot Sharing blob image 34 | ![image](https://user-images.githubusercontent.com/3206118/93725153-66407300-fbdf-11ea-9c6e-be0ddaab869d.png) 35 | 36 | 37 | ## Screenshot Loading & Save message 38 | ![image](https://user-images.githubusercontent.com/3206118/97063435-4df2b800-15d2-11eb-9ea9-abedad56a493.png) 39 | 40 | 41 | 42 | ## Installation 43 | ``` 44 | cd django-whatsapp-web-clone/ 45 | 46 | python3.7 -m venv env 47 | . env/bin/activate 48 | pip install -r requirements 49 | 50 | ``` 51 | 52 | ## How to run development server? 53 | 54 | #### create all the required tables 55 | ``` 56 | python manage.py migrate 57 | ``` 58 | 59 | #### create superuser 60 | ``` 61 | python manage.py createsuperuser 62 | ``` 63 | 64 | #### start redis service using podman 65 | ``` 66 | podman run -p 6379:6379 -d redis:5 67 | ``` 68 | 69 | ### create .env file 70 | 71 | add the following variable & replace it based on your own development keys 72 | 73 | API_KEY=YourOwnGiphYAPIKeysdfasjfdgdf 74 | 75 | SECRET_KEY=YourOwnSecretKey71041jkfohdslflasdfjhaljdfa 76 | 77 | 78 | #### run the development server 79 | ``` 80 | python3 manage.py runserver 81 | or 82 | daphne -b 0.0.0.0 -p 8088 django_channel_tutorial.asgi:application 83 | 84 | ``` 85 | 86 | ### Youtube video tutorial 87 | 88 | [Youtube](youtu.be/zv7ra-xw1mu) 89 | 90 | 91 | ### Help 92 | 93 | Need help? Open an issue in: [ISSUES](https://github.com/josnin/django-whatsapp-web-clone/issues) 94 | 95 | 96 | ### Contributing 97 | Want to improve and add feature? Fork the repo, add your changes and send a pull request. 98 | 99 | 100 | -------------------------------------------------------------------------------- /chat/templates/chat/room_message.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 |
3 | 4 | {% include 'chat/room_message_chat_header.html' %} 5 | 6 | 7 |
8 |
9 |
10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 |
40 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /Jenkinsfile.Deploy: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { label "sweetheart" } 3 | environment { 4 | MYHOME = "$HOME/Documents/django-whatsapp-web-clone" 5 | } 6 | stages { 7 | stage("Pod Svc") { 8 | steps { 9 | sh "podman pod create -n prod-pod -p 6379:6379 -p 8083:8083 -p 5432:5432 -p 8085:8080" 10 | } 11 | } 12 | stage("Redis Svc") { 13 | steps { 14 | // when the build is completed the svc still running but network port is dropped, 15 | // this will help JENKINS_NODE_COOKIE=dontKillMe 16 | sh """ 17 | JENKINS_NODE_COOKIE=dontKillMe podman run --pod=prod-pod -d --name redis-prod-svc \ 18 | --healthcheck-command 'CMD-SHELL redis-cli || exit 1' \ 19 | --healthcheck-start-period=2s \ 20 | --healthcheck-interval=10s \ 21 | --healthcheck-retries=3 \ 22 | registry.redhat.io/rhel8/redis-5:1-160.1647451816 23 | """ 24 | sh """ 25 | #!/bin/bash 26 | while [ "`podman healthcheck run redis-prod-svc`" = "unhealthy" ]; do 27 | sleep 3 28 | done 29 | """ 30 | } 31 | } 32 | stage("PSQL Svc") { 33 | steps { 34 | // when the build is completed the svc still running but network port is dropped, 35 | // this will help JENKINS_NODE_COOKIE=dontKillMe 36 | sh """ 37 | JENKINS_NODE_COOKIE=dontKillMe podman run --pod=prod-pod -d --name psql-prod-svc \ 38 | --healthcheck-command 'CMD-SHELL pg_isready || exit 1' \ 39 | --healthcheck-start-period=2s \ 40 | --healthcheck-interval=10s \ 41 | --healthcheck-retries=3 \ 42 | --env-file=$MYHOME/.env.production \ 43 | registry.redhat.io/rhel8/postgresql-12:1-99.1647451835 44 | """ 45 | sh """ 46 | #!/bin/bash 47 | while [ "`podman healthcheck run psql-prod-svc`" = "unhealthy" ]; do 48 | sleep 10 49 | done 50 | """ 51 | } 52 | } 53 | stage("App Svc") { 54 | steps { 55 | // when the build is completed the svc still running but network port is dropped, 56 | // this will help JENKINS_NODE_COOKIE=dontKillMe 57 | sh ''' 58 | JENKINS_NODE_COOKIE=dontKillMe podman run -u root --pod=prod-pod -d --name app-prod-svc \ 59 | --healthcheck-command "CMD-SHELL python manage.py test || exit 1" \ 60 | --healthcheck-interval=10s \ 61 | --healthcheck-start-period=10s \ 62 | --healthcheck-retries=3 \ 63 | --env-file=$MYHOME/.env.production -v $MYHOME:/opt/app-root/src/ \ 64 | registry.access.redhat.com/ubi8/python-38 bash -c "pip install -U pip && pip install -r requirements.txt && python manage.py migrate && python manage.py createsuperuser --no-input ; python manage.py runserver 0.0.0.0:8083" 65 | ''' 66 | sh """ 67 | #!/bin/bash 68 | while [ "`podman healthcheck run app-prod-svc`" = "unhealthy" ]; do 69 | sleep 15 70 | done 71 | """ 72 | } 73 | } 74 | stage("Nginx Svc") { 75 | steps { 76 | // when the build is completed the svc still running but network port is dropped, 77 | // this will help JENKINS_NODE_COOKIE=dontKillMe 78 | sh ''' 79 | JENKINS_NODE_COOKIE=dontKillMe podman run -u root -d --pod=prod-pod --name nginx-prod-svc \ 80 | --healthcheck-command "CMD-SHELL nginx -T || exit 1" \ 81 | --healthcheck-interval=5s \ 82 | --healthcheck-start-period=10s \ 83 | --healthcheck-retries=3 \ 84 | -v $MYHOME/nginx-cfg:/opt/app-root/etc/nginx.d/:Z \ 85 | -v $MYHOME:/opt/app-root/src/:Z \ 86 | registry.access.redhat.com/ubi8/nginx-118 nginx -g "daemon off;" 87 | ''' 88 | sh """ 89 | #!/bin/bash 90 | while [ "`podman healthcheck run nginx-prod-svc`" = "unhealthy" ]; do 91 | sleep 3 92 | done 93 | """ 94 | 95 | } 96 | } 97 | } 98 | post { 99 | // Clean after build 100 | always { 101 | cleanWs() 102 | } 103 | success { 104 | echo "Build success" 105 | } 106 | failure { 107 | echo "Build failed" 108 | } 109 | aborted { 110 | echo "Build aborted" 111 | sh "podman pod rm prod-pod -f" 112 | } 113 | } 114 | } 115 | 116 | 117 | -------------------------------------------------------------------------------- /django_channel_tutorial/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_channel_tutorial project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.0.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | from dotenv import load_dotenv 15 | 16 | load_dotenv('.env') 17 | 18 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 20 | 21 | 22 | # Quick-start development settings - unsuitable for production 23 | # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ 24 | 25 | # SECURITY WARNING: keep the secret key used in production secret! 26 | SECRET_KEY = os.getenv('SECRET_KEY') 27 | 28 | # SECURITY WARNING: don't run with debug turned on in production! 29 | DEBUG = True 30 | 31 | ALLOWED_HOSTS = ['*'] 32 | 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = [ 37 | 'django.contrib.admin', 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.messages', 42 | 'django.contrib.staticfiles', 43 | 'sslserver', 44 | 'channels', 45 | 'chat' 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | ] 57 | 58 | ROOT_URLCONF = 'django_channel_tutorial.urls' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'DIRS': [os.path.join(BASE_DIR, 'django_channel_tutorial/templates')], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | WSGI_APPLICATION = 'django_channel_tutorial.wsgi.application' 77 | 78 | 79 | # Database 80 | # https://docs.djangoproject.com/en/3.0/ref/settings/#databases 81 | 82 | DATABASES = { 83 | 'default': { 84 | 'ENGINE': 'django.db.backends.sqlite3', 85 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 | } 87 | } 88 | 89 | 90 | # Password validation 91 | # https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators 92 | 93 | AUTH_PASSWORD_VALIDATORS = [ 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 99 | }, 100 | { 101 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 102 | }, 103 | { 104 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 105 | }, 106 | ] 107 | 108 | 109 | # Internationalization 110 | # https://docs.djangoproject.com/en/3.0/topics/i18n/ 111 | 112 | LANGUAGE_CODE = 'en-us' 113 | 114 | TIME_ZONE = 'UTC' 115 | 116 | USE_I18N = True 117 | 118 | USE_L10N = True 119 | 120 | USE_TZ = True 121 | 122 | 123 | # Static files (CSS, JavaScript, Images) 124 | # https://docs.djangoproject.com/en/3.0/howto/static-files/ 125 | 126 | STATIC_URL = '/static/' 127 | STATICFILES_DIRS = [os.path.join(BASE_DIR, 'django_channel_tutorial/static')] 128 | 129 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 130 | 131 | ASGI_APPLICATION = "django_channel_tutorial.routing.application" 132 | CHANNEL_LAYERS = { 133 | 'default': { 134 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 135 | 'CONFIG': { 136 | "hosts": [('127.0.0.1', 6379)], 137 | }, 138 | }, 139 | } 140 | 141 | MEDIA_URL = '/media/' 142 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 143 | 144 | 145 | TORTOISE_INIT = { 146 | "db_url": "sqlite://db.sqlite3.tortoise", 147 | "modules" : { 148 | "models": ["chat.tortoise_models"] 149 | } 150 | } 151 | 152 | GIPHY_URL = 'https://api.giphy.com/v1/gifs' 153 | API_KEY = os.getenv('API_KEY') 154 | 155 | WS_URL = "ws://127.0.0.1:8088/ws/chat" -------------------------------------------------------------------------------- /chat/static/chat/style.css: -------------------------------------------------------------------------------- 1 | 2 | .main-container { 3 | display: grid; 4 | grid-template-columns: 2.2fr 4fr; 5 | width: 100vw; 6 | } 7 | 8 | .room-sidebar { 9 | justify-self: left; 10 | display: grid; 11 | grid-template-columns: 1fr; 12 | grid-template-rows: 59px 89px 50px repeat(auto-fit, minmax(73px, 73px)); 13 | width:100%; 14 | border-right:.75px solid #CCC; 15 | } 16 | 17 | .room-sidebar-header { 18 | background-color: #f0f0f0ff; 19 | display: grid; 20 | grid-template-columns: 48px auto 37px 37px 37px; 21 | grid-template-areas: "sb-avatar . sb-status sb-new-chat sb-menu" ; 22 | gap: 1rem; 23 | align-items:center; 24 | padding:.5rem; 25 | } 26 | 27 | 28 | .sb-avatar { 29 | grid-area: sb-avatar; 30 | } 31 | 32 | .sb-avatar img { 33 | width:48px; 34 | height: 48px; 35 | object-fit: cover; 36 | border-radius: 50%; 37 | } 38 | 39 | .sb-status { 40 | grid-area: sb-status; 41 | } 42 | 43 | .sb-new-chat { 44 | grid-area: sb-new-chat; 45 | } 46 | 47 | .sb-menu { 48 | grid-area: sb-menu; 49 | } 50 | 51 | .room-sidebar-get-notified { 52 | display:grid; 53 | grid-template-columns: 50px auto; 54 | background-color: #9DE1FE; 55 | align-items: center; 56 | padding: .5rem; 57 | } 58 | 59 | .room-sidebar-get-notified-alert { 60 | justify-self: center; 61 | } 62 | 63 | .room-sidebar-get-notified-msg { 64 | padding:.5rem; 65 | justify-self: left; 66 | display: grid; 67 | grid-template-columns: auto; 68 | grid-template-rows: 1fr 1fr; 69 | gap: .20rem; 70 | max-width: 442px; 71 | } 72 | 73 | .room-sidebar-get-notified-msg1 { 74 | font-size: 1rem; 75 | color: #303030F5; 76 | white-space: nowrap; 77 | } 78 | 79 | .room-sidebar-get-notified-msg2 { 80 | font-size: .87rem; 81 | display: grid; 82 | grid-template-columns: auto 20px; 83 | gap: .25rem; 84 | white-space: nowrap; 85 | } 86 | 87 | .room-sidebar-get-notified-arrow { 88 | align-self: end; 89 | } 90 | 91 | .room-sidebar-get-notified-msg2 a { 92 | text-decoration: none; 93 | color: #303030D9; 94 | } 95 | 96 | .room-sidebar-get-notified-msg2 a:hover { 97 | text-decoration: underline; 98 | } 99 | 100 | /* room side bar search or new chat */ 101 | .room-sidebar-search-new-chat { 102 | display: grid; 103 | align-items: center; 104 | background-color: #f0f0f0ff; 105 | width: 100%; 106 | padding: 0.40rem .62rem ; 107 | } 108 | 109 | .room-sidebar-search-new-chat2 { 110 | display:grid; 111 | grid-template-columns: 50px auto; 112 | box-sizing: border-box; 113 | outline:none; 114 | border-radius: 15px; 115 | background-color: white; 116 | } 117 | 118 | .room-sidebar-search { 119 | align-self: end; 120 | justify-self: center; 121 | } 122 | 123 | .room-sidebar-new-chat ::placeholder { 124 | font-size:.90rem; 125 | color: #a6a6a6; 126 | } 127 | 128 | .room-sidebar-new-chat input[type=text] { 129 | width: 90%; 130 | box-sizing: border-box; 131 | outline:none; 132 | border:0; 133 | padding: 9px 6px; 134 | } 135 | /* room side bar search or new chat */ 136 | 137 | 138 | /* room side bar groups */ 139 | .room-sidebar-groups { 140 | display:grid; 141 | grid-template-columns: 49px auto 65px; 142 | grid-template-areas: "g-img g-msg g-time"; 143 | height:73px; 144 | border-bottom:.75px solid #EEE; 145 | padding: .5rem; 146 | align-items: center; 147 | cursor: pointer; 148 | } 149 | 150 | .room-sidebar-groups-g-img { 151 | grid-area: g-img; 152 | } 153 | 154 | .room-sidebar-groups-g-img img { 155 | width:50px; 156 | height: 50px; 157 | object-fit: cover; 158 | border-radius: 50%; 159 | } 160 | 161 | .room-sidebar-groups-g-msg { 162 | grid-area: g-msg; 163 | display:grid; 164 | padding: .5rem; 165 | } 166 | 167 | .room-sidebar-groups-g-msg1 { 168 | font-size: 1.062rem; 169 | color: #303030F5; 170 | } 171 | 172 | .room-sidebar-groups-g-msg2 { 173 | font-size: .87rem; 174 | white-space: nowrap; 175 | text-overflow: ellipsis; 176 | overflow:hidden; 177 | } 178 | 179 | .room-sidebar-groups-g-time { 180 | grid-area: g-time; 181 | font-size: .70rem; 182 | color: #a2a2a2; 183 | align-self: start; 184 | justify-self: end; 185 | padding: .5rem; 186 | } 187 | /* room side bar groups */ 188 | 189 | .room-container__msg { 190 | display:grid; 191 | width: 100%; 192 | min-height: 99vh; 193 | height:99vh; 194 | grid-template-columns: 1fr; 195 | grid-template-rows: 59px auto 0px 62px; /* dynamically change to toggle giphy search */ 196 | grid-template-areas: "msg-header" 197 | "msg-box" 198 | "giphy-search" 199 | "msg-input"; 200 | overflow: hidden; /* hide unnecessary scroll */ 201 | } 202 | 203 | .room-container__preview-img { 204 | display:grid; 205 | width: 100%; 206 | height:99vh; 207 | grid-template-rows: 59px auto; 208 | grid-template-areas: "msg-header" 209 | "preview-msg-box"; 210 | } 211 | 212 | .preview-img-box { 213 | grid-area: preview-msg-box; 214 | } 215 | 216 | .msg-header { 217 | grid-area: msg-header; 218 | background-color: #EDEDED; 219 | display: grid; 220 | grid-template-columns: 42px auto 37px 37px; 221 | grid-template-areas: "h-avatar h-users h-search h-menu" ; 222 | align-items:center; 223 | gap: .7rem; 224 | padding: 0 1rem; 225 | border-bottom:.5px solid #CCC; 226 | } 227 | 228 | .h-avatar { 229 | grid-area: h-avatar; 230 | } 231 | 232 | .h-avatar img { 233 | width:42px; 234 | height: 42px; 235 | object-fit: cover; 236 | border-radius: 50%; 237 | } 238 | 239 | .h-users { 240 | grid-area: h-users; 241 | display:grid; 242 | grid-template-columns: 1fr; 243 | grid-template-rows: 1fr 1fr; 244 | grid-template-areas: "h-user-group" 245 | "h-user-member"; 246 | white-space: nowrap; 247 | } 248 | 249 | .h-user-group { 250 | grid-area: h-user-group; 251 | font-size: 1rem; 252 | overflow:hidden; 253 | text-overflow: ellipsis; 254 | } 255 | 256 | .h-user-member { 257 | grid-area: h-user-member; 258 | font-size: .812rem; 259 | color: #606060ff; 260 | overflow:hidden; 261 | text-overflow: ellipsis; 262 | } 263 | 264 | .h-search { 265 | grid-area: h-search; 266 | } 267 | 268 | svg { 269 | fill: #919191ff; 270 | cursor: pointer; 271 | } 272 | 273 | 274 | .h-menu { 275 | grid-area: h-menu; 276 | } 277 | 278 | .msg-box { 279 | grid-area: msg-box; 280 | padding-left:4rem; 281 | padding-right:4rem; 282 | } 283 | 284 | .giphy-search-show { 285 | grid-template-rows: 59px auto 300px 62px; 286 | } 287 | 288 | .giphy-search { 289 | grid-area: giphy-search; 290 | display: grid; 291 | grid-template-rows: 62px auto; 292 | grid-template-columns: 1fr; 293 | grid-template-areas: "ginput" 294 | "gresults"; 295 | padding: 1rem; 296 | } 297 | 298 | .giphy-search__input input[type=text] { 299 | display: grid; 300 | grid-area: ginput; 301 | width: 100%; 302 | background-color: #e7e5e5; 303 | padding: 10px 12px; 304 | box-sizing: border-box; 305 | outline:none; 306 | border-radius: 4px; 307 | border:0; 308 | font-size: .93rem; 309 | } 310 | 311 | .giphy-search__results { 312 | display: grid; 313 | grid-area: gresults; 314 | overflow-y: auto; 315 | overflow-x: hidden; 316 | gap: .5rem; 317 | grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); 318 | max-height: 350px; 319 | } 320 | 321 | .msg-box--background { 322 | background: #efe7dd url("https://cloud.githubusercontent.com/assets/398893/15136779/4e765036-1639-11e6-9201-67e728e86f39.jpg") repeat; 323 | overflow-y: scroll; 324 | } 325 | 326 | .preview-img { 327 | display: grid; 328 | padding:0; 329 | grid-template-rows: 49px 1fr 40px 1px 100px; 330 | gap: 1rem; 331 | } 332 | 333 | .preview-img__header { 334 | background-color: #E6E6E6; 335 | display: grid; 336 | grid-template-columns: 42px auto; 337 | align-items: center; 338 | align-content: center; 339 | padding-top: 1.9rem; 340 | padding-bottom: 1.9rem; 341 | padding-left: 1rem; 342 | } 343 | 344 | .preview-img__header__close { 345 | display: grid; /* need this to remove svg padding bottom */ 346 | } 347 | 348 | 349 | .preview-img__snipped-big { 350 | width: 100%; 351 | height: 100%; 352 | padding: 1rem; 353 | display: grid; 354 | justify-content: center; 355 | } 356 | 357 | .preview-img__snipped-big__border { 358 | display:grid; 359 | justify-content: center; 360 | } 361 | 362 | .preview-img__snipped-big__border img { 363 | height:100%; 364 | object-fit: cover; 365 | } 366 | 367 | 368 | .preview-img__caption { 369 | justify-self: center; 370 | width: 75%; 371 | display: grid; 372 | } 373 | 374 | .preview-img__caption input[type="text"] { 375 | width: 100%; 376 | box-sizing: border-box; 377 | outline:none; 378 | border:0; 379 | font-size: 1rem; 380 | border-radius: 15px; 381 | background-color: #F0F0F0; 382 | padding: 12px 12px 11px; 383 | } 384 | 385 | .preview-img__line { 386 | border-bottom: 1.5px solid #F0F0F0; 387 | width: 90%; 388 | display: grid; 389 | height: 0; 390 | justify-self: center; 391 | } 392 | 393 | 394 | .preview-img__footer { 395 | /* background-color: #d9d9d9ff; */ 396 | display: grid; 397 | grid-template-columns: 1fr 54px 54px 1fr; 398 | grid-template-areas: ". img-small add-file btn-img-send"; 399 | gap: .5rem; 400 | padding: 1rem; 401 | } 402 | 403 | .preview-img__footer__img-small { 404 | padding:2px; 405 | grid-area: img-small; 406 | width: 100%; 407 | height: 100%; 408 | } 409 | 410 | .preview-img__footer__img-small img { 411 | height: 100%; 412 | width: 100%; 413 | object-fit: cover; 414 | } 415 | 416 | .preview-img__footer__addfile { 417 | border: 1px solid #CCC; 418 | border-radius: 5px; 419 | display: grid; 420 | grid-area: add-file; 421 | align-items: center; 422 | justify-items: center; 423 | gap: .2rem; 424 | } 425 | 426 | 427 | .preview-img__footer__button { 428 | justify-self: end; 429 | grid-area: btn-img-send; 430 | padding: 0; 431 | background-color: #09e85eff; 432 | border-radius: 50%; 433 | box-shadow: 0 1px 3px rgba(0,0,0,.4); 434 | width: 60px; 435 | height: 60px; 436 | display:grid; 437 | align-items: center; 438 | justify-items: center; 439 | 440 | } 441 | 442 | .preview-img__footer__button-arrow { 443 | display: grid; 444 | } 445 | 446 | 447 | .msg-input { 448 | grid-area: msg-input; 449 | padding: 1rem; 450 | display: grid; 451 | grid-template-columns: 40px 40px 40px auto 40px; 452 | grid-template-areas: "close emoji gif type-a-message mic"; 453 | align-content: center; 454 | background-color: #f0f0f0ff; 455 | } 456 | 457 | .close { 458 | grid-area: close; 459 | padding: 0.5rem; 460 | justify-self: center; 461 | align-self:center; 462 | } 463 | 464 | .emoji { 465 | grid-area: emoji; 466 | padding: 0.5rem; 467 | justify-self: center; 468 | align-self:center; 469 | } 470 | 471 | .gif { 472 | grid-area: gif; 473 | padding: 0.5rem; 474 | justify-self: center; 475 | align-self:center; 476 | } 477 | 478 | .type-a-message { 479 | grid-area: type-a-message; 480 | padding: 0.5rem; 481 | } 482 | 483 | .mic { 484 | grid-area: mic; 485 | padding: 0.5rem; 486 | justify-self: center; 487 | align-self:center; 488 | } 489 | 490 | 491 | .right-msg-container { 492 | display:grid; 493 | grid-template-columns: 1fr 8px; 494 | padding: .3rem; 495 | } 496 | 497 | .left-msg-container { 498 | display:grid; 499 | grid-template-columns: 8px 1fr; 500 | padding: .3rem; 501 | } 502 | 503 | .s-tail { 504 | display: grid; /* tail need to be align start */ 505 | } 506 | 507 | .r-tail { 508 | /* justify-self:left; */ 509 | display: grid; /* tail need to be align start */ 510 | } 511 | 512 | .s-message { 513 | padding: 0.5rem; 514 | display: grid; 515 | grid-template-columns: 1fr; 516 | grid-template-rows: 1fr 20px; 517 | grid-template-areas: "s-msg" 518 | "s-time"; 519 | 520 | justify-self:right; 521 | background-color: #dcf8c6ff; 522 | border-radius: 7.5px; 523 | border-top-right-radius: 0; 524 | /*box-shadow: 0 1px .5px rgb(31, 30, 30),.13); */ 525 | box-shadow: 0 1px .5px rgba(0,0,0,.2); 526 | } 527 | 528 | .r-message { 529 | padding: 0.5rem; 530 | display: grid; 531 | grid-template-columns: 1fr; 532 | grid-template-rows: 20px 1fr; 533 | grid-template-areas: "r-user" 534 | "r-msg" 535 | "r-time"; 536 | 537 | justify-self:left; 538 | background-color: white; 539 | border-radius: 7.5px; 540 | border-top-left-radius: 0; 541 | /*box-shadow: 0 1px .5px rgb(31, 30, 30),.13); */ 542 | box-shadow: 0 1px .5px rgba(0,0,0,.2); 543 | } 544 | 545 | .s-msg { 546 | grid-area: s-msg; 547 | font-size: .90rem; 548 | } 549 | 550 | .s-msg img { 551 | max-width: 330px; 552 | max-height: 249px; 553 | } 554 | 555 | .s-time { 556 | grid-area: s-time; 557 | font-size: .70rem; 558 | justify-self: right; 559 | color: #a2a2a2; 560 | } 561 | 562 | .r-user a { 563 | text-decoration: none; 564 | color: #72dbb5ff; 565 | } 566 | 567 | .r-user a:hover { 568 | text-decoration: underline; 569 | color: #72dbb5ff; 570 | } 571 | 572 | .r-user { 573 | grid-area: r-user; 574 | font-size: .9rem; 575 | font-weight: bold; 576 | } 577 | 578 | .r-msg { 579 | grid-area: r-msg; 580 | font-size: .90rem; 581 | } 582 | 583 | .r-msg img { 584 | max-width: 330px; 585 | max-height: 249px; 586 | } 587 | 588 | .r-time { 589 | grid-area: r-time; 590 | font-size: .70rem; 591 | justify-self: right; 592 | color: #a2a2a2; 593 | } 594 | 595 | 596 | .type-a-message input[type="text"] { 597 | width: 100%; 598 | padding: 9px 12px 11px; 599 | box-sizing: border-box; 600 | outline:none; 601 | border-radius: 15px; 602 | border:0; 603 | font-size: .93rem; 604 | } 605 | 606 | .date_weekday { 607 | display: grid; 608 | justify-self: center; 609 | padding: 5px 12px 6px; 610 | font-size: 0.78rem; 611 | background-color: #e1f3faff; 612 | border-radius: 7.5px; 613 | width: fit-content; 614 | box-shadow: 0 1px .5px rgba(0,0,0,.13); 615 | line-height: 21px; 616 | } 617 | -------------------------------------------------------------------------------- /chat/templates/chat/room.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% load static %} 3 | 4 | {% block app_css %} 5 | 6 | {% endblock app_css %} 7 | 8 | {% block content %} 9 |
10 |
11 | 12 |
13 |
14 | Default Profile 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 |
33 |
34 | Get notified of new messages 35 | Turn on desktop notifications 36 | 37 | 38 | 39 | 40 |
41 |
42 | 43 | 44 | 45 |
46 |
47 | 50 |
51 |
52 |
53 |
54 |
55 | 56 | 57 | 58 | {% for grp in groups_participated %} 59 |
60 |
61 | {% if grp.icon %} 62 | 63 | {% else %} 64 | Default Profile 65 | {% endif %} 66 |
67 |
68 | {{grp.name|title}} 69 | {{grp.description|title}} 70 |
71 |
72 |
73 | {% endfor %} 74 | 75 | 76 |
77 | 78 |
79 | {% include 'chat/room_message.html' %} 80 |
81 | 82 | 85 | 86 |
87 | 88 | 362 | {% endblock content %} 363 | --------------------------------------------------------------------------------