| |
|
| | "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} |
| | /> |
| | </> |
| | ); |
| | } |
| |
|