import { useEffect, useMemo, useRef, useState } from "react";
import {
countUnanswered,
cursor,
Decisions,
Face,
PlanEditor,
QuestionnaireStore,
ThreadStore,
useHasPlanContent,
useQuestionnaires,
visibleDecisionView,
} from "@chopin/editor";
import { Chat } from "./chat/chat";
import { decisionAttention, DecisionViewControl } from "./decision-view-control";
import * as Identity from "./identity";
import { Wire } from "./wire";
import { paneId, usePaneOpen, Workspace } from "./workspace";
import type { Session } from "@chopin/protocol";
import type { DecisionView } from "@chopin/editor";
import type { Status } from "./wire";
/**
* 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 }: { onToggle: () => void; open: boolean }) {
return (
);
}
function Header(
{
chatOpen,
members,
onToggleChat,
reason,
room,
status,
}: {
chatOpen: boolean;
members: Session.Member[];
onToggleChat: () => void;
reason?: string;
room: string;
status: Status;
},
) {
return (
);
}
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");
// Written from inside the editor, read by the Decisions document view.
let [questions] = useState(() => new QuestionnaireStore());
let [threads] = useState(() => new ThreadStore());
let [reveal, setReveal] = useState<{ widget: string; token: number }>();
let [planScrollTop, setPlanScrollTop] = useState(0);
let entries = useQuestionnaires(questions);
let unanswered = countUnanswered(entries);
let hasPlanContent = useHasPlanContent(questions);
let [preferredView, setPreferredView] = useState(() => {
let stored = localStorage.getItem("chopin:view:document");
return stored === "decisions" ? "decisions" : "plan";
});
let view = visibleDecisionView(preferredView, hasPlanContent, unanswered);
let previousUnanswered = useRef(unanswered);
let [attention, setAttention] = useState(false);
useEffect(() => {
if (hasPlanContent) localStorage.setItem("chopin:view:document", preferredView);
}, [hasPlanContent, preferredView]);
useEffect(() => {
let previous = previousUnanswered.current;
previousUnanswered.current = unanswered;
if (!decisionAttention(previous, unanswered)) return;
setAttention(true);
let timer = window.setTimeout(() => setAttention(false), 200);
return () => window.clearTimeout(timer);
}, [unanswered]);
let selectView = (next: DecisionView) => {
setPreferredView(next);
if (hasPlanContent) localStorage.setItem("chopin:view:document", next);
};
let showPlan = (widget: string, question: string) => {
selectView("plan");
requestAnimationFrame(() => {
questions.reveal(widget, question);
let card = document.querySelector(
`[data-document-view="plan"] article[data-plan-sidecar-questionnaire="${
CSS.escape(widget)
}"]`,
);
if (!card) return;
card.tabIndex = -1;
card.focus();
});
};
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 (
{
selectView("decisions");
setReveal({ widget: widget || entries[0]?.id || "", token: Date.now() });
}}
waiting={unanswered}
wire={wire}
/>
}
chatOpen={chatOpen}
header={
setChatOpen(value => !value)}
reason={reason}
room={room}
status={status}
/>
}
controls={
}
decisions={
}
plan={
}
view={view}
/>
);
}
export function App() {
let [handle, setHandle] = useState(Identity.handle);
if (!handle) return ;
return ;
}