From 409de85d02f51b54169a6bde5e240b9576af185c Mon Sep 17 00:00:00 2001 From: Antoine Gaudron-desjardins <antoine.gaudrondesjardins@student-cs.fr> Date: Fri, 15 Jul 2022 11:26:53 +0200 Subject: [PATCH] websocket for comments --- backend/db/crud.py | 4 ++- backend/main.py | 1 + backend/routers/__init__.py | 3 ++- backend/routers/comments.py | 6 ++++- backend/routers/websocket.py | 38 +++++++++++++++++++++++++++++ frontend/package-lock.json | 16 ++++++++++++ frontend/package.json | 1 + frontend/src/components/Comments.js | 33 ++++++++++++++++--------- frontend/src/index.js | 7 +++++- frontend/src/styles/Comments.css | 3 +-- frontend/src/views/Restaurant.js | 8 +++--- 11 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 backend/routers/websocket.py diff --git a/backend/db/crud.py b/backend/db/crud.py index a78139b..82fa7c6 100644 --- a/backend/db/crud.py +++ b/backend/db/crud.py @@ -184,7 +184,9 @@ def get_comments(place: str, page: int, db: Session): comments = db.query(models.Comments).order_by(models.Comments.published_at.desc(), models.Comments.id.desc()).all() else: comments = db.query(models.Comments, models.Users.username).join(models.Users).filter(models.Comments.place == place).order_by(models.Comments.published_at.desc(), models.Comments.id.desc()).slice((page - 1) * 20, page * 20).all() - return list(schemas.Comment(**comment.__dict__, username=username) for comment, username in comments) + comments_list = list(schemas.Comment(**comment.__dict__, username=username) for comment, username in comments) + comments_list.reverse() + return comments_list def create_comment(user: schemas.User, place: str, new_comments: schemas.CommentBase, db: Session): diff --git a/backend/main.py b/backend/main.py index c0ec2d2..52115e3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -40,6 +40,7 @@ app.include_router(stats.router) app.include_router(comments.router) app.include_router(news.router) app.include_router(authentication.router) +app.include_router(websocket.router) @app.get('/api/records', response_model=List[schemas.Record]) diff --git a/backend/routers/__init__.py b/backend/routers/__init__.py index 92c71e3..9788c31 100644 --- a/backend/routers/__init__.py +++ b/backend/routers/__init__.py @@ -2,4 +2,5 @@ from . import authentication from . import comments from . import news from . import opening_hours -from . import stats \ No newline at end of file +from . import stats +from . import websocket \ No newline at end of file diff --git a/backend/routers/comments.py b/backend/routers/comments.py index 44033c9..cdd8599 100644 --- a/backend/routers/comments.py +++ b/backend/routers/comments.py @@ -1,9 +1,11 @@ from fastapi import APIRouter, Body, Cookie, Depends from sqlalchemy.orm import Session from typing import List +import json from db import schemas, crud from db.database import get_db +from routers.websocket import manager router = APIRouter(prefix="/api", tags=["comments"]) @@ -18,7 +20,9 @@ async def get_comments(place: str, page: int = 1, db: Session = Depends(get_db)) async def create_comment(place: str, connect_id: str = Cookie(...), comment: schemas.CommentBase = Body(...), db: Session = Depends(get_db)): user = crud.get_user(connect_id, db) if user: - return crud.create_comment(user, place, comment, db) + saved_comment = crud.create_comment(user, place, comment, db) + await manager.broadcast(json.dumps(saved_comment.dict(), default=str)) + return saved_comment else: raise Exception diff --git a/backend/routers/websocket.py b/backend/routers/websocket.py new file mode 100644 index 0000000..bbc383f --- /dev/null +++ b/backend/routers/websocket.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, WebSocket, WebSocketDisconnect +from typing import List + + +class ConnectionManager: + def __init__(self): + self.active_connections: List[WebSocket] = [] + + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def send_personal_message(self, message: str, websocket: WebSocket): + await websocket.send_text(message) + + async def broadcast(self, message: str): + for connection in self.active_connections: + try: + await connection.send_text(message) + except WebSocketDisconnect: + manager.disconnect(connection) + + +manager = ConnectionManager() +router = APIRouter(tags=["websocket"]) + + +@router.websocket("/api/ws") +async def websocket_endpoint(websocket: WebSocket): + await manager.connect(websocket) + try: + while True: + await websocket.receive_text() + except WebSocketDisconnect: + manager.disconnect(websocket) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f0ea8c..c531868 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,7 @@ "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "react-spring": "^9.4.5", + "react-use-websocket": "^4.2.0", "recharts": "^2.1.12" }, "devDependencies": { @@ -17521,6 +17522,15 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-use-websocket": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.2.0.tgz", + "integrity": "sha512-ZovaTlc/tWX6a590fi3kMWImhyoWj46BWJWvO5oucZJzRnVVhYtes2D9g+5MKXjSdR7Es3456hB89v4/1pcBKg==", + "peerDependencies": { + "react": ">= 18.0.0", + "react-dom": ">= 18.0.0" + } + }, "node_modules/react-zdog": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/react-zdog/-/react-zdog-1.0.11.tgz", @@ -34103,6 +34113,12 @@ "prop-types": "^15.6.2" } }, + "react-use-websocket": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/react-use-websocket/-/react-use-websocket-4.2.0.tgz", + "integrity": "sha512-ZovaTlc/tWX6a590fi3kMWImhyoWj46BWJWvO5oucZJzRnVVhYtes2D9g+5MKXjSdR7Es3456hB89v4/1pcBKg==", + "requires": {} + }, "react-zdog": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/react-zdog/-/react-zdog-1.0.11.tgz", diff --git a/frontend/package.json b/frontend/package.json index b186d81..b2c0c09 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", "react-spring": "^9.4.5", + "react-use-websocket": "^4.2.0", "recharts": "^2.1.12" }, "scripts": { diff --git a/frontend/src/components/Comments.js b/frontend/src/components/Comments.js index 2b93bcf..e0a52e0 100644 --- a/frontend/src/components/Comments.js +++ b/frontend/src/components/Comments.js @@ -9,7 +9,7 @@ import { getSiblings } from "../utils"; import "../styles/Comments.css"; -export default function Messages({ place, infos }) { +export default function Messages({ place, infos, lastMessage }) { const [user] = useContext(User); const [messages, setMessages] = useState([]); const [newComment, setNewComment] = useState(""); @@ -30,13 +30,10 @@ export default function Messages({ place, infos }) { { withCredentials: true }, ) .then((res) => { - if (messages.length) { - let update = messages.map((_, index) => (index ? messages[index - 1] : res.data)); - update.push(messages[messages.length - 1]); - setMessages(update); - } else { - setMessages([res.data]); - } + setMessages((messages) => { + messages.push(res.data); + return messages; + }); updateValue(""); }) .catch((e) => { @@ -108,7 +105,7 @@ export default function Messages({ place, infos }) { let position = chat.current.scrollHeight - chat.current.clientHeight; chat.current.scrollTop = position; } - }, [chat.current]); + }, [chat.current, messages.length]); useEffect(() => { if (input.current) { @@ -117,6 +114,18 @@ export default function Messages({ place, infos }) { } }, [newComment]); + useEffect(() => { + if (lastMessage?.data) { + let new_message = JSON.parse(lastMessage.data); + if (new_message.username != user) { + setMessages((messages) => { + messages.push(new_message); + return messages; + }); + } + } + }, [lastMessage]); + return ( <div className="comments-side-bar"> <div className="comments-title"> @@ -148,11 +157,13 @@ export default function Messages({ place, infos }) { ) ) : ( messages.map((message, index) => { - let [date, hour] = message.published_at.split("T"); + let [date, hour] = message.published_at.split(/[T\s]/); let [year, month, day] = date.split("-"); return ( <div key={index} className="comment"> - <div className={`comment-title${infos ? "-infos" : ""}`}>{infos ? message.title : message.username}</div> + <div className={`comment-title${infos ? "-infos" : ""}`}> + {infos ? message.title : message.username} + </div> <div className="comment-content">{message.content}</div> <div className="comment-date"> {`À ${hour.substring(0, 5)} le ${day}/${month}/${year}`} diff --git a/frontend/src/index.js b/frontend/src/index.js index fae0324..c793ad7 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,6 +1,7 @@ import React, { createContext, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import useWebSocket from "react-use-websocket"; import axios from "axios"; import { Footer, Header } from "./components"; @@ -9,6 +10,7 @@ import { HomePage, RestaurantPage, NotFoundPage, DetailsPage } from "./views"; import "bootstrap/dist/css/bootstrap.min.css"; import "./styles/index.css"; +const socketUrl = `${process.env.REACT_APP_SOCKET_URL}/ws`; export const User = createContext(null); export default function App() { @@ -16,6 +18,9 @@ export default function App() { const [selection, setSelection] = useState(null); const [loading, setLoading] = useState(true); const [user, setUser] = useState(localStorage.getItem("user")); + const { lastMessage } = useWebSocket(socketUrl, { + shouldReconnect: () => true, + }); useEffect(() => { axios @@ -55,7 +60,7 @@ export default function App() { /> <Route path="/:restaurant" - element={<RestaurantPage {...{ selection, setSelection }} />} + element={<RestaurantPage {...{ selection, setSelection, lastMessage }} />} > <Route path="details" element={<DetailsPage selection={selection} />} /> </Route> diff --git a/frontend/src/styles/Comments.css b/frontend/src/styles/Comments.css index ffb1833..1fdfc1e 100644 --- a/frontend/src/styles/Comments.css +++ b/frontend/src/styles/Comments.css @@ -35,13 +35,12 @@ .comments-scroll-bar { flex: 1; display: flex; - flex-direction: column-reverse; + flex-direction: column; overflow-y: auto; } .infos-scroll-bar { direction: rtl; - flex-direction: column; } .comment { diff --git a/frontend/src/views/Restaurant.js b/frontend/src/views/Restaurant.js index 827eaf7..5b0d73f 100644 --- a/frontend/src/views/Restaurant.js +++ b/frontend/src/views/Restaurant.js @@ -4,17 +4,17 @@ import { Graph, WaitingTime, Comments } from "../components"; import "../styles/restaurant.css"; -export default function RestaurantPage({ selection }) { +export default function RestaurantPage({ selection, lastMessage }) { return ( <> {selection && ( <div className="restaurant-container"> <Comments place={selection.name} infos /> <div className="restaurant-container" id="restaurant-main-page"> - <WaitingTime place={selection.name} /> - <Graph place={selection.name} type="current" /> + <WaitingTime place={selection.name} lastMessage={lastMessage} /> + <Graph place={selection.name} type="current" lastMessage={lastMessage} /> </div> - <Comments place={selection.name} /> + <Comments place={selection.name} lastMessage={lastMessage} /> {/*<Graph place={selection.name} type="avg" />*/} </div> )} -- GitLab