diff --git a/src/app/components/page/Page.tsx b/src/app/components/page/Page.tsx index bcad116d3..1bbdde74a 100644 --- a/src/app/components/page/Page.tsx +++ b/src/app/components/page/Page.tsx @@ -3,6 +3,7 @@ import { Box, Header, Line, Scroll, Text, as } from 'folds'; import classNames from 'classnames'; import { ContainerColor } from '$styles/ContainerColor.css'; import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; +import { mobileOrTabletLayout } from '$utils/user-agent'; import * as css from './style.css'; type PageRootProps = { @@ -16,7 +17,7 @@ export function PageRoot({ nav, children }: PageRootProps) { return ( {nav} - {screenSize !== ScreenSize.Mobile && ( + {screenSize !== ScreenSize.Mobile && !mobileOrTabletLayout() && ( )} {children} diff --git a/src/app/pages/MobileFriendly.tsx b/src/app/pages/MobileFriendly.tsx index 83009cda5..5fb203286 100644 --- a/src/app/pages/MobileFriendly.tsx +++ b/src/app/pages/MobileFriendly.tsx @@ -1,6 +1,7 @@ import type { ReactNode } from 'react'; import { useMatch } from 'react-router-dom'; import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; +import { mobileOrTabletLayout } from '$utils/user-agent'; import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from './paths'; type MobileFriendlyClientNavProps = { @@ -15,7 +16,7 @@ export function MobileFriendlyClientNav({ children }: MobileFriendlyClientNavPro const inboxMatch = useMatch({ path: INBOX_PATH, caseSensitive: true, end: true }); if ( - screenSize === ScreenSize.Mobile && + (screenSize === ScreenSize.Mobile || mobileOrTabletLayout()) && !(homeMatch || directMatch || spaceMatch || exploreMatch || inboxMatch) ) { return null; @@ -36,7 +37,7 @@ export function MobileFriendlyPageNav({ path, children }: MobileFriendlyPageNavP end: true, }); - if (screenSize === ScreenSize.Mobile && !exactPath) { + if ((screenSize === ScreenSize.Mobile || mobileOrTabletLayout()) && !exactPath) { return null; } diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index aec62cc86..fa1037b5f 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -16,6 +16,7 @@ import { Room } from '$features/room'; import { Lobby } from '$features/lobby'; import { PageRoot } from '$components/page'; import { ScreenSize } from '$hooks/useScreenSize'; +import { mobileOrTabletLayout } from '$utils/user-agent'; import { ReceiveSelfDeviceVerification } from '$components/DeviceVerification'; import { AutoRestoreBackupOnVerification } from '$components/BackupRestore'; import { RoomSettingsRenderer } from '$features/room-settings'; @@ -101,7 +102,7 @@ const getFirstSession = () => { export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => { const { hashRouter } = clientConfig; - const mobile = screenSize === ScreenSize.Mobile; + const mobile = screenSize === ScreenSize.Mobile || mobileOrTabletLayout(); const routes = createRoutesFromElements( diff --git a/src/app/pages/client/direct/Direct.tsx b/src/app/pages/client/direct/Direct.tsx index a8ba93f2b..e0fb1df81 100644 --- a/src/app/pages/client/direct/Direct.tsx +++ b/src/app/pages/client/direct/Direct.tsx @@ -54,7 +54,7 @@ import { import { useDirectCreateSelected } from '$hooks/router/useDirectSelected'; import { useDirectRooms } from './useDirectRooms'; import { SidebarResizer } from '$pages/client/sidebar/SidebarResizer'; -import { mobileOrTablet } from '$utils/user-agent'; +import { mobileOrTabletLayout } from '$utils/user-agent'; import { useScreenSizeContext, ScreenSize } from '$hooks/useScreenSize'; type DirectMenuProps = { @@ -254,7 +254,7 @@ export function Direct() { ); const screenSize = useScreenSizeContext(); - const isMobile = mobileOrTablet() || screenSize === ScreenSize.Mobile; + const isMobile = mobileOrTabletLayout() || screenSize === ScreenSize.Mobile; const hideText = curWidth <= 80 && !isMobile; return ( @@ -364,7 +364,7 @@ export function Direct() { )} - {!mobileOrTablet() && ( + {!mobileOrTabletLayout() && ( - {!mobileOrTablet() && ( + {!mobileOrTabletLayout() && ( )} - {!mobileOrTablet() && ( + {!mobileOrTabletLayout() && ( - {!mobileOrTablet() && ( + {!mobileOrTabletLayout() && ( - {!mobileOrTablet() && ( + {!mobileOrTabletLayout() && ( { const { os, device } = result; if (device.type === 'mobile' || device.type === 'tablet') return true; if (os.name === 'Android' || os.name === 'iOS') return true; + // iPad on iOS 13+ sends a macOS Safari user agent by default ("Request Desktop Website"). + // ua-parser-js therefore reports os.name === 'Mac OS' with no device.type. + // Real Macs never have maxTouchPoints > 1 (Magic Trackpad reports 1 at most in browsers), + // so this safely identifies iPads masquerading as desktop Safari. + if (os.name === 'Mac OS' && navigator.maxTouchPoints > 1) return true; return false; })(); @@ -15,11 +20,23 @@ const normalizeMacName = (os?: string) => { return os; }; +// True only for phone-form-factor devices for layout/nav decisions. +// Tablets (native iPadOS UA or "Request Desktop Website") always get the desktop +// two-panel layout; only phones collapse to the single-panel mobile layout. +const isMobileOrTabletLayout = result.device.type === 'mobile'; + const isMac = result.os.name === 'Mac OS'; export const ua = () => result; export const isMacOS = () => isMac; export const mobileOrTablet = () => isMobileOrTablet; +/** + * True only for phones. Use this for layout/nav decisions (sidebars, route registration). + * Tablets — whether using native iPadOS UA or iPad "Request Desktop Website" — return false, + * so they always get the full desktop two-panel layout. + * Use `mobileOrTablet` for touch/keyboard/scroll-lock behaviour instead. + */ +export const mobileOrTabletLayout = () => isMobileOrTabletLayout; export const deviceDisplayName = (): string => { const browser = result.browser.name;