diff --git a/native/navigation/community-drawer-content.react.js b/native/navigation/community-drawer-content.react.js index 8feef8d59..4219eea7e 100644 --- a/native/navigation/community-drawer-content.react.js +++ b/native/navigation/community-drawer-content.react.js @@ -1,128 +1,130 @@ // @flow import * as React from 'react'; -import { View, FlatList } from 'react-native'; +import { FlatList } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; import { useSelector } from 'react-redux'; import { childThreadInfos, communityThreadSelector, } from 'lib/selectors/thread-selectors'; import { type ThreadInfo, communitySubthreads } from 'lib/types/thread-types'; import { useNavigateToThread } from '../chat/message-list-types'; import { useStyles } from '../themes/colors'; import CommunityDrawerItemCommunity from './community-drawer-item-cummunity.react'; const maxDepth = 2; +const safeAreaEdges = ['top']; function CommunityDrawerContent(): React.Node { const communities = useSelector(communityThreadSelector); const communitiesSuffixed = React.useMemo(() => appendSuffix(communities), [ communities, ]); const styles = useStyles(unboundStyles); const [openCommunity, setOpenCommunity] = React.useState( communitiesSuffixed.length === 1 ? communitiesSuffixed[0].id : null, ); const navigateToThread = useNavigateToThread(); const childThreadInfosMap = useSelector(childThreadInfos); const setOpenCommunnityOrClose = React.useCallback((index: string) => { setOpenCommunity(open => (open === index ? null : index)); }, []); const renderItem = React.useCallback( ({ item }) => { const itemData = { threadInfo: item.threadInfo, itemChildren: item.itemChildren, }; return ( ); }, [navigateToThread, openCommunity, setOpenCommunnityOrClose], ); const drawerItemsData = React.useMemo( () => createRecursiveDrawerItemsData(childThreadInfosMap, communitiesSuffixed), [childThreadInfosMap, communitiesSuffixed], ); return ( - + - + ); } function createRecursiveDrawerItemsData( childThreadInfosMap: { +[id: string]: $ReadOnlyArray }, communities: $ReadOnlyArray, ) { const result = communities.map(community => ({ key: community.id, threadInfo: community, itemChildren: [], })); let queue = result.map(item => [item, 0]); for (let i = 0; i < queue.length; i++) { const [item, lvl] = queue[i]; const itemChildThreadInfos = childThreadInfosMap[item.threadInfo.id] ?? []; if (lvl < maxDepth) { item.itemChildren = itemChildThreadInfos .filter(childItem => communitySubthreads.includes(childItem.type)) .map(childItem => ({ threadInfo: childItem, itemChildren: [], })); queue = queue.concat( item.itemChildren.map(childItem => [childItem, lvl + 1]), ); } } return result; } function appendSuffix(chats: $ReadOnlyArray): ThreadInfo[] { const result = []; const names = new Map(); for (const chat of chats) { let name = chat.uiName; const numberOfOccurrences = names.get(name); names.set(name, (numberOfOccurrences ?? 0) + 1); if (numberOfOccurrences) { name = `${name} (${numberOfOccurrences.toString()})`; } result.push({ ...chat, uiName: name }); } return result; } const unboundStyles = { drawerContent: { flex: 1, paddingRight: 8, - paddingTop: 52, + paddingTop: 8, backgroundColor: 'drawerBackgroud', }, }; const MemoizedCommunityDrawerContent: React.ComponentType<{}> = React.memo( CommunityDrawerContent, ); export default MemoizedCommunityDrawerContent; diff --git a/native/navigation/community-drawer-item.react.js b/native/navigation/community-drawer-item.react.js index f0af969d5..63ea5023c 100644 --- a/native/navigation/community-drawer-item.react.js +++ b/native/navigation/community-drawer-item.react.js @@ -1,129 +1,137 @@ // @flow import * as React from 'react'; -import { View, Text, FlatList, TouchableOpacity } from 'react-native'; +import { View, FlatList, TouchableOpacity } from 'react-native'; import type { ThreadInfo } from 'lib/types/thread-types'; import type { MessageListParams } from '../chat/message-list-types'; +import { SingleLine } from '../components/single-line.react'; import { useStyles } from '../themes/colors'; import { ExpandButton, ExpandButtonDisabled } from './expand-buttons.react'; export type CommunityDrawerItemData = { +threadInfo: ThreadInfo, +itemChildren?: $ReadOnlyArray, }; export type DrawerItemProps = { +itemData: CommunityDrawerItemData, +toggleExpanded: (threadID: string) => void, +expanded: boolean, +navigateToThread: (params: MessageListParams) => void, }; function CommunityDrawerItem(props: DrawerItemProps): React.Node { const { itemData: { threadInfo, itemChildren }, navigateToThread, expanded, toggleExpanded, } = props; const styles = useStyles(unboundStyles); const renderItem = React.useCallback( ({ item }) => ( ), [navigateToThread], ); const children = React.useMemo(() => { if (!expanded) { return null; } return ; }, [expanded, itemChildren, renderItem]); const onExpandToggled = React.useCallback(() => { toggleExpanded(threadInfo.id); }, [toggleExpanded, threadInfo.id]); const itemExpandButton = React.useMemo(() => { if (!itemChildren?.length) { return ; } return ; }, [itemChildren?.length, expanded, onExpandToggled]); const onPress = React.useCallback(() => { navigateToThread({ threadInfo }); }, [navigateToThread, threadInfo]); return ( {itemExpandButton} - - {threadInfo.uiName} + + {threadInfo.uiName} {children} ); } const unboundStyles = { chatView: { marginLeft: 16, marginVertical: 6, - overflow: 'hidden', }, threadEntry: { flexDirection: 'row', }, title: { color: 'drawerItemLabel', fontSize: 16, lineHeight: 24, + fontWeight: '500', + }, + textTouchableWrapper: { + flex: 1, }, }; export type CommunityDrawerItemChatProps = { +itemData: CommunityDrawerItemData, +navigateToThread: (params: MessageListParams) => void, }; function CommunityDrawerItemChat( props: CommunityDrawerItemChatProps, ): React.Node { const [expanded, setExpanded] = React.useState(false); const styles = useStyles(unboundStyles); const toggleExpanded = React.useCallback(() => { setExpanded(isExpanded => !isExpanded); }, []); return ( ); } const MemoizedCommunityDrawerItemChat: React.ComponentType = React.memo( CommunityDrawerItemChat, ); const MemoizedCommunityDrawerItem: React.ComponentType = React.memo( CommunityDrawerItem, ); export default MemoizedCommunityDrawerItem; diff --git a/native/navigation/community-drawer-navigator.react.js b/native/navigation/community-drawer-navigator.react.js index f18b15fca..5cf1a21b6 100644 --- a/native/navigation/community-drawer-navigator.react.js +++ b/native/navigation/community-drawer-navigator.react.js @@ -1,72 +1,72 @@ // @flow import { createDrawerNavigator, type DrawerNavigationHelpers, type DrawerNavigationProp, } from '@react-navigation/drawer'; import * as React from 'react'; -import { Dimensions, View } from 'react-native'; +import { View } from 'react-native'; import { useStyles } from '../themes/colors'; import type { AppNavigationProp } from './app-navigator.react'; import CommunityDrawerContent from './community-drawer-content.react'; import { TabNavigatorRouteName } from './route-names'; import type { NavigationRoute, ScreenParamList, CommunityDrawerParamList, } from './route-names'; import TabNavigator from './tab-navigator.react'; const communityDrawerContent = () => ; export type CommunityDrawerNavigationProp< RouteName: $Keys = $Keys, > = DrawerNavigationProp; const Drawer = createDrawerNavigator< ScreenParamList, CommunityDrawerParamList, DrawerNavigationHelpers, >(); type Props = { +navigation: AppNavigationProp<'CommunityDrawerNavigator'>, +route: NavigationRoute<'CommunityDrawerNavigator'>, }; // eslint-disable-next-line no-unused-vars function CommunityDrawerNavigator(props: Props): React.Node { const styles = useStyles(unboundStyles); const screenOptions = React.useMemo( () => ({ drawerStyle: styles.drawerStyle, headerShown: false, }), [styles.drawerStyle], ); return ( ); } const unboundStyles = { drawerView: { flex: 1, }, drawerStyle: { - width: Dimensions.get('window').width - 36, + width: '80%', }, }; export { CommunityDrawerNavigator }; diff --git a/native/navigation/expand-buttons.react.js b/native/navigation/expand-buttons.react.js index ee32f7277..4c1460c06 100644 --- a/native/navigation/expand-buttons.react.js +++ b/native/navigation/expand-buttons.react.js @@ -1,56 +1,74 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome'; import * as React from 'react'; -import { TouchableOpacity } from 'react-native'; +import { TouchableOpacity, View } from 'react-native'; import { useStyles } from '../themes/colors'; -const ICON_SIZE = 20; -const PADDING_HORIZONTAL = 7.5; +const iconSize = 12; +const buttonSize = 24; +const hitSlopValue = 6; +const padding = (buttonSize - iconSize) / 2; + +const hitSlop = { + bottom: hitSlopValue, + left: hitSlopValue, + right: hitSlopValue, + top: hitSlopValue, +}; type Props = { +onPress: () => void, +expanded: boolean, }; function ExpandButton(props: Props): React.Node { const styles = useStyles(unboundStyles); const style = props.expanded ? styles.expandButtonExpanded : styles.expandButton; const icon = props.expanded ? 'caret-down' : 'caret-right'; + return ( - - + + ); } function ExpandButtonDisabled(): React.Node { const styles = useStyles(unboundStyles); return ( - + + + ); } const unboundStyles = { expandButton: { color: 'drawerExpandButton', - paddingHorizontal: PADDING_HORIZONTAL, }, expandButtonDisabled: { color: 'drawerExpandButtonDisabled', - paddingHorizontal: PADDING_HORIZONTAL, }, expandButtonExpanded: { color: 'drawerExpandButton', - paddingHorizontal: PADDING_HORIZONTAL, + }, + wrapper: { + width: buttonSize, + alignItems: 'center', + padding: padding, }, }; export { ExpandButton, ExpandButtonDisabled }; diff --git a/native/themes/colors.js b/native/themes/colors.js index 877ebf6e4..d48380804 100644 --- a/native/themes/colors.js +++ b/native/themes/colors.js @@ -1,316 +1,316 @@ // @flow import * as React from 'react'; import { StyleSheet } from 'react-native'; import { createSelector } from 'reselect'; import { selectBackgroundIsDark } from '../navigation/nav-selectors'; import { NavContext } from '../navigation/navigation-context'; import { useSelector } from '../redux/redux-utils'; import type { AppState } from '../redux/state-types'; import type { GlobalTheme } from '../types/themes'; const light = Object.freeze({ blockQuoteBackground: '#D3D3D3', blockQuoteBorder: '#C0C0C0', codeBackground: '#DCDCDC', disabledButton: '#D3D3D3', disconnectedBarBackground: '#F5F5F5', editButton: '#A4A4A2', floatingButtonBackground: '#999999', floatingButtonLabel: '#EEEEEE', greenButton: '#6EC472', greenText: 'green', headerChevron: '#0A0A0A', inlineSidebarBackground: '#E0E0E0', inlineSidebarLabel: '#000000', link: '#036AFF', listBackground: 'white', listBackgroundLabel: 'black', listBackgroundSecondaryLabel: '#444444', listBackgroundTernaryLabel: '#999999', listChatBubble: '#F1F0F5', listForegroundLabel: 'black', listForegroundQuaternaryLabel: '#AAAAAA', listForegroundSecondaryLabel: '#333333', listForegroundTertiaryLabel: '#666666', listInputBackground: '#F5F5F5', listInputBar: '#E2E2E2', listInputBorder: '#AAAAAAAA', listInputButton: '#8E8D92', listIosHighlightUnderlay: '#DDDDDDDD', listSearchBackground: '#F5F5F5', listSearchIcon: '#8E8D92', listSeparator: '#EEEEEE', listSeparatorLabel: '#555555', mintButton: '#44CC99', modalBackground: '#EEEEEE', modalBackgroundLabel: '#333333', modalBackgroundSecondaryLabel: '#AAAAAA', modalButton: '#BBBBBB', modalButtonLabel: 'black', modalContrastBackground: 'black', modalContrastForegroundLabel: 'white', modalContrastOpacity: 0.7, modalForeground: 'white', modalForegroundBorder: '#CCCCCC', modalForegroundLabel: 'black', modalForegroundSecondaryLabel: '#888888', modalForegroundTertiaryLabel: '#AAAAAA', modalIosHighlightUnderlay: '#CCCCCCDD', modalSubtext: '#CCCCCC', modalSubtextLabel: '#555555', navigationCard: '#FFFFFF', navigationChevron: '#BAB9BE', panelBackground: '#F5F5F5', panelBackgroundLabel: '#888888', panelForeground: 'white', panelForegroundBorder: '#CCCCCC', panelForegroundLabel: 'black', panelForegroundSecondaryLabel: '#333333', panelForegroundTertiaryLabel: '#888888', panelIosHighlightUnderlay: '#EEEEEEDD', panelSecondaryForeground: '#F5F5F5', panelSecondaryForegroundBorder: '#D1D1D6', purpleLink: '#7E57C2', purpleButton: '#7E57C2', redButton: '#BB8888', redText: '#FF4444', spoiler: '#33332C', tabBarAccent: '#7E57C2', tabBarBackground: '#F5F5F5', tabBarActiveTintColor: '#7E57C2', vibrantGreenButton: '#00C853', vibrantRedButton: '#F53100', tooltipBackground: '#E0E0E0', logInSpacer: '#FFFFFF33', logInText: 'white', siweButton: 'white', siweButtonText: '#1F1F1F', drawerExpandButton: '#808080', - drawerExpandButtonDisabled: '#404040', - drawerItemLabel: '#CCCCCC', - drawerOpenCommunityBackground: '#333333', - drawerBackgroud: '#404040', + drawerExpandButtonDisabled: '#CCCCCC', + drawerItemLabel: '#0A0A0A', + drawerOpenCommunityBackground: '#F5F5F5', + drawerBackgroud: '#FFFFFF', }); export type Colors = $Exact; const dark: Colors = Object.freeze({ blockQuoteBackground: '#A9A9A9', blockQuoteBorder: '#808080', codeBackground: '#0A0A0A', disabledButton: '#444444', disconnectedBarBackground: '#1D1D1D', editButton: '#5B5B5D', floatingButtonBackground: '#666666', floatingButtonLabel: 'white', greenButton: '#43A047', greenText: '#44FF44', headerChevron: '#FFFFFF', inlineSidebarBackground: '#666666', inlineSidebarLabel: '#FFFFFF', link: '#129AFF', listBackground: '#0A0A0A', listBackgroundLabel: '#C7C7CC', listBackgroundSecondaryLabel: '#BBBBBB', listBackgroundTernaryLabel: '#888888', listChatBubble: '#26252A', listForegroundLabel: 'white', listForegroundQuaternaryLabel: '#555555', listForegroundSecondaryLabel: '#CCCCCC', listForegroundTertiaryLabel: '#999999', listInputBackground: '#1D1D1D', listInputBar: '#555555', listInputBorder: '#333333', listInputButton: '#AAAAAA', listIosHighlightUnderlay: '#BBBBBB88', listSearchBackground: '#1D1D1D', listSearchIcon: '#AAAAAA', listSeparator: '#3A3A3C', listSeparatorLabel: '#EEEEEE', mintButton: '#44CC99', modalBackground: '#0A0A0A', modalBackgroundLabel: '#CCCCCC', modalBackgroundSecondaryLabel: '#555555', modalButton: '#666666', modalButtonLabel: 'white', modalContrastBackground: 'white', modalContrastForegroundLabel: 'black', modalContrastOpacity: 0.85, modalForeground: '#1C1C1E', modalForegroundBorder: '#1C1C1E', modalForegroundLabel: 'white', modalForegroundSecondaryLabel: '#AAAAAA', modalForegroundTertiaryLabel: '#666666', modalIosHighlightUnderlay: '#AAAAAA88', modalSubtext: '#444444', modalSubtextLabel: '#AAAAAA', navigationCard: '#2A2A2A', navigationChevron: '#5B5B5D', panelBackground: '#0A0A0A', panelBackgroundLabel: '#C7C7CC', panelForeground: '#1D1D1D', panelForegroundBorder: '#2C2C2E', panelForegroundLabel: 'white', panelForegroundSecondaryLabel: '#CCCCCC', panelForegroundTertiaryLabel: '#AAAAAA', panelIosHighlightUnderlay: '#313035', panelSecondaryForeground: '#333333', panelSecondaryForegroundBorder: '#666666', purpleLink: '#AE94DB', purpleButton: '#7E57C2', redButton: '#FF4444', redText: '#FF4444', spoiler: '#33332C', tabBarAccent: '#AE94DB', tabBarBackground: '#0A0A0A', tabBarActiveTintColor: '#AE94DB', vibrantGreenButton: '#00C853', vibrantRedButton: '#F53100', tooltipBackground: '#1F1F1F', logInSpacer: '#FFFFFF33', logInText: 'white', siweButton: 'white', siweButtonText: '#1F1F1F', drawerExpandButton: '#808080', drawerExpandButtonDisabled: '#404040', drawerItemLabel: '#CCCCCC', - drawerOpenCommunityBackground: '#333333', - drawerBackgroud: '#404040', + drawerOpenCommunityBackground: '#191919', + drawerBackgroud: '#1F1F1F', }); const colors = { light, dark }; const colorsSelector: (state: AppState) => Colors = createSelector( (state: AppState) => state.globalThemeInfo.activeTheme, (theme: ?GlobalTheme) => { const explicitTheme = theme ? theme : 'light'; return colors[explicitTheme]; }, ); const magicStrings = new Set(); for (const theme in colors) { for (const magicString in colors[theme]) { magicStrings.add(magicString); } } type Styles = { [name: string]: { [field: string]: mixed } }; type ReplaceField = (input: any) => any; export type StyleSheetOf = $ObjMap; function stylesFromColors( obj: IS, themeColors: Colors, ): StyleSheetOf { const result = {}; for (const key in obj) { const style = obj[key]; const filledInStyle = { ...style }; for (const styleKey in style) { const styleValue = style[styleKey]; if (typeof styleValue !== 'string') { continue; } if (magicStrings.has(styleValue)) { const mapped = themeColors[styleValue]; if (mapped) { filledInStyle[styleKey] = mapped; } } } result[key] = filledInStyle; } return StyleSheet.create(result); } function styleSelector( obj: IS, ): (state: AppState) => StyleSheetOf { return createSelector(colorsSelector, (themeColors: Colors) => stylesFromColors(obj, themeColors), ); } function useStyles(obj: IS): StyleSheetOf { const ourColors = useColors(); return React.useMemo(() => stylesFromColors(obj, ourColors), [ obj, ourColors, ]); } function useOverlayStyles(obj: IS): StyleSheetOf { const navContext = React.useContext(NavContext); const navigationState = navContext && navContext.state; const theme = useSelector( (state: AppState) => state.globalThemeInfo.activeTheme, ); const backgroundIsDark = React.useMemo( () => selectBackgroundIsDark(navigationState, theme), [navigationState, theme], ); const syntheticTheme = backgroundIsDark ? 'dark' : 'light'; return React.useMemo(() => stylesFromColors(obj, colors[syntheticTheme]), [ obj, syntheticTheme, ]); } function useColors(): Colors { return useSelector(colorsSelector); } function getStylesForTheme( obj: IS, theme: GlobalTheme, ): StyleSheetOf { return stylesFromColors(obj, colors[theme]); } export type IndicatorStyle = 'white' | 'black'; function useIndicatorStyle(): IndicatorStyle { const theme = useSelector( (state: AppState) => state.globalThemeInfo.activeTheme, ); return theme && theme === 'dark' ? 'white' : 'black'; } const indicatorStyleSelector: ( state: AppState, ) => IndicatorStyle = createSelector( (state: AppState) => state.globalThemeInfo.activeTheme, (theme: ?GlobalTheme) => { return theme && theme === 'dark' ? 'white' : 'black'; }, ); export type KeyboardAppearance = 'default' | 'light' | 'dark'; const keyboardAppearanceSelector: ( state: AppState, ) => KeyboardAppearance = createSelector( (state: AppState) => state.globalThemeInfo.activeTheme, (theme: ?GlobalTheme) => { return theme && theme === 'dark' ? 'dark' : 'light'; }, ); function useKeyboardAppearance(): KeyboardAppearance { return useSelector(keyboardAppearanceSelector); } export { colors, colorsSelector, styleSelector, useStyles, useOverlayStyles, useColors, getStylesForTheme, useIndicatorStyle, indicatorStyleSelector, useKeyboardAppearance, };