diff --git a/native/chat/new-messages-pill.react.js b/native/chat/new-messages-pill.react.js index 9fa757014..98b4d357d 100644 --- a/native/chat/new-messages-pill.react.js +++ b/native/chat/new-messages-pill.react.js @@ -1,73 +1,73 @@ // @flow import * as React from 'react'; import { TouchableOpacity, View, Text, Platform, Animated } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; import { useStyles } from '../themes/colors'; import type { ViewStyle } from '../types/styles'; type Props = {| onPress: () => mixed, newMessageCount: number, containerStyle?: ViewStyle, style?: ViewStyle, ...React.ElementConfig, |}; function NewMessagesPill(props: Props) { const { onPress, newMessageCount, containerStyle, style, ...containerProps } = props; const styles = useStyles(unboundStyles); return ( {newMessageCount} ); } const unboundStyles = { countBubble: { alignItems: 'center', - backgroundColor: 'greenButton', + backgroundColor: 'vibrantGreenButton', borderRadius: 25, height: 25, justifyContent: 'center', paddingBottom: Platform.OS === 'android' ? 2 : 0, paddingLeft: 1, position: 'absolute', right: -8, top: -8, width: 25, }, countText: { color: 'white', textAlign: 'center', }, button: { backgroundColor: 'floatingButtonBackground', borderColor: 'floatingButtonLabel', borderRadius: 30, borderWidth: 4, paddingHorizontal: 12, paddingVertical: 6, }, icon: { color: 'floatingButtonLabel', fontSize: 32, fontWeight: 'bold', }, }; export default NewMessagesPill; diff --git a/native/chat/relationship-prompt.react.js b/native/chat/relationship-prompt.react.js index b8ce5843d..ec5ed0c4c 100644 --- a/native/chat/relationship-prompt.react.js +++ b/native/chat/relationship-prompt.react.js @@ -1,212 +1,225 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Alert, Text, View } from 'react-native'; +import Icon from 'react-native-vector-icons/FontAwesome5'; import { updateRelationships as serverUpdateRelationships, updateRelationshipsActionTypes, } from 'lib/actions/relationship-actions'; import type { RelationshipAction } from 'lib/types/relationship-types'; import { relationshipActions, userRelationshipStatus, } from 'lib/types/relationship-types'; import type { ThreadInfo } from 'lib/types/thread-types'; import type { UserInfo } from 'lib/types/user-types'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import Button from '../components/button.react'; import { useSelector } from '../redux/redux-utils'; import { useStyles } from '../themes/colors'; type Props = {| +pendingPersonalThreadUserInfo: ?UserInfo, +threadInfo: ThreadInfo, |}; export default React.memo(function RelationshipPrompt({ pendingPersonalThreadUserInfo, threadInfo, }: Props) { // We're fetching the info from state because we need the most recent // relationship status. Additionally, member info does not contain info // about relationship. const otherUserInfo = useSelector((state) => { const currentUserID = state.currentUserInfo?.id; const otherUserID = threadInfo.members .map((member) => member.id) .find((id) => id !== currentUserID) ?? pendingPersonalThreadUserInfo?.id; const { userInfos } = state.userStore; return otherUserID && userInfos[otherUserID] ? userInfos[otherUserID] : pendingPersonalThreadUserInfo; }); const callUpdateRelationships = useServerCall(serverUpdateRelationships); const updateRelationship = React.useCallback( async (action: RelationshipAction) => { try { invariant(otherUserInfo, 'Other user info should be present'); return await callUpdateRelationships({ action, userIDs: [otherUserInfo.id], }); } catch (e) { Alert.alert('Unknown error', 'Uhh... try again?', [{ text: 'OK' }]); throw e; } }, [callUpdateRelationships, otherUserInfo], ); const dispatchActionPromise = useDispatchActionPromise(); const onButtonPress = React.useCallback( (action: RelationshipAction) => { invariant( otherUserInfo, 'User info should be present when a button is clicked', ); dispatchActionPromise( updateRelationshipsActionTypes, updateRelationship(action), ); }, [dispatchActionPromise, otherUserInfo, updateRelationship], ); const blockUser = React.useCallback( () => onButtonPress(relationshipActions.BLOCK), [onButtonPress], ); const unblockUser = React.useCallback( () => onButtonPress(relationshipActions.UNBLOCK), [onButtonPress], ); const friendUser = React.useCallback( () => onButtonPress(relationshipActions.FRIEND), [onButtonPress], ); const unfriendUser = React.useCallback( () => onButtonPress(relationshipActions.UNFRIEND), [onButtonPress], ); const styles = useStyles(unboundStyles); if ( !otherUserInfo || !otherUserInfo.username || otherUserInfo.relationshipStatus === userRelationshipStatus.FRIEND ) { return null; } if ( otherUserInfo.relationshipStatus === userRelationshipStatus.BLOCKED_VIEWER ) { return ( ); } if ( otherUserInfo.relationshipStatus === userRelationshipStatus.BOTH_BLOCKED || otherUserInfo.relationshipStatus === userRelationshipStatus.BLOCKED_BY_VIEWER ) { return ( ); } if ( otherUserInfo.relationshipStatus === userRelationshipStatus.REQUEST_RECEIVED ) { return ( ); } if ( otherUserInfo.relationshipStatus === userRelationshipStatus.REQUEST_SENT ) { return ( ); } return ( ); }); const unboundStyles = { container: { paddingVertical: 10, paddingHorizontal: 5, backgroundColor: 'panelBackground', flexDirection: 'row', }, button: { padding: 10, borderRadius: 5, flex: 1, + flexDirection: 'row', + justifyContent: 'center', marginHorizontal: 5, }, greenButton: { - backgroundColor: 'greenButton', + backgroundColor: 'vibrantGreenButton', }, redButton: { - backgroundColor: 'redButton', + backgroundColor: 'vibrantRedButton', }, buttonText: { - fontSize: 16, + fontSize: 11, + color: 'white', + fontWeight: 'bold', textAlign: 'center', + marginLeft: 5, }, }; diff --git a/native/chat/swipeable-thread.react.js b/native/chat/swipeable-thread.react.js index e0a1f0f76..804241120 100644 --- a/native/chat/swipeable-thread.react.js +++ b/native/chat/swipeable-thread.react.js @@ -1,118 +1,119 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons'; import { setThreadUnreadStatus, setThreadUnreadStatusActionTypes, } from 'lib/actions/activity-actions'; import type { SetThreadUnreadStatusPayload, SetThreadUnreadStatusRequest, } from 'lib/types/activity-types'; import type { ThreadInfo } from 'lib/types/thread-types'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import Swipeable from '../components/swipeable'; import { useColors } from '../themes/colors'; type Props = {| +threadInfo: ThreadInfo, +mostRecentNonLocalMessage: ?string, +onSwipeableWillOpen: (threadInfo: ThreadInfo) => void, +currentlyOpenedSwipeableId?: string, +iconSize: number, +children: React.Node, |}; function SwipeableThread(props: Props) { const swipeable = React.useRef(); const navigation = useNavigation(); React.useEffect(() => { return navigation.addListener('blur', () => { if (swipeable.current) { swipeable.current.close(); } }); }, [navigation, swipeable]); const { threadInfo, currentlyOpenedSwipeableId } = props; React.useEffect(() => { if (swipeable.current && threadInfo.id !== currentlyOpenedSwipeableId) { swipeable.current.close(); } }, [currentlyOpenedSwipeableId, swipeable, threadInfo.id]); const { onSwipeableWillOpen } = props; const onSwipeableRightWillOpen = React.useCallback(() => { onSwipeableWillOpen(threadInfo); }, [onSwipeableWillOpen, threadInfo]); const colors = useColors(); const { mostRecentNonLocalMessage, iconSize } = props; const updateUnreadStatus: ( request: SetThreadUnreadStatusRequest, ) => Promise = useServerCall( setThreadUnreadStatus, ); const dispatchActionPromise = useDispatchActionPromise(); const swipeableActions = React.useMemo(() => { const isUnread = threadInfo.currentUser.unread; const toggleUnreadStatus = () => { const request = { unread: !isUnread, threadID: threadInfo.id, latestMessage: mostRecentNonLocalMessage, }; dispatchActionPromise( setThreadUnreadStatusActionTypes, updateUnreadStatus(request), undefined, { threadID: threadInfo.id, unread: !isUnread, }, ); if (swipeable.current) { swipeable.current.close(); } }; return [ { key: 'action1', onPress: toggleUnreadStatus, - color: isUnread ? colors.redButton : colors.greenButton, + color: isUnread ? colors.vibrantRedButton : colors.vibrantGreenButton, content: ( ), }, ]; }, [ colors, threadInfo, mostRecentNonLocalMessage, iconSize, updateUnreadStatus, dispatchActionPromise, ]); return ( {props.children} ); } export default SwipeableThread; diff --git a/native/themes/colors.js b/native/themes/colors.js index 4fe45ed61..c2aefac16 100644 --- a/native/themes/colors.js +++ b/native/themes/colors.js @@ -1,261 +1,265 @@ // @flow import PropTypes from 'prop-types'; 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 type { AppState } from '../redux/redux-setup'; import { useSelector } from '../redux/redux-utils'; import type { GlobalTheme } from '../types/themes'; const light = Object.freeze({ redButton: '#BB8888', - greenButton: '#88BB88', + greenButton: '#6EC472', + vibrantRedButton: '#F53100', + vibrantGreenButton: '#00C853', mintButton: '#44CC99', redText: '#AA0000', greenText: 'green', link: '#036AFF', panelBackground: '#E9E9EF', panelBackgroundLabel: '#888888', panelForeground: 'white', panelForegroundBorder: '#CCCCCC', panelForegroundLabel: 'black', panelForegroundSecondaryLabel: '#333333', panelForegroundTertiaryLabel: '#888888', panelIosHighlightUnderlay: '#EEEEEEDD', panelSecondaryForeground: '#F5F5F5', panelSecondaryForegroundBorder: '#D1D1D6', modalForeground: 'white', modalForegroundBorder: '#CCCCCC', modalForegroundLabel: 'black', modalForegroundSecondaryLabel: '#888888', modalForegroundTertiaryLabel: '#AAAAAA', modalBackground: '#EEEEEE', modalBackgroundLabel: '#333333', modalBackgroundSecondaryLabel: '#AAAAAA', modalIosHighlightUnderlay: '#CCCCCCDD', modalSubtext: '#CCCCCC', modalSubtextLabel: '#555555', modalButton: '#BBBBBB', modalButtonLabel: 'black', modalContrastBackground: 'black', modalContrastForegroundLabel: 'white', modalContrastOpacity: 0.7, listForegroundLabel: 'black', listForegroundSecondaryLabel: '#333333', listForegroundTertiaryLabel: '#666666', listForegroundQuaternaryLabel: '#AAAAAA', listBackground: 'white', listBackgroundLabel: 'black', listBackgroundSecondaryLabel: '#444444', listBackgroundTernaryLabel: '#999999', listSeparator: '#EEEEEE', listSeparatorLabel: '#555555', listInputBar: '#E2E2E2', listInputBorder: '#AAAAAAAA', listInputButton: '#888888', listInputBackground: '#DDDDDD', listIosHighlightUnderlay: '#DDDDDDDD', listSearchBackground: '#DDDDDD', listSearchIcon: '#AAAAAA', listChatBubble: '#DDDDDDBB', navigationCard: '#FFFFFF', floatingButtonBackground: '#999999', floatingButtonLabel: '#EEEEEE', blockQuoteBackground: '#D3D3D3', blockQuoteBorder: '#C0C0C0', codeBackground: '#DCDCDC', disconnectedBarBackground: '#C6C6C6', }); export type Colors = $Exact; const colorsPropType = PropTypes.objectOf( PropTypes.oneOfType([PropTypes.string, PropTypes.number]), ); const dark: Colors = Object.freeze({ redButton: '#FF4444', - greenButton: '#44BB44', + greenButton: '#43A047', + vibrantRedButton: '#F53100', + vibrantGreenButton: '#00C853', mintButton: '#44CC99', redText: '#FF4444', greenText: '#44FF44', link: '#129AFF', panelBackground: '#1C1C1E', panelBackgroundLabel: '#C7C7CC', panelForeground: '#3A3A3C', panelForegroundBorder: '#2C2C2E', panelForegroundLabel: 'white', panelForegroundSecondaryLabel: '#CCCCCC', panelForegroundTertiaryLabel: '#AAAAAA', panelIosHighlightUnderlay: '#444444DD', panelSecondaryForeground: '#333333', panelSecondaryForegroundBorder: '#666666', modalForeground: '#1C1C1E', modalForegroundBorder: '#1C1C1E', modalForegroundLabel: 'white', modalForegroundSecondaryLabel: '#AAAAAA', modalForegroundTertiaryLabel: '#666666', modalBackground: '#2C2C2E', modalBackgroundLabel: '#CCCCCC', modalBackgroundSecondaryLabel: '#555555', modalIosHighlightUnderlay: '#AAAAAA88', modalSubtext: '#444444', modalSubtextLabel: '#AAAAAA', modalButton: '#666666', modalButtonLabel: 'white', modalContrastBackground: 'white', modalContrastForegroundLabel: 'black', modalContrastOpacity: 0.85, listForegroundLabel: 'white', listForegroundSecondaryLabel: '#CCCCCC', listForegroundTertiaryLabel: '#999999', listForegroundQuaternaryLabel: '#555555', listBackground: '#1C1C1E', listBackgroundLabel: '#C7C7CC', listBackgroundSecondaryLabel: '#BBBBBB', listBackgroundTernaryLabel: '#888888', listSeparator: '#3A3A3C', listSeparatorLabel: '#EEEEEE', listInputBar: '#555555', listInputBorder: '#333333', listInputButton: '#AAAAAA', listInputBackground: '#38383C', listIosHighlightUnderlay: '#BBBBBB88', listSearchBackground: '#555555', listSearchIcon: '#AAAAAA', listChatBubble: '#444444DD', navigationCard: '#2A2A2A', floatingButtonBackground: '#666666', floatingButtonLabel: 'white', blockQuoteBackground: '#A9A9A9', blockQuoteBorder: '#808080', codeBackground: '#222222', disconnectedBarBackground: '#666666', }); 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 (let theme in colors) { for (let 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 (let key in obj) { const style = obj[key]; const filledInStyle = { ...style }; for (let 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'; const indicatorStylePropType = PropTypes.oneOf(['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 { colorsPropType, colors, colorsSelector, styleSelector, useStyles, useOverlayStyles, useColors, getStylesForTheme, indicatorStylePropType, useIndicatorStyle, indicatorStyleSelector, };