'use client'; import Image from 'next/image'; import { useState, useRef, useEffect, useCallback } from 'react'; import data from '@emoji-mart/data'; import Picker from '@emoji-mart/react'; import { GiphyFetch } from '@giphy/js-fetch-api'; import { Grid } from '@giphy/react-components'; const gf = new GiphyFetch(process.env.NEXT_PUBLIC_GIPHY_API_KEY || ''); interface AttachedFile { file: File; preview?: string; // Object URL for image preview messageType: 'IMAGE' | 'FILE'; } interface MessageInputProps { onSend?: (message: string) => void; onSendGif?: (gifUrl: string) => void; onSendFile?: (file: File, messageType: 'IMAGE' | 'FILE') => void; onTypingStart?: () => void; onTypingStop?: () => void; isUploading?: boolean; } export function MessageInput({ onSend, onSendGif, onSendFile, onTypingStart, onTypingStop, isUploading }: MessageInputProps) { const [message, setMessage] = useState(''); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const [showGifPicker, setShowGifPicker] = useState(false); const [gifSearchQuery, setGifSearchQuery] = useState(''); const [attachedFile, setAttachedFile] = useState(null); const typingTimeoutRef = useRef(null); const isTypingRef = useRef(false); const textareaRef = useRef(null); const emojiPickerRef = useRef(null); const gifPickerRef = useRef(null); const emojiButtonRef = useRef(null); const gifButtonRef = useRef(null); const imageInputRef = useRef(null); const fileInputRef = useRef(null); // Handle typing indicator const handleTyping = useCallback(() => { if (!isTypingRef.current && onTypingStart) { isTypingRef.current = true; onTypingStart(); } if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } 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]); // Close pickers on click outside useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if ( showEmojiPicker && emojiPickerRef.current && !emojiPickerRef.current.contains(e.target as Node) && emojiButtonRef.current && !emojiButtonRef.current.contains(e.target as Node) ) { setShowEmojiPicker(false); } if ( showGifPicker && gifPickerRef.current && !gifPickerRef.current.contains(e.target as Node) && gifButtonRef.current && !gifButtonRef.current.contains(e.target as Node) ) { setShowGifPicker(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [showEmojiPicker, showGifPicker]); // Clean up object URL on unmount or when file changes useEffect(() => { return () => { if (attachedFile?.preview) { URL.revokeObjectURL(attachedFile.preview); } }; }, [attachedFile]); const handleImageSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const preview = URL.createObjectURL(file); setAttachedFile({ file, preview, messageType: 'IMAGE' }); // Reset input so same file can be selected again e.target.value = ''; }; const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setAttachedFile({ file, messageType: 'FILE' }); e.target.value = ''; }; const removeAttachment = () => { if (attachedFile?.preview) { URL.revokeObjectURL(attachedFile.preview); } setAttachedFile(null); }; const formatFileSize = (bytes: number): string => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; const handleSend = () => { // Send attached file if (attachedFile && onSendFile) { if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } if (isTypingRef.current && onTypingStop) { isTypingRef.current = false; onTypingStop(); } onSendFile(attachedFile.file, attachedFile.messageType); removeAttachment(); setMessage(''); return; } // Send text message if (message.trim() && onSend) { if (typingTimeoutRef.current) { clearTimeout(typingTimeoutRef.current); } if (isTypingRef.current && onTypingStop) { isTypingRef.current = false; onTypingStop(); } onSend(message.trim()); setMessage(''); } }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (!isUploading) handleSend(); } }; const handleChange = (e: React.ChangeEvent) => { setMessage(e.target.value); handleTyping(); }; const handleEmojiSelect = (emoji: { native: string }) => { const textarea = textareaRef.current; if (textarea) { const start = textarea.selectionStart; const end = textarea.selectionEnd; const newMessage = message.slice(0, start) + emoji.native + message.slice(end); setMessage(newMessage); // Restore cursor position after emoji setTimeout(() => { textarea.selectionStart = textarea.selectionEnd = start + emoji.native.length; textarea.focus(); }, 0); } else { setMessage((prev) => prev + emoji.native); } }; const handleGifSelect = (gif: { images: { original: { url: string } } }, e: React.SyntheticEvent) => { e.preventDefault(); const gifUrl = gif.images.original.url; if (onSendGif) { onSendGif(gifUrl); } setShowGifPicker(false); setGifSearchQuery(''); }; const toggleEmojiPicker = () => { setShowEmojiPicker((prev) => !prev); setShowGifPicker(false); }; const toggleGifPicker = () => { setShowGifPicker((prev) => !prev); setShowEmojiPicker(false); }; const fetchGifs = useCallback( (offset: number) => { if (gifSearchQuery.trim()) { return gf.search(gifSearchQuery, { offset, limit: 10 }); } return gf.trending({ offset, limit: 10 }); }, [gifSearchQuery] ); const canSend = message.trim() || attachedFile; return (
{/* Hidden file inputs */} {/* Attachment preview area */} {attachedFile && (
{attachedFile.messageType === 'IMAGE' && attachedFile.preview ? ( Preview ) : (
File
)}

{attachedFile.file.name}

{formatFileSize(attachedFile.file.size)}

)} {/* Text input area */}