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 { useLocation } from 'react-router-dom';
|
||||||
import { MobileScreenStack } from './MobileScreenStack';
|
import { MobileScreenStack } from './MobileScreenStack';
|
||||||
import { MobileBottomNav } from './MobileBottomNav';
|
import { MobileBottomNav } from './MobileBottomNav';
|
||||||
|
import { useSwipeGesture } from '../../hooks/useSwipeGesture';
|
||||||
|
|
||||||
// Screen components — imported as they are created in subsequent tasks.
|
// Screen components — imported as they are created in subsequent tasks.
|
||||||
// Inline placeholders are used until the real components exist.
|
// Inline placeholders are used until the real components exist.
|
||||||
@@ -45,6 +46,16 @@ export function MobileShell() {
|
|||||||
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
const currentVoiceChannelId = useVoiceStore((s) => s.currentVoiceChannelId);
|
||||||
const location = useLocation();
|
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)
|
// Reconstruct mobile stack from URL on mount (deep link / refresh support)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const path = location.pathname;
|
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