mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {useMemo} from 'react'
2import {msg} from '@lingui/macro'
3import {useLingui} from '@lingui/react'
4
5import {logEvent} from '#/lib/statsig/statsig'
6import {
7 ProgressGuideToast,
8 type ProgressGuideToastRef,
9} from '#/components/ProgressGuide/Toast'
10import {
11 usePreferencesQuery,
12 useSetActiveProgressGuideMutation,
13} from '../queries/preferences'
14
15export enum ProgressGuideAction {
16 Like = 'like',
17 Follow = 'follow',
18}
19
20type ProgressGuideName = 'like-10-and-follow-7' | 'follow-10'
21
22/**
23 * Progress Guides that extend this interface must specify their name in the `guide` field, so it can be used as a discriminated union
24 */
25interface BaseProgressGuide {
26 guide: ProgressGuideName
27 isComplete: boolean
28 [key: string]: any
29}
30
31export interface Like10AndFollow7ProgressGuide extends BaseProgressGuide {
32 guide: 'like-10-and-follow-7'
33 numLikes: number
34 numFollows: number
35}
36
37export interface Follow10ProgressGuide extends BaseProgressGuide {
38 guide: 'follow-10'
39 numFollows: number
40}
41
42export type ProgressGuide =
43 | Like10AndFollow7ProgressGuide
44 | Follow10ProgressGuide
45 | undefined
46
47const ProgressGuideContext = React.createContext<ProgressGuide>(undefined)
48ProgressGuideContext.displayName = 'ProgressGuideContext'
49
50const ProgressGuideControlContext = React.createContext<{
51 startProgressGuide(guide: ProgressGuideName): void
52 endProgressGuide(): void
53 captureAction(action: ProgressGuideAction, count?: number): void
54}>({
55 startProgressGuide: (_guide: ProgressGuideName) => {},
56 endProgressGuide: () => {},
57 captureAction: (_action: ProgressGuideAction, _count = 1) => {},
58})
59ProgressGuideControlContext.displayName = 'ProgressGuideControlContext'
60
61export function useProgressGuide(guide: ProgressGuideName) {
62 const ctx = React.useContext(ProgressGuideContext)
63 if (ctx?.guide === guide) {
64 return ctx
65 }
66 return undefined
67}
68
69export function useProgressGuideControls() {
70 return React.useContext(ProgressGuideControlContext)
71}
72
73export function Provider({children}: React.PropsWithChildren<{}>) {
74 const {_} = useLingui()
75 const {data: preferences} = usePreferencesQuery()
76 const {mutateAsync, variables, isPending} =
77 useSetActiveProgressGuideMutation()
78
79 const activeProgressGuide = useMemo(() => {
80 const rawProgressGuide = (
81 isPending ? variables : preferences?.bskyAppState?.activeProgressGuide
82 ) as ProgressGuide
83
84 if (!rawProgressGuide) return undefined
85
86 // ensure the unspecced attributes have the correct types
87 // clone then mutate
88 const {...maybeWronglyTypedProgressGuide} = rawProgressGuide
89 if (maybeWronglyTypedProgressGuide?.guide === 'like-10-and-follow-7') {
90 maybeWronglyTypedProgressGuide.numLikes =
91 Number(maybeWronglyTypedProgressGuide.numLikes) || 0
92 maybeWronglyTypedProgressGuide.numFollows =
93 Number(maybeWronglyTypedProgressGuide.numFollows) || 0
94 } else if (maybeWronglyTypedProgressGuide?.guide === 'follow-10') {
95 maybeWronglyTypedProgressGuide.numFollows =
96 Number(maybeWronglyTypedProgressGuide.numFollows) || 0
97 }
98
99 return maybeWronglyTypedProgressGuide
100 }, [isPending, variables, preferences])
101
102 const [localGuideState, setLocalGuideState] =
103 React.useState<ProgressGuide>(undefined)
104
105 if (activeProgressGuide && !localGuideState) {
106 // hydrate from the server if needed
107 setLocalGuideState(activeProgressGuide)
108 }
109
110 const firstLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
111 const fifthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
112 const tenthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
113
114 const fifthFollowToastRef = React.useRef<ProgressGuideToastRef | null>(null)
115 const tenthFollowToastRef = React.useRef<ProgressGuideToastRef | null>(null)
116
117 const controls = React.useMemo(() => {
118 return {
119 startProgressGuide(guide: ProgressGuideName) {
120 if (guide === 'like-10-and-follow-7') {
121 const guideObj = {
122 guide: 'like-10-and-follow-7',
123 numLikes: 0,
124 numFollows: 0,
125 isComplete: false,
126 } satisfies ProgressGuide
127 setLocalGuideState(guideObj)
128 mutateAsync(guideObj)
129 } else if (guide === 'follow-10') {
130 const guideObj = {
131 guide: 'follow-10',
132 numFollows: 0,
133 isComplete: false,
134 } satisfies ProgressGuide
135 setLocalGuideState(guideObj)
136 mutateAsync(guideObj)
137 }
138 },
139
140 endProgressGuide() {
141 setLocalGuideState(undefined)
142 mutateAsync(undefined)
143 logEvent('progressGuide:hide', {})
144 },
145
146 captureAction(action: ProgressGuideAction, count = 1) {
147 let guide = activeProgressGuide
148 if (!guide || guide?.isComplete) {
149 return
150 }
151 if (guide?.guide === 'like-10-and-follow-7') {
152 if (action === ProgressGuideAction.Like) {
153 guide = {
154 ...guide,
155 numLikes: (Number(guide.numLikes) || 0) + count,
156 }
157 if (guide.numLikes === 1) {
158 firstLikeToastRef.current?.open()
159 }
160 if (guide.numLikes === 5) {
161 fifthLikeToastRef.current?.open()
162 }
163 if (guide.numLikes === 10) {
164 tenthLikeToastRef.current?.open()
165 }
166 }
167 if (action === ProgressGuideAction.Follow) {
168 guide = {
169 ...guide,
170 numFollows: (Number(guide.numFollows) || 0) + count,
171 }
172 }
173 if (Number(guide.numLikes) >= 10 && Number(guide.numFollows) >= 7) {
174 guide = {
175 ...guide,
176 isComplete: true,
177 }
178 }
179 } else if (guide?.guide === 'follow-10') {
180 if (action === ProgressGuideAction.Follow) {
181 guide = {
182 ...guide,
183 numFollows: (Number(guide.numFollows) || 0) + count,
184 }
185
186 if (guide.numFollows === 5) {
187 fifthFollowToastRef.current?.open()
188 }
189 if (guide.numFollows === 10) {
190 tenthFollowToastRef.current?.open()
191 }
192 }
193 if (Number(guide.numFollows) >= 10) {
194 guide = {
195 ...guide,
196 isComplete: true,
197 }
198 }
199 }
200
201 setLocalGuideState(guide)
202 mutateAsync(guide?.isComplete ? undefined : guide)
203 },
204 }
205 }, [activeProgressGuide, mutateAsync, setLocalGuideState])
206
207 return (
208 <ProgressGuideContext.Provider value={localGuideState}>
209 <ProgressGuideControlContext.Provider value={controls}>
210 {children}
211 {localGuideState?.guide === 'like-10-and-follow-7' && (
212 <>
213 <ProgressGuideToast
214 ref={firstLikeToastRef}
215 title={_(msg`Your first like!`)}
216 subtitle={_(msg`Like 10 posts to train the Discover feed`)}
217 />
218 <ProgressGuideToast
219 ref={fifthLikeToastRef}
220 title={_(msg`Half way there!`)}
221 subtitle={_(msg`Like 10 posts to train the Discover feed`)}
222 />
223 <ProgressGuideToast
224 ref={tenthLikeToastRef}
225 title={_(msg`Task complete - 10 likes!`)}
226 subtitle={_(msg`The Discover feed now knows what you like`)}
227 />
228 <ProgressGuideToast
229 ref={fifthFollowToastRef}
230 title={_(msg`Half way there!`)}
231 subtitle={_(msg`Follow 10 accounts`)}
232 />
233 <ProgressGuideToast
234 ref={tenthFollowToastRef}
235 title={_(msg`Task complete - 10 follows!`)}
236 subtitle={_(msg`You've found some people to follow`)}
237 />
238 </>
239 )}
240 </ProgressGuideControlContext.Provider>
241 </ProgressGuideContext.Provider>
242 )
243}