| ... | @@ -7,6 +7,7 @@ import type { | ... | @@ -7,6 +7,7 @@ import type { |
| 7 | RuntimeMode, | 7 | RuntimeMode, |
| 8 | } from "@t3tools/contracts"; | 8 | } from "@t3tools/contracts"; |
| 9 | import type { LegendListRenderItemProps } from "@legendapp/list/react-native"; | 9 | import type { LegendListRenderItemProps } from "@legendapp/list/react-native"; |
| | 10 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; |
| 10 | import { AnimatedLegendList } from "@legendapp/list/reanimated"; | 11 | import { AnimatedLegendList } from "@legendapp/list/reanimated"; |
| 11 | import { HeaderHeightContext } from "@react-navigation/elements"; | 12 | import { HeaderHeightContext } from "@react-navigation/elements"; |
| 12 | import { | 13 | import { |
| ... | @@ -20,6 +21,7 @@ import { | ... | @@ -20,6 +21,7 @@ import { |
| 20 | type NativeStackNavigationProp, | 21 | type NativeStackNavigationProp, |
| 21 | } from "@react-navigation/native-stack"; | 22 | } from "@react-navigation/native-stack"; |
| 22 | import * as Haptics from "expo-haptics"; | 23 | import * as Haptics from "expo-haptics"; |
| | 24 | import { AsyncResult } from "effect/unstable/reactivity"; |
| 23 | import { | 25 | import { |
| 24 | createContext, | 26 | createContext, |
| 25 | use, | 27 | use, |
| ... | @@ -46,6 +48,7 @@ import type { ModelOption, ProviderGroup } from "../../lib/modelOptions"; | ... | @@ -46,6 +48,7 @@ import type { ModelOption, ProviderGroup } from "../../lib/modelOptions"; |
| 46 | import { applyProviderOptionSelection } from "../../lib/providerOptions"; | 48 | import { applyProviderOptionSelection } from "../../lib/providerOptions"; |
| 47 | import { resolveProviderOptionDescriptors } from "../../lib/providerOptions"; | 49 | import { resolveProviderOptionDescriptors } from "../../lib/providerOptions"; |
| 48 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | 50 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; |
| | 51 | import type { Preferences } from "../../persistence/mobile-preferences"; |
| 49 | import { | 52 | import { |
| 50 | NativeHeaderToolbar, | 53 | NativeHeaderToolbar, |
| 51 | NativeStackScreenOptions, | 54 | NativeStackScreenOptions, |
| ... | @@ -53,6 +56,7 @@ import { | ... | @@ -53,6 +56,7 @@ import { |
| 53 | } from "../../native/StackHeader"; | 56 | } from "../../native/StackHeader"; |
| 54 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | 57 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| 55 | import { serverEnvironment } from "../../state/server"; | 58 | import { serverEnvironment } from "../../state/server"; |
| | 59 | import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; |
| 56 | import { useAtomCommand } from "../../state/use-atom-command"; | 60 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 57 | import { useNewTaskFlow } from "./new-task-flow-provider"; | 61 | import { useNewTaskFlow } from "./new-task-flow-provider"; |
| 58 | import { MaterialScreenContent } from "../../components/MaterialScreenContent"; | 62 | import { MaterialScreenContent } from "../../components/MaterialScreenContent"; |
| ... | @@ -68,9 +72,12 @@ import { | ... | @@ -68,9 +72,12 @@ import { |
| 68 | import { RUNTIME_MODE_CHOICES, selectableChoices } from "./thread-settings-options"; | 72 | import { RUNTIME_MODE_CHOICES, selectableChoices } from "./thread-settings-options"; |
| 69 | import { | 73 | import { |
| 70 | canCommitPendingModel, | 74 | canCommitPendingModel, |
| | 75 | favoritesFirst, |
| | 76 | modelFavoriteKey, |
| 71 | modelMatchesCatalogQuery, | 77 | modelMatchesCatalogQuery, |
| 72 | pendingModelAfterPress, | 78 | pendingModelAfterPress, |
| 73 | providerSectionIsCollapsed, | 79 | providerSectionIsCollapsed, |
| | 80 | toggleModelFavorite, |
| 74 | } from "./thread-settings-sheet-state"; | 81 | } from "./thread-settings-sheet-state"; |
| 75 | | 82 | |
| 76 | /** | 83 | /** |
| ... | @@ -101,74 +108,103 @@ const THREAD_SETTINGS_HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects | ... | @@ -101,74 +108,103 @@ const THREAD_SETTINGS_HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects |
| 101 | Platform.OS, | 108 | Platform.OS, |
| 102 | Platform.Version, | 109 | Platform.Version, |
| 103 | ); | 110 | ); |
| | 111 | const EMPTY_MODEL_FAVORITES: ReadonlyArray<{ |
| | 112 | readonly provider: ProviderInstanceId; |
| | 113 | readonly model: string; |
| | 114 | }> = []; |
| | 115 | const FAVORITES_PROVIDER_FILTER = "@favorites"; |
| 104 | function ModelRow(props: { | 116 | function ModelRow(props: { |
| 105 | readonly option: ModelOption; | 117 | readonly option: ModelOption; |
| 106 | readonly selected: boolean; | 118 | readonly selected: boolean; |
| 107 | readonly onPress: () => void; | 119 | readonly onPress: () => void; |
| | 120 | readonly isFavorite: boolean; |
| | 121 | readonly favoritesLoaded: boolean; |
| | 122 | readonly onToggleFavorite: () => void; |
| 108 | readonly isFirst: boolean; | 123 | readonly isFirst: boolean; |
| 109 | readonly isLast: boolean; | 124 | readonly isLast: boolean; |
| 110 | }) { | 125 | }) { |
| 111 | const selectedMaterialRow = Platform.OS === "android" && props.selected; | 126 | const selectedMaterialRow = Platform.OS === "android" && props.selected; |
| 112 | return ( | 127 | return ( |
| 113 | <Pressable | 128 | <View |
| 114 | accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")} | 129 | style={Platform.OS === "android" ? { minHeight: 56 } : undefined} |
| 115 | accessibilityRole="radio" | | |
| 116 | accessibilityState={{ | | |
| 117 | checked: props.selected, | | |
| 118 | disabled: props.option.isUnavailable === true, | | |
| 119 | }} | | |
| 120 | disabled={props.option.isUnavailable} | | |
| 121 | onPress={props.onPress} | | |
| 122 | style={Platform.OS === "android" ? { minHeight: 56, paddingVertical: 12 } : undefined} | | |
| 123 | className={cn( | 130 | className={cn( |
| 124 | "mx-4 min-h-11 flex-row items-center gap-2 bg-card px-4 py-2 active:bg-subtle", | 131 | "mx-4 min-h-11 flex-row items-center gap-2 bg-card px-4", |
| 125 | selectedMaterialRow && "bg-thread-selected", | 132 | selectedMaterialRow && "bg-thread-selected", |
| 126 | props.isFirst && "rounded-t-2xl", | 133 | props.isFirst && "rounded-t-2xl", |
| 127 | props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle", | 134 | props.isLast ? "rounded-b-2xl" : "border-b border-border-subtle", |
| 128 | )} | 135 | )} |
| 129 | > | 136 | > |
| 130 | {Platform.OS === "android" ? <MaterialRadioIndicator selected={props.selected} /> : null} | 137 | <Pressable |
| 131 | <View className="min-w-0 flex-1"> | 138 | accessibilityLabel={[props.option.label, props.option.subtitle].filter(Boolean).join(", ")} |
| 132 | <View className="flex-row items-center gap-2"> | 139 | accessibilityRole="radio" |
| 133 | <Text | 140 | accessibilityState={{ |
| 134 | className="min-w-0 shrink text-base font-t3-medium text-foreground" | 141 | checked: props.selected, |
| 135 | numberOfLines={Platform.OS === "android" ? 2 : 1} | 142 | disabled: props.option.isUnavailable === true, |
| 136 | > | 143 | }} |
| 137 | {props.option.label} | 144 | className="min-h-11 min-w-0 flex-1 flex-row items-center gap-2 active:opacity-70" |
| 138 | </Text> | 145 | disabled={props.option.isUnavailable} |
| 139 | {props.option.isDefault ? ( | 146 | onPress={props.onPress} |
| 140 | <View className="rounded-md bg-subtle-strong px-1.5 py-0.5"> | 147 | > |
| 141 | <Text className="text-3xs font-t3-bold text-foreground-muted">Default</Text> | 148 | {Platform.OS === "android" ? <MaterialRadioIndicator selected={props.selected} /> : null} |
| 142 | </View> | 149 | <View className="min-w-0 flex-1"> |
| 143 | ) : null} | 150 | <View className="flex-row items-center gap-2"> |
| 144 | {props.option.isLegacy ? ( | 151 | <Text |
| 145 | <View className="rounded-md bg-subtle px-1.5 py-0.5"> | 152 | className="min-w-0 shrink text-base font-t3-medium text-foreground" |
| 146 | <Text className="text-3xs font-t3-bold text-foreground-muted">Legacy</Text> | 153 | numberOfLines={Platform.OS === "android" ? 2 : 1} |
| 147 | </View> | 154 | > |
| 148 | ) : null} | 155 | {props.option.label} |
| 149 | {props.option.isUnavailable ? ( | 156 | </Text> |
| 150 | <Text className="text-xs text-foreground">Unavailable</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> |
| 151 | ) : null} | 178 | ) : null} |
| 152 | </View> | 179 | </View> |
| 153 | {props.option.subtitle ? ( | 180 | {props.selected && Platform.OS !== "android" ? ( |
| 154 | <Text | 181 | <SymbolView |
| 155 | className="text-xs text-foreground-muted" | 182 | name="checkmark" |
| 156 | numberOfLines={Platform.OS === "android" ? 2 : 1} | 183 | size={16} |
| 157 | > | 184 | tintColorClassName="accent-icon" |
| 158 | {props.option.subtitle} | 185 | type="monochrome" |
| 159 | </Text> | 186 | weight="semibold" |
| | 187 | /> |
| 160 | ) : null} | 188 | ) : null} |
| 161 | </View> | 189 | </Pressable> |
| 162 | {props.selected && Platform.OS !== "android" ? ( | 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 | > |
| 163 | <SymbolView | 200 | <SymbolView |
| 164 | name="checkmark" | 201 | name={props.isFavorite ? "star.fill" : "star"} |
| 165 | size={16} | 202 | size={18} |
| 166 | tintColorClassName="accent-icon" | 203 | tintColorClassName={props.isFavorite ? "accent-icon" : "accent-icon-subtle"} |
| 167 | type="monochrome" | 204 | type="monochrome" |
| 168 | weight="semibold" | | |
| 169 | /> | 205 | /> |
| 170 | ) : null} | 206 | </Pressable> |
| 171 | </Pressable> | 207 | </View> |
| 172 | ); | 208 | ); |
| 173 | } | 209 | } |
| 174 | | 210 | |
| ... | @@ -384,6 +420,9 @@ type ThreadSettingsSessionValue = { | ... | @@ -384,6 +420,9 @@ type ThreadSettingsSessionValue = { |
| 384 | readonly environmentId: EnvironmentId | null; | 420 | readonly environmentId: EnvironmentId | null; |
| 385 | readonly providerInstanceId?: ProviderInstanceId; | 421 | readonly providerInstanceId?: ProviderInstanceId; |
| 386 | readonly providerGroups: ReadonlyArray<ProviderGroup>; | 422 | readonly providerGroups: ReadonlyArray<ProviderGroup>; |
| | 423 | readonly favoriteKeys: ReadonlySet<string>; |
| | 424 | readonly favoritesLoaded: boolean; |
| | 425 | readonly toggleFavorite: (option: ModelOption) => void; |
| 387 | readonly runtimeMode: RuntimeMode; | 426 | readonly runtimeMode: RuntimeMode; |
| 388 | readonly onUpdateRuntimeMode: (mode: RuntimeMode) => void; | 427 | readonly onUpdateRuntimeMode: (mode: RuntimeMode) => void; |
| 389 | readonly displayedDescriptors: ReadonlyArray<ProviderOptionDescriptor>; | 428 | readonly displayedDescriptors: ReadonlyArray<ProviderOptionDescriptor>; |
| ... | @@ -410,6 +449,32 @@ const ThreadSettingsSessionContext = createContext<ThreadSettingsSessionValue | | ... | @@ -410,6 +449,32 @@ const ThreadSettingsSessionContext = createContext<ThreadSettingsSessionValue | |
| 410 | function ThreadSettingsSessionProvider( | 449 | function ThreadSettingsSessionProvider( |
| 411 | props: ThreadSettingsSessionProps & { readonly children: ReactNode }, | 450 | props: ThreadSettingsSessionProps & { readonly children: ReactNode }, |
| 412 | ) { | 451 | ) { |
| | 452 | const preferences = useAtomValue(mobilePreferencesAtom); |
| | 453 | const savePreferences = useAtomSet(updateMobilePreferencesAtom); |
| | 454 | const favoritesLoaded = AsyncResult.isSuccess(preferences); |
| | 455 | const modelFavorites = favoritesLoaded |
| | 456 | ? (preferences.value.modelFavorites ?? EMPTY_MODEL_FAVORITES) |
| | 457 | : EMPTY_MODEL_FAVORITES; |
| | 458 | const favoriteKeys = useMemo( |
| | 459 | () => |
| | 460 | new Set( |
| | 461 | modelFavorites.map((favorite) => modelFavoriteKey(favorite.provider, favorite.model)), |
| | 462 | ), |
| | 463 | [modelFavorites], |
| | 464 | ); |
| | 465 | const toggleFavorite = useCallback( |
| | 466 | (option: ModelOption) => { |
| | 467 | if (!favoritesLoaded) return; |
| | 468 | void Haptics.selectionAsync(); |
| | 469 | savePreferences((current: Preferences) => ({ |
| | 470 | modelFavorites: toggleModelFavorite( |
| | 471 | current.modelFavorites ?? EMPTY_MODEL_FAVORITES, |
| | 472 | option, |
| | 473 | ), |
| | 474 | })); |
| | 475 | }, |
| | 476 | [favoritesLoaded, savePreferences], |
| | 477 | ); |
| 413 | const [showLegacyToggle, setShowLegacyToggle] = useState(false); | 478 | const [showLegacyToggle, setShowLegacyToggle] = useState(false); |
| 414 | const [providerFilter, setProviderFilter] = useState<string | null>(null); | 479 | const [providerFilter, setProviderFilter] = useState<string | null>(null); |
| 415 | const [searchQuery, setSearchQuery] = useState(""); | 480 | const [searchQuery, setSearchQuery] = useState(""); |
| ... | @@ -515,6 +580,8 @@ function ThreadSettingsSessionProvider( | ... | @@ -515,6 +580,8 @@ function ThreadSettingsSessionProvider( |
| 515 | runtimeMode: props.runtimeMode, | 580 | runtimeMode: props.runtimeMode, |
| 516 | onUpdateRuntimeMode: props.onUpdateRuntimeMode, | 581 | onUpdateRuntimeMode: props.onUpdateRuntimeMode, |
| 517 | displayedDescriptors, | 582 | displayedDescriptors, |
| | 583 | favoriteKeys, |
| | 584 | favoritesLoaded, |
| 518 | providerExpansionOverrides, | 585 | providerExpansionOverrides, |
| 519 | hasLegacyModels, | 586 | hasLegacyModels, |
| 520 | pendingModel, | 587 | pendingModel, |
| ... | @@ -530,11 +597,14 @@ function ThreadSettingsSessionProvider( | ... | @@ -530,11 +597,14 @@ function ThreadSettingsSessionProvider( |
| 530 | setSearchQuery, | 597 | setSearchQuery, |
| 531 | setShowLegacy: setShowLegacyToggle, | 598 | setShowLegacy: setShowLegacyToggle, |
| 532 | toggleProvider, | 599 | toggleProvider, |
| | 600 | toggleFavorite, |
| 533 | }), | 601 | }), |
| 534 | [ | 602 | [ |
| 535 | applyOptionChange, | 603 | applyOptionChange, |
| 536 | commitPendingModel, | 604 | commitPendingModel, |
| 537 | displayedDescriptors, | 605 | displayedDescriptors, |
| | 606 | favoriteKeys, |
| | 607 | favoritesLoaded, |
| 538 | providerExpansionOverrides, | 608 | providerExpansionOverrides, |
| 539 | hasLegacyModels, | 609 | hasLegacyModels, |
| 540 | isApplied, | 610 | isApplied, |
| ... | @@ -550,6 +620,7 @@ function ThreadSettingsSessionProvider( | ... | @@ -550,6 +620,7 @@ function ThreadSettingsSessionProvider( |
| 550 | searchQuery, | 620 | searchQuery, |
| 551 | showLegacyToggle, | 621 | showLegacyToggle, |
| 552 | toggleProvider, | 622 | toggleProvider, |
| | 623 | toggleFavorite, |
| 553 | ], | 624 | ], |
| 554 | ); | 625 | ); |
| 555 | | 626 | |
| ... | @@ -616,6 +687,9 @@ function ThreadSettingsModelListRow(props: { | ... | @@ -616,6 +687,9 @@ function ThreadSettingsModelListRow(props: { |
| 616 | isFirst={props.isFirst} | 687 | isFirst={props.isFirst} |
| 617 | isLast={props.isLast} | 688 | isLast={props.isLast} |
| 618 | onPress={onPress} | 689 | onPress={onPress} |
| | 690 | isFavorite={session.favoriteKeys.has(props.option.key)} |
| | 691 | favoritesLoaded={session.favoritesLoaded} |
| | 692 | onToggleFavorite={() => session.toggleFavorite(props.option)} |
| 619 | option={props.option} | 693 | option={props.option} |
| 620 | selected={session.isDisplayed(props.option)} | 694 | selected={session.isDisplayed(props.option)} |
| 621 | /> | 695 | /> |
| ... | @@ -649,19 +723,35 @@ function useThreadSettingsCatalogItems( | ... | @@ -649,19 +723,35 @@ function useThreadSettingsCatalogItems( |
| 649 | return useMemo( | 723 | return useMemo( |
| 650 | () => | 724 | () => |
| 651 | session.providerGroups.flatMap((group) => { | 725 | session.providerGroups.flatMap((group) => { |
| 652 | if (session.providerFilter !== null && group.providerKey !== session.providerFilter) { | 726 | if ( |
| | 727 | session.providerFilter !== null && |
| | 728 | session.providerFilter !== FAVORITES_PROVIDER_FILTER && |
| | 729 | group.providerKey !== session.providerFilter |
| | 730 | ) { |
| 653 | return []; | 731 | return []; |
| 654 | } | 732 | } |
| 655 | const driver = group.models[0]?.providerDriver ?? group.providerKey; | 733 | const driver = group.models[0]?.providerDriver ?? group.providerKey; |
| 656 | const catalogModels = session.showLegacy | 734 | const catalogModels = |
| 657 | ? group.models | 735 | session.showLegacy || session.providerFilter === FAVORITES_PROVIDER_FILTER |
| 658 | : group.models.filter((model) => !model.isLegacy || session.isDisplayed(model)); | 736 | ? group.models |
| 659 | const visibleModels = catalogModels.filter((model) => | 737 | : group.models.filter( |
| 660 | modelMatchesCatalogQuery({ | 738 | (model) => |
| 661 | model, | 739 | !model.isLegacy || |
| 662 | providerLabel: group.providerLabel, | 740 | session.isDisplayed(model) || |
| 663 | query: session.searchQuery, | 741 | session.favoriteKeys.has(model.key), |
| 664 | }), | 742 | ); |
| | 743 | const visibleModels = favoritesFirst( |
| | 744 | catalogModels.filter( |
| | 745 | (model) => |
| | 746 | (session.providerFilter !== FAVORITES_PROVIDER_FILTER || |
| | 747 | session.favoriteKeys.has(model.key)) && |
| | 748 | modelMatchesCatalogQuery({ |
| | 749 | model, |
| | 750 | providerLabel: group.providerLabel, |
| | 751 | query: session.searchQuery, |
| | 752 | }), |
| | 753 | ), |
| | 754 | session.favoriteKeys, |
| 665 | ); | 755 | ); |
| 666 | if (visibleModels.length === 0) { | 756 | if (visibleModels.length === 0) { |
| 667 | return []; | 757 | return []; |
| ... | @@ -705,6 +795,7 @@ function useThreadSettingsCatalogItems( | ... | @@ -705,6 +795,7 @@ function useThreadSettingsCatalogItems( |
| 705 | [ | 795 | [ |
| 706 | session.isApplied, | 796 | session.isApplied, |
| 707 | session.isDisplayed, | 797 | session.isDisplayed, |
| | 798 | session.favoriteKeys, |
| 708 | session.providerExpansionOverrides, | 799 | session.providerExpansionOverrides, |
| 709 | session.providerFilter, | 800 | session.providerFilter, |
| 710 | session.providerGroups, | 801 | session.providerGroups, |
| ... | @@ -834,7 +925,12 @@ function ThreadSettingsMainContent(props: { | ... | @@ -834,7 +925,12 @@ function ThreadSettingsMainContent(props: { |
| 834 | content = ( | 925 | content = ( |
| 835 | <View className="items-center px-8 py-14"> | 926 | <View className="items-center px-8 py-14"> |
| 836 | <Text className="text-center text-sm text-foreground-muted"> | 927 | <Text className="text-center text-sm text-foreground-muted"> |
| 837 | {hasActiveCatalogFilter ? "No matching models" : "No available models"} | 928 | {session.providerFilter === FAVORITES_PROVIDER_FILTER && |
| | 929 | session.searchQuery.trim().length === 0 |
| | 930 | ? "No favorite models" |
| | 931 | : hasActiveCatalogFilter |
| | 932 | ? "No matching models" |
| | 933 | : "No available models"} |
| 838 | </Text> | 934 | </Text> |
| 839 | </View> | 935 | </View> |
| 840 | ); | 936 | ); |
| ... | @@ -857,7 +953,13 @@ function ThreadSettingsMainContent(props: { | ... | @@ -857,7 +953,13 @@ function ThreadSettingsMainContent(props: { |
| 857 | </Animated.View> | 953 | </Animated.View> |
| 858 | ); | 954 | ); |
| 859 | }, | 955 | }, |
| 860 | [animationsReady, hasActiveCatalogFilter, props.onOpenSubmenu], | 956 | [ |
| | 957 | animationsReady, |
| | 958 | hasActiveCatalogFilter, |
| | 959 | props.onOpenSubmenu, |
| | 960 | session.providerFilter, |
| | 961 | session.searchQuery, |
| | 962 | ], |
| 861 | ); | 963 | ); |
| 862 | | 964 | |
| 863 | return ( | 965 | return ( |
| ... | @@ -918,6 +1020,38 @@ function ThreadSettingsMainContent(props: { | ... | @@ -918,6 +1020,38 @@ function ThreadSettingsMainContent(props: { |
| 918 | /> | 1020 | /> |
| 919 | ) : null} | 1021 | ) : null} |
| 920 | </View> | 1022 | </View> |
| | 1023 | <View className="flex-row gap-2 pt-3"> |
| | 1024 | <Pressable |
| | 1025 | accessibilityRole="button" |
| | 1026 | accessibilityState={{ |
| | 1027 | selected: session.providerFilter !== FAVORITES_PROVIDER_FILTER, |
| | 1028 | }} |
| | 1029 | className={cn( |
| | 1030 | "rounded-full px-4 py-2", |
| | 1031 | session.providerFilter !== FAVORITES_PROVIDER_FILTER |
| | 1032 | ? "bg-subtle-strong" |
| | 1033 | : "bg-subtle", |
| | 1034 | )} |
| | 1035 | onPress={() => session.setProviderFilter(null)} |
| | 1036 | > |
| | 1037 | <Text className="text-sm font-t3-medium text-foreground">All</Text> |
| | 1038 | </Pressable> |
| | 1039 | <Pressable |
| | 1040 | accessibilityRole="button" |
| | 1041 | accessibilityState={{ |
| | 1042 | selected: session.providerFilter === FAVORITES_PROVIDER_FILTER, |
| | 1043 | }} |
| | 1044 | className={cn( |
| | 1045 | "rounded-full px-4 py-2", |
| | 1046 | session.providerFilter === FAVORITES_PROVIDER_FILTER |
| | 1047 | ? "bg-subtle-strong" |
| | 1048 | : "bg-subtle", |
| | 1049 | )} |
| | 1050 | onPress={() => session.setProviderFilter(FAVORITES_PROVIDER_FILTER)} |
| | 1051 | > |
| | 1052 | <Text className="text-sm font-t3-medium text-foreground">Favorites</Text> |
| | 1053 | </Pressable> |
| | 1054 | </View> |
| 921 | </View> | 1055 | </View> |
| 922 | ) : null} | 1056 | ) : null} |
| 923 | </> | 1057 | </> |
| ... | @@ -1077,6 +1211,15 @@ function ThreadSettingsModelsScreen() { | ... | @@ -1077,6 +1211,15 @@ function ThreadSettingsModelsScreen() { |
| 1077 | state: session.providerFilter === null ? ("on" as const) : ("off" as const), | 1211 | state: session.providerFilter === null ? ("on" as const) : ("off" as const), |
| 1078 | onPress: () => session.setProviderFilter(null), | 1212 | onPress: () => session.setProviderFilter(null), |
| 1079 | }, | 1213 | }, |
| | 1214 | { |
| | 1215 | type: "action" as const, |
| | 1216 | title: "Favorites", |
| | 1217 | state: |
| | 1218 | session.providerFilter === FAVORITES_PROVIDER_FILTER |
| | 1219 | ? ("on" as const) |
| | 1220 | : ("off" as const), |
| | 1221 | onPress: () => session.setProviderFilter(FAVORITES_PROVIDER_FILTER), |
| | 1222 | }, |
| 1080 | ...session.providerGroups.map((group) => ({ | 1223 | ...session.providerGroups.map((group) => ({ |
| 1081 | type: "action" as const, | 1224 | type: "action" as const, |
| 1082 | title: group.providerLabel, | 1225 | title: group.providerLabel, |
| ... | @@ -1213,6 +1356,12 @@ function ThreadSettingsModelsScreen() { | ... | @@ -1213,6 +1356,12 @@ function ThreadSettingsModelsScreen() { |
| 1213 | > | 1356 | > |
| 1214 | All providers | 1357 | All providers |
| 1215 | </NativeHeaderToolbar.MenuAction> | 1358 | </NativeHeaderToolbar.MenuAction> |
| | 1359 | <NativeHeaderToolbar.MenuAction |
| | 1360 | isOn={session.providerFilter === FAVORITES_PROVIDER_FILTER} |
| | 1361 | onPress={() => session.setProviderFilter(FAVORITES_PROVIDER_FILTER)} |
| | 1362 | > |
| | 1363 | Favorites |
| | 1364 | </NativeHeaderToolbar.MenuAction> |
| 1216 | {session.providerGroups.map((group) => ( | 1365 | {session.providerGroups.map((group) => ( |
| 1217 | <NativeHeaderToolbar.MenuAction | 1366 | <NativeHeaderToolbar.MenuAction |
| 1218 | key={group.providerKey} | 1367 | key={group.providerKey} |