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>
This commit is contained in:
parent
2fed6c4021
commit
f6769b283f
30 changed files with 4343 additions and 319 deletions
|
@ -14,7 +14,7 @@ import {
|
|||
FlatNavigatorParams,
|
||||
AllNavigatorParams,
|
||||
} 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 {State, RouteParams} from 'lib/routes/types'
|
||||
import {colors} from 'lib/styles'
|
||||
|
|
32
src/lib/hooks/useMinimalShellMode.tsx
Normal file
32
src/lib/hooks/useMinimalShellMode.tsx
Normal 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}
|
||||
}
|
13
src/lib/hooks/useNavigationTabState.ts
Normal file
13
src/lib/hooks/useNavigationTabState.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
}
|
13
src/lib/hooks/useNavigationTabState.web.ts
Normal file
13
src/lib/hooks/useNavigationTabState.web.ts
Normal 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',
|
||||
}
|
||||
})
|
||||
}
|
8
src/lib/hooks/useWebMediaQueries.tsx
Normal file
8
src/lib/hooks/useWebMediaQueries.tsx
Normal file
|
@ -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 {useStores} from 'state/index'
|
||||
import {CenteredView} from '../util/Views'
|
||||
import {isDesktopWeb, isMobileWeb} from 'platform/detection'
|
||||
import {HelpTip} from './util/HelpTip'
|
||||
import {isMobileWeb} from 'platform/detection'
|
||||
|
||||
export const SplashScreen = ({
|
||||
onPressSignin,
|
||||
|
@ -40,24 +39,22 @@ export const SplashScreen = ({
|
|||
<Text style={isMobileWeb ? styles.subtitleMobile : styles.subtitle}>
|
||||
See what's next
|
||||
</Text>
|
||||
{isDesktopWeb && (
|
||||
<View testID="signinOrCreateAccount" style={styles.btns}>
|
||||
<TouchableOpacity
|
||||
testID="createAccountButton"
|
||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||
onPress={onPressCreateAccount}>
|
||||
<Text style={[s.white, styles.btnLabel]}>
|
||||
Create a new account
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="signInButton"
|
||||
style={[styles.btn, pal.btn]}
|
||||
onPress={onPressSignin}>
|
||||
<Text style={[pal.text, styles.btnLabel]}>Sign in</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
<View testID="signinOrCreateAccount" style={styles.btns}>
|
||||
<TouchableOpacity
|
||||
testID="createAccountButton"
|
||||
style={[styles.btn, {backgroundColor: colors.blue3}]}
|
||||
onPress={onPressCreateAccount}>
|
||||
<Text style={[s.white, styles.btnLabel]}>
|
||||
Create a new account
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
testID="signInButton"
|
||||
style={[styles.btn, pal.btn]}
|
||||
onPress={onPressSignin}>
|
||||
<Text style={[pal.text, styles.btnLabel]}>Sign in</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text
|
||||
type="xl"
|
||||
style={[styles.notice, pal.textLight]}
|
||||
|
@ -70,13 +67,6 @@ export const SplashScreen = ({
|
|||
</TouchableOpacity>{' '}
|
||||
to try the beta before it's publicly available.
|
||||
</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>
|
||||
</View>
|
||||
<Footer />
|
||||
|
@ -148,7 +138,8 @@ const styles = StyleSheet.create({
|
|||
paddingBottom: 30,
|
||||
},
|
||||
btns: {
|
||||
flexDirection: 'row',
|
||||
flexDirection: isMobileWeb ? 'column' : 'row',
|
||||
gap: 20,
|
||||
justifyContent: 'center',
|
||||
paddingBottom: 40,
|
||||
},
|
||||
|
@ -156,7 +147,6 @@ const styles = StyleSheet.create({
|
|||
borderRadius: 30,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
marginHorizontal: 10,
|
||||
minWidth: 220,
|
||||
},
|
||||
btnLabel: {
|
||||
|
|
|
@ -1,69 +1 @@
|
|||
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,
|
||||
},
|
||||
})
|
||||
export * from './FeedsTabBarMobile'
|
||||
|
|
|
@ -4,10 +4,18 @@ import {TabBar} from 'view/com/pager/TabBar'
|
|||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile'
|
||||
|
||||
export const FeedsTabBar = observer(
|
||||
(props: RenderTabBarFnProps & {onPressSelected: () => void}) => {
|
||||
const pal = usePalette('default')
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
|
||||
if (!isDesktop) {
|
||||
return <FeedsTabBarMobile {...props} />
|
||||
}
|
||||
|
||||
return (
|
||||
<CenteredView>
|
||||
<TabBar
|
||||
|
|
69
src/view/com/pager/FeedsTabBarMobile.tsx
Normal file
69
src/view/com/pager/FeedsTabBarMobile.tsx
Normal 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,
|
||||
},
|
||||
})
|
|
@ -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({
|
||||
container: {
|
||||
position: 'absolute',
|
||||
right: 20,
|
||||
left: 20,
|
||||
bottom: 20,
|
||||
width: 350,
|
||||
width: 'calc(100% - 40px)',
|
||||
maxWidth: 350,
|
||||
padding: 20,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
|
1
src/view/com/util/fab/FAB.tsx
Normal file
1
src/view/com/util/fab/FAB.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export {FABInner as FAB} from './FABInner'
|
14
src/view/com/util/fab/FAB.web.tsx
Normal file
14
src/view/com/util/fab/FAB.web.tsx
Normal 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 />
|
||||
}
|
72
src/view/com/util/fab/FABInner.tsx
Normal file
72
src/view/com/util/fab/FABInner.tsx
Normal 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',
|
||||
},
|
||||
})
|
|
@ -11,14 +11,15 @@ import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
|
|||
import {LoadLatestBtn} from '../com/util/LoadLatestBtn'
|
||||
import {FeedsTabBar} from '../com/pager/FeedsTabBar'
|
||||
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 {s} from 'lib/styles'
|
||||
import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
|
||||
import {useAnalytics} from 'lib/analytics'
|
||||
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'>
|
||||
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 {EmptyState} from '../com/util/EmptyState'
|
||||
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 {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
|
||||
import {useAnalytics} from 'lib/analytics'
|
||||
|
|
|
@ -15,12 +15,15 @@ import {
|
|||
import {useStores} from 'state/index'
|
||||
import {s} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import * as Mobile from './SearchMobile'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
|
||||
type Props = NativeStackScreenProps<SearchTabNavigatorParams, 'Search'>
|
||||
export const SearchScreen = withAuthRequired(
|
||||
observer(({route}: Props) => {
|
||||
observer(({navigation, route}: Props) => {
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const params = route.params || {}
|
||||
const foafs = React.useMemo<FoafsModel>(
|
||||
() => new FoafsModel(store),
|
||||
[store],
|
||||
|
@ -30,13 +33,13 @@ export const SearchScreen = withAuthRequired(
|
|||
[store],
|
||||
)
|
||||
const searchUIModel = React.useMemo<SearchUIModel | undefined>(
|
||||
() => (route.params.q ? new SearchUIModel(store) : undefined),
|
||||
[route.params.q, store],
|
||||
() => (params.q ? new SearchUIModel(store) : undefined),
|
||||
[params.q, store],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (route.params.q && searchUIModel) {
|
||||
searchUIModel.fetch(route.params.q)
|
||||
if (params.q && searchUIModel) {
|
||||
searchUIModel.fetch(params.q)
|
||||
}
|
||||
if (!foafs.hasData) {
|
||||
foafs.fetch()
|
||||
|
@ -44,12 +47,18 @@ export const SearchScreen = withAuthRequired(
|
|||
if (!suggestedActors.hasLoaded) {
|
||||
suggestedActors.loadMore(true)
|
||||
}
|
||||
}, [foafs, suggestedActors, searchUIModel, route.params.q])
|
||||
}, [foafs, suggestedActors, searchUIModel, params.q])
|
||||
|
||||
if (searchUIModel) {
|
||||
return <SearchResults model={searchUIModel} />
|
||||
}
|
||||
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
|
||||
if (!isDesktop) {
|
||||
return <Mobile.SearchScreen navigation={navigation} route={route} />
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
testID="searchScrollView"
|
||||
|
|
195
src/view/screens/SearchMobile.tsx
Normal file
195
src/view/screens/SearchMobile.tsx
Normal 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,
|
||||
},
|
||||
})
|
|
@ -4,6 +4,7 @@ import {StyleSheet, View} from 'react-native'
|
|||
import {ComposePost} from '../com/composer/Composer'
|
||||
import {ComposerOpts} from 'state/models/ui/shell'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {isMobileWeb} from 'platform/detection'
|
||||
|
||||
export const Composer = observer(
|
||||
({
|
||||
|
@ -60,7 +61,7 @@ const styles = StyleSheet.create({
|
|||
width: '100%',
|
||||
paddingVertical: 0,
|
||||
paddingHorizontal: 2,
|
||||
borderRadius: 8,
|
||||
borderRadius: isMobileWeb ? 0 : 8,
|
||||
marginBottom: '10vh',
|
||||
},
|
||||
})
|
||||
|
|
|
@ -8,11 +8,7 @@ import {
|
|||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
useNavigation,
|
||||
useNavigationState,
|
||||
StackActions,
|
||||
} from '@react-navigation/native'
|
||||
import {useNavigation, StackActions} from '@react-navigation/native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
|
@ -40,6 +36,8 @@ import {useAnalytics} from 'lib/analytics'
|
|||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {getTabState, TabState} from 'lib/routes/helpers'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {useNavigationTabState} from 'lib/hooks/useNavigationTabState'
|
||||
import {isWeb} from 'platform/detection'
|
||||
|
||||
export const DrawerContent = observer(() => {
|
||||
const theme = useTheme()
|
||||
|
@ -47,16 +45,7 @@ export const DrawerContent = observer(() => {
|
|||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {track} = useAnalytics()
|
||||
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState(
|
||||
state => {
|
||||
return {
|
||||
isAtHome: getTabState(state, 'Home') !== TabState.Outside,
|
||||
isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
|
||||
isAtNotifications:
|
||||
getTabState(state, 'Notifications') !== TabState.Outside,
|
||||
}
|
||||
},
|
||||
)
|
||||
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationTabState()
|
||||
|
||||
// events
|
||||
// =
|
||||
|
@ -66,14 +55,19 @@ export const DrawerContent = observer(() => {
|
|||
track('Menu:ItemClicked', {url: tab})
|
||||
const state = navigation.getState()
|
||||
store.shell.closeDrawer()
|
||||
const tabState = getTabState(state, tab)
|
||||
if (tabState === TabState.InsideAtRoot) {
|
||||
store.emitScreenSoftReset()
|
||||
} else if (tabState === TabState.Inside) {
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
} else {
|
||||
if (isWeb) {
|
||||
// @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],
|
||||
|
@ -240,20 +234,22 @@ export const DrawerContent = observer(() => {
|
|||
</View>
|
||||
<View style={s.flex1} />
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
onPress={onDarkmodePress}
|
||||
style={[
|
||||
styles.footerBtn,
|
||||
theme.colorScheme === 'light'
|
||||
? pal.btn
|
||||
: styles.footerBtnDarkMode,
|
||||
]}>
|
||||
<MoonIcon
|
||||
size={22}
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
{!isWeb && (
|
||||
<TouchableOpacity
|
||||
onPress={onDarkmodePress}
|
||||
style={[
|
||||
styles.footerBtn,
|
||||
theme.colorScheme === 'light'
|
||||
? pal.btn
|
||||
: styles.footerBtnDarkMode,
|
||||
]}>
|
||||
<MoonIcon
|
||||
size={22}
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
onPress={onPressFeedback}
|
||||
style={[
|
||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react'
|
|||
import {
|
||||
Animated,
|
||||
GestureResponderEvent,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
|
@ -13,7 +12,6 @@ import {observer} from 'mobx-react-lite'
|
|||
import {Text} from 'view/com/util/text/Text'
|
||||
import {useStores} from 'state/index'
|
||||
import {useAnalytics} from 'lib/analytics'
|
||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
||||
import {clamp} from 'lib/numbers'
|
||||
import {
|
||||
HomeIcon,
|
||||
|
@ -24,14 +22,14 @@ import {
|
|||
BellIconSolid,
|
||||
UserIcon,
|
||||
} from 'lib/icons'
|
||||
import {colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {getTabState, TabState} from 'lib/routes/helpers'
|
||||
import {styles} from './BottomBarStyles'
|
||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||
|
||||
export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const minimalShellInterp = useAnimatedValue(0)
|
||||
const safeAreaInsets = useSafeAreaInsets()
|
||||
const {track} = useAnalytics()
|
||||
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState(
|
||||
|
@ -52,26 +50,7 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
|
|||
},
|
||||
)
|
||||
|
||||
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])
|
||||
const footerMinimalShellTransform = {
|
||||
transform: [{translateY: Animated.multiply(minimalShellInterp, 100)}],
|
||||
}
|
||||
const {footerMinimalShellTransform} = useMinimalShellMode()
|
||||
|
||||
const onPressTab = React.useCallback(
|
||||
(tab: string) => {
|
||||
|
@ -217,62 +196,3 @@ function Btn({
|
|||
</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,
|
||||
},
|
||||
})
|
61
src/view/shell/bottom-bar/BottomBarStyles.tsx
Normal file
61
src/view/shell/bottom-bar/BottomBarStyles.tsx
Normal 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,
|
||||
},
|
||||
})
|
101
src/view/shell/bottom-bar/BottomBarWeb.tsx
Normal file
101
src/view/shell/bottom-bar/BottomBarWeb.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react'
|
||||
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 {DesktopLeftNav} from './desktop/LeftNav'
|
||||
import {DesktopRightNav} from './desktop/RightNav'
|
||||
|
@ -11,9 +11,13 @@ import {Composer} from './Composer.web'
|
|||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||
import {s, colors} from 'lib/styles'
|
||||
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 store = useStores()
|
||||
const {isDesktop} = useWebMediaQueries()
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -22,10 +26,14 @@ const ShellInner = observer(() => {
|
|||
<FlatNavigator />
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
<DesktopLeftNav />
|
||||
<DesktopRightNav />
|
||||
<View style={[styles.viewBorder, styles.viewBorderLeft]} />
|
||||
<View style={[styles.viewBorder, styles.viewBorderRight]} />
|
||||
{isDesktop && (
|
||||
<>
|
||||
<DesktopLeftNav />
|
||||
<DesktopRightNav />
|
||||
<View style={[styles.viewBorder, styles.viewBorderLeft]} />
|
||||
<View style={[styles.viewBorder, styles.viewBorderRight]} />
|
||||
</>
|
||||
)}
|
||||
<Composer
|
||||
active={store.shell.isComposerActive}
|
||||
onClose={() => store.shell.closeComposer()}
|
||||
|
@ -34,8 +42,18 @@ const ShellInner = observer(() => {
|
|||
quote={store.shell.composerOpts?.quote}
|
||||
onPost={store.shell.composerOpts?.onPost}
|
||||
/>
|
||||
{!isDesktop && <BottomBarWeb />}
|
||||
<ModalsContainer />
|
||||
<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: {
|
||||
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%',
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue