Over Yjs awareness each editor tells who it is (F names the person in the access message), the device it has chosen and the windows it has open, and nothing else: no pointer, nothing that changes without a cause, sent only when it changed. Others' states are read with care. A frame in their colour round a device they chose, a dot on a window they have open too, a circle with initials each beside the mode buttons; the bridge tells the dashboard who is there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
3.1 KiB
TypeScript
68 lines
3.1 KiB
TypeScript
// The editor on a project of server F (F5.3b): the shared document over
|
|
// /sync and the room over /sim, both signed in by the cookie F set at
|
|
// /__auth. Until the document is there, a word that it is coming; when F
|
|
// refuses or lets go — of the document or the room — a word why, instead of
|
|
// the editor. Who only reads gets the editor locked (Editor's readOnly).
|
|
// The others in the project are shown in the editor and told to whoever
|
|
// embeds it (`onPeople`: the dashboard's header, over the bridge).
|
|
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { peopleOf, type PresenceUser } from './presence.js';
|
|
import { usePresence } from './usePresence.js';
|
|
import { Editor } from '../Editor.js';
|
|
import { CATALOGS, useLanguage, useTranslate } from '../i18n/i18n.js';
|
|
import type { SimHostFactory } from '../simulation/useSimulation.js';
|
|
import { RemoteSimHost, simUrl } from './remoteSimHost.js';
|
|
import { connectSharedDoc, type SharedDocState } from './sharedDoc.js';
|
|
|
|
type Ready = Extract<SharedDocState, { kind: 'ready' }>;
|
|
|
|
/** The editor on the ready document, with the others in the project. */
|
|
function ReadyEditor({ state, createSimHost, onPeople }: { readonly state: Ready; readonly createSimHost: SimHostFactory; readonly onPeople: ((people: readonly PresenceUser[]) => void) | undefined }) {
|
|
const presence = usePresence(state.awareness, state.you);
|
|
const key = JSON.stringify(peopleOf(presence.peers, state.you.id));
|
|
useEffect(() => onPeople?.(JSON.parse(key) as PresenceUser[]), [key, onPeople]);
|
|
return <Editor doc={state.doc} createSimHost={createSimHost} readOnly={state.readOnly} frozen={state.frozen} presence={presence} />;
|
|
}
|
|
|
|
export interface SharedProjectProps {
|
|
/** F's address. */
|
|
readonly server: string;
|
|
readonly project: string;
|
|
/** The others in the project, each time they change. */
|
|
readonly onPeople?: (people: readonly PresenceUser[]) => void;
|
|
}
|
|
|
|
export function SharedProject({ server, project, onPeople }: SharedProjectProps) {
|
|
const t = useTranslate();
|
|
const language = useLanguage();
|
|
const [state, setState] = useState<SharedDocState>({ kind: 'connecting' });
|
|
/** F ended the room for this page: the editor goes as when the document is let go. */
|
|
const [roomEnd, setRoomEnd] = useState<string | null>(null);
|
|
useEffect(() => connectSharedDoc({ server, project, onState: setState }), [server, project]);
|
|
const createSimHost = useMemo<SimHostFactory>(
|
|
() => () => {
|
|
const host = new RemoteSimHost(simUrl(server, project));
|
|
host.onEnd((end) => setRoomEnd(end.reason));
|
|
return host;
|
|
},
|
|
[server, project],
|
|
);
|
|
const ended = roomEnd ?? (state.kind === 'refused' || state.kind === 'closed' ? state.reason : null);
|
|
if (ended !== null) {
|
|
const key = `remote.${ended}`;
|
|
return (
|
|
<p role="alert" className="remote-message">
|
|
{key in CATALOGS[language] ? t(key) : t('remote.refused', { reason: ended })}
|
|
</p>
|
|
);
|
|
}
|
|
if (state.kind !== 'ready')
|
|
return (
|
|
<p role="status" className="remote-message">
|
|
{t('remote.connecting')}
|
|
</p>
|
|
);
|
|
return <ReadyEditor key={state.doc.guid} state={state} createSimHost={createSimHost} onPeople={onPeople} />;
|
|
}
|