Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/mobile/src/components/ScrollComposeButton.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import { resolveScaledTextRole } from "../lib/appearancePreferences";
export function ScrollComposeButton(props: {
readonly expanded: boolean;
readonly onPress: () => void;
readonly size?: number;
readonly className?: string;
readonly style?: StyleProp<ViewStyle>;
}) {
const { appearance, themeVariables: colors } = useAppearancePreferences();
const typography = resolveScaledTextRole("footnote", appearance.baseFontSize);
const { iconSize, fabSize } = useAndroidControlSizing();
const { iconSize, fabSize: scaledFabSize } = useAndroidControlSizing();
const fabSize = props.size ?? scaledFabSize;
const nativeSize = Math.max(56, fabSize);
const scale = fabSize / nativeSize;
const nativeIconSize = Math.round(iconSize / scale);
Expand Down
31 changes: 14 additions & 17 deletions apps/mobile/src/features/home/AndroidHomeFab.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
type NativeSyntheticEvent,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { NewThreadButton } from "../../components/NewThreadButton";
import { ScrollComposeButton } from "../../components/ScrollComposeButton.android";
import {
androidHomeFabPosition,
type AndroidHomeFabLayout as SharedAndroidHomeFabLayout,
Expand All @@ -15,9 +15,9 @@ import { useWorkspaceState } from "../../state/workspace";
import { FabScrollContext } from "./FabScrollContext";
import { updateFabScroll } from "./fab-scroll";
import { useAndroidControlSizing } from "../../components/ui/useAndroidControlSizing";
import { useEnabledNavigationTabs } from "../workspace/navigation-tabs";
import { WorkspaceTabsContext, useWorkspaceTabs } from "../workspace/WorkspaceTabsContext";
import { FLOATING_TABS, floatingTabsMetrics } from "../workspace/floating-workspace-tabs";
import { useEnabledNavigationTabs } from "../workspace/navigation-tabs";
import {
useAppearancePreferences,
useAndroidFloatingTabs,
Expand All @@ -32,19 +32,16 @@ export function AndroidHomeFabLayout(props: ComponentProps<typeof SharedAndroidH
const { state } = useWorkspaceState();
const [expanded, setExpanded] = useState(true);
const { scale, fabSize } = useAndroidControlSizing();
const tabs = useEnabledNavigationTabs();
const floating = floatingTabsMetrics(
tabs.length + 1,
scale,
fabSize,
props.paneWidth ?? layoutWidth ?? undefined,
);
const enabledTabs = useEnabledNavigationTabs();
const barWidth =
layoutWidth === null
? undefined
: layoutWidth - (Math.max(insets.left, FLOATING_TABS.edge) - FLOATING_TABS.edge);
const size = floatingTabsEnabled
? floatingTabsMetrics(enabledTabs.length + 1, scale, fabSize, barWidth).height
Comment thread
none23 marked this conversation as resolved.
Resolved
: fabSize;
const workspace = useWorkspaceTabs();
const bottomClearance =
insets.bottom +
FLOATING_TABS.bottom +
FLOATING_TABS.edge +
Math.max(floating.height, floating.fabOffset + fabSize);
const bottomClearance = insets.bottom + FLOATING_TABS.bottom + FLOATING_TABS.edge + size;
const context = floatingTabsEnabled && workspace ? { ...workspace, bottomClearance } : workspace;
const scrollState = useRef({ anchor: 0, expanded: true });
const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
Expand All @@ -69,17 +66,17 @@ export function AndroidHomeFabLayout(props: ComponentProps<typeof SharedAndroidH
(!props.sidebar || floatingTabsEnabled) &&
state.hasConnections &&
layoutWidth !== null ? (
<NewThreadButton
<ScrollComposeButton
key={`${appearance.baseFontSize}:${fontScale}:${layoutWidth}`}
extended
size={size}
expanded={floatingTabsEnabled ? false : expanded}
onPress={props.onStartNewTask}
className="absolute"
style={
floatingTabsEnabled
? {
right: FLOATING_TABS.edge,
bottom: insets.bottom + FLOATING_TABS.bottom + floating.fabOffset,
bottom: insets.bottom + FLOATING_TABS.bottom,
}
: androidHomeFabPosition(insets.bottom, props.sidebar)
}
Expand Down
1 change: 0 additions & 1 deletion apps/mobile/src/features/home/AndroidHomeFab.shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export function AndroidHomeFabLayout(props: {
readonly children: ReactNode;
readonly sidebar?: boolean;
readonly disabled?: boolean;
readonly paneWidth?: number;
}) {
return <>{props.children}</>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,6 @@ function AdaptiveWorkspaceLayoutContent(
<AndroidHomeFabLayout
sidebar
disabled={workspacePage}
paneWidth={layout.listPaneWidth}
onStartNewTask={handleStartNewTask}
>
<ThreadNavigationSidebar
Expand Down
11 changes: 3 additions & 8 deletions apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ import type { LayoutChangeEvent, TextInputInstance } from "react-native";
import { Platform, StyleSheet, TextInput, View } from "react-native";
import { GestureDetector, useNativeGesture } from "react-native-gesture-handler";
import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";
import { useEnabledNavigationTabs } from "../workspace/navigation-tabs";
import { FLOATING_TABS, floatingTabsMetrics } from "../workspace/floating-workspace-tabs";
import { FLOATING_TABS } from "../workspace/floating-workspace-tabs";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { SearchBarCommands } from "react-native-screens";

Expand Down Expand Up @@ -152,9 +151,7 @@ function ThreadNavigationSidebarPane(
const drawerColor = materialTheme["--color-drawer"];

const insets = useSafeAreaInsets();
const { fabClearance, scale, fabSize } = useAndroidControlSizing();
const enabledTabs = useEnabledNavigationTabs();
const floatingTabs = floatingTabsMetrics(enabledTabs.length + 1, scale, fabSize, props.width);
const { fabClearance, fabSize } = useAndroidControlSizing();
const projects = useProjects();
const threads = useNavigationThreadShells();
const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState();
Expand Down Expand Up @@ -826,9 +823,7 @@ function ThreadNavigationSidebarPane(
paddingBottom:
Platform.OS === "android"
? floatingTabsEnabled
? FLOATING_TABS.bottom +
FLOATING_TABS.edge +
Math.max(floatingTabs.height, floatingTabs.fabOffset + fabSize)
? FLOATING_TABS.bottom + FLOATING_TABS.edge + fabSize
: Math.max(insets.bottom, 16) + fabClearance - insets.bottom
: 16 + insets.bottom,
paddingTop: Platform.OS === "android" ? 6 : topListInset,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ export function AndroidWorkspaceNavigation(props: {
);
// FloatingWorkspaceTabs starts the bar at the left inset, so a cutout narrows the room.
const barWidth =
props.paneWidth ??
window.width - (Math.max(insets.left, FLOATING_TABS.edge) - FLOATING_TABS.edge);
(props.paneWidth ?? window.width) -
(Math.max(insets.left, FLOATING_TABS.edge) - FLOATING_TABS.edge);
const metrics = floatingTabsMetrics(tabs.length, scale, fabSize, barWidth, props.fillWidth);
if (!rail && keyboardVisible) return null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { NAVIGATION_TAB_ORDER } from "./navigation-tab-order";
import { type ReactNode } from "react";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useAndroidControlSizing } from "../../components/ui/useAndroidControlSizing";
import { FLOATING_TABS, floatingTabsMetrics } from "./floating-workspace-tabs";
import { FLOATING_TABS } from "./floating-workspace-tabs";
import { View } from "react-native";
import { ScreenContent } from "../../components/ui/ScreenContent";
import { PullRequestsScreen } from "../pull-requests/PullRequestsScreen";
Expand All @@ -22,10 +22,9 @@ export function AndroidWorkspaceSidebar(props: {
const workspace = useWorkspaceTabs()!;
const enabled = useEnabledNavigationTabs();
const insets = useSafeAreaInsets();
const { scale, fabSize } = useAndroidControlSizing();
const floating = floatingTabsMetrics(enabled.length + 1, scale, fabSize);
const { fabSize } = useAndroidControlSizing();
const bottomClearance = floatingTabsEnabled
? insets.bottom + FLOATING_TABS.bottom + FLOATING_TABS.edge + floating.height
? insets.bottom + FLOATING_TABS.bottom + FLOATING_TABS.edge + fabSize
: 16;
const context = { ...workspace, bottomClearance };
if (enabled.length === 0) return <WorkspaceNoTabs />;
Expand Down
16 changes: 4 additions & 12 deletions apps/mobile/src/features/workspace/WorkspaceRootScreen.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { WorkspaceNoTabs } from "./WorkspaceNoTabs";
import { useEnabledNavigationTabs } from "./navigation-tabs";
import { NAVIGATION_TAB_ORDER } from "./navigation-tab-order";
import { WorkspaceExtraTab } from "./WorkspaceExtraTab";
import { useWindowDimensions, View } from "react-native";
import { View } from "react-native";
import { SafeAreaInsetsContext, useSafeAreaInsets } from "react-native-safe-area-context";
import { NativeStackScreenOptions } from "../../native/StackHeader";
import { AndroidScreenHeader } from "../../components/ui/AndroidScreenHeader";
Expand All @@ -15,7 +15,7 @@ import { PullRequestsScreen } from "../pull-requests/PullRequestsScreen";
import { PullRequestToolbar } from "../pull-requests/PullRequestToolbar.android";
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
import { AndroidWorkspaceNavigation, FloatingWorkspaceTabs } from "./AndroidWorkspaceNavigation";
import { FLOATING_TABS, floatingTabsMetrics } from "./floating-workspace-tabs";
import { FLOATING_TABS } from "./floating-workspace-tabs";
import { WorkspaceTabsContext, useWorkspaceTabs } from "./WorkspaceTabsContext";
import { WorkspaceTabPanes } from "./WorkspaceTabPanes";
import type { WorkspaceTab } from "./workspace-tabs-state";
Expand All @@ -28,21 +28,13 @@ export function WorkspaceRootScreen() {
const workspace = useWorkspaceTabs()!;
const enabled = useEnabledNavigationTabs();
const insets = useSafeAreaInsets();
const { fabClearance, scale, fabSize } = useAndroidControlSizing();
const window = useWindowDimensions();
const floating = floatingTabsMetrics(enabled.length + 1, scale, fabSize, window.width);
const { fabClearance, fabSize } = useAndroidControlSizing();
const context = {
...workspace,
bottomClearance: layout.usesSplitView
? workspace.bottomClearance
: floatingTabsEnabled
? insets.bottom +
FLOATING_TABS.bottom +
FLOATING_TABS.edge +
Math.max(
floating.height,
workspace.state.selectedTab === "threads" ? floating.fabOffset + fabSize : 0,
)
? insets.bottom + FLOATING_TABS.bottom + FLOATING_TABS.edge + fabSize
: workspace.state.selectedTab === "threads"
? fabClearance + 16
: 16,
Expand Down
31 changes: 15 additions & 16 deletions apps/mobile/src/features/workspace/floating-workspace-tabs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,21 @@ it.each([
);

it.each([
[475, 1, 56],
[411, 1, 56],
[475, 1.25, 70],
[475, 1, 56, 4, 56],
[411, 1, 56, 4, 56],
[475, 1.25, 70, 4, 70],
[320, 11 / 16, 48, 4, 48],
[320, 1, 56, 4, 55.2],
[320, 22 / 16, 77, 4, 55.2],
[300, 22 / 16, 77, 4, 51.2],
[411, 22 / 16, 77, 4, 73.4],
[320, 22 / 16, 77, 2, 77],
] as const)(
"fills a %sdp cover row at scale %s with a %sdp compose button",
(width, scale, fabSize) => {
const metrics = floatingTabsMetrics(4, scale, fabSize, width, true);
expect(metrics.sharesRow).toBe(true);
expect(metrics.pill + 2 * 16 + 12 + fabSize).toBe(width);
expect(metrics.height).toBe(fabSize);
"fills a %sdp row at scale %s beside a %sdp compose button",
(width, scale, fabSize, count, height) => {
const metrics = floatingTabsMetrics(count, scale, fabSize, width, true);
expect(metrics.pill + 2 * 16 + 12 + metrics.height).toBeCloseTo(width);
expect(metrics.height).toBeCloseTo(height);
expect(metrics.pill / count).toBeGreaterThanOrEqual(metrics.height - 0.001);
},
);

it("stacks compose above a full-width pill when a cover row is too narrow", () => {
const metrics = floatingTabsMetrics(4, 1, 56, 300, true);
expect(metrics.sharesRow).toBe(false);
expect(metrics.pill).toBe(268);
expect(metrics.fabOffset).toBe(68);
});
20 changes: 8 additions & 12 deletions apps/mobile/src/features/workspace/floating-workspace-tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,18 @@ export function floatingTabsMetrics(
fillWidth = false,
) {
const unit = Math.max(1, scale);
const height = fabSize;
const beside = 2 * FLOATING_TABS.edge + FLOATING_TABS.gap + fabSize;
const row = beside + count * 60 * unit;
const sharesRow = paneWidth === undefined || paneWidth >= row;
const room =
const spacing = 2 * FLOATING_TABS.edge + FLOATING_TABS.gap;
// Share width evenly when space is tight, retaining Android's 48dp touch target.
const height =
paneWidth === undefined
? count * 60 * unit
: sharesRow
? paneWidth - beside
: paneWidth - 2 * FLOATING_TABS.edge;
? fabSize
: Math.max(48, Math.min(fabSize, (paneWidth - spacing) / (count + 1)));
const beside = spacing + height;
const room = paneWidth === undefined ? count * 60 * unit : paneWidth - beside;
return {
// Reserve the compose slot even while its button is hidden.
pill: Math.max(0, fillWidth ? room : Math.min(count * 72 * unit, room)),
height,
row,
sharesRow,
fabOffset: sharesRow ? (height - fabSize) / 2 : height + FLOATING_TABS.gap,
row: beside + count * 60 * unit,
};
}
Loading