code refactor, created ChatProvider.tsx, use context instead of tons of props
This commit is contained in:
@@ -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: <Chat />,
|
||||
element: (
|
||||
<ChatProvider>
|
||||
<Chat />
|
||||
</ChatProvider>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 {
|
||||
|
||||
@@ -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<ContactsProps[]> {
|
||||
try {
|
||||
|
||||
@@ -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<FileWithPreviewProps[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isSending, setIsSending] = useState<boolean>(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}
|
||||
|
||||
@@ -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<React.SetStateAction<ChatMessagesProps[]>>;
|
||||
currentContact: ContactsProps | null;
|
||||
updateContactStatus: (contact: ContactsProps, read: boolean) => void;
|
||||
messageHandler: (msg: ChatMessagesProps) => void;
|
||||
setContactsList: React.Dispatch<React.SetStateAction<ContactsProps[]>>;
|
||||
fetchPreviousMessages: (contact: string | null) => Promise<void>;
|
||||
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<HTMLDivElement>(null);
|
||||
const user: UsernameType = useOutletContext();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
@@ -231,7 +215,7 @@ function MessagesArea({
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, []);
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="flex flex-col h-full overflow-y-auto">
|
||||
|
||||
@@ -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<Inputs>();
|
||||
const modalRef = useRef<HTMLDialogElement | null>(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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<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" />
|
||||
) : (
|
||||
<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 className="flex-grow"></div>
|
||||
|
||||
<div>
|
||||
<CreateGroupButton />
|
||||
</div>
|
||||
<div>
|
||||
{contact?.type === 'group' ? (
|
||||
<AddGroupMember contact={contact} />
|
||||
) : null}
|
||||
</div>
|
||||
<div>{currentContact?.type === 'group' ? <AddGroupMember /> : null}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<React.SetStateAction<ContactsProps[]>>;
|
||||
};
|
||||
|
||||
function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
|
||||
function ContactForm() {
|
||||
const { setContactsList, initializeContact } = useChat();
|
||||
const { register, handleSubmit, reset, watch } = useForm<Input>();
|
||||
const contactInput = watch('contact');
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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<React.SetStateAction<ContactsProps[]>>;
|
||||
contactsList: ContactsProps[];
|
||||
setCurrentContact: React.Dispatch<React.SetStateAction<ContactsProps | null>>;
|
||||
updateContactStatus: (contactObj: ContactsProps, read: boolean) => void;
|
||||
setMessages: React.Dispatch<React.SetStateAction<ChatMessagesProps[]>>;
|
||||
currentContact: ContactsProps | null;
|
||||
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -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<React.SetStateAction<MeProps>>;
|
||||
setGroupOwner: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
groupOwner: string | undefined;
|
||||
};
|
||||
|
||||
function ParticipantsBar({
|
||||
initializeContact,
|
||||
currentContact,
|
||||
setMe,
|
||||
setGroupOwner,
|
||||
groupOwner,
|
||||
}: ParticipantsBarProps) {
|
||||
function ParticipantsBar() {
|
||||
const { setMe, initializeContact, setGroupOwner, currentContact, me } =
|
||||
useChat();
|
||||
const [participants, setParticipants] = useState<ParticipantsProps[]>([]);
|
||||
const user: UsernameType = useOutletContext();
|
||||
const me = useContext(MeContext);
|
||||
const getParticipants = async () => {
|
||||
try {
|
||||
const response = await axiosClient.get(
|
||||
|
||||
6
client/src/context/chat/ChatContext.tsx
Normal file
6
client/src/context/chat/ChatContext.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createContext } from 'react';
|
||||
import { ChatContextType } from '@/types/types.ts';
|
||||
|
||||
export const ChatContext = createContext<ChatContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
181
client/src/context/chat/ChatProvider.tsx
Normal file
181
client/src/context/chat/ChatProvider.tsx
Normal 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>;
|
||||
};
|
||||
11
client/src/context/chat/useChat.ts
Normal file
11
client/src/context/chat/useChat.ts
Normal 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;
|
||||
};
|
||||
@@ -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<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>();
|
||||
|
||||
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 (
|
||||
<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">
|
||||
<ContactForm
|
||||
setContactsList={setContactsList}
|
||||
InitializeContact={initializeContact}
|
||||
/>
|
||||
<ContactsList
|
||||
initializeContact={initializeContact}
|
||||
contactsList={contactsList}
|
||||
setContactsList={setContactsList}
|
||||
setCurrentContact={setCurrentContact}
|
||||
updateContactStatus={updateContactStatus}
|
||||
setMessages={setMessages}
|
||||
currentContact={currentContact}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
<UserProfile />
|
||||
</div>
|
||||
<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">
|
||||
<ContactForm />
|
||||
<ContactsList />
|
||||
<UserProfile />
|
||||
</div>
|
||||
|
||||
{/*Chat area */}
|
||||
<div className="flex-grow flex flex-col h-screen bg-[#0a0a0a]">
|
||||
<div className="flex flex-grow overflow-hidden">
|
||||
{/* Messages Container and Participants Container */}
|
||||
<div className="flex-grow flex flex-col overflow-hidden">
|
||||
<div className="flex-shrink-0 border-b border-zinc-800 ">
|
||||
<ContactProfile contact={currentContact} />
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
<MessagesArea
|
||||
messages={messages}
|
||||
setMessages={setMessages}
|
||||
currentContact={currentContact}
|
||||
updateContactStatus={updateContactStatus}
|
||||
messageHandler={messageHandler}
|
||||
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>
|
||||
{/*Chat area */}
|
||||
<div className="flex-grow flex flex-col h-screen bg-[#0a0a0a]">
|
||||
<div className="flex flex-grow overflow-hidden">
|
||||
{/* Messages Container and Participants Container */}
|
||||
<div className="flex-grow flex flex-col overflow-hidden">
|
||||
<div className="flex-shrink-0 border-b border-zinc-800 ">
|
||||
<ContactProfile />
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
<MessagesArea />
|
||||
</div>
|
||||
<div className="flex-shrink-0 p-3 border-t border-zinc-800">
|
||||
{currentContact && currentContact.username?.length >= 4 ? (
|
||||
<MessageForm />
|
||||
) : null}
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
</MeContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<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>;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
5
client/src/utils/axiosClient.ts
Normal file
5
client/src/utils/axiosClient.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export const axiosClient = axios.create({
|
||||
baseURL: '/',
|
||||
});
|
||||
@@ -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<boolean>(false);
|
||||
|
||||
Reference in New Issue
Block a user