diff --git a/native/chat/chat-constants.js b/native/chat/chat-constants.js
index 53f834c6f..61e69a597 100644
--- a/native/chat/chat-constants.js
+++ b/native/chat/chat-constants.js
@@ -1,28 +1,33 @@
// @flow
export const composedMessageStyle = {
marginLeft: 12,
marginRight: 7,
};
export const inlineEngagementStyle = {
height: 38,
marginTop: 5,
marginBottom: 3,
};
export const inlineEngagementLeftStyle = {
topOffset: -10,
};
export const inlineEngagementCenterStyle = {
topOffset: -5,
};
export const inlineEngagementRightStyle = {
marginRight: 22,
topOffset: -10,
};
+export const inlineEngagementLabelStyle = {
+ height: 16,
+ topOffset: 8,
+};
+
export const clusterEndHeight = 7;
export const avatarOffset = 32;
diff --git a/native/chat/inline-engagement.react.js b/native/chat/inline-engagement.react.js
index 9534e6bbb..fb9108cda 100644
--- a/native/chat/inline-engagement.react.js
+++ b/native/chat/inline-engagement.react.js
@@ -1,342 +1,342 @@
// @flow
import { useNavigation } from '@react-navigation/native';
import * as React from 'react';
import { Text, View } from 'react-native';
import Animated, {
Extrapolate,
interpolateNode,
} from 'react-native-reanimated';
import useInlineEngagementText from 'lib/hooks/inline-engagement-text.react.js';
import type { ReactionInfo } from 'lib/selectors/chat-selectors.js';
import { stringForReactionList } from 'lib/shared/reaction-utils.js';
import type { ThreadInfo } from 'lib/types/thread-types.js';
import {
+ inlineEngagementLabelStyle,
inlineEngagementStyle,
inlineEngagementCenterStyle,
inlineEngagementRightStyle,
inlineEngagementLeftStyle,
composedMessageStyle,
avatarOffset,
} from './chat-constants.js';
import { useNavigateToThread } from './message-list-types.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 { useStyles } from '../themes/colors.js';
import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js';
-const editedLabelHeight = 24;
-
type Props = {
+threadInfo: ?ThreadInfo,
+reactions?: ReactionInfo,
+disabled?: boolean,
+positioning?: 'left' | 'right',
+label?: ?string,
+shouldRenderAvatars?: boolean,
};
function InlineEngagement(props: Props): React.Node {
const {
disabled = false,
reactions,
threadInfo,
positioning,
shouldRenderAvatars,
label,
} = props;
const repliesText = useInlineEngagementText(threadInfo);
const navigateToThread = useNavigateToThread();
const { navigate } = useNavigation();
const styles = useStyles(unboundStyles);
const unreadStyle = threadInfo?.currentUser.unread ? styles.unread : null;
const repliesStyles = React.useMemo(
() => [styles.repliesText, unreadStyle],
[styles.repliesText, unreadStyle],
);
const onPressThread = React.useCallback(() => {
if (threadInfo && !disabled) {
navigateToThread({ threadInfo });
}
}, [disabled, navigateToThread, threadInfo]);
const sidebarItem = React.useMemo(() => {
if (!threadInfo) {
return null;
}
return (
{repliesText}
);
}, [
threadInfo,
onPressThread,
styles.sidebar,
styles.icon,
repliesStyles,
repliesText,
]);
const onPressReactions = React.useCallback(() => {
navigate<'MessageReactionsModal'>({
name: MessageReactionsModalRouteName,
params: { reactions },
});
}, [navigate, reactions]);
const marginLeft = React.useMemo(
() => (sidebarItem ? styles.reactionMarginLeft : null),
[sidebarItem, styles.reactionMarginLeft],
);
const reactionList = React.useMemo(() => {
if (!reactions || Object.keys(reactions).length === 0) {
return null;
}
const reactionText = stringForReactionList(reactions);
const reactionItems = {reactionText};
return (
{reactionItems}
);
}, [
marginLeft,
onPressReactions,
reactions,
styles.reaction,
styles.reactionsContainer,
]);
const isLeft = positioning === 'left';
const editedLabel = React.useMemo(() => {
if (!label) {
return null;
}
const labelLeftRight = isLeft
? styles.messageLabelLeft
: styles.messageLabelRight;
return {label};
}, [isLeft, label, styles]);
const container = React.useMemo(() => {
if (!sidebarItem && !reactionList) {
return null;
}
return (
{sidebarItem}
{reactionList}
);
}, [reactionList, sidebarItem, styles.container]);
const inlineEngagementPositionStyle = [styles.inlineEngagement];
if (isLeft) {
inlineEngagementPositionStyle.push(styles.leftInlineEngagement);
} else {
inlineEngagementPositionStyle.push(styles.rightInlineEngagement);
}
if (shouldRenderAvatars) {
inlineEngagementPositionStyle.push({ marginLeft: avatarOffset });
}
let body;
if (isLeft) {
body = (
<>
{editedLabel}
{container}
>
);
} else {
body = (
<>
{container}
{editedLabel}
>
);
}
return {body};
}
const unboundStyles = {
container: {
flexDirection: 'row',
height: inlineEngagementStyle.height,
borderRadius: 16,
backgroundColor: 'inlineEngagementBackground',
alignSelf: 'baseline',
alignItems: 'center',
padding: 8,
},
unread: {
color: 'listForegroundLabel',
fontWeight: 'bold',
},
rightInlineEngagement: {
alignSelf: 'flex-end',
position: 'relative',
right: inlineEngagementRightStyle.marginRight,
top: inlineEngagementRightStyle.topOffset,
},
leftInlineEngagement: {
justifyContent: 'flex-start',
position: 'relative',
top: inlineEngagementLeftStyle.topOffset,
},
sidebar: {
flexDirection: 'row',
alignItems: 'center',
},
inlineEngagement: {
flexDirection: 'row',
marginBottom: inlineEngagementStyle.marginBottom,
marginTop: inlineEngagementStyle.marginTop,
alignItems: 'center',
},
icon: {
color: 'inlineEngagementLabel',
marginRight: 4,
},
repliesText: {
color: 'inlineEngagementLabel',
fontSize: 14,
lineHeight: 22,
},
reaction: {
color: 'inlineEngagementLabel',
fontSize: 14,
lineHeight: 22,
},
reactionMarginLeft: {
marginLeft: 12,
},
reactionsContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
messageLabel: {
color: 'messageLabel',
paddingHorizontal: 3,
fontSize: 13,
- top: 10,
+ top: inlineEngagementLabelStyle.topOffset,
+ height: inlineEngagementLabelStyle.height,
},
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;
const inlineEngagementStyles = React.useMemo(() => {
if (positioning === 'left') {
return {
position: 'absolute',
top:
inlineEngagementStyle.marginTop + inlineEngagementLeftStyle.topOffset,
left: composedMessageStyle.marginLeft,
};
} else if (positioning === 'right') {
return {
position: 'absolute',
right:
inlineEngagementRightStyle.marginRight +
composedMessageStyle.marginRight,
top:
inlineEngagementStyle.marginTop +
inlineEngagementRightStyle.topOffset,
};
} else if (positioning === 'center') {
return {
alignSelf: 'center',
top: inlineEngagementCenterStyle.topOffset,
};
}
}, [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, editedLabelHeight };
+export { InlineEngagement, TooltipInlineEngagement };
diff --git a/native/chat/utils.js b/native/chat/utils.js
index d4bd9be3d..23e6cb074 100644
--- a/native/chat/utils.js
+++ b/native/chat/utils.js
@@ -1,418 +1,424 @@
// @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 { clusterEndHeight, inlineEngagementStyle } from './chat-constants.js';
+import {
+ inlineEngagementLabelStyle,
+ clusterEndHeight,
+ inlineEngagementStyle,
+} from './chat-constants.js';
import { ChatContext, useHeightMeasurer } from './chat-context.js';
import { failedSendHeight } from './failed-send.react.js';
-import { editedLabelHeight } from './inline-engagement.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 += editedLabelHeight;
+ 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);
}
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);
}
export {
chatMessageItemKey,
chatMessageItemHeight,
useAnimatedMessageTooltipButton,
messageItemHeight,
getMessageTooltipKey,
isMessageTooltipKey,
useContentAndHeaderOpacity,
useDeliveryIconOpacity,
};