Changeset View
Changeset View
Standalone View
Standalone View
native/chat/composed-message.react.js
// @flow | // @flow | ||||
import Icon from '@expo/vector-icons/Feather.js'; | import Icon from '@expo/vector-icons/Feather.js'; | ||||
import invariant from 'invariant'; | import invariant from 'invariant'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { StyleSheet, View } from 'react-native'; | import { StyleSheet, View } from 'react-native'; | ||||
import Animated from 'react-native-reanimated'; | import Animated from 'react-native-reanimated'; | ||||
import { getAvatarForUser } from 'lib/shared/avatar-utils.js'; | |||||
import { createMessageReply } from 'lib/shared/message-utils.js'; | import { createMessageReply } from 'lib/shared/message-utils.js'; | ||||
import { assertComposableMessageType } from 'lib/types/message-types.js'; | import { assertComposableMessageType } from 'lib/types/message-types.js'; | ||||
import { | import { | ||||
clusterEndHeight, | clusterEndHeight, | ||||
inlineEngagementStyle, | inlineEngagementStyle, | ||||
inlineEngagementLeftStyle, | inlineEngagementLeftStyle, | ||||
inlineEngagementRightStyle, | inlineEngagementRightStyle, | ||||
composedMessageStyle, | composedMessageStyle, | ||||
avatarOffset, | |||||
} from './chat-constants.js'; | } from './chat-constants.js'; | ||||
import { useComposedMessageMaxWidth } from './composed-message-width.js'; | import { useComposedMessageMaxWidth } from './composed-message-width.js'; | ||||
import { FailedSend } from './failed-send.react.js'; | import { FailedSend } from './failed-send.react.js'; | ||||
import { InlineEngagement } from './inline-engagement.react.js'; | import { InlineEngagement } from './inline-engagement.react.js'; | ||||
import { MessageHeader } from './message-header.react.js'; | import { MessageHeader } from './message-header.react.js'; | ||||
import { useNavigateToSidebar } from './sidebar-navigation.js'; | import { useNavigateToSidebar } from './sidebar-navigation.js'; | ||||
import SwipeableMessage from './swipeable-message.react.js'; | import SwipeableMessage from './swipeable-message.react.js'; | ||||
import { useContentAndHeaderOpacity, useDeliveryIconOpacity } from './utils.js'; | import { useContentAndHeaderOpacity, useDeliveryIconOpacity } from './utils.js'; | ||||
import Avatar from '../components/avatar.react.js'; | |||||
import { type InputState, InputStateContext } from '../input/input-state.js'; | import { type InputState, InputStateContext } from '../input/input-state.js'; | ||||
import { type Colors, useColors } from '../themes/colors.js'; | import { type Colors, useColors } from '../themes/colors.js'; | ||||
import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; | import type { ChatMessageInfoItemWithHeight } from '../types/chat-types.js'; | ||||
import { type AnimatedStyleObj, AnimatedView } from '../types/styles.js'; | import { type AnimatedStyleObj, AnimatedView } from '../types/styles.js'; | ||||
import { useShouldRenderAvatars } from '../utils/avatar-utils.js'; | |||||
/* eslint-disable import/no-named-as-default-member */ | /* eslint-disable import/no-named-as-default-member */ | ||||
const { Node } = Animated; | const { Node } = Animated; | ||||
/* eslint-enable import/no-named-as-default-member */ | /* eslint-enable import/no-named-as-default-member */ | ||||
type SwipeOptions = 'reply' | 'sidebar' | 'both' | 'none'; | type SwipeOptions = 'reply' | 'sidebar' | 'both' | 'none'; | ||||
type BaseProps = { | type BaseProps = { | ||||
...React.ElementConfig<typeof View>, | ...React.ElementConfig<typeof View>, | ||||
+item: ChatMessageInfoItemWithHeight, | +item: ChatMessageInfoItemWithHeight, | ||||
+sendFailed: boolean, | +sendFailed: boolean, | ||||
+focused: boolean, | +focused: boolean, | ||||
+swipeOptions: SwipeOptions, | +swipeOptions: SwipeOptions, | ||||
+children: React.Node, | +children: React.Node, | ||||
}; | }; | ||||
type Props = { | type Props = { | ||||
...BaseProps, | ...BaseProps, | ||||
// Redux state | // Redux state | ||||
+composedMessageMaxWidth: number, | +composedMessageMaxWidth: number, | ||||
+colors: Colors, | +colors: Colors, | ||||
+contentAndHeaderOpacity: number | Node, | +contentAndHeaderOpacity: number | Node, | ||||
+deliveryIconOpacity: number | Node, | +deliveryIconOpacity: number | Node, | ||||
// withInputState | // withInputState | ||||
+inputState: ?InputState, | +inputState: ?InputState, | ||||
+navigateToSidebar: () => mixed, | +navigateToSidebar: () => mixed, | ||||
+shouldRenderAvatars: boolean, | |||||
}; | }; | ||||
class ComposedMessage extends React.PureComponent<Props> { | class ComposedMessage extends React.PureComponent<Props> { | ||||
render() { | render() { | ||||
assertComposableMessageType(this.props.item.messageInfo.type); | assertComposableMessageType(this.props.item.messageInfo.type); | ||||
const { | const { | ||||
item, | item, | ||||
sendFailed, | sendFailed, | ||||
focused, | focused, | ||||
swipeOptions, | swipeOptions, | ||||
children, | children, | ||||
composedMessageMaxWidth, | composedMessageMaxWidth, | ||||
colors, | colors, | ||||
inputState, | inputState, | ||||
navigateToSidebar, | navigateToSidebar, | ||||
contentAndHeaderOpacity, | contentAndHeaderOpacity, | ||||
deliveryIconOpacity, | deliveryIconOpacity, | ||||
shouldRenderAvatars, | |||||
...viewProps | ...viewProps | ||||
} = this.props; | } = this.props; | ||||
const { id, creator } = item.messageInfo; | const { id, creator } = item.messageInfo; | ||||
const { isViewer } = creator; | const { isViewer } = creator; | ||||
const alignStyle = isViewer | const alignStyle = isViewer | ||||
? styles.rightChatBubble | ? styles.rightChatBubble | ||||
: styles.leftChatBubble; | : styles.leftChatBubble; | ||||
let containerMarginBottom = 5; | let containerMarginBottom = 5; | ||||
if (item.endsCluster) { | if (item.endsCluster) { | ||||
containerMarginBottom += clusterEndHeight; | containerMarginBottom += clusterEndHeight; | ||||
} | } | ||||
const containerStyle = [ | const containerStyle = [ | ||||
styles.alignment, | styles.alignment, | ||||
{ marginBottom: containerMarginBottom }, | { marginBottom: containerMarginBottom }, | ||||
]; | ]; | ||||
const messageBoxStyle = { maxWidth: composedMessageMaxWidth }; | const swipeableMessageBoxStyle = [ | ||||
styles.swipeableContainer, | |||||
{ maxWidth: composedMessageMaxWidth }, | |||||
]; | |||||
const messageBoxStyleContainerStyle = [styles.messageBoxContainer]; | |||||
const positioningStyle = isViewer | |||||
? { alignItems: 'flex-end' } | |||||
: { alignItems: 'flex-start' }; | |||||
messageBoxStyleContainerStyle.push(positioningStyle); | |||||
let deliveryIcon = null; | let deliveryIcon = null; | ||||
let failedSendInfo = null; | let failedSendInfo = null; | ||||
if (isViewer) { | if (isViewer) { | ||||
let deliveryIconName; | let deliveryIconName; | ||||
let deliveryIconColor = `#${item.threadInfo.color}`; | let deliveryIconColor = `#${item.threadInfo.color}`; | ||||
if (id !== null && id !== undefined) { | if (id !== null && id !== undefined) { | ||||
deliveryIconName = 'check-circle'; | deliveryIconName = 'check-circle'; | ||||
Show All 19 Lines | render() { | ||||
const triggerReply = | const triggerReply = | ||||
swipeOptions === 'reply' || swipeOptions === 'both' | swipeOptions === 'reply' || swipeOptions === 'both' | ||||
? this.reply | ? this.reply | ||||
: undefined; | : undefined; | ||||
const triggerSidebar = | const triggerSidebar = | ||||
swipeOptions === 'sidebar' || swipeOptions === 'both' | swipeOptions === 'sidebar' || swipeOptions === 'both' | ||||
? navigateToSidebar | ? navigateToSidebar | ||||
: undefined; | : undefined; | ||||
let avatar; | |||||
if (!isViewer && item.endsCluster && shouldRenderAvatars) { | |||||
const avatarInfo = getAvatarForUser(item.messageInfo.creator); | |||||
avatar = ( | |||||
<View style={styles.avatarContainer}> | |||||
<Avatar size="small" avatarInfo={avatarInfo} /> | |||||
</View> | |||||
); | |||||
} else if (!isViewer && shouldRenderAvatars) { | |||||
avatar = <View style={styles.avatarOffset} />; | |||||
} | |||||
const messageBox = ( | const messageBox = ( | ||||
<View style={styles.messageBox}> | <View style={messageBoxStyleContainerStyle}> | ||||
<SwipeableMessage | <SwipeableMessage | ||||
triggerReply={triggerReply} | triggerReply={triggerReply} | ||||
triggerSidebar={triggerSidebar} | triggerSidebar={triggerSidebar} | ||||
isViewer={isViewer} | isViewer={isViewer} | ||||
messageBoxStyle={messageBoxStyle} | messageBoxStyle={swipeableMessageBoxStyle} | ||||
threadColor={item.threadInfo.color} | threadColor={item.threadInfo.color} | ||||
> | > | ||||
{avatar} | |||||
<AnimatedView style={{ opacity: contentAndHeaderOpacity }}> | <AnimatedView style={{ opacity: contentAndHeaderOpacity }}> | ||||
{children} | {children} | ||||
</AnimatedView> | </AnimatedView> | ||||
</SwipeableMessage> | </SwipeableMessage> | ||||
</View> | </View> | ||||
); | ); | ||||
let inlineEngagement = null; | let inlineEngagement = null; | ||||
if ( | if ( | ||||
item.threadCreatedFromMessage || | item.threadCreatedFromMessage || | ||||
Object.keys(item.reactions).length > 0 | Object.keys(item.reactions).length > 0 | ||||
) { | ) { | ||||
const positioning = isViewer ? 'right' : 'left'; | const positioning = isViewer ? 'right' : 'left'; | ||||
const inlineEngagementPositionStyle = | |||||
positioning === 'left' | const inlineEngagementPositionStyle = []; | ||||
? styles.leftInlineEngagement | if (positioning === 'left') { | ||||
: styles.rightInlineEngagement; | inlineEngagementPositionStyle.push(styles.leftInlineEngagement); | ||||
} else { | |||||
inlineEngagementPositionStyle.push(styles.rightInlineEngagement); | |||||
} | |||||
if (this.props.shouldRenderAvatars) { | |||||
inlineEngagementPositionStyle.push({ marginLeft: avatarOffset }); | |||||
} | |||||
inlineEngagement = ( | inlineEngagement = ( | ||||
<View style={[styles.inlineEngagement, inlineEngagementPositionStyle]}> | <View style={[styles.inlineEngagement, inlineEngagementPositionStyle]}> | ||||
<InlineEngagement | <InlineEngagement | ||||
threadInfo={item.threadCreatedFromMessage} | threadInfo={item.threadCreatedFromMessage} | ||||
reactions={item.reactions} | reactions={item.reactions} | ||||
/> | /> | ||||
</View> | </View> | ||||
); | ); | ||||
Show All 24 Lines | class ComposedMessage extends React.PureComponent<Props> { | ||||
}; | }; | ||||
} | } | ||||
const styles = StyleSheet.create({ | const styles = StyleSheet.create({ | ||||
alignment: { | alignment: { | ||||
marginLeft: composedMessageStyle.marginLeft, | marginLeft: composedMessageStyle.marginLeft, | ||||
marginRight: composedMessageStyle.marginRight, | marginRight: composedMessageStyle.marginRight, | ||||
}, | }, | ||||
avatarContainer: { | |||||
marginRight: 8, | |||||
}, | |||||
avatarOffset: { | |||||
width: avatarOffset, | |||||
}, | |||||
content: { | content: { | ||||
alignItems: 'center', | alignItems: 'center', | ||||
flexDirection: 'row-reverse', | flexDirection: 'row-reverse', | ||||
}, | }, | ||||
icon: { | icon: { | ||||
fontSize: 16, | fontSize: 16, | ||||
textAlign: 'center', | textAlign: 'center', | ||||
}, | }, | ||||
iconContainer: { | iconContainer: { | ||||
marginLeft: 2, | marginLeft: 2, | ||||
width: 16, | width: 16, | ||||
}, | }, | ||||
inlineEngagement: { | inlineEngagement: { | ||||
marginBottom: inlineEngagementStyle.marginBottom, | marginBottom: inlineEngagementStyle.marginBottom, | ||||
marginTop: inlineEngagementStyle.marginTop, | marginTop: inlineEngagementStyle.marginTop, | ||||
}, | }, | ||||
leftChatBubble: { | leftChatBubble: { | ||||
justifyContent: 'flex-end', | justifyContent: 'flex-end', | ||||
}, | }, | ||||
leftInlineEngagement: { | leftInlineEngagement: { | ||||
justifyContent: 'flex-start', | justifyContent: 'flex-start', | ||||
position: 'relative', | position: 'relative', | ||||
top: inlineEngagementLeftStyle.topOffset, | top: inlineEngagementLeftStyle.topOffset, | ||||
}, | }, | ||||
messageBox: { | messageBoxContainer: { | ||||
flex: 1, | |||||
marginRight: 5, | marginRight: 5, | ||||
}, | }, | ||||
rightChatBubble: { | rightChatBubble: { | ||||
justifyContent: 'flex-start', | justifyContent: 'flex-start', | ||||
}, | }, | ||||
rightInlineEngagement: { | rightInlineEngagement: { | ||||
alignSelf: 'flex-end', | alignSelf: 'flex-end', | ||||
position: 'relative', | position: 'relative', | ||||
right: inlineEngagementRightStyle.marginRight, | right: inlineEngagementRightStyle.marginRight, | ||||
top: inlineEngagementRightStyle.topOffset, | top: inlineEngagementRightStyle.topOffset, | ||||
}, | }, | ||||
swipeableContainer: { | |||||
alignItems: 'flex-end', | |||||
flexDirection: 'row', | |||||
}, | |||||
}); | }); | ||||
const ConnectedComposedMessage: React.ComponentType<BaseProps> = | const ConnectedComposedMessage: React.ComponentType<BaseProps> = | ||||
React.memo<BaseProps>(function ConnectedComposedMessage(props: BaseProps) { | React.memo<BaseProps>(function ConnectedComposedMessage(props: BaseProps) { | ||||
const composedMessageMaxWidth = useComposedMessageMaxWidth(); | const composedMessageMaxWidth = useComposedMessageMaxWidth(); | ||||
const colors = useColors(); | const colors = useColors(); | ||||
const inputState = React.useContext(InputStateContext); | const inputState = React.useContext(InputStateContext); | ||||
const navigateToSidebar = useNavigateToSidebar(props.item); | const navigateToSidebar = useNavigateToSidebar(props.item); | ||||
const contentAndHeaderOpacity = useContentAndHeaderOpacity(props.item); | const contentAndHeaderOpacity = useContentAndHeaderOpacity(props.item); | ||||
const deliveryIconOpacity = useDeliveryIconOpacity(props.item); | const deliveryIconOpacity = useDeliveryIconOpacity(props.item); | ||||
const shouldRenderAvatars = useShouldRenderAvatars(); | |||||
return ( | return ( | ||||
<ComposedMessage | <ComposedMessage | ||||
{...props} | {...props} | ||||
composedMessageMaxWidth={composedMessageMaxWidth} | composedMessageMaxWidth={composedMessageMaxWidth} | ||||
colors={colors} | colors={colors} | ||||
inputState={inputState} | inputState={inputState} | ||||
navigateToSidebar={navigateToSidebar} | navigateToSidebar={navigateToSidebar} | ||||
contentAndHeaderOpacity={contentAndHeaderOpacity} | contentAndHeaderOpacity={contentAndHeaderOpacity} | ||||
deliveryIconOpacity={deliveryIconOpacity} | deliveryIconOpacity={deliveryIconOpacity} | ||||
shouldRenderAvatars={shouldRenderAvatars} | |||||
/> | /> | ||||
); | ); | ||||
}); | }); | ||||
export default ConnectedComposedMessage; | export default ConnectedComposedMessage; |