Skip to content

Commit

Permalink
feat(rn-settings): impl gerenate settings
Browse files Browse the repository at this point in the history
  • Loading branch information
Innei committed Jan 20, 2025
1 parent 85384d1 commit 203c253
Show file tree
Hide file tree
Showing 19 changed files with 701 additions and 71 deletions.
1 change: 1 addition & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
favicon: iconPath,
},
plugins: [
"expo-localization",
[
"expo-router",
{
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"expo-image": "~2.0.3",
"expo-linear-gradient": "~14.0.1",
"expo-linking": "~7.0.3",
"expo-localization": "~16.0.1",
"expo-router": "4.0.11",
"expo-secure-store": "^14.0.1",
"expo-sharing": "~13.0.0",
Expand Down
44 changes: 44 additions & 0 deletions apps/mobile/src/atoms/settings/general.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { GeneralSettings } from "@/src/interfaces/settings/general"

import { createSettingAtom } from "./internal/helper"

const createDefaultSettings = (): GeneralSettings => ({
// App

language: "en",
translationLanguage: "zh-CN",

// Data control

sendAnonymousData: true,

autoGroup: true,

// view
unreadOnly: true,
// mark unread
scrollMarkUnread: true,

renderMarkUnread: false,
// UX
groupByDate: true,
autoExpandLongSocialMedia: false,

// Secure
jumpOutLinkWarn: true,
// TTS
voice: "en-US-AndrewMultilingualNeural",
})

export const {
useSettingKey: useGeneralSettingKey,
useSettingSelector: useGeneralSettingSelector,
useSettingKeys: useGeneralSettingKeys,
setSetting: setGeneralSetting,
clearSettings: clearGeneralSettings,
initializeDefaultSettings: initializeDefaultGeneralSettings,
getSettings: getGeneralSettings,
useSettingValue: useGeneralSettingValue,

settingAtom: __generalSettingAtom,
} = createSettingAtom("general", createDefaultSettings)
160 changes: 160 additions & 0 deletions apps/mobile/src/atoms/settings/internal/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { useRefValue } from "@follow/hooks"
import { createAtomHooks } from "@follow/utils"
import type { SetStateAction, WritableAtom } from "jotai"
import { atom as jotaiAtom, useAtomValue } from "jotai"
import { atomWithStorage, selectAtom } from "jotai/utils"
import { useMemo } from "react"
import { shallow } from "zustand/shallow"

import { JotaiPersistSyncStorage } from "@/src/lib/jotai"

const getStorageNS = (settingKey: string) => `follow-rn-${settingKey}`
type Nullable<T> = T | null | undefined

export const createSettingAtom = <T extends object>(
settingKey: string,
createDefaultSettings: () => T,
) => {
const atom = atomWithStorage(
getStorageNS(settingKey),
createDefaultSettings(),
JotaiPersistSyncStorage,
{
getOnInit: true,
},
) as WritableAtom<T, [SetStateAction<T>], void>

const [, , useSettingValue, , getSettings, setSettings] = createAtomHooks(atom)

const initializeDefaultSettings = () => {
const currentSettings = getSettings()
const defaultSettings = createDefaultSettings()
if (typeof currentSettings !== "object") setSettings(defaultSettings)
const newSettings = { ...defaultSettings, ...currentSettings }
setSettings(newSettings)
}

const selectAtomCacheMap = {} as Record<keyof ReturnType<typeof getSettings>, any>

const noopAtom = jotaiAtom(null)

const useMaybeSettingKey = <T extends keyof ReturnType<typeof getSettings>>(key: Nullable<T>) => {
// @ts-expect-error
let selectedAtom: Record<keyof T, any>[T] | null = null
if (key) {
selectedAtom = selectAtomCacheMap[key]
if (!selectedAtom) {
selectedAtom = selectAtom(atom, (s) => s[key])
selectAtomCacheMap[key] = selectedAtom
}
} else {
selectedAtom = noopAtom
}

return useAtomValue(selectedAtom) as ReturnType<typeof getSettings>[T]
}

const useSettingKey = <T extends keyof ReturnType<typeof getSettings>>(key: T) => {
return useMaybeSettingKey(key) as ReturnType<typeof getSettings>[T]
}

function useSettingKeys<
T extends keyof ReturnType<typeof getSettings>,
K1 extends T,
K2 extends T,
K3 extends T,
K4 extends T,
K5 extends T,
K6 extends T,
K7 extends T,
K8 extends T,
K9 extends T,
K10 extends T,
>(keys: [K1, K2?, K3?, K4?, K5?, K6?, K7?, K8?, K9?, K10?]) {
return [
useMaybeSettingKey(keys[0]),
useMaybeSettingKey(keys[1]),
useMaybeSettingKey(keys[2]),
useMaybeSettingKey(keys[3]),
useMaybeSettingKey(keys[4]),
useMaybeSettingKey(keys[5]),
useMaybeSettingKey(keys[6]),
useMaybeSettingKey(keys[7]),
useMaybeSettingKey(keys[8]),
useMaybeSettingKey(keys[9]),
] as [
ReturnType<typeof getSettings>[K1],
ReturnType<typeof getSettings>[K2],
ReturnType<typeof getSettings>[K3],
ReturnType<typeof getSettings>[K4],
ReturnType<typeof getSettings>[K5],
ReturnType<typeof getSettings>[K6],
ReturnType<typeof getSettings>[K7],
ReturnType<typeof getSettings>[K8],
ReturnType<typeof getSettings>[K9],
ReturnType<typeof getSettings>[K10],
]
}

const useSettingSelector = <
T extends keyof ReturnType<typeof getSettings>,
S extends ReturnType<typeof getSettings>,
R = S[T],
>(
selector: (s: S) => R,
): R => {
const stableSelector = useRefValue(selector)

return useAtomValue(
// @ts-expect-error
useMemo(() => selectAtom(atom, stableSelector.current, shallow), [stableSelector]),
)
}

const setSetting = <K extends keyof ReturnType<typeof getSettings>>(
key: K,
value: ReturnType<typeof getSettings>[K],
) => {
const updated = Date.now()
setSettings({
...getSettings(),
[key]: value,

updated,
})
}

const clearSettings = () => {
setSettings(createDefaultSettings())
}

Object.defineProperty(useSettingValue, "select", {
value: useSettingSelector,
})

return {
useSettingKey,
useSettingSelector,
setSetting,
clearSettings,
initializeDefaultSettings,

useSettingValue,
useSettingKeys,
getSettings,

settingAtom: atom,
} as {
useSettingKey: typeof useSettingKey
useSettingSelector: typeof useSettingSelector
setSetting: typeof setSetting
clearSettings: typeof clearSettings
initializeDefaultSettings: typeof initializeDefaultSettings
useSettingValue: typeof useSettingValue & {
select: <T extends keyof ReturnType<() => T>>(key: T) => Awaited<T[T]>
}
useSettingKeys: typeof useSettingKeys
getSettings: typeof getSettings
settingAtom: typeof atom
}
}
2 changes: 1 addition & 1 deletion apps/mobile/src/components/common/ThemedBlurView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const ThemedBlurView = forwardRef<BlurView, BlurViewProps>(({ tint, ...re
return (
<BlurView
ref={ref}
tint={colorScheme === "light" ? "systemMaterialLight" : "systemMaterialDark"}
tint={colorScheme === "light" ? "systemChromeMaterialLight" : "systemChromeMaterialDark"}
{...rest}
/>
)
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/components/ui/dropdown/DropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function DropdownMenu<T>({
const isActionMenu = options.every((option) => "title" in option)
return (
<ContextMenu
style={{ flex: 1 }}
dropdownMenuMode
actions={
isActionMenu
Expand Down
59 changes: 34 additions & 25 deletions apps/mobile/src/components/ui/form/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,34 +52,43 @@ export function Select<T>({
onValueChange(currentValue)
}, [])

return (
<View className="flex-1 flex-row items-center">
{!!label && <FormLabel className="pl-2" label={label} />}
const Trigger = (
<DropdownMenu<T>
options={options.map((option) => ({
label: option.label,
value: option.value,
}))}
currentValue={currentValue}
handleChangeValue={handleChangeValue}
>
<View
className={cn(
"flex-1 shrink flex-row items-center rounded-lg pl-3",

<View className="flex-1" />
{/* Trigger */}
<DropdownMenu<T>
options={options.map((option) => ({
label: option.label,
value: option.value,
}))}
currentValue={currentValue}
handleChangeValue={handleChangeValue}
wrapperClassName,
)}
style={wrapperStyle}
>
<View
className={cn(
"h-8 flex-row items-center rounded-lg pl-3 pr-2",
"min-w-[80px]",
wrapperClassName,
)}
style={wrapperStyle}
>
<Text className="font-semibold text-accent">{valueToLabelMap.get(currentValue)}</Text>
<View className="ml-auto shrink-0 pl-1">
<MingcuteDownLineIcon color={accentColor} height={18} width={18} />
</View>
<Text className="flex-1 font-semibold text-accent" ellipsizeMode="middle" numberOfLines={1}>
{valueToLabelMap.get(currentValue)}
</Text>
<View className="ml-auto shrink-0 pl-1">
<MingcuteDownLineIcon color={accentColor} height={18} width={18} />
</View>
</DropdownMenu>
</View>
</DropdownMenu>
)

if (!label) {
return Trigger
}

return (
<View className="flex-1 flex-row items-center">
<FormLabel className="pl-2" label={label} />
<View className="flex-1" />

{Trigger}
</View>
)
}
23 changes: 15 additions & 8 deletions apps/mobile/src/components/ui/form/Switch.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { forwardRef } from "react"
import type { StyleProp, SwitchProps, ViewStyle } from "react-native"
import { Switch, Text, View } from "react-native"

import { accentColor } from "@/src/theme/colors"
import type { StyleProp, Switch as NativeSwitch, ViewStyle } from "react-native"
import { Text, View } from "react-native"

import type { SwitchProps, SwitchRef } from "../switch/Switch"
import { Switch } from "../switch/Switch"
import { FormLabel } from "./Label"

interface Props {
Expand All @@ -12,19 +12,26 @@ interface Props {

label?: string
description?: string

size?: "sm" | "default"
}

export const FormSwitch = forwardRef<Switch, Props & SwitchProps>(
({ wrapperClassName, wrapperStyle, label, description, ...rest }, ref) => {
export const FormSwitch = forwardRef<SwitchRef, Props & SwitchProps>(
({ wrapperClassName, wrapperStyle, label, description, size = "default", ...rest }, ref) => {
const Trigger = <Switch size={size} ref={ref} {...rest} />

if (!label) {
return Trigger
}
return (
<View className={"w-full flex-row"}>
<View className="flex-1">
{!!label && <FormLabel className="pl-1" label={label} optional />}
<FormLabel className="pl-1" label={label} optional />
{!!description && (
<Text className="text-secondary-label mb-1 pl-1 text-sm">{description}</Text>
)}
</View>
<Switch trackColor={{ true: accentColor }} ref={ref} {...rest} />
{Trigger}
</View>
)
},
Expand Down
Loading

0 comments on commit 203c253

Please sign in to comment.