prepared backend for pagination, redirect to /chat if logged in, fixed messages from contact not appearing in real time,

This commit is contained in:
slawk0
2024-11-09 16:51:41 +01:00
parent b99e148841
commit 8907650fc0
13 changed files with 148 additions and 78 deletions

View File

@@ -51,12 +51,16 @@ export async function sendContact(contact: string) {
export async function getMessages(
contact: string | null,
): Promise<MessagesProps[]> {
cursor: number | null = 0,
limit: number = 50,
): Promise<{ messages: MessagesProps[]; nextCursor: number | null }> {
if (contact == null) {
return [];
return { messages: [], nextCursor: null };
}
try {
const response = await axiosClient.get(`/api/chat/messages/${contact}`);
const response = await axiosClient.get(
`/api/chat/messages/${contact}?limit=${limit}&cursor=${cursor}`,
);
console.log(response.data);
return response.data;
} catch (e) {

View File

@@ -10,15 +10,12 @@ type ContactsProps = {
read: boolean;
};
type InitializeContactsProps = {
type ContactFormProps = {
InitializeContact: (contact: string) => void;
setContactsList: React.Dispatch<React.SetStateAction<ContactsProps[]>>;
};
function ContactForm({
InitializeContact,
setContactsList,
}: InitializeContactsProps) {
function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
const { register, handleSubmit, reset } = useForm<Input>();
const submitContact: SubmitHandler<Input> = (data) => {

View File

@@ -6,15 +6,6 @@ function ContactProfile({ contact }: Contact) {
return (
<>
<div className="m-2 flex items-center">
{/*// TODO contact profile img*/}
{/*<div className="m-3 rounded-full w-10 h-10 overflow-hidden ">*/}
{/* <img*/}
{/* className="w-full h-full object-cover"*/}
{/* src={zdjecie}*/}
{/* alt="profile image"*/}
{/* draggable={false}*/}
{/* />*/}
{/*</div>*/}
<div className="text-center text-gray-200 flex">
<img className="w-4 mr-2 invert" src={profile} alt="profile img" />
<p>{contact ? contact : null}</p>

View File

@@ -6,6 +6,13 @@ 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;
@@ -13,6 +20,8 @@ type ContactsListProps = {
contactsList: ContactsProps[];
setCurrentContact: React.Dispatch<React.SetStateAction<string | null>>;
updateContactStatus: (contactObj: ContactsProps, read: true) => void;
setMessages: React.Dispatch<React.SetStateAction<MessagesProps[]>>;
currentContact: string | null;
};
function ContactsList({
@@ -21,6 +30,8 @@ function ContactsList({
setContactsList,
setCurrentContact,
updateContactStatus,
setMessages,
currentContact,
}: ContactsListProps) {
function contactHandler(contactsList: ContactsProps) {
setContactsList((prevContacts) => {
@@ -49,9 +60,11 @@ function ContactsList({
return;
}
fetchContacts().catch((e) =>
console.error('Failed to fetch contacts: ', e),
);
if (!currentContact === null) {
fetchContacts().catch((e) =>
console.error('Failed to fetch contacts: ', e),
);
}
return () => {
if (!socket) {
@@ -63,6 +76,7 @@ function ContactsList({
function removeContact(usernamecontact: string) {
deleteContact(usernamecontact); // Remove contact from server
setMessages([]);
// Remove contact on client
setContactsList((prevContacts) =>
prevContacts.filter(

View File

@@ -21,7 +21,12 @@ function messageForm({ contact }: MessaFormProps) {
if (!data.message) {
return;
}
// for (let i = 0; i < 100; i++) {
// let ii = i.toString();
// sendMessage(ii, contact);
// }
sendMessage(data.message, contact);
reset({ message: '' });
};

View File

@@ -1,7 +1,8 @@
import { useEffect, useRef } from 'react';
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, setContactStatus } from '../../api/contactsApi.tsx';
type ChatMessages = {
sender: string;
@@ -15,21 +16,31 @@ type ContactProps = {
usernamecontact: string;
read: boolean;
};
type ContactsProps = {
usernamecontact: string;
read: boolean;
};
type MessagesAreaProps = {
messages: ChatMessages[];
setMessages: React.Dispatch<React.SetStateAction<ChatMessages[]>>;
currentContact: string | null;
setContactStatus: (contact: string, read: boolean) => void;
updateStatus: (contact: ContactProps, read: boolean) => void;
updateContactStatus: (contact: ContactProps, read: boolean) => void;
setCursor: React.Dispatch<React.SetStateAction<number | null>>;
cursor: number | null;
messageHandler: (msg: ChatMessages) => void;
setContactsList: React.Dispatch<React.SetStateAction<ContactsProps[]>>;
};
function MessagesArea({
messages,
setMessages,
currentContact,
setContactStatus,
updateStatus,
updateContactStatus,
setCursor,
cursor,
setContactsList,
}: MessagesAreaProps) {
const messagesEndRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
@@ -67,22 +78,29 @@ function MessagesArea({
socket.on('chat message', (msg: ChatMessages) => {
console.log('Received message: ', msg);
if (msg.sender !== currentContact && msg.sender !== username) {
setContactStatus(msg.sender, false);
updateStatus({ usernamecontact: msg.sender, read: false }, false);
console.log('changed status to false');
// Add contact to list
setContactsList((prevContacts) => {
if (!prevContacts.some((c) => c.usernamecontact === msg.sender)) {
sendContact(msg.sender); // Send contact to server
return [
...prevContacts,
{ usernamecontact: msg.sender, read: false },
];
}
return prevContacts;
});
// Set contact status on client and server site
updateContactStatus(
{ usernamecontact: msg.sender, read: false },
false,
);
console.log('changed status to false', currentContact);
}
if (msg.sender == currentContact || msg.sender == username) {
messageHandler(msg);
}
});
socket.on('historical messages', (msg: ChatMessages[]) => {
console.log('Received historical messages: ', msg);
msg.forEach((historicalMsg) => {
messageHandler(historicalMsg);
});
});
return () => {
if (!socket) {
console.error('Socket not initialized');
@@ -95,7 +113,7 @@ function MessagesArea({
socket.off('chat message');
socket.off('historical');
};
}, []);
}, [currentContact, username, setContactsList, updateContactStatus]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView();

View File

@@ -1,4 +1,4 @@
import zdjecie from '../../../assets/walter.png';
import zdjecie from '../../../assets/profile.svg';
import logoutIcon from '../../../assets/logout.svg';
import Cookies from 'js-cookie';
import { useOutletContext } from 'react-router-dom';
@@ -23,8 +23,8 @@ function UserProfile() {
className="flex items-center cursor-pointer hs-dropdown-toggle"
onClick={toggleDropdown}
>
<div className="m-3 rounded-full w-12 h-12 overflow-hidden">
<img src={zdjecie} alt="Profile image" className="w-12 h-12" />
<div className="flex items-center justify-center m-3 w-12 h-12 overflow-hidden">
<img src={zdjecie} alt="Profile image" className="w-8 h-8 invert" />
</div>
<div className="text-gray-200">
<p>{username}</p>

View File

@@ -28,7 +28,8 @@ type ContactObjProps = {
function Chat() {
const [contactsList, setContactsList] = useState<ContactsProps[]>([]);
const [currentContact, setCurrentContact] = useState<string | null>('');
const [currentContact, setCurrentContact] = useState<string | null>(null);
const [cursor, setCursor] = useState<number | null>(0);
const [messages, setMessages] = useState<ChatMessages[]>([]);
useEffect(() => {
@@ -45,7 +46,9 @@ function Chat() {
function InitializeContact(newContact: string) {
setMessages([]); // Clear messages from previous contact
localStorage.setItem('contact', newContact); // Set current contact in localstorage
setCurrentContact(newContact); // Set state for current user (used in contactProfile.tsx)
setCurrentContact(newContact); // Set state for current user (used in contactProfile.tsx and for filtering messages)
console.log('Initialized contact');
fetchMessages(newContact).catch((e) =>
console.error('Failed to fetch messages: ', e),
);
@@ -59,12 +62,9 @@ function Chat() {
}
const fetchMessages = async (currentContact: string | null) => {
if (!currentContact) {
return;
}
const messages = await getMessages(currentContact);
console.log('Fetching messages for: ', currentContact);
messages.forEach(messageHandler);
messages.messages.forEach(messageHandler);
};
function messageHandler(msg: ChatMessages) {
setMessages((prevMessages) => {
@@ -81,7 +81,7 @@ function Chat() {
prevContacts.map((contact) => {
if (contact.usernamecontact === contactObj.usernamecontact) {
if (!contactObj.read) {
setContactStatus(contactObj.usernamecontact, true);
setContactStatus(contactObj.usernamecontact, read);
}
return { ...contact, read: read };
} else {
@@ -106,6 +106,8 @@ function Chat() {
setContactsList={setContactsList}
setCurrentContact={setCurrentContact}
updateContactStatus={updateContactStatus}
setMessages={setMessages}
currentContact={currentContact}
/>
<hr />
<UserProfile />
@@ -122,7 +124,11 @@ function Chat() {
setMessages={setMessages}
currentContact={currentContact}
setContactStatus={setContactStatus}
updateStatus={updateContactStatus}
updateContactStatus={updateContactStatus}
setCursor={setCursor}
cursor={cursor}
messageHandler={messageHandler}
setContactsList={setContactsList}
/>
</div>
<div className="flex-shrink-0 mb-2 mt-0">

View File

@@ -1,9 +1,10 @@
import { useForm, SubmitHandler } from 'react-hook-form';
import axios from 'axios';
import icon from '../../assets/icon.png';
import { Link, useNavigate } from 'react-router-dom';
import { useContext, useState } from 'react';
import { Link, useNavigate, useNavigation } from 'react-router-dom';
import { useContext, useEffect, useState } from 'react';
import { AuthContext } from '../utils/AuthProvider.tsx';
import Cookies from 'js-cookie';
type Inputs = {
username: string;
@@ -14,11 +15,17 @@ export default function Login() {
const { setAuthorized } = useContext(AuthContext);
const [message, setMessage] = useState('');
const navigate = useNavigate();
const { register, handleSubmit } = useForm<Inputs>({
mode: 'onChange',
});
useEffect(() => {
const token = Cookies.get('token');
if (token) {
navigate('/chat', { replace: true });
}
}, []);
const onSubmit: SubmitHandler<Inputs> = (data) => {
axios
.post('http://localhost:5173/api/auth/login', data, {

View File

@@ -2,8 +2,9 @@ import icon from '../../assets/icon.png';
import { useForm, SubmitHandler } from 'react-hook-form';
import { Link, useNavigate } from 'react-router-dom';
import axios from 'axios';
import { useContext, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { AuthContext } from '../utils/AuthProvider.tsx';
import Cookies from 'js-cookie';
type Inputs = {
username: string;
@@ -25,6 +26,13 @@ export default function Signup() {
const [match, setMatch] = useState(true);
const [message, setMessage] = useState('');
useEffect(() => {
const token = Cookies.get('token');
if (token) {
navigate('/chat', { replace: true });
}
}, []);
const onSubmit: SubmitHandler<Inputs> = (data) => {
if (data.password !== data.sPassword) {
setMatch(true);

View File

@@ -1,19 +1,20 @@
import axios from "axios";
import { useState, useEffect } from "react";
import Cookies from "js-cookie";
import axios from 'axios';
import { useState, useEffect } from 'react';
import Cookies from 'js-cookie';
function useAuth() {
const [authorized, setAuthorized] = useState<boolean>(false);
useEffect(() => {
async function validateToken() {
const token = Cookies.get("token");
const token = Cookies.get('token');
if (!token) {
setAuthorized(false);
return;
}
try {
await axios.get("/api/auth/validate", { withCredentials: true });
await axios.get('/api/auth/validate', { withCredentials: true });
setAuthorized(true);
} catch (error) {
setAuthorized(false);

View File

@@ -46,11 +46,12 @@ async function createTables() {
sender VARCHAR(128) NOT NULL,
recipient VARCHAR(128) NOT NULL,
message VARCHAR(500) NOT NULL,
timestamp VARCHAR(100) NOT NULL,
message_id SERIAL PRIMARY KEY
timestamp TIMESTAMP NOT NULL,
message_id SERIAL PRIMARY KEY,
UNIQUE (sender, recipient, message_id)
);
CREATE INDEX IF NOT EXISTS idx_messages_conversation
ON messages(sender, recipient, message_id ASC)
ON messages (sender, recipient, message_id ASC);
`);
} catch (e) {
console.error("Failed to create messages table ", e);
@@ -118,21 +119,39 @@ async function insertMessage(sender, recipient, message, timestamp) {
}
async function getMessages(username, recipient, limit = 50, cursor = 0) {
console.log(`getMessages for Username: ${username}, recipient: ${recipient}`);
const query = `
SELECT * FROM messages
WHERE (sender = $1 AND recipient = $2) OR (sender = $2 AND recipient = $1)
ORDER BY message_id ASC
LIMIT $3 OFFSET $4;
`;
console.log(
`getMessages for Username: ${username}, recipient: ${recipient}, limit: ${limit}, cursor: ${cursor}`,
);
let query;
let params;
if (cursor) {
query = `
SELECT * FROM messages
WHERE ((sender = $1 AND recipient = $2) OR (sender = $2 AND recipient = $1))
AND message_id < $3
ORDER BY message_id ASC
LIMIT $4;
`;
params = [username, recipient, cursor, limit + 1];
} else {
query = `
SELECT * FROM messages
WHERE (sender = $1 AND recipient = $2) OR (sender = $2 AND recipient = $1)
ORDER BY message_id DESC
LIMIT $3;
`;
params = [username, recipient, limit + 1];
}
try {
const results = await client.query(query, [
username,
recipient,
limit,
offset,
]);
return results.rows;
const results = await client.query(query, params);
const messages = results.rows;
const nextCursor =
messages.length > limit ? messages[limit - 1].message_id : null;
return { messages: messages.reverse(), nextCursor: nextCursor };
} catch (e) {
console.error("Failed to get messages ", e);
}
@@ -203,7 +222,7 @@ async function getContacts(username) {
const query = `
SELECT usernameContact, read FROM contacts
WHERE username = $1
ORDER BY lastActive DESC;
ORDER BY lastActive ASC;
`;
try {
const result = await client.query(query, [username]);

View File

@@ -187,16 +187,16 @@ app.get("/api/chat/messages/:contact", authorizeUser, async (req, res) => {
return res.status(400).json({ message: "Missing contact parameter" });
}
const limit = parseInt(req.query.limit);
const offset = parseInt(req.query.offset);
const cursor = parseInt(req.query.cursor);
const messages = await getMessages(
const { messages, nextCursor } = await getMessages(
req.user.username,
req.params.contact,
limit,
offset,
cursor,
);
console.log("Sent messages for: ", req.user.username, "messages: ", messages);
return res.status(200).json(messages);
return res.status(200).json({ messages, nextCursor });
});
initializeSocket(io);