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}