├── .gitignore ├── README.md ├── chat ├── __init__.py ├── admin.py ├── apps.py ├── consumers.py ├── managers.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_usersetting_is_online.py │ ├── 0003_message_isread.py │ ├── 0004_thread_unread_by_1_thread_unread_by_2.py │ └── __init__.py ├── models.py ├── static │ ├── arrow.svg │ ├── css │ │ ├── fonts.css │ │ └── style.css │ ├── fonts │ │ ├── Circular-Black.ttf │ │ ├── Circular-Bold.ttf │ │ ├── Circular-Book.ttf │ │ └── Circular-Medium.ttf │ ├── icon.svg │ ├── js │ │ ├── main.js │ │ └── settings.js │ ├── notifi.mp3 │ ├── screenshots │ │ └── homeview.png │ └── sendarrow.svg ├── tests.py └── views.py ├── chatapp ├── __init__.py ├── asgi.py ├── routing.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── media └── profile-pics │ └── default.jpg ├── requirements.txt └── templates ├── index-template.html ├── index.html ├── login.html ├── settings.html └── signup.html /.gitignore: -------------------------------------------------------------------------------- 1 | **/**.pyc 2 | 3 | __pycache__ 4 | db.sqlite3 5 | .vscode 6 | media/profile-pics/* 7 | !/media/profile-pics/default.jpg 8 | .venv 9 | .python-version -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # DjangoChat 4 | 5 | # Setup 6 | 7 | The first thing to do is to clone the repository: 8 | 9 | $ git clone https://github.com/Jalitko/DjangoChat.git 10 | $ cd DjangoChat 11 | 12 | Install project dependencies: 13 | 14 | $ pip install -r requirements.tx 15 | 16 | 17 | Create database tables and a superuser account: 18 | 19 | $ python manage.py migrate 20 | $ python manage.py createsuperuser 21 | 22 | 23 | You can now run the development server: 24 | 25 | $ python manage.py runserver 26 | 27 | The site should now be running at `http://localhost:8000`. 28 | To access Django administration site, log in as a superuser, then 29 | visit `http://localhost:8000/admin/` 30 | -------------------------------------------------------------------------------- /chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jalitko/DjangoChat/5b4c2758d122998016082e7497b423a55756867d/chat/__init__.py -------------------------------------------------------------------------------- /chat/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Message, Thread, UserSetting 3 | 4 | # Register your models here. 5 | admin.site.register(UserSetting) 6 | 7 | 8 | class MessageInline(admin.StackedInline): 9 | model = Message 10 | fields = ('sender', 'text', 'isread') 11 | readonly_fields = ('sender', 'text', 'isread') 12 | 13 | class ThreadAdmin(admin.ModelAdmin): 14 | model = Thread 15 | inlines = (MessageInline,) 16 | 17 | admin.site.register(Thread, ThreadAdmin) -------------------------------------------------------------------------------- /chat/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'chat' 7 | -------------------------------------------------------------------------------- /chat/consumers.py: -------------------------------------------------------------------------------- 1 | from channels.consumer import SyncConsumer, AsyncConsumer 2 | from channels.db import database_sync_to_async 3 | from asgiref.sync import async_to_sync, sync_to_async 4 | from django.contrib.auth.models import User 5 | from chat.models import Message, Thread, UserSetting 6 | import json 7 | from rich.console import Console 8 | console = Console(style='bold green') 9 | 10 | 11 | online_users = [] 12 | class WebConsumer(AsyncConsumer): 13 | async def websocket_connect(self, event): 14 | self.me = self.scope['user'] 15 | self.room_name = str(self.me.id) 16 | 17 | online_users.append(self.me.id) 18 | await self.channel_layer.group_add(self.room_name, self.channel_name) 19 | await self.send({ 20 | 'type': 'websocket.accept', 21 | }) 22 | 23 | console.print(f'You are connected {self.room_name}') 24 | 25 | async def websocket_receive(self, event): 26 | event = json.loads(event['text']) 27 | # console.print(f'Received message: {event["type"]}') 28 | 29 | if event['type'] == 'message': 30 | msg = await self.send_msg(event) 31 | await self.send_users(msg, [self.me.id, self.them_user.id]) 32 | elif event['type'] == 'online': 33 | msg = await self.send_online(event) 34 | console.print(online_users) 35 | await self.send_users(msg, []) 36 | elif event['type'] == 'read': 37 | msg = await sync_to_async(Message.objects.get)(id=event['id']) 38 | msg.isread = True 39 | await sync_to_async(msg.save)() 40 | 41 | msg_thread = await sync_to_async(Thread.objects.get)(message=msg) 42 | await self.unread(msg_thread, int(event['user']), -1) 43 | elif event['type'] == 'istyping': 44 | console.print(self.me, event) 45 | await self.send_istyping(event) 46 | 47 | 48 | async def websocket_message(self, event): 49 | await self.send( 50 | { 51 | 'type': 'websocket.send', 52 | 'text': event.get('text'), 53 | } 54 | ) 55 | 56 | async def websocket_disconnect(self, event): 57 | console.print(f'[{self.channel_name}] - Disconnected') 58 | 59 | event = json.loads('''{ 60 | "type": "online", 61 | "set": "false" 62 | }''') 63 | 64 | online_users.remove(self.me.id) 65 | msg = await self.send_online(event) 66 | await self.send_users(msg, []) 67 | 68 | await self.channel_layer.group_discard(self.room_name, self.channel_name) 69 | 70 | async def send_msg(self, msg): 71 | 72 | them_id = msg['to'] 73 | self.them_user = await sync_to_async(User.objects.get)(id=them_id) 74 | self.thread = await sync_to_async(Thread.objects.get_or_create_thread)(self.me, self.them_user) 75 | await self.store_message(msg['message']) 76 | await self.unread(self.thread, self.me.id, 1) 77 | 78 | await self.send_notifi([self.me.id, self.them_user.id]) 79 | return json.dumps({ 80 | 'type': 'message', 81 | 'sender': them_id, 82 | }) 83 | 84 | @database_sync_to_async 85 | def store_message(self, text): 86 | Message.objects.create( 87 | thread = self.thread, 88 | sender = self.scope['user'], 89 | text = text, 90 | ) 91 | 92 | async def send_users(self, msg, users=[]): 93 | if not users: users = online_users 94 | 95 | for user in users: 96 | await self.channel_layer.group_send( 97 | str(user), 98 | { 99 | 'type': 'websocket.message', 100 | 'text': msg, 101 | }, 102 | ) 103 | 104 | async def send_online(self, event): 105 | user = self.scope['user'] 106 | await self.store_is_online(user, event['set']) 107 | return json.dumps({ 108 | 'type': 'online', 109 | 'set': event['set'], 110 | 'user': user.id 111 | }) 112 | 113 | async def store_is_online(self, user, value): 114 | if value == 'true': value = True 115 | else: value = False 116 | 117 | settings = await sync_to_async(UserSetting.objects.get)(id=user.id) 118 | settings.is_online = value 119 | await sync_to_async(settings.save)() 120 | 121 | async def send_notifi(self, users): 122 | 123 | console.print(f'NOTIFI {users}') 124 | 125 | for i in range(len(users)): 126 | text = json.dumps({ 127 | 'type': 'notifi', 128 | 'user': users[i-1], 129 | 'sender': users[0] 130 | }) 131 | 132 | await self.channel_layer.group_send( 133 | str(users[i]), 134 | { 135 | 'type': 'websocket.message', 136 | 'text': text, 137 | }, 138 | ) 139 | 140 | async def unread(self, thread, user, plus): 141 | users = await sync_to_async(thread.users.first)() 142 | 143 | if(users.id != int(user)): 144 | thread.unread_by_1 += plus 145 | else: 146 | thread.unread_by_2 += plus 147 | 148 | await sync_to_async(thread.save)() 149 | 150 | async def send_istyping(self, event): 151 | text = json.dumps({ 152 | 'type': 'istyping', 153 | 'set': event['set'], 154 | }) 155 | 156 | await self.channel_layer.group_send( 157 | str(event['user']), 158 | { 159 | 'type': 'websocket.message', 160 | 'text': text, 161 | }, 162 | ) -------------------------------------------------------------------------------- /chat/managers.py: -------------------------------------------------------------------------------- 1 | from unicodedata import name 2 | from django.db import models 3 | from django.db.models import Count 4 | 5 | 6 | 7 | class ThreadManager(models.Manager): 8 | def get_or_create_thread(self, user1, user2) : 9 | 10 | pair = self.get_pair(user1.id, user2.id) 11 | 12 | threads = self.get_queryset().filter(name=pair) 13 | if threads.exists(): 14 | return threads.first() 15 | else: 16 | thread = self.create() 17 | thread.users.add(user1) 18 | thread.users.add(user2) 19 | thread.name = pair 20 | thread.save() 21 | return thread 22 | 23 | 24 | 25 | def get_pair(self, x, y): 26 | return x * x + x + y if x>y else y * y + y + x 27 | -------------------------------------------------------------------------------- /chat/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.2 on 2022-10-17 21:48 2 | 3 | import chat.models 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='UserSetting', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('username', models.CharField(default='', max_length=32)), 23 | ('profile_image', models.ImageField(blank=True, default='\\profile-pics\\default.jpg', null=True, upload_to=chat.models.random_file_name)), 24 | ('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | migrations.CreateModel( 28 | name='Thread', 29 | fields=[ 30 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 31 | ('created_at', models.DateTimeField(auto_now_add=True)), 32 | ('updated_at', models.DateTimeField(auto_now=True)), 33 | ('name', models.CharField(blank=True, max_length=50, null=True)), 34 | ('users', models.ManyToManyField(to=settings.AUTH_USER_MODEL)), 35 | ], 36 | options={ 37 | 'abstract': False, 38 | }, 39 | ), 40 | migrations.CreateModel( 41 | name='Message', 42 | fields=[ 43 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 44 | ('created_at', models.DateTimeField(auto_now_add=True)), 45 | ('updated_at', models.DateTimeField(auto_now=True)), 46 | ('text', models.TextField()), 47 | ('sender', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 48 | ('thread', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='chat.thread')), 49 | ], 50 | options={ 51 | 'abstract': False, 52 | }, 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /chat/migrations/0002_usersetting_is_online.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.2 on 2022-10-25 23:17 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('chat', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='usersetting', 15 | name='is_online', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /chat/migrations/0003_message_isread.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.2 on 2022-11-17 20:07 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('chat', '0002_usersetting_is_online'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='message', 15 | name='isread', 16 | field=models.BooleanField(default=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /chat/migrations/0004_thread_unread_by_1_thread_unread_by_2.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.2 on 2022-11-17 23:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('chat', '0003_message_isread'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='thread', 15 | name='unread_by_1', 16 | field=models.PositiveIntegerField(default=0), 17 | ), 18 | migrations.AddField( 19 | model_name='thread', 20 | name='unread_by_2', 21 | field=models.PositiveIntegerField(default=0), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /chat/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jalitko/DjangoChat/5b4c2758d122998016082e7497b423a55756867d/chat/migrations/__init__.py -------------------------------------------------------------------------------- /chat/models.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import thread 2 | from chat.managers import ThreadManager 3 | from django.db import models 4 | from django.contrib.auth.models import User 5 | import os, uuid 6 | 7 | 8 | # Create your models here. 9 | def random_file_name(instance, filename): 10 | ext = filename.split('.')[-1] 11 | filename = "%s.%s" % (uuid.uuid4(), ext) 12 | return os.path.join('profile-pics', filename) 13 | 14 | 15 | 16 | 17 | class UserSetting(models.Model): 18 | user = models.OneToOneField(User, null=True, on_delete=models.CASCADE) 19 | username = models.CharField(max_length=32, default="") 20 | profile_image = models.ImageField(upload_to=random_file_name, blank=True, null=True, default='\\profile-pics\\default.jpg') 21 | is_online = models.BooleanField(default=False) 22 | 23 | def __str__(self): 24 | return str(self.user) 25 | 26 | 27 | class TrackingModel(models.Model): 28 | created_at = models.DateTimeField(auto_now_add=True) 29 | updated_at = models.DateTimeField(auto_now=True) 30 | 31 | class Meta: 32 | abstract = True 33 | 34 | class Thread(TrackingModel): 35 | name = models.CharField(max_length=50, null=True, blank=True) 36 | users = models.ManyToManyField('auth.User') 37 | unread_by_1 = models.PositiveIntegerField(default=0) 38 | unread_by_2 = models.PositiveIntegerField(default=0) 39 | 40 | objects = ThreadManager() 41 | 42 | 43 | def __str__(self): 44 | return f'{self.name} \t -> \t {self.users.first()} - {self.users.last()}' 45 | 46 | 47 | 48 | class Message(TrackingModel): 49 | thread = models.ForeignKey(Thread, on_delete=models.CASCADE) 50 | sender = models.ForeignKey('auth.User', on_delete=models.CASCADE) 51 | text = models.TextField(blank=False, null=False) 52 | isread = models.BooleanField(default=False) 53 | 54 | 55 | def __str__(self): 56 | return f'From Thread - {self.thread.name}' 57 | -------------------------------------------------------------------------------- /chat/static/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chat/static/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Circular-Black'; 3 | src: url('/static/fonts/Circular-Black.ttf') format("truetype"); 4 | } 5 | 6 | @font-face { 7 | font-family: 'Circular-Bold'; 8 | src: url('/static/fonts/Circular-Bold.ttf') format("truetype"); 9 | } 10 | 11 | @font-face { 12 | font-family: 'Circular-Medium'; 13 | src: url('/static/fonts/Circular-Medium.ttf') format("truetype"); 14 | } 15 | 16 | @font-face { 17 | font-family: 'Circular-Book'; 18 | src: url('/static/fonts/Circular-Book.ttf') format("truetype"); 19 | } 20 | -------------------------------------------------------------------------------- /chat/static/css/style.css: -------------------------------------------------------------------------------- 1 | @import url("fonts.css"); 2 | 3 | :root{ 4 | --Yankees-Blue: #122643; 5 | --ScrollBar: #aaaaaa; 6 | --Space-Cadet: #172F4D; 7 | --Space-Cadet-hover: #1a2a42; 8 | --Melon: #FFB6B5; 9 | --Lines: #F2F3F5; 10 | --Text: #9FA5B0; 11 | --Them-Bubble: #D9D9D9; 12 | --Online-Dot: #38E081; 13 | --Offline-Dot: #ed4245; 14 | } 15 | 16 | body{ 17 | background-color: var(--Yankees-Blue); 18 | } 19 | 20 | ::-webkit-scrollbar{ 21 | background: transparent; 22 | width: 7px; 23 | } 24 | ::-webkit-scrollbar-thumb{ 25 | background: var(--ScrollBar); 26 | border-radius: 100px; 27 | } 28 | .scroller { 29 | scrollbar-color: var(--ScrollBar) transparent ; 30 | scrollbar-width: thick !important; 31 | } 32 | 33 | 34 | /* LOGIN AND SIGNUP PAGE */ 35 | .container{ 36 | background-color: var(--Space-Cadet); 37 | width: 450px; 38 | height: 550px; 39 | display: flex; 40 | position: absolute; 41 | transform: translate(-50%,-50%); 42 | top: 25%; 43 | left: 50%; 44 | border: 0; 45 | margin-top: 10em; 46 | border-radius: 10%; 47 | flex-wrap: wrap; 48 | letter-spacing: 0.5px; 49 | padding: 18px 20px; 50 | box-shadow: 6px 12px 34.5px 25.5px rgba(0, 0, 0, 0.25); 51 | } 52 | 53 | .logodiv{ 54 | height: 100px; 55 | width: auto; 56 | } 57 | .logo{ 58 | height: 80px; 59 | margin-top: 30px; 60 | margin-left: 37px; 61 | } 62 | 63 | .logotext{ 64 | font-family: 'Circular-Black'; 65 | font-size: 50px; 66 | margin-top: 40px; 67 | margin-left: 10px; 68 | color: white; 69 | float: right; 70 | } 71 | 72 | .loginForm{ 73 | margin-left: auto; 74 | margin-right: auto; 75 | bottom: 170px; 76 | height: 20%; 77 | width: 80%; 78 | position: relative; 79 | flex-wrap: wrap; 80 | margin-top: 50px; 81 | } 82 | 83 | .label{ 84 | display: block; 85 | margin-top: 30px; 86 | font-family: 'Circular-Medium'; 87 | color: white; 88 | font-size: 16px; 89 | font-weight: 500; 90 | } 91 | 92 | .login-signup-input{ 93 | display: block; 94 | height: 50px; 95 | width: 100%; 96 | background-color: rgba(255,255,255,0.07); 97 | border-radius: 200px; 98 | padding: 0 10px; 99 | margin-top: 8px; 100 | font-family: 'Circular-Book'; 101 | color: white; 102 | font-size: 14px; 103 | font-weight: 300; 104 | outline: none; 105 | border: none; 106 | } 107 | ::placeholder{ 108 | color: #e5e5e5; 109 | } 110 | 111 | .showpsd{ 112 | background-color: transparent; 113 | color:white; 114 | position: absolute; 115 | cursor: pointer; 116 | border: none; 117 | right:0; 118 | bottom: -92px; 119 | font-size: 20px; 120 | } 121 | 122 | .login-btn{ 123 | margin-top: 50px; 124 | width: 100%; 125 | background-color: var(--Melon); 126 | font-family: Circular-Bold; 127 | color: white; 128 | padding: 15px 0; 129 | font-size: 18px; 130 | font-weight: 600; 131 | border-radius: 200px; 132 | cursor: pointer; 133 | border: none; 134 | } 135 | 136 | .signup-btn{ 137 | cursor: pointer; 138 | font-family: 'Circular-Medium'; 139 | color: white; 140 | font-size: 16px; 141 | font-weight: 500; 142 | margin-top: 16px; 143 | display: flex; 144 | justify-content: center; 145 | text-decoration: none; 146 | } 147 | 148 | .error{ 149 | font-family: 'Circular-Book'; 150 | color: red; 151 | } 152 | 153 | 154 | 155 | 156 | 157 | 158 | /* INDEX */ 159 | .chats-container{ 160 | background-color: white; 161 | position: absolute; 162 | width: 40%; 163 | height: 80%; 164 | bottom: 0; 165 | left: 0; 166 | border-radius: 74.25px 74.25px 0px 0px; 167 | overflow: hidden; 168 | box-shadow: 6px 12px 34.5px 25.5px rgba(0, 0, 0, 0.25); 169 | box-sizing: border-box; 170 | padding: 42px 0px 0px 8px; 171 | } 172 | 173 | .chat-container{ 174 | background-color: white; 175 | position: absolute; 176 | width: 82%; 177 | height: 80%; 178 | bottom: 0; 179 | right: 0; 180 | z-index: -1; 181 | padding: 0px 0px 0px 0px; 182 | box-sizing: border-box; 183 | } 184 | 185 | .profile-picture { 186 | width: 75px; 187 | height: 75px; 188 | border-top-left-radius: 50% 50%; 189 | border-top-right-radius: 50% 50%; 190 | border-bottom-left-radius: 50% 50%; 191 | border-bottom-right-radius: 50% 50%; 192 | } 193 | .profile-picture-div{ 194 | position: relative; 195 | height: 75px; 196 | width: 75px; 197 | } 198 | .profile-picture-div.main-profile{ 199 | position: absolute; 200 | left: 36px; 201 | top: 39px; 202 | } 203 | .profile-picture-div.active{ 204 | position: absolute; 205 | left: 43%; 206 | top: 39px; 207 | z-index: -1; 208 | } 209 | 210 | .online-dot{ 211 | position: absolute; 212 | bottom: 2px; 213 | right: 7px; 214 | width: 15px; 215 | height: 15px; 216 | border-radius: 50%; 217 | 218 | z-index: 7; 219 | } 220 | .online-dot.offline{ 221 | background: var(--Offline-Dot); 222 | } 223 | .online-dot.online{ 224 | background: var(--Online-Dot); 225 | } 226 | 227 | .online-users{ 228 | position: relative; 229 | display: flex; 230 | overflow: auto; 231 | scroll-behavior: smooth; 232 | -ms-overflow-style: none; 233 | scrollbar-width: none; 234 | } 235 | .online-users::-webkit-scrollbar { 236 | display: none; 237 | } 238 | 239 | .online-user{ 240 | position: relative; 241 | width: 75px; 242 | text-align: center; 243 | padding-left: 29px; 244 | cursor: pointer; 245 | user-select: none; 246 | -moz-user-select: none; 247 | -khtml-user-select: none; 248 | -webkit-user-select: none; 249 | -o-user-select: none; 250 | } 251 | 252 | .messages{ 253 | position: absolute; 254 | left: 36px; 255 | top: 120px; 256 | font-family: 'Circular-Medium'; 257 | font-weight: 700; 258 | font-size: 30px; 259 | line-height: 38px; 260 | color: white; 261 | } 262 | .active-name{ 263 | position: absolute; 264 | left: calc(43% + 90px); 265 | top: 62px; 266 | font-family: 'Circular-Medium'; 267 | font-weight: 500; 268 | font-size: 24px; 269 | line-height: 30px; 270 | color: white; 271 | } 272 | .profile-name{ 273 | font-family: 'Circular-Book'; 274 | font-weight: 450; 275 | font-size: 12px; 276 | line-height: 15px; 277 | display: -webkit-box; 278 | -webkit-line-clamp: 1; 279 | -webkit-box-orient: vertical; 280 | overflow: hidden; 281 | } 282 | 283 | .arrow{ 284 | height: 25px; 285 | top: 0; 286 | right: 0; 287 | margin-top: 62px; 288 | position:absolute; 289 | padding: 5px; 290 | z-index: 1; 291 | user-select: none; 292 | } 293 | .arrow-left{ 294 | -webkit-transform: scaleX(-1); 295 | transform: scaleX(-1); 296 | left: 0; 297 | } 298 | 299 | .line{ 300 | width: auto; 301 | height: 0; 302 | margin-left: 29px; 303 | margin-right: 37px; 304 | margin-top: 14px; 305 | background: var(--Lines); 306 | border: 3.5px solid var(--Lines); 307 | border-radius: 3.5px; 308 | } 309 | .line2{ 310 | margin-left: 105px; 311 | margin-top: 5px; 312 | } 313 | 314 | 315 | .recents{ 316 | overflow-y: scroll; 317 | max-height: 100%; 318 | } 319 | .recent{ 320 | font-family: 'Circular-Medium'; 321 | display: flex; 322 | margin-top: 20px; 323 | margin-right: 37px; 324 | margin-left: 24px; 325 | padding: 8px 0px 8px 0px; 326 | border-radius: 30px; 327 | cursor: pointer; 328 | user-select: none; 329 | -moz-user-select: none; 330 | -khtml-user-select: none; 331 | -webkit-user-select: none; 332 | -o-user-select: none; 333 | } 334 | .recent:hover{ 335 | background-color:var(--Lines); 336 | 337 | } 338 | .profile-recent{ 339 | margin-left: 5px; 340 | } 341 | .recent-box{ 342 | width:calc(100% - 175px); 343 | } 344 | .profile-name-recent{ 345 | font-size: 20px; 346 | color: black; 347 | height: 24px; 348 | margin-top: 10px; 349 | margin-left: 10px; 350 | display: block; 351 | } 352 | .message-recent{ 353 | font-size: 17px; 354 | color: var(--Text); 355 | height: 24px; 356 | margin-top: 10px; 357 | margin-left: 10px; 358 | display: -webkit-box; 359 | -webkit-line-clamp: 1; 360 | -webkit-box-orient: vertical; 361 | overflow: hidden; 362 | } 363 | 364 | 365 | .date-recent{ 366 | color: var(--Text); 367 | margin-right: 5px; 368 | margin-left: 0px; 369 | text-align: end; 370 | } 371 | 372 | .unread-circle{ 373 | width: 24px; 374 | height: 24px; 375 | border-radius: 50%; 376 | float: right; 377 | margin-right: 0.5em; 378 | margin-top: 0.68em; 379 | background-color: var(--Melon); 380 | font-family: 'Circular-Medium'; 381 | color: var(--Yankees-Blue); 382 | } 383 | .unread-circle span { 384 | display: flex; 385 | margin-top: 0.12em; 386 | justify-content: center; 387 | align-items: center; 388 | } 389 | 390 | 391 | .chat-messages{ 392 | display: flex; 393 | word-break: break-word; 394 | flex-direction: column; 395 | box-sizing: border-box; 396 | overflow-y: scroll; 397 | overflow-x: hidden; 398 | height: calc(100% - 60px); 399 | padding-right: 15px; 400 | padding-top: 10px; 401 | padding-left: 30.5%; 402 | flex-direction: column-reverse; 403 | } 404 | .messages-input{ 405 | padding-left: 30.5%; 406 | z-index: 4; 407 | } 408 | 409 | 410 | 411 | .chat-date{ 412 | font-family: 'Circular-Medium'; 413 | font-size: 24px; 414 | height: 25px; 415 | color: var(--Text); 416 | text-transform: uppercase; 417 | margin-top: 10px; 418 | margin-bottom: 20px; 419 | width: 100%; 420 | position: relative; 421 | display: flex; 422 | justify-content: center; 423 | } 424 | .text-bubble{ 425 | box-sizing: border-box; 426 | margin-bottom: 10px; 427 | max-width: 60%; 428 | min-width: 10px; 429 | display:flex; 430 | box-sizing: content-box; 431 | padding: 15px; 432 | font-family: 'Circular-Book'; 433 | font-size: 18px; 434 | border-radius: 1em; 435 | } 436 | .from-them{ 437 | background: var(--Them-Bubble); 438 | align-self: flex-start; 439 | } 440 | .from-me{ 441 | background: var(--Melon); 442 | align-self: flex-end; 443 | } 444 | 445 | [data-title]:hover:after { 446 | opacity: 1; 447 | transition: all 0.5s ease 0.5s; 448 | visibility: visible; 449 | top: -1.8em; 450 | } 451 | [data-title]:after { 452 | content: attr(data-title); 453 | background-color: var(--Yankees-Blue); 454 | color: #111; 455 | font-size: 15px; 456 | color: #FFFFFF; 457 | position: absolute; 458 | padding: 2px 7px 3px 7px; 459 | top: -1.5em; 460 | left: 50%; 461 | transform: translate(-50%, 0); 462 | white-space: nowrap; 463 | opacity: 0; 464 | z-index: 99999; 465 | visibility: hidden; 466 | border-radius: 1em; 467 | } 468 | [data-title] { 469 | position: relative; 470 | } 471 | 472 | 473 | 474 | .line-input{ 475 | margin: 0 15px 0 0; 476 | top: 0; 477 | } 478 | 479 | .input-plusfile{ 480 | width: 27px; 481 | height: 27px; 482 | border-radius: 50%; 483 | float: left; 484 | background-color: var(--Melon); 485 | font-family: 'Circular-Medium'; 486 | color: var(--Yankees-Blue); 487 | font-size: 24px; 488 | margin-top: 0.4em; 489 | 490 | } 491 | .input-plusfile span { 492 | display: flex; 493 | margin-top: -0.055em; 494 | justify-content: center; 495 | align-items: center; 496 | } 497 | 498 | .type-message{ 499 | background-color: transparent; 500 | margin-top: 10px; 501 | margin-left: 0.5em; 502 | width: calc(90% - 100px); 503 | font-family: 'Circular-Book'; 504 | color: var(--Text); 505 | font-size: 20px; 506 | outline: none; 507 | border: none; 508 | display: inline-flex; 509 | 510 | } 511 | 512 | .sendarrow{ 513 | height: 27px; 514 | width: 70px; 515 | float: right; 516 | margin-top: 0.75em; 517 | margin-right: 15px; 518 | } 519 | 520 | .message-link{ 521 | display: contents; 522 | color: white; 523 | } 524 | .message-link:hover{ 525 | color: rgb(143, 143, 143); 526 | } 527 | 528 | .toast-container{ 529 | position: fixed; 530 | margin: 10px; 531 | display: flex; 532 | flex-direction: column; 533 | gap: 1rem; 534 | top: 0; 535 | right: 0; 536 | } 537 | .toast{ 538 | display: flex; 539 | position: relative; 540 | box-sizing: border-box; 541 | background-color: var(--Space-Cadet); 542 | overflow: hidden; 543 | box-shadow: 0px 10px 25.5px 10.5px rgba(0, 0, 0, 0.50); 544 | width: 350px; 545 | height: 75px; 546 | border-radius: 50px 50px; 547 | cursor: pointer; 548 | } 549 | .toast.hide{ 550 | transform: translateX(120%); 551 | transition: transform 250ms ease-in-out; 552 | } 553 | .toast.show{ 554 | transform: translateX(0); 555 | transition: transform 250ms ease-in-out; 556 | } 557 | .toast:hover{ 558 | background-color: var(--Space-Cadet-hover); 559 | } 560 | .toast-text{ 561 | flex-direction: column; 562 | color: white; 563 | font-family: 'Circular-Medium'; 564 | margin-left: 5px; 565 | } 566 | .toast-name{ 567 | font-size: 20px; 568 | height: 20px; 569 | margin-top: 7px; 570 | 571 | overflow: hidden; 572 | display: -webkit-box; 573 | -webkit-line-clamp: 1; 574 | -webkit-box-orient: vertical; 575 | } 576 | .toast-message{ 577 | font-size: 15px; 578 | margin-top: 15px; 579 | 580 | overflow: hidden; 581 | display: -webkit-box; 582 | -webkit-line-clamp: 1; 583 | -webkit-box-orient: vertical; 584 | } 585 | .toast::before{ 586 | content: ""; 587 | position: absolute; 588 | height: 4px; 589 | width: calc(100% * var(--progress)); 590 | background-color: white; 591 | bottom: 0; 592 | left: 0; 593 | right: 0; 594 | } 595 | 596 | .user-settings-wrapper{ 597 | position: absolute; 598 | display: flex; 599 | flex-direction: column; 600 | overflow: hidden; 601 | 602 | left: 25px; 603 | top: 120px; 604 | z-index: 998; 605 | gap: 0.1rem; 606 | 607 | width: 300px; 608 | height: auto; 609 | box-shadow: 0px 10px 25.5px 10.5px rgba(0, 0, 0, 0.50); 610 | border-radius: 50px 50px; 611 | background-color: var(--Space-Cadet); 612 | } 613 | .settings-element{ 614 | height: 50px; 615 | display: flex; 616 | position: relative; 617 | box-sizing: border-box; 618 | cursor:pointer; 619 | } 620 | .settings-element:hover{ 621 | background-color: var(--Space-Cadet-hover); 622 | } 623 | .settings-element-text{ 624 | color: white; 625 | font-family: 'Circular-Medium'; 626 | margin-left: 30px; 627 | margin-top: 16px; 628 | } 629 | .is-typing{ 630 | padding-left: 30.5%; 631 | bottom: 60px; 632 | position: absolute; 633 | z-index: 999; 634 | font-family: Circular-Bold; 635 | color: black; 636 | font-size: 22px; 637 | font-weight: 400; 638 | } 639 | 640 | 641 | 642 | 643 | 644 | 645 | /* SETTINGS PAGE */ 646 | body{ 647 | width: 100%; 648 | height: 100%; 649 | overflow: hidden; 650 | } 651 | .settings-container{ 652 | background-color: var(--Space-Cadet); 653 | width: 450px; 654 | height: auto; 655 | display: flex; 656 | flex-direction: column; 657 | position: absolute; 658 | transform: translate(-50%,-50%); 659 | top: 25%; 660 | left: 50%; 661 | border: 0; 662 | margin-top: 10em; 663 | border-radius: 50px 50px; 664 | flex-wrap: wrap; 665 | letter-spacing: 0.5px; 666 | padding: 18px 20px; 667 | box-shadow: 6px 12px 34.5px 25.5px rgba(0, 0, 0, 0.25); 668 | } 669 | .settings-con-el{ 670 | height: 75px; 671 | display: flex; 672 | position: relative; 673 | box-sizing: border-box; 674 | margin-top: 27px; 675 | margin-left: 35px; 676 | } 677 | .settings-avatar-buttons{ 678 | position: absolute; 679 | display: flex; 680 | right: 0; 681 | top: 13px; 682 | gap: 1rem; 683 | margin-right: 75px; 684 | } 685 | .settings-avatar-button{ 686 | background-color: var(--Melon); 687 | font-family: Circular-Bold; 688 | color: white; 689 | padding: 15px 10px; 690 | font-size: 13px; 691 | font-weight: 400; 692 | border-radius: 200px; 693 | cursor: pointer; 694 | border: none; 695 | } 696 | .settings-avatar-button.reset{ 697 | background-color: var(--Space-Cadet); 698 | } 699 | .settings-username{ 700 | font-family: Circular-Bold; 701 | color: white; 702 | font-size: 22px; 703 | font-weight: 600; 704 | margin-top: 18px; 705 | margin-right: 18px; 706 | } 707 | .settings-username-input{ 708 | display: block; 709 | height: 50px; 710 | width: 100%; 711 | background-color: rgba(255,255,255,0.07); 712 | border-radius: 200px; 713 | padding: 0 10px; 714 | margin-top: 8px; 715 | font-family: 'Circular-Book'; 716 | color: white; 717 | font-size: 15px; 718 | font-weight: 300; 719 | outline: none; 720 | border: none; 721 | } 722 | 723 | .save-reset{ 724 | position: absolute; 725 | display: flex; 726 | bottom: 50px; 727 | left: calc(50% - 450px/2); 728 | background-color: var(--Space-Cadet); 729 | box-shadow: 0px 10px 25.5px 10.5px rgba(0, 0, 0, 0.50); 730 | width: 450px; 731 | height: 40px; 732 | border-radius: 50px 50px; 733 | padding: 10px 0px; 734 | } 735 | .save-reset-text{ 736 | position: relative; 737 | margin-top: 11px; 738 | margin-left: 23px; 739 | font-family: 'Circular-Book'; 740 | color: white; 741 | font-size: 15px; 742 | font-weight: 300; 743 | width: 245px; 744 | } 745 | .save-reset-buttons{ 746 | margin-right: 23px; 747 | margin-top: -3px; 748 | } 749 | .save-reset-button{ 750 | background-color: var(--Melon); 751 | font-family: Circular-Bold; 752 | color: white; 753 | padding: 10px 8px; 754 | font-size: 13px; 755 | font-weight: 400; 756 | border-radius: 200px; 757 | cursor: pointer; 758 | border: none; 759 | } 760 | .save-reset.hide{ 761 | transform: translateY(180%); 762 | transition: transform 250ms ease-in-out; 763 | } 764 | .save-reset.show{ 765 | transform: translateY(0); 766 | transition: transform 250ms ease-in-out; 767 | } 768 | 769 | input[type="file"] { 770 | display: none; 771 | } -------------------------------------------------------------------------------- /chat/static/fonts/Circular-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jalitko/DjangoChat/5b4c2758d122998016082e7497b423a55756867d/chat/static/fonts/Circular-Black.ttf -------------------------------------------------------------------------------- /chat/static/fonts/Circular-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jalitko/DjangoChat/5b4c2758d122998016082e7497b423a55756867d/chat/static/fonts/Circular-Bold.ttf -------------------------------------------------------------------------------- /chat/static/fonts/Circular-Book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jalitko/DjangoChat/5b4c2758d122998016082e7497b423a55756867d/chat/static/fonts/Circular-Book.ttf -------------------------------------------------------------------------------- /chat/static/fonts/Circular-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jalitko/DjangoChat/5b4c2758d122998016082e7497b423a55756867d/chat/static/fonts/Circular-Medium.ttf -------------------------------------------------------------------------------- /chat/static/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /chat/static/js/main.js: -------------------------------------------------------------------------------- 1 | 2 | // LOGIN AND SIGNUP PAGE 3 | function TogglePassword() { 4 | var password = document.getElementById("password"); 5 | var eye = document.getElementById("toggleeye"); 6 | 7 | const type = password.getAttribute('type') === 'password' ? 'text' : 'password'; 8 | password.setAttribute('type', type); 9 | 10 | eye.classList.toggle('fa-eye-slash'); 11 | } 12 | 13 | 14 | 15 | 16 | // INDEX 17 | 18 | // Scroll button to online users list 19 | function Hscroll(side){ 20 | const width = 300; 21 | var bar = document.getElementById('online-users-bar'); 22 | 23 | if (side === 'left') { 24 | bar.scrollLeft -= width; 25 | } 26 | else{ 27 | bar.scrollLeft += width; 28 | } 29 | } 30 | 31 | // Select chat and messages 32 | function selectchat(id){ 33 | // Unselect chat 34 | if (id == 0) { 35 | id = getIds('chat') 36 | 37 | if(id == 0){ 38 | $('.active').hide() 39 | $('.active-name').hide() 40 | $('.messages-input').hide() 41 | return 42 | } 43 | } 44 | 45 | // Clear messages after repick chat 46 | if (id != getIds('chat')) clearMessages() 47 | 48 | // push id to URL 49 | window.history.pushState('data', 'title', id); 50 | 51 | // Select chat 52 | $('#chat-id').val(id) 53 | getOnlineUsers(id, data => { 54 | $(".active-profile").attr('src', data['user']['profile-image']); 55 | $(".active-name").text(data['user']['username']); 56 | $('.active').show() 57 | $('.active-name').show() 58 | $('.messages-input').show() 59 | $('.active > span').attr('id', `userid-${id}`) 60 | OnlineDot(($(`span[id="userid-${id}"]`)[1].classList[1] == 'online') ? true : false, id) 61 | }); 62 | 63 | getChatMessages(true) 64 | messageForm.reset() 65 | isTyping.myInput = '' 66 | $('.is-typing').hide() 67 | } 68 | 69 | // Push users to online users list 70 | function add_Online_users(){ 71 | getOnlineUsers('', data => { 72 | Object.keys(data).forEach(key => { 73 | document.getElementById('online-users-bar').innerHTML += ` 74 |