diff --git a/packages/pl-fe/src/components/status-action-button.tsx b/packages/pl-fe/src/components/status-action-button.tsx index dae7b897e..044d7a45b 100644 --- a/packages/pl-fe/src/components/status-action-button.tsx +++ b/packages/pl-fe/src/components/status-action-button.tsx @@ -1,10 +1,10 @@ -import { useLongPress } from '@uidotdev/usehooks'; import clsx from 'clsx'; import React from 'react'; import Emoji from 'pl-fe/components/ui/emoji'; import Icon from 'pl-fe/components/ui/icon'; import Text from 'pl-fe/components/ui/text'; +import { useLongPress } from 'pl-fe/hooks/use-long-press'; import { useSettings } from 'pl-fe/hooks/use-settings'; import AnimatedNumber from './animated-number'; @@ -43,7 +43,7 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes void; + onLongPress?: (event: React.MouseEvent | React.TouchEvent) => void; } const StatusActionButton = React.forwardRef((props, ref): JSX.Element => { diff --git a/packages/pl-fe/src/components/status-reactions-bar.tsx b/packages/pl-fe/src/components/status-reactions-bar.tsx index a15b8bb12..95396fd2d 100644 --- a/packages/pl-fe/src/components/status-reactions-bar.tsx +++ b/packages/pl-fe/src/components/status-reactions-bar.tsx @@ -1,4 +1,3 @@ -import { useLongPress } from '@uidotdev/usehooks'; import clsx from 'clsx'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -13,6 +12,7 @@ import unicodeMapping from 'pl-fe/features/emoji/mapping'; import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; import { useFeatures } from 'pl-fe/hooks/use-features'; import { useLoggedIn } from 'pl-fe/hooks/use-logged-in'; +import { useLongPress } from 'pl-fe/hooks/use-long-press'; import { useSettings } from 'pl-fe/hooks/use-settings'; import { useModalsStore } from 'pl-fe/stores/modals'; diff --git a/packages/pl-fe/src/hooks/use-long-press.ts b/packages/pl-fe/src/hooks/use-long-press.ts new file mode 100644 index 000000000..dad95491a --- /dev/null +++ b/packages/pl-fe/src/hooks/use-long-press.ts @@ -0,0 +1,123 @@ +/* +MIT License + +Copyright (c) 2023 ui.dev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +// Adapted from https://github.com/uidotdev/usehooks/pull/321/files +import React from 'react'; + +type Event = React.MouseEvent | React.TouchEvent; + +// eslint-disable-next-line compat/compat +const isTouchEvent = ({ nativeEvent }: Event) => window.TouchEvent + ? nativeEvent instanceof TouchEvent + : 'touches' in nativeEvent; + +const isMouseEvent = (event: Event) => event.nativeEvent instanceof MouseEvent; + +type LongPressOptions = { + threshold?: number; + onStart?: (e: Event) => void; + onFinish?: (e: Event) => void; + onCancel?: (e: Event) => void; + allowScroll?: boolean; + scrollThreshold?: number; +}; + +const useLongPress = (callback: (e: Event) => void, options: LongPressOptions = {}) => { + const { threshold = 400, onStart, onFinish, onCancel, allowScroll = false, scrollThreshold = 20 } = options; + const isLongPressActive = React.useRef(false); + const isPressed = React.useRef(false); + const timerId = React.useRef(); + let startY: number; + + return React.useMemo(() => { + if (typeof callback !== 'function') { + return {}; + } + + const start = (event: Event) => { + if (!isMouseEvent(event) && !isTouchEvent(event)) return; + + if ('touches' in event.nativeEvent) { + startY = event.nativeEvent.touches[0].clientY; + } + + if (onStart) { + onStart(event); + } + + isPressed.current = true; + timerId.current = setTimeout(() => { + callback(event); + isLongPressActive.current = true; + }, threshold); + }; + + const cancel = (event: Event) => { + if (!isMouseEvent(event) && !isTouchEvent(event)) return; + + if (isLongPressActive.current) { + if (onFinish) { + onFinish(event); + } + } else if (isPressed.current) { + if (onCancel) { + onCancel(event); + } + } + + isLongPressActive.current = false; + isPressed.current = false; + + if (timerId.current) { + window.clearTimeout(timerId.current); + } + }; + + const move = (event: Event) => { + if (!allowScroll && (!('touches' in event.nativeEvent) || Math.abs(event.nativeEvent.touches[0].clientY - startY) > scrollThreshold)) { + cancel(event); + } + }; + + const mouseHandlers = { + onMouseDown: start, + onMouseUp: cancel, + onMouseLeave: cancel, + onMouseMove: move, + }; + + const touchHandlers = { + onTouchStart: start, + onTouchEnd: cancel, + onTouchMove: move, + }; + + return { + ...mouseHandlers, + ...touchHandlers, + }; + }, [callback, threshold, onCancel, onFinish, onStart]); +}; + +export { useLongPress };