Bluesky app fork with some witchin' additions 馃挮
at main 244 lines 7.9 kB view raw
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}