|
|
| "use client"; |
|
|
| import { useState, useCallback, useEffect } from "react"; |
| import { useSearchParams, useRouter } from 'next/navigation'; |
| import type { ChatRecipient, User, Group, CallType } from "@/lib/types"; |
|
|
| import { useAuth } from "@/contexts/auth-context"; |
| import { useSettings } from "@/contexts/settings-context"; |
| import { useCalls } from "@/contexts/calls-context"; |
| import { useContacts } from "@/contexts/contacts-context"; |
| import { UserList } from "@/components/user-list"; |
| import { ChatWindow } from "@/app/chat-window"; |
| import { CreateGroupModal } from "@/components/create-group-modal"; |
| import { ViewProfileModal } from "@/components/view-profile-modal"; |
| import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; |
| import { ImagePreviewModal } from "@/components/image-preview-modal"; |
| import { AddGroupMembersModal } from "@/components/add-group-members-modal"; |
| import { CallModal } from "@/components/call-modal"; |
| import { SettingsPage } from "@/components/settings-page"; |
| import { MobileBottomNav } from "@/components/mobile-bottom-nav"; |
| import { Input } from "@/components/ui/input"; |
| import { ChangeNameModal } from "@/components/change-name-modal"; |
| import { ChangeStatusModal } from "@/components/change-status-modal"; |
| import { ChangeBioModal } from "@/components/change-bio-modal"; |
| import { cn } from "@/lib/utils"; |
| import { NetworkStatusIndicator } from "@/components/network-status-indicator"; |
| import { Capacitor } from '@capacitor/core'; |
| import { PushNotifications } from '@capacitor/push-notifications'; |
| import { WelcomeScreen } from "./welcome-screen"; |
|
|
|
|
| export function ChatInterface() { |
| const { currentUser, authStatus, signOutUser } = useAuth(); |
| const { t } = useSettings(); |
| const { callState, startVideoCall, answerCall, startAudioCall, endCall } = useCalls(); |
| const { findUserByPublicId } = useContacts(); |
| const searchParams = useSearchParams(); |
| const router = useRouter(); |
| |
| const [recipient, setRecipient] = useState<ChatRecipient | null>(null); |
| const [isCreateGroupModalOpen, setCreateGroupModalOpen] = useState(false); |
| const [isSettingsOpen, setSettingsOpen] = useState(false); |
| const [settingsSection, setSettingsSection] = useState<string | undefined>(undefined); |
| const [viewingProfile, setViewingProfile] = useState<User | null>(null); |
| const [isSignOutModalOpen, setSignOutModalOpen] = useState(false); |
| const [signOutConfirmText, setSignOutConfirmText] = useState(''); |
| const [imageToPreview, setImageToPreview] = useState<string | null>(null); |
| const [groupToAddMembers, setGroupToAddMembers] = useState<Group | null>(null); |
|
|
| |
| const [isChangeNameModalOpen, setChangeNameModalOpen] = useState(false); |
| const [isChangeStatusModalOpen, setChangeStatusModalOpen] = useState(false); |
| const [isChangeBioModalOpen, setChangeBioModalOpen] = useState(false); |
|
|
| |
| const handleStartCall = useCallback((peer: User, type: CallType) => { |
| if (type === 'video') { |
| startVideoCall(peer); |
| } else { |
| startAudioCall(peer); |
| } |
| }, [startAudioCall, startVideoCall]); |
| |
| const handleUserSelection = useCallback((userOrGroup: User | Group) => { |
| if (!currentUser) return; |
|
|
| if ('members' in userOrGroup) { |
| if (userOrGroup.members[currentUser.uid]) { |
| setRecipient({ ...userOrGroup.info, uid: userOrGroup.id, isGroup: true, displayName: userOrGroup.info.name, photoURL: userOrGroup.info.photoURL, publicId: userOrGroup.id }); |
| } |
| } else { |
| if (userOrGroup.uid !== currentUser.uid) { |
| setRecipient({ ...userOrGroup, isGroup: false, uid: userOrGroup.uid, displayName: userOrGroup.displayName }); |
| } |
| } |
| }, [currentUser]); |
|
|
|
|
| useEffect(() => { |
| const senderId = searchParams.get('senderId'); |
| if (senderId && currentUser) { |
| findUserByPublicId(senderId).then(user => { |
| if (user) { |
| handleUserSelection(user); |
| } |
| }); |
| } |
| }, [searchParams, currentUser, findUserByPublicId, handleUserSelection]); |
| |
| useEffect(() => { |
| if (Capacitor.getPlatform() === 'web') return; |
| |
| |
| PushNotifications.removeAllListeners().then(() => { |
| PushNotifications.addListener('pushNotificationActionPerformed', (action) => { |
| const { actionId, notification } = action; |
| const data = notification.data || {}; |
| |
| console.log(`[Push Action] Received action: ${actionId}`, data); |
| |
| if (data && data.type === 'incoming-call') { |
| |
| if (callState.status === 'incoming' && callState.channelName === data.channelName) { |
| if (actionId === 'answer') { |
| console.log('[Push Action] Answering call...'); |
| answerCall(); |
| } else if (actionId === 'decline') { |
| console.log('[Push Action] Declining call...'); |
| endCall(); |
| } |
| } else { |
| console.warn(`[Push Action] Received action for call ${data.channelName}, but current call state is ${callState.status} for channel ${callState.channelName}. Action ignored.`); |
| } |
| } |
| }); |
| }); |
| |
| |
| return () => { |
| PushNotifications.removeAllListeners(); |
| } |
| }, [callState.status, callState.channelName, answerCall, endCall]); |
|
|
| const openSettings = useCallback((section?: string) => { |
| setSettingsSection(section); |
| setSettingsOpen(true); |
| setRecipient(null); |
| }, []); |
|
|
| const openChangeNameModal = useCallback(() => setChangeNameModalOpen(true), []); |
| const openChangeStatusModal = useCallback(() => setChangeStatusModalOpen(true), []); |
| const openChangeBioModal = useCallback(() => setChangeBioModalOpen(true), []); |
|
|
| if (authStatus !== 'authenticated' || !currentUser) { |
| return null; |
| } |
|
|
| const handleSignOut = async () => { |
| await signOutUser(); |
| setSignOutModalOpen(false); |
| setSignOutConfirmText(''); |
| }; |
|
|
| const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; |
| const showMobileNav = isMobile && !recipient && !isSettingsOpen; |
| const isSignOutConfirmValid = signOutConfirmText.toLowerCase() === t('leaveWord') || signOutConfirmText.toLowerCase() === 'leave'; |
| const numericUid = parseInt(currentUser.uid.replace(/[^0-9]/g, '').substring(0, 8), 10) || Math.floor(Math.random() * 100000); |
|
|
| return ( |
| <> |
| <NetworkStatusIndicator /> |
| <div className="relative h-full w-full flex overflow-hidden"> |
| {/* Pane 1: User List (always visible on desktop, conditionally on mobile) */} |
| <div className={cn( |
| "h-full flex-col md:flex md:w-80 lg:w-96 md:flex-shrink-0 md:border-r", |
| (recipient || isSettingsOpen) ? "hidden" : "flex w-full" |
| )}> |
| <UserList |
| onSelectRecipient={(r) => { setRecipient(r); setSettingsOpen(false); }} |
| onOpenCreateGroup={() => setCreateGroupModalOpen(true)} |
| onOpenSettings={openSettings} |
| onSignOut={() => setSignOutModalOpen(true)} |
| onViewProfile={setViewingProfile} |
| /> |
| </div> |
| |
| {/* Pane 2: Main Content (Chat, Settings, or Welcome) */} |
| <div className={cn( |
| "h-full flex-1 flex-col", |
| // On mobile, this pane is only visible if a chat or settings is open |
| (!recipient && !isSettingsOpen) ? "hidden md:flex" : "flex" |
| )}> |
| {isSettingsOpen ? ( |
| <SettingsPage |
| onClose={() => setSettingsOpen(false)} |
| initialSection={settingsSection} |
| /> |
| ) : recipient ? ( |
| <ChatWindow |
| recipient={recipient} |
| onClose={() => setRecipient(null)} |
| onViewProfile={setViewingProfile} |
| onPreviewImage={setImageToPreview} |
| onAddMembers={(group) => setGroupToAddMembers(group)} |
| onStartCall={handleStartCall} |
| onOpenSettings={openSettings} |
| openChangeNameModal={openChangeNameModal} |
| openChangeStatusModal={openChangeStatusModal} |
| openChangeBioModal={openChangeBioModal} |
| /> |
| ) : ( |
| // This only shows on desktop when no chat/settings are open |
| <WelcomeScreen /> |
| )} |
| </div> |
| |
| {/* Mobile nav is only shown on the UserList screen */} |
| {showMobileNav && <MobileBottomNav onOpenSettings={openSettings} />} |
| </div> |
| |
| <CreateGroupModal isOpen={isCreateGroupModalOpen} onClose={() => setCreateGroupModalOpen(false)} /> |
| <ViewProfileModal |
| user={viewingProfile} |
| isOpen={!!viewingProfile} |
| onClose={() => setViewingProfile(null)} |
| onStartChat={handleUserSelection} |
| /> |
| |
| {groupToAddMembers && ( |
| <AddGroupMembersModal |
| isOpen={!!groupToAddMembers} |
| onClose={() => setGroupToAddMembers(null)} |
| group={groupToAddMembers} |
| /> |
| )} |
| <ImagePreviewModal imageUrl={imageToPreview} isOpen={!!imageToPreview} onClose={() => setImageToPreview(null)} /> |
| |
| <ChangeNameModal isOpen={isChangeNameModalOpen} onClose={() => setChangeNameModalOpen(false)} /> |
| <ChangeStatusModal isOpen={isChangeStatusModalOpen} onClose={() => setChangeStatusModalOpen(false)} /> |
| <ChangeBioModal isOpen={isChangeBioModalOpen} onClose={() => setChangeBioModalOpen(false)} /> |
| |
| <AlertDialog open={isSignOutModalOpen} onOpenChange={setSignOutModalOpen}> |
| <AlertDialogContent> |
| <AlertDialogHeader> |
| <AlertDialogTitle>{t('signOutConfirmationTitle')}</AlertDialogTitle> |
| <AlertDialogDescription> |
| {t('signOutConfirmationDescription', { word: t('leaveWord') })} |
| </AlertDialogDescription> |
| </AlertDialogHeader> |
| <Input |
| value={signOutConfirmText} |
| onChange={(e) => setSignOutConfirmText(e.target.value)} |
| placeholder={t('leaveWord')} |
| autoFocus |
| /> |
| <AlertDialogFooter> |
| <AlertDialogCancel onClick={() => setSignOutConfirmText('')}>{t('cancel')}</AlertDialogCancel> |
| <AlertDialogAction onClick={handleSignOut} disabled={!isSignOutConfirmValid} className="bg-destructive text-destructive-foreground hover:bg-destructive/90"> |
| {t('signOut')} |
| </AlertDialogAction> |
| </AlertDialogFooter> |
| </AlertDialogContent> |
| </AlertDialog> |
| |
| <CallModal |
| callState={callState} |
| onAnswerCall={answerCall} |
| onEndCall={endCall} |
| uid={numericUid} |
| userName={currentUser.displayName} |
| /> |
| </> |
| ); |
| } |
|
|