New user progress guides (#4716)

* Add the animated checkmark svg

* Add progress guide list and task components

* Add ProgressGuide Toast component

* Implement progress-guide controller

* Add 7 follows to the progress guide

* Wire up action captures

* Wire up progress-guide persistence

* Trigger progress guide on account creation

* Clear the progress guide from storage on complete

* Add progress guide interstitial, put behind gate

* Fix: read progress guide state from prefs

* Some defensive type checks

* Create separate toast for completion

* List tweaks

* Only show on Discover

* Spacing and progress tweaks

* Completely hide when complete

* Capture the progress guide in local state, and only render toasts while guide is active

* Fix: ensure persisted hydrates into local state

* Gate

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
This commit is contained in:
Paul Frazee 2024-07-03 19:05:19 -07:00 committed by GitHub
parent aa7117edb6
commit 0ed99b840d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 721 additions and 22 deletions

View file

@ -0,0 +1,185 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGate} from '#/lib/statsig/statsig'
import {
ProgressGuideToast,
ProgressGuideToastRef,
} from '#/components/ProgressGuide/Toast'
import {
usePreferencesQuery,
useSetActiveProgressGuideMutation,
} from '../queries/preferences'
export enum ProgressGuideAction {
Like = 'like',
Follow = 'follow',
}
type ProgressGuideName = 'like-10-and-follow-7'
interface BaseProgressGuide {
guide: string
isComplete: boolean
[key: string]: any
}
interface Like10AndFollow7ProgressGuide extends BaseProgressGuide {
numLikes: number
numFollows: number
}
type ProgressGuide = Like10AndFollow7ProgressGuide | undefined
const ProgressGuideContext = React.createContext<ProgressGuide>(undefined)
const ProgressGuideControlContext = React.createContext<{
startProgressGuide(guide: ProgressGuideName): void
endProgressGuide(): void
captureAction(action: ProgressGuideAction, count?: number): void
}>({
startProgressGuide: (_guide: ProgressGuideName) => {},
endProgressGuide: () => {},
captureAction: (_action: ProgressGuideAction, _count = 1) => {},
})
export function useProgressGuide(guide: ProgressGuideName) {
const ctx = React.useContext(ProgressGuideContext)
if (ctx?.guide === guide) {
return ctx
}
return undefined
}
export function useProgressGuideControls() {
return React.useContext(ProgressGuideControlContext)
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const {_} = useLingui()
const {data: preferences} = usePreferencesQuery()
const {mutateAsync, variables} = useSetActiveProgressGuideMutation()
const gate = useGate()
const activeProgressGuide = (variables ||
preferences?.bskyAppState?.activeProgressGuide) as ProgressGuide
// ensure the unspecced attributes have the correct types
if (activeProgressGuide?.guide === 'like-10-and-follow-7') {
activeProgressGuide.numLikes = Number(activeProgressGuide.numLikes) || 0
activeProgressGuide.numFollows = Number(activeProgressGuide.numFollows) || 0
}
const [localGuideState, setLocalGuideState] =
React.useState<ProgressGuide>(undefined)
if (activeProgressGuide && !localGuideState) {
// hydrate from the server if needed
setLocalGuideState(activeProgressGuide)
}
const firstLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
const fifthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
const tenthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
const guideCompleteToastRef = React.useRef<ProgressGuideToastRef | null>(null)
const controls = React.useMemo(() => {
return {
startProgressGuide(guide: ProgressGuideName) {
if (!gate('new_user_progress_guide')) {
return
}
if (guide === 'like-10-and-follow-7') {
const guideObj = {
guide: 'like-10-and-follow-7',
numLikes: 0,
numFollows: 0,
isComplete: false,
}
setLocalGuideState(guideObj)
mutateAsync(guideObj)
}
},
endProgressGuide() {
// update the persisted first
mutateAsync(undefined).then(() => {
// now clear local state, to avoid rehydrating from the server
setLocalGuideState(undefined)
})
},
captureAction(action: ProgressGuideAction, count = 1) {
let guide = activeProgressGuide
if (!guide || guide?.isComplete) {
return
}
if (guide?.guide === 'like-10-and-follow-7') {
if (action === ProgressGuideAction.Like) {
guide = {
...guide,
numLikes: (Number(guide.numLikes) || 0) + count,
}
if (guide.numLikes === 1) {
firstLikeToastRef.current?.open()
}
if (guide.numLikes === 5) {
fifthLikeToastRef.current?.open()
}
if (guide.numLikes === 10) {
tenthLikeToastRef.current?.open()
}
}
if (action === ProgressGuideAction.Follow) {
guide = {
...guide,
numFollows: (Number(guide.numFollows) || 0) + count,
}
}
if (Number(guide.numLikes) >= 10 && Number(guide.numFollows) >= 7) {
guide = {
...guide,
isComplete: true,
}
}
}
setLocalGuideState(guide)
mutateAsync(guide?.isComplete ? undefined : guide)
},
}
}, [activeProgressGuide, mutateAsync, gate, setLocalGuideState])
return (
<ProgressGuideContext.Provider value={localGuideState}>
<ProgressGuideControlContext.Provider value={controls}>
{children}
{localGuideState?.guide === 'like-10-and-follow-7' && (
<>
<ProgressGuideToast
ref={firstLikeToastRef}
title={_(msg`Your first like!`)}
subtitle={_(msg`Like 10 posts to train the Discover feed`)}
/>
<ProgressGuideToast
ref={fifthLikeToastRef}
title={_(msg`Half way there!`)}
subtitle={_(msg`Like 10 posts to train the Discover feed`)}
/>
<ProgressGuideToast
ref={tenthLikeToastRef}
title={_(msg`Task complete - 10 likes!`)}
subtitle={_(msg`The Discover feed now knows what you like`)}
/>
<ProgressGuideToast
ref={guideCompleteToastRef}
title={_(msg`Algorithm training complete!`)}
subtitle={_(msg`The Discover feed now knows what you like`)}
/>
</>
)}
</ProgressGuideControlContext.Provider>
</ProgressGuideContext.Provider>
)
}