Skip to content

Commit cfb23ad

Browse files
authored
fix(mobile): polish settings and discover ui (#4913)
1 parent e8ae098 commit cfb23ad

6 files changed

Lines changed: 115 additions & 118 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export function Select<T>({
113113
>
114114
<Text
115115
className={cn("flex-1 text-right font-semibold", disabled ? "text-gray" : "text-accent")}
116-
ellipsizeMode="middle"
116+
ellipsizeMode="tail"
117117
numberOfLines={1}
118118
>
119119
{displayValue || selectedOption?.label || "Select"}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function Select<T>({
5858
>
5959
<Text
6060
className={cn("flex-1 text-right font-semibold text-accent", disabled && "text-gray")}
61-
ellipsizeMode="middle"
61+
ellipsizeMode="tail"
6262
numberOfLines={1}
6363
>
6464
{displayValue || valueToLabelMap.get(currentValue) || "Select"}
Lines changed: 32 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
import { useEffect, useImperativeHandle } from "react"
1+
import { useImperativeHandle } from "react"
22
import type { SwitchChangeEvent } from "react-native"
3-
import { Pressable, StyleSheet } from "react-native"
4-
import Animated, {
5-
interpolate,
6-
interpolateColor,
7-
useAnimatedStyle,
8-
useSharedValue,
9-
withSpring,
10-
} from "react-native-reanimated"
3+
import { Platform, StyleSheet, Switch as NativeSwitch } from "react-native"
114

125
import { accentColor, useColor } from "@/src/theme/colors"
136

@@ -26,6 +19,10 @@ export interface SwitchProps {
2619
value?: boolean | undefined
2720

2821
size?: "sm" | "default"
22+
23+
disabled?: boolean | undefined
24+
25+
testID?: string | undefined
2926
}
3027

3128
export type SwitchRef = {
@@ -37,100 +34,39 @@ export const Switch = ({
3734
onValueChange,
3835
onChange,
3936
size = "default",
37+
disabled = false,
38+
testID,
4039
}: SwitchProps & { ref?: React.Ref<SwitchRef | null> }) => {
41-
const gray3 = useColor("gray3")
42-
const animatedValue = useSharedValue(value ? 1 : 0)
43-
44-
const dimensions =
45-
size === "sm"
46-
? { width: 40, height: 24, thumbSize: 20 }
47-
: { width: 48, height: 28, thumbSize: 24 }
48-
49-
useEffect(() => {
50-
animatedValue.value = withSpring(value ? 1 : 0, {
51-
damping: 15,
52-
stiffness: 200,
53-
})
54-
}, [animatedValue, value])
55-
56-
useImperativeHandle(ref, () => ({
57-
value: value || false,
58-
}))
59-
60-
const handlePress = () => {
61-
const newValue = !value
62-
onValueChange?.(newValue)
63-
onChange?.({
64-
nativeEvent: { value: newValue },
65-
} as SwitchChangeEvent)
66-
}
67-
68-
const trackAnimatedStyle = useAnimatedStyle(() => {
69-
const backgroundColor = interpolateColor(animatedValue.value, [0, 1], [gray3, accentColor])
70-
71-
return {
72-
backgroundColor,
73-
}
74-
})
75-
76-
const thumbAnimatedStyle = useAnimatedStyle(() => {
77-
const translateX = interpolate(
78-
animatedValue.value,
79-
[0, 1],
80-
[2, dimensions.width - dimensions.thumbSize - 2],
81-
)
82-
83-
return {
84-
transform: [{ translateX }],
85-
}
86-
})
40+
const gray4 = useColor("gray4")
41+
42+
useImperativeHandle(
43+
ref,
44+
() => ({
45+
value,
46+
}),
47+
[value],
48+
)
8749

8850
return (
89-
<Pressable onPress={handlePress} className="opacity-100" style={styles.container}>
90-
<Animated.View
91-
style={[
92-
styles.track,
93-
{ width: dimensions.width, height: dimensions.height },
94-
trackAnimatedStyle,
95-
]}
96-
>
97-
<Animated.View
98-
style={[
99-
styles.thumb,
100-
{
101-
width: dimensions.thumbSize,
102-
height: dimensions.thumbSize,
103-
},
104-
thumbAnimatedStyle,
105-
]}
106-
/>
107-
</Animated.View>
108-
</Pressable>
51+
<NativeSwitch
52+
disabled={disabled}
53+
ios_backgroundColor={gray4}
54+
onChange={onChange ?? undefined}
55+
onValueChange={onValueChange ?? undefined}
56+
style={size === "sm" ? styles.small : styles.default}
57+
testID={testID}
58+
thumbColor={Platform.OS === "android" ? "#FFFFFF" : undefined}
59+
trackColor={{ false: gray4, true: accentColor }}
60+
value={value}
61+
/>
10962
)
11063
}
11164

11265
const styles = StyleSheet.create({
113-
container: {
114-
justifyContent: "center",
115-
alignItems: "center",
116-
},
117-
track: {
118-
borderRadius: 999,
119-
justifyContent: "center",
120-
position: "relative",
66+
default: {
67+
transform: [{ scaleX: 0.94 }, { scaleY: 0.94 }],
12168
},
122-
thumb: {
123-
borderRadius: 999,
124-
backgroundColor: "#FFFFFF",
125-
position: "absolute",
126-
top: 2,
127-
shadowColor: "#000",
128-
shadowOffset: {
129-
width: 0,
130-
height: 1,
131-
},
132-
shadowOpacity: 0.2,
133-
shadowRadius: 2,
134-
elevation: 3,
69+
small: {
70+
transform: [{ scaleX: 0.8 }, { scaleY: 0.8 }],
13571
},
13672
})

apps/mobile/src/modules/discover/Category.tsx

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { RSSHubCategory } from "@follow/constants"
22
import { CategoryMap, RSSHubCategories } from "@follow/constants"
3+
import { Image } from "expo-image"
34
import { LinearGradient } from "expo-linear-gradient"
4-
import { memo } from "react"
5+
import { memo, useMemo, useState } from "react"
56
import { useTranslation } from "react-i18next"
6-
import { Pressable, StyleSheet, View } from "react-native"
7+
import { Platform, Pressable, StyleSheet, View } from "react-native"
78
import { useColor } from "react-native-uikit-colors"
89

910
import { Grid } from "@/src/components/ui/grid"
@@ -49,10 +50,46 @@ export const Category = () => {
4950
</>
5051
)
5152
}
53+
54+
const emojiCdnBaseUrl = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72"
55+
56+
const getEmojiImageUrl = (emoji: string) => {
57+
const codePoints = Array.from(emoji)
58+
.map((character) => character.codePointAt(0)?.toString(16))
59+
.filter((value): value is string => Boolean(value) && value !== "fe0f")
60+
61+
return `${emojiCdnBaseUrl}/${codePoints.join("-")}.png`
62+
}
63+
64+
const CategoryEmoji = ({ emoji }: { emoji: string }) => {
65+
const [useTextFallback, setUseTextFallback] = useState(false)
66+
const emojiImageUrl = useMemo(() => getEmojiImageUrl(emoji), [emoji])
67+
68+
return (
69+
<View style={styles.emojiContainer}>
70+
{useTextFallback ? (
71+
<Text allowFontScaling={false} style={styles.emojiText}>
72+
{emoji}
73+
</Text>
74+
) : (
75+
<Image
76+
allowDownscaling
77+
cachePolicy="memory-disk"
78+
contentFit="contain"
79+
onError={() => setUseTextFallback(true)}
80+
source={emojiImageUrl}
81+
style={styles.emojiImage}
82+
/>
83+
)}
84+
</View>
85+
)
86+
}
87+
5288
const CategoryItem = memo(({ category }: { category: RSSHubCategory }) => {
5389
const { t } = useTranslation("common")
5490
const name = t(`discover.category.${category}`)
5591
const navigation = useNavigation()
92+
const { emoji } = CategoryMap[category]
5693
return (
5794
<Pressable
5895
className="overflow-hidden rounded-2xl"
@@ -81,7 +118,7 @@ const CategoryItem = memo(({ category }: { category: RSSHubCategory }) => {
81118
style={styles.cardItem}
82119
>
83120
<View className="flex-1">
84-
<Text className="absolute right-2 top-2 text-4xl">{CategoryMap[category].emoji}</Text>
121+
<CategoryEmoji emoji={emoji} />
85122
<Text className="absolute bottom-0 left-2 text-lg font-bold text-white">{name}</Text>
86123
</View>
87124
</LinearGradient>
@@ -92,4 +129,27 @@ const styles = StyleSheet.create({
92129
cardItem: {
93130
aspectRatio: 16 / 9,
94131
},
132+
emojiContainer: {
133+
position: "absolute",
134+
right: 8,
135+
top: 8,
136+
width: 56,
137+
height: 56,
138+
alignItems: "center",
139+
justifyContent: "center",
140+
},
141+
emojiText: {
142+
fontSize: 40,
143+
lineHeight: 48,
144+
textAlign: "center",
145+
...(Platform.OS === "android"
146+
? {
147+
includeFontPadding: false,
148+
}
149+
: {}),
150+
},
151+
emojiImage: {
152+
width: 40,
153+
height: 40,
154+
},
95155
})

apps/mobile/src/modules/settings/routes/General.tsx

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { ACTION_LANGUAGE_KEYS } from "@follow/shared"
22
import i18next from "i18next"
33
import { useMemo } from "react"
44
import { useTranslation } from "react-i18next"
5-
import { View } from "react-native"
65

76
import type { MobileSupportedLanguages } from "@/src/@types/constants"
87
import { currentSupportedLanguages } from "@/src/@types/constants"
@@ -22,6 +21,8 @@ import { Switch } from "@/src/components/ui/switch/Switch"
2221
import { updateDayjsLocale } from "@/src/lib/i18n"
2322
import type { NavigationControllerView } from "@/src/lib/navigation/types"
2423

24+
const settingSelectWrapperClassName = "w-[200px]"
25+
2526
export function LanguageSelect({ settingKey }: { settingKey: "language" | "actionLanguage" }) {
2627
const { t } = useTranslation("settings")
2728
const languageMapWithTranslation = useMemo(() => {
@@ -80,10 +81,9 @@ function LanguageSetting({ settingKey }: { settingKey: "language" | "actionLangu
8081
? t("general.language.description")
8182
: t("general.action_language.description")
8283
}
84+
rightClassName={settingSelectWrapperClassName}
8385
>
84-
<View className="w-[100px]">
85-
<LanguageSelect settingKey={settingKey} />
86-
</View>
86+
<LanguageSelect settingKey={settingKey} />
8787
</GroupedInsetListCell>
8888
)
8989
}
@@ -96,19 +96,18 @@ function TranslationModeSetting() {
9696
<GroupedInsetListCell
9797
label={t("general.translation_mode.label")}
9898
description={t("general.translation_mode.description")}
99+
rightClassName={settingSelectWrapperClassName}
99100
>
100-
<View className="w-[120px]">
101-
<Select
102-
value={translationMode}
103-
onValueChange={(value) => {
104-
setGeneralSetting("translationMode", value as "bilingual" | "translation-only")
105-
}}
106-
options={[
107-
{ label: t("general.translation_mode.bilingual"), value: "bilingual" },
108-
{ label: t("general.translation_mode.translation-only"), value: "translation-only" },
109-
]}
110-
/>
111-
</View>
101+
<Select
102+
value={translationMode}
103+
onValueChange={(value) => {
104+
setGeneralSetting("translationMode", value as "bilingual" | "translation-only")
105+
}}
106+
options={[
107+
{ label: t("general.translation_mode.bilingual"), value: "bilingual" },
108+
{ label: t("general.translation_mode.translation-only"), value: "translation-only" },
109+
]}
110+
/>
112111
</GroupedInsetListCell>
113112
)
114113
}
@@ -151,6 +150,7 @@ export const GeneralScreen: NavigationControllerView = () => {
151150
>
152151
<Switch
153152
size="sm"
153+
testID="general-ai-summary-switch"
154154
value={summary}
155155
onValueChange={(value) => {
156156
setGeneralSetting("summary", value)

apps/mobile/src/screens/(stack)/feeds/[feedId]/FeedScreen.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export const FeedScreen: NavigationControllerView<{
3636
<FeedScreenEntryList />
3737
{!isSubscribed && isBizId(feedIdentifier) && (
3838
<Pressable
39-
className="absolute left-1/2 z-10 m-2 mx-auto -translate-x-1/2 rounded-full bg-accent px-4 py-2"
39+
className="absolute left-1/2 z-10 min-w-[112px] -translate-x-1/2 items-center justify-center overflow-hidden rounded-full bg-accent px-5 py-3"
40+
hitSlop={12}
4041
style={{
4142
bottom: Math.max(20, insets.bottom + 12),
4243
}}

0 commit comments

Comments
 (0)