diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-03-06 10:54:56 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-06 10:54:56 -0600 |
commit | eeac64cc88f37fe561e3c4361f40681fbe2f6d99 (patch) | |
tree | 314f889b8e283cd9f9b4dd416a849cee1ce7d82e /src | |
parent | 159615990d95aee47a983fd704a87443ec2f29b1 (diff) | |
download | voidsky-eeac64cc88f37fe561e3c4361f40681fbe2f6d99.tar.zst |
Look & feel updates: replace the "FAB" with a footer menu item, update the side menu (#263)
* Remove old tab controls from the mobile shell * Add 'compose' and 'profile' to the footer; remove the FAB * Fix lint * Tune the footer icons * Tune the 'current' state of footer icons * Add 2xl text styles * Tune the footer icons a bit more * Fix lint * More footer tuning
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/ThemeContext.tsx | 5 | ||||
-rw-r--r-- | src/lib/icons.tsx | 202 | ||||
-rw-r--r-- | src/lib/styles.ts | 1 | ||||
-rw-r--r-- | src/lib/themes.ts | 25 | ||||
-rw-r--r-- | src/view/index.ts | 2 | ||||
-rw-r--r-- | src/view/screens/Debug.tsx | 15 | ||||
-rw-r--r-- | src/view/screens/Home.tsx | 12 | ||||
-rw-r--r-- | src/view/screens/Profile.tsx | 6 | ||||
-rw-r--r-- | src/view/shell/mobile/Menu.tsx | 253 | ||||
-rw-r--r-- | src/view/shell/mobile/TabsSelector.tsx | 327 | ||||
-rw-r--r-- | src/view/shell/mobile/index.tsx | 288 |
11 files changed, 498 insertions, 638 deletions
diff --git a/src/lib/ThemeContext.tsx b/src/lib/ThemeContext.tsx index bcfc076f4..ef17c1e7a 100644 --- a/src/lib/ThemeContext.tsx +++ b/src/lib/ThemeContext.tsx @@ -28,6 +28,11 @@ export type ShapeName = 'button' | 'bigButton' | 'smallButton' export type Shapes = Record<ShapeName, ViewStyle> export type TypographyVariant = + | '2xl-thin' + | '2xl' + | '2xl-medium' + | '2xl-bold' + | '2xl-heavy' | 'xl-thin' | 'xl' | 'xl-medium' diff --git a/src/lib/icons.tsx b/src/lib/icons.tsx index 931e3c721..e763ed1b2 100644 --- a/src/lib/icons.tsx +++ b/src/lib/icons.tsx @@ -1,6 +1,6 @@ import React from 'react' import {StyleProp, TextStyle, ViewStyle} from 'react-native' -import Svg, {Path, Rect} from 'react-native-svg' +import Svg, {Path, Rect, Line, Ellipse} from 'react-native-svg' export function GridIcon({ style, @@ -72,9 +72,13 @@ export function HomeIcon({ export function HomeIconSolid({ style, size, + strokeWidth = 4, + fillOpacity = 1, }: { style?: StyleProp<ViewStyle> size?: string | number + strokeWidth?: number + fillOpacity?: number }) { return ( <Svg @@ -84,8 +88,13 @@ export function HomeIconSolid({ stroke="currentColor" style={style}> <Path - strokeWidth={2} fill="currentColor" + stroke="none" + opacity={fillOpacity} + d="M 23.951 2 C 23.631 2.011 23.323 2.124 23.072 2.322 L 8.859 13.52 C 7.055 14.941 6 17.114 6 19.41 L 6 38.5 C 6 39.864 7.136 41 8.5 41 L 18.5 41 C 19.864 41 21 39.864 21 38.5 L 21 28.5 C 21 28.205 21.205 28 21.5 28 L 26.5 28 C 26.795 28 27 28.205 27 28.5 L 27 38.5 C 27 39.864 28.136 41 29.5 41 L 39.5 41 C 40.864 41 42 39.864 42 38.5 L 42 19.41 C 42 17.114 40.945 14.941 39.141 13.52 L 24.928 2.322 C 24.65 2.103 24.304 1.989 23.951 2 Z" + /> + <Path + strokeWidth={strokeWidth} d="M 23.951 2 C 23.631 2.011 23.323 2.124 23.072 2.322 L 8.859 13.52 C 7.055 14.941 6 17.114 6 19.41 L 6 38.5 C 6 39.864 7.136 41 8.5 41 L 18.5 41 C 19.864 41 21 39.864 21 38.5 L 21 28.5 C 21 28.205 21.205 28 21.5 28 L 26.5 28 C 26.795 28 27 28.205 27 28.5 L 27 38.5 C 27 39.864 28.136 41 29.5 41 L 39.5 41 C 40.864 41 42 39.864 42 38.5 L 42 19.41 C 42 17.114 40.945 14.941 39.141 13.52 L 24.928 2.322 C 24.65 2.103 24.304 1.989 23.951 2 Z" /> </Svg> @@ -121,37 +130,74 @@ export function MagnifyingGlassIcon({ ) } -// https://github.com/Remix-Design/RemixIcon/blob/master/License -export function BellIcon({ +export function MagnifyingGlassIcon2({ style, size, + strokeWidth = 2, }: { style?: StyleProp<ViewStyle> size?: string | number + strokeWidth?: number }) { return ( <Svg fill="none" viewBox="0 0 24 24" + strokeWidth={strokeWidth} + stroke="currentColor" width={size || 24} height={size || 24} style={style}> - <Path fill="none" d="M0 0h24v24H0z" /> - <Path + <Ellipse cx="12" cy="11" rx="9" ry="9" /> + <Line x1="19" y1="17.3" x2="23.5" y2="21" strokeLinecap="round" /> + </Svg> + ) +} + +export function MagnifyingGlassIcon2Solid({ + style, + size, + strokeWidth = 2, + fillOpacity = 1, +}: { + style?: StyleProp<ViewStyle> + size?: string | number + strokeWidth?: number + fillOpacity?: number +}) { + return ( + <Svg + fill="none" + viewBox="0 0 24 24" + strokeWidth={strokeWidth} + stroke="currentColor" + width={size || 24} + height={size || 24} + style={style}> + <Ellipse + cx="12" + cy="11" + rx="7" + ry="7" + stroke="none" fill="currentColor" - d="M20 17h2v2H2v-2h2v-7a8 8 0 1 1 16 0v7zm-2 0v-7a6 6 0 1 0-12 0v7h12zm-9 4h6v2H9v-2z" + opacity={fillOpacity} /> + <Ellipse cx="12" cy="11" rx="9" ry="9" /> + <Line x1="19" y1="17.3" x2="23.5" y2="21" strokeLinecap="round" /> </Svg> ) } // https://github.com/Remix-Design/RemixIcon/blob/master/License -export function BellIconSolid({ +export function BellIcon({ style, size, + strokeWidth = 1.5, }: { style?: StyleProp<ViewStyle> size?: string | number + strokeWidth?: number }) { return ( <Svg @@ -159,12 +205,43 @@ export function BellIconSolid({ viewBox="0 0 24 24" width={size || 24} height={size || 24} + strokeWidth={strokeWidth} + stroke="currentColor" + style={style}> + <Path d="M 11.642 2 H 12.442 A 8.6 8.55 0 0 1 21.042 10.55 V 18.1 A 1 1 0 0 1 20.042 19.1 H 4.042 A 1 1 0 0 1 3.042 18.1 V 10.55 A 8.6 8.55 0 0 1 11.642 2 Z" /> + <Line x1="9" y1="22" x2="15" y2="22" /> + </Svg> + ) +} + +// https://github.com/Remix-Design/RemixIcon/blob/master/License +export function BellIconSolid({ + style, + size, + strokeWidth = 1.5, + fillOpacity = 1, +}: { + style?: StyleProp<ViewStyle> + size?: string | number + strokeWidth?: number + fillOpacity?: number +}) { + return ( + <Svg + viewBox="0 0 24 24" + width={size || 24} + height={size || 24} + strokeWidth={strokeWidth} + stroke="currentColor" style={style}> - <Path fill="none" d="M0 0h24v24H0z" /> <Path + d="M 11.642 2 H 12.442 A 8.6 8.55 0 0 1 21.042 10.55 V 18.1 A 1 1 0 0 1 20.042 19.1 H 4.042 A 1 1 0 0 1 3.042 18.1 V 10.55 A 8.6 8.55 0 0 1 11.642 2 Z" fill="currentColor" - d="M 20 17 L 22 17 L 22 19 L 2 19 L 2 17 L 4 17 L 4 10 C 4 3.842 10.667 -0.007 16 3.072 C 18.475 4.501 20 7.142 20 10 L 20 17 Z M 9 21 L 15 21 L 15 23 L 9 23 L 9 21 Z" + stroke="none" + opacity={fillOpacity} /> + <Path d="M 11.642 2 H 12.442 A 8.6 8.55 0 0 1 21.042 10.55 V 18.1 A 1 1 0 0 1 20.042 19.1 H 4.042 A 1 1 0 0 1 3.042 18.1 V 10.55 A 8.6 8.55 0 0 1 11.642 2 Z" /> + <Line x1="9" y1="22" x2="15" y2="22" /> </Svg> ) } @@ -527,6 +604,7 @@ export function RectTallIcon({ </Svg> ) } + export function ComposeIcon({ style, size, @@ -553,3 +631,107 @@ export function ComposeIcon({ </Svg> ) } + +export function ComposeIcon2({ + style, + size, + strokeWidth = 1.5, + backgroundColor, +}: { + style?: StyleProp<TextStyle> + size?: string | number + strokeWidth?: number + backgroundColor: string +}) { + return ( + <Svg + viewBox="0 0 24 24" + strokeWidth={strokeWidth} + stroke="currentColor" + width={size || 24} + height={size || 24} + style={style}> + <Rect + strokeWidth={strokeWidth} + x="4" + y="4" + width="16" + height="16" + rx="4" + ry="4" + /> + <Line + x1="10" + y1="14" + x2="22" + y2="2" + strokeWidth={strokeWidth * 4} + stroke={backgroundColor} + /> + <Line + strokeLinecap="round" + x1="10" + y1="14" + x2="18.5" + y2="5.5" + strokeWidth={strokeWidth * 1.5} + /> + <Line + strokeLinecap="round" + x1="20.5" + y1="3.5" + x2="21" + y2="3" + strokeWidth={strokeWidth * 1.5} + /> + </Svg> + ) +} + +export function SquarePlusIcon({ + style, + size, + strokeWidth = 1.5, +}: { + style?: StyleProp<TextStyle> + size?: string | number + strokeWidth?: number +}) { + return ( + <Svg + viewBox="0 0 24 24" + strokeWidth={strokeWidth} + stroke="currentColor" + width={size || 24} + height={size || 24} + style={style}> + <Line + stroke-linecap="round" + stroke-linejoin="round" + x1="12" + y1="5.5" + x2="12" + y2="18.5" + strokeWidth={strokeWidth * 1.5} + /> + <Line + stroke-linecap="round" + stroke-linejoin="round" + x1="5.5" + y1="12" + x2="18.5" + y2="12" + strokeWidth={strokeWidth * 1.5} + /> + <Rect + strokeWidth={strokeWidth} + x="4" + y="4" + width="16" + height="16" + rx="4" + ry="4" + /> + </Svg> + ) +} diff --git a/src/lib/styles.ts b/src/lib/styles.ts index f6e26d53f..d307e9ba8 100644 --- a/src/lib/styles.ts +++ b/src/lib/styles.ts @@ -23,6 +23,7 @@ export const colors = { blue3: '#0085ff', blue4: '#0062bd', blue5: '#034581', + blue6: '#012561', red1: '#ffe6f2', red2: '#fba2ce', diff --git a/src/lib/themes.ts b/src/lib/themes.ts index c544eebf2..aa166e323 100644 --- a/src/lib/themes.ts +++ b/src/lib/themes.ts @@ -82,6 +82,31 @@ export const defaultTheme: Theme = { }, }, typography: { + '2xl-thin': { + fontSize: 18, + letterSpacing: 0.25, + fontWeight: '300', + }, + '2xl': { + fontSize: 18, + letterSpacing: 0.25, + fontWeight: '400', + }, + '2xl-medium': { + fontSize: 18, + letterSpacing: 0.25, + fontWeight: '500', + }, + '2xl-bold': { + fontSize: 18, + letterSpacing: 0.25, + fontWeight: '700', + }, + '2xl-heavy': { + fontSize: 18, + letterSpacing: 0.25, + fontWeight: '800', + }, 'xl-thin': { fontSize: 17, letterSpacing: 0.25, diff --git a/src/view/index.ts b/src/view/index.ts index d1e9d2036..17e9dbbed 100644 --- a/src/view/index.ts +++ b/src/view/index.ts @@ -56,6 +56,7 @@ import {faPlus} from '@fortawesome/free-solid-svg-icons/faPlus' import {faShare} from '@fortawesome/free-solid-svg-icons/faShare' import {faShareFromSquare} from '@fortawesome/free-solid-svg-icons/faShareFromSquare' import {faShield} from '@fortawesome/free-solid-svg-icons/faShield' +import {faSquarePlus} from '@fortawesome/free-regular-svg-icons/faSquarePlus' import {faSignal} from '@fortawesome/free-solid-svg-icons/faSignal' import {faReply} from '@fortawesome/free-solid-svg-icons/faReply' import {faRetweet} from '@fortawesome/free-solid-svg-icons/faRetweet' @@ -131,6 +132,7 @@ export function setup() { faShare, faShareFromSquare, faShield, + faSquarePlus, faSignal, faUser, faUsers, diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx index 52a84c649..657f38d57 100644 --- a/src/view/screens/Debug.tsx +++ b/src/view/screens/Debug.tsx @@ -207,6 +207,21 @@ function TypographyView() { const pal = usePalette('default') return ( <View style={[pal.view]}> + <Text type="2xl-thin" style={[pal.text]}> + '2xl-thin' lorem ipsum dolor + </Text> + <Text type="2xl" style={[pal.text]}> + '2xl' lorem ipsum dolor + </Text> + <Text type="2xl-medium" style={[pal.text]}> + '2xl-medium' lorem ipsum dolor + </Text> + <Text type="2xl-bold" style={[pal.text]}> + '2xl-bold' lorem ipsum dolor + </Text> + <Text type="2xl-heavy" style={[pal.text]}> + '2xl-heavy' lorem ipsum dolor + </Text> <Text type="xl-thin" style={[pal.text]}> 'xl-thin' lorem ipsum dolor </Text> diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 5b5699bcc..b9611757c 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -4,7 +4,6 @@ import {observer} from 'mobx-react-lite' import useAppState from 'react-native-appstate-hook' import {ViewHeader} from '../com/util/ViewHeader' import {Feed} from '../com/posts/Feed' -import {FAB} from '../com/util/FAB' import {LoadLatestBtn} from '../com/util/LoadLatestBtn' import {useStores} from 'state/index' import {ScreenParams} from '../routes' @@ -17,7 +16,7 @@ const HEADER_HEIGHT = 42 export const Home = observer(function Home({navIdx, visible}: ScreenParams) { const store = useStores() const onMainScroll = useOnMainScroll(store) - const {screen, track} = useAnalytics() + const {screen} = useAnalytics() const scrollElRef = React.useRef<FlatList>(null) const [wasVisible, setWasVisible] = React.useState<boolean>(false) const {appState} = useAppState({ @@ -75,10 +74,6 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) { return cleanup }, [visible, store, store.me.mainFeed, navIdx, doPoll, wasVisible, scrollToTop, screen]) - const onPressCompose = (imagesOpen?: boolean) => { - track('Home:ComposeButtonPressed') - store.shell.openComposer({imagesOpen}) - } const onPressTryAgain = () => { store.me.mainFeed.refresh() } @@ -105,11 +100,6 @@ export const Home = observer(function Home({navIdx, visible}: ScreenParams) { {store.me.mainFeed.hasNewLatest && !store.me.mainFeed.isRefreshing && ( <LoadLatestBtn onPress={onPressLoadLatest} /> )} - <FAB - testID="composeFAB" - icon="plus" - onPress={() => onPressCompose(false)} - /> </View> ) }) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 03d973b96..7739814f5 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -13,7 +13,6 @@ import {ErrorScreen} from '../com/util/error/ErrorScreen' import {ErrorMessage} from '../com/util/error/ErrorMessage' import {EmptyState} from '../com/util/EmptyState' import {Text} from '../com/util/text/Text' -import {FAB} from '../com/util/FAB' import {s, colors} from 'lib/styles' import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' import {useAnalytics} from 'lib/analytics' @@ -87,10 +86,6 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { uiState.setup() } - const onPressCompose = () => { - store.shell.openComposer({}) - } - // rendering // = @@ -191,7 +186,6 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => { ) : ( <CenteredView>{renderHeader()}</CenteredView> )} - <FAB icon="plus" onPress={onPressCompose} /> </View> ) }) diff --git a/src/view/shell/mobile/Menu.tsx b/src/view/shell/mobile/Menu.tsx index ceeda8c58..23c09b82c 100644 --- a/src/view/shell/mobile/Menu.tsx +++ b/src/view/shell/mobile/Menu.tsx @@ -17,19 +17,23 @@ import {FEEDBACK_FORM_URL} from 'lib/constants' import {useStores} from 'state/index' import { HomeIcon, + HomeIconSolid, BellIcon, + BellIconSolid, UserIcon, CogIcon, - MagnifyingGlassIcon, + MagnifyingGlassIcon2, + MagnifyingGlassIcon2Solid, } from 'lib/icons' import {TabPurpose, TabPurposeMainPath} from 'state/models/navigation' import {UserAvatar} from '../../com/util/UserAvatar' import {Text} from '../../com/util/text/Text' -import {ToggleButton} from '../../com/util/forms/ToggleButton' +import {useTheme} from 'lib/ThemeContext' import {usePalette} from 'lib/hooks/usePalette' import {useAnalytics} from 'lib/analytics' export const Menu = observer(({onClose}: {onClose: () => void}) => { + const theme = useTheme() const pal = usePalette('default') const store = useStores() const {track} = useAnalytics() @@ -89,11 +93,8 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => { ) : undefined} </View> <Text - type="title" - style={[ - pal.text, - bold ? styles.menuItemLabelBold : styles.menuItemLabel, - ]} + type={bold ? '2xl-bold' : '2xl'} + style={[pal.text, s.flex1]} numberOfLines={1}> {label} </Text> @@ -105,68 +106,114 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => { 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, - pal.view, + theme.colorScheme === 'light' ? pal.view : styles.viewDarkMode, store.shell.minimalShellMode && styles.viewMinimalShell, ]}> <TouchableOpacity testID="profileCardButton" - onPress={() => onNavigate(`/profile/${store.me.handle}`)} - style={styles.profileCard}> + onPress={() => onNavigate(`/profile/${store.me.handle}`)}> <UserAvatar - size={60} + size={80} displayName={store.me.displayName} handle={store.me.handle} avatar={store.me.avatar} /> - <View style={s.flex1}> - <Text - type="title-lg" - style={[pal.text, styles.profileCardDisplayName]} - numberOfLines={1}> - {store.me.displayName || store.me.handle} - </Text> - <Text - style={[pal.textLight, styles.profileCardHandle]} - numberOfLines={1}> - @{store.me.handle} - </Text> - </View> - </TouchableOpacity> - <TouchableOpacity - testID="searchBtn" - style={[styles.searchBtn, pal.btn]} - onPress={() => onNavigate('/search')}> - <MagnifyingGlassIcon - style={pal.text as StyleProp<ViewStyle>} - size={25} - /> - <Text type="title" style={[pal.text, styles.searchBtnLabel]}> - Search + <Text + type="title-lg" + style={[pal.text, s.bold, styles.profileCardDisplayName]} + numberOfLines={1}> + {store.me.displayName || store.me.handle} + </Text> + <Text + type="2xl" + style={[pal.textLight, styles.profileCardHandle]} + numberOfLines={1}> + @{store.me.handle} </Text> </TouchableOpacity> - <View style={[styles.section, pal.border, s.pt5]}> + <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={<HomeIcon style={pal.text as StyleProp<ViewStyle>} size="26" />} + 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={<BellIcon style={pal.text as StyleProp<ViewStyle>} size="28" />} + 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="30" - strokeWidth={2} + size="26" + strokeWidth={1.5} /> } label="Profile" @@ -176,34 +223,46 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => { icon={ <CogIcon style={pal.text as StyleProp<ViewStyle>} - size="30" - strokeWidth={2} + size="26" + strokeWidth={1.75} /> } label="Settings" url="/settings" /> </View> - <View style={[styles.section, pal.border]}> - <ToggleButton - label="Dark mode" - isSelected={store.shell.darkMode} - onPress={onDarkmodePress} - /> - </View> <View style={s.flex1} /> <View style={styles.footer}> - <MenuItem - icon={ - <FontAwesomeIcon - style={pal.text as FontAwesomeIconStyle} - size={24} - icon={['far', 'message']} - /> - } - label="Feedback" + <TouchableOpacity + onPress={onDarkmodePress} + style={[ + styles.footerBtn, + theme.colorScheme === 'light' ? pal.btn : styles.footerBtnDarkMode, + ]}> + <CogIcon + style={pal.text as StyleProp<ViewStyle>} + size="26" + strokeWidth={1.75} + /> + </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> ) @@ -212,70 +271,37 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => { const styles = StyleSheet.create({ view: { flex: 1, + paddingTop: 10, paddingBottom: 90, + paddingLeft: 30, + }, + viewDarkMode: { + backgroundColor: colors.gray8, }, viewMinimalShell: { paddingBottom: 50, }, - section: { - paddingHorizontal: 10, - paddingTop: 10, - paddingBottom: 10, - borderBottomWidth: 1, - }, - heading: { - paddingVertical: 8, - paddingHorizontal: 4, - }, - profileCard: { - flexDirection: 'row', - alignItems: 'center', - margin: 10, - marginBottom: 6, - }, profileCardDisplayName: { - marginLeft: 12, + marginTop: 20, }, profileCardHandle: { - marginLeft: 12, - }, - - searchBtn: { - flexDirection: 'row', - borderRadius: 8, - margin: 10, - marginBottom: 0, - paddingVertical: 10, - paddingHorizontal: 12, - }, - searchBtnLabel: { - marginLeft: 14, - fontWeight: 'normal', + marginTop: 4, }, menuItem: { flexDirection: 'row', alignItems: 'center', - paddingVertical: 6, - paddingLeft: 6, + paddingVertical: 16, paddingRight: 10, }, menuItemIconWrapper: { - width: 36, - height: 36, + width: 24, + height: 24, alignItems: 'center', justifyContent: 'center', marginRight: 12, }, - menuItemLabel: { - flex: 1, - fontWeight: 'normal', - }, - menuItemLabelBold: { - flex: 1, - fontWeight: 'bold', - }, menuItemCount: { position: 'absolute', right: -6, @@ -292,6 +318,27 @@ const styles = StyleSheet.create({ }, footer: { - paddingHorizontal: 10, + flexDirection: 'row', + justifyContent: 'space-between', + paddingRight: 30, + paddingTop: 20, + }, + 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/TabsSelector.tsx b/src/view/shell/mobile/TabsSelector.tsx deleted file mode 100644 index 92854e3b6..000000000 --- a/src/view/shell/mobile/TabsSelector.tsx +++ /dev/null @@ -1,327 +0,0 @@ -import React, {createRef, useRef, useMemo, useState} from 'react' -import {observer} from 'mobx-react-lite' -import { - Animated, - ScrollView, - Share, - StyleSheet, - TouchableWithoutFeedback, - View, -} from 'react-native' -import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import {Text} from '../../com/util/text/Text' -import Swipeable from 'react-native-gesture-handler/Swipeable' -import {useStores} from 'state/index' -import {s, colors} from 'lib/styles' -import {toShareUrl} from 'lib/strings/url-helpers' -import {match} from '../../routes' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' - -const TAB_HEIGHT = 42 - -export const TabsSelector = observer( - ({ - active, - tabMenuInterp, - onClose, - }: { - active: boolean - tabMenuInterp: Animated.Value - onClose: () => void - }) => { - const store = useStores() - const insets = useSafeAreaInsets() - const [closingTabIndex, setClosingTabIndex] = useState<number | undefined>( - undefined, - ) - const closeInterp = useAnimatedValue(0) - const tabsContainerRef = useRef<View>(null) - const tabsRef = useRef<ScrollView>(null) - const tabRefs = useMemo( - () => - Array.from({length: store.nav.tabs.length}).map(() => - createRef<View>(), - ), - [store.nav.tabs.length], - ) - - const wrapperAnimStyle = { - transform: [ - { - translateY: tabMenuInterp.interpolate({ - inputRange: [0, 1.0], - outputRange: [320, 0], - }), - }, - ], - } - - // events - // = - - const onPressNewTab = () => { - store.nav.newTab('/') - onClose() - } - const onPressCloneTab = () => { - store.nav.newTab(store.nav.tab.current.url) - onClose() - } - const onPressShareTab = () => { - onClose() - Share.share({url: toShareUrl(store.nav.tab.current.url)}) - } - const onPressChangeTab = (tabIndex: number) => { - store.nav.setActiveTab(tabIndex) - onClose() - } - const onCloseTab = (tabIndex: number) => { - setClosingTabIndex(tabIndex) - closeInterp.setValue(0) - Animated.timing(closeInterp, { - toValue: 1, - duration: 300, - useNativeDriver: false, - }).start(() => { - setClosingTabIndex(undefined) - store.nav.closeTab(tabIndex) - }) - } - const onLayout = () => { - // focus the current tab - const targetTab = tabRefs[store.nav.tabIndex] - if (tabsContainerRef.current && tabsRef.current && targetTab.current) { - targetTab.current.measureLayout?.( - tabsContainerRef.current, - (_left: number, top: number) => { - tabsRef.current?.scrollTo({y: top, animated: false}) - }, - () => {}, - ) - } - } - - // rendering - // = - - const renderSwipeActions = () => { - return <View style={[s.p2]} /> - } - - const currentTabIndex = store.nav.tabIndex - const closingTabAnimStyle = { - height: Animated.multiply(TAB_HEIGHT, Animated.subtract(1, closeInterp)), - opacity: Animated.subtract(1, closeInterp), - marginBottom: Animated.multiply(4, Animated.subtract(1, closeInterp)), - } - - if (!active) { - return <View testID="emptyView" /> - } - - return ( - <Animated.View - testID="tabsSelectorView" - style={[ - styles.wrapper, - {bottom: insets.bottom + 55}, - wrapperAnimStyle, - ]}> - <View onLayout={onLayout}> - <View style={[s.p10, styles.section]}> - <View style={styles.btns}> - <TouchableWithoutFeedback - testID="shareButton" - onPress={onPressShareTab}> - <View style={[styles.btn]}> - <View style={styles.btnIcon}> - <FontAwesomeIcon size={16} icon="share" /> - </View> - <Text style={styles.btnText}>Share</Text> - </View> - </TouchableWithoutFeedback> - <TouchableWithoutFeedback - testID="cloneButton" - onPress={onPressCloneTab}> - <View style={[styles.btn]}> - <View style={styles.btnIcon}> - <FontAwesomeIcon size={16} icon={['far', 'clone']} /> - </View> - <Text style={styles.btnText}>Clone tab</Text> - </View> - </TouchableWithoutFeedback> - <TouchableWithoutFeedback - testID="newTabButton" - onPress={onPressNewTab}> - <View style={[styles.btn]}> - <View style={styles.btnIcon}> - <FontAwesomeIcon size={16} icon="plus" /> - </View> - <Text style={styles.btnText}>New tab</Text> - </View> - </TouchableWithoutFeedback> - </View> - </View> - <View - ref={tabsContainerRef} - style={[s.p10, styles.section, styles.sectionGrayBg]}> - <ScrollView ref={tabsRef} style={styles.tabs}> - {store.nav.tabs.map((tab, tabIndex) => { - const {icon} = match(tab.current.url) - const isActive = tabIndex === currentTabIndex - const isClosing = closingTabIndex === tabIndex - return ( - <Swipeable - key={tab.id} - testID="tabsSwipable" - renderLeftActions={renderSwipeActions} - renderRightActions={renderSwipeActions} - leftThreshold={100} - rightThreshold={100} - onSwipeableWillOpen={() => onCloseTab(tabIndex)}> - <Animated.View - style={[ - styles.tabOuter, - isClosing ? closingTabAnimStyle : undefined, - ]}> - <Animated.View - // HOTFIX - // TabsSelector.test.tsx snapshot fails if the - // ref was set like this: ref={tabRefs[tabIndex]} - ref={(ref: any) => (tabRefs[tabIndex] = ref)} - style={[ - styles.tab, - styles.existing, - isActive && styles.active, - ]}> - <TouchableWithoutFeedback - testID="changeTabButton" - onPress={() => onPressChangeTab(tabIndex)}> - <View style={styles.tabInner}> - <View style={styles.tabIcon}> - <FontAwesomeIcon size={20} icon={icon} /> - </View> - <Text - ellipsizeMode="tail" - numberOfLines={1} - suppressHighlighting={true} - style={[ - styles.tabText, - isActive && styles.tabTextActive, - ]}> - {tab.current.title || tab.current.url} - </Text> - </View> - </TouchableWithoutFeedback> - <TouchableWithoutFeedback - testID="closeTabButton" - onPress={() => onCloseTab(tabIndex)}> - <View style={styles.tabClose}> - <FontAwesomeIcon - size={14} - icon="x" - style={styles.tabCloseIcon} - /> - </View> - </TouchableWithoutFeedback> - </Animated.View> - </Animated.View> - </Swipeable> - ) - })} - </ScrollView> - </View> - </View> - </Animated.View> - ) - }, -) - -const styles = StyleSheet.create({ - wrapper: { - position: 'absolute', - width: '100%', - height: 320, - borderTopColor: colors.gray2, - borderTopWidth: 1, - backgroundColor: '#fff', - opacity: 1, - }, - section: { - borderBottomColor: colors.gray2, - borderBottomWidth: 1, - }, - sectionGrayBg: { - backgroundColor: colors.gray1, - }, - tabs: { - height: 240, - }, - tabOuter: { - height: TAB_HEIGHT + 4, - overflow: 'hidden', - }, - tab: { - flexDirection: 'row', - height: TAB_HEIGHT, - backgroundColor: colors.gray1, - alignItems: 'center', - borderRadius: 4, - }, - tabInner: { - flexDirection: 'row', - flex: 1, - alignItems: 'center', - paddingLeft: 12, - paddingVertical: 12, - }, - existing: { - borderColor: colors.gray4, - borderWidth: 1, - }, - active: { - backgroundColor: colors.white, - borderColor: colors.black, - borderWidth: 1, - }, - tabIcon: {}, - tabText: { - flex: 1, - paddingHorizontal: 10, - fontSize: 16, - }, - tabTextActive: { - fontWeight: '500', - }, - tabClose: { - paddingVertical: 16, - paddingRight: 16, - }, - tabCloseIcon: { - color: '#655', - }, - btns: { - flexDirection: 'row', - paddingTop: 2, - }, - btn: { - flexDirection: 'row', - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: colors.gray1, - borderRadius: 4, - marginRight: 5, - paddingLeft: 12, - paddingRight: 16, - paddingVertical: 10, - }, - btnIcon: { - marginRight: 8, - }, - btnText: { - fontWeight: '500', - fontSize: 16, - }, -}) diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx index 89a834ee1..6ab19d651 100644 --- a/src/view/shell/mobile/index.tsx +++ b/src/view/shell/mobile/index.tsx @@ -2,21 +2,17 @@ import React, {useState, useEffect} from 'react' import {observer} from 'mobx-react-lite' import { Animated, - Easing, GestureResponderEvent, StatusBar, StyleSheet, TouchableOpacity, TouchableWithoutFeedback, - useColorScheme, useWindowDimensions, View, } from 'react-native' import {ScreenContainer, Screen} from 'react-native-screens' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {IconProp} from '@fortawesome/fontawesome-svg-core' -import {TABS_ENABLED} from 'lib/build-flags' import {useStores} from 'state/index' import { NavigationModel, @@ -31,18 +27,18 @@ 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 {TabsSelector} from './TabsSelector' import {Composer} from './Composer' import {s, colors} from 'lib/styles' import {clamp} from 'lib/numbers' import { - GridIcon, - GridIconSolid, HomeIcon, HomeIconSolid, - MagnifyingGlassIcon, + MagnifyingGlassIcon2, + MagnifyingGlassIcon2Solid, + ComposeIcon2, BellIcon, BellIconSolid, + UserIcon, } from 'lib/icons' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useTheme} from 'lib/ThemeContext' @@ -52,74 +48,14 @@ import {useAnalytics} from 'lib/analytics' const Btn = ({ icon, notificationCount, - tabCount, onPress, onLongPress, }: { - icon: - | IconProp - | 'menu' - | 'menu-solid' - | 'home' - | 'home-solid' - | 'search' - | 'search-solid' - | 'bell' - | 'bell-solid' + icon: JSX.Element notificationCount?: number - tabCount?: number onPress?: (event: GestureResponderEvent) => void onLongPress?: (event: GestureResponderEvent) => void }) => { - const pal = usePalette('default') - let iconEl - if (icon === 'menu') { - iconEl = <GridIcon style={[styles.ctrlIcon, pal.text]} /> - } else if (icon === 'menu-solid') { - iconEl = <GridIconSolid style={[styles.ctrlIcon, pal.text]} /> - } else if (icon === 'home') { - iconEl = <HomeIcon size={27} style={[styles.ctrlIcon, pal.text]} /> - } else if (icon === 'home-solid') { - iconEl = <HomeIconSolid size={27} style={[styles.ctrlIcon, pal.text]} /> - } else if (icon === 'search') { - iconEl = ( - <MagnifyingGlassIcon - size={28} - style={[styles.ctrlIcon, pal.text, styles.bumpUpOnePixel]} - /> - ) - } else if (icon === 'search-solid') { - iconEl = ( - <MagnifyingGlassIcon - size={28} - strokeWidth={3} - style={[styles.ctrlIcon, pal.text, styles.bumpUpOnePixel]} - /> - ) - } else if (icon === 'bell') { - iconEl = ( - <BellIcon - size={27} - style={[styles.ctrlIcon, pal.text, styles.bumpUpOnePixel]} - /> - ) - } else if (icon === 'bell-solid') { - iconEl = ( - <BellIconSolid - size={27} - style={[styles.ctrlIcon, pal.text, styles.bumpUpOnePixel]} - /> - ) - } else { - iconEl = ( - <FontAwesomeIcon - icon={icon} - size={24} - style={[styles.ctrlIcon, pal.text]} - /> - ) - } - return ( <TouchableOpacity style={styles.ctrl} @@ -131,12 +67,7 @@ const Btn = ({ <Text style={styles.notificationCountLabel}>{notificationCount}</Text> </View> ) : undefined} - {tabCount && tabCount > 1 ? ( - <View style={styles.tabCount}> - <Text style={styles.tabCountLabel}>{tabCount}</Text> - </View> - ) : undefined} - {iconEl} + {icon} </TouchableOpacity> ) } @@ -145,15 +76,10 @@ export const MobileShell: React.FC = observer(() => { const theme = useTheme() const pal = usePalette('default') const store = useStores() - const [isTabsSelectorActive, setTabsSelectorActive] = useState(false) const winDim = useWindowDimensions() const [menuSwipingDirection, setMenuSwipingDirection] = useState(0) const swipeGestureInterp = useAnimatedValue(0) const minimalShellInterp = useAnimatedValue(0) - const tabMenuInterp = useAnimatedValue(0) - const newTabInterp = useAnimatedValue(0) - const [isRunningNewTabAnim, setIsRunningNewTabAnim] = useState(false) - const colorScheme = useColorScheme() const safeAreaInsets = useSafeAreaInsets() const screenRenderDesc = constructScreenRenderDesc(store.nav) const {track} = useAnalytics() @@ -188,6 +114,10 @@ export const MobileShell: React.FC = observer(() => { } } } + const onPressCompose = () => { + track('MobileShell:ComposeButtonPressed') + store.shell.openComposer({}) + } const onPressNotifications = () => { track('MobileShell:NotificationsButtonPressed') if (store.nav.tab.fixedTabPurpose === TabPurpose.Notifs) { @@ -203,8 +133,10 @@ export const MobileShell: React.FC = observer(() => { } } } - const onPressTabs = () => toggleTabsMenu(!isTabsSelectorActive) - const doNewTab = (url: string) => () => store.nav.newTab(url) + const onPressProfile = () => { + track('MobileShell:ProfileButtonPressed') + store.nav.navigate(`/profile/${store.me.handle}`) + } // minimal shell animation // = @@ -229,60 +161,6 @@ export const MobileShell: React.FC = observer(() => { transform: [{translateY: Animated.multiply(minimalShellInterp, 100)}], } - // tab selector animation - // = - const toggleTabsMenu = (active: boolean) => { - if (active) { - // will trigger the animation below - setTabsSelectorActive(true) - } else { - Animated.timing(tabMenuInterp, { - toValue: 0, - duration: 100, - useNativeDriver: false, - }).start(() => { - // hide once the animation has finished - setTabsSelectorActive(false) - }) - } - } - useEffect(() => { - if (isTabsSelectorActive) { - // trigger the animation once the tabs selector is rendering - Animated.timing(tabMenuInterp, { - toValue: 1, - duration: 100, - useNativeDriver: false, - }).start() - } - }, [tabMenuInterp, isTabsSelectorActive]) - - // new tab animation - // = - useEffect(() => { - if (screenRenderDesc.hasNewTab && !isRunningNewTabAnim) { - setIsRunningNewTabAnim(true) - } - }, [isRunningNewTabAnim, screenRenderDesc.hasNewTab]) - useEffect(() => { - if (isRunningNewTabAnim) { - const reset = () => { - store.nav.tab.setIsNewTab(false) - setIsRunningNewTabAnim(false) - } - Animated.timing(newTabInterp, { - toValue: 1, - duration: 250, - easing: Easing.out(Easing.exp), - useNativeDriver: false, - }).start(() => { - reset() - }) - } else { - newTabInterp.setValue(0) - } - }, [newTabInterp, store.nav.tab, isRunningNewTabAnim]) - // navigation swipes // = const isMenuActive = store.shell.isMainMenuOpen @@ -495,20 +373,6 @@ export const MobileShell: React.FC = observer(() => { )} </HorzSwipe> </View> - {isTabsSelectorActive ? ( - <View - style={[ - styles.topBarProtector, - colorScheme === 'dark' ? styles.topBarProtectorDark : undefined, - {height: safeAreaInsets.top}, - ]} - /> - ) : undefined} - <TabsSelector - active={isTabsSelectorActive} - tabMenuInterp={tabMenuInterp} - onClose={() => toggleTabsMenu(false)} - /> <Animated.View style={[ styles.bottomBar, @@ -518,28 +382,85 @@ export const MobileShell: React.FC = observer(() => { footerMinimalShellTransform, ]}> <Btn - icon={isAtHome ? 'home-solid' : 'home'} + icon={ + isAtHome ? ( + <HomeIconSolid + strokeWidth={4} + size={24} + style={[styles.ctrlIcon, pal.text, styles.homeIcon]} + /> + ) : ( + <HomeIcon + strokeWidth={4} + size={24} + style={[styles.ctrlIcon, pal.text, styles.homeIcon]} + /> + ) + } onPress={onPressHome} - onLongPress={TABS_ENABLED ? doNewTab('/') : undefined} /> <Btn - icon={isAtSearch ? 'search-solid' : 'search'} + icon={ + isAtSearch ? ( + <MagnifyingGlassIcon2Solid + size={25} + style={[styles.ctrlIcon, pal.text, styles.searchIcon]} + strokeWidth={1.8} + /> + ) : ( + <MagnifyingGlassIcon2 + size={25} + style={[styles.ctrlIcon, pal.text, styles.searchIcon]} + strokeWidth={1.8} + /> + ) + } onPress={onPressSearch} - onLongPress={TABS_ENABLED ? doNewTab('/') : undefined} /> - {TABS_ENABLED ? ( - <Btn - icon={isTabsSelectorActive ? 'clone' : ['far', 'clone']} - onPress={onPressTabs} - tabCount={store.nav.tabCount} - /> - ) : undefined} <Btn - icon={isAtNotifications ? 'bell-solid' : 'bell'} + icon={ + <View style={styles.ctrlIconSizingWrapper}> + <ComposeIcon2 + strokeWidth={1.5} + size={29} + style={[styles.ctrlIcon, pal.text, styles.composeIcon]} + backgroundColor={pal.colors.background} + /> + </View> + } + onPress={onPressCompose} + /> + <Btn + icon={ + isAtNotifications ? ( + <BellIconSolid + size={24} + strokeWidth={1.9} + style={[styles.ctrlIcon, pal.text, styles.bellIcon]} + /> + ) : ( + <BellIcon + size={24} + strokeWidth={1.9} + style={[styles.ctrlIcon, pal.text, styles.bellIcon]} + /> + ) + } onPress={onPressNotifications} - onLongPress={TABS_ENABLED ? doNewTab('/notifications') : undefined} notificationCount={store.me.notifications.unreadCount} /> + <Btn + icon={ + <View style={styles.ctrlIconSizingWrapper}> + <UserIcon + size={28} + strokeWidth={1.5} + style={[styles.ctrlIcon, pal.text, styles.profileIcon]} + /> + </View> + } + onPress={onPressProfile} + /> </Animated.View> <ModalsContainer /> <Lightbox /> @@ -650,46 +571,51 @@ const styles = StyleSheet.create({ flexDirection: 'row', borderTopWidth: 1, paddingLeft: 5, - paddingRight: 25, + paddingRight: 10, }, ctrl: { flex: 1, - paddingTop: 12, - paddingBottom: 5, + paddingTop: 13, + paddingBottom: 4, }, notificationCount: { position: 'absolute', - left: '60%', + left: '56%', top: 10, - backgroundColor: colors.red3, + backgroundColor: colors.blue3, paddingHorizontal: 4, paddingBottom: 1, borderRadius: 8, + zIndex: 1, }, notificationCountLabel: { fontSize: 12, fontWeight: 'bold', color: colors.white, }, - tabCount: { - position: 'absolute', - left: 46, - top: 30, - }, - tabCountLabel: { - fontSize: 12, - fontWeight: 'bold', - color: colors.black, - }, ctrlIcon: { marginLeft: 'auto', marginRight: 'auto', }, + ctrlIconSizingWrapper: { + height: 27, + }, inactive: { color: colors.gray3, }, - bumpUpOnePixel: { - position: 'relative', - top: -1, + homeIcon: { + top: 0, + }, + searchIcon: { + top: -2, + }, + bellIcon: { + top: -2.5, + }, + composeIcon: { + top: -4.5, + }, + profileIcon: { + top: -4, }, }) |