pending messages are displayed on the gray
This commit is contained in:
41
client/package-lock.json
generated
41
client/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<ContactsProps[]> {
|
||||
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: [] };
|
||||
}
|
||||
|
||||
@@ -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<React.SetStateAction<string | null>>;
|
||||
updateContactStatus: (contactObj: ContactsProps, read: true) => void;
|
||||
setMessages: React.Dispatch<React.SetStateAction<MessagesProps[]>>;
|
||||
setMessages: React.Dispatch<React.SetStateAction<ChatMessages[]>>;
|
||||
currentContact: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -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<React.SetStateAction<ChatMessages[]>>;
|
||||
messages: ChatMessages[];
|
||||
};
|
||||
|
||||
const MessageForm = ({ contact }: MessageFormProps) => {
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
watch,
|
||||
formState: {},
|
||||
} = useForm<Input>({ mode: 'onChange' });
|
||||
const MessageForm = ({ contact, setMessages }: MessageFormProps) => {
|
||||
const { username }: { username: string } = useOutletContext();
|
||||
const { register, handleSubmit, reset, watch } = useForm<Input>({
|
||||
mode: 'onChange',
|
||||
});
|
||||
|
||||
const message = watch('message', '');
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
@@ -37,20 +39,64 @@ const MessageForm = ({ contact }: MessageFormProps) => {
|
||||
adjustHeight();
|
||||
}, [message, adjustHeight]);
|
||||
|
||||
// Sending message
|
||||
const submitMessage: SubmitHandler<Input> = (data) => {
|
||||
if (!data.message) {
|
||||
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: '' });
|
||||
// Sending message
|
||||
const submitMessage: SubmitHandler<Input> = (data) => {
|
||||
if (!data.message) return;
|
||||
|
||||
if (!socket) {
|
||||
console.error('Socket not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
// Handle Enter and Ctrl+Enter
|
||||
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 in textarea
|
||||
const handleKeyPress: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const { username }: UsernameType = useOutletContext();
|
||||
const { username }: { username: string } = useOutletContext();
|
||||
const prevScrollHeight = useRef(0);
|
||||
const [isFetchingHistory, setIsFetchingHistory] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(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) => (
|
||||
<li
|
||||
className="whitespace-pre-wrap ml-2 rounded p-1 hover:bg-gray-800"
|
||||
key={msg.message_id}
|
||||
className={`whitespace-pre-wrap ml-2 rounded p-1 ${
|
||||
msg.pending
|
||||
? 'text-gray-400' // Gray style for pending messages
|
||||
: 'hover:bg-gray-800'
|
||||
}`}
|
||||
key={msg.message_id || msg.tempId}
|
||||
>
|
||||
{msg.message_id} {msg.sender}: {msg.message}
|
||||
{msg.tempId} {msg.message_id} {msg.sender}: {msg.message}
|
||||
</li>
|
||||
));
|
||||
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<div className="flex-shrink-0 mb-2 mt-0">
|
||||
{currentContact && currentContact.length >= 4 ? (
|
||||
<MessageForm contact={currentContact} />
|
||||
<MessageForm
|
||||
contact={currentContact}
|
||||
setMessages={setMessages}
|
||||
messages={messages}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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', {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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!" });
|
||||
});
|
||||
|
||||
|
||||
@@ -62,14 +62,21 @@ 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;
|
||||
}
|
||||
|
||||
try {
|
||||
const insertedMessage = await insertMessage(sender, recipient, message);
|
||||
if (!insertedMessage) {
|
||||
callback({ status: "error", message: "Failed to insert message" });
|
||||
return;
|
||||
}
|
||||
|
||||
const insertedMessage = await insertMessage(username, recipient, message);
|
||||
const { message_id, timestamp } = insertedMessage;
|
||||
console.log("(socket) received from chat message", msg);
|
||||
|
||||
@@ -86,6 +93,12 @@ function initializeSocket(io) {
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user