]> PHS Git Server - phs-home.git/commitdiff
Adding proper file upload management to chat #PHO-2.
authorcharleswrayjr <charleswrayjr@gmail.com>
Sat, 27 Sep 2025 05:20:12 +0000 (00:20 -0500)
committercharleswrayjr <charleswrayjr@gmail.com>
Sat, 27 Sep 2025 05:20:12 +0000 (00:20 -0500)
src/app/components/Chat.jsx [deleted file]
src/app/views/Chat/Chat.jsx [new file with mode: 0755]

diff --git a/src/app/components/Chat.jsx b/src/app/components/Chat.jsx
deleted file mode 100755 (executable)
index 1919425..0000000
+++ /dev/null
@@ -1,347 +0,0 @@
-/**
- * @file Chat component for real-time messaging
- * @module Chat
- */
-
-import React, { useEffect, useRef, useState } from 'react';
-import axios from 'axios';
-import { API_BASE_URL } from '../../App';
-import AddIcon from '@mui/icons-material/Add';
-import {
-  Button,
-  Card,
-  CardActions,
-  CardContent,
-  CardHeader,
-  Grid,
-  IconButton,
-  InputAdornment,
-  TextField
-} from '@mui/material';
-import ChatBubble from './ChatBubble';
-
-/**
- * @typedef {Object} User
- * @property {number} id
- * @property {string} first_name
- * @property {string} middle_name
- * @property {string} last_name
- */
-
-/**
- * Chat component
- * @param {Object} props - Component props
- * @param {Object|null} props.user - Current user
- * @param {Object} props.socket - Socket.io client instance
- * @returns {JSX.Element} Rendered component
- */
-const Chat = ( { user, socket } ) => {
-  const [conversations, setConversations] = useState( [] );
-  const [selectedConversation, setSelectedConversation] = useState( null );
-  const [messages, setMessages] = useState( [] );
-  const [content, setContent] = useState( '' );
-  const [attachedFiles, setAttachedFiles] = useState( [] );
-  const [users, setUsers] = useState( [] );
-  const [groups, setGroups] = useState( [] );
-  const [error, setError] = useState( null );
-  const [isSidebarOpen, setIsSidebarOpen] = useState( true );
-  const messagesEndRef = useRef( null );
-  const fileRef = useRef( null );
-
-  useEffect( () => {
-    if (user) {
-      const fetchData = async () => {
-        try {
-          const [usersRes, groupsRes, messagesRes] = await Promise.all( [
-            axios.get( `${ API_BASE_URL }/user`, { withCredentials:true } ),
-            axios.get( `${ API_BASE_URL }/message_group`, { withCredentials:true } ),
-            axios.get( `${ API_BASE_URL }/message`, { withCredentials:true } )
-          ] );
-          setUsers( Array.isArray( usersRes.data ) ? usersRes.data.filter( u => u.id !== user.id ) : [] );
-          setGroups( Array.isArray( groupsRes.data ) ? groupsRes.data : [] );
-
-          // Group messages by recipient_id or group_id
-          const messages = Array.isArray( messagesRes.data ) ? messagesRes.data : [];
-          const convoMap = {};
-          messages.forEach( msg => {
-            if (msg.group_id) {
-              // Group chat
-              const key = `group_${ msg.group_id }`;
-              if (!convoMap[key]) {
-                convoMap[key] = {
-                  id:key,
-                  type:'group',
-                  targetId:msg.group_id,
-                  name:groupsRes.data.find( g => g.id === msg.group_id )?.name || 'Group',
-                  lastMessage:msg.content,
-                  lastMessageTime:msg.created_at
-                };
-              }
-            } else {
-              // Private chat: Use sorted user IDs to ensure single conversation
-              const otherUserId = msg.sender_id === user.id ? msg.recipient_id : msg.sender_id;
-              const key = `user_${ [user.id, otherUserId].sort( ( a, b ) => a - b ).join( '_' ) }`;
-              if (!convoMap[key]) {
-                convoMap[key] = {
-                  id:key,
-                  type:'user',
-                  targetId:otherUserId,
-                  name:usersRes.data.find( u => u.id === otherUserId )?.first_name || 'User',
-                  lastMessage:msg.content,
-                  lastMessageTime:msg.created_at
-                };
-              }
-            }
-          } );
-          setConversations( Object.values( convoMap ).sort( ( a, b ) => new Date( b.lastMessageTime ) - new Date( a.lastMessageTime ) ) );
-          setMessages( messages );
-        } catch (err) {
-          console.error( 'Chat data fetch error:', err );
-          setError( 'Failed to load chat data' );
-        }
-      };
-      fetchData().catch();
-
-      socket.on( 'newMessage', msg => {
-        setMessages( prev => (Array.isArray( prev ) ? [...prev, msg] : [msg]) );
-        setConversations( prev => {
-          let key, convo;
-          if (msg.group_id) {
-            key = `group_${ msg.group_id }`;
-            convo = {
-              id:key,
-              type:'group',
-              targetId:msg.group_id,
-              name:groups.find( g => g.id === msg.group_id )?.name || 'Group',
-              lastMessage:msg.content,
-              lastMessageTime:msg.created_at
-            };
-          } else {
-            const otherUserId = msg.sender_id === user.id ? msg.recipient_id : msg.sender_id;
-            key = `user_${ [user.id, otherUserId].sort( ( a, b ) => a - b ).join( '_' ) }`;
-            convo = {
-              id:key,
-              type:'user',
-              targetId:otherUserId,
-              name:users.find( u => u.id === otherUserId )?.first_name || 'User',
-              lastMessage:msg.content,
-              lastMessageTime:msg.created_at
-            };
-          }
-          return [convo, ...prev.filter( c => c.id !== key )].sort( ( a, b ) => new Date( b.lastMessageTime ) - new Date( a.lastMessageTime ) );
-        } );
-      } );
-
-      socket.on( 'newReaction', ( { messageId, reaction_data } ) => {
-        setMessages( prev => {
-          if (!Array.isArray( prev )) return prev;
-          return prev.map( m => m.id === parseInt( messageId ) ? {
-            ...m,
-            reactions:[...(Array.isArray( m.reactions ) ? Array.from( new Set( m.reactions ) ) : []), reaction_data]
-          } : m );
-        } );
-      } );
-
-      return () => {
-        socket.off( 'newMessage' );
-        socket.off( 'newReaction' );
-      };
-    }
-  }, [user] );
-
-  useEffect( () => {
-    messagesEndRef.current?.scrollIntoView( { behavior:'smooth' } );
-  }, [messages] );
-
-  /**
-   * Select a conversation to view messages
-   * @param {Object} convo - Conversation object
-   */
-  const selectConversation = ( convo ) => {
-    setSelectedConversation( convo );
-    setMessages(
-      Array.isArray( messages )
-        ? messages.filter( m =>
-          (convo.type === 'group' && m.group_id === convo.targetId) ||
-          (convo.type === 'user' && (m.recipient_id === convo.targetId || m.sender_id === convo.targetId))
-        )
-        : []
-    );
-    setIsSidebarOpen( false );
-  };
-
-  /**
-   * Send a message with optional file attachments
-   */
-  const sendMessage = async () => {
-    if (content.trim() || attachedFiles.length) {
-      let fileIds = [];
-      try {
-        if (attachedFiles.length) {
-          const formData = new FormData();
-          attachedFiles.forEach( f => formData.append( 'files', f ) );
-          formData.append( 'visibility', selectedConversation?.type === 'group' ? 'family' : selectedConversation?.type === 'user' ? 'specific' : 'family' );
-          if (selectedConversation?.type === 'user') formData.append( 'specific_users', JSON.stringify( [selectedConversation.targetId] ) );
-          const res = await axios.post( `${ API_BASE_URL }/file/create`, formData, {
-            headers:{ 'Content-Type':'multipart/form-data' },
-            withCredentials:true
-          } );
-          fileIds = Array.isArray( res.data ) ? res.data.map( f => f.id ) : [res.data.id];
-        }
-        socket.emit( 'chatMessage', {
-          content,
-          recipientId:selectedConversation?.type === 'user' ? selectedConversation.targetId : null,
-          groupId:selectedConversation?.type === 'group' ? selectedConversation.targetId : null,
-          fileIds
-        } );
-        setContent( '' );
-        setAttachedFiles( [] );
-      } catch (err) {
-        console.error( 'Send message error:', err );
-        setError( 'Failed to send message' );
-      }
-    }
-  };
-
-  if (!user) {
-    return <div className="text-center pt-20">Please log in to access chat</div>;
-  }
-
-  if (error) {
-    return <div className="text-center pt-20 text-red-500">{ error }</div>;
-  }
-
-  /**
-   *
-   * @returns {React.ReactNode[]}
-   */
-  const getConvos = () => (
-    conversations.map( convo => (
-      <div key={ convo.id }
-           onClick={ () => selectConversation( convo ) }
-           className={ `p-2 mb-2 rounded cursor-pointer ${ selectedConversation?.id === convo.id ? 'bg-blue-200' : 'hover:bg-gray-200' }` }>
-        <p className="font-bold">{ convo.name } ({ convo.type === 'group' ? 'Group' : 'Private' })</p>
-        <p className="text-sm text-gray-600 truncate">{ convo.lastMessage }</p>
-        <p className="text-xs text-gray-500">{ new Date( convo.lastMessageTime ).toLocaleString() }</p>
-      </div>
-    ) )
-  );
-
-  /**
-   *
-   * @returns {React.ReactNode[]}
-   */
-  const getMessages = () => (
-    messages.map( ( msg, i ) => (
-      <Grid key={ i }
-            size={ { xs:12 } }>
-        <div className={ `mb-4 flex ${ msg.sender_id === user.id ? 'justify-end' : 'justify-start' }` }>
-          <ChatBubble user={ user }
-                      msg={ msg }
-                      socket={ socket }
-                      setError={ setError }/>
-        </div>
-      </Grid>
-    ) )
-  );
-
-  return (
-    <Grid container
-          className="chat-container"
-          spacing={ 0 }>
-      { isSidebarOpen && <Grid size={{ xs: 10, md: 6, lg: 3 }}>
-        { getConvos() }
-      </Grid> }
-      <Grid size="grow">
-        <Card className="chat-card">
-          <CardHeader className="chat-header"
-                      title={
-                        <div className="flex justify-between items-center p-4 border-b">
-                          <h2 className="text-2xl">
-                            { selectedConversation ? `${ selectedConversation.name } (${ selectedConversation.type === 'group' ? 'Group' : 'Private' })` : 'Select a conversation' }
-                          </h2>
-                          <button onClick={ () => setIsSidebarOpen( prevState => !prevState ) }
-                                  className="text-blue-600">☰
-                          </button>
-                        </div> }/>
-          <CardContent className="chat-content">
-            <div className="flex-1 overflow-y-auto p-4 bg-gray-50">
-              { selectedConversation ? (
-                messages.length === 0 ? (
-                  <p className="text-center text-gray-500">No messages in this conversation</p>
-                ) : (
-                  <Grid container
-                        columnSpacing={ 0 }
-                        rowSpacing={ 2 }
-                        maxWidth="xxl">
-                    { getMessages() }
-                  </Grid>
-                )
-              ) : (
-                <p className="text-center text-gray-500">Select a conversation to start chatting</p>
-              ) }
-              <div ref={ messagesEndRef }/>
-            </div>
-          </CardContent>
-          <CardActions className="chat-actions">
-            <input type="file"
-                   accept="image/!*,video/!*"
-                   multiple
-                   hidden
-                   ref={fileRef}
-                   onChange={ e => setAttachedFiles( Array.from( e.target.files ) ) }
-                   className="mb-2"/>
-            { selectedConversation ? (
-              <TextField value={ content }
-                         className='chat-input'
-                         onChange={ e => setContent( e.target.value ) }
-                         placeholder="Type a message"
-                         fullWidth
-                         slotProps={ {
-                           input:{
-                             startAdornment:
-                               <InputAdornment
-                                 position="start">
-                                 <Button
-                                   variant='contained'
-                                   onClick={fileRef?.current?.click}
-                                   size="small"
-                                   color="primary"><AddIcon/></Button>
-                             </InputAdornment>,
-                             endAdornment:
-                               <InputAdornment
-                                 variant="filled"
-                                 position="end">
-                               <Button
-                                 variant="contained"
-                                 color="primary"
-                                 onClick={sendMessage}
-                                 size="small">Send</Button>
-                             </InputAdornment>
-                           }
-                         } }/>
-              /*<div className="p-4 border-t">
-                <input type="file"
-                       accept="image/!*,video/!*"
-                       multiple
-                       onChange={ e => setAttachedFiles( Array.from( e.target.files ) ) }
-                       className="mb-2"/>
-                <div className="flex">
-                  <input value={ content }
-                         onChange={ e => setContent( e.target.value ) }
-                         placeholder="Type a message"
-                         className="border p-2 flex-grow rounded-l"/>
-                  <button onClick={ sendMessage }
-                          className="bg-blue-500 text-white p-2 rounded-r">Send
-                  </button>
-                </div>
-              </div>*/
-            ) : null }
-          </CardActions>
-        </Card>
-      </Grid>
-    </Grid>
-  );
-};
-
-export default Chat;
\ No newline at end of file
diff --git a/src/app/views/Chat/Chat.jsx b/src/app/views/Chat/Chat.jsx
new file mode 100755 (executable)
index 0000000..43a3cf3
--- /dev/null
@@ -0,0 +1,447 @@
+/**
+ * @file Chat component for real-time messaging
+ * @module Chat
+ */
+
+import React, { useEffect, useRef, useState } from 'react';
+import axios from 'axios';
+import { API_BASE_URL } from '../../../App';
+import AddIcon from '@mui/icons-material/Add';
+import {
+  Button,
+  Card,
+  CardActions,
+  CardContent,
+  CardHeader,
+  Grid,
+  IconButton,
+  InputAdornment,
+  TextField,
+  Box,
+  CardMedia,
+  Chip,
+  LinearProgress
+} from '@mui/material';
+import ChatBubble from '../../components/ChatBubble';
+
+/**
+ * @typedef {Object} User
+ * @property {number} id
+ * @property {string} first_name
+ * @property {string} middle_name
+ * @property {string} last_name
+ */
+
+/**
+ * Chat component
+ * @param {Object} props - Component props
+ * @param {Object|null} props.user - Current user
+ * @param {Object} props.socket - Socket.io client instance
+ * @returns {JSX.Element} Rendered component
+ */
+const Chat = ( { user, socket, maxSizeMB = 10 } ) => {
+  const [conversations, setConversations] = useState( [] );
+  const [selectedConversation, setSelectedConversation] = useState( null );
+  const [messages, setMessages] = useState( [] );
+  const [content, setContent] = useState( '' );
+  const [attachedFiles, setAttachedFiles] = useState( [] );
+  const [users, setUsers] = useState( [] );
+  const [groups, setGroups] = useState( [] );
+  const [error, setError] = useState( null );
+  const [isSidebarOpen, setIsSidebarOpen] = useState( true );
+  const messagesEndRef = useRef( null );
+  const fileRef = useRef( null );
+
+  /**
+   * @type {[MediaFile[], React.Dispatch<React.SetStateAction<MediaFile[]>>]}
+   */
+  const [selectedMedia, setSelectedMedia] = useState([]);
+  /**
+   * @type {[string[], React.Dispatch<React.SetStateAction<string[]>>]}
+   */
+  const [previews, setPreviews] = useState([]);
+  /**
+   * @type {[number, React.Dispatch<React.SetStateAction<number>>]}
+   */
+  const [progress, setProgress] = useState(0);
+
+  useEffect( () => {
+    if (user) {
+      const fetchData = async () => {
+        try {
+          const [usersRes, groupsRes, messagesRes] = await Promise.all( [
+            axios.get( `${ API_BASE_URL }/user`, { withCredentials:true } ),
+            axios.get( `${ API_BASE_URL }/message_group`, { withCredentials:true } ),
+            axios.get( `${ API_BASE_URL }/message`, { withCredentials:true } )
+          ] );
+          setUsers( Array.isArray( usersRes.data ) ? usersRes.data.filter( u => u.id !== user.id ) : [] );
+          setGroups( Array.isArray( groupsRes.data ) ? groupsRes.data : [] );
+
+          // Group messages by recipient_id or group_id
+          const messages = Array.isArray( messagesRes.data ) ? messagesRes.data : [];
+          const convoMap = {};
+          messages.forEach( msg => {
+            if (msg.group_id) {
+              // Group chat
+              const key = `group_${ msg.group_id }`;
+              if (!convoMap[key]) {
+                convoMap[key] = {
+                  id:key,
+                  type:'group',
+                  targetId:msg.group_id,
+                  name:groupsRes.data.find( g => g.id === msg.group_id )?.name || 'Group',
+                  lastMessage:msg.content,
+                  lastMessageTime:msg.created_at
+                };
+              }
+            } else {
+              // Private chat: Use sorted user IDs to ensure single conversation
+              const otherUserId = msg.sender_id === user.id ? msg.recipient_id : msg.sender_id;
+              const key = `user_${ [user.id, otherUserId].sort( ( a, b ) => a - b ).join( '_' ) }`;
+              if (!convoMap[key]) {
+                convoMap[key] = {
+                  id:key,
+                  type:'user',
+                  targetId:otherUserId,
+                  name:usersRes.data.find( u => u.id === otherUserId )?.first_name || 'User',
+                  lastMessage:msg.content,
+                  lastMessageTime:msg.created_at
+                };
+              }
+            }
+          } );
+          setConversations( Object.values( convoMap ).sort( ( a, b ) => new Date( b.lastMessageTime ) - new Date( a.lastMessageTime ) ) );
+          setMessages( messages );
+        } catch (err) {
+          console.error( 'Chat data fetch error:', err );
+          setError( 'Failed to load chat data' );
+        }
+      };
+      fetchData().catch();
+
+      socket.on( 'newMessage', msg => {
+        setMessages( prev => (Array.isArray( prev ) ? [...prev, msg] : [msg]) );
+        setConversations( prev => {
+          let key, convo;
+          if (msg.group_id) {
+            key = `group_${ msg.group_id }`;
+            convo = {
+              id:key,
+              type:'group',
+              targetId:msg.group_id,
+              name:groups.find( g => g.id === msg.group_id )?.name || 'Group',
+              lastMessage:msg.content,
+              lastMessageTime:msg.created_at
+            };
+          } else {
+            const otherUserId = msg.sender_id === user.id ? msg.recipient_id : msg.sender_id;
+            key = `user_${ [user.id, otherUserId].sort( ( a, b ) => a - b ).join( '_' ) }`;
+            convo = {
+              id:key,
+              type:'user',
+              targetId:otherUserId,
+              name:users.find( u => u.id === otherUserId )?.first_name || 'User',
+              lastMessage:msg.content,
+              lastMessageTime:msg.created_at
+            };
+          }
+          return [convo, ...prev.filter( c => c.id !== key )].sort( ( a, b ) => new Date( b.lastMessageTime ) - new Date( a.lastMessageTime ) );
+        } );
+      } );
+
+      socket.on( 'newReaction', ( { messageId, reaction_data } ) => {
+        setMessages( prev => {
+          if (!Array.isArray( prev )) return prev;
+          return prev.map( m => m.id === parseInt( messageId ) ? {
+            ...m,
+            reactions:[...(Array.isArray( m.reactions ) ? Array.from( new Set( m.reactions ) ) : []), reaction_data]
+          } : m );
+        } );
+      } );
+
+      return () => {
+        socket.off( 'newMessage' );
+        socket.off( 'newReaction' );
+      };
+    }
+  }, [user] );
+
+  useEffect( () => {
+    messagesEndRef.current?.scrollIntoView( { behavior:'smooth' } );
+  }, [messages] );
+
+  /**
+   * Select a conversation to view messages
+   * @param {Object} convo - Conversation object
+   */
+  const selectConversation = ( convo ) => {
+    setSelectedConversation( convo );
+    setMessages(
+      Array.isArray( messages )
+        ? messages.filter( m =>
+          (convo.type === 'group' && m.group_id === convo.targetId) ||
+          (convo.type === 'user' && (m.recipient_id === convo.targetId || m.sender_id === convo.targetId))
+        )
+        : []
+    );
+    setIsSidebarOpen( false );
+  };
+
+  /**
+   * Send a message with optional file attachments
+   */
+  const sendMessage = async () => {
+    if (content.trim() || attachedFiles.length) {
+      let fileIds = [];
+      try {
+        if (attachedFiles.length) {
+          const formData = new FormData();
+          attachedFiles.forEach( f => formData.append( 'files', f ) );
+          formData.append( 'visibility', selectedConversation?.type === 'group' ? 'family' : selectedConversation?.type === 'user' ? 'specific' : 'family' );
+          if (selectedConversation?.type === 'user') formData.append( 'specific_users', JSON.stringify( [selectedConversation.targetId] ) );
+          const res = await axios.post( `${ API_BASE_URL }/file/create`, formData, {
+            headers:{ 'Content-Type':'multipart/form-data' },
+            withCredentials:true
+          } );
+          fileIds = Array.isArray( res.data ) ? res.data.map( f => f.id ) : [res.data.id];
+        }
+        socket.emit( 'chatMessage', {
+          content,
+          recipientId:selectedConversation?.type === 'user' ? selectedConversation.targetId : null,
+          groupId:selectedConversation?.type === 'group' ? selectedConversation.targetId : null,
+          fileIds
+        } );
+        setContent( '' );
+        setAttachedFiles( [] );
+      } catch (err) {
+        console.error( 'Send message error:', err );
+        setError( 'Failed to send message' );
+      }
+    }
+  };
+
+  if (!user) {
+    return <div className="text-center pt-20">Please log in to access chat</div>;
+  }
+
+  if (error) {
+    return <div className="text-center pt-20 text-red-500">{ error }</div>;
+  }
+
+  /**
+   *
+   * @returns {React.ReactNode[]}
+   */
+  const getConvos = () => (
+    conversations.map( convo => (
+      <div key={ convo.id }
+           onClick={ () => selectConversation( convo ) }
+           className={ `p-2 mb-2 rounded cursor-pointer ${ selectedConversation?.id === convo.id ? 'bg-blue-200' : 'hover:bg-gray-200' }` }>
+        <p className="font-bold">{ convo.name } ({ convo.type === 'group' ? 'Group' : 'Private' })</p>
+        <p className="text-sm text-gray-600 truncate">{ convo.lastMessage }</p>
+        <p className="text-xs text-gray-500">{ new Date( convo.lastMessageTime ).toLocaleString() }</p>
+      </div>
+    ) )
+  );
+
+  /**
+   *
+   * @returns {React.ReactNode[]}
+   */
+  const getMessages = () => (
+    messages.map( ( msg, i ) => (
+      <Grid key={ i }
+            size={ { xs:12 } }>
+        <div className={ `mb-4 flex ${ msg.sender_id === user.id ? 'justify-end' : 'justify-start' }` }>
+          <ChatBubble user={ user }
+                      msg={ msg }
+                      socket={ socket }
+                      setError={ setError }/>
+        </div>
+      </Grid>
+    ) )
+  );
+
+  /**
+   * Handles file selection and validation
+   * @param {React.ChangeEvent<HTMLInputElement>} event
+   */
+  const handleFileChange = (event) => {
+    const files = Array.from(event.target.files);
+    const validMedia = files
+      .map((file) => ({
+        file,
+        type: file.type.startsWith('image/') ? 'image' : file.type.startsWith('video/') ? 'video' : null,
+      }))
+      .filter((media) => {
+        if (!media.type) {
+          alert('Please select image or video files only.');
+          return false;
+        }
+        if (media.file.size > maxSizeMB * 1024 * 1024) {
+          alert(`File size exceeds ${maxSizeMB}MB.`);
+          return false;
+        }
+        return true;
+      });
+
+    setSelectedMedia(validMedia);
+
+    // Generate previews
+    const newPreviews = validMedia.map((media) => URL.createObjectURL(media.file));
+    setPreviews(newPreviews);
+
+    // Trigger upload if provided
+    // if (onUpload && validMedia.length > 0) {
+    //   uploadFiles(validMedia.map((media) => media.file));
+    // }
+  };
+
+  /**
+   * Removes a file from the selection
+   * @param {number} index
+   */
+  const removeFile = (index) => {
+      setSelectedMedia((prev) => prev.filter((_, i) => i !== index));
+      setPreviews((prev) => {
+        const newPreviews = [...prev];
+        URL.revokeObjectURL(newPreviews[index]); // Free memory
+        newPreviews.splice(index, 1);
+        return newPreviews;
+      });
+    };
+
+  return (
+    <Grid container
+          className="page-container"
+          spacing={ 0 }>
+      { isSidebarOpen && <Grid size={{ xs: 10, md: 6, lg: 3 }}>
+        { getConvos() }
+      </Grid> }
+      <Grid size="grow">
+        <Card className="chat-card">
+          <CardHeader className="chat-header"
+                      title={
+                        <div className="flex justify-between items-center p-4 border-b">
+                          <h2 className="text-2xl">
+                            { selectedConversation ? `${ selectedConversation.name } (${ selectedConversation.type === 'group' ? 'Group' : 'Private' })` : 'Select a conversation' }
+                          </h2>
+                          <button onClick={ () => setIsSidebarOpen( prevState => !prevState ) }
+                                  className="text-blue-600">☰
+                          </button>
+                        </div> }/>
+          <CardContent className="chat-content">
+            <div className="flex-1 overflow-y-auto p-4 bg-gray-50">
+              { selectedConversation ? (
+                messages.length === 0 ? (
+                  <p className="text-center text-gray-500">No messages in this conversation</p>
+                ) : (
+                  <Grid container
+                        columnSpacing={ 0 }
+                        rowSpacing={ 2 }
+                        maxWidth="xxl">
+                    { getMessages() }
+                  </Grid>
+                )
+              ) : (
+                <p className="text-center text-gray-500">Select a conversation to start chatting</p>
+              ) }
+              <div ref={ messagesEndRef }/>
+            </div>
+
+            {/* Progress Bar */}
+            {progress > 0 && (
+              <LinearProgress variant="determinate" value={progress} sx={{ mt: 1 }} />
+            )}
+            { selectedConversation && previews.length > 0 && (
+              <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mt: 2, width: '100vw' }}>
+                {previews.map((preview, index) => (
+                  <Card key={index} sx={{ width: 150, position: 'relative' }}>
+                    {selectedMedia[index].type === 'image' ? (
+                      <CardMedia component="img" height="150" image={preview} alt="preview" />
+                    ) : (
+                      <CardMedia
+                        component="video"
+                        height="150"
+                        src={preview}
+                        controls
+                        sx={{ objectFit: 'cover' }}
+                      />
+                    )}
+                    <Chip
+                      label="Remove"
+                      size="small"
+                      onClick={() => removeFile(index)}
+                      sx={{ position: 'absolute', top: 4, right: 4 }}
+                    />
+                  </Card>
+                ))}
+              </Box>
+            ) }
+          </CardContent>
+          <CardActions className="chat-actions">
+            <input type="file"
+                   accept="image/*,video/*"
+                   multiple
+                   hidden
+                   ref={fileRef}
+                   onChange={ e => handleFileChange(e) }
+                   className="mb-2"/>
+            { selectedConversation ? (
+              <TextField value={ content }
+                         className='chat-input'
+                         onChange={ e => setContent( e.target.value ) }
+                         placeholder="Type a message"
+                         fullWidth
+                         slotProps={ {
+                           input:{
+                             startAdornment:
+                               <InputAdornment
+                                 position="start">
+                                 <Button
+                                   variant='contained'
+                                   onClick={ () => {
+                                     console.log(fileRef);
+                                     fileRef?.current?.click();
+                                   }}
+                                   size="small"
+                                   color="primary"><AddIcon/></Button>
+                             </InputAdornment>,
+                             endAdornment:
+                               <InputAdornment
+                                 variant="filled"
+                                 position="end">
+                               <Button
+                                 variant="contained"
+                                 color="primary"
+                                 onClick={sendMessage}
+                                 size="small">Send</Button>
+                             </InputAdornment>
+                           }
+                         } }/>
+
+              /*<div className="p-4 border-t">
+                <input type="file"
+                       accept="image/!*,video/!*"
+                       multiple
+                       onChange={ e => setAttachedFiles( Array.from( e.target.files ) ) }
+                       className="mb-2"/>
+                <div className="flex">
+                  <input value={ content }
+                         onChange={ e => setContent( e.target.value ) }
+                         placeholder="Type a message"
+                         className="border p-2 flex-grow rounded-l"/>
+                  <button onClick={ sendMessage }
+                          className="bg-blue-500 text-white p-2 rounded-r">Send
+                  </button>
+                </div>
+              </div>*/
+            ) : null }
+          </CardActions>
+        </Card>
+      </Grid>
+    </Grid>
+  );
+};
+
+export default Chat;
\ No newline at end of file