From 23b6f6793d9ac7b1d0330cd4263caab43b0cbc2b Mon Sep 17 00:00:00 2001 From: slawk0 Date: Tue, 19 Nov 2024 22:49:55 +0100 Subject: [PATCH] pending messages are displayed on the gray --- client/package-lock.json | 41 ++++++++--- client/package.json | 1 + client/src/api/contactsApi.tsx | 10 +-- client/src/components/chat/ContactsList.tsx | 11 +-- client/src/components/chat/MessageForm.tsx | 82 ++++++++++++++++----- client/src/components/chat/MessagesArea.tsx | 25 +++---- client/src/pages/Chat.tsx | 11 ++- client/src/socket/socket.tsx | 20 +++-- server/server.js | 3 + server/socket/socket.js | 47 +++++++----- 10 files changed, 163 insertions(+), 88 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index cddb895..1098187 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,6 +13,7 @@ "@types/socket.io-client": "^1.4.36", "axios": "^1.7.7", "js-cookie": "^3.0.5", + "nanoid": "^5.0.8", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", @@ -889,9 +890,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", - "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2045,9 +2046,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -3249,9 +3250,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.8.tgz", + "integrity": "sha512-TcJPw+9RV9dibz1hHUzlLVy8N4X9TnwirAjrU08Juo6BNKggzVfP2ZJ/3ZUSq15Xl5i85i+Z89XBO90pB2PghQ==", "funding": [ { "type": "github", @@ -3260,10 +3261,10 @@ ], "license": "MIT", "bin": { - "nanoid": "bin/nanoid.cjs" + "nanoid": "bin/nanoid.js" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^18 || >=20" } }, "node_modules/natural-compare": { @@ -3624,6 +3625,24 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/client/package.json b/client/package.json index cd790c9..a49c158 100644 --- a/client/package.json +++ b/client/package.json @@ -15,6 +15,7 @@ "@types/socket.io-client": "^1.4.36", "axios": "^1.7.7", "js-cookie": "^3.0.5", + "nanoid": "^5.0.8", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.53.0", diff --git a/client/src/api/contactsApi.tsx b/client/src/api/contactsApi.tsx index 9a55957..9c93cb8 100644 --- a/client/src/api/contactsApi.tsx +++ b/client/src/api/contactsApi.tsx @@ -1,16 +1,10 @@ import { axiosClient } from '../App.tsx'; +import { ChatMessages } from '../pages/Chat.tsx'; type ContactsProps = { usernamecontact: string; read: boolean; }; -type MessagesProps = { - sender: string; - message: string; - recipient: string; - message_id: number; - timestamp: string; -}; export async function getContactsList(): Promise { try { const response = await axiosClient.get(`/api/chat/contacts`); @@ -51,7 +45,7 @@ export async function getMessages( contact: string | null, cursor: number | null = 0, limit: number = 50, -): Promise<{ messages: MessagesProps[] }> { +): Promise<{ messages: ChatMessages[] }> { if (contact === null || cursor === null) { return { messages: [] }; } diff --git a/client/src/components/chat/ContactsList.tsx b/client/src/components/chat/ContactsList.tsx index 92a7c16..62d2d0b 100644 --- a/client/src/components/chat/ContactsList.tsx +++ b/client/src/components/chat/ContactsList.tsx @@ -1,17 +1,10 @@ import { useEffect } from 'react'; import { deleteContact, getContactsList } from '../../api/contactsApi.tsx'; - +import { ChatMessages } from '../../pages/Chat.tsx'; type ContactsProps = { usernamecontact: string; read: boolean; }; -type MessagesProps = { - sender: string; - message: string; - recipient: string; - message_id: number; - timestamp: string; -}; type ContactsListProps = { InitializeContact: (contact: string) => void; @@ -19,7 +12,7 @@ type ContactsListProps = { contactsList: ContactsProps[]; setCurrentContact: React.Dispatch>; updateContactStatus: (contactObj: ContactsProps, read: true) => void; - setMessages: React.Dispatch>; + setMessages: React.Dispatch>; currentContact: string | null; }; diff --git a/client/src/components/chat/MessageForm.tsx b/client/src/components/chat/MessageForm.tsx index 058211e..14a53bb 100644 --- a/client/src/components/chat/MessageForm.tsx +++ b/client/src/components/chat/MessageForm.tsx @@ -1,24 +1,26 @@ -import { useRef, useEffect, useCallback } from 'react'; +import { useRef, useCallback, useEffect } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import type { KeyboardEventHandler } from 'react'; -import { sendMessage } from '../../socket/socket.tsx'; - +import { socket } from '../../socket/socket.tsx'; +import { customAlphabet } from 'nanoid'; +import { useOutletContext } from 'react-router-dom'; +import { ChatMessages } from '../../pages/Chat.tsx'; +const nanoid = customAlphabet('1234567890', 5); type Input = { message: string; }; type MessageFormProps = { contact: string; + setMessages: React.Dispatch>; + messages: ChatMessages[]; }; -const MessageForm = ({ contact }: MessageFormProps) => { - const { - register, - handleSubmit, - reset, - watch, - formState: {}, - } = useForm({ mode: 'onChange' }); +const MessageForm = ({ contact, setMessages }: MessageFormProps) => { + const { username }: { username: string } = useOutletContext(); + const { register, handleSubmit, reset, watch } = useForm({ + mode: 'onChange', + }); const message = watch('message', ''); const textareaRef = useRef(null); @@ -37,20 +39,64 @@ const MessageForm = ({ contact }: MessageFormProps) => { adjustHeight(); }, [message, adjustHeight]); + if (!socket) { + console.error('Socket not initialized'); + return; + } + // Sending message const submitMessage: SubmitHandler = (data) => { - if (!data.message) { + if (!data.message) return; + + if (!socket) { + console.error('Socket not initialized'); return; } - // for (let i = 0; i <= 200; i++) { - // sendMessage(i, contact); - // } - sendMessage(data.message.trim(), contact); - reset({ message: '' }); + const tempId: string = nanoid(); // Temporary ID for unsent message + + const tempMessage: ChatMessages = { + sender: username, + message: data.message.trim(), + recipient: contact, + message_id: 0, // Set to 0 because of key={msg.message_id || msg.tempId} in messages list + pending: true, + tempId: tempId, + }; + + setMessages((prevMessages) => [...prevMessages, tempMessage]); // Display as gray + + socket.emit( + 'chat message', + { + message: data.message.trim(), + recipient: contact, + tempId: tempId, + }, + (response: { status: string; message_id: number; tempId: string }) => { + if (response.status === 'ok') { + setMessages((prevMessages) => + prevMessages.map((msg) => + msg.tempId === tempId + ? { ...msg, pending: false, message_id: response.message_id } + : msg, + ), + ); + + reset({ message: '' }); + } + console.log(response.status, response.tempId); + }, + ); + + console.log('sent message: ', { + message: data.message.trim(), + recipient: contact, + tempId: tempId, + }); }; - // Handle Enter and Ctrl+Enter + // Handle Enter and Ctrl+Enter in textarea const handleKeyPress: KeyboardEventHandler = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); diff --git a/client/src/components/chat/MessagesArea.tsx b/client/src/components/chat/MessagesArea.tsx index 1eb9250..6c54c74 100644 --- a/client/src/components/chat/MessagesArea.tsx +++ b/client/src/components/chat/MessagesArea.tsx @@ -1,17 +1,9 @@ import { useEffect, useRef, useState } from 'react'; import { socket } from '../../socket/socket.tsx'; import { useOutletContext } from 'react-router-dom'; -import { UsernameType } from '../../utils/ProtectedRoutes.tsx'; import { sendContact } from '../../api/contactsApi.tsx'; import LoadingWheel from './LoadingWheel.tsx'; - -type ChatMessages = { - sender: string; - message: string; - recipient: string; - message_id: number; - timestamp: string; -}; +import { ChatMessages } from '../../pages/Chat.tsx'; type ContactProps = { usernamecontact: string; @@ -45,7 +37,7 @@ function MessagesArea({ }: MessagesAreaProps) { const messagesEndRef = useRef(null); const containerRef = useRef(null); - const { username }: UsernameType = useOutletContext(); + const { username }: { username: string } = useOutletContext(); const prevScrollHeight = useRef(0); const [isFetchingHistory, setIsFetchingHistory] = useState(false); const [isLoading, setIsLoading] = useState(false); @@ -92,7 +84,7 @@ function MessagesArea({ ); console.log('changed status to false for: ', msg.sender); } - if (msg.sender == currentContact || msg.sender == username) { + if (msg.sender == currentContact) { messageHandler(msg); } }); @@ -107,7 +99,6 @@ function MessagesArea({ currentContainer.removeEventListener('scroll', handleScroll); } socket.off('chat message'); - socket.off('historical'); }; }, [currentContact, username, setContactsList, updateContactStatus]); @@ -127,10 +118,14 @@ function MessagesArea({ const messageList = messages.map((msg: ChatMessages) => (
  • - {msg.message_id} {msg.sender}: {msg.message} + {msg.tempId} {msg.message_id} {msg.sender}: {msg.message}
  • )); diff --git a/client/src/pages/Chat.tsx b/client/src/pages/Chat.tsx index 0c3b914..ca130a5 100644 --- a/client/src/pages/Chat.tsx +++ b/client/src/pages/Chat.tsx @@ -9,12 +9,13 @@ import { initializeSocket } from '../socket/socket.tsx'; import Cookies from 'js-cookie'; import { getMessages, setContactStatus } from '../api/contactsApi.tsx'; -type ChatMessages = { +export type ChatMessages = { sender: string; message: string; recipient: string; message_id: number; - timestamp: string; + tempId: string; + pending: boolean; }; type ContactsProps = { usernamecontact: string; @@ -162,7 +163,11 @@ function Chat() {
    {currentContact && currentContact.length >= 4 ? ( - + ) : null}
    diff --git a/client/src/socket/socket.tsx b/client/src/socket/socket.tsx index bf2e7a4..39b4ec2 100644 --- a/client/src/socket/socket.tsx +++ b/client/src/socket/socket.tsx @@ -1,7 +1,6 @@ import io from 'socket.io-client'; import Socket = SocketIOClient.Socket; let socket: Socket | null = null; - function initializeSocket(token: string): Socket | null { if (!socket && token) { socket = io({ @@ -21,20 +20,27 @@ function initializeSocket(token: string): Socket | null { return socket; } -function sendMessage(message: string, recipient: string) { +function sendMessage(message: string, recipient: string, tempId: string) { if (!socket) { console.error('Socket not initialized'); return; } - - socket.emit('chat message', { - message: message, - recipient: recipient, - }); + socket.emit( + 'chat message', + { + message: message, + recipient: recipient, + tempId: tempId, + }, + (response: { status: string; tempId: string }) => { + console.log(response.status, response.tempId); + }, + ); console.log('sent message: ', { message: message, recipient: recipient, + tempId: tempId, }); } diff --git a/server/server.js b/server/server.js index 6bb55f8..ef3ac38 100644 --- a/server/server.js +++ b/server/server.js @@ -253,6 +253,9 @@ app.get("/api/chat/messages/:contact", authorizeUser, async (req, res) => { }); app.post("/api/chat/sendmessage", authorizeUser, async (req, res) => { + const message = req.body.message?.trim(); + //TODO filter for invalid characters in message + return res.status(500).json({ message: "HUJ!" }); }); diff --git a/server/socket/socket.js b/server/socket/socket.js index 00bb61d..06f47cb 100644 --- a/server/socket/socket.js +++ b/server/socket/socket.js @@ -62,30 +62,43 @@ function initializeSocket(io) { } socket.join(username); // join username room - socket.on("chat message", async (msg) => { + socket.on("chat message", async (msg, callback) => { const { message, recipient } = msg; const sender = username; if (!message || !recipient) { + callback({ status: "error", message: "Invalid message or recipient" }); return; } - const insertedMessage = await insertMessage(username, recipient, message); - const { message_id, timestamp } = insertedMessage; - console.log("(socket) received from chat message", msg); + try { + const insertedMessage = await insertMessage(sender, recipient, message); + if (!insertedMessage) { + callback({ status: "error", message: "Failed to insert message" }); + return; + } - io.to(username).to(recipient).emit("chat message", { - sender, - message, - recipient, - message_id, - }); - console.log("(socket) sent on 'chat message' socket: ", { - sender, - message, - recipient, - timestamp, - message_id, - }); + const { message_id, timestamp } = insertedMessage; + console.log("(socket) received from chat message", msg); + + io.to(username).to(recipient).emit("chat message", { + sender, + message, + recipient, + message_id, + }); + console.log("(socket) sent on 'chat message' socket: ", { + sender, + message, + recipient, + timestamp, + message_id, + }); + + callback({ status: "ok", tempId: msg.tempId }); + } catch (e) { + console.error("(socket) Failed to insert message ", e); + callback({ status: "error", message: "Internal server error" }); + } }); socket.on("disconnect", (reason) => {