Move onboarding state to new persistence + reducer context (#1835)
parent
3a211017d3
commit
4afed4be28
|
@ -1,106 +0,0 @@
|
||||||
import {makeAutoObservable} from 'mobx'
|
|
||||||
import {RootStoreModel} from '../root-store'
|
|
||||||
import {hasProp} from 'lib/type-guards'
|
|
||||||
import {track} from 'lib/analytics/analytics'
|
|
||||||
import {SuggestedActorsModel} from './suggested-actors'
|
|
||||||
|
|
||||||
export const OnboardingScreenSteps = {
|
|
||||||
Welcome: 'Welcome',
|
|
||||||
RecommendedFeeds: 'RecommendedFeeds',
|
|
||||||
RecommendedFollows: 'RecommendedFollows',
|
|
||||||
Home: 'Home',
|
|
||||||
} as const
|
|
||||||
|
|
||||||
type OnboardingStep =
|
|
||||||
(typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
|
|
||||||
const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
|
|
||||||
export class OnboardingModel {
|
|
||||||
// state
|
|
||||||
step: OnboardingStep = 'Home' // default state to skip onboarding, only enabled for new users by calling start()
|
|
||||||
|
|
||||||
// data
|
|
||||||
suggestedActors: SuggestedActorsModel
|
|
||||||
|
|
||||||
constructor(public rootStore: RootStoreModel) {
|
|
||||||
this.suggestedActors = new SuggestedActorsModel(this.rootStore)
|
|
||||||
makeAutoObservable(this, {
|
|
||||||
rootStore: false,
|
|
||||||
hydrate: false,
|
|
||||||
serialize: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): unknown {
|
|
||||||
return {
|
|
||||||
step: this.step,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hydrate(v: unknown) {
|
|
||||||
if (typeof v === 'object' && v !== null) {
|
|
||||||
if (
|
|
||||||
hasProp(v, 'step') &&
|
|
||||||
typeof v.step === 'string' &&
|
|
||||||
OnboardingStepsArray.includes(v.step as OnboardingStep)
|
|
||||||
) {
|
|
||||||
this.step = v.step as OnboardingStep
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if there is no valid state, we'll just reset
|
|
||||||
this.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of the next screen in the onboarding process based on the current step or screen name provided.
|
|
||||||
* @param {OnboardingStep} [currentScreenName]
|
|
||||||
* @returns name of next screen in the onboarding process
|
|
||||||
*/
|
|
||||||
next(currentScreenName?: OnboardingStep) {
|
|
||||||
currentScreenName = currentScreenName || this.step
|
|
||||||
if (currentScreenName === 'Welcome') {
|
|
||||||
this.step = 'RecommendedFeeds'
|
|
||||||
return this.step
|
|
||||||
} else if (this.step === 'RecommendedFeeds') {
|
|
||||||
this.step = 'RecommendedFollows'
|
|
||||||
// prefetch recommended follows
|
|
||||||
this.suggestedActors.loadMore(true)
|
|
||||||
return this.step
|
|
||||||
} else if (this.step === 'RecommendedFollows') {
|
|
||||||
this.finish()
|
|
||||||
return this.step
|
|
||||||
} else {
|
|
||||||
// if we get here, we're in an invalid state, let's just go Home
|
|
||||||
return 'Home'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
this.step = 'Welcome'
|
|
||||||
track('Onboarding:Begin')
|
|
||||||
}
|
|
||||||
|
|
||||||
finish() {
|
|
||||||
this.rootStore.me.mainFeed.refresh() // load the selected content
|
|
||||||
this.step = 'Home'
|
|
||||||
track('Onboarding:Complete')
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.step = 'Welcome'
|
|
||||||
track('Onboarding:Reset')
|
|
||||||
}
|
|
||||||
|
|
||||||
skip() {
|
|
||||||
this.step = 'Home'
|
|
||||||
track('Onboarding:Skipped')
|
|
||||||
}
|
|
||||||
|
|
||||||
get isComplete() {
|
|
||||||
return this.step === 'Home'
|
|
||||||
}
|
|
||||||
|
|
||||||
get isActive() {
|
|
||||||
return !this.isComplete
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -27,7 +27,6 @@ import {logger} from '#/logger'
|
||||||
// remove after backend testing finishes
|
// remove after backend testing finishes
|
||||||
// -prf
|
// -prf
|
||||||
import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header'
|
import {applyDebugHeader} from 'lib/api/debug-appview-proxy-header'
|
||||||
import {OnboardingModel} from './discovery/onboarding'
|
|
||||||
|
|
||||||
export const appInfo = z.object({
|
export const appInfo = z.object({
|
||||||
build: z.string(),
|
build: z.string(),
|
||||||
|
@ -44,7 +43,6 @@ export class RootStoreModel {
|
||||||
shell = new ShellUiModel(this)
|
shell = new ShellUiModel(this)
|
||||||
preferences = new PreferencesModel(this)
|
preferences = new PreferencesModel(this)
|
||||||
me = new MeModel(this)
|
me = new MeModel(this)
|
||||||
onboarding = new OnboardingModel(this)
|
|
||||||
invitedUsers = new InvitedUsers(this)
|
invitedUsers = new InvitedUsers(this)
|
||||||
handleResolutions = new HandleResolutionsCache()
|
handleResolutions = new HandleResolutionsCache()
|
||||||
profiles = new ProfilesCache(this)
|
profiles = new ProfilesCache(this)
|
||||||
|
@ -71,7 +69,6 @@ export class RootStoreModel {
|
||||||
appInfo: this.appInfo,
|
appInfo: this.appInfo,
|
||||||
session: this.session.serialize(),
|
session: this.session.serialize(),
|
||||||
me: this.me.serialize(),
|
me: this.me.serialize(),
|
||||||
onboarding: this.onboarding.serialize(),
|
|
||||||
preferences: this.preferences.serialize(),
|
preferences: this.preferences.serialize(),
|
||||||
invitedUsers: this.invitedUsers.serialize(),
|
invitedUsers: this.invitedUsers.serialize(),
|
||||||
mutedThreads: this.mutedThreads.serialize(),
|
mutedThreads: this.mutedThreads.serialize(),
|
||||||
|
@ -89,9 +86,6 @@ export class RootStoreModel {
|
||||||
if (hasProp(v, 'me')) {
|
if (hasProp(v, 'me')) {
|
||||||
this.me.hydrate(v.me)
|
this.me.hydrate(v.me)
|
||||||
}
|
}
|
||||||
if (hasProp(v, 'onboarding')) {
|
|
||||||
this.onboarding.hydrate(v.onboarding)
|
|
||||||
}
|
|
||||||
if (hasProp(v, 'session')) {
|
if (hasProp(v, 'session')) {
|
||||||
this.session.hydrate(v.session)
|
this.session.hydrate(v.session)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import {cleanError} from 'lib/strings/errors'
|
||||||
import {getAge} from 'lib/strings/time'
|
import {getAge} from 'lib/strings/time'
|
||||||
import {track} from 'lib/analytics/analytics'
|
import {track} from 'lib/analytics/analytics'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
|
import {DispatchContext as OnboardingDispatchContext} from '#/state/shell/onboarding'
|
||||||
|
|
||||||
const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
|
const DEFAULT_DATE = new Date(Date.now() - 60e3 * 60 * 24 * 365 * 20) // default to 20 years ago
|
||||||
|
|
||||||
|
@ -90,7 +91,7 @@ export class CreateAccountModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
async submit(onboardingDispatch: OnboardingDispatchContext) {
|
||||||
if (!this.email) {
|
if (!this.email) {
|
||||||
this.setStep(2)
|
this.setStep(2)
|
||||||
return this.setError('Please enter your email.')
|
return this.setError('Please enter your email.')
|
||||||
|
@ -111,7 +112,7 @@ export class CreateAccountModel {
|
||||||
this.setIsProcessing(true)
|
this.setIsProcessing(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.rootStore.onboarding.start() // start now to avoid flashing the wrong view
|
onboardingDispatch({type: 'start'}) // start now to avoid flashing the wrong view
|
||||||
await this.rootStore.session.createAccount({
|
await this.rootStore.session.createAccount({
|
||||||
service: this.serviceUrl,
|
service: this.serviceUrl,
|
||||||
email: this.email,
|
email: this.email,
|
||||||
|
@ -122,7 +123,7 @@ export class CreateAccountModel {
|
||||||
/* dont await */ this.rootStore.preferences.setBirthDate(this.birthDate)
|
/* dont await */ this.rootStore.preferences.setBirthDate(this.birthDate)
|
||||||
track('Create Account')
|
track('Create Account')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
this.rootStore.onboarding.skip() // undo starting the onboard
|
onboardingDispatch({type: 'skip'}) // undo starting the onboard
|
||||||
let errMsg = e.toString()
|
let errMsg = e.toString()
|
||||||
if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
|
if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) {
|
||||||
errMsg =
|
errMsg =
|
||||||
|
|
|
@ -7,9 +7,9 @@ const accountSchema = z.object({
|
||||||
did: z.string(),
|
did: z.string(),
|
||||||
refreshJwt: z.string().optional(),
|
refreshJwt: z.string().optional(),
|
||||||
accessJwt: z.string().optional(),
|
accessJwt: z.string().optional(),
|
||||||
handle: z.string(),
|
handle: z.string().optional(),
|
||||||
displayName: z.string(),
|
displayName: z.string().optional(),
|
||||||
aviUrl: z.string(),
|
aviUrl: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const schema = z.object({
|
export const schema = z.object({
|
||||||
|
|
|
@ -27,7 +27,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
setState(persisted.get('colorMode'))
|
setState(persisted.get('colorMode'))
|
||||||
updateDocument(persisted.get('colorMode'))
|
updateDocument(persisted.get('colorMode'))
|
||||||
})
|
})
|
||||||
}, [setStateWrapped])
|
}, [setState])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<stateContext.Provider value={state}>
|
<stateContext.Provider value={state}>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {Provider as DrawerSwipableProvider} from './drawer-swipe-disabled'
|
||||||
import {Provider as MinimalModeProvider} from './minimal-mode'
|
import {Provider as MinimalModeProvider} from './minimal-mode'
|
||||||
import {Provider as ColorModeProvider} from './color-mode'
|
import {Provider as ColorModeProvider} from './color-mode'
|
||||||
import {Provider as AltTextRequiredProvider} from './alt-text-required'
|
import {Provider as AltTextRequiredProvider} from './alt-text-required'
|
||||||
|
import {Provider as OnboardingProvider} from './onboarding'
|
||||||
|
|
||||||
export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
|
export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
|
||||||
export {
|
export {
|
||||||
|
@ -16,6 +17,7 @@ export {
|
||||||
useRequireAltTextEnabled,
|
useRequireAltTextEnabled,
|
||||||
useSetRequireAltTextEnabled,
|
useSetRequireAltTextEnabled,
|
||||||
} from './alt-text-required'
|
} from './alt-text-required'
|
||||||
|
export {useOnboardingState, useOnboardingDispatch} from './onboarding'
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
return (
|
return (
|
||||||
|
@ -23,7 +25,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
<DrawerSwipableProvider>
|
<DrawerSwipableProvider>
|
||||||
<MinimalModeProvider>
|
<MinimalModeProvider>
|
||||||
<ColorModeProvider>
|
<ColorModeProvider>
|
||||||
<AltTextRequiredProvider>{children}</AltTextRequiredProvider>
|
<OnboardingProvider>
|
||||||
|
<AltTextRequiredProvider>{children}</AltTextRequiredProvider>
|
||||||
|
</OnboardingProvider>
|
||||||
</ColorModeProvider>
|
</ColorModeProvider>
|
||||||
</MinimalModeProvider>
|
</MinimalModeProvider>
|
||||||
</DrawerSwipableProvider>
|
</DrawerSwipableProvider>
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
import React from 'react'
|
||||||
|
import * as persisted from '#/state/persisted'
|
||||||
|
import {track} from '#/lib/analytics/analytics'
|
||||||
|
|
||||||
|
export const OnboardingScreenSteps = {
|
||||||
|
Welcome: 'Welcome',
|
||||||
|
RecommendedFeeds: 'RecommendedFeeds',
|
||||||
|
RecommendedFollows: 'RecommendedFollows',
|
||||||
|
Home: 'Home',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type OnboardingStep =
|
||||||
|
(typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
|
||||||
|
const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {type: 'set'; step: OnboardingStep}
|
||||||
|
| {type: 'next'; currentStep?: OnboardingStep}
|
||||||
|
| {type: 'start'}
|
||||||
|
| {type: 'finish'}
|
||||||
|
| {type: 'skip'}
|
||||||
|
|
||||||
|
export type StateContext = persisted.Schema['onboarding'] & {
|
||||||
|
isComplete: boolean
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
export type DispatchContext = (action: Action) => void
|
||||||
|
|
||||||
|
const stateContext = React.createContext<StateContext>(
|
||||||
|
compute(persisted.defaults.onboarding),
|
||||||
|
)
|
||||||
|
const dispatchContext = React.createContext<DispatchContext>((_: Action) => {})
|
||||||
|
|
||||||
|
function reducer(state: StateContext, action: Action): StateContext {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'set': {
|
||||||
|
if (OnboardingStepsArray.includes(action.step)) {
|
||||||
|
persisted.write('onboarding', {step: action.step})
|
||||||
|
return compute({...state, step: action.step})
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
case 'next': {
|
||||||
|
const currentStep = action.currentStep || state.step
|
||||||
|
let nextStep = 'Home'
|
||||||
|
if (currentStep === 'Welcome') {
|
||||||
|
nextStep = 'RecommendedFeeds'
|
||||||
|
} else if (currentStep === 'RecommendedFeeds') {
|
||||||
|
nextStep = 'RecommendedFollows'
|
||||||
|
} else if (currentStep === 'RecommendedFollows') {
|
||||||
|
nextStep = 'Home'
|
||||||
|
}
|
||||||
|
persisted.write('onboarding', {step: nextStep})
|
||||||
|
return compute({...state, step: nextStep})
|
||||||
|
}
|
||||||
|
case 'start': {
|
||||||
|
track('Onboarding:Begin')
|
||||||
|
persisted.write('onboarding', {step: 'Welcome'})
|
||||||
|
return compute({...state, step: 'Welcome'})
|
||||||
|
}
|
||||||
|
case 'finish': {
|
||||||
|
track('Onboarding:Complete')
|
||||||
|
persisted.write('onboarding', {step: 'Home'})
|
||||||
|
return compute({...state, step: 'Home'})
|
||||||
|
}
|
||||||
|
case 'skip': {
|
||||||
|
track('Onboarding:Skipped')
|
||||||
|
persisted.write('onboarding', {step: 'Home'})
|
||||||
|
return compute({...state, step: 'Home'})
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw new Error('Invalid action')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const [state, dispatch] = React.useReducer(
|
||||||
|
reducer,
|
||||||
|
compute(persisted.get('onboarding')),
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return persisted.onUpdate(() => {
|
||||||
|
dispatch({
|
||||||
|
type: 'set',
|
||||||
|
step: persisted.get('onboarding').step as OnboardingStep,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, [dispatch])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<stateContext.Provider value={state}>
|
||||||
|
<dispatchContext.Provider value={dispatch}>
|
||||||
|
{children}
|
||||||
|
</dispatchContext.Provider>
|
||||||
|
</stateContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOnboardingState() {
|
||||||
|
return React.useContext(stateContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useOnboardingDispatch() {
|
||||||
|
return React.useContext(dispatchContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOnboardingActive() {
|
||||||
|
return compute(persisted.get('onboarding')).isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
function compute(state: persisted.Schema['onboarding']): StateContext {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isActive: state.step !== 'Home',
|
||||||
|
isComplete: state.step === 'Home',
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,34 +4,35 @@ import {observer} from 'mobx-react-lite'
|
||||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||||
import {s} from 'lib/styles'
|
import {s} from 'lib/styles'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useStores} from 'state/index'
|
|
||||||
import {Welcome} from './onboarding/Welcome'
|
import {Welcome} from './onboarding/Welcome'
|
||||||
import {RecommendedFeeds} from './onboarding/RecommendedFeeds'
|
import {RecommendedFeeds} from './onboarding/RecommendedFeeds'
|
||||||
import {RecommendedFollows} from './onboarding/RecommendedFollows'
|
import {RecommendedFollows} from './onboarding/RecommendedFollows'
|
||||||
import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
|
import {useSetMinimalShellMode} from '#/state/shell/minimal-mode'
|
||||||
|
import {useOnboardingState, useOnboardingDispatch} from '#/state/shell'
|
||||||
|
|
||||||
export const Onboarding = observer(function OnboardingImpl() {
|
export const Onboarding = observer(function OnboardingImpl() {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
|
const onboardingState = useOnboardingState()
|
||||||
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setMinimalShellMode(true)
|
setMinimalShellMode(true)
|
||||||
}, [setMinimalShellMode])
|
}, [setMinimalShellMode])
|
||||||
|
|
||||||
const next = () => store.onboarding.next()
|
const next = () => onboardingDispatch({type: 'next'})
|
||||||
const skip = () => store.onboarding.skip()
|
const skip = () => onboardingDispatch({type: 'skip'})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView testID="onboardingView" style={[s.hContentRegion, pal.view]}>
|
<SafeAreaView testID="onboardingView" style={[s.hContentRegion, pal.view]}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{store.onboarding.step === 'Welcome' && (
|
{onboardingState.step === 'Welcome' && (
|
||||||
<Welcome skip={skip} next={next} />
|
<Welcome skip={skip} next={next} />
|
||||||
)}
|
)}
|
||||||
{store.onboarding.step === 'RecommendedFeeds' && (
|
{onboardingState.step === 'RecommendedFeeds' && (
|
||||||
<RecommendedFeeds next={next} />
|
<RecommendedFeeds next={next} />
|
||||||
)}
|
)}
|
||||||
{store.onboarding.step === 'RecommendedFollows' && (
|
{onboardingState.step === 'RecommendedFollows' && (
|
||||||
<RecommendedFollows next={next} />
|
<RecommendedFollows next={next} />
|
||||||
)}
|
)}
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {s} from 'lib/styles'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {CreateAccountModel} from 'state/models/ui/create-account'
|
import {CreateAccountModel} from 'state/models/ui/create-account'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
|
||||||
import {Step1} from './Step1'
|
import {Step1} from './Step1'
|
||||||
import {Step2} from './Step2'
|
import {Step2} from './Step2'
|
||||||
|
@ -29,6 +30,7 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const model = React.useMemo(() => new CreateAccountModel(store), [store])
|
const model = React.useMemo(() => new CreateAccountModel(store), [store])
|
||||||
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
screen('CreateAccount')
|
screen('CreateAccount')
|
||||||
|
@ -59,14 +61,14 @@ export const CreateAccount = observer(function CreateAccountImpl({
|
||||||
model.next()
|
model.next()
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
await model.submit()
|
await model.submit(onboardingDispatch)
|
||||||
} catch {
|
} catch {
|
||||||
// dont need to handle here
|
// dont need to handle here
|
||||||
} finally {
|
} finally {
|
||||||
track('Try Create Account')
|
track('Try Create Account')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [model, track])
|
}, [model, track, onboardingDispatch])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoggedOutLayout
|
<LoggedOutLayout
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {useStores} from 'state/index'
|
import {useStores} from 'state/index'
|
||||||
import {RecommendedFollowsItem} from './RecommendedFollowsItem'
|
import {RecommendedFollowsItem} from './RecommendedFollowsItem'
|
||||||
|
import {SuggestedActorsModel} from '#/state/models/discovery/suggested-actors'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
next: () => void
|
next: () => void
|
||||||
|
@ -21,16 +22,10 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {isTabletOrMobile} = useWebMediaQueries()
|
const {isTabletOrMobile} = useWebMediaQueries()
|
||||||
|
const suggestedActors = React.useMemo(() => {
|
||||||
React.useEffect(() => {
|
const model = new SuggestedActorsModel(store)
|
||||||
// Load suggested actors if not already loaded
|
model.refresh()
|
||||||
// prefetch should happen in the onboarding model
|
return model
|
||||||
if (
|
|
||||||
!store.onboarding.suggestedActors.hasLoaded ||
|
|
||||||
store.onboarding.suggestedActors.isEmpty
|
|
||||||
) {
|
|
||||||
store.onboarding.suggestedActors.loadMore(true)
|
|
||||||
}
|
|
||||||
}, [store])
|
}, [store])
|
||||||
|
|
||||||
const title = (
|
const title = (
|
||||||
|
@ -98,13 +93,19 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({
|
||||||
horizontal
|
horizontal
|
||||||
titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
|
titleStyle={isTabletOrMobile ? undefined : {minWidth: 470}}
|
||||||
contentStyle={{paddingHorizontal: 0}}>
|
contentStyle={{paddingHorizontal: 0}}>
|
||||||
{store.onboarding.suggestedActors.isLoading ? (
|
{suggestedActors.isLoading ? (
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={store.onboarding.suggestedActors.suggestions}
|
data={suggestedActors.suggestions}
|
||||||
renderItem={({item, index}) => (
|
renderItem={({item, index}) => (
|
||||||
<RecommendedFollowsItem item={item} index={index} />
|
<RecommendedFollowsItem
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
insertSuggestionsByActor={suggestedActors.insertSuggestionsByActor.bind(
|
||||||
|
suggestedActors,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item, index) => item.did + index.toString()}
|
keyExtractor={(item, index) => item.did + index.toString()}
|
||||||
style={{flex: 1}}
|
style={{flex: 1}}
|
||||||
|
@ -126,13 +127,19 @@ export const RecommendedFollows = observer(function RecommendedFollowsImpl({
|
||||||
users.
|
users.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{store.onboarding.suggestedActors.isLoading ? (
|
{suggestedActors.isLoading ? (
|
||||||
<ActivityIndicator size="large" />
|
<ActivityIndicator size="large" />
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={store.onboarding.suggestedActors.suggestions}
|
data={suggestedActors.suggestions}
|
||||||
renderItem={({item, index}) => (
|
renderItem={({item, index}) => (
|
||||||
<RecommendedFollowsItem item={item} index={index} />
|
<RecommendedFollowsItem
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
insertSuggestionsByActor={suggestedActors.insertSuggestionsByActor.bind(
|
||||||
|
suggestedActors,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item, index) => item.did + index.toString()}
|
keyExtractor={(item, index) => item.did + index.toString()}
|
||||||
style={{flex: 1}}
|
style={{flex: 1}}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, {useMemo} from 'react'
|
import React from 'react'
|
||||||
import {View, StyleSheet, ActivityIndicator} from 'react-native'
|
import {View, StyleSheet, ActivityIndicator} from 'react-native'
|
||||||
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
|
||||||
import {observer} from 'mobx-react-lite'
|
import {observer} from 'mobx-react-lite'
|
||||||
|
@ -18,22 +18,19 @@ import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
type Props = {
|
type Props = {
|
||||||
item: SuggestedActor
|
item: SuggestedActor
|
||||||
index: number
|
index: number
|
||||||
|
insertSuggestionsByActor: (did: string, index: number) => Promise<void>
|
||||||
}
|
}
|
||||||
export const RecommendedFollowsItem: React.FC<Props> = ({item, index}) => {
|
export const RecommendedFollowsItem: React.FC<Props> = ({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
insertSuggestionsByActor,
|
||||||
|
}) => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const store = useStores()
|
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const delay = useMemo(() => {
|
|
||||||
return (
|
|
||||||
50 *
|
|
||||||
(Math.abs(store.onboarding.suggestedActors.lastInsertedAtIndex - index) %
|
|
||||||
5)
|
|
||||||
)
|
|
||||||
}, [index, store.onboarding.suggestedActors.lastInsertedAtIndex])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeInRight.delay(delay).springify()}
|
entering={FadeInRight}
|
||||||
style={[
|
style={[
|
||||||
styles.cardContainer,
|
styles.cardContainer,
|
||||||
pal.view,
|
pal.view,
|
||||||
|
@ -43,7 +40,12 @@ export const RecommendedFollowsItem: React.FC<Props> = ({item, index}) => {
|
||||||
borderRightWidth: isMobile ? undefined : 1,
|
borderRightWidth: isMobile ? undefined : 1,
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<ProfileCard key={item.did} profile={item} index={index} />
|
<ProfileCard
|
||||||
|
key={item.did}
|
||||||
|
profile={item}
|
||||||
|
index={index}
|
||||||
|
insertSuggestionsByActor={insertSuggestionsByActor}
|
||||||
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -51,9 +53,11 @@ export const RecommendedFollowsItem: React.FC<Props> = ({item, index}) => {
|
||||||
export const ProfileCard = observer(function ProfileCardImpl({
|
export const ProfileCard = observer(function ProfileCardImpl({
|
||||||
profile,
|
profile,
|
||||||
index,
|
index,
|
||||||
|
insertSuggestionsByActor,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
index: number
|
index: number
|
||||||
|
insertSuggestionsByActor: (did: string, index: number) => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
const {track} = useAnalytics()
|
const {track} = useAnalytics()
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
@ -94,10 +98,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
|
||||||
onToggleFollow={async isFollow => {
|
onToggleFollow={async isFollow => {
|
||||||
if (isFollow) {
|
if (isFollow) {
|
||||||
setAddingMoreSuggestions(true)
|
setAddingMoreSuggestions(true)
|
||||||
await store.onboarding.suggestedActors.insertSuggestionsByActor(
|
await insertSuggestionsByActor(profile.did, index)
|
||||||
profile.did,
|
|
||||||
index,
|
|
||||||
)
|
|
||||||
setAddingMoreSuggestions(false)
|
setAddingMoreSuggestions(false)
|
||||||
track('Onboarding:SuggestedFollowFollowed')
|
track('Onboarding:SuggestedFollowFollowed')
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,19 +13,21 @@ import {Onboarding} from './Onboarding'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '../util/text/Text'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {STATUS_PAGE_URL} from 'lib/constants'
|
import {STATUS_PAGE_URL} from 'lib/constants'
|
||||||
|
import {useOnboardingState} from '#/state/shell'
|
||||||
|
|
||||||
export const withAuthRequired = <P extends object>(
|
export const withAuthRequired = <P extends object>(
|
||||||
Component: React.ComponentType<P>,
|
Component: React.ComponentType<P>,
|
||||||
): React.FC<P> =>
|
): React.FC<P> =>
|
||||||
observer(function AuthRequired(props: P) {
|
observer(function AuthRequired(props: P) {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
|
const onboardingState = useOnboardingState()
|
||||||
if (store.session.isResumingSession) {
|
if (store.session.isResumingSession) {
|
||||||
return <Loading />
|
return <Loading />
|
||||||
}
|
}
|
||||||
if (!store.session.hasSession) {
|
if (!store.session.hasSession) {
|
||||||
return <LoggedOut />
|
return <LoggedOut />
|
||||||
}
|
}
|
||||||
if (store.onboarding.isActive) {
|
if (onboardingState.isActive) {
|
||||||
return <Onboarding />
|
return <Onboarding />
|
||||||
}
|
}
|
||||||
return <Component {...props} />
|
return <Component {...props} />
|
||||||
|
|
|
@ -52,6 +52,7 @@ import {
|
||||||
useSetColorMode,
|
useSetColorMode,
|
||||||
useRequireAltTextEnabled,
|
useRequireAltTextEnabled,
|
||||||
useSetRequireAltTextEnabled,
|
useSetRequireAltTextEnabled,
|
||||||
|
useOnboardingDispatch,
|
||||||
} from '#/state/shell'
|
} from '#/state/shell'
|
||||||
|
|
||||||
// TEMPORARY (APP-700)
|
// TEMPORARY (APP-700)
|
||||||
|
@ -70,6 +71,7 @@ export const SettingsScreen = withAuthRequired(
|
||||||
const setMinimalShellMode = useSetMinimalShellMode()
|
const setMinimalShellMode = useSetMinimalShellMode()
|
||||||
const requireAltTextEnabled = useRequireAltTextEnabled()
|
const requireAltTextEnabled = useRequireAltTextEnabled()
|
||||||
const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
|
const setRequireAltTextEnabled = useSetRequireAltTextEnabled()
|
||||||
|
const onboardingDispatch = useOnboardingDispatch()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {isMobile} = useWebMediaQueries()
|
const {isMobile} = useWebMediaQueries()
|
||||||
const {screen, track} = useAnalytics()
|
const {screen, track} = useAnalytics()
|
||||||
|
@ -157,9 +159,9 @@ export const SettingsScreen = withAuthRequired(
|
||||||
}, [store])
|
}, [store])
|
||||||
|
|
||||||
const onPressResetOnboarding = React.useCallback(async () => {
|
const onPressResetOnboarding = React.useCallback(async () => {
|
||||||
store.onboarding.reset()
|
onboardingDispatch({type: 'start'})
|
||||||
Toast.show('Onboarding reset')
|
Toast.show('Onboarding reset')
|
||||||
}, [store])
|
}, [onboardingDispatch])
|
||||||
|
|
||||||
const onPressBuildInfo = React.useCallback(() => {
|
const onPressBuildInfo = React.useCallback(() => {
|
||||||
Clipboard.setString(
|
Clipboard.setString(
|
||||||
|
|
|
@ -17,12 +17,17 @@ import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {useAuxClick} from 'lib/hooks/useAuxClick'
|
import {useAuxClick} from 'lib/hooks/useAuxClick'
|
||||||
import {useIsDrawerOpen, useSetDrawerOpen} from '#/state/shell'
|
import {
|
||||||
|
useIsDrawerOpen,
|
||||||
|
useSetDrawerOpen,
|
||||||
|
useOnboardingState,
|
||||||
|
} from '#/state/shell'
|
||||||
|
|
||||||
const ShellInner = observer(function ShellInnerImpl() {
|
const ShellInner = observer(function ShellInnerImpl() {
|
||||||
const store = useStores()
|
const store = useStores()
|
||||||
const isDrawerOpen = useIsDrawerOpen()
|
const isDrawerOpen = useIsDrawerOpen()
|
||||||
const setDrawerOpen = useSetDrawerOpen()
|
const setDrawerOpen = useSetDrawerOpen()
|
||||||
|
const onboardingState = useOnboardingState()
|
||||||
const {isDesktop, isMobile} = useWebMediaQueries()
|
const {isDesktop, isMobile} = useWebMediaQueries()
|
||||||
const navigator = useNavigation<NavigationProp>()
|
const navigator = useNavigation<NavigationProp>()
|
||||||
useAuxClick()
|
useAuxClick()
|
||||||
|
@ -34,9 +39,9 @@ const ShellInner = observer(function ShellInnerImpl() {
|
||||||
})
|
})
|
||||||
}, [navigator, store.shell, setDrawerOpen])
|
}, [navigator, store.shell, setDrawerOpen])
|
||||||
|
|
||||||
const showBottomBar = isMobile && !store.onboarding.isActive
|
const showBottomBar = isMobile && !onboardingState.isActive
|
||||||
const showSideNavs =
|
const showSideNavs =
|
||||||
!isMobile && store.session.hasSession && !store.onboarding.isActive
|
!isMobile && store.session.hasSession && !onboardingState.isActive
|
||||||
return (
|
return (
|
||||||
<View style={[s.hContentRegion, {overflow: 'hidden'}]}>
|
<View style={[s.hContentRegion, {overflow: 'hidden'}]}>
|
||||||
<View style={s.hContentRegion}>
|
<View style={s.hContentRegion}>
|
||||||
|
|
Loading…
Reference in New Issue