Messenger/packages/web/src/app/App.tsx
Заид Омар Медхат | Zaid Omar Medhat 71e018d802 init presence
2026-07-10 16:54:40 +05:00

168 lines
4.7 KiB
TypeScript

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 (
<div className="theme-switch">
{PREFERENCES.map((option) => (
<button
key={option}
type="button"
aria-pressed={preference === option}
onClick={() => {
setPreference(option);
}}
>
{option}
</button>
))}
</div>
);
};
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<Conversation | null>(null);
const [onlineMap, setOnlineMap] = useState<Record<string, boolean>>({});
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<void> => {
try {
const presences = await getPresence(apiConfig, ids);
if (!cancelled) {
const map: Record<string, boolean> = {};
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 (
<div className="layout">
<header className="topbar">
<span>
<strong>@{user.username}</strong> · socket: {state}
</span>
<span className="topbar-actions">
<ThemeSwitch />
<button type="button" onClick={onLogout}>
Log out
</button>
</span>
</header>
<div className="workspace">
<div className="sidebar-column">
<ConversationSidebar
conversations={conversations}
currentId={current?.id ?? null}
onlineMap={onlineMap}
onSelect={setCurrent}
onStartDirect={async (username) => {
setCurrent(await startDirect(username));
}}
onCreateGroup={async (title, members) => {
setCurrent(await createGroupChat(title, members));
}}
/>
<ContactsPanel
onStartDirect={async (username) => {
setCurrent(await startDirect(username));
}}
/>
</div>
{current !== null ? (
<ChatView conversation={current} me={me} />
) : (
<section className="chat chat-empty">
<p>Search for a user or pick a contact to start chatting.</p>
</section>
)}
</div>
</div>
);
};
const Shell = (): ReactElement => {
const { status, user, logout } = useSession();
if (status === 'loading') {
return (
<main className="app">
<p>Loading</p>
</main>
);
}
if (user === null) {
return <AuthForm />;
}
return (
<RealtimeProvider>
<Dashboard
user={user}
onLogout={() => {
void logout();
}}
/>
</RealtimeProvider>
);
};
export const App = (): ReactElement => (
<SessionProvider>
<Shell />
</SessionProvider>
);