"use client"; import { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } from 'react'; import { useAuth } from '@/contexts/auth-context'; import { useGroups } from '@/contexts/groups-context'; import { useSettings } from '@/contexts/settings-context'; import { useChat } from '@/hooks/use-chat'; import { Message, ChatRecipient, User, Group, CallType, ActionPayload } from '@/lib/types'; import { Button } from '@/components/ui/button'; import { ArrowLeft, Ban, Info, MoreVertical, Loader2, MessageCircleX, Phone, Video, FileText, Search, X, Smile } from 'lucide-react'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { MessageInput } from '@/components/message-input'; import { MessageItem } from '@/components/message-item'; import { Skeleton } from '@/components/ui/skeleton'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; import { MediaPreviewModal } from '@/components/media-preview-modal'; import { GroupInfoPanel } from '@/components/group-info-panel'; import { SendAudioModal } from '@/components/send-audio-modal'; import { useChatUtils } from '@/contexts/chat-utils-context'; import { generateLocalSummary, type LocalSummary } from '@/lib/local-summarizer'; import { useTheme } from 'next-themes'; import { Input } from '@/components/ui/input'; import { cn } from '@/lib/utils'; import { generateSuggestedReplies } from '@/lib/suggested-replies'; import EmojiPicker, { EmojiStyle, Theme as EmojiTheme } from 'emoji-picker-react'; interface ChatWindowProps { recipient: ChatRecipient | null; onClose: () => void; onViewProfile: (user: User) => void; onPreviewImage: (imageUrl: string, isViewOnce?: boolean, onViewed?: () => void) => void; onAddMembers: (group: Group) => void; onStartCall: (peer: User, type: CallType) => void; openChangeNameModal: () => void; openChangeStatusModal: () => void; openChangeBioModal: () => void; onOpenSettings: (section?: string) => void; } const BlockedBanner = ({ recipient, isBlockedByYou, t }: { recipient: ChatRecipient; isBlockedByYou: boolean, t: (key: any, options?: any) => string }) => (

{isBlockedByYou ? t('blockedBannerYou', { name: recipient.displayName }) : t('blockedBannerOther', { name: recipient.displayName })}

); export function ChatWindow({ recipient, onClose, onViewProfile, onPreviewImage, onAddMembers, onStartCall, openChangeBioModal, openChangeNameModal, openChangeStatusModal, onOpenSettings, }: ChatWindowProps) { const { currentUser } = useAuth(); const { groups } = useGroups(); const { clearChatHistory, deleteMessage } = useChatUtils(); const { playSound, t, formatDistanceToNow, addToast, toggleSound, isSoundEnabled, privacySettings, updatePrivacySettings, fontStyle, setFontStyle, dataMode, setDataMode, language } = useSettings(); const { messages, loading, sendTextMessage, sendMediaMessage, sendVoiceMessage, editMessage, toggleReaction, chatId, typingUsers } = useChat(recipient); const { theme, setTheme } = useTheme(); const messagesEndRef = useRef(null); const chatContainerRef = useRef(null); const editorRef = useRef(null); const lastRange = useRef(null); const [confirmAction, setConfirmAction] = useState<{ type: 'block' | 'unblock' | 'remove' | 'clear_history' } | null>(null); const [messageToDelete, setMessageToDelete] = useState(null); const [deletingMessageId, setDeletingMessageId] = useState(null); const [mediaFile, setMediaFile] = useState<{ file: File, originalSize: number, compressedBlob?: Blob } | null>(null); const [audioToSend, setAudioToSend] = useState<{ file: File; duration: number } | null>(null); const [isGroupInfoOpen, setGroupInfoOpen] = useState(false); const [clientReadyStatus, setClientReadyStatus] = useState(null); const [isSummarizing, setIsSummarizing] = useState(false); const [summary, setSummary] = useState(null); const [isSearching, setIsSearching] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [viewedOnceMessages, setViewedOnceMessages] = useState>(new Set()); const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); const currentGroup = useMemo(() => { if (!recipient?.isGroup) return undefined; return groups.find(g => g.id === recipient.uid); }, [recipient, groups]); const scrollToBottom = useCallback(() => { if (messagesEndRef.current) { messagesEndRef.current.scrollIntoView({ behavior: "auto" }); } }, []); useLayoutEffect(() => { scrollToBottom(); }, [messages, typingUsers, chatId, scrollToBottom]); useEffect(() => { // This event is dispatched from MessageItem when an image loads const handleImageLoad = () => scrollToBottom(); window.addEventListener('imageLoaded', handleImageLoad); return () => { window.removeEventListener('imageLoaded', handleImageLoad); }; }, [scrollToBottom]); // Search logic effect useEffect(() => { if (isSearching && searchQuery.trim()) { setTimeout(() => { const firstResult = chatContainerRef.current?.querySelector('mark.search-highlight'); if (firstResult) { firstResult.scrollIntoView({ block: 'center', behavior: 'smooth' }); } }, 100); } }, [searchQuery, messages, isSearching]); const isBlockedByYou = useMemo(() => { if(!recipient || !currentUser?.blockedUsers) return false; return !!currentUser?.blockedUsers?.[recipient.uid] }, [currentUser, recipient]); const isBlockedByRecipient = useMemo(() => { if(!recipient || !currentUser?.blockedBy) return false; return !!currentUser?.blockedBy?.[recipient.uid] }, [currentUser, recipient]); useEffect(() => { if (!recipient) return; if (recipient.isGroup) { setClientReadyStatus(null); } else if ((recipient as User).isOnline) { setClientReadyStatus(t('online')); } else if ((recipient as User).lastSeen) { try { const lastSeenText = t('lastSeen', { time: formatDistanceToNow(new Date((recipient as User).lastSeen as number), { addSuffix: true }) }); setClientReadyStatus(lastSeenText); } catch (error) { setClientReadyStatus(t('offline')); } } else { setClientReadyStatus(t('offline')); } }, [recipient, t, formatDistanceToNow]); const handleConfirmAction = async () => { if (!confirmAction || !chatId) return; playSound('touch'); switch (confirmAction.type) { case 'block': // await blockUser(recipient); break; case 'unblock': // await unblockUser(recipient); break; case 'remove': onClose(); break; case 'clear_history': await clearChatHistory(chatId); break; } setConfirmAction(null); }; const handleDeleteMessage = () => { if (messageToDelete && messageToDelete.id && chatId) { playSound('touch'); setDeletingMessageId(messageToDelete.id); setTimeout(() => { deleteMessage(chatId, messageToDelete.id!); setDeletingMessageId(null); }, 400); } setMessageToDelete(null); }; const handleGetSuggestedReplies = useCallback(async (message: Message): Promise => { if (!message.text) return []; return generateSuggestedReplies(message.text, language); }, [language]); const handleSummarizeChat = async () => { if (messages.length === 0) { addToast("Chat is empty, nothing to summarize.", { variant: 'default' }); return; } setIsSummarizing(true); setSummary(null); const localSummary = generateLocalSummary(messages, currentUser!); setSummary(localSummary); }; const handlePrivateReply = useCallback(({ displayName, uid }: { displayName: string, uid: string }) => { if (editorRef.current) { const firstLetter = displayName.split(' ').map(n => n[0]).join('').toLowerCase(); editorRef.current.innerHTML = `@'${'\'\''}${firstLetter} `; editorRef.current.focus(); // Move cursor to the end const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(editorRef.current); range.collapse(false); sel?.removeAllRanges(); sel?.addRange(range); } }, []); const handleMarkAsViewed = useCallback((messageId: string) => { setViewedOnceMessages(prev => new Set(prev).add(messageId)); }, []); const handleEmojiSelect = (emojiData: any) => { const editor = editorRef.current; if (!editor) return; let range: Range; if (lastRange.current && editor.contains(lastRange.current.commonAncestorContainer)) { range = lastRange.current; } else { range = document.createRange(); range.selectNodeContents(editor); range.collapse(false); } range.deleteContents(); const img = document.createElement('img'); img.src = emojiData.imageUrl; img.alt = emojiData.emoji; img.className = 'emoji'; range.insertNode(img); range.setStartAfter(img); range.collapse(true); lastRange.current = range.cloneRange(); setIsEmojiPickerOpen(false); }; if (!currentUser || !recipient) { return null; } const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null; const recipientStatus = clientReadyStatus; const canCall = !recipient.isGroup && !isBlockedByRecipient && !isBlockedByYou; const handleSummaryModalClose = () => { setIsSummarizing(false); setSummary(null); } const toggleSearch = () => { setIsSearching(!isSearching); setSearchQuery(''); }; return (
{!isSearching ? ( ) : (
setSearchQuery(e.target.value)} className="pl-10 bg-muted border-none focus-visible:ring-primary w-full" autoFocus />
)}
{canCall && ( <> )} {recipient.isGroup && ( )} playSound('touch')}> {t('summarizeChat')} setConfirmAction({ type: 'clear_history' })}> {t('clearHistory')} {!recipient.isGroup && ( <> {isBlockedByYou ? ( setConfirmAction({ type: 'unblock' })}> {t('unblockUser')} ) : ( setConfirmAction({type: 'block'})} className="text-destructive"> {t('blockUser')} )} setConfirmAction({type: 'remove'})} className="text-destructive"> {t('removeContact')} )}
{(isBlockedByYou || isBlockedByRecipient) && }
{loading ? (
{[...Array(3)].map((_, i) => (
))}
) : ( messages.map(msg => ( { playSound('touch'); setMessageToDelete(msg); }} onToggleReaction={(emoji) => { msg.id && toggleReaction(msg.id, emoji); }} onPreviewImage={onPreviewImage} onEditMessage={(messageId, newText) => editMessage(messageId, newText)} onTranslate={() => {}} onPrivateReply={handlePrivateReply} currentGroup={currentGroup} searchQuery={searchQuery} isHighlighted={isSearching} isViewed={viewedOnceMessages.has(msg.id)} onMarkAsViewed={handleMarkAsViewed} /> )) )} {typingUsers.length > 0 && (
{typingUsers.join(', ')} {typingUsers.length > 1 ? t('typing_plural') : t('typing')}
)}
setMediaFile({ file, originalSize: file.size })} onSendAudio={(data) => setAudioToSend(data)} chatId={chatId || null} disabled={!!isBlockedByYou || !!isBlockedByRecipient} lastMessage={lastMessage} onGetSuggestedReplies={handleGetSuggestedReplies} isGroupChat={!!recipient.isGroup} currentGroup={currentGroup} editorRef={editorRef} onToggleEmojiPicker={() => { playSound('touch'); const willOpen = !isEmojiPickerOpen; if (willOpen) { // Save cursor position before blurring const selection = window.getSelection(); if (selection && selection.rangeCount > 0 && editorRef.current?.contains(selection.anchorNode)) { lastRange.current = selection.getRangeAt(0).cloneRange(); } // Blur the input to prevent keyboard from opening on mobile editorRef.current?.blur(); } setIsEmojiPickerOpen(willOpen); }} isEmojiPickerOpen={isEmojiPickerOpen} />
{currentGroup && ( setGroupInfoOpen(false)} group={currentGroup} onAddMembers={() => onAddMembers(currentGroup)} /> )} {mediaFile && ( setMediaFile(null)} onSend={sendMediaMessage} /> )} {audioToSend && ( setAudioToSend(null)} onSend={sendVoiceMessage} /> )} !isOpen && setConfirmAction(null)}> {t('areYouSure')} {confirmAction?.type === 'block' && t('blockUserConfirm', { name: recipient.displayName })} {confirmAction?.type === 'unblock' && t('unblockUserConfirm', { name: recipient.displayName })} {confirmAction?.type === 'remove' && t('removeContactConfirm', { name: recipient.displayName })} {confirmAction?.type === 'clear_history' && t('clearHistoryConfirm', { name: recipient.displayName })} playSound('touch')}>{t('cancel')} {t('confirm')} !isOpen && setMessageToDelete(null)}> {t('deleteMessageTitle')} {t('deleteMessageConfirm')} playSound('touch')}>{t('cancel')} {t('delete')} !isOpen && handleSummaryModalClose()}> {t('chatSummary')} A quick overview of the chat. {summary ? (

Topic: {summary.topic}

Participants: {Array.isArray(summary.participants) ? summary.participants.join(', ') : ''}

Summary:
    {summary.summaryPoints.map((point, index) =>
  • {point}
  • )}
) : (
)} {t('close')}
); }