From 2389b8301f5015ace0e1ad286187f150d1cd6a35 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 22 Jan 2024 12:20:21 -0600 Subject: [PATCH] WIP --- .../icons/check_stroke2_corner0_rounded.svg | 1 + src/components/forms/Toggle.tsx | 1 + src/components/icons/Check.tsx | 5 + src/screens/Onboarding/Layout.tsx | 2 +- src/screens/Onboarding/StepAlgoFeeds.tsx | 2 +- src/screens/Onboarding/StepFollowingFeed.tsx | 2 +- src/screens/Onboarding/StepInterests.tsx | 63 -------- .../StepInterests/InterestButton.tsx | 78 ++++++++++ src/screens/Onboarding/StepInterests/data.ts | 139 ++++++++++++++++++ .../Onboarding/StepInterests/index.tsx | 116 +++++++++++++++ .../SuggestedAccountCard.tsx | 110 ++++++++++++++ .../StepSuggestedAccounts/index.tsx | 107 ++++++++++++++ .../Onboarding/StepSuggestedFollows.tsx | 49 ------ src/screens/Onboarding/StepTopicalFeeds.tsx | 2 +- src/screens/Onboarding/context.ts | 36 ----- src/screens/Onboarding/index.tsx | 63 +------- src/screens/Onboarding/state.ts | 101 +++++++++++++ src/state/queries/profile.ts | 12 ++ 18 files changed, 679 insertions(+), 210 deletions(-) create mode 100644 assets/icons/check_stroke2_corner0_rounded.svg create mode 100644 src/components/icons/Check.tsx delete mode 100644 src/screens/Onboarding/StepInterests.tsx create mode 100644 src/screens/Onboarding/StepInterests/InterestButton.tsx create mode 100644 src/screens/Onboarding/StepInterests/data.ts create mode 100644 src/screens/Onboarding/StepInterests/index.tsx create mode 100644 src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx create mode 100644 src/screens/Onboarding/StepSuggestedAccounts/index.tsx delete mode 100644 src/screens/Onboarding/StepSuggestedFollows.tsx delete mode 100644 src/screens/Onboarding/context.ts create mode 100644 src/screens/Onboarding/state.ts diff --git a/assets/icons/check_stroke2_corner0_rounded.svg b/assets/icons/check_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..b336a518f5 --- /dev/null +++ b/assets/icons/check_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/forms/Toggle.tsx b/src/components/forms/Toggle.tsx index ad82bdff5f..fe919132c7 100644 --- a/src/components/forms/Toggle.tsx +++ b/src/components/forms/Toggle.tsx @@ -125,6 +125,7 @@ export function Group({ return ( { - setSaving(true) - await new Promise(y => setTimeout(y, 1000)) - setSaving(false) - dispatch({type: 'next'}) - }, [setSaving, dispatch]) - - return ( - - - - - - What are your interests? - - We'll use this to help customize your experience. - - - - - - - ) -} diff --git a/src/screens/Onboarding/StepInterests/InterestButton.tsx b/src/screens/Onboarding/StepInterests/InterestButton.tsx new file mode 100644 index 0000000000..e75a5addcd --- /dev/null +++ b/src/screens/Onboarding/StepInterests/InterestButton.tsx @@ -0,0 +1,78 @@ +import React from 'react' +import {View, ViewStyle, TextStyle} from 'react-native' + +import {useTheme, atoms as a} from '#/alf' +import * as Toggle from '#/components/forms/Toggle' +import {Text} from '#/components/Typography' + +import {InterestItem} from '#/screens/Onboarding/StepInterests/data' + +export function InterestButton({interest}: {interest: InterestItem}) { + const t = useTheme() + const ctx = Toggle.useItemContext() + + const styles = React.useMemo(() => { + const hovered: ViewStyle[] = [ + { + backgroundColor: + t.name === 'light' ? t.palette.contrast_200 : t.palette.contrast_50, + }, + ] + const focused: ViewStyle[] = [] + const pressed: ViewStyle[] = [] + const selected: ViewStyle[] = [ + { + backgroundColor: t.palette.contrast_900, + }, + ] + const selectedHover: ViewStyle[] = [ + { + backgroundColor: t.palette.contrast_800, + }, + ] + const textSelected: TextStyle[] = [ + { + color: t.palette.contrast_100, + }, + ] + + return { + hovered, + focused, + pressed, + selected, + selectedHover, + textSelected, + } + }, [t]) + + return ( + + + {interest.title} + + + ) +} diff --git a/src/screens/Onboarding/StepInterests/data.ts b/src/screens/Onboarding/StepInterests/data.ts new file mode 100644 index 0000000000..c996fbdea7 --- /dev/null +++ b/src/screens/Onboarding/StepInterests/data.ts @@ -0,0 +1,139 @@ +export type InterestItem = { + title: string + name: string + tags: string[] +} + +export const INTERESTS: InterestItem[] = [ + { + title: 'News', + name: 'news', + tags: [], + }, + { + title: 'Journalism', + name: 'journalism', + tags: [], + }, + { + title: 'Writers', + name: 'writers', + tags: [], + }, + { + title: 'Culture', + name: 'culture', + tags: [], + }, + { + title: 'Books', + name: 'books', + tags: [], + }, + { + title: 'Sports', + name: 'sports', + tags: [], + }, + { + title: 'Comedy', + name: 'comedy', + tags: [], + }, + { + title: 'Pets', + name: 'pets', + tags: [], + }, + { + title: 'Nature', + name: 'nature', + tags: [], + }, + { + title: 'Art', + name: 'art', + tags: [], + }, + { + title: 'Comics', + name: 'comics', + tags: [], + }, + { + title: 'Education', + name: 'education', + tags: [], + }, + { + title: 'Climate', + name: 'climate', + tags: [], + }, + { + title: 'Science', + name: 'science', + tags: [], + }, + { + title: 'Politics', + name: 'politics', + tags: [], + }, + { + title: 'Fitness', + name: 'fitness', + tags: [], + }, + { + title: 'Health', + name: 'health', + tags: [], + }, + { + title: 'Tech & Society', + name: 'tech_and_society', + tags: [], + }, + { + title: 'Software Dev', + name: 'software_development', + tags: [], + }, + { + title: 'Video Games', + name: 'video_games', + tags: [], + }, + { + title: 'Food & Cooking', + name: 'food_and_cooking', + tags: [], + }, +] + +export const TEMP_ACCOUNT_MAPPING: { + [key: string]: string[] +} = { + news: ['bsky.app', 'jay.bsky.team'], + journalism: ['bsky.app', 'jay.bsky.team'], + writers: ['bsky.app', 'jay.bsky.team'], + culture: ['bsky.app', 'jay.bsky.team'], + books: ['bsky.app', 'jay.bsky.team'], + sports: ['bsky.app', 'jay.bsky.team'], + comedy: ['bsky.app', 'jay.bsky.team'], + pets: ['bsky.app', 'jay.bsky.team'], + nature: ['bsky.app', 'jay.bsky.team'], + art: ['bsky.app', 'jay.bsky.team'], + comics: ['bsky.app', 'jay.bsky.team'], + education: ['bsky.app', 'jay.bsky.team'], + climate: ['bsky.app', 'jay.bsky.team'], + science: ['bsky.app', 'jay.bsky.team'], + politics: ['bsky.app', 'jay.bsky.team'], + fitness: ['bsky.app', 'jay.bsky.team'], + health: ['bsky.app', 'jay.bsky.team'], + tech_and_society: ['bsky.app', 'jay.bsky.team'], + software_development: ['bsky.app', 'jay.bsky.team'], + video_games: ['bsky.app', 'jay.bsky.team'], + food_and_cooking: ['bsky.app', 'jay.bsky.team'], +} diff --git a/src/screens/Onboarding/StepInterests/index.tsx b/src/screens/Onboarding/StepInterests/index.tsx new file mode 100644 index 0000000000..a7b2638c2a --- /dev/null +++ b/src/screens/Onboarding/StepInterests/index.tsx @@ -0,0 +1,116 @@ +import React from 'react' +import {View} from 'react-native' +import {useQueryClient} from '@tanstack/react-query' + +import {logger} from '#/logger' +import {useTheme, atoms as a} from '#/alf' +import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' +import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {Loader} from '#/components/Loader' +import * as Toggle from '#/components/forms/Toggle' +import {profilesQueryKey} from '#/state/queries/profile' + +import {Context} from '#/screens/Onboarding/state' +import { + Title, + Description, + OnboardingControls, +} from '#/screens/Onboarding/Layout' +import { + INTERESTS as INTEREST_OPTIONS, + TEMP_ACCOUNT_MAPPING, +} from '#/screens/Onboarding/StepInterests/data' +import {InterestButton} from '#/screens/Onboarding/StepInterests/InterestButton' + +export function StepInterests() { + const t = useTheme() + const query = useQueryClient() + const {state, dispatch} = React.useContext(Context) + const [saving, setSaving] = React.useState(false) + const [interests, setInterests] = React.useState([]) + + const saveInterests = React.useCallback(async () => { + setSaving(true) + + try { + const { + data: {accounts}, + } = {data: {accounts: interests.map(i => TEMP_ACCOUNT_MAPPING[i]).flat()}} + const suggestedAccounts = Array.from(new Set(accounts)) + + // wait max 2s for query to finish + await Promise.race([ + await query.prefetchQuery({ + queryKey: profilesQueryKey(suggestedAccounts), + }), + await new Promise(y => setTimeout(y, 2000)), + ]) + + // done + setSaving(false) + dispatch({ + type: 'setSuggestedAccountHandles', + suggestedAccountHandles: suggestedAccounts, + }) + dispatch({type: 'next'}) + } catch (e: any) { + logger.info(`onboading: error saving interests`) + logger.error(e) + } + }, [interests, setSaving, dispatch, query]) + + return ( + + + + + + What are your interests? + + We'll use this to help customize your experience. + + + + + + {INTEREST_OPTIONS.map(interest => ( + + + + ))} + + + + + + + + + ) +} diff --git a/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx b/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx new file mode 100644 index 0000000000..c99ed00461 --- /dev/null +++ b/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx @@ -0,0 +1,110 @@ +import React from 'react' +import {View, ViewStyle} from 'react-native' +import {AppBskyActorDefs} from '@atproto/api' + +import {useTheme, atoms as a, flatten} from '#/alf' +import {Text} from '#/components/Typography' +import {useItemContext} from '#/components/forms/Toggle' +import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' + +export function SuggestedAccountCard({ + profile, +}: { + profile: AppBskyActorDefs.ProfileViewDetailed +}) { + const t = useTheme() + const ctx = useItemContext() + + const styles = React.useMemo(() => { + const base: ViewStyle[] = [t.atoms.bg_contrast_25] + const hover: ViewStyle[] = [t.atoms.bg_contrast_50] + const selected: ViewStyle[] = [ + { + backgroundColor: t.palette.primary_975, + }, + ] + const selectedHover: ViewStyle[] = [ + { + backgroundColor: t.palette.primary_950, + }, + ] + const checkboxBase: ViewStyle[] = [t.atoms.bg_contrast_25] + const checkboxSelected: ViewStyle[] = [ + { + backgroundColor: t.palette.primary_500, + }, + ] + const avatarBase: ViewStyle[] = [t.atoms.bg_contrast_100] + const avatarSelected: ViewStyle[] = [ + { + backgroundColor: t.palette.primary_900, + }, + ] + + return { + base, + hover: flatten(hover), + selected: flatten(selected), + selectedHover: flatten(selectedHover), + checkboxBase: flatten(checkboxBase), + checkboxSelected: flatten(checkboxSelected), + avatarBase: flatten(avatarBase), + avatarSelected: flatten(avatarSelected), + } + }, [t]) + + return ( + + + + + + + {profile.displayName} + + {profile.handle} + + + + + {ctx.selected && } + + + + ) +} diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx new file mode 100644 index 0000000000..ad50cdab95 --- /dev/null +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -0,0 +1,107 @@ +import React from 'react' +import {View} from 'react-native' +import {AppBskyActorDefs} from '@atproto/api' + +import {useTheme, atoms as a, useBreakpoints} from '#/alf' +import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' +import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import {Text} from '#/components/Typography' +import {useProfilesQuery} from '#/state/queries/profile' +import {Loader} from '#/components/Loader' +import * as Toggle from '#/components/forms/Toggle' + +import {Context} from '#/screens/Onboarding/state' +import { + Title, + Description, + OnboardingControls, +} from '#/screens/Onboarding/Layout' +import {SuggestedAccountCard} from '#/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard' + +export function Inner({ + profiles, +}: { + profiles: AppBskyActorDefs.ProfileViewDetailed[] +}) { + const [dids, setDids] = React.useState(profiles.map(p => p.did)) + + return ( + + {profiles.map(profile => ( + + + + ))} + + ) +} + +export function StepSuggestedAccounts() { + const t = useTheme() + const {state, dispatch} = React.useContext(Context) + const {gtMobile} = useBreakpoints() + const {isLoading, isError, data} = useProfilesQuery({ + handles: state.suggestedAccountHandles, + }) + + return ( + + + + + + Here are some accounts for your to follow: + Based on your interest in Pets and Books. + + + {isLoading ? ( + + ) : isError || !data ? ( + Error + ) : ( + + )} + + + + + + + + + + ) +} diff --git a/src/screens/Onboarding/StepSuggestedFollows.tsx b/src/screens/Onboarding/StepSuggestedFollows.tsx deleted file mode 100644 index a41c39b9c4..0000000000 --- a/src/screens/Onboarding/StepSuggestedFollows.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react' -import {View} from 'react-native' - -import {atoms as a, useBreakpoints} from '#/alf' -import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import {Button, ButtonIcon, ButtonText} from '#/components/Button' - -import {Context} from '#/screens/Onboarding/context' -import { - Title, - Description, - OnboardingControls, -} from '#/screens/Onboarding/Layout' - -export function StepSuggestedFollows() { - const {state, dispatch} = React.useContext(Context) - const {gtMobile} = useBreakpoints() - - return ( - - Here are some accounts for your to follow: - Based on your interest in Pets and Books. - - - - - - - - - ) -} diff --git a/src/screens/Onboarding/StepTopicalFeeds.tsx b/src/screens/Onboarding/StepTopicalFeeds.tsx index 0d63db94be..c34376a1b0 100644 --- a/src/screens/Onboarding/StepTopicalFeeds.tsx +++ b/src/screens/Onboarding/StepTopicalFeeds.tsx @@ -5,7 +5,7 @@ import {atoms as a} from '#/alf' import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components/icons/Chevron' import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import {Context} from '#/screens/Onboarding/context' +import {Context} from '#/screens/Onboarding/state' import { Title, Description, diff --git a/src/screens/Onboarding/context.ts b/src/screens/Onboarding/context.ts deleted file mode 100644 index 51c2976e41..0000000000 --- a/src/screens/Onboarding/context.ts +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react' - -export type OnboardingState = { - hasPrev: boolean - totalSteps: number - activeStep: - | 'interests' - | 'suggestedFollows' - | 'followingFeed' - | 'algoFeeds' - | 'topicalFeeds' - activeStepIndex: number -} - -export type OnboardingAction = - | { - type: 'next' - } - | { - type: 'prev' - } - -export const initialState: OnboardingState = { - hasPrev: false, - totalSteps: 5, - activeStep: 'interests', - activeStepIndex: 1, -} - -export const Context = React.createContext<{ - state: OnboardingState - dispatch: React.Dispatch -}>({ - state: initialState, - dispatch: () => {}, -}) diff --git a/src/screens/Onboarding/index.tsx b/src/screens/Onboarding/index.tsx index e3bd3bde87..46847241f4 100644 --- a/src/screens/Onboarding/index.tsx +++ b/src/screens/Onboarding/index.tsx @@ -1,69 +1,16 @@ import React from 'react' -import {logger} from '#/logger' import {Portal} from '#/components/Portal' -import { - Context, - OnboardingState, - OnboardingAction, - initialState, -} from '#/screens/Onboarding/context' +import {Context, initialState, reducer} from '#/screens/Onboarding/state' import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout' import {StepInterests} from '#/screens/Onboarding/StepInterests' -import {StepSuggestedFollows} from '#/screens/Onboarding/StepSuggestedFollows' +import {StepSuggestedAccounts} from '#/screens/Onboarding/StepSuggestedAccounts' import {StepFollowingFeed} from '#/screens/Onboarding/StepFollowingFeed' import {StepAlgoFeeds} from '#/screens/Onboarding/StepAlgoFeeds' import {StepTopicalFeeds} from '#/screens/Onboarding/StepTopicalFeeds' -function reducer(s: OnboardingState, a: OnboardingAction): OnboardingState { - const next = s - - switch (a.type) { - case 'next': { - if (s.activeStep === 'interests') { - next.activeStep = 'suggestedFollows' - next.activeStepIndex = 2 - } else if (s.activeStep === 'suggestedFollows') { - next.activeStep = 'followingFeed' - next.activeStepIndex = 3 - } else if (s.activeStep === 'followingFeed') { - next.activeStep = 'algoFeeds' - next.activeStepIndex = 4 - } else if (s.activeStep === 'algoFeeds') { - next.activeStep = 'topicalFeeds' - next.activeStepIndex = 5 - } - break - } - case 'prev': { - if (s.activeStep === 'suggestedFollows') { - next.activeStep = 'interests' - next.activeStepIndex = 1 - } else if (s.activeStep === 'followingFeed') { - next.activeStep = 'suggestedFollows' - next.activeStepIndex = 2 - } else if (s.activeStep === 'algoFeeds') { - next.activeStep = 'followingFeed' - next.activeStepIndex = 3 - } else if (s.activeStep === 'topicalFeeds') { - next.activeStep = 'algoFeeds' - next.activeStepIndex = 4 - } - break - } - } - - const state = { - ...next, - hasPrev: next.activeStep !== 'interests', - } - - logger.debug(`onboarding`, state) - - return state -} - +// TODO lock focus? export function Onboarding() { const [state, dispatch] = React.useReducer(reducer, initialState) @@ -74,8 +21,8 @@ export function Onboarding() { value={React.useMemo(() => ({state, dispatch}), [state, dispatch])}> {state.activeStep === 'interests' && } - {state.activeStep === 'suggestedFollows' && ( - + {state.activeStep === 'suggestedAccounts' && ( + )} {state.activeStep === 'followingFeed' && } {state.activeStep === 'algoFeeds' && } diff --git a/src/screens/Onboarding/state.ts b/src/screens/Onboarding/state.ts new file mode 100644 index 0000000000..55e17eda33 --- /dev/null +++ b/src/screens/Onboarding/state.ts @@ -0,0 +1,101 @@ +import React from 'react' + +import {logger} from '#/logger' + +export type OnboardingState = { + hasPrev: boolean + totalSteps: number + activeStep: + | 'interests' + | 'suggestedAccounts' + | 'followingFeed' + | 'algoFeeds' + | 'topicalFeeds' + activeStepIndex: number + suggestedAccountHandles: string[] +} + +export type OnboardingAction = + | { + type: 'next' + } + | { + type: 'prev' + } + | { + type: 'setSuggestedAccountHandles' + suggestedAccountHandles: string[] + } + +export const initialState: OnboardingState = { + hasPrev: false, + totalSteps: 5, + activeStep: 'interests', + activeStepIndex: 1, + + // result of interests step + suggestedAccountHandles: [], +} + +export const Context = React.createContext<{ + state: OnboardingState + dispatch: React.Dispatch +}>({ + state: initialState, + dispatch: () => {}, +}) + +export function reducer( + s: OnboardingState, + a: OnboardingAction, +): OnboardingState { + const next = s + + switch (a.type) { + case 'next': { + if (s.activeStep === 'interests') { + next.activeStep = 'suggestedAccounts' + next.activeStepIndex = 2 + } else if (s.activeStep === 'suggestedAccounts') { + next.activeStep = 'followingFeed' + next.activeStepIndex = 3 + } else if (s.activeStep === 'followingFeed') { + next.activeStep = 'algoFeeds' + next.activeStepIndex = 4 + } else if (s.activeStep === 'algoFeeds') { + next.activeStep = 'topicalFeeds' + next.activeStepIndex = 5 + } + break + } + case 'prev': { + if (s.activeStep === 'suggestedAccounts') { + next.activeStep = 'interests' + next.activeStepIndex = 1 + } else if (s.activeStep === 'followingFeed') { + next.activeStep = 'suggestedAccounts' + next.activeStepIndex = 2 + } else if (s.activeStep === 'algoFeeds') { + next.activeStep = 'followingFeed' + next.activeStepIndex = 3 + } else if (s.activeStep === 'topicalFeeds') { + next.activeStep = 'algoFeeds' + next.activeStepIndex = 4 + } + break + } + case 'setSuggestedAccountHandles': { + next.suggestedAccountHandles = a.suggestedAccountHandles + break + } + } + + const state = { + ...next, + hasPrev: next.activeStep !== 'interests', + } + + logger.debug(`onboarding`, state) + + return state +} diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 21e2e5775b..f535041b9f 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -24,6 +24,7 @@ import {STALE} from '#/state/queries' import {track} from '#/lib/analytics/analytics' export const RQKEY = (did: string) => ['profile', did] +export const profilesQueryKey = (handles: string[]) => ['profiles', handles] export function useProfileQuery({did}: {did: string | undefined}) { const {currentAccount} = useSession() @@ -45,6 +46,17 @@ export function useProfileQuery({did}: {did: string | undefined}) { }) } +export function useProfilesQuery({handles}: {handles: string[]}) { + return useQuery({ + staleTime: STALE.MINUTES.FIVE, + queryKey: profilesQueryKey(handles), + queryFn: async () => { + const res = await getAgent().getProfiles({actors: handles}) + return res.data + }, + }) +} + interface ProfileUpdateParams { profile: AppBskyActorDefs.ProfileView updates: