Mobile Web (#427)

* WIP

* WIP

* Fix header offset on web

* Remove debug

* Fix web mobile feed and FAB layout

* Fix modals on mobile web

* Remove dead code

* Remove ios config that shouldnt be committed now

* Move bottom bar into its own folder

* Fix web drawer navigation and state behaviors

* Remove dark mode toggle from web drawer for now

* Fix search on mobile web

* Fix the logged out splash screen on mobile web

* Fixes to detox simulator

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
zio/stable
John Fawcett 2023-04-12 20:27:55 -05:00 committed by GitHub
parent 2fed6c4021
commit f6769b283f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 4343 additions and 319 deletions

View File

@ -14,14 +14,14 @@ module.exports = {
type: 'ios.app', type: 'ios.app',
binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/bluesky.app', binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/bluesky.app',
build: build:
'xcodebuild -workspace ios/bluesky.xcworkspace -scheme bluesky -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', 'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
}, },
'ios.release': { 'ios.release': {
type: 'ios.app', type: 'ios.app',
binaryPath: binaryPath:
'ios/build/Build/Products/Release-iphonesimulator/bluesky.app', 'ios/build/Build/Products/Release-iphonesimulator/bluesky.app',
build: build:
'xcodebuild -workspace ios/bluesky.xcworkspace -scheme bluesky -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', 'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Release -sdk iphonesimulator -derivedDataPath ios/build',
}, },
'android.debug': { 'android.debug': {
type: 'android.apk', type: 'android.apk',

View File

@ -93,7 +93,7 @@ describe('Profile screen', () => {
// have to wait for the toast to clear // have to wait for the toast to clear
await waitFor(element(by.id('searchTextInput'))) await waitFor(element(by.id('searchTextInput')))
.toBeVisible() .toBeVisible()
.withTimeout(2000) .withTimeout(5000)
await element(by.id('searchTextInput')).typeText('bob') await element(by.id('searchTextInput')).typeText('bob')
await element(by.id('searchAutoCompleteResult-bob.test')).tap() await element(by.id('searchAutoCompleteResult-bob.test')).tap()
await expect(element(by.id('profileView'))).toBeVisible() await expect(element(by.id('profileView'))).toBeVisible()

View File

@ -117,6 +117,7 @@
"react-native-version-number": "^0.3.6", "react-native-version-number": "^0.3.6",
"react-native-web": "^0.18.11", "react-native-web": "^0.18.11",
"react-native-web-linear-gradient": "^1.1.2", "react-native-web-linear-gradient": "^1.1.2",
"react-responsive": "^9.0.2",
"rn-fetch-blob": "^0.12.0", "rn-fetch-blob": "^0.12.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tlds": "^1.234.0", "tlds": "^1.234.0",
@ -143,6 +144,7 @@
"@types/lodash.shuffle": "^4.2.7", "@types/lodash.shuffle": "^4.2.7",
"@types/react-avatar-editor": "^13.0.0", "@types/react-avatar-editor": "^13.0.0",
"@types/react-native": "^0.67.3", "@types/react-native": "^0.67.3",
"@types/react-responsive": "^8.0.5",
"@types/react-test-renderer": "^17.0.1", "@types/react-test-renderer": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.2", "@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2", "@typescript-eslint/parser": "^5.48.2",

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,7 @@ import {
FlatNavigatorParams, FlatNavigatorParams,
AllNavigatorParams, AllNavigatorParams,
} from 'lib/routes/types' } from 'lib/routes/types'
import {BottomBar} from './view/shell/BottomBar' import {BottomBar} from './view/shell/bottom-bar/BottomBar'
import {buildStateObject} from 'lib/routes/helpers' import {buildStateObject} from 'lib/routes/helpers'
import {State, RouteParams} from 'lib/routes/types' import {State, RouteParams} from 'lib/routes/types'
import {colors} from 'lib/styles' import {colors} from 'lib/styles'

View File

@ -0,0 +1,32 @@
import React from 'react'
import {useStores} from 'state/index'
import {Animated} from 'react-native'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
export function useMinimalShellMode() {
const store = useStores()
const minimalShellInterp = useAnimatedValue(0)
const footerMinimalShellTransform = {
transform: [{translateY: Animated.multiply(minimalShellInterp, 100)}],
}
React.useEffect(() => {
if (store.shell.minimalShellMode) {
Animated.timing(minimalShellInterp, {
toValue: 1,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
} else {
Animated.timing(minimalShellInterp, {
toValue: 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}
}, [minimalShellInterp, store.shell.minimalShellMode])
return {footerMinimalShellTransform}
}

View File

@ -0,0 +1,13 @@
import {useNavigationState} from '@react-navigation/native'
import {getTabState, TabState} from 'lib/routes/helpers'
export function useNavigationTabState() {
return useNavigationState(state => {
return {
isAtHome: getTabState(state, 'Home') !== TabState.Outside,
isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
isAtNotifications:
getTabState(state, 'Notifications') !== TabState.Outside,
}
})
}

View File

@ -0,0 +1,13 @@
import {useNavigationState} from '@react-navigation/native'
import {getCurrentRoute} from 'lib/routes/helpers'
export function useNavigationTabState() {
return useNavigationState(state => {
let currentRoute = state ? getCurrentRoute(state).name : 'Home'
return {
isAtHome: currentRoute === 'Home',
isAtSearch: currentRoute === 'Search',
isAtNotifications: currentRoute === 'Notifications',
}
})
}

View File

@ -0,0 +1,8 @@
import {useMediaQuery} from 'react-responsive'
export function useWebMediaQueries() {
const isDesktop = useMediaQuery({
query: '(min-width: 1230px)',
})
return {isDesktop}
}

View File

@ -7,8 +7,7 @@ import {s, colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {isDesktopWeb, isMobileWeb} from 'platform/detection' import {isMobileWeb} from 'platform/detection'
import {HelpTip} from './util/HelpTip'
export const SplashScreen = ({ export const SplashScreen = ({
onPressSignin, onPressSignin,
@ -40,24 +39,22 @@ export const SplashScreen = ({
<Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}> <Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}>
See what's next See what's next
</Text> </Text>
{isDesktopWeb && ( <View testID="signinOrCreateAccount" style={styles.btns}>
<View testID="signinOrCreateAccount" style={styles.btns}> <TouchableOpacity
<TouchableOpacity testID="createAccountButton"
testID="createAccountButton" style={[styles.btn, {backgroundColor: colors.blue3}]}
style={[styles.btn, {backgroundColor: colors.blue3}]} onPress={onPressCreateAccount}>
onPress={onPressCreateAccount}> <Text style={[s.white, styles.btnLabel]}>
<Text style={[s.white, styles.btnLabel]}> Create a new account
Create a new account </Text>
</Text> </TouchableOpacity>
</TouchableOpacity> <TouchableOpacity
<TouchableOpacity testID="signInButton"
testID="signInButton" style={[styles.btn, pal.btn]}
style={[styles.btn, pal.btn]} onPress={onPressSignin}>
onPress={onPressSignin}> <Text style={[pal.text, styles.btnLabel]}>Sign in</Text>
<Text style={[pal.text, styles.btnLabel]}>Sign in</Text> </TouchableOpacity>
</TouchableOpacity> </View>
</View>
)}
<Text <Text
type="xl" type="xl"
style={[styles.notice, pal.textLight]} style={[styles.notice, pal.textLight]}
@ -70,13 +67,6 @@ export const SplashScreen = ({
</TouchableOpacity>{' '} </TouchableOpacity>{' '}
to try the beta before it's publicly available. to try the beta before it's publicly available.
</Text> </Text>
{isMobileWeb && (
<>
<View style={[s.p20, s.mt10]}>
<HelpTip text="Beta testers: the mobile web app isn't quite ready yet. Log in on desktop web or using the iPhone app." />
</View>
</>
)}
</ErrorBoundary> </ErrorBoundary>
</View> </View>
<Footer /> <Footer />
@ -148,7 +138,8 @@ const styles = StyleSheet.create({
paddingBottom: 30, paddingBottom: 30,
}, },
btns: { btns: {
flexDirection: 'row', flexDirection: isMobileWeb ? 'column' : 'row',
gap: 20,
justifyContent: 'center', justifyContent: 'center',
paddingBottom: 40, paddingBottom: 40,
}, },
@ -156,7 +147,6 @@ const styles = StyleSheet.create({
borderRadius: 30, borderRadius: 30,
paddingHorizontal: 24, paddingHorizontal: 24,
paddingVertical: 12, paddingVertical: 12,
marginHorizontal: 10,
minWidth: 220, minWidth: 220,
}, },
btnLabel: { btnLabel: {

View File

@ -1,69 +1 @@
import React from 'react' export * from './FeedsTabBarMobile'
import {Animated, StyleSheet, TouchableOpacity} from 'react-native'
import {observer} from 'mobx-react-lite'
import {TabBar} from 'view/com/pager/TabBar'
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
import {UserAvatar} from '../util/UserAvatar'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
export const FeedsTabBar = observer(
(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
) => {
const store = useStores()
const pal = usePalette('default')
const interp = useAnimatedValue(0)
React.useEffect(() => {
Animated.timing(interp, {
toValue: store.shell.minimalShellMode ? 1 : 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}, [interp, store.shell.minimalShellMode])
const transform = {
transform: [{translateY: Animated.multiply(interp, -100)}],
}
const onPressAvi = React.useCallback(() => {
store.shell.openDrawer()
}, [store])
return (
<Animated.View style={[pal.view, styles.tabBar, transform]}>
<TouchableOpacity
testID="viewHeaderDrawerBtn"
style={styles.tabBarAvi}
onPress={onPressAvi}>
<UserAvatar avatar={store.me.avatar} size={30} />
</TouchableOpacity>
<TabBar
{...props}
items={['Following', "What's hot"]}
indicatorPosition="bottom"
indicatorColor={pal.colors.link}
/>
</Animated.View>
)
},
)
const styles = StyleSheet.create({
tabBar: {
position: 'absolute',
zIndex: 1,
left: 0,
right: 0,
top: 0,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 18,
},
tabBarAvi: {
marginTop: 1,
marginRight: 18,
},
})

View File

@ -4,10 +4,18 @@ import {TabBar} from 'view/com/pager/TabBar'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {RenderTabBarFnProps} from 'view/com/pager/Pager' import {RenderTabBarFnProps} from 'view/com/pager/Pager'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile'
export const FeedsTabBar = observer( export const FeedsTabBar = observer(
(props: RenderTabBarFnProps & {onPressSelected: () => void}) => { (props: RenderTabBarFnProps & {onPressSelected: () => void}) => {
const pal = usePalette('default') const pal = usePalette('default')
const {isDesktop} = useWebMediaQueries()
if (!isDesktop) {
return <FeedsTabBarMobile {...props} />
}
return ( return (
<CenteredView> <CenteredView>
<TabBar <TabBar

View File

@ -0,0 +1,69 @@
import React from 'react'
import {Animated, StyleSheet, TouchableOpacity} from 'react-native'
import {observer} from 'mobx-react-lite'
import {TabBar} from 'view/com/pager/TabBar'
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
import {UserAvatar} from '../util/UserAvatar'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
export const FeedsTabBar = observer(
(
props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void},
) => {
const store = useStores()
const pal = usePalette('default')
const interp = useAnimatedValue(0)
React.useEffect(() => {
Animated.timing(interp, {
toValue: store.shell.minimalShellMode ? 1 : 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}, [interp, store.shell.minimalShellMode])
const transform = {
transform: [{translateY: Animated.multiply(interp, -100)}],
}
const onPressAvi = React.useCallback(() => {
store.shell.openDrawer()
}, [store])
return (
<Animated.View style={[pal.view, styles.tabBar, transform]}>
<TouchableOpacity
testID="viewHeaderDrawerBtn"
style={styles.tabBarAvi}
onPress={onPressAvi}>
<UserAvatar avatar={store.me.avatar} size={30} />
</TouchableOpacity>
<TabBar
{...props}
items={['Following', "What's hot"]}
indicatorPosition="bottom"
indicatorColor={pal.colors.link}
/>
</Animated.View>
)
},
)
const styles = StyleSheet.create({
tabBar: {
position: 'absolute',
zIndex: 1,
left: 0,
right: 0,
top: 0,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 18,
},
tabBarAvi: {
marginTop: 1,
marginRight: 18,
},
})

View File

@ -1,71 +0,0 @@
import React from 'react'
import {observer} from 'mobx-react-lite'
import {
Animated,
GestureResponderEvent,
StyleSheet,
TouchableWithoutFeedback,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {gradients} from 'lib/styles'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useStores} from 'state/index'
type OnPress = ((event: GestureResponderEvent) => void) | undefined
export const FAB = observer(
({
testID,
icon,
onPress,
}: {
testID?: string
icon: JSX.Element
onPress: OnPress
}) => {
const store = useStores()
const interp = useAnimatedValue(0)
React.useEffect(() => {
Animated.timing(interp, {
toValue: store.shell.minimalShellMode ? 1 : 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}, [interp, store.shell.minimalShellMode])
const transform = {
transform: [{translateY: Animated.multiply(interp, 60)}],
}
return (
<TouchableWithoutFeedback testID={testID} onPress={onPress}>
<Animated.View style={[styles.outer, transform]}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.inner}>
{icon}
</LinearGradient>
</Animated.View>
</TouchableWithoutFeedback>
)
},
)
const styles = StyleSheet.create({
outer: {
position: 'absolute',
zIndex: 1,
right: 28,
bottom: 94,
width: 60,
height: 60,
borderRadius: 30,
},
inner: {
width: 60,
height: 60,
borderRadius: 30,
justifyContent: 'center',
alignItems: 'center',
},
})

View File

@ -1,8 +0,0 @@
import React from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
type OnPress = ((event: GestureResponderEvent) => void) | undefined
export const FAB = (_opts: {icon: IconProp; onPress: OnPress}) => {
return <View />
}

View File

@ -55,9 +55,10 @@ export function show(text: string) {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
position: 'absolute', position: 'absolute',
right: 20, left: 20,
bottom: 20, bottom: 20,
width: 350, width: 'calc(100% - 40px)',
maxWidth: 350,
padding: 20, padding: 20,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',

View File

@ -0,0 +1 @@
export {FABInner as FAB} from './FABInner'

View File

@ -0,0 +1,14 @@
import React from 'react'
import {View} from 'react-native'
import {FABInner, FABProps} from './FABInner'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
export const FAB = (_opts: FABProps) => {
const {isDesktop} = useWebMediaQueries()
if (!isDesktop) {
return <FABInner {..._opts} />
}
return <View />
}

View File

@ -0,0 +1,72 @@
import React from 'react'
import {observer} from 'mobx-react-lite'
import {
Animated,
GestureResponderEvent,
StyleSheet,
TouchableWithoutFeedback,
} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {gradients} from 'lib/styles'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {useStores} from 'state/index'
import {isMobileWeb} from 'platform/detection'
type OnPress = ((event: GestureResponderEvent) => void) | undefined
export interface FABProps {
testID?: string
icon: JSX.Element
onPress: OnPress
}
export const FABInner = observer(({testID, icon, onPress}: FABProps) => {
const store = useStores()
const interp = useAnimatedValue(0)
React.useEffect(() => {
Animated.timing(interp, {
toValue: store.shell.minimalShellMode ? 1 : 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}, [interp, store.shell.minimalShellMode])
const transform = {
transform: [{translateY: Animated.multiply(interp, 60)}],
}
return (
<TouchableWithoutFeedback testID={testID} onPress={onPress}>
<Animated.View
style={[styles.outer, isMobileWeb && styles.mobileWebOuter, transform]}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.inner}>
{icon}
</LinearGradient>
</Animated.View>
</TouchableWithoutFeedback>
)
})
const styles = StyleSheet.create({
outer: {
position: 'absolute',
zIndex: 1,
right: 28,
bottom: 94,
width: 60,
height: 60,
borderRadius: 30,
},
mobileWebOuter: {
bottom: 114,
},
inner: {
width: 60,
height: 60,
borderRadius: 30,
justifyContent: 'center',
alignItems: 'center',
},
})

View File

@ -11,14 +11,15 @@ import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
import {LoadLatestBtn} from '../com/util/LoadLatestBtn' import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
import {FeedsTabBar} from '../com/pager/FeedsTabBar' import {FeedsTabBar} from '../com/pager/FeedsTabBar'
import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager' import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {FAB} from '../com/util/FAB' import {FAB} from '../com/util/fab/FAB'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {useAnalytics} from 'lib/analytics' import {useAnalytics} from 'lib/analytics'
import {ComposeIcon2} from 'lib/icons' import {ComposeIcon2} from 'lib/icons'
import {isDesktopWeb, isMobileWeb} from 'platform/detection'
const HEADER_OFFSET = 40 const HEADER_OFFSET = isDesktopWeb ? 0 : isMobileWeb ? 20 : 40
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export const HomeScreen = withAuthRequired((_opts: Props) => { export const HomeScreen = withAuthRequired((_opts: Props) => {

View File

@ -16,7 +16,7 @@ import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {ErrorMessage} from '../com/util/error/ErrorMessage' import {ErrorMessage} from '../com/util/error/ErrorMessage'
import {EmptyState} from '../com/util/EmptyState' import {EmptyState} from '../com/util/EmptyState'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {FAB} from '../com/util/FAB' import {FAB} from '../com/util/fab/FAB'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
import {useAnalytics} from 'lib/analytics' import {useAnalytics} from 'lib/analytics'

View File

@ -15,12 +15,15 @@ import {
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import * as Mobile from './SearchMobile'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'> type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
export const SearchScreen = withAuthRequired( export const SearchScreen = withAuthRequired(
observer(({route}: Props) => { observer(({navigation, route}: Props) => {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores() const store = useStores()
const params = route.params || {}
const foafs = React.useMemo<FoafsModel>( const foafs = React.useMemo<FoafsModel>(
() => new FoafsModel(store), () => new FoafsModel(store),
[store], [store],
@ -30,13 +33,13 @@ export const SearchScreen = withAuthRequired(
[store], [store],
) )
const searchUIModel = React.useMemo<SearchUIModel | undefined>( const searchUIModel = React.useMemo<SearchUIModel | undefined>(
() => (route.params.q ? new SearchUIModel(store) : undefined), () => (params.q ? new SearchUIModel(store) : undefined),
[route.params.q, store], [params.q, store],
) )
React.useEffect(() => { React.useEffect(() => {
if (route.params.q && searchUIModel) { if (params.q && searchUIModel) {
searchUIModel.fetch(route.params.q) searchUIModel.fetch(params.q)
} }
if (!foafs.hasData) { if (!foafs.hasData) {
foafs.fetch() foafs.fetch()
@ -44,12 +47,18 @@ export const SearchScreen = withAuthRequired(
if (!suggestedActors.hasLoaded) { if (!suggestedActors.hasLoaded) {
suggestedActors.loadMore(true) suggestedActors.loadMore(true)
} }
}, [foafs, suggestedActors, searchUIModel, route.params.q]) }, [foafs, suggestedActors, searchUIModel, params.q])
if (searchUIModel) { if (searchUIModel) {
return <SearchResults model={searchUIModel} /> return <SearchResults model={searchUIModel} />
} }
const {isDesktop} = useWebMediaQueries()
if (!isDesktop) {
return <Mobile.SearchScreen navigation={navigation} route={route} />
}
return ( return (
<ScrollView <ScrollView
testID="searchScrollView" testID="searchScrollView"

View File

@ -0,0 +1,195 @@
import React from 'react'
import {
Keyboard,
RefreshControl,
StyleSheet,
TouchableWithoutFeedback,
View,
} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ScrollView} from 'view/com/util/Views'
import {
NativeStackScreenProps,
SearchTabNavigatorParams,
} from 'lib/routes/types'
import {observer} from 'mobx-react-lite'
import {Text} from 'view/com/util/text/Text'
import {useStores} from 'state/index'
import {UserAutocompleteModel} from 'state/models/discovery/user-autocomplete'
import {SearchUIModel} from 'state/models/ui/search'
import {FoafsModel} from 'state/models/discovery/foafs'
import {SuggestedActorsModel} from 'state/models/discovery/suggested-actors'
import {HeaderWithInput} from 'view/com/search/HeaderWithInput'
import {Suggestions} from 'view/com/search/Suggestions'
import {SearchResults} from 'view/com/search/SearchResults'
import {s} from 'lib/styles'
import {ProfileCard} from 'view/com/profile/ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
export const SearchScreen = withAuthRequired(
observer<Props>(({}: Props) => {
const pal = usePalette('default')
const store = useStores()
const scrollElRef = React.useRef<ScrollView>(null)
const onMainScroll = useOnMainScroll(store)
const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false)
const [query, setQuery] = React.useState<string>('')
const autocompleteView = React.useMemo<UserAutocompleteModel>(
() => new UserAutocompleteModel(store),
[store],
)
const foafs = React.useMemo<FoafsModel>(
() => new FoafsModel(store),
[store],
)
const suggestedActors = React.useMemo<SuggestedActorsModel>(
() => new SuggestedActorsModel(store),
[store],
)
const [searchUIModel, setSearchUIModel] = React.useState<
SearchUIModel | undefined
>()
const [refreshing, setRefreshing] = React.useState(false)
const onSoftReset = () => {
scrollElRef.current?.scrollTo({x: 0, y: 0})
}
useFocusEffect(
React.useCallback(() => {
const softResetSub = store.onScreenSoftReset(onSoftReset)
const cleanup = () => {
softResetSub.remove()
}
store.shell.setMinimalShellMode(false)
autocompleteView.setup()
if (!foafs.hasData) {
foafs.fetch()
}
if (!suggestedActors.hasLoaded) {
suggestedActors.loadMore(true)
}
return cleanup
}, [store, autocompleteView, foafs, suggestedActors]),
)
const onChangeQuery = React.useCallback(
(text: string) => {
setQuery(text)
if (text.length > 0) {
autocompleteView.setActive(true)
autocompleteView.setPrefix(text)
} else {
autocompleteView.setActive(false)
}
},
[setQuery, autocompleteView],
)
const onPressClearQuery = React.useCallback(() => {
setQuery('')
}, [setQuery])
const onPressCancelSearch = React.useCallback(() => {
setQuery('')
autocompleteView.setActive(false)
setSearchUIModel(undefined)
store.shell.setIsDrawerSwipeDisabled(false)
}, [setQuery, autocompleteView, store])
const onSubmitQuery = React.useCallback(() => {
const model = new SearchUIModel(store)
model.fetch(query)
setSearchUIModel(model)
store.shell.setIsDrawerSwipeDisabled(true)
}, [query, setSearchUIModel, store])
const onRefresh = React.useCallback(async () => {
setRefreshing(true)
try {
await foafs.fetch()
} finally {
setRefreshing(false)
}
}, [foafs, setRefreshing])
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={[pal.view, styles.container]}>
<HeaderWithInput
isInputFocused={isInputFocused}
query={query}
setIsInputFocused={setIsInputFocused}
onChangeQuery={onChangeQuery}
onPressClearQuery={onPressClearQuery}
onPressCancelSearch={onPressCancelSearch}
onSubmitQuery={onSubmitQuery}
/>
{searchUIModel ? (
<SearchResults model={searchUIModel} />
) : (
<ScrollView
ref={scrollElRef}
testID="searchScrollView"
style={pal.view}
onScroll={onMainScroll}
scrollEventThrottle={100}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}>
{query && autocompleteView.searchRes.length ? (
<>
{autocompleteView.searchRes.map(item => (
<ProfileCard
key={item.did}
testID={`searchAutoCompleteResult-${item.handle}`}
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
/>
))}
</>
) : query && !autocompleteView.searchRes.length ? (
<View>
<Text style={[pal.textLight, styles.searchPrompt]}>
No results found for {autocompleteView.prefix}
</Text>
</View>
) : isInputFocused ? (
<View>
<Text style={[pal.textLight, styles.searchPrompt]}>
Search for users on the network
</Text>
</View>
) : (
<Suggestions foafs={foafs} suggestedActors={suggestedActors} />
)}
<View style={s.footerSpacer} />
</ScrollView>
)}
</View>
</TouchableWithoutFeedback>
)
}),
)
const styles = StyleSheet.create({
container: {
flex: 1,
},
searchPrompt: {
textAlign: 'center',
paddingTop: 10,
},
})

View File

@ -4,6 +4,7 @@ import {StyleSheet, View} from 'react-native'
import {ComposePost} from '../com/composer/Composer' import {ComposePost} from '../com/composer/Composer'
import {ComposerOpts} from 'state/models/ui/shell' import {ComposerOpts} from 'state/models/ui/shell'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {isMobileWeb} from 'platform/detection'
export const Composer = observer( export const Composer = observer(
({ ({
@ -60,7 +61,7 @@ const styles = StyleSheet.create({
width: '100%', width: '100%',
paddingVertical: 0, paddingVertical: 0,
paddingHorizontal: 2, paddingHorizontal: 2,
borderRadius: 8, borderRadius: isMobileWeb ? 0 : 8,
marginBottom: '10vh', marginBottom: '10vh',
}, },
}) })

View File

@ -8,11 +8,7 @@ import {
View, View,
ViewStyle, ViewStyle,
} from 'react-native' } from 'react-native'
import { import {useNavigation, StackActions} from '@react-navigation/native'
useNavigation,
useNavigationState,
StackActions,
} from '@react-navigation/native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
@ -40,6 +36,8 @@ import {useAnalytics} from 'lib/analytics'
import {pluralize} from 'lib/strings/helpers' import {pluralize} from 'lib/strings/helpers'
import {getTabState, TabState} from 'lib/routes/helpers' import {getTabState, TabState} from 'lib/routes/helpers'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
import {isWeb} from 'platform/detection'
export const DrawerContent = observer(() => { export const DrawerContent = observer(() => {
const theme = useTheme() const theme = useTheme()
@ -47,16 +45,7 @@ export const DrawerContent = observer(() => {
const store = useStores() const store = useStores()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const {track} = useAnalytics() const {track} = useAnalytics()
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState( const {isAtHome, isAtSearch, isAtNotifications} = useNavigationTabState()
state => {
return {
isAtHome: getTabState(state, 'Home') !== TabState.Outside,
isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
isAtNotifications:
getTabState(state, 'Notifications') !== TabState.Outside,
}
},
)
// events // events
// = // =
@ -66,14 +55,19 @@ export const DrawerContent = observer(() => {
track('Menu:ItemClicked', {url: tab}) track('Menu:ItemClicked', {url: tab})
const state = navigation.getState() const state = navigation.getState()
store.shell.closeDrawer() store.shell.closeDrawer()
const tabState = getTabState(state, tab) if (isWeb) {
if (tabState === TabState.InsideAtRoot) {
store.emitScreenSoftReset()
} else if (tabState === TabState.Inside) {
navigation.dispatch(StackActions.popToTop())
} else {
// @ts-ignore must be Home, Search, or Notifications // @ts-ignore must be Home, Search, or Notifications
navigation.navigate(`${tab}Tab`) navigation.navigate(tab)
} else {
const tabState = getTabState(state, tab)
if (tabState === TabState.InsideAtRoot) {
store.emitScreenSoftReset()
} else if (tabState === TabState.Inside) {
navigation.dispatch(StackActions.popToTop())
} else {
// @ts-ignore must be Home, Search, or Notifications
navigation.navigate(`${tab}Tab`)
}
} }
}, },
[store, track, navigation], [store, track, navigation],
@ -240,20 +234,22 @@ export const DrawerContent = observer(() => {
</View> </View>
<View style={s.flex1} /> <View style={s.flex1} />
<View style={styles.footer}> <View style={styles.footer}>
<TouchableOpacity {!isWeb && (
onPress={onDarkmodePress} <TouchableOpacity
style={[ onPress={onDarkmodePress}
styles.footerBtn, style={[
theme.colorScheme === 'light' styles.footerBtn,
? pal.btn theme.colorScheme === 'light'
: styles.footerBtnDarkMode, ? pal.btn
]}> : styles.footerBtnDarkMode,
<MoonIcon ]}>
size={22} <MoonIcon
style={pal.text as StyleProp<ViewStyle>} size={22}
strokeWidth={2} style={pal.text as StyleProp<ViewStyle>}
/> strokeWidth={2}
</TouchableOpacity> />
</TouchableOpacity>
)}
<TouchableOpacity <TouchableOpacity
onPress={onPressFeedback} onPress={onPressFeedback}
style={[ style={[

View File

@ -2,7 +2,6 @@ import React from 'react'
import { import {
Animated, Animated,
GestureResponderEvent, GestureResponderEvent,
StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
} from 'react-native' } from 'react-native'
@ -13,7 +12,6 @@ import {observer} from 'mobx-react-lite'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {useAnalytics} from 'lib/analytics' import {useAnalytics} from 'lib/analytics'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {clamp} from 'lib/numbers' import {clamp} from 'lib/numbers'
import { import {
HomeIcon, HomeIcon,
@ -24,14 +22,14 @@ import {
BellIconSolid, BellIconSolid,
UserIcon, UserIcon,
} from 'lib/icons' } from 'lib/icons'
import {colors} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {getTabState, TabState} from 'lib/routes/helpers' import {getTabState, TabState} from 'lib/routes/helpers'
import {styles} from './BottomBarStyles'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
export const BottomBar = observer(({navigation}: BottomTabBarProps) => { export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
const store = useStores() const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const minimalShellInterp = useAnimatedValue(0)
const safeAreaInsets = useSafeAreaInsets() const safeAreaInsets = useSafeAreaInsets()
const {track} = useAnalytics() const {track} = useAnalytics()
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState( const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState(
@ -52,26 +50,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
}, },
) )
React.useEffect(() => { const {footerMinimalShellTransform} = useMinimalShellMode()
if (store.shell.minimalShellMode) {
Animated.timing(minimalShellInterp, {
toValue: 1,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
} else {
Animated.timing(minimalShellInterp, {
toValue: 0,
duration: 100,
useNativeDriver: true,
isInteraction: false,
}).start()
}
}, [minimalShellInterp, store.shell.minimalShellMode])
const footerMinimalShellTransform = {
transform: [{translateY: Animated.multiply(minimalShellInterp, 100)}],
}
const onPressTab = React.useCallback( const onPressTab = React.useCallback(
(tab: string) => { (tab: string) => {
@ -217,62 +196,3 @@ function Btn({
</TouchableOpacity> </TouchableOpacity>
) )
} }
const styles = StyleSheet.create({
bottomBar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flexDirection: 'row',
borderTopWidth: 1,
paddingLeft: 5,
paddingRight: 10,
},
ctrl: {
flex: 1,
paddingTop: 13,
paddingBottom: 4,
},
notificationCount: {
position: 'absolute',
left: '52%',
top: 8,
backgroundColor: colors.blue3,
paddingHorizontal: 4,
paddingBottom: 1,
borderRadius: 6,
zIndex: 1,
},
notificationCountLight: {
borderColor: colors.white,
},
notificationCountDark: {
borderColor: colors.gray8,
},
notificationCountLabel: {
fontSize: 12,
fontWeight: 'bold',
color: colors.white,
fontVariant: ['tabular-nums'],
},
ctrlIcon: {
marginLeft: 'auto',
marginRight: 'auto',
},
ctrlIconSizingWrapper: {
height: 27,
},
homeIcon: {
top: 0,
},
searchIcon: {
top: -2,
},
bellIcon: {
top: -2.5,
},
profileIcon: {
top: -4,
},
})

View File

@ -0,0 +1,61 @@
import {StyleSheet} from 'react-native'
import {colors} from 'lib/styles'
export const styles = StyleSheet.create({
bottomBar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
flexDirection: 'row',
borderTopWidth: 1,
paddingLeft: 5,
paddingRight: 10,
},
ctrl: {
flex: 1,
paddingTop: 13,
paddingBottom: 4,
},
notificationCount: {
position: 'absolute',
left: '52%',
top: 8,
backgroundColor: colors.blue3,
paddingHorizontal: 4,
paddingBottom: 1,
borderRadius: 6,
zIndex: 1,
},
notificationCountLight: {
borderColor: colors.white,
},
notificationCountDark: {
borderColor: colors.gray8,
},
notificationCountLabel: {
fontSize: 12,
fontWeight: 'bold',
color: colors.white,
fontVariant: ['tabular-nums'],
},
ctrlIcon: {
marginLeft: 'auto',
marginRight: 'auto',
},
ctrlIconSizingWrapper: {
height: 27,
},
homeIcon: {
top: 0,
},
searchIcon: {
top: -2,
},
bellIcon: {
top: -2.5,
},
profileIcon: {
top: -4,
},
})

View File

@ -0,0 +1,101 @@
import React from 'react'
import {observer} from 'mobx-react-lite'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {Animated} from 'react-native'
import {useNavigationState} from '@react-navigation/native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {getCurrentRoute, isTab} from 'lib/routes/helpers'
import {styles} from './BottomBarStyles'
import {clamp} from 'lib/numbers'
import {
BellIcon,
BellIconSolid,
HomeIcon,
HomeIconSolid,
MagnifyingGlassIcon2,
MagnifyingGlassIcon2Solid,
UserIcon,
} from 'lib/icons'
import {Link} from 'view/com/util/Link'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
export const BottomBarWeb = observer(() => {
const store = useStores()
const pal = usePalette('default')
const safeAreaInsets = useSafeAreaInsets()
const {footerMinimalShellTransform} = useMinimalShellMode()
return (
<Animated.View
style={[
styles.bottomBar,
pal.view,
pal.border,
{paddingBottom: clamp(safeAreaInsets.bottom, 15, 30)},
footerMinimalShellTransform,
]}>
<NavItem routeName="Home" href="/">
{({isActive}) => {
const Icon = isActive ? HomeIconSolid : HomeIcon
return (
<Icon
strokeWidth={4}
size={24}
style={[styles.ctrlIcon, pal.text, styles.homeIcon]}
/>
)
}}
</NavItem>
<NavItem routeName="Search" href="/search">
{({isActive}) => {
const Icon = isActive
? MagnifyingGlassIcon2Solid
: MagnifyingGlassIcon2
return (
<Icon
size={25}
style={[styles.ctrlIcon, pal.text, styles.searchIcon]}
strokeWidth={1.8}
/>
)
}}
</NavItem>
<NavItem routeName="Notifications" href="/notifications">
{({isActive}) => {
const Icon = isActive ? BellIconSolid : BellIcon
return (
<Icon
size={24}
strokeWidth={1.9}
style={[styles.ctrlIcon, pal.text, styles.bellIcon]}
/>
)
}}
</NavItem>
<NavItem routeName="Profile" href={`/profile/${store.me.handle}`}>
{() => (
<UserIcon
size={28}
strokeWidth={1.5}
style={[styles.ctrlIcon, pal.text, styles.profileIcon]}
/>
)}
</NavItem>
</Animated.View>
)
})
const NavItem: React.FC<{
children: (props: {isActive: boolean}) => React.ReactChild
href: string
routeName: string
}> = ({children, href, routeName}) => {
const currentRoute = useNavigationState(getCurrentRoute)
const isActive = isTab(currentRoute.name, routeName)
return (
<Link href={href} style={styles.ctrl}>
{children({isActive})}
</Link>
)
}

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {View, StyleSheet} from 'react-native' import {View, StyleSheet, TouchableOpacity} from 'react-native'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {DesktopLeftNav} from './desktop/LeftNav' import {DesktopLeftNav} from './desktop/LeftNav'
import {DesktopRightNav} from './desktop/RightNav' import {DesktopRightNav} from './desktop/RightNav'
@ -11,9 +11,13 @@ import {Composer} from './Composer.web'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {s, colors} from 'lib/styles' import {s, colors} from 'lib/styles'
import {RoutesContainer, FlatNavigator} from '../../Navigation' import {RoutesContainer, FlatNavigator} from '../../Navigation'
import {DrawerContent} from './Drawer'
import {useWebMediaQueries} from '../../lib/hooks/useWebMediaQueries'
import {BottomBarWeb} from './bottom-bar/BottomBarWeb'
const ShellInner = observer(() => { const ShellInner = observer(() => {
const store = useStores() const store = useStores()
const {isDesktop} = useWebMediaQueries()
return ( return (
<> <>
@ -22,10 +26,14 @@ const ShellInner = observer(() => {
<FlatNavigator /> <FlatNavigator />
</ErrorBoundary> </ErrorBoundary>
</View> </View>
<DesktopLeftNav /> {isDesktop && (
<DesktopRightNav /> <>
<View style={[styles.viewBorder, styles.viewBorderLeft]} /> <DesktopLeftNav />
<View style={[styles.viewBorder, styles.viewBorderRight]} /> <DesktopRightNav />
<View style={[styles.viewBorder, styles.viewBorderLeft]} />
<View style={[styles.viewBorder, styles.viewBorderRight]} />
</>
)}
<Composer <Composer
active={store.shell.isComposerActive} active={store.shell.isComposerActive}
onClose={() => store.shell.closeComposer()} onClose={() => store.shell.closeComposer()}
@ -34,8 +42,18 @@ const ShellInner = observer(() => {
quote={store.shell.composerOpts?.quote} quote={store.shell.composerOpts?.quote}
onPost={store.shell.composerOpts?.onPost} onPost={store.shell.composerOpts?.onPost}
/> />
{!isDesktop && <BottomBarWeb />}
<ModalsContainer /> <ModalsContainer />
<Lightbox /> <Lightbox />
{!isDesktop && store.shell.isDrawerOpen && (
<TouchableOpacity
onPress={() => store.shell.closeDrawer()}
style={styles.drawerMask}>
<View style={styles.drawerContainer}>
<DrawerContent />
</View>
</TouchableOpacity>
)}
</> </>
) )
}) })
@ -71,4 +89,19 @@ const styles = StyleSheet.create({
viewBorderRight: { viewBorderRight: {
left: 'calc(50vw + 300px)', left: 'calc(50vw + 300px)',
}, },
drawerMask: {
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
backgroundColor: 'rgba(0,0,0,0.25)',
},
drawerContainer: {
display: 'flex',
position: 'absolute',
top: 0,
left: 0,
height: '100%',
},
}) })

View File

@ -4615,6 +4615,13 @@
dependencies: dependencies:
"@types/react" "^17" "@types/react" "^17"
"@types/react-responsive@^8.0.5":
version "8.0.5"
resolved "https://registry.yarnpkg.com/@types/react-responsive/-/react-responsive-8.0.5.tgz#77769862d2a0711434feb972be08e3e6c334440a"
integrity sha512-k3gQJgI87oP5IrVZe//3LKJFnAeFaqqWmmtl5eoYL2H3HqFcIhUaE30kRK1CsW3DHdojZxcVj4ZNc2ClsEu2PA==
dependencies:
"@types/react" "*"
"@types/react-test-renderer@^17.0.1": "@types/react-test-renderer@^17.0.1":
version "17.0.2" version "17.0.2"
resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.2.tgz#5f800a39b12ac8d2a2149e7e1885215bcf4edbbf" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.2.tgz#5f800a39b12ac8d2a2149e7e1885215bcf4edbbf"
@ -6836,6 +6843,11 @@ css-loader@^6.5.1:
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
semver "^7.3.8" semver "^7.3.8"
css-mediaquery@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/css-mediaquery/-/css-mediaquery-0.1.2.tgz#6a2c37344928618631c54bd33cedd301da18bea0"
integrity sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==
css-minimizer-webpack-plugin@^3.2.0, css-minimizer-webpack-plugin@^3.4.1: css-minimizer-webpack-plugin@^3.2.0, css-minimizer-webpack-plugin@^3.4.1:
version "3.4.1" version "3.4.1"
resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz#ab78f781ced9181992fe7b6e4f3422e76429878f" resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz#ab78f781ced9181992fe7b6e4f3422e76429878f"
@ -9627,7 +9639,7 @@ humps@^2.0.1:
resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa" resolved "https://registry.yarnpkg.com/humps/-/humps-2.0.1.tgz#dd02ea6081bd0568dc5d073184463957ba9ef9aa"
integrity sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g== integrity sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==
hyphenate-style-name@^1.0.3: hyphenate-style-name@^1.0.0, hyphenate-style-name@^1.0.3:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
@ -11896,6 +11908,13 @@ markdown-it@^13.0.1:
mdurl "^1.0.1" mdurl "^1.0.1"
uc.micro "^1.0.5" uc.micro "^1.0.5"
matchmediaquery@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/matchmediaquery/-/matchmediaquery-0.3.1.tgz#8247edc47e499ebb7c58f62a9ff9ccf5b815c6d7"
integrity sha512-Hlk20WQHRIm9EE9luN1kjRjYXAQToHOIAHPJn9buxBwuhfTHoKUcX+lXBbxc85DVQfXYbEQ4HcwQdd128E3qHQ==
dependencies:
css-mediaquery "^0.1.2"
md5-file@^3.2.3: md5-file@^3.2.3:
version "3.2.3" version "3.2.3"
resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-3.2.3.tgz#f9bceb941eca2214a4c0727f5e700314e770f06f" resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-3.2.3.tgz#f9bceb941eca2214a4c0727f5e700314e770f06f"
@ -14235,7 +14254,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.0, prompts@^2.4.2:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types@*, prop-types@^15.7.2, prop-types@^15.8.1: prop-types@*, prop-types@^15.6.1, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -14869,6 +14888,16 @@ react-refresh@^0.4.0:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.4.3.tgz#966f1750c191672e76e16c2efa569150cc73ab53" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.4.3.tgz#966f1750c191672e76e16c2efa569150cc73ab53"
integrity sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA== integrity sha512-Hwln1VNuGl/6bVwnd0Xdn1e84gT/8T9aYNL+HAKDArLCS7LWjwr7StE30IEYbIkx0Vi3vs+coQxe+SQDbGbbpA==
react-responsive@^9.0.2:
version "9.0.2"
resolved "https://registry.yarnpkg.com/react-responsive/-/react-responsive-9.0.2.tgz#34531ca77a61e7a8775714016d21241df7e4205c"
integrity sha512-+4CCab7z8G8glgJoRjAwocsgsv6VA2w7JPxFWHRc7kvz8mec1/K5LutNC2MG28Mn8mu6+bu04XZxHv5gyfT7xQ==
dependencies:
hyphenate-style-name "^1.0.0"
matchmediaquery "^0.3.0"
prop-types "^15.6.1"
shallow-equal "^1.2.1"
react-scripts@^5.0.1: react-scripts@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-5.0.1.tgz#6285dbd65a8ba6e49ca8d651ce30645a6d980003"
@ -15676,6 +15705,11 @@ shallow-clone@^3.0.0:
dependencies: dependencies:
kind-of "^6.0.2" kind-of "^6.0.2"
shallow-equal@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==
sharp@^0.31.2: sharp@^0.31.2:
version "0.31.3" version "0.31.3"
resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.31.3.tgz#60227edc5c2be90e7378a210466c99aefcf32688" resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.31.3.tgz#60227edc5c2be90e7378a210466c99aefcf32688"