Implement chat backend and frontend scaffolding
Expanded chat backend with message reply, edit, delete, and reaction support in consumers and models. Updated routing to use chat_id. Added chat-related view stubs. On the frontend, introduced ChatLayout and ChatPage scaffolding, and routed protected routes through ChatLayout.
This commit is contained in:
@@ -5,38 +5,147 @@ from account.models import UserProfile
|
|||||||
|
|
||||||
from channels.db import database_sync_to_async
|
from channels.db import database_sync_to_async
|
||||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||||
|
from asgiref.sync import sync_to_async, async_to_sync
|
||||||
|
|
||||||
|
|
||||||
class ChatConsumer(AsyncWebsocketConsumer):
|
class ChatConsumer(AsyncWebsocketConsumer):
|
||||||
|
# -- CONNECT --
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
|
self.chat_id = self.scope["url_route"]["kwargs"]["chat_id"]
|
||||||
|
self.chat_name = f"chat_{self.chat_id}"
|
||||||
|
|
||||||
user = self.scope["user"]
|
user = self.scope["user"]
|
||||||
|
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
await self.close(code=4401) # unauthorized
|
await self.close(code=4401) # unauthorized
|
||||||
return
|
return
|
||||||
|
|
||||||
|
#join chat group
|
||||||
|
async_to_sync(self.channel_layer.group_add)(
|
||||||
|
self.chat_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await self.accept()
|
await self.accept()
|
||||||
|
|
||||||
|
# -- DISCONNECT --
|
||||||
async def disconnect(self, close_code):
|
async def disconnect(self, close_code):
|
||||||
|
async_to_sync(self.channel_layer.group_discard)(
|
||||||
|
self.chat_name
|
||||||
|
)
|
||||||
|
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# -- RECIVE --
|
||||||
async def receive(self, data):
|
async def receive(self, data):
|
||||||
if data["type"] == "chat_message":
|
if data["type"] == "new_chat_message":
|
||||||
pass
|
|
||||||
|
message = data["message"]
|
||||||
|
|
||||||
|
# Send message to room group
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.chat_name, {"type": "chat.message", "message": message}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif data["type"] == "new_reply_chat_message":
|
||||||
|
message = data["message"]
|
||||||
|
reply_to_id = data["reply_to_id"]
|
||||||
|
|
||||||
|
# Send message to room group
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.chat_name, {"type": "reply.chat.message", "message": message, "reply_to_id": reply_to_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif data["type"] == "edit_chat_message":
|
||||||
|
message = data["message"]
|
||||||
|
|
||||||
|
# Send message to room group
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.chat_name, {"type": "edit.message", "message": message}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif data["type"] == "delete_chat_message":
|
||||||
|
message_id = data["message_id"]
|
||||||
|
|
||||||
|
# Send message to room group
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.chat_name, {"type": "delete.message", "message_id": message_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif data["type"] == "typing":
|
||||||
|
is_typing = data["is_typing"]
|
||||||
|
|
||||||
|
# Send typing status to room group
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.chat_name, {"type": "typing.status", "user": self.scope["user"].username, "is_typing": is_typing}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif data["type"] == "stop_typing":
|
||||||
|
# Send stop typing status to room group
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.chat_name, {"type": "stop.typing", "user": self.scope["user"].username}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif data["type"] == "reaction":
|
||||||
|
message_id = data["message_id"]
|
||||||
|
emoji = data["emoji"]
|
||||||
|
|
||||||
|
# Send reaction to room group
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.chat_name, {"type": "message.reaction", "message_id": message_id, "emoji": emoji, "user": self.scope["user"].username}
|
||||||
|
)
|
||||||
|
|
||||||
|
elif data["type"] == "unreaction":
|
||||||
|
message_id = data["message_id"]
|
||||||
|
emoji = data["emoji"]
|
||||||
|
|
||||||
|
# Send unreaction to room group
|
||||||
|
async_to_sync(self.channel_layer.group_send)(
|
||||||
|
self.chat_name, {"type": "message.unreaction", "message_id": message_id, "emoji": emoji, "user": self.scope["user"].username}
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.close(reason="Unsupported message type")
|
self.close(reason="Unsupported message type")
|
||||||
|
|
||||||
|
|
||||||
# -- CUSTOM METHODS --
|
# -- CUSTOM METHODS --
|
||||||
|
|
||||||
async def send_message_to_chat_group(self, event):
|
def send_message_to_chat_group(self, event):
|
||||||
message = event["message"]
|
message = event["message"]
|
||||||
await create_new_message()
|
create_new_message()
|
||||||
await self.send(text_data=json.dumps({"message": message}))
|
self.send(text_data=json.dumps({"message": message}))
|
||||||
|
|
||||||
|
def edit_message_in_chat_group(self, event):
|
||||||
|
message = event["message"]
|
||||||
|
self.send(text_data=json.dumps({"message": message}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -- MESSAGES --
|
||||||
|
@database_sync_to_async
|
||||||
|
def create_new_message():
|
||||||
|
return None
|
||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def create_new_message(user_id):
|
def create_new_reply_message():
|
||||||
|
return None
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def edit_message():
|
||||||
|
return None
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def delete_message():
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# -- REACTIONS --
|
||||||
|
@database_sync_to_async
|
||||||
|
def react_to_message():
|
||||||
|
return None
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def unreact_to_message():
|
||||||
return None
|
return None
|
||||||
@@ -47,6 +47,15 @@ class Message(models.Model):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='sent_messages'
|
related_name='sent_messages'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#odpověď na jinou zprávu
|
||||||
|
reply_to = models.ForeignKey(
|
||||||
|
'self',
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='replies'
|
||||||
|
)
|
||||||
|
|
||||||
content = models.TextField(blank=True)
|
content = models.TextField(blank=True)
|
||||||
|
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ from django.urls import re_path
|
|||||||
from . import consumers
|
from . import consumers
|
||||||
|
|
||||||
websocket_urlpatterns = [
|
websocket_urlpatterns = [
|
||||||
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
|
re_path(r"ws/chat/(?P<chat_id>\w+)/$", consumers.ChatConsumer.as_asgi()),
|
||||||
]
|
]
|
||||||
@@ -1,3 +1,25 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
# Create your views here.
|
# Create your views here.
|
||||||
|
|
||||||
|
|
||||||
|
def get_users_chats(request):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_chat(request):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def invite_user_to_chat(request, chat_id: int, user_ids: list):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def delete_chat(request, chat_id: int):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def leave_chat(request, chat_id: int):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def edit_chat(request, chat_object):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_chat_messages(request, chat_id: int, limit: int = 50, offset: int = 0):
|
||||||
|
return None
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||||
import Home from "./pages/home/home";
|
|
||||||
import HomeLayout from "./layouts/HomeLayout";
|
import HomeLayout from "./layouts/HomeLayout";
|
||||||
|
import ChatLayout from "./layouts/ChatLayout";
|
||||||
|
|
||||||
import Downloader from "./pages/downloader/Downloader";
|
import Downloader from "./pages/downloader/Downloader";
|
||||||
import PrivateRoute from "./routes/PrivateRoute";
|
import Home from "./pages/home/home";
|
||||||
import DroneServisSection from "./pages/home/components/Services/droneServis";
|
import DroneServisSection from "./pages/home/components/Services/droneServis";
|
||||||
|
|
||||||
|
|
||||||
|
import PrivateRoute from "./routes/PrivateRoute";
|
||||||
|
|
||||||
//import { UserContextProvider } from "./context/UserContext";
|
//import { UserContextProvider } from "./context/UserContext";
|
||||||
|
|
||||||
// Pages
|
// Pages
|
||||||
@@ -36,9 +40,10 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Example protected route group (kept for future use) */}
|
{/* Example protected route group (kept for future use) */}
|
||||||
<Route element={<PrivateRoute />}>
|
<Route element={<PrivateRoute />}>
|
||||||
<Route path="/" element={<HomeLayout />}>
|
<Route path="/" element={<ChatLayout />}>
|
||||||
<Route path="protected-downloader" element={<Downloader />} />
|
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
{/* </UserContextProvider> */}
|
{/* </UserContextProvider> */}
|
||||||
|
|||||||
21
frontend/src/layouts/ChatLayout.tsx
Normal file
21
frontend/src/layouts/ChatLayout.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import Footer from "../components/Footer/footer";
|
||||||
|
import { Outlet } from "react-router";
|
||||||
|
import SiteNav, { type User } from "../components/navbar/SiteNav";
|
||||||
|
|
||||||
|
|
||||||
|
const userexists: User = {
|
||||||
|
username: "Bruno",
|
||||||
|
email: "",
|
||||||
|
avatarUrl: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
import styles from "./HomeLayout.module.css";
|
||||||
|
|
||||||
|
export default function ChatLayout(){
|
||||||
|
return(
|
||||||
|
<div className={styles.root}>
|
||||||
|
<SiteNav user={userexists} onLogin={() => {}} onLogout={() => {}} />
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
0
frontend/src/pages/Chat/ChatPage.tsx
Normal file
0
frontend/src/pages/Chat/ChatPage.tsx
Normal file
Reference in New Issue
Block a user