add: added replies to messages

This commit is contained in:
Lukian 2025-05-11 21:10:46 +02:00
parent 05dd75fa74
commit b10a472ed7
10 changed files with 607 additions and 306 deletions

View file

@ -4,17 +4,18 @@ const {
getChannels, getChannels,
getChannel, getChannel,
addChannel, addChannel,
getMessages,
getMessage, getMessage,
getMessages,
addMessage, addMessage,
deleteMessage, deleteMessage,
addMention, addMention,
getMentions, getMentions,
getUserByUsername, getUserByUsername,
deleMentions,
deleteChannelMentions,
deleteChannelMessages, deleteChannelMessages,
deleteChannel deleteChannel,
setHasReplies,
addReply,
getMessageReplies
} = require('../libs/mysql'); } = require('../libs/mysql');
const rateLimit = require("express-rate-limit"); const rateLimit = require("express-rate-limit");
const slowDown = require("express-slow-down"); const slowDown = require("express-slow-down");
@ -70,7 +71,6 @@ router.post('/:name/purge', async (req, res) => {
return res.status(401).send({ error: 'Unauthorized' }); return res.status(401).send({ error: 'Unauthorized' });
} }
await deleteChannelMentions(connection, channel[0].id);
await deleteChannelMessages(connection, channel[0].id); await deleteChannelMessages(connection, channel[0].id);
connection.end(); connection.end();
@ -111,6 +111,23 @@ router.post('/:name/delete', async (req, res) => {
res.send({ message: 'Channel deleted' }); res.send({ message: 'Channel deleted' });
}); });
async function getReplies(message, connection) {
const replies = await getMessageReplies(connection, message.id);
for (const reply of replies) {
if (reply.has_replies) {
const subReplies = await getReplies(reply, connection);
reply.replies = subReplies;
}
if (reply.content.includes('@')) {
const mentions = await getMentions(connection, reply.id);
reply.mentions = mentions;
} else {
reply.mentions = [];
}
}
return replies;
}
router.get('/:name/messages', async (req, res) => { router.get('/:name/messages', async (req, res) => {
const name = req.params.name; const name = req.params.name;
const connection = await getConnection(); const connection = await getConnection();
@ -128,6 +145,14 @@ router.get('/:name/messages', async (req, res) => {
} else { } else {
message.mentions = []; message.mentions = [];
} }
if (message.has_replies) {
const replies = await getReplies(message, connection);
message.replies = replies;
}
else {
message.replies = [];
}
} }
connection.end(); connection.end();
@ -175,46 +200,6 @@ router.post('/:name/messages/send', speedLimiter, limiter, checkAuth, async (req
res.send({ message: 'Message sent' }); res.send({ message: 'Message sent' });
}); });
router.post('/:name/messages/delete', checkAuth, async (req, res) => {
const { message_id } = req.body;
const name = req.params.name;
const user = req.user;
if (!message_id) {
return res.status(400).send({ error: 'Missing message_id' });
}
const connection = await getConnection();
const message = await getMessage(connection, message_id);
if (!message[0]) {
connection.end();
return res.status(400).send({ error: 'No message found' });
}
const channel = await getChannel(connection, name);
if (!channel[0]) {
connection.end();
return res.status(400).send({ error: 'No channel found' });
}
if (user.id !== channel[0].owner_id && user.id !== message[0].user_id && user.admin !== 1) {
connection.end();
return res.status(401).send({ error: 'Unauthorized' });
}
await deleteMessage(connection, message_id);
connection.end();
req.sockets.emit({
type: 'delete_message',
channel_id: channel[0].id,
user_id: user.id,
});
res.send({ message: 'Message deleted' });
});
router.post('/add', speedLimiter, limiter, checkAuth, async (req, res) => { router.post('/add', speedLimiter, limiter, checkAuth, async (req, res) => {
const { name, description } = req.body; const { name, description } = req.body;
const user = req.user; const user = req.user;

View file

@ -14,6 +14,7 @@ router.get('/', async (req, res) => {
} else { } else {
message.mentions = []; message.mentions = [];
} }
message.replies = [];
} }
connection.end(); connection.end();

156
back/api/messages.js Normal file
View file

@ -0,0 +1,156 @@
const express = require('express');
const {
getConnection,
getChannel,
getMessage,
deleteMessage,
addMention,
getMentions,
getUserByUsername,
setHasReplies,
addReply,
getMessageReplies
} = require('../libs/mysql');
const rateLimit = require("express-rate-limit");
const slowDown = require("express-slow-down");
const { checkAuth } = require('../libs/middlewares');
const limiter = rateLimit({
windowMs: 1 * 1000,
max: 2,
});
const speedLimiter = slowDown({
windowMs: 1 * 1000,
delayAfter: 2,
delayMs: () => 5000,
});
const router = express.Router();
async function getReplies(message, connection) {
const replies = await getMessageReplies(connection, message.id);
for (const reply of replies) {
if (reply.has_replies) {
const subReplies = await getReplies(reply, connection);
reply.replies = subReplies;
}
if (reply.content.includes('@')) {
const mentions = await getMentions(connection, reply.id);
reply.mentions = mentions;
} else {
reply.mentions = [];
}
}
return replies;
}
router.get('/:message_id', async (req, res) => {
const message_id = req.params.message_id;
const connection = await getConnection();
const message = await getMessage(connection, message_id);
if (!message[0]) {
connection.end();
return res.send({ error: 'No message found' });
}
if (message[0].content.includes('@')) {
const mentions = await getMentions(connection, message.id);
message[0].mentions = mentions;
} else {
message[0].mentions = [];
}
if (message[0].has_replies) {
const replies = await getReplies(message[0], connection);
message[0].replies = replies;
} else {
message[0].replies = [];
}
connection.end();
res.send(message[0]);
});
router.post('/:message_id/reply', speedLimiter, limiter, checkAuth, async (req, res) => {
const { message } = req.body;
const { message_id } = req.params;
const user = req.user;
if (!message) {
return res.status(400).send({ error: 'Missing parameters' });
}
const connection = await getConnection();
const originalMessage = await getMessage(connection, message_id);
if (!originalMessage[0]) {
connection.end();
return res.status(400).send({ error: 'No message found' });
}
const channel = await getChannel(connection, originalMessage[0].channel_name);
const sent_message = await addReply(connection, channel[0].id, user.id, message.replace("\"", "'"), message_id);
await setHasReplies(connection, message_id, true);
const new_message_id = sent_message.insertId;
for (const word of message.split(' ')) {
if (word.startsWith('@')) {
const username = word.substring(1);
const mentionedUser = await getUserByUsername(connection, username);
if (mentionedUser[0]) {
await addMention(connection, new_message_id, mentionedUser[0].id);
}
}
}
connection.end();
req.sockets.emit({
type: 'new_message',
channel_id: channel[0].id,
user_id: user.id,
});
res.send({ message: 'Reply sent' });
});
router.post('/:message_id/delete', checkAuth, async (req, res) => {
const { message_id } = req.params;
const user = req.user;
const connection = await getConnection();
const message = await getMessage(connection, message_id);
if (!message[0]) {
connection.end();
return res.status(400).send({ error: 'No message found' });
}
const channel = await getChannel(connection, message[0].channel_name);
if (user.id !== channel[0].owner_id && user.id !== message[0].user_id && user.admin !== 1) {
connection.end();
return res.status(401).send({ error: 'Unauthorized' });
}
if (message[0].reply_to_id) {
const replies = await getMessageReplies(connection, message[0].reply_to_id);
if (replies.length === 1) {
await setHasReplies(connection, replyToId, false);
}
}
await deleteMessage(connection, message_id);
connection.end();
req.sockets.emit({
type: 'delete_message',
channel_id: channel[0].id,
user_id: user.id,
});
res.send({ message: 'Message deleted' });
});
module.exports = router;

View file

@ -37,6 +37,7 @@ router.get('/:username/lastmessages', async (req, res) => {
} else { } else {
message.mentions = []; message.mentions = [];
} }
message.replies = [];
} }
connection.end(); connection.end();

View file

@ -9,6 +9,41 @@ function getConnection() {
}); });
} }
// +---------------------------+
// | Users |
// +---------------------------+
function addUser(connection, username, password) {
return new Promise((resolve, reject) => {
connection.query(
`INSERT INTO users (username, password) VALUES (?, ?)`,
[username, password],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function deleteUser(connection, id) {
return new Promise((resolve, reject) => {
connection.query(
`DELETE FROM users WHERE id = ?`,
[id],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function getUsers(connection) { function getUsers(connection) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
@ -27,22 +62,7 @@ function getUser(connection, id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT * FROM users WHERE id = ?`, `SELECT * FROM users WHERE id = ?`,
[id], // Use parameterized query [id],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function searchUser(connection, search) {
return new Promise((resolve, reject) => {
connection.query(
`SELECT * FROM users WHERE username LIKE ? LIMIT 5`,
[`%${search}%`], // Use parameterized query
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -57,7 +77,7 @@ function getUserByUsername(connection, username) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT * FROM users WHERE username = ?`, `SELECT * FROM users WHERE username = ?`,
[username], // Use parameterized query [username],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -68,11 +88,11 @@ function getUserByUsername(connection, username) {
}); });
} }
function addUser(connection, username, password) { function searchUser(connection, search) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`INSERT INTO users (username, password) VALUES (?, ?)`, `SELECT * FROM users WHERE username LIKE ? LIMIT 5`,
[username, password], // Use parameterized query [`%${search}%`],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -83,46 +103,11 @@ function addUser(connection, username, password) {
}); });
} }
function getUserLastMessages(connection, username) {
return new Promise((resolve, reject) => {
connection.query(
`SELECT messages.id, user_id, username, content, date, channels.name AS channel_name
FROM messages
JOIN users ON messages.user_id = users.id
JOIN channels ON messages.channel_id = channels.id
WHERE username = ?
ORDER BY date DESC LIMIT 5`,
[username], // Use parameterized query
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function deleteUser(connection, id) {
return new Promise((resolve, reject) => {
connection.query(
`DELETE FROM users WHERE id = ?`,
[id], // Use parameterized query
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
};
function setUserPfp(connection, id, pfp) { function setUserPfp(connection, id, pfp) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`UPDATE users SET pfp = ? WHERE id = ?`, `UPDATE users SET pfp = ? WHERE id = ?`,
[pfp, id], // Use parameterized query [pfp, id],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -137,7 +122,7 @@ function setUserUsername(connection, id, username) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`UPDATE users SET username = ? WHERE id = ?`, `UPDATE users SET username = ? WHERE id = ?`,
[username, id], // Use parameterized query [username, id],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -152,7 +137,41 @@ function setUserPassword(connection, id, password) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`UPDATE users SET password = ? WHERE id = ?`, `UPDATE users SET password = ? WHERE id = ?`,
[password, id], // Use parameterized query [password, id],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
// +---------------------------+
// | Channels |
// +---------------------------+
function addChannel(connection, name, description, owner_id) {
return new Promise((resolve, reject) => {
connection.query(
`INSERT INTO channels (name, description, owner_id) VALUES (?, ?, ?)`,
[name, description, owner_id],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function deleteChannel(connection, channel_id) {
return new Promise((resolve, reject) => {
connection.query(
`DELETE FROM channels WHERE id = ?`,
[channel_id],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -177,6 +196,24 @@ function getChannels(connection) {
}); });
} }
function getChannel(connection, name) {
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
WHERE name = ?`,
[name],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function getActiveChannels(connection) { function getActiveChannels(connection) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
@ -223,7 +260,7 @@ function searchChannels(connection, search) {
JOIN users ON channels.owner_id = users.id JOIN users ON channels.owner_id = users.id
WHERE name LIKE ? WHERE name LIKE ?
LIMIT 5`, LIMIT 5`,
[`%${search}%`], // Use parameterized query [`%${search}%`],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -234,116 +271,30 @@ function searchChannels(connection, search) {
}); });
} }
function getChannel(connection, name) { // +---------------------------+
return new Promise((resolve, reject) => { // | Messages |
connection.query( // +---------------------------+
`SELECT channels.id, name, description, owner_id, username AS owner_username
FROM channels
JOIN users ON channels.owner_id = users.id
WHERE name = ?`,
[name], // Use parameterized query
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function addChannel(connection, name, description, owner_id) {
return new Promise((resolve, reject) => {
connection.query(
`INSERT INTO channels (name, description, owner_id) VALUES (?, ?, ?)`,
[name, description, owner_id], // Use parameterized query
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function deleteChannel(connection, channel_id) {
return new Promise((resolve, reject) => {
connection.query(
`DELETE FROM channels WHERE id = ?`,
[channel_id], // Use parameterized query
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function getMessages(connection, channel_id) {
return new Promise((resolve, reject) => {
connection.query(
`SELECT messages.id, user_id, username, content, date, channels.name AS channel_name
FROM messages
JOIN users ON messages.user_id = users.id
JOIN channels ON messages.channel_id = channels.id
WHERE channel_id = ?
ORDER BY date DESC`,
[channel_id], // Use parameterized query
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function getLastMessages(connection) {
return new Promise((resolve, reject) => {
connection.query(
`SELECT messages.id, user_id, username, content, date, channels.name AS channel_name
FROM messages
JOIN users ON messages.user_id = users.id
JOIN channels ON messages.channel_id = channels.id
ORDER BY date DESC LIMIT 5`,
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function getMessage(connection, message_id) {
return new Promise((resolve, reject) => {
connection.query(
`SELECT messages.id, user_id, username, content, date, channels.name AS channel_name
FROM messages
JOIN users ON messages.user_id = users.id
JOIN channels ON messages.channel_id = channels.id
WHERE messages.id = ?`,
[message_id], // Use parameterized query
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function addMessage(connection, channel_id, user_id, message) { function addMessage(connection, channel_id, user_id, message) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`INSERT INTO messages (channel_id, user_id, content, date) VALUES (?, ?, ?, ?)`, `INSERT INTO messages (channel_id, user_id, content, date) VALUES (?, ?, ?, ?)`,
[channel_id, user_id, message, Math.floor(Date.now() / 1000)], // Use parameterized query [channel_id, user_id, message, Math.floor(Date.now() / 1000)],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function addReply(connection, channel_id, user_id, message, message_id) {
return new Promise((resolve, reject) => {
connection.query(
`INSERT INTO messages (channel_id, user_id, content, date, reply_to_id) VALUES (?, ?, ?, ?, ?)`,
[channel_id, user_id, message, Math.floor(Date.now() / 1000), message_id],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -358,7 +309,7 @@ function deleteMessage(connection, message_id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`DELETE FROM messages WHERE id = ?`, `DELETE FROM messages WHERE id = ?`,
[message_id], // Use parameterized query [message_id],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -373,7 +324,7 @@ function deleteChannelMessages(connection, channel_id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`DELETE FROM messages WHERE channel_id = ?`, `DELETE FROM messages WHERE channel_id = ?`,
[channel_id], // Use parameterized query [channel_id],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -384,11 +335,16 @@ function deleteChannelMessages(connection, channel_id) {
}); });
} }
function deleteUserMessages(connection, user_id) { function getMessages(connection, channel_id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`DELETE FROM messages WHERE user_id = ?`, `SELECT messages.id, user_id, username, content, date, channels.name AS channel_name, has_replies
[user_id], // Use parameterized query FROM messages
JOIN users ON messages.user_id = users.id
JOIN channels ON messages.channel_id = channels.id
WHERE channel_id = ? AND reply_to_id IS NULL
ORDER BY date DESC`,
[channel_id],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -399,11 +355,106 @@ function deleteUserMessages(connection, user_id) {
}); });
} }
function getMessage(connection, message_id) {
return new Promise((resolve, reject) => {
connection.query(
`SELECT messages.id, user_id, username, content, date, channels.name AS channel_name, has_replies
FROM messages
JOIN users ON messages.user_id = users.id
JOIN channels ON messages.channel_id = channels.id
WHERE messages.id = ?`,
[message_id],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function getLastMessages(connection) {
return new Promise((resolve, reject) => {
connection.query(
`SELECT messages.id, user_id, username, content, date, channels.name AS channel_name, has_replies
FROM messages
JOIN users ON messages.user_id = users.id
JOIN channels ON messages.channel_id = channels.id
ORDER BY date DESC LIMIT 5`,
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function getUserLastMessages(connection, username) {
return new Promise((resolve, reject) => {
connection.query(
`SELECT messages.id, user_id, username, content, date, channels.name AS channel_name, has_replies
FROM messages
JOIN users ON messages.user_id = users.id
JOIN channels ON messages.channel_id = channels.id
WHERE username = ?
ORDER BY date DESC LIMIT 5`,
[username],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function getMessageReplies(connection, message_id) {
return new Promise((resolve, reject) => {
connection.query(
`SELECT messages.id, user_id, username, content, date, channels.name AS channel_name, has_replies
FROM messages
JOIN users ON messages.user_id = users.id
JOIN channels ON messages.channel_id = channels.id
WHERE reply_to_id = ?`,
[message_id],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function setHasReplies(connection, message_id, has_replies) {
return new Promise((resolve, reject) => {
connection.query(
`UPDATE messages SET has_replies = ? WHERE id = ?`,
[has_replies, message_id],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
// +---------------------------+
// | Mentions |
// +---------------------------+
function addMention(connection, message_id, user_id) { function addMention(connection, message_id, user_id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`INSERT INTO mentions (message_id, user_id) VALUES (?, ?)`, `INSERT INTO mentions (message_id, user_id) VALUES (?, ?)`,
[message_id, user_id], // Use parameterized query [message_id, user_id],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -420,7 +471,7 @@ function getMentions(connection, message_id) {
`SELECT users.username FROM mentions `SELECT users.username FROM mentions
JOIN users ON mentions.user_id = users.id JOIN users ON mentions.user_id = users.id
WHERE message_id = ?`, WHERE message_id = ?`,
[message_id], // Use parameterized query [message_id],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -431,11 +482,15 @@ function getMentions(connection, message_id) {
}); });
} }
function deleMentions(connection, message_id) { // +---------------------------+
// | Emojis |
// +---------------------------+
function addEmoji(connection, name, file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`DELETE FROM mentions WHERE message_id = ?`, `INSERT INTO emojis (name, file) VALUES (?, ?)`,
[message_id], // Use parameterized query [name, file],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -446,27 +501,11 @@ function deleMentions(connection, message_id) {
}); });
} }
function deleteChannelMentions(connection, channel_id) { function deleteEmoji(connection, id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`DELETE FROM mentions WHERE message_id IN `DELETE FROM emojis WHERE id = ?`,
(SELECT id FROM messages WHERE channel_id = ?)`, [id],
[channel_id], // Use parameterized query
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function deleteUserMentions(connection, user_id) {
return new Promise((resolve, reject) => {
connection.query(
`DELETE FROM mentions WHERE user_id = ?`,
[user_id], // Use parameterized query
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -491,41 +530,11 @@ function getEmojis(connection) {
}); });
} }
function addEmoji(connection, name, file) {
return new Promise((resolve, reject) => {
connection.query(
`INSERT INTO emojis (name, file) VALUES (?, ?)`,
[name, file], // Use parameterized query
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function deleteEmoji(connection, id) {
return new Promise((resolve, reject) => {
connection.query(
`DELETE FROM emojis WHERE id = ?`,
[id], // Use parameterized query
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
function getEmoji(connection, id) { function getEmoji(connection, id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT * FROM emojis WHERE id = ?`, `SELECT * FROM emojis WHERE id = ?`,
[id], // Use parameterized query [id],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -540,7 +549,7 @@ function getEmojiByName(connection, name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT * FROM emojis WHERE name = ?`, `SELECT * FROM emojis WHERE name = ?`,
[name], // Use parameterized query [name],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -555,7 +564,7 @@ function searchEmojis(connection, search) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT * FROM emojis WHERE name LIKE ? LIMIT 5`, `SELECT * FROM emojis WHERE name LIKE ? LIMIT 5`,
[`%${search}%`], // Use parameterized query [`%${search}%`],
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -567,39 +576,43 @@ function searchEmojis(connection, search) {
} }
module.exports = { module.exports = {
getUsers,
getConnection, getConnection,
addUser,
deleteUser,
getUsers,
getUser, getUser,
searchUser, searchUser,
getUserByUsername, getUserByUsername,
addUser,
deleteUser,
setUserPfp, setUserPfp,
setUserUsername, setUserUsername,
setUserPassword, setUserPassword,
getUserLastMessages,
addChannel,
deleteChannel,
getChannels, getChannels,
getChannel,
getActiveChannels, getActiveChannels,
getNewChannels, getNewChannels,
searchChannels, searchChannels,
getChannel,
addChannel,
deleteChannel,
getMessages,
getLastMessages,
getMessage,
addMessage, addMessage,
addReply,
deleteMessage, deleteMessage,
deleteChannelMessages, deleteChannelMessages,
deleteUserMessages, getMessages,
getMessage,
getUserLastMessages,
getLastMessages,
getMessageReplies,
setHasReplies,
addMention, addMention,
getMentions, getMentions,
deleMentions,
deleteUserMentions,
deleteChannelMentions,
getEmojis,
addEmoji, addEmoji,
deleteEmoji, deleteEmoji,
getEmojis,
getEmoji, getEmoji,
getEmojiByName, getEmojiByName,
searchEmojis, searchEmojis,

View file

@ -1,14 +1,21 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { Message, User, Channel } from "../types"; import { Message, User, Channel, Users, Emojis } from "../types";
import { useState, createRef, useEffect } from "react";
import axios from "axios";
import "../styles/MessageComponent.css"; import "../styles/MessageComponent.css";
export default function MessageComponent({ message, user, channel, deleteMessage }: { export default function MessageComponent({ message, user, channel }: {
message: Message; message: Message;
user: User | undefined; user: User | undefined;
channel: Channel | undefined; channel: Channel | undefined;
deleteMessage: ((messageId: number) => void) | undefined;
}) { }) {
const [reply, setReply] = useState(false);
const token = localStorage.getItem("token");
const [replyContent, setReplyContent] = useState("");
const [searchedUsers, setSearchedUsers] = useState<Users>([]);
const [searchedEmojis, setSearchedEmojis] = useState<Emojis>([]);
const ref = createRef<HTMLInputElement>();
function getMessageArray(message: Message) { function getMessageArray(message: Message) {
const array = [] const array = []
@ -39,6 +46,70 @@ export default function MessageComponent({ message, user, channel, deleteMessage
return array.filter((word) => word !== ''); return array.filter((word) => word !== '');
} }
function handleReply(e: React.FormEvent<HTMLFormElement>, messageId: number) {
e.preventDefault();
axios
.post(`/api/messages/${messageId}/reply`, {
token: token,
message: replyContent,
})
.then(() => {
setReplyContent("");
setReply(false);
setSearchedUsers([]);
setSearchedEmojis([]);
})
.catch((err) => {
console.error(err.response.data);
});
}
function handleDelete() {
if (!window.confirm("Are you sure you want to delete this message?")) {
return;
}
axios
.post(`/api/messages/${message.id}/delete`, {
token: token,
})
.catch((err) => {
console.error(err.response.data);
});
}
useEffect(() => {
const words = replyContent.toString().split(" ");
const lastWord = words[words.length - 1];
if (lastWord && lastWord.startsWith("@")) {
const username = lastWord.slice(1);
axios
.get(`/api/searchuser?search=${username}`)
.then((res) => {
setSearchedUsers(res.data);
})
.catch((err) => {
console.error(err.response.data);
});
} else {
setSearchedUsers([]);
}
if (lastWord && lastWord.startsWith(":")) {
const emojiName = lastWord.slice(1);
axios
.get(`/api/searchemojis?search=${emojiName}`)
.then((res) => {
setSearchedEmojis(res.data);
})
.catch((err) => {
console.error(err.response.data);
});
} else {
setSearchedEmojis([]);
}
}, [replyContent]);
return ( return (
<div key={message.id} className="message"> <div key={message.id} className="message">
<div className="message-content"> <div className="message-content">
@ -70,13 +141,81 @@ export default function MessageComponent({ message, user, channel, deleteMessage
<img src="/le_poisson_steve.png" alt="Le poisson steve" className="fish" /> <img src="/le_poisson_steve.png" alt="Le poisson steve" className="fish" />
)} )}
</div> </div>
{channel && deleteMessage ? ( {channel?.owner_id == user?.id || user?.admin == 1 || user?.username == message.username ? (
user?.id === message.user_id || user?.id === channel?.owner_id || user?.admin === 1) && ( user?.id === message.user_id || user?.id === channel?.owner_id || user?.admin === 1) && (
<button onClick={() => {deleteMessage(message.id)}}>Delete</button> <button onClick={handleDelete}>Delete</button>
) : ( ) : (
<p>In <Link to={`/c/${message.channel_name}`}>{message.channel_name}</Link></p> <p>In <Link to={`/c/${message.channel_name}`}>{message.channel_name}</Link></p>
) )}
} {channel && user && !reply && (
<button onClick={() => setReply(true)}>Reply</button>
)}
{reply && (
<form onSubmit={(e) => handleReply(e, message.id)} className="message-form">
<input
type="text"
placeholder="Message"
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
ref={ref}
/>
<button type="submit">Send</button>
{searchedUsers.length > 0 && (
<div className="mentions">
{searchedUsers.map((user) => (
<div key={user.id} className="mention">
<Link to={`/u/${user.username}`}>{user.username}</Link>
<button
type="button"
onClick={() => {
setReplyContent(
replyContent.split(" ").slice(0, -1).join(" ") + " @" + user.username + " "
);
setSearchedUsers([]);
ref.current?.focus();
}}
>
Mention
</button>
</div>
))}
</div>
)}
{searchedEmojis.length > 0 && (
<div className="emojis">
{searchedEmojis.map((emoji) => (
<div key={emoji.id} className="search-emoji">
<img src={`/api/emojis/${emoji.name}`} alt={emoji.name} className="emojis-emoji" />
<span>:{emoji.name}:</span>
<button
type="button"
onClick={() => {
setReplyContent(
replyContent.split(" ").slice(0, -1).join(" ") + " :" + emoji.name + ": "
);
setSearchedEmojis([]);
ref.current?.focus();
}}
>
Add Emoji
</button>
</div>
))}
</div>
)}
<button onClick={() => setReply(false)}>Cancel</button>
</form>
)}
{message.has_replies == true && (
<div className="message-replies">
{message.replies.map((reply) => (
<MessageComponent message={reply} user={user} channel={channel} key={reply.id} />
))}
</div>
)}
{!channel && (
<p>in: <Link to={`/c/${message.channel_name}`}>{message.channel_name}</Link></p>
)}
</div> </div>
); );
} }

View file

@ -34,14 +34,6 @@ export default function ChannelPage({socket}: {socket: WebSocket}) {
}); });
} }
function deleteMessage(message_id: number) {
axios
.post(`/api/channels/${name}/messages/delete`, { token, message_id })
.catch((err) => {
console.error(err.response);
});
}
function purgeChannel() { function purgeChannel() {
if (!window.confirm(`Are you sure you want to purge ${channel?.name}?`)) { if (!window.confirm(`Are you sure you want to purge ${channel?.name}?`)) {
return; return;
@ -261,7 +253,6 @@ export default function ChannelPage({socket}: {socket: WebSocket}) {
message={message} message={message}
user={user} user={user}
channel={channel} channel={channel}
deleteMessage={deleteMessage}
/> />
))} ))}
{messages.length > maxMessageToShown && ( {messages.length > maxMessageToShown && (

View file

@ -165,7 +165,6 @@ export default function Home({socket}: {socket: WebSocket}) {
message={message} message={message}
user={user} user={user}
channel={undefined} channel={undefined}
deleteMessage={undefined}
/> />
))} ))}
</div> </div>

View file

@ -1,7 +1,12 @@
.message { .message {
width: 95%; width: 97%;
border: 1px solid #270722; border: 1px solid #270722;
padding: 10px; padding: 10px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: start;
gap: 5px;
} }
.message-user-pfp { .message-user-pfp {
@ -37,6 +42,15 @@
max-height: 1em; max-height: 1em;
} }
.message-replies {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 10px;
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.message { .message {
border: 1px solid #a678af; border: 1px solid #a678af;

View file

@ -34,6 +34,8 @@ export type Message = {
channel_id: number channel_id: number
channel_name: string channel_name: string
mentions: Mentions mentions: Mentions
replies: Messages
has_replies: boolean
} }
export type Messages = Message[] export type Messages = Message[]