import { useCallback, useRef } from 'react'; import { useMediaQuery } from './useMediaQuery'; const LONG_PRESS_MS = 450; const MOVE_TOLERANCE_PX = 10; /** * [Gitea #166] Long-press detection for touch screens. Fires `onLongPress` * with the finger's viewport position after 450 ms of holding still (< 10 px * of movement); a scroll or a lift cancels it. Android also fires * `contextmenu` for a long-press — the returned `suppressContextMenu` ref is * true briefly after we fired so the caller can drop that duplicate. Handlers * are no-ops when the device has no coarse pointer. */ export function useLongPress(onLongPress: (x: number, y: number) => void) { const coarse = useMediaQuery('(pointer: coarse)'); const timer = useRef(undefined); const start = useRef<{ x: number; y: number } | null>(null); const fired = useRef(false); // [Gitea #199] The element being pressed carries `data-pressing` (set on the // DOM directly, no re-render) so iOS can't start a text selection under // the finger; see `[data-touch-actions]` in room/message/styles.css.ts. const pressed = useRef(null); const stopTimer = useCallback(() => { if (timer.current !== undefined) window.clearTimeout(timer.current); timer.current = undefined; start.current = null; }, []); // The finger is up (or the gesture was cancelled): only now drop // `data-pressing`, so the system selection can't start under a finger that // is still down after the sheet opened. const clear = useCallback(() => { stopTimer(); pressed.current?.removeAttribute('data-pressing'); pressed.current = null; }, [stopTimer]); const onTouchStart = useCallback( (evt: React.TouchEvent) => { if (!coarse || evt.touches.length !== 1) return; const t = evt.touches[0]; start.current = { x: t.clientX, y: t.clientY }; fired.current = false; pressed.current = evt.currentTarget as HTMLElement; pressed.current.setAttribute('data-pressing', ''); timer.current = window.setTimeout(() => { const s = start.current; stopTimer(); if (!s) return; fired.current = true; window.setTimeout(() => { fired.current = false; }, 1000); onLongPress(s.x, s.y); }, LONG_PRESS_MS); }, [coarse, stopTimer, onLongPress], ); const onTouchMove = useCallback( (evt: React.TouchEvent) => { const s = start.current; if (!s) return; const t = evt.touches[0]; if ( !t || Math.abs(t.clientX - s.x) > MOVE_TOLERANCE_PX || Math.abs(t.clientY - s.y) > MOVE_TOLERANCE_PX ) { clear(); } }, [clear], ); // Lifting the finger after a long-press would synthesise mousedown/click at // that point — on whatever the sheet just put there. Swallow it. const onTouchEnd = useCallback( (evt: React.TouchEvent) => { if (fired.current && evt.cancelable) evt.preventDefault(); clear(); }, [clear], ); return { coarse, handlers: coarse ? { // [Gitea #199] Marks the element for the iOS hardening CSS. 'data-touch-actions': '', onTouchStart, onTouchMove, onTouchEnd, onTouchCancel: clear, } : {}, /** True for ~1 s after a long-press fired (drop the Android contextmenu echo). */ suppressContextMenu: fired, }; }