diff options
Diffstat (limited to 'src/view/shell/desktop')
-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 | 145 |
3 files changed, 445 insertions, 0 deletions
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/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx new file mode 100644 index 000000000..7c96dbac2 --- /dev/null +++ b/src/view/shell/desktop/Search.tsx @@ -0,0 +1,145 @@ +import React from 'react' +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 {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() + const pal = usePalette('default') + const textInput = React.useRef<TextInput>(null) + const [isInputFocused, setIsInputFocused] = React.useState<boolean>(false) + const [query, setQuery] = React.useState<string>('') + const autocompleteView = React.useMemo<UserAutocompleteViewModel>( + () => new UserAutocompleteViewModel(store), + [store], + ) + + const onChangeQuery = (text: string) => { + setQuery(text) + if (text.length > 0 && isInputFocused) { + autocompleteView.setActive(true) + autocompleteView.setPrefix(text) + } else { + autocompleteView.setActive(false) + } + } + + const onPressCancelSearch = () => { + setQuery('') + autocompleteView.setActive(false) + } + + return ( + <View style={styles.container}> + <View + style={[{backgroundColor: pal.colors.backgroundLight}, styles.search]}> + <View style={[styles.inputContainer]}> + <MagnifyingGlassIcon2 + size={18} + style={[pal.textLight, styles.iconWrapper]} + /> + <TextInput + testID="searchTextInput" + ref={textInput} + placeholder="Search" + placeholderTextColor={pal.colors.textLight} + selectTextOnFocus + returnKeyType="search" + value={query} + style={[pal.textLight, styles.input]} + onFocus={() => setIsInputFocused(true)} + onBlur={() => setIsInputFocused(false)} + onChangeText={onChangeQuery} + /> + {query ? ( + <View style={styles.cancelBtn}> + <TouchableOpacity onPress={onPressCancelSearch}> + <Text type="lg" style={[pal.link]}> + Cancel + </Text> + </TouchableOpacity> + </View> + ) : undefined} + </View> + </View> + + {query !== '' && ( + <View style={[pal.view, pal.borderDark, styles.resultsContainer]}> + {autocompleteView.searchRes.length ? ( + <> + {autocompleteView.searchRes.map((item, i) => ( + <ProfileCard + key={item.did} + handle={item.handle} + displayName={item.displayName} + avatar={item.avatar} + noBorder={i === 0} + /> + ))} + </> + ) : ( + <View> + <Text style={[pal.textLight, styles.noResults]}> + No results found for {autocompleteView.prefix} + </Text> + </View> + )} + </View> + )} + </View> + ) +}) + +const styles = StyleSheet.create({ + container: { + position: 'relative', + width: 300, + }, + search: { + paddingHorizontal: 16, + paddingVertical: 2, + width: 300, + borderRadius: 20, + }, + inputContainer: { + flexDirection: 'row', + }, + iconWrapper: { + position: 'relative', + top: 2, + paddingVertical: 7, + marginRight: 8, + }, + input: { + flex: 1, + fontSize: 18, + width: '100%', + paddingTop: 7, + paddingBottom: 7, + }, + cancelBtn: { + paddingRight: 4, + paddingLeft: 10, + paddingVertical: 7, + }, + resultsContainer: { + // @ts-ignore supported by web + position: 'fixed', + marginTop: 40, + + flexDirection: 'column', + width: 300, + borderWidth: 1, + borderRadius: 6, + paddingVertical: 4, + }, + noResults: { + textAlign: 'center', + paddingVertical: 10, + }, +}) |