Skip to content

Commit c3759fe

Browse files
chore(ai): prefetch the markdown text to get around ios security blocking async call on gesture (#14354)
1 parent 9053d96 commit c3759fe

File tree

1 file changed

+34
-7
lines changed

1 file changed

+34
-7
lines changed

src/components/copyMarkdownButton.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import {Fragment, useEffect, useRef, useState} from 'react';
3+
import {Fragment, useCallback, useEffect, useRef, useState} from 'react';
44
import {createPortal} from 'react-dom';
55
import {Clipboard} from 'react-feather';
66
import Link from 'next/link';
@@ -19,10 +19,21 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
1919
const [error, setError] = useState(false);
2020
const [isOpen, setIsOpen] = useState(false);
2121
const [isMounted, setIsMounted] = useState(false);
22+
const [prefetchedContent, setPrefetchedContent] = useState<string | null>(null);
2223
const buttonRef = useRef<HTMLDivElement>(null);
2324
const dropdownRef = useRef<HTMLDivElement>(null);
2425
const {emit} = usePlausibleEvent();
2526

27+
const fetchMarkdownContent = useCallback(async (): Promise<string> => {
28+
// PSA: It's expected that this doesn't work on local development since we need
29+
// the generated markdown files, which only are generated in the deploy pipeline.
30+
const response = await fetch(`${window.location.origin}/${pathname}.md`);
31+
if (!response.ok) {
32+
throw new Error(`Failed to fetch markdown content: ${response.status}`);
33+
}
34+
return await response.text();
35+
}, [pathname]);
36+
2637
const copyMarkdownToClipboard = async () => {
2738
setIsLoading(true);
2839
setCopied(false);
@@ -32,14 +43,14 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
3243
emit('Copy Page', {props: {page: pathname, source: 'copy_button'}});
3344

3445
try {
35-
// This doesn't work on local development since we need the generated markdown
36-
// files, and we need to be aware of the origin since we have two different origins.
37-
const response = await fetch(`${window.location.origin}/${pathname}.md`);
38-
if (!response.ok) {
39-
throw new Error(`Failed to fetch markdown content: ${response.status}`);
46+
let content: string;
47+
if (prefetchedContent) {
48+
content = prefetchedContent;
49+
} else {
50+
content = await fetchMarkdownContent();
4051
}
4152

42-
await navigator.clipboard.writeText(await response.text());
53+
await navigator.clipboard.writeText(content);
4354
setCopied(true);
4455
setTimeout(() => setCopied(false), 2000);
4556
} catch (err) {
@@ -82,6 +93,22 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
8293
};
8394
}, []);
8495

96+
// Pre-fetch markdown content to avoid losing user gesture context. On iOS we can't async
97+
// fetch on tap because the user gesture is lost by the time we try to update the clipboard.
98+
useEffect(() => {
99+
if (!prefetchedContent) {
100+
const prefetchContent = async () => {
101+
try {
102+
const content = await fetchMarkdownContent();
103+
setPrefetchedContent(content);
104+
} catch (err) {
105+
// Silently fail - we'll fall back to regular fetch on click
106+
}
107+
};
108+
prefetchContent();
109+
}
110+
}, [pathname, prefetchedContent, fetchMarkdownContent]);
111+
85112
const getDropdownPosition = () => {
86113
if (!buttonRef.current) return {top: 0, left: 0};
87114
const rect = buttonRef.current.getBoundingClientRect();

0 commit comments

Comments
 (0)