authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-17 20:20:12-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-17 20:20:12-07:00
log1a2a03f44b7ef6e3159a67a262c1586dbd3d3d5b
treedcef67997eb5df90efaa4010b8bccc1c0fe421ee
parent18dcc43ace0a7b0d93f2fd61907b2a4e27522936
signature Signed by PGP key B5690EEEBB952194

refactor(mobile): share local environment lists (#12365)


3 files changed, 81 insertions(+), 83 deletions(-)

apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx+10-41
......@@ -1,17 +1,14 @@
11import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
22import { NativeHeaderToolbar } from "../../native/StackHeader";
33import { useNavigation } from "@react-navigation/native";
4import { SymbolView } from "../../components/AppSymbol";
54import type { EnvironmentId } from "@t3tools/contracts";
65import { useCallback, useState } from "react";
76import { Platform, View } from "react-native";
87import { useSafeAreaInsets } from "react-native-safe-area-context";
98
109import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
11import { AppText as Text } from "../../components/AppText";
12import { cn } from "../../lib/cn";
1310import { useRemoteConnections } from "../../state/use-remote-environment-registry";
14import { ConnectionEnvironmentRow } from "./ConnectionEnvironmentRow";
11import { LocalEnvironmentList } from "./LocalEnvironmentList";
1512import { GitHubRoutingSettings } from "./GitHubRoutingSettings";
1613
1714export function ConnectionsRouteScreen() {
......@@ -24,7 +21,6 @@ export function ConnectionsRouteScreen() {
2421 } = useRemoteConnections();
2522 const navigation = useNavigation();
2623 const insets = useSafeAreaInsets();
27 const hasEnvironments = connectedEnvironments.length > 0;
2824 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);
2925 const handleToggle = useCallback((environmentId: EnvironmentId) => {
3026 setExpandedId((prev) => (prev === environmentId ? null : environmentId));
......@@ -63,42 +59,15 @@ export function ConnectionsRouteScreen() {
6359 paddingTop: 16,
6460 }}
6561 >
66 {hasEnvironments ? (
67 <View collapsable={false} className="overflow-hidden rounded-[24px] bg-card">
68 {connectedEnvironments.map((environment, index) => (
69 <View
70 key={environment.environmentId}
71 collapsable={false}
72 className={cn(index !== 0 && "border-t border-border")}
73 >
74 <ConnectionEnvironmentRow
75 environment={environment}
76 expanded={expandedId === environment.environmentId}
77 onToggle={() => handleToggle(environment.environmentId)}
78 onReconnect={onReconnectEnvironment}
79 onRemove={onRemoveEnvironmentPress}
80 onSetEnabled={onSetEnvironmentEnabled}
81 onUpdate={onUpdateEnvironment}
82 />
83 </View>
84 ))}
85 </View>
86 ) : (
87 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card px-6 py-8">
88 <View className="h-12 w-12 items-center justify-center rounded-[16px] bg-subtle">
89 <SymbolView
90 name="point.3.connected.trianglepath.dotted"
91 size={20}
92 tintColorClassName={"accent-icon-muted"}
93 type="monochrome"
94 />
95 </View>
96 <Text className="text-center text-sm leading-normal text-foreground-muted">
97 No environments connected yet.{"\n"}Tap{" "}
98 <Text className="font-t3-bold text-foreground">+</Text> to add one.
99 </Text>
100 </View>
101 )}
62 <LocalEnvironmentList
63 environments={connectedEnvironments}
64 expandedId={expandedId}
65 onToggle={handleToggle}
66 onReconnect={onReconnectEnvironment}
67 onRemove={onRemoveEnvironmentPress}
68 onSetEnabled={onSetEnvironmentEnabled}
69 onUpdate={onUpdateEnvironment}
70 />
10271 <GitHubRoutingSettings />
10372 </ScrollView>
10473 </View>
apps/mobile/src/features/connection/LocalEnvironmentList.tsx created+60
......@@ -0,0 +1,60 @@
1import type { EnvironmentId } from "@t3tools/contracts";
2import type { ComponentProps } from "react";
3import { View } from "react-native";
4
5import { SymbolView } from "../../components/AppSymbol";
6import { AppText as Text } from "../../components/AppText";
7import { cn } from "../../lib/cn";
8import { ConnectionEnvironmentRow } from "./ConnectionEnvironmentRow";
9
10type EnvironmentRowProps = ComponentProps<typeof ConnectionEnvironmentRow>;
11
12/** Shared list and empty state for environment management entry points. */
13export function LocalEnvironmentList({
14 environments,
15 expandedId,
16 onToggle,
17 ...rowActions
18}: Pick<EnvironmentRowProps, "onReconnect" | "onRemove" | "onSetEnabled" | "onUpdate"> & {
19 readonly environments: ReadonlyArray<EnvironmentRowProps["environment"]>;
20 readonly expandedId: EnvironmentId | null;
21 readonly onToggle: (environmentId: EnvironmentId) => void;
22}) {
23 if (environments.length === 0) {
24 return (
25 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card px-6 py-8">
26 <View className="h-12 w-12 items-center justify-center rounded-[16px] bg-subtle">
27 <SymbolView
28 name="point.3.connected.trianglepath.dotted"
29 size={20}
30 tintColorClassName="accent-icon-muted"
31 type="monochrome"
32 />
33 </View>
34 <Text className="text-center text-sm leading-normal text-foreground-muted">
35 No environments connected yet.{"\n"}Tap{" "}
36 <Text className="font-t3-bold text-foreground">+</Text> to add one.
37 </Text>
38 </View>
39 );
40 }
41
42 return (
43 <View collapsable={false} className="overflow-hidden rounded-[24px] bg-card">
44 {environments.map((environment, index) => (
45 <View
46 key={environment.environmentId}
47 collapsable={false}
48 className={cn(index !== 0 && "border-t border-border")}
49 >
50 <ConnectionEnvironmentRow
51 environment={environment}
52 expanded={expandedId === environment.environmentId}
53 onToggle={() => onToggle(environment.environmentId)}
54 {...rowActions}
55 />
56 </View>
57 ))}
58 </View>
59 );
60}
apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx+11-42
......@@ -1,19 +1,16 @@
11import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
22import { NativeHeaderToolbar } from "../../native/StackHeader";
33import { useNavigation } from "@react-navigation/native";
4import { SymbolView } from "../../components/AppSymbol";
54import type { EnvironmentId } from "@t3tools/contracts";
65import { useCallback, useState } from "react";
7import { Platform, View } from "react-native";
6import { Platform } from "react-native";
87import { useSafeAreaInsets } from "react-native-safe-area-context";
98
10import { AppText as Text } from "../../components/AppText";
119import { SettingsScreen } from "./components/SettingsScreen";
1210import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";
13import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow";
11import { LocalEnvironmentList } from "../connection/LocalEnvironmentList";
1412import { GitHubRoutingSettings } from "../connection/GitHubRoutingSettings";
1513import { splitEnvironmentSections } from "../connection/environmentSections";
16import { cn } from "../../lib/cn";
1714import { useUniwindTheme } from "../../lib/useUniwindTheme";
1815import { useRemoteConnections } from "../../state/use-remote-environment-registry";
1916import {
......@@ -45,7 +42,6 @@ export function SettingsEnvironmentsRouteScreen() {
4542 const connectedCloudEnvironments = SHOWCASE_ENABLED
4643 ? SHOWCASE_CONNECTED_CLOUD_ENVIRONMENTS
4744 : environmentSections.connectedCloudEnvironments;
48 const hasLocalEnvironments = localEnvironments.length > 0;
4945 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);
5046 const headerIconColor = useUniwindTheme()["--color-icon"];
5147
......@@ -118,42 +114,15 @@ export function SettingsEnvironmentsRouteScreen() {
118114 paddingBottom: Math.max(insets.bottom, 18) + 18,
119115 }}
120116 >
121 {hasLocalEnvironments ? (
122 <View collapsable={false} className="overflow-hidden rounded-[24px] bg-card">
123 {localEnvironments.map((environment, index) => (
124 <View
125 key={environment.environmentId}
126 collapsable={false}
127 className={cn(index !== 0 && "border-t border-border")}
128 >
129 <ConnectionEnvironmentRow
130 environment={environment}
131 expanded={expandedId === environment.environmentId}
132 onToggle={() => handleToggle(environment.environmentId)}
133 onReconnect={onReconnectEnvironment}
134 onRemove={onRemoveEnvironmentPress}
135 onSetEnabled={onSetEnvironmentEnabled}
136 onUpdate={handleUpdateEnvironment}
137 />
138 </View>
139 ))}
140 </View>
141 ) : (
142 <View collapsable={false} className="items-center gap-3 rounded-[24px] bg-card px-6 py-8">
143 <View className="h-12 w-12 items-center justify-center rounded-[16px] bg-subtle">
144 <SymbolView
145 name="point.3.connected.trianglepath.dotted"
146 size={20}
147 tintColorClassName={"accent-icon-muted"}
148 type="monochrome"
149 />
150 </View>
151 <Text className="text-center text-sm leading-normal text-foreground-muted">
152 No environments connected yet.{"\n"}Tap{" "}
153 <Text className="font-t3-bold text-foreground">+</Text> to add one.
154 </Text>
155 </View>
156 )}
117 <LocalEnvironmentList
118 environments={localEnvironments}
119 expandedId={expandedId}
120 onToggle={handleToggle}
121 onReconnect={onReconnectEnvironment}
122 onRemove={onRemoveEnvironmentPress}
123 onSetEnabled={onSetEnvironmentEnabled}
124 onUpdate={handleUpdateEnvironment}
125 />
157126
158127 {/* Always mounted: already-connected relay environments must stay
159128 visible (and removable) even when cloud config is missing or the