code refactor, added animated message deletion, remove message button is not displayed only where you can actually use it
This commit is contained in:
@@ -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: [] };
|
||||
}
|
||||
|
||||
68
client/src/components/chat/AnimatedMessage.tsx
Normal file
68
client/src/components/chat/AnimatedMessage.tsx
Normal 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;
|
||||
@@ -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>>;
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user