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,
};