1
1
'use client' ;
2
2
3
- import { Fragment , useEffect , useRef , useState } from 'react' ;
3
+ import { Fragment , useCallback , useEffect , useRef , useState } from 'react' ;
4
4
import { createPortal } from 'react-dom' ;
5
5
import { Clipboard } from 'react-feather' ;
6
6
import Link from 'next/link' ;
@@ -19,10 +19,21 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
19
19
const [ error , setError ] = useState ( false ) ;
20
20
const [ isOpen , setIsOpen ] = useState ( false ) ;
21
21
const [ isMounted , setIsMounted ] = useState ( false ) ;
22
+ const [ prefetchedContent , setPrefetchedContent ] = useState < string | null > ( null ) ;
22
23
const buttonRef = useRef < HTMLDivElement > ( null ) ;
23
24
const dropdownRef = useRef < HTMLDivElement > ( null ) ;
24
25
const { emit} = usePlausibleEvent ( ) ;
25
26
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
+
26
37
const copyMarkdownToClipboard = async ( ) => {
27
38
setIsLoading ( true ) ;
28
39
setCopied ( false ) ;
@@ -32,14 +43,14 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
32
43
emit ( 'Copy Page' , { props : { page : pathname , source : 'copy_button' } } ) ;
33
44
34
45
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 ( ) ;
40
51
}
41
52
42
- await navigator . clipboard . writeText ( await response . text ( ) ) ;
53
+ await navigator . clipboard . writeText ( content ) ;
43
54
setCopied ( true ) ;
44
55
setTimeout ( ( ) => setCopied ( false ) , 2000 ) ;
45
56
} catch ( err ) {
@@ -82,6 +93,22 @@ export function CopyMarkdownButton({pathname}: CopyMarkdownButtonProps) {
82
93
} ;
83
94
} , [ ] ) ;
84
95
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
+
85
112
const getDropdownPosition = ( ) => {
86
113
if ( ! buttonRef . current ) return { top : 0 , left : 0 } ;
87
114
const rect = buttonRef . current . getBoundingClientRect ( ) ;
0 commit comments