1import { useAtomValue } from "@effect/atom-react";
2import { useEffect } from "react";
3import { AppState } from "react-native";
4import * as Atom from "effect/reactivity/Atom";
5import * as Schema from "effect/Schema";
6import { appAtomRegistry } from "../../state/atom-registry";
7import { writeFileAtomically } from "../../lib/atomic-file";
8import { SerializedAsyncQueue } from "../../lib/serialized-async-queue";
9import {
10 EMPTY_REVIEW_DRAFT,
11 ReviewDraftDocument,
12 pullRequestReviewKey,
13 type ReviewDraft,
14} from "./pullRequestReviewDrafts";
15import type { PullRequestParams } from "./usePullRequestDiff";
16
17const state = Atom.make<{
18 drafts: Record<string, ReviewDraft>;
19 ready: boolean;
20 error: string | null;
21}>({ drafts: {}, ready: false, error: null }).pipe(Atom.keepAlive);
22const decodeDocument = Schema.decodeUnknownSync(ReviewDraftDocument);
23const queue = new SerializedAsyncQueue();
24let loading: Promise<void> | null = null;
25let timer: ReturnType<typeof setTimeout> | null = null;
26let savedDrafts: Record<string, ReviewDraft> | null = null;
27async function draftFile() {
28 const { Directory, File, Paths } = await import("expo-file-system");
29 const directory = new Directory(Paths.document, "pull-request-drafts");
30 directory.create({ idempotent: true, intermediates: true });
31 return new File(directory, "drafts.json");
32}
33function load() {
34 if (loading) return loading;
35 loading = (async () => {
36 try {
37 const file = await draftFile();
38 const drafts = file.exists ? decodeDocument(JSON.parse(await file.text())).drafts : {};
39 savedDrafts = drafts;
40 appAtomRegistry.set(state, { drafts, ready: true, error: null });
41 } catch {
42 appAtomRegistry.set(state, {
43 ...appAtomRegistry.get(state),
44 error: "Could not restore review drafts. Reopen this screen to retry.",
45 });
46 loading = null;
47 }
48 })();
49 return loading;
50}
51async function persist() {
52 if (timer) clearTimeout(timer);
53 timer = null;
54 await queue.run(async () => {
55 const current = appAtomRegistry.get(state);
56 if (!current.ready || current.drafts === savedDrafts) return;
57 try {
58 const drafts = Object.fromEntries(
59 Object.entries(current.drafts).filter(
60 ([, draft]) => draft.summary || draft.comment || draft.comments.length,
61 ),
62 );
63 await writeFileAtomically(await draftFile(), JSON.stringify({ version: 1, drafts }));
64 savedDrafts = current.drafts;
65 if (appAtomRegistry.get(state).error)
66 appAtomRegistry.set(state, { ...appAtomRegistry.get(state), error: null });
67 } catch {
68 appAtomRegistry.set(state, {
69 ...appAtomRegistry.get(state),
70 error: "Drafts are kept in this session, but could not be saved on this device.",
71 });
72 }
73 });
74}
75export function usePullRequestReviewDraft(params: PullRequestParams) {
76 const snapshot = useAtomValue(state);
77 const key = pullRequestReviewKey(params);
78 useEffect(() => {
79 void load();
80 const listener = AppState.addEventListener("change", (phase) => {
81 if (phase !== "active") void persist();
82 });
83 return () => {
84 listener.remove();
85 void persist();
86 };
87 }, []);
88 const update = (transform: (current: ReviewDraft) => ReviewDraft) => {
89 const current = appAtomRegistry.get(state);
90 if (!current.ready) return;
91 appAtomRegistry.set(state, {
92 ...current,
93 drafts: { ...current.drafts, [key]: transform(current.drafts[key] ?? EMPTY_REVIEW_DRAFT) },
94 });
95 if (timer) clearTimeout(timer);
96 timer = setTimeout(() => void persist(), 200);
97 };
98 return {
99 draft: snapshot.drafts[key] ?? EMPTY_REVIEW_DRAFT,
100 ready: snapshot.ready,
101 error: snapshot.error,
102 update,
103 flush: persist,
104 };
105}