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 tuningzio/stable
parent
159615990d
commit
eeac64cc88
|
@ -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'
|
||||
|
|
|
@ -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,13 +130,74 @@ export function MagnifyingGlassIcon({
|
|||
)
|
||||
}
|
||||
|
||||
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}>
|
||||
<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"
|
||||
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 BellIcon({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
|
@ -135,12 +205,11 @@ export function BellIcon({
|
|||
viewBox="0 0 24 24"
|
||||
width={size || 24}
|
||||
height={size || 24}
|
||||
strokeWidth={strokeWidth}
|
||||
stroke="currentColor"
|
||||
style={style}>
|
||||
<Path fill="none" d="M0 0h24v24H0z" />
|
||||
<Path
|
||||
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"
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
@ -149,22 +218,30 @@ export function BellIcon({
|
|||
export function BellIconSolid({
|
||||
style,
|
||||
size,
|
||||
strokeWidth = 1.5,
|
||||
fillOpacity = 1,
|
||||
}: {
|
||||
style?: StyleProp<ViewStyle>
|
||||
size?: string | number
|
||||
strokeWidth?: number
|
||||
fillOpacity?: number
|
||||
}) {
|
||||
return (
|
||||
<Svg
|
||||
fill="none"
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ export const colors = {
|
|||
blue3: '#0085ff',
|
||||
blue4: '#0062bd',
|
||||
blue5: '#034581',
|
||||
blue6: '#012561',
|
||||
|
||||
red1: '#ffe6f2',
|
||||
red2: '#fba2ce',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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={<HomeIcon style={pal.text as StyleProp<ViewStyle>} size="26" />}
|
||||
label="Home"
|
||||
url="/"
|
||||
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={<BellIcon style={pal.text as StyleProp<ViewStyle>} size="28" />}
|
||||
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="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,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue