improved perfomance of LastActiveTime.tsx, added 🐢icon

This commit is contained in:
2025-01-16 21:54:26 +01:00
parent d418daea55
commit 45ba4bde8b
8 changed files with 164 additions and 142 deletions

BIN
client/assets/turtle.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

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

View File

@@ -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;
}) => (
<li
className="m-1 flex p-2 hover:bg-zinc-900 cursor-pointer transition-colors rounded-lg justify-between items-start min-h-[40px]"
onClick={() => onSelect(contact)}
>
<div className="flex flex-col w-full">
<div className="flex items-center justify-between">
<div className="flex items-center">
<span className="text-lg">{contact.username}</span>
{contact.type === 'group' && (
<img
src={GroupIcon}
alt="Group icon"
className="ml-2 invert w-5"
/>
)}
<p className="text-center text-2xl text-red-300 leading-none">
{contact.read ? '' : '•'}
</p>
</div>
{contact.type === 'group' ? (
<AlertDialog>
<AlertDialogTrigger asChild>
<button
onClick={(e) => e.stopPropagation()}
className="flex-shrink-0 w-6 h-6 rounded-full hover:text-red-500 flex items-center justify-center text-gray-500"
>
x
</button>
</AlertDialogTrigger>
<AlertDialogContent className="bg-zinc-950">
<AlertDialogHeader>
<AlertDialogTitle className="text-white">
Leave Group?
</AlertDialogTitle>
<AlertDialogDescription className="text-gray-200">
Are you sure you want to leave this group?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.stopPropagation();
onRemove(contact.id, contact.conversation_id);
}}
className="bg-red-600 hover:bg-red-500"
>
Leave Group
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
) : (
<button
onClick={(e) => {
e.stopPropagation();
onRemove(contact.id, contact.conversation_id);
}}
className="flex-shrink-0 w-6 h-6 rounded-full hover:text-red-500 flex items-center justify-center text-gray-700"
>
x
</button>
)}
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center">
{(contact.last_message ?? '').length > 0 ? (
(contact.last_message ?? '').length > 15 ? (
<div className="flex items-center">
{contact.last_message?.substring(0, 15)}
<Ellipsis className="w-3 mt-2" />
</div>
) : (
contact.last_message
)
) : contact.last_message_time ? (
<div className="flex items-center">
<Paperclip className="w-4" /> attachment
</div>
) : null}
</div>
<LastActiveTime contact={contact} />
</div>
</div>
</li>
),
);
export default ContactItem;

View File

@@ -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 }) => (
<li
className="m-1 flex p-2 hover:bg-zinc-900 cursor-pointer transition-colors rounded-lg justify-between items-start min-h-[40px]"
onClick={() => {
initializeContact(contact);
}}
>
<div className="flex flex-col w-full">
<div className="flex items-center justify-between">
<div className="flex items-center">
<span className="text-lg">{contact.username}</span>
{contact.type === 'group' && (
<img
src={GroupIcon}
alt="Group icon"
className="ml-2 invert w-5"
/>
)}
<p className="text-center text-2xl text-red-300 leading-none">
{contact.read ? '' : '•'}
</p>
</div>
{contact.type === 'group' ? (
<AlertDialog>
<AlertDialogTrigger asChild>
<button
onClick={(e) => e.stopPropagation()}
className="flex-shrink-0 w-6 h-6 rounded-full hover:text-red-500 flex items-center justify-center text-gray-500"
>
x
</button>
</AlertDialogTrigger>
<AlertDialogContent className="bg-zinc-950">
<AlertDialogHeader>
<AlertDialogTitle className="text-white">
Leave Group?
</AlertDialogTitle>
<AlertDialogDescription className="text-gray-200">
Are you sure you want to leave this group?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={(e) => {
e.stopPropagation();
removeContact(contact.id, contact.conversation_id);
}}
className="bg-red-600 hover:bg-red-500"
>
Leave Group
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
) : (
<button
onClick={(e) => {
e.stopPropagation();
removeContact(contact.id, contact.conversation_id);
}}
className="flex-shrink-0 w-6 h-6 rounded-full hover:text-red-500 flex items-center justify-center text-gray-700"
>
x
</button>
)}
</div>
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center">
{(contact.last_message ?? '').length > 0 ? (
(contact.last_message ?? '').length > 15 ? (
<div className="flex items-center">
{contact.last_message?.substring(0, 15)}
<Ellipsis className="w-3 mt-2" />
</div>
) : (
contact.last_message
)
) : contact.last_message_time ? (
<div className="flex items-center">
<Paperclip className="w-4" /> attachment
</div>
) : null}
</div>
<LastActiveTime contact={contact} />
</div>
</div>
</li>
);
return (
<div className=" flex-grow overflow-y-auto w-full p-1">
<div className="flex-grow overflow-y-auto w-full p-1">
<ul className="items-center text-center flex-grow-1">
{sortedContacts.map((contact: ContactsProps) => (
<ContactItem key={contact.conversation_id} contact={contact} />
<ContactItem
key={contact.conversation_id}
contact={contact}
onRemove={removeContact}
onSelect={initializeContact}
/>
))}
</ul>
</div>

View File

@@ -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<string | null>(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 <span className="text-xs font-bold text-gray-500">{timeAgo}</span>;
// Use a CSS transition for smooth updates
return (
<span className="text-xs font-bold text-gray-500 transition-opacity duration-200">
{timeAgo}
</span>
);
};
export default LastActiveTime;

View File

@@ -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';

View File

@@ -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';

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB