1import { SymbolView } from "../components/AppSymbol";
2import { memo, useEffect, useRef, useState } from "react";
3import { Alert, Pressable, type ColorValue } from "react-native";
4
5import { tryCopyTextWithHaptic } from "../lib/copyTextWithHaptic";
6
7const COPY_FEEDBACK_DURATION_MS = 1200;
8
9export const CopyTextButton = memo(function CopyTextButton(props: {
10 readonly accessibilityLabel: string;
11 readonly text: string;
12 readonly onCopy?: () => Promise<void>;
13 /** Lets a long-press menu wrap the button (`ControlPillMenu` injects it on Android). */
14 readonly onLongPress?: () => void;
15 readonly tintColor?: ColorValue;
16 readonly copiedTintColor?: ColorValue;
17 readonly backgroundColor?: ColorValue;
18 readonly borderColor?: ColorValue;
19 readonly iconSize?: number;
20 readonly buttonSize?: number;
21}) {
22 const [copied, setCopied] = useState(false);
23 const resetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
24
25 useEffect(
26 () => () => {
27 if (resetTimeoutRef.current) {
28 clearTimeout(resetTimeoutRef.current);
29 }
30 },
31 [],
32 );
33
34 return (
35 <Pressable
36 accessibilityRole="button"
37 accessibilityLabel={copied ? "Copied" : props.accessibilityLabel}
38 disabled={props.text.length === 0}
39 hitSlop={8}
40 onLongPress={props.onLongPress}
41 onPress={async () => {
42 try {
43 if (props.onCopy) await props.onCopy();
44 else if (!(await tryCopyTextWithHaptic(props.text))) {
45 // A refused clipboard write is the common failure, and silence reads as success.
46 Alert.alert("Could not copy", "Try again.");
47 return;
48 }
49 } catch {
50 Alert.alert("Could not copy", "Try again.");
51 return;
52 }
53 setCopied(true);
54 if (resetTimeoutRef.current) {
55 clearTimeout(resetTimeoutRef.current);
56 }
57 resetTimeoutRef.current = setTimeout(() => {
58 setCopied(false);
59 resetTimeoutRef.current = null;
60 }, COPY_FEEDBACK_DURATION_MS);
61 }}
62 style={({ pressed }) => ({
63 width: props.buttonSize ?? 30,
64 height: props.buttonSize ?? 30,
65 alignItems: "center",
66 justifyContent: "center",
67 borderRadius: 9,
68 borderWidth: props.borderColor ? 1 : 0,
69 borderColor: props.borderColor,
70 backgroundColor: props.backgroundColor,
71 opacity: pressed ? 0.52 : 1,
72 })}
73 >
74 <SymbolView
75 name={
76 copied
77 ? { ios: "checkmark", android: "check" }
78 : { ios: "doc.on.doc", android: "content_copy" }
79 }
80 size={props.iconSize ?? 13}
81 tintColor={copied ? (props.copiedTintColor ?? props.tintColor) : props.tintColor}
82 tintColorClassName={props.tintColor ? undefined : "accent-foreground"}
83 type="monochrome"
84 />
85 </Pressable>
86 );
87});