code refactor, added time indicator from last message
This commit is contained in:
11
client/package-lock.json
generated
11
client/package-lock.json
generated
@@ -20,6 +20,7 @@
|
||||
"axios": "^1.7.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.460.0",
|
||||
"nanoid": "^5.0.8",
|
||||
@@ -3281,6 +3282,16 @@
|
||||
"url": "https://opencollective.com/daisyui"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"axios": "^1.7.7",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lucide-react": "^0.460.0",
|
||||
"nanoid": "^5.0.8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import AttachmentPreview from './AttachmentPreview';
|
||||
import AttachmentPreview from './AttachmentPreview.tsx';
|
||||
import { ChatMessagesProps } from '@/pages/Chat.tsx';
|
||||
|
||||
type AnimatedMessageProps = {
|
||||
@@ -27,9 +27,8 @@ const AnimatedMessage = ({
|
||||
|
||||
return (
|
||||
<li
|
||||
className={`whitespace-pre-wrap ml-2 rounded p-1 group transition-all duration-300 ${
|
||||
message.pending ? 'text-gray-400' : 'hover:bg-gray-900'
|
||||
} ${isRemoving ? 'opacity-0 -translate-x-full' : 'opacity-100'}`}
|
||||
className={` whitespace-pre-wrap ml-2 rounded-lg p-1 group hover:bg-zinc-900
|
||||
${isRemoving ? 'transition-all duration-300 opacity-0 -translate-x-full' : 'opacity-100'}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import LoadingWheel from './LoadingWheel';
|
||||
import FileBox from './FileBox';
|
||||
import LoadingWheel from '../LoadingWheel.tsx';
|
||||
import FileBox from './FileBox.tsx';
|
||||
|
||||
// Cache to keep track of loaded media
|
||||
const loadedMedia = new Set();
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useRef, useCallback, useEffect, useState } from 'react';
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import type { KeyboardEventHandler } from 'react';
|
||||
import { socket } from '../../socket/socket.tsx';
|
||||
import { ChatMessagesProps, ContactsProps } from '../../pages/Chat.tsx';
|
||||
import { axiosClient } from '../../App.tsx';
|
||||
import { socket } from '../../../socket/socket.tsx';
|
||||
import { ChatMessagesProps, ContactsProps } from '../../../pages/Chat.tsx';
|
||||
import { axiosClient } from '../../../App.tsx';
|
||||
import { File, Paperclip, Send, X } from 'lucide-react';
|
||||
import LoadingWheel from '@/components/chat/LoadingWheel.tsx';
|
||||
type Input = {
|
||||
@@ -259,7 +259,7 @@ const MessageForm = ({ contact }: MessageFormProps) => {
|
||||
ref(e);
|
||||
textareaRef.current = e;
|
||||
}}
|
||||
className={` ml-2 w-full overflow-y-hidden resize-none bg-zinc-900 rounded-lg text-white min-h-[40px] max-h-96 placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-emerald-800
|
||||
className={` ml-2 w-full overflow-y-hidden resize-none bg-zinc-900 rounded-lg text-white min-h-[40px] max-h-96 placeholder:text-gray-400 focus:border-1 focus:ring-0 focus:border-emerald-800
|
||||
${isOverLimit ? 'border-2 border-red-500' : isNearLimit ? 'border-2 border-yellow-500' : ''} mx-auto`}
|
||||
autoFocus={!!contact}
|
||||
disabled={!contact}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { socket } from '../../socket/socket.tsx';
|
||||
import { socket } from '@/socket/socket.tsx';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { sendContact } from '../../api/contactsApi.tsx';
|
||||
import LoadingWheel from './LoadingWheel.tsx';
|
||||
import { ChatMessagesProps, MeProps } from '../../pages/Chat.tsx';
|
||||
import { ContactsProps } from '../../pages/Chat.tsx';
|
||||
import { sendContact } from '@/api/contactsApi.tsx';
|
||||
import LoadingWheel from '../LoadingWheel.tsx';
|
||||
import { ChatMessagesProps, MeProps } from '@/pages/Chat.tsx';
|
||||
import { ContactsProps } from '@/pages/Chat.tsx';
|
||||
import { UsernameType } from '@/utils/ProtectedRoutes.tsx';
|
||||
import AnimatedMessage from '@/components/chat/AnimatedMessage.tsx';
|
||||
import AnimatedMessage from '@/components/chat/chatArea/AnimatedMessage.tsx';
|
||||
|
||||
type MessagesAreaProps = {
|
||||
messages: ChatMessagesProps[];
|
||||
@@ -1,10 +1,11 @@
|
||||
import LoadingWheel from './LoadingWheel.tsx';
|
||||
import LoadingWheel from '../LoadingWheel.tsx';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { axiosClient } from '../../App.tsx';
|
||||
import { ContactsProps } from '../../pages/Chat.tsx';
|
||||
import { socket } from '../../socket/socket.tsx';
|
||||
import { axiosClient } from '../../../App.tsx';
|
||||
import { ContactsProps } from '../../../pages/Chat.tsx';
|
||||
import { socket } from '../../../socket/socket.tsx';
|
||||
import { UserRoundPlus } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button.tsx';
|
||||
|
||||
type Inputs = {
|
||||
username: string;
|
||||
@@ -130,10 +131,10 @@ function AddGroupMember({ contact }: AddGroupMemberProps) {
|
||||
<UserRoundPlus />
|
||||
</button>
|
||||
<dialog id="addMemberModal" className="modal" ref={modalRef}>
|
||||
<div className="modal-box bg-gray-800 text-center relative p-1">
|
||||
<div className="modal-box bg-zinc-950 text-center relative p-1 border">
|
||||
<div className="absolute right-2 top-2">
|
||||
<form method="dialog">
|
||||
<button className="btn btn-sm btn-circle btn-ghost">✕</button>
|
||||
<button className=" btn btn-sm btn-circle btn-ghost">✕</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -191,13 +192,13 @@ function AddGroupMember({ contact }: AddGroupMemberProps) {
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex justify-center">
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
className="btn btn-sm bg-green-500 text-black hover:bg-green-600"
|
||||
className="w-24 btn btn-sm bg-emerald-600 text-white hover:bg-green-500 border-black"
|
||||
>
|
||||
{isLoading ? <LoadingWheel /> : 'Add'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -1,7 +1,7 @@
|
||||
import profile from '../../../assets/profile.svg';
|
||||
import profile from '../../../../assets/profile.svg';
|
||||
import CreateGroupButton from './CreateGroupButton.tsx';
|
||||
import AddGroupMember from './AddGroupMember.tsx';
|
||||
import { ContactsProps } from '../../pages/Chat.tsx';
|
||||
import { ContactsProps } from '@/pages/Chat.tsx';
|
||||
import { UsersRound } from 'lucide-react';
|
||||
|
||||
type ContactProfileProps = {
|
||||
@@ -1,7 +1,7 @@
|
||||
import LoadingWheel from './LoadingWheel.tsx';
|
||||
import LoadingWheel from '../LoadingWheel.tsx';
|
||||
import { useRef, useState } from 'react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
import { axiosClient } from '../../App.tsx';
|
||||
import { axiosClient } from '@/App.tsx';
|
||||
import { Plus } from 'lucide-react';
|
||||
|
||||
type Inputs = {
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import { ContactsProps } from '../../pages/Chat.tsx';
|
||||
import { axiosClient } from '../../App.tsx';
|
||||
import { ContactsProps } from '../../../pages/Chat.tsx';
|
||||
import { axiosClient } from '../../../App.tsx';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { useEffect, useState } from 'react';
|
||||
import LoadingWheel from './LoadingWheel.tsx';
|
||||
import LoadingWheel from '../LoadingWheel.tsx';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
type Input = {
|
||||
@@ -93,7 +93,6 @@ function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (suggestions.length > 0) {
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
@@ -112,7 +111,10 @@ function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
|
||||
handleSubmit(submitContact)();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
reset({ contact: '' });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -128,8 +130,8 @@ function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search chats..."
|
||||
className="w-full bg-zinc-900 rounded-lg py-2 pl-10 outline-0 focus:ring-emerald-800"
|
||||
placeholder="Search for user"
|
||||
className="w-full bg-zinc-900 rounded-lg py-2 pl-10 focus:border-1 focus:ring-0 focus:border-emerald-800"
|
||||
onKeyDown={handleKeyDown}
|
||||
{...register('contact', {
|
||||
minLength: 4,
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { ChatMessagesProps, ContactsProps } from '../../pages/Chat.tsx';
|
||||
import { socket } from '../../socket/socket.tsx';
|
||||
import GroupIcon from '../../../assets/group.svg';
|
||||
import { ChatMessagesProps, ContactsProps } from '@/pages/Chat.tsx';
|
||||
import { socket } from '@/socket/socket.tsx';
|
||||
import GroupIcon from '../../../../assets/group.svg';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -12,8 +12,10 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
} from '@/components/ui/alert-dialog.tsx';
|
||||
import { axiosClient } from '@/App.tsx';
|
||||
import { Dot } from 'lucide-react';
|
||||
import LastActiveTime from '@/components/chat/leftSidebar/LastActiveTime.tsx';
|
||||
|
||||
type ContactsListProps = {
|
||||
initializeContact: (contact: ContactsProps) => void;
|
||||
@@ -109,7 +111,7 @@ function ContactsList({
|
||||
updateContactStatus(contact, true);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-grow">
|
||||
<div className="flex">
|
||||
<div className=" flex flex-col">
|
||||
<div className="flex items-center">
|
||||
<span className="text-lg">{contact.username}</span>
|
||||
@@ -121,9 +123,13 @@ function ContactsList({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="text-sm text-gray-500 text-left">
|
||||
{contact.last_message}
|
||||
</span>
|
||||
<Dot className="text-gray-200" />
|
||||
<LastActiveTime contact={contact} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-4 h-4 flex items-center justify-center ml-2">
|
||||
34
client/src/components/chat/leftSidebar/LastActiveTime.tsx
Normal file
34
client/src/components/chat/leftSidebar/LastActiveTime.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { formatDistanceToNow, differenceInSeconds } from 'date-fns';
|
||||
import { ContactsProps } from '@/pages/Chat.tsx';
|
||||
|
||||
type LastActiveTimeProps = {
|
||||
contact: ContactsProps;
|
||||
};
|
||||
|
||||
const LastActiveTime = ({ contact }: LastActiveTimeProps) => {
|
||||
const [timeAgo, setTimeAgo] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const updateTime = () => {
|
||||
const lastActiveDate = new Date(contact.last_message_time);
|
||||
const secondsDiff = differenceInSeconds(new Date(), lastActiveDate);
|
||||
|
||||
if (secondsDiff < 60) {
|
||||
setTimeAgo('now');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeAgo(formatDistanceToNow(lastActiveDate));
|
||||
};
|
||||
updateTime();
|
||||
|
||||
const intervalId = setInterval(updateTime, 60000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [contact?.last_message]);
|
||||
|
||||
return <span className="text-sm text-gray-500">{timeAgo}</span>;
|
||||
};
|
||||
|
||||
export default LastActiveTime;
|
||||
@@ -1,8 +1,8 @@
|
||||
import zdjecie from '../../../assets/turtleProfileImg3.webp';
|
||||
import logoutIcon from '../../../assets/logout.svg';
|
||||
import zdjecie from '../../../../assets/turtleProfileImg3.webp';
|
||||
import logoutIcon from '../../../../assets/logout.svg';
|
||||
import Cookies from 'js-cookie';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { UsernameType } from '../../utils/ProtectedRoutes.tsx';
|
||||
import { UsernameType } from '../../../utils/ProtectedRoutes.tsx';
|
||||
|
||||
function UserProfile() {
|
||||
const user: UsernameType = useOutletContext();
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { axiosClient } from '@/App.tsx';
|
||||
import { ContactsProps, MeProps } from '@/pages/Chat.tsx';
|
||||
import { socket } from '@/socket/socket.tsx';
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
} from '@/components/ui/context-menu';
|
||||
} from '@/components/ui/context-menu.tsx';
|
||||
import { UsernameType } from '@/utils/ProtectedRoutes.tsx';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import zdjecie from '../../../assets/turtleProfileImg3.webp';
|
||||
import zdjecie from '../../../../assets/turtleProfileImg3.webp';
|
||||
type ParticipantsProps = {
|
||||
user_id: string;
|
||||
username: string;
|
||||
@@ -23,18 +23,20 @@ type ParticipantsBarProps = {
|
||||
initializeContact: (contact: ContactsProps) => void;
|
||||
currentContact: ContactsProps | null;
|
||||
setMe: React.Dispatch<React.SetStateAction<MeProps>>;
|
||||
me: MeProps;
|
||||
setGroupOwner: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
groupOwner: string | undefined;
|
||||
};
|
||||
|
||||
function ParticipantsBar({
|
||||
initializeContact,
|
||||
currentContact,
|
||||
setMe,
|
||||
me,
|
||||
setGroupOwner,
|
||||
groupOwner,
|
||||
}: ParticipantsBarProps) {
|
||||
const [participants, setParticipants] = useState<ParticipantsProps[]>([]);
|
||||
const user: UsernameType = useOutletContext();
|
||||
|
||||
const me = useContext(MeContext);
|
||||
const getParticipants = async () => {
|
||||
try {
|
||||
const response = await axiosClient.get(
|
||||
@@ -116,7 +118,10 @@ function ParticipantsBar({
|
||||
(participant) =>
|
||||
participant.user_id === user.user_id && participant.isowner,
|
||||
);
|
||||
|
||||
const whoIsOwner = participants.find(
|
||||
(participant) => participant.isowner,
|
||||
);
|
||||
setGroupOwner(whoIsOwner?.user_id);
|
||||
setMe({ isGroupAdmin: userIsAdmin, isGroupOwner: userIsOwner });
|
||||
}
|
||||
}, [participants, user?.user_id]);
|
||||
@@ -1,15 +1,15 @@
|
||||
import MessageForm from '../components/chat/MessageForm.tsx';
|
||||
import ContactProfile from '../components/chat/ContactProfile.tsx';
|
||||
import UserProfile from '../components/chat/UserProfile.tsx';
|
||||
import ContactForm from '../components/chat/ContactForm.tsx';
|
||||
import MessagesArea from '../components/chat/MessagesArea.tsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ContactsList from '../components/chat/ContactsList.tsx';
|
||||
import MessageForm from '../components/chat/chatArea/MessageForm.tsx';
|
||||
import ContactProfile from '../components/chat/chatHeader/ContactProfile.tsx';
|
||||
import UserProfile from '../components/chat/leftSidebar/UserProfile.tsx';
|
||||
import ContactForm from '../components/chat/leftSidebar/ContactForm.tsx';
|
||||
import MessagesArea from '../components/chat/chatArea/MessagesArea.tsx';
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
import ContactsList from '../components/chat/leftSidebar/ContactsList.tsx';
|
||||
import { initializeSocket, joinRoom } from '../socket/socket.tsx';
|
||||
import Cookies from 'js-cookie';
|
||||
import { getMessages, setContactStatus } from '../api/contactsApi.tsx';
|
||||
import axios from 'axios';
|
||||
import ParticipantsBar from '@/components/chat/ParticipantsBar.tsx';
|
||||
import ParticipantsBar from '@/components/chat/rightSidebar/ParticipantsBar.tsx';
|
||||
|
||||
export type MeProps = {
|
||||
isGroupAdmin: boolean;
|
||||
@@ -21,7 +21,6 @@ export type ChatMessagesProps = {
|
||||
message: string;
|
||||
recipient: string; // conversation_id
|
||||
message_id: number;
|
||||
pending: boolean;
|
||||
attachment_urls: string[] | null;
|
||||
sender_id: string;
|
||||
conversation_id: string;
|
||||
@@ -42,6 +41,10 @@ export type ContactsProps = {
|
||||
};
|
||||
|
||||
function Chat() {
|
||||
const meDefaultValue = {
|
||||
isGroupAdmin: false,
|
||||
isGroupOwner: false,
|
||||
};
|
||||
const [contactsList, setContactsList] = useState<ContactsProps[]>([]);
|
||||
const [currentContact, setCurrentContact] = useState<ContactsProps | null>(
|
||||
null,
|
||||
@@ -50,10 +53,10 @@ function Chat() {
|
||||
const [messages, setMessages] = useState<ChatMessagesProps[]>([]);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [hasMoreMessages, setHasMoreMessages] = useState<boolean>(true);
|
||||
const [me, setMe] = useState<MeProps>({
|
||||
isGroupAdmin: false,
|
||||
isGroupOwner: false,
|
||||
});
|
||||
const [me, setMe] = useState<MeProps>(meDefaultValue);
|
||||
const MeContext = createContext(meDefaultValue);
|
||||
|
||||
const [groupOwner, setGroupOwner] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const token = Cookies.get('token');
|
||||
@@ -206,6 +209,7 @@ function Chat() {
|
||||
}
|
||||
|
||||
return (
|
||||
<MeContext.Provider value={me}>
|
||||
<div className="text-white flex h-screen">
|
||||
{/* Left Sidebar */}
|
||||
<div className="w-64 h-screen flex-shrink-0 flex flex-col bg-zinc-950 text-center border-r border-zinc-800">
|
||||
@@ -245,7 +249,6 @@ function Chat() {
|
||||
fetchPreviousMessages={fetchPreviousMessages}
|
||||
errorMessage={errorMessage}
|
||||
contactsList={contactsList}
|
||||
me={me}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-shrink-0 p-3 border-t border-zinc-800">
|
||||
@@ -260,15 +263,17 @@ function Chat() {
|
||||
<div className="w-64 flex-shrink-0 border-l border-zinc-800 bg-zinc-950">
|
||||
<ParticipantsBar
|
||||
setMe={setMe}
|
||||
me={me}
|
||||
initializeContact={initializeContact}
|
||||
currentContact={currentContact}
|
||||
setGroupOwner={setGroupOwner}
|
||||
groupOwner={groupOwner}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
important: true,
|
||||
darkMode: ['class'],
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
|
||||
Reference in New Issue
Block a user