diff --git a/native/chat/chat-item-height-measurer.react.js b/native/chat/chat-item-height-measurer.react.js
index e616b8cac..b401f8579 100644
--- a/native/chat/chat-item-height-measurer.react.js
+++ b/native/chat/chat-item-height-measurer.react.js
@@ -1,199 +1,206 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
+import { getMessageLabel } from 'lib/shared/edit-messages-utils.js';
import {
getInlineEngagementSidebarText,
reactionsToRawString,
} from 'lib/shared/inline-engagement-utils.js';
import { messageID } from 'lib/shared/message-utils.js';
import {
messageTypes,
type MessageType,
} from 'lib/types/message-types-enum.js';
import { entityTextToRawString } from 'lib/utils/entity-text.js';
import type { MeasurementTask } from './chat-context-provider.react.js';
import { useComposedMessageMaxWidth } from './composed-message-width.js';
import { dummyNodeForRobotextMessageHeightMeasurement } from './inner-robotext-message.react.js';
import { dummyNodeForTextMessageHeightMeasurement } from './inner-text-message.react.js';
import type { NativeChatMessageItem } from './message-data.react.js';
import { MessageListContextProvider } from './message-list-types.js';
import { multimediaMessageContentSizes } from './multimedia-message-utils.js';
import { chatMessageItemKey } from './utils.js';
import NodeHeightMeasurer from '../components/node-height-measurer.react.js';
import { InputStateContext } from '../input/input-state.js';
type Props = {
+measurement: MeasurementTask,
};
const heightMeasurerKey = (item: NativeChatMessageItem) => {
if (item.itemType !== 'message') {
return null;
}
const { messageInfo, threadCreatedFromMessage, reactions } = item;
if (messageInfo.type === messageTypes.TEXT) {
return JSON.stringify({ text: messageInfo.text });
} else if (item.robotext) {
const { threadID } = item.messageInfo;
return JSON.stringify({
robotext: entityTextToRawString(item.robotext, { threadID }),
sidebar: getInlineEngagementSidebarText(threadCreatedFromMessage),
reactions: reactionsToRawString(reactions),
});
}
return null;
};
// ESLint doesn't recognize that invariant always throws
// eslint-disable-next-line consistent-return
const heightMeasurerDummy = (item: NativeChatMessageItem) => {
invariant(
item.itemType === 'message',
'NodeHeightMeasurer asked for dummy for non-message item',
);
- const { messageInfo } = item;
+ const { messageInfo, hasBeenEdited } = item;
if (messageInfo.type === messageTypes.TEXT) {
- return dummyNodeForTextMessageHeightMeasurement(messageInfo.text);
+ const label = getMessageLabel(hasBeenEdited, messageInfo.threadID);
+ return dummyNodeForTextMessageHeightMeasurement(
+ messageInfo.text,
+ label,
+ item.threadCreatedFromMessage,
+ item.reactions,
+ );
} else if (item.robotext) {
return dummyNodeForRobotextMessageHeightMeasurement(
item.robotext,
item.messageInfo.threadID,
item.threadCreatedFromMessage,
item.reactions,
);
}
invariant(false, 'NodeHeightMeasurer asked for dummy for non-text message');
};
function ChatItemHeightMeasurer(props: Props) {
const composedMessageMaxWidth = useComposedMessageMaxWidth();
const inputState = React.useContext(InputStateContext);
const inputStatePendingUploads = inputState?.pendingUploads;
const { measurement } = props;
const { threadInfo } = measurement;
const heightMeasurerMergeItem = React.useCallback(
(item: NativeChatMessageItem, height: ?number) => {
if (item.itemType !== 'message') {
return item;
}
const { messageInfo } = item;
const messageType: MessageType = messageInfo.type;
invariant(
messageType !== messageTypes.SIDEBAR_SOURCE,
'Sidebar source messages should be replaced by sourceMessage before being measured',
);
if (
messageInfo.type === messageTypes.IMAGES ||
messageInfo.type === messageTypes.MULTIMEDIA
) {
// Conditional due to Flow...
const localMessageInfo = item.localMessageInfo
? item.localMessageInfo
: null;
const id = messageID(messageInfo);
const pendingUploads = inputStatePendingUploads?.[id];
const sizes = multimediaMessageContentSizes(
messageInfo,
composedMessageMaxWidth,
);
return {
itemType: 'message',
messageShapeType: 'multimedia',
messageInfo,
localMessageInfo,
threadInfo,
startsConversation: item.startsConversation,
startsCluster: item.startsCluster,
endsCluster: item.endsCluster,
threadCreatedFromMessage: item.threadCreatedFromMessage,
pendingUploads,
reactions: item.reactions,
hasBeenEdited: item.hasBeenEdited,
isPinned: item.isPinned,
...sizes,
};
}
invariant(
height !== null && height !== undefined,
'height should be set',
);
if (messageInfo.type === messageTypes.TEXT) {
// Conditional due to Flow...
const localMessageInfo = item.localMessageInfo
? item.localMessageInfo
: null;
return {
itemType: 'message',
messageShapeType: 'text',
messageInfo,
localMessageInfo,
threadInfo,
startsConversation: item.startsConversation,
startsCluster: item.startsCluster,
endsCluster: item.endsCluster,
threadCreatedFromMessage: item.threadCreatedFromMessage,
contentHeight: height,
reactions: item.reactions,
hasBeenEdited: item.hasBeenEdited,
isPinned: item.isPinned,
};
}
invariant(
item.messageInfoType !== 'composable',
'ChatItemHeightMeasurer was handed a messageInfoType=composable, but ' +
`does not know how to handle MessageType ${messageInfo.type}`,
);
invariant(
item.messageInfoType === 'robotext',
'ChatItemHeightMeasurer was handed a messageInfoType that it does ' +
`not recognize: ${item.messageInfoType}`,
);
return {
itemType: 'message',
messageShapeType: 'robotext',
messageInfo,
threadInfo,
startsConversation: item.startsConversation,
startsCluster: item.startsCluster,
endsCluster: item.endsCluster,
threadCreatedFromMessage: item.threadCreatedFromMessage,
robotext: item.robotext,
contentHeight: height,
reactions: item.reactions,
};
},
[composedMessageMaxWidth, inputStatePendingUploads, threadInfo],
);
return (
);
}
const MemoizedChatItemHeightMeasurer: React.ComponentType =
React.memo(ChatItemHeightMeasurer);
export default MemoizedChatItemHeightMeasurer;
diff --git a/native/chat/inner-text-message.react.js b/native/chat/inner-text-message.react.js
index 7af7d4170..7b73bec8a 100644
--- a/native/chat/inner-text-message.react.js
+++ b/native/chat/inner-text-message.react.js
@@ -1,178 +1,193 @@
// @flow
import * as React from 'react';
import { View, StyleSheet, TouchableWithoutFeedback } from 'react-native';
import Animated from 'react-native-reanimated';
+import type { ReactionInfo } from 'lib/selectors/chat-selectors.js';
import { colorIsDark } from 'lib/shared/color-utils.js';
+import type { ThreadInfo } from 'lib/types/thread-types.js';
import { useComposedMessageMaxWidth } from './composed-message-width.js';
+import { DummyInlineEngagementNode } from './inline-engagement.react.js';
import { useTextMessageMarkdownRules } from './message-list-types.js';
import {
allCorners,
filterCorners,
getRoundedContainerStyle,
} from './rounded-corners.js';
import {
TextMessageMarkdownContext,
useTextMessageMarkdown,
} from './text-message-markdown-context.js';
import GestureTouchableOpacity from '../components/gesture-touchable-opacity.react.js';
import Markdown from '../markdown/markdown.react.js';
import { useSelector } from '../redux/redux-utils.js';
import { useColors, colors } from '../themes/colors.js';
import type { ChatTextMessageInfoItemWithHeight } from '../types/chat-types.js';
/* eslint-disable import/no-named-as-default-member */
const { Node } = Animated;
/* eslint-enable import/no-named-as-default-member */
function dummyNodeForTextMessageHeightMeasurement(
text: string,
-): React.Element {
- return {text};
+ editedLabel?: ?string,
+ sidebarInfo: ?ThreadInfo,
+ reactions: ReactionInfo,
+): React.Element {
+ return (
+
+ {text}
+
+
+ );
}
type DummyTextNodeProps = {
...React.ElementConfig,
+children: string,
};
function DummyTextNode(props: DummyTextNodeProps): React.Node {
const { children, style, ...rest } = props;
const maxWidth = useComposedMessageMaxWidth();
const viewStyle = [props.style, styles.dummyMessage, { maxWidth }];
const rules = useTextMessageMarkdownRules(false);
return (
{children}
);
}
type Props = {
+item: ChatTextMessageInfoItemWithHeight,
+onPress: () => void,
+messageRef?: (message: ?React.ElementRef) => void,
+threadColorOverride?: ?Node,
+isThreadColorDarkOverride?: ?boolean,
};
function InnerTextMessage(props: Props): React.Node {
const { item } = props;
const { text, creator } = item.messageInfo;
const { isViewer } = creator;
const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme);
const boundColors = useColors();
const messageStyle = {};
let darkColor;
if (isViewer) {
const threadColor = item.threadInfo.color;
messageStyle.backgroundColor =
props.threadColorOverride ?? `#${threadColor}`;
darkColor = props.isThreadColorDarkOverride ?? colorIsDark(threadColor);
} else {
messageStyle.backgroundColor = boundColors.listChatBubble;
darkColor = activeTheme === 'dark';
}
const cornerStyle = getRoundedContainerStyle(filterCorners(allCorners, item));
if (!__DEV__) {
// We don't force view height in dev mode because we
// want to measure it in Message to see if it's correct
messageStyle.height = item.contentHeight;
}
const rules = useTextMessageMarkdownRules(darkColor);
const textMessageMarkdown = useTextMessageMarkdown(item.messageInfo);
const markdownStyles = React.useMemo(() => {
const textStyle = {
color: darkColor
? colors.dark.listForegroundLabel
: colors.light.listForegroundLabel,
};
return [styles.text, textStyle];
}, [darkColor]);
// If we need to render a Text with an onPress prop inside, we're going to
// have an issue: the GestureTouchableOpacity below will trigger too when the
// the onPress is pressed. We have to use a GestureTouchableOpacity in order
// for the message touch gesture to play nice with the message swipe gesture,
// so we need to find a way to disable the GestureTouchableOpacity.
//
// Our solution is to keep using the GestureTouchableOpacity for the padding
// around the text, and to have the Texts inside ALL implement an onPress prop
// that will default to the message touch gesture. Luckily, Text with onPress
// plays nice with the message swipe gesture.
let secondMessage;
if (textMessageMarkdown.markdownHasPressable) {
secondMessage = (
{text}
);
}
const message = (
{text}
{secondMessage}
);
// We need to set onLayout in order to allow .measure() to be on the ref
const onLayout = React.useCallback(() => {}, []);
const { messageRef } = props;
if (!messageRef) {
return message;
}
return (
{message}
);
}
const styles = StyleSheet.create({
dummyMessage: {
paddingHorizontal: 12,
paddingVertical: 6,
},
message: {
overflow: 'hidden',
paddingHorizontal: 12,
paddingVertical: 6,
},
text: {
fontFamily: 'Arial',
fontSize: 18,
},
});
export { InnerTextMessage, dummyNodeForTextMessageHeightMeasurement };
diff --git a/native/chat/utils.js b/native/chat/utils.js
index a92a5df9c..8a8061d5a 100644
--- a/native/chat/utils.js
+++ b/native/chat/utils.js
@@ -1,453 +1,436 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import Animated from 'react-native-reanimated';
import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js';
import { colorIsDark } from 'lib/shared/color-utils.js';
-import { getMessageLabel } from 'lib/shared/edit-messages-utils.js';
import { messageKey } from 'lib/shared/message-utils.js';
import { viewerIsMember } from 'lib/shared/thread-utils.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
-import {
- inlineEngagementLabelStyle,
- clusterEndHeight,
- inlineEngagementStyle,
-} from './chat-constants.js';
+import { clusterEndHeight } from './chat-constants.js';
import { ChatContext, useHeightMeasurer } from './chat-context.js';
import { failedSendHeight } from './failed-send.react.js';
import {
useNativeMessageListData,
type NativeChatMessageItem,
} from './message-data.react.js';
import { authorNameHeight } from './message-header.react.js';
import { multimediaMessageItemHeight } from './multimedia-message-utils.js';
import { getUnresolvedSidebarThreadInfo } from './sidebar-navigation.js';
import textMessageSendFailed from './text-message-send-failed.js';
import { timestampHeight } from './timestamp.react.js';
import { KeyboardContext } from '../keyboard/keyboard-state.js';
import { OverlayContext } from '../navigation/overlay-context.js';
import {
MultimediaMessageTooltipModalRouteName,
RobotextMessageTooltipModalRouteName,
TextMessageTooltipModalRouteName,
} from '../navigation/route-names.js';
import type {
ChatMessageInfoItemWithHeight,
ChatMessageItemWithHeight,
ChatTextMessageInfoItemWithHeight,
} from '../types/chat-types.js';
import type {
LayoutCoordinates,
VerticalBounds,
} from '../types/layout-types.js';
import type { AnimatedViewStyle } from '../types/styles.js';
/* eslint-disable import/no-named-as-default-member */
const {
Node,
Extrapolate,
interpolateNode,
interpolateColors,
block,
call,
eq,
cond,
sub,
} = Animated;
/* eslint-enable import/no-named-as-default-member */
function textMessageItemHeight(
item: ChatTextMessageInfoItemWithHeight,
): number {
- const { messageInfo, contentHeight, startsCluster, endsCluster, threadInfo } =
- item;
+ const { messageInfo, contentHeight, startsCluster, endsCluster } = item;
const { isViewer } = messageInfo.creator;
let height = 5 + contentHeight; // 5 from marginBottom in ComposedMessage
if (!isViewer && startsCluster) {
height += authorNameHeight;
}
if (endsCluster) {
height += clusterEndHeight;
}
if (textMessageSendFailed(item)) {
height += failedSendHeight;
}
- const label = getMessageLabel(item.hasBeenEdited, threadInfo.id);
- if (item.threadCreatedFromMessage || Object.keys(item.reactions).length > 0) {
- height +=
- inlineEngagementStyle.height +
- inlineEngagementStyle.marginTop +
- inlineEngagementStyle.marginBottom;
- } else if (label) {
- height +=
- inlineEngagementLabelStyle.height +
- inlineEngagementStyle.marginTop +
- inlineEngagementStyle.marginBottom;
- }
+
return height;
}
function messageItemHeight(item: ChatMessageInfoItemWithHeight): number {
let height = 0;
if (item.messageShapeType === 'text') {
height += textMessageItemHeight(item);
} else if (item.messageShapeType === 'multimedia') {
height += multimediaMessageItemHeight(item);
} else {
height += item.contentHeight;
}
if (item.startsConversation) {
height += timestampHeight;
}
return height;
}
function chatMessageItemHeight(item: ChatMessageItemWithHeight): number {
if (item.itemType === 'loader') {
return 56;
}
return messageItemHeight(item);
}
function useMessageTargetParameters(
sourceMessage: ChatMessageInfoItemWithHeight,
initialCoordinates: LayoutCoordinates,
messageListVerticalBounds: VerticalBounds,
currentInputBarHeight: number,
targetInputBarHeight: number,
sidebarThreadInfo: ?ThreadInfo,
): {
+position: number,
+color: string,
} {
const messageListData = useNativeMessageListData({
searching: false,
userInfoInputArray: [],
threadInfo: sidebarThreadInfo,
});
const [messagesWithHeight, setMessagesWithHeight] =
React.useState$ReadOnlyArray>(null);
const measureMessages = useHeightMeasurer();
React.useEffect(() => {
if (messageListData) {
measureMessages(
messageListData,
sidebarThreadInfo,
setMessagesWithHeight,
);
}
}, [measureMessages, messageListData, sidebarThreadInfo]);
const sourceMessageID = sourceMessage.messageInfo?.id;
const targetDistanceFromBottom = React.useMemo(() => {
if (!messagesWithHeight) {
return 0;
}
let offset = 0;
for (const message of messagesWithHeight) {
offset += chatMessageItemHeight(message);
if (message.messageInfo && message.messageInfo.id === sourceMessageID) {
return offset;
}
}
return (
messageListVerticalBounds.height + chatMessageItemHeight(sourceMessage)
);
}, [
messageListVerticalBounds.height,
messagesWithHeight,
sourceMessage,
sourceMessageID,
]);
if (!sidebarThreadInfo) {
return {
position: 0,
color: sourceMessage.threadInfo.color,
};
}
const authorNameComponentHeight = sourceMessage.messageInfo.creator.isViewer
? 0
: authorNameHeight;
const currentDistanceFromBottom =
messageListVerticalBounds.height +
messageListVerticalBounds.y -
initialCoordinates.y +
timestampHeight +
authorNameComponentHeight +
currentInputBarHeight;
return {
position:
targetDistanceFromBottom +
targetInputBarHeight -
currentDistanceFromBottom,
color: sidebarThreadInfo.color,
};
}
type AnimatedMessageArgs = {
+sourceMessage: ChatMessageInfoItemWithHeight,
+initialCoordinates: LayoutCoordinates,
+messageListVerticalBounds: VerticalBounds,
+progress: Node,
+targetInputBarHeight: ?number,
};
function useAnimatedMessageTooltipButton({
sourceMessage,
initialCoordinates,
messageListVerticalBounds,
progress,
targetInputBarHeight,
}: AnimatedMessageArgs): {
+style: AnimatedViewStyle,
+threadColorOverride: ?Node,
+isThreadColorDarkOverride: ?boolean,
} {
const chatContext = React.useContext(ChatContext);
invariant(chatContext, 'chatContext should be set');
const {
currentTransitionSidebarSourceID,
setCurrentTransitionSidebarSourceID,
chatInputBarHeights,
sidebarAnimationType,
setSidebarAnimationType,
} = chatContext;
const loggedInUserInfo = useLoggedInUserInfo();
const sidebarThreadInfo = React.useMemo(
() => getUnresolvedSidebarThreadInfo({ sourceMessage, loggedInUserInfo }),
[sourceMessage, loggedInUserInfo],
);
const currentInputBarHeight =
chatInputBarHeights.get(sourceMessage.threadInfo.id) ?? 0;
const keyboardState = React.useContext(KeyboardContext);
const newSidebarAnimationType =
!currentInputBarHeight ||
!targetInputBarHeight ||
keyboardState?.keyboardShowing ||
!viewerIsMember(sidebarThreadInfo)
? 'fade_source_message'
: 'move_source_message';
React.useEffect(() => {
setSidebarAnimationType(newSidebarAnimationType);
}, [setSidebarAnimationType, newSidebarAnimationType]);
const { position: targetPosition, color: targetColor } =
useMessageTargetParameters(
sourceMessage,
initialCoordinates,
messageListVerticalBounds,
currentInputBarHeight,
targetInputBarHeight ?? currentInputBarHeight,
sidebarThreadInfo,
);
React.useEffect(() => {
return () => setCurrentTransitionSidebarSourceID(null);
}, [setCurrentTransitionSidebarSourceID]);
const bottom = React.useMemo(
() =>
interpolateNode(progress, {
inputRange: [0.3, 1],
outputRange: [targetPosition, 0],
extrapolate: Extrapolate.CLAMP,
}),
[progress, targetPosition],
);
const [isThreadColorDarkOverride, setThreadColorDarkOverride] =
React.useState(null);
const setThreadColorBrightness = React.useCallback(() => {
const isSourceThreadDark = colorIsDark(sourceMessage.threadInfo.color);
const isTargetThreadDark = colorIsDark(targetColor);
if (isSourceThreadDark !== isTargetThreadDark) {
setThreadColorDarkOverride(isTargetThreadDark);
}
}, [sourceMessage.threadInfo.color, targetColor]);
const threadColorOverride = React.useMemo(() => {
if (
sourceMessage.messageShapeType !== 'text' ||
!currentTransitionSidebarSourceID
) {
return null;
}
return block([
cond(eq(progress, 1), call([], setThreadColorBrightness)),
interpolateColors(progress, {
inputRange: [0, 1],
outputColorRange: [
`#${targetColor}`,
`#${sourceMessage.threadInfo.color}`,
],
}),
]);
}, [
currentTransitionSidebarSourceID,
progress,
setThreadColorBrightness,
sourceMessage.messageShapeType,
sourceMessage.threadInfo.color,
targetColor,
]);
const messageContainerStyle = React.useMemo(() => {
return {
bottom: currentTransitionSidebarSourceID ? bottom : 0,
opacity:
currentTransitionSidebarSourceID &&
sidebarAnimationType === 'fade_source_message'
? 0
: 1,
};
}, [bottom, currentTransitionSidebarSourceID, sidebarAnimationType]);
return {
style: messageContainerStyle,
threadColorOverride,
isThreadColorDarkOverride,
};
}
function getMessageTooltipKey(item: ChatMessageInfoItemWithHeight): string {
return `tooltip|${messageKey(item.messageInfo)}`;
}
function isMessageTooltipKey(key: string): boolean {
return key.startsWith('tooltip|');
}
function useOverlayPosition(item: ChatMessageInfoItemWithHeight) {
const overlayContext = React.useContext(OverlayContext);
invariant(overlayContext, 'should be set');
for (const overlay of overlayContext.visibleOverlays) {
if (
(overlay.routeName === MultimediaMessageTooltipModalRouteName ||
overlay.routeName === TextMessageTooltipModalRouteName ||
overlay.routeName === RobotextMessageTooltipModalRouteName) &&
overlay.routeKey === getMessageTooltipKey(item)
) {
return overlay.position;
}
}
return undefined;
}
function useContentAndHeaderOpacity(
item: ChatMessageInfoItemWithHeight,
): number | Node {
const overlayPosition = useOverlayPosition(item);
const chatContext = React.useContext(ChatContext);
return React.useMemo(
() =>
overlayPosition &&
chatContext?.sidebarAnimationType === 'move_source_message'
? sub(
1,
interpolateNode(overlayPosition, {
inputRange: [0.05, 0.06],
outputRange: [0, 1],
extrapolate: Extrapolate.CLAMP,
}),
)
: 1,
[chatContext?.sidebarAnimationType, overlayPosition],
);
}
function useDeliveryIconOpacity(
item: ChatMessageInfoItemWithHeight,
): number | Node {
const overlayPosition = useOverlayPosition(item);
const chatContext = React.useContext(ChatContext);
return React.useMemo(() => {
if (
!overlayPosition ||
!chatContext?.currentTransitionSidebarSourceID ||
chatContext?.sidebarAnimationType === 'fade_source_message'
) {
return 1;
}
return interpolateNode(overlayPosition, {
inputRange: [0.05, 0.06, 1],
outputRange: [1, 0, 0],
extrapolate: Extrapolate.CLAMP,
});
}, [
chatContext?.currentTransitionSidebarSourceID,
chatContext?.sidebarAnimationType,
overlayPosition,
]);
}
function chatMessageItemKey(
item: ChatMessageItemWithHeight | NativeChatMessageItem,
): string {
if (item.itemType === 'loader') {
return 'loader';
}
return messageKey(item.messageInfo);
}
function modifyItemForResultScreen(
item: ChatMessageInfoItemWithHeight,
): ChatMessageInfoItemWithHeight {
if (item.messageShapeType === 'robotext') {
return item;
}
if (item.messageShapeType === 'multimedia') {
return {
...item,
startsConversation: false,
startsCluster: true,
endsCluster: true,
messageInfo: {
...item.messageInfo,
creator: {
...item.messageInfo.creator,
isViewer: false,
},
},
};
}
return {
...item,
startsConversation: false,
startsCluster: true,
endsCluster: true,
messageInfo: {
...item.messageInfo,
creator: {
...item.messageInfo.creator,
isViewer: false,
},
},
};
}
export {
chatMessageItemKey,
chatMessageItemHeight,
useAnimatedMessageTooltipButton,
messageItemHeight,
getMessageTooltipKey,
isMessageTooltipKey,
useContentAndHeaderOpacity,
useDeliveryIconOpacity,
modifyItemForResultScreen,
};