├── home ├── tests.py ├── __pycache__ │ ├── admin.cpython-37.pyc │ ├── apps.cpython-37.pyc │ ├── forms.cpython-37.pyc │ ├── models.cpython-37.pyc │ ├── urls.cpython-37.pyc │ ├── views.cpython-37.pyc │ ├── __init__.cpython-37.pyc │ ├── routing.cpython-37.pyc │ ├── signals.cpython-37.pyc │ └── consumers.cpython-37.pyc ├── migrations │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ └── 0001_initial.cpython-37.pyc │ └── 0001_initial.py ├── apps.py ├── routing.py ├── admin.py ├── urls.py ├── signals.py ├── templates │ └── home │ │ ├── logout.html │ │ ├── login.html │ │ ├── register.html │ │ ├── search_user.html │ │ ├── add_members.html │ │ ├── profile.html │ │ ├── group_profile.html │ │ ├── home.html │ │ └── group.html ├── forms.py ├── consumers.py ├── models.py └── views.py ├── readme images ├── group.png ├── home.png ├── login.png ├── register.png ├── search_user.png ├── user_profile.png └── group_profile.png ├── chat_group ├── __pycache__ │ ├── urls.cpython-37.pyc │ ├── routing.cpython-37.pyc │ ├── __init__.cpython-37.pyc │ └── settings.cpython-37.pyc ├── routing.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── static └── home │ ├── css │ ├── style_login.css │ ├── style_profile.css │ ├── style_add_members.css │ ├── style_home.css │ └── style.css │ └── javascript │ ├── main_home.js │ ├── main.js │ └── reconnecting-websocket.js ├── manage.py ├── requirements.txt └── README.md /home/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /readme images/group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/readme images/group.png -------------------------------------------------------------------------------- /readme images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/readme images/home.png -------------------------------------------------------------------------------- /readme images/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/readme images/login.png -------------------------------------------------------------------------------- /readme images/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/readme images/register.png -------------------------------------------------------------------------------- /readme images/search_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/readme images/search_user.png -------------------------------------------------------------------------------- /readme images/user_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/readme images/user_profile.png -------------------------------------------------------------------------------- /readme images/group_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/readme images/group_profile.png -------------------------------------------------------------------------------- /home/__pycache__/admin.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/__pycache__/admin.cpython-37.pyc -------------------------------------------------------------------------------- /home/__pycache__/apps.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/__pycache__/apps.cpython-37.pyc -------------------------------------------------------------------------------- /home/__pycache__/forms.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/__pycache__/forms.cpython-37.pyc -------------------------------------------------------------------------------- /home/__pycache__/models.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/__pycache__/models.cpython-37.pyc -------------------------------------------------------------------------------- /home/__pycache__/urls.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/__pycache__/urls.cpython-37.pyc -------------------------------------------------------------------------------- /home/__pycache__/views.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/__pycache__/views.cpython-37.pyc -------------------------------------------------------------------------------- /home/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /home/__pycache__/routing.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/__pycache__/routing.cpython-37.pyc -------------------------------------------------------------------------------- /home/__pycache__/signals.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/__pycache__/signals.cpython-37.pyc -------------------------------------------------------------------------------- /chat_group/__pycache__/urls.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/chat_group/__pycache__/urls.cpython-37.pyc -------------------------------------------------------------------------------- /home/__pycache__/consumers.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/__pycache__/consumers.cpython-37.pyc -------------------------------------------------------------------------------- /chat_group/__pycache__/routing.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/chat_group/__pycache__/routing.cpython-37.pyc -------------------------------------------------------------------------------- /chat_group/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/chat_group/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /chat_group/__pycache__/settings.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/chat_group/__pycache__/settings.cpython-37.pyc -------------------------------------------------------------------------------- /home/migrations/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/migrations/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /home/migrations/__pycache__/0001_initial.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RudreshVeerkhare/GroupChat/HEAD/home/migrations/__pycache__/0001_initial.cpython-37.pyc -------------------------------------------------------------------------------- /home/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HomeConfig(AppConfig): 5 | name = "home" 6 | 7 | def ready(self): 8 | import home.signals 9 | 10 | -------------------------------------------------------------------------------- /home/routing.py: -------------------------------------------------------------------------------- 1 | # chat/routing.py 2 | from django.urls import path 3 | 4 | from .consumers import ChatConsumer 5 | 6 | websocket_urlpatterns = [path("ws/chat//", ChatConsumer)] 7 | 8 | -------------------------------------------------------------------------------- /chat_group/routing.py: -------------------------------------------------------------------------------- 1 | from channels.auth import AuthMiddlewareStack 2 | from channels.routing import ProtocolTypeRouter, URLRouter 3 | import home.routing 4 | 5 | application = ProtocolTypeRouter( 6 | {"websocket": AuthMiddlewareStack(URLRouter(home.routing.websocket_urlpatterns))} 7 | ) 8 | 9 | -------------------------------------------------------------------------------- /chat_group/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI entrypoint. Configures Django and then runs the application 3 | defined in the ASGI_APPLICATION setting. 4 | """ 5 | 6 | import os 7 | import django 8 | from channels.routing import get_default_application 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chat_group.settings") 11 | django.setup() 12 | application = get_default_application() 13 | -------------------------------------------------------------------------------- /home/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Group, Messages, Profile, GroupProfile 3 | 4 | # To register group on admin page 5 | admin.site.register(Group) 6 | 7 | # To register Messages on admin page 8 | admin.site.register(Messages) 9 | 10 | # To register profile model 11 | admin.site.register(Profile) 12 | 13 | # To register group_profile model 14 | admin.site.register(GroupProfile) 15 | -------------------------------------------------------------------------------- /static/home/css/style_login.css: -------------------------------------------------------------------------------- 1 | body.bgc{ 2 | background-color: #32465a; 3 | } 4 | 5 | li:hover{ 6 | background-color: #32465a36; 7 | } 8 | 9 | #form-div{ 10 | width:500px; 11 | height:auto; 12 | padding:20px 20px; 13 | background-color : #e9ecef; 14 | border:2px solid #17a2b8; 15 | border-radius:10px; 16 | } 17 | 18 | .asteriskField{ 19 | display:none; 20 | } -------------------------------------------------------------------------------- /chat_group/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for chat_group 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/2.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', 'chat_group.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /home/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | 5 | app_name = "home" 6 | urlpatterns = [ 7 | path("", views.home, name="home"), 8 | path("", views.group, name="group"), 9 | path("profile/", views.profile, name="profile"), 10 | path("group_profile/", views.group_profile, name="group_profile"), 11 | path("search_user/", views.search_user, name="search_user"), 12 | path("/add_members", views.add_member, name="add_members"), 13 | ] 14 | -------------------------------------------------------------------------------- /static/home/javascript/main_home.js: -------------------------------------------------------------------------------- 1 | var coll = document.getElementsByClassName("collapse-button"); 2 | var i; 3 | 4 | for (i = 0; i < coll.length; i++) { 5 | coll[i].addEventListener("click", function () { 6 | this.classList.toggle("active"); 7 | var content = document.getElementsByClassName("collapse-content")[0]; 8 | if (content.style.maxHeight) { 9 | content.style.maxHeight = null; 10 | } else { 11 | content.style.maxHeight = content.scrollHeight + "px"; 12 | } 13 | }); 14 | } 15 | 16 | $(".expand-button").click(function () { 17 | $("#profile").toggleClass("expanded"); 18 | $("#contacts").toggleClass("expanded"); 19 | }); -------------------------------------------------------------------------------- /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', 'chat_group.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 | -------------------------------------------------------------------------------- /home/signals.py: -------------------------------------------------------------------------------- 1 | from .models import Profile, Group, GroupProfile 2 | from django.contrib.auth.models import User 3 | from django.db.models.signals import post_save 4 | from django.dispatch import receiver 5 | 6 | 7 | @receiver(post_save, sender=User) 8 | def create_profile(sender, instance, created, **kwargs): 9 | if created: 10 | Profile.objects.create(user=instance) 11 | 12 | 13 | @receiver(post_save, sender=User) 14 | def save_profile(sender, instance, **kwargs): 15 | instance.profile.save() 16 | 17 | 18 | @receiver(post_save, sender=Group) 19 | def create_group_profile(sender, instance, created, **kwargs): 20 | if created: 21 | GroupProfile.objects.create(group=instance) 22 | 23 | 24 | @receiver(post_save, sender=Group) 25 | def save_group_profile(sender, instance, **kwargs): 26 | instance.group_profile.save() 27 | 28 | -------------------------------------------------------------------------------- /static/home/css/style_profile.css: -------------------------------------------------------------------------------- 1 | #img-div{ 2 | height:300px; 3 | width:300px; 4 | margin-bottom:20px; 5 | } 6 | body.bgc{ 7 | background-color: #32465a; 8 | } 9 | 10 | li:hover{ 11 | background-color: #32465a36; 12 | } 13 | 14 | .asteriskField{ 15 | display : none; 16 | } 17 | 18 | .preview{ 19 | white-space: nowrap; 20 | overflow: hidden; 21 | margin: 5px 0 0 0; 22 | padding: 0 0 1px; 23 | } 24 | 25 | .group-box{ 26 | margin : 10px; 27 | } 28 | 29 | .account-img { 30 | height: 75px; 31 | width: 75px; 32 | margin-right: 20px; 33 | margin-bottom: 16px; 34 | } 35 | 36 | .account-heading { 37 | font-size: 1.5rem; 38 | color: black; 39 | } 40 | 41 | .profile-heading { 42 | font-size: 2.5rem; 43 | color: black; 44 | } 45 | 46 | a{ 47 | text-decoration: none !important; 48 | color:inherit; 49 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis==1.2.0 2 | asgiref==3.1.2 3 | asn1crypto==0.24.0 4 | async-timeout==3.0.1 5 | attrs==19.1.0 6 | autobahn==19.6.2 7 | Automat==0.7.0 8 | boto3==1.9.174 9 | botocore==1.12.174 10 | cffi==1.12.3 11 | channels==2.2.0 12 | channels-redis==2.4.0 13 | constantly==15.1.0 14 | cryptography==3.3.2 15 | daphne==2.3.0 16 | dj-database-url==0.5.0 17 | Django==2.2.18 18 | django-crispy-forms==1.7.2 19 | django-heroku==0.3.1 20 | django-storages==1.7.1 21 | docutils==0.14 22 | h2==3.1.0 23 | hiredis==1.0.0 24 | hpack==3.0.0 25 | hyperframe==5.2.0 26 | hyperlink==19.0.0 27 | idna==2.8 28 | incremental==17.5.0 29 | jmespath==0.9.4 30 | msgpack==0.6.1 31 | Pillow==8.1.1 32 | priority==1.3.0 33 | psycopg2==2.8.3 34 | psycopg2-binary==2.8.3 35 | pyasn1==0.4.5 36 | pyasn1-modules==0.2.5 37 | pycparser==2.19 38 | PyHamcrest==1.9.0 39 | pyOpenSSL==19.0.0 40 | python-dateutil==2.8.0 41 | pytz==2019.1 42 | s3transfer==0.2.1 43 | service-identity==18.1.0 44 | six==1.12.0 45 | sqlparse==0.3.0 46 | Twisted==20.3.0 47 | txaio==18.8.1 48 | urllib3==1.25.3 49 | whitenoise==4.1.2 50 | zope.interface==4.6.0 51 | -------------------------------------------------------------------------------- /home/templates/home/logout.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load crispy_forms_tags %} 3 | 4 | 5 | 6 | GroupChat 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

You Have Been Logged Out!

19 |

Login Again To Continue

20 | 21 | Login Now 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /chat_group/urls.py: -------------------------------------------------------------------------------- 1 | """chat_group URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.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 | from home import views as home_views 19 | from django.contrib.auth import views as auth_views 20 | from django.conf import settings 21 | from django.conf.urls.static import static 22 | 23 | urlpatterns = [ 24 | path("admin/", admin.site.urls), 25 | path("home/", include("home.urls")), 26 | path("register/", home_views.register, name="register"), 27 | path( 28 | "", auth_views.LoginView.as_view(template_name="home/login.html"), name="login" 29 | ), 30 | path( 31 | "logout/", 32 | auth_views.LogoutView.as_view(template_name="home/logout.html"), 33 | name="logout", 34 | ), 35 | ] 36 | 37 | 38 | if settings.DEBUG: 39 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 40 | -------------------------------------------------------------------------------- /home/templates/home/login.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load crispy_forms_tags %} 3 | 4 | 5 | 6 | GroupChat 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

Welcome To GroupChat

19 |

Please login to enjoy service

20 | 21 |
22 |
23 |
24 |
25 | {% csrf_token %} 26 |
27 | Log In 28 | {{ form|crispy }} 29 |
30 |
31 | 32 |
33 |
34 |
35 | 36 | Need An Account? Sign Up Now 37 | 38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /home/templates/home/register.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load crispy_forms_tags %} 3 | 4 | 5 | 6 | GroupChat 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

Welcome To GroupChat

19 |

Create An Account To Get Started

20 | 21 |
22 |
23 |
24 |
25 | {% csrf_token %} 26 |
27 | Join Today 28 | {{ form|crispy }} 29 |
30 |
31 | 32 |
33 |
34 |
35 | 36 | Already Have An Account? Sign In 37 | 38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GroupChat 2 | This is my another vacation project which is group chatting website.This website is based on Django python web framework and application of Websockets.Websockets are implemented with Django using django-channels and channel-redis.This website is deployed on Heroku with AWS S3 (Amazon Web Services) for storing user files. 3 | 4 | 5 | You can explore more on : 6 | # Overview Of Website 7 | * First page is a login page Which will ask for username name and password 8 | ![login page](https://raw.githubusercontent.com/RudreshVeerkhare/ChatGroup/master/readme%20images/login.png "login page") 9 | * At bottom of login container there is a link for Sign Up page for new users 10 | ![sign up page](https://raw.githubusercontent.com/RudreshVeerkhare/ChatGroup/master/readme%20images/register.png "register page") 11 | * After logging in you will be directed to homepage of website where you can see all your Groups listed. 12 | ![home page](https://raw.githubusercontent.com/RudreshVeerkhare/ChatGroup/master/readme%20images/home.png "home page") 13 | * You can click on any of the the group to enter chatting lobby. 14 | ![group page](https://raw.githubusercontent.com/RudreshVeerkhare/ChatGroup/master/readme%20images/group.png) 15 | * From here you will be directed to Group Profile if you click on the group name above, where you can change group profile image and info. 16 | ![group profile](https://raw.githubusercontent.com/RudreshVeerkhare/ChatGroup/master/readme%20images/group_profile.png "group profile") 17 | * There is a similar page for User Profile also. 18 | ![User Profile](https://raw.githubusercontent.com/RudreshVeerkhare/ChatGroup/master/readme%20images/user_profile.png) 19 | * There are also features like Search User to search available users on site, search finds the users whose name matchs to initails of entered text. 20 | ![search page](https://raw.githubusercontent.com/RudreshVeerkhare/ChatGroup/master/readme%20images/search_user.png) 21 | ## Modules and packages used : 22 | 1) django 23 | 2) django-channels 24 | 3) channel-redis 25 | 4) crispy-form-tags 26 | 5) Bootstrap 4 27 | 6) Reconnecting-websockets by [joewalnes](https://github.com/joewalnes/reconnecting-websocket "reconnecting-websockets github repository") 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /home/templates/home/search_user.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load crispy_forms_tags %} 3 | 4 | 5 | 6 | GroupChat 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 |

{{ user.username }}

28 |
29 |
30 | 36 |
37 |
    38 | {% for usr in users %} 39 |
  • 40 |
    41 | 42 |
    43 |

    {{ usr }}

    44 | {% if usr.profile.user_info %} 45 |

    {{ usr.profile.user_info }}

    46 | {% endif %} 47 |
    48 |
    49 |
  • 50 | {% endfor %} 51 |
52 |
53 | 54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /home/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.2 on 2019-06-22 04:17 2 | 3 | import datetime 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | import home.models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Group', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('group_name', models.SlugField(max_length=20)), 24 | ('group_info', models.CharField(blank=True, max_length=300, null=True)), 25 | ('creater', models.ForeignKey(on_delete=models.SET(home.models.get_sentinal_user), to=settings.AUTH_USER_MODEL)), 26 | ('members', models.ManyToManyField(related_name='all_groups', to=settings.AUTH_USER_MODEL)), 27 | ], 28 | ), 29 | migrations.CreateModel( 30 | name='Profile', 31 | fields=[ 32 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('user_info', models.CharField(blank=True, max_length=300, null=True)), 34 | ('image', models.ImageField(default='default.jpg', upload_to=home.models.get_image_path)), 35 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 36 | ], 37 | ), 38 | migrations.CreateModel( 39 | name='Messages', 40 | fields=[ 41 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 42 | ('message_text', models.TextField()), 43 | ('date_posted', models.DateTimeField(default=datetime.datetime.now)), 44 | ('parent_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='home.Group')), 45 | ('parent_user', models.ForeignKey(on_delete=models.SET(home.models.get_sentinal_user), to=settings.AUTH_USER_MODEL)), 46 | ], 47 | ), 48 | migrations.CreateModel( 49 | name='GroupProfile', 50 | fields=[ 51 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 52 | ('image', models.ImageField(default='default_group.jpg', upload_to=home.models.get_group_image_path)), 53 | ('group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='group_profile', to='home.Group')), 54 | ], 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /home/templates/home/add_members.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load crispy_forms_tags %} 3 | 4 | 5 | 6 | GroupChat 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 27 |

{{ group.group_name }}

28 |
29 |
30 | 36 |
37 | {% comment %}
    38 | {% for group in groups %} 39 |
  • 40 |
    41 | 42 |
    43 |

    {{ group.group_name }}

    44 |

    {{ group.messages.last.parent_user }} : {{ group.messages.last.message_text }}

    45 |
    46 |
    47 |
  • 48 | {% endfor %} 49 |
{% endcomment %} 50 | 51 |
52 | {% if list %} 53 |
54 | {% csrf_token %} 55 | {% crispy list %} 56 |
57 | {% endif %} 58 |
59 |
60 | 61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /static/home/javascript/main.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | $("#profile-img").click(function () { 4 | $("#status-options").toggleClass("active"); 5 | }); 6 | 7 | $(".expand-button").click(function () { 8 | $("#profile").toggleClass("expanded"); 9 | $("#contacts").toggleClass("expanded"); 10 | }); 11 | 12 | $("#status-options ul li").click(function () { 13 | $("#profile-img").removeClass(); 14 | $("#status-online").removeClass("active"); 15 | $("#status-away").removeClass("active"); 16 | $("#status-busy").removeClass("active"); 17 | $("#status-offline").removeClass("active"); 18 | $(this).addClass("active"); 19 | 20 | if ($("#status-online").hasClass("active")) { 21 | $("#profile-img").addClass("online"); 22 | } else if ($("#status-away").hasClass("active")) { 23 | $("#profile-img").addClass("away"); 24 | } else if ($("#status-busy").hasClass("active")) { 25 | $("#profile-img").addClass("busy"); 26 | } else if ($("#status-offline").hasClass("active")) { 27 | $("#profile-img").addClass("offline"); 28 | } else { 29 | $("#profile-img").removeClass(); 30 | }; 31 | 32 | $("#status-options").removeClass("active"); 33 | }); 34 | 35 | $('.messages').stop().animate({ 36 | scrollTop: $('.messages')[0].scrollHeight 37 | }, 800); 38 | 39 | function scroll() { 40 | $('.messages').stop().animate({ 41 | scrollTop: $('.messages')[0].scrollHeight 42 | }, 800); 43 | } 44 | 45 | function scroll_up() { 46 | $('.messages').animate({ scrollTop: 0 }, 'fast'); 47 | } 48 | 49 | $('.submit').click(function () { 50 | scroll(); 51 | }); 52 | 53 | $('#check_older_messages').click(function () { 54 | scroll_up(); 55 | }); 56 | 57 | 58 | // function newMessage() { 59 | // message = $(".message-input input").val(); 60 | // if($.trim(message) == '') { 61 | // return false; 62 | // } 63 | // $('
  • ' + message + '

  • ').appendTo($('.messages ul')); 64 | // $('.message-input input').val(null); 65 | // $('.contact.active .preview').html('You: ' + message); 66 | // $(".messages").animate({ scrollTop: $(document).height() }, "fast"); 67 | // }; 68 | // function newMessage() { 69 | // message = $(".message-input input").val(); 70 | // if($.trim(message) == '') { 71 | // return false; 72 | // } 73 | // $('
  • ' + message + '

  • ').appendTo($('.messages ul')); 74 | // $('.message-input input').val(null); 75 | // $('.contact.active .preview').html('You: ' + message); 76 | // $(".messages").animate({ scrollTop: $(document).height() }, "fast"); 77 | // }; 78 | 79 | // $('.submit').click(function() { 80 | // newMessage(); 81 | // }); 82 | 83 | // $(window).on('keydown', function(e) { 84 | // if (e.which == 13) { 85 | // newMessage(); 86 | // return false; 87 | // } 88 | // }); 89 | // $('.submit').click(function() { 90 | // newMessage(); 91 | // }); 92 | 93 | // $(window).on('keydown', function(e) { 94 | // if (e.which == 13) { 95 | // newMessage(); 96 | // return false; 97 | // } 98 | // }); -------------------------------------------------------------------------------- /home/templates/home/profile.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load crispy_forms_tags %} 3 | 4 | 5 | 6 | GroupChat 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 40 | 41 |
    42 |

    {{ required_user.username }}

    43 | 44 | 45 |

    {{ required_user.email }}

    46 | {% if required_user.profile.user_info %} 47 |

    {{ required_user.profile.user_info }}

    48 | {% endif %} 49 |
    50 | 51 | {% if required_user.all_groups %} 52 |

    Active In Groups

    53 | {% endif %} 54 | 55 |
    56 | {% for group in required_user.all_groups.all %} 57 |
    58 | 59 |
    60 | 61 | {% if group.group_info %} 62 |

    {{ group.group_info }}

    63 | {% endif %} 64 |
    65 |
    66 |
    67 | {% endfor %} 68 |
    69 |
    70 | 71 | 72 | {% if required_user == user %} 73 |
    74 | 75 |
    76 |
    77 | {% csrf_token %} 78 | Profile Info 79 | {{ u_form|crispy }} 80 | {{ p_form|crispy }} 81 |
    82 |
    83 | 84 |
    85 |
    86 |
    87 | {% endif %} 88 |
    89 | 90 | 91 | 98 | 99 | -------------------------------------------------------------------------------- /home/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.models import User 3 | from django.contrib.auth.forms import UserCreationForm 4 | from .models import Profile, Group, GroupProfile, Messages 5 | from crispy_forms.helper import FormHelper, Layout 6 | from crispy_forms.layout import Submit 7 | from crispy_forms.layout import Field 8 | 9 | 10 | class UserRegisterForm(UserCreationForm): 11 | email = forms.EmailField() 12 | 13 | class Meta: 14 | model = User 15 | fields = ["username", "email", "password1", "password2"] 16 | 17 | 18 | class UserUpdateForm(forms.ModelForm): 19 | email = forms.EmailField() 20 | 21 | class Meta: 22 | model = User 23 | fields = ["username", "email"] 24 | 25 | 26 | class ProfileUpdateForm(forms.ModelForm): 27 | 28 | user_info = forms.CharField(widget=forms.Textarea, required=False) 29 | 30 | class Meta: 31 | model = Profile 32 | fields = ["user_info", "image"] 33 | 34 | 35 | class GroupUpdateForm(forms.ModelForm): 36 | 37 | group_info = forms.CharField(widget=forms.Textarea, required=False) 38 | 39 | class Meta: 40 | model = Group 41 | fields = ["group_name", "group_info"] 42 | 43 | 44 | class GroupCreateForm(forms.ModelForm): 45 | 46 | group_info = forms.CharField(widget=forms.Textarea, required=False) 47 | 48 | def __init__(self, *args, **kwargs): 49 | 50 | super(GroupCreateForm, self).__init__(*args, **kwargs) 51 | 52 | self.fields["group_name"].label = "Group Name :" 53 | self.fields["group_info"].label = "Group Info :" 54 | self.fields["group_name"].widget.attrs["placeholder"] = "Enter Group Name..." 55 | self.fields["group_info"].widget.attrs["placeholder"] = "Enter Group Info..." 56 | 57 | self.helper = FormHelper() 58 | self.helper.form_class = "form-horizontal form-class" 59 | self.helper.label_class = "form-group col-lg-2" 60 | self.helper.field_class = "input-class col-lg-10" 61 | self.helper.layout = Layout( 62 | "group_name", "group_info", Submit("submit", "Create", css_class="col-lg-2") 63 | ) 64 | 65 | class Meta: 66 | model = Group 67 | fields = ["group_name", "group_info"] 68 | 69 | 70 | class GroupProfileUpdateForm(forms.ModelForm): 71 | class Meta: 72 | model = GroupProfile 73 | fields = ["image"] 74 | 75 | 76 | class GroupProfileCreateForm(forms.ModelForm): 77 | class Meta: 78 | model = GroupProfile 79 | fields = ["image"] 80 | 81 | 82 | class MessageCreateForm(forms.ModelForm): 83 | message_text = forms.CharField() 84 | 85 | class Meta: 86 | model = Messages 87 | fields = ["message_text"] 88 | 89 | 90 | class SearchUserForm(forms.Form): 91 | 92 | user_name = forms.CharField(max_length=150, label="Username", required=False) 93 | 94 | def __init__(self, *args, **kwargs): 95 | super().__init__(*args, **kwargs) 96 | self.fields["user_name"].label = "Username :" 97 | self.fields["user_name"].widget.attrs[ 98 | "placeholder" 99 | ] = "Search User Name Here..." 100 | 101 | 102 | class AddMemberForm(forms.Form): 103 | users = forms.MultipleChoiceField( 104 | widget=forms.CheckboxSelectMultiple, label="Select users to add : " 105 | ) 106 | 107 | def __init__(self, *args, **kwargs): 108 | super().__init__(*args, **kwargs) 109 | self.helper = FormHelper() 110 | self.helper.form_class = "form-class" 111 | self.helper.field_class = "form-field-class" 112 | self.helper.layout = Layout( 113 | "users", Submit("submit", "Add To Group", css_class="col-lg-2") 114 | ) 115 | 116 | -------------------------------------------------------------------------------- /home/templates/home/group_profile.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load crispy_forms_tags %} 3 | 4 | 5 | 6 | GroupChat 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 46 | 47 |
    48 |

    {{ group.group_name }}

    49 | 50 | 51 |

    Created By {{ group.creater }}

    52 | {% if group.group_info %} 53 |

    {{ group.group_info }}

    54 | {% endif %} 55 |
    56 | 57 | {% if group.members %} 58 |

    Members Of {{ group }}

    59 | {% endif %} 60 | 61 |
    62 | {% for member in members %} 63 |
    64 | 65 |
    66 | 67 | {% if member.profile.user_info %} 68 |

    {{ member.profile.user_info }}

    69 | {% endif %} 70 |
    71 |
    72 |
    73 | {% endfor %} 74 |
    75 |
    76 | 77 | 78 | {% if group.creater == user %} 79 |
    80 | 81 |
    82 |
    83 | {% csrf_token %} 84 | Profile Info 85 | {{ g_form|crispy }} 86 | {{ gp_form|crispy }} 87 |
    88 |
    89 | 90 |
    91 |
    92 |
    93 | {% endif %} 94 |
    95 | 96 | 97 | 104 | 105 | -------------------------------------------------------------------------------- /home/consumers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from asgiref.sync import async_to_sync 3 | from channels.generic.websocket import WebsocketConsumer 4 | import json 5 | from .models import Messages, Group 6 | 7 | 8 | class ChatConsumer(WebsocketConsumer): 9 | def __init__(self, *args, **kwargs): 10 | self.count = 0 11 | return super().__init__(*args, **kwargs) 12 | 13 | def fetch_old_messages(self, data): 14 | grp_name = data["grp_name"] 15 | messages = Group.last_10_messages(grp_name, times=self.count) 16 | if messages: 17 | self.count += 1 18 | else: 19 | self.count -= 1 20 | messages = Group.last_10_messages(grp_name, times=self.count) 21 | 22 | content = {"command": "messages", "messages": self.messages_to_json(messages)} 23 | self.send_message(content) 24 | 25 | def fetch_messages(self, data): 26 | self.count = 0 27 | grp_name = data["grp_name"] 28 | messages = Group.last_10_messages(grp_name, self.count) 29 | content = {"command": "messages", "messages": self.messages_to_json(messages)} 30 | self.send_message(content) 31 | 32 | def fetch_groups(self, data): 33 | user_name = data["username"] 34 | user = User.objects.get(username=user_name) 35 | groups = user.all_groups.all() 36 | content = { 37 | "command" : "groups", 38 | "groups" : self.groups_to_json(groups), 39 | } 40 | self.send_message(content) 41 | 42 | def groups_to_json(self, groups): 43 | results = [] 44 | for group in groups: 45 | last_msg = group.messages.last() 46 | results.append({ 47 | "group_name" : group.group_name, 48 | "profile_pic" : group.group_profile.image.url, 49 | "last_msg" : f"{last_msg.parent_user.username + ' : ' + last_msg.message_text if last_msg else ''}", 50 | }) 51 | return results 52 | 53 | def new_message(self, data): 54 | author = data["from"] 55 | grp_name = data["grp_name"] 56 | parent_user = User.objects.get(username=author) 57 | parent_group = Group.objects.get(group_name=grp_name) 58 | message = Messages.objects.create( 59 | parent_group=parent_group, 60 | parent_user=parent_user, 61 | message_text=data["message"], 62 | ) 63 | content = {"command": "new_message", "message": self.message_to_json(message)} 64 | return self.send_chat_message(content) 65 | 66 | def messages_to_json(self, messages): 67 | result = [] 68 | for message in messages: 69 | result.append(self.message_to_json(message)) 70 | return result 71 | 72 | def message_to_json(self, message): 73 | return { 74 | "author": message.parent_user.username, 75 | "author_profile_img": message.parent_user.profile.image.url, 76 | "content": message.message_text, 77 | "timestamp": str(message.date_posted), 78 | } 79 | 80 | commands = { 81 | "fetch_old_messages": fetch_old_messages, 82 | "fetch_messages": fetch_messages, 83 | "new_message": new_message, 84 | "fetch_groups" : fetch_groups, 85 | } 86 | 87 | def connect(self): 88 | self.room_name = self.scope["url_route"]["kwargs"]["room_name"] 89 | self.room_group_name = "chat_%s" % self.room_name 90 | async_to_sync(self.channel_layer.group_add)( 91 | self.room_group_name, self.channel_name 92 | ) 93 | self.accept() 94 | 95 | def disconnect(self, close_code): 96 | async_to_sync(self.channel_layer.group_discard)( 97 | self.room_group_name, self.channel_name 98 | ) 99 | 100 | def receive(self, text_data): 101 | data = json.loads(text_data) 102 | self.commands[data["command"]](self, data) 103 | 104 | def send_chat_message(self, message): 105 | async_to_sync(self.channel_layer.group_send)( 106 | self.room_group_name, {"type": "chat_message", "message": message} 107 | ) 108 | 109 | def send_message(self, message): 110 | self.send(text_data=json.dumps(message)) 111 | 112 | def chat_message(self, event): 113 | message = event["message"] 114 | self.send(text_data=json.dumps(message)) 115 | -------------------------------------------------------------------------------- /home/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth import get_user_model 3 | from django.utils import timezone 4 | from django.contrib.auth.models import User 5 | from PIL import Image 6 | from django.urls import reverse 7 | import io 8 | from django.core.files.storage import default_storage as storage 9 | 10 | # Create your models here. 11 | 12 | 13 | def get_sentinal_user(): 14 | return get_user_model().objects.get_or_create(username="deleted")[0] 15 | 16 | 17 | class Group(models.Model): 18 | group_name = models.SlugField(max_length=20) 19 | creater = models.ForeignKey(User, on_delete=models.SET(get_sentinal_user)) 20 | group_info = models.CharField(max_length=300, blank=True, null=True) 21 | members = models.ManyToManyField(User, related_name="all_groups") 22 | # last_opened = models.DateTimeField() 23 | 24 | def __str__(self): 25 | return self.group_name 26 | 27 | def last_10_messages(grp_name, times=0): 28 | group = Group.objects.get(group_name=grp_name) 29 | if not times: 30 | return list(group.messages.order_by("date_posted"))[-30:] 31 | return list(group.messages.order_by("date_posted"))[(-30*(times+1)):(-30*times)] 32 | 33 | class Messages(models.Model): 34 | parent_group = models.ForeignKey( 35 | Group, on_delete=models.CASCADE, related_name="messages" 36 | ) 37 | parent_user = models.ForeignKey(User, on_delete=models.SET(get_sentinal_user)) 38 | message_text = models.TextField() 39 | date_posted = models.DateTimeField(default=timezone.localtime().now) 40 | 41 | def __str__(self): 42 | tup = tuple([self.parent_user, self.parent_group, self.message_text]) 43 | return str(tup) 44 | 45 | def get_absolute_url(self): 46 | return reverse("home:group", kwargs={"grp_name": self.parent_group}) 47 | 48 | 49 | # To store user profile images dynamically 50 | def get_image_path(instance, filename): 51 | from os.path import join 52 | 53 | return join("profile_pics", instance.user.username, filename) 54 | 55 | 56 | class Profile(models.Model): 57 | user = models.OneToOneField(User, on_delete=models.CASCADE) 58 | user_info = models.CharField(max_length=300, blank=True, null=True) 59 | image = models.ImageField(default="default.jpg", upload_to=get_image_path) 60 | 61 | def __str__(self): 62 | return self.user.username 63 | 64 | # Overriding save() method to manupulate the uploaded image 65 | def save(self, *args, **kwargs): 66 | super().save(*args, **kwargs) 67 | 68 | img_read = storage.open(self.image.name, 'r') 69 | img = Image.open(img_read) 70 | 71 | if img.height > 300 or img.width > 300: 72 | output_size = (300, 300) 73 | img.thumbnail(output_size) 74 | in_mem_file = io.BytesIO() 75 | img.save(in_mem_file, format='JPEG') 76 | img_write = storage.open(self.image.name, 'w+') 77 | img_write.write(in_mem_file.getvalue()) 78 | img_write.close() 79 | 80 | img_read.close() 81 | 82 | 83 | def get_group_image_path(instance, filename): 84 | from os.path import join 85 | 86 | return join("group_profile_pics", instance.group.group_name, filename) 87 | 88 | 89 | class GroupProfile(models.Model): 90 | group = models.OneToOneField( 91 | Group, on_delete=models.CASCADE, related_name="group_profile" 92 | ) 93 | image = models.ImageField( 94 | default="default_group.jpg", upload_to=get_group_image_path 95 | ) 96 | 97 | def __str__(self): 98 | return self.group.group_name 99 | 100 | def save(self, *args, **kwargs): 101 | super().save(*args, **kwargs) 102 | 103 | img_read = storage.open(self.image.name, 'r') 104 | img = Image.open(img_read) 105 | 106 | if img.height > 300 or img.width > 300: 107 | output_size = (300, 300) 108 | img.thumbnail(output_size) 109 | in_mem_file = io.BytesIO() 110 | img.save(in_mem_file, format='JPEG') 111 | img_write = storage.open(self.image.name, 'w+') 112 | img_write.write(in_mem_file.getvalue()) 113 | img_write.close() 114 | 115 | img_read.close() 116 | 117 | # def save(self, *args, **kwargs): 118 | 119 | # super().save(*args, **kwargs) 120 | 121 | # img = Image.open(self.image.path) 122 | 123 | # if img.height > 300 or img.width > 300: 124 | # output_size = (300, 300) 125 | # img.thumbnail(output_size) 126 | # img.save(self.image.path) 127 | -------------------------------------------------------------------------------- /home/templates/home/home.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load crispy_forms_tags %} 3 | 4 | 5 | 6 | GroupChat 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
    23 |
    24 |
    25 |
    26 | 27 |

    {{ user.username }}

    28 | 29 |
    30 |
    31 | Search Users 32 | Profile 33 | Logout 34 |
    35 |
    36 |
    37 |
    38 |
    39 |
      40 | {% comment %}
    • 41 |
      42 | 43 | 44 |
      45 |

      Louis Litt

      46 |

      You just got LITT up, Mike.

      47 |
      48 |
      49 |
    • 50 |
    • 51 |
      52 | 53 | 54 |
      55 |

      Harvey Specter

      56 |

      Wrong. You take the gun, or you pull out a bigger one. Or, you call their bluff. Or, you do any one of a hundred and forty six other things.

      57 |
      58 |
      59 |
    • {% endcomment %} 60 | {% for group in groups %} 61 |
    • 62 |
      63 | 64 |
      65 |

      {{ group.group_name }}

      66 |

      {{ group.messages.last.parent_user }} : {{ group.messages.last.message_text }}

      67 |
      68 |
      69 |
    • 70 | {% endfor %} 71 |
    72 |
    73 | 74 |
    75 | 76 |
    77 |
    78 | 79 | {% csrf_token %} 80 |
    81 | Create Group 82 | {% crispy g_form %} 83 |
    84 | 85 |
    86 |
    87 | 88 |
    89 |
    90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /chat_group/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for chat_group project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | import django_heroku 13 | import os 14 | 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = "4qa44u+yj_@%=!2htpwb(c-=my#^27*@wp1e!pq$(lkjmx7dl&" 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = [] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | "channels", 36 | "home.apps.HomeConfig", 37 | "crispy_forms", 38 | "django.contrib.admin", 39 | "django.contrib.auth", 40 | "django.contrib.contenttypes", 41 | "django.contrib.sessions", 42 | "django.contrib.messages", 43 | "django.contrib.staticfiles", 44 | "storages", 45 | ] 46 | 47 | MIDDLEWARE = [ 48 | "django.middleware.security.SecurityMiddleware", 49 | "django.contrib.sessions.middleware.SessionMiddleware", 50 | "django.middleware.common.CommonMiddleware", 51 | "django.middleware.csrf.CsrfViewMiddleware", 52 | "django.contrib.auth.middleware.AuthenticationMiddleware", 53 | "django.contrib.messages.middleware.MessageMiddleware", 54 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 55 | ] 56 | 57 | ROOT_URLCONF = "chat_group.urls" 58 | 59 | TEMPLATES = [ 60 | { 61 | "BACKEND": "django.template.backends.django.DjangoTemplates", 62 | "DIRS": [], 63 | "APP_DIRS": True, 64 | "OPTIONS": { 65 | "context_processors": [ 66 | "django.template.context_processors.debug", 67 | "django.template.context_processors.request", 68 | "django.contrib.auth.context_processors.auth", 69 | "django.contrib.messages.context_processors.messages", 70 | ] 71 | }, 72 | } 73 | ] 74 | 75 | WSGI_APPLICATION = "chat_group.wsgi.application" 76 | ASGI_APPLICATION = "chat_group.routing.application" 77 | 78 | CHANNEL_LAYERS = { 79 | "default": { 80 | "BACKEND": "channels_redis.core.RedisChannelLayer", 81 | "CONFIG": {"hosts": [os.environ.get("REDIS_URL", "redis://localhost:6379")]}, 82 | } 83 | } 84 | 85 | 86 | # Database 87 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 88 | 89 | DATABASES = { 90 | "default": { 91 | "ENGINE": "django.db.backends.sqlite3", 92 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 93 | } 94 | } 95 | 96 | 97 | # Password validation 98 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 99 | 100 | AUTH_PASSWORD_VALIDATORS = [ 101 | { 102 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" 103 | }, 104 | {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 105 | {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 106 | {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 112 | 113 | LANGUAGE_CODE = "en-us" 114 | 115 | TIME_ZONE = "Asia/Kolkata" 116 | 117 | USE_I18N = True 118 | 119 | USE_L10N = True 120 | 121 | USE_TZ = True 122 | 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 126 | 127 | STATIC_URL = "/static/" 128 | STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] 129 | 130 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 131 | 132 | MEDIA_URL = "/media/" 133 | 134 | LOGIN_REDIRECT_URL = "home:home" 135 | 136 | LOGIN_URL = "login" 137 | 138 | CRISPY_TEMPLATE_PACK = "bootstrap4" 139 | 140 | AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID") 141 | AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY") 142 | AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_STORAGE_BUCKET_NAME") 143 | AWS_S3_FILE_OVERWRITE = False 144 | AWS_DEFAULT_ACL = None 145 | AWS_S3_REGION_NAME = 'ap-south-1' 146 | AWS_S3_SIGNATURE_VERSION = 's3v4' 147 | 148 | DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' 149 | 150 | 151 | # Activate Django-Heroku. 152 | django_heroku.settings(locals()) 153 | 154 | import dj_database_url 155 | 156 | DATABASES["default"] = dj_database_url.config() 157 | 158 | -------------------------------------------------------------------------------- /static/home/css/style_add_members.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | min-height: 100vh; 6 | background: #32465a85; 7 | font-family: "proxima-nova", "Source Sans Pro", sans-serif; 8 | font-size: 1em; 9 | letter-spacing: 0.1px; 10 | color: #32465a; 11 | text-rendering: optimizeLegibility; 12 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004); 13 | -webkit-font-smoothing: antialiased; 14 | } 15 | 16 | .asteriskField{ 17 | display : none; 18 | } 19 | 20 | .collapse-content { 21 | padding: 0 18px; 22 | max-height: 0; 23 | overflow: hidden; 24 | background-color: #32465a85; 25 | transition: max-height 0.2s ease-out; 26 | 27 | } 28 | 29 | input[type=checkbox]{ 30 | position:relative; 31 | 32 | } 33 | 34 | 35 | input[type=checkbox]:hover + #id__1{ 36 | transition : 0.2s; 37 | font-weight : bold; 38 | } 39 | 40 | input[type=checkbox]:checked +#id__1{ 41 | color: #47cf73; 42 | transition : 0.7s; 43 | font-weight : bold; 44 | } 45 | 46 | #form-legend{ 47 | text-decoration: none; 48 | color : white; 49 | font-weight: bolder; 50 | } 51 | 52 | 53 | .form-div{ 54 | margin-left: 40px; 55 | margin-top: 5px; 56 | } 57 | 58 | .input-class{ 59 | color: #496886; 60 | } 61 | 62 | #submit-id-submit{ 63 | display: block; 64 | margin: 10px 42%; 65 | } 66 | 67 | #frame { 68 | width: 100%; 69 | min-width: 360px; 70 | height: 100vh; 71 | min-height: 300px; 72 | background: #E6EAEA; 73 | } 74 | @media screen and (max-width: 360px) { 75 | #frame { 76 | width: 100%; 77 | height: 100vh; 78 | } 79 | } 80 | #frame #sidepanel { 81 | font-size: medium; 82 | float: left; 83 | min-width: 280px; 84 | /* max-width: 340px; */ 85 | width: 100%; 86 | height: 100%; 87 | background: #2c3e50; 88 | color: #f5f5f5; 89 | overflow: hidden; 90 | position: relative; 91 | } 92 | @media screen and (max-width: 735px) { 93 | #frame #sidepanel { 94 | width: 100px;max-width: 340px; 95 | min-width: 100px; 96 | } 97 | } 98 | #frame #sidepanel #profile { 99 | width: 80%; 100 | margin: 25px auto; 101 | } 102 | @media screen and (max-width: 735px) { 103 | #frame #sidepanel #profile { 104 | width: 100%; 105 | margin: 0 auto; 106 | padding: 5px 0 0 0; 107 | background: #32465a; 108 | } 109 | } 110 | #frame #sidepanel #profile.expanded .wrap { 111 | height: 210px; 112 | line-height: initial; 113 | } 114 | #frame #sidepanel #profile.expanded .wrap p { 115 | margin-top: 20px; 116 | } 117 | #frame #sidepanel #profile.expanded .wrap i.expand-button { 118 | -moz-transform: scaleY(-1); 119 | -o-transform: scaleY(-1); 120 | -webkit-transform: scaleY(-1); 121 | transform: scaleY(-1); 122 | filter: FlipH; 123 | -ms-filter: "FlipH"; 124 | } 125 | #frame #sidepanel #profile .wrap { 126 | height: 65px; 127 | line-height: 65px; 128 | overflow: hidden; 129 | -moz-transition: 0.3s height ease; 130 | -o-transition: 0.3s height ease; 131 | -webkit-transition: 0.3s height ease; 132 | transition: 0.3s height ease; 133 | } 134 | @media screen and (max-width: 735px) { 135 | #frame #sidepanel #profile .wrap { 136 | height: 65px; 137 | } 138 | } 139 | #frame #sidepanel #profile .wrap img { 140 | width: 60px; 141 | border-radius: 50%; 142 | padding: 3px; 143 | /* border: 2px solid #e74c3c; */ 144 | height: auto; 145 | float: left; 146 | cursor: pointer; 147 | -moz-transition: 0.3s border ease; 148 | -o-transition: 0.3s border ease; 149 | -webkit-transition: 0.3s border ease; 150 | transition: 0.3s border ease; 151 | } 152 | @media screen and (max-width: 735px) { 153 | #frame #sidepanel #profile .wrap img { 154 | width: 60px; 155 | margin-left: 4px; 156 | } 157 | } 158 | 159 | #frame #sidepanel #profile .wrap p { 160 | float: left; 161 | margin-left: 15px; 162 | } 163 | @media screen and (max-width: 735px) { 164 | #frame #sidepanel #profile .wrap p { 165 | display: none; 166 | } 167 | } 168 | 169 | #frame #sidepanel #search { 170 | border-top: 1px solid #32465a; 171 | border-bottom: 1px solid #32465a; 172 | font-weight: 300; 173 | } 174 | @media screen and (max-width: 735px) { 175 | #frame #sidepanel #search { 176 | display: none; 177 | } 178 | } 179 | #frame #sidepanel #search label { 180 | position: relative; 181 | margin: 10px 0 0 20px; 182 | } 183 | #frame #sidepanel #search input { 184 | font-family: "proxima-nova", "Source Sans Pro", sans-serif; 185 | padding: 10px 0 10px 10px; 186 | width: calc(100% - 25px); 187 | border: none; 188 | background: #32465a; 189 | color: #f5f5f5; 190 | margin-left: 20px; 191 | } 192 | #frame #sidepanel #search input:focus { 193 | outline: none; 194 | background: #435f7a; 195 | } 196 | #frame #sidepanel #search input::-webkit-input-placeholder { 197 | color: #f5f5f5; 198 | } 199 | #frame #sidepanel #search input::-moz-placeholder { 200 | color: #f5f5f5; 201 | } 202 | #frame #sidepanel #search input:-ms-input-placeholder { 203 | color: #f5f5f5; 204 | } 205 | #frame #sidepanel #search input:-moz-placeholder { 206 | color: #f5f5f5; 207 | } 208 | #frame #sidepanel #contacts { 209 | height: calc(100% - 177px); 210 | overflow-y: scroll; 211 | overflow-x: hidden; 212 | } 213 | @media screen and (max-width: 735px) { 214 | #frame #sidepanel #contacts { 215 | height: calc(100% - 149px); 216 | overflow-y: scroll; 217 | overflow-x: hidden; 218 | } 219 | #frame #sidepanel #contacts::-webkit-scrollbar { 220 | display: none; 221 | } 222 | } 223 | #frame #sidepanel #contacts.expanded { 224 | height: calc(100% - 334px); 225 | } 226 | #frame #sidepanel #contacts::-webkit-scrollbar { 227 | width: 8px; 228 | background: #2c3e50; 229 | } 230 | #frame #sidepanel #contacts::-webkit-scrollbar-thumb { 231 | background-color: #243140; 232 | } 233 | #frame #sidepanel #contacts ul li.contact { 234 | position: relative; 235 | padding: 10px 0 15px 0; 236 | font-size: 0.9em; 237 | cursor: pointer; 238 | } 239 | @media screen and (max-width: 735px) { 240 | #frame #sidepanel #contacts ul li.contact { 241 | padding: 6px 0 66px 8px; 242 | } 243 | } 244 | #frame #sidepanel #contacts ul li.contact:hover { 245 | background: #32465a; 246 | } 247 | #frame #sidepanel #contacts ul li.contact.active { 248 | background: #32465a; 249 | border-right: 5px solid #435f7a; 250 | } 251 | #frame #sidepanel #contacts ul li.contact.active span.contact-status { 252 | border: 2px solid #32465a !important; 253 | } 254 | #frame #sidepanel #contacts ul li.contact .wrap { 255 | width: 88%; 256 | margin: 0 auto; 257 | position: relative; 258 | } 259 | @media screen and (max-width: 735px) { 260 | #frame #sidepanel #contacts ul li.contact .wrap { 261 | width: 100%; 262 | } 263 | } 264 | #frame #sidepanel #contacts ul li.contact .wrap span { 265 | position: absolute; 266 | left: 0; 267 | margin: -2px 0 0 -2px; 268 | width: 10px; 269 | height: 10px; 270 | border-radius: 50%; 271 | border: 2px solid #2c3e50; 272 | background: #95a5a6; 273 | } 274 | #frame #sidepanel #contacts ul li.contact .wrap span.online { 275 | background: #2ecc71; 276 | } 277 | #frame #sidepanel #contacts ul li.contact .wrap span.away { 278 | background: #f1c40f; 279 | } 280 | #frame #sidepanel #contacts ul li.contact .wrap span.busy { 281 | background: #e74c3c; 282 | } 283 | 284 | #frame #sidepanel #contacts ul li.contact .wrap img { 285 | width: 50px; 286 | border-radius: 50%; 287 | float: left; 288 | margin-right: 10px; 289 | } 290 | @media screen and (max-width: 735px) { 291 | #frame #sidepanel #contacts ul li.contact .wrap img { 292 | margin-right: 0px; 293 | } 294 | } 295 | #frame #sidepanel #contacts ul li.contact .wrap .meta { 296 | padding: 5px 0 0 0; 297 | } 298 | @media screen and (max-width: 735px) { 299 | #frame #sidepanel #contacts ul li.contact .wrap .meta { 300 | display: none; 301 | } 302 | } 303 | #frame #sidepanel #contacts ul li.contact .wrap .meta .name { 304 | font-weight: 600; 305 | } 306 | #frame #sidepanel #contacts ul li.contact .wrap .meta .preview { 307 | margin: 5px 0 0 0; 308 | padding: 0 0 1px; 309 | font-weight: 400; 310 | white-space: nowrap; 311 | overflow: hidden; 312 | text-overflow: ellipsis; 313 | -moz-transition: 1s all ease; 314 | -o-transition: 1s all ease; 315 | -webkit-transition: 1s all ease; 316 | transition: 1s all ease; 317 | } 318 | #frame #sidepanel #contacts ul li.contact .wrap .meta .preview span { 319 | position: initial; 320 | border-radius: initial; 321 | background: none; 322 | border: none; 323 | padding: 0 2px 0 0; 324 | margin: 0 0 0 1px; 325 | opacity: .5; 326 | } 327 | 328 | .fa { 329 | font-size: 1.5em !important; 330 | } -------------------------------------------------------------------------------- /home/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, get_object_or_404 2 | from django.http import HttpResponse 3 | from .models import Group, Messages 4 | from django.contrib.auth.mixins import LoginRequiredMixin 5 | from django.views.generic import CreateView 6 | from .forms import ( 7 | UserRegisterForm, 8 | UserUpdateForm, 9 | ProfileUpdateForm, 10 | GroupUpdateForm, 11 | GroupProfileUpdateForm, 12 | MessageCreateForm, 13 | GroupCreateForm, 14 | GroupProfileCreateForm, 15 | SearchUserForm, 16 | AddMemberForm 17 | ) 18 | from django.contrib.auth.decorators import login_required 19 | from django.http import Http404 20 | from django.contrib.auth.models import User 21 | import json 22 | from django.utils.safestring import mark_safe 23 | 24 | # Create your views here. 25 | 26 | 27 | @login_required 28 | def home(request): 29 | groups = request.user.all_groups.all() 30 | if request.method == "POST": 31 | g_form = GroupCreateForm(request.POST) 32 | 33 | if g_form.is_valid(): 34 | group = g_form.save(commit=False) 35 | group.creater = request.user 36 | group.save() 37 | group.members.add(request.user) 38 | return redirect("home:group_profile", grp_name=group.group_name) 39 | else: 40 | g_form = GroupCreateForm() 41 | 42 | context = { 43 | "groups": groups, 44 | "g_form" : g_form 45 | } 46 | return render(request, "home/home.html", context) 47 | 48 | 49 | # class GroupCreateView(LoginRequiredMixin, CreateView): 50 | 51 | # template_name = "home/home.html" 52 | # fields = ['group_name' , 'group_info'] 53 | 54 | # def get_queryset(self): 55 | # return self.request.user.all_groups.all() 56 | 57 | # def get_context_data(self, **kwargs): 58 | # context = super().get_context_data(**kwargs) 59 | # context["groups"] = self.request.user.all_groups.all() 60 | # return context 61 | 62 | 63 | 64 | 65 | 66 | # @login_required 67 | # def group(request, grp_name): 68 | # group = get_object_or_404(Group, group_name=grp_name) 69 | # # To check wether user is member of group or not 70 | # try: 71 | # request.user.all_groups.get(group_name=grp_name) 72 | # # above line will throw exception if user is not permitted for the view 73 | # msgs = Messages.objects.filter(parent_group=group).all() 74 | # context = {"msgs": msgs, "group_name": grp_name} 75 | # return render(request, "home/group.html", context) 76 | 77 | # except Group.DoesNotExist: 78 | # raise Http404("Group Does not exist") 79 | 80 | 81 | # class MessageCreateView(LoginRequiredMixin, CreateView): 82 | 83 | # # queryset = Messages.objects.filter(group_name=grp_name) 84 | # form_class = MessageCreateForm 85 | # model = Messages 86 | # template_name = 'home/group.html' 87 | 88 | 89 | # def dispatch(self, request, *args, **kwargs): 90 | # self.grp_name = kwargs['grp_name'] 91 | # return super().dispatch(request, *args, **kwargs) 92 | 93 | # def get_context_data(self, **kwargs): 94 | # context = super().get_context_data(**kwargs) 95 | # grp = Group.objects.get(group_name=self.grp_name) 96 | # context["msgs"] = reversed(grp.messages.order_by('date_posted')[:50]) 97 | # context["group_name"] = self.grp_name 98 | # return context 99 | 100 | # def form_valid(self, form): 101 | # form.instance.parent_group = Group.objects.get(group_name=self.grp_name) 102 | # form.instance.parent_user = self.request.user 103 | # return super().form_valid(form) 104 | 105 | @login_required 106 | def group(request, grp_name): 107 | 108 | try: 109 | group = request.user.all_groups.get(group_name=grp_name) 110 | except Group.DoesNotExist: 111 | raise Http404("Group Does not exist or You are not member of this group") 112 | 113 | return render(request, 'home/group.html', { 114 | 'room_name_json': mark_safe(json.dumps(grp_name)), 115 | 'group' : group 116 | }) 117 | 118 | def register(request): 119 | if request.method == "POST": 120 | form = UserRegisterForm(request.POST) 121 | if form.is_valid(): 122 | form.save() 123 | return redirect("login") 124 | 125 | else: 126 | form = UserRegisterForm() 127 | 128 | return render(request, "home/register.html", {"form": form}) 129 | 130 | 131 | @login_required 132 | def profile(request, user_name): 133 | 134 | # This will allow us to show updation form only if user is on own profile 135 | if request.user.username == user_name: 136 | if request.method == "POST": 137 | u_form = UserUpdateForm(request.POST, instance=request.user) 138 | p_form = ProfileUpdateForm( 139 | request.POST, request.FILES, instance=request.user.profile 140 | ) 141 | 142 | if u_form.is_valid() and p_form.is_valid(): 143 | u_form.save() 144 | p_form.save() 145 | return redirect("home:profile", user_name=user_name) 146 | 147 | else: 148 | u_form = UserUpdateForm(instance=request.user) 149 | p_form = ProfileUpdateForm(instance=request.user.profile) 150 | 151 | context = {"required_user": request.user, "u_form": u_form, "p_form": p_form} 152 | 153 | else: 154 | required_user = get_object_or_404(User, username=user_name) 155 | context = {"required_user": required_user} 156 | 157 | return render(request, "home/profile.html", context) 158 | 159 | 160 | @login_required 161 | def group_profile(request, grp_name): 162 | try: 163 | group = request.user.all_groups.get(group_name=grp_name) 164 | # above line will throw exception if user is not permitted for the view 165 | members = group.members.all() 166 | 167 | if request.user.username == group.creater.username: 168 | if request.method == "POST": 169 | g_form = GroupUpdateForm(request.POST, instance=group) 170 | gp_form = GroupProfileUpdateForm( 171 | request.POST, request.FILES, instance=group.group_profile 172 | ) 173 | 174 | if g_form.is_valid() and gp_form.is_valid(): 175 | g_form.save() 176 | gp_form.save() 177 | 178 | return redirect("home:group_profile", grp_name=group.group_name) 179 | 180 | else: 181 | g_form = GroupUpdateForm(instance=group) 182 | gp_form = GroupProfileUpdateForm(instance=group.group_profile) 183 | 184 | context = { 185 | "members": members, 186 | "group": group, 187 | "g_form": g_form, 188 | "gp_form": gp_form, 189 | } 190 | 191 | else: 192 | context = {"members": members, "group": group} 193 | 194 | return render(request, "home/group_profile.html", context) 195 | 196 | except Group.DoesNotExist: 197 | raise Http404("Group Does not exist") 198 | 199 | @login_required 200 | def search_user(request): 201 | if request.method == 'POST': 202 | form = SearchUserForm(request.POST) 203 | 204 | if form.is_valid(): 205 | users = User.objects.filter(username__startswith=form.cleaned_data.get("user_name")) 206 | 207 | context = { 208 | "users" : users, 209 | "form" : form 210 | } 211 | return render(request, "home/search_user.html", context) 212 | else: 213 | form = SearchUserForm() 214 | context = { 215 | "form" : form 216 | } 217 | 218 | return render(request, "home/search_user.html", context) 219 | 220 | 221 | 222 | @login_required 223 | def add_member(request, grp_name): 224 | 225 | 226 | 227 | try: 228 | global group 229 | group = Group.objects.get(group_name=grp_name) 230 | flag = group.members.get(username=request.user.username) 231 | except Group.DoesNotExist: 232 | raise Http404("Group Does not exist") 233 | 234 | 235 | 236 | if flag: 237 | print(group.group_profile.image.url) 238 | if request.method == 'POST': 239 | if 'user_name' in request.POST and 'users' not in request.POST: 240 | 241 | form = SearchUserForm(request.POST) 242 | userlistform = AddMemberForm() 243 | 244 | if form.is_valid(): 245 | global users_for_choices # To access it accross if statements 246 | users_for_choices = [ (user.id, user.username) for user in User.objects.filter(username__startswith=form.cleaned_data.get("user_name"))] 247 | userlistform.fields['users'].choices = users_for_choices 248 | form = SearchUserForm() 249 | context = { 250 | "group" : group, 251 | "form" : form 252 | } 253 | if len(users_for_choices) > 0: 254 | context["list"] = userlistform 255 | print(users_for_choices) 256 | return render(request, "home/add_members.html", context) 257 | 258 | # else: 259 | if 'users' in request.POST: 260 | userlist = AddMemberForm(request.POST) 261 | userlist.fields['users'].choices = users_for_choices 262 | 263 | if userlist.is_valid(): 264 | users = userlist.cleaned_data.get('users') 265 | for user in users: 266 | group.members.add(User.objects.get(id=user[0])) 267 | 268 | return redirect('home:group', grp_name=grp_name) 269 | 270 | 271 | else: 272 | print(userlist.errors.as_text()) 273 | form = SearchUserForm() 274 | context = { 275 | "form" : form, 276 | "group" : group 277 | } 278 | 279 | 280 | 281 | 282 | 283 | else: 284 | form = SearchUserForm() 285 | context = { 286 | "form" : form, 287 | "group" : group 288 | } 289 | 290 | 291 | return render(request, "home/add_members.html", context) 292 | -------------------------------------------------------------------------------- /home/templates/home/group.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | GroupChat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
    22 |
    23 |
    24 |
    25 | 26 |

    {{ user.username }}

    27 | 28 |
    29 |
      30 |
    • Online

    • 31 |
    • Away

    • 32 |
    • Busy

    • 33 |
    • Offline

    • 34 |
    35 |
    36 |
    37 |
    38 | Search Users 39 |
    40 |
    41 | Home 42 |
    43 |
    44 | Logout 45 |
    46 |
    47 |
    48 |
    49 |
    50 |
      51 | {% comment %}
    • 52 |
      53 | 54 | 55 |
      56 |

      Louis Litt

      57 |

      You just got LITT up, Mike.

      58 |
      59 |
      60 |
    • 61 |
    • 62 |
      63 | 64 | 65 |
      66 |

      Harvey Specter

      67 |

      Wrong. You take the gun, or you pull out a bigger one. Or, you call their bluff. Or, you do any one of a hundred and forty six other things.

      68 |
      69 |
      70 |
    • {% endcomment %} 71 |
    72 |
    73 |
    74 | 75 | {% comment %} {% endcomment %} 76 |
    77 |
    78 | 79 | 80 | 81 |
    82 |
    83 | 84 |

    {{ group.group_name }}

    85 |
    86 | 87 |
    88 | 89 |
    90 |
    91 |
      92 | {% comment %}
    • 93 | 94 |

      How the hell am I supposed to get a jury to believe you when I am not even sure that I do?!

      95 |
    • 96 |
    • 97 | 98 |

      When you're backed against the wall, break the god damn thing down.

      99 |
    • {% endcomment %} 100 |
    101 |
    102 |
    103 |
    104 | 105 | 106 | 109 |
    110 |
    111 |
    112 |
    113 | 114 | 115 | 116 | 324 | 325 | 326 | -------------------------------------------------------------------------------- /static/home/javascript/reconnecting-websocket.js: -------------------------------------------------------------------------------- 1 | // MIT License: 2 | // 3 | // Copyright (c) 2010-2012, Joe Walnes 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in 13 | // all copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | // THE SOFTWARE. 22 | 23 | /** 24 | * This behaves like a WebSocket in every way, except if it fails to connect, 25 | * or it gets disconnected, it will repeatedly poll until it successfully connects 26 | * again. 27 | * 28 | * It is API compatible, so when you have: 29 | * ws = new WebSocket('ws://....'); 30 | * you can replace with: 31 | * ws = new ReconnectingWebSocket('ws://....'); 32 | * 33 | * The event stream will typically look like: 34 | * onconnecting 35 | * onopen 36 | * onmessage 37 | * onmessage 38 | * onclose // lost connection 39 | * onconnecting 40 | * onopen // sometime later... 41 | * onmessage 42 | * onmessage 43 | * etc... 44 | * 45 | * It is API compatible with the standard WebSocket API, apart from the following members: 46 | * 47 | * - `bufferedAmount` 48 | * - `extensions` 49 | * - `binaryType` 50 | * 51 | * Latest version: https://github.com/joewalnes/reconnecting-websocket/ 52 | * - Joe Walnes 53 | * 54 | * Syntax 55 | * ====== 56 | * var socket = new ReconnectingWebSocket(url, protocols, options); 57 | * 58 | * Parameters 59 | * ========== 60 | * url - The url you are connecting to. 61 | * protocols - Optional string or array of protocols. 62 | * options - See below 63 | * 64 | * Options 65 | * ======= 66 | * Options can either be passed upon instantiation or set after instantiation: 67 | * 68 | * var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 }); 69 | * 70 | * or 71 | * 72 | * var socket = new ReconnectingWebSocket(url); 73 | * socket.debug = true; 74 | * socket.reconnectInterval = 4000; 75 | * 76 | * debug 77 | * - Whether this instance should log debug messages. Accepts true or false. Default: false. 78 | * 79 | * automaticOpen 80 | * - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close(). 81 | * 82 | * reconnectInterval 83 | * - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000. 84 | * 85 | * maxReconnectInterval 86 | * - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000. 87 | * 88 | * reconnectDecay 89 | * - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5. 90 | * 91 | * timeoutInterval 92 | * - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000. 93 | * 94 | */ 95 | (function (global, factory) { 96 | if (typeof define === 'function' && define.amd) { 97 | define([], factory); 98 | } else if (typeof module !== 'undefined' && module.exports) { 99 | module.exports = factory(); 100 | } else { 101 | global.ReconnectingWebSocket = factory(); 102 | } 103 | })(this, function () { 104 | 105 | if (!('WebSocket' in window)) { 106 | return; 107 | } 108 | 109 | function ReconnectingWebSocket(url, protocols, options) { 110 | 111 | // Default settings 112 | var settings = { 113 | 114 | /** Whether this instance should log debug messages. */ 115 | debug: false, 116 | 117 | /** Whether or not the websocket should attempt to connect immediately upon instantiation. */ 118 | automaticOpen: true, 119 | 120 | /** The number of milliseconds to delay before attempting to reconnect. */ 121 | reconnectInterval: 1000, 122 | /** The maximum number of milliseconds to delay a reconnection attempt. */ 123 | maxReconnectInterval: 30000, 124 | /** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */ 125 | reconnectDecay: 1.5, 126 | 127 | /** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */ 128 | timeoutInterval: 2000, 129 | 130 | /** The maximum number of reconnection attempts to make. Unlimited if null. */ 131 | maxReconnectAttempts: null, 132 | 133 | /** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */ 134 | binaryType: 'blob' 135 | } 136 | if (!options) { options = {}; } 137 | 138 | // Overwrite and define settings with options if they exist. 139 | for (var key in settings) { 140 | if (typeof options[key] !== 'undefined') { 141 | this[key] = options[key]; 142 | } else { 143 | this[key] = settings[key]; 144 | } 145 | } 146 | 147 | // These should be treated as read-only properties 148 | 149 | /** The URL as resolved by the constructor. This is always an absolute URL. Read only. */ 150 | this.url = url; 151 | 152 | /** The number of attempted reconnects since starting, or the last successful connection. Read only. */ 153 | this.reconnectAttempts = 0; 154 | 155 | /** 156 | * The current state of the connection. 157 | * Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED 158 | * Read only. 159 | */ 160 | this.readyState = WebSocket.CONNECTING; 161 | 162 | /** 163 | * A string indicating the name of the sub-protocol the server selected; this will be one of 164 | * the strings specified in the protocols parameter when creating the WebSocket object. 165 | * Read only. 166 | */ 167 | this.protocol = null; 168 | 169 | // Private state variables 170 | 171 | var self = this; 172 | var ws; 173 | var forcedClose = false; 174 | var timedOut = false; 175 | var eventTarget = document.createElement('div'); 176 | 177 | // Wire up "on*" properties as event handlers 178 | 179 | eventTarget.addEventListener('open', function (event) { self.onopen(event); }); 180 | eventTarget.addEventListener('close', function (event) { self.onclose(event); }); 181 | eventTarget.addEventListener('connecting', function (event) { self.onconnecting(event); }); 182 | eventTarget.addEventListener('message', function (event) { self.onmessage(event); }); 183 | eventTarget.addEventListener('error', function (event) { self.onerror(event); }); 184 | 185 | // Expose the API required by EventTarget 186 | 187 | this.addEventListener = eventTarget.addEventListener.bind(eventTarget); 188 | this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget); 189 | this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget); 190 | 191 | /** 192 | * This function generates an event that is compatible with standard 193 | * compliant browsers and IE9 - IE11 194 | * 195 | * This will prevent the error: 196 | * Object doesn't support this action 197 | * 198 | * http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563 199 | * @param s String The name that the event should use 200 | * @param args Object an optional object that the event will use 201 | */ 202 | function generateEvent(s, args) { 203 | var evt = document.createEvent("CustomEvent"); 204 | evt.initCustomEvent(s, false, false, args); 205 | return evt; 206 | }; 207 | 208 | this.open = function (reconnectAttempt) { 209 | ws = new WebSocket(self.url, protocols || []); 210 | ws.binaryType = this.binaryType; 211 | 212 | if (reconnectAttempt) { 213 | if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) { 214 | return; 215 | } 216 | } else { 217 | eventTarget.dispatchEvent(generateEvent('connecting')); 218 | this.reconnectAttempts = 0; 219 | } 220 | 221 | if (self.debug || ReconnectingWebSocket.debugAll) { 222 | console.debug('ReconnectingWebSocket', 'attempt-connect', self.url); 223 | } 224 | 225 | var localWs = ws; 226 | var timeout = setTimeout(function () { 227 | if (self.debug || ReconnectingWebSocket.debugAll) { 228 | console.debug('ReconnectingWebSocket', 'connection-timeout', self.url); 229 | } 230 | timedOut = true; 231 | localWs.close(); 232 | timedOut = false; 233 | }, self.timeoutInterval); 234 | 235 | ws.onopen = function (event) { 236 | clearTimeout(timeout); 237 | if (self.debug || ReconnectingWebSocket.debugAll) { 238 | console.debug('ReconnectingWebSocket', 'onopen', self.url); 239 | } 240 | self.protocol = ws.protocol; 241 | self.readyState = WebSocket.OPEN; 242 | self.reconnectAttempts = 0; 243 | var e = generateEvent('open'); 244 | e.isReconnect = reconnectAttempt; 245 | reconnectAttempt = false; 246 | eventTarget.dispatchEvent(e); 247 | }; 248 | 249 | ws.onclose = function (event) { 250 | clearTimeout(timeout); 251 | ws = null; 252 | if (forcedClose) { 253 | self.readyState = WebSocket.CLOSED; 254 | eventTarget.dispatchEvent(generateEvent('close')); 255 | } else { 256 | self.readyState = WebSocket.CONNECTING; 257 | var e = generateEvent('connecting'); 258 | e.code = event.code; 259 | e.reason = event.reason; 260 | e.wasClean = event.wasClean; 261 | eventTarget.dispatchEvent(e); 262 | if (!reconnectAttempt && !timedOut) { 263 | if (self.debug || ReconnectingWebSocket.debugAll) { 264 | console.debug('ReconnectingWebSocket', 'onclose', self.url); 265 | } 266 | eventTarget.dispatchEvent(generateEvent('close')); 267 | } 268 | 269 | var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts); 270 | setTimeout(function () { 271 | self.reconnectAttempts++; 272 | self.open(true); 273 | }, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout); 274 | } 275 | }; 276 | ws.onmessage = function (event) { 277 | if (self.debug || ReconnectingWebSocket.debugAll) { 278 | console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data); 279 | } 280 | var e = generateEvent('message'); 281 | e.data = event.data; 282 | eventTarget.dispatchEvent(e); 283 | }; 284 | ws.onerror = function (event) { 285 | if (self.debug || ReconnectingWebSocket.debugAll) { 286 | console.debug('ReconnectingWebSocket', 'onerror', self.url, event); 287 | } 288 | eventTarget.dispatchEvent(generateEvent('error')); 289 | }; 290 | } 291 | 292 | // Whether or not to create a websocket upon instantiation 293 | if (this.automaticOpen == true) { 294 | this.open(false); 295 | } 296 | 297 | /** 298 | * Transmits data to the server over the WebSocket connection. 299 | * 300 | * @param data a text string, ArrayBuffer or Blob to send to the server. 301 | */ 302 | this.send = function (data) { 303 | if (ws) { 304 | if (self.debug || ReconnectingWebSocket.debugAll) { 305 | console.debug('ReconnectingWebSocket', 'send', self.url, data); 306 | } 307 | return ws.send(data); 308 | } else { 309 | throw 'INVALID_STATE_ERR : Pausing to reconnect websocket'; 310 | } 311 | }; 312 | 313 | /** 314 | * Closes the WebSocket connection or connection attempt, if any. 315 | * If the connection is already CLOSED, this method does nothing. 316 | */ 317 | this.close = function (code, reason) { 318 | // Default CLOSE_NORMAL code 319 | if (typeof code == 'undefined') { 320 | code = 1000; 321 | } 322 | forcedClose = true; 323 | if (ws) { 324 | ws.close(code, reason); 325 | } 326 | }; 327 | 328 | /** 329 | * Additional public API method to refresh the connection if still open (close, re-open). 330 | * For example, if the app suspects bad data / missed heart beats, it can try to refresh. 331 | */ 332 | this.refresh = function () { 333 | if (ws) { 334 | ws.close(); 335 | } 336 | }; 337 | } 338 | 339 | /** 340 | * An event listener to be called when the WebSocket connection's readyState changes to OPEN; 341 | * this indicates that the connection is ready to send and receive data. 342 | */ 343 | ReconnectingWebSocket.prototype.onopen = function (event) { }; 344 | /** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */ 345 | ReconnectingWebSocket.prototype.onclose = function (event) { }; 346 | /** An event listener to be called when a connection begins being attempted. */ 347 | ReconnectingWebSocket.prototype.onconnecting = function (event) { }; 348 | /** An event listener to be called when a message is received from the server. */ 349 | ReconnectingWebSocket.prototype.onmessage = function (event) { }; 350 | /** An event listener to be called when an error occurs. */ 351 | ReconnectingWebSocket.prototype.onerror = function (event) { }; 352 | 353 | /** 354 | * Whether all instances of ReconnectingWebSocket should log debug messages. 355 | * Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true. 356 | */ 357 | ReconnectingWebSocket.debugAll = false; 358 | 359 | ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING; 360 | ReconnectingWebSocket.OPEN = WebSocket.OPEN; 361 | ReconnectingWebSocket.CLOSING = WebSocket.CLOSING; 362 | ReconnectingWebSocket.CLOSED = WebSocket.CLOSED; 363 | 364 | return ReconnectingWebSocket; 365 | }); 366 | -------------------------------------------------------------------------------- /static/home/css/style_home.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | min-height: 100vh; 6 | background: #32465a85; 7 | font-family: "proxima-nova", "Source Sans Pro", sans-serif; 8 | font-size: 1em; 9 | letter-spacing: 0.1px; 10 | color: #32465a; 11 | text-rendering: optimizeLegibility; 12 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004); 13 | -webkit-font-smoothing: antialiased; 14 | } 15 | 16 | a {text-decoration: none !important ; 17 | color: inherit;} 18 | 19 | .asteriskField{ 20 | display : none; 21 | } 22 | 23 | .collapse-content { 24 | padding: 0 18px; 25 | max-height: 0; 26 | overflow: hidden; 27 | background-color: #32465a85; 28 | transition: max-height 0.2s ease-out; 29 | 30 | } 31 | 32 | 33 | #form-legend{ 34 | text-decoration: none; 35 | color : white; 36 | font-weight: bolder; 37 | } 38 | 39 | 40 | 41 | .form-class{ 42 | margin-left: 100px; 43 | margin-top: 20%; 44 | } 45 | 46 | .input-class{ 47 | color: #496886; 48 | } 49 | 50 | #submit-id-submit{ 51 | display: block; 52 | margin: 10px 42%; 53 | } 54 | 55 | #frame { 56 | width: 100%; 57 | min-width: 360px; 58 | height: 100vh; 59 | min-height: 300px; 60 | background: #E6EAEA; 61 | } 62 | @media screen and (max-width: 360px) { 63 | #frame { 64 | width: 100%; 65 | height: 100vh; 66 | } 67 | } 68 | #frame #sidepanel { 69 | font-size: medium; 70 | float: left; 71 | min-width: 280px; 72 | /* max-width: 340px; */ 73 | width: 100%; 74 | height: 100%; 75 | background: #2c3e50; 76 | color: #f5f5f5; 77 | overflow: hidden; 78 | position: relative; 79 | } 80 | @media screen and (max-width: 735px) { 81 | #frame #sidepanel { 82 | 83 | min-width: 100px; 84 | } 85 | } 86 | #frame #sidepanel #profile { 87 | width: 80%; 88 | margin: 25px auto; 89 | } 90 | @media screen and (max-width: 735px) { 91 | #frame #sidepanel #profile { 92 | width: 100%; 93 | margin: 0 auto; 94 | padding: 5px 0 0 0; 95 | background: #32465a; 96 | } 97 | } 98 | #frame #sidepanel #profile.expanded .wrap { 99 | height: 210px; 100 | line-height: initial; 101 | } 102 | #frame #sidepanel #profile.expanded .wrap p { 103 | margin-top: 20px; 104 | } 105 | #frame #sidepanel #profile.expanded .wrap i.expand-button { 106 | -moz-transform: scaleY(-1); 107 | -o-transform: scaleY(-1); 108 | -webkit-transform: scaleY(-1); 109 | transform: scaleY(-1); 110 | filter: FlipH; 111 | -ms-filter: "FlipH"; 112 | } 113 | #frame #sidepanel #profile .wrap { 114 | height: 65px; 115 | line-height: 65px; 116 | overflow: hidden; 117 | -moz-transition: 0.3s height ease; 118 | -o-transition: 0.3s height ease; 119 | -webkit-transition: 0.3s height ease; 120 | transition: 0.3s height ease; 121 | } 122 | @media screen and (max-width: 735px) { 123 | #frame #sidepanel #profile .wrap { 124 | height: 65px; 125 | } 126 | } 127 | #frame #sidepanel #profile .wrap img { 128 | width: 60px; 129 | border-radius: 50%; 130 | padding: 3px; 131 | border: 0; 132 | height: auto; 133 | float: left; 134 | cursor: pointer; 135 | -moz-transition: 0.3s border ease; 136 | -o-transition: 0.3s border ease; 137 | -webkit-transition: 0.3s border ease; 138 | transition: 0.3s border ease; 139 | } 140 | @media screen and (max-width: 735px) { 141 | #frame #sidepanel #profile .wrap img { 142 | width: 60px; 143 | margin-left: 4px; 144 | } 145 | } 146 | /* #frame #sidepanel #profile .wrap img.online { 147 | border: 2px solid #2ecc71; 148 | } 149 | #frame #sidepanel #profile .wrap img.away { 150 | border: 2px solid #f1c40f; 151 | } 152 | #frame #sidepanel #profile .wrap img.busy { 153 | border: 2px solid #e74c3c; 154 | } 155 | #frame #sidepanel #profile .wrap img.offline { 156 | border: 2px solid #95a5a6; 157 | } */ 158 | #frame #sidepanel #profile .wrap p { 159 | float: left; 160 | margin-left: 15px; 161 | } 162 | @media screen and (max-width: 735px) { 163 | #frame #sidepanel #profile .wrap p { 164 | display: none; 165 | } 166 | } 167 | #frame #sidepanel #profile .wrap i.expand-button { 168 | float: right; 169 | margin-top: 23px; 170 | font-size: 0.8em; 171 | cursor: pointer; 172 | color: #435f7a; 173 | } 174 | @media screen and (max-width: 735px) { 175 | #frame #sidepanel #profile .wrap i.expand-button { 176 | display: none; 177 | } 178 | } 179 | #frame #sidepanel #profile .wrap #status-options { 180 | position: absolute; 181 | opacity: 0; 182 | visibility: hidden; 183 | width: 150px; 184 | margin: 70px 0 0 0; 185 | border-radius: 6px; 186 | z-index: 99; 187 | line-height: initial; 188 | background: #435f7a; 189 | -moz-transition: 0.3s all ease; 190 | -o-transition: 0.3s all ease; 191 | -webkit-transition: 0.3s all ease; 192 | transition: 0.3s all ease; 193 | } 194 | @media screen and (max-width: 735px) { 195 | #frame #sidepanel #profile .wrap #status-options { 196 | width: 58px; 197 | margin-top: 57px; 198 | } 199 | } 200 | #frame #sidepanel #profile .wrap #status-options.active { 201 | opacity: 1; 202 | visibility: visible; 203 | margin: 75px 0 0 0; 204 | } 205 | @media screen and (max-width: 735px) { 206 | #frame #sidepanel #profile .wrap #status-options.active { 207 | margin-top: 62px; 208 | } 209 | } 210 | #frame #sidepanel #profile .wrap #status-options:before { 211 | content: ''; 212 | position: absolute; 213 | width: 0; 214 | height: 0; 215 | border-left: 6px solid transparent; 216 | border-right: 6px solid transparent; 217 | border-bottom: 8px solid #435f7a; 218 | margin: -8px 0 0 24px; 219 | } 220 | @media screen and (max-width: 735px) { 221 | #frame #sidepanel #profile .wrap #status-options:before { 222 | margin-left: 23px; 223 | } 224 | } 225 | #frame #sidepanel #profile .wrap #status-options ul { 226 | overflow: hidden; 227 | border-radius: 6px; 228 | } 229 | #frame #sidepanel #profile .wrap #status-options ul li { 230 | padding: 15px 0 30px 18px; 231 | display: block; 232 | cursor: pointer; 233 | } 234 | @media screen and (max-width: 735px) { 235 | #frame #sidepanel #profile .wrap #status-options ul li { 236 | padding: 15px 0 35px 22px; 237 | } 238 | } 239 | #frame #sidepanel #profile .wrap #status-options ul li:hover { 240 | background: #496886; 241 | } 242 | #frame #sidepanel #profile .wrap #status-options ul li span.status-circle { 243 | position: absolute; 244 | width: 10px; 245 | height: 10px; 246 | border-radius: 50%; 247 | margin: 5px 0 0 0; 248 | } 249 | @media screen and (max-width: 735px) { 250 | #frame #sidepanel #profile .wrap #status-options ul li span.status-circle { 251 | width: 14px; 252 | height: 14px; 253 | } 254 | } 255 | #frame #sidepanel #profile .wrap #status-options ul li span.status-circle:before { 256 | content: ''; 257 | position: absolute; 258 | width: 14px; 259 | height: 14px; 260 | margin: -3px 0 0 -3px; 261 | background: transparent; 262 | border-radius: 50%; 263 | z-index: 0; 264 | } 265 | @media screen and (max-width: 735px) { 266 | #frame #sidepanel #profile .wrap #status-options ul li span.status-circle:before { 267 | height: 18px; 268 | width: 18px; 269 | } 270 | } 271 | #frame #sidepanel #profile .wrap #status-options ul li p { 272 | padding-left: 12px; 273 | } 274 | @media screen and (max-width: 735px) { 275 | #frame #sidepanel #profile .wrap #status-options ul li p { 276 | display: none; 277 | } 278 | } 279 | #frame #sidepanel #profile .wrap #status-options ul li#status-online span.status-circle { 280 | background: #2ecc71; 281 | } 282 | #frame #sidepanel #profile .wrap #status-options ul li#status-online.active span.status-circle:before { 283 | border: 1px solid #2ecc71; 284 | } 285 | #frame #sidepanel #profile .wrap #status-options ul li#status-away span.status-circle { 286 | background: #f1c40f; 287 | } 288 | #frame #sidepanel #profile .wrap #status-options ul li#status-away.active span.status-circle:before { 289 | border: 1px solid #f1c40f; 290 | } 291 | #frame #sidepanel #profile .wrap #status-options ul li#status-busy span.status-circle { 292 | background: #e74c3c; 293 | } 294 | #frame #sidepanel #profile .wrap #status-options ul li#status-busy.active span.status-circle:before { 295 | border: 1px solid #e74c3c; 296 | } 297 | #frame #sidepanel #profile .wrap #status-options ul li#status-offline span.status-circle { 298 | background: #95a5a6; 299 | } 300 | #frame #sidepanel #profile .wrap #status-options ul li#status-offline.active span.status-circle:before { 301 | border: 1px solid #95a5a6; 302 | } 303 | #frame #sidepanel #profile .wrap #expanded { 304 | padding: 100px 0 0 0; 305 | display: block; 306 | line-height: initial !important; 307 | } 308 | #frame #sidepanel #profile .wrap #expanded label { 309 | float: left; 310 | clear: both; 311 | margin: 0 8px 5px 0; 312 | padding: 5px 0; 313 | } 314 | #frame #sidepanel #profile .wrap #expanded input { 315 | border: none; 316 | margin-bottom: 6px; 317 | background: #32465a; 318 | border-radius: 3px; 319 | color: #f5f5f5; 320 | padding: 7px; 321 | width: calc(100% - 43px); 322 | } 323 | #frame #sidepanel #profile .wrap #expanded input:focus { 324 | outline: none; 325 | background: #435f7a; 326 | } 327 | #frame #sidepanel #search { 328 | border-top: 1px solid #32465a; 329 | border-bottom: 1px solid #32465a; 330 | font-weight: 300; 331 | } 332 | @media screen and (max-width: 735px) { 333 | #frame #sidepanel #search { 334 | display: none; 335 | } 336 | } 337 | #frame #sidepanel #search label { 338 | position: absolute; 339 | margin: 10px 0 0 20px; 340 | } 341 | #frame #sidepanel #search input { 342 | font-family: "proxima-nova", "Source Sans Pro", sans-serif; 343 | padding: 10px 0 10px 46px; 344 | width: calc(100% - 25px); 345 | border: none; 346 | background: #32465a; 347 | color: #f5f5f5; 348 | } 349 | #frame #sidepanel #search input:focus { 350 | outline: none; 351 | background: #435f7a; 352 | } 353 | #frame #sidepanel #search input::-webkit-input-placeholder { 354 | color: #f5f5f5; 355 | } 356 | #frame #sidepanel #search input::-moz-placeholder { 357 | color: #f5f5f5; 358 | } 359 | #frame #sidepanel #search input:-ms-input-placeholder { 360 | color: #f5f5f5; 361 | } 362 | #frame #sidepanel #search input:-moz-placeholder { 363 | color: #f5f5f5; 364 | } 365 | #frame #sidepanel #contacts { 366 | height: calc(100% - 177px); 367 | overflow-y: scroll; 368 | overflow-x: hidden; 369 | } 370 | @media screen and (max-width: 735px) { 371 | #frame #sidepanel #contacts { 372 | 373 | overflow-y: scroll; 374 | overflow-x: hidden; 375 | } 376 | #frame #sidepanel #contacts::-webkit-scrollbar { 377 | display: none; 378 | } 379 | } 380 | #frame #sidepanel #contacts.expanded { 381 | height: calc(100% - 334px); 382 | } 383 | #frame #sidepanel #contacts::-webkit-scrollbar { 384 | width: 8px; 385 | background: #2c3e50; 386 | } 387 | #frame #sidepanel #contacts::-webkit-scrollbar-thumb { 388 | background-color: #243140; 389 | } 390 | #frame #sidepanel #contacts ul li.contact { 391 | position: relative; 392 | padding: 10px 0 15px 0; 393 | font-size: 0.9em; 394 | cursor: pointer; 395 | } 396 | @media screen and (max-width: 735px) { 397 | #frame #sidepanel #contacts ul li.contact { 398 | padding: 6px 0 66px 8px; 399 | } 400 | } 401 | #frame #sidepanel #contacts ul li.contact:hover { 402 | background: #32465a; 403 | } 404 | #frame #sidepanel #contacts ul li.contact.active { 405 | background: #32465a; 406 | border-right: 5px solid #435f7a; 407 | } 408 | #frame #sidepanel #contacts ul li.contact.active span.contact-status { 409 | border: 2px solid #32465a !important; 410 | } 411 | #frame #sidepanel #contacts ul li.contact .wrap { 412 | width: 88%; 413 | margin: 0 auto; 414 | position: relative; 415 | } 416 | @media screen and (max-width: 735px) { 417 | #frame #sidepanel #contacts ul li.contact .wrap { 418 | width: 100%; 419 | } 420 | } 421 | #frame #sidepanel #contacts ul li.contact .wrap span { 422 | position: absolute; 423 | left: 0; 424 | margin: -2px 0 0 -2px; 425 | width: 10px; 426 | height: 10px; 427 | border-radius: 50%; 428 | border: 2px solid #2c3e50; 429 | background: #95a5a6; 430 | } 431 | #frame #sidepanel #contacts ul li.contact .wrap span.online { 432 | background: #2ecc71; 433 | } 434 | #frame #sidepanel #contacts ul li.contact .wrap span.away { 435 | background: #f1c40f; 436 | } 437 | #frame #sidepanel #contacts ul li.contact .wrap span.busy { 438 | background: #e74c3c; 439 | } 440 | #frame #sidepanel #contacts ul li.contact .wrap img { 441 | width: 50px; 442 | border-radius: 50%; 443 | float: left; 444 | margin-right: 10px; 445 | } 446 | @media screen and (max-width: 735px) { 447 | #frame #sidepanel #contacts ul li.contact .wrap img { 448 | margin-right: 0px; 449 | } 450 | } 451 | #frame #sidepanel #contacts ul li.contact .wrap .meta { 452 | padding: 5px 0 0 0; 453 | } 454 | @media screen and (max-width: 735px) { 455 | #frame #sidepanel #contacts ul li.contact .wrap .meta { 456 | display: none; 457 | } 458 | } 459 | #frame #sidepanel #contacts ul li.contact .wrap .meta .name { 460 | font-weight: 600; 461 | } 462 | #frame #sidepanel #contacts ul li.contact .wrap .meta .preview { 463 | margin: 5px 0 0 0; 464 | padding: 0 0 1px; 465 | font-weight: 400; 466 | white-space: nowrap; 467 | overflow: hidden; 468 | text-overflow: ellipsis; 469 | -moz-transition: 1s all ease; 470 | -o-transition: 1s all ease; 471 | -webkit-transition: 1s all ease; 472 | transition: 1s all ease; 473 | } 474 | #frame #sidepanel #contacts ul li.contact .wrap .meta .preview span { 475 | position: initial; 476 | border-radius: initial; 477 | background: none; 478 | border: none; 479 | padding: 0 2px 0 0; 480 | margin: 0 0 0 1px; 481 | opacity: .5; 482 | } 483 | #frame #sidepanel #bottom-bar { 484 | position: absolute; 485 | width: 100%; 486 | bottom: 0; 487 | } 488 | #frame #sidepanel #bottom-bar button:not(#submit-id-submit) { 489 | float: left; 490 | border: none; 491 | width: 100%; 492 | padding: 10px 0; 493 | background: #32465a; 494 | color: #f5f5f5; 495 | cursor: pointer; 496 | font-size: 0.85em; 497 | font-family: "proxima-nova", "Source Sans Pro", sans-serif; 498 | } 499 | @media screen and (max-width: 735px) { 500 | #frame #sidepanel #bottom-bar button:not(#submit-id-submit) { 501 | float: none; 502 | width: 100%; 503 | padding: 15px 0; 504 | } 505 | } 506 | #frame #sidepanel #bottom-bar button:not(#submit-id-submit):focus { 507 | outline: none; 508 | } 509 | #frame #sidepanel #bottom-bar button:not(#submit-id-submit):nth-child(1) { 510 | border-right: 1px solid #2c3e50; 511 | } 512 | @media screen and (max-width: 735px) { 513 | #frame #sidepanel #bottom-bar button:not(#submit-id-submit):nth-child(1) { 514 | border-right: none; 515 | border-bottom: 1px solid #2c3e50; 516 | } 517 | } 518 | #frame #sidepanel #bottom-bar button:not(#submit-id-submit):hover { 519 | background: #435f7a; 520 | } 521 | #frame #sidepanel #bottom-bar button:not(#submit-id-submit) i { 522 | margin-right: 3px; 523 | font-size: 1em; 524 | } 525 | @media screen and (max-width: 735px) { 526 | #frame #sidepanel #bottom-bar button:not(#submit-id-submit) i { 527 | font-size: 1.3em; 528 | } 529 | } 530 | @media screen and (max-width: 735px) { 531 | #frame #sidepanel #bottom-bar button span { 532 | display: none; 533 | } 534 | } 535 | 536 | .fa { 537 | font-size: 1.5em !important; 538 | } -------------------------------------------------------------------------------- /static/home/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | min-height: 100vh; 6 | background: #32465a; 7 | font-family: "proxima-nova", "Source Sans Pro", sans-serif; 8 | font-size: 1em; 9 | letter-spacing: 0.1px; 10 | color: #32465a; 11 | text-rendering: optimizeLegibility; 12 | text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.004); 13 | -webkit-font-smoothing: antialiased; 14 | } 15 | 16 | #older_msg_div{ 17 | text-align: end; 18 | } 19 | 20 | #check_older_messages{ 21 | background-color: #2c3e50; /* Green */ 22 | border: none; 23 | color: white; 24 | font-size: 12px; 25 | text-align: center; 26 | text-decoration: none; 27 | display: inline-block; 28 | cursor: pointer; 29 | } 30 | 31 | .no-mp{ 32 | margin:0 !important; 33 | padding:0 !important; 34 | } 35 | 36 | .time_posted{ 37 | font-size: 12px; 38 | } 39 | 40 | #frame { 41 | width: 100%; 42 | min-width: 360px; 43 | height: 100vh; 44 | min-height: 300px; 45 | background: #E6EAEA; 46 | } 47 | @media screen and (max-width: 360px) { 48 | #frame { 49 | width: 100%; 50 | height: 100vh; 51 | } 52 | } 53 | #frame #sidepanel { 54 | font-size: medium; 55 | float: left; 56 | min-width: 280px; 57 | max-width: 340px; 58 | width: 40%; 59 | height: 100%; 60 | background: #2c3e50; 61 | color: #f5f5f5; 62 | overflow: hidden; 63 | position: relative; 64 | } 65 | @media screen and (max-width: 735px) { 66 | #frame #sidepanel { 67 | width: 100px; 68 | min-width: 100px; 69 | } 70 | } 71 | #frame #sidepanel #profile { 72 | width: 80%; 73 | margin: 25px auto; 74 | } 75 | @media screen and (max-width: 735px) { 76 | #frame #sidepanel #profile { 77 | width: 100%; 78 | margin: 0 auto; 79 | padding: 5px 0 0 0; 80 | background: #32465a; 81 | } 82 | } 83 | #frame #sidepanel #profile.expanded .wrap { 84 | height: 210px; 85 | line-height: initial; 86 | } 87 | #frame #sidepanel #profile.expanded .wrap p { 88 | margin-top: 20px; 89 | } 90 | #frame #sidepanel #profile.expanded .wrap i.expand-button { 91 | -moz-transform: scaleY(-1); 92 | -o-transform: scaleY(-1); 93 | -webkit-transform: scaleY(-1); 94 | transform: scaleY(-1); 95 | filter: FlipH; 96 | -ms-filter: "FlipH"; 97 | } 98 | #frame #sidepanel #profile .wrap { 99 | height: 65px; 100 | line-height: 65px; 101 | overflow: hidden; 102 | -moz-transition: 0.3s height ease; 103 | -o-transition: 0.3s height ease; 104 | -webkit-transition: 0.3s height ease; 105 | transition: 0.3s height ease; 106 | } 107 | @media screen and (max-width: 735px) { 108 | #frame #sidepanel #profile .wrap { 109 | height: 65px; 110 | } 111 | } 112 | #frame #sidepanel #profile .wrap img { 113 | width: 60px; 114 | border-radius: 50%; 115 | padding: 3px; 116 | border: 2px solid #e74c3c; 117 | height: auto; 118 | float: left; 119 | cursor: pointer; 120 | -moz-transition: 0.3s border ease; 121 | -o-transition: 0.3s border ease; 122 | -webkit-transition: 0.3s border ease; 123 | transition: 0.3s border ease; 124 | } 125 | @media screen and (max-width: 735px) { 126 | #frame #sidepanel #profile .wrap img { 127 | width: 60px; 128 | margin-left: 4px; 129 | } 130 | } 131 | #frame #sidepanel #profile .wrap img.online { 132 | border: 2px solid #2ecc71; 133 | } 134 | #frame #sidepanel #profile .wrap img.away { 135 | border: 2px solid #f1c40f; 136 | } 137 | #frame #sidepanel #profile .wrap img.busy { 138 | border: 2px solid #e74c3c; 139 | } 140 | #frame #sidepanel #profile .wrap img.offline { 141 | border: 2px solid #95a5a6; 142 | } 143 | #frame #sidepanel #profile .wrap p { 144 | float: left; 145 | margin-left: 15px; 146 | } 147 | @media screen and (max-width: 735px) { 148 | #frame #sidepanel #profile .wrap p { 149 | display: none; 150 | } 151 | } 152 | #frame #sidepanel #profile .wrap i.expand-button { 153 | float: right; 154 | margin-top: 23px; 155 | font-size: 0.8em; 156 | cursor: pointer; 157 | color: #435f7a; 158 | } 159 | @media screen and (max-width: 735px) { 160 | #frame #sidepanel #profile .wrap i.expand-button { 161 | display: none; 162 | } 163 | } 164 | #frame #sidepanel #profile .wrap #status-options { 165 | position: absolute; 166 | opacity: 0; 167 | visibility: hidden; 168 | width: 150px; 169 | margin: 70px 0 0 0; 170 | border-radius: 6px; 171 | z-index: 99; 172 | line-height: initial; 173 | background: #435f7a; 174 | -moz-transition: 0.3s all ease; 175 | -o-transition: 0.3s all ease; 176 | -webkit-transition: 0.3s all ease; 177 | transition: 0.3s all ease; 178 | } 179 | @media screen and (max-width: 735px) { 180 | #frame #sidepanel #profile .wrap #status-options { 181 | width: 58px; 182 | margin-top: 57px; 183 | } 184 | } 185 | #frame #sidepanel #profile .wrap #status-options.active { 186 | opacity: 1; 187 | visibility: visible; 188 | margin: 75px 0 0 0; 189 | } 190 | @media screen and (max-width: 735px) { 191 | #frame #sidepanel #profile .wrap #status-options.active { 192 | margin-top: 62px; 193 | } 194 | } 195 | #frame #sidepanel #profile .wrap #status-options:before { 196 | content: ''; 197 | position: absolute; 198 | width: 0; 199 | height: 0; 200 | border-left: 6px solid transparent; 201 | border-right: 6px solid transparent; 202 | border-bottom: 8px solid #435f7a; 203 | margin: -8px 0 0 24px; 204 | } 205 | @media screen and (max-width: 735px) { 206 | #frame #sidepanel #profile .wrap #status-options:before { 207 | margin-left: 23px; 208 | } 209 | } 210 | #frame #sidepanel #profile .wrap #status-options ul { 211 | overflow: hidden; 212 | border-radius: 6px; 213 | } 214 | #frame #sidepanel #profile .wrap #status-options ul li { 215 | padding: 15px 0 30px 18px; 216 | display: block; 217 | cursor: pointer; 218 | } 219 | @media screen and (max-width: 735px) { 220 | #frame #sidepanel #profile .wrap #status-options ul li { 221 | padding: 15px 0 35px 22px; 222 | } 223 | } 224 | #frame #sidepanel #profile .wrap #status-options ul li:hover { 225 | background: #496886; 226 | } 227 | #frame #sidepanel #profile .wrap #status-options ul li span.status-circle { 228 | position: absolute; 229 | width: 10px; 230 | height: 10px; 231 | border-radius: 50%; 232 | margin: 5px 0 0 0; 233 | } 234 | @media screen and (max-width: 735px) { 235 | #frame #sidepanel #profile .wrap #status-options ul li span.status-circle { 236 | width: 14px; 237 | height: 14px; 238 | } 239 | } 240 | #frame #sidepanel #profile .wrap #status-options ul li span.status-circle:before { 241 | content: ''; 242 | position: absolute; 243 | width: 14px; 244 | height: 14px; 245 | margin: -3px 0 0 -3px; 246 | background: transparent; 247 | border-radius: 50%; 248 | z-index: 0; 249 | } 250 | @media screen and (max-width: 735px) { 251 | #frame #sidepanel #profile .wrap #status-options ul li span.status-circle:before { 252 | height: 18px; 253 | width: 18px; 254 | } 255 | } 256 | #frame #sidepanel #profile .wrap #status-options ul li p { 257 | padding-left: 12px; 258 | } 259 | @media screen and (max-width: 735px) { 260 | #frame #sidepanel #profile .wrap #status-options ul li p { 261 | display: none; 262 | } 263 | } 264 | #frame #sidepanel #profile .wrap #status-options ul li#status-online span.status-circle { 265 | background: #2ecc71; 266 | } 267 | #frame #sidepanel #profile .wrap #status-options ul li#status-online.active span.status-circle:before { 268 | border: 1px solid #2ecc71; 269 | } 270 | #frame #sidepanel #profile .wrap #status-options ul li#status-away span.status-circle { 271 | background: #f1c40f; 272 | } 273 | #frame #sidepanel #profile .wrap #status-options ul li#status-away.active span.status-circle:before { 274 | border: 1px solid #f1c40f; 275 | } 276 | #frame #sidepanel #profile .wrap #status-options ul li#status-busy span.status-circle { 277 | background: #e74c3c; 278 | } 279 | #frame #sidepanel #profile .wrap #status-options ul li#status-busy.active span.status-circle:before { 280 | border: 1px solid #e74c3c; 281 | } 282 | #frame #sidepanel #profile .wrap #status-options ul li#status-offline span.status-circle { 283 | background: #95a5a6; 284 | } 285 | #frame #sidepanel #profile .wrap #status-options ul li#status-offline.active span.status-circle:before { 286 | border: 1px solid #95a5a6; 287 | } 288 | #frame #sidepanel #profile .wrap #expanded { 289 | padding: 100px 0 0 0; 290 | display: block; 291 | line-height: initial !important; 292 | } 293 | #frame #sidepanel #profile .wrap #expanded label { 294 | float: left; 295 | clear: both; 296 | margin: 0 8px 5px 0; 297 | padding: 5px 0; 298 | } 299 | #frame #sidepanel #profile .wrap #expanded input { 300 | border: none; 301 | margin-bottom: 6px; 302 | background: #32465a; 303 | border-radius: 3px; 304 | color: #f5f5f5; 305 | padding: 7px; 306 | width: calc(100% - 43px); 307 | } 308 | #frame #sidepanel #profile .wrap #expanded input:focus { 309 | outline: none; 310 | background: #435f7a; 311 | } 312 | #frame #sidepanel #search { 313 | border-top: 1px solid #32465a; 314 | border-bottom: 1px solid #32465a; 315 | font-weight: 300; 316 | } 317 | @media screen and (max-width: 735px) { 318 | #frame #sidepanel #search { 319 | display: none; 320 | } 321 | } 322 | #frame #sidepanel #search label { 323 | position: absolute; 324 | margin: 10px 0 0 20px; 325 | } 326 | #frame #sidepanel #search input { 327 | font-family: "proxima-nova", "Source Sans Pro", sans-serif; 328 | padding: 10px 0 10px 46px; 329 | width: calc(100% - 25px); 330 | border: none; 331 | background: #32465a; 332 | color: #f5f5f5; 333 | } 334 | #frame #sidepanel #search input:focus { 335 | outline: none; 336 | background: #435f7a; 337 | } 338 | #frame #sidepanel #search input::-webkit-input-placeholder { 339 | color: #f5f5f5; 340 | } 341 | #frame #sidepanel #search input::-moz-placeholder { 342 | color: #f5f5f5; 343 | } 344 | #frame #sidepanel #search input:-ms-input-placeholder { 345 | color: #f5f5f5; 346 | } 347 | #frame #sidepanel #search input:-moz-placeholder { 348 | color: #f5f5f5; 349 | } 350 | #frame #sidepanel #contacts { 351 | height: calc(100% - 177px); 352 | overflow-y: scroll; 353 | overflow-x: hidden; 354 | } 355 | @media screen and (max-width: 735px) { 356 | #frame #sidepanel #contacts { 357 | height: calc(100% - 149px); 358 | overflow-y: scroll; 359 | overflow-x: hidden; 360 | } 361 | #frame #sidepanel #contacts::-webkit-scrollbar { 362 | display: none; 363 | } 364 | } 365 | #frame #sidepanel #contacts.expanded { 366 | height: calc(100% - 334px); 367 | } 368 | #frame #sidepanel #contacts::-webkit-scrollbar { 369 | width: 8px; 370 | background: #2c3e50; 371 | } 372 | #frame #sidepanel #contacts::-webkit-scrollbar-thumb { 373 | background-color: #243140; 374 | } 375 | #frame #sidepanel #contacts ul li.contact { 376 | position: relative; 377 | padding: 10px 0 15px 0; 378 | font-size: 0.9em; 379 | cursor: pointer; 380 | } 381 | @media screen and (max-width: 735px) { 382 | #frame #sidepanel #contacts ul li.contact { 383 | padding: 6px 0 66px 8px; 384 | } 385 | } 386 | #frame #sidepanel #contacts ul li.contact:hover { 387 | background: #32465a; 388 | } 389 | #frame #sidepanel #contacts ul li.contact.active { 390 | background: #32465a; 391 | border-right: 5px solid #435f7a; 392 | } 393 | #frame #sidepanel #contacts ul li.contact.active span.contact-status { 394 | border: 2px solid #32465a !important; 395 | } 396 | #frame #sidepanel #contacts ul li.contact .wrap { 397 | width: 88%; 398 | margin: 0 auto; 399 | position: relative; 400 | } 401 | @media screen and (max-width: 735px) { 402 | #frame #sidepanel #contacts ul li.contact .wrap { 403 | width: 100%; 404 | } 405 | } 406 | #frame #sidepanel #contacts ul li.contact .wrap span { 407 | position: absolute; 408 | left: 0; 409 | margin: -2px 0 0 -2px; 410 | width: 10px; 411 | height: 10px; 412 | border-radius: 50%; 413 | border: 2px solid #2c3e50; 414 | background: #95a5a6; 415 | } 416 | #frame #sidepanel #contacts ul li.contact .wrap span.online { 417 | background: #2ecc71; 418 | } 419 | #frame #sidepanel #contacts ul li.contact .wrap span.away { 420 | background: #f1c40f; 421 | } 422 | #frame #sidepanel #contacts ul li.contact .wrap span.busy { 423 | background: #e74c3c; 424 | } 425 | #frame #sidepanel #contacts ul li.contact .wrap img { 426 | width: 50px; 427 | border-radius: 50%; 428 | float: left; 429 | margin-right: 10px; 430 | } 431 | @media screen and (max-width: 735px) { 432 | #frame #sidepanel #contacts ul li.contact .wrap img { 433 | margin-right: 0px; 434 | } 435 | } 436 | #frame #sidepanel #contacts ul li.contact .wrap .meta { 437 | padding: 5px 0 0 0; 438 | } 439 | @media screen and (max-width: 735px) { 440 | #frame #sidepanel #contacts ul li.contact .wrap .meta { 441 | display: none; 442 | } 443 | } 444 | #frame #sidepanel #contacts ul li.contact .wrap .meta .name { 445 | font-weight: 600; 446 | } 447 | #frame #sidepanel #contacts ul li.contact .wrap .meta .preview { 448 | margin: 5px 0 0 0; 449 | padding: 0 0 1px; 450 | font-weight: 400; 451 | white-space: nowrap; 452 | overflow: hidden; 453 | text-overflow: ellipsis; 454 | -moz-transition: 1s all ease; 455 | -o-transition: 1s all ease; 456 | -webkit-transition: 1s all ease; 457 | transition: 1s all ease; 458 | } 459 | #frame #sidepanel #contacts ul li.contact .wrap .meta .preview span { 460 | position: initial; 461 | border-radius: initial; 462 | background: none; 463 | border: none; 464 | padding: 0 2px 0 0; 465 | margin: 0 0 0 1px; 466 | opacity: .5; 467 | } 468 | #frame #sidepanel #bottom-bar { 469 | position: absolute; 470 | width: 100%; 471 | bottom: 0; 472 | } 473 | #frame #sidepanel #bottom-bar button { 474 | float: left; 475 | border: none; 476 | width: 100%; 477 | padding: 10px 0; 478 | background: #32465a; 479 | color: #f5f5f5; 480 | cursor: pointer; 481 | font-size: 0.85em; 482 | font-family: "proxima-nova", "Source Sans Pro", sans-serif; 483 | } 484 | @media screen and (max-width: 735px) { 485 | #frame #sidepanel #bottom-bar button { 486 | float: none; 487 | width: 100%; 488 | padding: 15px 0; 489 | } 490 | } 491 | #frame #sidepanel #bottom-bar button:focus { 492 | outline: none; 493 | } 494 | #frame #sidepanel #bottom-bar button:nth-child(1) { 495 | border-right: 1px solid #2c3e50; 496 | } 497 | @media screen and (max-width: 735px) { 498 | #frame #sidepanel #bottom-bar button:nth-child(1) { 499 | border-right: none; 500 | border-bottom: 1px solid #2c3e50; 501 | } 502 | } 503 | #frame #sidepanel #bottom-bar button:hover { 504 | background: #435f7a; 505 | } 506 | #frame #sidepanel #bottom-bar button i { 507 | margin-right: 3px; 508 | font-size: 1em; 509 | } 510 | @media screen and (max-width: 735px) { 511 | #frame #sidepanel #bottom-bar button i { 512 | font-size: 1.3em; 513 | } 514 | } 515 | @media screen and (max-width: 735px) { 516 | #frame #sidepanel #bottom-bar button span { 517 | display: none; 518 | } 519 | } 520 | #frame .content { 521 | float: right; 522 | width: 60%; 523 | height: 100%; 524 | overflow: hidden; 525 | position: relative; 526 | } 527 | @media screen and (max-width: 735px) { 528 | #frame .content { 529 | width: calc(100% - 100px); 530 | min-width: 300px !important; 531 | } 532 | } 533 | @media screen and (min-width: 900px) { 534 | #frame .content { 535 | width: calc(100% - 340px); 536 | } 537 | } 538 | #frame .content .contact-profile { 539 | width: 100%; 540 | height: 80px; 541 | line-height: 80px; 542 | background: #f5f5f5; 543 | } 544 | #frame .content .contact-profile img { 545 | width: 60px; 546 | border-radius: 50%; 547 | float: left; 548 | margin: 9px 12px 0 9px; 549 | } 550 | #frame .content .contact-profile p { 551 | float: left; 552 | font-size: 18px; 553 | } 554 | #frame .content .contact-profile .social-media { 555 | float: right; 556 | } 557 | #frame .content .contact-profile .social-media i { 558 | margin-left: 14px; 559 | cursor: pointer; 560 | } 561 | #frame .content .contact-profile .social-media i:nth-last-child(1) { 562 | margin-right: 20px; 563 | } 564 | #frame .content .contact-profile .social-media i:hover { 565 | color: #435f7a; 566 | } 567 | #frame .content .messages { 568 | height: calc(100% - 138px); 569 | /* min-height: calc(100% - 93px); 570 | max-height: calc(100% - 93px); */ 571 | /* min-height: 79%; 572 | max-height: 79%; */ 573 | overflow-y: scroll; 574 | overflow-x: hidden; 575 | width: 100%; 576 | } 577 | @media screen and (max-width: 735px) { 578 | #frame .content .messages { 579 | max-height: calc(100% - 105px); 580 | } 581 | } 582 | #frame .content .messages::-webkit-scrollbar { 583 | width: 8px; 584 | background: transparent; 585 | } 586 | #frame .content .messages::-webkit-scrollbar-thumb { 587 | background-color: rgba(0, 0, 0, 0.3); 588 | } 589 | #frame .content .messages ul li { 590 | display: inline-block; 591 | clear: both; 592 | float: left; 593 | margin: 15px 15px 5px 15px; 594 | width: calc(100% - 25px); 595 | font-size: 0.9em; 596 | } 597 | #frame .content .messages ul li:nth-last-child(1) { 598 | margin-bottom: 20px; 599 | } 600 | #frame .content .messages ul li.sent img { 601 | margin: 6px 8px 0 0; 602 | } 603 | #frame .content .messages ul li.sent p { 604 | background: #435f7a; 605 | color: #f5f5f5; 606 | } 607 | #frame .content .messages ul li.replies img { 608 | float: right; 609 | margin: 6px 0 0 8px; 610 | } 611 | #frame .content .messages ul li.replies p { 612 | background: #f5f5f5; 613 | float: right; 614 | } 615 | #frame .content .messages ul li img { 616 | width: 42px; 617 | height :42px; 618 | border-radius: 50%; 619 | float: left; 620 | } 621 | #frame .content .messages ul li p { 622 | display: inline-block; 623 | padding: 10px 15px; 624 | border-radius: 20px; 625 | max-width: 300px; 626 | line-height: 130%; 627 | font-size: 18px; 628 | } 629 | @media screen and (min-width: 735px) { 630 | #frame .content .messages ul li p { 631 | max-width: 300px; 632 | } 633 | } 634 | #frame .content .message-input { 635 | position: relative; 636 | bottom: 0; 637 | width: 100%; 638 | z-index: 99; 639 | } 640 | #frame .content .message-input .wrap { 641 | position: relative; 642 | } 643 | #frame .content .message-input .wrap input { 644 | font-family: "proxima-nova", "Source Sans Pro", sans-serif; 645 | float: left; 646 | border: none; 647 | width: calc(100% - 90px); 648 | padding: 11px 32px 10px 8px; 649 | font-size: 1.8em; 650 | color: #32465a; 651 | } 652 | @media screen and (max-width: 735px) { 653 | #frame .content .message-input .wrap input { 654 | padding: 15px 32px 16px 8px; 655 | } 656 | } 657 | #frame .content .message-input .wrap input:focus { 658 | outline: none; 659 | } 660 | #frame .content .message-input .wrap .attachment { 661 | position: absolute; 662 | right: 60px; 663 | z-index: 4; 664 | margin-top: 10px; 665 | font-size: 1.8em; 666 | color: #435f7a; 667 | opacity: .5; 668 | cursor: pointer; 669 | } 670 | @media screen and (max-width: 735px) { 671 | #frame .content .message-input .wrap .attachment { 672 | margin-top: 19px; 673 | right: 65px; 674 | } 675 | } 676 | #frame .content .message-input .wrap .attachment:hover { 677 | opacity: 1; 678 | } 679 | #frame .content .message-input .wrap button { 680 | float: right; 681 | border: none; 682 | width: 50px; 683 | padding: 12px 0; 684 | cursor: pointer; 685 | background: #32465a; 686 | color: #f5f5f5; 687 | } 688 | @media screen and (max-width: 735px) { 689 | #frame .content .message-input .wrap button { 690 | padding: 21px 0; 691 | } 692 | } 693 | #frame .content .message-input .wrap button:hover { 694 | background: #435f7a; 695 | } 696 | #frame .content .message-input .wrap button:focus { 697 | outline: none; 698 | } 699 | 700 | .fa { 701 | font-size: 1.5em !important; 702 | } --------------------------------------------------------------------------------