prepared backend for pagination, redirect to /chat if logged in, fixed messages from contact not appearing in real time,
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: '' });
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user