import { useCallback, useEffect, useRef, useState } from "react"; import { documentPath } from "@chopin/protocol/document-url"; import * as Api from "./api"; import { documentRouteIdentity } from "./document-route-swap"; import { canManageProject } from "./navigation-model"; import { holdTyping, requestTitleEdit } from "./title-edit"; export type DocumentCreationPhase = "creating" | "opening"; type Attempt = { phase: DocumentCreationPhase; destination?: string; }; export function useDocumentCreation( { routeKey, navigationRevision, onCreated, onNavigate, onAccessChanged }: { routeKey: string; navigationRevision: number; onCreated: (channel: Api.Channel) => void; onNavigate: (documentId: string, path: string) => void; onAccessChanged: () => void; }, ) { let attempts = useRef(new Map()); let [pending, setPending] = useState>(() => new Map()); let [error, setError] = useState<{ project: Api.NavigationProject; message: string }>(); let latest = useRef(undefined); // Canonicalizing the same document changes its route key without a navigation. let location = useRef({ routeKey, navigationRevision }); if (location.current.navigationRevision !== navigationRevision) { location.current = { routeKey, navigationRevision }; } else location.current.routeKey = routeKey; let publish = useCallback(() => { setPending(new Map([...attempts.current].map(([id, attempt]) => [id, attempt.phase]))); }, []); let finish = useCallback((projectId: string, attempt: Attempt) => { if (attempts.current.get(projectId) !== attempt) return; attempts.current.delete(projectId); publish(); }, [publish]); useEffect(() => () => { latest.current = undefined; location.current = { routeKey: "", navigationRevision: -1 }; }, []); useEffect(() => { for (let [id, attempt] of attempts.current) { if (attempt.phase === "opening" && attempt.destination !== routeKey) { finish(id, attempt); holdTyping(false); } } setError(undefined); }, [finish, routeKey]); let settled = useCallback((key: string) => { if (location.current.routeKey !== key) return; for (let [id, attempt] of attempts.current) { if (attempt.phase === "opening" && attempt.destination === key) finish(id, attempt); } }, [finish]); let create = async (project: Api.NavigationProject) => { let id = project.repositoryId; if (!project.available || !canManageProject(project) || attempts.current.has(id)) return; let attempt: Attempt = { phase: "creating" }; let origin = location.current; attempts.current.set(id, attempt); holdTyping(true); latest.current = attempt; setError(undefined); publish(); try { let created = await Api.createChannel(project.repositoryOwner, project.repositoryName); onCreated(created.channel); // A successful POST still belongs in the catalogue after the user moves on. if (location.current !== origin || latest.current !== attempt) { finish(id, attempt); holdTyping(false); return; } attempt.phase = "opening"; attempt.destination = documentRouteIdentity({ page: "document", owner: created.repository.owner, repository: created.repository.name, slug: created.channel.slug, }); publish(); requestTitleEdit(created.channel.id, "new"); onNavigate( created.channel.id, documentPath( created.repository.owner, created.repository.name, created.channel.slug, ), ); } catch (reason) { finish(id, attempt); holdTyping(false); if (location.current === origin && latest.current === attempt) { setError({ project, message: reason instanceof Error ? reason.message : "Could not create document.", }); } if (reason instanceof Api.ApiError && reason.status === 403) onAccessChanged(); } }; return { create, error, pending, settled }; }