diff --git a/native/chat/chat-tab-bar-button.react.js b/native/chat/chat-tab-bar-button.react.js new file mode 100644 --- /dev/null +++ b/native/chat/chat-tab-bar-button.react.js @@ -0,0 +1,145 @@ +// @flow + +import invariant from 'invariant'; +import * as React from 'react'; +import { TouchableOpacity, View, Text } from 'react-native'; + +import { threadSettingsNotificationsCopy } from 'lib/shared/thread-settings-notifications-utils.js'; +import { useSelector } from 'lib/utils/redux-utils.js'; + +import { + nuxTip, + NUXTipsContext, +} from '../components/nux-tips-context.react.js'; +import { useStyles } from '../themes/colors.js'; +import { LightTheme, DarkTheme } from '../themes/navigation.js'; + +type Props = { + +title: string, + +tabBarIcon?: + | string + | (({ +color: string, +focused: boolean }) => React$Node), + +onPress?: () => void, + +onLongPress?: () => void, + +isFocused?: boolean, + +tabBarAccessibilityLabel?: string, + +tabBarTestID?: string, +}; + +const ButtonTitleToTip = Object.freeze({ + [threadSettingsNotificationsCopy.MUTED]: nuxTip.MUTED, + [threadSettingsNotificationsCopy.HOME]: nuxTip.HOME, +}); + +const unboundStyles = { + button: { + flex: 1, + backgroundColor: 'tabBarBackground', + }, + innerContainer: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + }, + tabBarIndicatorStyle: { + borderColor: 'tabBarAccent', + borderBottomWidth: 2, + }, +}; + +export default function TabBarButton(props: Props): React.Node { + const { + title, + tabBarIcon, + onPress, + onLongPress, + isFocused, + tabBarAccessibilityLabel, + tabBarTestID, + } = props; + + const styles = useStyles(unboundStyles); + + const viewRef = React.useRef>(); + + const tipsContext = React.useContext(NUXTipsContext); + invariant(tipsContext, 'NUXTipsContext should be defined'); + + const onLayout = () => { + const button = viewRef.current; + if (!button) { + return; + } + const tipType = ButtonTitleToTip[title]; + if (!tipType) { + return; + } + button.measure((x, y, width, height, pageX, pageY) => { + tipsContext.registerTipButton(tipType, { + x, + y, + width, + height, + pageX, + pageY, + }); + }); + }; + + const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); + + const textColor = React.useMemo(() => { + const color = + activeTheme === 'dark' ? DarkTheme.colors.text : LightTheme.colors.text; + + if (!isFocused) { + return 'gray'; + } + return color; + }, [activeTheme, isFocused]); + + const icon = React.useMemo( + () => + typeof tabBarIcon === 'string' || !tabBarIcon + ? tabBarIcon + : tabBarIcon({ color: textColor, focused: isFocused ?? false }), + [isFocused, tabBarIcon, textColor], + ); + + const buttonStyle = React.useMemo(() => { + if (isFocused) { + return [styles.button, styles.tabBarIndicatorStyle]; + } + return styles.button; + }, [isFocused, styles.button, styles.tabBarIndicatorStyle]); + + const textStyle = React.useMemo( + () => ({ + color: textColor, + textTransform: 'uppercase', + fontSize: 14, + margin: 4, + textAlign: 'center', + backgroundColor: 'transparent', + }), + [textColor], + ); + + return ( + + + {icon} + {title} + + + ); +} diff --git a/native/chat/chat-tab-bar.react.js b/native/chat/chat-tab-bar.react.js new file mode 100644 --- /dev/null +++ b/native/chat/chat-tab-bar.react.js @@ -0,0 +1,73 @@ +// @flow + +import type { MaterialTopTabBarProps } from '@react-navigation/core'; +import invariant from 'invariant'; +import * as React from 'react'; +import { View } from 'react-native'; + +import ChatTabBarButton from './chat-tab-bar-button.react.js'; +import { NUXTipsContext } from '../components/nux-tips-context.react.js'; +import { useStyles } from '../themes/colors.js'; + +const unboundStyles = { + container: { + flexDirection: 'row', + backgroundColor: 'white', + height: 48, + }, +}; + +export default function TabBar(props: MaterialTopTabBarProps): React.Node { + const styles = useStyles(unboundStyles); + const { state, descriptors, navigation } = props; + + const tipsContext = React.useContext(NUXTipsContext); + invariant(tipsContext, 'NUXTipsContext should be defined'); + + const buttons = React.useMemo( + () => + state.routes.map((route, index) => { + const { options } = descriptors[route.key]; + const { title, tabBarIcon } = options; + invariant( + title && tabBarIcon, + 'title and tabBarIcon should be defined', + ); + + const isFocused = state.index === index; + + const onPress = () => { + // $FlowFixMe + const event = navigation.emit({ + type: 'tabPress', + target: route.key, + }); + + if (!isFocused && !event.defaultPrevented) { + navigation.navigate(route.name); + } + }; + + const onLongPress = () => { + // $FlowFixMe + navigation.emit({ + type: 'tabLongPress', + target: route.key, + }); + }; + + return ( + + ); + }), + [descriptors, navigation, state.index, state.routes], + ); + return {buttons}; +} diff --git a/native/chat/chat.react.js b/native/chat/chat.react.js --- a/native/chat/chat.react.js +++ b/native/chat/chat.react.js @@ -38,6 +38,7 @@ type ChatRouterNavigationHelpers, type ChatRouterNavigationAction, } from './chat-router.js'; +import TabBar from './chat-tab-bar.react.js'; import ComposeSubchannel from './compose-subchannel.react.js'; import ComposeThreadButton from './compose-thread-button.react.js'; import FullScreenThreadMediaGallery from './fullscreen-thread-media-gallery.react.js'; @@ -119,15 +120,21 @@ export type ChatTopTabsNavigationHelpers = MaterialTopTabNavigationHelpers; -const homeChatThreadListOptions = { +const homeChatThreadListOptions: { + title: string, + tabBarIcon: ({ +color: string, ... }) => React$Node, +} = { title: threadSettingsNotificationsCopy.HOME, - tabBarIcon: ({ color }: { +color: string, ... }) => ( + tabBarIcon: ({ color }) => ( ), }; -const backgroundChatThreadListOptions = { +const backgroundChatThreadListOptions: { + title: string, + tabBarIcon: ({ +color: string, ... }) => React$Node, +} = { title: threadSettingsNotificationsCopy.MUTED, - tabBarIcon: ({ color }: { +color: string, ... }) => ( + tabBarIcon: ({ color }) => ( ), }; @@ -156,8 +163,9 @@ }), [tabBarAccent, tabBarBackground], ); + return ( - + ); } + +export { backgroundChatThreadListOptions, homeChatThreadListOptions }; diff --git a/native/components/nux-tips-context.react.js b/native/components/nux-tips-context.react.js --- a/native/components/nux-tips-context.react.js +++ b/native/components/nux-tips-context.react.js @@ -9,6 +9,7 @@ const nuxTip = Object.freeze({ COMMUNITY_DRAWER: 'community_drawer', MUTED: 'muted', + HOME: 'home', }); export type NUXTip = $Values;