code refactor

This commit is contained in:
slawk0
2025-01-02 20:10:08 +01:00
parent c2b53e1c6c
commit 0ac655b83a
16 changed files with 130 additions and 156 deletions

View File

@@ -3,18 +3,17 @@ import {
Navigate, Navigate,
RouterProvider, RouterProvider,
} from 'react-router-dom'; } from 'react-router-dom';
import { useEffect, useState } from 'react'; import { Suspense } from 'react';
import Chat from './pages/Chat.tsx'; import Chat from './pages/Chat';
import Login from './pages/Login.tsx'; import Login from './pages/Login';
import Signup from './pages/Signup.tsx'; import Signup from './pages/Signup';
import Settings from './pages/Settings.tsx'; import Settings from './pages/Settings';
import Lost from './pages/404.tsx'; import Lost from './pages/404';
import { AuthContext } from './utils/AuthProvider.tsx'; import { AuthProvider } from '@/utils/AuthProvider.tsx';
import ProtectedRoutes from './utils/ProtectedRoutes.tsx'; import ProtectedRoutes from './utils/ProtectedRoutes';
import PublicRoute from '@/utils/PublicRoute.tsx'; import PublicRoute from './utils/PublicRoute';
import Cookies from 'js-cookie'; import { ChatProvider } from './context/chat/ChatProvider';
import { ChatProvider } from '@/context/chat/ChatProvider.tsx'; import LoadingScreen from './components/LoadingScreen';
import { axiosClient } from '@/utils/axiosClient.ts';
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@@ -27,9 +26,11 @@ const router = createBrowserRouter([
{ {
path: '/chat', path: '/chat',
element: ( element: (
<ChatProvider> <Suspense fallback={<LoadingScreen />}>
<Chat /> <ChatProvider>
</ChatProvider> <Chat />
</ChatProvider>
</Suspense>
), ),
}, },
{ {
@@ -58,34 +59,10 @@ const router = createBrowserRouter([
]); ]);
function App() { function App() {
const hasToken = Boolean(Cookies.get('token'));
const [authorized, setAuthorized] = useState(false);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function validateToken() {
if (!hasToken) return;
try {
await axiosClient.get('/api/auth/validate', {
withCredentials: true,
});
setAuthorized(true);
} catch (e) {
setAuthorized(false);
console.log('Failed to validate token: ', e);
} finally {
setIsLoading(false);
}
}
validateToken();
}, [hasToken]);
return ( return (
<AuthContext.Provider value={{ authorized, isLoading, setAuthorized }}> <AuthProvider>
<RouterProvider router={router} /> <RouterProvider router={router} />
</AuthContext.Provider> </AuthProvider>
); );
} }

View File

@@ -1,22 +1,18 @@
import { useState } from 'react'; import { useState } from 'react';
import { Trash2 } from 'lucide-react'; import { Trash2 } from 'lucide-react';
import AttachmentPreview from './AttachmentPreview.tsx'; import AttachmentPreview from './AttachmentPreview.tsx';
import { ChatMessagesProps, UserType } from '@/types/types.ts';
import { ChatMessagesProps } from '@/types/types.ts'; import { useOutletContext } from 'react-router-dom';
import { useChat } from '@/context/chat/useChat.ts';
type AnimatedMessageProps = { type AnimatedMessageProps = {
message: ChatMessagesProps; message: ChatMessagesProps;
isAdmin: boolean;
currentUsername: string | null;
onDelete: (messageId: number) => void; onDelete: (messageId: number) => void;
}; };
const AnimatedMessage = ({ const AnimatedMessage = ({ onDelete, message }: AnimatedMessageProps) => {
message, const user: UserType = useOutletContext();
isAdmin, const { me, groupOwner } = useChat();
currentUsername,
onDelete,
}: AnimatedMessageProps) => {
const [isRemoving, setIsRemoving] = useState(false); const [isRemoving, setIsRemoving] = useState(false);
const handleDelete = () => { const handleDelete = () => {
@@ -54,7 +50,11 @@ const AnimatedMessage = ({
</div> </div>
)} )}
</div> </div>
{message.sender === currentUsername || isAdmin ? ( {me.isGroupOwner ||
message.sender == user.username ||
(me.isGroupAdmin &&
me.isGroupOwner &&
message.sender_id !== groupOwner) ? (
<Trash2 <Trash2
className="opacity-0 group-hover:opacity-100 h-5 w-5 ml-2 flex-shrink-0 cursor-pointer text-gray-400 hover:text-red-500 transition-colors duration-200" className="opacity-0 group-hover:opacity-100 h-5 w-5 ml-2 flex-shrink-0 cursor-pointer text-gray-400 hover:text-red-500 transition-colors duration-200"
onClick={handleDelete} onClick={handleDelete}

View File

@@ -109,7 +109,8 @@ const MessageForm = () => {
}; };
const submitMessage: SubmitHandler<InputProps> = async (data) => { const submitMessage: SubmitHandler<InputProps> = async (data) => {
if ((!data.message && files.length === 0) || isSending) return; if ((data.message.trim().length < 1 && files.length === 0) || isSending)
return;
setErrorMessage(null); setErrorMessage(null);
setIsSending(true); setIsSending(true);
if (!socket) { if (!socket) {

View File

@@ -4,7 +4,7 @@ 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 { ChatMessagesProps, UsernameType } from '@/types/types.ts'; import { ChatMessagesProps, UserType } from '@/types/types.ts';
import { useChat } from '@/context/chat/useChat.ts'; import { useChat } from '@/context/chat/useChat.ts';
function MessagesArea() { function MessagesArea() {
@@ -17,10 +17,9 @@ function MessagesArea() {
setContactsList, setContactsList,
errorMessage, errorMessage,
fetchPreviousMessages, fetchPreviousMessages,
me,
} = useChat(); } = useChat();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const user: UsernameType = useOutletContext(); const user: UserType = useOutletContext();
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [shouldScrollToBottom, setShouldScrollToBottom] = useState(true); const [shouldScrollToBottom, setShouldScrollToBottom] = useState(true);
const previousMessagesLength = useRef(messages.length); const previousMessagesLength = useRef(messages.length);
@@ -223,13 +222,7 @@ function MessagesArea() {
<ul className="flex-grow list-none"> <ul className="flex-grow list-none">
{isLoading ? <LoadingWheel /> : null} {isLoading ? <LoadingWheel /> : null}
{messages.map((msg: ChatMessagesProps) => ( {messages.map((msg: ChatMessagesProps) => (
<AnimatedMessage <AnimatedMessage message={msg} onDelete={deleteMessage} />
key={msg.message_id}
message={msg}
isAdmin={me.isGroupAdmin || me.isGroupOwner}
currentUsername={user.username}
onDelete={deleteMessage}
/>
))} ))}
</ul> </ul>
</div> </div>

View File

@@ -2,17 +2,16 @@ import zdjecie from '../../../../assets/turtleProfileImg3.webp';
import logoutIcon from '../../../../assets/logout.svg'; import logoutIcon from '../../../../assets/logout.svg';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { useOutletContext } from 'react-router-dom'; import { useOutletContext } from 'react-router-dom';
import { UserType } from '@/types/types.ts';
import { UsernameType } from '@/types/types.ts';
function UserProfile() { function UserProfile() {
const user: UsernameType = useOutletContext(); const user: UserType = useOutletContext();
function logout() { function logout() {
Cookies.remove('token'); Cookies.remove('token');
localStorage.removeItem('contact'); localStorage.removeItem('contact');
window.location.reload(); window.location.reload();
} }
return ( return (
<div className="dropdown dropdown-top border-t border-zinc-800"> <div className="dropdown dropdown-top border-t border-zinc-800">
<div <div

View File

@@ -9,7 +9,7 @@ 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 { ParticipantsProps, UsernameType } from '@/types/types.ts'; import { ParticipantsProps, UserType } from '@/types/types.ts';
import { useChat } from '@/context/chat/useChat.ts'; import { useChat } from '@/context/chat/useChat.ts';
import { axiosClient } from '@/utils/axiosClient.ts'; import { axiosClient } from '@/utils/axiosClient.ts';
@@ -17,7 +17,7 @@ function ParticipantsBar() {
const { setMe, initializeContact, setGroupOwner, currentContact, me } = const { setMe, initializeContact, setGroupOwner, currentContact, me } =
useChat(); useChat();
const [participants, setParticipants] = useState<ParticipantsProps[]>([]); const [participants, setParticipants] = useState<ParticipantsProps[]>([]);
const user: UsernameType = useOutletContext(); const user: UserType = useOutletContext();
const getParticipants = async () => { const getParticipants = async () => {
try { try {
const response = await axiosClient.get( const response = await axiosClient.get(
@@ -90,22 +90,23 @@ function ParticipantsBar() {
}; };
useEffect(() => { useEffect(() => {
if (participants.length > 0 && user?.user_id) { console.error(participants.length, user.id);
if (participants.length > 0 && user?.id) {
const userIsAdmin = participants.some( const userIsAdmin = participants.some(
(participant) => (participant) => participant.user_id === user.id && participant.isadmin,
participant.user_id === user.user_id && participant.isadmin,
); );
const userIsOwner = participants.some( const userIsOwner = participants.some(
(participant) => (participant) => participant.user_id === user.id && participant.isowner,
participant.user_id === user.user_id && participant.isowner,
); );
const whoIsOwner = participants.find( const whoIsOwner = participants.find(
(participant) => participant.isowner, (participant) => participant.isowner,
); );
setGroupOwner(whoIsOwner?.user_id); setGroupOwner(whoIsOwner?.user_id);
setMe({ isGroupAdmin: userIsAdmin, isGroupOwner: userIsOwner }); setMe({ isGroupAdmin: userIsAdmin, isGroupOwner: userIsOwner });
console.error('SETME: ', userIsAdmin, userIsOwner);
} }
}, [participants, user?.user_id]); }, [participants, user?.id]);
useEffect(() => { useEffect(() => {
if (currentContact) { if (currentContact) {
@@ -140,7 +141,7 @@ function ParticipantsBar() {
const { group_id } = msg; const { group_id } = msg;
if ( if (
msg.group_id == currentContact?.conversation_id && msg.group_id == currentContact?.conversation_id &&
msg.user_id == user?.user_id msg.user_id == user?.id
) { ) {
initializeContact({ initializeContact({
read: true, read: true,
@@ -172,7 +173,7 @@ function ParticipantsBar() {
const handleLeftGroup = (msg: { user_id: string; group_id: string }) => { const handleLeftGroup = (msg: { user_id: string; group_id: string }) => {
if ( if (
msg.group_id == currentContact?.conversation_id && msg.group_id == currentContact?.conversation_id &&
msg.user_id == user?.user_id msg.user_id == user?.id
) { ) {
setParticipants([]); setParticipants([]);
initializeContact(currentContact); initializeContact(currentContact);
@@ -200,7 +201,7 @@ function ParticipantsBar() {
}) => { }) => {
console.log('(socket) removed administrator: ', msg); console.log('(socket) removed administrator: ', msg);
if (msg.group_id === currentContact.conversation_id) { if (msg.group_id === currentContact.conversation_id) {
if (msg.user_id === user?.user_id) { if (msg.user_id === user?.id) {
setMe({ isGroupAdmin: false, isGroupOwner: false }); setMe({ isGroupAdmin: false, isGroupOwner: false });
} }
setParticipants((prevMembers) => setParticipants((prevMembers) =>
@@ -222,7 +223,7 @@ function ParticipantsBar() {
socket?.off('added to group'); socket?.off('added to group');
socket?.off('left group'); socket?.off('left group');
}; };
}, [socket, currentContact, currentContact, user?.user_id]); }, [socket, currentContact, currentContact, user?.id]);
const ParticipantsList = sortedParticipants?.map( const ParticipantsList = sortedParticipants?.map(
(participant: ParticipantsProps) => ( (participant: ParticipantsProps) => (
@@ -255,7 +256,7 @@ function ParticipantsBar() {
</span> </span>
</li> </li>
</ContextMenuTrigger> </ContextMenuTrigger>
{user.user_id !== participant.user_id && {user.id !== participant.user_id &&
me.isGroupAdmin && me.isGroupAdmin &&
(!participant.isadmin || me.isGroupOwner) ? ( (!participant.isadmin || me.isGroupOwner) ? (
<ContextMenuContent className="p-0"> <ContextMenuContent className="p-0">
@@ -298,7 +299,6 @@ function ParticipantsBar() {
<h1 className="text-xl font-semibold text-gray-200 text-left p-4 border-b border-zinc-800"> <h1 className="text-xl font-semibold text-gray-200 text-left p-4 border-b border-zinc-800">
Members Members
</h1> </h1>
<ul className=" rounded">{ParticipantsList}</ul> <ul className=" rounded">{ParticipantsList}</ul>
</div> </div>
); );

View File

@@ -93,7 +93,7 @@ const SidebarProvider = React.forwardRef<
: setOpen((open) => !open); : setOpen((open) => !open);
}, [isMobile, setOpen, setOpenMobile]); }, [isMobile, setOpen, setOpenMobile]);
useEffect(() => { React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if ( if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT && event.key === SIDEBAR_KEYBOARD_SHORTCUT &&

View File

@@ -2,7 +2,7 @@ import { useForm, SubmitHandler } from 'react-hook-form';
import icon from '../../assets/icon.png'; import icon from '../../assets/icon.png';
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/AuthContext.tsx';
import LoadingWheel from '../components/chat/LoadingWheel.tsx'; import LoadingWheel from '../components/chat/LoadingWheel.tsx';
import { axiosClient } from '@/utils/axiosClient.ts'; import { axiosClient } from '@/utils/axiosClient.ts';

View File

@@ -2,7 +2,7 @@ import icon from '../../assets/icon.png';
import { useForm, SubmitHandler } from 'react-hook-form'; 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/AuthContext.tsx';
import LoadingWheel from '../components/chat/LoadingWheel.tsx'; import LoadingWheel from '../components/chat/LoadingWheel.tsx';
import { axiosClient } from '@/utils/axiosClient.ts'; import { axiosClient } from '@/utils/axiosClient.ts';

View File

@@ -39,9 +39,9 @@ export type FileWithPreviewProps = {
preview: string | null; preview: string | null;
}; };
export type UsernameType = { export type UserType = {
username: string | null; username: string | null;
user_id: string | null; id: string | null;
}; };
export type ChatContextType = { export type ChatContextType = {

View File

@@ -0,0 +1,16 @@
import { createContext } from 'react';
import { UserType } from '@/types/types.ts';
interface AuthContextType {
authorized: boolean;
isLoading: boolean;
user: UserType | null;
setAuthorized: (value: boolean) => void;
}
export const AuthContext = createContext<AuthContextType>({
authorized: false,
isLoading: true,
user: null,
setAuthorized: () => {},
});

View File

@@ -1,13 +1,48 @@
import { createContext } from 'react'; import { AuthContext } from './AuthContext';
import Cookies from 'js-cookie';
import { axiosClient } from '@/utils/axiosClient.ts';
import { ReactNode, useEffect, useState } from 'react';
import { UserType } from '@/types/types.ts';
type AuthContextType = { export function AuthProvider({ children }: { children: ReactNode }) {
authorized: boolean; const [authorized, setAuthorized] = useState(false);
isLoading: boolean; const [isLoading, setIsLoading] = useState(true);
setAuthorized: (value: boolean) => void; const [user, setUser] = useState<UserType | null>(null);
}; const token = Cookies.get('token');
export const AuthContext = createContext<AuthContextType>({ useEffect(() => {
authorized: false, async function validateAuth() {
isLoading: true, if (!token) {
setAuthorized: () => {}, setIsLoading(false);
}); return;
}
try {
const response = await axiosClient.get('/api/auth/validate', {
withCredentials: true,
});
setUser({
username: response.data.username,
id: response.data.user_id,
});
setAuthorized(true);
} catch (error) {
console.error('Auth validation failed:', error);
setAuthorized(false);
setUser(null);
} finally {
setIsLoading(false);
}
}
validateAuth();
}, [token]);
return (
<AuthContext.Provider
value={{ authorized, isLoading, user, setAuthorized }}
>
{children}
</AuthContext.Provider>
);
}

View File

@@ -1,34 +1,15 @@
import { Navigate, Outlet } from 'react-router-dom'; import { Navigate, Outlet } from 'react-router-dom';
import { useContext, useEffect, useState } from 'react'; import { useAuth } from '@/utils/useAuth.tsx';
import { AuthContext } from './AuthProvider.tsx'; import LoadingScreen from '@/components/LoadingScreen';
import LoadingScreen from '../components/LoadingScreen.tsx';
import { UsernameType } from '@/types/types.ts';
import { axiosClient } from '@/utils/axiosClient.ts';
function ProtectedRoutes() { function ProtectedRoutes() {
const { authorized, isLoading } = useContext(AuthContext); const { authorized, isLoading, user } = useAuth();
const [user, setUser] = useState<UsernameType>({
username: null,
user_id: null,
});
useEffect(() => {
if (authorized) {
axiosClient
.get('/api/auth/validate', { withCredentials: true })
.then((res) => {
setUser(res.data);
})
.catch(console.error);
}
}, [authorized]);
if (isLoading) { if (isLoading) {
return <LoadingScreen />; return <LoadingScreen />;
} }
return authorized ? ( return authorized ? (
<Outlet context={user satisfies UsernameType} /> <Outlet context={user} />
) : ( ) : (
<Navigate to="/login" replace /> <Navigate to="/login" replace />
); );

View File

@@ -1,10 +1,9 @@
import { Navigate, Outlet } from 'react-router-dom'; import { Navigate, Outlet } from 'react-router-dom';
import { useContext } from 'react'; import { useAuth } from '@/utils/useAuth.tsx';
import { AuthContext } from './AuthProvider'; import LoadingScreen from '@/components/LoadingScreen';
import LoadingScreen from '../components/LoadingScreen';
function PublicRoute() { function PublicRoute() {
const { authorized, isLoading } = useContext(AuthContext); const { authorized, isLoading } = useAuth();
if (isLoading) { if (isLoading) {
return <LoadingScreen />; return <LoadingScreen />;

View File

@@ -1,33 +1,4 @@
import { useState, useEffect } from 'react'; import { useContext } from 'react';
import Cookies from 'js-cookie'; import { AuthContext } from '@/utils/AuthContext.tsx';
import { axiosClient } from '@/utils/axiosClient.ts'; export const useAuth = () => useContext(AuthContext);
function useAuth() {
const [authorized, setAuthorized] = useState<boolean>(false);
useEffect(() => {
async function validateToken() {
const token = Cookies.get('token');
if (!token) {
setAuthorized(false);
return;
}
try {
await axiosClient.get('/api/auth/validate', { withCredentials: true });
setAuthorized(true);
} catch (e) {
setAuthorized(false);
console.log('Failed to validate token: ', e);
}
}
validateToken();
}, []);
return authorized;
}
export default useAuth;

View File

@@ -13,6 +13,8 @@ services:
PG_HOST: db PG_HOST: db
ports: ports:
- "3000" - "3000"
volumes:
- attachments:/app/client/server/attachments
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy