From fbf78213204055b5a069b84ae454836b9e485052 Mon Sep 17 00:00:00 2001 From: Lukian Date: Mon, 7 Apr 2025 22:46:04 +0200 Subject: [PATCH] add: improved the frontend and the backend by adding websockets --- back/api/channels.js | 69 +++++++++++++++++++++ back/api/lastmessages.js | 1 - back/api/newchannels.js | 13 ++++ back/index.js | 1 + back/libs/mysql.js | 20 +++++- back/package.json | 1 + front/package-lock.json | 21 +++++++ front/src/components/MessageComponent.tsx | 2 + front/src/index.css | 1 + front/src/pages/ChannelPage.tsx | 29 ++++++--- front/src/pages/ChannelsPage.tsx | 36 ++++++++--- front/src/pages/CreateChannel.tsx | 2 +- front/src/pages/Home.tsx | 74 ++++++++++++++++++----- front/src/pages/UserPage.tsx | 37 ++++-------- front/src/styles/ChannelPage.css | 1 - front/src/styles/ChannelsPage.css | 5 +- front/src/styles/MessageComponent.css | 2 +- front/src/styles/UserPage.css | 5 +- 18 files changed, 255 insertions(+), 65 deletions(-) create mode 100644 back/api/newchannels.js diff --git a/back/api/channels.js b/back/api/channels.js index 9055c15..a504296 100644 --- a/back/api/channels.js +++ b/back/api/channels.js @@ -11,6 +11,16 @@ router.get('/', async (req, res) => { res.send(channels); }); +const sockets = new Set(); + +router.ws('/', function(ws, req) { + sockets.add(ws); + + ws.on('close', () => { + sockets.delete(ws); + }); +}); + router.get('/:name', async (req, res) => { const name = req.params.name; const connection = await getConnection(); @@ -23,6 +33,22 @@ router.get('/:name', async (req, res) => { } }); +const channelSockets = new Map(); + +router.ws('/:name', function(ws, req) { + const name = req.params.name; + + if (!channelSockets.has(name)) { + channelSockets.set(name, new Set()); + } + + channelSockets.get(name).add(ws); + + ws.on('close', () => { + channelSockets.get(name).delete(ws); + }); +}); + router.get('/:name/messages', async (req, res) => { const name = req.params.name; const connection = await getConnection(); @@ -78,6 +104,23 @@ router.post('/:name/messages/send', async (req, res) => { } connection.end(); + + if (sockets.size > 0) { + for (const client of sockets) { + if (client.readyState === 1) { + client.send('new_message'); + } + } + } + + if (channelSockets.has(name)) { + for (const client of channelSockets.get(name)) { + if (client.readyState === 1) { + client.send('new_message'); + } + } + } + res.send({ message: 'Message sent' }); }); @@ -113,6 +156,23 @@ router.post('/:name/messages/delete', async (req, res) => { await deleteMessage(connection, message_id); await deleMentions(connection, message_id); connection.end(); + + if (sockets.size > 0) { + for (const client of sockets) { + if (client.readyState === 1) { + client.send('delete_message'); + } + } + } + + if (channelSockets.has(name)) { + for (const client of channelSockets.get(name)) { + if (client.readyState === 1) { + client.send('delete_message'); + } + } + } + res.send({ message: 'Message deleted' }); }); @@ -140,6 +200,15 @@ router.post('/add', async (req, res) => { await addChannel(connection, name, description, user.id); connection.end(); + + if (sockets.size > 0) { + for (const client of sockets) { + if (client.readyState === 1) { + client.send('new_channel'); + } + } + } + res.send({ message: 'Channel added' }); }); diff --git a/back/api/lastmessages.js b/back/api/lastmessages.js index 2de089d..8803ec6 100644 --- a/back/api/lastmessages.js +++ b/back/api/lastmessages.js @@ -1,5 +1,4 @@ const express = require('express'); -const jwt = require('jsonwebtoken'); const { getConnection, getLastMessages, getMentions } = require('../libs/mysql'); const router = express.Router(); diff --git a/back/api/newchannels.js b/back/api/newchannels.js new file mode 100644 index 0000000..2bdf456 --- /dev/null +++ b/back/api/newchannels.js @@ -0,0 +1,13 @@ +const express = require('express'); +const { getConnection, getNewChannels } = require('../libs/mysql'); + +const router = express.Router(); + +router.get('/', async (req, res) => { + const connection = await getConnection(); + const channels = await getNewChannels(connection); + connection.end(); + res.send(channels); +}); + +module.exports = router; \ No newline at end of file diff --git a/back/index.js b/back/index.js index 83877dc..aefd4a6 100644 --- a/back/index.js +++ b/back/index.js @@ -7,6 +7,7 @@ const cors = require("cors"); require("dotenv").config(); const app = express(); +var expressWs = require('express-ws')(app); const port = config.port || 3000; app.use(express.json()); diff --git a/back/libs/mysql.js b/back/libs/mysql.js index c108ac7..1f99fa8 100644 --- a/back/libs/mysql.js +++ b/back/libs/mysql.js @@ -110,7 +110,7 @@ function getActiveChannels(connection) { FROM messages JOIN channels ON messages.channel_id = channels.id JOIN users ON messages.user_id = users.id - WHERE date > (SELECT max(date) FROM messages) - 7 * 24 * 60 * 60 + WHERE date > (SELECT max(date) FROM messages) - 3 * 24 * 60 * 60 GROUP BY channel_id ORDER BY count(*) DESC LIMIT 5;`, @@ -124,6 +124,23 @@ function getActiveChannels(connection) { }); } +function getNewChannels(connection) { + return new Promise((resolve, reject) => { + connection.query( + `SELECT channels.id, name, description, owner_id, username AS owner_username + FROM channels + JOIN users ON channels.owner_id = users.id + ORDER BY channels.id DESC LIMIT 5`, + (error, result) => { + if (error) { + reject(new Error(error)); + } + resolve(result); + } + ); + }); +} + function searchChannels(connection, search) { return new Promise((resolve, reject) => { connection.query( @@ -319,6 +336,7 @@ module.exports = { getUserLastMessages, getChannels, getActiveChannels, + getNewChannels, searchChannels, getChannel, addChannel, diff --git a/back/package.json b/back/package.json index 3a629cf..9f40754 100644 --- a/back/package.json +++ b/back/package.json @@ -16,6 +16,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-ws": "^5.0.2", "fs": "^0.0.1-security", "https": "^1.0.0", "jsonwebtoken": "^9.0.2", diff --git a/front/package-lock.json b/front/package-lock.json index a6fd7ee..6580df6 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -1387,6 +1387,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.14.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", + "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/react": { "version": "19.0.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.12.tgz", @@ -3400,6 +3412,15 @@ "typescript": ">=4.8.4 <5.9.0" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", diff --git a/front/src/components/MessageComponent.tsx b/front/src/components/MessageComponent.tsx index 328886a..b02e407 100644 --- a/front/src/components/MessageComponent.tsx +++ b/front/src/components/MessageComponent.tsx @@ -19,6 +19,8 @@ export default function MessageComponent({ message, user, channel, deleteMessage if (mention) { return {word} ; } + } else if (word.startsWith("https://") || word.startsWith("http://")) { + return {word} } return {word} ; })} diff --git a/front/src/index.css b/front/src/index.css index 73dcf47..99b54d8 100644 --- a/front/src/index.css +++ b/front/src/index.css @@ -1,5 +1,6 @@ html { background: linear-gradient(#FFE0E3, #f59ebb); + min-height: 100vh; } .cat { diff --git a/front/src/pages/ChannelPage.tsx b/front/src/pages/ChannelPage.tsx index d747d5c..0f2c4c9 100644 --- a/front/src/pages/ChannelPage.tsx +++ b/front/src/pages/ChannelPage.tsx @@ -71,15 +71,17 @@ export default function ChannelPage() { }, [name]); useEffect(() => { - const id = setInterval(() => { - axios - .get(`/api/channels/${name}/messages`).then((res) => { - setMessages(res.data) - }) - }, 2000) - - return () => { clearInterval(id) } - }, []) + const socket = new WebSocket(`/api/channels/${name}`); + + socket.addEventListener('message', function (event) { + if (event.data === "new_message" || event.data === "delete_message") { + axios + .get(`/api/channels/${name}/messages`).then((res) => { + setMessages(res.data) + }) + } + }); + }, [name]) useEffect(() => { const words = message.toString().split(" "); @@ -102,7 +104,14 @@ export default function ChannelPage() { if (!channel || !messages) { - return
Loading...
; + return ( +
+ +
+

Loading...

+
+
+ ) } return ( diff --git a/front/src/pages/ChannelsPage.tsx b/front/src/pages/ChannelsPage.tsx index 2e6e11e..5d04e95 100644 --- a/front/src/pages/ChannelsPage.tsx +++ b/front/src/pages/ChannelsPage.tsx @@ -34,22 +34,40 @@ export default function ChannelsPage() { }, []) useEffect(() => { - const id = setInterval(() => { - axios - .get("/api/channels").then((res) => { - setChannels(res.data) - } - ) - }, 2000) + const socket = new WebSocket("/api/channels"); - return () => { clearInterval(id) } + socket.addEventListener('message', function (event) { + if (event.data === "new_channel") { + axios + .get("/api/channels") + .then((res) => { + setChannels(res.data) + }) + .catch((err) => { + console.error(err.response) + }) + } + }); }, []) + if (!channels) { + return ( +
+ +
+

Channels

+

Loading...

+
+
+ ) + } + return (

Channels

+ Create channel setSearch(e.target.value)} />
    - {channels?.filter((channel) => channel.name.toLowerCase().includes(search.toLowerCase())).map((channel) => ( + {channels?.sort().filter((channel) => channel.name.toLowerCase().includes(search.toLowerCase())).map((channel) => (
  • {channel.name}
  • diff --git a/front/src/pages/CreateChannel.tsx b/front/src/pages/CreateChannel.tsx index 47936a2..db335dc 100644 --- a/front/src/pages/CreateChannel.tsx +++ b/front/src/pages/CreateChannel.tsx @@ -31,7 +31,7 @@ export default function CreateChannel() { axios .post("/api/channels/add", { name, description, token }) .then(() => { - navigate("/"); + navigate(`/c/${name}`); }) .catch((err) => { console.error(err.response.data.message); diff --git a/front/src/pages/Home.tsx b/front/src/pages/Home.tsx index 15209c9..2517fa6 100644 --- a/front/src/pages/Home.tsx +++ b/front/src/pages/Home.tsx @@ -12,6 +12,7 @@ export default function Home() { const [channels, setChannels] = useState(); const [messages , setMessages] = useState(); const [searchedChannels, setSearchedChannels] = useState([]); + const [newChannels, setNewChannels] = useState([]); const [search, setSearch] = useState(""); useEffect(() => { @@ -45,27 +46,60 @@ export default function Home() { }) .catch((err) => { console.error(err.response) - } - ) + }) + + axios + .get("/api/newchannels") + .then((res) => { + setNewChannels(res.data) + }) + .catch((err) => { + console.error(err.response) + }) }, []) useEffect(() => { - const id = setInterval(() => { - axios - .get("/api/activechannels").then((res) => { - setChannels(res.data) - } - ) + const socket = new WebSocket("/api/channels"); - axios - .get("/api/lastmessages").then((res) => { - setMessages(res.data) - } - ) + socket.addEventListener('message', function (event) { + if (event.data === "new_message" || event.data === "delete_message") { + axios + .get("/api/lastmessages") + .then((res) => { + setMessages(res.data) + }) + .catch((err) => { + console.error(err.response) + }) - }, 2000) + axios + .get("/api/activechannels") + .then((res) => { + setChannels(res.data) + }) + .catch((err) => { + console.error(err.response) + }) + } else if (event.data === "new_channel") { + axios + .get("/api/activechannels") + .then((res) => { + setChannels(res.data) + }) + .catch((err) => { + console.error(err.response) + }) - return () => { clearInterval(id) } + axios + .get("/api/newchannels") + .then((res) => { + setNewChannels(res.data) + }) + .catch((err) => { + console.error(err.response) + }) + } + }); }, []) useEffect(() => { @@ -125,6 +159,16 @@ export default function Home() { ))}
+
+

Last created channels

+
    + {newChannels?.map((channel) => ( +
  • + {channel.name} +
  • + ))} +
+

Search channels

{ - const id = setInterval(() => { - axios - .get(`/api/users/${username}/lastmessages`).then((res) => { - setMessages(res.data) - }) - }, 2000) - - return () => { clearInterval(id) } - }, [username]) - if (!pageUser) { return
Loading...
; } @@ -55,19 +44,19 @@ export default function UserPage() {

{pageUser.username}

{pageUser.admin ?

Admin

:

User

} -
-

Last messages

-
- {messages?.map((message) => ( - - ))} -
+
+
+

Last messages

+
+ {messages?.map((message) => ( + + ))}
diff --git a/front/src/styles/ChannelPage.css b/front/src/styles/ChannelPage.css index d172599..5296825 100644 --- a/front/src/styles/ChannelPage.css +++ b/front/src/styles/ChannelPage.css @@ -4,7 +4,6 @@ justify-content: start; align-items: center; width: 100%; - min-height: 100vh; gap: 20px; } diff --git a/front/src/styles/ChannelsPage.css b/front/src/styles/ChannelsPage.css index 93ced85..bb8094e 100644 --- a/front/src/styles/ChannelsPage.css +++ b/front/src/styles/ChannelsPage.css @@ -4,7 +4,6 @@ justify-content: start; align-items: center; width: 100%; - min-height: 100vh; gap: 20px; } @@ -13,4 +12,8 @@ border: 1px solid #270722; padding: 10px; background-color: #fff6fd; + display: flex; + flex-direction: column; + align-items: start; + gap: 10px; } \ No newline at end of file diff --git a/front/src/styles/MessageComponent.css b/front/src/styles/MessageComponent.css index 6bd7d62..c0378dd 100644 --- a/front/src/styles/MessageComponent.css +++ b/front/src/styles/MessageComponent.css @@ -2,4 +2,4 @@ width: 95%; border: 1px solid #270722; padding: 10px; -} \ No newline at end of file +} diff --git a/front/src/styles/UserPage.css b/front/src/styles/UserPage.css index 2ff4aa5..8dc8fb6 100644 --- a/front/src/styles/UserPage.css +++ b/front/src/styles/UserPage.css @@ -4,7 +4,6 @@ justify-content: start; align-items: center; width: 100%; - min-height: 100vh; gap: 20px; } @@ -18,8 +17,12 @@ } .user-messages { + width: 97%; border: 1px solid #270722; padding: 10px; + display: flex; + flex-direction: column; + background-color: #fff6fd; } .messages-list {