1import { usePullRequestConnected } from "./usePullRequestConnected";
2import { useAtomRefresh, useAtomValue } from "@effect/atom-react";
3import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
4import type { PullRequestDetail } from "@t3tools/contracts";
5import { useCallback, useEffect, useRef, useState } from "react";
6import { composerPullRequests } from "../../state/pull-requests";
7import { useEnvironmentQuery } from "../../state/query";
8import { useAtomCommand } from "../../state/use-atom-command";
9import { pullRequestTarget, type PullRequestParams } from "./usePullRequestDiff";
10import { pullRequestReviewKey } from "./pullRequestReviewDrafts";
11
12export function usePullRequestFilesViewed(params: PullRequestParams, pr: PullRequestDetail | null) {
13 const target = pullRequestTarget(params);
14 const scopeKey = pullRequestReviewKey(params);
15 const supported = Boolean(pr?.capabilities.viewedFiles);
16 const permission = useAtomValue(
17 composerPullRequests.setFilesViewed.permissionAtom(target.environmentId),
18 );
19 const connected = usePullRequestConnected(params.environmentId);
20 const allowed = connected && permission;
21 const query = useEnvironmentQuery(supported ? composerPullRequests.filesViewed(target) : null);
22 const refresh = useAtomRefresh(composerPullRequests.filesViewed(target));
23 const write = useAtomCommand(composerPullRequests.setFilesViewed, { reportFailure: false });
24 const [state, setState] = useState<{
25 key: string;
26 overlay: Record<string, boolean>;
27 error: string | null;
28 }>({ key: scopeKey, overlay: {}, error: null });
29 const overlay = state.key === scopeKey ? state.overlay : {};
30 const error = state.key === scopeKey ? state.error : null;
31 const setOverlay = useCallback(
32 (overlay: Record<string, boolean>) =>
33 setState((current) => ({
34 key: scopeKey,
35 overlay,
36 error: current.key === scopeKey ? current.error : null,
37 })),
38 [scopeKey],
39 );
40 const setError = (error: string | null) =>
41 setState((current) => ({
42 key: scopeKey,
43 error,
44 overlay: current.key === scopeKey ? current.overlay : {},
45 }));
46 const versions = useRef(new Map<string, number>());
47 const pressed = useRef<Record<string, boolean>>({});
48 const reads = useRef(query.data);
49 useEffect(() => {
50 reads.current = query.data;
51 }, [query.data]);
52 const awaitingRead = useRef(new Map<string, typeof query.data>());
53 const scope = useRef(scopeKey);
54 useEffect(() => {
55 scope.current = scopeKey;
56 versions.current.clear();
57 awaitingRead.current.clear();
58 pressed.current = {};
59 }, [scopeKey]);
60 useEffect(() => {
61 if (supported && pr?.headSha) refresh();
62 }, [supported, pr?.headSha, refresh]);
63 useEffect(() => {
64 let changed = false;
65 const next = { ...pressed.current };
66 for (const [path, from] of awaitingRead.current) {
67 if (from === query.data) continue;
68 delete next[path];
69 awaitingRead.current.delete(path);
70 changed = true;
71 }
72 if (changed) {
73 pressed.current = next;
74 setOverlay(next);
75 }
76 }, [query.data, setOverlay]);
77 const isViewed = (path: string) =>
78 overlay[path] ??
79 query.data?.files.some((file) => file.path === path && file.state === "viewed") ??
80 false;
81 const toggle = async (path: string) => {
82 if (!supported || !allowed || !query.data) return;
83 const viewed = !(
84 pressed.current[path] ??
85 query.data.files.some((file) => file.path === path && file.state === "viewed")
86 );
87 const version = (versions.current.get(path) ?? 0) + 1;
88 versions.current.set(path, version);
89 awaitingRead.current.delete(path);
90 pressed.current = { ...pressed.current, [path]: viewed };
91 setOverlay(pressed.current);
92 setError(null);
93 const response = await write({
94 ...target,
95 input: { ...target.input, files: [{ path, viewed }] },
96 });
97 if (scope.current !== scopeKey) return;
98 if (response._tag === "Failure" && versions.current.get(path) === version) {
99 const next = { ...pressed.current };
100 delete next[path];
101 pressed.current = next;
102 setOverlay(next);
103 const cause = squashAtomCommandFailure(response);
104 setError(cause instanceof Error ? cause.message : "Could not update viewed files.");
105 }
106 if (response._tag === "Success" && versions.current.get(path) === version)
107 awaitingRead.current.set(path, reads.current);
108 refresh();
109 };
110 return {
111 supported,
112 canToggle: supported && allowed && Boolean(query.data),
113 isViewed,
114 toggle,
115 error: error ?? query.error,
116 truncated: query.data?.truncated,
117 };
118}