refactor: enhance mobile swipe gesture handling and improve outlet behavior on compact routes
Some checks failed
Build / increment-version (push) Successful in 6s
Build / build-android (push) Failing after 1m32s
Build / create-release (push) Has been skipped

This commit is contained in:
2026-07-11 20:36:10 +10:00
parent f709998c50
commit c7b6e8f5f0
10 changed files with 298 additions and 7 deletions

View File

@@ -0,0 +1,30 @@
import { matchPath, useLocation } from 'react-router-dom';
import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from '../pages/paths';
import { ScreenSize, useScreenSizeContext } from './useScreenSize';
/** Skinny window layout (≤750px): master-detail instead of 3-column desktop. */
export const useCompactNav = (): boolean => {
const screenSize = useScreenSizeContext();
return screenSize === ScreenSize.Mobile;
};
const LIST_ROUTE_PATHS = [HOME_PATH, DIRECT_PATH, SPACE_PATH, EXPLORE_PATH, INBOX_PATH] as const;
/**
* Routes where sidebar + channel list are shown together (no room/content pane).
* Uses matchPath on the current pathname so trailing-slash / encoding quirks
* after DM → space navigations still count as master list routes.
*/
export const useIsCompactListRoute = (): boolean => {
const { pathname } = useLocation();
return LIST_ROUTE_PATHS.some(
(path) => matchPath({ path, caseSensitive: true, end: true }, pathname) != null
);
};
export const useShowCompactMasterView = (): boolean => {
const compact = useCompactNav();
const isListRoute = useIsCompactListRoute();
return compact && isListRoute;
};

View File

@@ -0,0 +1,67 @@
import { useEffect, useState } from 'react';
const KEYBOARD_HEIGHT_THRESHOLD_PX = 120;
const getIsLandscape = (): boolean => {
if (typeof window === 'undefined') return false;
if (window.matchMedia?.('(orientation: landscape)').matches) return true;
return window.innerWidth > window.innerHeight;
};
const getKeyboardOpen = (): boolean => {
if (typeof window === 'undefined') return false;
const viewport = window.visualViewport;
if (viewport) {
const obscured = window.innerHeight - viewport.height - viewport.offsetTop;
if (obscured > KEYBOARD_HEIGHT_THRESHOLD_PX) return true;
}
const active = document.activeElement;
if (!(active instanceof HTMLElement)) return false;
if (active.isContentEditable) return true;
const tag = active.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA';
};
export type MobileKeyboardLayout = {
isLandscape: boolean;
keyboardOpen: boolean;
};
/**
* Landscape + soft-keyboard layout signals for compact room chrome.
* Uses visualViewport when available (Capacitor/Android WebView).
*/
export const useMobileKeyboardLayout = (): MobileKeyboardLayout => {
const [isLandscape, setIsLandscape] = useState(getIsLandscape);
const [keyboardOpen, setKeyboardOpen] = useState(getKeyboardOpen);
useEffect(() => {
const sync = () => {
setIsLandscape(getIsLandscape());
setKeyboardOpen(getKeyboardOpen());
};
sync();
const orientationQuery = window.matchMedia?.('(orientation: landscape)');
orientationQuery?.addEventListener?.('change', sync);
window.addEventListener('resize', sync);
window.visualViewport?.addEventListener('resize', sync);
window.visualViewport?.addEventListener('scroll', sync);
window.addEventListener('focusin', sync);
window.addEventListener('focusout', sync);
return () => {
orientationQuery?.removeEventListener?.('change', sync);
window.removeEventListener('resize', sync);
window.visualViewport?.removeEventListener('resize', sync);
window.visualViewport?.removeEventListener('scroll', sync);
window.removeEventListener('focusin', sync);
window.removeEventListener('focusout', sync);
};
}, []);
return { isLandscape, keyboardOpen };
};

View File

@@ -0,0 +1,57 @@
import { useCallback, useEffect } from 'react';
import { useLocation, useNavigate, NavigateOptions } from 'react-router-dom';
import { supportsViewTransitions, withViewTransition } from '../utils/viewTransitions';
export const useViewTransitions = () => {
const location = useLocation();
useEffect(() => {
if (!supportsViewTransitions()) {
return;
}
document.documentElement.classList.add('view-transitions-enabled');
return () => {
document.documentElement.classList.remove('view-transitions-enabled');
};
}, []);
useEffect(() => {
// Route changed
}, [location]);
return {
supportsViewTransitions: supportsViewTransitions(),
};
};
/**
* Navigate helper. On Capacitor / coarse-pointer, View Transitions are disabled
* so DM ↔ space switches cannot leave a stuck hit-blocking overlay.
*/
export const useNavigateWithTransition = () => {
const navigate = useNavigate();
return useCallback(
(to: string | number, options?: NavigateOptions) => {
if (!supportsViewTransitions()) {
if (typeof to === 'number') {
navigate(to);
} else {
navigate(to, options);
}
return;
}
withViewTransition(() => {
if (typeof to === 'number') {
navigate(to);
} else {
navigate(to, options);
}
});
},
[navigate]
);
};