diff --git a/native/chat/chat-constants.js b/native/chat/chat-constants.js index 0a78171ab..ad12f2603 100644 --- a/native/chat/chat-constants.js +++ b/native/chat/chat-constants.js @@ -1,30 +1,29 @@ // @flow export const composedMessageStyle = { marginLeft: 12, marginRight: 7, }; export const inlineEngagementStyle = { - height: 38, marginTop: 5, marginBottom: 3, topOffset: -10, }; export const inlineEngagementCenterStyle = { topOffset: -5, }; export const inlineEngagementRightStyle = { marginLeft: 22, }; export const inlineEngagementLabelStyle = { height: 16, topOffset: 8, }; export const clusterEndHeight = 7; export const avatarOffset = 32; diff --git a/native/chat/chat-item-height-measurer.react.js b/native/chat/chat-item-height-measurer.react.js index 3cf82f3bc..6c2e66aff 100644 --- a/native/chat/chat-item-height-measurer.react.js +++ b/native/chat/chat-item-height-measurer.react.js @@ -1,212 +1,234 @@ // @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 { dummyNodeForInlineEngagementHeightMeasurement } from './inline-engagement.react.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, hasBeenEdited, threadCreatedFromMessage, reactions } = item; if (messageInfo.type === messageTypes.TEXT) { return JSON.stringify({ text: messageInfo.text, edited: getMessageLabel(hasBeenEdited, messageInfo.threadID), sidebar: getInlineEngagementSidebarText(threadCreatedFromMessage), reactions: reactionsToRawString(reactions), }); } else if (item.robotext) { const { threadID } = item.messageInfo; return JSON.stringify({ robotext: entityTextToRawString(item.robotext, { threadID }), sidebar: getInlineEngagementSidebarText(threadCreatedFromMessage), reactions: reactionsToRawString(reactions), }); + } else if (threadCreatedFromMessage || Object.keys(reactions).length > 0) { + // we enter this condition when the item is a multimedia message with an + // inline engagement + return JSON.stringify({ + 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, hasBeenEdited } = item; + const { messageInfo, hasBeenEdited, threadCreatedFromMessage, reactions } = + item; + if (messageInfo.type === messageTypes.TEXT) { const label = getMessageLabel(hasBeenEdited, messageInfo.threadID); return dummyNodeForTextMessageHeightMeasurement( messageInfo.text, label, - item.threadCreatedFromMessage, - item.reactions, + threadCreatedFromMessage, + reactions, ); } else if (item.robotext) { return dummyNodeForRobotextMessageHeightMeasurement( item.robotext, - item.messageInfo.threadID, - item.threadCreatedFromMessage, - item.reactions, + messageInfo.threadID, + threadCreatedFromMessage, + reactions, + ); + } else if (threadCreatedFromMessage || Object.keys(reactions).length > 0) { + // we enter this condition when the item is a multimedia message with an + // inline engagement + + return dummyNodeForInlineEngagementHeightMeasurement( + threadCreatedFromMessage, + reactions, ); } - invariant(false, 'NodeHeightMeasurer asked for dummy for non-text message'); + invariant( + false, + 'NodeHeightMeasurer asked for dummy for multimedia message with no inline engagement', + ); }; 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, + inlineEngagementHeight: height, ...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 9b815b040..9974f3553 100644 --- a/native/chat/inline-engagement.react.js +++ b/native/chat/inline-engagement.react.js @@ -1,550 +1,569 @@ // @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'; +function dummyNodeForInlineEngagementHeightMeasurement( + sidebarInfo: ?ThreadInfo, + reactions: ReactionInfo, +): React.Element { + return ( + + + + ); +} + 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; } return ( {label} ); }, [editedLabelStyle, label]); const unreadStyle = sidebarThreadInfo?.currentUser.unread ? styles.unread : null; const repliesStyles = React.useMemo( () => [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, 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, 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}`} ); }); }, [ 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', 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: { fontSize: 14, lineHeight: 22, }, repliesTextColor: { color: 'inlineEngagementLabel', }, unread: { color: 'listForegroundLabel', fontWeight: 'bold', }, reactionsContainer: { display: 'flex', flexDirection: 'row', alignItems: 'center', 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: { fontSize: 14, lineHeight: 22, }, reactionColor: { color: 'inlineEngagementLabel', }, 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, DummyInlineEngagementNode }; +export { + InlineEngagement, + TooltipInlineEngagement, + DummyInlineEngagementNode, + dummyNodeForInlineEngagementHeightMeasurement, +}; diff --git a/native/chat/multimedia-message-utils.js b/native/chat/multimedia-message-utils.js index 1269d2ce6..3f92c8098 100644 --- a/native/chat/multimedia-message-utils.js +++ b/native/chat/multimedia-message-utils.js @@ -1,143 +1,149 @@ // @flow import invariant from 'invariant'; import { messageKey } from 'lib/shared/message-utils.js'; import type { MediaInfo } from 'lib/types/media-types.js'; import type { MultimediaMessageInfo } from 'lib/types/message-types.js'; -import { inlineEngagementStyle, clusterEndHeight } from './chat-constants.js'; +import { clusterEndHeight } from './chat-constants.js'; import { failedSendHeight } from './failed-send.react.js'; import { authorNameHeight } from './message-header.react.js'; import type { ChatMultimediaMessageInfoItem, MultimediaContentSizes, } from '../types/chat-types.js'; const spaceBetweenImages = 4; function getMediaPerRow(mediaCount: number): number { if (mediaCount === 0) { return 0; // ??? } else if (mediaCount === 1) { return 1; } else if (mediaCount === 2) { return 2; } else if (mediaCount === 3) { return 3; } else if (mediaCount === 4) { return 2; } else { return 3; } } function multimediaMessageSendFailed( item: ChatMultimediaMessageInfoItem, ): boolean { const { messageInfo, localMessageInfo, pendingUploads } = item; const { id: serverID } = messageInfo; if (serverID !== null && serverID !== undefined) { return false; } const { isViewer } = messageInfo.creator; if (!isViewer) { return false; } if (localMessageInfo && localMessageInfo.sendFailed) { return true; } for (const media of messageInfo.media) { const pendingUpload = pendingUploads && pendingUploads[media.id]; if (pendingUpload && pendingUpload.failed) { return true; } } return !pendingUploads; } // The results are merged into ChatMultimediaMessageInfoItem function multimediaMessageContentSizes( messageInfo: MultimediaMessageInfo, composedMessageMaxWidth: number, ): MultimediaContentSizes { invariant(messageInfo.media.length > 0, 'should have media'); if (messageInfo.media.length === 1) { const [media] = messageInfo.media; const { height, width } = media.dimensions; let imageHeight = height; if (width > composedMessageMaxWidth) { imageHeight = (height * composedMessageMaxWidth) / width; } if (imageHeight < 50) { imageHeight = 50; } let contentWidth = height ? (width * imageHeight) / height : 0; if (contentWidth > composedMessageMaxWidth) { contentWidth = composedMessageMaxWidth; } return { imageHeight, contentHeight: imageHeight, contentWidth }; } const contentWidth = composedMessageMaxWidth; const mediaPerRow = getMediaPerRow(messageInfo.media.length); const marginSpace = spaceBetweenImages * (mediaPerRow - 1); const imageHeight = (contentWidth - marginSpace) / mediaPerRow; const numRows = Math.ceil(messageInfo.media.length / mediaPerRow); const contentHeight = numRows * imageHeight + (numRows - 1) * spaceBetweenImages; return { imageHeight, contentHeight, contentWidth }; } // Given a ChatMultimediaMessageInfoItem, determines exact height of row function multimediaMessageItemHeight( item: ChatMultimediaMessageInfoItem, ): number { - const { messageInfo, contentHeight, startsCluster, endsCluster } = item; + const { + messageInfo, + contentHeight, + startsCluster, + endsCluster, + inlineEngagementHeight, + } = item; + const { creator } = messageInfo; const { isViewer } = creator; - let height = 5 + contentHeight; // 5 from marginBottom in ComposedMessage + + // 5 from marginBottom in ComposedMessage + let height = 5 + contentHeight; if (!isViewer && startsCluster) { height += authorNameHeight; } if (endsCluster) { height += clusterEndHeight; } if (multimediaMessageSendFailed(item)) { height += failedSendHeight; } - if (item.threadCreatedFromMessage || Object.keys(item.reactions).length > 0) { - height += - inlineEngagementStyle.height + - inlineEngagementStyle.marginTop + - inlineEngagementStyle.marginBottom; + if (inlineEngagementHeight) { + height += inlineEngagementHeight; } return height; } function getMediaKey( item: ChatMultimediaMessageInfoItem, mediaInfo: MediaInfo, ): string { return `multimedia|${messageKey(item.messageInfo)}|${mediaInfo.index}`; } export { multimediaMessageContentSizes, multimediaMessageItemHeight, multimediaMessageSendFailed, getMediaPerRow, spaceBetweenImages, getMediaKey, }; diff --git a/native/types/chat-types.js b/native/types/chat-types.js index 74b84807e..ccfabb4b8 100644 --- a/native/types/chat-types.js +++ b/native/types/chat-types.js @@ -1,74 +1,78 @@ // @flow import type { ReactionInfo } from 'lib/selectors/chat-selectors.js'; import type { LocalMessageInfo, MultimediaMessageInfo, RobotextMessageInfo, } from 'lib/types/message-types.js'; import type { TextMessageInfo } from 'lib/types/messages/text.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import type { EntityText } from 'lib/utils/entity-text.js'; import type { MessagePendingUploads } from '../input/input-state.js'; export type ChatRobotextMessageInfoItemWithHeight = { +itemType: 'message', +messageShapeType: 'robotext', +messageInfo: RobotextMessageInfo, +threadInfo: ThreadInfo, +startsConversation: boolean, +startsCluster: boolean, +endsCluster: boolean, +robotext: EntityText, +threadCreatedFromMessage: ?ThreadInfo, +contentHeight: number, +reactions: ReactionInfo, }; export type ChatTextMessageInfoItemWithHeight = { +itemType: 'message', +messageShapeType: 'text', +messageInfo: TextMessageInfo, +localMessageInfo: ?LocalMessageInfo, +threadInfo: ThreadInfo, +startsConversation: boolean, +startsCluster: boolean, +endsCluster: boolean, +contentHeight: number, +threadCreatedFromMessage: ?ThreadInfo, +reactions: ReactionInfo, +hasBeenEdited: ?boolean, +isPinned: ?boolean, }; +// We "measure" the contentHeight of a multimedia message using the media +// dimensions. This means for multimedia messages we only need to actually +// measure the inline engagement node export type MultimediaContentSizes = { +imageHeight: number, +contentHeight: number, +contentWidth: number, }; export type ChatMultimediaMessageInfoItem = { ...MultimediaContentSizes, +itemType: 'message', +messageShapeType: 'multimedia', +messageInfo: MultimediaMessageInfo, +localMessageInfo: ?LocalMessageInfo, +threadInfo: ThreadInfo, +startsConversation: boolean, +startsCluster: boolean, +endsCluster: boolean, +threadCreatedFromMessage: ?ThreadInfo, +pendingUploads: ?MessagePendingUploads, +reactions: ReactionInfo, +hasBeenEdited: ?boolean, +isPinned: ?boolean, + +inlineEngagementHeight: ?number, }; export type ChatMessageInfoItemWithHeight = | ChatRobotextMessageInfoItemWithHeight | ChatTextMessageInfoItemWithHeight | ChatMultimediaMessageInfoItem; export type ChatMessageItemWithHeight = | { itemType: 'loader' } | ChatMessageInfoItemWithHeight;