From 81aeb5a92695450daeef0a7fa82ec978fce1460f Mon Sep 17 00:00:00 2001 From: slawk0 Date: Thu, 2 Jan 2025 00:25:31 +0100 Subject: [PATCH] code refactor, added time indicator from last message --- client/package-lock.json | 11 ++ client/package.json | 1 + .../chat/{ => chatArea}/AnimatedMessage.tsx | 7 +- .../chat/{ => chatArea}/AttachmentPreview.tsx | 4 +- .../chat/{ => chatArea}/FileBox.tsx | 0 .../chat/{ => chatArea}/MessageForm.tsx | 8 +- .../chat/{ => chatArea}/MessagesArea.tsx | 12 +- .../chat/{ => chatHeader}/AddGroupMember.tsx | 19 +-- .../chat/{ => chatHeader}/ContactProfile.tsx | 4 +- .../{ => chatHeader}/CreateGroupButton.tsx | 4 +- .../chat/{ => leftSidebar}/ContactForm.tsx | 50 +++--- .../chat/{ => leftSidebar}/ContactsList.tsx | 22 ++- .../chat/leftSidebar/LastActiveTime.tsx | 34 ++++ .../chat/{ => leftSidebar}/UserProfile.tsx | 6 +- .../{ => rightSidebar}/ParticipantsBar.tsx | 19 ++- client/src/pages/Chat.tsx | 147 +++++++++--------- client/tailwind.config.js | 1 + 17 files changed, 207 insertions(+), 142 deletions(-) rename client/src/components/chat/{ => chatArea}/AnimatedMessage.tsx (86%) rename client/src/components/chat/{ => chatArea}/AttachmentPreview.tsx (96%) rename client/src/components/chat/{ => chatArea}/FileBox.tsx (100%) rename client/src/components/chat/{ => chatArea}/MessageForm.tsx (97%) rename client/src/components/chat/{ => chatArea}/MessagesArea.tsx (95%) rename client/src/components/chat/{ => chatHeader}/AddGroupMember.tsx (91%) rename client/src/components/chat/{ => chatHeader}/ContactProfile.tsx (89%) rename client/src/components/chat/{ => chatHeader}/CreateGroupButton.tsx (97%) rename client/src/components/chat/{ => leftSidebar}/ContactForm.tsx (85%) rename client/src/components/chat/{ => leftSidebar}/ContactsList.tsx (90%) create mode 100644 client/src/components/chat/leftSidebar/LastActiveTime.tsx rename client/src/components/chat/{ => leftSidebar}/UserProfile.tsx (88%) rename client/src/components/chat/{ => rightSidebar}/ParticipantsBar.tsx (95%) diff --git a/client/package-lock.json b/client/package-lock.json index 5da663b..cc061fe 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -20,6 +20,7 @@ "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "js-cookie": "^3.0.5", "lucide-react": "^0.460.0", "nanoid": "^5.0.8", @@ -3281,6 +3282,16 @@ "url": "https://opencollective.com/daisyui" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", diff --git a/client/package.json b/client/package.json index 6a72697..6fb178c 100644 --- a/client/package.json +++ b/client/package.json @@ -22,6 +22,7 @@ "axios": "^1.7.7", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "js-cookie": "^3.0.5", "lucide-react": "^0.460.0", "nanoid": "^5.0.8", diff --git a/client/src/components/chat/AnimatedMessage.tsx b/client/src/components/chat/chatArea/AnimatedMessage.tsx similarity index 86% rename from client/src/components/chat/AnimatedMessage.tsx rename to client/src/components/chat/chatArea/AnimatedMessage.tsx index 074afb2..340712d 100644 --- a/client/src/components/chat/AnimatedMessage.tsx +++ b/client/src/components/chat/chatArea/AnimatedMessage.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { Trash2 } from 'lucide-react'; -import AttachmentPreview from './AttachmentPreview'; +import AttachmentPreview from './AttachmentPreview.tsx'; import { ChatMessagesProps } from '@/pages/Chat.tsx'; type AnimatedMessageProps = { @@ -27,9 +27,8 @@ const AnimatedMessage = ({ return (
  • diff --git a/client/src/components/chat/AttachmentPreview.tsx b/client/src/components/chat/chatArea/AttachmentPreview.tsx similarity index 96% rename from client/src/components/chat/AttachmentPreview.tsx rename to client/src/components/chat/chatArea/AttachmentPreview.tsx index 66a94f8..09e7cc2 100644 --- a/client/src/components/chat/AttachmentPreview.tsx +++ b/client/src/components/chat/chatArea/AttachmentPreview.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -import LoadingWheel from './LoadingWheel'; -import FileBox from './FileBox'; +import LoadingWheel from '../LoadingWheel.tsx'; +import FileBox from './FileBox.tsx'; // Cache to keep track of loaded media const loadedMedia = new Set(); diff --git a/client/src/components/chat/FileBox.tsx b/client/src/components/chat/chatArea/FileBox.tsx similarity index 100% rename from client/src/components/chat/FileBox.tsx rename to client/src/components/chat/chatArea/FileBox.tsx diff --git a/client/src/components/chat/MessageForm.tsx b/client/src/components/chat/chatArea/MessageForm.tsx similarity index 97% rename from client/src/components/chat/MessageForm.tsx rename to client/src/components/chat/chatArea/MessageForm.tsx index 9f39c8d..a47dfa8 100644 --- a/client/src/components/chat/MessageForm.tsx +++ b/client/src/components/chat/chatArea/MessageForm.tsx @@ -1,9 +1,9 @@ import { useRef, useCallback, useEffect, useState } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import type { KeyboardEventHandler } from 'react'; -import { socket } from '../../socket/socket.tsx'; -import { ChatMessagesProps, ContactsProps } from '../../pages/Chat.tsx'; -import { axiosClient } from '../../App.tsx'; +import { socket } from '../../../socket/socket.tsx'; +import { ChatMessagesProps, ContactsProps } from '../../../pages/Chat.tsx'; +import { axiosClient } from '../../../App.tsx'; import { File, Paperclip, Send, X } from 'lucide-react'; import LoadingWheel from '@/components/chat/LoadingWheel.tsx'; type Input = { @@ -259,7 +259,7 @@ const MessageForm = ({ contact }: MessageFormProps) => { ref(e); textareaRef.current = e; }} - 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:outline-none focus:ring-1 focus:ring-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`} autoFocus={!!contact} disabled={!contact} diff --git a/client/src/components/chat/MessagesArea.tsx b/client/src/components/chat/chatArea/MessagesArea.tsx similarity index 95% rename from client/src/components/chat/MessagesArea.tsx rename to client/src/components/chat/chatArea/MessagesArea.tsx index c0a5ea5..3a1d834 100644 --- a/client/src/components/chat/MessagesArea.tsx +++ b/client/src/components/chat/chatArea/MessagesArea.tsx @@ -1,12 +1,12 @@ import { useEffect, useRef, useState } from 'react'; -import { socket } from '../../socket/socket.tsx'; +import { socket } from '@/socket/socket.tsx'; import { useOutletContext } from 'react-router-dom'; -import { sendContact } from '../../api/contactsApi.tsx'; -import LoadingWheel from './LoadingWheel.tsx'; -import { ChatMessagesProps, MeProps } from '../../pages/Chat.tsx'; -import { ContactsProps } from '../../pages/Chat.tsx'; +import { sendContact } from '@/api/contactsApi.tsx'; +import LoadingWheel from '../LoadingWheel.tsx'; +import { ChatMessagesProps, MeProps } from '@/pages/Chat.tsx'; +import { ContactsProps } from '@/pages/Chat.tsx'; import { UsernameType } from '@/utils/ProtectedRoutes.tsx'; -import AnimatedMessage from '@/components/chat/AnimatedMessage.tsx'; +import AnimatedMessage from '@/components/chat/chatArea/AnimatedMessage.tsx'; type MessagesAreaProps = { messages: ChatMessagesProps[]; diff --git a/client/src/components/chat/AddGroupMember.tsx b/client/src/components/chat/chatHeader/AddGroupMember.tsx similarity index 91% rename from client/src/components/chat/AddGroupMember.tsx rename to client/src/components/chat/chatHeader/AddGroupMember.tsx index 6954a4a..d755d0a 100644 --- a/client/src/components/chat/AddGroupMember.tsx +++ b/client/src/components/chat/chatHeader/AddGroupMember.tsx @@ -1,10 +1,11 @@ -import LoadingWheel from './LoadingWheel.tsx'; +import LoadingWheel from '../LoadingWheel.tsx'; import { useEffect, useRef, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { axiosClient } from '../../App.tsx'; -import { ContactsProps } from '../../pages/Chat.tsx'; -import { socket } from '../../socket/socket.tsx'; +import { axiosClient } from '../../../App.tsx'; +import { ContactsProps } from '../../../pages/Chat.tsx'; +import { socket } from '../../../socket/socket.tsx'; import { UserRoundPlus } from 'lucide-react'; +import { Button } from '@/components/ui/button.tsx'; type Inputs = { username: string; @@ -130,10 +131,10 @@ function AddGroupMember({ contact }: AddGroupMemberProps) { -
    +
    - +
    @@ -191,13 +192,13 @@ function AddGroupMember({ contact }: AddGroupMemberProps) {
    - +
    diff --git a/client/src/components/chat/ContactProfile.tsx b/client/src/components/chat/chatHeader/ContactProfile.tsx similarity index 89% rename from client/src/components/chat/ContactProfile.tsx rename to client/src/components/chat/chatHeader/ContactProfile.tsx index fabd155..093159e 100644 --- a/client/src/components/chat/ContactProfile.tsx +++ b/client/src/components/chat/chatHeader/ContactProfile.tsx @@ -1,7 +1,7 @@ -import profile from '../../../assets/profile.svg'; +import profile from '../../../../assets/profile.svg'; import CreateGroupButton from './CreateGroupButton.tsx'; import AddGroupMember from './AddGroupMember.tsx'; -import { ContactsProps } from '../../pages/Chat.tsx'; +import { ContactsProps } from '@/pages/Chat.tsx'; import { UsersRound } from 'lucide-react'; type ContactProfileProps = { diff --git a/client/src/components/chat/CreateGroupButton.tsx b/client/src/components/chat/chatHeader/CreateGroupButton.tsx similarity index 97% rename from client/src/components/chat/CreateGroupButton.tsx rename to client/src/components/chat/chatHeader/CreateGroupButton.tsx index a497aa9..12b64c9 100644 --- a/client/src/components/chat/CreateGroupButton.tsx +++ b/client/src/components/chat/chatHeader/CreateGroupButton.tsx @@ -1,7 +1,7 @@ -import LoadingWheel from './LoadingWheel.tsx'; +import LoadingWheel from '../LoadingWheel.tsx'; import { useRef, useState } from 'react'; import { SubmitHandler, useForm } from 'react-hook-form'; -import { axiosClient } from '../../App.tsx'; +import { axiosClient } from '@/App.tsx'; import { Plus } from 'lucide-react'; type Inputs = { diff --git a/client/src/components/chat/ContactForm.tsx b/client/src/components/chat/leftSidebar/ContactForm.tsx similarity index 85% rename from client/src/components/chat/ContactForm.tsx rename to client/src/components/chat/leftSidebar/ContactForm.tsx index b6f6815..e6d2ddf 100644 --- a/client/src/components/chat/ContactForm.tsx +++ b/client/src/components/chat/leftSidebar/ContactForm.tsx @@ -1,9 +1,9 @@ import { useForm, SubmitHandler } from 'react-hook-form'; -import { ContactsProps } from '../../pages/Chat.tsx'; -import { axiosClient } from '../../App.tsx'; +import { ContactsProps } from '../../../pages/Chat.tsx'; +import { axiosClient } from '../../../App.tsx'; import { AxiosResponse } from 'axios'; import { useEffect, useState } from 'react'; -import LoadingWheel from './LoadingWheel.tsx'; +import LoadingWheel from '../LoadingWheel.tsx'; import { Search } from 'lucide-react'; type Input = { @@ -93,26 +93,28 @@ function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) { }; const handleKeyDown = (e: React.KeyboardEvent) => { - if (suggestions.length > 0) { - switch (e.key) { - case 'ArrowDown': + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setSelectedIndex((prev) => (prev + 1) % suggestions.length); + break; + case 'ArrowUp': + e.preventDefault(); + setSelectedIndex( + (prev) => (prev - 1 + suggestions.length) % suggestions.length, + ); + break; + case 'Enter': + if (suggestions.length > 0) { e.preventDefault(); - setSelectedIndex((prev) => (prev + 1) % suggestions.length); - break; - case 'ArrowUp': - e.preventDefault(); - setSelectedIndex( - (prev) => (prev - 1 + suggestions.length) % suggestions.length, - ); - break; - case 'Enter': - if (suggestions.length > 0) { - e.preventDefault(); - reset({ contact: suggestions[selectedIndex] }); - handleSubmit(submitContact)(); - } - break; - } + reset({ contact: suggestions[selectedIndex] }); + handleSubmit(submitContact)(); + } + break; + case 'Escape': + e.preventDefault(); + reset({ contact: '' }); + break; } }; @@ -128,8 +130,8 @@ function ContactForm({ InitializeContact, setContactsList }: ContactFormProps) {
    void; @@ -109,7 +111,7 @@ function ContactsList({ updateContactStatus(contact, true); }} > -
    +
    {contact.username} @@ -121,9 +123,13 @@ function ContactsList({ /> )}
    - - {contact.last_message} - +
    + + {contact.last_message} + + + +
    diff --git a/client/src/components/chat/leftSidebar/LastActiveTime.tsx b/client/src/components/chat/leftSidebar/LastActiveTime.tsx new file mode 100644 index 0000000..1ee0562 --- /dev/null +++ b/client/src/components/chat/leftSidebar/LastActiveTime.tsx @@ -0,0 +1,34 @@ +import { useState, useEffect } from 'react'; +import { formatDistanceToNow, differenceInSeconds } from 'date-fns'; +import { ContactsProps } from '@/pages/Chat.tsx'; + +type LastActiveTimeProps = { + contact: ContactsProps; +}; + +const LastActiveTime = ({ contact }: LastActiveTimeProps) => { + const [timeAgo, setTimeAgo] = useState(''); + + useEffect(() => { + const updateTime = () => { + const lastActiveDate = new Date(contact.last_message_time); + const secondsDiff = differenceInSeconds(new Date(), lastActiveDate); + + if (secondsDiff < 60) { + setTimeAgo('now'); + return; + } + + setTimeAgo(formatDistanceToNow(lastActiveDate)); + }; + updateTime(); + + const intervalId = setInterval(updateTime, 60000); + + return () => clearInterval(intervalId); + }, [contact?.last_message]); + + return {timeAgo}; +}; + +export default LastActiveTime; diff --git a/client/src/components/chat/UserProfile.tsx b/client/src/components/chat/leftSidebar/UserProfile.tsx similarity index 88% rename from client/src/components/chat/UserProfile.tsx rename to client/src/components/chat/leftSidebar/UserProfile.tsx index eb14b32..563f473 100644 --- a/client/src/components/chat/UserProfile.tsx +++ b/client/src/components/chat/leftSidebar/UserProfile.tsx @@ -1,8 +1,8 @@ -import zdjecie from '../../../assets/turtleProfileImg3.webp'; -import logoutIcon from '../../../assets/logout.svg'; +import zdjecie from '../../../../assets/turtleProfileImg3.webp'; +import logoutIcon from '../../../../assets/logout.svg'; import Cookies from 'js-cookie'; import { useOutletContext } from 'react-router-dom'; -import { UsernameType } from '../../utils/ProtectedRoutes.tsx'; +import { UsernameType } from '../../../utils/ProtectedRoutes.tsx'; function UserProfile() { const user: UsernameType = useOutletContext(); diff --git a/client/src/components/chat/ParticipantsBar.tsx b/client/src/components/chat/rightSidebar/ParticipantsBar.tsx similarity index 95% rename from client/src/components/chat/ParticipantsBar.tsx rename to client/src/components/chat/rightSidebar/ParticipantsBar.tsx index 4d2a9c7..1852a42 100644 --- a/client/src/components/chat/ParticipantsBar.tsx +++ b/client/src/components/chat/rightSidebar/ParticipantsBar.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useContext, useEffect, useMemo, useState } from 'react'; import { axiosClient } from '@/App.tsx'; import { ContactsProps, MeProps } from '@/pages/Chat.tsx'; import { socket } from '@/socket/socket.tsx'; @@ -8,10 +8,10 @@ import { ContextMenuContent, ContextMenuItem, ContextMenuTrigger, -} from '@/components/ui/context-menu'; +} from '@/components/ui/context-menu.tsx'; import { UsernameType } from '@/utils/ProtectedRoutes.tsx'; import { useOutletContext } from 'react-router-dom'; -import zdjecie from '../../../assets/turtleProfileImg3.webp'; +import zdjecie from '../../../../assets/turtleProfileImg3.webp'; type ParticipantsProps = { user_id: string; username: string; @@ -23,18 +23,20 @@ type ParticipantsBarProps = { initializeContact: (contact: ContactsProps) => void; currentContact: ContactsProps | null; setMe: React.Dispatch>; - me: MeProps; + setGroupOwner: React.Dispatch>; + groupOwner: string | undefined; }; function ParticipantsBar({ initializeContact, currentContact, setMe, - me, + setGroupOwner, + groupOwner, }: ParticipantsBarProps) { const [participants, setParticipants] = useState([]); const user: UsernameType = useOutletContext(); - + const me = useContext(MeContext); const getParticipants = async () => { try { const response = await axiosClient.get( @@ -116,7 +118,10 @@ function ParticipantsBar({ (participant) => participant.user_id === user.user_id && participant.isowner, ); - + const whoIsOwner = participants.find( + (participant) => participant.isowner, + ); + setGroupOwner(whoIsOwner?.user_id); setMe({ isGroupAdmin: userIsAdmin, isGroupOwner: userIsOwner }); } }, [participants, user?.user_id]); diff --git a/client/src/pages/Chat.tsx b/client/src/pages/Chat.tsx index ff3f1a5..f3808e5 100644 --- a/client/src/pages/Chat.tsx +++ b/client/src/pages/Chat.tsx @@ -1,15 +1,15 @@ -import MessageForm from '../components/chat/MessageForm.tsx'; -import ContactProfile from '../components/chat/ContactProfile.tsx'; -import UserProfile from '../components/chat/UserProfile.tsx'; -import ContactForm from '../components/chat/ContactForm.tsx'; -import MessagesArea from '../components/chat/MessagesArea.tsx'; -import { useEffect, useState } from 'react'; -import ContactsList from '../components/chat/ContactsList.tsx'; +import MessageForm from '../components/chat/chatArea/MessageForm.tsx'; +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 ContactsList from '../components/chat/leftSidebar/ContactsList.tsx'; import { initializeSocket, joinRoom } 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/ParticipantsBar.tsx'; +import ParticipantsBar from '@/components/chat/rightSidebar/ParticipantsBar.tsx'; export type MeProps = { isGroupAdmin: boolean; @@ -21,7 +21,6 @@ export type ChatMessagesProps = { message: string; recipient: string; // conversation_id message_id: number; - pending: boolean; attachment_urls: string[] | null; sender_id: string; conversation_id: string; @@ -42,6 +41,10 @@ export type ContactsProps = { }; function Chat() { + const meDefaultValue = { + isGroupAdmin: false, + isGroupOwner: false, + }; const [contactsList, setContactsList] = useState([]); const [currentContact, setCurrentContact] = useState( null, @@ -50,10 +53,10 @@ function Chat() { const [messages, setMessages] = useState([]); const [errorMessage, setErrorMessage] = useState(null); const [hasMoreMessages, setHasMoreMessages] = useState(true); - const [me, setMe] = useState({ - isGroupAdmin: false, - isGroupOwner: false, - }); + const [me, setMe] = useState(meDefaultValue); + const MeContext = createContext(meDefaultValue); + + const [groupOwner, setGroupOwner] = useState(); useEffect(() => { const token = Cookies.get('token'); @@ -206,69 +209,71 @@ function Chat() { } return ( -
    - {/* Left Sidebar */} -
    - - - -
    + +
    + {/* Left Sidebar */} +
    + + + +
    - {/*Chat area */} -
    -
    - {/* Messages Container and Participants Container */} -
    -
    - -
    -
    - -
    -
    - {currentContact && currentContact.username?.length >= 4 ? ( - - ) : null} + {/*Chat area */} +
    +
    + {/* Messages Container and Participants Container */} +
    +
    + +
    +
    + +
    +
    + {currentContact && currentContact.username?.length >= 4 ? ( + + ) : null} +
    + + {/* Right Sidebar - Participants */} + {currentContact?.type === 'group' && ( +
    + +
    + )}
    - - {/* Right Sidebar - Participants */} - {currentContact?.type === 'group' && ( -
    - -
    - )}
    -
    + ); } diff --git a/client/tailwind.config.js b/client/tailwind.config.js index 3689267..17a195c 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -1,5 +1,6 @@ /** @type {import('tailwindcss').Config} */ export default { + important: true, darkMode: ['class'], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], theme: {