diff --git a/client/src/api/contactsApi.tsx b/client/src/api/contactsApi.tsx index 451fa42..b86b144 100644 --- a/client/src/api/contactsApi.tsx +++ b/client/src/api/contactsApi.tsx @@ -1,5 +1,5 @@ import { axiosClient } from '../App.tsx'; -import { ChatMessages } from '../pages/Chat.tsx'; +import { ChatMessagesProps } from '../pages/Chat.tsx'; import { ContactsProps } from '../pages/Chat.tsx'; export async function getContactsList(): Promise { @@ -43,7 +43,7 @@ export async function getMessages( contact: string | null, cursor: number | null = 0, limit: number = 50, -): Promise<{ messages: ChatMessages[] }> { +): Promise<{ messages: ChatMessagesProps[] }> { if (contact === null || cursor === null) { return { messages: [] }; } diff --git a/client/src/components/chat/AnimatedMessage.tsx b/client/src/components/chat/AnimatedMessage.tsx new file mode 100644 index 0000000..074afb2 --- /dev/null +++ b/client/src/components/chat/AnimatedMessage.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; +import { Trash2 } from 'lucide-react'; +import AttachmentPreview from './AttachmentPreview'; +import { ChatMessagesProps } from '@/pages/Chat.tsx'; + +type AnimatedMessageProps = { + message: ChatMessagesProps; + isAdmin: boolean; + currentUsername: string | null; + onDelete: (messageId: number) => void; +}; + +const AnimatedMessage = ({ + message, + isAdmin, + currentUsername, + onDelete, +}: AnimatedMessageProps) => { + const [isRemoving, setIsRemoving] = useState(false); + + const handleDelete = () => { + setIsRemoving(true); + setTimeout(() => { + onDelete(message.message_id); + }, 300); + }; + + return ( +
  • +
    +
    + + {message.sender}: {message.message} + + {message.attachment_urls && ( +
    + {message.attachment_urls.length > 0 + ? message.attachment_urls.map((url, index) => ( + + )) + : null} +
    + )} +
    + {message.sender === currentUsername || isAdmin ? ( + + ) : null} +
    +
  • + ); +}; + +export default AnimatedMessage; diff --git a/client/src/components/chat/ContactsList.tsx b/client/src/components/chat/ContactsList.tsx index a5e4d85..40014a0 100644 --- a/client/src/components/chat/ContactsList.tsx +++ b/client/src/components/chat/ContactsList.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { getContactsList } from '../../api/contactsApi.tsx'; -import { ChatMessages, ContactsProps } from '../../pages/Chat.tsx'; +import { ChatMessagesProps, ContactsProps } from '../../pages/Chat.tsx'; import { socket } from '../../socket/socket.tsx'; import GroupIcon from '../../../assets/group.svg'; import { @@ -21,7 +21,7 @@ type ContactsListProps = { contactsList: ContactsProps[]; setCurrentContact: React.Dispatch>; updateContactStatus: (contactObj: ContactsProps, read: boolean) => void; - setMessages: React.Dispatch>; + setMessages: React.Dispatch>; currentContact: ContactsProps | null; setErrorMessage: React.Dispatch>; }; diff --git a/client/src/components/chat/MessageForm.tsx b/client/src/components/chat/MessageForm.tsx index ed92af0..c35b15e 100644 --- a/client/src/components/chat/MessageForm.tsx +++ b/client/src/components/chat/MessageForm.tsx @@ -2,7 +2,7 @@ import { useRef, useCallback, useEffect, useState } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import type { KeyboardEventHandler } from 'react'; import { socket } from '../../socket/socket.tsx'; -import { ChatMessages, ContactsProps } from '../../pages/Chat.tsx'; +import { ChatMessagesProps, ContactsProps } from '../../pages/Chat.tsx'; import { axiosClient } from '../../App.tsx'; import { File, Paperclip, X } from 'lucide-react'; import LoadingWheel from '@/components/chat/LoadingWheel.tsx'; @@ -14,7 +14,7 @@ type Input = { type MessageFormProps = { contact: ContactsProps; - messages: ChatMessages[]; + messages: ChatMessagesProps[]; }; type FileWithPreview = { diff --git a/client/src/components/chat/MessagesArea.tsx b/client/src/components/chat/MessagesArea.tsx index 61324ea..37913dd 100644 --- a/client/src/components/chat/MessagesArea.tsx +++ b/client/src/components/chat/MessagesArea.tsx @@ -3,22 +3,22 @@ import { socket } from '../../socket/socket.tsx'; import { useOutletContext } from 'react-router-dom'; import { sendContact } from '../../api/contactsApi.tsx'; import LoadingWheel from './LoadingWheel.tsx'; -import { ChatMessages } from '../../pages/Chat.tsx'; +import { ChatMessagesProps, MeProps } from '../../pages/Chat.tsx'; import { ContactsProps } from '../../pages/Chat.tsx'; -import { Trash2 } from 'lucide-react'; -import AttachmentPreview from './AttachmentPreview.tsx'; import { UsernameType } from '@/utils/ProtectedRoutes.tsx'; +import AnimatedMessage from '@/components/chat/AnimatedMessage.tsx'; type MessagesAreaProps = { - messages: ChatMessages[]; - setMessages: React.Dispatch>; + messages: ChatMessagesProps[]; + setMessages: React.Dispatch>; currentContact: ContactsProps | null; updateContactStatus: (contact: ContactsProps, read: boolean) => void; - messageHandler: (msg: ChatMessages) => void; + messageHandler: (msg: ChatMessagesProps) => void; setContactsList: React.Dispatch>; fetchPreviousMessages: (contact: string | null) => Promise; errorMessage: string | null; contactsList: ContactsProps[]; + me: MeProps; }; function MessagesArea({ @@ -31,6 +31,7 @@ function MessagesArea({ errorMessage, setMessages, contactsList, + me, }: MessagesAreaProps) { const containerRef = useRef(null); const user: UsernameType = useOutletContext(); @@ -73,7 +74,6 @@ function MessagesArea({ const deleteMessage = async (message_id: number) => { try { - console.log('delete message: ', message_id); socket?.emit( 'delete message', { @@ -101,7 +101,7 @@ function MessagesArea({ currentContainer.addEventListener('scroll', handleScroll); } - socket.on('chat message', (msg: ChatMessages) => { + socket.on('chat message', (msg: ChatMessagesProps) => { console.log('Received message: ', msg); if ( msg.conversation_id !== currentContact?.conversation_id && @@ -233,52 +233,20 @@ function MessagesArea({ scrollToBottom(); }, []); - const messageList = messages.map((msg: ChatMessages) => ( -
  • -
    -
    - - {msg.sender}: {msg.message} - - {msg.attachment_urls && ( -
    - {msg.attachment_urls.length > 0 - ? msg.attachment_urls.map((url, index) => ( - - )) - : null} -
    - )} -
    - { - deleteMessage(msg.message_id); - }} - /> -
    -
  • - )); - return (

    {errorMessage}

      {isLoading ? : null} - {messageList} + {messages.map((msg: ChatMessagesProps) => ( + + ))}
    ); diff --git a/client/src/components/chat/ParticipantsBar.tsx b/client/src/components/chat/ParticipantsBar.tsx index accaa9c..99c9076 100644 --- a/client/src/components/chat/ParticipantsBar.tsx +++ b/client/src/components/chat/ParticipantsBar.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from 'react'; import { axiosClient } from '@/App.tsx'; -import { ContactsProps } from '@/pages/Chat.tsx'; +import { ContactsProps, MeProps } from '@/pages/Chat.tsx'; import { socket } from '@/socket/socket.tsx'; import { Crown, Sword } from 'lucide-react'; import { @@ -22,22 +22,17 @@ type ParticipantsProps = { type ParticipantsBarProps = { initializeContact: (contact: ContactsProps) => void; currentContact: ContactsProps | null; -}; - -type MeProps = { - isGroupAdmin: boolean; - iGroupOwner: boolean; + setMe: React.Dispatch>; + me: MeProps; }; function ParticipantsBar({ initializeContact, currentContact, + setMe, + me, }: ParticipantsBarProps) { const [participants, setParticipants] = useState([]); - const [me, setMe] = useState({ - isGroupAdmin: false, - iGroupOwner: false, - }); const user: UsernameType = useOutletContext(); const getParticipants = async () => { @@ -122,7 +117,7 @@ function ParticipantsBar({ participant.user_id === user.user_id && participant.isowner, ); - setMe({ isGroupAdmin: userIsAdmin, iGroupOwner: userIsOwner }); + setMe({ isGroupAdmin: userIsAdmin, isGroupOwner: userIsOwner }); } }, [participants, user?.user_id]); @@ -216,6 +211,9 @@ function ParticipantsBar({ }) => { console.log('(socket) removed administrator: ', msg); if (msg.group_id === currentContact.conversation_id) { + if (msg.user_id === user?.user_id) { + setMe({ isGroupAdmin: false, isGroupOwner: false }); + } setParticipants((prevMembers) => prevMembers.map((member) => member.user_id === msg.user_id @@ -262,7 +260,7 @@ function ParticipantsBar({ {user.user_id !== participant.user_id && me.isGroupAdmin && - (!participant.isadmin || me.iGroupOwner) ? ( + (!participant.isadmin || me.isGroupOwner) ? ( (0); - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState([]); const [errorMessage, setErrorMessage] = useState(null); const [hasMoreMessages, setHasMoreMessages] = useState(true); + const [me, setMe] = useState({ + isGroupAdmin: false, + isGroupOwner: false, + }); useEffect(() => { const token = Cookies.get('token'); @@ -161,7 +170,7 @@ function Chat() { } }; - function messageHandler(msg: ChatMessages) { + function messageHandler(msg: ChatMessagesProps) { setMessages((prevMessages) => { // Check if the message already exists in the state if (!prevMessages.some((m) => m.message_id === msg.message_id)) { @@ -236,6 +245,7 @@ function Chat() { fetchPreviousMessages={fetchPreviousMessages} errorMessage={errorMessage} contactsList={contactsList} + me={me} /> @@ -243,6 +253,8 @@ function Chat() { {currentContact?.type == 'group' ? (
    diff --git a/server/db/db.js b/server/db/db.js index b16361b..4757060 100644 --- a/server/db/db.js +++ b/server/db/db.js @@ -810,7 +810,7 @@ async function contactSuggestion(username) { } } -async function deleteMessage(user_id, message_id) { +async function deleteMessage(user_id, conversation_id, message_id) { const checkMessageOwnershipQuery = ` SELECT user_id FROM Messages WHERE message_id = $1; `; @@ -820,17 +820,19 @@ async function deleteMessage(user_id, message_id) { `; try { - const checkResult = await client.query(checkMessageOwnershipQuery, [ - message_id, - ]); - if (checkResult.rows.length === 0) { - return { message: "Message not found." }; - } - - const messageOwnerId = checkResult.rows[0].user_id; - if (user_id !== messageOwnerId) { - console.error("User is not authorized to delete this message"); - return { message: "It's not your message bro" }; + const isAdminResult = await isAdmin(user_id, conversation_id); + if (!isAdminResult) { + const ownershipResult = await client.query(checkMessageOwnershipQuery, [ + message_id, + ]); + if (ownershipResult.rows.length === 0) { + return { message: "Message not found." }; + } + const messageOwnerId = ownershipResult.rows[0].user_id; + if (user_id !== messageOwnerId) { + console.error("User is not authorized to delete this message"); + return { message: "It's not your message bro" }; + } } const deleteResult = await client.query(deleteMessageQuery, [message_id]); if (deleteResult.rowCount > 0) { diff --git a/server/socket/socket.js b/server/socket/socket.js index f2fcc1e..38e58d2 100644 --- a/server/socket/socket.js +++ b/server/socket/socket.js @@ -179,7 +179,6 @@ function initializeSocket(io) { }); socket.on("delete message", async (msg, callback) => { - console.log("(socket) delete message for message_id: ", msg); const { conversation_id, message_id } = msg; if (!message_id) { return callback({ status: "error", message: "No message id provided" }); @@ -191,7 +190,11 @@ function initializeSocket(io) { }); } - const result = await deleteMessage(socket.user_id, message_id); + const result = await deleteMessage( + socket.user_id, + conversation_id, + message_id, + ); if (result?.message !== undefined) { return callback({ status: "error", message: result.message }); } else {