Skip to content

Commit 2d92a4f

Browse files
authored
feat(mobile): implement onboarding screen (#3029)
* feat(mobile): implement onboarding screen * refactor(mobile): extract avatar setting functionality into a separate utility function * feat(mobile): integrate StepInterests into onboarding * feat(mobile): add preset feeds configuration for onboarding * feat(mobile): add onboarding completion and welcome steps * chore: tweak styles * fix: icon * chore: add personalization prompt to onboarding preferences * chore: enhance onboarding with improved layout * chore: clean code * refactor: compatible with modal for EditProfileModal
1 parent d54535d commit 2d92a4f

16 files changed

+799
-55
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as React from "react"
2+
import Svg, { Path } from "react-native-svg"
3+
4+
interface ListCheck2CuteReIconProps {
5+
width?: number
6+
height?: number
7+
color?: string
8+
}
9+
10+
export const ListCheck2CuteReIcon = ({
11+
width = 24,
12+
height = 24,
13+
color = "#10161F",
14+
}: ListCheck2CuteReIconProps) => {
15+
return (
16+
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
17+
<Path
18+
stroke={color}
19+
strokeLinecap="round"
20+
strokeLinejoin="round"
21+
strokeWidth={2}
22+
d="M11 5h9m-9 7h9m-9 7h9M7.945 3.72c-.941.725-1.754 1.53-2.475 2.475A4.225 4.225 0 0 0 4.056 4.78m0 7c.592.373 1.05.818 1.414 1.415a13.22 13.22 0 0 1 2.475-2.475m-3.89 8.06c.593.373 1.051.817 1.415 1.415a13.22 13.22 0 0 1 2.475-2.475"
23+
/>
24+
</Svg>
25+
)
26+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from "react"
2+
import Svg, { Path } from "react-native-svg"
3+
4+
interface Shuffle2CuteReIconProps {
5+
width?: number
6+
height?: number
7+
color?: string
8+
}
9+
10+
export const Shuffle2CuteReIcon = ({
11+
width = 24,
12+
height = 24,
13+
color = "#10161F",
14+
}: Shuffle2CuteReIconProps) => {
15+
return (
16+
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
17+
<Path
18+
stroke={color}
19+
strokeLinecap="round"
20+
strokeWidth={2}
21+
d="M4 7v0c1.69 0 2.535 0 3.273.308a4 4 0 0 1 .837.473c.644.475 1.078 1.2 1.948 2.649l1.884 3.14c.87 1.45 1.304 2.174 1.948 2.649.259.19.54.35.836.473C15.466 17 16.31 17 18 17v0m0-10h-1.084c-.632 0-.948 0-1.241.044a4 4 0 0 0-2.5 1.415c-.188.229-.35.5-.675 1.041v0M4 17h1.085c.631 0 .947 0 1.24-.044a4 4 0 0 0 2.5-1.415c.188-.229.35-.5.675-1.041"
22+
/>
23+
<Path
24+
fill={color}
25+
fillRule="evenodd"
26+
d="M17.847 4.507c-.503-.263-1.102.127-1.15.748-.032.407-.064.994-.064 1.708 0 .748.035 1.357.068 1.764.048.58.592.914 1.09.654.33-.171.806-.438 1.376-.808.57-.37 1.013-.701 1.31-.936.448-.355.465-1.051.042-1.388a17.102 17.102 0 0 0-1.325-.95c-.55-.358-1.019-.62-1.347-.792M17.847 14.617c-.503-.263-1.102.127-1.15.748-.032.406-.064.994-.064 1.708 0 .747.035 1.357.068 1.764.048.58.592.914 1.09.654.33-.171.806-.438 1.376-.808.57-.37 1.013-.701 1.31-.936.448-.355.465-1.051.042-1.388a17.13 17.13 0 0 0-1.325-.95c-.55-.358-1.019-.62-1.347-.792"
27+
clipRule="evenodd"
28+
/>
29+
</Svg>
30+
)
31+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { setGeneralSetting, useGeneralSettingKey } from "@/src/atoms/settings/general"
2+
3+
type ReadingBehavior = "radical" | "balanced" | "conservative"
4+
5+
export const useReadingBehavior = () => {
6+
const markAsReadWhenScrolling = useGeneralSettingKey("scrollMarkUnread")
7+
const markAsReadWhenInView = useGeneralSettingKey("renderMarkUnread")
8+
9+
const behavior: ReadingBehavior =
10+
markAsReadWhenInView && markAsReadWhenScrolling
11+
? "radical"
12+
: !markAsReadWhenInView && !markAsReadWhenScrolling
13+
? "conservative"
14+
: "balanced"
15+
16+
const updateSettings = (behavior: ReadingBehavior) => {
17+
switch (behavior) {
18+
case "radical": {
19+
setGeneralSetting("scrollMarkUnread", true)
20+
setGeneralSetting("renderMarkUnread", true)
21+
break
22+
}
23+
case "balanced": {
24+
setGeneralSetting("scrollMarkUnread", true)
25+
setGeneralSetting("renderMarkUnread", false)
26+
break
27+
}
28+
case "conservative": {
29+
setGeneralSetting("scrollMarkUnread", false)
30+
setGeneralSetting("renderMarkUnread", false)
31+
break
32+
}
33+
}
34+
}
35+
return {
36+
behavior,
37+
markAsReadWhenScrolling,
38+
markAsReadWhenInView,
39+
updateSettings,
40+
}
41+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { FeedViewType } from "@follow/constants"
2+
3+
export type PresetFeedConfig = {
4+
title: string
5+
feedId: string
6+
url: string
7+
view: FeedViewType
8+
}
9+
10+
export const presetFeeds: PresetFeedConfig[] = [
11+
{
12+
feedId: "41358761177015296",
13+
title: "知乎热榜 - 全站",
14+
url: "rsshub://zhihu/hot/total",
15+
view: FeedViewType.Articles,
16+
},
17+
{
18+
feedId: "100020530265058357",
19+
title: "阮一峰的网络日志",
20+
url: "https://feeds.feedburner.com/ruanyifeng",
21+
view: FeedViewType.Articles,
22+
},
23+
24+
{
25+
feedId: "41358830592746496",
26+
title: "微博热搜榜",
27+
url: "rsshub://weibo/search/hot",
28+
view: FeedViewType.SocialMedia,
29+
},
30+
{
31+
feedId: "100411504863520768",
32+
title: "Twitter @Elon Musk",
33+
url: "rsshub://twitter/user/elonmusk",
34+
view: FeedViewType.SocialMedia,
35+
},
36+
{
37+
feedId: "41324816676184077",
38+
title: "Twitter @DIŸgöd ☀️",
39+
url: "rsshub://twitter/user/DIYgod",
40+
view: FeedViewType.SocialMedia,
41+
},
42+
43+
{
44+
feedId: "78806242632741888",
45+
title: "bilibili 排行榜-全站",
46+
url: "rsshub://bilibili/ranking/0",
47+
view: FeedViewType.Videos,
48+
},
49+
50+
{
51+
feedId: "60338304723722240",
52+
title: "实时财经快讯 - FastBull",
53+
url: "rsshub://fastbull/express-news",
54+
view: FeedViewType.Articles,
55+
},
56+
{
57+
feedId: "55611390687386624",
58+
title: "格隆汇快讯-7x24小时市场快讯-财经市场热点",
59+
url: "rsshub://gelonghui/live",
60+
view: FeedViewType.Articles,
61+
},
62+
{
63+
feedId: "49375919416104960",
64+
title: "深潮TechFlow - 快讯",
65+
url: "rsshub://techflowpost/express",
66+
view: FeedViewType.Articles,
67+
},
68+
{
69+
feedId: "55982073122828305",
70+
title: "TED Talks Daily",
71+
url: "https://feeds.acast.com/public/shows/67587e77c705e441797aff96",
72+
view: FeedViewType.Articles,
73+
},
74+
{
75+
feedId: "72541715399995392",
76+
title: "TheBlockBeats - 快讯",
77+
url: "rsshub://theblockbeats/newsflash/0",
78+
view: FeedViewType.Articles,
79+
},
80+
{
81+
feedId: "100184911354754055",
82+
title: "小Lin说",
83+
url: "https://www.youtube.com/feeds/videos.xml?channel_id=UCilwQlk62k1z7aUEZPOB6yw",
84+
view: FeedViewType.Videos,
85+
},
86+
{
87+
feedId: "100185810923910148",
88+
title: "张小珺Jùn|商业访谈录",
89+
url: "https://feed.xyzfm.space/dk4yh3pkpjp3",
90+
view: FeedViewType.Articles,
91+
},
92+
{
93+
feedId: "56584656988676096",
94+
title: "迷因电波",
95+
url: "rsshub://xiaoyuzhou/podcast/61d52b3bee197a3aac3dac44",
96+
view: FeedViewType.Articles,
97+
},
98+
{
99+
feedId: "76051724651752448",
100+
title: "极致音乐汇 的 bilibili 空间",
101+
url: "rsshub://bilibili/user/video/1691501735",
102+
view: FeedViewType.Videos,
103+
},
104+
{
105+
feedId: "66701376672681984",
106+
title: "AP Top News - AP News",
107+
url: "rsshub://apnews/api/apf-topnews",
108+
view: FeedViewType.Articles,
109+
},
110+
{
111+
feedId: "44366244616936448",
112+
title: "金十数据",
113+
url: "rsshub://jin10",
114+
view: FeedViewType.Articles,
115+
},
116+
{
117+
feedId: "52325519371718656",
118+
title: "Hacker News",
119+
url: "rsshub://hackernews",
120+
view: FeedViewType.Articles,
121+
},
122+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Text, View } from "react-native"
2+
3+
import { Logo } from "@/src/components/ui/logo"
4+
5+
export const StepFinished = () => (
6+
<View className="flex-1 items-center justify-center">
7+
<Logo width={80} height={80} />
8+
<Text className="text-text my-4 text-3xl font-bold">You're all set!</Text>
9+
<Text className="text-label mb-8 px-6 text-center text-lg">
10+
You have completed the guide. Enjoy your journey!
11+
</Text>
12+
</View>
13+
)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { cn } from "@follow/utils"
2+
import { useCallback, useState } from "react"
3+
import { Text, TouchableOpacity, View } from "react-native"
4+
import Animated, { FadeIn, FadeOut } from "react-native-reanimated"
5+
6+
import { Search3CuteReIcon } from "@/src/icons/search_3_cute_re"
7+
import { Shuffle2CuteReIcon } from "@/src/icons/shuffle_2_cute_re"
8+
import { toast } from "@/src/lib/toast"
9+
import { useSubscription } from "@/src/store/subscription/hooks"
10+
import { subscriptionSyncService } from "@/src/store/subscription/store"
11+
import { accentColor } from "@/src/theme/colors"
12+
13+
import type { PresetFeedConfig } from "./preset"
14+
import { presetFeeds } from "./preset"
15+
16+
const subscribeFeed = async (config: PresetFeedConfig) => {
17+
await subscriptionSyncService.subscribe({
18+
feedId: config.feedId,
19+
title: config.title,
20+
url: config.url,
21+
view: config.view,
22+
category: "",
23+
isPrivate: false,
24+
})
25+
26+
toast.success(`Subscribed to ${config.title}`, {
27+
position: "bottom",
28+
})
29+
}
30+
31+
const unsubscribeFeed = async (feedId: string) => {
32+
await subscriptionSyncService.unsubscribe(feedId)
33+
toast.success(`Unsubscribed from feed`, {
34+
position: "bottom",
35+
})
36+
}
37+
38+
export const StepInterests = () => {
39+
const [displayFeeds, setDisplayFeeds] = useState<PresetFeedConfig[]>(presetFeeds.slice(0, 7))
40+
41+
const shuffleFeeds = useCallback(() => {
42+
const shuffled = [...presetFeeds].sort(() => Math.random() - 0.5).slice(0, 7)
43+
setDisplayFeeds(shuffled)
44+
}, [])
45+
return (
46+
<View className="mt-[10vh] flex-1 items-center">
47+
<View className="mb-10 flex items-center gap-4">
48+
<Search3CuteReIcon height={80} width={80} color={accentColor} />
49+
<Text className="text-text mt-2 text-2xl font-bold">Discover Interests</Text>
50+
<Text className="text-label mb-8 px-6 text-center text-lg">
51+
Subscribe to feeds that match your interests.
52+
</Text>
53+
</View>
54+
55+
<View className="w-full items-center gap-4">
56+
<View className="flex flex-row">
57+
<Text className="mr-2 text-base">Suggestions feed</Text>
58+
<TouchableOpacity
59+
onPress={shuffleFeeds}
60+
className="bg-accent/10 flex-row items-center rounded-full px-3 py-1"
61+
>
62+
<Shuffle2CuteReIcon height={16} width={16} color={accentColor} />
63+
<Text className="text-accent ml-1 text-sm">Shuffle</Text>
64+
</TouchableOpacity>
65+
</View>
66+
67+
<View className="flex-row flex-wrap justify-center gap-2 px-4">
68+
{displayFeeds.map((feed) => (
69+
<FeedChip key={feed.feedId} {...feed} />
70+
))}
71+
</View>
72+
</View>
73+
</View>
74+
)
75+
}
76+
77+
const FeedChip = (feed: PresetFeedConfig) => {
78+
const isSubscribed = useSubscription(feed.feedId)
79+
80+
const handleSubscribe = useCallback(
81+
async (feed: PresetFeedConfig) => {
82+
if (isSubscribed) {
83+
await unsubscribeFeed(feed.feedId)
84+
return
85+
}
86+
await subscribeFeed(feed)
87+
},
88+
[isSubscribed],
89+
)
90+
91+
return (
92+
<Animated.View
93+
key={feed.feedId}
94+
entering={FadeIn.duration(300)}
95+
exiting={FadeOut.duration(300)}
96+
>
97+
<TouchableOpacity
98+
onPress={() => handleSubscribe(feed)}
99+
className={cn(
100+
"flex rounded-full px-4 py-2",
101+
isSubscribed ? "bg-accent" : "bg-secondary-system-fill",
102+
)}
103+
>
104+
<Text className={`text-center text-sm ${isSubscribed ? "text-white" : "text-label"}`}>
105+
{feed.title}
106+
</Text>
107+
</TouchableOpacity>
108+
</Animated.View>
109+
)
110+
}

0 commit comments

Comments
 (0)