code refactor, added time indicator from last message

This commit is contained in:
slawk0
2025-01-02 00:25:31 +01:00
parent 867e465a23
commit 81aeb5a926
17 changed files with 207 additions and 142 deletions

View File

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

View File

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

View File

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

View File

@@ -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();

View File

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

View File

@@ -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[];

View File

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

View File

@@ -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 = {

View File

@@ -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 = {

View File

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

View File

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

View 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;

View File

@@ -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();

View File

@@ -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]);

View File

@@ -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>
);
}

View File

@@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
important: true,
darkMode: ['class'],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {