code refactor, created ChatProvider.tsx, use context instead of tons of props

This commit is contained in:
slawk0
2025-01-02 14:04:54 +01:00
parent 6db8d2d574
commit 9295b2f049
21 changed files with 383 additions and 374 deletions

View File

@@ -1,7 +1,7 @@
import { import {
RouterProvider,
createBrowserRouter, createBrowserRouter,
Navigate, Navigate,
RouterProvider,
} from 'react-router-dom'; } from 'react-router-dom';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import Chat from './pages/Chat.tsx'; import Chat from './pages/Chat.tsx';
@@ -12,12 +12,9 @@ import Lost from './pages/404.tsx';
import { AuthContext } from './utils/AuthProvider.tsx'; import { AuthContext } from './utils/AuthProvider.tsx';
import ProtectedRoutes from './utils/ProtectedRoutes.tsx'; import ProtectedRoutes from './utils/ProtectedRoutes.tsx';
import PublicRoute from '@/utils/PublicRoute.tsx'; import PublicRoute from '@/utils/PublicRoute.tsx';
import axios from 'axios';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { ChatProvider } from '@/context/chat/ChatProvider.tsx';
export const axiosClient = axios.create({ import { axiosClient } from '@/utils/axiosClient.ts';
baseURL: '/',
});
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -29,7 +26,11 @@ const router = createBrowserRouter([
children: [ children: [
{ {
path: '/chat', path: '/chat',
element: <Chat />, element: (
<ChatProvider>
<Chat />
</ChatProvider>
),
}, },
{ {
path: '/settings', path: '/settings',
@@ -57,14 +58,12 @@ const router = createBrowserRouter([
]); ]);
function App() { function App() {
// Check for token immediately and set initial states accordingly
const hasToken = Boolean(Cookies.get('token')); const hasToken = Boolean(Cookies.get('token'));
const [authorized, setAuthorized] = useState(false); const [authorized, setAuthorized] = useState(false);
const [isLoading, setIsLoading] = useState(hasToken); // Only start loading if there's a token const [isLoading, setIsLoading] = useState(true);
useEffect(() => { useEffect(() => {
async function validateToken() { async function validateToken() {
// If there's no token, we're already in the correct state
if (!hasToken) return; if (!hasToken) return;
try { try {

View File

@@ -1,6 +1,5 @@
import { axiosClient } from '../App.tsx';
import { ChatMessagesProps, ContactsProps } from '@/types/types.ts'; import { ChatMessagesProps, ContactsProps } from '@/types/types.ts';
import { axiosClient } from '@/utils/axiosClient.ts';
export async function getContactsList(): Promise<ContactsProps[]> { export async function getContactsList(): Promise<ContactsProps[]> {
try { try {

View File

@@ -1,17 +1,20 @@
import type { KeyboardEventHandler } from 'react'; import type { KeyboardEventHandler } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form'; import { SubmitHandler, useForm } from 'react-hook-form';
import { socket } from '../../../socket/socket.tsx'; import { socket } from '@/socket/socket.tsx';
import { axiosClient } from '../../../App.tsx';
import { File, Paperclip, Send, X } from 'lucide-react'; import { File, Paperclip, Send, X } from 'lucide-react';
import LoadingWheel from '@/components/chat/LoadingWheel.tsx'; import LoadingWheel from '@/components/chat/LoadingWheel.tsx';
import { import { FileWithPreviewProps } from '@/types/types.ts';
FileWithPreviewProps, import { useChat } from '@/context/chat/useChat.ts';
InputProps, import { axiosClient } from '@/utils/axiosClient.ts';
MessageFormProps,
} from '@/types/types.ts';
const MessageForm = ({ contact }: MessageFormProps) => { export type InputProps = {
message: string;
attachments: FileList | null;
};
const MessageForm = () => {
const { currentContact } = useChat();
const [files, setFiles] = useState<FileWithPreviewProps[]>([]); const [files, setFiles] = useState<FileWithPreviewProps[]>([]);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [isSending, setIsSending] = useState<boolean>(false); const [isSending, setIsSending] = useState<boolean>(false);
@@ -130,9 +133,9 @@ const MessageForm = ({ contact }: MessageFormProps) => {
'chat message', 'chat message',
{ {
message: data.message.trim(), message: data.message.trim(),
recipient: contact.conversation_id, recipient: currentContact?.conversation_id,
attachment_urls: attachmentUrls, attachment_urls: attachmentUrls,
recipient_id: contact.user_id, recipient_id: currentContact?.user_id,
}, },
(response: { status: string; message: string }) => { (response: { status: string; message: string }) => {
if (response.status === 'ok') { if (response.status === 'ok') {
@@ -148,9 +151,9 @@ const MessageForm = ({ contact }: MessageFormProps) => {
); );
console.log('sent: ', { console.log('sent: ', {
message: data.message.trim(), message: data.message.trim(),
recipient: contact.conversation_id, recipient: currentContact?.conversation_id,
attachment_urls: attachmentUrls, 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 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`} ${isOverLimit ? 'border-2 border-red-500' : isNearLimit ? 'border-2 border-yellow-500' : ''} mx-auto`}
autoFocus={!!contact} autoFocus={!!currentContact}
disabled={!contact} disabled={!currentContact}
placeholder="Enter message" placeholder="Enter message"
onKeyDown={handleKeyPress} onKeyDown={handleKeyPress}
rows={1} rows={1}

View File

@@ -4,37 +4,21 @@ import { useOutletContext } from 'react-router-dom';
import { sendContact } from '@/api/contactsApi.tsx'; import { sendContact } from '@/api/contactsApi.tsx';
import LoadingWheel from '../LoadingWheel.tsx'; import LoadingWheel from '../LoadingWheel.tsx';
import AnimatedMessage from '@/components/chat/chatArea/AnimatedMessage.tsx'; import AnimatedMessage from '@/components/chat/chatArea/AnimatedMessage.tsx';
import { import { ChatMessagesProps, UsernameType } from '@/types/types.ts';
ChatMessagesProps, import { useChat } from '@/context/chat/useChat.ts';
ContactsProps,
MeProps,
UsernameType,
} from '@/types/types.ts';
type MessagesAreaProps = { function MessagesArea() {
messages: ChatMessagesProps[]; const {
setMessages: React.Dispatch<React.SetStateAction<ChatMessagesProps[]>>; messages,
currentContact: ContactsProps | null; setMessages,
updateContactStatus: (contact: ContactsProps, read: boolean) => void; currentContact,
messageHandler: (msg: ChatMessagesProps) => void; updateContactStatus,
setContactsList: React.Dispatch<React.SetStateAction<ContactsProps[]>>; messageHandler,
fetchPreviousMessages: (contact: string | null) => Promise<void>; setContactsList,
errorMessage: string | null; errorMessage,
contactsList: ContactsProps[]; fetchPreviousMessages,
me: MeProps; me,
}; } = useChat();
function MessagesArea({
messages,
currentContact,
updateContactStatus,
setContactsList,
messageHandler,
fetchPreviousMessages,
errorMessage,
setMessages,
me,
}: MessagesAreaProps) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const user: UsernameType = useOutletContext(); const user: UsernameType = useOutletContext();
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
@@ -231,7 +215,7 @@ function MessagesArea({
useEffect(() => { useEffect(() => {
scrollToBottom(); scrollToBottom();
}, []); });
return ( return (
<div ref={containerRef} className="flex flex-col h-full overflow-y-auto"> <div ref={containerRef} className="flex flex-col h-full overflow-y-auto">

View File

@@ -1,21 +1,19 @@
import LoadingWheel from '../LoadingWheel.tsx'; import LoadingWheel from '../LoadingWheel.tsx';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form'; 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 { UserRoundPlus } from 'lucide-react';
import { Button } from '@/components/ui/button.tsx'; 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 = { type Inputs = {
username: string; username: string;
}; };
interface AddGroupMemberProps { function AddGroupMember() {
contact?: ContactsProps; const { currentContact } = useChat();
}
function AddGroupMember({ contact }: AddGroupMemberProps) {
const { register, handleSubmit, watch, reset } = useForm<Inputs>(); const { register, handleSubmit, watch, reset } = useForm<Inputs>();
const modalRef = useRef<HTMLDialogElement | null>(null); const modalRef = useRef<HTMLDialogElement | null>(null);
const contactInput = watch('username'); const contactInput = watch('username');
@@ -43,12 +41,16 @@ function AddGroupMember({ contact }: AddGroupMemberProps) {
setIsLoading(false); setIsLoading(false);
setNotFound(false); setNotFound(false);
} }
} catch (e: any) { } catch (e) {
setIsLoading(false); if (axios.isAxiosError(e)) {
console.error('Error fetching suggestions:', e); setIsLoading(false);
setErrorMessage( console.error('Error fetching suggestions:', e);
e.response?.data?.message || 'Failed to fetch suggestions', setErrorMessage(
); e.response?.data?.message || 'Failed to fetch suggestions',
);
} else {
console.error('Unexpected error occurred');
}
} }
} else { } else {
setNotFound(false); setNotFound(false);
@@ -76,20 +78,26 @@ function AddGroupMember({ contact }: AddGroupMemberProps) {
setIsLoading(true); setIsLoading(true);
setErrorMessage(null); setErrorMessage(null);
const response = await axiosClient.post(`/api/chat/groups/addMember/`, { const response = await axiosClient.post(`/api/chat/groups/addMember/`, {
group_id: contact?.conversation_id, group_id: currentContact?.conversation_id,
username: contactToSubmit, username: contactToSubmit,
}); });
console.log('Add member to group', response); console.log('Add member to group', response);
setIsLoading(false); 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) { if (modalRef.current) {
modalRef.current.close(); modalRef.current.close();
} }
reset(); reset();
} catch (e: any) { } catch (e) {
console.error('Failed to add group member: ', e); if (axios.isAxiosError(e)) {
setIsLoading(false); console.error('Failed to add group member: ', e);
setErrorMessage(e.response?.data?.message || 'Failed to add member'); setIsLoading(false);
setErrorMessage(e.response?.data?.message || 'Failed to add member');
} else {
console.error('Unexpected error occurred');
}
} }
}; };

View File

@@ -2,34 +2,27 @@ import profile from '../../../../assets/profile.svg';
import CreateGroupButton from './CreateGroupButton.tsx'; import CreateGroupButton from './CreateGroupButton.tsx';
import AddGroupMember from './AddGroupMember.tsx'; import AddGroupMember from './AddGroupMember.tsx';
import { UsersRound } from 'lucide-react'; import { UsersRound } from 'lucide-react';
import { ContactsProps } from '@/types/types.ts'; import { useChat } from '@/context/chat/useChat.ts';
type ContactProfileProps = { function ContactProfile() {
contact: ContactsProps | null; const { currentContact } = useChat();
};
function ContactProfile({ contact }: ContactProfileProps) {
return ( return (
<div className="flex items-center p-2"> <div className="flex items-center p-2">
<div className="flex items-center p-2"> <div className="flex items-center p-2">
{contact?.type === 'group' ? ( {currentContact?.type === 'group' ? (
<UsersRound className="w-5 mr-2" /> <UsersRound className="w-5 mr-2" />
) : ( ) : (
<img className="w-4 mr-2 invert" src={profile} alt="profile img" /> <img className="w-4 mr-2 invert" src={profile} alt="profile img" />
)} )}
<p>{contact ? contact.username : null}</p> <p>{currentContact ? currentContact.username : null}</p>
</div> </div>
<div className="flex-grow"></div> <div className="flex-grow"></div>
<div> <div>
<CreateGroupButton /> <CreateGroupButton />
</div> </div>
<div> <div>{currentContact?.type === 'group' ? <AddGroupMember /> : null}</div>
{contact?.type === 'group' ? (
<AddGroupMember contact={contact} />
) : null}
</div>
</div> </div>
); );
} }

View File

@@ -1,8 +1,8 @@
import LoadingWheel from '../LoadingWheel.tsx'; import LoadingWheel from '../LoadingWheel.tsx';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { SubmitHandler, useForm } from 'react-hook-form'; import { SubmitHandler, useForm } from 'react-hook-form';
import { axiosClient } from '@/App.tsx';
import { Plus } from 'lucide-react'; import { Plus } from 'lucide-react';
import { axiosClient } from '@/utils/axiosClient.ts';
type Inputs = { type Inputs = {
groupName: string; groupName: string;

View File

@@ -1,21 +1,18 @@
import { useForm, SubmitHandler } from 'react-hook-form'; import { useForm, SubmitHandler } from 'react-hook-form';
import { axiosClient } from '../../../App.tsx';
import { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import LoadingWheel from '../LoadingWheel.tsx'; import LoadingWheel from '../LoadingWheel.tsx';
import { Search } from 'lucide-react'; import { Search } from 'lucide-react';
import { 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 Input = { type Input = {
contact: string; contact: string;
}; };
type ContactFormProps = { function ContactForm() {
InitializeContact: (contact: ContactsProps) => void; const { setContactsList, initializeContact } = useChat();
setContactsList: React.Dispatch<React.SetStateAction<ContactsProps[]>>;
};
function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
const { register, handleSubmit, reset, watch } = useForm<Input>(); const { register, handleSubmit, reset, watch } = useForm<Input>();
const contactInput = watch('contact'); const contactInput = watch('contact');
const [suggestions, setSuggestions] = useState<string[]>([]); const [suggestions, setSuggestions] = useState<string[]>([]);
@@ -76,7 +73,7 @@ function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
); );
console.log('contact post response: ', response.data); console.log('contact post response: ', response.data);
InitializeContact(response.data); initializeContact(response.data);
setContactsList((prevContacts) => { setContactsList((prevContacts) => {
if (!prevContacts.some((c) => c.username === contactToSubmit)) { if (!prevContacts.some((c) => c.username === contactToSubmit)) {

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import { useEffect } from 'react';
import { socket } from '@/socket/socket.tsx'; import { socket } from '@/socket/socket.tsx';
import GroupIcon from '../../../../assets/group.svg'; import GroupIcon from '../../../../assets/group.svg';
import { import {
@@ -12,36 +12,27 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from '@/components/ui/alert-dialog.tsx'; } from '@/components/ui/alert-dialog.tsx';
import { axiosClient } from '@/App.tsx';
import { Dot } from 'lucide-react'; import { Dot } from 'lucide-react';
import LastActiveTime from '@/components/chat/leftSidebar/LastActiveTime.tsx'; 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 = { function ContactsList() {
initializeContact: (contact: ContactsProps) => void; const {
setContactsList: React.Dispatch<React.SetStateAction<ContactsProps[]>>; initializeContact,
contactsList: ContactsProps[]; contactsList,
setCurrentContact: React.Dispatch<React.SetStateAction<ContactsProps | null>>; setContactsList,
updateContactStatus: (contactObj: ContactsProps, read: boolean) => void; setCurrentContact,
setMessages: React.Dispatch<React.SetStateAction<ChatMessagesProps[]>>; updateContactStatus,
currentContact: ContactsProps | null; setMessages,
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>; setErrorMessage,
}; } = useChat();
function ContactsList({
initializeContact,
contactsList,
setContactsList,
setCurrentContact,
updateContactStatus,
setMessages,
setErrorMessage,
}: ContactsListProps) {
useEffect(() => { useEffect(() => {
fetchContacts().catch((e) => fetchContacts().catch((e) =>
console.error('Failed to fetch contacts: ', e), console.error('Failed to fetch contacts: ', e),
); );
}, []); });
useEffect(() => { useEffect(() => {
if (!socket) return; if (!socket) return;

View File

@@ -1,5 +1,4 @@
import { useContext, useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { axiosClient } from '@/App.tsx';
import { socket } from '@/socket/socket.tsx'; import { socket } from '@/socket/socket.tsx';
import { Crown, Sword } from 'lucide-react'; import { Crown, Sword } from 'lucide-react';
import { import {
@@ -10,32 +9,15 @@ import {
} from '@/components/ui/context-menu.tsx'; } from '@/components/ui/context-menu.tsx';
import { useOutletContext } from 'react-router-dom'; import { useOutletContext } from 'react-router-dom';
import zdjecie from '../../../../assets/turtleProfileImg3.webp'; import zdjecie from '../../../../assets/turtleProfileImg3.webp';
import { ContactsProps, MeProps, UsernameType } from '@/types/types.ts'; import { ParticipantsProps, UsernameType } from '@/types/types.ts';
type ParticipantsProps = { import { useChat } from '@/context/chat/useChat.ts';
user_id: string; import { axiosClient } from '@/utils/axiosClient.ts';
username: string;
isadmin: boolean;
isowner: boolean;
};
type ParticipantsBarProps = { function ParticipantsBar() {
initializeContact: (contact: ContactsProps) => void; const { setMe, initializeContact, setGroupOwner, currentContact, me } =
currentContact: ContactsProps | null; useChat();
setMe: React.Dispatch<React.SetStateAction<MeProps>>;
setGroupOwner: React.Dispatch<React.SetStateAction<string | undefined>>;
groupOwner: string | undefined;
};
function ParticipantsBar({
initializeContact,
currentContact,
setMe,
setGroupOwner,
groupOwner,
}: ParticipantsBarProps) {
const [participants, setParticipants] = useState<ParticipantsProps[]>([]); const [participants, setParticipants] = useState<ParticipantsProps[]>([]);
const user: UsernameType = useOutletContext(); const user: UsernameType = useOutletContext();
const me = useContext(MeContext);
const getParticipants = async () => { const getParticipants = async () => {
try { try {
const response = await axiosClient.get( const response = await axiosClient.get(

View File

@@ -0,0 +1,6 @@
import { createContext } from 'react';
import { ChatContextType } from '@/types/types.ts';
export const ChatContext = createContext<ChatContextType | undefined>(
undefined,
);

View File

@@ -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<ContactsProps[]>([]);
const [currentContact, setCurrentContact] = useState<ContactsProps | null>(
null,
);
const [cursor, setCursor] = useState<number>(0);
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 [groupOwner, setGroupOwner] = useState<string | undefined>();
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 <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
};

View File

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

View File

@@ -3,33 +3,15 @@ import ContactProfile from '../components/chat/chatHeader/ContactProfile.tsx';
import UserProfile from '../components/chat/leftSidebar/UserProfile.tsx'; import UserProfile from '../components/chat/leftSidebar/UserProfile.tsx';
import ContactForm from '../components/chat/leftSidebar/ContactForm.tsx'; import ContactForm from '../components/chat/leftSidebar/ContactForm.tsx';
import MessagesArea from '../components/chat/chatArea/MessagesArea.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 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 Cookies from 'js-cookie';
import { getMessages, setContactStatus } from '../api/contactsApi.tsx';
import axios from 'axios';
import ParticipantsBar from '@/components/chat/rightSidebar/ParticipantsBar.tsx'; 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() { function Chat() {
const meDefaultValue = { const { initializeContact, currentContact } = useChat();
isGroupAdmin: false,
isGroupOwner: false,
};
const [contactsList, setContactsList] = useState<ContactsProps[]>([]);
const [currentContact, setCurrentContact] = useState<ContactsProps | null>(
null,
);
const [cursor, setCursor] = useState<number>(0);
const [messages, setMessages] = useState<ChatMessagesProps[]>([]);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [hasMoreMessages, setHasMoreMessages] = useState<boolean>(true);
const [me, setMe] = useState<MeProps>(meDefaultValue);
const MeContext = createContext(meDefaultValue);
const [groupOwner, setGroupOwner] = useState<string | undefined>();
useEffect(() => { useEffect(() => {
const token = Cookies.get('token'); const token = Cookies.get('token');
if (token) { if (token) {
@@ -47,205 +29,44 @@ function Chat() {
localStorage.removeItem('contact'); 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 ( return (
<MeContext.Provider value={me}> <div className="text-white flex h-screen">
<div className="text-white flex h-screen"> {/* Left Sidebar */}
{/* Left Sidebar */} <div className="w-64 h-screen flex-shrink-0 flex flex-col bg-zinc-950 text-center border-r border-zinc-800">
<div className="w-64 h-screen flex-shrink-0 flex flex-col bg-zinc-950 text-center border-r border-zinc-800"> <ContactForm />
<ContactForm <ContactsList />
setContactsList={setContactsList} <UserProfile />
InitializeContact={initializeContact} </div>
/>
<ContactsList
initializeContact={initializeContact}
contactsList={contactsList}
setContactsList={setContactsList}
setCurrentContact={setCurrentContact}
updateContactStatus={updateContactStatus}
setMessages={setMessages}
currentContact={currentContact}
setErrorMessage={setErrorMessage}
/>
<UserProfile />
</div>
{/*Chat area */} {/*Chat area */}
<div className="flex-grow flex flex-col h-screen bg-[#0a0a0a]"> <div className="flex-grow flex flex-col h-screen bg-[#0a0a0a]">
<div className="flex flex-grow overflow-hidden"> <div className="flex flex-grow overflow-hidden">
{/* Messages Container and Participants Container */} {/* Messages Container and Participants Container */}
<div className="flex-grow flex flex-col overflow-hidden"> <div className="flex-grow flex flex-col overflow-hidden">
<div className="flex-shrink-0 border-b border-zinc-800 "> <div className="flex-shrink-0 border-b border-zinc-800 ">
<ContactProfile contact={currentContact} /> <ContactProfile />
</div> </div>
<div className="flex-grow overflow-y-auto"> <div className="flex-grow overflow-y-auto">
<MessagesArea <MessagesArea />
messages={messages} </div>
setMessages={setMessages} <div className="flex-shrink-0 p-3 border-t border-zinc-800">
currentContact={currentContact} {currentContact && currentContact.username?.length >= 4 ? (
updateContactStatus={updateContactStatus} <MessageForm />
messageHandler={messageHandler} ) : null}
setContactsList={setContactsList}
fetchPreviousMessages={fetchPreviousMessages}
errorMessage={errorMessage}
contactsList={contactsList}
/>
</div>
<div className="flex-shrink-0 p-3 border-t border-zinc-800">
{currentContact && currentContact.username?.length >= 4 ? (
<MessageForm contact={currentContact} messages={messages} />
) : null}
</div>
</div> </div>
{/* Right Sidebar - Participants */}
{currentContact?.type === 'group' && (
<div className="w-64 flex-shrink-0 border-l border-zinc-800 bg-zinc-950">
<ParticipantsBar
setMe={setMe}
initializeContact={initializeContact}
currentContact={currentContact}
setGroupOwner={setGroupOwner}
groupOwner={groupOwner}
/>
</div>
)}
</div> </div>
{/* Right Sidebar - Participants */}
{currentContact?.type === 'group' && (
<div className="w-64 flex-shrink-0 border-l border-zinc-800 bg-zinc-950">
<ParticipantsBar />
</div>
)}
</div> </div>
</div> </div>
</MeContext.Provider> </div>
); );
} }

View File

@@ -4,7 +4,8 @@ import { Link, useNavigate } from 'react-router-dom';
import { useContext, useState } from 'react'; import { useContext, useState } from 'react';
import { AuthContext } from '../utils/AuthProvider.tsx'; import { AuthContext } from '../utils/AuthProvider.tsx';
import LoadingWheel from '../components/chat/LoadingWheel.tsx'; import LoadingWheel from '../components/chat/LoadingWheel.tsx';
import { axiosClient } from '../App.tsx';
import { axiosClient } from '@/utils/axiosClient.ts';
export type Inputs = { export type Inputs = {
username: string; username: string;

View File

@@ -3,8 +3,8 @@ 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';
import { AuthContext } from '../utils/AuthProvider.tsx'; import { AuthContext } from '../utils/AuthProvider.tsx';
import { axiosClient } from '../App.tsx';
import LoadingWheel from '../components/chat/LoadingWheel.tsx'; import LoadingWheel from '../components/chat/LoadingWheel.tsx';
import { axiosClient } from '@/utils/axiosClient.ts';
type Inputs = { type Inputs = {
username: string; username: string;

View File

@@ -1,4 +1,4 @@
import { File } from 'lucide-react'; import { Dispatch, SetStateAction } from 'react';
export type MeProps = { export type MeProps = {
isGroupAdmin: boolean; isGroupAdmin: boolean;
@@ -26,19 +26,46 @@ export type ContactsProps = {
last_message_time: string; last_message_time: string;
last_message_sender: string; last_message_sender: string;
}; };
export type InputProps = {
message: string; export type ParticipantsProps = {
attachments: FileList | null; user_id: string;
}; username: string;
export type MessageFormProps = { isadmin: boolean;
contact: ContactsProps; isowner: boolean;
messages: ChatMessagesProps[];
}; };
export type FileWithPreviewProps = { export type FileWithPreviewProps = {
file: File; file: File;
preview: string | null; preview: string | null;
}; };
export type UsernameType = { export type UsernameType = {
username: string | null; username: string | null;
user_id: 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<SetStateAction<ContactsProps[]>>;
setCurrentContact: Dispatch<SetStateAction<ContactsProps | null>>;
setCursor: Dispatch<SetStateAction<number>>;
setMessages: Dispatch<SetStateAction<ChatMessagesProps[]>>;
setErrorMessage: Dispatch<SetStateAction<string | null>>;
setHasMoreMessages: Dispatch<SetStateAction<boolean>>;
setMe: Dispatch<SetStateAction<MeProps>>;
setGroupOwner: Dispatch<SetStateAction<string | undefined>>;
initializeContact: (newContact: ContactsProps) => Promise<boolean>;
fetchMessages: (conversation_id: string) => Promise<void>;
messageHandler: (msg: ChatMessagesProps) => void;
updateContactStatus: (contactObj: ContactsProps, read: boolean) => void;
fetchPreviousMessages: (contact: string | null) => Promise<void>;
};

View File

@@ -2,8 +2,8 @@ import { Navigate, Outlet } from 'react-router-dom';
import { useContext, useEffect, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { AuthContext } from './AuthProvider.tsx'; import { AuthContext } from './AuthProvider.tsx';
import LoadingScreen from '../components/LoadingScreen.tsx'; import LoadingScreen from '../components/LoadingScreen.tsx';
import { axiosClient } from '../App.tsx';
import { UsernameType } from '@/types/types.ts'; import { UsernameType } from '@/types/types.ts';
import { axiosClient } from '@/utils/axiosClient.ts';
function ProtectedRoutes() { function ProtectedRoutes() {
const { authorized, isLoading } = useContext(AuthContext); const { authorized, isLoading } = useContext(AuthContext);

View File

@@ -0,0 +1,5 @@
import axios from 'axios';
export const axiosClient = axios.create({
baseURL: '/',
});

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { axiosClient } from '../App.tsx';
import { axiosClient } from '@/utils/axiosClient.ts';
function useAuth() { function useAuth() {
const [authorized, setAuthorized] = useState<boolean>(false); const [authorized, setAuthorized] = useState<boolean>(false);