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";...@@ -13,7 +13,10 @@ import { ConfirmDialogHost } from "./components/ConfirmDialogHost";
13import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider";13import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider";
14import { prepareNativeShowcaseCapture } from "./features/showcase/nativeShowcaseScene";14import { prepareNativeShowcaseCapture } from "./features/showcase/nativeShowcaseScene";
15import { IncomingShareProvider } from "./features/sharing/IncomingShareProvider";15import { IncomingShareProvider } from "./features/sharing/IncomingShareProvider";
16import { AppearancePreferencesProvider } from "./features/settings/appearance/AppearancePreferencesProvider";16import {
17 AppearancePreferencesProvider,
18 useAppearancePreferences,
19} from "./features/settings/appearance/AppearancePreferencesProvider";
17import { RootStack } from "./Stack";20import { RootStack } from "./Stack";
18import { appAtomRegistry } from "./state/atom-registry";21import { appAtomRegistry } from "./state/atom-registry";
19import { OverlayPortalHost } from "./components/OverlayPortal";22import { OverlayPortalHost } from "./components/OverlayPortal";
...@@ -26,6 +29,10 @@ if (process.env.EXPO_PUBLIC_SHOWCASE === "1") {...@@ -26,6 +29,10 @@ if (process.env.EXPO_PUBLIC_SHOWCASE === "1") {
26 prepareNativeShowcaseCapture();29 prepareNativeShowcaseCapture();
27}30}
2831
32void SplashScreen.preventAutoHideAsync().catch(() => {
33 // The native module can be unavailable in non-native test environments.
34});
35
29const appLinking = {36const appLinking = {
30 prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"],37 prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"],
31 // The Expo dev client launches the app via38 // The Expo dev client launches the app via
...@@ -40,18 +47,25 @@ const appLinking = {...@@ -40,18 +47,25 @@ const appLinking = {
4047
41const Navigation = createStaticNavigation(RootStack);48const 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
43export default function App() {60export default function App() {
44 const colorScheme = useColorScheme();61 const colorScheme = useColorScheme();
45 const statusBarBg = useThemeColor("--color-status-bar");62 const statusBarBg = useThemeColor("--color-status-bar");
4663
47 useEffect(() => {
48 SplashScreen.hide();
49 }, []);
50
51 return (64 return (
52 <RegistryContext.Provider value={appAtomRegistry}>65 <RegistryContext.Provider value={appAtomRegistry}>
53 <CloudAuthProvider>66 <CloudAuthProvider>
54 <AppearancePreferencesProvider>67 <AppearancePreferencesProvider>
68 <SplashScreenCoordinator />
55 <GestureHandlerRootView className="flex-1">69 <GestureHandlerRootView className="flex-1">
56 <KeyboardProvider statusBarTranslucent>70 <KeyboardProvider statusBarTranslucent>
57 <SafeAreaProvider>71 <SafeAreaProvider>
apps/mobile/src/features/home/HomeHeader.tsx+1-39
...@@ -1,8 +1,4 @@...@@ -1,8 +1,4 @@
1import type {1import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts";
2 EnvironmentId,
3 SidebarProjectGroupingMode,
4 SidebarThreadSortOrder,
5} from "@t3tools/contracts";
6import type { MenuAction } from "@react-native-menu/menu";2import type { MenuAction } from "@react-native-menu/menu";
7import { useAtomValue } from "@effect/atom-react";3import { useAtomValue } from "@effect/atom-react";
8import { AsyncResult } from "effect/unstable/reactivity";4import { AsyncResult } from "effect/unstable/reactivity";
...@@ -28,7 +24,6 @@ import {...@@ -28,7 +24,6 @@ import {
28} from "./home-list-filter-menu";24} from "./home-list-filter-menu";
29import {25import {
30 hasCustomHomeListOptions,26 hasCustomHomeListOptions,
31 PROJECT_GROUPING_OPTIONS,
32 PROJECT_SORT_OPTIONS,27 PROJECT_SORT_OPTIONS,
33 THREAD_SORT_OPTIONS,28 THREAD_SORT_OPTIONS,
34} from "./home-list-options";29} from "./home-list-options";
...@@ -43,13 +38,11 @@ export function HomeHeader(props: {...@@ -43,13 +38,11 @@ export function HomeHeader(props: {
43 readonly selectedProjectKey: string | null;38 readonly selectedProjectKey: string | null;
44 readonly projectSortOrder: HomeProjectSortOrder;39 readonly projectSortOrder: HomeProjectSortOrder;
45 readonly threadSortOrder: SidebarThreadSortOrder;40 readonly threadSortOrder: SidebarThreadSortOrder;
46 readonly projectGroupingMode: SidebarProjectGroupingMode;
47 readonly onSearchQueryChange: (query: string) => void;41 readonly onSearchQueryChange: (query: string) => void;
48 readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void;42 readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void;
49 readonly onProjectChange: (projectKey: string | null) => void;43 readonly onProjectChange: (projectKey: string | null) => void;
50 readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;44 readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
51 readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;45 readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
52 readonly onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void;
53 readonly onOpenSettings: () => void;46 readonly onOpenSettings: () => void;
54 readonly onStartNewTask: () => void;47 readonly onStartNewTask: () => void;
55}) {48}) {
...@@ -143,20 +136,10 @@ function AndroidHomeHeader(props: HomeHeaderProps) {...@@ -143,20 +136,10 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
143 state: checkedMenuState(props.threadSortOrder === option.value),136 state: checkedMenuState(props.threadSortOrder === option.value),
144 })),137 })),
145 },138 },
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 },
155 ] satisfies MenuAction[])),139 ] satisfies MenuAction[])),
156 ],140 ],
157 [141 [
158 props.environments,142 props.environments,
159 props.projectGroupingMode,
160 props.projectSortOrder,143 props.projectSortOrder,
161 props.projects,144 props.projects,
162 props.selectedEnvironmentId,145 props.selectedEnvironmentId,
...@@ -210,13 +193,6 @@ function AndroidHomeHeader(props: HomeHeaderProps) {...@@ -210,13 +193,6 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
210 props.onThreadSortOrderChange(threadSort.value);193 props.onThreadSortOrderChange(threadSort.value);
211 return;194 return;
212 }195 }
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 }
220 },196 },
221 [props],197 [props],
222 );198 );
...@@ -480,20 +456,6 @@ function IosHomeHeader(props: HomeHeaderProps) {...@@ -480,20 +456,6 @@ function IosHomeHeader(props: HomeHeaderProps) {
480 </NativeHeaderToolbar.MenuAction>456 </NativeHeaderToolbar.MenuAction>
481 ))}457 ))}
482 </NativeHeaderToolbar.Menu>458 </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>
497 </NativeHeaderToolbar.Menu>459 </NativeHeaderToolbar.Menu>
498 <NativeHeaderToolbar.Spacer width={8} sharesBackground={false} />460 <NativeHeaderToolbar.Spacer width={8} sharesBackground={false} />
499 <NativeHeaderToolbar.SearchBarSlot />461 <NativeHeaderToolbar.SearchBarSlot />
apps/mobile/src/features/home/HomeRouteScreen.tsx-4
...@@ -54,7 +54,6 @@ export function HomeRouteScreen() {...@@ -54,7 +54,6 @@ export function HomeRouteScreen() {
54 const {54 const {
55 options: listOptions,55 options: listOptions,
56 setSelectedEnvironmentId,56 setSelectedEnvironmentId,
57 setProjectGroupingMode,
58 setProjectSortOrder,57 setProjectSortOrder,
59 setThreadSortOrder,58 setThreadSortOrder,
60 } = useHomeListOptions(availableEnvironmentIds);59 } = useHomeListOptions(availableEnvironmentIds);
...@@ -119,11 +118,9 @@ export function HomeRouteScreen() {...@@ -119,11 +118,9 @@ export function HomeRouteScreen() {
119 selectedProjectKey={selectedProjectKey}118 selectedProjectKey={selectedProjectKey}
120 projectSortOrder={listOptions.projectSortOrder}119 projectSortOrder={listOptions.projectSortOrder}
121 threadSortOrder={listOptions.threadSortOrder}120 threadSortOrder={listOptions.threadSortOrder}
122 projectGroupingMode={listOptions.projectGroupingMode}
123 onEnvironmentChange={setSelectedEnvironmentId}121 onEnvironmentChange={setSelectedEnvironmentId}
124 onProjectChange={setSelectedProjectKey}122 onProjectChange={setSelectedProjectKey}
125 onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}123 onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}
126 onProjectGroupingModeChange={setProjectGroupingMode}
127 onProjectSortOrderChange={setProjectSortOrder}124 onProjectSortOrderChange={setProjectSortOrder}
128 onSearchQueryChange={setSearchQuery}125 onSearchQueryChange={setSearchQuery}
129 onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}126 onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
...@@ -146,7 +143,6 @@ export function HomeRouteScreen() {...@@ -146,7 +143,6 @@ export function HomeRouteScreen() {
146 navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" })143 navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" })
147 }144 }
148 onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}145 onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}
149 onProjectGroupingModeChange={setProjectGroupingMode}
150 onProjectSortOrderChange={setProjectSortOrder}146 onProjectSortOrderChange={setProjectSortOrder}
151 onSearchQueryChange={setSearchQuery}147 onSearchQueryChange={setSearchQuery}
152 onSelectThread={(thread) => {148 onSelectThread={(thread) => {
apps/mobile/src/features/home/HomeScreen.tsx-1
...@@ -82,7 +82,6 @@ interface HomeScreenProps {...@@ -82,7 +82,6 @@ interface HomeScreenProps {
82 readonly onProjectChange: (projectKey: string | null) => void;82 readonly onProjectChange: (projectKey: string | null) => void;
83 readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;83 readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
84 readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;84 readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
85 readonly onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void;
86 readonly onAddConnection: () => void;85 readonly onAddConnection: () => void;
87 readonly onOpenEnvironments: () => void;86 readonly onOpenEnvironments: () => void;
88 readonly onOpenSettings: () => void;87 readonly onOpenSettings: () => void;
apps/mobile/src/features/home/home-list-filter-menu.test.ts-2
...@@ -15,12 +15,10 @@ describe("buildHomeListFilterMenu", () => {...@@ -15,12 +15,10 @@ describe("buildHomeListFilterMenu", () => {
15 selectedProjectKey: "environment-1:project-1",15 selectedProjectKey: "environment-1:project-1",
16 projectSortOrder: "updated_at",16 projectSortOrder: "updated_at",
17 threadSortOrder: "updated_at",17 threadSortOrder: "updated_at",
18 projectGroupingMode: "repository",
19 onEnvironmentChange: vi.fn(),18 onEnvironmentChange: vi.fn(),
20 onProjectChange,19 onProjectChange,
21 onProjectSortOrderChange: vi.fn(),20 onProjectSortOrderChange: vi.fn(),
22 onThreadSortOrderChange: vi.fn(),21 onThreadSortOrderChange: vi.fn(),
23 onProjectGroupingModeChange: vi.fn(),
24 });22 });
2523
26 const projectMenu = menu.items.find(24 const projectMenu = menu.items.find(
apps/mobile/src/features/home/home-list-filter-menu.ts+2-23
...@@ -1,15 +1,7 @@...@@ -1,15 +1,7 @@
1import type {1import type { EnvironmentId, SidebarThreadSortOrder } from "@t3tools/contracts";
2 EnvironmentId,
3 SidebarProjectGroupingMode,
4 SidebarThreadSortOrder,
5} from "@t3tools/contracts";
62
7import type { HomeProjectSortOrder } from "./homeThreadList";3import type { HomeProjectSortOrder } from "./homeThreadList";
8import {4import { PROJECT_SORT_OPTIONS, THREAD_SORT_OPTIONS } from "./home-list-options";
9 PROJECT_GROUPING_OPTIONS,
10 PROJECT_SORT_OPTIONS,
11 THREAD_SORT_OPTIONS,
12} from "./home-list-options";
135
14export interface HomeListFilterMenuEnvironment {6export interface HomeListFilterMenuEnvironment {
15 readonly environmentId: EnvironmentId;7 readonly environmentId: EnvironmentId;
...@@ -47,12 +39,10 @@ export function buildHomeListFilterMenu(props: {...@@ -47,12 +39,10 @@ export function buildHomeListFilterMenu(props: {
47 readonly selectedProjectKey: string | null;39 readonly selectedProjectKey: string | null;
48 readonly projectSortOrder: HomeProjectSortOrder;40 readonly projectSortOrder: HomeProjectSortOrder;
49 readonly threadSortOrder: SidebarThreadSortOrder;41 readonly threadSortOrder: SidebarThreadSortOrder;
50 readonly projectGroupingMode: SidebarProjectGroupingMode;
51 readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void;42 readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void;
52 readonly onProjectChange: (projectKey: string | null) => void;43 readonly onProjectChange: (projectKey: string | null) => void;
53 readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;44 readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
54 readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;45 readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
55 readonly onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void;
56 readonly onOpenSettings?: () => void;46 readonly onOpenSettings?: () => void;
57 /** False hides the sort/group submenus. Thread List v2 uses a fixed47 /** False hides the sort/group submenus. Thread List v2 uses a fixed
58 creation-order layout, so offering those controls while it silently48 creation-order layout, so offering those controls while it silently
...@@ -136,17 +126,6 @@ export function buildHomeListFilterMenu(props: {...@@ -136,17 +126,6 @@ export function buildHomeListFilterMenu(props: {
136 onPress: () => props.onThreadSortOrderChange(option.value),126 onPress: () => props.onThreadSortOrderChange(option.value),
137 })),127 })),
138 },128 },
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 },
150 );129 );
151 }130 }
152131
apps/mobile/src/features/home/home-list-options.test.ts+1-4
...@@ -1,5 +1,4 @@...@@ -1,5 +1,4 @@
1import {1import {
2 DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE,
3 DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,2 DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,
4 DEFAULT_SIDEBAR_THREAD_SORT_ORDER,3 DEFAULT_SIDEBAR_THREAD_SORT_ORDER,
5} from "@t3tools/contracts";4} from "@t3tools/contracts";
...@@ -14,7 +13,6 @@ const defaults: HomeListOptions = {...@@ -14,7 +13,6 @@ const defaults: HomeListOptions = {
14 ? "updated_at"13 ? "updated_at"
15 : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,14 : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,
16 threadSortOrder: DEFAULT_SIDEBAR_THREAD_SORT_ORDER,15 threadSortOrder: DEFAULT_SIDEBAR_THREAD_SORT_ORDER,
17 projectGroupingMode: DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE,
18};16};
1917
20describe("home list options", () => {18describe("home list options", () => {
...@@ -22,11 +20,10 @@ describe("home list options", () => {...@@ -22,11 +20,10 @@ describe("home list options", () => {
22 expect(hasCustomHomeListOptions(defaults)).toBe(false);20 expect(hasCustomHomeListOptions(defaults)).toBe(false);
23 });21 });
2422
25 it("marks environment filters and grouping changes as customized", () => {23 it("marks environment filters as customized", () => {
26 expect(24 expect(
27 hasCustomHomeListOptions({ ...defaults, selectedEnvironmentId: "environment-1" as never }),25 hasCustomHomeListOptions({ ...defaults, selectedEnvironmentId: "environment-1" as never }),
28 ).toBe(true);26 ).toBe(true);
29 expect(hasCustomHomeListOptions({ ...defaults, projectGroupingMode: "separate" })).toBe(true);
30 expect(27 expect(
31 hasCustomHomeListOptions({ ...defaults, selectedProjectKey: "environment-1:project-1" }),28 hasCustomHomeListOptions({ ...defaults, selectedProjectKey: "environment-1:project-1" }),
32 ).toBe(true);29 ).toBe(true);
apps/mobile/src/features/home/home-list-options.ts+27-35
...@@ -4,7 +4,6 @@ import type {...@@ -4,7 +4,6 @@ import type {
4 SidebarThreadSortOrder,4 SidebarThreadSortOrder,
5} from "@t3tools/contracts";5} from "@t3tools/contracts";
6import {6import {
7 DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE,
8 DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,7 DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,
9 DEFAULT_SIDEBAR_THREAD_SORT_ORDER,8 DEFAULT_SIDEBAR_THREAD_SORT_ORDER,
10} from "@t3tools/contracts";9} from "@t3tools/contracts";
...@@ -26,9 +25,18 @@ export interface HomeListOptions {...@@ -26,9 +25,18 @@ export interface HomeListOptions {
26 readonly selectedEnvironmentId: EnvironmentId | null;25 readonly selectedEnvironmentId: EnvironmentId | null;
27 readonly projectSortOrder: HomeProjectSortOrder;26 readonly projectSortOrder: HomeProjectSortOrder;
28 readonly threadSortOrder: SidebarThreadSortOrder;27 readonly threadSortOrder: SidebarThreadSortOrder;
28}
29
30export interface ResolvedHomeListOptions extends HomeListOptions {
29 readonly projectGroupingMode: SidebarProjectGroupingMode;31 readonly projectGroupingMode: SidebarProjectGroupingMode;
30}32}
3133
34export function resolveProjectGroupingMode(
35 projectGroupingEnabled: boolean | undefined,
36): SidebarProjectGroupingMode {
37 return projectGroupingEnabled === false ? "separate" : "repository";
38}
39
32export const PROJECT_SORT_OPTIONS: ReadonlyArray<{40export const PROJECT_SORT_OPTIONS: ReadonlyArray<{
33 readonly value: HomeProjectSortOrder;41 readonly value: HomeProjectSortOrder;
34 readonly label: string;42 readonly label: string;
...@@ -45,28 +53,6 @@ export const THREAD_SORT_OPTIONS: ReadonlyArray<{...@@ -45,28 +53,6 @@ export const THREAD_SORT_OPTIONS: ReadonlyArray<{
45 { value: "created_at", label: "Created at" },53 { value: "created_at", label: "Created at" },
46];54];
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
70function defaultHomeListOptions(): HomeListOptions {56function defaultHomeListOptions(): HomeListOptions {
71 return {57 return {
72 selectedEnvironmentId: null,58 selectedEnvironmentId: null,
...@@ -75,26 +61,34 @@ function defaultHomeListOptions(): HomeListOptions {...@@ -75,26 +61,34 @@ function defaultHomeListOptions(): HomeListOptions {
75 ? "updated_at"61 ? "updated_at"
76 : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,62 : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER,
77 threadSortOrder: DEFAULT_SIDEBAR_THREAD_SORT_ORDER,63 threadSortOrder: DEFAULT_SIDEBAR_THREAD_SORT_ORDER,
78 projectGroupingMode: DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE,
79 };64 };
80}65}
8166
82interface HomeListOptionsContextValue {67interface HomeListOptionsContextValue {
83 readonly options: HomeListOptions;68 readonly options: HomeListOptions;
84 readonly setOptions: Dispatch<SetStateAction<HomeListOptions>>;69 readonly setOptions: Dispatch<SetStateAction<HomeListOptions>>;
70 readonly projectGroupingMode: SidebarProjectGroupingMode;
85}71}
8672
87const HomeListOptionsContext = createContext<HomeListOptionsContextValue | null>(null);73const HomeListOptionsContext = createContext<HomeListOptionsContextValue | null>(null);
8874
89/** Keeps list preferences stable while the app moves between compact and split shells. */75/** 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 }>) {
91 const [options, setOptions] = useState<HomeListOptions>(defaultHomeListOptions);80 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 );
93 return createElement(HomeListOptionsContext, { value }, children);85 return createElement(HomeListOptionsContext, { value }, children);
94}86}
9587
96export function hasCustomHomeListOptions(88export function hasCustomHomeListOptions(
97 options: HomeListOptions & { readonly selectedProjectKey?: string | null },89 options: HomeListOptions & {
90 readonly selectedProjectKey?: string | null;
91 },
98): boolean {92): boolean {
99 const defaultProjectSortOrder =93 const defaultProjectSortOrder =
100 DEFAULT_SIDEBAR_PROJECT_SORT_ORDER === "manual"94 DEFAULT_SIDEBAR_PROJECT_SORT_ORDER === "manual"
...@@ -104,8 +98,7 @@ export function hasCustomHomeListOptions(...@@ -104,8 +98,7 @@ export function hasCustomHomeListOptions(
104 options.selectedEnvironmentId !== null ||98 options.selectedEnvironmentId !== null ||
105 (options.selectedProjectKey !== null && options.selectedProjectKey !== undefined) ||99 (options.selectedProjectKey !== null && options.selectedProjectKey !== undefined) ||
106 options.projectSortOrder !== defaultProjectSortOrder ||100 options.projectSortOrder !== defaultProjectSortOrder ||
107 options.threadSortOrder !== DEFAULT_SIDEBAR_THREAD_SORT_ORDER ||101 options.threadSortOrder !== DEFAULT_SIDEBAR_THREAD_SORT_ORDER
108 options.projectGroupingMode !== DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE
109 );102 );
110}103}
111104
...@@ -119,10 +112,14 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet<Environm...@@ -119,10 +112,14 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet<Environm
119 availableEnvironmentIds.has(options.selectedEnvironmentId)112 availableEnvironmentIds.has(options.selectedEnvironmentId)
120 ? options.selectedEnvironmentId113 ? options.selectedEnvironmentId
121 : null;114 : null;
122 const resolvedOptions =115 const availableOptions =
123 selectedEnvironmentId === options.selectedEnvironmentId116 selectedEnvironmentId === options.selectedEnvironmentId
124 ? options117 ? options
125 : { ...options, selectedEnvironmentId };118 : { ...options, selectedEnvironmentId };
119 const resolvedOptions: ResolvedHomeListOptions = {
120 ...availableOptions,
121 projectGroupingMode: shared?.projectGroupingMode ?? "repository",
122 };
126123
127 const setSelectedEnvironmentId = useCallback((value: EnvironmentId | null) => {124 const setSelectedEnvironmentId = useCallback((value: EnvironmentId | null) => {
128 setOptions((current) => ({ ...current, selectedEnvironmentId: value }));125 setOptions((current) => ({ ...current, selectedEnvironmentId: value }));
...@@ -133,15 +130,10 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet<Environm...@@ -133,15 +130,10 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet<Environm
133 const setThreadSortOrder = useCallback((value: SidebarThreadSortOrder) => {130 const setThreadSortOrder = useCallback((value: SidebarThreadSortOrder) => {
134 setOptions((current) => ({ ...current, threadSortOrder: value }));131 setOptions((current) => ({ ...current, threadSortOrder: value }));
135 }, []);132 }, []);
136 const setProjectGroupingMode = useCallback((value: SidebarProjectGroupingMode) => {
137 setOptions((current) => ({ ...current, projectGroupingMode: value }));
138 }, []);
139
140 return {133 return {
141 options: resolvedOptions,134 options: resolvedOptions,
142 setSelectedEnvironmentId,135 setSelectedEnvironmentId,
143 setProjectSortOrder,136 setProjectSortOrder,
144 setThreadSortOrder,137 setThreadSortOrder,
145 setProjectGroupingMode,
146 } as const;138 } as const;
147}139}
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+31-3
...@@ -2,7 +2,8 @@ import type {...@@ -2,7 +2,8 @@ import type {
2 EnvironmentProject,2 EnvironmentProject,
3 EnvironmentThreadShell,3 EnvironmentThreadShell,
4} from "@t3tools/client-runtime/state/shell";4} 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";
6import { useFocusEffect } from "@react-navigation/native";7import { useFocusEffect } from "@react-navigation/native";
7import {8import {
8 NavigationContext,9 NavigationContext,
...@@ -22,6 +23,7 @@ import {...@@ -22,6 +23,7 @@ import {
22} from "react";23} from "react";
23import { useWindowDimensions, View } from "react-native";24import { useWindowDimensions, View } from "react-native";
24import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";25import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated";
26import { AsyncResult } from "effect/unstable/reactivity";
2527
26import {28import {
27 deriveFileInspectorPaneLayout,29 deriveFileInspectorPaneLayout,
...@@ -34,11 +36,12 @@ import {...@@ -34,11 +36,12 @@ import {
34} from "../../lib/layout";36} from "../../lib/layout";
35import { resolveThreadSelectionNavigationAction } from "../../lib/adaptive-navigation";37import { resolveThreadSelectionNavigationAction } from "../../lib/adaptive-navigation";
36import { scopedThreadKey } from "../../lib/scopedEntities";38import { scopedThreadKey } from "../../lib/scopedEntities";
39import { mobilePreferencesAtom } from "../../state/preferences";
37import {40import {
38 parseActiveThreadPath,41 parseActiveThreadPath,
39 useHardwareKeyboardCommand,42 useHardwareKeyboardCommand,
40} from "../keyboard/hardwareKeyboardCommands";43} from "../keyboard/hardwareKeyboardCommands";
41import { HomeListOptionsProvider } from "../home/home-list-options";44import { HomeListOptionsProvider, resolveProjectGroupingMode } from "../home/home-list-options";
42import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar";45import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar";
43import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation";46import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation";
44import { WorkspaceInspectorPane } from "./workspace-inspector-pane";47import { WorkspaceInspectorPane } from "./workspace-inspector-pane";
...@@ -184,6 +187,31 @@ export function AdaptiveWorkspaceLayout(props: {...@@ -184,6 +187,31 @@ export function AdaptiveWorkspaceLayout(props: {
184 readonly children: ReactNode;187 readonly children: ReactNode;
185 readonly pathname: string;188 readonly pathname: string;
186}) {189}) {
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;
187 const { width, height } = useWindowDimensions();215 const { width, height } = useWindowDimensions();
188 const pathname = props.pathname;216 const pathname = props.pathname;
189 const navigation = useNavigation();217 const navigation = useNavigation();
...@@ -474,7 +502,7 @@ export function AdaptiveWorkspaceLayout(props: {...@@ -474,7 +502,7 @@ export function AdaptiveWorkspaceLayout(props: {
474 );502 );
475503
476 return (504 return (
477 <HomeListOptionsProvider>505 <HomeListOptionsProvider projectGroupingMode={projectGroupingMode}>
478 <AdaptiveWorkspaceContext.Provider value={contextValue}>506 <AdaptiveWorkspaceContext.Provider value={contextValue}>
479 <View testID="adaptive-workspace-layout" className="flex-1 flex-row">507 <View testID="adaptive-workspace-layout" className="flex-1 flex-row">
480 {shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? (508 {shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? (
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+23
...@@ -119,6 +119,8 @@ function LocalSettingsRouteScreen() {...@@ -119,6 +119,8 @@ function LocalSettingsRouteScreen() {
119 />119 />
120 </SettingsSection>120 </SettingsSection>
121121
122 <GeneralSettingsSection />
123
122 <SettingsSection title="Appearance">124 <SettingsSection title="Appearance">
123 <SettingsRow icon="paintbrush" label="Appearance" target="SettingsAppearance" />125 <SettingsRow icon="paintbrush" label="Appearance" target="SettingsAppearance" />
124 </SettingsSection>126 </SettingsSection>
...@@ -504,6 +506,8 @@ function ConfiguredSettingsRouteScreen() {...@@ -504,6 +506,8 @@ function ConfiguredSettingsRouteScreen() {
504 />506 />
505 </SettingsSection>507 </SettingsSection>
506508
509 <GeneralSettingsSection />
510
507 <SettingsSection title="Appearance">511 <SettingsSection title="Appearance">
508 <SettingsRow icon="paintbrush" label="Appearance" target="SettingsAppearance" />512 <SettingsRow icon="paintbrush" label="Appearance" target="SettingsAppearance" />
509 </SettingsSection>513 </SettingsSection>
...@@ -518,6 +522,25 @@ function ConfiguredSettingsRouteScreen() {...@@ -518,6 +522,25 @@ function ConfiguredSettingsRouteScreen() {
518 );522 );
519}523}
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
521/**544/**
522 * Device-local beta toggles. Mobile has no client-settings sync, so this is545 * Device-local beta toggles. Mobile has no client-settings sync, so this is
523 * the counterpart of web's Settings → Beta backed by mobile preferences.546 * 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...@@ -33,7 +33,6 @@ import { useSavedRemoteConnections } from "../../state/use-remote-environment-re
33import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";33import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";
34import {34import {
35 hasCustomHomeListOptions,35 hasCustomHomeListOptions,
36 PROJECT_GROUPING_OPTIONS,
37 PROJECT_SORT_OPTIONS,36 PROJECT_SORT_OPTIONS,
38 THREAD_SORT_OPTIONS,37 THREAD_SORT_OPTIONS,
39 useHomeListOptions,38 useHomeListOptions,
...@@ -217,13 +216,8 @@ function ThreadNavigationSidebarPane(...@@ -217,13 +216,8 @@ function ThreadNavigationSidebarPane(
217 () => new Set(environments.map((environment) => environment.environmentId)),216 () => new Set(environments.map((environment) => environment.environmentId)),
218 [environments],217 [environments],
219 );218 );
220 const {219 const { options, setSelectedEnvironmentId, setProjectSortOrder, setThreadSortOrder } =
221 options,220 useHomeListOptions(availableEnvironmentIds);
222 setSelectedEnvironmentId,
223 setProjectGroupingMode,
224 setProjectSortOrder,
225 setThreadSortOrder,
226 } = useHomeListOptions(availableEnvironmentIds);
227 const [selectedProjectKey, setSelectedProjectKey] = useState<string | null>(null);221 const [selectedProjectKey, setSelectedProjectKey] = useState<string | null>(null);
228 const projectScopes = useMemo(222 const projectScopes = useMemo(
229 () =>223 () =>
...@@ -551,16 +545,6 @@ function ThreadNavigationSidebarPane(...@@ -551,16 +545,6 @@ function ThreadNavigationSidebarPane(
551 state: options.threadSortOrder === option.value ? "on" : "off",545 state: options.threadSortOrder === option.value ? "on" : "off",
552 })),546 })),
553 },547 },
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 },
564 ] satisfies MenuAction[])),548 ] satisfies MenuAction[])),
565 ],549 ],
566 [environments, options, projectFilterOptions, selectedProjectKey, threadListV2Enabled],550 [environments, options, projectFilterOptions, selectedProjectKey, threadListV2Enabled],
...@@ -604,15 +588,10 @@ function ThreadNavigationSidebarPane(...@@ -604,15 +588,10 @@ function ThreadNavigationSidebarPane(
604 setThreadSortOrder(threadSort.value);588 setThreadSortOrder(threadSort.value);
605 return;589 return;
606 }590 }
607 const grouping = PROJECT_GROUPING_OPTIONS.find(
608 (option) => `project-grouping:${option.value}` === event,
609 );
610 if (grouping) setProjectGroupingMode(grouping.value);
611 },591 },
612 [592 [
613 environments,593 environments,
614 projectFilterOptions,594 projectFilterOptions,
615 setProjectGroupingMode,
616 setProjectSortOrder,595 setProjectSortOrder,
617 setSelectedEnvironmentId,596 setSelectedEnvironmentId,
618 setThreadSortOrder,597 setThreadSortOrder,
...@@ -908,12 +887,10 @@ function ThreadNavigationSidebarPane(...@@ -908,12 +887,10 @@ function ThreadNavigationSidebarPane(
908 selectedProjectKey,887 selectedProjectKey,
909 projectSortOrder: options.projectSortOrder,888 projectSortOrder: options.projectSortOrder,
910 threadSortOrder: options.threadSortOrder,889 threadSortOrder: options.threadSortOrder,
911 projectGroupingMode: options.projectGroupingMode,
912 onEnvironmentChange: setSelectedEnvironmentId,890 onEnvironmentChange: setSelectedEnvironmentId,
913 onProjectChange: setSelectedProjectKey,891 onProjectChange: setSelectedProjectKey,
914 onProjectSortOrderChange: setProjectSortOrder,892 onProjectSortOrderChange: setProjectSortOrder,
915 onThreadSortOrderChange: setThreadSortOrder,893 onThreadSortOrderChange: setThreadSortOrder,
916 onProjectGroupingModeChange: setProjectGroupingMode,
917 listOrganization: !threadListV2Enabled,894 listOrganization: !threadListV2Enabled,
918 }),895 }),
919 [896 [
...@@ -921,7 +898,6 @@ function ThreadNavigationSidebarPane(...@@ -921,7 +898,6 @@ function ThreadNavigationSidebarPane(
921 options,898 options,
922 projectFilterOptions,899 projectFilterOptions,
923 selectedProjectKey,900 selectedProjectKey,
924 setProjectGroupingMode,
925 setProjectSortOrder,901 setProjectSortOrder,
926 setSelectedEnvironmentId,902 setSelectedEnvironmentId,
927 setThreadSortOrder,903 setThreadSortOrder,
apps/mobile/src/persistence/mobile-preferences.ts+5
...@@ -22,6 +22,7 @@ export interface Preferences {...@@ -22,6 +22,7 @@ export interface Preferences {
22 readonly codeWordBreak?: boolean;22 readonly codeWordBreak?: boolean;
23 readonly connectOnboardingOptOutAccounts?: ReadonlyArray<string>;23 readonly connectOnboardingOptOutAccounts?: ReadonlyArray<string>;
24 readonly collapsedProjectGroups?: readonly string[];24 readonly collapsedProjectGroups?: readonly string[];
25 readonly projectGroupingEnabled?: boolean;
25 /**26 /**
26 * Device-local mirror of the web beta's `sidebarV2Enabled`. Mobile has no27 * Device-local mirror of the web beta's `sidebarV2Enabled`. Mobile has no
27 * client-settings sync, so the flat v2 thread list is opted into per28 * client-settings sync, so the flat v2 thread list is opted into per
...@@ -77,6 +78,7 @@ function sanitizePreferences(parsed: Preferences): Preferences {...@@ -77,6 +78,7 @@ function sanitizePreferences(parsed: Preferences): Preferences {
77 codeWordBreak?: boolean;78 codeWordBreak?: boolean;
78 connectOnboardingOptOutAccounts?: ReadonlyArray<string>;79 connectOnboardingOptOutAccounts?: ReadonlyArray<string>;
79 collapsedProjectGroups?: readonly string[];80 collapsedProjectGroups?: readonly string[];
81 projectGroupingEnabled?: boolean;
80 threadListV2Enabled?: boolean;82 threadListV2Enabled?: boolean;
81 } = {};83 } = {};
8284
...@@ -104,6 +106,9 @@ function sanitizePreferences(parsed: Preferences): Preferences {...@@ -104,6 +106,9 @@ function sanitizePreferences(parsed: Preferences): Preferences {
104 (key): key is string => typeof key === "string",106 (key): key is string => typeof key === "string",
105 );107 );
106 }108 }
109 if (typeof parsed.projectGroupingEnabled === "boolean") {
110 preferences.projectGroupingEnabled = parsed.projectGroupingEnabled;
111 }
107 if (typeof parsed.threadListV2Enabled === "boolean") {112 if (typeof parsed.threadListV2Enabled === "boolean") {
108 preferences.threadListV2Enabled = parsed.threadListV2Enabled;113 preferences.threadListV2Enabled = parsed.threadListV2Enabled;
109 }114 }