1import { useAtomValue } from "@effect/atom-react";
2import { scopedThreadKey, scopeThreadRef } from "@t3tools/client-runtime/environment";
3import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
4import {
5 createLinkedPullRequestSummaryAtomFamily,
6 pullRequestDetailToVcsStatus,
7} from "@t3tools/client-runtime/state/pull-requests";
8import { Atom } from "effect/reactivity";
9import { useCallback, useEffect, useMemo } from "react";
10
11import { connectionAtomRuntime } from "../connection/runtime";
12import { appAtomRegistry } from "./atom-registry";
13import { serverEnvironment } from "./server";
14import { useEnvironmentQuery } from "./query";
15import {
16 resolveThreadPrSource,
17 presentThreadPr,
18 type ThreadPrPresentation,
19} from "./thread-pr-presentation";
20
21const pullRequestSummaryAtom = createLinkedPullRequestSummaryAtomFamily(connectionAtomRuntime);
22const MAX_THREAD_PR_SNAPSHOTS = 500;
23
24interface ThreadPrSnapshot {
25 readonly identity: string;
26 readonly presentation: ThreadPrPresentation;
27}
28
29// One bounded cache survives row virtualization without retaining one live
30// atom for every thread or pull request ever seen.
31const threadPrSnapshotsAtom = Atom.make<ReadonlyMap<string, ThreadPrSnapshot>>(new Map()).pipe(
32 Atom.keepAlive,
33 Atom.withLabel("mobile:thread-pr-snapshots"),
34);
35
36export {
37 presentThreadPr,
38 type ThreadPr,
39 type ThreadPrPresentation,
40} from "./thread-pr-presentation";
41
42/**
43 * Linked PRs use server snapshots. Branch fallback and legacy references share
44 * a live summary request across visible rows in the same environment.
45 */
46export function useThreadPr(thread: EnvironmentThreadShell): ThreadPrPresentation | null {
47 const supportsLinks = useAtomValue(
48 serverEnvironment.configValueAtom(thread.environmentId),
49 (config) => config?.environment.capabilities.threadPullRequests === true,
50 );
51 const { linkedPresentation, pullRequestRef } = useMemo(
52 () =>
53 resolveThreadPrSource(
54 {
55 pullRequests: thread.pullRequests,
56 linkedPullRequest: thread.linkedPullRequest,
57 branchPullRequest: thread.branchPullRequest,
58 },
59 { threadPullRequests: supportsLinks },
60 ),
61 [thread.pullRequests, thread.linkedPullRequest, thread.branchPullRequest, supportsLinks],
62 );
63 const threadKey = scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id));
64 const snapshotIdentity = JSON.stringify(pullRequestRef);
65 // Select this row's entry so writes for other rows do not re-render it.
66 const snapshotEntry = useAtomValue(
67 threadPrSnapshotsAtom,
68 useCallback(
69 (current: ReadonlyMap<string, ThreadPrSnapshot>) => current.get(threadKey),
70 [threadKey],
71 ),
72 );
73 const snapshot = snapshotEntry?.identity === snapshotIdentity ? snapshotEntry.presentation : null;
74 const pullRequestSummary = useEnvironmentQuery(
75 pullRequestRef === null
76 ? null
77 : pullRequestSummaryAtom({
78 environmentId: thread.environmentId,
79 input: {
80 projectId: pullRequestRef.projectId,
81 repository: pullRequestRef.repository,
82 number: pullRequestRef.number,
83 },
84 }),
85 );
86
87 const live = useMemo<ThreadPrPresentation | null | undefined>(() => {
88 if (pullRequestRef === null) return null;
89 const summary = pullRequestSummary.data;
90 return summary === null
91 ? undefined
92 : presentThreadPr(pullRequestDetailToVcsStatus(summary), {
93 kind: summary.provider,
94 name: summary.provider,
95 baseUrl: "",
96 });
97 }, [pullRequestRef, pullRequestSummary.data]);
98
99 useEffect(() => {
100 if (live === undefined) return;
101 appAtomRegistry.modify(threadPrSnapshotsAtom, (current) => {
102 const existing = current.get(threadKey);
103 if (live === null) {
104 if (existing === undefined) return [false, current];
105 const next = new Map(current);
106 next.delete(threadKey);
107 return [true, next];
108 }
109 if (existing?.identity === snapshotIdentity && existing.presentation === live) {
110 return [false, current];
111 }
112 const next = new Map(current);
113 next.delete(threadKey);
114 next.set(threadKey, { identity: snapshotIdentity, presentation: live });
115 while (next.size > MAX_THREAD_PR_SNAPSHOTS) {
116 const oldestKey = next.keys().next().value;
117 if (oldestKey === undefined) break;
118 next.delete(oldestKey);
119 }
120 return [true, next];
121 });
122 }, [live, snapshotIdentity, threadKey]);
123
124 return linkedPresentation ?? (live === undefined ? snapshot : live);
125}