Compare commits

...

2 Commits

13 changed files with 96 additions and 140 deletions

View File

@@ -13,7 +13,7 @@ export async function getContactsList(): Promise<ContactsProps[]> {
}
}
export async function setContactStatus(conversation_id: number, read: boolean) {
export async function setContactStatus(conversation_id: string, read: boolean) {
try {
const response = await axiosClient.put(
`/api/chat/contacts/${conversation_id}`,
@@ -40,7 +40,7 @@ export async function sendContact(contact: string) {
}
export async function getMessages(
contact: number | null,
contact: string | null,
cursor: number | null = 0,
limit: number = 50,
): Promise<{ messages: ChatMessages[] }> {

View File

@@ -48,8 +48,8 @@ function ContactsList({
'added to group',
(msg: {
username: string;
user_id: number;
group_id: number;
user_id: string;
group_id: string;
isadmin: false;
}) => {
console.log('added to group, fetching contacts');
@@ -57,10 +57,6 @@ function ContactsList({
fetchContacts();
},
);
return () => {
socket?.off('added to group');
};
}, []);
const fetchContacts = async () => {
@@ -70,7 +66,7 @@ function ContactsList({
setContactsList(contacts);
};
async function removeContact(contactId: number, conversation_id: number) {
async function removeContact(contactId: number, conversation_id: string) {
try {
console.log('delete contact, conversation_id: ', conversation_id);

View File

@@ -16,7 +16,7 @@ type MessagesAreaProps = {
updateContactStatus: (contact: ContactsProps, read: boolean) => void;
messageHandler: (msg: ChatMessages) => void;
setContactsList: React.Dispatch<React.SetStateAction<ContactsProps[]>>;
fetchPreviousMessages: (contact: number | null) => Promise<void>;
fetchPreviousMessages: (contact: string | null) => Promise<void>;
errorMessage: string | null;
contactsList: ContactsProps[];
};
@@ -194,7 +194,7 @@ function MessagesArea({
socket.on(
'delete message',
(msg: { conversation_id: number; message_id: number }) => {
(msg: { conversation_id: string; message_id: number }) => {
console.log('deleted message from state');
setMessages((prevMessages) =>
prevMessages.filter(

View File

@@ -14,7 +14,7 @@ import { useOutletContext } from 'react-router-dom';
import LoadingWheel from '@/components/chat/LoadingWheel.tsx';
type ParticipantsProps = {
user_id: number;
user_id: string;
username: string;
isadmin: boolean;
};
@@ -75,8 +75,8 @@ function ParticipantsBar({
'added to group',
(msg: {
username: string;
user_id: number;
group_id: number;
user_id: string;
group_id: string;
isadmin: false;
}) => {
const { group_id } = msg;
@@ -112,7 +112,7 @@ function ParticipantsBar({
},
);
socket.on('left group', (msg: { user_id: number; group_id: number }) => {
socket.on('left group', (msg: { user_id: string; group_id: string }) => {
console.log(`(socket on "left group")`, msg);
// Initialize contact again to clear messages and participants list
if (
@@ -128,10 +128,11 @@ function ParticipantsBar({
return () => {
socket?.off('left group');
socket?.off('added to group');
};
}, [contact, participants]);
const handleRemoveUser = async (userId: number) => {
const handleRemoveUser = async (userId: string) => {
try {
setIsLoading(true);
socket?.emit(

View File

@@ -24,9 +24,7 @@ function UserProfile() {
</div>
<div className="text-gray-200">
<p>
{user?.user_id} {user.username}
</p>
<p>{user.username}</p>
</div>
</div>

View File

@@ -14,22 +14,22 @@ import ParticipantsBar from '@/components/chat/ParticipantsBar.tsx';
export type ChatMessages = {
sender: string;
message: string;
recipient: number; // conversation_id
recipient: string; // conversation_id
message_id: number;
pending: boolean;
attachment_urls: string[] | null;
sender_id: number;
conversation_id: number;
sender_id: string;
conversation_id: string;
sent_at: Date;
};
export type ContactsProps = {
read: boolean;
username: string;
user_id: number;
user_id: string;
id: number;
type: 'direct' | 'group';
conversation_id: number;
conversation_id: string;
last_active: string;
};
@@ -92,7 +92,7 @@ function Chat() {
}
}
const fetchMessages = async (conversation_id: number) => {
const fetchMessages = async (conversation_id: string) => {
console.log('Fetching messages for: ', conversation_id);
try {
const messages = await getMessages(conversation_id);
@@ -115,7 +115,7 @@ function Chat() {
}
};
const fetchPreviousMessages = async (contact: number | null) => {
const fetchPreviousMessages = async (contact: string | null) => {
if (!hasMoreMessages) {
return;
}

View File

@@ -22,7 +22,7 @@ function initializeSocket(token: string): Socket | null {
}
async function joinRoom(
conversation_id: number,
conversation_id: string,
): Promise<{ success: boolean; message: string }> {
console.log('Attempting to join room:', conversation_id);

View File

@@ -7,7 +7,7 @@ import { axiosClient } from '../App.tsx';
export type UsernameType = {
username: string | null;
user_id: number | null;
user_id: string | null;
};
function ProtectedRoutes() {

View File

@@ -33,10 +33,16 @@ client
// Creating database tables
async function createTables() {
try {
// Create Accounts Table
// Enable UUID extension
await client.query(`
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
`);
console.log("Successfully enabled UUID extension");
// Create Accounts Table with UUID
await client.query(`
CREATE TABLE IF NOT EXISTS Accounts (
user_id SERIAL PRIMARY KEY,
user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
@@ -44,16 +50,11 @@ async function createTables() {
CREATE INDEX IF NOT EXISTS idx_username ON Accounts (username);
`);
console.log("Successfully created Accounts table");
} catch (e) {
console.error("Failed to create Accounts table: ", e);
throw e;
}
try {
// Create Conversations Table
// Create Conversations Table with UUID
await client.query(`
CREATE TABLE IF NOT EXISTS Conversations (
conversation_id SERIAL PRIMARY KEY,
conversation_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
conversation_type VARCHAR(10) NOT NULL CHECK (conversation_type IN ('direct', 'group')),
name VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW(),
@@ -62,18 +63,13 @@ async function createTables() {
CREATE INDEX IF NOT EXISTS idx_conversation_type ON Conversations (conversation_type);
`);
console.log("Successfully created Conversations table");
} catch (e) {
console.error("Failed to create Conversations table: ", e);
throw e;
}
try {
// Create Messages Table
// Create Messages Table with SERIAL but referencing UUID
await client.query(`
CREATE TABLE IF NOT EXISTS Messages (
message_id SERIAL PRIMARY KEY,
conversation_id INT REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
user_id INT REFERENCES Accounts(user_id) ON DELETE CASCADE,
conversation_id UUID REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
user_id UUID REFERENCES Accounts(user_id) ON DELETE CASCADE,
content TEXT NOT NULL,
attachment_urls TEXT[],
sent_at TIMESTAMPTZ DEFAULT NOW()
@@ -83,17 +79,12 @@ async function createTables() {
CREATE INDEX IF NOT EXISTS idx_messages_conversation_sent_at ON Messages (conversation_id, sent_at);
`);
console.log("Successfully created Messages table");
} catch (e) {
console.error("Failed to create Messages table: ", e);
throw e;
}
try {
// Create Memberships Table
// Create Memberships Table referencing UUID
await client.query(`
CREATE TABLE IF NOT EXISTS Memberships (
conversation_id INT NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
user_id INT REFERENCES Accounts(user_id) ON DELETE CASCADE,
conversation_id UUID NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
user_id UUID REFERENCES Accounts(user_id) ON DELETE CASCADE,
joined_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (conversation_id, user_id)
);
@@ -102,18 +93,13 @@ async function createTables() {
CREATE INDEX IF NOT EXISTS idx_memberships_conversation_joined_at ON Memberships (conversation_id, joined_at);
`);
console.log("Successfully created Memberships table");
} catch (e) {
console.error("Failed to create Memberships table: ", e);
throw e;
}
try {
// Create Contacts Table
// Create Contacts Table with SERIAL but referencing UUID
await client.query(`
CREATE TABLE IF NOT EXISTS Contacts (
contact_id SERIAL PRIMARY KEY,
user_id INT REFERENCES Accounts(user_id) ON DELETE CASCADE,
conversation_id INT NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
user_id UUID REFERENCES Accounts(user_id) ON DELETE CASCADE,
conversation_id UUID NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
read BOOLEAN NOT NULL DEFAULT FALSE,
last_active TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT unique_contact UNIQUE (user_id, conversation_id)
@@ -123,19 +109,14 @@ async function createTables() {
CREATE INDEX IF NOT EXISTS idx_contacts_contact_id ON Contacts (contact_id);
`);
console.log("Successfully created Contacts table");
} catch (e) {
console.error("Failed to create Contacts table: ", e);
throw e;
}
try {
// Create GroupAdmins Table with Trigger
// Create GroupAdmins Table with Trigger, referencing UUID
await client.query(`
-- Create the base table
CREATE TABLE IF NOT EXISTS GroupAdmins (
conversation_id INT NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
user_id INT NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
granted_by INT NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
conversation_id UUID NOT NULL REFERENCES Conversations(conversation_id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
granted_by UUID NOT NULL REFERENCES Accounts(user_id) ON DELETE CASCADE,
granted_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (conversation_id, user_id)
);
@@ -181,7 +162,7 @@ async function createTables() {
`);
console.log("Successfully created GroupAdmins table with trigger");
} catch (e) {
console.error("Failed to create GroupAdmins table: ", e);
console.error("Failed to create tables: ", e);
throw e;
}
@@ -803,16 +784,18 @@ async function updateConversationLastActive(conversation_id, userId) {
async function getConversationsForUser(user_id) {
const query = `
SELECT conversation_id
FROM Memberships
WHERE user_id = $1;
SELECT DISTINCT m.conversation_id
FROM Memberships m
JOIN Conversations c ON m.conversation_id = c.conversation_id
WHERE m.user_id = $1
AND c.conversation_type = 'group';
`;
try {
const result = await client.query(query, [user_id]);
return result.rows.map((row) => row.conversation_id);
} catch (e) {
console.error("Failed to get conversations for user ", e);
} catch (error) {
console.error("Failed to get group conversations for user:", error);
return [];
}
}

View File

@@ -13,7 +13,6 @@
"express": "^4.21.0",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"nanoid": "^5.0.7",
"pg": "^8.13.0",
"socket.io": "^4.8.0",
"uuid": "^10.0.0"
@@ -436,36 +435,27 @@
}
},
"node_modules/cookie": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-parser": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
"integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz",
"integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==",
"license": "MIT",
"dependencies": {
"cookie": "0.4.1",
"cookie": "0.7.2",
"cookie-signature": "1.0.6"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/cookie-parser/node_modules/cookie": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
"integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -602,9 +592,9 @@
}
},
"node_modules/engine.io": {
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.1.tgz",
"integrity": "sha512-NEpDCw9hrvBW+hVEOK4T7v0jFJ++KgtPl4jKFwsZVfG1XhS0dCrSb3VMb9gPAd7VAdW52VT1EnaNiU2vM8C0og==",
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
"integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.4.1",
@@ -612,7 +602,7 @@
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.4.1",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
@@ -668,9 +658,9 @@
}
},
"node_modules/express": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
"integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
@@ -678,7 +668,7 @@
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -692,7 +682,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.10",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -707,12 +697,16 @@
},
"engines": {
"node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -1408,24 +1402,6 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/nanoid": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
"integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.js"
},
"engines": {
"node": "^18 || >=20"
}
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -1589,9 +1565,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
"node_modules/pg": {

View File

@@ -12,7 +12,6 @@
"express": "^4.21.0",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"nanoid": "^5.0.7",
"pg": "^8.13.0",
"socket.io": "^4.8.0",
"uuid": "^10.0.0"

View File

@@ -362,7 +362,7 @@ app.post("/api/chat/groups/create", authorizeUser, async (req, res) => {
return res.status(500).json({ message: "Failed to create group" });
}
console.log("Successfully created group: ", groupname, "id: ", group_id);
console.log("io.to: ", contact_user_id, "added to group");
console.log(`io.to: ${contact_user_id} added to group`);
io.to(contact_user_id).emit("added to group", { group_id });
return res.status(200).json({
message: `Successfully created group: ${groupname}`,

View File

@@ -73,6 +73,7 @@ function initializeSocket(io) {
conversations = await getConversationsForUser(socket.user_id);
conversations.push(socket.user_id);
socket.join(conversations);
console.log(`User: ${socket.username} joined to: ${conversations}`);
} catch (e) {
console.error("(socket) Failed to get user conversations");
}
@@ -92,7 +93,7 @@ function initializeSocket(io) {
} else {
return callback({
status: "error",
message: "You are not member of this conversation",
message: "You are not member of this group",
});
}
});
@@ -139,8 +140,7 @@ function initializeSocket(io) {
insertedMessage;
console.log("(socket) received from chat message", msg);
console.log(`io to ${username}, ${recipient}, ${recipient_id}`);
io.to(username).to(recipient).to(recipient_id).emit("chat message", {
io.to(recipient).to(recipient_id).emit("chat message", {
sender,
message: content,
attachment_urls,
@@ -150,15 +150,18 @@ function initializeSocket(io) {
sent_at,
conversation_id,
});
console.log("(socket) sent on 'chat message' socket: ", {
sender,
message,
attachment_urls,
recipient,
sent_at,
message_id,
sender_id,
});
console.log(
`(socket) sent on 'chat message' socket to: recipient: ${recipient}, recipient_id: ${recipient_id} `,
{
sender,
message,
attachment_urls,
recipient,
sent_at,
message_id,
sender_id,
},
);
callback({
message_id,