diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts
index 4c52df2d3b..a7be96d38f 100644
--- a/apps/mobile/app.config.ts
+++ b/apps/mobile/app.config.ts
@@ -50,6 +50,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
favicon: iconPath,
},
plugins: [
+ "expo-localization",
[
"expo-router",
{
diff --git a/apps/mobile/global.d.ts b/apps/mobile/global.d.ts
index 8e9d706bfd..9aed8ba265 100644
--- a/apps/mobile/global.d.ts
+++ b/apps/mobile/global.d.ts
@@ -1,7 +1,8 @@
import type { DOMProps } from "expo/dom"
import type { FC } from "react"
+import type WebView from "react-native-webview"
declare global {
- export type WebComponent
= FC
+ export type WebComponent
= FC
>
}
export {}
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index fc809f4867..d53cfa95b7 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "@follow/mobile",
- "version": "1.0.0",
+ "version": "0.1.0",
"private": true,
"main": "src/main.tsx",
"scripts": {
@@ -43,7 +43,7 @@
"expo-blur": "~14.0.1",
"expo-build-properties": "^0.13.1",
"expo-clipboard": "~7.0.0",
- "expo-constants": "~17.0.3",
+ "expo-constants": "~17.0.4",
"expo-dev-client": "^5.0.9",
"expo-file-system": "~18.0.6",
"expo-font": "~13.0.1",
@@ -51,6 +51,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",
diff --git a/apps/mobile/src/atoms/settings/general.ts b/apps/mobile/src/atoms/settings/general.ts
new file mode 100644
index 0000000000..756503704d
--- /dev/null
+++ b/apps/mobile/src/atoms/settings/general.ts
@@ -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)
diff --git a/apps/mobile/src/atoms/settings/internal/helper.ts b/apps/mobile/src/atoms/settings/internal/helper.ts
new file mode 100644
index 0000000000..d6d37fce4e
--- /dev/null
+++ b/apps/mobile/src/atoms/settings/internal/helper.ts
@@ -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 | null | undefined
+
+export const createSettingAtom = (
+ settingKey: string,
+ createDefaultSettings: () => T,
+) => {
+ const atom = atomWithStorage(
+ getStorageNS(settingKey),
+ createDefaultSettings(),
+ JotaiPersistSyncStorage,
+ {
+ getOnInit: true,
+ },
+ ) as WritableAtom], 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, any>
+
+ const noopAtom = jotaiAtom(null)
+
+ const useMaybeSettingKey = >(key: Nullable) => {
+ // @ts-expect-error
+ let selectedAtom: Record[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[T]
+ }
+
+ const useSettingKey = >(key: T) => {
+ return useMaybeSettingKey(key) as ReturnType[T]
+ }
+
+ function useSettingKeys<
+ T extends keyof ReturnType,
+ 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[K1],
+ ReturnType[K2],
+ ReturnType[K3],
+ ReturnType[K4],
+ ReturnType[K5],
+ ReturnType[K6],
+ ReturnType[K7],
+ ReturnType[K8],
+ ReturnType[K9],
+ ReturnType[K10],
+ ]
+ }
+
+ const useSettingSelector = <
+ T extends keyof ReturnType,
+ S extends ReturnType,
+ 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 = >(
+ key: K,
+ value: ReturnType[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>>(key: T) => Awaited
+ }
+ useSettingKeys: typeof useSettingKeys
+ getSettings: typeof getSettings
+ settingAtom: typeof atom
+ }
+}
diff --git a/apps/mobile/src/components/common/BlurEffect.tsx b/apps/mobile/src/components/common/BlurEffect.tsx
new file mode 100644
index 0000000000..aff6b96efe
--- /dev/null
+++ b/apps/mobile/src/components/common/BlurEffect.tsx
@@ -0,0 +1,34 @@
+import { StyleSheet } from "react-native"
+import { useColor } from "react-native-uikit-colors"
+
+import { ThemedBlurView } from "@/src/components/common/ThemedBlurView"
+
+const node = (
+
+)
+export const BlurEffect = () => {
+ return node
+}
+
+const InternalBlurEffectWithBottomBorder = () => {
+ const border = useColor("opaqueSeparator")
+ return (
+
+ )
+}
+
+export const BlurEffectWithBottomBorder = () =>
diff --git a/apps/mobile/src/components/common/FollowWebView.tsx b/apps/mobile/src/components/common/FollowWebView.tsx
index 474f2fba20..2bfbd02e6c 100644
--- a/apps/mobile/src/components/common/FollowWebView.tsx
+++ b/apps/mobile/src/components/common/FollowWebView.tsx
@@ -1,14 +1,13 @@
import { callWebviewExpose } from "@follow/shared"
-import { parseSafeUrl, transformVideoUrl } from "@follow/utils"
import * as Linking from "expo-linking"
import type { RefObject } from "react"
import { useCallback, useEffect, useState } from "react"
import { Platform } from "react-native"
-import type { WebViewNavigation, WebViewProps } from "react-native-webview"
+import type { WebViewProps } from "react-native-webview"
import { WebView } from "react-native-webview"
+import { useWebViewNavigation } from "@/src/hooks/useWebViewNavigation"
import { signOut } from "@/src/lib/auth"
-import { useOpenLink } from "@/src/lib/hooks/use-open-link"
const presetUri = Platform.select({
ios: "rn-web/index.html",
@@ -16,8 +15,6 @@ const presetUri = Platform.select({
default: "https://app.follow.is",
})
-const allowHosts = new Set(["app.follow.is"])
-
interface FollowWebViewProps extends WebViewProps {
customUrl?: string
}
@@ -80,32 +77,6 @@ export const FollowWebView = ({
)
}
-const useWebViewNavigation = ({ webViewRef }: { webViewRef: RefObject }) => {
- const openLink = useOpenLink()
-
- const onNavigationStateChange = useCallback(
- (newNavState: WebViewNavigation) => {
- const { url: urlStr } = newNavState
- const url = parseSafeUrl(urlStr)
- if (!url) return
- if (url.protocol === "file:") return
- if (allowHosts.has(url.host)) return
-
- webViewRef.current?.stopLoading()
-
- const formattedUrl = transformVideoUrl({ url: urlStr })
- if (formattedUrl) {
- openLink(formattedUrl)
- return
- }
- openLink(urlStr)
- },
- [openLink, webViewRef],
- )
-
- return { onNavigationStateChange }
-}
-
// We only need to handle deep link at the first time the app is opened
let lastInitialUrl: string | null = null
const useDeepLink = ({
diff --git a/apps/mobile/src/components/common/HeaderBlur.tsx b/apps/mobile/src/components/common/HeaderBlur.tsx
deleted file mode 100644
index 06f8f01592..0000000000
--- a/apps/mobile/src/components/common/HeaderBlur.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { StyleSheet } from "react-native"
-
-import { ThemedBlurView } from "@/src/components/common/ThemedBlurView"
-
-const node = (
-
-)
-export const BlurEffect = () => {
- return node
-}
diff --git a/apps/mobile/src/components/common/SafeNavigationScrollView.tsx b/apps/mobile/src/components/common/SafeNavigationScrollView.tsx
index 089e8f3d86..ce3f8643a5 100644
--- a/apps/mobile/src/components/common/SafeNavigationScrollView.tsx
+++ b/apps/mobile/src/components/common/SafeNavigationScrollView.tsx
@@ -1,20 +1,119 @@
import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs"
import { useHeaderHeight } from "@react-navigation/elements"
-import type { FC } from "react"
+import { router, Stack, useNavigation } from "expo-router"
+import type { FC, PropsWithChildren } from "react"
+import { createContext, useContext, useEffect, useMemo, useState } from "react"
import type { ScrollViewProps } from "react-native"
-import { ScrollView, View } from "react-native"
+import {
+ Animated as RNAnimated,
+ StyleSheet,
+ TouchableOpacity,
+ useAnimatedValue,
+ View,
+} from "react-native"
+import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes"
import { useSafeAreaInsets } from "react-native-safe-area-context"
+import { useColor } from "react-native-uikit-colors"
-export const SafeNavigationScrollView: FC = ({ children, ...props }) => {
- const headerHeight = useHeaderHeight()
- const tabBarHeight = useBottomTabBarHeight()
+import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line"
+
+import { AnimatedScrollView } from "./AnimatedComponents"
+import { ThemedBlurView } from "./ThemedBlurView"
+
+type SafeNavigationScrollViewProps = Omit & {
+ withHeaderBlur?: boolean
+ onScroll?: (e: ReanimatedScrollEvent) => void
+
+ // For scroll view content adjustment behavior
+ withTopInset?: boolean
+ withBottomInset?: boolean
+} & PropsWithChildren
+const NavigationContext = createContext<{
+ scrollY: RNAnimated.Value
+} | null>(null)
+
+export const SafeNavigationScrollView: FC = ({
+ children,
+
+ withHeaderBlur = true,
+ onScroll,
+
+ withBottomInset = false,
+ withTopInset = false,
+
+ ...props
+}) => {
const insets = useSafeAreaInsets()
+ const tabBarHeight = useBottomTabBarHeight()
+ const headerHeight = useHeaderHeight()
+
+ const scrollY = useAnimatedValue(0)
return (
-
-
- {children}
-
-
+ ({ scrollY }), [scrollY])}>
+ {withHeaderBlur && }
+
+
+ {children}
+
+
+
+ )
+}
+
+export interface NavigationBlurEffectHeaderProps {
+ title?: string
+}
+export const NavigationBlurEffectHeader = (props: NavigationBlurEffectHeaderProps) => {
+ const label = useColor("label")
+
+ const canBack = useNavigation().canGoBack()
+
+ const { scrollY } = useContext(NavigationContext)!
+
+ const border = useColor("opaqueSeparator")
+
+ const [opacity, setOpacity] = useState(0)
+
+ useEffect(() => {
+ const id = scrollY.addListener(({ value }) => {
+ setOpacity(Math.min(1, Math.max(0, Math.min(1, value / 10))))
+ })
+
+ return () => {
+ scrollY.removeListener(id)
+ }
+ }, [scrollY])
+
+ return (
+ (
+
+ ),
+ headerTransparent: true,
+
+ headerLeft: canBack
+ ? () => (
+ router.back()}>
+
+
+ )
+ : undefined,
+ title: props.title,
+ }}
+ />
)
}
diff --git a/apps/mobile/src/components/common/ThemedBlurView.tsx b/apps/mobile/src/components/common/ThemedBlurView.tsx
index 806c3bdf83..f917ab97bb 100644
--- a/apps/mobile/src/components/common/ThemedBlurView.tsx
+++ b/apps/mobile/src/components/common/ThemedBlurView.tsx
@@ -5,10 +5,12 @@ import { forwardRef } from "react"
export const ThemedBlurView = forwardRef(({ tint, ...rest }, ref) => {
const { colorScheme } = useColorScheme()
+
return (
)
diff --git a/apps/mobile/src/components/ui/dropdown/DropdownMenu.tsx b/apps/mobile/src/components/ui/dropdown/DropdownMenu.tsx
new file mode 100644
index 0000000000..cf61d2d8ce
--- /dev/null
+++ b/apps/mobile/src/components/ui/dropdown/DropdownMenu.tsx
@@ -0,0 +1,53 @@
+import type { NativeSyntheticEvent } from "react-native"
+import type { ContextMenuOnPressNativeEvent } from "react-native-context-menu-view"
+import ContextMenu from "react-native-context-menu-view"
+
+interface DropdownMenuAction {
+ title: string
+ actions?: DropdownMenuAction[]
+ selected?: boolean
+}
+
+interface DropdownMenuSelector {
+ label: string
+ value: T
+}
+
+export function DropdownMenu({
+ options,
+ currentValue,
+ handleChangeValue,
+ handlePress,
+ children,
+}: {
+ options: DropdownMenuSelector[] | DropdownMenuAction[]
+ currentValue?: T
+ handleChangeValue?: (value: T) => void
+ handlePress?: (e: NativeSyntheticEvent) => void
+ children: React.ReactNode
+}) {
+ const isActionMenu = options.every((option) => "title" in option)
+ return (
+ ({
+ title: option.label,
+ selected: option.value === currentValue,
+ }))
+ }
+ onPress={(e) => {
+ if (!isActionMenu) {
+ const { index } = e.nativeEvent
+ handleChangeValue?.(options[index]!.value)
+ }
+ handlePress?.(e)
+ }}
+ >
+ {children}
+
+ )
+}
diff --git a/apps/mobile/src/components/ui/form/PickerIos.tsx b/apps/mobile/src/components/ui/form/PickerIos.tsx
index 9456ce60f4..bf8cbe5eab 100644
--- a/apps/mobile/src/components/ui/form/PickerIos.tsx
+++ b/apps/mobile/src/components/ui/form/PickerIos.tsx
@@ -11,7 +11,7 @@ import { useEventCallback } from "usehooks-ts"
import { MingcuteDownLineIcon } from "@/src/icons/mingcute_down_line"
import { useColor } from "@/src/theme/colors"
-import { BlurEffect } from "../../common/HeaderBlur"
+import { BlurEffect } from "../../common/BlurEffect"
interface PickerIosProps {
options: { label: string; value: T }[]
diff --git a/apps/mobile/src/components/ui/form/Select.tsx b/apps/mobile/src/components/ui/form/Select.tsx
index ccd1d8fa4b..f60abab9de 100644
--- a/apps/mobile/src/components/ui/form/Select.tsx
+++ b/apps/mobile/src/components/ui/form/Select.tsx
@@ -2,12 +2,12 @@ import { cn } from "@follow/utils"
import { useEffect, useMemo, useState } from "react"
import type { StyleProp, ViewStyle } from "react-native"
import { Text, View } from "react-native"
-import ContextMenu from "react-native-context-menu-view"
import { useEventCallback } from "usehooks-ts"
import { MingcuteDownLineIcon } from "@/src/icons/mingcute_down_line"
import { accentColor } from "@/src/theme/colors"
+import { DropdownMenu } from "../dropdown/DropdownMenu"
import { FormLabel } from "./Label"
interface SelectProps {
@@ -52,36 +52,43 @@ export function Select({
onValueChange(currentValue)
}, [])
- return (
-
- {!!label && }
-
- {/* Trigger */}
- ({
- title: option.label,
- selected: option.value === currentValue,
- }))}
- onPress={(e) => {
- const { index } = e.nativeEvent
- handleChangeValue(options[index]!.value)
- }}
+ const Trigger = (
+
+ options={options.map((option) => ({
+ label: option.label,
+ value: option.value,
+ }))}
+ currentValue={currentValue}
+ handleChangeValue={handleChangeValue}
+ >
+
-
- {valueToLabelMap.get(currentValue)}
-
-
-
+
+ {valueToLabelMap.get(currentValue)}
+
+
+
-
+
+
+ )
+
+ if (!label) {
+ return Trigger
+ }
+
+ return (
+
+
+
+
+ {Trigger}
)
}
diff --git a/apps/mobile/src/components/ui/form/Switch.tsx b/apps/mobile/src/components/ui/form/Switch.tsx
index 7d179faefb..20fa0a280b 100644
--- a/apps/mobile/src/components/ui/form/Switch.tsx
+++ b/apps/mobile/src/components/ui/form/Switch.tsx
@@ -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, 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 {
@@ -12,19 +12,26 @@ interface Props {
label?: string
description?: string
+
+ size?: "sm" | "default"
}
-export const FormSwitch = forwardRef(
- ({ wrapperClassName, wrapperStyle, label, description, ...rest }, ref) => {
+export const FormSwitch = forwardRef(
+ ({ wrapperClassName, wrapperStyle, label, description, size = "default", ...rest }, ref) => {
+ const Trigger =
+
+ if (!label) {
+ return Trigger
+ }
return (
- {!!label && }
+
{!!description && (
{description}
)}
-
+ {Trigger}
)
},
diff --git a/apps/mobile/src/components/ui/grouped/GroupedList.tsx b/apps/mobile/src/components/ui/grouped/GroupedList.tsx
index 5f566ce9fd..69c8a4bad1 100644
--- a/apps/mobile/src/components/ui/grouped/GroupedList.tsx
+++ b/apps/mobile/src/components/ui/grouped/GroupedList.tsx
@@ -8,17 +8,33 @@ import { Pressable, StyleSheet, Text, View } from "react-native"
import { RightCuteReIcon } from "@/src/icons/right_cute_re"
import { useColor } from "@/src/theme/colors"
-export const GroupedInsetListCard: FC = ({ children }) => {
+export const GroupedInsetListCard: FC = ({
+ children,
+ className,
+ ...props
+}) => {
return (
-
+
{React.Children.map(children, (child, index) => {
const isLast = index === React.Children.count(children) - 1
+
+ const isNavigationLink =
+ React.isValidElement(child) &&
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
+ (child.type as Function).name === GroupedInsetListNavigationLink.name
+
return (
{child}
{!isLast && (
)}
@@ -41,9 +57,15 @@ export const GroupedInsetListSectionHeader: FC<{
)
}
-export const GroupedInsetListItem: FC = ({ children, ...props }) => {
+export const GroupedInsetListBaseCell: FC = ({
+ children,
+ ...props
+}) => {
return (
-
+
{children}
)
@@ -59,8 +81,8 @@ export const GroupedInsetListNavigationLink: FC<{
return (
{({ pressed }) => (
-
-
+
+
{icon}
{label}
@@ -69,8 +91,42 @@ export const GroupedInsetListNavigationLink: FC<{
-
+
)}
)
}
+
+export const GroupedInsetListNavigationLinkIcon: FC<
+ {
+ backgroundColor: string
+ } & PropsWithChildren
+> = ({ backgroundColor, children }) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export const GroupedInsetListCell: FC<{
+ label: string
+ description?: string
+ children: React.ReactNode
+}> = ({ label, description, children }) => {
+ return (
+
+
+ {label}
+ {!!description && {description}}
+
+
+ {children}
+
+ )
+}
diff --git a/apps/mobile/src/components/ui/switch/Switch.tsx b/apps/mobile/src/components/ui/switch/Switch.tsx
new file mode 100644
index 0000000000..fe22f83732
--- /dev/null
+++ b/apps/mobile/src/components/ui/switch/Switch.tsx
@@ -0,0 +1,159 @@
+import { forwardRef, useEffect, useImperativeHandle } from "react"
+import type { SwitchChangeEvent } from "react-native"
+import { Pressable, StyleSheet, View } from "react-native"
+import Animated, {
+ interpolate,
+ interpolateColor,
+ useAnimatedStyle,
+ useSharedValue,
+ withSpring,
+ withTiming,
+} from "react-native-reanimated"
+
+import { accentColor, useColor } from "@/src/theme/colors"
+
+export interface SwitchProps {
+ onChange?: ((event: SwitchChangeEvent) => Promise | void) | null | undefined
+
+ /**
+ * Invoked with the new value when the value changes.
+ */
+ onValueChange?: ((value: boolean) => Promise | void) | null | undefined
+
+ /**
+ * The value of the switch. If true the switch will be turned on.
+ * Default value is false.
+ */
+ value?: boolean | undefined
+
+ size?: "sm" | "default"
+}
+
+export type SwitchRef = {
+ value: boolean
+}
+export const Switch = forwardRef(
+ ({ value, onValueChange, onChange, size = "default" }, ref) => {
+ const progress = useSharedValue(value ? 1 : 0)
+ const scale = useSharedValue(1)
+ const translateX = useSharedValue(0)
+
+ const onTouchStart = () => {
+ scale.value = withSpring(1.1)
+ if (value) {
+ translateX.value = withSpring(size === "sm" ? -4 : -7)
+ }
+ }
+
+ const onTouchEnd = () => {
+ scale.value = withSpring(1)
+ translateX.value = withSpring(0)
+ }
+
+ useImperativeHandle(ref, () => ({
+ value: !!value,
+ }))
+
+ const activeBgColor = accentColor
+ const inactiveBgColor = useColor("secondarySystemFill")
+
+ const toggleStyle = useAnimatedStyle(() => {
+ const backgroundColor = interpolateColor(
+ progress.value,
+ [0, 1],
+ [inactiveBgColor, activeBgColor],
+ )
+
+ return {
+ backgroundColor,
+ }
+ })
+
+ const circleStyle = useAnimatedStyle(() => {
+ const marginLeft = interpolate(progress.value, [0, 1], size === "sm" ? [2, 20] : [2.3, 22])
+
+ const width = interpolate(scale.value, [1, 1.1], size === "sm" ? [18, 21] : [27.8, 35])
+
+ return {
+ marginLeft,
+ width,
+ transform: [{ translateX: translateX.value }, { translateY: -0.4 }, { scale: scale.value }],
+ }
+ })
+
+ useEffect(() => {
+ // Update progress when value changes
+ if (value && progress.value === 0) {
+ progress.value = withTiming(1)
+ } else if (!value && progress.value === 1) {
+ progress.value = withTiming(0)
+ }
+ }, [progress, value])
+
+ return (
+
+ {
+ onValueChange?.(!value)
+ onChange?.({ target: { value: !value } as any } as SwitchChangeEvent)
+ }}
+ >
+
+
+
+
+
+ )
+ },
+)
+
+const styles = StyleSheet.create({
+ container: { display: "flex", justifyContent: "space-between" },
+ toggleContainer: {
+ width: 52,
+ height: 32.7,
+ borderRadius: 4000,
+ justifyContent: "center",
+ },
+ toggleContainerSm: {
+ width: 40,
+ height: 24,
+ borderRadius: 4000,
+ justifyContent: "center",
+ },
+ toggleWheelStyle: {
+ height: 28.5,
+ backgroundColor: "#ffffff",
+ borderRadius: 200,
+ shadowColor: "#515151",
+ shadowOffset: {
+ width: 0,
+ height: 0,
+ },
+ shadowOpacity: 0.2,
+ shadowRadius: 2.5,
+ elevation: 1.5,
+ },
+ toggleWheelStyleSm: {
+ height: 20,
+ backgroundColor: "#ffffff",
+ borderRadius: 200,
+ shadowColor: "#515151",
+ shadowOffset: {
+ width: 0,
+ height: 0,
+ },
+ shadowOpacity: 0.2,
+ shadowRadius: 2.5,
+ elevation: 1.5,
+ },
+})
diff --git a/apps/mobile/src/components/ui/typography/Markdown.tsx b/apps/mobile/src/components/ui/typography/Markdown.tsx
new file mode 100644
index 0000000000..2baf6552cb
--- /dev/null
+++ b/apps/mobile/src/components/ui/typography/Markdown.tsx
@@ -0,0 +1,41 @@
+import type { FC } from "react"
+import { useRef } from "react"
+import { Linking } from "react-native"
+import type { WebView } from "react-native-webview"
+
+import MarkdownWeb from "./MarkdownWeb"
+
+export const Markdown: FC<{
+ value: string
+ style?: React.CSSProperties
+ className?: string
+
+ webViewProps?: import("expo/dom").DOMProps
+}> = ({ value, style, className, webViewProps }) => {
+ const ref = useRef(null)
+
+ return (
+ {
+ const { type, url } = JSON.parse(event.nativeEvent.data)
+ if (type === "openLinkInModal") {
+ Linking.openURL(url)
+ }
+ },
+ injectedJavaScriptBeforeContentLoaded: `window.openLinkInModal = (url) => {
+ window.ReactNativeWebView.postMessage(JSON.stringify({
+ type: "openLinkInModal",
+ url,
+ }))
+ }`,
+ }}
+ />
+ )
+}
diff --git a/apps/mobile/src/components/ui/typography/MarkdownWeb.tsx b/apps/mobile/src/components/ui/typography/MarkdownWeb.tsx
index 37158ce36a..e50ea79880 100644
--- a/apps/mobile/src/components/ui/typography/MarkdownWeb.tsx
+++ b/apps/mobile/src/components/ui/typography/MarkdownWeb.tsx
@@ -8,29 +8,58 @@ import { useDarkMode } from "usehooks-ts"
import { useCSSInjection } from "@/src/theme/web"
-const MarkdownWeb: WebComponent<{ value: string; style?: React.CSSProperties }> = ({
- value,
- style,
-}) => {
+declare const window: {
+ openLinkInModal: (url: string) => void
+}
+/**
+ * @internal
+ */
+const MarkdownWeb: WebComponent<{
+ value: string
+ style?: React.CSSProperties
+ className?: string
+}> = ({ value, style, className }) => {
useCSSInjection()
const { isDarkMode } = useDarkMode()
return (
)
}
diff --git a/apps/mobile/src/hooks/useWebViewNavigation.tsx b/apps/mobile/src/hooks/useWebViewNavigation.tsx
new file mode 100644
index 0000000000..5bb727d60c
--- /dev/null
+++ b/apps/mobile/src/hooks/useWebViewNavigation.tsx
@@ -0,0 +1,34 @@
+import { parseSafeUrl, transformVideoUrl } from "@follow/utils"
+import type { RefObject } from "react"
+import { useCallback } from "react"
+import type WebView from "react-native-webview"
+import type { WebViewNavigation } from "react-native-webview"
+
+import { useOpenLink } from "../lib/hooks/use-open-link"
+
+const allowHosts = new Set(["app.follow.is"])
+export function useWebViewNavigation({ webViewRef }: { webViewRef: RefObject }) {
+ const openLink = useOpenLink()
+
+ const onNavigationStateChange = useCallback(
+ (newNavState: WebViewNavigation) => {
+ const { url: urlStr } = newNavState
+ const url = parseSafeUrl(urlStr)
+ if (!url) return
+ if (url.protocol === "file:") return
+ if (allowHosts.has(url.host)) return
+
+ webViewRef.current?.stopLoading()
+
+ const formattedUrl = transformVideoUrl({ url: urlStr })
+ if (formattedUrl) {
+ openLink(formattedUrl)
+ return
+ }
+ openLink(urlStr)
+ },
+ [openLink, webViewRef],
+ )
+
+ return { onNavigationStateChange }
+}
diff --git a/apps/mobile/src/interfaces/settings/general.ts b/apps/mobile/src/interfaces/settings/general.ts
new file mode 100644
index 0000000000..990adc3e29
--- /dev/null
+++ b/apps/mobile/src/interfaces/settings/general.ts
@@ -0,0 +1,20 @@
+export interface GeneralSettings {
+ language: string
+ translationLanguage: string
+
+ sendAnonymousData: boolean
+ unreadOnly: boolean
+ scrollMarkUnread: boolean
+
+ renderMarkUnread: boolean
+ groupByDate: boolean
+ jumpOutLinkWarn: boolean
+ // TTS
+ voice: string
+ autoGroup: boolean
+
+ /**
+ * Auto expand long social media
+ */
+ autoExpandLongSocialMedia: boolean
+}
diff --git a/apps/mobile/src/lib/language.ts b/apps/mobile/src/lib/language.ts
new file mode 100644
index 0000000000..cbcf4e61a4
--- /dev/null
+++ b/apps/mobile/src/lib/language.ts
@@ -0,0 +1,33 @@
+import type { languageSchema } from "@follow/shared/src/hono"
+import type { z } from "zod"
+
+export type SupportedLanguages = z.infer
+export const LanguageMap: Record<
+ SupportedLanguages,
+ {
+ label: string
+ value: string
+ code: string
+ }
+> = {
+ en: {
+ value: "en",
+ label: "English",
+ code: "eng",
+ },
+ ja: {
+ value: "ja",
+ label: "Japanese",
+ code: "jpn",
+ },
+ "zh-CN": {
+ value: "zh-CN",
+ label: "Simplified Chinese",
+ code: "cmn",
+ },
+ "zh-TW": {
+ value: "zh-TW",
+ label: "Traditional Chinese (Taiwan)",
+ code: "cmn",
+ },
+}
diff --git a/apps/mobile/src/modules/discover/search.tsx b/apps/mobile/src/modules/discover/search.tsx
index 6b1347489b..9d82df9187 100644
--- a/apps/mobile/src/modules/discover/search.tsx
+++ b/apps/mobile/src/modules/discover/search.tsx
@@ -17,7 +17,7 @@ import {
} from "react-native"
import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-context"
-import { BlurEffect } from "@/src/components/common/HeaderBlur"
+import { BlurEffect } from "@/src/components/common/BlurEffect"
import { Search2CuteReIcon } from "@/src/icons/search_2_cute_re"
import { accentColor, useColor } from "@/src/theme/colors"
diff --git a/apps/mobile/src/modules/feed-drawer/header.tsx b/apps/mobile/src/modules/feed-drawer/header.tsx
index 67471adc9e..57d5676637 100644
--- a/apps/mobile/src/modules/feed-drawer/header.tsx
+++ b/apps/mobile/src/modules/feed-drawer/header.tsx
@@ -3,7 +3,7 @@ import type { LayoutChangeEvent } from "react-native"
import { Text, View } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
-import { BlurEffect } from "@/src/components/common/HeaderBlur"
+import { BlurEffect } from "@/src/components/common/BlurEffect"
import { useList } from "@/src/store/list/hooks"
import { SortActionButton } from "../subscription/header-actions"
diff --git a/apps/mobile/src/modules/settings/SettingNavigationLink.tsx b/apps/mobile/src/modules/settings/SettingNavigationLink.tsx
deleted file mode 100644
index 373e08114f..0000000000
--- a/apps/mobile/src/modules/settings/SettingNavigationLink.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { FC, PropsWithChildren } from "react"
-import { View } from "react-native"
-
-export const SettingNavigationLinkIcon: FC<
- {
- backgroundColor: string
- } & PropsWithChildren
-> = ({ backgroundColor, children }) => {
- return (
-
- {children}
-
- )
-}
diff --git a/apps/mobile/src/modules/settings/SettingsList.tsx b/apps/mobile/src/modules/settings/SettingsList.tsx
index d1623edaad..c18426a769 100644
--- a/apps/mobile/src/modules/settings/SettingsList.tsx
+++ b/apps/mobile/src/modules/settings/SettingsList.tsx
@@ -9,6 +9,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"
import {
GroupedInsetListCard,
GroupedInsetListNavigationLink,
+ GroupedInsetListNavigationLinkIcon,
} from "@/src/components/ui/grouped/GroupedList"
import { SetBottomTabBarVisibleContext } from "@/src/contexts/BottomTabBarVisibleContext"
import { BellRingingCuteFiIcon } from "@/src/icons/bell_ringing_cute_fi"
@@ -24,7 +25,6 @@ import { TrophyCuteFiIcon } from "@/src/icons/trophy_cute_fi"
import { User3CuteFiIcon } from "@/src/icons/user_3_cute_fi"
import { useSettingsNavigation } from "./hooks"
-import { SettingNavigationLinkIcon } from "./SettingNavigationLink"
interface GroupNavigationLink {
label: string
@@ -88,7 +88,7 @@ const SettingGroupNavigationLinks: GroupNavigationLink[] = [
onPress: (navigation) => {
navigation.navigate("Data")
},
- iconBackgroundColor: "#F59E0B",
+ iconBackgroundColor: "#CBAD6D",
},
]
@@ -101,19 +101,20 @@ const DataGroupNavigationLinks: GroupNavigationLink[] = [
},
iconBackgroundColor: "#059669",
},
+
{
- label: "Lists",
- icon: RadaCuteFiIcon,
+ label: "Feeds",
+ icon: CertificateCuteFiIcon,
onPress: (navigation) => {
- navigation.navigate("Lists")
+ navigation.navigate("Feeds")
},
iconBackgroundColor: "#10B981",
},
{
- label: "Feeds",
- icon: CertificateCuteFiIcon,
+ label: "Lists",
+ icon: RadaCuteFiIcon,
onPress: (navigation) => {
- navigation.navigate("Feeds")
+ navigation.navigate("Lists")
},
iconBackgroundColor: "#34D399",
},
@@ -162,9 +163,9 @@ export const SettingsList: FC<{ scrollRef: RefObject }> = ({ scrollR
key={link.label}
label={link.label}
icon={
-
+
-
+
}
onPress={() => link.onPress(navigation, scrollRef)}
/>
@@ -178,9 +179,9 @@ export const SettingsList: FC<{ scrollRef: RefObject }> = ({ scrollR
key={link.label}
label={link.label}
icon={
-
+
-
+
}
onPress={() => link.onPress(navigation, scrollRef)}
/>
@@ -194,9 +195,9 @@ export const SettingsList: FC<{ scrollRef: RefObject }> = ({ scrollR
key={link.label}
label={link.label}
icon={
-
+
-
+
}
onPress={() => link.onPress(navigation, scrollRef)}
/>
@@ -210,9 +211,9 @@ export const SettingsList: FC<{ scrollRef: RefObject }> = ({ scrollR
key={link.label}
label={link.label}
icon={
-
+
-
+
}
onPress={() => link.onPress(navigation, scrollRef)}
/>
diff --git a/apps/mobile/src/modules/settings/routes/About.tsx b/apps/mobile/src/modules/settings/routes/About.tsx
index a5bf7f84f1..bfc13c009a 100644
--- a/apps/mobile/src/modules/settings/routes/About.tsx
+++ b/apps/mobile/src/modules/settings/routes/About.tsx
@@ -1,9 +1,94 @@
-import { Text, View } from "react-native"
+import Constants from "expo-constants"
+import { Linking, Text, View } from "react-native"
+
+import {
+ NavigationBlurEffectHeader,
+ SafeNavigationScrollView,
+} from "@/src/components/common/SafeNavigationScrollView"
+import {
+ GroupedInsetListCard,
+ GroupedInsetListNavigationLink,
+ GroupedInsetListNavigationLinkIcon,
+ GroupedInsetListSectionHeader,
+} from "@/src/components/ui/grouped/GroupedList"
+import { Logo } from "@/src/components/ui/logo"
+import { Markdown } from "@/src/components/ui/typography/Markdown"
+import { DiscordCuteFiIcon } from "@/src/icons/discord_cute_fi"
+import { Github2CuteFiIcon } from "@/src/icons/github_2_cute_fi"
+import { SocialXCuteReIcon } from "@/src/icons/social_x_cute_re"
+
+const about = `
+Follow is in the early stages of development. If you have any feedback or suggestions, please feel free to open an issue on the [GitHub repository](https://github.com/RSSNext/follow).
+
+The icon library used is copyrighted by https://mgc.mingcute.com/ and cannot be redistributed.
+
+Copyright © 2025 Follow. All rights reserved.
+`
+
+const links = [
+ {
+ title: "Github",
+ icon: Github2CuteFiIcon,
+ url: "https://github.com/RSSNext/follow",
+ iconBackgroundColor: "#000000",
+ iconColor: "#FFFFFF",
+ },
+ {
+ title: "X",
+ icon: SocialXCuteReIcon,
+ url: "https://x.com/follow_app_",
+ iconBackgroundColor: "#000000",
+ iconColor: "#FFFFFF",
+ },
+ {
+ title: "Discord",
+ icon: DiscordCuteFiIcon,
+ url: "https://discord.gg/followapp",
+ iconBackgroundColor: "#5865F2",
+ iconColor: "#FFFFFF",
+ },
+]
export const AboutScreen = () => {
+ const buildId = Constants.expoConfig?.extra?.eas?.buildId || "Development"
+ const appVersion = Constants.expoConfig?.version || "0.0.0"
+
return (
-
- About
-
+
+
+
+
+ Follow
+
+ {appVersion} ({buildId})
+
+
+
+
+
+
+
+
+ {links.map((link) => (
+
+
+
+ }
+ onPress={() => Linking.openURL(link.url)}
+ />
+ ))}
+
+
+
)
}
diff --git a/apps/mobile/src/modules/settings/routes/General.tsx b/apps/mobile/src/modules/settings/routes/General.tsx
index 5332b07f30..3e99304e5b 100644
--- a/apps/mobile/src/modules/settings/routes/General.tsx
+++ b/apps/mobile/src/modules/settings/routes/General.tsx
@@ -1,9 +1,140 @@
+import { useLocales } from "expo-localization"
import { Text, View } from "react-native"
+import { setGeneralSetting, useGeneralSettingKey } from "@/src/atoms/settings/general"
+import {
+ NavigationBlurEffectHeader,
+ SafeNavigationScrollView,
+} from "@/src/components/common/SafeNavigationScrollView"
+import { Select } from "@/src/components/ui/form/Select"
+import {
+ GroupedInsetListBaseCell,
+ GroupedInsetListCard,
+ GroupedInsetListCell,
+ GroupedInsetListSectionHeader,
+} from "@/src/components/ui/grouped/GroupedList"
+import { Switch } from "@/src/components/ui/switch/Switch"
+import { LanguageMap } from "@/src/lib/language"
+
export const GeneralScreen = () => {
+ const locales = useLocales()
+ const translationLanguage = useGeneralSettingKey("translationLanguage")
+ const autoGroup = useGeneralSettingKey("autoGroup")
+ const showUnreadOnLaunch = useGeneralSettingKey("unreadOnly")
+ const groupByDate = useGeneralSettingKey("groupByDate")
+ const expandLongSocialMedia = useGeneralSettingKey("autoExpandLongSocialMedia")
+ const markAsReadWhenScrolling = useGeneralSettingKey("scrollMarkUnread")
+ const markAsReadWhenInView = useGeneralSettingKey("renderMarkUnread")
+
return (
-
- General Settings
-
+
+
+ {/* Language */}
+
+
+
+
+ Language
+
+ {locales[0]?.languageTag}
+
+
+
+ Translation Language
+
+
+
+
+ {/* Subscriptions */}
+
+
+
+
+ {
+ setGeneralSetting("autoGroup", value)
+ }}
+ />
+
+
+
+ {
+ setGeneralSetting("unreadOnly", value)
+ }}
+ />
+
+
+
+ {
+ setGeneralSetting("groupByDate", value)
+ }}
+ />
+
+
+
+ {
+ setGeneralSetting("autoExpandLongSocialMedia", value)
+ }}
+ />
+
+
+
+ {/* Unread */}
+
+
+
+
+ {
+ setGeneralSetting("scrollMarkUnread", value)
+ }}
+ />
+
+
+
+ {
+ setGeneralSetting("renderMarkUnread", value)
+ }}
+ />
+
+
+
+
+
)
}
diff --git a/apps/mobile/src/modules/settings/routes/Privacy.tsx b/apps/mobile/src/modules/settings/routes/Privacy.tsx
index 047e8b2b0d..6fb193d7a0 100644
--- a/apps/mobile/src/modules/settings/routes/Privacy.tsx
+++ b/apps/mobile/src/modules/settings/routes/Privacy.tsx
@@ -1,9 +1,26 @@
-import { Text, View } from "react-native"
+import { router } from "expo-router"
+
+import {
+ NavigationBlurEffectHeader,
+ SafeNavigationScrollView,
+} from "@/src/components/common/SafeNavigationScrollView"
+import {
+ GroupedInsetListCard,
+ GroupedInsetListNavigationLink,
+} from "@/src/components/ui/grouped/GroupedList"
export const PrivacyScreen = () => {
return (
-
- Privacy Settings
-
+
+
+
+ {
+ router.push("/teams")
+ }}
+ />
+
+
)
}
diff --git a/apps/mobile/src/modules/settings/routes/Profile.tsx b/apps/mobile/src/modules/settings/routes/Profile.tsx
index e7c3b1f2d6..a446277e69 100644
--- a/apps/mobile/src/modules/settings/routes/Profile.tsx
+++ b/apps/mobile/src/modules/settings/routes/Profile.tsx
@@ -7,7 +7,7 @@ import Animated, { useAnimatedScrollHandler, useSharedValue } from "react-native
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { ReAnimatedScrollView } from "@/src/components/common/AnimatedComponents"
-import { BlurEffect } from "@/src/components/common/HeaderBlur"
+import { BlurEffect } from "@/src/components/common/BlurEffect"
import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon"
import type { FeedIconRequiredFeed } from "@/src/components/ui/icon/feed-icon"
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
diff --git a/apps/mobile/src/modules/subscription/header-actions.tsx b/apps/mobile/src/modules/subscription/header-actions.tsx
index ff8c091f35..ad2cff7a74 100644
--- a/apps/mobile/src/modules/subscription/header-actions.tsx
+++ b/apps/mobile/src/modules/subscription/header-actions.tsx
@@ -1,7 +1,7 @@
import { TouchableOpacity } from "react-native"
import type { ContextMenuAction } from "react-native-context-menu-view"
-import ContextMenu from "react-native-context-menu-view"
+import { DropdownMenu } from "@/src/components/ui/dropdown/DropdownMenu"
import { AZSortAscendingLettersCuteReIcon } from "@/src/icons/AZ_sort_ascending_letters_cute_re"
import { AZSortDescendingLettersCuteReIcon } from "@/src/icons/AZ_sort_descending_letters_cute_re"
import { Numbers90SortAscendingCuteReIcon } from "@/src/icons/numbers_90_sort_ascending_cute_re"
@@ -42,10 +42,10 @@ export const SortActionButton = () => {
const Icon = map[sortMethod][sortOrder]
return (
- {
+ {
const [firstArgs, secondary] = e.nativeEvent.indexPath
switch (firstArgs) {
@@ -74,6 +74,6 @@ export const SortActionButton = () => {
-
+
)
}
diff --git a/apps/mobile/src/screens/(headless)/teams.tsx b/apps/mobile/src/screens/(headless)/teams.tsx
index 03a76ffaa9..667c95be1b 100644
--- a/apps/mobile/src/screens/(headless)/teams.tsx
+++ b/apps/mobile/src/screens/(headless)/teams.tsx
@@ -1,7 +1,9 @@
-import { Stack } from "expo-router"
-import { View } from "react-native"
+import { router, Stack, useNavigation } from "expo-router"
+import { TouchableOpacity, View } from "react-native"
+import { useColor } from "react-native-uikit-colors"
-import MarkdownWeb from "@/src/components/ui/typography/MarkdownWeb"
+import { Markdown } from "@/src/components/ui/typography/Markdown"
+import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line"
const txt = `# Terms of Service
@@ -82,19 +84,32 @@ Follow takes your privacy seriously. As a user, you acknowledge that we may coll
export const TeamsMarkdown = () => {
return (
-
)
}
export default function Teams() {
+ const canGoBack = useNavigation().canGoBack()
+ const label = useColor("label")
return (
(
+ router.back()}>
+
+
+ )
+ : undefined,
+ }}
/>
diff --git a/apps/mobile/src/screens/(modal)/rsshub-form.tsx b/apps/mobile/src/screens/(modal)/rsshub-form.tsx
index 8b054f0987..3114fe79a1 100644
--- a/apps/mobile/src/screens/(modal)/rsshub-form.tsx
+++ b/apps/mobile/src/screens/(modal)/rsshub-form.tsx
@@ -23,7 +23,7 @@ import {
import { FormProvider, useFormContext } from "@/src/components/ui/form/FormProvider"
import { Select } from "@/src/components/ui/form/Select"
import { TextField } from "@/src/components/ui/form/TextField"
-import MarkdownWeb from "@/src/components/ui/typography/MarkdownWeb"
+import { Markdown } from "@/src/components/ui/typography/Markdown"
import { toast } from "@/src/lib/toast"
import { feedSyncServices } from "@/src/store/feed/store"
@@ -167,7 +167,6 @@ function FormImpl({ route, routePrefix, name }: RsshubFormParams) {
render={({ field: { onChange, value } }) => (
)}
diff --git a/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx b/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx
index 7580b1f23c..c1eb696810 100644
--- a/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx
+++ b/apps/mobile/src/screens/(stack)/(tabs)/_layout.tsx
@@ -36,9 +36,11 @@ export default function TabLayout() {
const [tabBarVisible, setTabBarVisible] = useState(true)
useEffect(() => {
- RNAnimated.spring(animatedTransformY, {
+ RNAnimated.timing(animatedTransformY, {
toValue: tabBarVisible ? 1 : 0,
useNativeDriver: true,
+ duration: 250,
+ easing: Easing.inOut(Easing.ease),
}).start()
}, [animatedTransformY, tabBarVisible])
diff --git a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx
index a561167ac2..4adee5550d 100644
--- a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx
+++ b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx
@@ -3,7 +3,7 @@ import { useEffect } from "react"
import { Text, TouchableOpacity, View } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
-import { BlurEffect } from "@/src/components/common/HeaderBlur"
+import { BlurEffect } from "@/src/components/common/BlurEffect"
import { SafeNavigationScrollView } from "@/src/components/common/SafeNavigationScrollView"
import { views } from "@/src/constants/views"
import { AddCuteReIcon } from "@/src/icons/add_cute_re"
@@ -37,7 +37,7 @@ export default function Index() {
}}
/>
-
+
EntryList Placeholder
diff --git a/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx b/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx
index d4554f3308..9719a55176 100644
--- a/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx
+++ b/apps/mobile/src/screens/(stack)/(tabs)/settings.tsx
@@ -3,12 +3,14 @@ import { useIsFocused } from "@react-navigation/native"
import { createNativeStackNavigator } from "@react-navigation/native-stack"
import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"
import type { NativeScrollEvent, NativeSyntheticEvent, ScrollView } from "react-native"
-import { findNodeHandle, UIManager } from "react-native"
-import { useSharedValue, withTiming } from "react-native-reanimated"
+import { findNodeHandle, Text, UIManager } from "react-native"
+import type { SharedValue } from "react-native-reanimated"
+import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useEventCallback } from "usehooks-ts"
import { ReAnimatedScrollView } from "@/src/components/common/AnimatedComponents"
+import { BlurEffect } from "@/src/components/common/BlurEffect"
import { BottomTabBarBackgroundContext } from "@/src/contexts/BottomTabBarBackgroundContext"
import { SetBottomTabBarVisibleContext } from "@/src/contexts/BottomTabBarVisibleContext"
import { SettingRoutes } from "@/src/modules/settings/routes"
@@ -92,20 +94,42 @@ function Settings() {
const scrollRef = useRef(null)
return (
- {
- setContentSize({ height: h, width: w })
- }}
- style={{ paddingTop: insets.top }}
- className="bg-system-grouped-background flex-1"
- scrollIndicatorInsets={{ bottom: tabBarHeight - insets.bottom }}
+ <>
+ {
+ setContentSize({ height: h, width: w })
+ }}
+ style={{ paddingTop: insets.top }}
+ className="bg-system-grouped-background flex-1"
+ scrollIndicatorInsets={{ bottom: tabBarHeight - insets.bottom }}
+ >
+
+
+
+
+
+ >
+ )
+}
+
+const SettingHeader = ({ scrollY }: { scrollY: SharedValue }) => {
+ const styles = useAnimatedStyle(() => {
+ return {
+ opacity: scrollY.value / 100,
+ }
+ })
+ return (
+
-
+
-
-
+ Settings
+
)
}
diff --git a/apps/mobile/src/screens/(stack)/feeds/[feedId]/index.tsx b/apps/mobile/src/screens/(stack)/feeds/[feedId]/index.tsx
index 6ba20ff837..74e84f84bd 100644
--- a/apps/mobile/src/screens/(stack)/feeds/[feedId]/index.tsx
+++ b/apps/mobile/src/screens/(stack)/feeds/[feedId]/index.tsx
@@ -2,7 +2,7 @@ import { Stack, useLocalSearchParams } from "expo-router"
import { useState } from "react"
import { View } from "react-native"
-import { BlurEffect } from "@/src/components/common/HeaderBlur"
+import { BlurEffect } from "@/src/components/common/BlurEffect"
import { EntryColumnGrid } from "@/src/modules/entry/gird"
import { getEntry } from "@/src/store/entry/getter"
import { useEntryIdsByFeedId, usePrefetchEntries } from "@/src/store/entry/hooks"
diff --git a/apps/mobile/src/store/entry/store.ts b/apps/mobile/src/store/entry/store.ts
index a6c14e0327..0def9845aa 100644
--- a/apps/mobile/src/store/entry/store.ts
+++ b/apps/mobile/src/store/entry/store.ts
@@ -63,7 +63,7 @@ class EntryActions {
}
const subscription = getSubscription(feedId)
- if (subscription?.view) {
+ if (typeof subscription?.view === "number") {
draft.entryIdByView[subscription.view].add(entryId)
}
if (subscription?.category) {
diff --git a/apps/mobile/src/theme/navigation.ts b/apps/mobile/src/theme/navigation.ts
index 80c9157905..bf8c512dd2 100644
--- a/apps/mobile/src/theme/navigation.ts
+++ b/apps/mobile/src/theme/navigation.ts
@@ -11,6 +11,7 @@ export const DefaultTheme: Theme = {
colors: {
...NativeDefaultTheme.colors,
primary: accentColor,
+ background: "white",
},
}
@@ -19,5 +20,6 @@ export const DarkTheme: Theme = {
colors: {
...NativeDarkTheme.colors,
primary: accentColor,
+ background: "black",
},
}
diff --git a/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx b/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx
index d463cf38cc..0ce5bfd026 100644
--- a/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx
+++ b/apps/renderer/src/components/ui/dropdown-menu/dropdown-menu.tsx
@@ -95,9 +95,10 @@ const DropdownMenuItem = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
inset?: boolean
- icon?: React.ReactNode
+ icon?: React.ReactNode | ((props?: { isActive?: boolean }) => React.ReactNode)
+ active?: boolean
}
->(({ className, inset, icon, ...props }, ref) => (
+>(({ className, inset, icon, active, ...props }, ref) => (
{!!icon && (
- {icon}
+
+ {typeof icon === "function" ? icon({ isActive: active }) : icon}
+
)}
{props.children}
{/* Justify Fill */}
diff --git a/apps/renderer/src/hooks/biz/useEntryActions.tsx b/apps/renderer/src/hooks/biz/useEntryActions.tsx
index e85810aac1..8025669af9 100644
--- a/apps/renderer/src/hooks/biz/useEntryActions.tsx
+++ b/apps/renderer/src/hooks/biz/useEntryActions.tsx
@@ -128,16 +128,10 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
hide: isInbox || feed?.ownerUserId === whoami()?.id,
shortcut: shortcuts.entry.tip.key,
},
- {
- id: COMMAND_ID.entry.unstar,
- onClick: runCmdFn(COMMAND_ID.entry.unstar, [{ entryId }]),
- hide: !entry?.collections,
- shortcut: shortcuts.entry.toggleStarred.key,
- },
{
id: COMMAND_ID.entry.star,
onClick: runCmdFn(COMMAND_ID.entry.star, [{ entryId, view }]),
- hide: !!entry?.collections,
+ active: !!entry?.collections,
shortcut: shortcuts.entry.toggleStarred.key,
},
{
@@ -159,13 +153,8 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
{
id: COMMAND_ID.entry.viewSourceContent,
onClick: runCmdFn(COMMAND_ID.entry.viewSourceContent, [{ entryId }]),
- hide: isMobile() || isShowSourceContent || !entry?.entries.url,
- },
- {
- id: COMMAND_ID.entry.viewEntryContent,
- onClick: runCmdFn(COMMAND_ID.entry.viewEntryContent, []),
- hide: !isShowSourceContent,
- active: true,
+ hide: isMobile() || !entry?.entries.url,
+ active: isShowSourceContent,
},
{
id: COMMAND_ID.entry.toggleAISummary,
@@ -198,13 +187,8 @@ export const useEntryActions = ({ entryId, view }: { entryId: string; view?: Fee
{
id: COMMAND_ID.entry.read,
onClick: runCmdFn(COMMAND_ID.entry.read, [{ entryId }]),
- hide: !hasEntry || entry.read || !!entry.collections || !!inList,
- shortcut: shortcuts.entry.toggleRead.key,
- },
- {
- id: COMMAND_ID.entry.unread,
- onClick: runCmdFn(COMMAND_ID.entry.unread, [{ entryId }]),
- hide: !hasEntry || !entry.read || !!entry.collections || !!inList,
+ hide: !hasEntry || !!entry.collections || !!inList,
+ active: !!entry?.read,
shortcut: shortcuts.entry.toggleRead.key,
},
{
diff --git a/apps/renderer/src/modules/command/commands/entry.tsx b/apps/renderer/src/modules/command/commands/entry.tsx
index 5499844de4..60d95a1d17 100644
--- a/apps/renderer/src/modules/command/commands/entry.tsx
+++ b/apps/renderer/src/modules/command/commands/entry.tsx
@@ -1,13 +1,17 @@
import { FeedViewType, UserRole } from "@follow/constants"
import { IN_ELECTRON } from "@follow/shared/constants"
-import { getOS } from "@follow/utils/utils"
+import { cn, getOS } from "@follow/utils/utils"
import { useMutation } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { toggleShowAISummary } from "~/atoms/ai-summary"
import { toggleShowAITranslation } from "~/atoms/ai-translation"
-import { setShowSourceContent, useSourceContentModal } from "~/atoms/source-content"
+import {
+ getShowSourceContent,
+ toggleShowSourceContent,
+ useSourceContentModal,
+} from "~/atoms/source-content"
import { useUserRole } from "~/atoms/user"
import { navigateEntry } from "~/hooks/biz/useNavigateEntry"
import { getRouteParams } from "~/hooks/biz/useRouteParams"
@@ -112,7 +116,13 @@ export const useRegisterEntryCommands = () => {
{
id: COMMAND_ID.entry.star,
label: t("entry_actions.star"),
- icon: ,
+ icon: (props) => (
+
+ ),
run: ({ entryId, view }) => {
const entry = useEntryStore.getState().flatMapEntries[entryId]
if (!entry) {
@@ -128,20 +138,11 @@ export const useRegisterEntryCommands = () => {
// width: 252,
// })
// }
- collect.mutate({ entryId, view })
- },
- },
- {
- id: COMMAND_ID.entry.unstar,
- label: t("entry_actions.unstar"),
- icon: ,
- run: ({ entryId }) => {
- const entry = useEntryStore.getState().flatMapEntries[entryId]
- if (!entry) {
- toast.error("Failed to unstar: entry is not available", { duration: 3000 })
- return
+ if (entry.collections) {
+ uncollect.mutate(entry.entries.id)
+ } else {
+ collect.mutate({ entryId, view })
}
- uncollect.mutate(entry.entries.id)
},
},
{
@@ -211,37 +212,31 @@ export const useRegisterEntryCommands = () => {
label: t("entry_actions.view_source_content"),
icon: ,
run: ({ entryId }) => {
- const entry = useEntryStore.getState().flatMapEntries[entryId]
- if (!entry || !entry.entries.url) {
- toast.error("Failed to view source content: url is not available", { duration: 3000 })
- return
- }
- const routeParams = getRouteParams()
- const viewPreviewInModal = [
- FeedViewType.SocialMedia,
- FeedViewType.Videos,
- FeedViewType.Pictures,
- ].includes(routeParams.view)
- if (viewPreviewInModal) {
- showSourceContentModal({
- title: entry.entries.title ?? undefined,
- src: entry.entries.url,
- })
- return
- }
- const layoutEntryId = routeParams.entryId
- if (layoutEntryId !== entry.entries.id) {
- navigateEntry({ entryId: entry.entries.id })
+ if (!getShowSourceContent()) {
+ const entry = useEntryStore.getState().flatMapEntries[entryId]
+ if (!entry || !entry.entries.url) {
+ toast.error("Failed to view source content: url is not available", { duration: 3000 })
+ return
+ }
+ const routeParams = getRouteParams()
+ const viewPreviewInModal = [
+ FeedViewType.SocialMedia,
+ FeedViewType.Videos,
+ FeedViewType.Pictures,
+ ].includes(routeParams.view)
+ if (viewPreviewInModal) {
+ showSourceContentModal({
+ title: entry.entries.title ?? undefined,
+ src: entry.entries.url,
+ })
+ return
+ }
+ const layoutEntryId = routeParams.entryId
+ if (layoutEntryId !== entry.entries.id) {
+ navigateEntry({ entryId: entry.entries.id })
+ }
}
- setShowSourceContent(true)
- },
- },
- {
- id: COMMAND_ID.entry.viewEntryContent,
- label: t("entry_actions.view_source_content"),
- icon: ,
- run: () => {
- setShowSourceContent(false)
+ toggleShowSourceContent()
},
},
{
@@ -274,27 +269,20 @@ export const useRegisterEntryCommands = () => {
{
id: COMMAND_ID.entry.read,
label: t("entry_actions.mark_as_read"),
- icon: ,
+ icon: (props) => (
+
+ ),
run: ({ entryId }) => {
const entry = useEntryStore.getState().flatMapEntries[entryId]
if (!entry) {
toast.error("Failed to mark as unread: feed is not available", { duration: 3000 })
return
}
- read.mutate({ entryId, feedId: entry.feedId })
- },
- },
- {
- id: COMMAND_ID.entry.unread,
- label: t("entry_actions.mark_as_unread"),
- icon: ,
- run: ({ entryId }) => {
- const entry = useEntryStore.getState().flatMapEntries[entryId]
- if (!entry) {
- toast.error("Failed to mark as unread: feed is not available", { duration: 3000 })
- return
+ if (entry.read) {
+ unread.mutate({ entryId, feedId: entry.feedId })
+ } else {
+ read.mutate({ entryId, feedId: entry.feedId })
}
- unread.mutate({ entryId, feedId: entry.feedId })
},
},
])
diff --git a/apps/renderer/src/modules/command/commands/id.ts b/apps/renderer/src/modules/command/commands/id.ts
index bf044ea928..ce56110512 100644
--- a/apps/renderer/src/modules/command/commands/id.ts
+++ b/apps/renderer/src/modules/command/commands/id.ts
@@ -2,16 +2,13 @@ export const COMMAND_ID = {
entry: {
tip: "entry:tip",
star: "entry:star",
- unstar: "entry:unstar",
delete: "entry:delete",
copyLink: "entry:copy-link",
copyTitle: "entry:copy-title",
openInBrowser: "entry:open-in-browser",
viewSourceContent: "entry:view-source-content",
- viewEntryContent: "entry:view-entry-content",
share: "entry:share",
read: "entry:read",
- unread: "entry:unread",
toggleAISummary: "entry:toggle-ai-summary",
toggleAITranslation: "entry:toggle-ai-translation",
},
diff --git a/apps/renderer/src/modules/command/commands/types.ts b/apps/renderer/src/modules/command/commands/types.ts
index f6a4f97bf2..dabd5a6fb0 100644
--- a/apps/renderer/src/modules/command/commands/types.ts
+++ b/apps/renderer/src/modules/command/commands/types.ts
@@ -14,10 +14,6 @@ export type StarCommand = Command<{
id: typeof COMMAND_ID.entry.star
fn: (data: { entryId: string; view?: FeedViewType }) => void
}>
-export type UnStarCommand = Command<{
- id: typeof COMMAND_ID.entry.unstar
- fn: (data: { entryId: string }) => void
-}>
export type DeleteCommand = Command<{
id: typeof COMMAND_ID.entry.delete
@@ -43,10 +39,6 @@ export type ViewSourceContentCommand = Command<{
id: typeof COMMAND_ID.entry.viewSourceContent
fn: (data: { entryId: string }) => void
}>
-export type ViewEntryContentCommand = Command<{
- id: typeof COMMAND_ID.entry.viewEntryContent
- fn: () => void
-}>
export type ShareCommand = Command<{
id: typeof COMMAND_ID.entry.share
@@ -58,11 +50,6 @@ export type ReadCommand = Command<{
fn: ({ entryId }) => void
}>
-export type UnReadCommand = Command<{
- id: typeof COMMAND_ID.entry.unread
- fn: ({ entryId }) => void
-}>
-
export type ToggleAISummaryCommand = Command<{
id: typeof COMMAND_ID.entry.toggleAISummary
fn: () => void
@@ -76,16 +63,13 @@ export type ToggleAITranslationCommand = Command<{
export type EntryCommand =
| TipCommand
| StarCommand
- | UnStarCommand
| DeleteCommand
| CopyLinkCommand
| CopyTitleCommand
| OpenInBrowserCommand
| ViewSourceContentCommand
- | ViewEntryContentCommand
| ShareCommand
| ReadCommand
- | UnReadCommand
| ToggleAISummaryCommand
| ToggleAITranslationCommand
diff --git a/apps/renderer/src/modules/command/hooks/use-register-command.test-d.ts b/apps/renderer/src/modules/command/hooks/use-register-command.test-d.ts
index c4264195cc..1cffcd196e 100644
--- a/apps/renderer/src/modules/command/hooks/use-register-command.test-d.ts
+++ b/apps/renderer/src/modules/command/hooks/use-register-command.test-d.ts
@@ -33,13 +33,6 @@ test("useRegisterFollowCommand types", () => {
expectTypeOf(entryId).toEqualTypeOf()
},
},
- {
- id: COMMAND_ID.entry.viewEntryContent,
- label: "",
- run: (...args) => {
- expectTypeOf(args).toEqualTypeOf<[]>()
- },
- },
]),
)
@@ -71,13 +64,6 @@ test("useRegisterFollowCommand types", () => {
expectTypeOf(entryId).toEqualTypeOf()
},
},
- {
- id: COMMAND_ID.entry.viewEntryContent,
- label: "",
- run: (...args) => {
- expectTypeOf(args).toEqualTypeOf<[]>()
- },
- },
]),
)
})
diff --git a/apps/renderer/src/modules/command/registry/command.test-d.ts b/apps/renderer/src/modules/command/registry/command.test-d.ts
index ec00d544c7..5588538668 100644
--- a/apps/renderer/src/modules/command/registry/command.test-d.ts
+++ b/apps/renderer/src/modules/command/registry/command.test-d.ts
@@ -45,16 +45,6 @@ test("defineFollowCommand types", () => {
})
test("defineFollowCommand with keyBinding types", () => {
- assertType(
- defineFollowCommand({
- id: COMMAND_ID.entry.viewEntryContent,
- label: "",
- when: true,
- keyBinding: "",
- run: () => {},
- }),
- )
-
assertType(
defineFollowCommand({
id: COMMAND_ID.entry.viewSourceContent,
diff --git a/apps/renderer/src/modules/command/types.ts b/apps/renderer/src/modules/command/types.ts
index 31199f7e28..4ea4f57334 100644
--- a/apps/renderer/src/modules/command/types.ts
+++ b/apps/renderer/src/modules/command/types.ts
@@ -58,7 +58,7 @@ export interface Command<
title: string
subTitle?: string
}
- readonly icon?: ReactNode
+ readonly icon?: ReactNode | ((props?: { isActive?: boolean }) => ReactNode)
readonly category: CommandCategory
readonly run: T["fn"]
@@ -82,7 +82,7 @@ export interface CommandOptions<
| (() => string)
| { title: string; subTitle?: string }
| (() => { title: string; subTitle?: string })
- icon?: ReactNode
+ icon?: ReactNode | ((props?: { isActive?: boolean }) => ReactNode)
category?: CommandCategory
run: T["fn"]
diff --git a/apps/renderer/src/modules/customize-toolbar/constant.ts b/apps/renderer/src/modules/customize-toolbar/constant.ts
index 698ba47edd..d92c4f85a8 100644
--- a/apps/renderer/src/modules/customize-toolbar/constant.ts
+++ b/apps/renderer/src/modules/customize-toolbar/constant.ts
@@ -9,7 +9,7 @@ export interface ToolbarActionOrder {
export const DEFAULT_ACTION_ORDER: ToolbarActionOrder = {
main: Object.values(COMMAND_ID.entry)
- .filter((id) => !([COMMAND_ID.entry.read, COMMAND_ID.entry.unread] as string[]).includes(id))
+ .filter((id) => !([COMMAND_ID.entry.read] as string[]).includes(id))
.filter(
(id) =>
!([COMMAND_ID.entry.copyLink, COMMAND_ID.entry.openInBrowser] as string[]).includes(id),
@@ -22,7 +22,6 @@ export const DEFAULT_ACTION_ORDER: ToolbarActionOrder = {
COMMAND_ID.entry.copyLink,
COMMAND_ID.entry.openInBrowser,
COMMAND_ID.entry.read,
- COMMAND_ID.entry.unread,
] as string[]
).includes(id),
),
diff --git a/apps/renderer/src/modules/customize-toolbar/dnd.tsx b/apps/renderer/src/modules/customize-toolbar/dnd.tsx
index d8c0e846d9..369051e0ca 100644
--- a/apps/renderer/src/modules/customize-toolbar/dnd.tsx
+++ b/apps/renderer/src/modules/customize-toolbar/dnd.tsx
@@ -46,7 +46,9 @@ export const SortableActionButton = ({ id }: { id: UniqueIdentifier }) => {
return (
-
{cmd.icon}
+
+ {typeof cmd.icon === "function" ? cmd.icon({ isActive: false }) : cmd.icon}
+
{cmd.label.title}
diff --git a/apps/renderer/src/modules/discover/form.tsx b/apps/renderer/src/modules/discover/form.tsx
index b77a670fde..fb9ae3434f 100644
--- a/apps/renderer/src/modules/discover/form.tsx
+++ b/apps/renderer/src/modules/discover/form.tsx
@@ -262,8 +262,7 @@ export function DiscoverForm({ type = "search" }: { type?: string }) {
{mutation.isSuccess && (
- Found {mutation.data?.length || 0} feed
- {mutation.data?.length > 1 && "s"}
+ {t("discover.search.results", { count: mutation.data?.length || 0 })}
{discoverSearchData?.map((item) => (
diff --git a/apps/renderer/src/modules/entry-column/Items/social-media-item.tsx b/apps/renderer/src/modules/entry-column/Items/social-media-item.tsx
index 8513749d30..03c680e540 100644
--- a/apps/renderer/src/modules/entry-column/Items/social-media-item.tsx
+++ b/apps/renderer/src/modules/entry-column/Items/social-media-item.tsx
@@ -135,10 +135,7 @@ const ActionBar = ({ entryId }: { entryId: string }) => {
{entryActions
.filter(
- (item) =>
- item.id !== COMMAND_ID.entry.read &&
- item.id !== COMMAND_ID.entry.unread &&
- item.id !== COMMAND_ID.entry.openInBrowser,
+ (item) => item.id !== COMMAND_ID.entry.read && item.id !== COMMAND_ID.entry.openInBrowser,
)
.map((item) => (
diff --git a/apps/renderer/src/modules/entry-content/actions/more-actions.tsx b/apps/renderer/src/modules/entry-content/actions/more-actions.tsx
index ddff0a5028..ea3688859a 100644
--- a/apps/renderer/src/modules/entry-content/actions/more-actions.tsx
+++ b/apps/renderer/src/modules/entry-content/actions/more-actions.tsx
@@ -37,11 +37,21 @@ export const MoreActions = ({ entryId, view }: { entryId: string; view?: FeedVie
{availableActions.map((config) => (
-
+
))}
{extraAction.map((config) => (
-
+
))}
@@ -51,14 +61,22 @@ export const MoreActions = ({ entryId, view }: { entryId: string; view?: FeedVie
const CommandDropdownMenuItem = ({
commandId,
onClick,
+ active,
}: {
commandId: FollowCommandId
onClick: () => void
+ active?: boolean
}) => {
const command = useCommand(commandId)
if (!command) return null
return (
-
+
{command.label.title}
)
diff --git a/apps/renderer/src/modules/entry-content/header.mobile.tsx b/apps/renderer/src/modules/entry-content/header.mobile.tsx
index fb180c2424..1c376dabfc 100644
--- a/apps/renderer/src/modules/entry-content/header.mobile.tsx
+++ b/apps/renderer/src/modules/entry-content/header.mobile.tsx
@@ -187,6 +187,7 @@ const HeaderRightActions = ({
{
setCtxOpen(false)
item.onClick?.()
@@ -207,9 +208,11 @@ const HeaderRightActions = ({
const CommandMotionButton = ({
commandId,
onClick,
+ active,
}: {
commandId: FollowCommandId
onClick: () => void
+ active?: boolean
}) => {
const command = useCommand(commandId)
if (!command) return null
@@ -219,7 +222,7 @@ const CommandMotionButton = ({
layout={false}
className="flex w-full items-center gap-2 px-4 py-2"
>
- {command.icon}
+ {typeof command.icon === "function" ? command.icon({ isActive: active }) : command.icon}
{command.label.title}
)
diff --git a/apps/renderer/src/modules/rsshub/set-modal-content.tsx b/apps/renderer/src/modules/rsshub/set-modal-content.tsx
index 5a0fa58813..3f6a0751a4 100644
--- a/apps/renderer/src/modules/rsshub/set-modal-content.tsx
+++ b/apps/renderer/src/modules/rsshub/set-modal-content.tsx
@@ -131,6 +131,7 @@ export function SetModalContent({
inputMode="numeric"
pattern="[0-9]*"
max={12}
+ min={hasPurchase ? 0 : 1}
{...field}
/>
diff --git a/apps/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx b/apps/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx
index e9f194c7e2..351652ea6b 100644
--- a/apps/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx
+++ b/apps/renderer/src/pages/(main)/(layer)/(subview)/discover/index.tsx
@@ -91,7 +91,7 @@ export function Component() {
}}
className="max-w-full"
>
-
+
{currentTabs.map((tab) => (
的支持",
"discover.import.click_to_upload": "點擊上傳 OPML 文件",
"discover.import.conflictItems": "衝突項目",
- "discover.import.new_import_opml": "如果您以前使用過 RSS,您可以將資料配置匯出到 OPML 文件,並在此處匯入",
+ "discover.import.new_import_opml": "如果您以前使用過 RSS,您可以將資料配置匯出到 OPML 文件,並在此處匯入",
"discover.import.noItems": "沒有項目",
"discover.import.opml": "OPML 檔案",
"discover.import.parsedErrorItems": "解析錯誤項目",
@@ -379,7 +379,7 @@
"tip_modal.no_wallet": "您尚未擁有錢包。請創建錢包以進行贊助。",
"tip_modal.tip_amount_sent": "已經發送給作者。",
"tip_modal.tip_now": "立刻贊助",
- "tip_modal.tip_sent": "贊助成功!謝謝您的支持",
+ "tip_modal.tip_sent": "贊助成功!謝謝您的支持。",
"tip_modal.tip_support": "⭐ 贊助以表示你的支持!",
"tip_modal.tip_title": "贊助 Power",
"tip_modal.unclaimed_feed": "目前尚無人認領此 RSS 摘要。收到的 Power 將安全的存放在區塊鏈合約中,直到被認領為止。",
diff --git a/locales/errors/zh-TW.json b/locales/errors/zh-TW.json
index d834c91ab9..c704c2a131 100644
--- a/locales/errors/zh-TW.json
+++ b/locales/errors/zh-TW.json
@@ -26,6 +26,8 @@
"4004": "目標使用者錢包錯誤",
"4005": "每日 power 計算中",
"4006": "無效加成數量",
+ "4007": "無效的雙因素驗證碼",
+ "4008": "需要雙因素驗證碼",
"4010": "空投不符合資格",
"4011": "空投正在發送",
"4012": "空投已發送",
diff --git a/locales/external/zh-TW.json b/locales/external/zh-TW.json
index bd7ebe7e67..28ce81c4d5 100644
--- a/locales/external/zh-TW.json
+++ b/locales/external/zh-TW.json
@@ -55,6 +55,7 @@
"login.signOut": "登出",
"login.signUp": "使用電子信箱註冊",
"login.submit": "送出",
+ "login.two_factor.code": "雙因素驗證碼",
"login.welcomeTo": "歡迎來到 ",
"redirect.continueInBrowser": "在瀏覽器中繼續",
"redirect.instruction": "現在是時候打開 {{app_name}} 並安全地關閉此頁面。",
diff --git a/locales/settings/zh-TW.json b/locales/settings/zh-TW.json
index da9bc90382..8e76c952cd 100644
--- a/locales/settings/zh-TW.json
+++ b/locales/settings/zh-TW.json
@@ -97,6 +97,11 @@
"appearance.zen_mode.description": "禪定模式是一種防干擾的閱讀模式,你可以專注於內容而不受其他干擾。啟用禪定模式將會隱藏側邊欄。",
"appearance.zen_mode.label": "禪定模式",
"common.give_star": "喜歡我們的產品嗎? 在 GitHub 上給我們 star 吧!",
+ "customizeToolbar.more_actions.description": "將顯示在下拉選單中",
+ "customizeToolbar.more_actions.title": "更多操作",
+ "customizeToolbar.quick_actions.description": "自訂並重新排列您常用的操作",
+ "customizeToolbar.quick_actions.title": "快速操作",
+ "customizeToolbar.reset_layout": "重置為預設版面",
"customizeToolbar.title": "自訂工具欄",
"data_control.app_cache_limit.description": "程式快取大小的上限。一旦快取達到此上限,最早的項目將被刪除以釋放空間。",
"data_control.app_cache_limit.label": "程式快取限制",
@@ -284,11 +289,23 @@
"profile.name.description": "你的公開顯示名稱。",
"profile.name.label": "顯示名稱",
"profile.new_password.label": "新密碼",
+ "profile.no_password": "重設密碼以設定新密碼。",
"profile.password.label": "密碼",
"profile.reset_password_mail_sent": "重設密碼信件已發送。",
"profile.sidebar_title": "個人資料",
"profile.submit": "送出",
"profile.title": "個人資料設定",
+ "profile.totp_code.init": "使用 Authenticator 掃描 QR Code",
+ "profile.totp_code.invalid": "無效的雙因素驗證碼。",
+ "profile.totp_code.label": "雙因素驗證碼",
+ "profile.totp_code.title": "輸入雙因素驗證碼",
+ "profile.two_factor.disable": "停用雙因素驗證",
+ "profile.two_factor.disabled": "雙因素驗證已停用。",
+ "profile.two_factor.enable": "啟用雙因素驗證",
+ "profile.two_factor.enable_notice": "需要啟用雙因素驗證才能執行此操作。",
+ "profile.two_factor.enabled": "雙因素驗證已啟用。",
+ "profile.two_factor.label": "雙因素驗證",
+ "profile.two_factor.no_password": "啟用雙因素驗證之前需要設定密碼。",
"profile.updateSuccess": "個人資料已更新。",
"profile.update_password_success": "密碼已更新。",
"rsshub.addModal.access_key_label": "存取金鑰(選填)",
@@ -304,6 +321,7 @@
"rsshub.table.official": "官方",
"rsshub.table.owner": "建立者",
"rsshub.table.price": "每月價格",
+ "rsshub.table.private": "私人",
"rsshub.table.unlimited": "無限制",
"rsshub.table.use": "使用",
"rsshub.table.userCount": "使用者數量",
diff --git a/locales/shortcuts/zh-TW.json b/locales/shortcuts/zh-TW.json
index 04c1b17943..a88b0dd67a 100644
--- a/locales/shortcuts/zh-TW.json
+++ b/locales/shortcuts/zh-TW.json
@@ -21,7 +21,7 @@
"keys.feeds.switchToView": "切換到指定類別",
"keys.layout.showShortcuts": "顯示/隱藏快捷鍵",
"keys.layout.toggleSidebar": "顯示/隱藏摘要側邊欄",
- "keys.layout.toggleWideMode": "切換寬屏模式",
+ "keys.layout.toggleWideMode": "切換寬螢幕模式",
"keys.layout.zenMode": "禪定模式",
"keys.misc.quickSearch": "快速搜尋",
"keys.type.audio": "音訊",
diff --git a/packages/components/src/ui/button/action-button.tsx b/packages/components/src/ui/button/action-button.tsx
index be2947c248..91644dd1c8 100644
--- a/packages/components/src/ui/button/action-button.tsx
+++ b/packages/components/src/ui/button/action-button.tsx
@@ -10,7 +10,7 @@ import { KbdCombined } from "../kbd/Kbd"
import { Tooltip, TooltipContent, TooltipPortal, TooltipTrigger } from "../tooltip"
export interface ActionButtonProps {
- icon?: React.ReactNode | React.FC
+ icon?: React.ReactNode | ((props: { isActive?: boolean; className: string }) => React.ReactNode)
tooltip?: React.ReactNode
tooltipSide?: "top" | "bottom"
active?: boolean
@@ -74,7 +74,7 @@ export const ActionButton = React.forwardRef<
onFocusCapture={stopPropagation}
className={cn(
"no-drag-region pointer-events-auto inline-flex items-center justify-center",
- active && "bg-zinc-500/15 hover:bg-zinc-500/20",
+ active && typeof icon !== "function" && "bg-zinc-500/15 hover:bg-zinc-500/20",
"rounded-md duration-200 hover:bg-theme-button-hover data-[state=open]:bg-theme-button-hover",
"disabled:cursor-not-allowed disabled:opacity-50",
clickableDisabled && "cursor-not-allowed opacity-50",
@@ -103,6 +103,8 @@ export const ActionButton = React.forwardRef<
) : typeof icon === "function" ? (
React.createElement(icon, {
className: "size-4 grayscale text-current",
+
+ isActive: active,
})
) : (
icon
diff --git a/packages/hooks/exports.ts b/packages/hooks/exports.ts
index 40f0b93bfb..0e0519b002 100644
--- a/packages/hooks/exports.ts
+++ b/packages/hooks/exports.ts
@@ -1 +1,2 @@
+export { useRefValue } from "./src/useRefValue"
export { useTypeScriptHappyCallback } from "./src/useTypescriptHappyCallback"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c233d4aae0..44acc184af 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -494,8 +494,8 @@ importers:
specifier: ~7.0.0
version: 7.0.0(vc5zx7mqwgzirpvpjamp5nboge)
expo-constants:
- specifier: ~17.0.3
- version: 17.0.3(yviy5suycsk3aacmyts4vro3la)
+ specifier: ~17.0.4
+ version: 17.0.4(yviy5suycsk3aacmyts4vro3la)
expo-dev-client:
specifier: ^5.0.9
version: 5.0.9(expo@52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))
@@ -517,9 +517,12 @@ importers:
expo-linking:
specifier: ~7.0.3
version: 7.0.3(vc5zx7mqwgzirpvpjamp5nboge)
+ expo-localization:
+ specifier: ~16.0.1
+ version: 16.0.1(expo@52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1)
expo-router:
specifier: 4.0.11
- version: 4.0.11(4kc7a2oyapueyif6rxtpvh4774)
+ version: 4.0.11(osuwzlrjsxkcgr2xnkwgib4g5q)
expo-secure-store:
specifier: ^14.0.1
version: 14.0.1(expo@52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))
@@ -3391,6 +3394,9 @@ packages:
'@expo/env@0.4.0':
resolution: {integrity: sha512-g2JYFqck3xKIwJyK+8LxZ2ENZPWtRgjFWpeht9abnKgzXVXBeSNECFBkg+WQjQocSIdxXhEWM6hz4ZAe7Tc4ng==}
+ '@expo/env@0.4.1':
+ resolution: {integrity: sha512-oDtbO3i9yXD1nx93acWiPTWGljJ3vABn35x1NAbqtQ2JL6mFOcRcArt1dwi4imZyLnG4VCcjabT9irj+LgYntw==}
+
'@expo/fingerprint@0.11.3':
resolution: {integrity: sha512-9lgXmcIePvZ7Wef63XtvuN3HfCUevF4E4tQPdEbH9/dUWwpOvvwQ3KT4OJ9jdh8JJ3nTdO9eDQ/8k8xr1aQ5Kg==}
hasBin: true
@@ -8756,8 +8762,8 @@ packages:
react: '*'
react-native: '*'
- expo-constants@17.0.3:
- resolution: {integrity: sha512-lnbcX2sAu8SucHXEXxSkhiEpqH+jGrf+TF+MO6sHWIESjwOUVVYlT8qYdjR9xbxWmqFtrI4KV44FkeJf2DaFjQ==}
+ expo-constants@17.0.4:
+ resolution: {integrity: sha512-5c0VlZycmDyQUCMCr3Na3cpHAsVJJ+5o6KkkD4rmATQZ0++Xp/S2gpnjWyEo2riRmO91vxoyHwmAySXuktJddQ==}
peerDependencies:
expo: '*'
react-native: '*'
@@ -8840,6 +8846,12 @@ packages:
react: '*'
react-native: '*'
+ expo-localization@16.0.1:
+ resolution: {integrity: sha512-kUrXiV/Pq9r7cG+TMt+Qa49IUQ9Y/czVwen4hmiboTclTopcWdIeCzYZv6JGtufoPpjEO9vVx1QJrXYl9V2u0Q==}
+ peerDependencies:
+ expo: '*'
+ react: '*'
+
expo-manifests@0.15.5:
resolution: {integrity: sha512-3X3eQomnTa4G0Y9GoJeyewHPTscuzWMrTB3x4CknqOyXpGOJjOuCKjhzvccHxXZAt0XswqBI94iTbqIofo9Uqw==}
peerDependencies:
@@ -13303,6 +13315,9 @@ packages:
rou3@0.5.1:
resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==}
+ rtl-detect@1.1.2:
+ resolution: {integrity: sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==}
+
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -17372,7 +17387,7 @@ snapshots:
'@expo/config': 10.0.8
'@expo/config-plugins': 9.0.14
'@expo/devcert': 1.1.4
- '@expo/env': 0.4.0
+ '@expo/env': 0.4.1
'@expo/image-utils': 0.6.3
'@expo/json-file': 9.0.1
'@expo/metro-config': 0.19.8
@@ -17574,6 +17589,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@expo/env@0.4.1':
+ dependencies:
+ chalk: 4.1.2
+ debug: 4.4.0(supports-color@8.1.1)
+ dotenv: 16.4.7
+ dotenv-expand: 11.0.7
+ getenv: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+
'@expo/fingerprint@0.11.3':
dependencies:
'@expo/spawn-async': 1.7.2
@@ -24609,7 +24634,7 @@ snapshots:
dependencies:
'@expo/image-utils': 0.6.3
expo: 52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)
- expo-constants: 17.0.3(4zmahxl5ymv4iqnmz7u7srsqim)
+ expo-constants: 17.0.4(4zmahxl5ymv4iqnmz7u7srsqim)
invariant: 2.2.4
md5-file: 3.2.3
react: 18.3.1
@@ -24622,7 +24647,7 @@ snapshots:
dependencies:
'@expo/image-utils': 0.6.3
expo: 52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)
- expo-constants: 17.0.3(yviy5suycsk3aacmyts4vro3la)
+ expo-constants: 17.0.4(yviy5suycsk3aacmyts4vro3la)
invariant: 2.2.4
md5-file: 3.2.3
react: 18.3.1
@@ -24656,20 +24681,20 @@ snapshots:
react: 18.3.1
react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)
- expo-constants@17.0.3(4zmahxl5ymv4iqnmz7u7srsqim):
+ expo-constants@17.0.4(4zmahxl5ymv4iqnmz7u7srsqim):
dependencies:
'@expo/config': 10.0.8
- '@expo/env': 0.4.0
+ '@expo/env': 0.4.1
expo: 52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)
react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)
transitivePeerDependencies:
- supports-color
optional: true
- expo-constants@17.0.3(yviy5suycsk3aacmyts4vro3la):
+ expo-constants@17.0.4(yviy5suycsk3aacmyts4vro3la):
dependencies:
'@expo/config': 10.0.8
- '@expo/env': 0.4.0
+ '@expo/env': 0.4.1
expo: 52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)
react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)
transitivePeerDependencies:
@@ -24774,7 +24799,7 @@ snapshots:
expo-linking@7.0.3(vc5zx7mqwgzirpvpjamp5nboge):
dependencies:
- expo-constants: 17.0.3(yviy5suycsk3aacmyts4vro3la)
+ expo-constants: 17.0.4(yviy5suycsk3aacmyts4vro3la)
invariant: 2.2.4
react: 18.3.1
react-native: 0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)
@@ -24782,6 +24807,12 @@ snapshots:
- expo
- supports-color
+ expo-localization@16.0.1(expo@52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1):
+ dependencies:
+ expo: 52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)
+ react: 18.3.1
+ rtl-detect: 1.1.2
+
expo-manifests@0.15.5(expo@52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)):
dependencies:
'@expo/config': 10.0.8
@@ -24810,7 +24841,7 @@ snapshots:
invariant: 2.2.4
optional: true
- expo-router@4.0.11(4kc7a2oyapueyif6rxtpvh4774):
+ expo-router@4.0.11(osuwzlrjsxkcgr2xnkwgib4g5q):
dependencies:
'@expo/metro-runtime': 4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))
'@expo/server': 0.5.0(typescript@5.7.2)
@@ -24820,7 +24851,7 @@ snapshots:
'@react-navigation/native-stack': 7.2.0(36el4dfrgnbt7wqu67o6leoq5q)
client-only: 0.0.1
expo: 52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)
- expo-constants: 17.0.3(yviy5suycsk3aacmyts4vro3la)
+ expo-constants: 17.0.4(yviy5suycsk3aacmyts4vro3la)
expo-linking: 7.0.3(vc5zx7mqwgzirpvpjamp5nboge)
react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-native-helmet-async: 2.0.4(react@18.3.1)
@@ -24934,7 +24965,7 @@ snapshots:
'@expo/vector-icons': 14.0.4
babel-preset-expo: 12.0.4(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))
expo-asset: 11.0.1(vc5zx7mqwgzirpvpjamp5nboge)
- expo-constants: 17.0.3(yviy5suycsk3aacmyts4vro3la)
+ expo-constants: 17.0.4(yviy5suycsk3aacmyts4vro3la)
expo-file-system: 18.0.6(yviy5suycsk3aacmyts4vro3la)
expo-font: 13.0.2(expo@52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1)
expo-keep-awake: 14.0.1(expo@52.0.18(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1)
@@ -24970,7 +25001,7 @@ snapshots:
'@expo/vector-icons': 14.0.4
babel-preset-expo: 12.0.4(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))
expo-asset: 11.0.1(q65rno7mdzia6zdespm3xhq57e)
- expo-constants: 17.0.3(4zmahxl5ymv4iqnmz7u7srsqim)
+ expo-constants: 17.0.4(4zmahxl5ymv4iqnmz7u7srsqim)
expo-file-system: 18.0.6(4zmahxl5ymv4iqnmz7u7srsqim)
expo-font: 13.0.2(expo@52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1)
expo-keep-awake: 14.0.1(expo@52.0.23(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@expo/metro-runtime@4.0.0(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)))(bufferutil@4.0.8)(encoding@0.1.13)(graphql@16.8.1)(react-native-webview@13.12.5(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@react-native-community/cli-server-api@14.1.0(bufferutil@4.0.8))(@types/react@18.3.14)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react@18.3.1)
@@ -30053,6 +30084,8 @@ snapshots:
rou3@0.5.1: {}
+ rtl-detect@1.1.2: {}
+
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3