diff --git a/package-lock.json b/package-lock.json index c4bd738..59123a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "next-auth": "^5.0.0-beta.30", "react": "19.2.3", "react-dom": "19.2.3", - "react-select": "^5.10.2" + "react-select": "^5.10.2", + "socket.io-client": "^4.8.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -1414,6 +1415,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -3076,6 +3083,28 @@ "dev": true, "license": "MIT" }, + "node_modules/engine.io-client": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.4", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", @@ -6304,6 +6333,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -7002,6 +7059,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index e849246..346d077 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "NODE_OPTIONS='--max-old-space-size=4096' next dev", "build": "next build", "start": "next start", "lint": "eslint" @@ -14,7 +14,8 @@ "next-auth": "^5.0.0-beta.30", "react": "19.2.3", "react-dom": "19.2.3", - "react-select": "^5.10.2" + "react-select": "^5.10.2", + "socket.io-client": "^4.8.3" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/src/components/layout/CommonHeader.tsx b/src/components/layout/CommonHeader.tsx index 78ad5e6..3362f79 100644 --- a/src/components/layout/CommonHeader.tsx +++ b/src/components/layout/CommonHeader.tsx @@ -165,7 +165,8 @@ export function CommonHeader() { alt="Profile" width={35} height={35} - className="w-full h-full object-cover" + className="object-cover" + style={{ width: '100%', height: '100%' }} /> )} @@ -201,7 +202,8 @@ export function CommonHeader() { alt="Profile" width={42} height={42} - className="w-full h-full object-cover" + className="object-cover" + style={{ width: '100%', height: '100%' }} /> )} diff --git a/src/components/message/ChatHeader.tsx b/src/components/message/ChatHeader.tsx index a2333be..c41739d 100644 --- a/src/components/message/ChatHeader.tsx +++ b/src/components/message/ChatHeader.tsx @@ -10,6 +10,8 @@ interface ChatHeaderProps { isOnline?: boolean; pronouns?: string; expertise?: string[]; + isTyping?: boolean; + typingText?: string; } export function ChatHeader({ @@ -20,6 +22,8 @@ export function ChatHeader({ isOnline = false, pronouns, expertise = [], + isTyping = false, + typingText, }: ChatHeaderProps) { return (
@@ -34,7 +38,11 @@ export function ChatHeader({ {role} - {lastSeen} + {isTyping ? ( + {typingText} + ) : ( + lastSeen + )}
@@ -67,7 +75,8 @@ export function ChatHeader({ alt={name} width={50} height={50} - className="w-full h-full object-cover rounded-full" + className="object-cover rounded-full" + style={{ width: '100%', height: '100%' }} />
{isOnline && ( diff --git a/src/components/message/MessageInput.tsx b/src/components/message/MessageInput.tsx index 9cff087..1a16032 100644 --- a/src/components/message/MessageInput.tsx +++ b/src/components/message/MessageInput.tsx @@ -1,17 +1,64 @@ 'use client'; import Image from 'next/image'; -import { useState } from 'react'; +import { useState, useRef, useEffect, useCallback } from 'react'; interface MessageInputProps { onSend?: (message: string) => void; + onTypingStart?: () => void; + onTypingStop?: () => void; } -export function MessageInput({ onSend }: MessageInputProps) { +export function MessageInput({ onSend, onTypingStart, onTypingStop }: MessageInputProps) { const [message, setMessage] = useState(''); + const typingTimeoutRef = useRef(null); + const isTypingRef = useRef(false); + + // Handle typing indicator + const handleTyping = useCallback(() => { + // Start typing indicator + if (!isTypingRef.current && onTypingStart) { + isTypingRef.current = true; + onTypingStart(); + } + + // Clear existing timeout + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + } + + // Set timeout to stop typing indicator after 2 seconds of inactivity + typingTimeoutRef.current = setTimeout(() => { + if (isTypingRef.current && onTypingStop) { + isTypingRef.current = false; + onTypingStop(); + } + }, 2000); + }, [onTypingStart, onTypingStop]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + } + if (isTypingRef.current && onTypingStop) { + onTypingStop(); + } + }; + }, [onTypingStop]); const handleSend = () => { if (message.trim() && onSend) { + // Stop typing indicator before sending + if (typingTimeoutRef.current) { + clearTimeout(typingTimeoutRef.current); + } + if (isTypingRef.current && onTypingStop) { + isTypingRef.current = false; + onTypingStop(); + } + onSend(message.trim()); setMessage(''); } @@ -24,13 +71,18 @@ export function MessageInput({ onSend }: MessageInputProps) { } }; + const handleChange = (e: React.ChangeEvent) => { + setMessage(e.target.value); + handleTyping(); + }; + return (
{/* Text input area */}