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