├── .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 | ![Home View](static/../chat/static/screenshots/homeview.png?raw=true "Title") 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 | 3 | 4 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 |
75 |
76 | 77 | 78 |
79 | ${data[key]['username']} 80 |
` 81 | }); 82 | }); 83 | 84 | } 85 | 86 | // Push recent Chats 87 | function add_Recent_chats(){ 88 | getOnlineUsers('', data => { 89 | Object.keys(data).forEach(key => { 90 | const id = data[key]['id'] 91 | document.getElementById('recents').innerHTML += ` 92 |
93 | 94 |
95 | 96 | 97 |
98 | 99 |
100 | ${data[key]['username']} 101 | 102 |
103 | 104 |
105 | 106 | 107 | 110 |
111 |
112 |
113 | ` 114 | allUsers.push(id) 115 | getLastMessage(id) 116 | }); 117 | }); 118 | } 119 | 120 | // Create WebSocket 121 | function WebSocketCreate(){ 122 | var loc = window.location 123 | var url = 'ws://' + loc.host + '/ws' + loc.pathname 124 | ws = new WebSocket(url) 125 | 126 | ws.onopen = function(event){ 127 | console.log('Connection is opened'); 128 | 129 | ws.send(`{ 130 | "type": "online", 131 | "set": "true" 132 | }`) 133 | } 134 | 135 | ws.onmessage = function(event){ 136 | unread() 137 | 138 | var data = JSON.parse(event.data); 139 | // console.log(data) 140 | 141 | if(data['type'] === 'message'){ 142 | if(data['sender'] != getIds('chat')) return 143 | getChatMessages() 144 | } 145 | else if(data['type'] === 'online'){ 146 | OnlineDot(data['set'], data['user']) 147 | } 148 | else if(data['type'] === 'notifi'){ 149 | var id = data['sender'] 150 | 151 | if(id != parseInt(getIds('my'))){ 152 | notifi.pause() 153 | notifi.currentTime = 0 154 | notifi.play(); 155 | new Toast(id) 156 | } 157 | getLastMessage(data['user']) 158 | } 159 | else if(data['type'] === 'istyping'){ 160 | console.log(data['set']) 161 | if(data['set'] == 'true') $('.is-typing').show() 162 | else $('.is-typing').hide() 163 | } 164 | } 165 | 166 | ws.onclose = function(event){ 167 | console.log('Connection is closed'); 168 | id = getIds('my') 169 | OnlineDot('false', id) 170 | } 171 | 172 | ws.onerror = function(event){ 173 | console.log('Something went wrong'); 174 | } 175 | } 176 | 177 | // Set online dot color by id 178 | function OnlineDot(setOnline, id){ 179 | var user = `span[id="userid-${id}"]` 180 | var onClass = 'online' 181 | var offClass = 'offline' 182 | 183 | if(setOnline == true || setOnline == 'true'){ 184 | $(user).removeClass(offClass).addClass(onClass) 185 | } 186 | else{ 187 | $(user).removeClass(onClass).addClass(offClass) 188 | } 189 | } 190 | 191 | // Get all user online status 192 | function getAllOnlineStatus(){ 193 | getOnlineUsers('', data => { 194 | Object.keys(data).forEach(key => { 195 | var id = data[key]['id'] 196 | var online = data[key]['is-online'] 197 | OnlineDot(online, id) 198 | }); 199 | }); 200 | 201 | } 202 | 203 | // Get onilne users from rest api 204 | function getOnlineUsers(id, callback){ 205 | $.getJSON(`/online-users/${id}`, callback); 206 | } 207 | 208 | // Get messages from rest api 209 | function getMessages(id, count, callback){ 210 | $.getJSON(`/chat-messages/${id}?${count}`, callback); 211 | } 212 | 213 | // Get messages to chat 214 | function getChatMessages(all = false) { 215 | id = getIds('chat') 216 | if(id==0) return 217 | 218 | var count = 'count=1' 219 | if(all){ 220 | count = '' 221 | resetCurrent() 222 | clearMessages() 223 | } 224 | 225 | var open = setInterval(() => { 226 | if(ws.readyState === WebSocket.OPEN){ 227 | getMessages(id, count, data =>{ 228 | Object.keys(data).forEach(key => { 229 | var date = new Date(data[key]['timestamp']) 230 | recieveMessages(data[key]['sender'], data[key]['text'], date) 231 | 232 | if(getIds('chat') == data[key]['sender']){ 233 | if(data[key]['isread']) return 234 | ws.send(`{ 235 | "type": "read", 236 | "id": "${key}", 237 | "user": "${getIds('chat')}" 238 | }`) 239 | } 240 | }) 241 | }) 242 | clearInterval(open) 243 | } 244 | },100) 245 | 246 | setTimeout(() =>{ 247 | unread() 248 | }, 1000) 249 | } 250 | 251 | // Get last message to recent chats 252 | function getLastMessage(id){ 253 | 254 | getMessages(id,'count=1', data =>{ 255 | var key = Object.keys(data)[0] 256 | var data = data[key] 257 | var time = new Date(data['timestamp']) 258 | var now = new Date(Date.now()) 259 | var timeStr 260 | 261 | //Not this year 262 | if(time.getFullYear() != now.getFullYear()){ 263 | timeStr = time.toLocaleDateString() 264 | } 265 | //Not today 266 | else if(time.getDate() != now.getDate() || time.getMonth() != now.getMonth()){ 267 | var day = time.getDate() 268 | var month = time.toLocaleString('default', { month: 'long' }).slice(0, 3).toLowerCase() 269 | timeStr = `${day} ${month}` 270 | } 271 | else{ 272 | timeStr = time.toLocaleTimeString(['en-US'], {timeStyle: 'short'}).toLowerCase() 273 | } 274 | 275 | var from = '' 276 | if(id != data.sender) from = 'You:' 277 | 278 | $(`#recentid-${id} > div > span.message-recent`).html(`${from} ${data.text}`) 279 | $(`#recentid-${id} > div > span.date-recent`).html(`${timeStr}`) 280 | }) 281 | } 282 | 283 | 284 | // Push messages to chat thread 285 | function recieveMessages(senderId, text, Date){ 286 | var time = Date.toLocaleTimeString(['en-US'], {timeStyle: 'short'}).toLowerCase() 287 | var date = parseInt(Date.toLocaleString('default', { day: 'numeric' })) 288 | var month = Date.toLocaleString('default', { month: 'long' }) 289 | var monthInt = parseInt(Date.toLocaleString('default', { month: 'numeric' })) 290 | var year = parseInt(Date.toLocaleString('default', { year: 'numeric' })) 291 | 292 | // assign chat bubble to sender 293 | var sender = 'from-them' 294 | if(senderId == getIds('my')) sender = 'from-me' 295 | 296 | 297 | // Pust date separator 298 | var chatDate = '' 299 | if(date > currentDate || monthInt > currentMonth || year > currentYear){ 300 | chatDate = `
${date} ${month}
` 301 | currentDate = date 302 | currentMonth = monthInt 303 | currentYear = year 304 | } 305 | 306 | // Push message bubble 307 | div_calss = 'chat-messages' 308 | div = ` 309 |
310 | ${urlify(text)} 311 |
312 | ${chatDate} 313 | ` 314 | document.getElementById(div_calss).innerHTML = div + document.getElementById(div_calss).innerHTML 315 | } 316 | 317 | // Clear chat thread 318 | function clearMessages(){ 319 | document.getElementById('chat-messages').innerHTML = '' 320 | } 321 | 322 | // Reset date separator vars 323 | function resetCurrent(){ 324 | currentDate = 0 325 | currentMonth = 0 326 | currentYear = 0 327 | } 328 | 329 | // Send message from input field 330 | function sendMessaage(e){ 331 | message = document.getElementById('message').value.trim() 332 | 333 | if (e.preventDefault) e.preventDefault() 334 | if(message=='') return 0 335 | 336 | message = `{ 337 | "type": "message", 338 | "to": "${getIds('chat')}", 339 | "message": "${message}" 340 | }` 341 | ws.send(message) 342 | messageForm.reset() 343 | } 344 | 345 | // Add clickable element to message 346 | function urlify(text) { 347 | var urlRegex = /(https?:\/\/[^\s]+)/g; 348 | return text.replace(urlRegex, function(url) { 349 | return `${url}` 350 | }) 351 | } 352 | 353 | // Get id from html 354 | function getIds(id){ 355 | if(id === 'chat') return $('#chat-id').val() 356 | else return $('#my-id').val() 357 | } 358 | 359 | // Create Toast to toast container 360 | class Toast { 361 | #container 362 | #toastElem 363 | #delay = 3000 364 | #visibleSince 365 | #moveBind 366 | #id 367 | 368 | constructor(options){ 369 | this.#id = options 370 | getOnlineUsers(options, data => { 371 | var user = data['user'] 372 | getMessages(options, 'count=1', data =>{ 373 | var key = Object.keys(data)[0] 374 | var message = data[key] 375 | 376 | options = { 377 | user: options, 378 | name: user['username'], 379 | image: user['profile-image'], 380 | text: message['text'], 381 | } 382 | this.show(options) 383 | }) 384 | }) 385 | 386 | this.#moveBind = this.moveToChat.bind(this) 387 | this.#container = document.getElementById('toast-container') 388 | this.autoClose(this.#delay) 389 | this.#visibleSince = new Date() 390 | } 391 | 392 | show(options) { 393 | this.#toastElem = document.createElement("div") 394 | this.#toastElem.classList.add("toast", "hide") 395 | this.#toastElem.addEventListener('click', this.#moveBind) 396 | this.#container.append(this.#toastElem) 397 | 398 | this.#toastElem.innerHTML += ` 399 | 400 |
401 |
${options.name}
402 |
${options.text}
403 |
404 | ` 405 | setTimeout(() => { 406 | this.#toastElem.classList.remove('hide') 407 | this.#toastElem.classList.add('show') 408 | }, 20) 409 | 410 | this.showProgress() 411 | } 412 | 413 | remove(){ 414 | this.hide() 415 | setTimeout(() => this.#toastElem.remove(), 220) 416 | } 417 | 418 | autoClose(value){ 419 | setTimeout(() => this.remove(), value) 420 | } 421 | 422 | hide(){ 423 | this.#toastElem.classList.add('hide') 424 | this.#toastElem.classList.remove('show') 425 | } 426 | 427 | showProgress(){ 428 | setInterval(() => { 429 | const timeVisible = new Date() - this.#visibleSince 430 | this.#toastElem.style.setProperty( 431 | '--progress', 432 | 1 - timeVisible / this.#delay 433 | ) 434 | }, 10) 435 | } 436 | 437 | moveToChat(){ 438 | selectchat(this.#id) 439 | this.remove() 440 | } 441 | } 442 | 443 | // Get unread messages from REST api 444 | function getUnread(callback){ 445 | $.getJSON(`/unread/`, callback); 446 | } 447 | 448 | // Count unread messages 449 | function unread(){ 450 | unreadOb = {total : -1, count: -1, now: 0,} 451 | 452 | getUnread(data =>{ 453 | unreadOb.count = Object.keys(data).length 454 | Object.keys(data).forEach(key => { 455 | if(unreadOb.total == -1) unreadOb.total = 0 456 | unreadOb.total += data[key]['count'] 457 | unreadOb.now += 1 458 | badge(data[key]['sender'], data[key]['count']) 459 | }) 460 | }) 461 | 462 | var intter = setInterval(() => { 463 | if(unreadOb.count == unreadOb.now){ 464 | if(unreadOb.total == 0) document.title = 'DjangoChat' 465 | else document.title = `DjangoChat (${unreadOb.total})` 466 | 467 | clearInterval(intter) 468 | } 469 | }, 20) 470 | } 471 | 472 | // Get unread count to badge 473 | function badge(id, value){ 474 | if(value == 0) { 475 | $(`#badge-${id}`).hide() 476 | } 477 | else{ 478 | if(value > 9) value = '9+' 479 | $(`#badge-${id} > span`).text(value) 480 | $(`#badge-${id}`).show() 481 | } 482 | } 483 | 484 | // Settings popup 485 | function settingsPopup(){ 486 | $('.user-settings-wrapper').toggle() 487 | } 488 | 489 | // Chacking if user typing new message 490 | function isTyping(){ 491 | setInterval(() => { 492 | var message = $('#message').val() 493 | if(isTypingObj.myInput != message && message != ''){ 494 | isTypingObj.myInput = $('#message').val() 495 | if(!isTypingObj.myState) sendIsTyping(true) 496 | } 497 | else{ 498 | if(isTypingObj.myState) sendIsTyping(false) 499 | } 500 | }, 500) 501 | } 502 | 503 | // Send to WebSocket isTyping state 504 | function sendIsTyping(state){ 505 | isTypingObj.myState = state 506 | ws.send(`{ 507 | "type": "istyping", 508 | "set": "${state}", 509 | "user": "${getIds('chat')}" 510 | }`) 511 | } 512 | 513 | var ws 514 | var currentDate, currentMonth, currentYear 515 | var notifi = new Audio('/static/notifi.mp3') 516 | var allUsers = [] 517 | 518 | WebSocketCreate() 519 | const messageForm = document.getElementById('message-form') 520 | messageForm.addEventListener('submit', sendMessaage) 521 | add_Recent_chats() 522 | add_Online_users() 523 | resetCurrent() 524 | selectchat(0) 525 | getAllOnlineStatus() 526 | var unreadOb 527 | unread() 528 | 529 | var isTypingObj = { 530 | myInput : '', 531 | myState: false, 532 | senderState: false, 533 | } 534 | isTyping() -------------------------------------------------------------------------------- /chat/static/js/settings.js: -------------------------------------------------------------------------------- 1 | var ws 2 | WebSocketCreate() 3 | setAvatar() 4 | clearAvatarInput() 5 | 6 | const avatarChange = document.querySelector("#avatar-upload") 7 | const usernameInput = document.getElementById('username-input') 8 | var username = usernameInput.value 9 | var avatar = $('#avatar').attr('src') 10 | 11 | 12 | // Craete WebSocket 13 | function WebSocketCreate(){ 14 | var loc = window.location 15 | var url = 'ws://' + loc.host + '/ws/' 16 | ws = new WebSocket(url) 17 | 18 | ws.onopen = function(event){ 19 | console.log('Connection is opened'); 20 | 21 | ws.send(`{ 22 | "type": "online", 23 | "set": "true" 24 | }`) 25 | } 26 | 27 | ws.onmessage = function(event){ 28 | var data = JSON.parse(event.data); 29 | console.log(`online: ${data['set']} user: ${data['user']}`) 30 | } 31 | 32 | ws.onclose = function(event){ 33 | console.log('Connection is closed'); 34 | } 35 | 36 | ws.onerror = function(event){ 37 | console.log('Something went wrong'); 38 | } 39 | } 40 | 41 | // Set avatar image 42 | function setAvatar(){ 43 | var avatar = $('#avatar') 44 | var src = `${window.location.origin}/${avatar.attr('src')}` 45 | avatar.attr('src', src) 46 | } 47 | 48 | // Reset avatar to default 49 | function avatarReset(def){ 50 | clearAvatarInput() 51 | const defsrc = `${window.location.origin}/media/\\profile-pics\\default.jpg` 52 | if(def){ 53 | $('#avatar').attr('src', defsrc) 54 | saveChanges(true) 55 | } 56 | else{ 57 | $('#avatar').attr('src', avatar) 58 | } 59 | } 60 | 61 | // Clear file from input form element 62 | function clearAvatarInput(){ 63 | $('#avatar-upload').val(null) 64 | } 65 | 66 | // Get file from input form element 67 | function getAvatarInput(){ 68 | var image = $('#avatar-upload')[0].files[0] 69 | if(image == null) return '' 70 | return image 71 | } 72 | 73 | // Upload a new avatar file and show preview 74 | avatarChange.addEventListener("change", function(){ 75 | const reader = new FileReader() 76 | reader.addEventListener("load", () => { 77 | $('#avatar').attr('src', reader.result) 78 | }) 79 | reader.readAsDataURL(this.files[0]) 80 | saveChanges(true) 81 | }) 82 | 83 | // Detect username changes 84 | usernameInput.addEventListener('change', () =>{ 85 | if(usernameInput.value != username) saveChanges(true) 86 | else saveChanges(false) 87 | }) 88 | 89 | // Show or hide popup to save or reset changes 90 | function saveChanges(show){ 91 | if(show) $('.save-reset').addClass('show').removeClass('hide') 92 | else $('.save-reset').addClass('hide').removeClass('show') 93 | } 94 | 95 | // Reset all changes 96 | function SettingsReset(){ 97 | avatarReset(false) 98 | usernameInput.value = username 99 | saveChanges(false) 100 | clearAvatarInput() 101 | } 102 | 103 | // Send changes with POST request and save changes 104 | function SettingsSave(){ 105 | const csrf_token = $('#csrf-token').val() 106 | let formData = new FormData() 107 | formData.append('csrfmiddlewaretoken', csrf_token) 108 | formData.append("avatar", getAvatarInput()) 109 | formData.append("username", usernameInput.value) 110 | 111 | $.ajax({ 112 | type: "POST", 113 | url: "/settings/", 114 | processData: false, 115 | contentType: false, 116 | data: formData, 117 | success: function (data) { 118 | console.log("success") 119 | location.reload() 120 | }, 121 | failure: function (data) { 122 | console.log("failure") 123 | }, 124 | }) 125 | } -------------------------------------------------------------------------------- /chat/static/notifi.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jalitko/DjangoChat/5b4c2758d122998016082e7497b423a55756867d/chat/static/notifi.mp3 -------------------------------------------------------------------------------- /chat/static/screenshots/homeview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jalitko/DjangoChat/5b4c2758d122998016082e7497b423a55756867d/chat/static/screenshots/homeview.png -------------------------------------------------------------------------------- /chat/static/sendarrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /chat/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /chat/views.py: -------------------------------------------------------------------------------- 1 | # python manage.py runserver 0.0.0.0:8000 2 | # python manage.py migrate --run-syncdb 3 | 4 | from concurrent.futures import thread 5 | from rich.console import Console 6 | console = Console(style='bold green') 7 | import re, json 8 | from django.shortcuts import render, redirect 9 | from .models import Message, UserSetting, Thread 10 | from .managers import ThreadManager 11 | from django.conf import settings 12 | from django.http import JsonResponse, HttpResponse 13 | from django.contrib.auth.decorators import login_required 14 | from django.contrib.auth import authenticate, login, logout 15 | from django.contrib.auth.models import User 16 | 17 | def email_valid(email): 18 | regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b' 19 | if(re.fullmatch(regex, email)): return True 20 | return False 21 | 22 | @login_required 23 | def api_online_users(request, id=0): 24 | users_json = {} 25 | 26 | if id != 0: 27 | user = User.objects.get(id=id) 28 | user_settings = UserSetting.objects.get(user=user) 29 | users_json['user'] = get_dictionary(user, user_settings) 30 | 31 | else: 32 | all_users = User.objects.all().exclude(username=request.user) 33 | for user in all_users: 34 | user_settings = UserSetting.objects.get(user=user) 35 | users_json[user.id] = get_dictionary(user, user_settings) 36 | 37 | return HttpResponse( 38 | json.dumps(users_json), 39 | content_type = 'application/javascript; charset=utf8' 40 | ) 41 | def get_dictionary(user, user_settings): 42 | return { 43 | 'id': user.id, 44 | 'username': user_settings.username, 45 | 'profile-image': user_settings.profile_image.url, 46 | 'is-online': user_settings.is_online 47 | } 48 | 49 | @login_required 50 | def api_chat_messages(request, id): 51 | messages_json = {} 52 | count = int(request.GET.get('count', 0)) 53 | 54 | thread_name = ThreadManager.get_pair('self', request.user.id, id) 55 | thread = Thread.objects.get(name=thread_name) 56 | messages = Message.objects.filter(thread=thread).order_by('-id') 57 | 58 | for i, message in enumerate(messages, start=1): 59 | messages_json[message.id] = { 60 | 'sender': message.sender.id, 61 | 'text': message.text, 62 | 'timestamp': message.created_at.isoformat(), 63 | 'isread': message.isread, 64 | } 65 | if i == count: break 66 | 67 | 68 | return HttpResponse( 69 | json.dumps(messages_json), 70 | content_type = 'application/javascript; charset=utf8' 71 | ) 72 | 73 | @login_required 74 | def api_unread(request): 75 | messages_json = {} 76 | 77 | user = request.user 78 | threads = Thread.objects.filter(users=user) 79 | for i, thread in enumerate(threads): 80 | if(user == thread.users.first()): 81 | sender = thread.users.last() 82 | unread = thread.unread_by_1 83 | else: 84 | sender = thread.users.first() 85 | unread = thread.unread_by_2 86 | 87 | messages_json[i] = { 88 | 'sender': sender.id, 89 | 'count': unread, 90 | } 91 | 92 | return HttpResponse( 93 | json.dumps(messages_json), 94 | content_type = 'application/javascript; charset=utf8' 95 | ) 96 | 97 | @login_required 98 | def index(request, id=0): 99 | user = User.objects.get(username=request.user) 100 | Usettings = UserSetting.objects.get(user=user) 101 | 102 | context = { 103 | "settings" : Usettings, 104 | 'id' : id, 105 | } 106 | return render(request, 'index.html', context=context) 107 | 108 | 109 | def login_view(request): 110 | logout(request) 111 | context = {} 112 | 113 | 114 | if request.POST: 115 | email = request.POST['email'] 116 | password = request.POST['password'] 117 | 118 | user = authenticate(username=email, password=password) 119 | 120 | if user is not None: 121 | if user.is_active: 122 | login(request, user) 123 | return redirect('/') 124 | else: 125 | context = { 126 | "error": 'Email or Password was wrong.', 127 | } 128 | 129 | return render(request, 'login.html',context) 130 | 131 | 132 | def signup_view(request): 133 | logout(request) 134 | 135 | 136 | if request.method == 'POST': 137 | email = request.POST.get('email') 138 | username = request.POST.get('username') 139 | password = request.POST.get('password') 140 | error = '' 141 | 142 | if not email_valid(email): 143 | error = "Wrong email address." 144 | try: 145 | if User.objects.get(username=email) is not None: 146 | error = 'This email is already used.' 147 | except: pass 148 | 149 | if error: return render(request, "signup.html", context={'error': error}) 150 | 151 | user = User.objects.create_user( 152 | username = email, 153 | password = password, 154 | ) 155 | userset = UserSetting.objects.create(user=user, username=username) 156 | 157 | login(request, user) 158 | return redirect('/') 159 | 160 | 161 | return render(request, 'signup.html') 162 | 163 | @login_required 164 | def settings_view(request): 165 | user = User.objects.get(username=request.user) 166 | Usettings = UserSetting.objects.get(user=user) 167 | 168 | if request.method == 'POST': 169 | try: avatar = request.FILES["avatar"] 170 | except: avatar = None 171 | username = request.POST['username'] 172 | 173 | Usettings.username = username 174 | if(avatar != None): 175 | Usettings.profile_image.delete(save=True) 176 | Usettings.profile_image = avatar 177 | Usettings.save() 178 | 179 | context = { 180 | "settings" : Usettings, 181 | 'user' : user, 182 | } 183 | return render(request, 'settings.html', context=context) -------------------------------------------------------------------------------- /chatapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jalitko/DjangoChat/5b4c2758d122998016082e7497b423a55756867d/chatapp/__init__.py -------------------------------------------------------------------------------- /chatapp/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for chatapp project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chatapp.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /chatapp/routing.py: -------------------------------------------------------------------------------- 1 | from channels.routing import ProtocolTypeRouter, URLRouter 2 | from channels.auth import AuthMiddlewareStack 3 | from django.urls import path 4 | from chat.consumers import WebConsumer 5 | 6 | application = ProtocolTypeRouter({ 7 | 'websocket':AuthMiddlewareStack( 8 | URLRouter([ 9 | path('ws/', WebConsumer.as_asgi()), 10 | path('ws/', WebConsumer.as_asgi()), 11 | ]) 12 | ) 13 | }) -------------------------------------------------------------------------------- /chatapp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for chatapp project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.1.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | from pathlib import Path 15 | 16 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 17 | BASE_DIR = Path(__file__).resolve().parent.parent 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'django-insecure-%3!necin$ajp+$svkm!vumv&jpe9wqbu_d%(f!ljb-n^74^8(u' 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 | 37 | 'django.contrib.admin', 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.messages', 42 | 'django.contrib.staticfiles', 43 | 'chat', 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'django.middleware.security.SecurityMiddleware', 48 | 'django.contrib.sessions.middleware.SessionMiddleware', 49 | 'django.middleware.common.CommonMiddleware', 50 | 'django.middleware.csrf.CsrfViewMiddleware', 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 52 | 'django.contrib.messages.middleware.MessageMiddleware', 53 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 54 | ] 55 | 56 | ROOT_URLCONF = 'chatapp.urls' 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [ 62 | str(BASE_DIR.joinpath('templates')) 63 | ], 64 | 'APP_DIRS': True, 65 | 'OPTIONS': { 66 | 'context_processors': [ 67 | 'django.template.context_processors.debug', 68 | 'django.template.context_processors.request', 69 | 'django.contrib.auth.context_processors.auth', 70 | 'django.contrib.messages.context_processors.messages', 71 | ], 72 | }, 73 | }, 74 | ] 75 | 76 | ASGI_APPLICATION = 'chatapp.routing.application' 77 | # WSGI_APPLICATION = 'chatapp.wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/4.1/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.sqlite3', 86 | 'NAME': BASE_DIR / 'db.sqlite3', 87 | } 88 | } 89 | 90 | 91 | # Password validation 92 | # https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators 93 | 94 | AUTH_PASSWORD_VALIDATORS = [ 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 106 | }, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/4.1/topics/i18n/ 112 | 113 | LANGUAGE_CODE = 'en-us' 114 | 115 | TIME_ZONE = 'UTC' 116 | 117 | USE_I18N = True 118 | 119 | USE_TZ = True 120 | 121 | 122 | # Static files (CSS, JavaScript, Images) 123 | # https://docs.djangoproject.com/en/4.1/howto/static-files/ 124 | 125 | STATIC_URL = 'static/' 126 | 127 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 128 | MEDIA_URL = '/media/' 129 | 130 | # Default primary key field type 131 | # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field 132 | 133 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 134 | 135 | 136 | 137 | LOGIN_URL = 'login_view' 138 | 139 | 140 | # REDIS 141 | REDIS = False 142 | 143 | if REDIS : 144 | CHANNEL_LAYERS = { 145 | "default": { 146 | "BACKEND": "channels_redis.core.RedisChannelLayer", 147 | "CONFIG": { 148 | "hosts": [("localhost", 6379)], 149 | }, 150 | }, 151 | } 152 | else: 153 | CHANNEL_LAYERS = { 154 | "default": { 155 | "BACKEND": "channels.layers.InMemoryChannelLayer" 156 | } 157 | } -------------------------------------------------------------------------------- /chatapp/urls.py: -------------------------------------------------------------------------------- 1 | 2 | from django.contrib import admin 3 | from django.urls import path, include 4 | from django.conf import settings 5 | from django.conf.urls.static import static 6 | from chat import views as chat_views 7 | 8 | 9 | urlpatterns = [ 10 | path('admin/', admin.site.urls), 11 | path('', chat_views.index, name='index'), 12 | path('', chat_views.index, name='index'), 13 | path('login/', chat_views.login_view, name='login_view'), 14 | path('signup/', chat_views.signup_view, name='signup_view'), 15 | path('settings/', chat_views.settings_view, name='settings_view'), 16 | 17 | # APIs 18 | path('online-users/', chat_views.api_online_users, name='online-users'), 19 | path('online-users/', chat_views.api_online_users, name='online-users'), 20 | path('chat-messages/', chat_views.api_chat_messages, name='chat_messages'), 21 | path('unread/', chat_views.api_unread, name='api_unread'), 22 | 23 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 24 | 25 | 26 | -------------------------------------------------------------------------------- /chatapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for chatapp 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/4.1/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', 'chatapp.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chatapp.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /media/profile-pics/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jalitko/DjangoChat/5b4c2758d122998016082e7497b423a55756867d/media/profile-pics/default.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.5.2 2 | async-timeout==4.0.2 3 | attrs==22.1.0 4 | autobahn==22.7.1 5 | Automat==20.2.0 6 | cffi==1.15.1 7 | channels==3.0.5 8 | channels-redis==4.0.0 9 | commonmark==0.9.1 10 | constantly==15.1.0 11 | cryptography==38.0.1 12 | daphne==3.0.2 13 | Deprecated==1.2.13 14 | Django==4.1.2 15 | hyperlink==21.0.0 16 | idna==3.4 17 | incremental==22.10.0 18 | msgpack==1.0.4 19 | packaging==21.3 20 | Pillow==9.2.0 21 | pyasn1==0.4.8 22 | pyasn1-modules==0.2.8 23 | pycparser==2.21 24 | Pygments==2.13.0 25 | pyOpenSSL==22.1.0 26 | pyparsing==3.0.9 27 | redis==4.3.4 28 | rich==12.6.0 29 | service-identity==21.1.0 30 | six==1.16.0 31 | sqlparse==0.4.3 32 | Twisted==22.8.0 33 | txaio==22.2.1 34 | typing_extensions==4.4.0 35 | wrapt==1.14.1 36 | zope.interface==5.5.0 37 | -------------------------------------------------------------------------------- /templates/index-template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% load static %} 5 | DjangoChat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Messages 16 | 17 | 18 | Milan Aladin 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 | 31 |
32 | 33 | Milan Aladin 34 |
35 | 36 |
37 | 38 | Cherise Wtnhetnaio 39 |
40 | 41 |
42 | 43 | Sue Barker 44 |
45 | 46 |
47 | 48 | Autumn W.. 49 |
50 | 51 |
52 | 53 | Jac Schro.. 54 |
55 | 56 | 57 |
58 | 59 | Jac Schro.. 60 |
61 | 62 | 63 |
64 | 65 | Jac Schro.. 66 |
67 | 68 | Jac Schro.. 69 |
70 | 71 | Jac Schro.. 72 |
73 | 74 | Jac Schro.. 75 |
76 | 77 | Jac Schro.. 78 |
79 | 80 | Jac Schro.. 81 |
82 | 83 | Jac Schro.. 84 |
85 | 86 | Jac Schro.. 87 |
88 | 89 | Jac Schro.. 90 |
91 | 92 | 93 | 94 | 95 | 96 |
97 | 98 |
99 | 100 |
101 | 102 |
103 | 104 | 105 |
106 | Milan Aladin 107 | text of the last message 108 |
109 | 110 |
111 | 7:27 112 | 113 |
114 | 5 115 |
116 |
117 |
118 |
119 | 120 |
121 | 122 | 123 |
124 | Milan Aladin 2 125 | text of the last message 126 |
127 | 128 |
129 | 7:27 130 | 131 |
132 | 5 133 |
134 |
135 |
136 |
137 | 138 | 139 |
140 | 141 | 142 |
143 | Milan Aladin 3 144 | text of the last message 145 |
146 | 147 |
148 | 7:27 149 | 150 |
151 | 5 152 |
153 |
154 |
155 |
156 | 157 |
158 | 159 | 160 |
161 | Milan Aladin 4 162 | text of the last message 163 |
164 | 165 |
166 | 7:27 167 | 168 |
169 | 5 170 |
171 |
172 |
173 |
174 | 175 | 176 |
177 | Milan Aladin 5 178 | text of the last message 179 |
180 | 181 |
182 | 7:27 183 | 184 |
185 | 5 186 |
187 |
188 |
189 |
190 | 191 | 192 |
193 | Milan Aladin 6 194 | text of the last message 195 |
196 | 197 |
198 | 7:27 199 | 200 |
201 | 5 202 |
203 |
204 |
205 |
206 | 207 | 208 |
209 | Milan Aladin 7 210 | text of the last message 211 |
212 | 213 |
214 | 4:20 215 | 216 |
217 | 9+ 218 |
219 |
220 |
221 |
222 |
223 | 224 | 225 |
226 | Milan Aladin 8 227 | text of the last message 228 |
229 | 230 |
231 | 4:20 232 | 233 |
234 | 9+ 235 |
236 |
237 |
238 |
239 | 240 | 241 | 242 | 243 | 244 | 245 |
246 |
247 | 248 | 249 | 250 | 251 |
252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 |
262 | 263 |
264 | 265 |
20 september
266 | 267 |
268 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever sianmore. 269 |
270 |
271 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever sianmore. 272 |
273 |
274 | Lorem Ipsum is simply 275 |
276 | 277 |
278 | Lorem Ipsum is simply 279 |
280 |
281 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. 282 |
283 | 284 |
285 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever sianmore. 286 |
287 | 288 |
24 september
289 | 290 |
291 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. 292 |
293 | 294 |
295 | There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.
296 | 297 | 298 |
299 | 300 | 301 |
302 |
303 | 304 |
+
305 | 306 | 307 | 308 | 309 |
310 | 311 | 312 |
313 | 314 | 315 | 316 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% load static %} 5 | DjangoChat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% comment %} MAIN ACCOUNT {% endcomment %} 16 |
17 | 18 | 19 |
20 | 21 | 27 | Messages 28 | 29 | {% comment %} CHSTTING USER {% endcomment %} 30 | 34 | 35 | 36 | 37 |
38 | 39 | {% comment %} TOAST CONTAINER {% endcomment %} 40 | 41 |
42 | 43 |
44 | 45 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | {% comment %} ONLINE USERS {% endcomment %} 53 | 54 | 55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 |
63 | 64 |
65 | 66 | {% comment %} RECENT CHATS {% endcomment %} 67 | 68 |
69 | 70 | 71 | 72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 | 80 |
81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
91 | 92 |
93 | is typing... 94 |
95 | 96 |
97 | 98 | {% comment %} MESSAGES {% endcomment %} 99 | 100 | 101 |
102 | 103 | 104 |
105 |
106 | 107 |
+
108 | 109 |
110 | {% csrf_token %} 111 | 112 | 113 |
114 | 115 |
116 | 117 | 118 |
119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% load static %} 5 | Login • DjangoChat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | DjangoChat 19 |
20 | 21 |
22 | {% csrf_token %} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% if error %} 32 | {{ error }} 33 | 34 | 35 | {% else %} 36 | 37 | 38 | {% endif %} 39 | 40 | 41 |
42 | 43 |
44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /templates/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% load static %} 5 | Login • DjangoChat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 |
24 |
25 | 26 |
27 |
Username
28 | 29 |
30 | 31 |
32 | 33 |
34 |
35 | You have unsaved changes! 36 |
37 | 38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /templates/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% load static %} 5 | Sign up • DjangoChat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | DjangoChat 19 |
20 | 21 |
22 | {% csrf_token %} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {% if error %} 35 | {{ error }} 36 | 37 | 38 | {% else %} 39 | 40 | 41 | {% endif %} 42 | 43 | 44 |
45 | 46 |
47 | 48 | 49 | 50 | 51 | --------------------------------------------------------------------------------