Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Guided tour for new users (#4690)

* Add home guided tour (WIP)

* Add web handling of the tour

* Switch to our fork of rn-tourguide

* Bump guided-tour

* Fix alignment on android

* Implement home page tour trigger after account creation

* Add new_user_guided_tour gate

* Add a title line to the tour tooltips

* A11y improvements: proper labels, focus capture, scroll capture

* Silence type error

* Native a11y

* Use FocusScope

* Switch to useWebBodyScrollLock()

---------

Co-authored-by: Eric Bailey <git@esb.lol>

authored by

Paul Frazee
Eric Bailey
and committed by
GitHub
a3d4fb65 6694a336

+541 -39
+1
package.json
··· 193 193 "react-responsive": "^9.0.2", 194 194 "react-textarea-autosize": "^8.5.3", 195 195 "rn-fetch-blob": "^0.12.0", 196 + "rn-tourguide": "bluesky-social/rn-tourguide", 196 197 "sentry-expo": "~7.0.1", 197 198 "statsig-react-native-expo": "^4.6.1", 198 199 "tippy.js": "^6.3.7",
+7 -4
src/App.native.tsx
··· 55 55 import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' 56 56 import {Provider as PortalProvider} from '#/components/Portal' 57 57 import {Splash} from '#/Splash' 58 + import {Provider as TourProvider} from '#/tours' 58 59 import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 59 60 import I18nProvider from './locale/i18nProvider' 60 61 import {listenSessionDropped} from './state/events' ··· 117 118 <UnreadNotifsProvider> 118 119 <BackgroundNotificationPreferencesProvider> 119 120 <MutedThreadsProvider> 120 - <GestureHandlerRootView style={s.h100pct}> 121 - <TestCtrls /> 122 - <Shell /> 123 - </GestureHandlerRootView> 121 + <TourProvider> 122 + <GestureHandlerRootView style={s.h100pct}> 123 + <TestCtrls /> 124 + <Shell /> 125 + </GestureHandlerRootView> 126 + </TourProvider> 124 127 </MutedThreadsProvider> 125 128 </BackgroundNotificationPreferencesProvider> 126 129 </UnreadNotifsProvider>
+4 -1
src/App.web.tsx
··· 43 43 import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 44 44 import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' 45 45 import {Provider as PortalProvider} from '#/components/Portal' 46 + import {Provider as TourProvider} from '#/tours' 46 47 import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 47 48 import I18nProvider from './locale/i18nProvider' 48 49 import {listenSessionDropped} from './state/events' ··· 102 103 <BackgroundNotificationPreferencesProvider> 103 104 <MutedThreadsProvider> 104 105 <SafeAreaProvider> 105 - <Shell /> 106 + <TourProvider> 107 + <Shell /> 108 + </TourProvider> 106 109 </SafeAreaProvider> 107 110 </MutedThreadsProvider> 108 111 </BackgroundNotificationPreferencesProvider>
+1
src/lib/statsig/gates.ts
··· 6 6 | 'request_notifications_permission_after_onboarding_v2' 7 7 | 'show_avi_follow_button' 8 8 | 'show_follow_back_label_v2' 9 + | 'new_user_guided_tour' 9 10 | 'suggested_feeds_interstitial' 10 11 | 'suggested_follows_interstitial'
+4
src/screens/Onboarding/StepFinished.tsx
··· 42 42 import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2' 43 43 import {Loader} from '#/components/Loader' 44 44 import {Text} from '#/components/Typography' 45 + import {TOURS, useSetQueuedTour} from '#/tours' 45 46 46 47 export function StepFinished() { 47 48 const {_} = useLingui() ··· 56 57 const activeStarterPack = useActiveStarterPack() 57 58 const setActiveStarterPack = useSetActiveStarterPack() 58 59 const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() 60 + const setQueuedTour = useSetQueuedTour() 59 61 60 62 const finishOnboarding = React.useCallback(async () => { 61 63 setSaving(true) ··· 182 184 setSaving(false) 183 185 setActiveStarterPack(undefined) 184 186 setHasCheckedForStarterPack(true) 187 + setQueuedTour(TOURS.HOME) 185 188 dispatch({type: 'finish'}) 186 189 onboardDispatch({type: 'finish'}) 187 190 track('OnboardingV2:StepFinished:End') ··· 214 217 requestNotificationsPermission, 215 218 setActiveStarterPack, 216 219 setHasCheckedForStarterPack, 220 + setQueuedTour, 217 221 ]) 218 222 219 223 React.useEffect(() => {
+18
src/tours/Debug.tsx
··· 1 + import React from 'react' 2 + import {useTourGuideController} from 'rn-tourguide' 3 + 4 + import {Button} from '#/components/Button' 5 + import {Text} from '#/components/Typography' 6 + 7 + export function TourDebugButton() { 8 + const {start} = useTourGuideController('home') 9 + return ( 10 + <Button 11 + label="Start tour" 12 + onPress={() => { 13 + start() 14 + }}> 15 + {() => <Text>t</Text>} 16 + </Button> 17 + ) 18 + }
+93
src/tours/HomeTour.tsx
··· 1 + import React from 'react' 2 + import {msg} from '@lingui/macro' 3 + import {useLingui} from '@lingui/react' 4 + import { 5 + IStep, 6 + TourGuideZone, 7 + TourGuideZoneByPosition, 8 + useTourGuideController, 9 + } from 'rn-tourguide' 10 + 11 + import {DISCOVER_FEED_URI} from '#/lib/constants' 12 + import {isWeb} from '#/platform/detection' 13 + import {useSetSelectedFeed} from '#/state/shell/selected-feed' 14 + import {TOURS} from '.' 15 + import {useHeaderPosition} from './positioning' 16 + 17 + export function HomeTour() { 18 + const {_} = useLingui() 19 + const {tourKey, eventEmitter} = useTourGuideController(TOURS.HOME) 20 + const setSelectedFeed = useSetSelectedFeed() 21 + const headerPosition = useHeaderPosition() 22 + 23 + React.useEffect(() => { 24 + const handleOnStepChange = (step?: IStep) => { 25 + if (step?.order === 2) { 26 + setSelectedFeed('following') 27 + } else if (step?.order === 3) { 28 + setSelectedFeed(`feedgen|${DISCOVER_FEED_URI}`) 29 + } 30 + } 31 + eventEmitter?.on('stepChange', handleOnStepChange) 32 + return () => { 33 + eventEmitter?.off('stepChange', handleOnStepChange) 34 + } 35 + }, [eventEmitter, setSelectedFeed]) 36 + 37 + return ( 38 + <> 39 + <TourGuideZoneByPosition 40 + isTourGuide 41 + tourKey={tourKey} 42 + zone={1} 43 + top={headerPosition.top} 44 + left={headerPosition.left} 45 + width={headerPosition.width} 46 + height={headerPosition.height} 47 + borderRadiusObject={headerPosition.borderRadiusObject} 48 + text={_(msg`Switch between feeds to control your experience.`)} 49 + /> 50 + <TourGuideZoneByPosition 51 + isTourGuide 52 + tourKey={tourKey} 53 + zone={2} 54 + top={headerPosition.top} 55 + left={headerPosition.left} 56 + width={headerPosition.width} 57 + height={headerPosition.height} 58 + borderRadiusObject={headerPosition.borderRadiusObject} 59 + text={_(msg`Following shows the latest posts from people you follow.`)} 60 + /> 61 + <TourGuideZoneByPosition 62 + isTourGuide 63 + tourKey={tourKey} 64 + zone={3} 65 + top={headerPosition.top} 66 + left={headerPosition.left} 67 + width={headerPosition.width} 68 + height={headerPosition.height} 69 + borderRadiusObject={headerPosition.borderRadiusObject} 70 + text={_(msg`Discover learns which posts you like as you browse.`)} 71 + /> 72 + </> 73 + ) 74 + } 75 + 76 + export function HomeTourExploreWrapper({ 77 + children, 78 + }: React.PropsWithChildren<{}>) { 79 + const {_} = useLingui() 80 + const {tourKey} = useTourGuideController(TOURS.HOME) 81 + return ( 82 + <TourGuideZone 83 + tourKey={tourKey} 84 + zone={4} 85 + tooltipBottomOffset={50} 86 + shape={isWeb ? 'rectangle' : 'circle'} 87 + text={_( 88 + msg`Find more feeds and accounts to follow in the Explore page.`, 89 + )}> 90 + {children} 91 + </TourGuideZone> 92 + ) 93 + }
+168
src/tours/Tooltip.tsx
··· 1 + import * as React from 'react' 2 + import { 3 + AccessibilityInfo, 4 + findNodeHandle, 5 + Pressable, 6 + Text as RNText, 7 + View, 8 + } from 'react-native' 9 + import {msg, Trans} from '@lingui/macro' 10 + import {useLingui} from '@lingui/react' 11 + import {FocusScope} from '@tamagui/focus-scope' 12 + import {IStep, Labels} from 'rn-tourguide' 13 + 14 + import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock' 15 + import {useA11y} from '#/state/a11y' 16 + import {Logo} from '#/view/icons/Logo' 17 + import {atoms as a, useTheme} from '#/alf' 18 + import {Button, ButtonText} from '#/components/Button' 19 + import {leading, Text} from '#/components/Typography' 20 + 21 + const stopPropagation = (e: any) => e.stopPropagation() 22 + 23 + export interface TooltipComponentProps { 24 + isFirstStep?: boolean 25 + isLastStep?: boolean 26 + currentStep: IStep 27 + labels?: Labels 28 + handleNext?: () => void 29 + handlePrev?: () => void 30 + handleStop?: () => void 31 + } 32 + 33 + export function TooltipComponent({ 34 + isLastStep, 35 + handleNext, 36 + handleStop, 37 + currentStep, 38 + labels, 39 + }: TooltipComponentProps) { 40 + const t = useTheme() 41 + const {_} = useLingui() 42 + const btnRef = React.useRef<View>(null) 43 + const textRef = React.useRef<RNText>(null) 44 + const {screenReaderEnabled} = useA11y() 45 + useWebBodyScrollLock(true) 46 + 47 + const focusTextNode = () => { 48 + const node = textRef.current ? findNodeHandle(textRef.current) : undefined 49 + if (node) { 50 + AccessibilityInfo.setAccessibilityFocus(node) 51 + } 52 + } 53 + 54 + // handle initial focus immediately on mount 55 + React.useLayoutEffect(() => { 56 + focusTextNode() 57 + }, []) 58 + 59 + // handle focus between steps 60 + const innerHandleNext = () => { 61 + handleNext?.() 62 + setTimeout(() => focusTextNode(), 200) 63 + } 64 + 65 + return ( 66 + <FocusScope loop enabled trapped> 67 + <View 68 + role="alert" 69 + aria-role="alert" 70 + aria-label={_(msg`A help tooltip`)} 71 + accessibilityLiveRegion="polite" 72 + // iOS 73 + accessibilityViewIsModal 74 + // Android 75 + importantForAccessibility="yes" 76 + // @ts-ignore web only 77 + onClick={stopPropagation} 78 + onStartShouldSetResponder={_ => true} 79 + onTouchEnd={stopPropagation} 80 + style={[ 81 + t.atoms.bg, 82 + a.px_lg, 83 + a.py_lg, 84 + a.flex_col, 85 + a.gap_md, 86 + a.rounded_sm, 87 + a.shadow_md, 88 + {maxWidth: 300}, 89 + ]}> 90 + {screenReaderEnabled && ( 91 + <Pressable 92 + style={[ 93 + a.absolute, 94 + a.inset_0, 95 + a.z_10, 96 + {height: 10, bottom: 'auto'}, 97 + ]} 98 + accessibilityLabel={_( 99 + msg`Start of onboarding tour window. Do not move backward. Instead, go forward for more options, or press to skip.`, 100 + )} 101 + accessibilityHint={undefined} 102 + onPress={handleStop} 103 + /> 104 + )} 105 + 106 + <View style={[a.flex_row, a.align_center, a.gap_sm]}> 107 + <Logo width={16} style={{position: 'relative', top: 0}} /> 108 + <Text 109 + accessible={false} 110 + style={[a.text_sm, a.font_semibold, t.atoms.text_contrast_medium]}> 111 + <Trans>Quick tip</Trans> 112 + </Text> 113 + </View> 114 + <RNText 115 + ref={textRef} 116 + testID="stepDescription" 117 + accessibilityLabel={_( 118 + msg`Onboarding tour step ${currentStep.name}: ${currentStep.text}`, 119 + )} 120 + accessibilityHint={undefined} 121 + style={[ 122 + a.text_md, 123 + t.atoms.text, 124 + a.pb_sm, 125 + { 126 + lineHeight: leading(a.text_md, a.leading_snug), 127 + }, 128 + ]}> 129 + {currentStep.text} 130 + </RNText> 131 + {!isLastStep ? ( 132 + <Button 133 + ref={btnRef} 134 + variant="gradient" 135 + color="gradient_sky" 136 + size="medium" 137 + onPress={innerHandleNext} 138 + label={labels?.next || _(msg`Go to the next step of the tour`)}> 139 + <ButtonText>{labels?.next || _(msg`Next`)}</ButtonText> 140 + </Button> 141 + ) : ( 142 + <Button 143 + variant="gradient" 144 + color="gradient_sky" 145 + size="medium" 146 + onPress={handleStop} 147 + label={ 148 + labels?.finish || 149 + _(msg`Finish tour and begin using the application`) 150 + }> 151 + <ButtonText>{labels?.finish || _(msg`Let's go!`)}</ButtonText> 152 + </Button> 153 + )} 154 + 155 + {screenReaderEnabled && ( 156 + <Pressable 157 + style={[a.absolute, a.inset_0, a.z_10, {height: 10, top: 'auto'}]} 158 + accessibilityLabel={_( 159 + msg`End of onboarding tour window. Do not move forward. Instead, go backward for more options, or press to skip.`, 160 + )} 161 + accessibilityHint={undefined} 162 + onPress={handleStop} 163 + /> 164 + )} 165 + </View> 166 + </FocusScope> 167 + ) 168 + }
+62
src/tours/index.tsx
··· 1 + import React from 'react' 2 + import {InteractionManager} from 'react-native' 3 + import {TourGuideProvider, useTourGuideController} from 'rn-tourguide' 4 + 5 + import {useGate} from '#/lib/statsig/statsig' 6 + import {useColorModeTheme} from '#/alf/util/useColorModeTheme' 7 + import {HomeTour} from './HomeTour' 8 + import {TooltipComponent} from './Tooltip' 9 + 10 + export enum TOURS { 11 + HOME = 'home', 12 + } 13 + 14 + type StateContext = TOURS | null 15 + type SetContext = (v: TOURS | null) => void 16 + 17 + const stateContext = React.createContext<StateContext>(null) 18 + const setContext = React.createContext<SetContext>((_: TOURS | null) => {}) 19 + 20 + export function Provider({children}: React.PropsWithChildren<{}>) { 21 + const theme = useColorModeTheme() 22 + const [state, setState] = React.useState<TOURS | null>(() => null) 23 + 24 + return ( 25 + <TourGuideProvider 26 + androidStatusBarVisible 27 + tooltipComponent={TooltipComponent} 28 + backdropColor={ 29 + theme === 'light' ? 'rgba(0, 0, 0, 0.15)' : 'rgba(0, 0, 0, 0.8)' 30 + } 31 + preventOutsideInteraction> 32 + <stateContext.Provider value={state}> 33 + <setContext.Provider value={setState}> 34 + <HomeTour /> 35 + {children} 36 + </setContext.Provider> 37 + </stateContext.Provider> 38 + </TourGuideProvider> 39 + ) 40 + } 41 + 42 + export function useTriggerTourIfQueued(tour: TOURS) { 43 + const {start} = useTourGuideController(tour) 44 + const setQueuedTour = React.useContext(setContext) 45 + const queuedTour = React.useContext(stateContext) 46 + const gate = useGate() 47 + 48 + return React.useCallback(() => { 49 + if (queuedTour === tour) { 50 + setQueuedTour(null) 51 + InteractionManager.runAfterInteractions(() => { 52 + if (gate('new_user_guided_tour')) { 53 + start() 54 + } 55 + }) 56 + } 57 + }, [tour, queuedTour, setQueuedTour, start, gate]) 58 + } 59 + 60 + export function useSetQueuedTour() { 61 + return React.useContext(setContext) 62 + }
+23
src/tours/positioning.ts
··· 1 + import {useWindowDimensions} from 'react-native' 2 + import {useSafeAreaInsets} from 'react-native-safe-area-context' 3 + 4 + import {useShellLayout} from '#/state/shell/shell-layout' 5 + 6 + export function useHeaderPosition() { 7 + const {headerHeight} = useShellLayout() 8 + const {width} = useWindowDimensions() 9 + const insets = useSafeAreaInsets() 10 + 11 + return { 12 + top: insets.top, 13 + left: 10, 14 + width: width - 20, 15 + height: headerHeight.value, 16 + borderRadiusObject: { 17 + topLeft: 4, 18 + topRight: 4, 19 + bottomLeft: 4, 20 + bottomRight: 4, 21 + }, 22 + } 23 + }
+27
src/tours/positioning.web.ts
··· 1 + import {useWindowDimensions} from 'react-native' 2 + 3 + import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 4 + import {useShellLayout} from '#/state/shell/shell-layout' 5 + 6 + export function useHeaderPosition() { 7 + const {headerHeight} = useShellLayout() 8 + const winDim = useWindowDimensions() 9 + const {isMobile} = useWebMediaQueries() 10 + 11 + let left = 0 12 + let width = winDim.width 13 + if (width > 590 && !isMobile) { 14 + left = winDim.width / 2 - 295 15 + width = 590 16 + } 17 + 18 + let offset = isMobile ? 45 : 0 19 + 20 + return { 21 + top: headerHeight.value - offset, 22 + left, 23 + width, 24 + height: 45, 25 + borderRadiusObject: undefined, 26 + } 27 + }
+5 -3
src/view/com/home/HomeHeaderLayoutMobile.tsx
··· 72 72 {width: 100}, 73 73 ]}> 74 74 {IS_DEV && ( 75 - <Link to="/sys/debug"> 76 - <ColorPalette size="md" /> 77 - </Link> 75 + <> 76 + <Link to="/sys/debug"> 77 + <ColorPalette size="md" /> 78 + </Link> 79 + </> 78 80 )} 79 81 {hasSession && ( 80 82 <Link
+9 -1
src/view/screens/Home.tsx
··· 28 28 import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' 29 29 import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' 30 30 import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' 31 + import {TOURS, useTriggerTourIfQueued} from '#/tours' 31 32 import {HomeHeader} from '../com/home/HomeHeader' 32 33 33 34 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'> ··· 86 87 const selectedIndex = Math.max(0, maybeFoundIndex) 87 88 const selectedFeed = allFeeds[selectedIndex] 88 89 const requestNotificationsPermission = useRequestNotificationsPermission() 90 + const triggerTourIfQueued = useTriggerTourIfQueued(TOURS.HOME) 89 91 90 92 useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName) 91 93 useOTAUpdates() ··· 113 115 React.useCallback(() => { 114 116 setMinimalShellMode(false) 115 117 setDrawerSwipeDisabled(selectedIndex > 0) 118 + triggerTourIfQueued() 116 119 return () => { 117 120 setDrawerSwipeDisabled(false) 118 121 } 119 - }, [setDrawerSwipeDisabled, selectedIndex, setMinimalShellMode]), 122 + }, [ 123 + setDrawerSwipeDisabled, 124 + selectedIndex, 125 + setMinimalShellMode, 126 + triggerTourIfQueued, 127 + ]), 120 128 ) 121 129 122 130 useFocusEffect(
+2 -1
src/view/screens/Settings/index.tsx
··· 252 252 }, [clearPreferences]) 253 253 254 254 const onPressResetOnboarding = React.useCallback(async () => { 255 + navigation.navigate('Home') 255 256 onboardingDispatch({type: 'start'}) 256 257 Toast.show(_(msg`Onboarding reset`)) 257 - }, [onboardingDispatch, _]) 258 + }, [navigation, onboardingDispatch, _]) 258 259 259 260 const onPressBuildInfo = React.useCallback(() => { 260 261 setStringAsync(
+14 -11
src/view/shell/bottom-bar/BottomBar.tsx
··· 45 45 Message_Stroke2_Corner0_Rounded as Message, 46 46 Message_Stroke2_Corner0_Rounded_Filled as MessageFilled, 47 47 } from '#/components/icons/Message' 48 + import {HomeTourExploreWrapper} from '#/tours/HomeTour' 48 49 import {styles} from './BottomBarStyles' 49 50 50 51 type TabOptions = ··· 162 163 <Btn 163 164 testID="bottomBarSearchBtn" 164 165 icon={ 165 - isAtSearch ? ( 166 - <MagnifyingGlassFilled 167 - width={iconWidth + 2} 168 - style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 169 - /> 170 - ) : ( 171 - <MagnifyingGlass 172 - width={iconWidth + 2} 173 - style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 174 - /> 175 - ) 166 + <HomeTourExploreWrapper> 167 + {isAtSearch ? ( 168 + <MagnifyingGlassFilled 169 + width={iconWidth + 2} 170 + style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 171 + /> 172 + ) : ( 173 + <MagnifyingGlass 174 + width={iconWidth + 2} 175 + style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 176 + /> 177 + )} 178 + </HomeTourExploreWrapper> 176 179 } 177 180 onPress={onPressSearch} 178 181 accessibilityRole="search"
+7 -4
src/view/shell/bottom-bar/BottomBarWeb.tsx
··· 41 41 UserCircle_Filled_Corner0_Rounded as UserCircleFilled, 42 42 UserCircle_Stroke2_Corner0_Rounded as UserCircle, 43 43 } from '#/components/icons/UserCircle' 44 + import {HomeTourExploreWrapper} from '#/tours/HomeTour' 44 45 import {styles} from './BottomBarStyles' 45 46 46 47 export function BottomBarWeb() { ··· 94 95 {({isActive}) => { 95 96 const Icon = isActive ? MagnifyingGlassFilled : MagnifyingGlass 96 97 return ( 97 - <Icon 98 - width={iconWidth + 2} 99 - style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 100 - /> 98 + <HomeTourExploreWrapper> 99 + <Icon 100 + width={iconWidth + 2} 101 + style={[styles.ctrlIcon, pal.text, styles.searchIcon]} 102 + /> 103 + </HomeTourExploreWrapper> 101 104 ) 102 105 }} 103 106 </NavItem>
+14 -8
src/view/shell/desktop/LeftNav.tsx
··· 63 63 UserCircle_Filled_Corner0_Rounded as UserCircleFilled, 64 64 UserCircle_Stroke2_Corner0_Rounded as UserCircle, 65 65 } from '#/components/icons/UserCircle' 66 + import {HomeTourExploreWrapper} from '#/tours/HomeTour' 66 67 import {router} from '../../../routes' 67 68 68 69 const NAV_ICON_WIDTH = 28 ··· 340 341 iconFilled={<HomeFilled width={NAV_ICON_WIDTH} style={pal.text} />} 341 342 label={_(msg`Home`)} 342 343 /> 343 - <NavItem 344 - href="/search" 345 - icon={<MagnifyingGlass style={pal.text} width={NAV_ICON_WIDTH} />} 346 - iconFilled={ 347 - <MagnifyingGlassFilled style={pal.text} width={NAV_ICON_WIDTH} /> 348 - } 349 - label={_(msg`Search`)} 350 - /> 344 + <HomeTourExploreWrapper> 345 + <NavItem 346 + href="/search" 347 + icon={<MagnifyingGlass style={pal.text} width={NAV_ICON_WIDTH} />} 348 + iconFilled={ 349 + <MagnifyingGlassFilled 350 + style={pal.text} 351 + width={NAV_ICON_WIDTH} 352 + /> 353 + } 354 + label={_(msg`Search`)} 355 + /> 356 + </HomeTourExploreWrapper> 351 357 <NavItem 352 358 href="/notifications" 353 359 count={numUnreadNotifications}
+82 -6
yarn.lock
··· 10172 10172 resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" 10173 10173 integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== 10174 10174 10175 + commander@2, commander@^2.20.0: 10176 + version "2.20.3" 10177 + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 10178 + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 10179 + 10175 10180 commander@2.20.0: 10176 10181 version "2.20.0" 10177 10182 resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" ··· 10181 10186 version "10.0.1" 10182 10187 resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" 10183 10188 integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== 10184 - 10185 - commander@^2.20.0: 10186 - version "2.20.3" 10187 - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 10188 - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 10189 10189 10190 10190 commander@^4.0.0: 10191 10191 version "4.1.1" ··· 10678 10678 resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" 10679 10679 integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== 10680 10680 10681 + d3-array@^1.2.0: 10682 + version "1.2.4" 10683 + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" 10684 + integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== 10685 + 10686 + d3-polygon@^1.0.3: 10687 + version "1.0.6" 10688 + resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e" 10689 + integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ== 10690 + 10681 10691 dag-map@~1.0.0: 10682 10692 version "1.0.2" 10683 10693 resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-1.0.2.tgz#e8379f041000ed561fc515475c1ed2c85eece8d7" ··· 11159 11169 resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" 11160 11170 integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== 11161 11171 11172 + earcut@^2.1.1: 11173 + version "2.2.4" 11174 + resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a" 11175 + integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== 11176 + 11162 11177 eastasianwidth@^0.2.0: 11163 11178 version "0.2.0" 11164 11179 resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" ··· 12712 12727 resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.215.0.tgz#9b153fa27ab238bcc0bb1ff73b63bdb15d3f277d" 12713 12728 integrity sha512-8bjwzy8vi+fNDy8YoTBNtQUSZa53i7UWJJTunJojOtjab9cMNhOCwohionuMgDQUU0y21QTTtPOX6OQEOQT72A== 12714 12729 12730 + flubber@~0.4.2: 12731 + version "0.4.2" 12732 + resolved "https://registry.yarnpkg.com/flubber/-/flubber-0.4.2.tgz#14452d4a838cc3b9f2fb6175da94e35acd55fbaa" 12733 + integrity sha512-79RkJe3rA4nvRCVc2uXjj7U/BAUq84TS3KHn6c0Hr9K64vhj83ZNLUziNx4pJoBumSPhOl5VjH+Z0uhi+eE8Uw== 12734 + dependencies: 12735 + d3-array "^1.2.0" 12736 + d3-polygon "^1.0.3" 12737 + earcut "^2.1.1" 12738 + svg-path-properties "^0.2.1" 12739 + svgpath "^2.2.1" 12740 + topojson-client "^3.0.0" 12741 + 12715 12742 follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0: 12716 12743 version "1.15.2" 12717 12744 resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" ··· 13327 13354 integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== 13328 13355 dependencies: 13329 13356 react-is "^16.7.0" 13357 + 13358 + hoist-non-react-statics@~3.0.1: 13359 + version "3.0.1" 13360 + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.0.1.tgz#fba3e7df0210eb9447757ca1a7cb607162f0a364" 13361 + integrity sha512-1kXwPsOi0OGQIZNVMPvgWJ9tSnGMiMfJdihqEzrPEXlHOBh9AAHXX/QYmAJTXztnz/K+PQ8ryCb4eGaN6HlGbQ== 13362 + dependencies: 13363 + react-is "^16.3.2" 13330 13364 13331 13365 hoopy@^0.1.4: 13332 13366 version "0.1.4" ··· 15806 15840 resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" 15807 15841 integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w== 15808 15842 15843 + lodash.clamp@~4.0.3: 15844 + version "4.0.3" 15845 + resolved "https://registry.yarnpkg.com/lodash.clamp/-/lodash.clamp-4.0.3.tgz#5c24bedeeeef0753560dc2b4cb4671f90a6ddfaa" 15846 + integrity sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg== 15847 + 15809 15848 lodash.debounce@^4.0.8: 15810 15849 version "4.0.8" 15811 15850 resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" ··· 16089 16128 integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== 16090 16129 dependencies: 16091 16130 fs-monkey "^1.0.4" 16131 + 16132 + memoize-one@5.1.1: 16133 + version "5.1.1" 16134 + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0" 16135 + integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA== 16092 16136 16093 16137 memoize-one@^5.0.0: 16094 16138 version "5.2.1" ··· 16516 16560 dependencies: 16517 16561 minipass "^3.0.0" 16518 16562 yallist "^4.0.0" 16563 + 16564 + mitt@~1.1.3: 16565 + version "1.1.3" 16566 + resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.1.3.tgz#528c506238a05dce11cd914a741ea2cc332da9b8" 16567 + integrity sha512-mUDCnVNsAi+eD6qA0HkRkwYczbLHJ49z17BGe2PYRhZL4wpZUFZGJHU7/5tmvohoma+Hdn0Vh/oJTiPEmgSruA== 16519 16568 16520 16569 mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: 16521 16570 version "0.5.3" ··· 18780 18829 resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" 18781 18830 integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== 18782 18831 18783 - react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.4: 18832 + react-is@^16.13.0, react-is@^16.13.1, react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.4: 18784 18833 version "16.13.1" 18785 18834 resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" 18786 18835 integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== ··· 19585 19634 base-64 "0.1.0" 19586 19635 glob "7.0.6" 19587 19636 19637 + rn-tourguide@bluesky-social/rn-tourguide: 19638 + version "3.3.0" 19639 + resolved "https://codeload.github.com/bluesky-social/rn-tourguide/tar.gz/a14bb85536b317b94d82801900df4cf57f81aef7" 19640 + dependencies: 19641 + flubber "~0.4.2" 19642 + hoist-non-react-statics "~3.0.1" 19643 + lodash.clamp "~4.0.3" 19644 + memoize-one "5.1.1" 19645 + mitt "~1.1.3" 19646 + 19588 19647 roarr@^7.0.4: 19589 19648 version "7.15.1" 19590 19649 resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.1.tgz#e4d93105c37b5ea7dd1200d96a3500f757ddc39f" ··· 20711 20770 resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" 20712 20771 integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== 20713 20772 20773 + svg-path-properties@^0.2.1: 20774 + version "0.2.2" 20775 + resolved "https://registry.yarnpkg.com/svg-path-properties/-/svg-path-properties-0.2.2.tgz#b073d81be7292eae0e233ab8a83f58dc27113296" 20776 + integrity sha512-GmrB+b6woz6CCdQe6w1GHs/1lt25l7SR5hmhF8jRdarpv/OgjLyuQygLu1makJapixeb1aQhP/Oa1iKi93o/aQ== 20777 + 20714 20778 svgo@^1.2.2: 20715 20779 version "1.3.2" 20716 20780 resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" ··· 20742 20806 csso "^4.2.0" 20743 20807 picocolors "^1.0.0" 20744 20808 stable "^0.1.8" 20809 + 20810 + svgpath@^2.2.1: 20811 + version "2.6.0" 20812 + resolved "https://registry.yarnpkg.com/svgpath/-/svgpath-2.6.0.tgz#5b160ef3d742b7dfd2d721bf90588d3450d7a90d" 20813 + integrity sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg== 20745 20814 20746 20815 symbol-tree@^3.2.4: 20747 20816 version "3.2.4" ··· 21053 21122 dependencies: 21054 21123 "@tokenizer/token" "^0.3.0" 21055 21124 ieee754 "^1.2.1" 21125 + 21126 + topojson-client@^3.0.0: 21127 + version "3.1.0" 21128 + resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99" 21129 + integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw== 21130 + dependencies: 21131 + commander "2" 21056 21132 21057 21133 totalist@^3.0.0: 21058 21134 version "3.0.1"