feat(mobile): trending language, add trending into onboarding

This commit is contained in:
DIYgod 2025-05-16 13:18:51 +08:00
parent 7d2ba96886
commit ad9b47eeed
No known key found for this signature in database
7 changed files with 67 additions and 152 deletions

View File

@ -1,19 +1,17 @@
import { Pressable, Text } from "react-native"
import { useColor } from "react-native-uikit-colors"
import { destination } from "@/src/lib/navigation/biz/Destination"
import { accentColor } from "@/src/theme/colors"
import { Logo } from "../ui/logo"
export function NoLoginInfo({ target }: { target: "timeline" | "subscriptions" }) {
const color = useColor("secondaryLabel")
return (
<Pressable
className="flex-1 items-center justify-center gap-3"
onPress={() => destination.Login()}
>
<Logo width={40} height={40} color={color} />
<Logo width={40} height={40} color={accentColor} />
<Text className="text-secondary-label text-xl">{`Sign in to see your ${target}`}</Text>
</Pressable>
)

View File

@ -50,7 +50,7 @@ interface ShowDialogOptions<Ctx> {
}
}
const entering = SlideInUp.springify().damping(16.5).stiffness(100)
const entering = SlideInUp.duration(200)
const exiting = SlideOutUp.duration(200)
type DialogContextType = {
@ -200,7 +200,7 @@ class DialogStatic {
<FullWindowOverlay>
<Overlay onPress={handleClose} />
<Animated.View
className="bg-secondary-system-background absolute inset-x-0 -top-8 z-10 pt-8"
className="bg-system-background absolute inset-x-0 -top-8 z-10 pt-8"
entering={entering}
exiting={exiting}
>

View File

@ -6,8 +6,8 @@ import { useTranslation } from "react-i18next"
import { Pressable, StyleSheet, Text, View } from "react-native"
import { Grid } from "@/src/components/ui/grid"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import { FilterCuteReIcon } from "@/src/icons/filter_cute_re"
// import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
// import { FilterCuteReIcon } from "@/src/icons/filter_cute_re"
import { Grid2CuteReIcon } from "@/src/icons/grid_2_cute_re"
import { TrendingUpCuteReIcon } from "@/src/icons/trending_up_cute_re"
import { useNavigation } from "@/src/lib/navigation/hooks"
@ -23,21 +23,21 @@ export function DiscoverContent() {
<TrendingUpCuteReIcon width={24} height={24} />
<Text className="text-label text-2xl font-bold leading-[1.1]">Trending</Text>
</View>
<ItemPressable className="rounded-lg p-1">
{/* <ItemPressable className="rounded-lg p-1">
<FilterCuteReIcon width={20} height={20} />
</ItemPressable>
</ItemPressable> */}
</View>
<Trending />
<Trending className="mt-4" />
<View className="mt-4 flex-row items-center justify-between pb-1 pl-6 pr-5 pt-4">
<View className="flex-row items-center gap-2">
<Grid2CuteReIcon width={24} height={24} />
<Text className="text-label text-2xl font-bold leading-[1.1]">Categories</Text>
</View>
<ItemPressable className="rounded-lg p-1">
{/* <ItemPressable className="rounded-lg p-1">
<FilterCuteReIcon width={20} height={20} />
</ItemPressable>
</ItemPressable> */}
</View>
<DiscoverGrid />

View File

@ -18,11 +18,13 @@ type SearchResultItem = Awaited<ReturnType<typeof apiClient.discover.$post>>["da
export const FeedSummary = ({
item,
children,
preChildren,
className,
simple,
}: {
item: SearchResultItem
children?: React.ReactNode
preChildren?: React.ReactNode
className?: string
simple?: boolean
}) => {
@ -30,7 +32,7 @@ export const FeedSummary = ({
return (
<ItemPressable
itemStyle={ItemPressableStyle.Plain}
itemStyle={ItemPressableStyle.UnStyled}
onPress={() => {
if (item.feed?.id) {
navigation.presentControllerView(FollowScreen, {
@ -46,6 +48,7 @@ export const FeedSummary = ({
}}
className={className}
>
{preChildren}
{/* Headline */}
<View className="flex-row items-center gap-2 pr-2">
<View className="size-[32px] overflow-hidden rounded-lg">

View File

@ -1,5 +1,6 @@
import { cn } from "@follow/utils"
import { useQuery } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
import { Text, View } from "react-native"
import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator"
@ -7,43 +8,50 @@ import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformA
import { fetchFeedTrending } from "./api"
import { FeedSummary } from "./FeedSummary"
export const Trending = () => {
export const Trending = ({ className }: { className?: string }) => {
const { i18n } = useTranslation()
const { data, isLoading } = useQuery({
queryKey: ["trending", "feeds"],
queryFn: () =>
fetchFeedTrending({
lang: i18n.language.startsWith("zh") ? undefined : "eng",
limit: 20,
}).then((res) => res.data),
})
return (
<View className="mt-4">
<View className={className}>
{isLoading ? (
<View className="mt-5 flex h-12 items-center justify-center">
<PlatformActivityIndicator />
</View>
) : (
data?.map((item, index) => (
<View key={item.feed?.id} className="flex flex-row items-center px-6 py-3">
<View
className={cn(
"flex size-6 items-center justify-center rounded-full",
index < 3
? cn(
"bg-accent text-white",
index === 0 && "bg-accent",
index === 1 && "bg-accent/90",
index === 2 && "bg-accent/80",
)
: "bg-gray-5/60",
)}
>
<Text className={cn("text-xs font-medium", index < 3 && "text-white")}>
{index + 1}
</Text>
</View>
<FeedSummary item={item} className="flex-1 pl-4" simple />
</View>
<FeedSummary
key={item.feed?.id}
item={item}
className="flex flex-1 flex-row items-center bg-none px-6 py-3"
simple
preChildren={
<View
className={cn(
"mr-4 flex size-6 items-center justify-center rounded-full",
index < 3
? cn(
"bg-accent text-white",
index === 0 && "bg-accent",
index === 1 && "bg-accent/90",
index === 2 && "bg-accent/80",
)
: "bg-gray-5/60",
)}
>
<Text className={cn("text-xs font-medium", index < 3 && "text-white")}>
{index + 1}
</Text>
</View>
}
/>
))
)}
</View>

View File

@ -1,35 +1,18 @@
import { cn } from "@follow/utils"
import { useCallback, useState } from "react"
import { useTranslation } from "react-i18next"
import { Text, TouchableOpacity, View } from "react-native"
import Animated, { FadeIn, FadeOut } from "react-native-reanimated"
import { Text, View } from "react-native"
import { Search3CuteReIcon } from "@/src/icons/search_3_cute_re"
import { Shuffle2CuteReIcon } from "@/src/icons/shuffle_2_cute_re"
import { useSubscription } from "@/src/store/subscription/hooks"
import { subscriptionSyncService } from "@/src/store/subscription/store"
import { accentColor } from "@/src/theme/colors"
import type { PresetFeedConfig } from "./preset"
import { getPresetFeeds } from "./preset"
import { Trending } from "../discover/Trending"
import { OnboardingSectionScreenContainer } from "./shared"
export const StepInterests = () => {
const { t, i18n } = useTranslation()
const isEnglishUser = i18n.language === "en"
const [displayFeeds, setDisplayFeeds] = useState<PresetFeedConfig[]>(
getPresetFeeds(isEnglishUser).slice(0, 7),
)
const shuffleFeeds = useCallback(() => {
const presetFeeds = getPresetFeeds(isEnglishUser)
const shuffled = [...presetFeeds].sort(() => Math.random() - 0.5).slice(0, 7)
setDisplayFeeds(shuffled)
}, [isEnglishUser])
const { t } = useTranslation()
return (
<OnboardingSectionScreenContainer>
<View className="mb-10 flex items-center gap-4">
<View className="flex items-center gap-4">
<Search3CuteReIcon height={80} width={80} color={accentColor} />
<Text className="text-text mt-2 text-2xl font-bold">{t("onboarding.interests_title")}</Text>
<Text className="text-label mb-8 px-6 text-center text-lg">
@ -37,84 +20,7 @@ export const StepInterests = () => {
</Text>
</View>
<View className="w-full items-center gap-4">
<View className="flex flex-row">
<Text className="mr-2 text-base">{t("onboarding.suggestions_feed")}</Text>
<TouchableOpacity
onPress={shuffleFeeds}
className="bg-accent/10 flex-row items-center rounded-full px-3 py-1"
>
<Shuffle2CuteReIcon height={16} width={16} color={accentColor} />
<Text className="text-accent ml-1 text-sm">{t("onboarding.shuffle")}</Text>
</TouchableOpacity>
</View>
<View className="flex-row flex-wrap justify-center gap-2 px-4">
{displayFeeds.map((feed) => (
<FeedChip key={feed.feedId} feed={feed} />
))}
</View>
</View>
<Trending className="mb-4 w-full" />
</OnboardingSectionScreenContainer>
)
}
const FeedChip = ({ feed }: { feed: PresetFeedConfig }) => {
const subscription = useSubscription(feed.feedId)
const [loading, setLoading] = useState(false)
const [isSubscribed, setIsSubscribed] = useState(!!subscription)
const handleSubscribe = useCallback(
async (feed: PresetFeedConfig) => {
if (loading) return
setLoading(true)
if (isSubscribed) {
try {
setIsSubscribed(false)
await subscriptionSyncService.unsubscribe(feed.feedId)
} catch {
setIsSubscribed(true)
} finally {
setLoading(false)
}
return
}
try {
setIsSubscribed(true)
await subscriptionSyncService.subscribe({
feedId: feed.feedId,
title: feed.title,
url: feed.url,
view: feed.view,
category: "",
isPrivate: false,
})
} catch {
setIsSubscribed(false)
} finally {
setLoading(false)
}
},
[isSubscribed, loading],
)
return (
<Animated.View
key={feed.feedId}
entering={FadeIn.duration(300)}
exiting={FadeOut.duration(300)}
>
<TouchableOpacity
onPress={() => handleSubscribe(feed)}
className={cn(
"flex rounded-full px-4 py-2",
isSubscribed ? "bg-accent" : "bg-secondary-system-fill",
)}
>
<Text className={`text-center text-sm ${isSubscribed ? "text-white" : "text-label"}`}>
{feed.title}
</Text>
</TouchableOpacity>
</Animated.View>
)
}

View File

@ -59,6 +59,23 @@ export const StepPreferences = () => {
</Text>
</PreferenceCard>
{/* Import Card */}
<PreferenceCard
title={t("onboarding.import_content")}
icon={
<GroupedInsetListNavigationLinkIcon backgroundColor="#CBAD6D">
<FileImportCuteReIcon color="#fff" width={40} height={40} />
</GroupedInsetListNavigationLinkIcon>
}
onPress={importOpml}
>
<View className="flex-row">
<Text className="text-secondary-label flex-1">
{t("onboarding.import_description")}
</Text>
</View>
</PreferenceCard>
<PreferenceCard
title={t("onboarding.edit_profile")}
icon={
@ -103,23 +120,6 @@ export const StepPreferences = () => {
</Text>
)}
</PreferenceCard>
{/* Import Card */}
<PreferenceCard
title={t("onboarding.import_content")}
icon={
<GroupedInsetListNavigationLinkIcon backgroundColor="#CBAD6D">
<FileImportCuteReIcon color="#fff" width={40} height={40} />
</GroupedInsetListNavigationLinkIcon>
}
onPress={importOpml}
>
<View className="flex-row">
<Text className="text-secondary-label flex-1">
{t("onboarding.import_description")}
</Text>
</View>
</PreferenceCard>
</View>
</OnboardingSectionScreenContainer>
)