Files
bsky-social-app/src/state/shell/progress-guide.tsx
T
Samuel Newman 226f7f4fd2 hoist useprefs
2026-03-23 19:17:39 +02:00

245 lines
7.7 KiB
TypeScript

import {createContext, useContext, useMemo, useRef, useState} from 'react'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {
ProgressGuideToast,
type ProgressGuideToastRef,
} from '#/components/ProgressGuide/Toast'
import {useAnalytics} from '#/analytics'
import {
usePreferences,
useSetActiveProgressGuideMutation,
} from '../queries/preferences'
export enum ProgressGuideAction {
Like = 'like',
Follow = 'follow',
}
type ProgressGuideName = 'like-10-and-follow-7' | 'follow-10'
/**
* Progress Guides that extend this interface must specify their name in the `guide` field, so it can be used as a discriminated union
*/
interface BaseProgressGuide {
guide: ProgressGuideName
isComplete: boolean
[key: string]: any
}
export interface Like10AndFollow7ProgressGuide extends BaseProgressGuide {
guide: 'like-10-and-follow-7'
numLikes: number
numFollows: number
}
export interface Follow10ProgressGuide extends BaseProgressGuide {
guide: 'follow-10'
numFollows: number
}
export type ProgressGuide =
| Like10AndFollow7ProgressGuide
| Follow10ProgressGuide
| undefined
const ProgressGuideContext = createContext<ProgressGuide>(undefined)
ProgressGuideContext.displayName = 'ProgressGuideContext'
const ProgressGuideControlContext = createContext<{
startProgressGuide(guide: ProgressGuideName): void
endProgressGuide(): void
captureAction(action: ProgressGuideAction, count?: number): void
}>({
startProgressGuide: (_guide: ProgressGuideName) => {},
endProgressGuide: () => {},
captureAction: (_action: ProgressGuideAction, _count = 1) => {},
})
ProgressGuideControlContext.displayName = 'ProgressGuideControlContext'
export function useProgressGuide(guide: ProgressGuideName) {
const ctx = useContext(ProgressGuideContext)
if (ctx?.guide === guide) {
return ctx
}
return undefined
}
export function useProgressGuideControls() {
return useContext(ProgressGuideControlContext)
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const ax = useAnalytics()
const {_} = useLingui()
const preferences = usePreferences()
const {mutateAsync, variables, isPending} =
useSetActiveProgressGuideMutation()
const activeProgressGuide = useMemo(() => {
const rawProgressGuide = (
isPending ? variables : preferences?.bskyAppState?.activeProgressGuide
) as ProgressGuide
if (!rawProgressGuide) return undefined
// ensure the unspecced attributes have the correct types
// clone then mutate
const {...maybeWronglyTypedProgressGuide} = rawProgressGuide
if (maybeWronglyTypedProgressGuide?.guide === 'like-10-and-follow-7') {
maybeWronglyTypedProgressGuide.numLikes =
Number(maybeWronglyTypedProgressGuide.numLikes) || 0
maybeWronglyTypedProgressGuide.numFollows =
Number(maybeWronglyTypedProgressGuide.numFollows) || 0
} else if (maybeWronglyTypedProgressGuide?.guide === 'follow-10') {
maybeWronglyTypedProgressGuide.numFollows =
Number(maybeWronglyTypedProgressGuide.numFollows) || 0
}
return maybeWronglyTypedProgressGuide
}, [isPending, variables, preferences])
const [localGuideState, setLocalGuideState] =
useState<ProgressGuide>(undefined)
if (activeProgressGuide && !localGuideState) {
// hydrate from the server if needed
setLocalGuideState(activeProgressGuide)
}
const firstLikeToastRef = useRef<ProgressGuideToastRef | null>(null)
const fifthLikeToastRef = useRef<ProgressGuideToastRef | null>(null)
const tenthLikeToastRef = useRef<ProgressGuideToastRef | null>(null)
const fifthFollowToastRef = useRef<ProgressGuideToastRef | null>(null)
const tenthFollowToastRef = useRef<ProgressGuideToastRef | null>(null)
const controls = useMemo(() => {
return {
startProgressGuide(guide: ProgressGuideName) {
if (guide === 'like-10-and-follow-7') {
const guideObj = {
guide: 'like-10-and-follow-7',
numLikes: 0,
numFollows: 0,
isComplete: false,
} satisfies ProgressGuide
setLocalGuideState(guideObj)
mutateAsync(guideObj)
} else if (guide === 'follow-10') {
const guideObj = {
guide: 'follow-10',
numFollows: 0,
isComplete: false,
} satisfies ProgressGuide
setLocalGuideState(guideObj)
mutateAsync(guideObj)
}
},
endProgressGuide() {
setLocalGuideState(undefined)
mutateAsync(undefined)
ax.metric('progressGuide:hide', {})
},
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,
}
}
} else if (guide?.guide === 'follow-10') {
if (action === ProgressGuideAction.Follow) {
guide = {
...guide,
numFollows: (Number(guide.numFollows) || 0) + count,
}
if (guide.numFollows === 5) {
fifthFollowToastRef.current?.open()
}
if (guide.numFollows === 10) {
tenthFollowToastRef.current?.open()
}
}
if (Number(guide.numFollows) >= 10) {
guide = {
...guide,
isComplete: true,
}
}
}
setLocalGuideState(guide)
mutateAsync(guide?.isComplete ? undefined : guide)
},
}
}, [ax, activeProgressGuide, mutateAsync, 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={fifthFollowToastRef}
title={_(msg`Half way there!`)}
subtitle={_(msg`Follow 10 accounts`)}
/>
<ProgressGuideToast
ref={tenthFollowToastRef}
title={_(msg`Task complete - 10 follows!`)}
subtitle={_(msg`You've found some people to follow`)}
/>
</>
)}
</ProgressGuideControlContext.Provider>
</ProgressGuideContext.Provider>
)
}