feat(mobile): trending language, add trending into onboarding
This commit is contained in:
parent
7d2ba96886
commit
ad9b47eeed
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue