Chat, scrolling, sockets and timestamps updated

This commit is contained in:
Askar Akhmetkhanov
2024-10-09 17:37:50 +03:00
parent 13f4d43761
commit 4cf909c607
9 changed files with 381 additions and 419 deletions

View File

@@ -1,43 +0,0 @@
const WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 8080 });
const clients = new Map();
wss.on("connection", (ws, req) => {
console.log("New client connected");
ws.on("message", (message) => {
try {
const parsedMessage = JSON.parse(message);
if (parsedMessage.type === "register") {
clients.set(parsedMessage.userId, ws);
console.log(`User registered: ${parsedMessage.userId}`);
} else if (parsedMessage.type === "message") {
const recipientWs = clients.get(parsedMessage.recipientId);
if (recipientWs) {
recipientWs.send(
JSON.stringify({
senderId: parsedMessage.senderId,
message: parsedMessage.message,
timestamp: new Date().toISOString(),
})
);
} else {
console.error(`User ${parsedMessage.recipientId} is not connected.`);
}
}
} catch (err) {
console.error("Error processing message:", err.message);
}
});
ws.on("close", () => {
console.log("Client disconnected");
[...clients.entries()].forEach(([userId, clientWs]) => {
if (clientWs === ws) {
clients.delete(userId);
console.log(`User disconnected: ${userId}`);
}
});
});
});

31
src/backend/server.js Normal file
View File

@@ -0,0 +1,31 @@
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
require("dotenv").config();
const app = express();
const server = http.createServer(app);
const io = new Server(server);
io.on("connection", (socket) => {
console.log("New connection:", socket.id);
// For messages
socket.on("sendMessage", (message) => {
console.log("Message received:", message);
socket.broadcast.emit("receiveMessage", message);
});
socket.on("disconnect", () => {
console.log("User disconnected:", socket.id);
});
});
app.get("/", (req, res) => {
res.send("Socket.IO Server is running");
});
const PORT = process.env.PORT || 8099;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

View File

@@ -1,76 +1,92 @@
import React, {useEffect, useState} from 'react';
import React, { useEffect, useState } from "react";
import Card from "./Card.jsx";
import {get} from "../../backend/api";
import {displayMessage} from "../../backend/notifications/notifications";
import {MessageType} from "../../backend/notifications/message";
import { get } from "../../backend/api";
import { displayMessage } from "../../backend/notifications/notifications";
import { MessageType } from "../../backend/notifications/message";
const ChatsList = (props) => {
const { chats } = props;
const { chats } = props;
const [customChats, setCustomChats] = useState([]);
const [customChats, setCustomChats] = useState([]);
const updateList = async () => {
const username = localStorage.getItem("username");
if (!username) return null;
const updateList = async () => {
const username = localStorage.getItem("username");
if (!username) {return null;}
const updatedChats = await Promise.all(
chats.map(async (chat) => {
const interlocutorId = chat.id1 === username ? chat.id2 : chat.id1;
const updatedChats = await Promise.all(
chats.map(async (chat) => {
const interlocutorId = chat.id1 === username ? chat.id2 : chat.id1
const { ok, data } = await get("/auth/" + interlocutorId);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
return null;
}
const {ok, data} = await get('/auth/' + interlocutorId);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
return null;
}
const interlocutor = data.user;
const interlocutor = data.user;
const lastMessage =
chat.messages.length > 0
? chat.messages[chat.messages.length - 1]
: { data: "", timestamp: new Date(0).toISOString() };
return {
id: interlocutorId,
name: interlocutor.nickname,
lastMessage: chat.messages.length > 0 ? chat.messages[chat.messages.length - 1].data : "",
}
})
);
return {
id: interlocutorId,
name: interlocutor.nickname,
lastMessageData: lastMessage.data,
lastMessageTimestamp: lastMessage.timestamp,
};
})
);
setCustomChats(updatedChats.filter(chat => chat !== null));
};
const validChats = updatedChats.filter((chat) => chat !== null);
useEffect(() => {updateList().then();}, [chats])
validChats.sort(
(a, b) =>
new Date(b.lastMessageTimestamp) - new Date(a.lastMessageTimestamp)
);
setCustomChats(validChats);
};
const colorMap = {
orange: 'FFA500FF',
aqua: '00FFFFFF',
crimson: 'DC143CFF',
red: 'FF0000FF',
violet: '8A2BE2FF',
seagreen: '20B2AAFF',
green: 'ADFF2FFF',
blue: '0000FFFF',
pink: 'FF1493FF',
cyan: '72FAFAFF'
}
useEffect(() => {
updateList().then();
}, [chats]);
function getColor(chatId) {
const keys = Object.keys(colorMap);
const numericId = Array.from(chatId).reduce((sum, char) => sum + char.charCodeAt(0), 0);
const index = numericId % keys.length;
return colorMap[keys[index]];
}
const colorMap = {
orange: "FFA500FF",
aqua: "00FFFFFF",
crimson: "DC143CFF",
red: "FF0000FF",
violet: "8A2BE2FF",
seagreen: "20B2AAFF",
green: "ADFF2FFF",
blue: "0000FFFF",
pink: "FF1493FF",
cyan: "72FAFAFF",
};
function getColor(chatId) {
const keys = Object.keys(colorMap);
const numericId = Array.from(chatId).reduce(
(sum, char) => sum + char.charCodeAt(0),
0
);
const index = numericId % keys.length;
return colorMap[keys[index]];
}
return (
<div className="ChatsList">
{customChats.map((item, index) => (
<Card
key={index}
name={item.name}
lastMessage={item.lastMessage}
id={item.id}
color={getColor(item.id)}
/>
))}
</div>
<div className="ChatsList">
{customChats.map((item, index) => (
<Card
key={index}
name={item.name}
lastMessage={item.lastMessageData}
id={item.id}
color={getColor(item.id)}
/>
))}
</div>
);
};

View File

@@ -2,9 +2,10 @@ import React, { useEffect, useState, useRef } from "react";
import { useNavigate } from "react-router-dom";
import "./css/Chat.css";
import { FaPaperPlane, FaSmile } from "react-icons/fa";
import {get, post} from "../backend/api";
import {displayMessage} from "../backend/notifications/notifications";
import {MessageType} from "../backend/notifications/message";
import { get, post } from "../backend/api";
import { displayMessage } from "../backend/notifications/notifications";
import { MessageType } from "../backend/notifications/message";
import io from "socket.io-client";
const emojis = [
"😀",
@@ -88,8 +89,7 @@ const Chat = () => {
const [myId, setMyId] = useState("");
useEffect(() => {
// const id = parseInt(localStorage.getItem("interlocutorId"), 10) || 0;
const id = localStorage.getItem("interlocutorId")
const id = localStorage.getItem("interlocutorId");
setInterlocutorId(id);
const username = localStorage.getItem("username");
@@ -100,36 +100,24 @@ const Chat = () => {
return () => {};
}
socket.current = new WebSocket("ws://localhost:8080");
socket.current = io("http://localhost:8099");
socket.current.onopen = () => {
console.log("WebSocket connected");
socket.current.send(
JSON.stringify({ type: "register", userId: "yourUserId" })
);
};
socket.current.on("receiveMessage", (message) => {
setMessages((prev) => [...prev, message]);
});
socket.current.onmessage = (event) => {
const receivedData = JSON.parse(event.data);
setMessages((prev) => [...prev, receivedData]);
};
socket.current.onerror = (event) => {
console.error("WebSocket error observed:", event);
};
socket.current.onclose = () => {
console.log("WebSocket closed");
};
socket.current.on("connect_error", (err) => {
console.error("Connection Error:", err.message);
});
return () => {
socket.current.close();
socket.current.disconnect();
};
}, []);
useEffect(() => {
retrieveMessages().then();
}, [myId, interlocutorId])
}, [myId, interlocutorId]);
useEffect(() => {
if (chatRef.current) {
@@ -137,25 +125,24 @@ const Chat = () => {
}
}, [messages]);
// The function for sending message to the DB
async function sendMessageToDB (messageData) {
const { ok, data } = post('/chat/message/' + myId + '/' + interlocutorId, { message: messageData });
async function sendMessageToDB(messageData) {
const { ok, data } = await post(
"/chat/message/" + myId + "/" + interlocutorId,
{ message: messageData }
);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
}
}
// The function retrieves messages from the DB for the current chat
async function retrieveMessages () {
if (!myId || !interlocutorId) {return;}
const { ok, data } = await get('/chat/item/' + myId + '/' + interlocutorId);
async function retrieveMessages() {
if (!myId || !interlocutorId) return;
const { ok, data } = await get("/chat/item/" + myId + "/" + interlocutorId);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
return;
}
setMessages(data.chat.messages);
}
@@ -165,15 +152,11 @@ const Chat = () => {
senderId: myId,
recipientId: interlocutorId,
data: newMessage,
timestamp: new Date().toLocaleTimeString(),
timestamp: new Date().toLocaleString(),
};
socket.current.send(JSON.stringify(messageData));
socket.current.emit("sendMessage", messageData);
setMessages((prev) => [...prev, messageData]);
sendMessageToDB(messageData).then();
console.log('format:', messageData);
setNewMessage("");
}
};
@@ -198,8 +181,7 @@ const Chat = () => {
className="home-button"
>
Home
</button>{" "}
{}
</button>
</div>
<div className="chat-messages" ref={chatRef}>
{messages.map((msg, index) => (
@@ -210,8 +192,7 @@ const Chat = () => {
}`}
>
<div className="message-content">
<b>{msg.senderId === myId ? "You" : "They"}:</b>{" "}
{msg.data}
<b>{msg.senderId === myId ? "You" : "They"}:</b> {msg.data}
</div>
<span className="message-timestamp">{msg.timestamp}</span>
</div>

View File

@@ -1,77 +1,86 @@
import React, {useEffect, useState} from "react";
import React, { useEffect, useState } from "react";
import HomeTitle from "../components/home/HomeTitle.jsx";
import ChatsList from "../components/home/ChatsList.jsx";
import Header from "../components/home/Header.jsx";
import {displayMessage} from "../backend/notifications/notifications";
import {MessageType} from "../backend/notifications/message";
import {get, post} from "../backend/api";
import { displayMessage } from "../backend/notifications/notifications";
import { MessageType } from "../backend/notifications/message";
import { get, post } from "../backend/api";
import InputField from "../components/reg/InputField.jsx";
import Search from "../components/home/Search.jsx";
import {URLs} from "../__data__/urls";
import { URLs } from "../__data__/urls";
const Home = () => {
const [chats, setChats] = useState([])
const [interlocutor, setInterlocutor] = useState("")
const [chats, setChats] = useState([]);
const [interlocutor, setInterlocutor] = useState("");
async function retrieveChats() {
const username = localStorage.getItem("username");
if (!username) {
displayMessage("You're not logged in!", MessageType.WARN);
return;
}
const {ok, data} = await get('/chat/list/' + username);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
return;
}
setChats(data.chats);
async function retrieveChats() {
const username = localStorage.getItem("username");
if (!username) {
displayMessage("You're not logged in!", MessageType.WARN);
return;
}
async function createChat(alias) {
const username = localStorage.getItem("username");
if (!username) {
displayMessage("You're not logged in!", MessageType.WARN);
return;
}
displayMessage("Sent", MessageType.INFO);
const {ok, data} = await post('/chat/item/' + username + '/' + alias);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
} else {
localStorage.setItem('message', 'Successfully opened chat!');
localStorage.setItem('interlocutorId', alias);
window.location.href = URLs.chat.url;
}
const { ok, data } = await get("/chat/list/" + username);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
return;
}
useEffect(() => {retrieveChats().then()}, [])
const sortedChats = data.chats.sort((a, b) => {
const lastMessageA = new Date(a.lastMessageTimestamp);
const lastMessageB = new Date(b.lastMessageTimestamp);
return lastMessageB - lastMessageA;
});
return (
<div className="homeWrapper">
<div className="headerPos">
<Header/>
</div>
setChats(sortedChats);
}
<HomeTitle/>
async function createChat(alias) {
const username = localStorage.getItem("username");
if (!username) {
displayMessage("You're not logged in!", MessageType.WARN);
return;
}
<div className="search-input">
<InputField
title="Create new chat"
value={interlocutor}
setValue={setInterlocutor}
placeholder="Enter the username (id)"
/>
</div>
displayMessage("Sent", MessageType.INFO);
<Search search={createChat} item={interlocutor}/>
const { ok, data } = await post("/chat/item/" + username + "/" + alias);
if (!ok) {
displayMessage(data.message, MessageType.ERROR);
} else {
localStorage.setItem("message", "Successfully opened chat!");
localStorage.setItem("interlocutorId", alias);
window.location.href = URLs.chat.url;
}
}
<p>Your chats</p>
<ChatsList chats={chats} />
useEffect(() => {
retrieveChats();
}, []);
return (
<div className="homeWrapper">
<div className="headerPos">
<Header />
</div>
)
}
export default Home
<HomeTitle />
<div className="search-input">
<InputField
title="Create new chat"
value={interlocutor}
setValue={setInterlocutor}
placeholder="Enter the username (id)"
/>
</div>
<Search search={createChat} item={interlocutor} />
<p>Your chats</p>
<ChatsList chats={chats} />
</div>
);
};
export default Home;

View File

@@ -1,3 +1,7 @@
body {
background: linear-gradient(to right, #e0f7fa, #fffde7);
}
.chat-container {
display: flex;
flex-direction: column;
@@ -11,10 +15,13 @@
}
.chat-header {
padding: 10px;
padding: 15px;
background-color: #007bff;
color: white;
text-align: center;
font-weight: bold;
letter-spacing: 1px;
border-bottom: 2px solid #0056b3;
}
.chat-messages {
@@ -24,6 +31,7 @@
padding: 10px;
overflow-y: auto;
background-color: #fff;
max-height: 400px;
}
.message-bubble {
@@ -33,6 +41,12 @@
max-width: 70%;
word-wrap: break-word;
display: inline-block;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.message-bubble:hover {
transform: translateY(-2px);
}
.sent {
@@ -58,8 +72,9 @@
.message-timestamp {
font-size: 10px;
color: #999;
margin-top: 5px;
color: black;
text-align: right;
margin-top: 4px;
display: block;
}