diff --git a/native/chat/chat-item-height-measurer.react.js b/native/chat/chat-item-height-measurer.react.js
index edc3b12a1..6314ae598 100644
--- a/native/chat/chat-item-height-measurer.react.js
+++ b/native/chat/chat-item-height-measurer.react.js
@@ -1,167 +1,170 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import type { ChatMessageItem } from 'lib/selectors/chat-selectors';
import { messageID } from 'lib/shared/message-utils';
import { messageTypes, type MessageType } from 'lib/types/message-types';
import NodeHeightMeasurer from '../components/node-height-measurer.react';
import { InputStateContext } from '../input/input-state';
import type { MeasurementTask } from './chat-context-provider.react';
import { useComposedMessageMaxWidth } from './composed-message-width';
import { dummyNodeForRobotextMessageHeightMeasurement } from './inner-robotext-message.react';
import { dummyNodeForTextMessageHeightMeasurement } from './inner-text-message.react';
import { MessageListContextProvider } from './message-list-types';
import { multimediaMessageContentSizes } from './multimedia-message-utils';
import { chatMessageItemKey } from './utils';
type Props = {
+measurement: MeasurementTask,
};
const heightMeasurerKey = (item: ChatMessageItem) => {
if (item.itemType !== 'message') {
return null;
}
const { messageInfo } = item;
if (messageInfo.type === messageTypes.TEXT) {
return messageInfo.text;
} else if (item.robotext && typeof item.robotext === 'string') {
return item.robotext;
}
return null;
};
const heightMeasurerDummy = (item: ChatMessageItem) => {
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 && typeof item.robotext === 'string') {
return dummyNodeForRobotextMessageHeightMeasurement(item.robotext);
}
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: ChatMessageItem, 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,
...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,
};
} else {
invariant(
typeof item.robotext === 'string',
"Flow can't handle our fancy types :(",
);
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/multimedia-message-utils.js b/native/chat/multimedia-message-utils.js
index 015a9bd3d..3b0fcea36 100644
--- a/native/chat/multimedia-message-utils.js
+++ b/native/chat/multimedia-message-utils.js
@@ -1,143 +1,143 @@
// @flow
import invariant from 'invariant';
import { messageKey } from 'lib/shared/message-utils';
import type { MediaInfo } from 'lib/types/media-types';
import type { MultimediaMessageInfo } from 'lib/types/message-types';
import type {
ChatMultimediaMessageInfoItem,
MultimediaContentSizes,
} from '../types/chat-types';
import { inlineSidebarStyle, clusterEndHeight } from './chat-constants';
import { failedSendHeight } from './failed-send.react';
import { authorNameHeight } from './message-header.react';
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 { creator } = messageInfo;
const { isViewer } = creator;
let height = 5 + contentHeight; // 5 from marginBottom in ComposedMessage
if (!isViewer && startsCluster) {
height += authorNameHeight;
}
if (endsCluster) {
height += clusterEndHeight;
}
if (multimediaMessageSendFailed(item)) {
height += failedSendHeight;
}
- if (item.threadCreatedFromMessage) {
+ if (item.threadCreatedFromMessage || item.reactions.size > 0) {
height +=
inlineSidebarStyle.height +
inlineSidebarStyle.marginTop +
inlineSidebarStyle.marginBottom;
}
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/chat/utils.js b/native/chat/utils.js
index 82fc4e28f..ed14e626d 100644
--- a/native/chat/utils.js
+++ b/native/chat/utils.js
@@ -1,420 +1,420 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
import Animated from 'react-native-reanimated';
import { useMessageListData } from 'lib/selectors/chat-selectors';
import type { ChatMessageItem } from 'lib/selectors/chat-selectors';
import { messageKey } from 'lib/shared/message-utils';
import { colorIsDark, viewerIsMember } from 'lib/shared/thread-utils';
import type { ThreadInfo } from 'lib/types/thread-types';
import { KeyboardContext } from '../keyboard/keyboard-state';
import { OverlayContext } from '../navigation/overlay-context';
import {
MultimediaMessageTooltipModalRouteName,
RobotextMessageTooltipModalRouteName,
TextMessageTooltipModalRouteName,
} from '../navigation/route-names';
import { useSelector } from '../redux/redux-utils';
import type {
ChatMessageInfoItemWithHeight,
ChatMessageItemWithHeight,
ChatRobotextMessageInfoItemWithHeight,
ChatTextMessageInfoItemWithHeight,
} from '../types/chat-types';
import type { LayoutCoordinates, VerticalBounds } from '../types/layout-types';
import type { AnimatedViewStyle } from '../types/styles';
import { clusterEndHeight, inlineSidebarStyle } from './chat-constants';
import { ChatContext, useHeightMeasurer } from './chat-context';
import { failedSendHeight } from './failed-send.react';
import { authorNameHeight } from './message-header.react';
import { multimediaMessageItemHeight } from './multimedia-message-utils';
import { getSidebarThreadInfo } from './sidebar-navigation';
import textMessageSendFailed from './text-message-send-failed';
import { timestampHeight } from './timestamp.react';
/* 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 } = 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;
}
- if (item.threadCreatedFromMessage) {
+ if (item.threadCreatedFromMessage || item.reactions.size > 0) {
height +=
inlineSidebarStyle.height +
inlineSidebarStyle.marginTop +
inlineSidebarStyle.marginBottom;
}
return height;
}
function robotextMessageItemHeight(
item: ChatRobotextMessageInfoItemWithHeight,
): number {
- if (item.threadCreatedFromMessage) {
+ if (item.threadCreatedFromMessage || item.reactions.size > 0) {
return item.contentHeight + inlineSidebarStyle.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);
}
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 = useMessageListData({
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 viewerID = useSelector(
state => state.currentUserInfo && state.currentUserInfo.id,
);
const sidebarThreadInfo = React.useMemo(() => {
return getSidebarThreadInfo(sourceMessage, viewerID);
}, [sourceMessage, viewerID]);
const currentInputBarHeight =
chatInputBarHeights.get(sourceMessage.threadInfo.id) ?? 0;
const keyboardState = React.useContext(KeyboardContext);
const viewerIsSidebarMember = viewerIsMember(sidebarThreadInfo);
React.useEffect(() => {
const newSidebarAnimationType =
!currentInputBarHeight ||
!targetInputBarHeight ||
keyboardState?.keyboardShowing ||
!viewerIsSidebarMember
? 'fade_source_message'
: 'move_source_message';
setSidebarAnimationType(newSidebarAnimationType);
}, [
currentInputBarHeight,
keyboardState?.keyboardShowing,
setSidebarAnimationType,
sidebarThreadInfo,
targetInputBarHeight,
viewerIsSidebarMember,
]);
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 | ChatMessageItem,
): string {
if (item.itemType === 'loader') {
return 'loader';
}
return messageKey(item.messageInfo);
}
export {
chatMessageItemKey,
chatMessageItemHeight,
useAnimatedMessageTooltipButton,
messageItemHeight,
getMessageTooltipKey,
isMessageTooltipKey,
useContentAndHeaderOpacity,
useDeliveryIconOpacity,
};
diff --git a/native/types/chat-types.js b/native/types/chat-types.js
index f1dfb974d..a74ddfe41 100644
--- a/native/types/chat-types.js
+++ b/native/types/chat-types.js
@@ -1,65 +1,69 @@
// @flow
+import type { MessageReactionInfo } from 'lib/selectors/chat-selectors';
import type {
LocalMessageInfo,
MultimediaMessageInfo,
RobotextMessageInfo,
} from 'lib/types/message-types';
import type { TextMessageInfo } from 'lib/types/messages/text';
import type { ThreadInfo } from 'lib/types/thread-types';
import type { MessagePendingUploads } from '../input/input-state';
export type ChatRobotextMessageInfoItemWithHeight = {
+itemType: 'message',
+messageShapeType: 'robotext',
+messageInfo: RobotextMessageInfo,
+threadInfo: ThreadInfo,
+startsConversation: boolean,
+startsCluster: boolean,
+endsCluster: boolean,
+robotext: string,
+threadCreatedFromMessage: ?ThreadInfo,
+contentHeight: number,
+ +reactions: $ReadOnlyMap,
};
export type ChatTextMessageInfoItemWithHeight = {
+itemType: 'message',
+messageShapeType: 'text',
+messageInfo: TextMessageInfo,
+localMessageInfo: ?LocalMessageInfo,
+threadInfo: ThreadInfo,
+startsConversation: boolean,
+startsCluster: boolean,
+endsCluster: boolean,
+contentHeight: number,
+threadCreatedFromMessage: ?ThreadInfo,
+ +reactions: $ReadOnlyMap,
};
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: $ReadOnlyMap,
};
export type ChatMessageInfoItemWithHeight =
| ChatRobotextMessageInfoItemWithHeight
| ChatTextMessageInfoItemWithHeight
| ChatMultimediaMessageInfoItem;
export type ChatMessageItemWithHeight =
| { itemType: 'loader' }
| ChatMessageInfoItemWithHeight;