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>
zio/stable
Paul Frazee 2024-07-02 21:25:19 -07:00 committed by GitHub
parent 6694a33603
commit a3d4fb652b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 541 additions and 39 deletions

View File

@ -193,6 +193,7 @@
"react-responsive": "^9.0.2", "react-responsive": "^9.0.2",
"react-textarea-autosize": "^8.5.3", "react-textarea-autosize": "^8.5.3",
"rn-fetch-blob": "^0.12.0", "rn-fetch-blob": "^0.12.0",
"rn-tourguide": "bluesky-social/rn-tourguide",
"sentry-expo": "~7.0.1", "sentry-expo": "~7.0.1",
"statsig-react-native-expo": "^4.6.1", "statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",

View File

@ -55,6 +55,7 @@ import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {Splash} from '#/Splash' import {Splash} from '#/Splash'
import {Provider as TourProvider} from '#/tours'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import I18nProvider from './locale/i18nProvider' import I18nProvider from './locale/i18nProvider'
import {listenSessionDropped} from './state/events' import {listenSessionDropped} from './state/events'
@ -117,10 +118,12 @@ function InnerApp() {
<UnreadNotifsProvider> <UnreadNotifsProvider>
<BackgroundNotificationPreferencesProvider> <BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider> <MutedThreadsProvider>
<TourProvider>
<GestureHandlerRootView style={s.h100pct}> <GestureHandlerRootView style={s.h100pct}>
<TestCtrls /> <TestCtrls />
<Shell /> <Shell />
</GestureHandlerRootView> </GestureHandlerRootView>
</TourProvider>
</MutedThreadsProvider> </MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider> </BackgroundNotificationPreferencesProvider>
</UnreadNotifsProvider> </UnreadNotifsProvider>

View File

@ -43,6 +43,7 @@ import {ThemeProvider as Alf} from '#/alf'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme' import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry' import {useStarterPackEntry} from '#/components/hooks/useStarterPackEntry'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as TourProvider} from '#/tours'
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
import I18nProvider from './locale/i18nProvider' import I18nProvider from './locale/i18nProvider'
import {listenSessionDropped} from './state/events' import {listenSessionDropped} from './state/events'
@ -102,7 +103,9 @@ function InnerApp() {
<BackgroundNotificationPreferencesProvider> <BackgroundNotificationPreferencesProvider>
<MutedThreadsProvider> <MutedThreadsProvider>
<SafeAreaProvider> <SafeAreaProvider>
<TourProvider>
<Shell /> <Shell />
</TourProvider>
</SafeAreaProvider> </SafeAreaProvider>
</MutedThreadsProvider> </MutedThreadsProvider>
</BackgroundNotificationPreferencesProvider> </BackgroundNotificationPreferencesProvider>

View File

@ -6,5 +6,6 @@ export type Gate =
| 'request_notifications_permission_after_onboarding_v2' | 'request_notifications_permission_after_onboarding_v2'
| 'show_avi_follow_button' | 'show_avi_follow_button'
| 'show_follow_back_label_v2' | 'show_follow_back_label_v2'
| 'new_user_guided_tour'
| 'suggested_feeds_interstitial' | 'suggested_feeds_interstitial'
| 'suggested_follows_interstitial' | 'suggested_follows_interstitial'

View File

@ -42,6 +42,7 @@ import {News2_Stroke2_Corner0_Rounded as News} from '#/components/icons/News2'
import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2' import {Trending2_Stroke2_Corner2_Rounded as Trending} from '#/components/icons/Trending2'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {TOURS, useSetQueuedTour} from '#/tours'
export function StepFinished() { export function StepFinished() {
const {_} = useLingui() const {_} = useLingui()
@ -56,6 +57,7 @@ export function StepFinished() {
const activeStarterPack = useActiveStarterPack() const activeStarterPack = useActiveStarterPack()
const setActiveStarterPack = useSetActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack()
const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
const setQueuedTour = useSetQueuedTour()
const finishOnboarding = React.useCallback(async () => { const finishOnboarding = React.useCallback(async () => {
setSaving(true) setSaving(true)
@ -182,6 +184,7 @@ export function StepFinished() {
setSaving(false) setSaving(false)
setActiveStarterPack(undefined) setActiveStarterPack(undefined)
setHasCheckedForStarterPack(true) setHasCheckedForStarterPack(true)
setQueuedTour(TOURS.HOME)
dispatch({type: 'finish'}) dispatch({type: 'finish'})
onboardDispatch({type: 'finish'}) onboardDispatch({type: 'finish'})
track('OnboardingV2:StepFinished:End') track('OnboardingV2:StepFinished:End')
@ -214,6 +217,7 @@ export function StepFinished() {
requestNotificationsPermission, requestNotificationsPermission,
setActiveStarterPack, setActiveStarterPack,
setHasCheckedForStarterPack, setHasCheckedForStarterPack,
setQueuedTour,
]) ])
React.useEffect(() => { React.useEffect(() => {

View File

@ -0,0 +1,18 @@
import React from 'react'
import {useTourGuideController} from 'rn-tourguide'
import {Button} from '#/components/Button'
import {Text} from '#/components/Typography'
export function TourDebugButton() {
const {start} = useTourGuideController('home')
return (
<Button
label="Start tour"
onPress={() => {
start()
}}>
{() => <Text>t</Text>}
</Button>
)
}

View File

@ -0,0 +1,93 @@
import React from 'react'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {
IStep,
TourGuideZone,
TourGuideZoneByPosition,
useTourGuideController,
} from 'rn-tourguide'
import {DISCOVER_FEED_URI} from '#/lib/constants'
import {isWeb} from '#/platform/detection'
import {useSetSelectedFeed} from '#/state/shell/selected-feed'
import {TOURS} from '.'
import {useHeaderPosition} from './positioning'
export function HomeTour() {
const {_} = useLingui()
const {tourKey, eventEmitter} = useTourGuideController(TOURS.HOME)
const setSelectedFeed = useSetSelectedFeed()
const headerPosition = useHeaderPosition()
React.useEffect(() => {
const handleOnStepChange = (step?: IStep) => {
if (step?.order === 2) {
setSelectedFeed('following')
} else if (step?.order === 3) {
setSelectedFeed(`feedgen|${DISCOVER_FEED_URI}`)
}
}
eventEmitter?.on('stepChange', handleOnStepChange)
return () => {
eventEmitter?.off('stepChange', handleOnStepChange)
}
}, [eventEmitter, setSelectedFeed])
return (
<>
<TourGuideZoneByPosition
isTourGuide
tourKey={tourKey}
zone={1}
top={headerPosition.top}
left={headerPosition.left}
width={headerPosition.width}
height={headerPosition.height}
borderRadiusObject={headerPosition.borderRadiusObject}
text={_(msg`Switch between feeds to control your experience.`)}
/>
<TourGuideZoneByPosition
isTourGuide
tourKey={tourKey}
zone={2}
top={headerPosition.top}
left={headerPosition.left}
width={headerPosition.width}
height={headerPosition.height}
borderRadiusObject={headerPosition.borderRadiusObject}
text={_(msg`Following shows the latest posts from people you follow.`)}
/>
<TourGuideZoneByPosition
isTourGuide
tourKey={tourKey}
zone={3}
top={headerPosition.top}
left={headerPosition.left}
width={headerPosition.width}
height={headerPosition.height}
borderRadiusObject={headerPosition.borderRadiusObject}
text={_(msg`Discover learns which posts you like as you browse.`)}
/>
</>
)
}
export function HomeTourExploreWrapper({
children,
}: React.PropsWithChildren<{}>) {
const {_} = useLingui()
const {tourKey} = useTourGuideController(TOURS.HOME)
return (
<TourGuideZone
tourKey={tourKey}
zone={4}
tooltipBottomOffset={50}
shape={isWeb ? 'rectangle' : 'circle'}
text={_(
msg`Find more feeds and accounts to follow in the Explore page.`,
)}>
{children}
</TourGuideZone>
)
}

View File

@ -0,0 +1,168 @@
import * as React from 'react'
import {
AccessibilityInfo,
findNodeHandle,
Pressable,
Text as RNText,
View,
} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FocusScope} from '@tamagui/focus-scope'
import {IStep, Labels} from 'rn-tourguide'
import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
import {useA11y} from '#/state/a11y'
import {Logo} from '#/view/icons/Logo'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {leading, Text} from '#/components/Typography'
const stopPropagation = (e: any) => e.stopPropagation()
export interface TooltipComponentProps {
isFirstStep?: boolean
isLastStep?: boolean
currentStep: IStep
labels?: Labels
handleNext?: () => void
handlePrev?: () => void
handleStop?: () => void
}
export function TooltipComponent({
isLastStep,
handleNext,
handleStop,
currentStep,
labels,
}: TooltipComponentProps) {
const t = useTheme()
const {_} = useLingui()
const btnRef = React.useRef<View>(null)
const textRef = React.useRef<RNText>(null)
const {screenReaderEnabled} = useA11y()
useWebBodyScrollLock(true)
const focusTextNode = () => {
const node = textRef.current ? findNodeHandle(textRef.current) : undefined
if (node) {
AccessibilityInfo.setAccessibilityFocus(node)
}
}
// handle initial focus immediately on mount
React.useLayoutEffect(() => {
focusTextNode()
}, [])
// handle focus between steps
const innerHandleNext = () => {
handleNext?.()
setTimeout(() => focusTextNode(), 200)
}
return (
<FocusScope loop enabled trapped>
<View
role="alert"
aria-role="alert"
aria-label={_(msg`A help tooltip`)}
accessibilityLiveRegion="polite"
// iOS
accessibilityViewIsModal
// Android
importantForAccessibility="yes"
// @ts-ignore web only
onClick={stopPropagation}
onStartShouldSetResponder={_ => true}
onTouchEnd={stopPropagation}
style={[
t.atoms.bg,
a.px_lg,
a.py_lg,
a.flex_col,
a.gap_md,
a.rounded_sm,
a.shadow_md,
{maxWidth: 300},
]}>
{screenReaderEnabled && (
<Pressable
style={[
a.absolute,
a.inset_0,
a.z_10,
{height: 10, bottom: 'auto'},
]}
accessibilityLabel={_(
msg`Start of onboarding tour window. Do not move backward. Instead, go forward for more options, or press to skip.`,
)}
accessibilityHint={undefined}
onPress={handleStop}
/>
)}
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Logo width={16} style={{position: 'relative', top: 0}} />
<Text
accessible={false}
style={[a.text_sm, a.font_semibold, t.atoms.text_contrast_medium]}>
<Trans>Quick tip</Trans>
</Text>
</View>
<RNText
ref={textRef}
testID="stepDescription"
accessibilityLabel={_(
msg`Onboarding tour step ${currentStep.name}: ${currentStep.text}`,
)}
accessibilityHint={undefined}
style={[
a.text_md,
t.atoms.text,
a.pb_sm,
{
lineHeight: leading(a.text_md, a.leading_snug),
},
]}>
{currentStep.text}
</RNText>
{!isLastStep ? (
<Button
ref={btnRef}
variant="gradient"
color="gradient_sky"
size="medium"
onPress={innerHandleNext}
label={labels?.next || _(msg`Go to the next step of the tour`)}>
<ButtonText>{labels?.next || _(msg`Next`)}</ButtonText>
</Button>
) : (
<Button
variant="gradient"
color="gradient_sky"
size="medium"
onPress={handleStop}
label={
labels?.finish ||
_(msg`Finish tour and begin using the application`)
}>
<ButtonText>{labels?.finish || _(msg`Let's go!`)}</ButtonText>
</Button>
)}
{screenReaderEnabled && (
<Pressable
style={[a.absolute, a.inset_0, a.z_10, {height: 10, top: 'auto'}]}
accessibilityLabel={_(
msg`End of onboarding tour window. Do not move forward. Instead, go backward for more options, or press to skip.`,
)}
accessibilityHint={undefined}
onPress={handleStop}
/>
)}
</View>
</FocusScope>
)
}

View File

@ -0,0 +1,62 @@
import React from 'react'
import {InteractionManager} from 'react-native'
import {TourGuideProvider, useTourGuideController} from 'rn-tourguide'
import {useGate} from '#/lib/statsig/statsig'
import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
import {HomeTour} from './HomeTour'
import {TooltipComponent} from './Tooltip'
export enum TOURS {
HOME = 'home',
}
type StateContext = TOURS | null
type SetContext = (v: TOURS | null) => void
const stateContext = React.createContext<StateContext>(null)
const setContext = React.createContext<SetContext>((_: TOURS | null) => {})
export function Provider({children}: React.PropsWithChildren<{}>) {
const theme = useColorModeTheme()
const [state, setState] = React.useState<TOURS | null>(() => null)
return (
<TourGuideProvider
androidStatusBarVisible
tooltipComponent={TooltipComponent}
backdropColor={
theme === 'light' ? 'rgba(0, 0, 0, 0.15)' : 'rgba(0, 0, 0, 0.8)'
}
preventOutsideInteraction>
<stateContext.Provider value={state}>
<setContext.Provider value={setState}>
<HomeTour />
{children}
</setContext.Provider>
</stateContext.Provider>
</TourGuideProvider>
)
}
export function useTriggerTourIfQueued(tour: TOURS) {
const {start} = useTourGuideController(tour)
const setQueuedTour = React.useContext(setContext)
const queuedTour = React.useContext(stateContext)
const gate = useGate()
return React.useCallback(() => {
if (queuedTour === tour) {
setQueuedTour(null)
InteractionManager.runAfterInteractions(() => {
if (gate('new_user_guided_tour')) {
start()
}
})
}
}, [tour, queuedTour, setQueuedTour, start, gate])
}
export function useSetQueuedTour() {
return React.useContext(setContext)
}

View File

@ -0,0 +1,23 @@
import {useWindowDimensions} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {useShellLayout} from '#/state/shell/shell-layout'
export function useHeaderPosition() {
const {headerHeight} = useShellLayout()
const {width} = useWindowDimensions()
const insets = useSafeAreaInsets()
return {
top: insets.top,
left: 10,
width: width - 20,
height: headerHeight.value,
borderRadiusObject: {
topLeft: 4,
topRight: 4,
bottomLeft: 4,
bottomRight: 4,
},
}
}

View File

@ -0,0 +1,27 @@
import {useWindowDimensions} from 'react-native'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {useShellLayout} from '#/state/shell/shell-layout'
export function useHeaderPosition() {
const {headerHeight} = useShellLayout()
const winDim = useWindowDimensions()
const {isMobile} = useWebMediaQueries()
let left = 0
let width = winDim.width
if (width > 590 && !isMobile) {
left = winDim.width / 2 - 295
width = 590
}
let offset = isMobile ? 45 : 0
return {
top: headerHeight.value - offset,
left,
width,
height: 45,
borderRadiusObject: undefined,
}
}

View File

@ -72,9 +72,11 @@ export function HomeHeaderLayoutMobile({
{width: 100}, {width: 100},
]}> ]}>
{IS_DEV && ( {IS_DEV && (
<>
<Link to="/sys/debug"> <Link to="/sys/debug">
<ColorPalette size="md" /> <ColorPalette size="md" />
</Link> </Link>
</>
)} )}
{hasSession && ( {hasSession && (
<Link <Link

View File

@ -28,6 +28,7 @@ import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState' import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed' import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
import {TOURS, useTriggerTourIfQueued} from '#/tours'
import {HomeHeader} from '../com/home/HomeHeader' import {HomeHeader} from '../com/home/HomeHeader'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
@ -86,6 +87,7 @@ function HomeScreenReady({
const selectedIndex = Math.max(0, maybeFoundIndex) const selectedIndex = Math.max(0, maybeFoundIndex)
const selectedFeed = allFeeds[selectedIndex] const selectedFeed = allFeeds[selectedIndex]
const requestNotificationsPermission = useRequestNotificationsPermission() const requestNotificationsPermission = useRequestNotificationsPermission()
const triggerTourIfQueued = useTriggerTourIfQueued(TOURS.HOME)
useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName) useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName)
useOTAUpdates() useOTAUpdates()
@ -113,10 +115,16 @@ function HomeScreenReady({
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
setDrawerSwipeDisabled(selectedIndex > 0) setDrawerSwipeDisabled(selectedIndex > 0)
triggerTourIfQueued()
return () => { return () => {
setDrawerSwipeDisabled(false) setDrawerSwipeDisabled(false)
} }
}, [setDrawerSwipeDisabled, selectedIndex, setMinimalShellMode]), }, [
setDrawerSwipeDisabled,
selectedIndex,
setMinimalShellMode,
triggerTourIfQueued,
]),
) )
useFocusEffect( useFocusEffect(

View File

@ -252,9 +252,10 @@ export function SettingsScreen({}: Props) {
}, [clearPreferences]) }, [clearPreferences])
const onPressResetOnboarding = React.useCallback(async () => { const onPressResetOnboarding = React.useCallback(async () => {
navigation.navigate('Home')
onboardingDispatch({type: 'start'}) onboardingDispatch({type: 'start'})
Toast.show(_(msg`Onboarding reset`)) Toast.show(_(msg`Onboarding reset`))
}, [onboardingDispatch, _]) }, [navigation, onboardingDispatch, _])
const onPressBuildInfo = React.useCallback(() => { const onPressBuildInfo = React.useCallback(() => {
setStringAsync( setStringAsync(

View File

@ -45,6 +45,7 @@ import {
Message_Stroke2_Corner0_Rounded as Message, Message_Stroke2_Corner0_Rounded as Message,
Message_Stroke2_Corner0_Rounded_Filled as MessageFilled, Message_Stroke2_Corner0_Rounded_Filled as MessageFilled,
} from '#/components/icons/Message' } from '#/components/icons/Message'
import {HomeTourExploreWrapper} from '#/tours/HomeTour'
import {styles} from './BottomBarStyles' import {styles} from './BottomBarStyles'
type TabOptions = type TabOptions =
@ -162,7 +163,8 @@ export function BottomBar({navigation}: BottomTabBarProps) {
<Btn <Btn
testID="bottomBarSearchBtn" testID="bottomBarSearchBtn"
icon={ icon={
isAtSearch ? ( <HomeTourExploreWrapper>
{isAtSearch ? (
<MagnifyingGlassFilled <MagnifyingGlassFilled
width={iconWidth + 2} width={iconWidth + 2}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]} style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
@ -172,7 +174,8 @@ export function BottomBar({navigation}: BottomTabBarProps) {
width={iconWidth + 2} width={iconWidth + 2}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]} style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
/> />
) )}
</HomeTourExploreWrapper>
} }
onPress={onPressSearch} onPress={onPressSearch}
accessibilityRole="search" accessibilityRole="search"

View File

@ -41,6 +41,7 @@ import {
UserCircle_Filled_Corner0_Rounded as UserCircleFilled, UserCircle_Filled_Corner0_Rounded as UserCircleFilled,
UserCircle_Stroke2_Corner0_Rounded as UserCircle, UserCircle_Stroke2_Corner0_Rounded as UserCircle,
} from '#/components/icons/UserCircle' } from '#/components/icons/UserCircle'
import {HomeTourExploreWrapper} from '#/tours/HomeTour'
import {styles} from './BottomBarStyles' import {styles} from './BottomBarStyles'
export function BottomBarWeb() { export function BottomBarWeb() {
@ -94,10 +95,12 @@ export function BottomBarWeb() {
{({isActive}) => { {({isActive}) => {
const Icon = isActive ? MagnifyingGlassFilled : MagnifyingGlass const Icon = isActive ? MagnifyingGlassFilled : MagnifyingGlass
return ( return (
<HomeTourExploreWrapper>
<Icon <Icon
width={iconWidth + 2} width={iconWidth + 2}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]} style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
/> />
</HomeTourExploreWrapper>
) )
}} }}
</NavItem> </NavItem>

View File

@ -63,6 +63,7 @@ import {
UserCircle_Filled_Corner0_Rounded as UserCircleFilled, UserCircle_Filled_Corner0_Rounded as UserCircleFilled,
UserCircle_Stroke2_Corner0_Rounded as UserCircle, UserCircle_Stroke2_Corner0_Rounded as UserCircle,
} from '#/components/icons/UserCircle' } from '#/components/icons/UserCircle'
import {HomeTourExploreWrapper} from '#/tours/HomeTour'
import {router} from '../../../routes' import {router} from '../../../routes'
const NAV_ICON_WIDTH = 28 const NAV_ICON_WIDTH = 28
@ -340,14 +341,19 @@ export function DesktopLeftNav() {
iconFilled={<HomeFilled width={NAV_ICON_WIDTH} style={pal.text} />} iconFilled={<HomeFilled width={NAV_ICON_WIDTH} style={pal.text} />}
label={_(msg`Home`)} label={_(msg`Home`)}
/> />
<HomeTourExploreWrapper>
<NavItem <NavItem
href="/search" href="/search"
icon={<MagnifyingGlass style={pal.text} width={NAV_ICON_WIDTH} />} icon={<MagnifyingGlass style={pal.text} width={NAV_ICON_WIDTH} />}
iconFilled={ iconFilled={
<MagnifyingGlassFilled style={pal.text} width={NAV_ICON_WIDTH} /> <MagnifyingGlassFilled
style={pal.text}
width={NAV_ICON_WIDTH}
/>
} }
label={_(msg`Search`)} label={_(msg`Search`)}
/> />
</HomeTourExploreWrapper>
<NavItem <NavItem
href="/notifications" href="/notifications"
count={numUnreadNotifications} count={numUnreadNotifications}

View File

@ -10172,6 +10172,11 @@ commander@11.0.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67" resolved "https://registry.yarnpkg.com/commander/-/commander-11.0.0.tgz#43e19c25dbedc8256203538e8d7e9346877a6f67"
integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==
commander@2, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@2.20.0: commander@2.20.0:
version "2.20.0" version "2.20.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
@ -10182,11 +10187,6 @@ commander@^10.0.0, commander@^10.0.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06"
integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^4.0.0: commander@^4.0.0:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
@ -10678,6 +10678,16 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
d3-array@^1.2.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
d3-polygon@^1.0.3:
version "1.0.6"
resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.6.tgz#0bf8cb8180a6dc107f518ddf7975e12abbfbd38e"
integrity sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==
dag-map@~1.0.0: dag-map@~1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-1.0.2.tgz#e8379f041000ed561fc515475c1ed2c85eece8d7" resolved "https://registry.yarnpkg.com/dag-map/-/dag-map-1.0.2.tgz#e8379f041000ed561fc515475c1ed2c85eece8d7"
@ -11159,6 +11169,11 @@ duplexer@^0.1.2:
resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6"
integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==
earcut@^2.1.1:
version "2.2.4"
resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a"
integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==
eastasianwidth@^0.2.0: eastasianwidth@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
@ -12712,6 +12727,18 @@ flow-parser@0.*:
resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.215.0.tgz#9b153fa27ab238bcc0bb1ff73b63bdb15d3f277d" resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.215.0.tgz#9b153fa27ab238bcc0bb1ff73b63bdb15d3f277d"
integrity sha512-8bjwzy8vi+fNDy8YoTBNtQUSZa53i7UWJJTunJojOtjab9cMNhOCwohionuMgDQUU0y21QTTtPOX6OQEOQT72A== integrity sha512-8bjwzy8vi+fNDy8YoTBNtQUSZa53i7UWJJTunJojOtjab9cMNhOCwohionuMgDQUU0y21QTTtPOX6OQEOQT72A==
flubber@~0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/flubber/-/flubber-0.4.2.tgz#14452d4a838cc3b9f2fb6175da94e35acd55fbaa"
integrity sha512-79RkJe3rA4nvRCVc2uXjj7U/BAUq84TS3KHn6c0Hr9K64vhj83ZNLUziNx4pJoBumSPhOl5VjH+Z0uhi+eE8Uw==
dependencies:
d3-array "^1.2.0"
d3-polygon "^1.0.3"
earcut "^2.1.1"
svg-path-properties "^0.2.1"
svgpath "^2.2.1"
topojson-client "^3.0.0"
follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0: follow-redirects@^1.0.0, follow-redirects@^1.14.9, follow-redirects@^1.15.0:
version "1.15.2" version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
@ -13328,6 +13355,13 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
dependencies: dependencies:
react-is "^16.7.0" react-is "^16.7.0"
hoist-non-react-statics@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.0.1.tgz#fba3e7df0210eb9447757ca1a7cb607162f0a364"
integrity sha512-1kXwPsOi0OGQIZNVMPvgWJ9tSnGMiMfJdihqEzrPEXlHOBh9AAHXX/QYmAJTXztnz/K+PQ8ryCb4eGaN6HlGbQ==
dependencies:
react-is "^16.3.2"
hoopy@^0.1.4: hoopy@^0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" resolved "https://registry.yarnpkg.com/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d"
@ -15806,6 +15840,11 @@ lodash.chunk@^4.2.0:
resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc"
integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w== integrity sha512-ZzydJKfUHJwHa+hF5X66zLFCBrWn5GeF28OHEr4WVWtNDXlQ/IjWKPBiikqKo2ne0+v6JgCgJ0GzJp8k8bHC7w==
lodash.clamp@~4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash.clamp/-/lodash.clamp-4.0.3.tgz#5c24bedeeeef0753560dc2b4cb4671f90a6ddfaa"
integrity sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg==
lodash.debounce@^4.0.8: lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@ -16090,6 +16129,11 @@ memfs@^3.1.2, memfs@^3.4.3:
dependencies: dependencies:
fs-monkey "^1.0.4" fs-monkey "^1.0.4"
memoize-one@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
memoize-one@^5.0.0: memoize-one@^5.0.0:
version "5.2.1" version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
@ -16517,6 +16561,11 @@ minizlib@^2.1.1:
minipass "^3.0.0" minipass "^3.0.0"
yallist "^4.0.0" yallist "^4.0.0"
mitt@~1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-1.1.3.tgz#528c506238a05dce11cd914a741ea2cc332da9b8"
integrity sha512-mUDCnVNsAi+eD6qA0HkRkwYczbLHJ49z17BGe2PYRhZL4wpZUFZGJHU7/5tmvohoma+Hdn0Vh/oJTiPEmgSruA==
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3" version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
@ -18780,7 +18829,7 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-is@^16.13.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.4: 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:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -19585,6 +19634,16 @@ rn-fetch-blob@^0.12.0:
base-64 "0.1.0" base-64 "0.1.0"
glob "7.0.6" glob "7.0.6"
rn-tourguide@bluesky-social/rn-tourguide:
version "3.3.0"
resolved "https://codeload.github.com/bluesky-social/rn-tourguide/tar.gz/a14bb85536b317b94d82801900df4cf57f81aef7"
dependencies:
flubber "~0.4.2"
hoist-non-react-statics "~3.0.1"
lodash.clamp "~4.0.3"
memoize-one "5.1.1"
mitt "~1.1.3"
roarr@^7.0.4: roarr@^7.0.4:
version "7.15.1" version "7.15.1"
resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.1.tgz#e4d93105c37b5ea7dd1200d96a3500f757ddc39f" resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.15.1.tgz#e4d93105c37b5ea7dd1200d96a3500f757ddc39f"
@ -20711,6 +20770,11 @@ svg-parser@^2.0.2:
resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==
svg-path-properties@^0.2.1:
version "0.2.2"
resolved "https://registry.yarnpkg.com/svg-path-properties/-/svg-path-properties-0.2.2.tgz#b073d81be7292eae0e233ab8a83f58dc27113296"
integrity sha512-GmrB+b6woz6CCdQe6w1GHs/1lt25l7SR5hmhF8jRdarpv/OgjLyuQygLu1makJapixeb1aQhP/Oa1iKi93o/aQ==
svgo@^1.2.2: svgo@^1.2.2:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
@ -20743,6 +20807,11 @@ svgo@^2.7.0:
picocolors "^1.0.0" picocolors "^1.0.0"
stable "^0.1.8" stable "^0.1.8"
svgpath@^2.2.1:
version "2.6.0"
resolved "https://registry.yarnpkg.com/svgpath/-/svgpath-2.6.0.tgz#5b160ef3d742b7dfd2d721bf90588d3450d7a90d"
integrity sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==
symbol-tree@^3.2.4: symbol-tree@^3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -21054,6 +21123,13 @@ token-types@^4.1.1:
"@tokenizer/token" "^0.3.0" "@tokenizer/token" "^0.3.0"
ieee754 "^1.2.1" ieee754 "^1.2.1"
topojson-client@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99"
integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==
dependencies:
commander "2"
totalist@^3.0.0: totalist@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8"