Move to expo and react-navigation (#288)
* WIP - adding expo * WIP - adding expo 2 * Fix tsc * Finish adding expo * Disable the 'require cycle' warning * Tweak plist * Modify some dependency versions to make expo happy * Fix icon fill * Get Web compiling for expo * 1.7 * Switch to react-navigation in expo2 (#287) * WIP Switch to react-navigation * WIP Switch to react-navigation 2 * WIP Switch to react-navigation 3 * Convert all screens to react navigation * Update BottomBar for react navigation * Update mobile menu to be react-native drawer * Fixes to drawer and bottombar * Factor out some helpers * Replace the navigation model with react-navigation * Restructure the shell folder and fix the header positioning * Restore the error boundary * Fix tsc * Implement not-found page * Remove react-native-gesture-handler (no longer used) * Handle notifee card presses * Handle all navigations from the state layer * Fix drawer behaviors * Fix two linking issues * Switch to our react-native-progress fork to fix an svg rendering issue * Get Web working with react-navigation * Refactor routes and navigation for a bit more clarity * Remove dead code * Rework Web shell to left/right nav to make this easier * Fix ViewHeader for desktop web * Hide profileheader back btn on desktop web * Move the compose button to the left nav * Implement reply prompt in threads for desktop web * Composer refactors * Factor out all platform-specific text input behaviors from the composer * Small fix * Update the web build to use tiptap for the composer * Tune up the mention autocomplete dropdown * Simplify the default avatar and banner * Fixes to link cards in web composer * Fix dropdowns on web * Tweak load latest on desktop * Add web beta message and feedback link * Fix up links in desktop web
This commit is contained in:
parent
503e03d91e
commit
56cf890deb
222 changed files with 8705 additions and 6338 deletions
|
@ -6,13 +6,14 @@ import {
|
|||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {StackActions, useNavigationState} from '@react-navigation/native'
|
||||
import {BottomTabBarProps} from '@react-navigation/bottom-tabs'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
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 {TabPurpose, TabPurposeMainPath} from 'state/models/navigation'
|
||||
import {clamp} from 'lib/numbers'
|
||||
import {
|
||||
HomeIcon,
|
||||
|
@ -25,13 +26,24 @@ import {
|
|||
} from 'lib/icons'
|
||||
import {colors} from 'lib/styles'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {getTabState, TabState} from 'lib/routes/helpers'
|
||||
|
||||
export const BottomBar = observer(() => {
|
||||
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(
|
||||
state => {
|
||||
return {
|
||||
isAtHome: getTabState(state, 'Home') !== TabState.Outside,
|
||||
isAtSearch: getTabState(state, 'Search') !== TabState.Outside,
|
||||
isAtNotifications:
|
||||
getTabState(state, 'Notifications') !== TabState.Outside,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (store.shell.minimalShellMode) {
|
||||
|
@ -54,62 +66,34 @@ export const BottomBar = observer(() => {
|
|||
transform: [{translateY: Animated.multiply(minimalShellInterp, 100)}],
|
||||
}
|
||||
|
||||
const onPressHome = React.useCallback(() => {
|
||||
track('MobileShell:HomeButtonPressed')
|
||||
if (store.nav.tab.fixedTabPurpose === TabPurpose.Default) {
|
||||
if (!store.nav.tab.canGoBack) {
|
||||
const onPressTab = React.useCallback(
|
||||
(tab: string) => {
|
||||
track(`MobileShell:${tab}ButtonPressed`)
|
||||
const state = navigation.getState()
|
||||
const tabState = getTabState(state, tab)
|
||||
if (tabState === TabState.InsideAtRoot) {
|
||||
store.emitScreenSoftReset()
|
||||
} else if (tabState === TabState.Inside) {
|
||||
navigation.dispatch(StackActions.popToTop())
|
||||
} else {
|
||||
store.nav.tab.fixedTabReset()
|
||||
navigation.navigate(`${tab}Tab`)
|
||||
}
|
||||
} else {
|
||||
store.nav.switchTo(TabPurpose.Default, false)
|
||||
if (store.nav.tab.index === 0) {
|
||||
store.nav.tab.fixedTabReset()
|
||||
}
|
||||
}
|
||||
}, [store, track])
|
||||
const onPressSearch = React.useCallback(() => {
|
||||
track('MobileShell:SearchButtonPressed')
|
||||
if (store.nav.tab.fixedTabPurpose === TabPurpose.Search) {
|
||||
if (!store.nav.tab.canGoBack) {
|
||||
store.emitScreenSoftReset()
|
||||
} else {
|
||||
store.nav.tab.fixedTabReset()
|
||||
}
|
||||
} else {
|
||||
store.nav.switchTo(TabPurpose.Search, false)
|
||||
if (store.nav.tab.index === 0) {
|
||||
store.nav.tab.fixedTabReset()
|
||||
}
|
||||
}
|
||||
}, [store, track])
|
||||
const onPressNotifications = React.useCallback(() => {
|
||||
track('MobileShell:NotificationsButtonPressed')
|
||||
if (store.nav.tab.fixedTabPurpose === TabPurpose.Notifs) {
|
||||
if (!store.nav.tab.canGoBack) {
|
||||
store.emitScreenSoftReset()
|
||||
} else {
|
||||
store.nav.tab.fixedTabReset()
|
||||
}
|
||||
} else {
|
||||
store.nav.switchTo(TabPurpose.Notifs, false)
|
||||
if (store.nav.tab.index === 0) {
|
||||
store.nav.tab.fixedTabReset()
|
||||
}
|
||||
}
|
||||
}, [store, track])
|
||||
},
|
||||
[store, track, navigation],
|
||||
)
|
||||
const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
|
||||
const onPressSearch = React.useCallback(
|
||||
() => onPressTab('Search'),
|
||||
[onPressTab],
|
||||
)
|
||||
const onPressNotifications = React.useCallback(
|
||||
() => onPressTab('Notifications'),
|
||||
[onPressTab],
|
||||
)
|
||||
const onPressProfile = React.useCallback(() => {
|
||||
track('MobileShell:ProfileButtonPressed')
|
||||
store.nav.navigate(`/profile/${store.me.handle}`)
|
||||
}, [store, track])
|
||||
|
||||
const isAtHome =
|
||||
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Default]
|
||||
const isAtSearch =
|
||||
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Search]
|
||||
const isAtNotifications =
|
||||
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Notifs]
|
||||
navigation.navigate('Profile', {name: store.me.handle})
|
||||
}, [navigation, track, store.me.handle])
|
||||
|
||||
return (
|
||||
<Animated.View
|
|
@ -1,7 +1,7 @@
|
|||
import React, {useEffect} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Animated, Easing, Platform, StyleSheet, View} from 'react-native'
|
||||
import {ComposePost} from '../../com/composer/ComposePost'
|
||||
import {ComposePost} from '../com/composer/Composer'
|
||||
import {ComposerOpts} from 'state/models/shell-ui'
|
||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
|
@ -11,7 +11,6 @@ export const Composer = observer(
|
|||
active,
|
||||
winHeight,
|
||||
replyTo,
|
||||
imagesOpen,
|
||||
onPost,
|
||||
onClose,
|
||||
quote,
|
||||
|
@ -19,7 +18,6 @@ export const Composer = observer(
|
|||
active: boolean
|
||||
winHeight: number
|
||||
replyTo?: ComposerOpts['replyTo']
|
||||
imagesOpen?: ComposerOpts['imagesOpen']
|
||||
onPost?: ComposerOpts['onPost']
|
||||
onClose: () => void
|
||||
quote?: ComposerOpts['quote']
|
||||
|
@ -61,7 +59,6 @@ export const Composer = observer(
|
|||
<Animated.View style={[styles.wrapper, pal.view, wrapperAnimStyle]}>
|
||||
<ComposePost
|
||||
replyTo={replyTo}
|
||||
imagesOpen={imagesOpen}
|
||||
onPost={onPost}
|
||||
onClose={onClose}
|
||||
quote={quote}
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {ComposePost} from '../../com/composer/ComposePost'
|
||||
import {ComposePost} from '../com/composer/Composer'
|
||||
import {ComposerOpts} from 'state/models/shell-ui'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
|
||||
|
@ -9,14 +9,12 @@ export const Composer = observer(
|
|||
({
|
||||
active,
|
||||
replyTo,
|
||||
imagesOpen,
|
||||
onPost,
|
||||
onClose,
|
||||
}: {
|
||||
active: boolean
|
||||
winHeight: number
|
||||
replyTo?: ComposerOpts['replyTo']
|
||||
imagesOpen?: ComposerOpts['imagesOpen']
|
||||
onPost?: ComposerOpts['onPost']
|
||||
onClose: () => void
|
||||
}) => {
|
||||
|
@ -32,12 +30,7 @@ export const Composer = observer(
|
|||
return (
|
||||
<View style={styles.mask}>
|
||||
<View style={[styles.container, pal.view]}>
|
||||
<ComposePost
|
||||
replyTo={replyTo}
|
||||
imagesOpen={imagesOpen}
|
||||
onPost={onPost}
|
||||
onClose={onClose}
|
||||
/>
|
||||
<ComposePost replyTo={replyTo} onPost={onPost} onClose={onClose} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
386
src/view/shell/Drawer.tsx
Normal file
386
src/view/shell/Drawer.tsx
Normal file
|
@ -0,0 +1,386 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
Linking,
|
||||
SafeAreaView,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
useNavigation,
|
||||
useNavigationState,
|
||||
StackActions,
|
||||
} from '@react-navigation/native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {FEEDBACK_FORM_URL} from 'lib/constants'
|
||||
import {useStores} from 'state/index'
|
||||
import {
|
||||
HomeIcon,
|
||||
HomeIconSolid,
|
||||
BellIcon,
|
||||
BellIconSolid,
|
||||
UserIcon,
|
||||
CogIcon,
|
||||
MagnifyingGlassIcon2,
|
||||
MagnifyingGlassIcon2Solid,
|
||||
MoonIcon,
|
||||
} from 'lib/icons'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
import {getCurrentRoute, isTab, getTabState, TabState} from 'lib/routes/helpers'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
|
||||
export const DrawerContent = observer(() => {
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const {track} = useAnalytics()
|
||||
const {isAtHome, isAtSearch, isAtNotifications} = useNavigationState(
|
||||
state => {
|
||||
const currentRoute = state ? getCurrentRoute(state) : false
|
||||
return {
|
||||
isAtHome: currentRoute ? isTab(currentRoute.name, 'Home') : true,
|
||||
isAtSearch: currentRoute ? isTab(currentRoute.name, 'Search') : false,
|
||||
isAtNotifications: currentRoute
|
||||
? isTab(currentRoute.name, 'Notifications')
|
||||
: false,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// events
|
||||
// =
|
||||
|
||||
const onPressTab = React.useCallback(
|
||||
(tab: string) => {
|
||||
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 {
|
||||
// @ts-ignore must be Home, Search, or Notifications
|
||||
navigation.navigate(`${tab}Tab`)
|
||||
}
|
||||
},
|
||||
[store, track, navigation],
|
||||
)
|
||||
|
||||
const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
|
||||
|
||||
const onPressSearch = React.useCallback(
|
||||
() => onPressTab('Search'),
|
||||
[onPressTab],
|
||||
)
|
||||
|
||||
const onPressNotifications = React.useCallback(
|
||||
() => onPressTab('Notifications'),
|
||||
[onPressTab],
|
||||
)
|
||||
|
||||
const onPressProfile = React.useCallback(() => {
|
||||
track('Menu:ItemClicked', {url: 'Profile'})
|
||||
navigation.navigate('Profile', {name: store.me.handle})
|
||||
store.shell.closeDrawer()
|
||||
}, [navigation, track, store.me.handle, store.shell])
|
||||
|
||||
const onPressSettings = React.useCallback(() => {
|
||||
track('Menu:ItemClicked', {url: 'Settings'})
|
||||
navigation.navigate('Settings')
|
||||
store.shell.closeDrawer()
|
||||
}, [navigation, track, store.shell])
|
||||
|
||||
const onPressFeedback = () => {
|
||||
track('Menu:FeedbackClicked')
|
||||
Linking.openURL(FEEDBACK_FORM_URL)
|
||||
}
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
||||
const MenuItem = ({
|
||||
icon,
|
||||
label,
|
||||
count,
|
||||
bold,
|
||||
onPress,
|
||||
}: {
|
||||
icon: JSX.Element
|
||||
label: string
|
||||
count?: number
|
||||
bold?: boolean
|
||||
onPress: () => void
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
testID={`menuItemButton-${label}`}
|
||||
style={styles.menuItem}
|
||||
onPress={onPress}>
|
||||
<View style={[styles.menuItemIconWrapper]}>
|
||||
{icon}
|
||||
{count ? (
|
||||
<View style={styles.menuItemCount}>
|
||||
<Text style={styles.menuItemCountLabel}>{count}</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
</View>
|
||||
<Text
|
||||
type={bold ? '2xl-bold' : '2xl'}
|
||||
style={[pal.text, s.flex1]}
|
||||
numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
|
||||
const onDarkmodePress = () => {
|
||||
track('Menu:ItemClicked', {url: '/darkmode'})
|
||||
store.shell.setDarkMode(!store.shell.darkMode)
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
testID="menuView"
|
||||
style={[
|
||||
styles.view,
|
||||
theme.colorScheme === 'light' ? pal.view : styles.viewDarkMode,
|
||||
]}>
|
||||
<SafeAreaView style={s.flex1}>
|
||||
<TouchableOpacity testID="profileCardButton" onPress={onPressProfile}>
|
||||
<UserAvatar size={80} avatar={store.me.avatar} />
|
||||
<Text
|
||||
type="title-lg"
|
||||
style={[pal.text, s.bold, styles.profileCardDisplayName]}>
|
||||
{store.me.displayName || store.me.handle}
|
||||
</Text>
|
||||
<Text type="2xl" style={[pal.textLight, styles.profileCardHandle]}>
|
||||
@{store.me.handle}
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}>
|
||||
<Text type="xl-medium" style={pal.text}>
|
||||
{store.me.followersCount || 0}
|
||||
</Text>{' '}
|
||||
{pluralize(store.me.followersCount || 0, 'follower')} ·{' '}
|
||||
<Text type="xl-medium" style={pal.text}>
|
||||
{store.me.followsCount || 0}
|
||||
</Text>{' '}
|
||||
following
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
<View>
|
||||
<MenuItem
|
||||
icon={
|
||||
isAtSearch ? (
|
||||
<MagnifyingGlassIcon2Solid
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size={24}
|
||||
strokeWidth={1.7}
|
||||
/>
|
||||
) : (
|
||||
<MagnifyingGlassIcon2
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size={24}
|
||||
strokeWidth={1.7}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label="Search"
|
||||
bold={isAtSearch}
|
||||
onPress={onPressSearch}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={
|
||||
isAtHome ? (
|
||||
<HomeIconSolid
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="24"
|
||||
strokeWidth={3.25}
|
||||
/>
|
||||
) : (
|
||||
<HomeIcon
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="24"
|
||||
strokeWidth={3.25}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label="Home"
|
||||
bold={isAtHome}
|
||||
onPress={onPressHome}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={
|
||||
isAtNotifications ? (
|
||||
<BellIconSolid
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="24"
|
||||
strokeWidth={1.7}
|
||||
/>
|
||||
) : (
|
||||
<BellIcon
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="24"
|
||||
strokeWidth={1.7}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label="Notifications"
|
||||
count={store.me.notifications.unreadCount}
|
||||
bold={isAtNotifications}
|
||||
onPress={onPressNotifications}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={
|
||||
<UserIcon
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="26"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
label="Profile"
|
||||
onPress={onPressProfile}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={
|
||||
<CogIcon
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="26"
|
||||
strokeWidth={1.75}
|
||||
/>
|
||||
}
|
||||
label="Settings"
|
||||
onPress={onPressSettings}
|
||||
/>
|
||||
</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>
|
||||
<TouchableOpacity
|
||||
onPress={onPressFeedback}
|
||||
style={[
|
||||
styles.footerBtn,
|
||||
styles.footerBtnFeedback,
|
||||
theme.colorScheme === 'light'
|
||||
? styles.footerBtnFeedbackLight
|
||||
: styles.footerBtnFeedbackDark,
|
||||
]}>
|
||||
<FontAwesomeIcon
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
size={19}
|
||||
icon={['far', 'message']}
|
||||
/>
|
||||
<Text type="2xl-medium" style={[pal.link, s.pl10]}>
|
||||
Feedback
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
view: {
|
||||
flex: 1,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 50,
|
||||
paddingLeft: 20,
|
||||
},
|
||||
viewDarkMode: {
|
||||
backgroundColor: '#1B1919',
|
||||
},
|
||||
|
||||
profileCardDisplayName: {
|
||||
marginTop: 20,
|
||||
paddingRight: 30,
|
||||
},
|
||||
profileCardHandle: {
|
||||
marginTop: 4,
|
||||
paddingRight: 30,
|
||||
},
|
||||
profileCardFollowers: {
|
||||
marginTop: 16,
|
||||
paddingRight: 30,
|
||||
},
|
||||
|
||||
menuItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 16,
|
||||
paddingRight: 10,
|
||||
},
|
||||
menuItemIconWrapper: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
menuItemCount: {
|
||||
position: 'absolute',
|
||||
right: -6,
|
||||
top: -2,
|
||||
backgroundColor: colors.red3,
|
||||
paddingHorizontal: 4,
|
||||
paddingBottom: 1,
|
||||
borderRadius: 6,
|
||||
},
|
||||
menuItemCountLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
color: colors.white,
|
||||
},
|
||||
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingRight: 30,
|
||||
paddingTop: 80,
|
||||
},
|
||||
footerBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
borderRadius: 25,
|
||||
},
|
||||
footerBtnDarkMode: {
|
||||
backgroundColor: colors.black,
|
||||
},
|
||||
footerBtnFeedback: {
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
footerBtnFeedbackLight: {
|
||||
backgroundColor: '#DDEFFF',
|
||||
},
|
||||
footerBtnFeedbackDark: {
|
||||
backgroundColor: colors.blue6,
|
||||
},
|
||||
})
|
254
src/view/shell/desktop/LeftNav.tsx
Normal file
254
src/view/shell/desktop/LeftNav.tsx
Normal file
|
@ -0,0 +1,254 @@
|
|||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {useNavigation, useNavigationState} from '@react-navigation/native'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {Link} from 'view/com/util/Link'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useStores} from 'state/index'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {
|
||||
HomeIcon,
|
||||
HomeIconSolid,
|
||||
MagnifyingGlassIcon2,
|
||||
MagnifyingGlassIcon2Solid,
|
||||
BellIcon,
|
||||
BellIconSolid,
|
||||
UserIcon,
|
||||
UserIconSolid,
|
||||
CogIcon,
|
||||
CogIconSolid,
|
||||
ComposeIcon2,
|
||||
} from 'lib/icons'
|
||||
import {getCurrentRoute, isTab, isStateAtTabRoot} from 'lib/routes/helpers'
|
||||
import {NavigationProp} from 'lib/routes/types'
|
||||
import {router} from '../../../routes'
|
||||
|
||||
const ProfileCard = observer(() => {
|
||||
const store = useStores()
|
||||
return (
|
||||
<Link href={`/profile/${store.me.handle}`} style={styles.profileCard}>
|
||||
<UserAvatar avatar={store.me.avatar} size={64} />
|
||||
</Link>
|
||||
)
|
||||
})
|
||||
|
||||
function BackBtn() {
|
||||
const pal = usePalette('default')
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const shouldShow = useNavigationState(state => !isStateAtTabRoot(state))
|
||||
|
||||
const onPressBack = React.useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
} else {
|
||||
navigation.navigate('Home')
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
if (!shouldShow) {
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
<TouchableOpacity
|
||||
testID="viewHeaderBackOrMenuBtn"
|
||||
onPress={onPressBack}
|
||||
style={styles.backBtn}>
|
||||
<FontAwesomeIcon
|
||||
size={24}
|
||||
icon="angle-left"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
interface NavItemProps {
|
||||
count?: number
|
||||
href: string
|
||||
icon: JSX.Element
|
||||
iconFilled: JSX.Element
|
||||
label: string
|
||||
}
|
||||
const NavItem = observer(
|
||||
({count, href, icon, iconFilled, label}: NavItemProps) => {
|
||||
const pal = usePalette('default')
|
||||
const [pathName] = React.useMemo(() => router.matchPath(href), [href])
|
||||
const currentRouteName = useNavigationState(state => {
|
||||
if (!state) {
|
||||
return 'Home'
|
||||
}
|
||||
return getCurrentRoute(state).name
|
||||
})
|
||||
const isCurrent = isTab(currentRouteName, pathName)
|
||||
|
||||
return (
|
||||
<Link href={href} style={styles.navItem}>
|
||||
<View style={[styles.navItemIconWrapper]}>
|
||||
{isCurrent ? iconFilled : icon}
|
||||
{typeof count === 'number' && count > 0 && (
|
||||
<Text type="button" style={styles.navItemCount}>
|
||||
{count}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text type="title" style={[isCurrent ? s.bold : s.normal, pal.text]}>
|
||||
{label}
|
||||
</Text>
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
function ComposeBtn() {
|
||||
const store = useStores()
|
||||
const onPressCompose = () => store.shell.openComposer({})
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={[styles.newPostBtn]} onPress={onPressCompose}>
|
||||
<View style={styles.newPostBtnIconWrapper}>
|
||||
<ComposeIcon2
|
||||
size={19}
|
||||
strokeWidth={2}
|
||||
style={styles.newPostBtnLabel}
|
||||
/>
|
||||
</View>
|
||||
<Text type="button" style={styles.newPostBtnLabel}>
|
||||
New Post
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
}
|
||||
|
||||
export const DesktopLeftNav = observer(function DesktopLeftNav() {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
|
||||
return (
|
||||
<View style={styles.leftNav}>
|
||||
<ProfileCard />
|
||||
<BackBtn />
|
||||
<NavItem
|
||||
href="/"
|
||||
icon={<HomeIcon size={24} style={pal.text} />}
|
||||
iconFilled={
|
||||
<HomeIconSolid strokeWidth={4} size={24} style={pal.text} />
|
||||
}
|
||||
label="Home"
|
||||
/>
|
||||
<NavItem
|
||||
href="/search"
|
||||
icon={
|
||||
<MagnifyingGlassIcon2 strokeWidth={2} size={24} style={pal.text} />
|
||||
}
|
||||
iconFilled={
|
||||
<MagnifyingGlassIcon2Solid
|
||||
strokeWidth={2}
|
||||
size={24}
|
||||
style={pal.text}
|
||||
/>
|
||||
}
|
||||
label="Search"
|
||||
/>
|
||||
<NavItem
|
||||
href="/notifications"
|
||||
count={store.me.notifications.unreadCount}
|
||||
icon={<BellIcon strokeWidth={2} size={24} style={pal.text} />}
|
||||
iconFilled={
|
||||
<BellIconSolid strokeWidth={1.5} size={24} style={pal.text} />
|
||||
}
|
||||
label="Notifications"
|
||||
/>
|
||||
<NavItem
|
||||
href={`/profile/${store.me.handle}`}
|
||||
icon={<UserIcon strokeWidth={1.75} size={28} style={pal.text} />}
|
||||
iconFilled={
|
||||
<UserIconSolid strokeWidth={1.75} size={28} style={pal.text} />
|
||||
}
|
||||
label="Profile"
|
||||
/>
|
||||
<NavItem
|
||||
href="/settings"
|
||||
icon={<CogIcon strokeWidth={1.75} size={28} style={pal.text} />}
|
||||
iconFilled={
|
||||
<CogIconSolid strokeWidth={1.5} size={28} style={pal.text} />
|
||||
}
|
||||
label="Settings"
|
||||
/>
|
||||
<ComposeBtn />
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
leftNav: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 'calc(50vw + 300px)',
|
||||
width: 220,
|
||||
},
|
||||
|
||||
profileCard: {
|
||||
marginVertical: 10,
|
||||
width: 60,
|
||||
},
|
||||
|
||||
backBtn: {
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
width: 30,
|
||||
height: 30,
|
||||
},
|
||||
|
||||
navItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingTop: 14,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
navItemIconWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 28,
|
||||
height: 28,
|
||||
marginRight: 10,
|
||||
marginTop: 2,
|
||||
},
|
||||
navItemCount: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 15,
|
||||
backgroundColor: colors.blue3,
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
paddingHorizontal: 4,
|
||||
borderRadius: 6,
|
||||
},
|
||||
|
||||
newPostBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 136,
|
||||
borderRadius: 24,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: colors.blue3,
|
||||
marginTop: 20,
|
||||
},
|
||||
newPostBtnIconWrapper: {
|
||||
marginRight: 8,
|
||||
},
|
||||
newPostBtnLabel: {
|
||||
color: colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
})
|
46
src/view/shell/desktop/RightNav.tsx
Normal file
46
src/view/shell/desktop/RightNav.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {DesktopSearch} from './Search'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {TextLink} from 'view/com/util/Link'
|
||||
import {FEEDBACK_FORM_URL} from 'lib/constants'
|
||||
|
||||
export const DesktopRightNav = observer(function DesktopRightNav() {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View style={[styles.rightNav, pal.view]}>
|
||||
<DesktopSearch />
|
||||
<View style={styles.message}>
|
||||
<Text type="md" style={[pal.textLight, styles.messageLine]}>
|
||||
Welcome to Bluesky! This is a beta application that's still in
|
||||
development.
|
||||
</Text>
|
||||
<TextLink
|
||||
type="md"
|
||||
style={pal.link}
|
||||
href={FEEDBACK_FORM_URL}
|
||||
text="Send feedback"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
rightNav: {
|
||||
position: 'absolute',
|
||||
top: 20,
|
||||
left: 'calc(50vw + 330px)',
|
||||
width: 300,
|
||||
},
|
||||
|
||||
message: {
|
||||
marginTop: 20,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
messageLine: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
})
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react'
|
||||
import {TextInput, View, StyleSheet, TouchableOpacity, Text} from 'react-native'
|
||||
import {TextInput, View, StyleSheet, TouchableOpacity} from 'react-native'
|
||||
import {UserAutocompleteViewModel} from 'state/models/user-autocomplete-view'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {useStores} from 'state/index'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {MagnifyingGlassIcon} from 'lib/icons'
|
||||
import {ProfileCard} from '../../com/profile/ProfileCard'
|
||||
import {MagnifyingGlassIcon2} from 'lib/icons'
|
||||
import {ProfileCard} from 'view/com/profile/ProfileCard'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
|
||||
export const DesktopSearch = observer(function DesktopSearch() {
|
||||
const store = useStores()
|
||||
|
@ -35,9 +36,10 @@ export const DesktopSearch = observer(function DesktopSearch() {
|
|||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={[pal.borderDark, pal.view, styles.search]}>
|
||||
<View
|
||||
style={[{backgroundColor: pal.colors.backgroundLight}, styles.search]}>
|
||||
<View style={[styles.inputContainer]}>
|
||||
<MagnifyingGlassIcon
|
||||
<MagnifyingGlassIcon2
|
||||
size={18}
|
||||
style={[pal.textLight, styles.iconWrapper]}
|
||||
/>
|
||||
|
@ -57,7 +59,9 @@ export const DesktopSearch = observer(function DesktopSearch() {
|
|||
{query ? (
|
||||
<View style={styles.cancelBtn}>
|
||||
<TouchableOpacity onPress={onPressCancelSearch}>
|
||||
<Text style={[pal.link]}>Cancel</Text>
|
||||
<Text type="lg" style={[pal.link]}>
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : undefined}
|
||||
|
@ -97,21 +101,23 @@ const styles = StyleSheet.create({
|
|||
width: 300,
|
||||
},
|
||||
search: {
|
||||
paddingHorizontal: 10,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 2,
|
||||
width: 300,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
},
|
||||
inputContainer: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
iconWrapper: {
|
||||
position: 'relative',
|
||||
top: 2,
|
||||
paddingVertical: 7,
|
||||
marginRight: 4,
|
||||
marginRight: 8,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontSize: 18,
|
||||
width: '100%',
|
||||
paddingTop: 7,
|
||||
paddingBottom: 7,
|
139
src/view/shell/index.tsx
Normal file
139
src/view/shell/index.tsx
Normal file
|
@ -0,0 +1,139 @@
|
|||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {StatusBar, StyleSheet, useWindowDimensions, View} from 'react-native'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {Drawer} from 'react-native-drawer-layout'
|
||||
import {useNavigationState} from '@react-navigation/native'
|
||||
import {useStores} from 'state/index'
|
||||
import {Login} from 'view/screens/Login'
|
||||
import {ModalsContainer} from 'view/com/modals/Modal'
|
||||
import {Lightbox} from 'view/com/lightbox/Lightbox'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
|
||||
import {DrawerContent} from './Drawer'
|
||||
import {Composer} from './Composer'
|
||||
import {s} from 'lib/styles'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {RoutesContainer, TabsNavigator} from '../../Navigation'
|
||||
import {isStateAtTabRoot} from 'lib/routes/helpers'
|
||||
|
||||
const ShellInner = observer(() => {
|
||||
const store = useStores()
|
||||
const winDim = useWindowDimensions()
|
||||
const safeAreaInsets = useSafeAreaInsets()
|
||||
const containerPadding = React.useMemo(
|
||||
() => ({height: '100%', paddingTop: safeAreaInsets.top}),
|
||||
[safeAreaInsets],
|
||||
)
|
||||
const renderDrawerContent = React.useCallback(() => <DrawerContent />, [])
|
||||
const onOpenDrawer = React.useCallback(
|
||||
() => store.shell.openDrawer(),
|
||||
[store],
|
||||
)
|
||||
const onCloseDrawer = React.useCallback(
|
||||
() => store.shell.closeDrawer(),
|
||||
[store],
|
||||
)
|
||||
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={containerPadding}>
|
||||
<ErrorBoundary>
|
||||
<Drawer
|
||||
renderDrawerContent={renderDrawerContent}
|
||||
open={store.shell.isDrawerOpen}
|
||||
onOpen={onOpenDrawer}
|
||||
onClose={onCloseDrawer}
|
||||
swipeEdgeWidth={winDim.width}
|
||||
swipeEnabled={!canGoBack}>
|
||||
<TabsNavigator />
|
||||
</Drawer>
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
<ModalsContainer />
|
||||
<Lightbox />
|
||||
<Composer
|
||||
active={store.shell.isComposerActive}
|
||||
onClose={() => store.shell.closeComposer()}
|
||||
winHeight={winDim.height}
|
||||
replyTo={store.shell.composerOpts?.replyTo}
|
||||
onPost={store.shell.composerOpts?.onPost}
|
||||
quote={store.shell.composerOpts?.quote}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export const Shell: React.FC = observer(() => {
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
|
||||
if (store.hackUpgradeNeeded) {
|
||||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
<View style={[s.flexCol, s.p20, s.h100pct]}>
|
||||
<View style={s.flex1} />
|
||||
<View>
|
||||
<Text type="title-2xl" style={s.pb10}>
|
||||
Update required
|
||||
</Text>
|
||||
<Text style={[s.pb20, s.bold]}>
|
||||
Please update your app to the latest version. If no update is
|
||||
available yet, please check the App Store in a day or so.
|
||||
</Text>
|
||||
<Text type="title" style={s.pb10}>
|
||||
What's happening?
|
||||
</Text>
|
||||
<Text style={s.pb10}>
|
||||
We're in the final stages of the AT Protocol's v1 development. To
|
||||
make sure everything works as well as possible, we're making final
|
||||
breaking changes to the APIs.
|
||||
</Text>
|
||||
<Text>
|
||||
If we didn't botch this process, a new version of the app should
|
||||
be available now.
|
||||
</Text>
|
||||
</View>
|
||||
<View style={s.flex1} />
|
||||
<View style={s.footerSpacer} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!store.session.hasSession) {
|
||||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
<StatusBar
|
||||
barStyle={
|
||||
theme.colorScheme === 'dark' ? 'light-content' : 'dark-content'
|
||||
}
|
||||
/>
|
||||
<Login />
|
||||
<ModalsContainer />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
|
||||
<StatusBar
|
||||
barStyle={
|
||||
theme.colorScheme === 'dark' ? 'light-content' : 'dark-content'
|
||||
}
|
||||
/>
|
||||
<RoutesContainer>
|
||||
<ShellInner />
|
||||
</RoutesContainer>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outerContainer: {
|
||||
height: '100%',
|
||||
},
|
||||
})
|
113
src/view/shell/index.web.tsx
Normal file
113
src/view/shell/index.web.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {View, StyleSheet} from 'react-native'
|
||||
import {useStores} from 'state/index'
|
||||
import {DesktopLeftNav} from './desktop/LeftNav'
|
||||
import {DesktopRightNav} from './desktop/RightNav'
|
||||
import {Login} from '../screens/Login'
|
||||
import {ErrorBoundary} from '../com/util/ErrorBoundary'
|
||||
import {Lightbox} from '../com/lightbox/Lightbox'
|
||||
import {ModalsContainer} from '../com/modals/Modal'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {Composer} from './Composer.web'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {isMobileWeb} from 'platform/detection'
|
||||
import {RoutesContainer, FlatNavigator} from '../../Navigation'
|
||||
|
||||
const ShellInner = observer(() => {
|
||||
const store = useStores()
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style={s.hContentRegion}>
|
||||
<ErrorBoundary>
|
||||
<FlatNavigator />
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
<DesktopLeftNav />
|
||||
<DesktopRightNav />
|
||||
<View style={[styles.viewBorder, styles.viewBorderLeft]} />
|
||||
<View style={[styles.viewBorder, styles.viewBorderRight]} />
|
||||
<Composer
|
||||
active={store.shell.isComposerActive}
|
||||
onClose={() => store.shell.closeComposer()}
|
||||
winHeight={0}
|
||||
replyTo={store.shell.composerOpts?.replyTo}
|
||||
onPost={store.shell.composerOpts?.onPost}
|
||||
/>
|
||||
<ModalsContainer />
|
||||
<Lightbox />
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
export const Shell: React.FC = observer(() => {
|
||||
const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)
|
||||
const store = useStores()
|
||||
|
||||
if (isMobileWeb) {
|
||||
return <NoMobileWeb />
|
||||
}
|
||||
|
||||
if (!store.session.hasSession) {
|
||||
return (
|
||||
<View style={[s.hContentRegion, pageBg]}>
|
||||
<Login />
|
||||
<ModalsContainer />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[s.hContentRegion, pageBg]}>
|
||||
<RoutesContainer>
|
||||
<ShellInner />
|
||||
</RoutesContainer>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
function NoMobileWeb() {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View style={[pal.view, styles.noMobileWeb]}>
|
||||
<Text type="title-2xl" style={s.pb20}>
|
||||
We're so sorry!
|
||||
</Text>
|
||||
<Text type="lg">
|
||||
This app is not available for mobile Web yet. Please open it on your
|
||||
desktop or download the iOS app.
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
bgLight: {
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
bgDark: {
|
||||
backgroundColor: colors.black, // TODO
|
||||
},
|
||||
viewBorder: {
|
||||
position: 'absolute',
|
||||
width: 1,
|
||||
height: '100%',
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: colors.gray2,
|
||||
},
|
||||
viewBorderLeft: {
|
||||
left: 'calc(50vw - 300px)',
|
||||
},
|
||||
viewBorderRight: {
|
||||
left: 'calc(50vw + 300px)',
|
||||
},
|
||||
noMobileWeb: {
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
})
|
|
@ -1,354 +0,0 @@
|
|||
import React from 'react'
|
||||
import {
|
||||
Linking,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {FEEDBACK_FORM_URL} from 'lib/constants'
|
||||
import {useStores} from 'state/index'
|
||||
import {
|
||||
HomeIcon,
|
||||
HomeIconSolid,
|
||||
BellIcon,
|
||||
BellIconSolid,
|
||||
UserIcon,
|
||||
CogIcon,
|
||||
MagnifyingGlassIcon2,
|
||||
MagnifyingGlassIcon2Solid,
|
||||
MoonIcon,
|
||||
} from 'lib/icons'
|
||||
import {TabPurpose, TabPurposeMainPath} from 'state/models/navigation'
|
||||
import {UserAvatar} from '../../com/util/UserAvatar'
|
||||
import {Text} from '../../com/util/text/Text'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useAnalytics} from 'lib/analytics'
|
||||
import {pluralize} from 'lib/strings/helpers'
|
||||
|
||||
export const Menu = observer(({onClose}: {onClose: () => void}) => {
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const {track} = useAnalytics()
|
||||
|
||||
// events
|
||||
// =
|
||||
|
||||
const onNavigate = (url: string) => {
|
||||
track('Menu:ItemClicked', {url})
|
||||
|
||||
onClose()
|
||||
if (url === TabPurposeMainPath[TabPurpose.Notifs]) {
|
||||
store.nav.switchTo(TabPurpose.Notifs, true)
|
||||
} else if (url === TabPurposeMainPath[TabPurpose.Search]) {
|
||||
store.nav.switchTo(TabPurpose.Search, true)
|
||||
} else {
|
||||
store.nav.switchTo(TabPurpose.Default, true)
|
||||
if (url !== '/') {
|
||||
store.nav.navigate(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onPressFeedback = () => {
|
||||
track('Menu:FeedbackClicked')
|
||||
Linking.openURL(FEEDBACK_FORM_URL)
|
||||
}
|
||||
|
||||
// rendering
|
||||
// =
|
||||
|
||||
const MenuItem = ({
|
||||
icon,
|
||||
label,
|
||||
count,
|
||||
url,
|
||||
bold,
|
||||
onPress,
|
||||
}: {
|
||||
icon: JSX.Element
|
||||
label: string
|
||||
count?: number
|
||||
url?: string
|
||||
bold?: boolean
|
||||
onPress?: () => void
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
testID={`menuItemButton-${label}`}
|
||||
style={styles.menuItem}
|
||||
onPress={onPress ? onPress : () => onNavigate(url || '/')}>
|
||||
<View style={[styles.menuItemIconWrapper]}>
|
||||
{icon}
|
||||
{count ? (
|
||||
<View style={styles.menuItemCount}>
|
||||
<Text style={styles.menuItemCountLabel}>{count}</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
</View>
|
||||
<Text
|
||||
type={bold ? '2xl-bold' : '2xl'}
|
||||
style={[pal.text, s.flex1]}
|
||||
numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
|
||||
const onDarkmodePress = () => {
|
||||
track('Menu:ItemClicked', {url: '/darkmode'})
|
||||
store.shell.setDarkMode(!store.shell.darkMode)
|
||||
}
|
||||
|
||||
const isAtHome =
|
||||
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Default]
|
||||
const isAtSearch =
|
||||
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Search]
|
||||
const isAtNotifications =
|
||||
store.nav.tab.current.url === TabPurposeMainPath[TabPurpose.Notifs]
|
||||
|
||||
return (
|
||||
<View
|
||||
testID="menuView"
|
||||
style={[
|
||||
styles.view,
|
||||
theme.colorScheme === 'light' ? pal.view : styles.viewDarkMode,
|
||||
]}>
|
||||
<TouchableOpacity
|
||||
testID="profileCardButton"
|
||||
onPress={() => onNavigate(`/profile/${store.me.handle}`)}>
|
||||
<UserAvatar
|
||||
size={80}
|
||||
displayName={store.me.displayName}
|
||||
handle={store.me.handle}
|
||||
avatar={store.me.avatar}
|
||||
/>
|
||||
<Text
|
||||
type="title-lg"
|
||||
style={[pal.text, s.bold, styles.profileCardDisplayName]}>
|
||||
{store.me.displayName || store.me.handle}
|
||||
</Text>
|
||||
<Text type="2xl" style={[pal.textLight, styles.profileCardHandle]}>
|
||||
@{store.me.handle}
|
||||
</Text>
|
||||
<Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}>
|
||||
<Text type="xl-medium" style={pal.text}>
|
||||
{store.me.followersCount || 0}
|
||||
</Text>{' '}
|
||||
{pluralize(store.me.followersCount || 0, 'follower')} ·{' '}
|
||||
<Text type="xl-medium" style={pal.text}>
|
||||
{store.me.followsCount || 0}
|
||||
</Text>{' '}
|
||||
following
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={s.flex1} />
|
||||
<View>
|
||||
<MenuItem
|
||||
icon={
|
||||
isAtSearch ? (
|
||||
<MagnifyingGlassIcon2Solid
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size={24}
|
||||
strokeWidth={1.7}
|
||||
/>
|
||||
) : (
|
||||
<MagnifyingGlassIcon2
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size={24}
|
||||
strokeWidth={1.7}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label="Search"
|
||||
url="/search"
|
||||
bold={isAtSearch}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={
|
||||
isAtHome ? (
|
||||
<HomeIconSolid
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="24"
|
||||
strokeWidth={3.25}
|
||||
fillOpacity={1}
|
||||
/>
|
||||
) : (
|
||||
<HomeIcon
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="24"
|
||||
strokeWidth={3.25}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label="Home"
|
||||
url="/"
|
||||
bold={isAtHome}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={
|
||||
isAtNotifications ? (
|
||||
<BellIconSolid
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="24"
|
||||
strokeWidth={1.7}
|
||||
fillOpacity={1}
|
||||
/>
|
||||
) : (
|
||||
<BellIcon
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="24"
|
||||
strokeWidth={1.7}
|
||||
/>
|
||||
)
|
||||
}
|
||||
label="Notifications"
|
||||
url="/notifications"
|
||||
count={store.me.notifications.unreadCount}
|
||||
bold={isAtNotifications}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={
|
||||
<UserIcon
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="26"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
}
|
||||
label="Profile"
|
||||
url={`/profile/${store.me.handle}`}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={
|
||||
<CogIcon
|
||||
style={pal.text as StyleProp<ViewStyle>}
|
||||
size="26"
|
||||
strokeWidth={1.75}
|
||||
/>
|
||||
}
|
||||
label="Settings"
|
||||
url="/settings"
|
||||
/>
|
||||
</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>
|
||||
<TouchableOpacity
|
||||
onPress={onPressFeedback}
|
||||
style={[
|
||||
styles.footerBtn,
|
||||
styles.footerBtnFeedback,
|
||||
theme.colorScheme === 'light'
|
||||
? styles.footerBtnFeedbackLight
|
||||
: styles.footerBtnFeedbackDark,
|
||||
]}>
|
||||
<FontAwesomeIcon
|
||||
style={pal.link as FontAwesomeIconStyle}
|
||||
size={19}
|
||||
icon={['far', 'message']}
|
||||
/>
|
||||
<Text type="2xl-medium" style={[pal.link, s.pl10]}>
|
||||
Feedback
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
view: {
|
||||
flex: 1,
|
||||
paddingTop: 20,
|
||||
paddingBottom: 50,
|
||||
paddingLeft: 30,
|
||||
},
|
||||
viewDarkMode: {
|
||||
backgroundColor: '#1B1919',
|
||||
},
|
||||
|
||||
profileCardDisplayName: {
|
||||
marginTop: 20,
|
||||
paddingRight: 20,
|
||||
},
|
||||
profileCardHandle: {
|
||||
marginTop: 4,
|
||||
paddingRight: 20,
|
||||
},
|
||||
profileCardFollowers: {
|
||||
marginTop: 16,
|
||||
paddingRight: 20,
|
||||
},
|
||||
|
||||
menuItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 16,
|
||||
paddingRight: 10,
|
||||
},
|
||||
menuItemIconWrapper: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
menuItemCount: {
|
||||
position: 'absolute',
|
||||
right: -6,
|
||||
top: -2,
|
||||
backgroundColor: colors.red3,
|
||||
paddingHorizontal: 4,
|
||||
paddingBottom: 1,
|
||||
borderRadius: 6,
|
||||
},
|
||||
menuItemCountLabel: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
color: colors.white,
|
||||
},
|
||||
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
paddingRight: 30,
|
||||
paddingTop: 80,
|
||||
},
|
||||
footerBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
borderRadius: 25,
|
||||
},
|
||||
footerBtnDarkMode: {
|
||||
backgroundColor: colors.black,
|
||||
},
|
||||
footerBtnFeedback: {
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
footerBtnFeedbackLight: {
|
||||
backgroundColor: '#DDEFFF',
|
||||
},
|
||||
footerBtnFeedbackDark: {
|
||||
backgroundColor: colors.blue6,
|
||||
},
|
||||
})
|
|
@ -1,335 +0,0 @@
|
|||
import React, {useState} from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {
|
||||
Animated,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
TouchableWithoutFeedback,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {ScreenContainer, Screen} from 'react-native-screens'
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
import {useStores} from 'state/index'
|
||||
import {NavigationModel} from 'state/models/navigation'
|
||||
import {match, MatchResult} from '../../routes'
|
||||
import {Login} from '../../screens/Login'
|
||||
import {Menu} from './Menu'
|
||||
import {BottomBar} from './BottomBar'
|
||||
import {HorzSwipe} from '../../com/util/gestures/HorzSwipe'
|
||||
import {ModalsContainer} from '../../com/modals/Modal'
|
||||
import {Lightbox} from '../../com/lightbox/Lightbox'
|
||||
import {Text} from '../../com/util/text/Text'
|
||||
import {ErrorBoundary} from '../../com/util/ErrorBoundary'
|
||||
import {Composer} from './Composer'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
|
||||
export const MobileShell: React.FC = observer(() => {
|
||||
const theme = useTheme()
|
||||
const pal = usePalette('default')
|
||||
const store = useStores()
|
||||
const winDim = useWindowDimensions()
|
||||
const [menuSwipingDirection, setMenuSwipingDirection] = useState(0)
|
||||
const swipeGestureInterp = useAnimatedValue(0)
|
||||
const safeAreaInsets = useSafeAreaInsets()
|
||||
const screenRenderDesc = constructScreenRenderDesc(store.nav)
|
||||
|
||||
// navigation swipes
|
||||
// =
|
||||
const isMenuActive = store.shell.isMainMenuOpen
|
||||
const canSwipeLeft = store.nav.tab.canGoBack || !isMenuActive
|
||||
const canSwipeRight = isMenuActive
|
||||
const onNavSwipeStartDirection = (dx: number) => {
|
||||
if (dx < 0 && !store.nav.tab.canGoBack) {
|
||||
setMenuSwipingDirection(dx)
|
||||
} else if (dx > 0 && isMenuActive) {
|
||||
setMenuSwipingDirection(dx)
|
||||
} else {
|
||||
setMenuSwipingDirection(0)
|
||||
}
|
||||
}
|
||||
const onNavSwipeEnd = (dx: number) => {
|
||||
if (dx < 0) {
|
||||
if (store.nav.tab.canGoBack) {
|
||||
store.nav.tab.goBack()
|
||||
} else {
|
||||
store.shell.setMainMenuOpen(true)
|
||||
}
|
||||
} else if (dx > 0) {
|
||||
if (isMenuActive) {
|
||||
store.shell.setMainMenuOpen(false)
|
||||
}
|
||||
}
|
||||
setMenuSwipingDirection(0)
|
||||
}
|
||||
const swipeTranslateX = Animated.multiply(
|
||||
swipeGestureInterp,
|
||||
winDim.width * -1,
|
||||
)
|
||||
const swipeTransform = store.nav.tab.canGoBack
|
||||
? {transform: [{translateX: swipeTranslateX}]}
|
||||
: undefined
|
||||
let shouldRenderMenu = false
|
||||
let menuTranslateX
|
||||
const menuDrawerWidth = winDim.width - 100
|
||||
if (isMenuActive) {
|
||||
// menu is active, interpret swipes as closes
|
||||
menuTranslateX = Animated.multiply(swipeGestureInterp, menuDrawerWidth * -1)
|
||||
shouldRenderMenu = true
|
||||
} else if (!store.nav.tab.canGoBack) {
|
||||
// at back of history, interpret swipes as opens
|
||||
menuTranslateX = Animated.subtract(
|
||||
menuDrawerWidth * -1,
|
||||
Animated.multiply(swipeGestureInterp, menuDrawerWidth),
|
||||
)
|
||||
shouldRenderMenu = true
|
||||
}
|
||||
const menuSwipeTransform = menuTranslateX
|
||||
? {
|
||||
transform: [{translateX: menuTranslateX}],
|
||||
}
|
||||
: undefined
|
||||
const swipeOpacity = {
|
||||
opacity: swipeGestureInterp.interpolate({
|
||||
inputRange: [-1, 0, 1],
|
||||
outputRange: [0, 0.6, 0],
|
||||
}),
|
||||
}
|
||||
const menuSwipeOpacity =
|
||||
menuSwipingDirection !== 0
|
||||
? {
|
||||
opacity: swipeGestureInterp.interpolate({
|
||||
inputRange: menuSwipingDirection > 0 ? [0, 1] : [-1, 0],
|
||||
outputRange: [0.6, 0],
|
||||
}),
|
||||
}
|
||||
: undefined
|
||||
|
||||
if (store.hackUpgradeNeeded) {
|
||||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
<View style={[s.flexCol, s.p20, s.h100pct]}>
|
||||
<View style={s.flex1} />
|
||||
<View>
|
||||
<Text type="title-2xl" style={s.pb10}>
|
||||
Update required
|
||||
</Text>
|
||||
<Text style={[s.pb20, s.bold]}>
|
||||
Please update your app to the latest version. If no update is
|
||||
available yet, please check the App Store in a day or so.
|
||||
</Text>
|
||||
<Text type="title" style={s.pb10}>
|
||||
What's happening?
|
||||
</Text>
|
||||
<Text style={s.pb10}>
|
||||
We're in the final stages of the AT Protocol's v1 development. To
|
||||
make sure everything works as well as possible, we're making final
|
||||
breaking changes to the APIs.
|
||||
</Text>
|
||||
<Text>
|
||||
If we didn't botch this process, a new version of the app should
|
||||
be available now.
|
||||
</Text>
|
||||
</View>
|
||||
<View style={s.flex1} />
|
||||
<View style={s.footerSpacer} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!store.session.hasSession) {
|
||||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
<StatusBar
|
||||
barStyle={
|
||||
theme.colorScheme === 'dark' ? 'light-content' : 'dark-content'
|
||||
}
|
||||
/>
|
||||
<Login />
|
||||
<ModalsContainer />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const screenBg = {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? colors.black : colors.gray1,
|
||||
}
|
||||
return (
|
||||
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
|
||||
<StatusBar
|
||||
barStyle={
|
||||
theme.colorScheme === 'dark' ? 'light-content' : 'dark-content'
|
||||
}
|
||||
/>
|
||||
<View style={[styles.innerContainer, {paddingTop: safeAreaInsets.top}]}>
|
||||
<HorzSwipe
|
||||
distThresholdDivisor={2.5}
|
||||
useNativeDriver
|
||||
panX={swipeGestureInterp}
|
||||
swipeEnabled
|
||||
canSwipeLeft={canSwipeLeft}
|
||||
canSwipeRight={canSwipeRight}
|
||||
onSwipeStartDirection={onNavSwipeStartDirection}
|
||||
onSwipeEnd={onNavSwipeEnd}>
|
||||
<ScreenContainer style={styles.screenContainer}>
|
||||
{screenRenderDesc.screens.map(
|
||||
({Com, navIdx, params, key, current, previous}) => {
|
||||
if (isMenuActive) {
|
||||
// HACK menu is active, treat current as previous
|
||||
if (previous) {
|
||||
previous = false
|
||||
} else if (current) {
|
||||
current = false
|
||||
previous = true
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Screen
|
||||
key={key}
|
||||
style={[StyleSheet.absoluteFill]}
|
||||
activityState={current ? 2 : previous ? 1 : 0}>
|
||||
<Animated.View
|
||||
style={
|
||||
current ? [styles.screenMask, swipeOpacity] : undefined
|
||||
}
|
||||
/>
|
||||
<Animated.View
|
||||
style={[
|
||||
s.h100pct,
|
||||
screenBg,
|
||||
current ? [swipeTransform] : undefined,
|
||||
]}>
|
||||
<ErrorBoundary>
|
||||
<Com
|
||||
params={params}
|
||||
navIdx={navIdx}
|
||||
visible={current}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Animated.View>
|
||||
</Screen>
|
||||
)
|
||||
},
|
||||
)}
|
||||
</ScreenContainer>
|
||||
<BottomBar />
|
||||
{isMenuActive || menuSwipingDirection !== 0 ? (
|
||||
<TouchableWithoutFeedback
|
||||
onPress={() => store.shell.setMainMenuOpen(false)}>
|
||||
<Animated.View style={[styles.screenMask, menuSwipeOpacity]} />
|
||||
</TouchableWithoutFeedback>
|
||||
) : undefined}
|
||||
{shouldRenderMenu && (
|
||||
<Animated.View style={[styles.menuDrawer, menuSwipeTransform]}>
|
||||
<Menu onClose={() => store.shell.setMainMenuOpen(false)} />
|
||||
</Animated.View>
|
||||
)}
|
||||
</HorzSwipe>
|
||||
</View>
|
||||
<ModalsContainer />
|
||||
<Lightbox />
|
||||
<Composer
|
||||
active={store.shell.isComposerActive}
|
||||
onClose={() => store.shell.closeComposer()}
|
||||
winHeight={winDim.height}
|
||||
replyTo={store.shell.composerOpts?.replyTo}
|
||||
imagesOpen={store.shell.composerOpts?.imagesOpen}
|
||||
onPost={store.shell.composerOpts?.onPost}
|
||||
quote={store.shell.composerOpts?.quote}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* This method produces the information needed by the shell to
|
||||
* render the current screens with screen-caching behaviors.
|
||||
*/
|
||||
type ScreenRenderDesc = MatchResult & {
|
||||
key: string
|
||||
navIdx: string
|
||||
current: boolean
|
||||
previous: boolean
|
||||
isNewTab: boolean
|
||||
}
|
||||
function constructScreenRenderDesc(nav: NavigationModel): {
|
||||
icon: IconProp
|
||||
hasNewTab: boolean
|
||||
screens: ScreenRenderDesc[]
|
||||
} {
|
||||
let hasNewTab = false
|
||||
let icon: IconProp = 'magnifying-glass'
|
||||
let screens: ScreenRenderDesc[] = []
|
||||
for (const tab of nav.tabs) {
|
||||
const tabScreens = [
|
||||
...tab.getBackList(5),
|
||||
Object.assign({}, tab.current, {index: tab.index}),
|
||||
]
|
||||
const parsedTabScreens = tabScreens.map(screen => {
|
||||
const isCurrent = nav.isCurrentScreen(tab.id, screen.index)
|
||||
const isPrevious = nav.isCurrentScreen(tab.id, screen.index + 1)
|
||||
const matchRes = match(screen.url)
|
||||
if (isCurrent) {
|
||||
icon = matchRes.icon
|
||||
}
|
||||
hasNewTab = hasNewTab || tab.isNewTab
|
||||
return Object.assign(matchRes, {
|
||||
key: `t${tab.id}-s${screen.index}`,
|
||||
navIdx: `${tab.id}-${screen.id}`,
|
||||
current: isCurrent,
|
||||
previous: isPrevious,
|
||||
isNewTab: tab.isNewTab,
|
||||
}) as ScreenRenderDesc
|
||||
})
|
||||
screens = screens.concat(parsedTabScreens)
|
||||
}
|
||||
return {
|
||||
icon,
|
||||
hasNewTab,
|
||||
screens,
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outerContainer: {
|
||||
height: '100%',
|
||||
},
|
||||
innerContainer: {
|
||||
height: '100%',
|
||||
},
|
||||
screenContainer: {
|
||||
height: '100%',
|
||||
},
|
||||
screenMask: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: '#000',
|
||||
opacity: 0.6,
|
||||
},
|
||||
menuDrawer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 100,
|
||||
},
|
||||
topBarProtector: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 50, // will be overwritten by insets
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
topBarProtectorDark: {
|
||||
backgroundColor: colors.black,
|
||||
},
|
||||
})
|
|
@ -1,222 +0,0 @@
|
|||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {Pressable, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||
import {useStores} from 'state/index'
|
||||
import {colors} from 'lib/styles'
|
||||
import {
|
||||
ComposeIcon,
|
||||
HomeIcon,
|
||||
HomeIconSolid,
|
||||
BellIcon,
|
||||
BellIconSolid,
|
||||
MagnifyingGlassIcon,
|
||||
CogIcon,
|
||||
} from 'lib/icons'
|
||||
import {DesktopSearch} from './DesktopSearch'
|
||||
|
||||
interface NavItemProps {
|
||||
count?: number
|
||||
href: string
|
||||
icon: JSX.Element
|
||||
iconFilled: JSX.Element
|
||||
isProfile?: boolean
|
||||
}
|
||||
export const NavItem = observer(
|
||||
({count, href, icon, iconFilled}: NavItemProps) => {
|
||||
const store = useStores()
|
||||
const hoverBg = useColorSchemeStyle(
|
||||
styles.navItemHoverBgLight,
|
||||
styles.navItemHoverBgDark,
|
||||
)
|
||||
const isCurrent = store.nav.tab.current.url === href
|
||||
const onPress = () => store.nav.navigate(href)
|
||||
return (
|
||||
<Pressable
|
||||
style={state => [
|
||||
styles.navItem,
|
||||
// @ts-ignore Pressable state differs for RNW -prf
|
||||
(state.hovered || isCurrent) && hoverBg,
|
||||
]}
|
||||
onPress={onPress}>
|
||||
<View style={[styles.navItemIconWrapper]}>
|
||||
{isCurrent ? iconFilled : icon}
|
||||
{typeof count === 'number' && count > 0 && (
|
||||
<Text type="button" style={styles.navItemCount}>
|
||||
{count}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const ProfileItem = observer(() => {
|
||||
const store = useStores()
|
||||
const hoverBg = useColorSchemeStyle(
|
||||
styles.navItemHoverBgLight,
|
||||
styles.navItemHoverBgDark,
|
||||
)
|
||||
const href = `/profile/${store.me.handle}`
|
||||
const isCurrent = store.nav.tab.current.url === href
|
||||
const onPress = () => store.nav.navigate(href)
|
||||
return (
|
||||
<Pressable
|
||||
style={state => [
|
||||
styles.navItem,
|
||||
// @ts-ignore Pressable state differs for RNW -prf
|
||||
(state.hovered || isCurrent) && hoverBg,
|
||||
]}
|
||||
onPress={onPress}>
|
||||
<View style={[styles.navItemIconWrapper]}>
|
||||
<UserAvatar
|
||||
handle={store.me.handle}
|
||||
displayName={store.me.displayName}
|
||||
avatar={store.me.avatar}
|
||||
size={28}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
})
|
||||
|
||||
export const DesktopHeader = observer(function DesktopHeader({}: {
|
||||
canGoBack?: boolean
|
||||
}) {
|
||||
const store = useStores()
|
||||
const pal = usePalette('default')
|
||||
const onPressCompose = () => store.shell.openComposer({})
|
||||
|
||||
return (
|
||||
<View style={[styles.header, pal.borderDark, pal.view]}>
|
||||
<Text type="title-xl" style={[pal.text, styles.title]}>
|
||||
Bluesky
|
||||
</Text>
|
||||
<View style={styles.space30} />
|
||||
<NavItem
|
||||
href="/"
|
||||
icon={<HomeIcon size={24} />}
|
||||
iconFilled={<HomeIconSolid size={24} />}
|
||||
/>
|
||||
<View style={styles.space15} />
|
||||
<NavItem
|
||||
href="/search"
|
||||
icon={<MagnifyingGlassIcon size={24} />}
|
||||
iconFilled={<MagnifyingGlassIcon strokeWidth={3} size={24} />}
|
||||
/>
|
||||
<View style={styles.space15} />
|
||||
<NavItem
|
||||
href="/notifications"
|
||||
count={store.me.notifications.unreadCount}
|
||||
icon={<BellIcon size={24} />}
|
||||
iconFilled={<BellIconSolid size={24} />}
|
||||
/>
|
||||
<View style={styles.spaceFlex} />
|
||||
<TouchableOpacity style={[styles.newPostBtn]} onPress={onPressCompose}>
|
||||
<View style={styles.newPostBtnIconWrapper}>
|
||||
<ComposeIcon
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
style={styles.newPostBtnLabel}
|
||||
/>
|
||||
</View>
|
||||
<Text type="md" style={styles.newPostBtnLabel}>
|
||||
New Post
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.space20} />
|
||||
<DesktopSearch />
|
||||
<View style={styles.space15} />
|
||||
<ProfileItem />
|
||||
<NavItem
|
||||
href="/settings"
|
||||
icon={<CogIcon strokeWidth={2} size={28} />}
|
||||
iconFilled={<CogIcon strokeWidth={2.5} size={28} />}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
// paddingTop: 18,
|
||||
// paddingBottom: 18,
|
||||
paddingLeft: 30,
|
||||
paddingRight: 40,
|
||||
borderBottomWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
|
||||
spaceFlex: {
|
||||
flex: 1,
|
||||
},
|
||||
space15: {
|
||||
width: 15,
|
||||
},
|
||||
space20: {
|
||||
width: 20,
|
||||
},
|
||||
space30: {
|
||||
width: 30,
|
||||
},
|
||||
|
||||
title: {},
|
||||
|
||||
navItem: {
|
||||
paddingTop: 14,
|
||||
paddingBottom: 10,
|
||||
paddingHorizontal: 10,
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: 'transparent',
|
||||
},
|
||||
navItemHoverBgLight: {
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: colors.blue3,
|
||||
},
|
||||
navItemHoverBgDark: {
|
||||
borderBottomWidth: 2,
|
||||
backgroundColor: colors.blue3,
|
||||
},
|
||||
navItemIconWrapper: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: 28,
|
||||
height: 28,
|
||||
marginBottom: 2,
|
||||
},
|
||||
navItemCount: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 15,
|
||||
backgroundColor: colors.red3,
|
||||
color: colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
paddingHorizontal: 4,
|
||||
borderRadius: 6,
|
||||
},
|
||||
|
||||
newPostBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 24,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
paddingHorizontal: 18,
|
||||
backgroundColor: colors.blue3,
|
||||
},
|
||||
newPostBtnIconWrapper: {
|
||||
marginRight: 8,
|
||||
},
|
||||
newPostBtnLabel: {
|
||||
color: colors.white,
|
||||
},
|
||||
})
|
|
@ -1,150 +0,0 @@
|
|||
import React from 'react'
|
||||
import {observer} from 'mobx-react-lite'
|
||||
import {View, StyleSheet} from 'react-native'
|
||||
import {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
import {useStores} from 'state/index'
|
||||
import {NavigationModel} from 'state/models/navigation'
|
||||
import {match, MatchResult} from '../../routes'
|
||||
import {DesktopHeader} from './DesktopHeader'
|
||||
import {Login} from '../../screens/Login'
|
||||
import {ErrorBoundary} from '../../com/util/ErrorBoundary'
|
||||
import {Lightbox} from '../../com/lightbox/Lightbox'
|
||||
import {ModalsContainer} from '../../com/modals/Modal'
|
||||
import {Text} from 'view/com/util/text/Text'
|
||||
import {Composer} from './Composer'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
|
||||
import {s, colors} from 'lib/styles'
|
||||
import {isMobileWeb} from 'platform/detection'
|
||||
|
||||
export const WebShell: React.FC = observer(() => {
|
||||
const pageBg = useColorSchemeStyle(styles.bgLight, styles.bgDark)
|
||||
const store = useStores()
|
||||
const screenRenderDesc = constructScreenRenderDesc(store.nav)
|
||||
|
||||
if (isMobileWeb) {
|
||||
return <NoMobileWeb />
|
||||
}
|
||||
|
||||
if (!store.session.hasSession) {
|
||||
return (
|
||||
<View style={styles.outerContainer}>
|
||||
<Login />
|
||||
<ModalsContainer />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.outerContainer, pageBg]}>
|
||||
<DesktopHeader />
|
||||
{screenRenderDesc.screens.map(({Com, navIdx, params, key, current}) => (
|
||||
<View
|
||||
key={key}
|
||||
style={[s.hContentRegion, current ? styles.visible : styles.hidden]}>
|
||||
<ErrorBoundary>
|
||||
<Com params={params} navIdx={navIdx} visible={current} />
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
))}
|
||||
<Composer
|
||||
active={store.shell.isComposerActive}
|
||||
onClose={() => store.shell.closeComposer()}
|
||||
winHeight={0}
|
||||
replyTo={store.shell.composerOpts?.replyTo}
|
||||
imagesOpen={store.shell.composerOpts?.imagesOpen}
|
||||
onPost={store.shell.composerOpts?.onPost}
|
||||
/>
|
||||
<ModalsContainer />
|
||||
<Lightbox />
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* This method produces the information needed by the shell to
|
||||
* render the current screens with screen-caching behaviors.
|
||||
*/
|
||||
type ScreenRenderDesc = MatchResult & {
|
||||
key: string
|
||||
navIdx: string
|
||||
current: boolean
|
||||
previous: boolean
|
||||
isNewTab: boolean
|
||||
}
|
||||
function constructScreenRenderDesc(nav: NavigationModel): {
|
||||
icon: IconProp
|
||||
hasNewTab: boolean
|
||||
screens: ScreenRenderDesc[]
|
||||
} {
|
||||
let hasNewTab = false
|
||||
let icon: IconProp = 'magnifying-glass'
|
||||
let screens: ScreenRenderDesc[] = []
|
||||
for (const tab of nav.tabs) {
|
||||
const tabScreens = [
|
||||
...tab.getBackList(5),
|
||||
Object.assign({}, tab.current, {index: tab.index}),
|
||||
]
|
||||
const parsedTabScreens = tabScreens.map(screen => {
|
||||
const isCurrent = nav.isCurrentScreen(tab.id, screen.index)
|
||||
const isPrevious = nav.isCurrentScreen(tab.id, screen.index + 1)
|
||||
const matchRes = match(screen.url)
|
||||
if (isCurrent) {
|
||||
icon = matchRes.icon
|
||||
}
|
||||
hasNewTab = hasNewTab || tab.isNewTab
|
||||
return Object.assign(matchRes, {
|
||||
key: `t${tab.id}-s${screen.index}`,
|
||||
navIdx: `${tab.id}-${screen.id}`,
|
||||
current: isCurrent,
|
||||
previous: isPrevious,
|
||||
isNewTab: tab.isNewTab,
|
||||
}) as ScreenRenderDesc
|
||||
})
|
||||
screens = screens.concat(parsedTabScreens)
|
||||
}
|
||||
return {
|
||||
icon,
|
||||
hasNewTab,
|
||||
screens,
|
||||
}
|
||||
}
|
||||
|
||||
function NoMobileWeb() {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<View style={[pal.view, styles.noMobileWeb]}>
|
||||
<Text type="title-2xl" style={s.pb20}>
|
||||
We're so sorry!
|
||||
</Text>
|
||||
<Text type="lg">
|
||||
This app is not available for mobile Web yet. Please open it on your
|
||||
desktop or download the iOS app.
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outerContainer: {
|
||||
height: '100%',
|
||||
},
|
||||
bgLight: {
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
bgDark: {
|
||||
backgroundColor: colors.black, // TODO
|
||||
},
|
||||
visible: {
|
||||
display: 'flex',
|
||||
},
|
||||
hidden: {
|
||||
display: 'none',
|
||||
},
|
||||
noMobileWeb: {
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 40,
|
||||
},
|
||||
})
|
Loading…
Add table
Add a link
Reference in a new issue