Changeset 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 messageBoxStyle = [{ maxWidth: composedMessageMaxWidth }]; | ||||
if (shouldRenderAvatars) { | |||||
messageBoxStyle.push(styles.swipeableContainer); | |||||
ashoat: What happens if we always include this? Does it break rendering in the pre-avatars world? | |||||
ginsuAuthorUnsubmitted Done Inline ActionsAt one point as I was implementing this new solution the rendering broke; however, after trying to repro it to get a screenshot, everything seems to be copacetic now both in the pre and post avatar world. I think I had a style out of place as I was tinkering, which caused my misassumption. I will fix this and show this in the test plan ginsu: At one point as I was implementing this new solution the rendering broke; however, after trying… | |||||
} | |||||
const messageBoxStyleContainerStyle = [styles.messageBox]; | |||||
ashoatUnsubmitted Not Done Inline ActionsIt's pretty confusing that styles.messageBox is passed to messageBoxStyleContainerStyle rather than to messageBoxStyle. Can you clean up the naming? (The easiest thing would probably be to rename styles.messageBox) ashoat: It's pretty confusing that `styles.messageBox` is passed to `messageBoxStyleContainerStyle`… | |||||
if (shouldRenderAvatars) { | |||||
messageBoxStyleContainerStyle.push({ flex: 1 }); | |||||
ashoatUnsubmitted Not Done Inline ActionsWhat happens if we just add flex: 1 to styles.messageBox? Does it break rendering in the pre-avatars world? In general we should move all "static" styles to styles rather than defining them inline, so if we really need this it would be good to do that. ashoat: What happens if we just add `flex: 1` to `styles.messageBox`? Does it break rendering in the… | |||||
ginsuAuthorUnsubmitted Done Inline ActionsAt one point as I was implementing this new solution the rendering broke; however, after trying to repro it to get a screenshot, everything seems to be copacetic now both in the pre and post avatar world. I think I had a style out of place as I was tinkering, which caused my misassumption. I will fix this and show this in the test plan ginsu: At one point as I was implementing this new solution the rendering broke; however, after trying… | |||||
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={messageBoxStyle} | ||||
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', | ||||
}, | }, | ||||
Show All 20 Lines | 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', | |||||
ashoatUnsubmitted Not Done Inline ActionsWhat does this line do? ashoat: What does this line do? | |||||
ginsuAuthorUnsubmitted Done Inline Actionsginsu: It brings the avatar down to the bottom of the message box. Without it our avatar would be… | |||||
ashoatUnsubmitted Not Done Inline ActionsDo we need display: 'flex' here, or does React Native "assume" that by default or something? ashoat: Do we need `display: 'flex'` here, or does React Native "assume" that by default or something? | |||||
ginsuAuthorUnsubmitted Done Inline ActionsIn RN everything is display: 'flex' by default ginsu: In RN everything is `display: 'flex'` by default
https://blog.logrocket.com/using-flexbox… | |||||
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; |
What happens if we always include this? Does it break rendering in the pre-avatars world?