forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React, {useMemo} from 'react'
2import {msg} from '@lingui/macro'
3import {useLingui} from '@lingui/react'
4
5import {
6 ProgressGuideToast,
7 type ProgressGuideToastRef,
8} from '#/components/ProgressGuide/Toast'
9import {useAnalytics} from '#/analytics'
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 ax = useAnalytics()
75 const {_} = useLingui()
76 const {data: preferences} = usePreferencesQuery()
77 const {mutateAsync, variables, isPending} =
78 useSetActiveProgressGuideMutation()
79
80 const activeProgressGuide = useMemo(() => {
81 const rawProgressGuide = (
82 isPending ? variables : preferences?.bskyAppState?.activeProgressGuide
83 ) as ProgressGuide
84
85 if (!rawProgressGuide) return undefined
86
87 // ensure the unspecced attributes have the correct types
88 // clone then mutate
89 const {...maybeWronglyTypedProgressGuide} = rawProgressGuide
90 if (maybeWronglyTypedProgressGuide?.guide === 'like-10-and-follow-7') {
91 maybeWronglyTypedProgressGuide.numLikes =
92 Number(maybeWronglyTypedProgressGuide.numLikes) || 0
93 maybeWronglyTypedProgressGuide.numFollows =
94 Number(maybeWronglyTypedProgressGuide.numFollows) || 0
95 } else if (maybeWronglyTypedProgressGuide?.guide === 'follow-10') {
96 maybeWronglyTypedProgressGuide.numFollows =
97 Number(maybeWronglyTypedProgressGuide.numFollows) || 0
98 }
99
100 return maybeWronglyTypedProgressGuide
101 }, [isPending, variables, preferences])
102
103 const [localGuideState, setLocalGuideState] =
104 React.useState<ProgressGuide>(undefined)
105
106 if (activeProgressGuide && !localGuideState) {
107 // hydrate from the server if needed
108 setLocalGuideState(activeProgressGuide)
109 }
110
111 const firstLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
112 const fifthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
113 const tenthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
114
115 const fifthFollowToastRef = React.useRef<ProgressGuideToastRef | null>(null)
116 const tenthFollowToastRef = React.useRef<ProgressGuideToastRef | null>(null)
117
118 const controls = React.useMemo(() => {
119 return {
120 startProgressGuide(guide: ProgressGuideName) {
121 if (guide === 'like-10-and-follow-7') {
122 const guideObj = {
123 guide: 'like-10-and-follow-7',
124 numLikes: 0,
125 numFollows: 0,
126 isComplete: false,
127 } satisfies ProgressGuide
128 setLocalGuideState(guideObj)
129 mutateAsync(guideObj)
130 } else if (guide === 'follow-10') {
131 const guideObj = {
132 guide: 'follow-10',
133 numFollows: 0,
134 isComplete: false,
135 } satisfies ProgressGuide
136 setLocalGuideState(guideObj)
137 mutateAsync(guideObj)
138 }
139 },
140
141 endProgressGuide() {
142 setLocalGuideState(undefined)
143 mutateAsync(undefined)
144 ax.metric('progressGuide:hide', {})
145 },
146
147 captureAction(action: ProgressGuideAction, count = 1) {
148 let guide = activeProgressGuide
149 if (!guide || guide?.isComplete) {
150 return
151 }
152 if (guide?.guide === 'like-10-and-follow-7') {
153 if (action === ProgressGuideAction.Like) {
154 guide = {
155 ...guide,
156 numLikes: (Number(guide.numLikes) || 0) + count,
157 }
158 if (guide.numLikes === 1) {
159 firstLikeToastRef.current?.open()
160 }
161 if (guide.numLikes === 5) {
162 fifthLikeToastRef.current?.open()
163 }
164 if (guide.numLikes === 10) {
165 tenthLikeToastRef.current?.open()
166 }
167 }
168 if (action === ProgressGuideAction.Follow) {
169 guide = {
170 ...guide,
171 numFollows: (Number(guide.numFollows) || 0) + count,
172 }
173 }
174 if (Number(guide.numLikes) >= 10 && Number(guide.numFollows) >= 7) {
175 guide = {
176 ...guide,
177 isComplete: true,
178 }
179 }
180 } else if (guide?.guide === 'follow-10') {
181 if (action === ProgressGuideAction.Follow) {
182 guide = {
183 ...guide,
184 numFollows: (Number(guide.numFollows) || 0) + count,
185 }
186
187 if (guide.numFollows === 5) {
188 fifthFollowToastRef.current?.open()
189 }
190 if (guide.numFollows === 10) {
191 tenthFollowToastRef.current?.open()
192 }
193 }
194 if (Number(guide.numFollows) >= 10) {
195 guide = {
196 ...guide,
197 isComplete: true,
198 }
199 }
200 }
201
202 setLocalGuideState(guide)
203 mutateAsync(guide?.isComplete ? undefined : guide)
204 },
205 }
206 }, [ax, activeProgressGuide, mutateAsync, setLocalGuideState])
207
208 return (
209 <ProgressGuideContext.Provider value={localGuideState}>
210 <ProgressGuideControlContext.Provider value={controls}>
211 {children}
212 {localGuideState?.guide === 'like-10-and-follow-7' && (
213 <>
214 <ProgressGuideToast
215 ref={firstLikeToastRef}
216 title={_(msg`Your first like!`)}
217 subtitle={_(msg`Like 10 skeets to train the Discover feed`)}
218 />
219 <ProgressGuideToast
220 ref={fifthLikeToastRef}
221 title={_(msg`Half way there!`)}
222 subtitle={_(msg`Like 10 skeets to train the Discover feed`)}
223 />
224 <ProgressGuideToast
225 ref={tenthLikeToastRef}
226 title={_(msg`Task complete - 10 likes!`)}
227 subtitle={_(msg`The Discover feed now knows what you like`)}
228 />
229 <ProgressGuideToast
230 ref={fifthFollowToastRef}
231 title={_(msg`Half way there!`)}
232 subtitle={_(msg`Follow 10 accounts`)}
233 />
234 <ProgressGuideToast
235 ref={tenthFollowToastRef}
236 title={_(msg`Task complete - 10 follows!`)}
237 subtitle={_(msg`You've found some people to follow`)}
238 />
239 </>
240 )}
241 </ProgressGuideControlContext.Provider>
242 </ProgressGuideContext.Provider>
243 )
244}