add_user_conv impl

This commit is contained in:
2025-12-29 17:22:38 +01:00
parent 8af10eb381
commit 39867c2c36
6 changed files with 245 additions and 33 deletions

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { createContext } from 'react'
import reactLogo from './assets/react.svg' import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg' import viteLogo from '/vite.svg'
import './App.css' import './App.css'
@@ -8,15 +9,20 @@ import { API_BASE } from './config.js'
import Login from './components/login.jsx' import Login from './components/login.jsx'
import MainApp from './components/MainApp.jsx' import MainApp from './components/MainApp.jsx'
export const ClientContext = createContext(null);
export function userContext() {}
export default function App() { export default function App() {
const [loading, setLoading] = useState(true); // true while checking auth const [loading, setLoading] = useState(true); // true while checking auth
const [loggedIn, setLoggedIn] = useState(false); const [loggedIn, setLoggedIn] = useState(false);
const [clientId, setClientId] = useState([]);
useEffect(() => { useEffect(() => {
async function checkAuth() { async function checkAuth() {
try { try {
// This endpoint should validate the refresh token cookie and return an access token // This endpoint should validate the refresh token cookie and return an access token
const res = await fetch(`${API_BASE}/auth/login_refresh_token`, { const res = await fetch(`${API_BASE}/auth/login_refresh_token`, {
method: "POST", method: "POST",
headers: { headers: {
@@ -28,7 +34,9 @@ export default function App() {
device_id : "147ac10b-58cc-4372-a567-0e02b2c3d479", // or persistent device ID device_id : "147ac10b-58cc-4372-a567-0e02b2c3d479", // or persistent device ID
}), }),
}); });
console.log("sent checkAuth request") console.log("sent checkAuth request")
if (!res.ok) { if (!res.ok) {
console.log(res) console.log(res)
console.log("checkAuth request not ok") console.log("checkAuth request not ok")
@@ -40,6 +48,8 @@ export default function App() {
const data = await res.json(); const data = await res.json();
console.log(data) console.log(data)
localStorage.setItem("access_tokens", JSON.stringify(data.tokens)); // store for API calls localStorage.setItem("access_tokens", JSON.stringify(data.tokens)); // store for API calls
setClientId(data.user_id)
setLoggedIn(true); setLoggedIn(true);
} }
} catch (err) { } catch (err) {
@@ -57,7 +67,7 @@ export default function App() {
// Show main app if logged in, otherwise show login // Show main app if logged in, otherwise show login
return loggedIn ? ( return loggedIn ? (
<MainApp /> <MainApp resClientId ={clientId} />
) : ( ) : (
<Login onLogin={() => setLoggedIn(true)} /> <Login onLogin={() => setLoggedIn(true)} />
); );

View File

@@ -7,14 +7,18 @@ export function useHotel() {
return useContext(HotelContext); return useContext(HotelContext);
} }
export function HotelProvider({ accessToken, children }) { export function HotelProvider({ accessToken, children, resClientId}) {
const [rooms, setRooms] = useState([]); const [rooms, setRooms] = useState([]);
const [conversations, setConversations] = useState([]); const [conversations, setConversations] = useState([]);
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [usersById, setUsersById] = useState([]);
const [clientId, setClientId] = useState(resClientId|| null);
const tokens = JSON.parse(accessToken); const tokens = JSON.parse(accessToken);
const accessTokenSingle = tokens[0]; const accessTokenSingle = tokens[0];
// --- API FUNCTIONS --- // --- API FUNCTIONS ---
async function fetchRooms() { async function fetchRooms() {
const res = await fetch( `${API_BASE}/rooms/rooms`, { const res = await fetch( `${API_BASE}/rooms/rooms`, {
@@ -46,7 +50,17 @@ export function HotelProvider({ accessToken, children }) {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}); });
return res.json();
const users = await res.json();
const map = {};
for (const u of users) {
map[u.id] = u.username;
}
setUsersById(map);
// setUsers(users)
return users;
} }
async function fetchConversations() { async function fetchConversations() {
@@ -71,7 +85,7 @@ export function HotelProvider({ accessToken, children }) {
timestamp: "2025-09-25 11:05:33", timestamp: "2025-09-25 11:05:33",
}; };
console.log(JSON.stringify(payload)); //console.log(JSON.stringify(payload));
const res = await fetch(`${API_BASE}/chat/get_message`, { const res = await fetch(`${API_BASE}/chat/get_message`, {
method: "POST", method: "POST",
@@ -87,15 +101,42 @@ export function HotelProvider({ accessToken, children }) {
//return res.json //return res.json
} }
async function fetchConvUsers({ conv_id }) {
const payload = {
conv_id,
timestamp: "2025-09-25 11:05:33",
};
//console.log(JSON.stringify(payload));
const res = await fetch(`${API_BASE}/chat/get_conv_users/${conv_id}`, {
method: "POST",
headers: {
Authorization: `Bearer ${accessTokenSingle}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
return res.json();
//return res.json
}
async function sendMessage({ conv_id, message }) { async function sendMessage({ conv_id, message }) {
if (!conv_id) return; if (!conv_id) {
console.log("conv_id null at sendMessage") console.log("conv_id null at sendMessage")
return
};
const payload = { const payload = {
conv_id, conv_id,
message message
}; };
console.log(JSON.stringify(payload)); //console.log(JSON.stringify(payload));
const res = await fetch(`${API_BASE}/chat/send_message`, { const res = await fetch(`${API_BASE}/chat/send_message`, {
method: "POST" , method: "POST" ,
@@ -107,6 +148,27 @@ export function HotelProvider({ accessToken, children }) {
}) })
} }
async function addUserToConv({ conv_id, users}) {
if (!conv_id || users) {
console.log("error in convs or user to add")
};
const payload = {
conv_id,
users
};
const res = await fetch(`${API_BASE}/chat/add_users_conv`, {
method: "PUT" ,
headers: {
Authorization: `Bearer ${accessTokenSingle}`,
"Content-Type" : "application/json",
},
body: JSON.stringify(payload),
})
}
// --- INITIAL DATA LOADING --- // --- INITIAL DATA LOADING ---
useEffect(() => { useEffect(() => {
@@ -119,12 +181,15 @@ export function HotelProvider({ accessToken, children }) {
fetchHotelUsers(), fetchHotelUsers(),
]); ]);
setClientId(resClientId);
setRooms(roomsData); setRooms(roomsData);
setConversations(convData); setConversations(convData);
//console.log("USERS =",users)
setUsers(usersData); setUsers(usersData);
} }
load(); load();
//console.log("USERS 2 =",usersById)
}, [accessToken]); }, [accessToken]);
return ( return (
@@ -133,9 +198,14 @@ export function HotelProvider({ accessToken, children }) {
rooms, rooms,
conversations, conversations,
users, users,
usersById,
clientId,
updateRoomStatus, updateRoomStatus,
fetchMessages, fetchMessages,
fetchConvUsers,
sendMessage, sendMessage,
fetchHotelUsers,
addUserToConv,
}} }}
> >
{children} {children}

View File

@@ -4,11 +4,11 @@ import ChatWidget from "./widget/chatWidget"
import "./MainApp.css" import "./MainApp.css"
export default function MainAppWrapper() { export default function MainAppWrapper({resClientId}) {
const accessToken = localStorage.getItem("access_tokens"); const accessToken = localStorage.getItem("access_tokens");
return ( return (
<HotelProvider accessToken={accessToken}> <HotelProvider accessToken={accessToken} resClientId={resClientId}>
<MainApp /> <MainApp />
</HotelProvider> </HotelProvider>
); );
@@ -37,7 +37,4 @@ function MainApp() {
</div> </div>
); );
} }

View File

@@ -1,3 +1,21 @@
.messagebox{
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* your messages */
.msg--mine {
align-self: flex-end;
background-color: #464b43;
text-align: right;
}
/* others */
.msg--theirs {
align-self: flex-start;
background-color: #473f3f;
}
.grid { .grid {
display: flex; display: flex;
justify-items: space-around; justify-items: space-around;

View File

@@ -1,23 +1,27 @@
import { useHotel } from "../HotelContext"; import { useHotel } from "../HotelContext";
import { useState } from "react"; import { useState, useEffect } from "react";
import { useRef } from "react";
//import {fetchMessage} from . //import {fetchMessage} from .
import "./chatWidget.css" import "./chatWidget.css"
export default function ChatWidget({convlist}) { export default function ChatWidget({convlist}) {
const [messages, setMessages] = useState([]); const { fetchMessages, sendMessage,
const { fetchMessages, sendMessage } = useHotel(); usersById, clientId, fetchConvUsers,
const [activeConvId, setActiveConvId] = useState(null); addUserToConv
} = useHotel();
const [activeConvId, setActiveConvId] = useState(null);
const [showAddUsers, setShowAddUsers] = useState(false);
const handleOpenConv = async (conv_id) => { const handleOpenConv = async (conv_id) => {
setActiveConvId(conv_id); setActiveConvId(conv_id);
const msg = await fetchMessages({ conv_id });
setMessages(msg);
}; };
return ( console.log("client id in chat widget");
return (
<div className="chatWidget"> <div className="chatWidget">
<div className="convlist"> <div className="convlist">
{convlist.map(conv => ( {convlist.map(conv => (
@@ -25,12 +29,39 @@ export default function ChatWidget({convlist}) {
key={conv.id} key={conv.id}
id={conv.id} id={conv.id}
title={conv.title} title={conv.title}
onOpenConv={handleOpenConv} onOpenConv={setActiveConvId}
/> />
))} ))}
<div className="convUtils">
<button
disabled={!activeConvId}
onClick={() => setShowAddUsers(v => !v)}
>
Add users
</button>
</div>
</div> </div>
<MessagesBox messages={messages} /> {showAddUsers && activeConvId && (
<AddUsersMenu
convId={activeConvId}
usersById={usersById}
fetchConvUsers={fetchConvUsers}
onValidate={addUserToConv}
onClose={() => setShowAddUsers(false)}
/>
)}
{activeConvId && (
<MessagesBox
conv_id={activeConvId}
fetchMessages={fetchMessages}
usersById={usersById}
clientId={clientId}
/>
)}
<SenderBox <SenderBox
conv_id={activeConvId} conv_id={activeConvId}
onSend={sendMessage} onSend={sendMessage}
@@ -41,9 +72,11 @@ export default function ChatWidget({convlist}) {
} }
function SenderBox({ conv_id, onSend, disabled }) { function SenderBox({ conv_id, onSend, disabled }) {
const [text, setText] = useState(""); const [text, setText] = useState("");
const handleSend = () => { const handleSend = () => {
console.log("conv_Id: ",conv_id)
if (!text.trim() || !conv_id) return; if (!text.trim() || !conv_id) return;
onSend({ onSend({
@@ -55,17 +88,78 @@ function SenderBox({ conv_id, onSend, disabled }) {
}; };
return ( return (
<div className="senderbox"> <div hidden={disabled} className="senderbox">
<input <input
value={text} value={text}
onChange={e => setText(e.target.value)} onChange={e => setText(e.target.value)}
placeholder="Type a message…" placeholder="Type a message…"
/> />
<button disabled={disabled} onClick={handleSend}>Send</button> <button onClick={handleSend}>Send</button>
</div> </div>
); );
} }
function AddUsersMenu({ convId, usersById, fetchConvUsers, onValidate, onClose }) {
const [selected, setSelected] = useState(new Set());
//new function to fetch already present users in the conv
//let current = fetchConvUsers(convId);
const toggleUser = (id) => {
setSelected(prev => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
};
const handleValidate = () => {
if (selected.size === 0) return;
onValidate({
conv_id: convId,
users: [...selected],
});
onClose();
};
useEffect(() => {
async function load() {
console.log("fetching conv users ar :", convId);
const ids = await fetchConvUsers({ conv_id: convId });
setSelected(new Set(ids));
}
load();
}, [convId, fetchConvUsers]);
return (
<div className="addUsersMenu">
<h3>Add users</h3>
<ul>
{Object.entries(usersById).map(([id, name]) => (
<li key={id}>
<label>
<input
type="checkbox"
checked={selected.has(Number(id))}
onChange={() => toggleUser(Number(id))}
/>
{name}
</label>
</li>
))}
</ul>
<button onClick={handleValidate}>Validate</button>
<button onClick={onClose}>Cancel</button>
</div>
);
}
function ConvCard({id, title, onOpenConv}) { function ConvCard({id, title, onOpenConv}) {
return( return(
@@ -77,21 +171,43 @@ function ConvCard({id, title, onOpenConv}) {
) )
} }
function MessagesBox({ messages }) { function MessagesBox({ conv_id, fetchMessages, usersById, clientId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
async function load() {
const msg = await fetchMessages({ conv_id });
setMessages(msg);
}
load();
}, [conv_id]);
if (!messages || messages.length === 0) { if (!messages || messages.length === 0) {
return <div className="messagesbox empty">No messages</div> return <div className="messagesbox empty">No messages</div>
} }
console.log("FETCH RESULT =", messages); //console.log("FETCH RESULT =", messages);
return ( return (
<div className="messagebox"> <div className="messagebox">
{messages.map(msg => ( {messages
<div key = {msg.id} className = "msg"> .slice() // dont mutate state
<p><strong>Sender:</strong> {msg.sender_id}</p> .sort((a, b) => new Date(a.sent_at) - new Date(b.sent_at))
<p>{msg.content}</p> .map(msg => {
<small>{msg.sent_at}</small> const isMine = msg.sender_id === clientId
</div>
))} return(
<div key = {msg.id}
className={`msg ${isMine ? "msg--mine" : "msg--theirs"}`}>
<p>
<strong>Sender:</strong>{" "}
{usersById[msg.sender_id] ?? "Unknown user"}
</p>
<p>{msg.content}</p>
<small>{msg.sent_at}</small>
</div>
)
})}
</div> </div>
); );

View File

@@ -1 +1,2 @@
//export const API_BASE = "http://79.137.75.155:8080";
export const API_BASE = "http://localhost:7080"; export const API_BASE = "http://localhost:7080";