Skip to content

Commit 203c253

Browse files
committed
feat(rn-settings): impl gerenate settings
1 parent 85384d1 commit 203c253

File tree

19 files changed

+701
-71
lines changed

19 files changed

+701
-71
lines changed

apps/mobile/app.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
5050
favicon: iconPath,
5151
},
5252
plugins: [
53+
"expo-localization",
5354
[
5455
"expo-router",
5556
{

apps/mobile/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"expo-image": "~2.0.3",
5151
"expo-linear-gradient": "~14.0.1",
5252
"expo-linking": "~7.0.3",
53+
"expo-localization": "~16.0.1",
5354
"expo-router": "4.0.11",
5455
"expo-secure-store": "^14.0.1",
5556
"expo-sharing": "~13.0.0",
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { GeneralSettings } from "@/src/interfaces/settings/general"
2+
3+
import { createSettingAtom } from "./internal/helper"
4+
5+
const createDefaultSettings = (): GeneralSettings => ({
6+
// App
7+
8+
language: "en",
9+
translationLanguage: "zh-CN",
10+
11+
// Data control
12+
13+
sendAnonymousData: true,
14+
15+
autoGroup: true,
16+
17+
// view
18+
unreadOnly: true,
19+
// mark unread
20+
scrollMarkUnread: true,
21+
22+
renderMarkUnread: false,
23+
// UX
24+
groupByDate: true,
25+
autoExpandLongSocialMedia: false,
26+
27+
// Secure
28+
jumpOutLinkWarn: true,
29+
// TTS
30+
voice: "en-US-AndrewMultilingualNeural",
31+
})
32+
33+
export const {
34+
useSettingKey: useGeneralSettingKey,
35+
useSettingSelector: useGeneralSettingSelector,
36+
useSettingKeys: useGeneralSettingKeys,
37+
setSetting: setGeneralSetting,
38+
clearSettings: clearGeneralSettings,
39+
initializeDefaultSettings: initializeDefaultGeneralSettings,
40+
getSettings: getGeneralSettings,
41+
useSettingValue: useGeneralSettingValue,
42+
43+
settingAtom: __generalSettingAtom,
44+
} = createSettingAtom("general", createDefaultSettings)
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { useRefValue } from "@follow/hooks"
2+
import { createAtomHooks } from "@follow/utils"
3+
import type { SetStateAction, WritableAtom } from "jotai"
4+
import { atom as jotaiAtom, useAtomValue } from "jotai"
5+
import { atomWithStorage, selectAtom } from "jotai/utils"
6+
import { useMemo } from "react"
7+
import { shallow } from "zustand/shallow"
8+
9+
import { JotaiPersistSyncStorage } from "@/src/lib/jotai"
10+
11+
const getStorageNS = (settingKey: string) => `follow-rn-${settingKey}`
12+
type Nullable<T> = T | null | undefined
13+
14+
export const createSettingAtom = <T extends object>(
15+
settingKey: string,
16+
createDefaultSettings: () => T,
17+
) => {
18+
const atom = atomWithStorage(
19+
getStorageNS(settingKey),
20+
createDefaultSettings(),
21+
JotaiPersistSyncStorage,
22+
{
23+
getOnInit: true,
24+
},
25+
) as WritableAtom<T, [SetStateAction<T>], void>
26+
27+
const [, , useSettingValue, , getSettings, setSettings] = createAtomHooks(atom)
28+
29+
const initializeDefaultSettings = () => {
30+
const currentSettings = getSettings()
31+
const defaultSettings = createDefaultSettings()
32+
if (typeof currentSettings !== "object") setSettings(defaultSettings)
33+
const newSettings = { ...defaultSettings, ...currentSettings }
34+
setSettings(newSettings)
35+
}
36+
37+
const selectAtomCacheMap = {} as Record<keyof ReturnType<typeof getSettings>, any>
38+
39+
const noopAtom = jotaiAtom(null)
40+
41+
const useMaybeSettingKey = <T extends keyof ReturnType<typeof getSettings>>(key: Nullable<T>) => {
42+
// @ts-expect-error
43+
let selectedAtom: Record<keyof T, any>[T] | null = null
44+
if (key) {
45+
selectedAtom = selectAtomCacheMap[key]
46+
if (!selectedAtom) {
47+
selectedAtom = selectAtom(atom, (s) => s[key])
48+
selectAtomCacheMap[key] = selectedAtom
49+
}
50+
} else {
51+
selectedAtom = noopAtom
52+
}
53+
54+
return useAtomValue(selectedAtom) as ReturnType<typeof getSettings>[T]
55+
}
56+
57+
const useSettingKey = <T extends keyof ReturnType<typeof getSettings>>(key: T) => {
58+
return useMaybeSettingKey(key) as ReturnType<typeof getSettings>[T]
59+
}
60+
61+
function useSettingKeys<
62+
T extends keyof ReturnType<typeof getSettings>,
63+
K1 extends T,
64+
K2 extends T,
65+
K3 extends T,
66+
K4 extends T,
67+
K5 extends T,
68+
K6 extends T,
69+
K7 extends T,
70+
K8 extends T,
71+
K9 extends T,
72+
K10 extends T,
73+
>(keys: [K1, K2?, K3?, K4?, K5?, K6?, K7?, K8?, K9?, K10?]) {
74+
return [
75+
useMaybeSettingKey(keys[0]),
76+
useMaybeSettingKey(keys[1]),
77+
useMaybeSettingKey(keys[2]),
78+
useMaybeSettingKey(keys[3]),
79+
useMaybeSettingKey(keys[4]),
80+
useMaybeSettingKey(keys[5]),
81+
useMaybeSettingKey(keys[6]),
82+
useMaybeSettingKey(keys[7]),
83+
useMaybeSettingKey(keys[8]),
84+
useMaybeSettingKey(keys[9]),
85+
] as [
86+
ReturnType<typeof getSettings>[K1],
87+
ReturnType<typeof getSettings>[K2],
88+
ReturnType<typeof getSettings>[K3],
89+
ReturnType<typeof getSettings>[K4],
90+
ReturnType<typeof getSettings>[K5],
91+
ReturnType<typeof getSettings>[K6],
92+
ReturnType<typeof getSettings>[K7],
93+
ReturnType<typeof getSettings>[K8],
94+
ReturnType<typeof getSettings>[K9],
95+
ReturnType<typeof getSettings>[K10],
96+
]
97+
}
98+
99+
const useSettingSelector = <
100+
T extends keyof ReturnType<typeof getSettings>,
101+
S extends ReturnType<typeof getSettings>,
102+
R = S[T],
103+
>(
104+
selector: (s: S) => R,
105+
): R => {
106+
const stableSelector = useRefValue(selector)
107+
108+
return useAtomValue(
109+
// @ts-expect-error
110+
useMemo(() => selectAtom(atom, stableSelector.current, shallow), [stableSelector]),
111+
)
112+
}
113+
114+
const setSetting = <K extends keyof ReturnType<typeof getSettings>>(
115+
key: K,
116+
value: ReturnType<typeof getSettings>[K],
117+
) => {
118+
const updated = Date.now()
119+
setSettings({
120+
...getSettings(),
121+
[key]: value,
122+
123+
updated,
124+
})
125+
}
126+
127+
const clearSettings = () => {
128+
setSettings(createDefaultSettings())
129+
}
130+
131+
Object.defineProperty(useSettingValue, "select", {
132+
value: useSettingSelector,
133+
})
134+
135+
return {
136+
useSettingKey,
137+
useSettingSelector,
138+
setSetting,
139+
clearSettings,
140+
initializeDefaultSettings,
141+
142+
useSettingValue,
143+
useSettingKeys,
144+
getSettings,
145+
146+
settingAtom: atom,
147+
} as {
148+
useSettingKey: typeof useSettingKey
149+
useSettingSelector: typeof useSettingSelector
150+
setSetting: typeof setSetting
151+
clearSettings: typeof clearSettings
152+
initializeDefaultSettings: typeof initializeDefaultSettings
153+
useSettingValue: typeof useSettingValue & {
154+
select: <T extends keyof ReturnType<() => T>>(key: T) => Awaited<T[T]>
155+
}
156+
useSettingKeys: typeof useSettingKeys
157+
getSettings: typeof getSettings
158+
settingAtom: typeof atom
159+
}
160+
}

apps/mobile/src/components/common/ThemedBlurView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export const ThemedBlurView = forwardRef<BlurView, BlurViewProps>(({ tint, ...re
88
return (
99
<BlurView
1010
ref={ref}
11-
tint={colorScheme === "light" ? "systemMaterialLight" : "systemMaterialDark"}
11+
tint={colorScheme === "light" ? "systemChromeMaterialLight" : "systemChromeMaterialDark"}
1212
{...rest}
1313
/>
1414
)

apps/mobile/src/components/ui/dropdown/DropdownMenu.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export function DropdownMenu<T>({
2929
const isActionMenu = options.every((option) => "title" in option)
3030
return (
3131
<ContextMenu
32+
style={{ flex: 1 }}
3233
dropdownMenuMode
3334
actions={
3435
isActionMenu

apps/mobile/src/components/ui/form/Select.tsx

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -52,34 +52,43 @@ export function Select<T>({
5252
onValueChange(currentValue)
5353
}, [])
5454

55-
return (
56-
<View className="flex-1 flex-row items-center">
57-
{!!label && <FormLabel className="pl-2" label={label} />}
55+
const Trigger = (
56+
<DropdownMenu<T>
57+
options={options.map((option) => ({
58+
label: option.label,
59+
value: option.value,
60+
}))}
61+
currentValue={currentValue}
62+
handleChangeValue={handleChangeValue}
63+
>
64+
<View
65+
className={cn(
66+
"flex-1 shrink flex-row items-center rounded-lg pl-3",
5867

59-
<View className="flex-1" />
60-
{/* Trigger */}
61-
<DropdownMenu<T>
62-
options={options.map((option) => ({
63-
label: option.label,
64-
value: option.value,
65-
}))}
66-
currentValue={currentValue}
67-
handleChangeValue={handleChangeValue}
68+
wrapperClassName,
69+
)}
70+
style={wrapperStyle}
6871
>
69-
<View
70-
className={cn(
71-
"h-8 flex-row items-center rounded-lg pl-3 pr-2",
72-
"min-w-[80px]",
73-
wrapperClassName,
74-
)}
75-
style={wrapperStyle}
76-
>
77-
<Text className="font-semibold text-accent">{valueToLabelMap.get(currentValue)}</Text>
78-
<View className="ml-auto shrink-0 pl-1">
79-
<MingcuteDownLineIcon color={accentColor} height={18} width={18} />
80-
</View>
72+
<Text className="flex-1 font-semibold text-accent" ellipsizeMode="middle" numberOfLines={1}>
73+
{valueToLabelMap.get(currentValue)}
74+
</Text>
75+
<View className="ml-auto shrink-0 pl-1">
76+
<MingcuteDownLineIcon color={accentColor} height={18} width={18} />
8177
</View>
82-
</DropdownMenu>
78+
</View>
79+
</DropdownMenu>
80+
)
81+
82+
if (!label) {
83+
return Trigger
84+
}
85+
86+
return (
87+
<View className="flex-1 flex-row items-center">
88+
<FormLabel className="pl-2" label={label} />
89+
<View className="flex-1" />
90+
91+
{Trigger}
8392
</View>
8493
)
8594
}

apps/mobile/src/components/ui/form/Switch.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { forwardRef } from "react"
2-
import type { StyleProp, SwitchProps, ViewStyle } from "react-native"
3-
import { Switch, Text, View } from "react-native"
4-
5-
import { accentColor } from "@/src/theme/colors"
2+
import type { StyleProp, Switch as NativeSwitch, ViewStyle } from "react-native"
3+
import { Text, View } from "react-native"
64

5+
import type { SwitchProps, SwitchRef } from "../switch/Switch"
6+
import { Switch } from "../switch/Switch"
77
import { FormLabel } from "./Label"
88

99
interface Props {
@@ -12,19 +12,26 @@ interface Props {
1212

1313
label?: string
1414
description?: string
15+
16+
size?: "sm" | "default"
1517
}
1618

17-
export const FormSwitch = forwardRef<Switch, Props & SwitchProps>(
18-
({ wrapperClassName, wrapperStyle, label, description, ...rest }, ref) => {
19+
export const FormSwitch = forwardRef<SwitchRef, Props & SwitchProps>(
20+
({ wrapperClassName, wrapperStyle, label, description, size = "default", ...rest }, ref) => {
21+
const Trigger = <Switch size={size} ref={ref} {...rest} />
22+
23+
if (!label) {
24+
return Trigger
25+
}
1926
return (
2027
<View className={"w-full flex-row"}>
2128
<View className="flex-1">
22-
{!!label && <FormLabel className="pl-1" label={label} optional />}
29+
<FormLabel className="pl-1" label={label} optional />
2330
{!!description && (
2431
<Text className="text-secondary-label mb-1 pl-1 text-sm">{description}</Text>
2532
)}
2633
</View>
27-
<Switch trackColor={{ true: accentColor }} ref={ref} {...rest} />
34+
{Trigger}
2835
</View>
2936
)
3037
},

0 commit comments

Comments
 (0)