"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 (
{(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')}
);
}