diff --git a/lib/selectors/chat-selectors.js b/lib/selectors/chat-selectors.js --- a/lib/selectors/chat-selectors.js +++ b/lib/selectors/chat-selectors.js @@ -275,7 +275,7 @@ export type RobotextChatMessageInfoItem = { +itemType: 'message', +messageInfoType: 'robotext', - +messageInfo: RobotextMessageInfo, + +messageInfos: $ReadOnlyArray, +startsConversation: boolean, +startsCluster: boolean, endsCluster: boolean, @@ -549,7 +549,7 @@ chatMessageItems.push({ itemType: 'message', messageInfoType: 'robotext', - messageInfo: originalMessageInfo, + messageInfos: [originalMessageInfo], startsConversation, startsCluster, endsCluster: false, diff --git a/lib/shared/chat-message-item-utils.js b/lib/shared/chat-message-item-utils.js --- a/lib/shared/chat-message-item-utils.js +++ b/lib/shared/chat-message-item-utils.js @@ -14,12 +14,19 @@ // This complicated type matches both ChatMessageItem and // ChatMessageItemWithHeight, and is a disjoint union of types -type BaseChatMessageInfoItem = { - +itemType: 'message', - +messageInfo: ChatMessageItemMessageInfo, - +messageInfos?: ?void, - ... -}; +type BaseChatMessageInfoItem = + | { + +itemType: 'message', + +messageInfo: ChatMessageItemMessageInfo, + +messageInfos?: ?void, + ... + } + | { + +itemType: 'message', + +messageInfos: $ReadOnlyArray, + +messageInfo?: ?void, + ... + }; type BaseChatMessageItem = | BaseChatMessageInfoItem | { @@ -33,26 +40,49 @@ if (item.itemType === 'loader') { return 'loader'; } - return messageKey(item.messageInfo); + if (item.messageInfo) { + return messageKey(item.messageInfo); + } + return item.messageInfos.map(messageKey).join('^'); } function chatMessageInfoItemTimestamp(item: BaseChatMessageInfoItem): string { - return longAbsoluteDate(item.messageInfo.time); + // If there's an array of messageInfos, we expect at least one, + // and the most recent should be first + const messageInfo = item.messageInfo + ? item.messageInfo + : item.messageInfos[0]; + return longAbsoluteDate(messageInfo.time); } +// If the ChatMessageInfoItem can be the target of operations like sidebar +// creation, reaction, or pinning, then this function returns the RawMessageInfo +// that would be the target. Currently, the only reason that a +// ChatMessageInfoItem can't be such a target would be if it's a combined +// RobotextChatMessageInfoItem that has multiple messageInfos. function chatMessageItemEngagementTargetMessageInfo( item: BaseChatMessageInfoItem, -): ComposableMessageInfo | RobotextMessageInfo { - return item.messageInfo; +): ?ComposableMessageInfo | RobotextMessageInfo { + if (item.messageInfo) { + return item.messageInfo; + } else if (item.messageInfos && item.messageInfos.length === 1) { + return item.messageInfos[0]; + } + return null; } function chatMessageItemHasNonViewerMessage( item: BaseChatMessageItem, viewerID: ?string, ): boolean { - return ( - item.itemType === 'message' && item.messageInfo.creator.id !== viewerID - ); + if (item.messageInfo) { + return item.messageInfo.creator.id !== viewerID; + } else if (item.messageInfos) { + return item.messageInfos.some( + messageInfo => messageInfo.creator.id !== viewerID, + ); + } + return false; } type BaseChatMessageItemForEngagementCheck = { diff --git a/lib/shared/edit-messages-utils.js b/lib/shared/edit-messages-utils.js --- a/lib/shared/edit-messages-utils.js +++ b/lib/shared/edit-messages-utils.js @@ -100,7 +100,7 @@ function useCanEditMessage( threadInfo: ThreadInfo, - targetMessageInfo: ComposableMessageInfo | RobotextMessageInfo, + targetMessageInfo: ?ComposableMessageInfo | RobotextMessageInfo, ): boolean { const currentUserInfo = useSelector(state => state.currentUserInfo); const currentUserCanEditMessage = useThreadHasPermission( @@ -111,7 +111,11 @@ return false; } - if (!targetMessageInfo.id || targetMessageInfo.type !== messageTypes.TEXT) { + if ( + !targetMessageInfo || + !targetMessageInfo.id || + targetMessageInfo.type !== messageTypes.TEXT + ) { return false; } diff --git a/lib/shared/reaction-utils.js b/lib/shared/reaction-utils.js --- a/lib/shared/reaction-utils.js +++ b/lib/shared/reaction-utils.js @@ -76,12 +76,11 @@ function useCanCreateReactionFromMessage( threadInfo: ThreadInfo, - targetMessageInfo: ComposableMessageInfo | RobotextMessageInfo, + targetMessageInfo: ?ComposableMessageInfo | RobotextMessageInfo, ): boolean { - const targetMessageCreatorRelationship = useSelector( - state => - state.userStore.userInfos[targetMessageInfo.creator.id] - ?.relationshipStatus, + const creatorID = targetMessageInfo?.creator.id; + const targetMessageCreatorRelationship = useSelector(state => + creatorID ? state.userStore.userInfos[creatorID]?.relationshipStatus : null, ); const userHasReactionPermission = useThreadHasPermission( @@ -93,6 +92,7 @@ } if ( + !targetMessageInfo || (!targetMessageInfo.id && !threadTypeIsThick(threadInfo.type)) || (threadInfo.sourceMessageID && threadInfo.sourceMessageID === targetMessageInfo.id) diff --git a/lib/shared/sidebar-utils.js b/lib/shared/sidebar-utils.js --- a/lib/shared/sidebar-utils.js +++ b/lib/shared/sidebar-utils.js @@ -197,10 +197,11 @@ function useCanCreateSidebarFromMessage( threadInfo: ThreadInfo, - messageInfo: ComposableMessageInfo | RobotextMessageInfo, + messageInfo: ?ComposableMessageInfo | RobotextMessageInfo, ): boolean { - const messageCreatorUserInfo = useSelector( - state => state.userStore.userInfos[messageInfo.creator.id], + const creatorID = messageInfo?.creator.id; + const messageCreatorUserInfo = useSelector(state => + creatorID ? state.userStore.userInfos[creatorID] : null, ); const hasCreateSidebarsPermission = useThreadHasPermission( threadInfo, @@ -211,6 +212,7 @@ } if ( + !messageInfo || (!messageInfo.id && !threadTypeIsThick(threadInfo.type)) || (threadInfo.sourceMessageID && threadInfo.sourceMessageID === messageInfo.id) || diff --git a/lib/utils/message-pinning-utils.js b/lib/utils/message-pinning-utils.js --- a/lib/utils/message-pinning-utils.js +++ b/lib/utils/message-pinning-utils.js @@ -27,16 +27,18 @@ } function useCanToggleMessagePin( - messageInfo: MessageInfo, + messageInfo: ?MessageInfo, threadInfo: ThreadInfo, ): boolean { - const isValidMessage = !isInvalidPinSourceForThread(messageInfo, threadInfo); const hasManagePinsPermission = useThreadHasPermission( threadInfo, threadPermissions.MANAGE_PINS, ); - - return isValidMessage && hasManagePinsPermission; + return ( + !!messageInfo && + hasManagePinsPermission && + !isInvalidPinSourceForThread(messageInfo, threadInfo) + ); } function pinnedMessageCountText(pinnedCount: number): string { diff --git a/native/chat/chat-item-height-measurer.react.js b/native/chat/chat-item-height-measurer.react.js --- a/native/chat/chat-item-height-measurer.react.js +++ b/native/chat/chat-item-height-measurer.react.js @@ -38,7 +38,7 @@ const { messageInfo, hasBeenEdited, threadCreatedFromMessage, reactions } = item; - if (messageInfo.type === messageTypes.TEXT) { + if (messageInfo && messageInfo.type === messageTypes.TEXT) { return JSON.stringify({ text: messageInfo.text, edited: getMessageLabel(hasBeenEdited, messageInfo.threadID), @@ -46,7 +46,7 @@ reactions: reactionsToRawString(reactions), }); } else if (item.robotext) { - const { threadID } = item.messageInfo; + const { threadID } = item.messageInfos[0]; return JSON.stringify({ robotext: entityTextToRawString(item.robotext, { threadID }), sidebar: getInlineEngagementSidebarText(threadCreatedFromMessage), @@ -73,7 +73,7 @@ const { messageInfo, hasBeenEdited, threadCreatedFromMessage, reactions } = item; - if (messageInfo.type === messageTypes.TEXT) { + if (messageInfo && messageInfo.type === messageTypes.TEXT) { const label = getMessageLabel(hasBeenEdited, messageInfo.threadID); return dummyNodeForTextMessageHeightMeasurement( messageInfo.text, @@ -84,7 +84,7 @@ } else if (item.robotext) { return dummyNodeForRobotextMessageHeightMeasurement( item.robotext, - messageInfo.threadID, + item.messageInfos[0].threadID, threadCreatedFromMessage, reactions, ); @@ -116,6 +116,26 @@ return item; } + if (item.messageInfoType !== 'composable') { + invariant( + height !== null && height !== undefined, + 'height should be set', + ); + return { + itemType: 'message', + messageShapeType: 'robotext', + messageInfos: item.messageInfos, + threadInfo, + startsConversation: item.startsConversation, + startsCluster: item.startsCluster, + endsCluster: item.endsCluster, + threadCreatedFromMessage: item.threadCreatedFromMessage, + robotext: item.robotext, + contentHeight: height, + reactions: item.reactions, + }; + } + const { messageInfo } = item; const messageType: MessageType = messageInfo.type; invariant( @@ -181,29 +201,11 @@ isPinned: item.isPinned, }; } - invariant( - item.messageInfoType !== 'composable', + + throw new Error( '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], ); diff --git a/native/chat/inner-robotext-message.react.js b/native/chat/inner-robotext-message.react.js --- a/native/chat/inner-robotext-message.react.js +++ b/native/chat/inner-robotext-message.react.js @@ -54,8 +54,8 @@ const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const styles = useOverlayStyles(unboundStyles); - const { messageInfo, robotext } = item; - const { threadID } = messageInfo; + const { messageInfos, robotext } = item; + const { threadID } = messageInfos[0]; const resolvedRobotext = useResolvedEntityText(robotext); invariant( resolvedRobotext, diff --git a/native/chat/robotext-message.react.js b/native/chat/robotext-message.react.js --- a/native/chat/robotext-message.react.js +++ b/native/chat/robotext-message.react.js @@ -66,7 +66,10 @@ const engagementTargetMessageInfo = chatMessageItemEngagementTargetMessageInfo(item); let inlineEngagement = null; - if (chatMessageItemHasEngagement(item, item.threadInfo.id)) { + if ( + engagementTargetMessageInfo && + chatMessageItemHasEngagement(item, item.threadInfo.id) + ) { inlineEngagement = ( { + if (!messageID) { + return; + } setSidebarSourceID && setSidebarSourceID(messageID); navigateToSidebar(); }, [setSidebarSourceID, messageID, navigateToSidebar]); diff --git a/native/chat/utils.js b/native/chat/utils.js --- a/native/chat/utils.js +++ b/native/chat/utils.js @@ -153,9 +153,10 @@ }; } - const authorNameComponentHeight = sourceMessage.messageInfo.creator.isViewer - ? 0 - : authorNameHeight; + const authorNameComponentHeight = + !sourceMessage.messageInfo || sourceMessage.messageInfo.creator.isViewer + ? 0 + : authorNameHeight; const currentDistanceFromBottom = messageListVerticalBounds.height + messageListVerticalBounds.y - diff --git a/native/search/message-search.react.js b/native/search/message-search.react.js --- a/native/search/message-search.react.js +++ b/native/search/message-search.react.js @@ -228,8 +228,21 @@ function oldestMessage(data: $ReadOnlyArray) { for (let i = data.length - 1; i >= 0; i--) { - if (data[i].itemType === 'message' && data[i].messageInfo.id) { - return data[i].messageInfo; + const item = data[i]; + if (item.itemType !== 'message') { + continue; + } + if (item.messageShapeType !== 'robotext') { + if (item.messageInfo.id) { + return item.messageInfo; + } + continue; + } + for (let j = item.messageInfos.length - 1; j >= 0; j--) { + const messageInfo = item.messageInfos[j]; + if (messageInfo.id) { + return messageInfo; + } } } return undefined; diff --git a/native/types/chat-types.js b/native/types/chat-types.js --- a/native/types/chat-types.js +++ b/native/types/chat-types.js @@ -15,7 +15,7 @@ export type ChatRobotextMessageInfoItemWithHeight = { +itemType: 'message', +messageShapeType: 'robotext', - +messageInfo: RobotextMessageInfo, + +messageInfos: $ReadOnlyArray, +threadInfo: ThreadInfo, +startsConversation: boolean, +startsCluster: boolean, diff --git a/web/chat/chat-message-list.react.js b/web/chat/chat-message-list.react.js --- a/web/chat/chat-message-list.react.js +++ b/web/chat/chat-message-list.react.js @@ -135,6 +135,7 @@ (hasNewMessage && messageListData && messageListData[0].itemType === 'message' && + messageListData[0].messageInfoType === 'composable' && messageListData[0].messageInfo.localID) || (hasNewMessage && snapshot && diff --git a/web/chat/robotext-message.react.js b/web/chat/robotext-message.react.js --- a/web/chat/robotext-message.react.js +++ b/web/chat/robotext-message.react.js @@ -41,9 +41,12 @@ let inlineEngagement; const { item, threadInfo } = props; const { threadCreatedFromMessage, reactions } = item; - if (threadCreatedFromMessage || Object.keys(reactions).length > 0) { - const engagementTargetMessageInfo = - chatMessageItemEngagementTargetMessageInfo(item); + const engagementTargetMessageInfo = + chatMessageItemEngagementTargetMessageInfo(item); + if ( + engagementTargetMessageInfo && + (threadCreatedFromMessage || Object.keys(reactions).length > 0) + ) { inlineEngagement = (
) => mixed { const dispatch = useDispatch(); @@ -79,7 +79,7 @@ return React.useCallback( async (event: SyntheticEvent) => { event.preventDefault(); - if (!loggedInUserInfo) { + if (!loggedInUserInfo || !messageInfo) { return; } const pendingSidebarInfo = await createPendingSidebar({ diff --git a/web/tooltips/tooltip-action-utils.js b/web/tooltips/tooltip-action-utils.js --- a/web/tooltips/tooltip-action-utils.js +++ b/web/tooltips/tooltip-action-utils.js @@ -175,7 +175,6 @@ item: ChatMessageInfoItem, threadInfo: ThreadInfo, ): ?MessageTooltipAction { - const { messageInfo } = item; const { popModal } = useModalContext(); const inputState = React.useContext(InputStateContext); invariant(inputState, 'inputState is required'); @@ -184,8 +183,18 @@ threadInfo, threadPermissions.VOICED, ); + + let messageInfo; + if ( + item.messageInfoType === 'composable' && + item.messageInfo.type === messageTypes.TEXT && + currentUserIsVoiced + ) { + messageInfo = item.messageInfo; + } + return React.useMemo(() => { - if (item.messageInfo.type !== messageTypes.TEXT || !currentUserIsVoiced) { + if (!messageInfo) { return null; } const buttonContent = ; @@ -202,34 +211,35 @@ onClick, label: 'Reply', }; - }, [ - popModal, - addReply, - item.messageInfo.type, - messageInfo, - currentUserIsVoiced, - ]); + }, [popModal, addReply, messageInfo]); } const copiedMessageDurationMs = 2000; function useMessageCopyAction( item: ChatMessageInfoItem, ): ?MessageTooltipAction { - const { messageInfo } = item; - const [successful, setSuccessful] = useResettingState( false, copiedMessageDurationMs, ); + let messageInfo; + if ( + item.messageInfoType === 'composable' && + item.messageInfo.type === messageTypes.TEXT + ) { + messageInfo = item.messageInfo; + } + + const messageText = messageInfo?.text; return React.useMemo(() => { - if (messageInfo.type !== messageTypes.TEXT) { + if (!messageText) { return null; } const buttonContent = ; const onClick = async () => { try { - await navigator.clipboard.writeText(messageInfo.text); + await navigator.clipboard.writeText(messageText); setSuccessful(true); } catch (e) { setSuccessful(false); @@ -240,7 +250,7 @@ onClick, label: successful ? 'Copied!' : 'Copy', }; - }, [messageInfo.text, messageInfo.type, setSuccessful, successful]); + }, [messageText, setSuccessful, successful]); } function useMessageReactAction( @@ -337,6 +347,10 @@ item.messageInfoType === 'composable', 'canEditMessage should only be true for composable messages!', ); + invariant( + messageInfo && messageInfo.type === messageTypes.TEXT, + 'canEditMessage should only be true for text messages!', + ); const buttonContent = ; const onClickEdit = () => { const callback = (maxHeight: number) =>