feat(mobile): add swipe-from-left-edge back gesture

This commit is contained in:
Jannis Braun
2026-03-17 15:37:27 +01:00
parent e45bf1927e
commit 0222946f04
2 changed files with 79 additions and 0 deletions
@@ -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;
+68
View File
@@ -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]);
}