import type { RefObject } from 'react'; import type React from '../../../../lib/teact/teact'; import { useEffect, useRef } from '../../../../lib/teact/teact'; import { requestMeasure } from '../../../../lib/fasterdom/fasterdom'; import { getActions } from '../../../../global'; import { IS_ANDROID, IS_TOUCH_ENV } from '../../../../util/windowEnvironment'; import windowSize from '../../../../util/windowSize'; import { captureEvents, SwipeDirection } from '../../../../util/captureEvents'; import useFlag from '../../../../hooks/useFlag'; import { preventMessageInputBlur } from '../../helpers/preventMessageInputBlur'; import stopEvent from '../../../../util/stopEvent'; import { REM } from '../../../common/helpers/mediaDimensions'; import useThrottledCallback from '../../../../hooks/useThrottledCallback'; const ANDROID_KEYBOARD_HIDE_DELAY_MS = 350; const SWIPE_ANIMATION_DURATION = 150; const QUICK_REACTION_DOUBLE_TAP_DELAY = 200; const QUICK_REACTION_AREA_WIDTH = 3 * REM; const QUICK_REACTION_AREA_HEIGHT = Number(REM); const GROUP_MESSAGE_HOVER_ATTRIBUTE = 'data-is-document-group-hover'; export default function useOuterHandlers( selectMessage: (e?: React.MouseEvent, groupedId?: string) => void, containerRef: RefObject, messageId: number, isAlbum: boolean, isInSelectMode: boolean, canReply: boolean, isProtected: boolean, onContextMenu: (e: React.MouseEvent) => void, handleBeforeContextMenu: (e: React.MouseEvent) => void, chatId: string, isContextMenuShown: boolean, quickReactionRef: RefObject, isOwn: boolean, shouldHandleMouseLeave: boolean, ) { const { setReplyingToId, sendDefaultReaction } = getActions(); const [isQuickReactionVisible, markQuickReactionVisible, unmarkQuickReactionVisible] = useFlag(); const [isSwiped, markSwiped, unmarkSwiped] = useFlag(); const doubleTapTimeoutRef = useRef(); function handleMouseDown(e: React.MouseEvent) { preventMessageInputBlur(e); handleBeforeContextMenu(e); } const handleMouseMove = useThrottledCallback((e: React.MouseEvent) => { const quickReactionContainer = quickReactionRef.current; if (!quickReactionContainer) return; const { clientX, clientY } = e; const { x: quickReactionX, width: quickReactionWidth, y: quickReactionY, height: quickReactionHeight, } = quickReactionContainer.getBoundingClientRect(); const x = quickReactionX + quickReactionWidth / 2; const y = quickReactionY + quickReactionHeight / 2; const isVisibleX = Math.abs(x - clientX) < QUICK_REACTION_AREA_WIDTH; const isVisibleY = Math.abs(y - clientY) < QUICK_REACTION_AREA_HEIGHT; if (isVisibleX && isVisibleY) { markQuickReactionVisible(); } else { unmarkQuickReactionVisible(); } }, [quickReactionRef], requestMeasure); function handleSendQuickReaction(e: React.MouseEvent) { e.stopPropagation(); sendDefaultReaction({ chatId, messageId, }); } function handleTap(e: React.MouseEvent) { if (IS_ANDROID) { const target = e.target as HTMLDivElement; if (!target.classList.contains('text-content') && !target.classList.contains('Message')) { return; } if (windowSize.getIsKeyboardVisible()) { setTimeout(() => { onContextMenu(e); }, ANDROID_KEYBOARD_HIDE_DELAY_MS); } else { onContextMenu(e); } } } function handleDoubleTap() { sendDefaultReaction({ chatId, messageId, }); } function handleClick(e: React.MouseEvent) { if (isInSelectMode) { selectMessage(e); return; } if (!IS_TOUCH_ENV) return; if (doubleTapTimeoutRef.current) { clearInterval(doubleTapTimeoutRef.current); doubleTapTimeoutRef.current = undefined; handleDoubleTap(); return; } doubleTapTimeoutRef.current = setTimeout(() => { doubleTapTimeoutRef.current = undefined; handleTap(e); }, QUICK_REACTION_DOUBLE_TAP_DELAY); } function handleContextMenu(e: React.MouseEvent) { if (IS_ANDROID) { if ((e.target as HTMLElement).matches('a[href]') || isContextMenuShown) { return; } e.preventDefault(); selectMessage(); } else { onContextMenu(e); } } function handleContainerDoubleClick() { if (IS_TOUCH_ENV || !canReply) return; setReplyingToId({ messageId }); } function stopPropagation(e: React.MouseEvent) { e.stopPropagation(); } useEffect(() => { if (!IS_TOUCH_ENV || isInSelectMode || !canReply || isContextMenuShown) { return undefined; } let startedAt: number | undefined; return captureEvents(containerRef.current!, { selectorToPreventScroll: '.MessageList', excludedClosestSelector: '.no-word-wrap', onSwipe: ((e, direction) => { if (direction === SwipeDirection.Left) { if (!startedAt) { startedAt = Date.now(); } markSwiped(); return true; } return false; }), onRelease: () => { if (!startedAt || !canReply) { return; } setReplyingToId({ messageId }); setTimeout(unmarkSwiped, Math.max(0, SWIPE_ANIMATION_DURATION - (Date.now() - startedAt))); startedAt = undefined; }, }); }, [ containerRef, isInSelectMode, messageId, setReplyingToId, markSwiped, unmarkSwiped, canReply, isContextMenuShown, ]); function handleMouseLeave(e: React.MouseEvent) { // Because `mousemove` event is throttled, we need to also throttle `mouseleave` event, // so the order of events is preserved requestMeasure(unmarkQuickReactionVisible); if (shouldHandleMouseLeave) handleDocumentGroupMouseLeave(e); } return { handleMouseDown: !isInSelectMode ? handleMouseDown : undefined, handleClick, handleContextMenu: !isInSelectMode ? handleContextMenu : (isProtected ? stopEvent : undefined), handleDoubleClick: !isInSelectMode ? handleContainerDoubleClick : undefined, handleContentDoubleClick: !IS_TOUCH_ENV ? stopPropagation : undefined, handleMouseMove, handleSendQuickReaction, handleMouseLeave, isSwiped, isQuickReactionVisible, handleDocumentGroupMouseEnter, }; } function handleDocumentGroupMouseEnter(e: React.MouseEvent) { const lastGroupElement = getLastElementInDocumentGroup(e.currentTarget); if (lastGroupElement) { lastGroupElement.setAttribute(GROUP_MESSAGE_HOVER_ATTRIBUTE, ''); } } function handleDocumentGroupMouseLeave(e: React.MouseEvent) { const lastGroupElement = getLastElementInDocumentGroup(e.currentTarget); if (lastGroupElement) { lastGroupElement.removeAttribute(GROUP_MESSAGE_HOVER_ATTRIBUTE); } } function getLastElementInDocumentGroup(element: Element) { let current: Element | null = element; do { current = current.nextElementSibling; } while (current && !current.classList.contains('last-in-document-group')); return current; }