| | '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([]); |
| | const replies = await onGetSuggestedReplies(lastMessage); |
| | setSuggestedReplies(replies); |
| | } else { |
| | setShowSuggestions(false); |
| | setSuggestedReplies([]); |
| | } |
| | }; |
| |
|
| | |
| | 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); |
| | } |
| | |
| | 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> |
| | ); |
| | } |
| |
|