import { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import type { Conversation, PublicUser, User } from '@altricade/core'; import { getPresence, heartbeat } from '@altricade/core/api'; import { SessionProvider, useSession } from '../entities/session'; import { AuthForm } from '../features/auth'; import { RealtimeProvider, useRealtime } from '../features/realtime'; import { useConversations, ConversationSidebar } from '../features/conversations'; import { ContactsPanel } from '../features/contacts'; import { ChatView } from '../features/messaging'; import { apiConfig } from '../shared/api'; import { useTheme } from '../shared/theme'; import type { ThemePreference } from '../shared/theme'; const PREFERENCES: readonly ThemePreference[] = ['light', 'dark', 'system']; const ThemeSwitch = (): ReactElement => { const { preference, setPreference } = useTheme(); return (
{PREFERENCES.map((option) => ( ))}
); }; const Dashboard = ({ user, onLogout }: { user: User; onLogout: () => void }): ReactElement => { const me: PublicUser = { id: user.id, username: user.username, displayName: user.displayName, avatarUrl: user.avatarUrl, }; const { state } = useRealtime(); const { conversations, startDirect, createGroupChat } = useConversations(user.id); const [current, setCurrent] = useState(null); const [onlineMap, setOnlineMap] = useState>({}); const peerKey = conversations .flatMap((conversation) => (conversation.peer === null ? [] : [conversation.peer.id])) .join(','); // Poll presence for the people we have DMs with. useEffect(() => { const ids = peerKey.split(',').filter((id) => id.length > 0); if (ids.length === 0) { return undefined; } let cancelled = false; const poll = async (): Promise => { try { const presences = await getPresence(apiConfig, ids); if (!cancelled) { const map: Record = {}; for (const presence of presences) { map[presence.userId] = presence.online; } setOnlineMap(map); } } catch { // presence is best-effort } }; void poll(); const interval = setInterval(() => { void poll(); }, 20000); return () => { cancelled = true; clearInterval(interval); }; }, [peerKey]); // Keep-alive heartbeat so our own last-seen stays fresh while connected. useEffect(() => { const interval = setInterval(() => { void heartbeat(apiConfig); }, 45000); return () => { clearInterval(interval); }; }, []); return (
@{user.username} · socket: {state}
{ setCurrent(await startDirect(username)); }} onCreateGroup={async (title, members) => { setCurrent(await createGroupChat(title, members)); }} /> { setCurrent(await startDirect(username)); }} />
{current !== null ? ( ) : (

Search for a user or pick a contact to start chatting.

)}
); }; const Shell = (): ReactElement => { const { status, user, logout } = useSession(); if (status === 'loading') { return (

Loading…

); } if (user === null) { return ; } return ( { void logout(); }} /> ); }; export const App = (): ReactElement => ( );