| 1 | import { useNavigation, useRoute, StackActions } from "@react-navigation/native"; |
| 2 | import { nativePullRequestDestination } from "../lib/pullRequestNavigation"; |
| 3 | import { useCallback, useRef } from "react"; |
| 4 | import { useAtomValue } from "@effect/atom-react"; |
| 5 | import { parseChangeRequestUrl, type ChangeRequestLink } from "@t3tools/shared/changeRequestUrl"; |
| 6 | import { |
| 7 | isAtomCommandInterrupted, |
| 8 | squashAtomCommandFailure, |
| 9 | } from "@t3tools/client-runtime/state/runtime"; |
| 10 | import { Alert, Platform } from "react-native"; |
| 11 | import { CommonActions } from "@react-navigation/native"; |
| 12 | import { androidWorkspaceNavigationState } from "../lib/android-workspace-navigation"; |
| 13 | import { |
| 14 | AuthPreviewOperateScope, |
| 15 | EnvironmentId, |
| 16 | FILL_PREVIEW_VIEWPORT, |
| 17 | MERGEBOT_BROWSER_THREAD_ID, |
| 18 | ThreadId, |
| 19 | mergebotPullRequestUrl, |
| 20 | parseMergebotPullRequestUrl, |
| 21 | } from "@t3tools/contracts"; |
| 22 | import { useThreadShell } from "../state/entities"; |
| 23 | import { previewEnvironment } from "../state/preview"; |
| 24 | import { environmentServerConfigsAtom } from "../state/server"; |
| 25 | import { readEnvironmentScope } from "../state/session"; |
| 26 | import { useAtomCommand } from "../state/use-atom-command"; |
| 27 | import { useAtomQueryRunner } from "../state/use-atom-query-runner"; |
| 28 | import { tryOpenExternalUrl } from "../lib/openExternalUrl"; |
| 29 | |
| 30 | /** Use the thread's environment; the shared router resolves the repository's project there. */ |
| 31 | export function useOpenPullRequest(options?: { |
| 32 | readonly replace?: boolean; |
| 33 | readonly context?: { readonly environmentId: string; readonly projectId: string }; |
| 34 | }) { |
| 35 | const navigation = useNavigation(); |
| 36 | const route = useRoute(); |
| 37 | const routeContext = route.params as |
| 38 | | { |
| 39 | environmentId?: string | string[]; |
| 40 | projectId?: string | string[]; |
| 41 | threadId?: string | string[]; |
| 42 | } |
| 43 | | undefined; |
| 44 | const first = (value: string | string[] | undefined) => { |
| 45 | const text = Array.isArray(value) ? value[0] : value; |
| 46 | return text?.trim() ? text : undefined; |
| 47 | }; |
| 48 | const environmentId = options?.context?.environmentId ?? first(routeContext?.environmentId); |
| 49 | const threadId = first(routeContext?.threadId); |
| 50 | const thread = useThreadShell( |
| 51 | environmentId && threadId |
| 52 | ? { environmentId: EnvironmentId.make(environmentId), threadId: ThreadId.make(threadId) } |
| 53 | : null, |
| 54 | ); |
| 55 | const projectId = |
| 56 | options?.context?.projectId ?? first(routeContext?.projectId) ?? thread?.projectId; |
| 57 | const replace = options?.replace; |
| 58 | const configurations = useAtomValue(environmentServerConfigsAtom); |
| 59 | const openPreview = useAtomCommand(previewEnvironment.open, { reportFailure: false }); |
| 60 | const listPreviews = useAtomQueryRunner(previewEnvironment.list, { |
| 61 | refresh: true, |
| 62 | reportFailure: false, |
| 63 | }); |
| 64 | const busy = useRef(false); |
| 65 | return useCallback( |
| 66 | async (url: string) => { |
| 67 | const id = environmentId ? EnvironmentId.make(environmentId) : null; |
| 68 | const configuration = id ? configurations.get(id) : undefined; |
| 69 | const parsed: ChangeRequestLink | null = |
| 70 | parseMergebotPullRequestUrl(configuration?.mergebot, url) ?? parseChangeRequestUrl(url); |
| 71 | const mergebotUrl = parsed |
| 72 | ? mergebotPullRequestUrl(configuration?.mergebot, { |
| 73 | host: parsed.authority ?? parsed.host, |
| 74 | repository: parsed.repository, |
| 75 | number: parsed.number, |
| 76 | }) |
| 77 | : null; |
| 78 | if (id && mergebotUrl) { |
| 79 | if (busy.current) return true; |
| 80 | if ( |
| 81 | !configuration?.environment.capabilities.serverBrowser || |
| 82 | !readEnvironmentScope(id, AuthPreviewOperateScope) |
| 83 | ) { |
| 84 | Alert.alert( |
| 85 | "Mergebot browser unavailable", |
| 86 | "Connect with browser access to an environment that can open Mergebot.", |
| 87 | ); |
| 88 | return true; |
| 89 | } |
| 90 | busy.current = true; |
| 91 | try { |
| 92 | const owner = ThreadId.make(threadId ?? MERGEBOT_BROWSER_THREAD_ID); |
| 93 | const listed = await listPreviews({ environmentId: id, input: { threadId: owner } }); |
| 94 | if (listed._tag === "Failure") { |
| 95 | if (isAtomCommandInterrupted(listed)) return true; |
| 96 | throw squashAtomCommandFailure(listed); |
| 97 | } |
| 98 | const matching = listed.value.sessions.find( |
| 99 | (session) => |
| 100 | session.runtime === "server" && |
| 101 | session.navStatus._tag !== "Idle" && |
| 102 | session.navStatus.url === mergebotUrl, |
| 103 | ); |
| 104 | const opened = matching |
| 105 | ? null |
| 106 | : await openPreview({ |
| 107 | environmentId: id, |
| 108 | input: { |
| 109 | threadId: owner, |
| 110 | url: mergebotUrl, |
| 111 | runtime: "server", |
| 112 | viewport: FILL_PREVIEW_VIEWPORT, |
| 113 | }, |
| 114 | }); |
| 115 | if (opened?._tag === "Failure") { |
| 116 | if (isAtomCommandInterrupted(opened)) return true; |
| 117 | throw squashAtomCommandFailure(opened); |
| 118 | } |
| 119 | navigation.navigate("ThreadBrowserPreview", { |
| 120 | environmentId: id, |
| 121 | threadId: owner, |
| 122 | tabId: matching?.tabId ?? (opened?._tag === "Success" ? opened.value.tabId : undefined), |
| 123 | }); |
| 124 | return true; |
| 125 | } catch (error) { |
| 126 | Alert.alert( |
| 127 | "Could not open Mergebot", |
| 128 | error instanceof Error ? error.message : "Try opening the pull request again.", |
| 129 | ); |
| 130 | return true; |
| 131 | } finally { |
| 132 | busy.current = false; |
| 133 | } |
| 134 | } |
| 135 | const params = nativePullRequestDestination( |
| 136 | url, |
| 137 | environmentId && projectId |
| 138 | ? { environmentId: String(environmentId), projectId: String(projectId) } |
| 139 | : null, |
| 140 | ); |
| 141 | if (!params) return tryOpenExternalUrl(url, "pull-request"); |
| 142 | if (Platform.OS === "android") |
| 143 | navigation.dispatch((state) => |
| 144 | CommonActions.reset( |
| 145 | androidWorkspaceNavigationState(state, { name: "PullRequest", params }), |
| 146 | ), |
| 147 | ); |
| 148 | else if (replace) navigation.dispatch(StackActions.replace("PullRequest", params)); |
| 149 | else navigation.navigate("PullRequest", params); |
| 150 | return true; |
| 151 | }, |
| 152 | [ |
| 153 | configurations, |
| 154 | environmentId, |
| 155 | listPreviews, |
| 156 | navigation, |
| 157 | openPreview, |
| 158 | projectId, |
| 159 | replace, |
| 160 | threadId, |
| 161 | ], |
| 162 | ); |
| 163 | } |