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' && (
+

+ )}
+
+ {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 ? (
+
+ ) : 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' && (
-

- )}
-
- {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 ? (
-
- ) : null}
-
-
-
-
-
- );
-
return (
-
+
{sortedContacts.map((contact: ContactsProps) => (
-
+
))}
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