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:
John Fawcett 2023-04-12 20:27:55 -05:00 committed by GitHub
parent 2fed6c4021
commit f6769b283f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 4343 additions and 319 deletions

View file

@ -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: {

View file

@ -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'

View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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