improved perfomance of LastActiveTime.tsx, added 🐢icon
This commit is contained in:
BIN
client/assets/turtle.webp
Normal file
BIN
client/assets/turtle.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -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,
|
||||
|
||||
118
client/src/components/chat/leftSidebar/ContactItem.tsx
Normal file
118
client/src/components/chat/leftSidebar/ContactItem.tsx
Normal 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;
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
BIN
client/turtle.webp/image.webp
Normal file
BIN
client/turtle.webp/image.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
Reference in New Issue
Block a user