import React, { useEffect, useRef, useState, useCallback } from 'react'; import { useLocation } from 'react-router-dom'; import { useSpaceStore } from '../../stores/spaceStore'; import { useChatStore } from '../../stores/chatStore'; import { useUIStore } from '../../stores/uiStore'; import { useAuthStore } from '../../stores/authStore'; import { MessageList } from '../chat/MessageList'; import { MessageInput } from '../chat/MessageInput'; import { VoiceGrid } from '../voice/VoiceGrid'; import { VoiceControlBar } from '../voice/VoiceControlBar'; import { VoiceChatPanel } from '../voice/VoiceChatPanel'; import { FriendsPage } from '../chat/FriendsPage'; import { ExplorePage } from '../chat/ExplorePage'; import { Avatar } from '../ui/Avatar'; import { useVoiceStore } from '../../stores/voiceStore'; import { wsSend } from '../../hooks/useWebSocket'; import { MemberListToggleButton } from './MemberListToggleButton'; import { TransferIndicator } from './TransferIndicator'; import { isSelf, parseFederatedUsername, isFederationGlobeApplicable } from '../../utils/identity'; import { useCanonicalUserView } from '../../utils/userViewLookup'; import type { User } from '@backspace/shared'; import { Tooltip } from '../ui/Tooltip'; import { joinVoiceChannel } from '../../utils/voice'; import { SearchPopover } from '../chat/SearchPopover'; import { isDmChannel, getChannelOrigin } from '../../stores/spaceStore'; export function MainContent() { // 1. ALL HOOKS AT THE TOP const channels = useSpaceStore((s) => s.channels); const currentChannelId = useChatStore((s) => s.currentChannelId); const currentSpaceId = useSpaceStore((s) => s.currentSpaceId); const voiceChatOpen = useUIStore((s) => s.voiceChatOpen); const voiceFullscreen = useUIStore((s) => s.voiceFullscreen); const setVoiceFullscreen = useUIStore((s) => s.setVoiceFullscreen); const participants = useVoiceStore((s) => s.participants); const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId); const isLiveKitConnected = useVoiceStore((s) => s.isLiveKitConnected); const connectionError = useVoiceStore((s) => s.connectionError); const showDms = useUIStore((s) => s.showDms); const location = useLocation(); const isExplorePage = location.pathname === '/explore'; const activeDmCall = useVoiceStore((s) => s.activeDmCall); const outgoingCall = useVoiceStore((s) => s.outgoingCall); const dmChannels = useSpaceStore((s) => s.dmChannels); const authUser = useAuthStore((s) => s.user); const openModal = useUIStore((s) => s.openModal); const voiceContainerRef = useRef(null); const searchButtonRef = useRef(null); const [searchOpen, setSearchOpen] = useState(false); const [jumpToMessageId, setJumpToMessageId] = useState(null); // Resolve the DM header's "first other" member through the canonical view // cache. The hook must be called unconditionally at the component top, so we // pass a fallback when there is no current DM channel or 1-on-1 partner. const _dmChannel = dmChannels.find(dm => dm.id === currentChannelId); const _rawFirstOther = _dmChannel?.members.filter(m => !isSelf(m, authUser))[0] ?? null; const _FALLBACK_USER = { id: '', username: '', createdAt: 0, isAdmin: false, replicatedInstances: [] } as unknown as User; const _canonicalFirstOther = useCanonicalUserView(_rawFirstOther ?? _FALLBACK_USER); // Reset search when channel changes useEffect(() => { setSearchOpen(false); }, [currentChannelId]); // Handle actual browser fullscreen API useEffect(() => { const handleFullscreenChange = () => { setVoiceFullscreen(!!document.fullscreenElement); }; document.addEventListener('fullscreenchange', handleFullscreenChange); return () => document.removeEventListener('fullscreenchange', handleFullscreenChange); }, [setVoiceFullscreen]); useEffect(() => { if (voiceFullscreen && voiceContainerRef.current && !document.fullscreenElement) { voiceContainerRef.current.requestFullscreen().catch(err => { console.error('Error attempting to enable full-screen mode:', err); }); } else if (!voiceFullscreen && document.fullscreenElement) { document.exitFullscreen().catch(() => {}); } }, [voiceFullscreen]); // 2. LOGIC AND EARLY RETURNS const channel = channels.find(c => c.id === currentChannelId); const isVoiceChannel = channel?.type === 'voice'; if (showDms || isExplorePage || !currentSpaceId) { if (!currentChannelId) { if (isExplorePage) return ; return ; } const dmChannel = dmChannels.find(dm => dm.id === currentChannelId); const otherMembers = dmChannel?.members.filter(m => !isSelf(m, authUser)) ?? []; const isGroupDm = !!dmChannel?.ownerId; // Use the canonicalized view of the 1-on-1 partner (resolved above the // conditional so the hook is called unconditionally). const firstOther = _rawFirstOther ? _canonicalFirstOther : (otherMembers[0] ?? null); const { baseName: firstBaseName } = parseFederatedUsername(firstOther?.username ?? ''); const dmName = isGroupDm ? otherMembers.map(m => m.displayName ?? parseFederatedUsername(m.username).baseName).join(', ') : firstOther?.displayName ?? (firstBaseName || 'Direct Message'); const isInDmCall = activeDmCall?.dmChannelId === currentChannelId; const isCallingThisDm = outgoingCall?.dmChannelId === currentChannelId; const handleStartVoiceCall = () => { if (!currentChannelId) return; useVoiceStore.getState().setOutgoingCall({ dmChannelId: currentChannelId }); wsSend({ type: 'dm_call_start', dmChannelId: currentChannelId }, getChannelOrigin(currentChannelId)); }; const handleCancelCall = () => { if (!currentChannelId) return; useVoiceStore.getState().setOutgoingCall(null); const { federatedCallId, callOrigin } = useVoiceStore.getState(); const origin = callOrigin || getChannelOrigin(currentChannelId); wsSend({ type: 'dm_call_end', dmChannelId: currentChannelId, federatedCallId }, origin); }; if (isInDmCall) { return (
{dmName} {connectionError ? ( Connection Failed ) : isLiveKitConnected ? ( <> Connected {participants.length} in call ) : ( Connecting... )}
{voiceChatOpen && !voiceFullscreen && ( )}
); } return (
{isCallingThisDm && (
Calling {dmName}...
)}
{dmName} {!isGroupDm && firstOther && isFederationGlobeApplicable(firstOther) && ( )} {isGroupDm && ( ({dmChannel?.members.length} Members) )}
setJumpToMessageId(null)} /> setSearchOpen(false)} anchorRef={searchButtonRef} channelId={currentChannelId} isDm={true} onJumpToMessage={(id) => { setJumpToMessageId(id); setSearchOpen(false); }} />
); } if (!currentChannelId || !channel) { return (
Select a channel

Select a text or voice channel to get started

); } if (isVoiceChannel) { const isInThisChannel = currentVoiceChannelId === currentChannelId; if (!isInThisChannel) { return (
{channel.name}

{channel.name}

No one is currently in this voice channel.

); } return (
{channel.name} {connectionError ? ( Connection Failed ) : isLiveKitConnected ? ( <> Connected {participants.length} connected ) : ( Connecting... )}
{voiceChatOpen && !voiceFullscreen && ( )}
); } return (
# {channel.name} {channel.topic && ( <>
{channel.topic} )}
setJumpToMessageId(null)} /> setSearchOpen(false)} anchorRef={searchButtonRef} channelId={currentChannelId} isDm={false} onJumpToMessage={(id) => { setJumpToMessageId(id); setSearchOpen(false); }} />
); }