diff options
Diffstat (limited to 'src/view/shell')
-rw-r--r-- | src/view/shell/BottomBar.tsx (renamed from src/view/shell/mobile/BottomBar.tsx) | 90 | ||||
-rw-r--r-- | src/view/shell/Composer.tsx (renamed from src/view/shell/mobile/Composer.tsx) | 5 | ||||
-rw-r--r-- | src/view/shell/Composer.web.tsx (renamed from src/view/shell/web/Composer.tsx) | 11 | ||||
-rw-r--r-- | src/view/shell/Drawer.tsx | 386 | ||||
-rw-r--r-- | src/view/shell/desktop/LeftNav.tsx | 254 | ||||
-rw-r--r-- | src/view/shell/desktop/RightNav.tsx | 46 | ||||
-rw-r--r-- | src/view/shell/desktop/Search.tsx (renamed from src/view/shell/web/DesktopSearch.tsx) | 26 | ||||
-rw-r--r-- | src/view/shell/index.tsx | 139 | ||||
-rw-r--r-- | src/view/shell/index.web.tsx | 113 | ||||
-rw-r--r-- | src/view/shell/mobile/Menu.tsx | 354 | ||||
-rw-r--r-- | src/view/shell/mobile/index.tsx | 335 | ||||
-rw-r--r-- | src/view/shell/web/DesktopHeader.tsx | 222 | ||||
-rw-r--r-- | src/view/shell/web/index.tsx | 150 |
13 files changed, 994 insertions, 1137 deletions
diff --git a/src/view/shell/mobile/BottomBar.tsx b/src/view/shell/BottomBar.tsx index 73c2501ab..18b06968f 100644 --- a/src/view/shell/mobile/BottomBar.tsx +++ b/src/view/shell/BottomBar.tsx @@ -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() - } - } else { - store.nav.switchTo(TabPurpose.Default, false) - if (store.nav.tab.index === 0) { - store.nav.tab.fixedTabReset() + navigation.navigate(`${tab}Tab`) } - } - }, [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 diff --git a/src/view/shell/mobile/Composer.tsx b/src/view/shell/Composer.tsx index 5fca118bd..2ab01c656 100644 --- a/src/view/shell/mobile/Composer.tsx +++ b/src/view/shell/Composer.tsx @@ -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} diff --git a/src/view/shell/web/Composer.tsx b/src/view/shell/Composer.web.tsx index 0d8484262..465b475fb 100644 --- a/src/view/shell/web/Composer.tsx +++ b/src/view/shell/Composer.web.tsx @@ -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> ) diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx new file mode 100644 index 000000000..80944e10a --- /dev/null +++ b/src/view/shell/Drawer.tsx @@ -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, + }, +}) diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx new file mode 100644 index 000000000..46c77178b --- /dev/null +++ b/src/view/shell/desktop/LeftNav.tsx @@ -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', + }, +}) diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx new file mode 100644 index 000000000..a196951af --- /dev/null +++ b/src/view/shell/desktop/RightNav.tsx @@ -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, + }, +}) diff --git a/src/view/shell/web/DesktopSearch.tsx b/src/view/shell/desktop/Search.tsx index 43f13ca2b..7c96dbac2 100644 --- a/src/view/shell/web/DesktopSearch.tsx +++ b/src/view/shell/desktop/Search.tsx @@ -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, diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx new file mode 100644 index 000000000..116915ff4 --- /dev/null +++ b/src/view/shell/index.tsx @@ -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%', + }, +}) diff --git a/src/view/shell/index.web.tsx b/src/view/shell/index.web.tsx new file mode 100644 index 000000000..9a97505e8 --- /dev/null +++ b/src/view/shell/index.web.tsx @@ -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, + }, +}) diff --git a/src/view/shell/mobile/Menu.tsx b/src/view/shell/mobile/Menu.tsx deleted file mode 100644 index 927e712e1..000000000 --- a/src/view/shell/mobile/Menu.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx deleted file mode 100644 index 01df6c165..000000000 --- a/src/view/shell/mobile/index.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/shell/web/DesktopHeader.tsx b/src/view/shell/web/DesktopHeader.tsx deleted file mode 100644 index 8748ebbde..000000000 --- a/src/view/shell/web/DesktopHeader.tsx +++ /dev/null @@ -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, - }, -}) diff --git a/src/view/shell/web/index.tsx b/src/view/shell/web/index.tsx deleted file mode 100644 index a76ae8060..000000000 --- a/src/view/shell/web/index.tsx +++ /dev/null @@ -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, - }, -}) |