diff --git a/native/chat/chat-item-height-measurer.react.js b/native/chat/chat-item-height-measurer.react.js
index 659af3aeb..a5d2d139d 100644
--- a/native/chat/chat-item-height-measurer.react.js
+++ b/native/chat/chat-item-height-measurer.react.js
@@ -1,190 +1,192 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
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 } = 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 }),
});
}
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;
if (messageInfo.type === messageTypes.TEXT) {
return dummyNodeForTextMessageHeightMeasurement(messageInfo.text);
} 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/inline-engagement.react.js b/native/chat/inline-engagement.react.js
index f2f90d076..9b815b040 100644
--- a/native/chat/inline-engagement.react.js
+++ b/native/chat/inline-engagement.react.js
@@ -1,422 +1,550 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import invariant from 'invariant';
import * as React from 'react';
import { Text, View } from 'react-native';
import Animated, {
Extrapolate,
interpolateNode,
} from 'react-native-reanimated';
import type { ReactionInfo } from 'lib/selectors/chat-selectors.js';
import { getInlineEngagementSidebarText } from 'lib/shared/inline-engagement-utils.js';
import { localIDPrefix } from 'lib/shared/message-utils.js';
import type { MessageInfo } from 'lib/types/message-types.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import {
inlineEngagementLabelStyle,
inlineEngagementStyle,
inlineEngagementCenterStyle,
inlineEngagementRightStyle,
composedMessageStyle,
avatarOffset,
} from './chat-constants.js';
import { useNavigateToThread } from './message-list-types.js';
import { useSendReaction } from './reaction-message-utils.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 { useSelector } from '../redux/redux-utils.js';
import { useStyles } from '../themes/colors.js';
import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js';
+type DummyInlineEngagementNodeProps = {
+ ...React.ElementConfig,
+ +editedLabel?: ?string,
+ +sidebarInfo: ?ThreadInfo,
+ +reactions: ReactionInfo,
+};
+function DummyInlineEngagementNode(
+ props: DummyInlineEngagementNodeProps,
+): React.Node {
+ const { editedLabel, sidebarInfo, reactions, ...rest } = props;
+
+ const dummyEditedLabel = React.useMemo(() => {
+ if (!editedLabel) {
+ return null;
+ }
+
+ return (
+
+
+ {editedLabel}
+
+
+ );
+ }, [editedLabel]);
+
+ const dummySidebarItem = React.useMemo(() => {
+ if (!sidebarInfo) {
+ return null;
+ }
+
+ const repliesText = getInlineEngagementSidebarText(sidebarInfo);
+ return (
+
+ {repliesText}
+
+ );
+ }, [sidebarInfo]);
+
+ const dummyReactionsList = React.useMemo(() => {
+ if (Object.keys(reactions).length === 0) {
+ return null;
+ }
+
+ return Object.keys(reactions).map(reaction => {
+ const numOfReacts = reactions[reaction].users.length;
+ return (
+
+ {`${reaction} ${numOfReacts}`}
+
+ );
+ });
+ }, [reactions]);
+
+ const dummyContainerStyle = React.useMemo(
+ () => [unboundStyles.inlineEngagement, unboundStyles.dummyInlineEngagement],
+ [],
+ );
+
+ if (!dummyEditedLabel && !dummySidebarItem && !dummyReactionsList) {
+ return null;
+ }
+
+ return (
+
+ {dummyEditedLabel}
+ {dummySidebarItem}
+ {dummyReactionsList}
+
+ );
+}
+
type Props = {
+messageInfo: MessageInfo,
+threadInfo: ThreadInfo,
+sidebarThreadInfo: ?ThreadInfo,
+reactions: ReactionInfo,
+disabled?: boolean,
+positioning?: 'left' | 'right' | 'center',
+label?: ?string,
};
function InlineEngagement(props: Props): React.Node {
const {
messageInfo,
threadInfo,
sidebarThreadInfo,
reactions,
disabled = false,
positioning,
label,
} = props;
const isLeft = positioning === 'left';
const isRight = positioning === 'right';
const isCenter = positioning === 'center';
const navigateToThread = useNavigateToThread();
const { navigate } = useNavigation();
const styles = useStyles(unboundStyles);
+ const editedLabelStyle = React.useMemo(() => {
+ const stylesResult = [styles.messageLabel, styles.messageLabelColor];
+ if (isLeft) {
+ stylesResult.push(styles.messageLabelLeft);
+ } else {
+ stylesResult.push(styles.messageLabelRight);
+ }
+
+ return stylesResult;
+ }, [
+ isLeft,
+ styles.messageLabel,
+ styles.messageLabelColor,
+ styles.messageLabelLeft,
+ styles.messageLabelRight,
+ ]);
+
const editedLabel = React.useMemo(() => {
if (!label) {
return null;
}
- const labelLeftRight = isLeft
- ? styles.messageLabelLeft
- : styles.messageLabelRight;
-
return (
- {label}
+ {label}
);
- }, [isLeft, label, styles]);
+ }, [editedLabelStyle, label]);
const unreadStyle = sidebarThreadInfo?.currentUser.unread
? styles.unread
: null;
const repliesStyles = React.useMemo(
- () => [styles.repliesText, unreadStyle],
- [styles.repliesText, unreadStyle],
+ () => [styles.repliesText, styles.repliesTextColor, unreadStyle],
+ [styles.repliesText, styles.repliesTextColor, unreadStyle],
);
const onPressSidebar = React.useCallback(() => {
if (sidebarThreadInfo && !disabled) {
navigateToThread({ threadInfo: sidebarThreadInfo });
}
}, [disabled, navigateToThread, sidebarThreadInfo]);
const repliesText = getInlineEngagementSidebarText(sidebarThreadInfo);
const sidebarStyle = React.useMemo(() => {
- const stylesResult = [styles.sidebar];
+ const stylesResult = [styles.sidebar, styles.sidebarColor];
if (Object.keys(reactions).length === 0) {
return stylesResult;
}
if (isRight) {
stylesResult.push(styles.sidebarMarginLeft);
} else {
stylesResult.push(styles.sidebarMarginRight);
}
return stylesResult;
}, [
isRight,
reactions,
styles.sidebar,
+ styles.sidebarColor,
styles.sidebarMarginLeft,
styles.sidebarMarginRight,
]);
const sidebarItem = React.useMemo(() => {
if (!sidebarThreadInfo) {
return null;
}
return (
{repliesText}
);
}, [
sidebarThreadInfo,
onPressSidebar,
sidebarStyle,
styles.icon,
repliesStyles,
repliesText,
]);
const nextLocalID = useSelector(state => state.nextLocalID);
const localID = `${localIDPrefix}${nextLocalID}`;
const sendReaction = useSendReaction(
messageInfo.id,
localID,
threadInfo.id,
reactions,
);
const onPressReaction = React.useCallback(
(reaction: string) => sendReaction(reaction),
[sendReaction],
);
const onLongPressReaction = React.useCallback(() => {
navigate<'MessageReactionsModal'>({
name: MessageReactionsModalRouteName,
params: { reactions },
});
}, [navigate, reactions]);
const reactionStyle = React.useMemo(() => {
- const stylesResult = [styles.reactionsContainer];
+ const stylesResult = [
+ styles.reactionsContainer,
+ styles.reactionsContainerColor,
+ ];
if (isRight) {
stylesResult.push(styles.reactionsContainerMarginLeft);
} else {
stylesResult.push(styles.reactionsContainerMarginRight);
}
return stylesResult;
}, [
isRight,
styles.reactionsContainer,
+ styles.reactionsContainerColor,
styles.reactionsContainerMarginLeft,
styles.reactionsContainerMarginRight,
]);
const reactionList = React.useMemo(() => {
if (Object.keys(reactions).length === 0) {
return null;
}
return Object.keys(reactions).map(reaction => {
const reactionInfo = reactions[reaction];
const numOfReacts = reactionInfo.users.length;
const style = reactionInfo.viewerReacted
? [...reactionStyle, styles.reactionsContainerSelected]
: reactionStyle;
return (
onPressReaction(reaction)}
onLongPress={onLongPressReaction}
activeOpacity={0.7}
key={reaction}
>
- {`${reaction} ${numOfReacts}`}
+ {`${reaction} ${numOfReacts}`}
);
});
}, [
onLongPressReaction,
onPressReaction,
reactionStyle,
reactions,
styles.reaction,
+ styles.reactionColor,
styles.reactionsContainerSelected,
]);
const inlineEngagementPositionStyle = React.useMemo(() => {
const styleResult = [styles.inlineEngagement];
if (isRight) {
styleResult.push(styles.rightInlineEngagement);
} else if (isCenter) {
styleResult.push(styles.centerInlineEngagement);
}
return styleResult;
}, [
isCenter,
isRight,
styles.centerInlineEngagement,
styles.inlineEngagement,
styles.rightInlineEngagement,
]);
return (
{editedLabel}
{sidebarItem}
{reactionList}
);
}
const unboundStyles = {
inlineEngagement: {
flexDirection: 'row',
marginBottom: inlineEngagementStyle.marginBottom,
marginLeft: avatarOffset,
flexWrap: 'wrap',
top: inlineEngagementStyle.topOffset,
},
+ dummyInlineEngagement: {
+ marginRight: 8,
+ },
centerInlineEngagement: {
marginLeft: 20,
marginRight: 20,
justifyContent: 'center',
},
rightInlineEngagement: {
flexDirection: 'row-reverse',
marginLeft: inlineEngagementRightStyle.marginLeft,
},
sidebar: {
flexDirection: 'row',
alignItems: 'center',
- backgroundColor: 'inlineEngagementBackground',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 8,
marginTop: inlineEngagementStyle.marginTop,
},
+ dummySidebar: {
+ paddingRight: 8,
+ // 14 (icon) + 4 (marginRight of icon) + 8 (original left padding)
+ paddingLeft: 26,
+ marginRight: 4,
+ },
+ sidebarColor: {
+ backgroundColor: 'inlineEngagementBackground',
+ },
sidebarMarginLeft: {
marginLeft: 4,
},
sidebarMarginRight: {
marginRight: 4,
},
icon: {
color: 'inlineEngagementLabel',
marginRight: 4,
},
repliesText: {
- color: 'inlineEngagementLabel',
fontSize: 14,
lineHeight: 22,
},
+ repliesTextColor: {
+ color: 'inlineEngagementLabel',
+ },
unread: {
color: 'listForegroundLabel',
fontWeight: 'bold',
},
reactionsContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
- backgroundColor: 'inlineEngagementBackground',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 8,
marginTop: inlineEngagementStyle.marginTop,
},
+ dummyReactionContainer: {
+ marginRight: 4,
+ },
+ reactionsContainerColor: {
+ backgroundColor: 'inlineEngagementBackground',
+ },
reactionsContainerSelected: {
borderWidth: 1,
borderColor: 'inlineEngagementLabel',
paddingHorizontal: 7,
paddingVertical: 3,
},
reactionsContainerMarginLeft: {
marginLeft: 4,
},
reactionsContainerMarginRight: {
marginRight: 4,
},
reaction: {
- color: 'inlineEngagementLabel',
fontSize: 14,
lineHeight: 22,
},
+ reactionColor: {
+ color: 'inlineEngagementLabel',
+ },
messageLabel: {
- color: 'messageLabel',
paddingHorizontal: 3,
fontSize: 13,
top: inlineEngagementLabelStyle.topOffset,
height: inlineEngagementLabelStyle.height,
marginTop: inlineEngagementStyle.marginTop,
},
+ dummyMessageLabel: {
+ marginLeft: 9,
+ marginRight: 4,
+ },
+ messageLabelColor: {
+ color: 'messageLabel',
+ },
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;
// ESLint doesn't recognize that invariant always throws
// eslint-disable-next-line consistent-return
const inlineEngagementStyles = React.useMemo(() => {
if (positioning === 'left') {
return {
position: 'absolute',
top: inlineEngagementStyle.marginTop + inlineEngagementStyle.topOffset,
left: composedMessageStyle.marginLeft,
};
} else if (positioning === 'right') {
return {
position: 'absolute',
right:
inlineEngagementRightStyle.marginLeft +
composedMessageStyle.marginRight,
top: inlineEngagementStyle.marginTop + inlineEngagementStyle.topOffset,
};
} else if (positioning === 'center') {
return {
alignSelf: 'center',
top: inlineEngagementCenterStyle.topOffset,
};
}
invariant(
false,
`${positioning} is not a valid positioning value for InlineEngagement`,
);
}, [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 };
+export { InlineEngagement, TooltipInlineEngagement, DummyInlineEngagementNode };
diff --git a/native/chat/inner-robotext-message.react.js b/native/chat/inner-robotext-message.react.js
index 19f38aafb..38b9b6eac 100644
--- a/native/chat/inner-robotext-message.react.js
+++ b/native/chat/inner-robotext-message.react.js
@@ -1,146 +1,157 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import { Text, TouchableWithoutFeedback, View } from 'react-native';
+import type { ReactionInfo } from 'lib/selectors/chat-selectors.js';
import { threadInfoSelector } from 'lib/selectors/thread-selectors.js';
+import type { ThreadInfo } from 'lib/types/thread-types.js';
import {
entityTextToReact,
entityTextToRawString,
useENSNamesForEntityText,
type EntityText,
} from 'lib/utils/entity-text.js';
+import { DummyInlineEngagementNode } from './inline-engagement.react.js';
import { useNavigateToThread } from './message-list-types.js';
import Markdown from '../markdown/markdown.react.js';
import { inlineMarkdownRules } from '../markdown/rules.react.js';
import { useSelector } from '../redux/redux-utils.js';
import { useOverlayStyles } from '../themes/colors.js';
import type { ChatRobotextMessageInfoItemWithHeight } from '../types/chat-types.js';
function dummyNodeForRobotextMessageHeightMeasurement(
robotext: EntityText,
threadID: string,
+ sidebarInfo: ?ThreadInfo,
+ reactions: ReactionInfo,
): React.Element {
return (
-
-
- {entityTextToRawString(robotext, { threadID })}
-
+
+
+
+ {entityTextToRawString(robotext, { threadID })}
+
+
+
);
}
type InnerRobotextMessageProps = {
+item: ChatRobotextMessageInfoItemWithHeight,
+onPress: () => void,
+onLongPress?: () => void,
};
function InnerRobotextMessage(props: InnerRobotextMessageProps): React.Node {
const { item, onLongPress, onPress } = props;
const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme);
const styles = useOverlayStyles(unboundStyles);
const { messageInfo, robotext } = item;
const { threadID } = messageInfo;
const robotextWithENSNames = useENSNamesForEntityText(robotext);
invariant(
robotextWithENSNames,
'useENSNamesForEntityText only returns falsey when passed falsey',
);
const textParts = React.useMemo(() => {
const darkColor = activeTheme === 'dark';
return entityTextToReact(robotextWithENSNames, threadID, {
// eslint-disable-next-line react/display-name
renderText: ({ text }) => (
{text}
),
// eslint-disable-next-line react/display-name
renderThread: ({ id, name }) => ,
// eslint-disable-next-line react/display-name
renderColor: ({ hex }) => ,
});
}, [robotextWithENSNames, activeTheme, threadID, styles.robotext]);
const viewStyle = [styles.robotextContainer];
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
viewStyle.push({ height: item.contentHeight });
}
return (
{textParts}
);
}
type ThreadEntityProps = {
+id: string,
+name: string,
};
function ThreadEntity(props: ThreadEntityProps) {
const threadID = props.id;
const threadInfo = useSelector(state => threadInfoSelector(state)[threadID]);
const styles = useOverlayStyles(unboundStyles);
const navigateToThread = useNavigateToThread();
const onPressThread = React.useCallback(() => {
invariant(threadInfo, 'onPressThread should have threadInfo');
navigateToThread({ threadInfo });
}, [threadInfo, navigateToThread]);
if (!threadInfo) {
return {props.name};
}
return (
{props.name}
);
}
function ColorEntity(props: { +color: string }) {
const colorStyle = { color: props.color };
return {props.color};
}
const unboundStyles = {
link: {
color: 'link',
},
robotextContainer: {
paddingTop: 6,
paddingBottom: 11,
paddingHorizontal: 24,
},
robotext: {
color: 'listForegroundSecondaryLabel',
fontFamily: 'Arial',
fontSize: 15,
textAlign: 'center',
},
dummyRobotext: {
fontFamily: 'Arial',
fontSize: 15,
textAlign: 'center',
},
};
const MemoizedInnerRobotextMessage: React.ComponentType =
React.memo(InnerRobotextMessage);
export {
dummyNodeForRobotextMessageHeightMeasurement,
MemoizedInnerRobotextMessage as InnerRobotextMessage,
};
diff --git a/native/chat/utils.js b/native/chat/utils.js
index a19d6a72d..6282f9acd 100644
--- a/native/chat/utils.js
+++ b/native/chat/utils.js
@@ -1,463 +1,453 @@
// @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 { 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,
- ChatRobotextMessageInfoItemWithHeight,
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 { 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);
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 robotextMessageItemHeight(
- item: ChatRobotextMessageInfoItemWithHeight,
-): number {
- if (item.threadCreatedFromMessage || Object.keys(item.reactions).length > 0) {
- return item.contentHeight + inlineEngagementStyle.height;
- }
- return item.contentHeight;
-}
-
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 += robotextMessageItemHeight(item);
+ 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,
};