diff --git a/src/app/profile/[user]/post/[post]/tree.tsx b/src/app/profile/[user]/post/[post]/tree.tsx index b35a2fc..ec70967 100644 --- a/src/app/profile/[user]/post/[post]/tree.tsx +++ b/src/app/profile/[user]/post/[post]/tree.tsx @@ -22,6 +22,43 @@ const TreeVisualizationInner = memo(function TreeVisualization({ onSetSelected: (node: LayoutNode) => void onExpandNode: (node: LayoutNode) => void }) { + const touchTimeout = useRef(null) + const lastTouchTime = useRef(0) + + const handleTouchStart = useCallback( + (node: LayoutNode, event: React.TouchEvent) => { + event.preventDefault() + const now = Date.now() + const timeSinceLastTouch = now - lastTouchTime.current + + if (timeSinceLastTouch < 300) { + // Double tap detected + if (touchTimeout.current) { + clearTimeout(touchTimeout.current) + touchTimeout.current = null + } + onExpandNode(node) + } else { + // Single tap - wait to see if it's a double tap + touchTimeout.current = setTimeout(() => { + onSetSelected(node) + touchTimeout.current = null + }, 300) + } + + lastTouchTime.current = now + }, + [onSetSelected, onExpandNode], + ) + + useEffect(() => { + return () => { + if (touchTimeout.current) { + clearTimeout(touchTimeout.current) + } + } + }, []) + return ( <> @@ -50,6 +87,7 @@ const TreeVisualizationInner = memo(function TreeVisualization({ className="transition-transform duration-300 ease-out" onMouseOver={() => onSetSelected(node)} onDoubleClick={() => onExpandNode(node)} + onTouchStart={(e) => handleTouchStart(node, e)} > + {zoomState.transform && ( ({ position: null, @@ -15,6 +21,11 @@ export function useZoomState() { }) const animationFrame = useRef(null) const [transform, setTransform] = useState('') + const touchState = useRef({ + touchCount: 0, + lastTouchDistance: null, + lastTouchCenter: null, + }) useEffect(() => { return () => { @@ -123,10 +134,91 @@ export function useZoomState() { [updateTransform], ) + const handleTouchStart = useCallback((event: React.TouchEvent) => { + event.preventDefault() + touchState.current.touchCount = event.touches.length + + if (event.touches.length === 2) { + const touch1 = event.touches[0] + const touch2 = event.touches[1] + touchState.current.lastTouchDistance = Math.hypot( + touch2.clientX - touch1.clientX, + touch2.clientY - touch1.clientY, + ) + touchState.current.lastTouchCenter = { + x: (touch1.clientX + touch2.clientX) / 2, + y: (touch1.clientY + touch2.clientY) / 2, + } + } + }, []) + + const handleTouchMove = useCallback( + (event: React.TouchEvent) => { + event.preventDefault() + if (!innerState.current.position) return + + if (event.touches.length === 1) { + // Single touch - pan + const touch = event.touches[0] + const prevTouch = event.changedTouches[0] + const movementX = touch.clientX - prevTouch.clientX + const movementY = touch.clientY - prevTouch.clientY + + innerState.current.position.offset.x += movementX + innerState.current.position.offset.y += movementY + } else if (event.touches.length === 2) { + // Pinch to zoom + const touch1 = event.touches[0] + const touch2 = event.touches[1] + const currentDistance = Math.hypot( + touch2.clientX - touch1.clientX, + touch2.clientY - touch1.clientY, + ) + const currentCenter = { + x: (touch1.clientX + touch2.clientX) / 2, + y: (touch1.clientY + touch2.clientY) / 2, + } + + if (touchState.current.lastTouchDistance && touchState.current.lastTouchCenter) { + const scale = currentDistance / touchState.current.lastTouchDistance + const oldScale = innerState.current.position.scale + innerState.current.position.scale *= scale + + // Adjust offset to zoom around the center point of the pinch + const scaleDelta = innerState.current.position.scale / oldScale + innerState.current.position.offset.x -= + (currentCenter.x - innerState.current.position.offset.x) * (scaleDelta - 1) + innerState.current.position.offset.y -= + (currentCenter.y - innerState.current.position.offset.y) * (scaleDelta - 1) + } + + touchState.current.lastTouchDistance = currentDistance + touchState.current.lastTouchCenter = currentCenter + } + + if (!animationFrame.current) { + animationFrame.current = requestAnimationFrame(updateTransform) + } + }, + [updateTransform], + ) + + const handleTouchEnd = useCallback((event: React.TouchEvent) => { + event.preventDefault() + touchState.current.touchCount = event.touches.length + if (event.touches.length < 2) { + touchState.current.lastTouchDistance = null + touchState.current.lastTouchCenter = null + } + }, []) + return { transform, handleWheel, handleMouseMove, + handleTouchStart, + handleTouchMove, + handleTouchEnd, setBounds, setTarget, }