Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

Select the types of activity you want to include in your feed.

at a876aae44ea07494ebea9727350aa060b81f317b 244 lines 7.9 kB view raw
1import {createContext, useContext, useMemo, useRef, useState} from 'react' 2import {msg} from '@lingui/core/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 = createContext<ProgressGuide>(undefined) 48ProgressGuideContext.displayName = 'ProgressGuideContext' 49 50const ProgressGuideControlContext = 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 = useContext(ProgressGuideContext) 63 if (ctx?.guide === guide) { 64 return ctx 65 } 66 return undefined 67} 68 69export function useProgressGuideControls() { 70 return 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 useState<ProgressGuide>(undefined) 105 106 if (activeProgressGuide && !localGuideState) { 107 // hydrate from the server if needed 108 setLocalGuideState(activeProgressGuide) 109 } 110 111 const firstLikeToastRef = useRef<ProgressGuideToastRef | null>(null) 112 const fifthLikeToastRef = useRef<ProgressGuideToastRef | null>(null) 113 const tenthLikeToastRef = useRef<ProgressGuideToastRef | null>(null) 114 115 const fifthFollowToastRef = useRef<ProgressGuideToastRef | null>(null) 116 const tenthFollowToastRef = useRef<ProgressGuideToastRef | null>(null) 117 118 const controls = 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 posts to train the Discover feed`)} 218 /> 219 <ProgressGuideToast 220 ref={fifthLikeToastRef} 221 title={_(msg`Half way there!`)} 222 subtitle={_(msg`Like 10 posts 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}