code refactor, added animated message deletion, remove message button is not displayed only where you can actually use it

This commit is contained in:
slawk0
2024-12-22 23:58:11 +01:00
parent 0b06201ade
commit fb3501e21f
9 changed files with 135 additions and 84 deletions

View File

@@ -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<ContactsProps[]> {
@@ -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: [] };
}

View File

@@ -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 (
<li
className={`whitespace-pre-wrap ml-2 rounded p-1 group transition-all duration-300 ${
message.pending ? 'text-gray-400' : 'hover:bg-gray-900'
} ${isRemoving ? 'opacity-0 -translate-x-full' : 'opacity-100'}`}
>
<div className="flex items-center justify-between">
<div>
<span
title={`${new Intl.DateTimeFormat('en-GB', {
timeStyle: 'medium',
dateStyle: 'short',
})?.format(message.sent_at)}`}
>
{message.sender}: {message.message}
</span>
{message.attachment_urls && (
<div className="mt-2 flex flex-col gap-2">
{message.attachment_urls.length > 0
? message.attachment_urls.map((url, index) => (
<AttachmentPreview
key={`${message.message_id}-${index}`}
url={url}
/>
))
: null}
</div>
)}
</div>
{message.sender === currentUsername || isAdmin ? (
<Trash2
className="opacity-0 group-hover:opacity-100 h-5 w-5 ml-2 flex-shrink-0 cursor-pointer text-gray-400 hover:text-red-500 transition-colors duration-200"
onClick={handleDelete}
/>
) : null}
</div>
</li>
);
};
export default AnimatedMessage;

View File

@@ -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<React.SetStateAction<ContactsProps | null>>;
updateContactStatus: (contactObj: ContactsProps, read: boolean) => void;
setMessages: React.Dispatch<React.SetStateAction<ChatMessages[]>>;
setMessages: React.Dispatch<React.SetStateAction<ChatMessagesProps[]>>;
currentContact: ContactsProps | null;
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>;
};

View File

@@ -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 = {

View File

@@ -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<React.SetStateAction<ChatMessages[]>>;
messages: ChatMessagesProps[];
setMessages: React.Dispatch<React.SetStateAction<ChatMessagesProps[]>>;
currentContact: ContactsProps | null;
updateContactStatus: (contact: ContactsProps, read: boolean) => void;
messageHandler: (msg: ChatMessages) => void;
messageHandler: (msg: ChatMessagesProps) => void;
setContactsList: React.Dispatch<React.SetStateAction<ContactsProps[]>>;
fetchPreviousMessages: (contact: string | null) => Promise<void>;
errorMessage: string | null;
contactsList: ContactsProps[];
me: MeProps;
};
function MessagesArea({
@@ -31,6 +31,7 @@ function MessagesArea({
errorMessage,
setMessages,
contactsList,
me,
}: MessagesAreaProps) {
const containerRef = useRef<HTMLDivElement>(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) => (
<li
className={`whitespace-pre-wrap ml-2 rounded p-1 group ${
msg.pending ? 'text-gray-400' : 'hover:bg-gray-900'
}`}
key={msg.message_id}
>
<div className="flex items-center justify-between">
<div>
<span
title={`${new Intl.DateTimeFormat('en-Gb', {
timeStyle: 'medium',
dateStyle: 'short',
})?.format(msg.sent_at)}`}
>
{msg.sender}: {msg.message}
</span>
{msg.attachment_urls && (
<div className="mt-2 flex flex-col gap-2">
{msg.attachment_urls.length > 0
? msg.attachment_urls.map((url, index) => (
<AttachmentPreview
key={`${msg.message_id}-${index}`}
url={url}
/>
))
: null}
</div>
)}
</div>
<Trash2
className="opacity-0 group-hover:opacity-100 h-5 w-5 ml-2 flex-shrink-0 cursor-pointer text-gray-400 hover:text-red-500"
onClick={() => {
deleteMessage(msg.message_id);
}}
/>
</div>
</li>
));
return (
<div ref={containerRef} className="flex flex-col h-full overflow-y-auto">
<p className="text-center text-gray-400">{errorMessage}</p>
<ul className="flex-grow list-none">
{isLoading ? <LoadingWheel /> : null}
{messageList}
{messages.map((msg: ChatMessagesProps) => (
<AnimatedMessage
key={msg.message_id}
message={msg}
isAdmin={me.isGroupAdmin || me.isGroupOwner}
currentUsername={user.username}
onDelete={deleteMessage}
/>
))}
</ul>
</div>
);

View File

@@ -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<React.SetStateAction<MeProps>>;
me: MeProps;
};
function ParticipantsBar({
initializeContact,
currentContact,
setMe,
me,
}: ParticipantsBarProps) {
const [participants, setParticipants] = useState<ParticipantsProps[]>([]);
const [me, setMe] = useState<MeProps>({
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({
</ContextMenuTrigger>
{user.user_id !== participant.user_id &&
me.isGroupAdmin &&
(!participant.isadmin || me.iGroupOwner) ? (
(!participant.isadmin || me.isGroupOwner) ? (
<ContextMenuContent className="p-0">
<ContextMenuItem
className="bg-zinc-900 text-white outline-1 hover:bg-zinc-800 hover:cursor-pointer"

View File

@@ -11,7 +11,12 @@ import { getMessages, setContactStatus } from '../api/contactsApi.tsx';
import axios from 'axios';
import ParticipantsBar from '@/components/chat/ParticipantsBar.tsx';
export type ChatMessages = {
export type MeProps = {
isGroupAdmin: boolean;
isGroupOwner: boolean;
};
export type ChatMessagesProps = {
sender: string;
message: string;
recipient: string; // conversation_id
@@ -39,9 +44,13 @@ function Chat() {
null,
);
const [cursor, setCursor] = useState<number>(0);
const [messages, setMessages] = useState<ChatMessages[]>([]);
const [messages, setMessages] = useState<ChatMessagesProps[]>([]);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [hasMoreMessages, setHasMoreMessages] = useState<boolean>(true);
const [me, setMe] = useState<MeProps>({
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}
/>
</div>
@@ -243,6 +253,8 @@ function Chat() {
{currentContact?.type == 'group' ? (
<div className="w-80 bg-[#1E1E1E] flex-shrink-0">
<ParticipantsBar
setMe={setMe}
me={me}
initializeContact={initializeContact}
currentContact={currentContact}
/>

View File

@@ -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) {

View File

@@ -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 {