Outer layout, some primitive updates

This commit is contained in:
Eric Bailey
2024-01-20 15:19:33 -06:00
parent 60c9b58573
commit f0a602a51c
25 changed files with 684 additions and 49 deletions
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 756 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M15.707 3.293a1 1 0 0 1 0 1.414L8.414 12l7.293 7.293a1 1 0 0 1-1.414 1.414l-8-8a1 1 0 0 1 0-1.414l8-8a1 1 0 0 1 1.414 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 263 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 263 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 246 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 3a1 1 0 0 1 1 1v7h7a1 1 0 1 1 0 2h-7v7a1 1 0 1 1-2 0v-7H4a1 1 0 1 1 0-2h7V4a1 1 0 0 1 1-1Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 237 B

+6
View File
@@ -104,6 +104,9 @@ export const atoms = {
flex: {
display: 'flex',
},
flex_col: {
flexDirection: 'column',
},
flex_row: {
flexDirection: 'row',
},
@@ -188,6 +191,9 @@ export const atoms = {
lineHeight: tokens.fontSize._5xl,
},
leading_tight: {
lineHeight: 1.15,
},
leading_snug: {
lineHeight: 1.25,
},
leading_normal: {
+1
View File
@@ -2,6 +2,7 @@ import React from 'react'
import {Dimensions} from 'react-native'
import * as themes from '#/alf/themes'
export * from '#/alf/types'
export * as tokens from '#/alf/tokens'
export {atoms} from '#/alf/atoms'
export * from '#/alf/util/platform'
+10
View File
@@ -1,3 +1,5 @@
import {StyleProp, ViewStyle, TextStyle} from 'react-native'
type LiteralToCommon<T extends PropertyKey> = T extends number
? number
: T extends string
@@ -14,3 +16,11 @@ export type Mutable<T> = {
? LiteralToCommon<T[K]>
: Mutable<T[K]>
}
export type TextStyleProp = {
style?: StyleProp<TextStyle>
}
export type ViewStyleProp = {
style?: StyleProp<ViewStyle>
}
+7 -8
View File
@@ -273,15 +273,15 @@ export function Button({
if (shape === 'default') {
if (size === 'large') {
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_sm)
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
} else if (size === 'small') {
baseStyles.push({paddingVertical: 9}, a.px_md, a.rounded_sm, a.gap_sm)
}
} else if (shape === 'round' || shape === 'square') {
if (size === 'large') {
baseStyles.push(a.p_lg)
baseStyles.push({padding: 15})
} else if (size === 'small') {
baseStyles.push(a.p_md)
baseStyles.push({padding: 10})
}
if (shape === 'round') {
@@ -360,6 +360,7 @@ export function Button({
flatten(style),
a.flex_row,
a.align_center,
a.justify_center,
a.overflow_hidden,
...baseStyles,
...(state.hovered || state.pressed ? hoverStyles : []),
@@ -506,9 +507,7 @@ export function ButtonText({children, style, ...rest}: ButtonTextProps) {
const textStyles = useSharedButtonTextStyles()
return (
<Text
{...rest}
style={[a.w_full, a.font_bold, a.text_center, textStyles, style]}>
<Text {...rest} style={[a.font_bold, a.text_center, textStyles, style]}>
{children}
</Text>
)
@@ -519,11 +518,11 @@ export function ButtonIcon({
}: {
icon: React.ComponentType<SVGIconProps>
}) {
const {size} = useButtonContext()
const {size, disabled} = useButtonContext()
const textStyles = useSharedButtonTextStyles()
return (
<View style={[a.z_20]}>
<View style={[a.z_20, {marginTop: -1, opacity: disabled ? 0.7 : 1}]}>
<Comp
size={size === 'large' ? 'md' : 'sm'}
style={[{color: textStyles.color, pointerEvents: 'none'}]}
+41
View File
@@ -0,0 +1,41 @@
import React from 'react'
import Animated, {
Easing,
useSharedValue,
useAnimatedStyle,
withRepeat,
withTiming,
} from 'react-native-reanimated'
import {atoms as a} from '#/alf'
import {Props, useCommonSVGProps} from '#/components/icons/common'
import {Loader_Stroke2_Corner0_Rounded as Icon} from '#/components/icons/Loader'
export function Loader(props: Props) {
const common = useCommonSVGProps(props)
const rotation = useSharedValue(0)
const animatedStyles = useAnimatedStyle(() => ({
transform: [{rotate: rotation.value + 'deg'}],
}))
React.useEffect(() => {
rotation.value = withRepeat(
withTiming(360, {duration: 500, easing: Easing.linear}),
-1,
)
}, [rotation])
return (
<Animated.View
style={[
a.relative,
a.justify_center,
a.align_center,
{width: common.size, height: common.size},
animatedStyles,
]}>
<Icon {...props} style={[a.absolute, a.inset_0, props.style]} />
</Animated.View>
)
}
+45 -36
View File
@@ -12,45 +12,54 @@ type ComponentMap = {
[id: string]: Component
}
export const Context = React.createContext<ContextType>({
outlet: null,
append: () => {},
remove: () => {},
})
export function createPortalGroup() {
const Context = React.createContext<ContextType>({
outlet: null,
append: () => {},
remove: () => {},
})
export function Provider(props: React.PropsWithChildren<{}>) {
const map = React.useRef<ComponentMap>({})
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
function Provider(props: React.PropsWithChildren<{}>) {
const map = React.useRef<ComponentMap>({})
const [outlet, setOutlet] = React.useState<ContextType['outlet']>(null)
const append = React.useCallback<ContextType['append']>((id, component) => {
if (map.current[id]) return
map.current[id] = <React.Fragment key={id}>{component}</React.Fragment>
setOutlet(<>{Object.values(map.current)}</>)
}, [])
const append = React.useCallback<ContextType['append']>((id, component) => {
if (map.current[id]) return
map.current[id] = <React.Fragment key={id}>{component}</React.Fragment>
setOutlet(<>{Object.values(map.current)}</>)
}, [])
const remove = React.useCallback<ContextType['remove']>(id => {
delete map.current[id]
setOutlet(<>{Object.values(map.current)}</>)
}, [])
const remove = React.useCallback<ContextType['remove']>(id => {
delete map.current[id]
setOutlet(<>{Object.values(map.current)}</>)
}, [])
return (
<Context.Provider value={{outlet, append, remove}}>
{props.children}
</Context.Provider>
)
return (
<Context.Provider value={{outlet, append, remove}}>
{props.children}
</Context.Provider>
)
}
function Outlet() {
const ctx = React.useContext(Context)
return ctx.outlet
}
function Portal({children}: React.PropsWithChildren<{}>) {
const {append, remove} = React.useContext(Context)
const id = React.useId()
React.useEffect(() => {
append(id, children as Component)
return () => remove(id)
}, [id, children, append, remove])
return null
}
return {Provider, Outlet, Portal}
}
export function Outlet() {
const ctx = React.useContext(Context)
return ctx.outlet
}
export function Portal({children}: React.PropsWithChildren<{}>) {
const {append, remove} = React.useContext(Context)
const id = React.useId()
React.useEffect(() => {
append(id, children as Component)
return () => remove(id)
}, [id, children, append, remove])
return null
}
const DefaultPortal = createPortalGroup()
export const Provider = DefaultPortal.Provider
export const Outlet = DefaultPortal.Outlet
export const Portal = DefaultPortal.Portal
+18 -3
View File
@@ -3,6 +3,19 @@ import {Text as RNText, TextProps} from 'react-native'
import {useTheme, atoms, web, flatten} from '#/alf'
/**
* Util to calculate lineHeight from a text size atom and a leading atom
*
* Example:
* `leading(atoms.text_md, atoms.leading_normal)` // => 24
*/
export function leading<
Size extends {fontSize: number},
Leading extends {lineHeight: number},
>(textSize: Size, leading: Leading) {
return (textSize.fontSize || atoms.text_md.fontSize) * leading.lineHeight
}
export function Text({style, ...rest}: TextProps) {
const t = useTheme()
return <RNText style={[atoms.text_sm, t.atoms.text, style]} {...rest} />
@@ -111,9 +124,11 @@ export function P({style, ...rest}: TextProps) {
role: 'paragraph',
}) || {}
const _style = flatten(style)
const lineHeight =
(_style?.lineHeight || atoms.text_md.lineHeight) *
atoms.leading_normal.lineHeight
const lineHeight = leading(
// @ts-ignore
_style?.fontSize ? _style : atoms.text_md,
atoms.leading_normal,
)
return (
<RNText
{...attr}
+4
View File
@@ -3,3 +3,7 @@ import {createSinglePathSVG} from './TEMPLATE'
export const ChevronLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M15.707 3.293a1 1 0 0 1 0 1.414L8.414 12l7.293 7.293a1 1 0 0 1-1.414 1.414l-8-8a1 1 0 0 1 0-1.414l8-8a1 1 0 0 1 1.414 0Z',
})
export const ChevronRight_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Loader_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 5a7 7 0 0 0-5.218 11.666A1 1 0 0 1 5.292 18a9 9 0 1 1 13.416 0 1 1 0 1 1-1.49-1.334A7 7 0 0 0 12 5Z',
})
+5
View File
@@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const PlusLarge_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12 3a1 1 0 0 1 1 1v7h7a1 1 0 1 1 0 2h-7v7a1 1 0 1 1-2 0v-7H4a1 1 0 1 1 0-2h7V4a1 1 0 0 1 1-1Z',
})
+2
View File
@@ -1,2 +1,4 @@
export const LOGIN_INCLUDE_DEV_SERVERS = true
export const PWI_ENABLED = true
// TODO
export const NEW_ONBOARDING_ENABLED = true
+173
View File
@@ -0,0 +1,173 @@
import React from 'react'
import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {IS_DEV} from '#/env'
import {isWeb} from '#/platform/detection'
import {useOnboardingDispatch} from '#/state/shell'
import {
useTheme,
atoms as a,
useBreakpoints,
web,
native,
flatten,
TextStyleProp,
} from '#/alf'
import {H2, P, leading} from '#/components/Typography'
import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
import {Button, ButtonIcon} from '#/components/Button'
import {ScrollView} from '#/view/com/util/Views'
import {createPortalGroup} from '#/components/Portal'
import {Context} from '#/screens/Onboarding/context'
const COL_WIDTH = 500
export const OnboardingControls = createPortalGroup()
export function Layout({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
const insets = useSafeAreaInsets()
const {gtMobile} = useBreakpoints()
const onboardDispatch = useOnboardingDispatch()
const {state, dispatch} = React.useContext(Context)
return (
<View style={[a.absolute, a.inset_0, a.flex_1, t.atoms.bg]}>
{IS_DEV && (
<View style={[a.absolute, a.p_xl, a.z_10, {right: 0, top: insets.top}]}>
<Button
variant="ghost"
color="negative"
size="small"
onPress={() => onboardDispatch({type: 'skip'})}
label="Clear onboarding state">
Clear
</Button>
</View>
)}
{state.hasPrev && (
<View
style={[
web(a.fixed),
native(a.absolute),
a.flex_row,
a.w_full,
a.justify_center,
a.z_20,
a.px_xl,
{
top: a.py_5xl.paddingTop + insets.top,
},
]}>
<View style={[a.w_full, a.align_start, {maxWidth: COL_WIDTH}]}>
<Button
key={state.activeStep} // remove focus state on nav
variant="ghost"
color="secondary"
size="small"
shape="round"
label="Go back"
style={[a.absolute, t.atoms.shadow_sm]}
onPress={() => dispatch({type: 'prev'})}>
<ButtonIcon icon={ChevronLeft} />
</Button>
</View>
</View>
)}
<ScrollView
style={[a.h_full, a.w_full, {paddingTop: insets.top}]}
contentContainerStyle={{borderWidth: 0}}>
<View
style={[a.flex_row, a.justify_center, gtMobile ? a.px_5xl : a.px_xl]}>
<View style={[a.flex_1, {maxWidth: COL_WIDTH}]}>
<View style={[a.w_full, a.align_center, a.pt_5xl]}>
<View
style={[
a.flex_row,
a.gap_sm,
a.w_full,
{paddingTop: 17, maxWidth: '60%'},
]}>
{Array(state.totalSteps)
.fill(0)
.map((_, i) => (
<View
key={i}
style={[
a.flex_1,
a.pt_xs,
a.rounded_full,
t.atoms.bg_contrast_50,
{
backgroundColor:
i + 1 <= state.activeStepIndex
? t.palette.primary_500
: t.palette.contrast_100,
},
]}
/>
))}
</View>
</View>
<View style={[a.w_full, a.mb_5xl, {paddingTop: 120}]}>
{children}
{isWeb && gtMobile && (
<View style={[a.w_full, a.flex_row, a.justify_end, a.pt_5xl]}>
<OnboardingControls.Outlet />
</View>
)}
</View>
</View>
</View>
</ScrollView>
{(!isWeb || !gtMobile) && (
<View
style={[
a.align_center,
gtMobile ? a.px_5xl : a.px_xl,
{
paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom),
},
]}>
<View style={[a.w_full, {maxWidth: COL_WIDTH}]}>
<OnboardingControls.Outlet />
</View>
</View>
)}
</View>
)
}
export function Title({
children,
style,
}: React.PropsWithChildren<TextStyleProp>) {
return (
<H2
style={[
a.pb_sm,
{
lineHeight: leading(a.text_4xl, a.leading_tight),
},
flatten(style),
]}>
{children}
</H2>
)
}
export function Description({
children,
style,
}: React.PropsWithChildren<TextStyleProp>) {
const t = useTheme()
return <P style={[t.atoms.text_contrast_700, flatten(style)]}>{children}</P>
}
+39
View File
@@ -0,0 +1,39 @@
import React from 'react'
import {View} from 'react-native'
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 {
Title,
Description,
OnboardingControls,
} from '#/screens/Onboarding/Layout'
export function StepAlgoFeeds() {
const {state, dispatch} = React.useContext(Context)
return (
<View style={[a.align_start]}>
<Title>Choose your algorithmic feeds</Title>
<Description>
Feeds are created by users and can give you entirely new experiences.
</Description>
<OnboardingControls.Portal>
<Button
key={state.activeStep} // remove focus state on nav
variant="gradient"
color="gradient_sky"
size="large"
label="Continue setting up your account"
onPress={() => dispatch({type: 'next'})}>
<ButtonText>Continue</ButtonText>
<ButtonIcon icon={ChevronRight} />
</Button>
</OnboardingControls.Portal>
</View>
)
}
@@ -0,0 +1,40 @@
import React from 'react'
import {View} from 'react-native'
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 {
Title,
Description,
OnboardingControls,
} from '#/screens/Onboarding/Layout'
export function StepFollowingFeed() {
const {state, dispatch} = React.useContext(Context)
return (
<View style={[a.align_start]}>
<Title>Your default feed is "Following"</Title>
<Description>
It only show posts from the people your follow, in the order they were
posted.
</Description>
<OnboardingControls.Portal>
<Button
key={state.activeStep} // remove focus state on nav
variant="gradient"
color="gradient_sky"
size="large"
label="Continue setting up your account"
onPress={() => dispatch({type: 'next'})}>
<ButtonText>Continue</ButtonText>
<ButtonIcon icon={ChevronRight} />
</Button>
</OnboardingControls.Portal>
</View>
)
}
+63
View File
@@ -0,0 +1,63 @@
import React from 'react'
import {View} from 'react-native'
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 {Context} from '#/screens/Onboarding/context'
import {
Title,
Description,
OnboardingControls,
} from '#/screens/Onboarding/Layout'
export function StepInterests() {
const t = useTheme()
const {state, dispatch} = React.useContext(Context)
const [saving, setSaving] = React.useState(false)
const saveInterests = React.useCallback(async () => {
setSaving(true)
await new Promise(y => setTimeout(y, 1000))
setSaving(false)
dispatch({type: 'next'})
}, [setSaving, dispatch])
return (
<View style={[a.align_start]}>
<View
style={[
a.p_lg,
a.rounded_full,
{
backgroundColor:
t.name === 'light' ? t.palette.primary_25 : t.palette.primary_975,
},
]}>
<At size="xl" fill={t.palette.primary_500} />
</View>
<Title>What are your interests?</Title>
<Description>
We'll use this to help customize your experience.
</Description>
<OnboardingControls.Portal>
<Button
disabled={saving}
key={state.activeStep} // remove focus state on nav
variant="gradient"
color="gradient_sky"
size="large"
label="Continue setting up your account"
onPress={saveInterests}>
<ButtonText>Continue</ButtonText>
<ButtonIcon icon={saving ? Loader : ChevronRight} />
</Button>
</OnboardingControls.Portal>
</View>
)
}
@@ -0,0 +1,49 @@
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 (
<View style={[a.align_start]}>
<Title>Here are some accounts for your to follow:</Title>
<Description>Based on your interest in Pets and Books.</Description>
<OnboardingControls.Portal>
<View style={[a.gap_md, gtMobile ? a.flex_row : a.flex_col]}>
<Button
key={state.activeStep} // remove focus state on nav
variant="gradient"
color="gradient_sky"
size="large"
label="Continue setting up your account"
onPress={() => dispatch({type: 'next'})}>
<ButtonText>Follow All</ButtonText>
<ButtonIcon icon={Plus} />
</Button>
<Button
key={state.activeStep + '2'} // remove focus state on nav
variant="outline"
color="secondary"
size="large"
label="Continue setting up your account"
onPress={() => dispatch({type: 'next'})}>
Skip
</Button>
</View>
</OnboardingControls.Portal>
</View>
)
}
@@ -0,0 +1,40 @@
import React from 'react'
import {View} from 'react-native'
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 {
Title,
Description,
OnboardingControls,
} from '#/screens/Onboarding/Layout'
export function StepTopicalFeeds() {
const {state, dispatch} = React.useContext(Context)
return (
<View style={[a.align_start]}>
<Title>Feeds can be topic-based as well!</Title>
<Description>
Here are some topic-based feeds based on your interests in Pets and
Books.
</Description>
<OnboardingControls.Portal>
<Button
key={state.activeStep} // remove focus state on nav
variant="gradient"
color="gradient_sky"
size="large"
label="Continue setting up your account"
onPress={() => dispatch({type: 'next'})}>
<ButtonText>Continue</ButtonText>
<ButtonIcon icon={ChevronRight} />
</Button>
</OnboardingControls.Portal>
</View>
)
}
+36
View File
@@ -0,0 +1,36 @@
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<OnboardingAction>
}>({
state: initialState,
dispatch: () => {},
})
+88
View File
@@ -0,0 +1,88 @@
import React from 'react'
import {logger} from '#/logger'
import {Portal} from '#/components/Portal'
import {
Context,
OnboardingState,
OnboardingAction,
initialState,
} from '#/screens/Onboarding/context'
import {Layout, OnboardingControls} from '#/screens/Onboarding/Layout'
import {StepInterests} from '#/screens/Onboarding/StepInterests'
import {StepSuggestedFollows} from '#/screens/Onboarding/StepSuggestedFollows'
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
}
export function Onboarding() {
const [state, dispatch] = React.useReducer(reducer, initialState)
return (
<Portal>
<OnboardingControls.Provider>
<Context.Provider
value={React.useMemo(() => ({state, dispatch}), [state, dispatch])}>
<Layout>
{state.activeStep === 'interests' && <StepInterests />}
{state.activeStep === 'suggestedFollows' && (
<StepSuggestedFollows />
)}
{state.activeStep === 'followingFeed' && <StepFollowingFeed />}
{state.activeStep === 'algoFeeds' && <StepAlgoFeeds />}
{state.activeStep === 'topicalFeeds' && <StepTopicalFeeds />}
</Layout>
</Context.Provider>
</OnboardingControls.Provider>
</Portal>
)
}
@@ -1,6 +1,6 @@
import * as React from 'react'
import {View} from 'react-native'
import {PWI_ENABLED} from '#/lib/build-flags'
import {PWI_ENABLED, NEW_ONBOARDING_ENABLED} from '#/lib/build-flags'
// Based on @react-navigation/native-stack/src/createNativeStackNavigator.ts
// MIT License
@@ -37,6 +37,7 @@ import {useSession} from '#/state/session'
import {isWeb} from 'platform/detection'
import {LoggedOut} from '../com/auth/LoggedOut'
import {Onboarding} from '../com/auth/Onboarding'
import {Onboarding as NewOnboarding} from '#/screens/Onboarding'
type NativeStackNavigationOptionsWithAuth = NativeStackNavigationOptions & {
requireAuth?: boolean
@@ -107,7 +108,11 @@ function NativeStackNavigator({
return <LoggedOut onDismiss={() => setShowLoggedOut(false)} />
}
if (onboardingState.isActive) {
return <Onboarding />
if (NEW_ONBOARDING_ENABLED) {
return <NewOnboarding />
} else {
return <Onboarding />
}
}
const newDescriptors: typeof descriptors = {}
for (let key in descriptors) {