5ea8fa1c20
Replace the mock data source with a real data layer that talks to the webgw gateway over REST + SSE. The UI components keep their look and props; only where the data comes from changed. - src/api.ts: the single repository layer. fetch wrappers (same-origin cookie) for login/logout/me and rooms list/create/join/send, plus streamRoom() which opens an EventSource and yields each decrypted message. Wire->UI mappers (roomFromWire, messageFromWire). - src/types.ts: add the gateway wire shapes (MeInfo, RoomWire, MsgWire) next to the existing UI types. - App.tsx: probe /api/me on mount to resume an existing session; otherwise show Login. Logout calls the gateway. - Login.tsx: the password field now unlocks the gateway session (operator passphrase); shows a basic error and a loading state. Wallet-per-browser is phase 2. - ChatShell.tsx: load rooms from /api/rooms with loading / empty / error states; same Flex layout. - ChatPanel.tsx: stream messages over SSE for the active room (dedup by id), composer sends through the gateway; no optimistic insert (the peer's own echo returns over SSE with the real frame id). - vite.config.ts: dev proxy /api (REST + SSE) -> the gateway on :8481. mock.ts is left untouched (no longer imported) to avoid churn with the parallel styling work on master. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
2.3 KiB
TypeScript
93 lines
2.3 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { Flex, Box, Center, Loader, Stack, Text, Button } from "@mantine/core";
|
|
import { Sidebar } from "./Sidebar";
|
|
import { ChatPanel } from "./ChatPanel";
|
|
import { api } from "./api";
|
|
import type { Room, User } from "./types";
|
|
|
|
export function ChatShell({
|
|
user,
|
|
onLogout,
|
|
}: {
|
|
user: User;
|
|
onLogout: () => void;
|
|
}) {
|
|
const [rooms, setRooms] = useState<Room[]>([]);
|
|
const [activeId, setActiveId] = useState<string>("");
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const load = useCallback(() => {
|
|
setLoading(true);
|
|
api
|
|
.listRooms()
|
|
.then((rs) => {
|
|
setRooms(rs);
|
|
setActiveId((cur) => cur || rs[0]?.id || "");
|
|
setError(null);
|
|
})
|
|
.catch((e) => setError(e?.message ?? "No se pudieron cargar las rooms"))
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [load]);
|
|
|
|
const active = rooms.find((r) => r.id === activeId);
|
|
|
|
// El panel derecho muestra el estado de carga/error/empty sin tocar el layout.
|
|
let panel = <ChatPanel room={active} />;
|
|
if (loading && rooms.length === 0) {
|
|
panel = (
|
|
<Center h="100%">
|
|
<Loader color="brand" />
|
|
</Center>
|
|
);
|
|
} else if (error) {
|
|
panel = (
|
|
<Center h="100%">
|
|
<Stack align="center" gap="sm">
|
|
<Text c="red" size="sm">
|
|
{error}
|
|
</Text>
|
|
<Button variant="light" color="brand" onClick={load}>
|
|
Reintentar
|
|
</Button>
|
|
</Stack>
|
|
</Center>
|
|
);
|
|
} else if (rooms.length === 0) {
|
|
panel = (
|
|
<Center h="100%">
|
|
<Text c="dimmed">No perteneces a ninguna room todavía</Text>
|
|
</Center>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Flex h="100vh" w="100vw" style={{ overflow: "hidden" }}>
|
|
<Box
|
|
w={320}
|
|
h="100%"
|
|
bg="dark.8"
|
|
style={{
|
|
borderRight: "1px solid var(--mantine-color-dark-4)",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<Sidebar
|
|
user={user}
|
|
rooms={rooms}
|
|
activeId={activeId}
|
|
onSelect={setActiveId}
|
|
onLogout={onLogout}
|
|
/>
|
|
</Box>
|
|
<Box flex={1} h="100%" bg="dark.7" style={{ minWidth: 0 }}>
|
|
{panel}
|
|
</Box>
|
|
</Flex>
|
|
);
|
|
}
|