diff --git a/native/chat/inline-engagement.react.js b/native/chat/inline-engagement.react.js index e436eab6c..64f1b1811 100644 --- a/native/chat/inline-engagement.react.js +++ b/native/chat/inline-engagement.react.js @@ -1,294 +1,340 @@ // @flow import { useNavigation } from '@react-navigation/native'; import * as React from 'react'; import { Text, View } from 'react-native'; import Animated, { Extrapolate, interpolateNode, } from 'react-native-reanimated'; import useInlineEngagementText from 'lib/hooks/inline-engagement-text.react.js'; import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; import { stringForReactionList } from 'lib/shared/reaction-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { inlineEngagementStyle, inlineEngagementCenterStyle, inlineEngagementRightStyle, inlineEngagementLeftStyle, composedMessageStyle, avatarOffset, } from './chat-constants.js'; import { useNavigateToThread } from './message-list-types.js'; import CommIcon from '../components/comm-icon.react.js'; import GestureTouchableOpacity from '../components/gesture-touchable-opacity.react.js'; import { MessageReactionsModalRouteName } from '../navigation/route-names.js'; import { useStyles } from '../themes/colors.js'; import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; type Props = { +threadInfo: ?ThreadInfo, +reactions?: ReactionInfo, +disabled?: boolean, +positioning?: 'left' | 'right', + +label?: ?string, +shouldRenderAvatars?: boolean, }; function InlineEngagement(props: Props): React.Node { const { disabled = false, reactions, threadInfo, positioning, shouldRenderAvatars, + label, } = props; const repliesText = useInlineEngagementText(threadInfo); const navigateToThread = useNavigateToThread(); const { navigate } = useNavigation(); const styles = useStyles(unboundStyles); const unreadStyle = threadInfo?.currentUser.unread ? styles.unread : null; const repliesStyles = React.useMemo( () => [styles.repliesText, unreadStyle], [styles.repliesText, unreadStyle], ); const onPressThread = React.useCallback(() => { if (threadInfo && !disabled) { navigateToThread({ threadInfo }); } }, [disabled, navigateToThread, threadInfo]); const sidebarItem = React.useMemo(() => { if (!threadInfo) { return null; } return ( {repliesText} ); }, [ threadInfo, onPressThread, styles.sidebar, styles.icon, repliesStyles, repliesText, ]); const onPressReactions = React.useCallback(() => { navigate<'MessageReactionsModal'>({ name: MessageReactionsModalRouteName, params: { reactions }, }); }, [navigate, reactions]); const marginLeft = React.useMemo( () => (sidebarItem ? styles.reactionMarginLeft : null), [sidebarItem, styles.reactionMarginLeft], ); const reactionList = React.useMemo(() => { if (!reactions || Object.keys(reactions).length === 0) { return null; } const reactionText = stringForReactionList(reactions); const reactionItems = {reactionText}; return ( {reactionItems} ); }, [ marginLeft, onPressReactions, reactions, styles.reaction, styles.reactionsContainer, ]); + const isLeft = positioning === 'left'; + + const editedLabel = React.useMemo(() => { + if (!label) { + return null; + } + + const labelLeftRight = isLeft + ? styles.messageLabelLeft + : styles.messageLabelRight; + + return {label}; + }, [isLeft, label, styles]); + const container = React.useMemo(() => { + if (!sidebarItem && !reactionList) { + return null; + } return ( {sidebarItem} {reactionList} ); }, [reactionList, sidebarItem, styles.container]); - const inlineEngagementPositionStyle = []; - if (positioning === 'left') { + const inlineEngagementPositionStyle = [styles.inlineEngagement]; + if (isLeft) { inlineEngagementPositionStyle.push(styles.leftInlineEngagement); } else { inlineEngagementPositionStyle.push(styles.rightInlineEngagement); } if (shouldRenderAvatars) { inlineEngagementPositionStyle.push({ marginLeft: avatarOffset }); } - return ( - - {container} - - ); + let body; + if (isLeft) { + body = ( + <> + {editedLabel} + {container} + + ); + } else { + body = ( + <> + {container} + {editedLabel} + + ); + } + + return {body}; } const unboundStyles = { container: { flexDirection: 'row', height: inlineEngagementStyle.height, borderRadius: 16, backgroundColor: 'inlineEngagementBackground', alignSelf: 'baseline', alignItems: 'center', padding: 8, }, unread: { color: 'listForegroundLabel', fontWeight: 'bold', }, rightInlineEngagement: { alignSelf: 'flex-end', position: 'relative', right: inlineEngagementRightStyle.marginRight, top: inlineEngagementRightStyle.topOffset, }, leftInlineEngagement: { justifyContent: 'flex-start', position: 'relative', top: inlineEngagementLeftStyle.topOffset, }, sidebar: { flexDirection: 'row', alignItems: 'center', }, inlineEngagement: { flexDirection: 'row', marginBottom: inlineEngagementStyle.marginBottom, marginTop: inlineEngagementStyle.marginTop, alignItems: 'center', }, icon: { color: 'inlineEngagementLabel', marginRight: 4, }, repliesText: { color: 'inlineEngagementLabel', fontSize: 14, lineHeight: 22, }, reaction: { color: 'inlineEngagementLabel', fontSize: 14, lineHeight: 22, }, reactionMarginLeft: { marginLeft: 12, }, reactionsContainer: { display: 'flex', flexDirection: 'row', alignItems: 'center', }, + messageLabel: { + color: 'messageLabel', + paddingHorizontal: 3, + fontSize: 13, + top: 10, + }, + messageLabelLeft: { + marginLeft: 9, + marginRight: 4, + }, + messageLabelRight: { + marginRight: 10, + marginLeft: 4, + }, avatarOffset: { width: avatarOffset, }, }; type TooltipInlineEngagementProps = { +item: ChatMessageInfoItemWithHeight, +isOpeningSidebar: boolean, +progress: Animated.Node, +windowWidth: number, +positioning: 'left' | 'right' | 'center', +initialCoordinates: { +x: number, +y: number, +width: number, +height: number, }, }; function TooltipInlineEngagement( props: TooltipInlineEngagementProps, ): React.Node { const { item, isOpeningSidebar, progress, windowWidth, initialCoordinates, positioning, } = props; const inlineEngagementStyles = React.useMemo(() => { if (positioning === 'left') { return { position: 'absolute', top: inlineEngagementStyle.marginTop + inlineEngagementLeftStyle.topOffset, left: composedMessageStyle.marginLeft, }; } else if (positioning === 'right') { return { position: 'absolute', right: inlineEngagementRightStyle.marginRight + composedMessageStyle.marginRight, top: inlineEngagementStyle.marginTop + inlineEngagementRightStyle.topOffset, }; } else if (positioning === 'center') { return { alignSelf: 'center', top: inlineEngagementCenterStyle.topOffset, }; } }, [positioning]); const inlineEngagementContainer = React.useMemo(() => { const opacity = isOpeningSidebar ? 0 : interpolateNode(progress, { inputRange: [0, 1], outputRange: [1, 0], extrapolate: Extrapolate.CLAMP, }); return { position: 'absolute', width: windowWidth, top: initialCoordinates.height, left: -initialCoordinates.x, opacity, }; }, [ initialCoordinates.height, initialCoordinates.x, isOpeningSidebar, progress, windowWidth, ]); return ( ); } export { InlineEngagement, TooltipInlineEngagement }; diff --git a/native/themes/colors.js b/native/themes/colors.js index 5c9fbdec1..7acfa3c70 100644 --- a/native/themes/colors.js +++ b/native/themes/colors.js @@ -1,315 +1,317 @@ // @flow import * as React from 'react'; import { StyleSheet } from 'react-native'; import { createSelector } from 'reselect'; import { selectBackgroundIsDark } from '../navigation/nav-selectors.js'; import { NavContext } from '../navigation/navigation-context.js'; import { useSelector } from '../redux/redux-utils.js'; import type { AppState } from '../redux/state-types.js'; import type { GlobalTheme } from '../types/themes.js'; const light = Object.freeze({ blockQuoteBackground: '#E0E0E0', blockQuoteBorder: '#CCCCCC', codeBackground: '#E0E0E0', disabledButton: '#E0E0E0', disconnectedBarBackground: '#F5F5F5', editButton: '#A4A4A2', floatingButtonBackground: '#999999', floatingButtonLabel: '#EBEBEB', headerChevron: '#0A0A0A', inlineEngagementBackground: '#E0E0E0', inlineEngagementLabel: '#0A0A0A', link: '#7E57C2', listBackground: '#FFFFFF', listBackgroundLabel: '#0A0A0A', listBackgroundSecondaryLabel: '#444444', listBackgroundTernaryLabel: '#999999', listChatBubble: '#F1F0F5', listForegroundLabel: '#0A0A0A', listForegroundSecondaryLabel: '#333333', listForegroundTertiaryLabel: '#666666', listInputBackground: '#F5F5F5', listInputBar: '#E2E2E2', listInputButton: '#8E8D92', listIosHighlightUnderlay: '#DDDDDDDD', listSearchBackground: '#F5F5F5', listSearchIcon: '#8E8D92', listSeparatorLabel: '#666666', modalBackground: '#EBEBEB', modalBackgroundLabel: '#333333', modalBackgroundSecondaryLabel: '#AAAAAA', modalButton: '#BBBBBB', modalButtonLabel: '#0A0A0A', modalContrastBackground: '#0A0A0A', modalContrastForegroundLabel: '#FFFFFF', modalContrastOpacity: 0.7, modalForeground: '#FFFFFF', modalForegroundBorder: '#CCCCCC', modalForegroundLabel: '#0A0A0A', modalForegroundSecondaryLabel: '#888888', modalForegroundTertiaryLabel: '#AAAAAA', modalIosHighlightUnderlay: '#CCCCCCDD', modalSubtext: '#CCCCCC', modalSubtextLabel: '#666666', navigationCard: '#FFFFFF', navigationChevron: '#CCCCCC', panelBackground: '#F5F5F5', panelBackgroundLabel: '#888888', panelForeground: '#FFFFFF', panelForegroundBorder: '#CCCCCC', panelForegroundLabel: '#0A0A0A', panelForegroundSecondaryLabel: '#333333', panelForegroundTertiaryLabel: '#888888', panelIosHighlightUnderlay: '#EBEBEBDD', panelSecondaryForeground: '#F5F5F5', panelSecondaryForegroundBorder: '#CCCCCC', purpleLink: '#7E57C2', purpleButton: '#7E57C2', reactionSelectionPopoverItemBackground: '#404040', redText: '#F53100', spoiler: '#33332C', tabBarAccent: '#7E57C2', tabBarBackground: '#F5F5F5', tabBarActiveTintColor: '#7E57C2', vibrantGreenButton: '#00C853', vibrantRedButton: '#F53100', tooltipBackground: '#E0E0E0', logInSpacer: '#FFFFFF33', logInText: '#FFFFFF', siweButton: '#FFFFFF', siweButtonText: '#1F1F1F', drawerExpandButton: '#808080', drawerExpandButtonDisabled: '#CCCCCC', drawerItemLabelLevel0: '#0A0A0A', drawerItemLabelLevel1: '#0A0A0A', drawerItemLabelLevel2: '#1F1F1F', drawerOpenCommunityBackground: '#F5F5F5', drawerBackground: '#FFFFFF', subthreadsModalClose: '#808080', subthreadsModalBackground: '#EBEBEB', subthreadsModalSearch: '#00000008', + messageLabel: '#0A0A0A', }); export type Colors = $Exact; const dark: Colors = Object.freeze({ blockQuoteBackground: '#A9A9A9', blockQuoteBorder: '#808080', codeBackground: '#0A0A0A', disabledButton: '#404040', disconnectedBarBackground: '#1F1F1F', editButton: '#666666', floatingButtonBackground: '#666666', floatingButtonLabel: '#FFFFFF', headerChevron: '#FFFFFF', inlineEngagementBackground: '#666666', inlineEngagementLabel: '#FFFFFF', link: '#AE94DB', listBackground: '#0A0A0A', listBackgroundLabel: '#CCCCCC', listBackgroundSecondaryLabel: '#BBBBBB', listBackgroundTernaryLabel: '#808080', listChatBubble: '#26252A', listForegroundLabel: '#FFFFFF', listForegroundSecondaryLabel: '#CCCCCC', listForegroundTertiaryLabel: '#808080', listInputBackground: '#1F1F1F', listInputBar: '#666666', listInputButton: '#CCCCCC', listIosHighlightUnderlay: '#BBBBBB88', listSearchBackground: '#1F1F1F', listSearchIcon: '#CCCCCC', listSeparatorLabel: '#EBEBEB', modalBackground: '#0A0A0A', modalBackgroundLabel: '#CCCCCC', modalBackgroundSecondaryLabel: '#666666', modalButton: '#666666', modalButtonLabel: '#FFFFFF', modalContrastBackground: '#FFFFFF', modalContrastForegroundLabel: '#0A0A0A', modalContrastOpacity: 0.85, modalForeground: '#1F1F1F', modalForegroundBorder: '#1F1F1F', modalForegroundLabel: '#FFFFFF', modalForegroundSecondaryLabel: '#AAAAAA', modalForegroundTertiaryLabel: '#666666', modalIosHighlightUnderlay: '#AAAAAA88', modalSubtext: '#404040', modalSubtextLabel: '#AAAAAA', navigationCard: '#2A2A2A', navigationChevron: '#666666', panelBackground: '#0A0A0A', panelBackgroundLabel: '#CCCCCC', panelForeground: '#1F1F1F', panelForegroundBorder: '#2C2C2E', panelForegroundLabel: '#FFFFFF', panelForegroundSecondaryLabel: '#CCCCCC', panelForegroundTertiaryLabel: '#AAAAAA', panelIosHighlightUnderlay: '#313035', panelSecondaryForeground: '#333333', panelSecondaryForegroundBorder: '#666666', purpleLink: '#AE94DB', purpleButton: '#7E57C2', reactionSelectionPopoverItemBackground: '#404040', redText: '#F53100', spoiler: '#33332C', tabBarAccent: '#AE94DB', tabBarBackground: '#0A0A0A', tabBarActiveTintColor: '#AE94DB', vibrantGreenButton: '#00C853', vibrantRedButton: '#F53100', tooltipBackground: '#1F1F1F', logInSpacer: '#FFFFFF33', logInText: '#FFFFFF', siweButton: '#FFFFFF', siweButtonText: '#1F1F1F', drawerExpandButton: '#808080', drawerExpandButtonDisabled: '#404040', drawerItemLabelLevel0: '#CCCCCC', drawerItemLabelLevel1: '#CCCCCC', drawerItemLabelLevel2: '#F5F5F5', drawerOpenCommunityBackground: '#191919', drawerBackground: '#1F1F1F', subthreadsModalClose: '#808080', subthreadsModalBackground: '#1F1F1F', subthreadsModalSearch: '#FFFFFF04', typeaheadTooltipBackground: '#1F1F1f', typeaheadTooltipBorder: '#404040', typeaheadTooltipText: 'white', + messageLabel: '#CCCCCC', }); 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, };