looood / src /components /chat /message-input.tsx
looda3131's picture
Clean push without any binary history
cc276cc
'use client';
import { useState, useRef, useEffect } from 'react';
import { Button } from '../ui/button';
import { Textarea } from '../ui/textarea';
import { Paperclip, Send, Smile, Mic, Sparkles, X, CornerDownLeft } from 'lucide-react';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { CSSTransition } from 'react-transition-group';
import { VoiceRecorder } from '../voice-recorder';
import type { ReplyTo, Message, Group } from '@/lib/types';
import { useAppContext } from '@/contexts/app-context';
import { cn } from '@/lib/utils';
import { Skeleton } from '../ui/skeleton';
import Picker from '@emoji-mart/react';
import data from '@emoji-mart/data';
interface MessageInputProps {
chatId: string | null;
disabled?: boolean;
lastMessage: Message | null;
isGroupChat?: boolean;
currentGroup?: Group;
onGetSuggestedReplies: (message: Message) => Promise<string[]>;
sendTextMessage: (text: string, replyTo?: ReplyTo | null) => void;
onSelectMedia: (file: File) => void;
onSendAudio: (data: { file: File, duration: number }) => void;
}
const QuotedMessagePreview = ({ replyTo, onCancel }: { replyTo: ReplyTo | null, onCancel: () => void }) => (
<CSSTransition in={!!replyTo} timeout={200} classNames="reply-bar" unmountOnExit>
<div className="bg-muted/70 px-4 pt-2 pb-1 border-b">
<div className="bg-background/50 rounded-lg p-2 flex justify-between items-center border-l-4 border-primary">
{replyTo && (
<>
<div>
<p className="font-bold text-primary text-sm">{replyTo.displayName}</p>
<p className="text-sm text-muted-foreground truncate quoted-message-content">
{replyTo.text || (replyTo.imageKey ? 'Image' : replyTo.videoKey ? 'Video' : 'Voice Message')}
</p>
</div>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={onCancel}>
<X className="h-4 w-4" />
</Button>
</>
)}
</div>
</div>
</CSSTransition>
);
export function MessageInput({
sendTextMessage,
onSelectMedia,
onSendAudio,
chatId,
disabled,
lastMessage,
onGetSuggestedReplies,
isGroupChat,
currentGroup,
}: MessageInputProps) {
const { currentUser, setUserTyping, replyTo, setReplyTo, playSound, t } = useAppContext();
const [message, setMessage] = useState('');
const [isRecording, setIsRecording] = useState(false);
const [suggestedReplies, setSuggestedReplies] = useState<string[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleSend = () => {
if (message.trim() || isRecording) {
playSound('send');
if (message.trim()) {
sendTextMessage(message, replyTo);
}
setMessage('');
setReplyTo(null);
if (textareaRef.current) {
textareaRef.current.style.height = '40px';
}
}
};
useEffect(() => {
const fetchSuggestions = async () => {
if (lastMessage && lastMessage.sender !== currentUser?.uid && lastMessage.text) {
setShowSuggestions(true);
setSuggestedReplies([]); // Clear old suggestions immediately
const replies = await onGetSuggestedReplies(lastMessage);
setSuggestedReplies(replies);
} else {
setShowSuggestions(false);
setSuggestedReplies([]);
}
};
// Only fetch suggestions if the last message changes
if (lastMessage?.id) {
fetchSuggestions();
}
}, [lastMessage?.id, lastMessage?.sender, lastMessage?.text, currentUser?.uid, onGetSuggestedReplies]);
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setMessage(e.target.value);
if (textareaRef.current) {
textareaRef.current.style.height = 'auto';
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`;
}
if (chatId) {
if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current);
else setUserTyping(chatId, true);
typingTimeoutRef.current = setTimeout(() => {
setUserTyping(chatId, false);
typingTimeoutRef.current = null;
}, 2000);
}
};
const handleSendSuggestion = (reply: string) => {
sendTextMessage(reply, null);
setShowSuggestions(false);
}
const handleKeyPress = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSend();
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
onSelectMedia(file);
}
// Reset the input value to allow selecting the same file again
e.target.value = '';
};
const handleEmojiSelect = (emoji: any) => {
setMessage(prev => prev + emoji.native);
};
if (disabled) {
return (
<div className="p-4 border-t text-center text-sm text-muted-foreground">
{t('messagingDisabled')}
</div>
);
}
const sendingMode = currentGroup?.info?.settings?.sendingMode || 'everyone';
const isMuted = isGroupChat && currentGroup?.info.mutedMembers?.[currentUser?.uid || ''];
const canSend = !isGroupChat || (
(sendingMode === 'everyone' && !isMuted) ||
(sendingMode === 'admins' && currentGroup?.admins[currentUser?.uid || '']) ||
(sendingMode === 'owner' && currentGroup?.info.createdBy === currentUser?.uid)
);
if (!canSend) {
return (
<div className="p-4 border-t text-center text-sm text-muted-foreground">
<p>{isMuted ? t('youAreMuted') : t('adminsCanSend')}</p>
</div>
);
}
if (isRecording) {
return <VoiceRecorder onCancel={() => setIsRecording(false)} onSend={onSendAudio} />;
}
return (
<div className="flex flex-col border-t bg-background/80 backdrop-blur-sm">
<QuotedMessagePreview replyTo={replyTo} onCancel={() => setReplyTo(null)} />
{showSuggestions && (
<div className="p-2 border-b">
<div className="flex gap-2 overflow-x-auto pb-2">
{suggestedReplies.length > 0 ? (
suggestedReplies.map((reply, i) => (
<Button key={i} variant="outline" size="sm" className="flex-shrink-0" onClick={() => handleSendSuggestion(reply)}>
{reply}
</Button>
))
) : (
Array.from({ length: 3 }).map((_, i) => <Skeleton key={i} className="h-9 w-24 rounded-md" />)
)}
<Button variant="ghost" size="icon" className="h-9 w-9 flex-shrink-0" onClick={() => setShowSuggestions(false)}><X className="h-4 w-4"/></Button>
</div>
</div>
)}
<div className="flex items-center gap-2 p-2 md:p-4">
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" title="Attach file">
<Paperclip />
<span className="sr-only">Attach file</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2">
<input type="file" id="file-upload" className="hidden" onChange={handleFileSelect} accept="image/*,video/*" />
<Button asChild variant="outline" size="sm">
<label htmlFor="file-upload" className="cursor-pointer">
<Paperclip className="mr-2 h-4 w-4"/> Media
</label>
</Button>
</PopoverContent>
</Popover>
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" title="Add emoji">
<Smile />
<span className="sr-only">Add emoji</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0 border-0" side="top" align="end">
<Picker data={data} onEmojiSelect={handleEmojiSelect} />
</PopoverContent>
</Popover>
<Textarea
ref={textareaRef}
placeholder={t('typeMessage')}
value={message}
onChange={handleInputChange}
onKeyDown={handleKeyPress}
className="message-input-textarea"
rows={1}
/>
{message.trim() ? (
<Button onClick={handleSend} size="icon" className="rounded-full h-10 w-10 flex-shrink-0" title="Send message">
<Send />
<span className="sr-only">Send</span>
</Button>
) : (
<Button onClick={() => { playSound('touch'); setIsRecording(true); }} size="icon" className="rounded-full h-10 w-10 flex-shrink-0" title="Record voice message">
<Mic />
<span className="sr-only">Record</span>
</Button>
)}
</div>
</div>
);
}