fix: chat accepts quotes now
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Distinguishes a tap from a press-and-hold on the same control.
|
||||
*
|
||||
* Built on Pointer Events so one implementation covers touch, mouse and pen.
|
||||
* Two details make it behave:
|
||||
*
|
||||
* - A hold that fires must swallow the click that follows it, or the tap
|
||||
* action runs straight after the hold action.
|
||||
* - Small finger movement during a hold is normal, so the press only
|
||||
* cancels once it drifts past a slop threshold — otherwise holds fail
|
||||
* constantly on touch.
|
||||
*
|
||||
* Timing and slop match the chat bubbles' gesture handler so a hold feels the
|
||||
* same everywhere in the app.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
const LONG_PRESS_MS = 450;
|
||||
const LONG_PRESS_SLOP_PX = 8;
|
||||
|
||||
export function useLongPress({
|
||||
onClick,
|
||||
onLongPress,
|
||||
disabled = false,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
onLongPress: () => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const start = useRef<{ x: number; y: number } | null>(null);
|
||||
const fired = useRef(false);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
if (timer.current) clearTimeout(timer.current);
|
||||
timer.current = null;
|
||||
start.current = null;
|
||||
}, []);
|
||||
|
||||
// A hold can end with the pointer outside the button, or with the component
|
||||
// unmounting behind whatever the hold opened; either would leak the timer.
|
||||
useEffect(() => clear, [clear]);
|
||||
|
||||
const onPointerDown = (e: React.PointerEvent) => {
|
||||
if (disabled) return;
|
||||
fired.current = false;
|
||||
start.current = { x: e.clientX, y: e.clientY };
|
||||
timer.current = setTimeout(() => {
|
||||
fired.current = true;
|
||||
clear();
|
||||
onLongPress();
|
||||
}, LONG_PRESS_MS);
|
||||
};
|
||||
|
||||
const onPointerMove = (e: React.PointerEvent) => {
|
||||
if (!start.current) return;
|
||||
const dx = e.clientX - start.current.x;
|
||||
const dy = e.clientY - start.current.y;
|
||||
if (Math.abs(dx) > LONG_PRESS_SLOP_PX || Math.abs(dy) > LONG_PRESS_SLOP_PX) {
|
||||
clear();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp: clear,
|
||||
onPointerCancel: clear,
|
||||
onPointerLeave: clear,
|
||||
// Suppress iOS's press-and-hold callout, which would otherwise cover the
|
||||
// sheet the hold just opened.
|
||||
onContextMenu: (e: React.MouseEvent) => {
|
||||
if (fired.current) e.preventDefault();
|
||||
},
|
||||
onClick: () => {
|
||||
// The hold already acted; the browser's trailing click is not a tap.
|
||||
if (fired.current) {
|
||||
fired.current = false;
|
||||
return;
|
||||
}
|
||||
if (!disabled) onClick();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user