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,