1import { useFocusEffect, useNavigation, type StaticScreenProps } from "@react-navigation/native";
2import { useCallback, useRef, useState } from "react";
3import { Alert, View } from "react-native";
4import type { PullRequestReviewPosition } from "@t3tools/contracts";
5import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText";
6import { MaterialButton } from "../../components/MaterialButton";
7import { uuidv4 } from "../../lib/uuid";
8import { composerPullRequests } from "../../state/pull-requests";
9import { useEnvironmentQuery } from "../../state/query";
10import { PullRequestSheetContent } from "./PullRequestSheetContent";
11import { usePullRequestReviewDraft } from "./usePullRequestReviewDraft";
12import { reviewPositionLabel } from "./pullRequestDiffComments";
13import { pullRequestTarget, type PullRequestParams } from "./usePullRequestDiff";
14import { usePullRequestPermissions } from "./usePullRequestPermissions";
15
16export type InlineCommentParams = PullRequestParams & {
17 readonly draftId?: string;
18 readonly path: string;
19 readonly oldPath?: string;
20 readonly position: PullRequestReviewPosition;
21 readonly headSha: string;
22 readonly code: string;
23};
24export function PullRequestInlineCommentScreen({ route }: StaticScreenProps<InlineCommentParams>) {
25 const params = route.params;
26 const navigation = useNavigation();
27 const store = usePullRequestReviewDraft(params);
28 const saved = store.draft.comments.find((draft) => draft.id === params.draftId);
29 const [editedBody, setBody] = useState<string | null>(null);
30 const body = editedBody ?? saved?.body ?? "";
31 const detail = useEnvironmentQuery(composerPullRequests.detail(pullRequestTarget(params)));
32 const { refresh: refreshDetail } = detail;
33 useFocusEffect(useCallback(() => refreshDetail(), [refreshDetail]));
34 const permissions = usePullRequestPermissions(params, detail.data);
35 const stale = detail.data && detail.data.headSha !== params.headSha;
36 const pending = useRef(false);
37 const [busy, setBusy] = useState(false);
38 const save = async () => {
39 if (
40 pending.current ||
41 !store.ready ||
42 !body.trim() ||
43 detail.isPending ||
44 detail.error ||
45 stale ||
46 !permissions.canInlineComment
47 )
48 return;
49 pending.current = true;
50 setBusy(true);
51 store.update((draft) => ({
52 ...draft,
53 comments: [
54 ...draft.comments.filter((comment) => comment.id !== params.draftId),
55 {
56 id: params.draftId ?? uuidv4(),
57 path: params.path,
58 ...(params.oldPath ? { oldPath: params.oldPath } : {}),
59 position: params.position,
60 headSha: params.headSha,
61 code: params.code,
62 body,
63 },
64 ],
65 }));
66 try {
67 await store.flush();
68 navigation.goBack();
69 } finally {
70 pending.current = false;
71 setBusy(false);
72 }
73 };
74 return (
75 <PullRequestSheetContent
76 busy={busy}
77 title={saved ? "Edit review comment" : "Review comment"}
78 subtitle={`#${params.number} · ${reviewPositionLabel(params.position)}`}
79 footer={
80 <MaterialButton
81 fullWidth
82 tone="contrast"
83 label={busy ? "Saving…" : "Save draft"}
84 loading={busy}
85 disabled={
86 !store.ready ||
87 !body.trim() ||
88 detail.isPending ||
89 detail.error !== null ||
90 Boolean(stale) ||
91 !permissions.canInlineComment
92 }
93 onPress={() => void save()}
94 />
95 }
96 >
97 <View className="gap-2 rounded-[20px] bg-grouped-card p-4">
98 <Text className="font-mono text-xs text-foreground-muted">{params.path}</Text>
99 <Text selectable className="font-mono text-xs">
100 {params.code}
101 </Text>
102 </View>
103 <TextInput
104 multiline
105 editable={store.ready && !busy}
106 autoFocus
107 accessibilityLabel="Inline review comment"
108 placeholder="Write feedback on this line…"
109 placeholderTextColorClassName="accent-foreground-muted"
110 maxLength={65536}
111 value={body}
112 onChangeText={setBody}
113 className="min-h-32 rounded-[20px] bg-grouped-card p-4 text-base text-foreground"
114 style={{ textAlignVertical: "top" }}
115 />
116 <Text className="text-xs text-foreground-muted">
117 Saved comments are private until you submit your review.
118 </Text>
119 {stale ? (
120 <Text className="text-sm text-danger-foreground">
121 The branch changed. Return to the current diff and select the line again.
122 </Text>
123 ) : null}
124 {store.error || detail.error ? (
125 <Text className="text-sm text-danger-foreground">{store.error ?? detail.error}</Text>
126 ) : null}
127 {saved ? (
128 <MaterialButton
129 label="Delete draft"
130 onPress={() =>
131 Alert.alert("Delete this draft?", "Your other review comments will be kept.", [
132 { text: "Cancel", style: "cancel" },
133 {
134 text: "Delete",
135 style: "destructive",
136 onPress: () => {
137 store.update((draft) => ({
138 ...draft,
139 comments: draft.comments.filter((comment) => comment.id !== saved.id),
140 }));
141 navigation.goBack();
142 },
143 },
144 ])
145 }
146 />
147 ) : null}
148 </PullRequestSheetContent>
149 );
150}