import { useEffect, useMemo, useState } from "react";
import {
cursor,
Decisions,
Face,
PlanEditor,
QuestionnaireStore,
ThreadStore,
useQuestionnaires,
} from "@chopin/editor";
import { Chat } from "./chat/chat";
import * as Identity from "./identity";
import { Wire } from "./wire";
import { paneId, usePaneOpen, Workspace } from "./workspace";
import type { Session } from "@chopin/protocol";
import type { Status } from "./wire";
import type { Pane } from "./workspace";
/**
* Claim a handle.
*
* Deliberately not a login. The field exists so that two windows are two
* people, which is the whole of what identity has to achieve here.
*/
function SignIn({ onDone }: { onDone: (handle: string) => void }) {
let [value, setValue] = useState("");
let valid = Identity.validHandle(value);
return (
);
}
const TONE: Record = {
connecting: "text-text-tertiary",
connected: "text-text-tertiary",
reconnecting: "text-warning-ink",
denied: "text-destructive-ink",
closed: "text-text-tertiary",
};
function PaneToggle(
{ onToggle, open, pane }: { onToggle: () => void; open: boolean; pane: Pane },
) {
let label = pane === "chat" ? "conversation" : "decisions";
let divider = pane === "chat" ? "M8 3.5v13" : "M12 3.5v13";
return (
);
}
function Header(
{
chatOpen,
decisionsOpen,
handle,
members,
onToggleChat,
onToggleDecisions,
reason,
room,
status,
}: {
chatOpen: boolean;
decisionsOpen: boolean;
handle: string;
members: Session.Member[];
onToggleChat: () => void;
onToggleDecisions: () => void;
reason?: string;
room: string;
status: Status;
},
) {
// Presence over the prose already shows who is editing; this is the room
// roster, which includes people who have it open but are not in the doc.
let others = members.filter(member => member.handle !== handle);
return (
chopin/r/{room}
{status === "connected" ? reason : reason ?? status}
{/* The tallest row a face sits in, so the only one drawn at 24px. */}
@{handle}
{others.length > 0 && ` ยท with ${others.map(member => `@${member.handle}`).join(", ")}`}
);
}
function Room({ handle }: { handle: string }) {
let [wire, setWire] = useState();
let [status, setStatus] = useState("connecting");
let [reason, setReason] = useState();
let [members, setMembers] = useState([]);
let room = Identity.room();
let user = useMemo(() => cursor(handle), [handle]);
let [chatOpen, setChatOpen] = usePaneOpen("chat");
let [decisionsOpen, setDecisionsOpen] = usePaneOpen("decisions");
// Written from inside the editor, read by the decisions pane beside it.
let [questions] = useState(() => new QuestionnaireStore());
let [threads] = useState(() => new ThreadStore());
let [reveal, setReveal] = useState<{ widget: string; token: number }>();
let unanswered = useQuestionnaires(questions).filter(entry =>
entry.value.questions.some(question => question.answer === undefined)
);
useEffect(() => {
let socket = new Wire({
room,
handle,
key: Identity.key(),
onStatus: (next, why) => {
setStatus(next);
setReason(why);
},
});
setWire(socket);
let off = [
socket.on("session:hello", frame => setMembers(frame.members)),
socket.on("session:presence", frame => setMembers(frame.members)),
threads.listen(socket),
];
return () => {
for (let unsubscribe of off) unsubscribe();
socket.dispose();
setWire(undefined);
};
}, [room, handle, threads]);
return (
setReveal({ widget: widget || unanswered[0]?.id || "", token: Date.now() })}
waiting={unanswered.length}
wire={wire}
/>
}
chatOpen={chatOpen}
header={
setChatOpen(value => !value)}
onToggleDecisions={() => setDecisionsOpen(value => !value)}
reason={reason}
room={room}
status={status}
/>
}
decisionsOpen={decisionsOpen}
decisions={
}
plan={
}
/>
);
}
export function App() {
let [handle, setHandle] = useState(Identity.handle);
if (!handle) return ;
return ;
}