Skip to content
Source
Sign inSign up

usePullRequestFilesViewed.ts

85 lines3.8 KB

1import { usePullRequestConnected } from "./usePullRequestConnected";
2import { RegistryContext, useAtomRefresh, useAtomValue } from "@effect/atom-react";
3import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
4import type { PullRequestDetail } from "@t3tools/contracts";
5import { AsyncResult } from "effect/reactivity";
6import { use, useState } from "react";
7import { composerPullRequests } from "../../state/pull-requests";
8import { useEnvironmentQuery } from "../../state/query";
9import { useAtomCommand } from "../../state/use-atom-command";
10import { pullRequestTarget, type PullRequestParams } from "./usePullRequestDiff";
11import { pullRequestReviewKey } from "./pullRequestReviewDrafts";
12import { viewedOverlay, type ViewedState } from "./pullRequestFilesViewed";
13​
14export function usePullRequestFilesViewed(params: PullRequestParams, pr: PullRequestDetail | null) {
15 const registry = use(RegistryContext);
16 const target = pullRequestTarget(params);
17 const scopeKey = pullRequestReviewKey(params);
18 const supported = Boolean(pr?.capabilities.viewedFiles);
19 const permission = useAtomValue(
20 composerPullRequests.setFilesViewed.permissionAtom(target.environmentId),
21 );
22 const connected = usePullRequestConnected(params.environmentId);
23 const allowed = connected && permission;
24 const query = useEnvironmentQuery(supported ? composerPullRequests.filesViewed(target) : null);
25 const refresh = useAtomRefresh(composerPullRequests.filesViewed(target));
26 const write = useAtomCommand(composerPullRequests.setFilesViewed, { reportFailure: false });
27 const [state, setState] = useState<ViewedState>({ key: scopeKey, presses: {}, error: null });
28 const presses = state.key === scopeKey ? state.presses : {};
29 const error = state.key === scopeKey ? state.error : null;
30 const overlay = viewedOverlay(presses, query.data);
31 const update = (change: (current: ViewedState) => ViewedState) =>
32 setState((current) =>
33 change(current.key === scopeKey ? current : { key: scopeKey, presses: {}, error: null }),
34 );
35 const serverViewed = (path: string) =>
36 query.data?.files.some((file) => file.path === path && file.state === "viewed") ?? false;
37 const isViewed = (path: string) => overlay[path] ?? serverViewed(path);
38 const toggle = async (path: string) => {
39 if (!supported || !allowed || !query.data) return;
40 const viewed = !isViewed(path);
41 const version = (presses[path]?.version ?? 0) + 1;
42 update((current) => ({
43 ...current,
44 error: null,
45 presses: { ...current.presses, [path]: { viewed, version, awaitingReadAfter: undefined } },
46 }));
47 const response = await write({
48 ...target,
49 input: { ...target.input, files: [{ path, viewed }] },
50 });
51 // The read that is current once the write lands still predates it; a newer one carries it.
52 const readAtWrite = AsyncResult.getOrElse(
53 registry.get(composerPullRequests.filesViewed(target)),
54 () => null,
55 );
56 setState((current) => {
57 if (current.key !== scopeKey || current.presses[path]?.version !== version) return current;
58 if (response._tag === "Success")
59 return {
60 ...current,
61 presses: {
62 ...current.presses,
63 [path]: { viewed, version, awaitingReadAfter: { read: readAtWrite } },
64 },
65 };
66 const rest = { ...current.presses };
67 delete rest[path];
68 const cause = squashAtomCommandFailure(response);
69 return {
70 ...current,
71 presses: rest,
72 error: cause instanceof Error ? cause.message : "Could not update viewed files.",
73 };
74 });
75 refresh();
76 };
77 return {
78 supported,
79 canToggle: supported && allowed && Boolean(query.data),
80 isViewed,
81 toggle,
82 error: error ?? query.error,
83 truncated: query.data?.truncated,
84 };