diff --git a/assets/icons/at_stroke2_corner0_rounded.svg b/assets/icons/at_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..8d30d7c8c5
--- /dev/null
+++ b/assets/icons/at_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/chevronLeft_stroke2_corner0_rounded.svg b/assets/icons/chevronLeft_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..d9a8660f7f
--- /dev/null
+++ b/assets/icons/chevronLeft_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/chevronRight_stroke2_corner0_rounded.svg b/assets/icons/chevronRight_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..b57fd03986
--- /dev/null
+++ b/assets/icons/chevronRight_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/loader_stroke2_corner0_rounded.svg b/assets/icons/loader_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..9dbc013798
--- /dev/null
+++ b/assets/icons/loader_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/plusLarge_stroke2_corner0_rounded.svg b/assets/icons/plusLarge_stroke2_corner0_rounded.svg
new file mode 100644
index 0000000000..8d568437b5
--- /dev/null
+++ b/assets/icons/plusLarge_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts
index 203c2f282a..c5de29c024 100644
--- a/src/alf/atoms.ts
+++ b/src/alf/atoms.ts
@@ -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: {
diff --git a/src/alf/index.tsx b/src/alf/index.tsx
index 69a8798539..06d6ebf01c 100644
--- a/src/alf/index.tsx
+++ b/src/alf/index.tsx
@@ -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'
diff --git a/src/alf/types.ts b/src/alf/types.ts
index 76ac05d40e..dd8d816d2b 100644
--- a/src/alf/types.ts
+++ b/src/alf/types.ts
@@ -1,3 +1,5 @@
+import {StyleProp, ViewStyle, TextStyle} from 'react-native'
+
type LiteralToCommon = T extends number
? number
: T extends string
@@ -14,3 +16,11 @@ export type Mutable = {
? LiteralToCommon
: Mutable
}
+
+export type TextStyleProp = {
+ style?: StyleProp
+}
+
+export type ViewStyleProp = {
+ style?: StyleProp
+}
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 491362007c..77aa1c4d4e 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -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 (
-
+
{children}
)
@@ -519,11 +518,11 @@ export function ButtonIcon({
}: {
icon: React.ComponentType
}) {
- const {size} = useButtonContext()
+ const {size, disabled} = useButtonContext()
const textStyles = useSharedButtonTextStyles()
return (
-
+
({
+ transform: [{rotate: rotation.value + 'deg'}],
+ }))
+
+ React.useEffect(() => {
+ rotation.value = withRepeat(
+ withTiming(360, {duration: 500, easing: Easing.linear}),
+ -1,
+ )
+ }, [rotation])
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/Portal.tsx b/src/components/Portal.tsx
index 1813d9e05e..d696f986b4 100644
--- a/src/components/Portal.tsx
+++ b/src/components/Portal.tsx
@@ -12,45 +12,54 @@ type ComponentMap = {
[id: string]: Component
}
-export const Context = React.createContext({
- outlet: null,
- append: () => {},
- remove: () => {},
-})
+export function createPortalGroup() {
+ const Context = React.createContext({
+ outlet: null,
+ append: () => {},
+ remove: () => {},
+ })
-export function Provider(props: React.PropsWithChildren<{}>) {
- const map = React.useRef({})
- const [outlet, setOutlet] = React.useState(null)
+ function Provider(props: React.PropsWithChildren<{}>) {
+ const map = React.useRef({})
+ const [outlet, setOutlet] = React.useState(null)
- const append = React.useCallback((id, component) => {
- if (map.current[id]) return
- map.current[id] = {component}
- setOutlet(<>{Object.values(map.current)}>)
- }, [])
+ const append = React.useCallback((id, component) => {
+ if (map.current[id]) return
+ map.current[id] = {component}
+ setOutlet(<>{Object.values(map.current)}>)
+ }, [])
- const remove = React.useCallback(id => {
- delete map.current[id]
- setOutlet(<>{Object.values(map.current)}>)
- }, [])
+ const remove = React.useCallback(id => {
+ delete map.current[id]
+ setOutlet(<>{Object.values(map.current)}>)
+ }, [])
- return (
-
- {props.children}
-
- )
+ return (
+
+ {props.children}
+
+ )
+ }
+
+ 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
diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx
index 66cf0720de..daf06aedfe 100644
--- a/src/components/Typography.tsx
+++ b/src/components/Typography.tsx
@@ -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
@@ -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 (
) {
+ const t = useTheme()
+ const insets = useSafeAreaInsets()
+ const {gtMobile} = useBreakpoints()
+ const onboardDispatch = useOnboardingDispatch()
+ const {state, dispatch} = React.useContext(Context)
+
+ return (
+
+ {IS_DEV && (
+
+
+
+ )}
+
+ {state.hasPrev && (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ {Array(state.totalSteps)
+ .fill(0)
+ .map((_, i) => (
+
+ ))}
+
+
+
+
+ {children}
+
+ {isWeb && gtMobile && (
+
+
+
+ )}
+
+
+
+
+
+ {(!isWeb || !gtMobile) && (
+
+
+
+
+
+ )}
+
+ )
+}
+
+export function Title({
+ children,
+ style,
+}: React.PropsWithChildren) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function Description({
+ children,
+ style,
+}: React.PropsWithChildren) {
+ const t = useTheme()
+ return {children}
+}
diff --git a/src/screens/Onboarding/StepAlgoFeeds.tsx b/src/screens/Onboarding/StepAlgoFeeds.tsx
new file mode 100644
index 0000000000..3cf4955d12
--- /dev/null
+++ b/src/screens/Onboarding/StepAlgoFeeds.tsx
@@ -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 (
+
+ Choose your algorithmic feeds
+
+ Feeds are created by users and can give you entirely new experiences.
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Onboarding/StepFollowingFeed.tsx b/src/screens/Onboarding/StepFollowingFeed.tsx
new file mode 100644
index 0000000000..9f64242679
--- /dev/null
+++ b/src/screens/Onboarding/StepFollowingFeed.tsx
@@ -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 (
+
+ Your default feed is "Following"
+
+ It only show posts from the people your follow, in the order they were
+ posted.
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Onboarding/StepInterests.tsx b/src/screens/Onboarding/StepInterests.tsx
new file mode 100644
index 0000000000..8f69aa51f1
--- /dev/null
+++ b/src/screens/Onboarding/StepInterests.tsx
@@ -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 (
+
+
+
+
+
+ What are your interests?
+
+ We'll use this to help customize your experience.
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Onboarding/StepSuggestedFollows.tsx b/src/screens/Onboarding/StepSuggestedFollows.tsx
new file mode 100644
index 0000000000..a41c39b9c4
--- /dev/null
+++ b/src/screens/Onboarding/StepSuggestedFollows.tsx
@@ -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 (
+
+ 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
new file mode 100644
index 0000000000..0d63db94be
--- /dev/null
+++ b/src/screens/Onboarding/StepTopicalFeeds.tsx
@@ -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 (
+
+ Feeds can be topic-based as well!
+
+ Here are some topic-based feeds based on your interests in Pets and
+ Books.
+
+
+
+
+
+
+ )
+}
diff --git a/src/screens/Onboarding/context.ts b/src/screens/Onboarding/context.ts
new file mode 100644
index 0000000000..51c2976e41
--- /dev/null
+++ b/src/screens/Onboarding/context.ts
@@ -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
+}>({
+ state: initialState,
+ dispatch: () => {},
+})
diff --git a/src/screens/Onboarding/index.tsx b/src/screens/Onboarding/index.tsx
new file mode 100644
index 0000000000..e3bd3bde87
--- /dev/null
+++ b/src/screens/Onboarding/index.tsx
@@ -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 (
+
+
+ ({state, dispatch}), [state, dispatch])}>
+
+ {state.activeStep === 'interests' && }
+ {state.activeStep === 'suggestedFollows' && (
+
+ )}
+ {state.activeStep === 'followingFeed' && }
+ {state.activeStep === 'algoFeeds' && }
+ {state.activeStep === 'topicalFeeds' && }
+
+
+
+
+ )
+}
diff --git a/src/view/shell/createNativeStackNavigatorWithAuth.tsx b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
index 9fea6e49f1..948da3f94c 100644
--- a/src/view/shell/createNativeStackNavigatorWithAuth.tsx
+++ b/src/view/shell/createNativeStackNavigatorWithAuth.tsx
@@ -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 setShowLoggedOut(false)} />
}
if (onboardingState.isActive) {
- return
+ if (NEW_ONBOARDING_ENABLED) {
+ return
+ } else {
+ return
+ }
}
const newDescriptors: typeof descriptors = {}
for (let key in descriptors) {