# Files changed · PR \#22977 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/22977/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(mobile\): respect Duo safe areas in overlays](https://git.cafe/pingdotgg/t3code/pull/22977.md)

Version: 9

Head commit: e4779fd19c54c4368e54f87ac163673f92e3a214

Base commit: 7153c10fce9f43da2b036ae398e0ea821f2ed391

Comparison base: 7153c10fce9f43da2b036ae398e0ea821f2ed391

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/mobile/src/features/devices/DevicePreviewRouteScreen.tsx

Status: modified; +8; −1

```diff
@@ -27,6 +27,8 @@
   findHeaderMenuAction,
 } from "../../components/ui/headerMenu.android";
 import type { AppBarMenuItem } from "../../components/ui/AppBar.types";
+import { deriveBottomControlInsets } from "../../lib/reserved-regions";
+import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics";
 import { deviceEnvironment, refreshDeviceHubAccess, useDeviceHubAccess } from "../../state/device";
 import { useEnvironmentQuery } from "../../state/query";
 import { useAtomCommand } from "../../state/use-atom-command";
@@ -75,6 +77,8 @@
   readonly onClose: () => void;
 }) {
   const insets = useSafeAreaInsets();
+  // NativeColumnContent already insets the sides, so only a book fold moves the reveal handle.
+  const handleBounds = deriveBottomControlInsets(useNativeColumnLayoutMetrics());
   const { themeVariables } = useAppearancePreferences();
   const focused = useIsFocused();
   const [foreground, setForeground] = useState(AppState.currentState !== "background");
@@ -318,7 +322,10 @@
           accessibilityHint="Shaking the phone also shows them"
           hitSlop={12}
           className="absolute h-6 w-16 items-center justify-center self-center"
+          style={{
-          style={{ top: Math.max(insets.top - 18, 4) }}
+            top: Math.max(insets.top - 18, 4),
+            transform: [{ translateX: (handleBounds.left - handleBounds.right) / 2 }],
+          }}
           onPress={() => setControlsVisible(true)}
         >
           <View className="h-1.5 w-10 rounded-full bg-white/40" />
```

## apps/mobile/src/features/keyboard/CommandPalette.tsx

Status: modified; +8; −2

```diff
@@ -22,7 +22,9 @@
 import { AppText as Text } from "../../components/ui/AppText";
 import { SymbolView, type AppSymbolName } from "../../components/ui/AppSymbol";
 import { cn } from "../../lib/cn";
+import { deriveBottomControlInsets } from "../../lib/reserved-regions";
 import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities";
+import { useNativeLayoutMetrics } from "../../native/native-layout-metrics";
 import { T3KeyboardCommands } from "../../native/T3KeyboardCommands";
 import { useProjects, useThreadShell, useThreadShells } from "../../state/entities";
 import { useThreadSearch } from "../../state/queries";
@@ -156,6 +158,9 @@
   const inputRef = useRef<TextInputInstance>(null);
   const listRef = useRef<FlatList<CommandPaletteItem>>(null);
   const { width, height } = useWindowDimensions();
+  // The palette floats above every column, so it keeps itself inside the window's side insets
+  // and to one side of a book fold.
+  const bounds = deriveBottomControlInsets(useNativeLayoutMetrics());
   const searchEnvironmentIds = useMemo(
     () =>
       environments
@@ -423,7 +428,8 @@
         <T3KeyboardCommands enabledCommands={PALETTE_COMMANDS} onCommand={onCommand}>
           <KeyboardAvoidingView
             behavior="padding"
+            className="flex-1 items-center justify-center py-4"
+            style={{ paddingLeft: 16 + bounds.left, paddingRight: 16 + bounds.right }}
-            className="flex-1 items-center justify-center p-4"
           >
             <Pressable
               className="absolute inset-0 bg-backdrop"
@@ -436,7 +442,7 @@
               tintColorClassName="accent-sheet/20"
               fallbackColor={themeVariables["--color-sheet"]}
               style={{
-                width: Math.min(600, width - 32),
+                width: Math.min(600, width - 32 - bounds.left - bounds.right),
                 height: Math.min(520, height - 80),
                 borderRadius: 20,
               }}
```

## apps/mobile/src/features/settings/components/SettingsEnvironmentFilterHeader.tsx

Status: modified; +0; −2

```diff
@@ -54,7 +54,6 @@
       options={{
         unstable_headerRightItems: () => [
           withNativeGlassHeaderItem({
-            axisBehavior: "horizontalOnly",
             accessibilityLabel: "Filter settings environments and projects",
             icon: { name: filterIcon, type: "sfSymbol" },
             label: "",
@@ -121,7 +120,6 @@
           ...(closeSettings
             ? [
                 withNativeGlassHeaderItem({
-                  axisBehavior: "horizontalOnly",
                   accessibilityLabel: "Close settings",
                   icon: { name: "xmark", type: "sfSymbol" },
                   identifier: "settings-close",
```

## apps/mobile/src/features/threads/ThreadArrangementSheet.tsx

Status: modified; +178; −176

```diff
@@ -9,7 +9,7 @@
 import { Gesture, GestureDetector, GestureHandlerRootView } from "react-native-gesture-handler";
 import Reanimated, { ReduceMotion, useAnimatedStyle, withTiming } from "react-native-reanimated";
 import { threadDragGapOffset } from "./threadDragGap";
-import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
 
 import { AppText as Text } from "../../components/ui/AppText";
 import { SymbolView } from "../../components/ui/AppSymbol";
@@ -149,7 +149,6 @@
 }
 
 export function ThreadArrangementSheet(props: { onClose: () => void }) {
-  const insets = useSafeAreaInsets();
   const threads = useAtomValue(environmentThreadShells.navigationThreadShellsAtom);
   const configs = useAtomValue(environmentServerConfigsAtom);
   const queuedThreadKeys = useAtomValue(queuedThreadKeysAtom);
@@ -373,184 +372,187 @@
       onRequestClose={props.onClose}
     >
       <GestureHandlerRootView style={{ flex: 1 }}>
+        {/* A Modal is its own native hierarchy, so measure its own safe area, including side
+            insets for the camera, instead of the presenting screen's. */}
+        <SafeAreaProvider style={{ flex: 1 }}>
+          <View className="flex-1 bg-screen">
+            <SafeAreaView style={{ flex: 1 }}>
+              <SheetHeader
+                presentation="inline"
+                title="Arrange threads"
+                onClose={props.onClose}
+                accessibilityLabel="Close arrange threads"
+              />
+              <Text className="px-5 pb-3 text-sm text-foreground-muted">
+                Drag to reorder, pin, or settle. Changes save when you drop.
+              </Text>
+              <View
+                onLayout={(event) => {
+                  geometry.current.height = event.nativeEvent.layout.height;
+                }}
+                className="flex-1"
+                style={{ overflow: "hidden" }}
+              >
+                <FlatList
+                  ref={list}
+                  data={rows}
+                  keyExtractor={(row) => row.key}
+                  scrollEnabled={visiblePreview === null}
+                  removeClippedSubviews={false}
+                  onScroll={(event) => {
+                    geometry.current.offset = event.nativeEvent.contentOffset.y;
+                  }}
+                  scrollEventThrottle={16}
+                  getItemLayout={(_, index) => ({
+                    length: rows[index]!.height,
+                    offset: rows[index]!.offset,
+                    index,
+                  })}
+                  renderItem={({ item }) => {
+                    const thread = item.thread;
+                    const planner =
+                      item.section === "pinned" || item.section === "active"
+                        ? planners[item.section]
+                        : null;
-        <View
-          className="flex-1 bg-screen"
-          style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}
-        >
-          <SheetHeader
-            presentation="inline"
-            title="Arrange threads"
-            onClose={props.onClose}
-            accessibilityLabel="Close arrange threads"
-          />
-          <Text className="px-5 pb-3 text-sm text-foreground-muted">
-            Drag to reorder, pin, or settle. Changes save when you drop.
-          </Text>
-          <View
-            onLayout={(event) => {
-              geometry.current.height = event.nativeEvent.layout.height;
-            }}
-            className="flex-1"
-            style={{ overflow: "hidden" }}
-          >
-            <FlatList
-              ref={list}
+                    const capabilities =
+                      thread && configs.get(thread.environmentId)?.environment.capabilities;
+                    const sectionActions = thread
+                      ? (["pinned", "active", "settled"] as const).flatMap<{
+                          name: "pinned" | "active" | "settled";
+                          label: string;
+                        }>((section) => {
+                          if (section === item.section) return [];
+                          const label = threadDragAction(item.section, section);
-              data={rows}
+                          if (!label) return [];
+                          if (section === "settled")
+                            return capabilities?.threadSettlement ? [{ name: section, label }] : [];
+                          if (
+                            ((section === "pinned" || thread.pinnedAt != null) &&
+                              !capabilities?.threadPinning) ||
+                            (section === "active" &&
+                              item.section === "settled" &&
+                              !capabilities?.threadSettlement) ||
+                            (section === "active" &&
+                              item.section === "snoozed" &&
+                              !capabilities?.threadSnooze)
-              keyExtractor={(row) => row.key}
-              scrollEnabled={visiblePreview === null}
-              removeClippedSubviews={false}
-              onScroll={(event) => {
-                geometry.current.offset = event.nativeEvent.contentOffset.y;
-              }}
-              scrollEventThrottle={16}
-              getItemLayout={(_, index) => ({
-                length: rows[index]!.height,
-                offset: rows[index]!.offset,
-                index,
-              })}
-              renderItem={({ item }) => {
-                const thread = item.thread;
-                const planner =
-                  item.section === "pinned" || item.section === "active"
-                    ? planners[item.section]
-                    : null;
-                const capabilities =
-                  thread && configs.get(thread.environmentId)?.environment.capabilities;
-                const sectionActions = thread
-                  ? (["pinned", "active", "settled"] as const).flatMap<{
-                      name: "pinned" | "active" | "settled";
-                      label: string;
-                    }>((section) => {
-                      if (section === item.section) return [];
-                      const label = threadDragAction(item.section, section);
-                      if (!label) return [];
-                      if (section === "settled")
-                        return capabilities?.threadSettlement ? [{ name: section, label }] : [];
-                      if (
-                        ((section === "pinned" || thread.pinnedAt != null) &&
-                          !capabilities?.threadPinning) ||
-                        (section === "active" &&
-                          item.section === "settled" &&
-                          !capabilities?.threadSettlement) ||
-                        (section === "active" &&
-                          item.section === "snoozed" &&
-                          !capabilities?.threadSnooze)
-                      )
-                        return [];
-                      return planners[section](item.key, {
-                        section,
-                        targetId: null,
-                        placement: "before",
-                      })
-                        ? [{ name: section, label }]
-                        : [];
-                    })
-                  : [];
-                return (
-                  <ArrangementRow
-                    height={item.height}
-                    dragging={visiblePreview !== null}
-                    lifted={item.key === sourceRow?.key}
-                    offset={
-                      sourceRow && insertionOffset !== undefined
-                        ? threadDragGapOffset(
-                            item.offset,
-                            sourceRow.offset,
-                            sourceRow.height,
-                            insertionOffset,
                           )
-                        : 0
-                    }
-                  >
-                    {thread ? (
-                      <>
-                        <Text numberOfLines={2} className="flex-1 text-base">
-                          {thread.title}
-                        </Text>
-                        <DragHandle
-                          title={thread.title}
-                          disabled={
-                            dropBusy ||
-                            pendingOrder !== null ||
-                            !(
-                              configs.get(thread.environmentId)?.environment.capabilities
-                                .threadPinReorder ||
-                              configs.get(thread.environmentId)?.environment.capabilities
-                                .threadActiveReorder
-                            )
-                          }
-                          sectionActions={sectionActions}
-                          onSectionMove={(section) => {
-                            void moveThread(thread, {
-                              section,
-                              targetId: null,
-                              placement: "before",
+                            return [];
+                          return planners[section](item.key, {
+                            section,
+                            targetId: null,
+                            placement: "before",
+                          })
+                            ? [{ name: section, label }]
+                            : [];
+                        })
+                      : [];
+                    return (
+                      <ArrangementRow
+                        height={item.height}
+                        dragging={visiblePreview !== null}
+                        lifted={item.key === sourceRow?.key}
+                        offset={
+                          sourceRow && insertionOffset !== undefined
+                            ? threadDragGapOffset(
+                                item.offset,
+                                sourceRow.offset,
+                                sourceRow.height,
+                                insertionOffset,
+                              )
-                            });
-                          }}
-                          canMoveUp={planner?.(item.key, "up") != null}
-                          canMoveDown={planner?.(item.key, "down") != null}
-                          onStep={(direction) => {
-                            void moveThread(thread, direction);
-                          }}
-                          onStart={() => start(item)}
-                          onMove={update}
-                          onEnd={(cancelled) => {
-                            const current = drag.current;
-                            if (cancelled || !current?.destination) {
-                              stop();
-                              return;
+                            : 0
+                        }
-                            }
-                            if (frame.current !== null) cancelAnimationFrame(frame.current);
-                            frame.current = null;
-                            drag.current = null;
-                            void moveThread(current.thread, current.destination).finally(stop);
-                          }}
-                        />
-                      </>
-                    ) : (
-                      <Pressable
-                        disabled={item.section === "pinned" || item.section === "active"}
-                        className="flex-1 justify-center self-stretch"
-                        onPress={() => {
-                          const section = item.section;
-                          if (section === "snoozed" || section === "settled")
-                            setExpanded((value) => ({ ...value, [section]: !value[section] }));
-                        }}
                       >
+                        {thread ? (
+                          <>
+                            <Text numberOfLines={2} className="flex-1 text-base">
+                              {thread.title}
+                            </Text>
+                            <DragHandle
+                              title={thread.title}
+                              disabled={
+                                dropBusy ||
+                                pendingOrder !== null ||
+                                !(
+                                  configs.get(thread.environmentId)?.environment.capabilities
+                                    .threadPinReorder ||
+                                  configs.get(thread.environmentId)?.environment.capabilities
+                                    .threadActiveReorder
+                                )
+                              }
+                              sectionActions={sectionActions}
+                              onSectionMove={(section) => {
+                                void moveThread(thread, {
+                                  section,
+                                  targetId: null,
+                                  placement: "before",
+                                });
+                              }}
+                              canMoveUp={planner?.(item.key, "up") != null}
+                              canMoveDown={planner?.(item.key, "down") != null}
+                              onStep={(direction) => {
+                                void moveThread(thread, direction);
+                              }}
+                              onStart={() => start(item)}
+                              onMove={update}
+                              onEnd={(cancelled) => {
-                        <Text className="text-sm font-t3-semibold text-foreground-muted">
-                          {item.section[0]!.toUpperCase() + item.section.slice(1)} (
-                          {sections[item.section].length})
-                        </Text>
-                      </Pressable>
-                    )}
-                  </ArrangementRow>
-                );
-              }}
-            />
-            {visiblePreview ? (
-              <Animated.View
-                pointerEvents="none"
-                className="absolute left-5 right-5 justify-center rounded-xl border border-border bg-screen px-4"
-                style={{ top: 0, height: ROW_HEIGHT, transform: [{ translateY }] }}
-              >
-                <Text
-                  numberOfLines={visiblePreview.destination?.section ? 1 : 2}
-                  className="text-base font-t3-medium"
-                >
-                  {visiblePreview.thread.title}
-                </Text>
-                {visiblePreview.destination?.section ? (
+                                const current = drag.current;
-                  <Text className="text-xs text-foreground-muted">
+                                if (cancelled || !current?.destination) {
+                                  stop();
+                                  return;
-                    {threadDragAction(
-                      visiblePreview.sourceSection,
-                      visiblePreview.destination.section,
-                    )}
-                  </Text>
+                                }
+                                if (frame.current !== null) cancelAnimationFrame(frame.current);
+                                frame.current = null;
+                                drag.current = null;
+                                void moveThread(current.thread, current.destination).finally(stop);
+                              }}
+                            />
+                          </>
+                        ) : (
+                          <Pressable
+                            disabled={item.section === "pinned" || item.section === "active"}
+                            className="flex-1 justify-center self-stretch"
+                            onPress={() => {
+                              const section = item.section;
+                              if (section === "snoozed" || section === "settled")
+                                setExpanded((value) => ({ ...value, [section]: !value[section] }));
+                            }}
+                          >
+                            <Text className="text-sm font-t3-semibold text-foreground-muted">
+                              {item.section[0]!.toUpperCase() + item.section.slice(1)} (
+                              {sections[item.section].length})
+                            </Text>
+                          </Pressable>
+                        )}
+                      </ArrangementRow>
+                    );
+                  }}
+                />
+                {visiblePreview ? (
+                  <Animated.View
+                    pointerEvents="none"
+                    className="absolute left-5 right-5 justify-center rounded-xl border border-border bg-screen px-4"
+                    style={{ top: 0, height: ROW_HEIGHT, transform: [{ translateY }] }}
+                  >
+                    <Text
+                      numberOfLines={visiblePreview.destination?.section ? 1 : 2}
+                      className="text-base font-t3-medium"
+                    >
+                      {visiblePreview.thread.title}
+                    </Text>
+                    {visiblePreview.destination?.section ? (
+                      <Text className="text-xs text-foreground-muted">
+                        {threadDragAction(
+                          visiblePreview.sourceSection,
+                          visiblePreview.destination.section,
+                        )}
+                      </Text>
+                    ) : null}
+                  </Animated.View>
                 ) : null}
+              </View>
-              </Animated.View>
+            </SafeAreaView>
-            ) : null}
           </View>
-        </View>
+        </SafeAreaProvider>
       </GestureHandlerRootView>
     </Modal>
   );
```
