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 @@...@@ -1,17 +1,14 @@
1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
2import { NativeHeaderToolbar } from "../../native/StackHeader";2import { NativeHeaderToolbar } from "../../native/StackHeader";
3import { useNavigation } from "@react-navigation/native";3import { useNavigation } from "@react-navigation/native";
4import { SymbolView } from "../../components/AppSymbol";
5import type { EnvironmentId } from "@t3tools/contracts";4import type { EnvironmentId } from "@t3tools/contracts";
6import { useCallback, useState } from "react";5import { useCallback, useState } from "react";
7import { Platform, View } from "react-native";6import { Platform, View } from "react-native";
8import { useSafeAreaInsets } from "react-native-safe-area-context";7import { useSafeAreaInsets } from "react-native-safe-area-context";
98
10import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";9import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
11import { AppText as Text } from "../../components/AppText";
12import { cn } from "../../lib/cn";
13import { useRemoteConnections } from "../../state/use-remote-environment-registry";10import { useRemoteConnections } from "../../state/use-remote-environment-registry";
14import { ConnectionEnvironmentRow } from "./ConnectionEnvironmentRow";11import { LocalEnvironmentList } from "./LocalEnvironmentList";
15import { GitHubRoutingSettings } from "./GitHubRoutingSettings";12import { GitHubRoutingSettings } from "./GitHubRoutingSettings";
1613
17export function ConnectionsRouteScreen() {14export function ConnectionsRouteScreen() {
...@@ -24,7 +21,6 @@ export function ConnectionsRouteScreen() {...@@ -24,7 +21,6 @@ export function ConnectionsRouteScreen() {
24 } = useRemoteConnections();21 } = useRemoteConnections();
25 const navigation = useNavigation();22 const navigation = useNavigation();
26 const insets = useSafeAreaInsets();23 const insets = useSafeAreaInsets();
27 const hasEnvironments = connectedEnvironments.length > 0;
28 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);24 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);
29 const handleToggle = useCallback((environmentId: EnvironmentId) => {25 const handleToggle = useCallback((environmentId: EnvironmentId) => {
30 setExpandedId((prev) => (prev === environmentId ? null : environmentId));26 setExpandedId((prev) => (prev === environmentId ? null : environmentId));
...@@ -63,42 +59,15 @@ export function ConnectionsRouteScreen() {...@@ -63,42 +59,15 @@ export function ConnectionsRouteScreen() {
63 paddingTop: 16,59 paddingTop: 16,
64 }}60 }}
65 >61 >
66 {hasEnvironments ? (62 <LocalEnvironmentList
67 <View collapsable={false} className="overflow-hidden rounded-[24px] bg-card">63 environments={connectedEnvironments}
68 {connectedEnvironments.map((environment, index) => (64 expandedId={expandedId}
69 <View65 onToggle={handleToggle}
70 key={environment.environmentId}66 onReconnect={onReconnectEnvironment}
71 collapsable={false}67 onRemove={onRemoveEnvironmentPress}
72 className={cn(index !== 0 && "border-t border-border")}68 onSetEnabled={onSetEnvironmentEnabled}
73 >69 onUpdate={onUpdateEnvironment}
74 <ConnectionEnvironmentRow70 />
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 )}
102 <GitHubRoutingSettings />71 <GitHubRoutingSettings />
103 </ScrollView>72 </ScrollView>
104 </View>73 </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 @@...@@ -1,19 +1,16 @@
1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";1import { ScreenScrollView as ScrollView } from "../../components/ScreenScrollView";
2import { NativeHeaderToolbar } from "../../native/StackHeader";2import { NativeHeaderToolbar } from "../../native/StackHeader";
3import { useNavigation } from "@react-navigation/native";3import { useNavigation } from "@react-navigation/native";
4import { SymbolView } from "../../components/AppSymbol";
5import type { EnvironmentId } from "@t3tools/contracts";4import type { EnvironmentId } from "@t3tools/contracts";
6import { useCallback, useState } from "react";5import { useCallback, useState } from "react";
7import { Platform, View } from "react-native";6import { Platform } from "react-native";
8import { useSafeAreaInsets } from "react-native-safe-area-context";7import { useSafeAreaInsets } from "react-native-safe-area-context";
98
10import { AppText as Text } from "../../components/AppText";
11import { SettingsScreen } from "./components/SettingsScreen";9import { SettingsScreen } from "./components/SettingsScreen";
12import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";10import { CloudEnvironmentRows } from "../connection/CloudEnvironmentRows";
13import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow";11import { LocalEnvironmentList } from "../connection/LocalEnvironmentList";
14import { GitHubRoutingSettings } from "../connection/GitHubRoutingSettings";12import { GitHubRoutingSettings } from "../connection/GitHubRoutingSettings";
15import { splitEnvironmentSections } from "../connection/environmentSections";13import { splitEnvironmentSections } from "../connection/environmentSections";
16import { cn } from "../../lib/cn";
17import { useUniwindTheme } from "../../lib/useUniwindTheme";14import { useUniwindTheme } from "../../lib/useUniwindTheme";
18import { useRemoteConnections } from "../../state/use-remote-environment-registry";15import { useRemoteConnections } from "../../state/use-remote-environment-registry";
19import {16import {
...@@ -45,7 +42,6 @@ export function SettingsEnvironmentsRouteScreen() {...@@ -45,7 +42,6 @@ export function SettingsEnvironmentsRouteScreen() {
45 const connectedCloudEnvironments = SHOWCASE_ENABLED42 const connectedCloudEnvironments = SHOWCASE_ENABLED
46 ? SHOWCASE_CONNECTED_CLOUD_ENVIRONMENTS43 ? SHOWCASE_CONNECTED_CLOUD_ENVIRONMENTS
47 : environmentSections.connectedCloudEnvironments;44 : environmentSections.connectedCloudEnvironments;
48 const hasLocalEnvironments = localEnvironments.length > 0;
49 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);45 const [expandedId, setExpandedId] = useState<EnvironmentId | null>(null);
50 const headerIconColor = useUniwindTheme()["--color-icon"];46 const headerIconColor = useUniwindTheme()["--color-icon"];
5147
...@@ -118,42 +114,15 @@ export function SettingsEnvironmentsRouteScreen() {...@@ -118,42 +114,15 @@ export function SettingsEnvironmentsRouteScreen() {
118 paddingBottom: Math.max(insets.bottom, 18) + 18,114 paddingBottom: Math.max(insets.bottom, 18) + 18,
119 }}115 }}
120 >116 >
121 {hasLocalEnvironments ? (117 <LocalEnvironmentList
122 <View collapsable={false} className="overflow-hidden rounded-[24px] bg-card">118 environments={localEnvironments}
123 {localEnvironments.map((environment, index) => (119 expandedId={expandedId}
124 <View120 onToggle={handleToggle}
125 key={environment.environmentId}121 onReconnect={onReconnectEnvironment}
126 collapsable={false}122 onRemove={onRemoveEnvironmentPress}
127 className={cn(index !== 0 && "border-t border-border")}123 onSetEnabled={onSetEnvironmentEnabled}
128 >124 onUpdate={handleUpdateEnvironment}
129 <ConnectionEnvironmentRow125 />
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 )}
157126
158 {/* Always mounted: already-connected relay environments must stay127 {/* Always mounted: already-connected relay environments must stay
159 visible (and removable) even when cloud config is missing or the128 visible (and removable) even when cloud config is missing or the