From 9295b2f04960ccd0233ade89bf1c5d7596e26176 Mon Sep 17 00:00:00 2001 From: slawk0 Date: Thu, 2 Jan 2025 14:04:54 +0100 Subject: [PATCH] code refactor, created ChatProvider.tsx, use context instead of tons of props --- client/src/App.tsx | 19 +- client/src/api/contactsApi.tsx | 3 +- .../components/chat/chatArea/MessageForm.tsx | 31 ++- .../components/chat/chatArea/MessagesArea.tsx | 46 ++-- .../chat/chatHeader/AddGroupMember.tsx | 48 ++-- .../chat/chatHeader/ContactProfile.tsx | 19 +- .../chat/chatHeader/CreateGroupButton.tsx | 2 +- .../chat/leftSidebar/ContactForm.tsx | 13 +- .../chat/leftSidebar/ContactsList.tsx | 39 ++- .../chat/rightSidebar/ParticipantsBar.tsx | 32 +-- client/src/context/ContactContext.tsx | 0 client/src/context/chat/ChatContext.tsx | 6 + client/src/context/chat/ChatProvider.tsx | 181 +++++++++++++ client/src/context/chat/useChat.ts | 11 + client/src/pages/Chat.tsx | 249 +++--------------- client/src/pages/Login.tsx | 3 +- client/src/pages/Signup.tsx | 2 +- client/src/types/types.ts | 43 ++- client/src/utils/ProtectedRoutes.tsx | 2 +- client/src/utils/axiosClient.ts | 5 + client/src/utils/useAuth.tsx | 3 +- 21 files changed, 383 insertions(+), 374 deletions(-) delete mode 100644 client/src/context/ContactContext.tsx create mode 100644 client/src/context/chat/ChatContext.tsx create mode 100644 client/src/context/chat/ChatProvider.tsx create mode 100644 client/src/context/chat/useChat.ts create mode 100644 client/src/utils/axiosClient.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 69f6be8..a06f1f1 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,7 @@ import { - RouterProvider, createBrowserRouter, Navigate, + RouterProvider, } from 'react-router-dom'; import { useEffect, useState } from 'react'; import Chat from './pages/Chat.tsx'; @@ -12,12 +12,9 @@ import Lost from './pages/404.tsx'; import { AuthContext } from './utils/AuthProvider.tsx'; import ProtectedRoutes from './utils/ProtectedRoutes.tsx'; import PublicRoute from '@/utils/PublicRoute.tsx'; -import axios from 'axios'; import Cookies from 'js-cookie'; - -export const axiosClient = axios.create({ - baseURL: '/', -}); +import { ChatProvider } from '@/context/chat/ChatProvider.tsx'; +import { axiosClient } from '@/utils/axiosClient.ts'; const router = createBrowserRouter([ { @@ -29,7 +26,11 @@ const router = createBrowserRouter([ children: [ { path: '/chat', - element: , + element: ( + + + + ), }, { path: '/settings', @@ -57,14 +58,12 @@ const router = createBrowserRouter([ ]); function App() { - // Check for token immediately and set initial states accordingly const hasToken = Boolean(Cookies.get('token')); const [authorized, setAuthorized] = useState(false); - const [isLoading, setIsLoading] = useState(hasToken); // Only start loading if there's a token + const [isLoading, setIsLoading] = useState(true); useEffect(() => { async function validateToken() { - // If there's no token, we're already in the correct state if (!hasToken) return; try { diff --git a/client/src/api/contactsApi.tsx b/client/src/api/contactsApi.tsx index 1e63c7b..9000f95 100644 --- a/client/src/api/contactsApi.tsx +++ b/client/src/api/contactsApi.tsx @@ -1,6 +1,5 @@ -import { axiosClient } from '../App.tsx'; - import { ChatMessagesProps, ContactsProps } from '@/types/types.ts'; +import { axiosClient } from '@/utils/axiosClient.ts'; export async function getContactsList(): Promise { try { diff --git a/client/src/components/chat/chatArea/MessageForm.tsx b/client/src/components/chat/chatArea/MessageForm.tsx index 740762f..8203241 100644 --- a/client/src/components/chat/chatArea/MessageForm.tsx +++ b/client/src/components/chat/chatArea/MessageForm.tsx @@ -1,17 +1,20 @@ import type { KeyboardEventHandler } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { socket } from '../../../socket/socket.tsx'; -import { axiosClient } from '../../../App.tsx'; +import { socket } from '@/socket/socket.tsx'; import { File, Paperclip, Send, X } from 'lucide-react'; import LoadingWheel from '@/components/chat/LoadingWheel.tsx'; -import { - FileWithPreviewProps, - InputProps, - MessageFormProps, -} from '@/types/types.ts'; +import { FileWithPreviewProps } from '@/types/types.ts'; +import { useChat } from '@/context/chat/useChat.ts'; +import { axiosClient } from '@/utils/axiosClient.ts'; -const MessageForm = ({ contact }: MessageFormProps) => { +export type InputProps = { + message: string; + attachments: FileList | null; +}; + +const MessageForm = () => { + const { currentContact } = useChat(); const [files, setFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [isSending, setIsSending] = useState(false); @@ -130,9 +133,9 @@ const MessageForm = ({ contact }: MessageFormProps) => { 'chat message', { message: data.message.trim(), - recipient: contact.conversation_id, + recipient: currentContact?.conversation_id, attachment_urls: attachmentUrls, - recipient_id: contact.user_id, + recipient_id: currentContact?.user_id, }, (response: { status: string; message: string }) => { if (response.status === 'ok') { @@ -148,9 +151,9 @@ const MessageForm = ({ contact }: MessageFormProps) => { ); console.log('sent: ', { message: data.message.trim(), - recipient: contact.conversation_id, + recipient: currentContact?.conversation_id, attachment_urls: attachmentUrls, - recipient_id: contact.user_id, + recipient_id: currentContact?.user_id, }); }; @@ -252,8 +255,8 @@ const MessageForm = ({ contact }: MessageFormProps) => { }} 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} + autoFocus={!!currentContact} + disabled={!currentContact} placeholder="Enter message" onKeyDown={handleKeyPress} rows={1} diff --git a/client/src/components/chat/chatArea/MessagesArea.tsx b/client/src/components/chat/chatArea/MessagesArea.tsx index 6adc663..30da247 100644 --- a/client/src/components/chat/chatArea/MessagesArea.tsx +++ b/client/src/components/chat/chatArea/MessagesArea.tsx @@ -4,37 +4,21 @@ import { useOutletContext } from 'react-router-dom'; import { sendContact } from '@/api/contactsApi.tsx'; import LoadingWheel from '../LoadingWheel.tsx'; import AnimatedMessage from '@/components/chat/chatArea/AnimatedMessage.tsx'; -import { - ChatMessagesProps, - ContactsProps, - MeProps, - UsernameType, -} from '@/types/types.ts'; +import { ChatMessagesProps, UsernameType } from '@/types/types.ts'; +import { useChat } from '@/context/chat/useChat.ts'; -type MessagesAreaProps = { - messages: ChatMessagesProps[]; - setMessages: React.Dispatch>; - currentContact: ContactsProps | null; - updateContactStatus: (contact: ContactsProps, read: boolean) => void; - messageHandler: (msg: ChatMessagesProps) => void; - setContactsList: React.Dispatch>; - fetchPreviousMessages: (contact: string | null) => Promise; - errorMessage: string | null; - contactsList: ContactsProps[]; - me: MeProps; -}; - -function MessagesArea({ - messages, - currentContact, - updateContactStatus, - setContactsList, - messageHandler, - fetchPreviousMessages, - errorMessage, - setMessages, - me, -}: MessagesAreaProps) { +function MessagesArea() { + const { + messages, + setMessages, + currentContact, + updateContactStatus, + messageHandler, + setContactsList, + errorMessage, + fetchPreviousMessages, + me, + } = useChat(); const containerRef = useRef(null); const user: UsernameType = useOutletContext(); const [isLoading, setIsLoading] = useState(false); @@ -231,7 +215,7 @@ function MessagesArea({ useEffect(() => { scrollToBottom(); - }, []); + }); return (
diff --git a/client/src/components/chat/chatHeader/AddGroupMember.tsx b/client/src/components/chat/chatHeader/AddGroupMember.tsx index 48dadb1..3c58632 100644 --- a/client/src/components/chat/chatHeader/AddGroupMember.tsx +++ b/client/src/components/chat/chatHeader/AddGroupMember.tsx @@ -1,21 +1,19 @@ import LoadingWheel from '../LoadingWheel.tsx'; import { useEffect, useRef, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { axiosClient } from '../../../App.tsx'; -import { socket } from '../../../socket/socket.tsx'; +import { socket } from '@/socket/socket.tsx'; import { UserRoundPlus } from 'lucide-react'; import { Button } from '@/components/ui/button.tsx'; -import { ContactsProps } from '@/types/types.ts'; +import { useChat } from '@/context/chat/useChat.ts'; +import { axiosClient } from '@/utils/axiosClient.ts'; +import axios from 'axios'; type Inputs = { username: string; }; -interface AddGroupMemberProps { - contact?: ContactsProps; -} - -function AddGroupMember({ contact }: AddGroupMemberProps) { +function AddGroupMember() { + const { currentContact } = useChat(); const { register, handleSubmit, watch, reset } = useForm(); const modalRef = useRef(null); const contactInput = watch('username'); @@ -43,12 +41,16 @@ function AddGroupMember({ contact }: AddGroupMemberProps) { setIsLoading(false); setNotFound(false); } - } catch (e: any) { - setIsLoading(false); - console.error('Error fetching suggestions:', e); - setErrorMessage( - e.response?.data?.message || 'Failed to fetch suggestions', - ); + } catch (e) { + if (axios.isAxiosError(e)) { + setIsLoading(false); + console.error('Error fetching suggestions:', e); + setErrorMessage( + e.response?.data?.message || 'Failed to fetch suggestions', + ); + } else { + console.error('Unexpected error occurred'); + } } } else { setNotFound(false); @@ -76,20 +78,26 @@ function AddGroupMember({ contact }: AddGroupMemberProps) { setIsLoading(true); setErrorMessage(null); const response = await axiosClient.post(`/api/chat/groups/addMember/`, { - group_id: contact?.conversation_id, + group_id: currentContact?.conversation_id, username: contactToSubmit, }); console.log('Add member to group', response); setIsLoading(false); - socket?.emit('added to group', { group_id: contact?.conversation_id }); + socket?.emit('added to group', { + group_id: currentContact?.conversation_id, + }); if (modalRef.current) { modalRef.current.close(); } reset(); - } catch (e: any) { - console.error('Failed to add group member: ', e); - setIsLoading(false); - setErrorMessage(e.response?.data?.message || 'Failed to add member'); + } catch (e) { + if (axios.isAxiosError(e)) { + console.error('Failed to add group member: ', e); + setIsLoading(false); + setErrorMessage(e.response?.data?.message || 'Failed to add member'); + } else { + console.error('Unexpected error occurred'); + } } }; diff --git a/client/src/components/chat/chatHeader/ContactProfile.tsx b/client/src/components/chat/chatHeader/ContactProfile.tsx index c492719..21b15cb 100644 --- a/client/src/components/chat/chatHeader/ContactProfile.tsx +++ b/client/src/components/chat/chatHeader/ContactProfile.tsx @@ -2,34 +2,27 @@ import profile from '../../../../assets/profile.svg'; import CreateGroupButton from './CreateGroupButton.tsx'; import AddGroupMember from './AddGroupMember.tsx'; import { UsersRound } from 'lucide-react'; -import { ContactsProps } from '@/types/types.ts'; +import { useChat } from '@/context/chat/useChat.ts'; -type ContactProfileProps = { - contact: ContactsProps | null; -}; - -function ContactProfile({ contact }: ContactProfileProps) { +function ContactProfile() { + const { currentContact } = useChat(); return (
- {contact?.type === 'group' ? ( + {currentContact?.type === 'group' ? ( ) : ( profile img )} -

{contact ? contact.username : null}

+

{currentContact ? currentContact.username : null}

-
- {contact?.type === 'group' ? ( - - ) : null} -
+
{currentContact?.type === 'group' ? : null}
); } diff --git a/client/src/components/chat/chatHeader/CreateGroupButton.tsx b/client/src/components/chat/chatHeader/CreateGroupButton.tsx index 12b64c9..6fe2298 100644 --- a/client/src/components/chat/chatHeader/CreateGroupButton.tsx +++ b/client/src/components/chat/chatHeader/CreateGroupButton.tsx @@ -1,8 +1,8 @@ import LoadingWheel from '../LoadingWheel.tsx'; import { useRef, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { axiosClient } from '@/App.tsx'; import { Plus } from 'lucide-react'; +import { axiosClient } from '@/utils/axiosClient.ts'; type Inputs = { groupName: string; diff --git a/client/src/components/chat/leftSidebar/ContactForm.tsx b/client/src/components/chat/leftSidebar/ContactForm.tsx index a3b6eb3..b78b43e 100644 --- a/client/src/components/chat/leftSidebar/ContactForm.tsx +++ b/client/src/components/chat/leftSidebar/ContactForm.tsx @@ -1,21 +1,18 @@ import { useForm, SubmitHandler } from 'react-hook-form'; -import { axiosClient } from '../../../App.tsx'; import { AxiosResponse } from 'axios'; import { useEffect, useState } from 'react'; import LoadingWheel from '../LoadingWheel.tsx'; import { Search } from 'lucide-react'; import { ContactsProps } from '@/types/types.ts'; +import { useChat } from '@/context/chat/useChat.ts'; +import { axiosClient } from '@/utils/axiosClient.ts'; type Input = { contact: string; }; -type ContactFormProps = { - InitializeContact: (contact: ContactsProps) => void; - setContactsList: React.Dispatch>; -}; - -function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) { +function ContactForm() { + const { setContactsList, initializeContact } = useChat(); const { register, handleSubmit, reset, watch } = useForm(); const contactInput = watch('contact'); const [suggestions, setSuggestions] = useState([]); @@ -76,7 +73,7 @@ function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) { ); console.log('contact post response: ', response.data); - InitializeContact(response.data); + initializeContact(response.data); setContactsList((prevContacts) => { if (!prevContacts.some((c) => c.username === contactToSubmit)) { diff --git a/client/src/components/chat/leftSidebar/ContactsList.tsx b/client/src/components/chat/leftSidebar/ContactsList.tsx index c78ceff..11d8a4b 100644 --- a/client/src/components/chat/leftSidebar/ContactsList.tsx +++ b/client/src/components/chat/leftSidebar/ContactsList.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { socket } from '@/socket/socket.tsx'; import GroupIcon from '../../../../assets/group.svg'; import { @@ -12,36 +12,27 @@ import { AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog.tsx'; -import { axiosClient } from '@/App.tsx'; import { Dot } from 'lucide-react'; import LastActiveTime from '@/components/chat/leftSidebar/LastActiveTime.tsx'; -import { ChatMessagesProps, ContactsProps } from '@/types/types.ts'; +import { ContactsProps } from '@/types/types.ts'; +import { useChat } from '@/context/chat/useChat.ts'; +import { axiosClient } from '@/utils/axiosClient.ts'; -type ContactsListProps = { - initializeContact: (contact: ContactsProps) => void; - setContactsList: React.Dispatch>; - contactsList: ContactsProps[]; - setCurrentContact: React.Dispatch>; - updateContactStatus: (contactObj: ContactsProps, read: boolean) => void; - setMessages: React.Dispatch>; - currentContact: ContactsProps | null; - setErrorMessage: React.Dispatch>; -}; - -function ContactsList({ - initializeContact, - contactsList, - setContactsList, - setCurrentContact, - updateContactStatus, - setMessages, - setErrorMessage, -}: ContactsListProps) { +function ContactsList() { + const { + initializeContact, + contactsList, + setContactsList, + setCurrentContact, + updateContactStatus, + setMessages, + setErrorMessage, + } = useChat(); useEffect(() => { fetchContacts().catch((e) => console.error('Failed to fetch contacts: ', e), ); - }, []); + }); useEffect(() => { if (!socket) return; diff --git a/client/src/components/chat/rightSidebar/ParticipantsBar.tsx b/client/src/components/chat/rightSidebar/ParticipantsBar.tsx index 5280f40..e60d6e2 100644 --- a/client/src/components/chat/rightSidebar/ParticipantsBar.tsx +++ b/client/src/components/chat/rightSidebar/ParticipantsBar.tsx @@ -1,5 +1,4 @@ -import { useContext, useEffect, useMemo, useState } from 'react'; -import { axiosClient } from '@/App.tsx'; +import { useEffect, useMemo, useState } from 'react'; import { socket } from '@/socket/socket.tsx'; import { Crown, Sword } from 'lucide-react'; import { @@ -10,32 +9,15 @@ import { } from '@/components/ui/context-menu.tsx'; import { useOutletContext } from 'react-router-dom'; import zdjecie from '../../../../assets/turtleProfileImg3.webp'; -import { ContactsProps, MeProps, UsernameType } from '@/types/types.ts'; -type ParticipantsProps = { - user_id: string; - username: string; - isadmin: boolean; - isowner: boolean; -}; +import { ParticipantsProps, UsernameType } from '@/types/types.ts'; +import { useChat } from '@/context/chat/useChat.ts'; +import { axiosClient } from '@/utils/axiosClient.ts'; -type ParticipantsBarProps = { - initializeContact: (contact: ContactsProps) => void; - currentContact: ContactsProps | null; - setMe: React.Dispatch>; - setGroupOwner: React.Dispatch>; - groupOwner: string | undefined; -}; - -function ParticipantsBar({ - initializeContact, - currentContact, - setMe, - setGroupOwner, - groupOwner, -}: ParticipantsBarProps) { +function ParticipantsBar() { + const { setMe, initializeContact, setGroupOwner, currentContact, me } = + useChat(); const [participants, setParticipants] = useState([]); const user: UsernameType = useOutletContext(); - const me = useContext(MeContext); const getParticipants = async () => { try { const response = await axiosClient.get( diff --git a/client/src/context/ContactContext.tsx b/client/src/context/ContactContext.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/context/chat/ChatContext.tsx b/client/src/context/chat/ChatContext.tsx new file mode 100644 index 0000000..bc88dc8 --- /dev/null +++ b/client/src/context/chat/ChatContext.tsx @@ -0,0 +1,6 @@ +import { createContext } from 'react'; +import { ChatContextType } from '@/types/types.ts'; + +export const ChatContext = createContext( + undefined, +); diff --git a/client/src/context/chat/ChatProvider.tsx b/client/src/context/chat/ChatProvider.tsx new file mode 100644 index 0000000..c3156ea --- /dev/null +++ b/client/src/context/chat/ChatProvider.tsx @@ -0,0 +1,181 @@ +import { ChatContext } from '@/context/chat/ChatContext.tsx'; +import { ReactNode, useState } from 'react'; +import { ChatMessagesProps, ContactsProps, MeProps } from '@/types/types.ts'; +import { joinRoom } from '@/socket/socket.tsx'; +import { getMessages, setContactStatus } from '@/api/contactsApi.tsx'; +import axios from 'axios'; + +export const ChatProvider = ({ children }: { children: ReactNode }) => { + const [contactsList, setContactsList] = useState([]); + const [currentContact, setCurrentContact] = useState( + null, + ); + const [cursor, setCursor] = useState(0); + const [messages, setMessages] = useState([]); + const [errorMessage, setErrorMessage] = useState(null); + const [hasMoreMessages, setHasMoreMessages] = useState(true); + const [me, setMe] = useState({ + isGroupAdmin: false, + isGroupOwner: false, + }); + const [groupOwner, setGroupOwner] = useState(); + + async function initializeContact(newContact: ContactsProps) { + setMessages([]); // Clear messages from previous contact + localStorage.setItem('contact', JSON.stringify(newContact)); // Set current contact in localstorage + setCurrentContact(newContact); + console.log('Initialized contact: ', newContact); + try { + const joinResult = await joinRoom(newContact.conversation_id); + if (!joinResult.success) { + setErrorMessage(joinResult.message); + return false; + } + try { + await fetchMessages(newContact.conversation_id); + } catch (e) { + console.error('Failed to fetch messages: ', e); + return false; + } + setContactsList((prevContacts) => + prevContacts.map((c) => + c.username === newContact.username ? { ...c, read: true } : c, + ), + ); + console.log('Current contact is now: ', newContact); + return true; + } catch (e) { + console.error('Failed to initialize contact:', e); + return false; + } + } + + const fetchMessages = async (conversation_id: string) => { + console.log('Fetching messages for: ', conversation_id); + try { + const messages = await getMessages(conversation_id); + if (messages.messages.length < 50) { + setHasMoreMessages(false); + setErrorMessage('No more messages found'); + } + + setCursor(() => { + return Math.min( + ...messages.messages.map((message) => message.message_id), + ); + }); + + messages.messages.forEach(messageHandler); + } catch (e) { + if (axios.isAxiosError(e)) { + setErrorMessage(e.message); + } + } + }; + + function messageHandler(msg: ChatMessagesProps) { + setMessages((prevMessages) => { + // Check if the message already exists in the state + if (!prevMessages.some((m) => m.message_id === msg.message_id)) { + // Convert sent_at to Date object before adding to state + const messageWithDate = { + ...msg, + sent_at: new Date(msg.sent_at), + }; + return [...prevMessages, messageWithDate]; + } + return prevMessages; + }); + } + + function updateContactStatus(contactObj: ContactsProps, read: boolean) { + console.log('Update contact status: ', contactObj); + + setContactsList((prevContacts) => + prevContacts.map((contact) => { + if (contact.conversation_id === contactObj.conversation_id) { + if (!contactObj.read) { + setContactStatus(contactObj.conversation_id, read); + } + return { ...contact, read: read }; + } else { + return contact; + } + }), + ); + } + + const fetchPreviousMessages = async (contact: string | null) => { + if (!hasMoreMessages) { + return; + } + + console.log( + 'Fetching messages for: ', + contact, + 'with cursor: ', + cursor, + 'hasmoremessages: ', + hasMoreMessages, + ); + + try { + const messages = await getMessages(contact, cursor, 50); + if (messages.messages.length < 50) { + setHasMoreMessages(false); + setErrorMessage('No more messages found'); + } + messages.messages.forEach((msg) => { + setMessages((prevMessages) => { + if (!prevMessages.some((m) => m.message_id === msg.message_id)) { + const messageWithDate = { + ...msg, + sent_at: new Date(msg.sent_at), + }; + + return [messageWithDate, ...prevMessages]; + } + return prevMessages; + }); + }); + + setCursor(() => { + return Math.min( + ...messages.messages.map((message) => message.message_id), + ); + }); + } catch (e) { + if (axios.isAxiosError(e)) { + setErrorMessage(e.message); + } + } + }; + + const value = { + contactsList, + currentContact, + cursor, + messages, + errorMessage, + hasMoreMessages, + me, + groupOwner, + + setContactsList, + setCurrentContact, + setCursor, + setMessages, + setErrorMessage, + setHasMoreMessages, + setMe, + setGroupOwner, + + initializeContact, + fetchMessages, + messageHandler, + updateContactStatus, + fetchPreviousMessages, + }; + + return {children}; +}; diff --git a/client/src/context/chat/useChat.ts b/client/src/context/chat/useChat.ts new file mode 100644 index 0000000..fea046c --- /dev/null +++ b/client/src/context/chat/useChat.ts @@ -0,0 +1,11 @@ +import { useContext } from 'react'; +import { ChatContext } from '@/context/chat/ChatContext.tsx'; +import { ChatContextType } from '@/types/types.ts'; + +export const useChat = (): ChatContextType => { + const context = useContext(ChatContext); + if (!context) { + throw new Error('useChat must be used within ChatProvider'); + } + return context; +}; diff --git a/client/src/pages/Chat.tsx b/client/src/pages/Chat.tsx index c8f6eca..8ab8e25 100644 --- a/client/src/pages/Chat.tsx +++ b/client/src/pages/Chat.tsx @@ -3,33 +3,15 @@ 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 { useEffect } from 'react'; import ContactsList from '../components/chat/leftSidebar/ContactsList.tsx'; -import { initializeSocket, joinRoom } from '../socket/socket.tsx'; +import { initializeSocket } 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/rightSidebar/ParticipantsBar.tsx'; -import { ChatMessagesProps, ContactsProps, MeProps } from '@/types/types.ts'; +import { useChat } from '@/context/chat/useChat.ts'; function Chat() { - const meDefaultValue = { - isGroupAdmin: false, - isGroupOwner: false, - }; - const [contactsList, setContactsList] = useState([]); - const [currentContact, setCurrentContact] = useState( - null, - ); - const [cursor, setCursor] = useState(0); - const [messages, setMessages] = useState([]); - const [errorMessage, setErrorMessage] = useState(null); - const [hasMoreMessages, setHasMoreMessages] = useState(true); - const [me, setMe] = useState(meDefaultValue); - const MeContext = createContext(meDefaultValue); - - const [groupOwner, setGroupOwner] = useState(); - + const { initializeContact, currentContact } = useChat(); useEffect(() => { const token = Cookies.get('token'); if (token) { @@ -47,205 +29,44 @@ function Chat() { localStorage.removeItem('contact'); } } - }, []); - - async function initializeContact(newContact: ContactsProps) { - setMessages([]); // Clear messages from previous contact - localStorage.setItem('contact', JSON.stringify(newContact)); // Set current contact in localstorage - setCurrentContact(newContact); - console.log('Initialized contact: ', newContact); - try { - const joinResult = await joinRoom(newContact.conversation_id); - if (!joinResult.success) { - setErrorMessage(joinResult.message); - return false; - } - try { - await fetchMessages(newContact.conversation_id); - } catch (e) { - console.error('Failed to fetch messages: ', e); - return false; - } - setContactsList((prevContacts) => - prevContacts.map((c) => - c.username === newContact.username ? { ...c, read: true } : c, - ), - ); - console.log('Current contact is now: ', newContact); - return true; - } catch (e) { - console.error('Failed to initialize contact:', e); - return false; - } - } - - const fetchMessages = async (conversation_id: string) => { - console.log('Fetching messages for: ', conversation_id); - try { - const messages = await getMessages(conversation_id); - if (messages.messages.length < 50) { - setHasMoreMessages(false); - setErrorMessage('No more messages found'); - } - - setCursor(() => { - return Math.min( - ...messages.messages.map((message) => message.message_id), - ); - }); - - messages.messages.forEach(messageHandler); - } catch (e) { - if (axios.isAxiosError(e)) { - setErrorMessage(e.message); - } - } - }; - - const fetchPreviousMessages = async (contact: string | null) => { - if (!hasMoreMessages) { - return; - } - - console.log( - 'Fetching messages for: ', - contact, - 'with cursor: ', - cursor, - 'hasmoremessages: ', - hasMoreMessages, - ); - - try { - const messages = await getMessages(contact, cursor, 50); - if (messages.messages.length < 50) { - setHasMoreMessages(false); - setErrorMessage('No more messages found'); - } - messages.messages.forEach((msg) => { - setMessages((prevMessages) => { - if (!prevMessages.some((m) => m.message_id === msg.message_id)) { - const messageWithDate = { - ...msg, - sent_at: new Date(msg.sent_at), - }; - - return [messageWithDate, ...prevMessages]; - } - return prevMessages; - }); - }); - - setCursor(() => { - return Math.min( - ...messages.messages.map((message) => message.message_id), - ); - }); - } catch (e) { - if (axios.isAxiosError(e)) { - setErrorMessage(e.message); - } - } - }; - - function messageHandler(msg: ChatMessagesProps) { - setMessages((prevMessages) => { - // Check if the message already exists in the state - if (!prevMessages.some((m) => m.message_id === msg.message_id)) { - // Convert sent_at to Date object before adding to state - const messageWithDate = { - ...msg, - sent_at: new Date(msg.sent_at), - }; - return [...prevMessages, messageWithDate]; - } - return prevMessages; - }); - } - - function updateContactStatus(contactObj: ContactsProps, read: boolean) { - console.log('Update contact status: ', contactObj); - - setContactsList((prevContacts) => - prevContacts.map((contact) => { - if (contact.conversation_id === contactObj.conversation_id) { - if (!contactObj.read) { - setContactStatus(contactObj.conversation_id, read); - } - return { ...contact, read: read }; - } else { - return contact; - } - }), - ); - } + }); 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/src/pages/Login.tsx b/client/src/pages/Login.tsx index b2042db..bf0ee8b 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -4,7 +4,8 @@ import { Link, useNavigate } from 'react-router-dom'; import { useContext, useState } from 'react'; import { AuthContext } from '../utils/AuthProvider.tsx'; import LoadingWheel from '../components/chat/LoadingWheel.tsx'; -import { axiosClient } from '../App.tsx'; + +import { axiosClient } from '@/utils/axiosClient.ts'; export type Inputs = { username: string; diff --git a/client/src/pages/Signup.tsx b/client/src/pages/Signup.tsx index 694504f..892cd9a 100644 --- a/client/src/pages/Signup.tsx +++ b/client/src/pages/Signup.tsx @@ -3,8 +3,8 @@ import { useForm, SubmitHandler } from 'react-hook-form'; import { Link, useNavigate } from 'react-router-dom'; import { useContext, useState } from 'react'; import { AuthContext } from '../utils/AuthProvider.tsx'; -import { axiosClient } from '../App.tsx'; import LoadingWheel from '../components/chat/LoadingWheel.tsx'; +import { axiosClient } from '@/utils/axiosClient.ts'; type Inputs = { username: string; diff --git a/client/src/types/types.ts b/client/src/types/types.ts index 3d3ff83..4ac30fe 100644 --- a/client/src/types/types.ts +++ b/client/src/types/types.ts @@ -1,4 +1,4 @@ -import { File } from 'lucide-react'; +import { Dispatch, SetStateAction } from 'react'; export type MeProps = { isGroupAdmin: boolean; @@ -26,19 +26,46 @@ export type ContactsProps = { last_message_time: string; last_message_sender: string; }; -export type InputProps = { - message: string; - attachments: FileList | null; -}; -export type MessageFormProps = { - contact: ContactsProps; - messages: ChatMessagesProps[]; + +export type ParticipantsProps = { + user_id: string; + username: string; + isadmin: boolean; + isowner: boolean; }; + export type FileWithPreviewProps = { file: File; preview: string | null; }; + export type UsernameType = { username: string | null; user_id: string | null; }; + +export type ChatContextType = { + contactsList: ContactsProps[]; + currentContact: ContactsProps | null; + cursor: number; + messages: ChatMessagesProps[]; + errorMessage: string | null; + hasMoreMessages: boolean; + me: MeProps; + groupOwner: string | undefined; + + setContactsList: Dispatch>; + setCurrentContact: Dispatch>; + setCursor: Dispatch>; + setMessages: Dispatch>; + setErrorMessage: Dispatch>; + setHasMoreMessages: Dispatch>; + setMe: Dispatch>; + setGroupOwner: Dispatch>; + + initializeContact: (newContact: ContactsProps) => Promise; + fetchMessages: (conversation_id: string) => Promise; + messageHandler: (msg: ChatMessagesProps) => void; + updateContactStatus: (contactObj: ContactsProps, read: boolean) => void; + fetchPreviousMessages: (contact: string | null) => Promise; +}; diff --git a/client/src/utils/ProtectedRoutes.tsx b/client/src/utils/ProtectedRoutes.tsx index af39a3e..e09acfb 100644 --- a/client/src/utils/ProtectedRoutes.tsx +++ b/client/src/utils/ProtectedRoutes.tsx @@ -2,8 +2,8 @@ import { Navigate, Outlet } from 'react-router-dom'; import { useContext, useEffect, useState } from 'react'; import { AuthContext } from './AuthProvider.tsx'; import LoadingScreen from '../components/LoadingScreen.tsx'; -import { axiosClient } from '../App.tsx'; import { UsernameType } from '@/types/types.ts'; +import { axiosClient } from '@/utils/axiosClient.ts'; function ProtectedRoutes() { const { authorized, isLoading } = useContext(AuthContext); diff --git a/client/src/utils/axiosClient.ts b/client/src/utils/axiosClient.ts new file mode 100644 index 0000000..d8fbcb3 --- /dev/null +++ b/client/src/utils/axiosClient.ts @@ -0,0 +1,5 @@ +import axios from 'axios'; + +export const axiosClient = axios.create({ + baseURL: '/', +}); diff --git a/client/src/utils/useAuth.tsx b/client/src/utils/useAuth.tsx index a097963..44db8f4 100644 --- a/client/src/utils/useAuth.tsx +++ b/client/src/utils/useAuth.tsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import Cookies from 'js-cookie'; -import { axiosClient } from '../App.tsx'; + +import { axiosClient } from '@/utils/axiosClient.ts'; function useAuth() { const [authorized, setAuthorized] = useState(false);