Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
15d0a92
WIP: Fixing UI
ashley-arellano Apr 13, 2025
ef87272
WIP: Typing Bubble
ashley-arellano Apr 14, 2025
a760e93
WIP: Typing Bubble
ashley-arellano Apr 14, 2025
a501d7e
WIP: Typing Bubble
ashley-arellano Apr 14, 2025
f55b27d
WIP: Typing bubbles work but visual bug
ashley-arellano Apr 15, 2025
c653259
Typing Bubble Done
ashley-arellano Apr 15, 2025
d1f6a8c
WIP: Seen Status
ashley-arellano Apr 15, 2025
571263d
WIP: Seen Status
ashley-arellano Apr 15, 2025
b55e3c6
Seen Status Works + Gitignore Node Modules
ashley-arellano Apr 15, 2025
c64a5c7
Merge branch 'main' into chat-feature-improvements
ashley-arellano Apr 15, 2025
b907f41
Working on adding new chat pop-up
ashley-arellano Apr 16, 2025
7949236
Basic Pop-up
ashley-arellano Apr 16, 2025
825915c
Update jwt_middleware.py
ashley-arellano Apr 18, 2025
8cfc24c
Adjusted logic for handling messages
ashley-arellano Apr 19, 2025
008705f
WIP: Updated Timestamps
ashley-arellano Apr 19, 2025
64a0f6e
Issue: Timing when retrieving message
ashley-arellano Apr 19, 2025
ae1aa7c
WIP: Seen Status
ashley-arellano Apr 19, 2025
b409405
WIP: Marking as seen when opening websocket
ashley-arellano Apr 19, 2025
e3b2d78
Modified Seen Status code to account for GET
ashley-arellano Apr 19, 2025
6837c7d
Removing comments + logs from debugging
ashley-arellano Apr 19, 2025
1074f08
Merge branch 'main' into chat-feature-improvements
ashley-arellano Apr 19, 2025
548b32d
Update
ashley-arellano Apr 19, 2025
17ad837
best attempt at fixing UI layout
ashley-arellano Apr 19, 2025
d528ece
fixed layout
ashley-arellano Apr 19, 2025
c0a598a
Merge branch 'main' into chat-feature-improvements
ashley-arellano Apr 20, 2025
003a8bc
Update .coveragerc
ashley-arellano Apr 20, 2025
a15a16a
Merge branch 'chat-feature-improvements' of https://github.com/UNLV-C…
ashley-arellano Apr 20, 2025
b2bc7c7
Added for codecov
ashley-arellano Apr 20, 2025
963c1bd
Create codecov.yml
ashley-arellano Apr 20, 2025
4d10216
Merge branch 'main' into chat-feature-improvements
ashley-arellano Apr 20, 2025
a45fc67
Merge branch 'main' into chat-feature-improvements
ashley-arellano Apr 21, 2025
9e7c9c2
Merge branch 'main' into chat-feature-improvements
ashley-arellano Apr 25, 2025
5efbc67
Merge branch 'main' into chat-feature-improvements
ashley-arellano Apr 25, 2025
d07a189
Merge branch 'main' into chat-feature-improvements
ashley-arellano Apr 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
/dataSources.local.xml

/.idea/
node_modules/


# file contains very sensitive information.
# you do not want this as everybody can see this information and use it to attack the site.
Expand Down
107 changes: 90 additions & 17 deletions backend/apps/chat/consumers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# The below code is just for testing purposes (it works!)
# Tested it using https://websocketking.com/
# and entered ws://localhost:8000/ws/apps/chat/practice/
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from asgiref.sync import sync_to_async
Expand Down Expand Up @@ -33,35 +30,111 @@ async def disconnect(self, close_code):

# This function receive messages from WebSocket.
async def receive(self, text_data):
# Message is received
text_data_json = json.loads(text_data)
message = text_data_json['message']
if 'message' in text_data_json:
message = text_data_json['message']

# Create a new message object
username = await self.save_message(self.user_id, self.room_name, message)
# Create a new message object
username, id, timestamp = await self.save_message(self.user_id, self.room_name, message)

await self.channel_layer.group_send( # sends message to all users in a channel group
self.room_group_name,
{
'type': 'chat_message', # Calls chat_message()
'message': message,
'username': username
}
)
# Serialize timestamp
formatted = timestamp.isoformat()

await self.channel_layer.group_send( # sends message to all users in a channel group
self.room_group_name,
{
'type': 'chat_message', # Calls chat_message()
'message': message,
'username': username,
'id': id,
'timestamp': formatted
}
)

# Typing status is received
elif 'typing' in text_data_json:
typing = text_data_json['typing']
username = await self.get_username(self.user_id)
await self.channel_layer.group_send( # sends message to all users in a channel group
self.room_group_name,
{
'type': 'typing_status', # Calls typing_status()
'typing': typing,
'username': username
}
)
# Seen status is received
elif 'seen' in text_data_json:
sender_username, message_ids = await self.mark_as_seen(self.room_name, self.user_id)

await self.channel_layer.group_send( # sends message to all users in a channel group
self.room_group_name,
{
'type': 'seen_status', # Calls seen_status()
'username': sender_username,
'ids': message_ids
}
)

@sync_to_async
def mark_as_seen(self, room_name, user_id):
chat = Chat.objects.get(name = room_name)
# Get all unseen messages (excluding those from the receiver)
unseen_messages = chat.messages.filter(status=Message.NOT_SEEN).exclude(sender_id=user_id).order_by('-timestamp')

# Get sender (assumes all chats only have 2 people)
first_unseen = unseen_messages.first()
sender_username = None

# If there are unseen messages from sender
if first_unseen:
sender_username = first_unseen.sender.username

# Get message ids (maintaining order)
message_ids = list(unseen_messages.values_list('id', flat=True))

# Mark unseen messages as seen
unseen_messages.update(status=Message.SEEN)

return sender_username, message_ids


async def seen_status(self, event):
await self.send(text_data=json.dumps({
'message': 'seen_successful',
'ids': event['ids'],
'username': event['username']
}))

async def typing_status(self, event):
await self.send(text_data=json.dumps({
'message': 'successful',
'typing': event['typing'],
'username': event['username']
}))

async def chat_message(self, event):
await self.send(text_data=json.dumps({
'message': 'successful',
'body': event['message'],
'username': event['username']
'username': event['username'],
'id': event['id'],
'timestamp': event['timestamp']
}))

@sync_to_async
def get_username(self, id):
user = User.objects.get(id=id)
return user.username

@sync_to_async
def save_message(self, id, room_name, content):
try:
user = User.objects.get(id=id)
chat = Chat.objects.get(name = room_name)
Message.objects.create(content=content, chat = chat, sender = user) # Creates and saves message
return user.username
message = Message.objects.create(content=content, chat = chat, sender = user) # Creates and saves message
return user.username, message.id, message.timestamp
except (Chat.DoesNotExist):
print(f"Chat room '{room_name}' not found. Message not saved.")
return "Unknown"
2 changes: 1 addition & 1 deletion backend/apps/chat/jwt_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def get_token_from_subprotocol(self, scope):
# WebSocket subprotocols are sent as a list of protocols during the handshake.

subprotocols = scope.get("subprotocols", [])
if subprotocols:
if len(subprotocols) > 1:
# Assuming the token is the second protocol (after the real protocol)
return subprotocols[1] # The access token is the second subprotocol
return None
Expand Down
18 changes: 18 additions & 0 deletions backend/apps/chat/migrations/0007_message_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.1.6 on 2025-04-15 04:31

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('chat', '0006_chat_name'),
]

operations = [
migrations.AddField(
model_name='message',
name='status',
field=models.PositiveSmallIntegerField(choices=[(1, 'Seen'), (2, 'Not seen'), (3, 'Not sent')], default=2),
),
]
11 changes: 10 additions & 1 deletion backend/apps/chat/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,24 @@ def __str__(self):
return f"Chat between {self.user1} and {self.user2}"

class Message(models.Model):
SEEN = 1
NOT_SEEN = 2
NOT_SENT = 3
STATUS_CHOICES = (
(SEEN, 'Seen'),
(NOT_SEEN, 'Not seen'),
(NOT_SENT, 'Not sent'),
)
chat = models.ForeignKey(Chat, on_delete=models.CASCADE, related_name="messages")
status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, null=False, blank=False, default=NOT_SEEN)
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sent_messages")
content = models.TextField()
timestamp = models.DateTimeField(default=now)

objects = MessageManager()

class Meta:
ordering = ['timestamp']
ordering = ['timestamp']

def __str__(self):
return f"From {self.sender} in Chat {self.chat.id} at {self.timestamp}"
Expand Down
5 changes: 5 additions & 0 deletions backend/apps/chat/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
from rest_framework import status
from rest_framework.test import APITestCase, APIClient
from .models import Chat, Message
from apps.chat.consumers import ChatConsumer

def test_chat_consumer_init():
consumer = ChatConsumer()
assert consumer is not None

class ChatAPITestCase(APITestCase):

Expand Down
14 changes: 14 additions & 0 deletions backend/codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
coverage:
status:
project:
default:
enabled: false
patch:
default:
enabled: false

comment:
layout: "reach, diff, flags, files"
behavior: default
require_changes: false
after_n_builds: 1
1 change: 1 addition & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useEffect } from "react";
import Header from "./Components/Header.jsx";
import Footer from "./Components/Footer.jsx";
import StudentLayout from "./Components/StudentLayout.jsx";
import '@fortawesome/fontawesome-free/css/all.min.css';


// Components
Expand Down
Loading