diff --git a/client/src/components/chat/AttachmentPreview.tsx b/client/src/components/chat/chatArea/AttachmentPreview.tsx
similarity index 96%
rename from client/src/components/chat/AttachmentPreview.tsx
rename to client/src/components/chat/chatArea/AttachmentPreview.tsx
index 66a94f8..09e7cc2 100644
--- a/client/src/components/chat/AttachmentPreview.tsx
+++ b/client/src/components/chat/chatArea/AttachmentPreview.tsx
@@ -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();
diff --git a/client/src/components/chat/FileBox.tsx b/client/src/components/chat/chatArea/FileBox.tsx
similarity index 100%
rename from client/src/components/chat/FileBox.tsx
rename to client/src/components/chat/chatArea/FileBox.tsx
diff --git a/client/src/components/chat/MessageForm.tsx b/client/src/components/chat/chatArea/MessageForm.tsx
similarity index 97%
rename from client/src/components/chat/MessageForm.tsx
rename to client/src/components/chat/chatArea/MessageForm.tsx
index 9f39c8d..a47dfa8 100644
--- a/client/src/components/chat/MessageForm.tsx
+++ b/client/src/components/chat/chatArea/MessageForm.tsx
@@ -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}
diff --git a/client/src/components/chat/MessagesArea.tsx b/client/src/components/chat/chatArea/MessagesArea.tsx
similarity index 95%
rename from client/src/components/chat/MessagesArea.tsx
rename to client/src/components/chat/chatArea/MessagesArea.tsx
index c0a5ea5..3a1d834 100644
--- a/client/src/components/chat/MessagesArea.tsx
+++ b/client/src/components/chat/chatArea/MessagesArea.tsx
@@ -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[];
diff --git a/client/src/components/chat/AddGroupMember.tsx b/client/src/components/chat/chatHeader/AddGroupMember.tsx
similarity index 91%
rename from client/src/components/chat/AddGroupMember.tsx
rename to client/src/components/chat/chatHeader/AddGroupMember.tsx
index 6954a4a..d755d0a 100644
--- a/client/src/components/chat/AddGroupMember.tsx
+++ b/client/src/components/chat/chatHeader/AddGroupMember.tsx
@@ -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) {
diff --git a/client/src/components/chat/ContactProfile.tsx b/client/src/components/chat/chatHeader/ContactProfile.tsx
similarity index 89%
rename from client/src/components/chat/ContactProfile.tsx
rename to client/src/components/chat/chatHeader/ContactProfile.tsx
index fabd155..093159e 100644
--- a/client/src/components/chat/ContactProfile.tsx
+++ b/client/src/components/chat/chatHeader/ContactProfile.tsx
@@ -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 = {
diff --git a/client/src/components/chat/CreateGroupButton.tsx b/client/src/components/chat/chatHeader/CreateGroupButton.tsx
similarity index 97%
rename from client/src/components/chat/CreateGroupButton.tsx
rename to client/src/components/chat/chatHeader/CreateGroupButton.tsx
index a497aa9..12b64c9 100644
--- a/client/src/components/chat/CreateGroupButton.tsx
+++ b/client/src/components/chat/chatHeader/CreateGroupButton.tsx
@@ -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 = {
diff --git a/client/src/components/chat/ContactForm.tsx b/client/src/components/chat/leftSidebar/ContactForm.tsx
similarity index 85%
rename from client/src/components/chat/ContactForm.tsx
rename to client/src/components/chat/leftSidebar/ContactForm.tsx
index b6f6815..e6d2ddf 100644
--- a/client/src/components/chat/ContactForm.tsx
+++ b/client/src/components/chat/leftSidebar/ContactForm.tsx
@@ -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,26 +93,28 @@ function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
};
const handleKeyDown = (e: React.KeyboardEvent
) => {
- if (suggestions.length > 0) {
- switch (e.key) {
- case 'ArrowDown':
+ switch (e.key) {
+ case 'ArrowDown':
+ e.preventDefault();
+ setSelectedIndex((prev) => (prev + 1) % suggestions.length);
+ break;
+ case 'ArrowUp':
+ e.preventDefault();
+ setSelectedIndex(
+ (prev) => (prev - 1 + suggestions.length) % suggestions.length,
+ );
+ break;
+ case 'Enter':
+ if (suggestions.length > 0) {
e.preventDefault();
- setSelectedIndex((prev) => (prev + 1) % suggestions.length);
- break;
- case 'ArrowUp':
- e.preventDefault();
- setSelectedIndex(
- (prev) => (prev - 1 + suggestions.length) % suggestions.length,
- );
- break;
- case 'Enter':
- if (suggestions.length > 0) {
- e.preventDefault();
- reset({ contact: suggestions[selectedIndex] });
- handleSubmit(submitContact)();
- }
- break;
- }
+ reset({ contact: suggestions[selectedIndex] });
+ handleSubmit(submitContact)();
+ }
+ break;
+ case 'Escape':
+ e.preventDefault();
+ reset({ contact: '' });
+ break;
}
};
@@ -128,8 +130,8 @@ function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
void;
@@ -109,7 +111,7 @@ function ContactsList({
updateContactStatus(contact, true);
}}
>
-
+
{contact.username}
@@ -121,9 +123,13 @@ function ContactsList({
/>
)}
-
- {contact.last_message}
-
+
+
+ {contact.last_message}
+
+
+
+
diff --git a/client/src/components/chat/leftSidebar/LastActiveTime.tsx b/client/src/components/chat/leftSidebar/LastActiveTime.tsx
new file mode 100644
index 0000000..1ee0562
--- /dev/null
+++ b/client/src/components/chat/leftSidebar/LastActiveTime.tsx
@@ -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
{timeAgo};
+};
+
+export default LastActiveTime;
diff --git a/client/src/components/chat/UserProfile.tsx b/client/src/components/chat/leftSidebar/UserProfile.tsx
similarity index 88%
rename from client/src/components/chat/UserProfile.tsx
rename to client/src/components/chat/leftSidebar/UserProfile.tsx
index eb14b32..563f473 100644
--- a/client/src/components/chat/UserProfile.tsx
+++ b/client/src/components/chat/leftSidebar/UserProfile.tsx
@@ -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();
diff --git a/client/src/components/chat/ParticipantsBar.tsx b/client/src/components/chat/rightSidebar/ParticipantsBar.tsx
similarity index 95%
rename from client/src/components/chat/ParticipantsBar.tsx
rename to client/src/components/chat/rightSidebar/ParticipantsBar.tsx
index 4d2a9c7..1852a42 100644
--- a/client/src/components/chat/ParticipantsBar.tsx
+++ b/client/src/components/chat/rightSidebar/ParticipantsBar.tsx
@@ -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
>;
- me: MeProps;
+ setGroupOwner: React.Dispatch>;
+ groupOwner: string | undefined;
};
function ParticipantsBar({
initializeContact,
currentContact,
setMe,
- me,
+ setGroupOwner,
+ groupOwner,
}: ParticipantsBarProps) {
const [participants, setParticipants] = useState([]);
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]);
diff --git a/client/src/pages/Chat.tsx b/client/src/pages/Chat.tsx
index ff3f1a5..f3808e5 100644
--- a/client/src/pages/Chat.tsx
+++ b/client/src/pages/Chat.tsx
@@ -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([]);
const [currentContact, setCurrentContact] = useState(
null,
@@ -50,10 +53,10 @@ function Chat() {
const [messages, setMessages] = useState([]);
const [errorMessage, setErrorMessage] = useState(null);
const [hasMoreMessages, setHasMoreMessages] = useState(true);
- const [me, setMe] = useState({
- isGroupAdmin: false,
- isGroupOwner: false,
- });
+ const [me, setMe] = useState(meDefaultValue);
+ const MeContext = createContext(meDefaultValue);
+
+ const [groupOwner, setGroupOwner] = useState();
useEffect(() => {
const token = Cookies.get('token');
@@ -206,69 +209,71 @@ function Chat() {
}
return (
-
- {/* Left Sidebar */}
-
-
-
-
-
+
+
+ {/* Left Sidebar */}
+
+
+
+
+
- {/*Chat area */}
-
-
- {/* Messages Container and Participants Container */}
-
-
-
-
-
-
-
-
- {currentContact && currentContact.username?.length >= 4 ? (
-
- ) : null}
+ {/*Chat area */}
+
+
+ {/* Messages Container and Participants Container */}
+
+
+
+
+
+
+
+
+ {currentContact && currentContact.username?.length >= 4 ? (
+
+ ) : null}
+
+
+ {/* Right Sidebar - Participants */}
+ {currentContact?.type === 'group' && (
+
+ )}
-
- {/* Right Sidebar - Participants */}
- {currentContact?.type === 'group' && (
-
- )}
-
+
);
}
diff --git a/client/tailwind.config.js b/client/tailwind.config.js
index 3689267..17a195c 100644
--- a/client/tailwind.config.js
+++ b/client/tailwind.config.js
@@ -1,5 +1,6 @@
/** @type {import('tailwindcss').Config} */
export default {
+ important: true,
darkMode: ['class'],
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {