diff --git a/native/chat/sidebar-list-modal.react.js b/native/chat/sidebar-list-modal.react.js index f55b6d437..e05ebd984 100644 --- a/native/chat/sidebar-list-modal.react.js +++ b/native/chat/sidebar-list-modal.react.js @@ -1,144 +1,146 @@ // @flow import * as React from 'react'; import { View } from 'react-native'; import { useSearchSidebars } from 'lib/hooks/search-threads'; import type { ThreadInfo, SidebarInfo } from 'lib/types/thread-types'; import ExtendedArrow from '../components/arrow-extended.react'; import Arrow from '../components/arrow.react'; import Button from '../components/button.react'; import type { RootNavigationProp } from '../navigation/root-navigator.react'; import type { NavigationRoute } from '../navigation/route-names'; import { useColors, useStyles } from '../themes/colors'; import { SidebarItem } from './sidebar-item.react'; import ThreadListModal from './thread-list-modal.react'; export type SidebarListModalParams = { +threadInfo: ThreadInfo, }; type Props = { +navigation: RootNavigationProp<'SidebarListModal'>, +route: NavigationRoute<'SidebarListModal'>, }; function SidebarListModal(props: Props): React.Node { const { listData, searchState, setSearchState, onChangeSearchInputText, } = useSearchSidebars(props.route.params.threadInfo); const numOfSidebarsWithExtendedArrow = listData.length - 1; const createRenderItem = React.useCallback( ( onPressItem: (threadInfo: ThreadInfo) => void, // eslint-disable-next-line react/display-name ) => (row: { +item: SidebarInfo, +index: number, ... }) => { let extendArrow: boolean = false; if (row.index < numOfSidebarsWithExtendedArrow) { extendArrow = true; } return ( ); }, [numOfSidebarsWithExtendedArrow], ); return ( ); } function Item(props: { item: SidebarInfo, onPressItem: (threadInfo: ThreadInfo) => void, extendArrow: boolean, }): React.Node { const { item, onPressItem, extendArrow } = props; const { threadInfo } = item; const onPressButton = React.useCallback(() => onPressItem(threadInfo), [ onPressItem, threadInfo, ]); const colors = useColors(); const styles = useStyles(unboundStyles); let arrow; if (extendArrow) { arrow = ( ); } else { arrow = ( ); } return ( ); } const unboundStyles = { arrow: { position: 'absolute', top: -12, }, extendedArrow: { position: 'absolute', top: -6, }, sidebar: { paddingLeft: 0, paddingRight: 5, + height: 38, }, sidebarItemContainer: { flex: 1, }, sidebarRowContainer: { flex: 1, flexDirection: 'row', }, spacer: { width: 30, }, }; export default SidebarListModal; diff --git a/native/chat/subchannel-item.react.js b/native/chat/subchannel-item.react.js index a91a47a66..e75be2a8d 100644 --- a/native/chat/subchannel-item.react.js +++ b/native/chat/subchannel-item.react.js @@ -1,114 +1,112 @@ // @flow import * as React from 'react'; import { Text, View } from 'react-native'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; import { shortAbsoluteDate } from 'lib/utils/date-utils'; import { SingleLine } from '../components/single-line.react'; import SWMansionIcon from '../components/swmansion-icon.react'; import { useStyles } from '../themes/colors'; import MessagePreview from './message-preview.react'; type Props = { +subchannelInfo: ChatThreadItem, }; function SubchannelItem(props: Props): React.Node { const { lastUpdatedTime, threadInfo, mostRecentMessageInfo, } = props.subchannelInfo; const lastActivity = shortAbsoluteDate(lastUpdatedTime); const styles = useStyles(unboundStyles); const unreadStyle = threadInfo.currentUser.unread ? styles.unread : null; const lastMessage = React.useMemo(() => { if (!mostRecentMessageInfo) { return ( No messages ); } return ( ); }, [mostRecentMessageInfo, threadInfo, styles]); return ( {threadInfo.uiName} {lastMessage} {lastActivity} ); } - +const fontSize = 14; const unboundStyles = { outerView: { flex: 1, flexDirection: 'column', justifyContent: 'center', paddingVertical: 8, - paddingHorizontal: 16, + paddingHorizontal: 8, height: 60, }, itemRowContainer: { flexDirection: 'row', - height: 24, alignItems: 'center', }, unread: { color: 'listForegroundLabel', fontWeight: 'bold', }, name: { color: 'listForegroundSecondaryLabel', flex: 1, fontSize: 16, - paddingLeft: 3, - paddingBottom: 2, + paddingBottom: 8, }, lastActivity: { color: 'listForegroundTertiaryLabel', - fontSize: 14, + fontSize, marginLeft: 10, }, iconWrapper: { marginRight: 8, alignItems: 'center', }, icon: { fontSize: 22, color: 'listForegroundSecondaryLabel', alignItems: 'center', height: 24, }, noMessages: { color: 'listForegroundTertiaryLabel', flex: 1, - fontSize: 14, + fontSize, fontStyle: 'italic', }, }; export default SubchannelItem; diff --git a/native/chat/subchannels-list-modal.react.js b/native/chat/subchannels-list-modal.react.js index 8dc3f4b64..05ab325b6 100644 --- a/native/chat/subchannels-list-modal.react.js +++ b/native/chat/subchannels-list-modal.react.js @@ -1,99 +1,100 @@ // @flow import * as React from 'react'; import { View } from 'react-native'; import { useSearchSubchannels } from 'lib/hooks/search-threads'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; import { type ThreadInfo } from 'lib/types/thread-types'; import Button from '../components/button.react'; import type { RootNavigationProp } from '../navigation/root-navigator.react'; import type { NavigationRoute } from '../navigation/route-names'; import { useColors, useStyles } from '../themes/colors'; import SubchannelItem from './subchannel-item.react'; import ThreadListModal from './thread-list-modal.react'; export type SubchannelListModalParams = { +threadInfo: ThreadInfo, }; type Props = { +navigation: RootNavigationProp<'SubchannelsListModal'>, +route: NavigationRoute<'SubchannelsListModal'>, }; function SubchannelListModal(props: Props): React.Node { const { listData, searchState, setSearchState, onChangeSearchInputText, } = useSearchSubchannels(props.route.params.threadInfo); return ( ); } const createRenderItem = ( onPressItem: (threadInfo: ThreadInfo) => void, // eslint-disable-next-line react/display-name ) => (row: { +item: ChatThreadItem, +index: number, ... }) => { return ; }; function Item(props: { onPressItem: (threadInfo: ThreadInfo) => void, subchannelInfo: ChatThreadItem, }): React.Node { const { onPressItem, subchannelInfo } = props; const { threadInfo } = subchannelInfo; const onPressButton = React.useCallback(() => onPressItem(threadInfo), [ onPressItem, threadInfo, ]); const colors = useColors(); const styles = useStyles(unboundStyles); return ( ); } const unboundStyles = { subchannel: { paddingLeft: 0, paddingRight: 5, }, subchannelItemContainer: { flex: 1, }, subchannelRowContainer: { flex: 1, flexDirection: 'row', }, }; export default SubchannelListModal; diff --git a/native/chat/thread-list-modal.react.js b/native/chat/thread-list-modal.react.js index a9c964de5..7b94d8ed4 100644 --- a/native/chat/thread-list-modal.react.js +++ b/native/chat/thread-list-modal.react.js @@ -1,118 +1,184 @@ // @flow +import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; -import { TextInput, FlatList, StyleSheet } from 'react-native'; +import { + Text, + TextInput, + FlatList, + View, + TouchableOpacity, +} from 'react-native'; import type { ThreadSearchState } from 'lib/hooks/search-threads'; import type { ChatThreadItem } from 'lib/selectors/chat-selectors'; import type { SetState } from 'lib/types/hook-types'; import type { ThreadInfo, SidebarInfo } from 'lib/types/thread-types'; import Modal from '../components/modal.react'; import Search from '../components/search.react'; -import { useIndicatorStyle } from '../themes/colors'; +import SWMansionIcon from '../components/swmansion-icon.react'; +import { useIndicatorStyle, useStyles } from '../themes/colors'; import { waitForModalInputFocus } from '../utils/timers'; import { useNavigateToThread } from './message-list-types'; function keyExtractor(sidebarInfo: SidebarInfo | ChatThreadItem) { return sidebarInfo.threadInfo.id; } function getItemLayout( data: ?$ReadOnlyArray, index: number, ) { return { length: 24, offset: 24 * index, index }; } type Props = { +threadInfo: ThreadInfo, +createRenderItem: ( onPressItem: (threadInfo: ThreadInfo) => void, ) => (row: { +item: U, +index: number, ... }) => React.Node, +listData: $ReadOnlyArray, +searchState: ThreadSearchState, +setSearchState: SetState, +onChangeSearchInputText: (text: string) => mixed, - +searchPlaceholder: string, + +searchPlaceholder?: string, + +modalTitle: string, }; function ThreadListModal( props: Props, ): React.Node { const { searchState, setSearchState, onChangeSearchInputText, listData, createRenderItem, searchPlaceholder, + modalTitle, } = props; const searchTextInputRef = React.useRef(); const setSearchTextInputRef = React.useCallback( async (textInput: ?React.ElementRef) => { searchTextInputRef.current = textInput; if (!textInput) { return; } await waitForModalInputFocus(); if (searchTextInputRef.current) { searchTextInputRef.current.focus(); } }, [], ); const navigateToThread = useNavigateToThread(); const onPressItem = React.useCallback( (threadInfo: ThreadInfo) => { setSearchState({ text: '', results: new Set(), }); if (searchTextInputRef.current) { searchTextInputRef.current.blur(); } navigateToThread({ threadInfo }); }, [navigateToThread, setSearchState], ); const renderItem = React.useMemo(() => createRenderItem(onPressItem), [ createRenderItem, onPressItem, ]); + const styles = useStyles(unboundStyles); const indicatorStyle = useIndicatorStyle(); + const navigation = useNavigation(); + return ( - - - + + + {modalTitle} + + + + + + + + ); } -const styles = StyleSheet.create({ +const unboundStyles = { + body: { + paddingHorizontal: 16, + flex: 1, + }, + header: { + borderBottomColor: 'subthreadsModalSearch', + borderBottomWidth: 1, + height: 72, + padding: 16, + flexDirection: 'row', + justifyContent: 'space-between', + }, + modal: { + borderRadius: 8, + paddingHorizontal: 0, + backgroundColor: 'subthreadsModalBackgroud', + paddingTop: 0, + justifyContent: 'flex-start', + }, search: { - marginBottom: 8, + height: 40, + marginVertical: 16, + backgroundColor: 'subthreadsModalSearch', }, -}); + title: { + color: 'listForegroundLabel', + fontSize: 20, + fontWeight: '500', + lineHeight: 26, + alignSelf: 'center', + marginLeft: 2, + }, + closeIcon: { + color: 'subthreadsModalClose', + }, + closeButton: { + marginRight: 2, + height: 40, + alignItems: 'center', + justifyContent: 'center', + }, +}; export default ThreadListModal; diff --git a/native/themes/colors.js b/native/themes/colors.js index 5c14c547b..9f5b61fd1 100644 --- a/native/themes/colors.js +++ b/native/themes/colors.js @@ -1,320 +1,326 @@ // @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: '#CCCCCC', drawerItemLabelLevel0: '#0A0A0A', drawerItemLabelLevel1: '#0A0A0A', drawerItemLabelLevel2: '#1F1F1F', drawerOpenCommunityBackground: '#F5F5F5', drawerBackgroud: '#FFFFFF', + subthreadsModalClose: '#808080', + subthreadsModalBackgroud: '#EEEEEE', + subthreadsModalSearch: 'rgba(0, 0, 0, 0.08)', }); 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', drawerItemLabelLevel0: '#CCCCCC', drawerItemLabelLevel1: '#CCCCCC', drawerItemLabelLevel2: '#F5F5F5', drawerOpenCommunityBackground: '#191919', drawerBackgroud: '#1F1F1F', + subthreadsModalClose: '#808080', + subthreadsModalBackgroud: '#1F1F1F', + subthreadsModalSearch: 'rgba(255, 255, 255, 0.04)', }); 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, };