Skip to content
Snippets Groups Projects
Commit 409de85d authored by Antoine Gaudron-Desjardins's avatar Antoine Gaudron-Desjardins
Browse files

websocket for comments

parent 26709f86
Branches
No related tags found
1 merge request!40Websocket
Pipeline #44130 passed with warnings
...@@ -184,7 +184,9 @@ def get_comments(place: str, page: int, db: Session): ...@@ -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() comments = db.query(models.Comments).order_by(models.Comments.published_at.desc(), models.Comments.id.desc()).all()
else: 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() 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): def create_comment(user: schemas.User, place: str, new_comments: schemas.CommentBase, db: Session):
......
...@@ -40,6 +40,7 @@ app.include_router(stats.router) ...@@ -40,6 +40,7 @@ app.include_router(stats.router)
app.include_router(comments.router) app.include_router(comments.router)
app.include_router(news.router) app.include_router(news.router)
app.include_router(authentication.router) app.include_router(authentication.router)
app.include_router(websocket.router)
@app.get('/api/records', response_model=List[schemas.Record]) @app.get('/api/records', response_model=List[schemas.Record])
......
...@@ -3,3 +3,4 @@ from . import comments ...@@ -3,3 +3,4 @@ from . import comments
from . import news from . import news
from . import opening_hours from . import opening_hours
from . import stats from . import stats
from . import websocket
\ No newline at end of file
from fastapi import APIRouter, Body, Cookie, Depends from fastapi import APIRouter, Body, Cookie, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import List from typing import List
import json
from db import schemas, crud from db import schemas, crud
from db.database import get_db from db.database import get_db
from routers.websocket import manager
router = APIRouter(prefix="/api", tags=["comments"]) router = APIRouter(prefix="/api", tags=["comments"])
...@@ -18,7 +20,9 @@ async def get_comments(place: str, page: int = 1, db: Session = Depends(get_db)) ...@@ -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)): 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) user = crud.get_user(connect_id, db)
if user: 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: else:
raise Exception raise Exception
......
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)
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-spring": "^9.4.5", "react-spring": "^9.4.5",
"react-use-websocket": "^4.2.0",
"recharts": "^2.1.12" "recharts": "^2.1.12"
}, },
"devDependencies": { "devDependencies": {
...@@ -17521,6 +17522,15 @@ ...@@ -17521,6 +17522,15 @@
"react-dom": ">=16.6.0" "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": { "node_modules/react-zdog": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/react-zdog/-/react-zdog-1.0.11.tgz", "resolved": "https://registry.npmjs.org/react-zdog/-/react-zdog-1.0.11.tgz",
...@@ -34103,6 +34113,12 @@ ...@@ -34103,6 +34113,12 @@
"prop-types": "^15.6.2" "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": { "react-zdog": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/react-zdog/-/react-zdog-1.0.11.tgz", "resolved": "https://registry.npmjs.org/react-zdog/-/react-zdog-1.0.11.tgz",
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-spring": "^9.4.5", "react-spring": "^9.4.5",
"react-use-websocket": "^4.2.0",
"recharts": "^2.1.12" "recharts": "^2.1.12"
}, },
"scripts": { "scripts": {
......
...@@ -9,7 +9,7 @@ import { getSiblings } from "../utils"; ...@@ -9,7 +9,7 @@ import { getSiblings } from "../utils";
import "../styles/Comments.css"; import "../styles/Comments.css";
export default function Messages({ place, infos }) { export default function Messages({ place, infos, lastMessage }) {
const [user] = useContext(User); const [user] = useContext(User);
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
const [newComment, setNewComment] = useState(""); const [newComment, setNewComment] = useState("");
...@@ -30,13 +30,10 @@ export default function Messages({ place, infos }) { ...@@ -30,13 +30,10 @@ export default function Messages({ place, infos }) {
{ withCredentials: true }, { withCredentials: true },
) )
.then((res) => { .then((res) => {
if (messages.length) { setMessages((messages) => {
let update = messages.map((_, index) => (index ? messages[index - 1] : res.data)); messages.push(res.data);
update.push(messages[messages.length - 1]); return messages;
setMessages(update); });
} else {
setMessages([res.data]);
}
updateValue(""); updateValue("");
}) })
.catch((e) => { .catch((e) => {
...@@ -108,7 +105,7 @@ export default function Messages({ place, infos }) { ...@@ -108,7 +105,7 @@ export default function Messages({ place, infos }) {
let position = chat.current.scrollHeight - chat.current.clientHeight; let position = chat.current.scrollHeight - chat.current.clientHeight;
chat.current.scrollTop = position; chat.current.scrollTop = position;
} }
}, [chat.current]); }, [chat.current, messages.length]);
useEffect(() => { useEffect(() => {
if (input.current) { if (input.current) {
...@@ -117,6 +114,18 @@ export default function Messages({ place, infos }) { ...@@ -117,6 +114,18 @@ export default function Messages({ place, infos }) {
} }
}, [newComment]); }, [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 ( return (
<div className="comments-side-bar"> <div className="comments-side-bar">
<div className="comments-title"> <div className="comments-title">
...@@ -148,11 +157,13 @@ export default function Messages({ place, infos }) { ...@@ -148,11 +157,13 @@ export default function Messages({ place, infos }) {
) )
) : ( ) : (
messages.map((message, index) => { 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("-"); let [year, month, day] = date.split("-");
return ( return (
<div key={index} className="comment"> <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-content">{message.content}</div>
<div className="comment-date"> <div className="comment-date">
{${hour.substring(0, 5)} le ${day}/${month}/${year}`} {${hour.substring(0, 5)} le ${day}/${month}/${year}`}
......
import React, { createContext, useEffect, useState } from "react"; import React, { createContext, useEffect, useState } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import useWebSocket from "react-use-websocket";
import axios from "axios"; import axios from "axios";
import { Footer, Header } from "./components"; import { Footer, Header } from "./components";
...@@ -9,6 +10,7 @@ import { HomePage, RestaurantPage, NotFoundPage, DetailsPage } from "./views"; ...@@ -9,6 +10,7 @@ import { HomePage, RestaurantPage, NotFoundPage, DetailsPage } from "./views";
import "bootstrap/dist/css/bootstrap.min.css"; import "bootstrap/dist/css/bootstrap.min.css";
import "./styles/index.css"; import "./styles/index.css";
const socketUrl = `${process.env.REACT_APP_SOCKET_URL}/ws`;
export const User = createContext(null); export const User = createContext(null);
export default function App() { export default function App() {
...@@ -16,6 +18,9 @@ export default function App() { ...@@ -16,6 +18,9 @@ export default function App() {
const [selection, setSelection] = useState(null); const [selection, setSelection] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [user, setUser] = useState(localStorage.getItem("user")); const [user, setUser] = useState(localStorage.getItem("user"));
const { lastMessage } = useWebSocket(socketUrl, {
shouldReconnect: () => true,
});
useEffect(() => { useEffect(() => {
axios axios
...@@ -55,7 +60,7 @@ export default function App() { ...@@ -55,7 +60,7 @@ export default function App() {
/> />
<Route <Route
path="/:restaurant" path="/:restaurant"
element={<RestaurantPage {...{ selection, setSelection }} />} element={<RestaurantPage {...{ selection, setSelection, lastMessage }} />}
> >
<Route path="details" element={<DetailsPage selection={selection} />} /> <Route path="details" element={<DetailsPage selection={selection} />} />
</Route> </Route>
......
...@@ -35,13 +35,12 @@ ...@@ -35,13 +35,12 @@
.comments-scroll-bar { .comments-scroll-bar {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column;
overflow-y: auto; overflow-y: auto;
} }
.infos-scroll-bar { .infos-scroll-bar {
direction: rtl; direction: rtl;
flex-direction: column;
} }
.comment { .comment {
......
...@@ -4,17 +4,17 @@ import { Graph, WaitingTime, Comments } from "../components"; ...@@ -4,17 +4,17 @@ import { Graph, WaitingTime, Comments } from "../components";
import "../styles/restaurant.css"; import "../styles/restaurant.css";
export default function RestaurantPage({ selection }) { export default function RestaurantPage({ selection, lastMessage }) {
return ( return (
<> <>
{selection && ( {selection && (
<div className="restaurant-container"> <div className="restaurant-container">
<Comments place={selection.name} infos /> <Comments place={selection.name} infos />
<div className="restaurant-container" id="restaurant-main-page"> <div className="restaurant-container" id="restaurant-main-page">
<WaitingTime place={selection.name} /> <WaitingTime place={selection.name} lastMessage={lastMessage} />
<Graph place={selection.name} type="current" /> <Graph place={selection.name} type="current" lastMessage={lastMessage} />
</div> </div>
<Comments place={selection.name} /> <Comments place={selection.name} lastMessage={lastMessage} />
{/*<Graph place={selection.name} type="avg" />*/} {/*<Graph place={selection.name} type="avg" />*/}
</div> </div>
)} )}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment