1import { useCallback, useEffect, useMemo, useState } from "react";
2import type { NativeSyntheticEvent } from "react-native";
3import { useNavigation } from "@react-navigation/native";
4import * as Arr from "effect/Array";
5import { pipe } from "effect/Function";
6import * as Result from "effect/Result";
7
8import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
9import {
10 buildReviewCommentTarget,
11 clearReviewCommentTarget,
12 formatReviewSelectedRangeLabel,
13 getSelectedReviewCommentLines,
14 setReviewCommentTarget,
15 useReviewCommentTarget,
16} from "./reviewCommentSelection";
17import type {
18 NativeReviewDiffData,
19 NativeReviewDiffCommentTarget,
20} from "./nativeReviewDiffAdapter";
21import type { ReviewSectionItem } from "./reviewModel";
22
23interface PendingNativeCommentSelection extends NativeReviewDiffCommentTarget {
24 readonly sectionId: string;
25 readonly sectionTitle: string;
26 readonly rowId: string;
27}
28
29export function useReviewCommentSelectionController(input: {
30 readonly environmentId?: EnvironmentId;
31 readonly threadId?: ThreadId;
32 readonly selectedSection: ReviewSectionItem | null;
33 readonly nativeReviewDiffData: NativeReviewDiffData;
34}) {
35 const { environmentId, nativeReviewDiffData, selectedSection, threadId } = input;
36 const navigation = useNavigation();
37 const activeCommentTarget = useReviewCommentTarget();
38 const [pendingNativeCommentSelection, setPendingNativeCommentSelection] =
39 useState<PendingNativeCommentSelection | null>(null);
40
41 const openReviewCommentSheet = useCallback(() => {
42 if (!environmentId || !threadId) {
43 return;
44 }
45
46 navigation.navigate("ThreadReviewComment", {
47 environmentId,
48 threadId,
49 });
50 }, [environmentId, navigation, threadId]);
51
52 const selectedRowIds = useMemo(() => {
53 if (
54 activeCommentTarget &&
55 activeCommentTarget.sectionTitle === selectedSection?.title &&
56 activeCommentTarget.startIndex !== activeCommentTarget.endIndex
57 ) {
58 return pipe(
59 getSelectedReviewCommentLines(activeCommentTarget),
60 Arr.filterMap((line) => {
61 const rowId = nativeReviewDiffData.rowIdByCommentLineId.get(line.id);
62 return rowId ? Result.succeed(rowId) : Result.failVoid;
63 }),
64 );
65 }
66
67 return pendingNativeCommentSelection ? [pendingNativeCommentSelection.rowId] : [];
68 }, [
69 activeCommentTarget,
70 nativeReviewDiffData.rowIdByCommentLineId,
71 pendingNativeCommentSelection,
72 selectedSection?.title,
73 ]);
74
75 const selectionAction = useMemo(() => {
76 if (
77 activeCommentTarget &&
78 activeCommentTarget.sectionTitle === selectedSection?.title &&
79 activeCommentTarget.startIndex !== activeCommentTarget.endIndex
80 ) {
81 return {
82 title: `Comment on ${formatReviewSelectedRangeLabel(activeCommentTarget)}`,
83 onOpenComment: openReviewCommentSheet,
84 };
85 }
86
87 if (
88 pendingNativeCommentSelection &&
89 pendingNativeCommentSelection.sectionTitle === selectedSection?.title
90 ) {
91 return {
92 title: "Select range end",
93 onOpenComment: null,
94 };
95 }
96
97 return null;
98 }, [
99 activeCommentTarget,
100 openReviewCommentSheet,
101 pendingNativeCommentSelection,
102 selectedSection?.title,
103 ]);
104
105 useEffect(() => {
106 clearReviewCommentTarget();
107 setPendingNativeCommentSelection(null);
108 }, [selectedSection?.id]);
109
110 useEffect(() => {
111 if (activeCommentTarget === null) {
112 setPendingNativeCommentSelection(null);
113 }
114 }, [activeCommentTarget]);
115
116 const onPressLine = useCallback(
117 (
118 event: NativeSyntheticEvent<{
119 readonly rowId?: string;
120 readonly gesture?: "tap" | "longPress";
121 }>,
122 ) => {
123 if (!selectedSection) {
124 return;
125 }
126
127 const { rowId, gesture } = event.nativeEvent;
128 if (!rowId) {
129 return;
130 }
131
132 const target = nativeReviewDiffData.commentTargetsByRowId.get(rowId);
133 if (!target) {
134 return;
135 }
136
137 if (gesture === "longPress") {
138 clearReviewCommentTarget();
139 setPendingNativeCommentSelection({
140 ...target,
141 sectionId: selectedSection.id,
142 sectionTitle: selectedSection.title,
143 rowId,
144 });
145 return;
146 }
147
148 if (
149 pendingNativeCommentSelection &&
150 pendingNativeCommentSelection.sectionTitle === selectedSection.title &&
151 pendingNativeCommentSelection.filePath === target.filePath
152 ) {
153 setReviewCommentTarget(
154 buildReviewCommentTarget(
155 {
156 sectionTitle: pendingNativeCommentSelection.sectionTitle,
157 sectionId: pendingNativeCommentSelection.sectionId,
158 filePath: pendingNativeCommentSelection.filePath,
159 lines: pendingNativeCommentSelection.lines,
160 },
161 pendingNativeCommentSelection.lineIndex,
162 target.lineIndex,
163 ),
164 );
165 return;
166 }
167
168 setPendingNativeCommentSelection(null);
169 setReviewCommentTarget({
170 sectionTitle: selectedSection.title,
171 sectionId: selectedSection.id,
172 filePath: target.filePath,
173 lines: target.lines,
174 startIndex: target.lineIndex,
175 endIndex: target.lineIndex,
176 });
177 openReviewCommentSheet();
178 },
179 [
180 nativeReviewDiffData.commentTargetsByRowId,
181 openReviewCommentSheet,
182 pendingNativeCommentSelection,
183 selectedSection,
184 ],
185 );
186
187 const clearSelection = useCallback(() => {
188 clearReviewCommentTarget();
189 setPendingNativeCommentSelection(null);
190 }, []);
191
192 return {
193 selectedRowIds,
194 selectionAction,
195 onPressLine,
196 clearSelection,
197 };
198}