feat(mobile): add swipe-from-left-edge back gesture
This commit is contained in:
@@ -4,6 +4,7 @@ import { useVoiceStore } from '../../stores/voiceStore';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { MobileScreenStack } from './MobileScreenStack';
|
||||
import { MobileBottomNav } from './MobileBottomNav';
|
||||
import { useSwipeGesture } from '../../hooks/useSwipeGesture';
|
||||
|
||||
// Screen components — imported as they are created in subsequent tasks.
|
||||
// Inline placeholders are used until the real components exist.
|
||||
@@ -45,6 +46,16 @@ export function MobileShell() {
|
||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||
const location = useLocation();
|
||||
|
||||
// Edge swipe back gesture
|
||||
useSwipeGesture({
|
||||
onSwipeRight: () => {
|
||||
if (mobileStack.length > 0) {
|
||||
popMobileScreen();
|
||||
}
|
||||
},
|
||||
enabled: mobileStack.length > 0,
|
||||
});
|
||||
|
||||
// Reconstruct mobile stack from URL on mount (deep link / refresh support)
|
||||
useEffect(() => {
|
||||
const path = location.pathname;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
interface SwipeGestureOptions {
|
||||
onSwipeRight?: () => void;
|
||||
edgeThreshold?: number; // px from left edge to start detection (default: 20)
|
||||
swipeThreshold?: number; // px of horizontal movement to trigger (default: 50)
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export function useSwipeGesture({
|
||||
onSwipeRight,
|
||||
edgeThreshold = 20,
|
||||
swipeThreshold = 50,
|
||||
enabled = true,
|
||||
}: SwipeGestureOptions) {
|
||||
const touchStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const swipingRef = useRef(false);
|
||||
|
||||
const handleTouchStart = useCallback((e: TouchEvent) => {
|
||||
if (!enabled) return;
|
||||
const touch = e.touches[0];
|
||||
if (touch.clientX <= edgeThreshold) {
|
||||
touchStartRef.current = { x: touch.clientX, y: touch.clientY };
|
||||
swipingRef.current = false;
|
||||
}
|
||||
}, [enabled, edgeThreshold]);
|
||||
|
||||
const handleTouchMove = useCallback((e: TouchEvent) => {
|
||||
if (!touchStartRef.current) return;
|
||||
const touch = e.touches[0];
|
||||
const dx = touch.clientX - touchStartRef.current.x;
|
||||
const dy = touch.clientY - touchStartRef.current.y;
|
||||
|
||||
// If vertical movement exceeds horizontal, cancel swipe detection
|
||||
if (Math.abs(dy) > Math.abs(dx) && !swipingRef.current) {
|
||||
touchStartRef.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (dx > swipeThreshold) {
|
||||
swipingRef.current = true;
|
||||
e.preventDefault();
|
||||
}
|
||||
}, [swipeThreshold]);
|
||||
|
||||
const handleTouchEnd = useCallback(() => {
|
||||
if (swipingRef.current && onSwipeRight) {
|
||||
onSwipeRight();
|
||||
}
|
||||
touchStartRef.current = null;
|
||||
swipingRef.current = false;
|
||||
}, [onSwipeRight]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
document.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||
document.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
document.addEventListener('touchend', handleTouchEnd);
|
||||
document.addEventListener('touchcancel', handleTouchEnd);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('touchstart', handleTouchStart);
|
||||
document.removeEventListener('touchmove', handleTouchMove);
|
||||
document.removeEventListener('touchend', handleTouchEnd);
|
||||
document.removeEventListener('touchcancel', handleTouchEnd);
|
||||
};
|
||||
}, [enabled, handleTouchStart, handleTouchMove, handleTouchEnd]);
|
||||
}
|
||||
Reference in New Issue
Block a user