BangSo/apps/mobile/components/markdown-artifact-preview.tsx

130 lines
4.1 KiB
TypeScript
Raw Permalink Normal View History

2026-09-01 16:51:19 +00:00
import { ChatMarkdown } from "@rakazo/chat-ui/native";
import { useEffect, useState } from "react";
import { Alert, Modal, Pressable, SafeAreaView, ScrollView, Text, View } from "react-native";
import {
type MobileArtifactTarget,
openMobileArtifact,
readMobileArtifactText,
} from "../lib/artifact-open";
import { NativeSymbol } from "./native-symbol";
export type MarkdownArtifactPreviewTarget = {
artifactId: string;
name: string;
mimeType: string;
};
export function MarkdownArtifactPreview({
threadTarget,
target,
onClose,
}: {
threadTarget: MobileArtifactTarget;
target: MarkdownArtifactPreviewTarget;
onClose: () => void;
}) {
const [state, setState] = useState<
| { status: "loading" }
| { status: "ready"; markdown: string }
| { status: "error"; message: string }
>({ status: "loading" });
const targetBotId = "botId" in threadTarget ? threadTarget.botId : undefined;
const targetGroupId = "groupId" in threadTarget ? threadTarget.groupId : undefined;
useEffect(() => {
let cancelled = false;
const requestTarget: MobileArtifactTarget =
targetBotId !== undefined ? { botId: targetBotId } : { groupId: targetGroupId! };
void readMobileArtifactText(requestTarget, target.artifactId, target.mimeType)
.then((markdown) => {
if (!cancelled) setState({ status: "ready", markdown });
})
.catch((error) => {
if (cancelled) return;
setState({
status: "error",
message: error instanceof Error ? error.message : "Could not load this file.",
});
});
return () => {
cancelled = true;
};
}, [targetBotId, targetGroupId, target.artifactId, target.mimeType]);
return (
<Modal animationType="fade" presentationStyle="fullScreen" onRequestClose={onClose}>
<SafeAreaView style={{ flex: 1, backgroundColor: "#0D0D0F" }}>
<View
style={{
height: 54,
flexDirection: "row",
alignItems: "center",
borderBottomWidth: 1,
borderBottomColor: "#27272B",
paddingHorizontal: 12,
}}
>
<Text
numberOfLines={1}
style={{ flex: 1, color: "#E7E7E9", fontSize: 15, fontWeight: "500" }}
>
{target.name}
</Text>
<Pressable
accessibilityLabel={`Share ${target.name}`}
hitSlop={8}
onPress={() =>
void openMobileArtifact(
threadTarget,
target.artifactId,
target.name,
target.mimeType,
).catch((error) =>
Alert.alert(
"Could not share file",
error instanceof Error ? error.message : "Try again.",
),
)
}
style={{ padding: 10 }}
>
<NativeSymbol
ios="square.and.arrow.up"
android="share-social-outline"
size={19}
color="#A8A8AD"
/>
</Pressable>
<Pressable
accessibilityLabel="Close preview"
hitSlop={8}
onPress={onClose}
style={{ padding: 10 }}
>
<NativeSymbol ios="xmark" android="close" size={19} color="#A8A8AD" />
</Pressable>
</View>
<ScrollView contentContainerStyle={{ paddingHorizontal: 24, paddingVertical: 28 }}>
{state.status === "loading" ? (
<Text style={{ color: "#85858A", fontSize: 15 }}>Loading preview</Text>
) : state.status === "error" ? (
<View
style={{
borderRadius: 14,
borderWidth: 1,
borderColor: "#5A2A2A",
backgroundColor: "#2A1717",
padding: 14,
}}
>
<Text style={{ color: "#F1A8A8", fontSize: 15 }}>{state.message}</Text>
</View>
) : (
<ChatMarkdown>{state.markdown}</ChatMarkdown>
)}
</ScrollView>
</SafeAreaView>
</Modal>
);
}