diff --git a/client/assets/turtle.webp b/client/assets/turtle.webp new file mode 100644 index 0000000..25a14de Binary files /dev/null and b/client/assets/turtle.webp differ diff --git a/client/src/components/chat/chatArea/MessagesArea.tsx b/client/src/components/chat/chatArea/MessagesArea.tsx index d48c5bf..ae95706 100644 --- a/client/src/components/chat/chatArea/MessagesArea.tsx +++ b/client/src/components/chat/chatArea/MessagesArea.tsx @@ -87,7 +87,7 @@ function MessagesArea() { : contact, ), ); - document.title = 'New message ❗'; + document.title = 'New message 🔔'; } else { socket?.emit('read message', { conversation_id: msg.conversation_id, diff --git a/client/src/components/chat/leftSidebar/ContactItem.tsx b/client/src/components/chat/leftSidebar/ContactItem.tsx new file mode 100644 index 0000000..e3d1c5c --- /dev/null +++ b/client/src/components/chat/leftSidebar/ContactItem.tsx @@ -0,0 +1,118 @@ +import { memo } from 'react'; +import { ContactsProps } from '@/types/types.ts'; +import GroupIcon from '../../../../assets/group.svg'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog.tsx'; +import { Ellipsis, Paperclip } from 'lucide-react'; +import LastActiveTime from '@/components/chat/leftSidebar/LastActiveTime.tsx'; + +const ContactItem = memo( + ({ + contact, + onRemove, + onSelect, + }: { + contact: ContactsProps; + onRemove: (id: number, conversationId: string) => void; + onSelect: (contact: ContactsProps) => void; + }) => ( +
  • onSelect(contact)} + > +
    +
    +
    + {contact.username} + {contact.type === 'group' && ( + Group icon + )} +

    + {contact.read ? '' : '•'} +

    +
    + + {contact.type === 'group' ? ( + + + + + + + + Leave Group? + + + Are you sure you want to leave this group? + + + + Cancel + { + e.stopPropagation(); + onRemove(contact.id, contact.conversation_id); + }} + className="bg-red-600 hover:bg-red-500" + > + Leave Group + + + + + ) : ( + + )} +
    + +
    +
    + {(contact.last_message ?? '').length > 0 ? ( + (contact.last_message ?? '').length > 15 ? ( +
    + {contact.last_message?.substring(0, 15)} + +
    + ) : ( + contact.last_message + ) + ) : contact.last_message_time ? ( +
    + attachment +
    + ) : null} +
    + +
    +
    +
  • + ), +); + +export default ContactItem; diff --git a/client/src/components/chat/leftSidebar/ContactsList.tsx b/client/src/components/chat/leftSidebar/ContactsList.tsx index bb1f3df..a7f8540 100644 --- a/client/src/components/chat/leftSidebar/ContactsList.tsx +++ b/client/src/components/chat/leftSidebar/ContactsList.tsx @@ -1,22 +1,9 @@ import { useEffect } from 'react'; import { socket } from '@/socket/socket.ts'; -import GroupIcon from '../../../../assets/group.svg'; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@/components/ui/alert-dialog.tsx'; -import { Ellipsis, Paperclip } from 'lucide-react'; -import LastActiveTime from '@/components/chat/leftSidebar/LastActiveTime.tsx'; import { ContactsProps } from '@/types/types.ts'; import { useChat } from '@/context/chat/useChat.ts'; import { axiosClient } from '@/utils/axiosClient.ts'; +import ContactItem from '@/components/chat/leftSidebar/ContactItem.tsx'; function ContactsList() { const { @@ -27,6 +14,7 @@ function ContactsList() { setMessages, setErrorMessage, } = useChat(); + useEffect(() => { fetchContacts().catch((e) => console.error('Failed to fetch contacts: ', e), @@ -53,17 +41,14 @@ function ContactsList() { console.log('Get contacts list response: ', response.data); const fetchedContacts: ContactsProps[] = response.data; - const updatedContacts = fetchedContacts.map((contact) => { - if ( + const updatedContacts = fetchedContacts.map((contact) => ({ + ...contact, + read: !( contact.last_message_id && contact.last_read_message_id && contact.last_message_id !== contact.last_read_message_id - ) { - return { ...contact, read: false }; - } else { - return { ...contact, read: true }; - } - }); + ), + })); setContactsList(updatedContacts); }; @@ -76,7 +61,7 @@ function ContactsList() { 'delete contact', conversation_id, (response: { status: string; message: string }) => { - if (response.status == 'ok') { + if (response.status === 'ok') { console.log('(socket) response: ', response); setCurrentContact(null); localStorage.removeItem('contact'); @@ -96,122 +81,23 @@ function ContactsList() { } const sortedContacts = [...contactsList].sort((a, b) => { - // First, sort by read status (unread first) - if (a.read !== b.read) { - return a.read ? 1 : -1; - } - - // If both are read or both are unread, sort by last_message_time (if available) - if (a.last_message_time !== null && b.last_message_time !== null) { + if (a.read !== b.read) return a.read ? 1 : -1; + if (a.last_message_time && b.last_message_time) { return -a.last_message_time.localeCompare(b.last_message_time); } - - // If one or both last_message_time are null, sort by last_active_time - if (a.last_message_time === null || b.last_message_time === null) { - return -a.last_active.localeCompare(b.last_active); - } - - // Default case (should not be reached) - return 0; + return -a.last_active.localeCompare(b.last_active); }); - const ContactItem = ({ contact }: { contact: ContactsProps }) => ( -
  • { - initializeContact(contact); - }} - > -
    -
    -
    - {contact.username} - {contact.type === 'group' && ( - Group icon - )} -

    - {contact.read ? '' : '•'} -

    -
    - - {contact.type === 'group' ? ( - - - - - - - - Leave Group? - - - Are you sure you want to leave this group? - - - - Cancel - { - e.stopPropagation(); - removeContact(contact.id, contact.conversation_id); - }} - className="bg-red-600 hover:bg-red-500" - > - Leave Group - - - - - ) : ( - - )} -
    - -
    -
    - {(contact.last_message ?? '').length > 0 ? ( - (contact.last_message ?? '').length > 15 ? ( -
    - {contact.last_message?.substring(0, 15)} - -
    - ) : ( - contact.last_message - ) - ) : contact.last_message_time ? ( -
    - attachment -
    - ) : null} -
    - -
    -
    -
  • - ); - return ( -
    +
    diff --git a/client/src/components/chat/leftSidebar/LastActiveTime.tsx b/client/src/components/chat/leftSidebar/LastActiveTime.tsx index 1d4489a..f6441de 100644 --- a/client/src/components/chat/leftSidebar/LastActiveTime.tsx +++ b/client/src/components/chat/leftSidebar/LastActiveTime.tsx @@ -1,6 +1,5 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { differenceInSeconds, formatDistanceToNowStrict } from 'date-fns'; - import { ContactsProps } from '@/types/types.ts'; type LastActiveTimeProps = { @@ -9,30 +8,49 @@ type LastActiveTimeProps = { const LastActiveTime = ({ contact }: LastActiveTimeProps) => { const [timeAgo, setTimeAgo] = useState(''); + const previousTimeRef = useRef(''); + const previousDateRef = useRef(null); useEffect(() => { const updateTime = () => { if (!contact?.last_message_time) { return; } + + // If it's a new message time, store it + if (contact.last_message_time !== previousDateRef.current) { + previousDateRef.current = contact.last_message_time; + } + const lastActiveDate = new Date(contact.last_message_time); const secondsDiff = differenceInSeconds(new Date(), lastActiveDate); + let newTimeAgo; if (secondsDiff < 60) { - setTimeAgo('now'); - return; + newTimeAgo = 'now'; + } else { + newTimeAgo = formatDistanceToNowStrict(lastActiveDate); } - setTimeAgo(formatDistanceToNowStrict(lastActiveDate)); + // Only update state if the formatted time has actually changed + if (newTimeAgo !== previousTimeRef.current) { + previousTimeRef.current = newTimeAgo; + setTimeAgo(newTimeAgo); + } }; - updateTime(); + updateTime(); const intervalId = setInterval(updateTime, 60000); return () => clearInterval(intervalId); }, [contact?.last_message_time]); - return {timeAgo}; + // Use a CSS transition for smooth updates + return ( + + {timeAgo} + + ); }; export default LastActiveTime; diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx index 21a1248..d4adf6a 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -1,5 +1,5 @@ import { useForm, SubmitHandler } from 'react-hook-form'; -import icon from '../../assets/icon.png'; +import icon from '../../assets/turtle.webp'; import { Link, useNavigate } from 'react-router-dom'; import { useContext, useState } from 'react'; import LoadingWheel from '../components/chat/LoadingWheel.tsx'; diff --git a/client/src/pages/Signup.tsx b/client/src/pages/Signup.tsx index 5d19749..1539bbc 100644 --- a/client/src/pages/Signup.tsx +++ b/client/src/pages/Signup.tsx @@ -1,4 +1,4 @@ -import icon from '../../assets/icon.png'; +import icon from '../../assets/turtle.webp'; import { useForm, SubmitHandler } from 'react-hook-form'; import { Link, useNavigate } from 'react-router-dom'; import { useContext, useState } from 'react'; diff --git a/client/turtle.webp/image.webp b/client/turtle.webp/image.webp new file mode 100644 index 0000000..25a14de Binary files /dev/null and b/client/turtle.webp/image.webp differ