1import {
2 EnvironmentId,
3 ProjectId,
4 type PullRequestDiffResult,
5 type PullRequestListEntry,
6} from "@t3tools/contracts";
7import { useAtomRefresh } from "@effect/atom-react";
8import { useCallback, useMemo, useState } from "react";
9
10import { composerPullRequests } from "../../state/pull-requests";
11import { useEnvironmentQuery } from "../../state/query";
12import { usePullToRefresh } from "../../state/use-pull-to-refresh";
13import type { ReviewParsedDiff } from "../review/reviewModel";
14import {
15 combinePullRequestDiffPages,
16 retainPullRequestDiffPage,
17 type PullRequestDiffPage,
18} from "./pullRequestDiffPages";
19
20export type PullRequestParams = {
21 readonly environmentId: string;
22 readonly projectId: string;
23 readonly repository: string;
24 readonly number: number;
25 readonly host?: string;
26 readonly initialSummary?: PullRequestListEntry;
27};
28
29export type PullRequestDiffParams = PullRequestParams & { readonly commit?: string };
30
31export function pullRequestTarget(params: PullRequestParams) {
32 return {
33 environmentId: EnvironmentId.make(params.environmentId),
34 input: {
35 projectId: ProjectId.make(params.projectId),
36 repository: params.repository,
37 number: params.number,
38 ...(params.host ? { host: params.host } : {}),
39 },
40 };
41}
42
43const NO_PAGES: readonly PullRequestDiffPage[] = [];
44
45export function usePullRequestDiff(params: PullRequestDiffParams) {
46 const reference = pullRequestTarget(params);
47 const target = {
48 ...reference,
49 input: { ...reference.input, ...(params.commit ? { commit: params.commit } : {}) },
50 };
51 const key = JSON.stringify(target);
52 const firstPageAtom = composerPullRequests.diff(target);
53 const refreshFirstPage = useAtomRefresh(firstPageAtom);
54 const { refreshing, onRefresh } = usePullToRefresh(firstPageAtom);
55 const [state, setState] = useState<{
56 key: string;
57 cursor: string | null;
58 pages: readonly PullRequestDiffPage[];
59 }>({ key, cursor: null, pages: NO_PAGES });
60 const cursor = state.key === key ? state.cursor : null;
61 const query = useEnvironmentQuery(
62 composerPullRequests.diff({
63 ...target,
64 input: { ...target.input, ...(cursor ? { cursor } : {}) },
65 }),
66 );
67 const pages = useMemo(() => {
68 const loaded = state.key === key ? state.pages : NO_PAGES;
69 return query.data ? retainPullRequestDiffPage(loaded, cursor, query.data) : loaded;
70 }, [cursor, key, query.data, state.key, state.pages]);
71 if (state.key !== key || state.pages !== pages) setState({ key, cursor, pages });
72
73 const [parseCache] = useState(() => new WeakMap<PullRequestDiffResult, ReviewParsedDiff>());
74 const parsed = useMemo(
75 () => combinePullRequestDiffPages(pages, parseCache, key),
76 [key, pages, parseCache],
77 );
78 const patch = useMemo(() => pages.map((page) => page.result.patch).join("\n"), [pages]);
79 const nextCursor = pages.at(-1)?.result.nextCursor ?? null;
80 const canLoadMore =
81 nextCursor !== null && nextCursor !== cursor && !query.isPending && query.error === null;
82 const loadMore = useCallback(() => {
83 if (!canLoadMore) return;
84 setState({ key, cursor: nextCursor, pages });
85 }, [canLoadMore, key, nextCursor, pages, setState]);
86 const refresh = useCallback(() => {
87 setState({ key, cursor: null, pages: NO_PAGES });
88 refreshFirstPage();
89 }, [key, refreshFirstPage, setState]);
90 const onPullRefresh = useCallback(() => {
91 setState({ key, cursor: null, pages: NO_PAGES });
92 onRefresh();
93 }, [key, onRefresh, setState]);
94 return {
95 ...query,
96 refresh,
97 parsed,
98 patch,
99 nextCursor,
100 canLoadMore,
101 loadMore,
102 refreshing,
103 onPullRefresh,
104 };
105}