245 lines
7.7 KiB
TypeScript
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>
|
|
)
|
|
}
|