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
parent
2fed6c4021
commit
f6769b283f
|
@ -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',
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
@ -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'
|
||||||
|
|
|
@ -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}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -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',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import {useMediaQuery} from 'react-responsive'
|
||||||
|
|
||||||
|
export function useWebMediaQueries() {
|
||||||
|
const isDesktop = useMediaQuery({
|
||||||
|
query: '(min-width: 1230px)',
|
||||||
|
})
|
||||||
|
return {isDesktop}
|
||||||
|
}
|
|
@ -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: {
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
|
@ -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',
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -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 />
|
|
||||||
}
|
|
|
@ -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',
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export {FABInner as FAB} from './FABInner'
|
|
@ -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 />
|
||||||
|
}
|
|
@ -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',
|
||||||
|
},
|
||||||
|
})
|
|
@ -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) => {
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
|
@ -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',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -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={[
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
})
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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%',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
38
yarn.lock
38
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue