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:
Paul Frazee 2023-03-13 16:01:43 -05:00 committed by GitHub
parent 503e03d91e
commit 56cf890deb
222 changed files with 8705 additions and 6338 deletions

View file

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

View file

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

View file

@ -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
View 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')} &middot;{' '}
<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,
},
})

View 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',
},
})

View 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,
},
})

View file

@ -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
View 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%',
},
})

View 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,
},
})

View file

@ -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')} &middot;{' '}
<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,
},
})

View file

@ -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,
},
})

View file

@ -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,
},
})

View file

@ -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,
},
})