import { AsyncResult } from "effect/reactivity"; import { PullRequestCommentForm } from "./PullRequestCommentForm"; import { useAtomRefresh } from "@effect/atom-react"; import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { sourceControlClients } from "@t3tools/client-runtime/source-control-clients"; import { settlePromise, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import type { PullRequestReviewVerdict } from "@t3tools/contracts"; import { useRef, useState } from "react"; import { useFocusEffect } from "@react-navigation/native"; import { Alert, Pressable, View } from "react-native"; import { clearSubmittedReview } from "./pullRequestReviewDrafts"; import { PullRequestPendingBadge, PullRequestPendingCommentActions, } from "./PullRequestPendingComment"; import { setReviewDraftBusy, retireReviewDraftComments, usePullRequestReviewDraft, } from "./usePullRequestReviewDraft"; import { reviewPositionLabel } from "./pullRequestDiffComments"; import { AppText as Text } from "../../components/ui/AppText"; import { Button } from "../../components/ui/Button"; import { Menu } from "../../components/ui/Menu"; import { SymbolView } from "../../components/ui/AppSymbol"; import { composerPullRequests } from "../../state/pull-requests"; import { useEnvironmentQuery } from "../../state/query"; import { useAtomCommand } from "../../state/use-atom-command"; import { PullRequestSheetContent as SheetContent } from "./PullRequestSheetContent"; import { pullRequestTarget, type PullRequestParams } from "./usePullRequestDiff"; import { usePullRequestPermissions } from "./usePullRequestPermissions"; function useActionSubmission(params: PullRequestParams) { const navigation = useNavigation(); const target = pullRequestTarget(params); const refreshDetail = useAtomRefresh( composerPullRequests.detail({ ...target, input: { ...target.input, allowStale: false } }), ); const refreshActivity = useAtomRefresh(composerPullRequests.activity(pullRequestTarget(params))); const pending = useRef(false); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const submit = async (operation: () => Promise) => { if (pending.current) return; pending.current = true; setBusy(true); setError(null); const result = await settlePromise(async () => { await operation(); refreshDetail(); refreshActivity(); navigation.goBack(); }); pending.current = false; setBusy(false); if (AsyncResult.isFailure(result)) { const cause = squashAtomCommandFailure(result); setError( cause instanceof Error ? cause.message : "The provider could not complete this action. Try again.", ); } }; return { busy, error, submit }; } const VERDICTS = [ { value: "comment", title: "Comment" }, { value: "approve", title: "Approve" }, { value: "request-changes", title: "Request changes", }, ] as const; export function PullRequestReviewSheet({ route, }: StaticScreenProps) { const params = route.params; const target = pullRequestTarget(params); const result = useEnvironmentQuery( composerPullRequests.detail({ ...target, input: { ...target.input, allowStale: false } }), ); const { refresh: refreshDetail } = result; useFocusEffect(refreshDetail); const permissions = usePullRequestPermissions(params, result.data); const submission = useActionSubmission(params); const [uploading, setUploading] = useState(false); const review = useAtomCommand(composerPullRequests.submitReview, { reportFailure: false }); const comment = useAtomCommand(composerPullRequests.comment, { reportFailure: false }); const navigation = useNavigation(); const store = usePullRequestReviewDraft(params); const body = params.mode === "review" ? store.draft.summary : store.draft.comment; const setBody = (body: string) => store.update((draft) => ({ ...draft, ...(params.mode === "review" ? { summary: body } : { comment: body }), })); const stale = store.comments.some((comment) => comment.headSha !== result.data?.headSha); const [requestedVerdict, setRequestedVerdict] = useState("comment"); const offered = VERDICTS.filter((option) => permissions.verdicts.includes(option.value)); const verdict = offered.find((option) => option.value === requestedVerdict) ?? offered[0]; const reviewing = params.mode === "review"; const canSubmitComments = !store.comments.length || permissions.canInlineComment; // Forgejo requires a summary when requesting changes, even with inline comments. const summaryRequired = verdict !== undefined && result.data ? sourceControlClients.get(result.data.provider).reviewSummaryRequired(verdict.value) : false; const canSubmit = store.ready && !uploading && !store.busy && (!reviewing || (store.activityLoaded && store.activityError === null && !stale && canSubmitComments)) && (reviewing ? verdict !== undefined && (summaryRequired ? body.trim().length > 0 : verdict.value === "approve" || body.trim().length > 0 || store.comments.length > 0 || Boolean(store.pendingReviewId)) : permissions.canComment && body.trim().length > 0); const perform = () => submission.submit(async () => { if (!canSubmit || !store.isReady()) throw new Error("Add your feedback and check your permissions before submitting."); if (!setReviewDraftBusy(store.scope, true)) throw new Error("Wait for the pending comment to finish saving."); return await (async () => { const submitted = store.draft; const submittedComments = store.comments; const response = reviewing && verdict ? await review({ ...target, input: { ...target.input, verdict: verdict.value, body, ...(store.pendingReviewId ? { pendingReviewId: store.pendingReviewId } : {}), ...(store.outdatedComments.length ? { outdatedComments: store.outdatedComments } : {}), comments: submittedComments.map( ({ id: _id, headSha: _sha, code: _code, ...comment }) => comment, ), }, }) : await comment({ ...target, input: { ...target.input, body } }); if (response._tag === "Failure") throw squashAtomCommandFailure(response); if (reviewing) retireReviewDraftComments( store.scope, submittedComments, store.outdatedComments.map((comment) => comment.id), ); store.complete((current) => reviewing ? clearSubmittedReview(current, submitted) : { ...current, comment: current.comment === submitted.comment ? "" : current.comment }, ); await store.flush(); })().finally(async () => { setReviewDraftBusy(store.scope, false); }); }); const verdictMenu = reviewing && offered.length > 1 ? ( ({ id: option.value, title: option.title, state: option.value === verdict?.value ? ("on" as const) : ("off" as const), }))} onPressAction={({ nativeEvent }) => { const next = offered.find((option) => option.value === nativeEvent.event); if (next && !submission.busy) setRequestedVerdict(next.value); }} > {verdict?.title ?? "Comment"} ) : null; return ( {verdictMenu}