refactor: enhance mobile swipe gesture handling and improve outlet behavior on compact routes
This commit is contained in:
30
overlay/src/app/hooks/useCompactNav.ts
Normal file
30
overlay/src/app/hooks/useCompactNav.ts
Normal 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;
|
||||
};
|
||||
67
overlay/src/app/hooks/useMobileKeyboardLayout.ts
Normal file
67
overlay/src/app/hooks/useMobileKeyboardLayout.ts
Normal 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 };
|
||||
};
|
||||
57
overlay/src/app/hooks/useViewTransitions.ts
Normal file
57
overlay/src/app/hooks/useViewTransitions.ts
Normal 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]
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user