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,
|
: contact,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
document.title = 'New message ❗';
|
document.title = 'New message 🔔';
|
||||||
} else {
|
} else {
|
||||||
socket?.emit('read message', {
|
socket?.emit('read message', {
|
||||||
conversation_id: msg.conversation_id,
|
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 { useEffect } from 'react';
|
||||||
import { socket } from '@/socket/socket.ts';
|
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 { ContactsProps } from '@/types/types.ts';
|
||||||
import { useChat } from '@/context/chat/useChat.ts';
|
import { useChat } from '@/context/chat/useChat.ts';
|
||||||
import { axiosClient } from '@/utils/axiosClient.ts';
|
import { axiosClient } from '@/utils/axiosClient.ts';
|
||||||
|
import ContactItem from '@/components/chat/leftSidebar/ContactItem.tsx';
|
||||||
|
|
||||||
function ContactsList() {
|
function ContactsList() {
|
||||||
const {
|
const {
|
||||||
@@ -27,6 +14,7 @@ function ContactsList() {
|
|||||||
setMessages,
|
setMessages,
|
||||||
setErrorMessage,
|
setErrorMessage,
|
||||||
} = useChat();
|
} = useChat();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchContacts().catch((e) =>
|
fetchContacts().catch((e) =>
|
||||||
console.error('Failed to fetch contacts: ', e),
|
console.error('Failed to fetch contacts: ', e),
|
||||||
@@ -53,17 +41,14 @@ function ContactsList() {
|
|||||||
console.log('Get contacts list response: ', response.data);
|
console.log('Get contacts list response: ', response.data);
|
||||||
const fetchedContacts: ContactsProps[] = response.data;
|
const fetchedContacts: ContactsProps[] = response.data;
|
||||||
|
|
||||||
const updatedContacts = fetchedContacts.map((contact) => {
|
const updatedContacts = fetchedContacts.map((contact) => ({
|
||||||
if (
|
...contact,
|
||||||
|
read: !(
|
||||||
contact.last_message_id &&
|
contact.last_message_id &&
|
||||||
contact.last_read_message_id &&
|
contact.last_read_message_id &&
|
||||||
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);
|
setContactsList(updatedContacts);
|
||||||
};
|
};
|
||||||
@@ -76,7 +61,7 @@ function ContactsList() {
|
|||||||
'delete contact',
|
'delete contact',
|
||||||
conversation_id,
|
conversation_id,
|
||||||
(response: { status: string; message: string }) => {
|
(response: { status: string; message: string }) => {
|
||||||
if (response.status == 'ok') {
|
if (response.status === 'ok') {
|
||||||
console.log('(socket) response: ', response);
|
console.log('(socket) response: ', response);
|
||||||
setCurrentContact(null);
|
setCurrentContact(null);
|
||||||
localStorage.removeItem('contact');
|
localStorage.removeItem('contact');
|
||||||
@@ -96,122 +81,23 @@ function ContactsList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sortedContacts = [...contactsList].sort((a, b) => {
|
const sortedContacts = [...contactsList].sort((a, b) => {
|
||||||
// First, sort by read status (unread first)
|
if (a.read !== b.read) return a.read ? 1 : -1;
|
||||||
if (a.read !== b.read) {
|
if (a.last_message_time && b.last_message_time) {
|
||||||
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) {
|
|
||||||
return -a.last_message_time.localeCompare(b.last_message_time);
|
return -a.last_message_time.localeCompare(b.last_message_time);
|
||||||
}
|
}
|
||||||
|
return -a.last_active.localeCompare(b.last_active);
|
||||||
// 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 (
|
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">
|
<ul className="items-center text-center flex-grow-1">
|
||||||
{sortedContacts.map((contact: ContactsProps) => (
|
{sortedContacts.map((contact: ContactsProps) => (
|
||||||
<ContactItem key={contact.conversation_id} contact={contact} />
|
<ContactItem
|
||||||
|
key={contact.conversation_id}
|
||||||
|
contact={contact}
|
||||||
|
onRemove={removeContact}
|
||||||
|
onSelect={initializeContact}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { differenceInSeconds, formatDistanceToNowStrict } from 'date-fns';
|
import { differenceInSeconds, formatDistanceToNowStrict } from 'date-fns';
|
||||||
|
|
||||||
import { ContactsProps } from '@/types/types.ts';
|
import { ContactsProps } from '@/types/types.ts';
|
||||||
|
|
||||||
type LastActiveTimeProps = {
|
type LastActiveTimeProps = {
|
||||||
@@ -9,30 +8,49 @@ type LastActiveTimeProps = {
|
|||||||
|
|
||||||
const LastActiveTime = ({ contact }: LastActiveTimeProps) => {
|
const LastActiveTime = ({ contact }: LastActiveTimeProps) => {
|
||||||
const [timeAgo, setTimeAgo] = useState('');
|
const [timeAgo, setTimeAgo] = useState('');
|
||||||
|
const previousTimeRef = useRef('');
|
||||||
|
const previousDateRef = useRef<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateTime = () => {
|
const updateTime = () => {
|
||||||
if (!contact?.last_message_time) {
|
if (!contact?.last_message_time) {
|
||||||
return;
|
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 lastActiveDate = new Date(contact.last_message_time);
|
||||||
const secondsDiff = differenceInSeconds(new Date(), lastActiveDate);
|
const secondsDiff = differenceInSeconds(new Date(), lastActiveDate);
|
||||||
|
|
||||||
|
let newTimeAgo;
|
||||||
if (secondsDiff < 60) {
|
if (secondsDiff < 60) {
|
||||||
setTimeAgo('now');
|
newTimeAgo = 'now';
|
||||||
return;
|
} 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);
|
const intervalId = setInterval(updateTime, 60000);
|
||||||
|
|
||||||
return () => clearInterval(intervalId);
|
return () => clearInterval(intervalId);
|
||||||
}, [contact?.last_message_time]);
|
}, [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;
|
export default LastActiveTime;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
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 { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import LoadingWheel from '../components/chat/LoadingWheel.tsx';
|
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 { useForm, SubmitHandler } from 'react-hook-form';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useContext, useState } from 'react';
|
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