authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-07-23 18:06:44+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-23 18:06:44+02:00
log719c905eacdcb04e11dd4ce1e515a44455ed0874
tree73e61b2bdca255616510ddc9ea59adfa8d9802ea
parent0542abc75e378428165cf8a492d02dd4cc3dc348
signature Signed by PGP key B5690EEEBB952194

[codex] Move mobile project grouping to General settings (#4315)

Co-authored-by: codex <codex@users.noreply.github.com>

12 files changed, 111 insertions(+), 142 deletions(-)

apps/mobile/src/App.tsx+19-5
......@@ -13,7 +13,10 @@ import { ConfirmDialogHost } from "./components/ConfirmDialogHost";
1313import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider";
1414import { prepareNativeShowcaseCapture } from "./features/showcase/nativeShowcaseScene";
1515import { IncomingShareProvider } from "./features/sharing/IncomingShareProvider";
16import { AppearancePreferencesProvider } from "./features/settings/appearance/AppearancePreferencesProvider";
16import {
17 AppearancePreferencesProvider,
18 useAppearancePreferences,
19} from "./features/settings/appearance/AppearancePreferencesProvider";
1720import { RootStack } from "./Stack";
1821import { appAtomRegistry } from "./state/atom-registry";
1922import { OverlayPortalHost } from "./components/OverlayPortal";
......@@ -26,6 +29,10 @@ if (process.env.EXPO_PUBLIC_SHOWCASE === "1") {
2629 prepareNativeShowcaseCapture();
2730}
2831
32void SplashScreen.preventAutoHideAsync().catch(() => {
33 // The native module can be unavailable in non-native test environments.
34});
35
2936const appLinking = {
3037 prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"],
3138 // The Expo dev client launches the app via
......@@ -40,18 +47,25 @@ const appLinking = {
4047
4148const Navigation = createStaticNavigation(RootStack);
4249
50function SplashScreenCoordinator() {
51 const { isReady } = useAppearancePreferences();
52
53 useEffect(() => {
54 if (isReady) void SplashScreen.hide();
55 }, [isReady]);
56
57 return null;
58}
59
4360export default function App() {
4461 const colorScheme = useColorScheme();
4562 const statusBarBg = useThemeColor("--color-status-bar");
4663
47 useEffect(() => {
48 SplashScreen.hide();
49 }, []);
50
5164 return (
5265 <RegistryContext.Provider value={appAtomRegistry}>
5366 <CloudAuthProvider>
5467 <AppearancePreferencesProvider>
68 <SplashScreenCoordinator />
5569 <GestureHandlerRootView className="flex-1">
5670 <KeyboardProvider statusBarTranslucent>
5771 <SafeAreaProvider>
apps/mobile/src/features/home/HomeHeader.tsx+1-39
......@@ -1,8 +1,4 @@
1import type {
2 EnvironmentId,
3 SidebarProjectGroupingMode,
4 SidebarThreadSortOrder,
5} from "@t3tools/contracts";
1import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts";
62import type { MenuAction } from "@react-native-menu/menu";
73import { useAtomValue } from "@effect/atom-react";
84import { AsyncResult } from "effect/unstable/reactivity";
......@@ -28,7 +24,6 @@ import {
2824} from "./home-list-filter-menu";
2925import {
3026 hasCustomHomeListOptions,
31 PROJECT_GROUPING_OPTIONS,
3227 PROJECT_SORT_OPTIONS,
3328 THREAD_SORT_OPTIONS,
3429} from "./home-list-options";
......@@ -43,13 +38,11 @@ export function HomeHeader(props: {
4338 readonly selectedProjectKey: string | null;
4439 readonly projectSortOrder: HomeProjectSortOrder;
4540 readonly threadSortOrder: SidebarThreadSortOrder;
46 readonly projectGroupingMode: SidebarProjectGroupingMode;
4741 readonly onSearchQueryChange: (query: string) => void;
4842 readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void;
4943 readonly onProjectChange: (projectKey: string | null) => void;
5044 readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
5145 readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
52 readonly onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void;
5346 readonly onOpenSettings: () => void;
5447 readonly onStartNewTask: () => void;
5548}) {
......@@ -143,20 +136,10 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
143136 state: checkedMenuState(props.threadSortOrder === option.value),
144137 })),
145138 },
146 {
147 id: "project-grouping",
148 title: "Group projects",
149 subactions: PROJECT_GROUPING_OPTIONS.map((option) => ({
150 id: `project-grouping:${option.value}`,
151 title: option.label,
152 state: checkedMenuState(props.projectGroupingMode === option.value),
153 })),
154 },
155139 ] satisfies MenuAction[])),
156140 ],
157141 [
158142 props.environments,
159 props.projectGroupingMode,
160143 props.projectSortOrder,
161144 props.projects,
162145 props.selectedEnvironmentId,
......@@ -210,13 +193,6 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
210193 props.onThreadSortOrderChange(threadSort.value);
211194 return;
212195 }
213
214 const grouping = PROJECT_GROUPING_OPTIONS.find(
215 (option) => id === `project-grouping:${option.value}`,
216 );
217 if (grouping) {
218 props.onProjectGroupingModeChange(grouping.value);
219 }
220196 },
221197 [props],
222198 );
......@@ -480,20 +456,6 @@ function IosHomeHeader(props: HomeHeaderProps) {
480456 </NativeHeaderToolbar.MenuAction>
481457 ))}
482458 </NativeHeaderToolbar.Menu>
483
484 <NativeHeaderToolbar.Menu title="Group projects">
485 <NativeHeaderToolbar.Label>Group projects</NativeHeaderToolbar.Label>
486 {PROJECT_GROUPING_OPTIONS.map((option) => (
487 <NativeHeaderToolbar.MenuAction
488 key={option.value}
489 isOn={props.projectGroupingMode === option.value}
490 onPress={() => props.onProjectGroupingModeChange(option.value)}
491 subtitle={option.subtitle}
492 >
493 <NativeHeaderToolbar.Label>{option.label}</NativeHeaderToolbar.Label>
494 </NativeHeaderToolbar.MenuAction>
495 ))}
496 </NativeHeaderToolbar.Menu>
497459 </NativeHeaderToolbar.Menu>
498460 <NativeHeaderToolbar.Spacer width={8} sharesBackground={false} />
499461 <NativeHeaderToolbar.SearchBarSlot />
apps/mobile/src/features/home/HomeRouteScreen.tsx-4
......@@ -54,7 +54,6 @@ export function HomeRouteScreen() {
5454 const {
5555 options: listOptions,
5656 setSelectedEnvironmentId,
57 setProjectGroupingMode,
5857 setProjectSortOrder,
5958 setThreadSortOrder,
6059 } = useHomeListOptions(availableEnvironmentIds);
......@@ -119,11 +118,9 @@ export function HomeRouteScreen() {
119118 selectedProjectKey={selectedProjectKey}
120119 projectSortOrder={listOptions.projectSortOrder}
121120 threadSortOrder={listOptions.threadSortOrder}
122 projectGroupingMode={listOptions.projectGroupingMode}
123121 onEnvironmentChange={setSelectedEnvironmentId}
124122 onProjectChange={setSelectedProjectKey}
125123 onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}
126 onProjectGroupingModeChange={setProjectGroupingMode}
127124 onProjectSortOrderChange={setProjectSortOrder}
128125 onSearchQueryChange={setSearchQuery}
129126 onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
......@@ -146,7 +143,6 @@ export function HomeRouteScreen() {
146143 navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" })
147144 }
148145 onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}
149 onProjectGroupingModeChange={setProjectGroupingMode}
150146 onProjectSortOrderChange={setProjectSortOrder}
151147 onSearchQueryChange={setSearchQuery}
152148 onSelectThread={(thread) => {
apps/mobile/src/features/home/HomeScreen.tsx-1
......@@ -82,7 +82,6 @@ interface HomeScreenProps {
8282 readonly onProjectChange: (projectKey: string | null) => void;
8383 readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
8484 readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
85 readonly onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void;
8685 readonly onAddConnection: () => void;
8786 readonly onOpenEnvironments: () => void;
8887 readonly onOpenSettings: () => void;
apps/mobile/src/features/home/home-list-filter-menu.test.ts-2
......@@ -15,12 +15,10 @@ describe("buildHomeListFilterMenu", () => {
1515 selectedProjectKey: "environment-1:project-1",
1616 projectSortOrder: "updated_at",
1717 threadSortOrder: "updated_at",
18 projectGroupingMode: "repository",
1918 onEnvironmentChange: vi.fn(),
2019 onProjectChange,
2120 onProjectSortOrderChange: vi.fn(),
2221 onThreadSortOrderChange: vi.fn(),
23 onProjectGroupingModeChange: vi.fn(),
2422 });
2523
2624 const projectMenu = menu.items.find(
apps/mobile/src/features/home/home-list-filter-menu.ts+2-23
......@@ -1,15 +1,7 @@
1import type {
2 EnvironmentId,
3 SidebarProjectGroupingMode,
4 SidebarThreadSortOrder,
5} from "@t3tools/contracts";
1import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts";
62
73import type { HomeProjectSortOrder } from "./homeThreadList";
8import {
9 PROJECT_GROUPING_OPTIONS,
10 PROJECT_SORT_OPTIONS,
11 THREAD_SORT_OPTIONS,
12} from "./home-list-options";
4import { PROJECT_SORT_OPTIONS, THREAD_SORT_OPTIONS } from "./home-list-options";
135
146export interface HomeListFilterMenuEnvironment {
157 readonly environmentId: EnvironmentId;
......@@ -47,12 +39,10 @@ export function buildHomeListFilterMenu(props: {
4739 readonly selectedProjectKey: string | null;
4840 readonly projectSortOrder: HomeProjectSortOrder;
4941 readonly threadSortOrder: SidebarThreadSortOrder;
50 readonly projectGroupingMode: SidebarProjectGroupingMode;
5142 readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void;
5243 readonly onProjectChange: (projectKey: string | null) => void;
5344 readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
5445 readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
55 readonly onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void;
5646 readonly onOpenSettings?: () => void;
5747 /** False hides the sort/group submenus. Thread List v2 uses a fixed
5848 creation-order layout, so offering those controls while it silently
......@@ -136,17 +126,6 @@ export function buildHomeListFilterMenu(props: {
136126 onPress: () => props.onThreadSortOrderChange(option.value),
137127 })),
138128 },
139 {
140 type: "submenu",
141 title: "Group projects",
142 items: PROJECT_GROUPING_OPTIONS.map((option) => ({
143 type: "action",
144 title: option.label,
145 subtitle: option.subtitle,
146 state: props.projectGroupingMode === option.value ? "on" : "off",
147 onPress: () => props.onProjectGroupingModeChange(option.value),
148 })),
149 },
150129 );
151130 }
152131
apps/mobile/src/features/home/home-list-options.test.ts+1-4
......@@ -1,5 +1,4 @@
11import {
2 DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE,
32 DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,
43 DEFAULT_SIDEBAR_THREAD_SORT_ORDER,
54} from "@t3tools/contracts";
......@@ -14,7 +13,6 @@ const defaults: HomeListOptions = {
1413 ? "updated_at"
1514 : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,
1615 threadSortOrder: DEFAULT_SIDEBAR_THREAD_SORT_ORDER,
17 projectGroupingMode: DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE,
1816};
1917
2018describe("home list options", () => {
......@@ -22,11 +20,10 @@ describe("home list options", () => {
2220 expect(hasCustomHomeListOptions(defaults)).toBe(false);
2321 });
2422
25 it("marks environment filters and grouping changes as customized", () => {
23 it("marks environment filters as customized", () => {
2624 expect(
2725 hasCustomHomeListOptions({ ...defaults, selectedEnvironmentId: "environment-1" as never }),
2826 ).toBe(true);
29 expect(hasCustomHomeListOptions({ ...defaults, projectGroupingMode: "separate" })).toBe(true);
3027 expect(
3128 hasCustomHomeListOptions({ ...defaults, selectedProjectKey: "environment-1:project-1" }),
3229 ).toBe(true);
apps/mobile/src/features/home/home-list-options.ts+27-35
......@@ -4,7 +4,6 @@ import type {
44 SidebarThreadSortOrder,
55} from "@t3tools/contracts";
66import {
7 DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE,
87 DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,
98 DEFAULT_SIDEBAR_THREAD_SORT_ORDER,
109} from "@t3tools/contracts";
......@@ -26,9 +25,18 @@ export interface HomeListOptions {
2625 readonly selectedEnvironmentId: EnvironmentId | null;
2726 readonly projectSortOrder: HomeProjectSortOrder;
2827 readonly threadSortOrder: SidebarThreadSortOrder;
28}
29
30export interface ResolvedHomeListOptions extends HomeListOptions {
2931 readonly projectGroupingMode: SidebarProjectGroupingMode;
3032}
3133
34export function resolveProjectGroupingMode(
35 projectGroupingEnabled: boolean | undefined,
36): SidebarProjectGroupingMode {
37 return projectGroupingEnabled === false ? "separate" : "repository";
38}
39
3240export const PROJECT_SORT_OPTIONS: ReadonlyArray<{
3341 readonly value: HomeProjectSortOrder;
3442 readonly label: string;
......@@ -45,28 +53,6 @@ export const THREAD_SORT_OPTIONS: ReadonlyArray<{
4553 { value: "created_at", label: "Created at" },
4654];
4755
48export const PROJECT_GROUPING_OPTIONS: ReadonlyArray<{
49 readonly value: SidebarProjectGroupingMode;
50 readonly label: string;
51 readonly subtitle: string;
52}> = [
53 {
54 value: "repository",
55 label: "Group by repository",
56 subtitle: "Combine matching repositories across environments",
57 },
58 {
59 value: "repository_path",
60 label: "Group by repository path",
61 subtitle: "Combine only matching paths within a repository",
62 },
63 {
64 value: "separate",
65 label: "Keep separate",
66 subtitle: "Show every project path separately",
67 },
68];
69
7056function defaultHomeListOptions(): HomeListOptions {
7157 return {
7258 selectedEnvironmentId: null,
......@@ -75,26 +61,34 @@ function defaultHomeListOptions(): HomeListOptions {
7561 ? "updated_at"
7662 : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,
7763 threadSortOrder: DEFAULT_SIDEBAR_THREAD_SORT_ORDER,
78 projectGroupingMode: DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE,
7964 };
8065}
8166
8267interface HomeListOptionsContextValue {
8368 readonly options: HomeListOptions;
8469 readonly setOptions: Dispatch<SetStateAction<HomeListOptions>>;
70 readonly projectGroupingMode: SidebarProjectGroupingMode;
8571}
8672
8773const HomeListOptionsContext = createContext<HomeListOptionsContextValue | null>(null);
8874
8975/** Keeps list preferences stable while the app moves between compact and split shells. */
90export function HomeListOptionsProvider({ children }: PropsWithChildren) {
76export function HomeListOptionsProvider({
77 children,
78 projectGroupingMode,
79}: PropsWithChildren<{ readonly projectGroupingMode: SidebarProjectGroupingMode }>) {
9180 const [options, setOptions] = useState<HomeListOptions>(defaultHomeListOptions);
92 const value = useMemo(() => ({ options, setOptions }), [options]);
81 const value = useMemo(
82 () => ({ options, setOptions, projectGroupingMode }),
83 [options, projectGroupingMode],
84 );
9385 return createElement(HomeListOptionsContext, { value }, children);
9486}
9587
9688export function hasCustomHomeListOptions(
97 options: HomeListOptions & { readonly selectedProjectKey?: string | null },
89 options: HomeListOptions & {
90 readonly selectedProjectKey?: string | null;
91 },
9892): boolean {
9993 const defaultProjectSortOrder =
10094 DEFAULT_SIDEBAR_PROJECT_SORT_ORDER === "manual"
......@@ -104,8 +98,7 @@ export function hasCustomHomeListOptions(
10498 options.selectedEnvironmentId !== null ||
10599 (options.selectedProjectKey !== null && options.selectedProjectKey !== undefined) ||
106100 options.projectSortOrder !== defaultProjectSortOrder ||
107 options.threadSortOrder !== DEFAULT_SIDEBAR_THREAD_SORT_ORDER ||
108 options.projectGroupingMode !== DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE
101 options.threadSortOrder !== DEFAULT_SIDEBAR_THREAD_SORT_ORDER
109102 );
110103}
111104
......@@ -119,10 +112,14 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet<Environm
119112 availableEnvironmentIds.has(options.selectedEnvironmentId)
120113 ? options.selectedEnvironmentId
121114 : null;
122 const resolvedOptions =
115 const availableOptions =
123116 selectedEnvironmentId === options.selectedEnvironmentId
124117 ? options
125118 : { ...options, selectedEnvironmentId };
119 const resolvedOptions: ResolvedHomeListOptions = {
120 ...availableOptions,
121 projectGroupingMode: shared?.projectGroupingMode ?? "repository",
122 };
126123
127124 const setSelectedEnvironmentId = useCallback((value: EnvironmentId | null) => {
128125 setOptions((current) => ({ ...current, selectedEnvironmentId: value }));
......@@ -133,15 +130,10 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet<Environm
133130 const setThreadSortOrder = useCallback((value: SidebarThreadSortOrder) => {
134131 setOptions((current) => ({ ...current, threadSortOrder: value }));
135132 }, []);
136 const setProjectGroupingMode = useCallback((value: SidebarProjectGroupingMode) => {
137 setOptions((current) => ({ ...current, projectGroupingMode: value }));
138 }, []);
139
140133 return {
141134 options: resolvedOptions,
142135 setSelectedEnvironmentId,
143136 setProjectSortOrder,
144137 setThreadSortOrder,
145 setProjectGroupingMode,
146138 } as const;
147139}
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+31-3
......@@ -2,7 +2,8 @@ import type {
22 EnvironmentProject,
33 EnvironmentThreadShell,
44} from "@t3tools/client-runtime/state/shell";
5import { EnvironmentId, ThreadId } from "@t3tools/contracts";
5import { EnvironmentId, ThreadId, type SidebarProjectGroupingMode } from "@t3tools/contracts";
6import { useAtomValue } from "@effect/atom-react";
67import { useFocusEffect } from "@react-navigation/native";
78import {
89 NavigationContext,
......@@ -22,6 +23,7 @@ import {
2223} from "react";
2324import { useWindowDimensions, View } from "react-native";
2425import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
26import { AsyncResult } from "effect/unstable/reactivity";
2527
2628import {
2729 deriveFileInspectorPaneLayout,
......@@ -34,11 +36,12 @@ import {
3436} from "../../lib/layout";
3537import { resolveThreadSelectionNavigationAction } from "../../lib/adaptive-navigation";
3638import { scopedThreadKey } from "../../lib/scopedEntities";
39import { mobilePreferencesAtom } from "../../state/preferences";
3740import {
3841 parseActiveThreadPath,
3942 useHardwareKeyboardCommand,
4043} from "../keyboard/hardwareKeyboardCommands";
41import { HomeListOptionsProvider } from "../home/home-list-options";
44import { HomeListOptionsProvider, resolveProjectGroupingMode } from "../home/home-list-options";
4245import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar";
4346import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation";
4447import { WorkspaceInspectorPane } from "./workspace-inspector-pane";
......@@ -184,6 +187,31 @@ export function AdaptiveWorkspaceLayout(props: {
184187 readonly children: ReactNode;
185188 readonly pathname: string;
186189}) {
190 const preferencesResult = useAtomValue(mobilePreferencesAtom);
191 if (!AsyncResult.isSuccess(preferencesResult)) {
192 return AsyncResult.isFailure(preferencesResult) ? (
193 <AdaptiveWorkspaceLayoutContent {...props} projectGroupingMode="repository" />
194 ) : null;
195 }
196 return (
197 <AdaptiveWorkspaceLayoutContent
198 {...props}
199 projectGroupingMode={resolveProjectGroupingMode(
200 preferencesResult.value.projectGroupingEnabled,
201 )}
202 />
203 );
204}
205
206function AdaptiveWorkspaceLayoutContent(
207 props: {
208 readonly children: ReactNode;
209 readonly pathname: string;
210 } & {
211 readonly projectGroupingMode: SidebarProjectGroupingMode;
212 },
213) {
214 const projectGroupingMode = props.projectGroupingMode;
187215 const { width, height } = useWindowDimensions();
188216 const pathname = props.pathname;
189217 const navigation = useNavigation();
......@@ -474,7 +502,7 @@ export function AdaptiveWorkspaceLayout(props: {
474502 );
475503
476504 return (
477 <HomeListOptionsProvider>
505 <HomeListOptionsProvider projectGroupingMode={projectGroupingMode}>
478506 <AdaptiveWorkspaceContext.Provider value={contextValue}>
479507 <View testID="adaptive-workspace-layout" className="flex-1 flex-row">
480508 {shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? (
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+23
......@@ -119,6 +119,8 @@ function LocalSettingsRouteScreen() {
119119 />
120120 </SettingsSection>
121121
122 <GeneralSettingsSection />
123
122124 <SettingsSection title="Appearance">
123125 <SettingsRow icon="paintbrush" label="Appearance" target="SettingsAppearance" />
124126 </SettingsSection>
......@@ -504,6 +506,8 @@ function ConfiguredSettingsRouteScreen() {
504506 />
505507 </SettingsSection>
506508
509 <GeneralSettingsSection />
510
507511 <SettingsSection title="Appearance">
508512 <SettingsRow icon="paintbrush" label="Appearance" target="SettingsAppearance" />
509513 </SettingsSection>
......@@ -518,6 +522,25 @@ function ConfiguredSettingsRouteScreen() {
518522 );
519523}
520524
525function GeneralSettingsSection() {
526 const preferencesResult = useAtomValue(mobilePreferencesAtom);
527 const savePreferences = useAtomSet(updateMobilePreferencesAtom);
528 const projectGroupingEnabled = AsyncResult.isSuccess(preferencesResult)
529 ? preferencesResult.value.projectGroupingEnabled !== false
530 : true;
531
532 return (
533 <SettingsSection title="General">
534 <SettingsSwitchRow
535 icon="folder"
536 label="Project Grouping"
537 value={projectGroupingEnabled}
538 onValueChange={(value) => savePreferences({ projectGroupingEnabled: value })}
539 />
540 </SettingsSection>
541 );
542}
543
521544/**
522545 * Device-local beta toggles. Mobile has no client-settings sync, so this is
523546 * the counterpart of web's Settings → Beta backed by mobile preferences.
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+2-26
......@@ -33,7 +33,6 @@ import { useSavedRemoteConnections } from "../../state/use-remote-environment-re
3333import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";
3434import {
3535 hasCustomHomeListOptions,
36 PROJECT_GROUPING_OPTIONS,
3736 PROJECT_SORT_OPTIONS,
3837 THREAD_SORT_OPTIONS,
3938 useHomeListOptions,
......@@ -217,13 +216,8 @@ function ThreadNavigationSidebarPane(
217216 () => new Set(environments.map((environment) => environment.environmentId)),
218217 [environments],
219218 );
220 const {
221 options,
222 setSelectedEnvironmentId,
223 setProjectGroupingMode,
224 setProjectSortOrder,
225 setThreadSortOrder,
226 } = useHomeListOptions(availableEnvironmentIds);
219 const { options, setSelectedEnvironmentId, setProjectSortOrder, setThreadSortOrder } =
220 useHomeListOptions(availableEnvironmentIds);
227221 const [selectedProjectKey, setSelectedProjectKey] = useState<string | null>(null);
228222 const projectScopes = useMemo(
229223 () =>
......@@ -551,16 +545,6 @@ function ThreadNavigationSidebarPane(
551545 state: options.threadSortOrder === option.value ? "on" : "off",
552546 })),
553547 },
554 {
555 id: "project-grouping",
556 title: "Group projects",
557 subactions: PROJECT_GROUPING_OPTIONS.map((option) => ({
558 id: `project-grouping:${option.value}`,
559 title: option.label,
560 subtitle: option.subtitle,
561 state: options.projectGroupingMode === option.value ? "on" : "off",
562 })),
563 },
564548 ] satisfies MenuAction[])),
565549 ],
566550 [environments, options, projectFilterOptions, selectedProjectKey, threadListV2Enabled],
......@@ -604,15 +588,10 @@ function ThreadNavigationSidebarPane(
604588 setThreadSortOrder(threadSort.value);
605589 return;
606590 }
607 const grouping = PROJECT_GROUPING_OPTIONS.find(
608 (option) => `project-grouping:${option.value}` === event,
609 );
610 if (grouping) setProjectGroupingMode(grouping.value);
611591 },
612592 [
613593 environments,
614594 projectFilterOptions,
615 setProjectGroupingMode,
616595 setProjectSortOrder,
617596 setSelectedEnvironmentId,
618597 setThreadSortOrder,
......@@ -908,12 +887,10 @@ function ThreadNavigationSidebarPane(
908887 selectedProjectKey,
909888 projectSortOrder: options.projectSortOrder,
910889 threadSortOrder: options.threadSortOrder,
911 projectGroupingMode: options.projectGroupingMode,
912890 onEnvironmentChange: setSelectedEnvironmentId,
913891 onProjectChange: setSelectedProjectKey,
914892 onProjectSortOrderChange: setProjectSortOrder,
915893 onThreadSortOrderChange: setThreadSortOrder,
916 onProjectGroupingModeChange: setProjectGroupingMode,
917894 listOrganization: !threadListV2Enabled,
918895 }),
919896 [
......@@ -921,7 +898,6 @@ function ThreadNavigationSidebarPane(
921898 options,
922899 projectFilterOptions,
923900 selectedProjectKey,
924 setProjectGroupingMode,
925901 setProjectSortOrder,
926902 setSelectedEnvironmentId,
927903 setThreadSortOrder,
apps/mobile/src/persistence/mobile-preferences.ts+5
......@@ -22,6 +22,7 @@ export interface Preferences {
2222 readonly codeWordBreak?: boolean;
2323 readonly connectOnboardingOptOutAccounts?: ReadonlyArray<string>;
2424 readonly collapsedProjectGroups?: readonly string[];
25 readonly projectGroupingEnabled?: boolean;
2526 /**
2627 * Device-local mirror of the web beta's `sidebarV2Enabled`. Mobile has no
2728 * client-settings sync, so the flat v2 thread list is opted into per
......@@ -77,6 +78,7 @@ function sanitizePreferences(parsed: Preferences): Preferences {
7778 codeWordBreak?: boolean;
7879 connectOnboardingOptOutAccounts?: ReadonlyArray<string>;
7980 collapsedProjectGroups?: readonly string[];
81 projectGroupingEnabled?: boolean;
8082 threadListV2Enabled?: boolean;
8183 } = {};
8284
......@@ -104,6 +106,9 @@ function sanitizePreferences(parsed: Preferences): Preferences {
104106 (key): key is string => typeof key === "string",
105107 );
106108 }
109 if (typeof parsed.projectGroupingEnabled === "boolean") {
110 preferences.projectGroupingEnabled = parsed.projectGroupingEnabled;
111 }
107112 if (typeof parsed.threadListV2Enabled === "boolean") {
108113 preferences.threadListV2Enabled = parsed.threadListV2Enabled;
109114 }