From a8e0effb325d07dbe02a998b6ff6fce623b3fec1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 30 Oct 2026 13:42:38 +0700 Subject: [PATCH] Follow system rotation on mobile devices (#8256) The app no longer locks to portrait. Phones and tablets on Android and iOS follow the device rotation and respect the system rotation lock, so Android tablets fill the display in landscape instead of letterboxing. Modals pass all orientations explicitly because React Native presents iPhone modals in portrait by default. Closes #1669 Co-authored-by: yz <43207690+yzim@users.noreply.github.com> Co-authored-by: magiclaw <930198090@qq.com> --- packages/app/app.config.js | 2 +- packages/app/app.config.test.ts | 35 +++++++++++++++++++ .../app/src/command-center/command-center.tsx | 2 +- .../app/src/components/add-project-flow.tsx | 10 ++---- packages/app/src/components/agent-list.tsx | 2 +- .../src/components/attachment-lightbox.tsx | 3 +- .../fence/mermaid/fullscreen-viewer.web.tsx | 3 +- .../markdown/fence/mermaid/host.native.tsx | 2 +- packages/app/src/components/ui/combobox.tsx | 2 +- .../src/components/ui/menu/menu-overlay.tsx | 2 +- packages/app/src/components/ui/modal.tsx | 13 +++++++ packages/app/src/components/ui/tooltip.tsx | 2 +- packages/app/src/hosts/host-chooser.tsx | 10 ++---- 13 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 packages/app/src/components/ui/modal.tsx diff --git a/packages/app/app.config.js b/packages/app/app.config.js index 7ddfc95029..eb7907e5ce 100644 --- a/packages/app/app.config.js +++ b/packages/app/app.config.js @@ -102,7 +102,7 @@ export default { name: variant.name, slug: "voice-mobile", version: nativeReleaseVersion.appVersion, - orientation: "portrait", + orientation: "default", icon: "./assets/images/icon.png", scheme: "paseo", userInterfaceStyle: "automatic", diff --git a/packages/app/app.config.test.ts b/packages/app/app.config.test.ts index b98258ff9a..d0de16bd03 100644 --- a/packages/app/app.config.test.ts +++ b/packages/app/app.config.test.ts @@ -59,3 +59,38 @@ describe("Android app config", () => { 30_000, ); }); + +it("follows system rotation on Android", async () => { + const manifest = await resolveAndroidManifest(); + const activity = manifest.application[0].activity.find( + (entry: ManifestEntry) => entry.$["android:name"] === ".MainActivity", + ); + expect(activity.$["android:screenOrientation"]).toBe("unspecified"); +}, 30_000); + +it("allows all orientations on iPhone and iPad", async () => { + const { mkdtemp, rm } = await import("node:fs/promises"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + const root = await mkdtemp(join(tmpdir(), "paseo-ios-config-")); + try { + const { exp } = await getPrebuildConfigAsync(__dirname, { platforms: ["ios"] }); + await compileModsAsync(exp, { + projectRoot: root, + introspect: true, + platforms: ["ios"], + assertMissingModProviders: false, + }); + const plist = exp._internal.modResults.ios.infoPlist; + const orientations = [ + "UIInterfaceOrientationPortrait", + "UIInterfaceOrientationPortraitUpsideDown", + "UIInterfaceOrientationLandscapeLeft", + "UIInterfaceOrientationLandscapeRight", + ]; + expect(plist.UISupportedInterfaceOrientations).toEqual(orientations); + expect(plist["UISupportedInterfaceOrientations~ipad"]).toEqual(orientations); + } finally { + await rm(root, { recursive: true, force: true }); + } +}, 30_000); diff --git a/packages/app/src/command-center/command-center.tsx b/packages/app/src/command-center/command-center.tsx index 7a44f8ccd9..309564bcea 100644 --- a/packages/app/src/command-center/command-center.tsx +++ b/packages/app/src/command-center/command-center.tsx @@ -1,8 +1,8 @@ +import { Modal } from "@/components/ui/modal"; import equal from "fast-deep-equal"; import { recordRenderProfileReasons } from "@/utils/render-profiler"; import { FlatList, - Modal, Pressable, Text, View, diff --git a/packages/app/src/components/add-project-flow.tsx b/packages/app/src/components/add-project-flow.tsx index a1cb5cf779..7fdf21dd83 100644 --- a/packages/app/src/components/add-project-flow.tsx +++ b/packages/app/src/components/add-project-flow.tsx @@ -1,3 +1,4 @@ +import { Modal } from "@/components/ui/modal"; import { usePanelStore } from "@/stores/panel-store"; import { getHostRuntimeStore } from "@/runtime/host-runtime"; import { router } from "expo-router"; @@ -22,14 +23,7 @@ import { type ComponentType, type RefObject, } from "react"; -import { - Modal, - Pressable, - ScrollView, - Text, - View, - type PressableStateCallbackType, -} from "react-native"; +import { Pressable, ScrollView, Text, View, type PressableStateCallbackType } from "react-native"; import { EditingTextInput as TextInput, type EditingTextInputHandle, diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx index b487e53660..8921102f26 100644 --- a/packages/app/src/components/agent-list.tsx +++ b/packages/app/src/components/agent-list.tsx @@ -1,8 +1,8 @@ +import { Modal } from "@/components/ui/modal"; import { View, Text, Pressable, - Modal, RefreshControl, FlatList, type ListRenderItem, diff --git a/packages/app/src/components/attachment-lightbox.tsx b/packages/app/src/components/attachment-lightbox.tsx index 0f14f2c515..08c6042bf8 100644 --- a/packages/app/src/components/attachment-lightbox.tsx +++ b/packages/app/src/components/attachment-lightbox.tsx @@ -1,5 +1,6 @@ +import { Modal } from "@/components/ui/modal"; import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react"; -import { Modal, Pressable, Text, View } from "react-native"; +import { Pressable, Text, View } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { StyleSheet } from "react-native-unistyles"; import { SafeAreaProvider, useSafeAreaInsets } from "react-native-safe-area-context"; diff --git a/packages/app/src/components/markdown/fence/mermaid/fullscreen-viewer.web.tsx b/packages/app/src/components/markdown/fence/mermaid/fullscreen-viewer.web.tsx index 8b68ae61cb..02ca7108d8 100644 --- a/packages/app/src/components/markdown/fence/mermaid/fullscreen-viewer.web.tsx +++ b/packages/app/src/components/markdown/fence/mermaid/fullscreen-viewer.web.tsx @@ -1,5 +1,6 @@ +import { Modal } from "@/components/ui/modal"; import { useCallback, useEffect, useMemo, useRef } from "react"; -import { Modal, View } from "react-native"; +import { View } from "react-native"; import { X } from "lucide-react-native"; import { useTranslation } from "react-i18next"; import { StyleSheet } from "react-native-unistyles"; diff --git a/packages/app/src/components/markdown/fence/mermaid/host.native.tsx b/packages/app/src/components/markdown/fence/mermaid/host.native.tsx index 001ec5cf1c..5f86efe05c 100644 --- a/packages/app/src/components/markdown/fence/mermaid/host.native.tsx +++ b/packages/app/src/components/markdown/fence/mermaid/host.native.tsx @@ -1,6 +1,6 @@ +import { Modal } from "@/components/ui/modal"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { - Modal, Pressable, StyleSheet as RNStyleSheet, ScrollView, diff --git a/packages/app/src/components/ui/combobox.tsx b/packages/app/src/components/ui/combobox.tsx index d436758d45..adcd91afcb 100644 --- a/packages/app/src/components/ui/combobox.tsx +++ b/packages/app/src/components/ui/combobox.tsx @@ -1,3 +1,4 @@ +import { Modal } from "@/components/ui/modal"; import { useCallback, useEffect, @@ -12,7 +13,6 @@ import { View, Text, Pressable, - Modal, ScrollView, Platform, StatusBar, diff --git a/packages/app/src/components/ui/menu/menu-overlay.tsx b/packages/app/src/components/ui/menu/menu-overlay.tsx index c6935bfd91..3979c5b633 100644 --- a/packages/app/src/components/ui/menu/menu-overlay.tsx +++ b/packages/app/src/components/ui/menu/menu-overlay.tsx @@ -1,3 +1,4 @@ +import { Modal } from "@/components/ui/modal"; import { useCallback, useEffect, @@ -12,7 +13,6 @@ import { createPortal } from "react-dom"; import { useTranslation } from "react-i18next"; import { Dimensions, - Modal, Platform, Pressable, View, diff --git a/packages/app/src/components/ui/modal.tsx b/packages/app/src/components/ui/modal.tsx new file mode 100644 index 0000000000..884377fd8c --- /dev/null +++ b/packages/app/src/components/ui/modal.tsx @@ -0,0 +1,13 @@ +import { Modal as NativeModal, type ModalProps } from "react-native"; + +// React Native defaults iPhone modals to portrait even when the app supports rotation. +const orientations: NonNullable = [ + "portrait", + "portrait-upside-down", + "landscape-left", + "landscape-right", +]; + +export function Modal(props: ModalProps) { + return ; +} diff --git a/packages/app/src/components/ui/tooltip.tsx b/packages/app/src/components/ui/tooltip.tsx index 697a80fdcf..99f446001f 100644 --- a/packages/app/src/components/ui/tooltip.tsx +++ b/packages/app/src/components/ui/tooltip.tsx @@ -1,3 +1,4 @@ +import { Modal } from "@/components/ui/modal"; import { Children, cloneElement, @@ -16,7 +17,6 @@ import { createPortal } from "react-dom"; import { Dimensions, Platform, - Modal, Pressable, View, type PressableProps, diff --git a/packages/app/src/hosts/host-chooser.tsx b/packages/app/src/hosts/host-chooser.tsx index 5e4d7f053c..3d807ba3a1 100644 --- a/packages/app/src/hosts/host-chooser.tsx +++ b/packages/app/src/hosts/host-chooser.tsx @@ -1,13 +1,7 @@ +import { Modal } from "@/components/ui/modal"; import { usePanelStore } from "@/stores/panel-store"; import { createElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - Modal, - Pressable, - ScrollView, - Text, - View, - type PressableStateCallbackType, -} from "react-native"; +import { Pressable, ScrollView, Text, View, type PressableStateCallbackType } from "react-native"; import { EditingTextInput as TextInput, type EditingTextInputHandle,