authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-18 12:55:14-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 12:55:14-07:00
log7230f6b1be4d1ad839b9a6ab45d0d70b4abc9a54
treef8e7e320eb0323102ea06d399775340f85ace813
parentdec0732281f5b7e787ad3a84e5cbe9a1607ea4ca
signature Signed by PGP key B5690EEEBB952194

refactor(mobile): split settings selection rows by platform (#12384)


4 files changed, 208 insertions(+), 136 deletions(-)

apps/mobile/src/features/threads/ThreadSettingsRows.android.tsx created+29
...@@ -0,0 +1,29 @@
1import { MaterialRadioIndicator } from "../../components/MaterialRadioIndicator";
2import {
3 ModelRowContent,
4 ChoiceRowContent,
5 type ModelRowProps,
6 type ChoiceRowProps,
7} from "./ThreadSettingsRows.shared";
8
9export function ModelRow(props: ModelRowProps) {
10 return (
11 <ModelRowContent
12 {...props}
13 labelNumberOfLines={2}
14 minimumHeight={56}
15 selectedClassName={props.selected ? "bg-thread-selected" : undefined}
16 leadingSelection={<MaterialRadioIndicator selected={props.selected} />}
17 />
18 );
19}
20
21export function ChoiceRow(props: ChoiceRowProps) {
22 return (
23 <ChoiceRowContent
24 {...props}
25 minimumHeight={56}
26 leadingSelection={<MaterialRadioIndicator selected={props.selected} />}
27 />
28 );
29}
apps/mobile/src/features/threads/ThreadSettingsRows.shared.tsx created+140
...@@ -0,0 +1,140 @@
1import type { ReactNode } from "react";
2import { Pressable, View } from "react-native";
3import { AppText as Text } from "../../components/AppText";
4import { SymbolView } from "../../components/AppSymbol";
5import { cn } from "../../lib/cn";
6import type { ModelOption } from "../../lib/modelOptions";
7
8export type ModelRowProps = {
9 readonly option: ModelOption;
10 readonly selected: boolean;
11 readonly onPress: () => void;
12 readonly isFavorite: boolean;
13 readonly favoritesLoaded: boolean;
14 readonly onToggleFavorite: () => void;
15 readonly isFirst: boolean;
16 readonly isLast: boolean;
17};
18
19export type ChoiceRowProps = {
20 readonly label: string;
21 readonly description?: string;
22 readonly selected: boolean;
23 readonly onPress: () => void;
24 readonly isLast: boolean;
25};
26
27type RowSelectionProps = {
28 readonly leadingSelection?: ReactNode;
29 readonly trailingSelection?: ReactNode;
30 readonly minimumHeight?: number;
31};
32
33export function ModelRowContent(
34 props: ModelRowProps &
35 RowSelectionProps & {
36 readonly labelNumberOfLines: number;
37 readonly selectedClassName?: string;
38 },
39) {
40 return (
41 <View
42 style={props.minimumHeight === undefined ? undefined : { minHeight: props.minimumHeight }}
43 className={cn(
44 "mx-4 min-h-11 flex-row items-center gap-2 bg-card px-4",
45 props.selectedClassName,
46 props.isFirst && "rounded-t-2xl",
47 props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle",
48 )}
49 >
50 <Pressable
51 accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")}
52 accessibilityRole="radio"
53 accessibilityState={{
54 checked: props.selected,
55 disabled: props.option.isUnavailable === true,
56 }}
57 className="min-h-11 min-w-0 flex-1 flex-row items-center gap-2 active:opacity-70"
58 disabled={props.option.isUnavailable}
59 onPress={props.onPress}
60 >
61 {props.leadingSelection}
62 <View className="min-w-0 flex-1">
63 <View className="flex-row items-center gap-2">
64 <Text
65 className="min-w-0 shrink text-base font-t3-medium text-foreground"
66 numberOfLines={props.labelNumberOfLines}
67 >
68 {props.option.label}
69 </Text>
70 {props.option.isDefault ? (
71 <View className="rounded-md bg-subtle-strong px-1.5 py-0.5">
72 <Text className="text-3xs font-t3-bold text-foreground-muted">Default</Text>
73 </View>
74 ) : null}
75 {props.option.isLegacy ? (
76 <View className="rounded-md bg-subtle px-1.5 py-0.5">
77 <Text className="text-3xs font-t3-bold text-foreground-muted">Legacy</Text>
78 </View>
79 ) : null}
80 {props.option.isUnavailable ? (
81 <Text className="text-xs text-foreground">Unavailable</Text>
82 ) : null}
83 </View>
84 {props.option.subtitle ? (
85 <Text
86 className="text-xs text-foreground-muted"
87 numberOfLines={props.labelNumberOfLines}
88 >
89 {props.option.subtitle}
90 </Text>
91 ) : null}
92 </View>
93 {props.trailingSelection}
94 </Pressable>
95 <Pressable
96 accessibilityLabel={`${props.isFavorite ? "Remove from" : "Add to"} favorites: ${
97 props.option.providerLabel
98 }, ${props.option.label}`}
99 accessibilityRole="button"
100 accessibilityState={{ disabled: !props.favoritesLoaded, selected: props.isFavorite }}
101 className="min-h-11 min-w-11 items-center justify-center"
102 disabled={!props.favoritesLoaded}
103 onPress={props.onToggleFavorite}
104 >
105 <SymbolView
106 name={props.isFavorite ? "star.fill" : "star"}
107 size={18}
108 tintColorClassName={props.isFavorite ? "accent-icon" : "accent-icon-subtle"}
109 type="monochrome"
110 />
111 </Pressable>
112 </View>
113 );
114}
115
116/** Single option inside a submenu panel. */
117export function ChoiceRowContent(props: ChoiceRowProps & RowSelectionProps) {
118 return (
119 <Pressable
120 accessibilityLabel={props.description ? `${props.label}. ${props.description}` : props.label}
121 accessibilityRole="radio"
122 accessibilityState={{ checked: props.selected }}
123 onPress={props.onPress}
124 style={props.minimumHeight === undefined ? undefined : { minHeight: props.minimumHeight }}
125 className={cn(
126 "min-h-14 flex-row items-center gap-3 bg-card px-4 py-3 active:bg-subtle",
127 !props.isLast && "border-b border-border-subtle",
128 )}
129 >
130 {props.leadingSelection}
131 <View className="min-w-0 flex-1 gap-0.5">
132 <Text className="text-base font-t3-medium text-foreground">{props.label}</Text>
133 {props.description ? (
134 <Text className="text-sm leading-5 text-foreground-muted">{props.description}</Text>
135 ) : null}
136 </View>
137 {props.trailingSelection}
138 </Pressable>
139 );
140}
apps/mobile/src/features/threads/ThreadSettingsRows.tsx created+38
...@@ -0,0 +1,38 @@
1import { SymbolView } from "../../components/AppSymbol";
2import {
3 ModelRowContent,
4 ChoiceRowContent,
5 type ModelRowProps,
6 type ChoiceRowProps,
7} from "./ThreadSettingsRows.shared";
8
9function SelectedCheckmark(props: { readonly selected: boolean }) {
10 return props.selected ? (
11 <SymbolView
12 name="checkmark"
13 size={16}
14 tintColorClassName="accent-icon"
15 type="monochrome"
16 weight="semibold"
17 />
18 ) : null;
19}
20
21export function ModelRow(props: ModelRowProps) {
22 return (
23 <ModelRowContent
24 {...props}
25 labelNumberOfLines={1}
26 trailingSelection={<SelectedCheckmark selected={props.selected} />}
27 />
28 );
29}
30
31export function ChoiceRow(props: ChoiceRowProps) {
32 return (
33 <ChoiceRowContent
34 {...props}
35 trailingSelection={<SelectedCheckmark selected={props.selected} />}
36 />
37 );
38}
apps/mobile/src/features/threads/ThreadSettingsSheet.tsx+1-136
...@@ -40,7 +40,6 @@ import { AppText as Text } from "../../components/AppText";...@@ -40,7 +40,6 @@ import { AppText as Text } from "../../components/AppText";
40import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";40import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
41import { MaterialButton } from "../../components/MaterialButton";41import { MaterialButton } from "../../components/MaterialButton";
42import { MaterialIconButton } from "../../components/MaterialIconButton";42import { MaterialIconButton } from "../../components/MaterialIconButton";
43import { MaterialRadioIndicator } from "../../components/MaterialRadioIndicator";
44import { ProviderIcon } from "../../components/ProviderIcon";43import { ProviderIcon } from "../../components/ProviderIcon";
45import { ThemedSwitch } from "../../components/ThemedSwitch";44import { ThemedSwitch } from "../../components/ThemedSwitch";
46import { cn } from "../../lib/cn";45import { cn } from "../../lib/cn";
...@@ -69,6 +68,7 @@ import {...@@ -69,6 +68,7 @@ import {
69 NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET,68 NATIVE_MAIL_SEARCH_TOOLBAR_CONTENT_INSET,
70 NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,69 NATIVE_MAIL_SEARCH_TOOLBAR_SUPPORTED,
71} from "../layout/native-mail-search-toolbar";70} from "../layout/native-mail-search-toolbar";
71import { ModelRow, ChoiceRow } from "./ThreadSettingsRows";
72import { RUNTIME_MODE_CHOICES, selectableChoices } from "./thread-settings-options";72import { RUNTIME_MODE_CHOICES, selectableChoices } from "./thread-settings-options";
73import {73import {
74 canCommitPendingModel,74 canCommitPendingModel,
...@@ -113,101 +113,6 @@ const EMPTY_MODEL_FAVORITES: ReadonlyArray<{...@@ -113,101 +113,6 @@ const EMPTY_MODEL_FAVORITES: ReadonlyArray<{
113 readonly model: string;113 readonly model: string;
114}> = [];114}> = [];
115const FAVORITES_PROVIDER_FILTER = "@favorites";115const FAVORITES_PROVIDER_FILTER = "@favorites";
116function ModelRow(props: {
117 readonly option: ModelOption;
118 readonly selected: boolean;
119 readonly onPress: () => void;
120 readonly isFavorite: boolean;
121 readonly favoritesLoaded: boolean;
122 readonly onToggleFavorite: () => void;
123 readonly isFirst: boolean;
124 readonly isLast: boolean;
125}) {
126 const selectedMaterialRow = Platform.OS === "android" && props.selected;
127 return (
128 <View
129 style={Platform.OS === "android" ? { minHeight: 56 } : undefined}
130 className={cn(
131 "mx-4 min-h-11 flex-row items-center gap-2 bg-card px-4",
132 selectedMaterialRow && "bg-thread-selected",
133 props.isFirst && "rounded-t-2xl",
134 props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle",
135 )}
136 >
137 <Pressable
138 accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")}
139 accessibilityRole="radio"
140 accessibilityState={{
141 checked: props.selected,
142 disabled: props.option.isUnavailable === true,
143 }}
144 className="min-h-11 min-w-0 flex-1 flex-row items-center gap-2 active:opacity-70"
145 disabled={props.option.isUnavailable}
146 onPress={props.onPress}
147 >
148 {Platform.OS === "android" ? <MaterialRadioIndicator selected={props.selected} /> : null}
149 <View className="min-w-0 flex-1">
150 <View className="flex-row items-center gap-2">
151 <Text
152 className="min-w-0 shrink text-base font-t3-medium text-foreground"
153 numberOfLines={Platform.OS === "android" ? 2 : 1}
154 >
155 {props.option.label}
156 </Text>
157 {props.option.isDefault ? (
158 <View className="rounded-md bg-subtle-strong px-1.5 py-0.5">
159 <Text className="text-3xs font-t3-bold text-foreground-muted">Default</Text>
160 </View>
161 ) : null}
162 {props.option.isLegacy ? (
163 <View className="rounded-md bg-subtle px-1.5 py-0.5">
164 <Text className="text-3xs font-t3-bold text-foreground-muted">Legacy</Text>
165 </View>
166 ) : null}
167 {props.option.isUnavailable ? (
168 <Text className="text-xs text-foreground">Unavailable</Text>
169 ) : null}
170 </View>
171 {props.option.subtitle ? (
172 <Text
173 className="text-xs text-foreground-muted"
174 numberOfLines={Platform.OS === "android" ? 2 : 1}
175 >
176 {props.option.subtitle}
177 </Text>
178 ) : null}
179 </View>
180 {props.selected && Platform.OS !== "android" ? (
181 <SymbolView
182 name="checkmark"
183 size={16}
184 tintColorClassName="accent-icon"
185 type="monochrome"
186 weight="semibold"
187 />
188 ) : null}
189 </Pressable>
190 <Pressable
191 accessibilityLabel={`${props.isFavorite ? "Remove from" : "Add to"} favorites: ${
192 props.option.providerLabel
193 }, ${props.option.label}`}
194 accessibilityRole="button"
195 accessibilityState={{ disabled: !props.favoritesLoaded, selected: props.isFavorite }}
196 className="min-h-11 min-w-11 items-center justify-center"
197 disabled={!props.favoritesLoaded}
198 onPress={props.onToggleFavorite}
199 >
200 <SymbolView
201 name={props.isFavorite ? "star.fill" : "star"}
202 size={18}
203 tintColorClassName={props.isFavorite ? "accent-icon" : "accent-icon-subtle"}
204 type="monochrome"
205 />
206 </Pressable>
207 </View>
208 );
209}
210
211/** Provider catalog header with its harness logo and disclosure state. */116/** Provider catalog header with its harness logo and disclosure state. */
212function ProviderHeader(props: {117function ProviderHeader(props: {
213 readonly driver: string | undefined;118 readonly driver: string | undefined;
...@@ -296,46 +201,6 @@ function DisclosureRow(props: {...@@ -296,46 +201,6 @@ function DisclosureRow(props: {
296 );201 );
297}202}
298203
299/** Single option inside a submenu panel. */
300function ChoiceRow(props: {
301 readonly label: string;
302 readonly description?: string;
303 readonly selected: boolean;
304 readonly onPress: () => void;
305 readonly isLast: boolean;
306}) {
307 return (
308 <Pressable
309 accessibilityLabel={props.description ? `${props.label}. ${props.description}` : props.label}
310 accessibilityRole="radio"
311 accessibilityState={{ checked: props.selected }}
312 onPress={props.onPress}
313 style={Platform.OS === "android" ? { minHeight: 56 } : undefined}
314 className={cn(
315 "min-h-14 flex-row items-center gap-3 bg-card px-4 py-3 active:bg-subtle",
316 !props.isLast && "border-b border-border-subtle",
317 )}
318 >
319 {Platform.OS === "android" ? <MaterialRadioIndicator selected={props.selected} /> : null}
320 <View className="min-w-0 flex-1 gap-0.5">
321 <Text className="text-base font-t3-medium text-foreground">{props.label}</Text>
322 {props.description ? (
323 <Text className="text-sm leading-5 text-foreground-muted">{props.description}</Text>
324 ) : null}
325 </View>
326 {props.selected && Platform.OS !== "android" ? (
327 <SymbolView
328 name="checkmark"
329 size={16}
330 tintColorClassName="accent-icon"
331 type="monochrome"
332 weight="semibold"
333 />
334 ) : null}
335 </Pressable>
336 );
337}
338
339function SwitchRow(props: {204function SwitchRow(props: {
340 readonly label: string;205 readonly label: string;
341 readonly value: boolean;206 readonly value: boolean;