1import { splitPullRequestBody } from "@t3tools/client-runtime/pull-request-markdown";
2import type { ReactNode } from "react";
3import { View } from "react-native";
4import { useProject } from "../../state/entities";
5import { pullRequestTarget, type PullRequestParams } from "./usePullRequestDiff";
6import {
7 PullRequestMedia,
8 PullRequestMediaContext,
9 usePullRequestMediaUrl,
10} from "./PullRequestMedia";
11import { videoMimeType } from "@t3tools/shared/video";
12import { createContext, useContext, useCallback, useMemo, useState } from "react";
13import { githubMarkdownLinks } from "@t3tools/shared/githubMarkdownLinks";
14
15import { useOpenPullRequest } from "../../native/useOpenPullRequest";
16import { SelectableMarkdownText } from "../../native/SelectableMarkdownText";
17import { useMarkdownPreviewStyles } from "../files/FileMarkdownPreview";
18import { markdownImageSources } from "../../lib/markdownImageSources";
19import { FilePreviewGallery, useFilePreviewGallery } from "../../components/FilePreviewGallery";
20import { pullRequestImageSource } from "./pullRequestMediaSource";
21import {
22 FilePreviewModal,
23 type FilePreviewSource,
24 type FilePreviewItem,
25} from "../../components/FilePreviewModal";
26import { ThreadMarkdownImageView } from "../threads/ThreadMarkdownImage";
27import type { MarkdownImageRenderer } from "../../native/SelectableMarkdownText";
28
29const RepositoryContext = createContext<string | null>(null);
30
31export function PullRequestMarkdownRepository(props: {
32 readonly value: string | null;
33 readonly params: PullRequestParams;
34 readonly children: ReactNode;
35}) {
36 const target = pullRequestTarget(props.params);
37 const project = useProject({
38 environmentId: target.environmentId,
39 projectId: target.input.projectId,
40 });
41 const cwd = project?.workspaceRoot;
42 const media = useMemo(
43 () => (cwd ? { environmentId: target.environmentId, cwd } : null),
44 [target.environmentId, cwd],
45 );
46 return (
47 <RepositoryContext value={props.value}>
48 <PullRequestMediaContext value={media}>{props.children}</PullRequestMediaContext>
49 </RepositoryContext>
50 );
51}
52
53function MarkdownImage(props: {
54 readonly href: string;
55 readonly alt: string | null;
56 readonly onPreview: (source: FilePreviewSource) => void;
57}) {
58 const media = usePullRequestMediaUrl(props.href);
59 const context = useContext(PullRequestMediaContext);
60 const withGallery = useFilePreviewGallery();
61 return (
62 <ThreadMarkdownImageView
63 uri={media.uri}
64 sourceKey={props.href}
65 unavailable={media.unavailable || !/^https?:\/\//i.test(props.href)}
66 alt={props.alt}
67 onPressPreview={(source) =>
68 props.onPreview(
69 withGallery({
70 ...pullRequestImageSource(props.href, context),
71 name: source.name,
72 sourceIdentifier: source.sourceIdentifier,
73 }),
74 )
75 }
76 />
77 );
78}
79
80export function PullRequestMarkdown(props: {
81 readonly markdown: string;
82 readonly imageGroupMarkdown?: string;
83}) {
84 const segments = useMemo(() => splitPullRequestBody(props.markdown), [props.markdown]);
85 return (
86 <View style={{ gap: 12 }}>
87 {segments.map((segment) =>
88 segment.kind === "markdown" ? (
89 <PullRequestMarkdownContent
90 key={segment.id}
91 markdown={segment.text}
92 imageGroupMarkdown={props.imageGroupMarkdown ?? props.markdown}
93 />
94 ) : (
95 <PullRequestMedia key={segment.id} url={segment.url} kind={segment.media} />
96 ),
97 )}
98 </View>
99 );
100}
101
102function PullRequestMarkdownContent(props: {
103 readonly markdown: string;
104 readonly imageGroupMarkdown?: string;
105}) {
106 const repositoryUrl = useContext(RepositoryContext);
107 const mediaContext = useContext(PullRequestMediaContext);
108 const markdown = useMemo(
109 () => githubMarkdownLinks(props.markdown, repositoryUrl),
110 [props.markdown, repositoryUrl],
111 );
112 const openPullRequest = useOpenPullRequest();
113 const openLink = useCallback(
114 (href: string) => {
115 void openPullRequest(href);
116 },
117 [openPullRequest],
118 );
119 const gallery = useMemo<FilePreviewItem[]>(
120 () =>
121 markdownImageSources(props.imageGroupMarkdown ?? props.markdown)
122 .filter(
123 (uri) =>
124 /^https?:\/\//i.test(uri) &&
125 videoMimeType({ name: uri.split(/[?#]/)[0] ?? uri, mimeType: "" }) === null,
126 )
127 .map((uri) => pullRequestImageSource(uri, mediaContext)),
128 [props.markdown, props.imageGroupMarkdown, mediaContext],
129 );
130 const [preview, setPreview] = useState<FilePreviewSource | null>(null);
131 const renderImage = useCallback<MarkdownImageRenderer>(
132 (image) => <MarkdownImage href={image.href} alt={image.alt} onPreview={setPreview} />,
133 [],
134 );
135 const styles = useMarkdownPreviewStyles();
136 return (
137 <FilePreviewGallery value={gallery}>
138 <SelectableMarkdownText
139 markdown={markdown}
140 textStyle={styles.nativeTextStyle}
141 onLinkPress={openLink}
142 renderImage={renderImage}
143 onImagePress={(href) => setPreview(pullRequestImageSource(href, mediaContext))}
144 />
145 <FilePreviewModal source={preview} onRequestClose={() => setPreview(null)} />
146 </FilePreviewGallery>
147 );
148}