diff options
author | Paul Frazee <pfrazee@gmail.com> | 2024-07-03 19:05:19 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-04 03:05:19 +0100 |
commit | 0ed99b840d8de13465f010a6434dea50c72b3f62 (patch) | |
tree | 75ebec28653a081793ca0cbca0c428a816980c6a /src/state/shell | |
parent | aa7117edb60711a67464f7559118334185f01680 (diff) | |
download | voidsky-0ed99b840d8de13465f010a6434dea50c72b3f62.tar.zst |
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>
Diffstat (limited to 'src/state/shell')
-rw-r--r-- | src/state/shell/progress-guide.tsx | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/src/state/shell/progress-guide.tsx b/src/state/shell/progress-guide.tsx new file mode 100644 index 000000000..d10d58297 --- /dev/null +++ b/src/state/shell/progress-guide.tsx @@ -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> + ) +} |