diff --git a/native/chat/multimedia-message-multimedia.react.js b/native/chat/multimedia-message-multimedia.react.js index 7131efbbc..26f034f9b 100644 --- a/native/chat/multimedia-message-multimedia.react.js +++ b/native/chat/multimedia-message-multimedia.react.js @@ -1,350 +1,350 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, StyleSheet } from 'react-native'; import Animated from 'react-native-reanimated'; -import { useSelector } from 'react-redux'; import { messageKey } from 'lib/shared/message-utils'; import { relationshipBlockedInEitherDirection } from 'lib/shared/relationship-utils'; import { threadHasPermission } from 'lib/shared/thread-utils'; import { type MediaInfo } from 'lib/types/media-types'; import { threadPermissions } from 'lib/types/thread-types'; import type { UserInfo } from 'lib/types/user-types'; import { type PendingMultimediaUpload } from '../input/input-state'; import { type KeyboardState, KeyboardContext, } from '../keyboard/keyboard-state'; import { OverlayContext, type OverlayContextType, } from '../navigation/overlay-context'; import { type NavigationRoute, VideoPlaybackModalRouteName, MultimediaModalRouteName, MultimediaTooltipModalRouteName, } from '../navigation/route-names'; +import { useSelector } from '../redux/redux-utils'; import { type Colors, useColors } from '../themes/colors'; import { type VerticalBounds } from '../types/layout-types'; import type { ViewStyle } from '../types/styles'; import type { ChatNavigationProp } from './chat.react'; import InlineMultimedia from './inline-multimedia.react'; import type { ChatMultimediaMessageInfoItem } from './multimedia-message.react'; import { multimediaTooltipHeight } from './multimedia-tooltip-modal.react'; /* eslint-disable import/no-named-as-default-member */ const { Value, sub, interpolate, Extrapolate } = Animated; /* eslint-enable import/no-named-as-default-member */ type BaseProps = {| +mediaInfo: MediaInfo, +item: ChatMultimediaMessageInfoItem, +navigation: ChatNavigationProp<'MessageList'>, +route: NavigationRoute<'MessageList'>, +verticalBounds: ?VerticalBounds, +verticalOffset: number, +style: ViewStyle, +postInProgress: boolean, +pendingUpload: ?PendingMultimediaUpload, +messageFocused: boolean, +toggleMessageFocus: (messageKey: string) => void, |}; type Props = {| ...BaseProps, // Redux state +colors: Colors, +messageCreatorUserInfo: UserInfo, // withKeyboardState +keyboardState: ?KeyboardState, // withOverlayContext +overlayContext: ?OverlayContextType, |}; type State = {| +opacity: number | Value, |}; class MultimediaMessageMultimedia extends React.PureComponent { view: ?React.ElementRef; clickable = true; constructor(props: Props) { super(props); this.state = { opacity: this.getOpacity(), }; } static getStableKey(props: Props) { const { item, mediaInfo } = props; return `multimedia|${messageKey(item.messageInfo)}|${mediaInfo.index}`; } static getOverlayContext(props: Props) { const { overlayContext } = props; invariant( overlayContext, 'MultimediaMessageMultimedia should have OverlayContext', ); return overlayContext; } static getModalOverlayPosition(props: Props) { const overlayContext = MultimediaMessageMultimedia.getOverlayContext(props); const { visibleOverlays } = overlayContext; for (let overlay of visibleOverlays) { if ( overlay.routeName === MultimediaModalRouteName && overlay.presentedFrom === props.route.key && overlay.routeKey === MultimediaMessageMultimedia.getStableKey(props) ) { return overlay.position; } } return undefined; } getOpacity() { const overlayPosition = MultimediaMessageMultimedia.getModalOverlayPosition( this.props, ); if (!overlayPosition) { return 1; } return sub( 1, interpolate(overlayPosition, { inputRange: [0.1, 0.11], outputRange: [0, 1], extrapolate: Extrapolate.CLAMP, }), ); } componentDidUpdate(prevProps: Props) { const overlayPosition = MultimediaMessageMultimedia.getModalOverlayPosition( this.props, ); const prevOverlayPosition = MultimediaMessageMultimedia.getModalOverlayPosition( prevProps, ); if (overlayPosition !== prevOverlayPosition) { this.setState({ opacity: this.getOpacity() }); } const scrollIsDisabled = MultimediaMessageMultimedia.getOverlayContext(this.props) .scrollBlockingModalStatus !== 'closed'; const scrollWasDisabled = MultimediaMessageMultimedia.getOverlayContext(prevProps) .scrollBlockingModalStatus !== 'closed'; if (!scrollIsDisabled && scrollWasDisabled) { this.clickable = true; } } render() { const { opacity } = this.state; const wrapperStyles = [styles.container, { opacity }, this.props.style]; const { mediaInfo, pendingUpload, postInProgress } = this.props; return ( ); } onLayout = () => {}; viewRef = (view: ?React.ElementRef) => { this.view = view; }; onPress = () => { if (this.dismissKeyboardIfShowing()) { return; } const { view, props: { verticalBounds }, } = this; if (!view || !verticalBounds) { return; } if (!this.clickable) { return; } this.clickable = false; const overlayContext = MultimediaMessageMultimedia.getOverlayContext( this.props, ); overlayContext.setScrollBlockingModalStatus('open'); const { mediaInfo, item } = this.props; view.measure((x, y, width, height, pageX, pageY) => { const coordinates = { x: pageX, y: pageY, width, height }; this.props.navigation.navigate({ name: mediaInfo.type === 'video' ? VideoPlaybackModalRouteName : MultimediaModalRouteName, key: MultimediaMessageMultimedia.getStableKey(this.props), params: { presentedFrom: this.props.route.key, mediaInfo, item, initialCoordinates: coordinates, verticalBounds, }, }); }); }; visibleEntryIDs() { const result = ['save']; const canCreateSidebars = threadHasPermission( this.props.item.threadInfo, threadPermissions.CREATE_SIDEBARS, ); const creatorRelationship = this.props.messageCreatorUserInfo .relationshipStatus; const creatorRelationshipHasBlock = creatorRelationship && relationshipBlockedInEitherDirection(creatorRelationship); if (this.props.item.threadCreatedFromMessage) { result.push('open_sidebar'); } else if (canCreateSidebars && !creatorRelationshipHasBlock) { result.push('create_sidebar'); } return result; } onLongPress = () => { if (this.dismissKeyboardIfShowing()) { return; } const { view, props: { verticalBounds }, } = this; if (!view || !verticalBounds) { return; } if (!this.clickable) { return; } this.clickable = false; const { messageFocused, toggleMessageFocus, item, mediaInfo, verticalOffset, } = this.props; if (!messageFocused) { toggleMessageFocus(messageKey(item.messageInfo)); } const overlayContext = MultimediaMessageMultimedia.getOverlayContext( this.props, ); overlayContext.setScrollBlockingModalStatus('open'); view.measure((x, y, width, height, pageX, pageY) => { const coordinates = { x: pageX, y: pageY, width, height }; const multimediaTop = pageY; const multimediaBottom = pageY + height; const boundsTop = verticalBounds.y; const boundsBottom = verticalBounds.y + verticalBounds.height; const belowMargin = 20; const belowSpace = multimediaTooltipHeight + belowMargin; const { isViewer } = item.messageInfo.creator; const directlyAboveMargin = isViewer ? 30 : 50; const aboveMargin = verticalOffset === 0 ? directlyAboveMargin : 20; const aboveSpace = multimediaTooltipHeight + aboveMargin; let location = 'below', margin = belowMargin; if ( multimediaBottom + belowSpace > boundsBottom && multimediaTop - aboveSpace > boundsTop ) { location = 'above'; margin = aboveMargin; } this.props.navigation.navigate({ name: MultimediaTooltipModalRouteName, params: { presentedFrom: this.props.route.key, mediaInfo, item, initialCoordinates: coordinates, verticalOffset, verticalBounds, location, margin, visibleEntryIDs: this.visibleEntryIDs(), }, }); }); }; dismissKeyboardIfShowing = () => { const { keyboardState } = this.props; return !!(keyboardState && keyboardState.dismissKeyboardIfShowing()); }; } const styles = StyleSheet.create({ container: { flex: 1, overflow: 'hidden', }, expand: { flex: 1, }, }); export default React.memo( function ConnectedMultimediaMessageMultimedia(props: BaseProps) { const colors = useColors(); const keyboardState = React.useContext(KeyboardContext); const overlayContext = React.useContext(OverlayContext); const messageCreatorUserInfo = useSelector( (state) => state.userStore.userInfos[props.item.messageInfo.creator.id], ); return ( ); }, ); diff --git a/native/chat/robotext-message.react.js b/native/chat/robotext-message.react.js index 134f70b0c..780cf044c 100644 --- a/native/chat/robotext-message.react.js +++ b/native/chat/robotext-message.react.js @@ -1,223 +1,223 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View } from 'react-native'; -import { useSelector } from 'react-redux'; import { messageKey } from 'lib/shared/message-utils'; import { relationshipBlockedInEitherDirection } from 'lib/shared/relationship-utils'; import { threadHasPermission } from 'lib/shared/thread-utils'; import type { RobotextMessageInfo } from 'lib/types/message-types'; import { type ThreadInfo, threadPermissions } from 'lib/types/thread-types'; import type { UserInfo } from 'lib/types/user-types'; import { KeyboardContext } from '../keyboard/keyboard-state'; import { OverlayContext } from '../navigation/overlay-context'; import { RobotextMessageTooltipModalRouteName } from '../navigation/route-names'; import type { NavigationRoute } from '../navigation/route-names'; +import { useSelector } from '../redux/redux-utils'; import { useStyles } from '../themes/colors'; import type { VerticalBounds } from '../types/layout-types'; import type { ChatNavigationProp } from './chat.react'; import { InlineSidebar, inlineSidebarHeight } from './inline-sidebar.react'; import { InnerRobotextMessage } from './inner-robotext-message.react'; import { robotextMessageTooltipHeight } from './robotext-message-tooltip-modal.react'; import { Timestamp } from './timestamp.react'; export type ChatRobotextMessageInfoItemWithHeight = {| +itemType: 'message', +messageShapeType: 'robotext', +messageInfo: RobotextMessageInfo, +threadInfo: ThreadInfo, +startsConversation: boolean, +startsCluster: boolean, +endsCluster: boolean, +robotext: string, +threadCreatedFromMessage: ?ThreadInfo, +contentHeight: number, |}; function robotextMessageItemHeight( item: ChatRobotextMessageInfoItemWithHeight, ) { if (item.threadCreatedFromMessage) { return item.contentHeight + inlineSidebarHeight; } return item.contentHeight; } type Props = {| ...React.ElementConfig, +item: ChatRobotextMessageInfoItemWithHeight, +navigation: ChatNavigationProp<'MessageList'>, +route: NavigationRoute<'MessageList'>, +focused: boolean, +toggleFocus: (messageKey: string) => void, +verticalBounds: ?VerticalBounds, |}; function RobotextMessage(props: Props) { const { item, navigation, route, focused, toggleFocus, verticalBounds, ...viewProps } = props; let timestamp = null; if (focused || item.startsConversation) { timestamp = ( ); } const styles = useStyles(unboundStyles); let inlineSidebar = null; if (item.threadCreatedFromMessage) { inlineSidebar = ( ); } const keyboardState = React.useContext(KeyboardContext); const key = messageKey(item.messageInfo); const onPress = React.useCallback(() => { const didDismiss = keyboardState && keyboardState.dismissKeyboardIfShowing(); if (!didDismiss) { toggleFocus(key); } }, [keyboardState, toggleFocus, key]); const overlayContext = React.useContext(OverlayContext); const viewRef = React.useRef>(); const messageCreatorUserInfo: ?UserInfo = useSelector( (state) => state.userStore.userInfos[props.item.messageInfo.creator.id], ); const creatorRelationship = messageCreatorUserInfo?.relationshipStatus; const visibleEntryIDs = React.useMemo(() => { const canCreateSidebars = threadHasPermission( item.threadInfo, threadPermissions.CREATE_SIDEBARS, ); const creatorRelationshipHasBlock = creatorRelationship && relationshipBlockedInEitherDirection(creatorRelationship); if (item.threadCreatedFromMessage) { return ['open_sidebar']; } else if (canCreateSidebars && !creatorRelationshipHasBlock) { return ['create_sidebar']; } return []; }, [item.threadCreatedFromMessage, item.threadInfo, creatorRelationship]); const openRobotextTooltipModal = React.useCallback( (x, y, width, height, pageX, pageY) => { invariant( verticalBounds, 'verticalBounds should be present in openRobotextTooltipModal', ); const coordinates = { x: pageX, y: pageY, width, height }; const messageTop = pageY; const messageBottom = pageY + height; const boundsTop = verticalBounds.y; const boundsBottom = verticalBounds.y + verticalBounds.height; const belowMargin = 20; const belowSpace = robotextMessageTooltipHeight + belowMargin; const { isViewer } = item.messageInfo.creator; const aboveMargin = isViewer ? 30 : 50; const aboveSpace = robotextMessageTooltipHeight + aboveMargin; let location = 'below', margin = 0; if ( messageBottom + belowSpace > boundsBottom && messageTop - aboveSpace > boundsTop ) { location = 'above'; margin = aboveMargin; } props.navigation.navigate({ name: RobotextMessageTooltipModalRouteName, params: { presentedFrom: props.route.key, initialCoordinates: coordinates, verticalBounds, visibleEntryIDs, location, margin, item, }, }); }, [item, props.navigation, props.route.key, verticalBounds, visibleEntryIDs], ); const onLongPress = React.useCallback(() => { if (visibleEntryIDs.length === 0) { return; } if (keyboardState && keyboardState.dismissKeyboardIfShowing()) { return; } if (!viewRef.current || !verticalBounds) { return; } if (!focused) { toggleFocus(messageKey(item.messageInfo)); } invariant(overlayContext, 'RobotextMessage should have OverlayContext'); overlayContext.setScrollBlockingModalStatus('open'); viewRef.current?.measure(openRobotextTooltipModal); }, [ focused, item, keyboardState, overlayContext, toggleFocus, verticalBounds, viewRef, visibleEntryIDs, openRobotextTooltipModal, ]); const onLayout = React.useCallback(() => {}, []); return ( {timestamp} {inlineSidebar} ); } const unboundStyles = { sidebar: { marginTop: -5, marginBottom: 5, }, }; export { robotextMessageItemHeight, RobotextMessage }; diff --git a/native/chat/settings/color-picker-modal.react.js b/native/chat/settings/color-picker-modal.react.js index 18357ed67..7946fa00c 100644 --- a/native/chat/settings/color-picker-modal.react.js +++ b/native/chat/settings/color-picker-modal.react.js @@ -1,171 +1,171 @@ // @flow import * as React from 'react'; import { TouchableHighlight, Alert } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; -import { useSelector } from 'react-redux'; import { changeThreadSettingsActionTypes, changeThreadSettings, } from 'lib/actions/thread-actions'; import { type ThreadInfo, type ChangeThreadSettingsPayload, type UpdateThreadRequest, } from 'lib/types/thread-types'; import type { DispatchActionPromise } from 'lib/utils/action-utils'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import ColorPicker from '../../components/color-picker.react'; import Modal from '../../components/modal.react'; import type { RootNavigationProp } from '../../navigation/root-navigator.react'; import type { NavigationRoute } from '../../navigation/route-names'; +import { useSelector } from '../../redux/redux-utils'; import { type Colors, useStyles, useColors } from '../../themes/colors'; export type ColorPickerModalParams = {| presentedFrom: string, color: string, threadInfo: ThreadInfo, setColor: (color: string) => void, |}; type BaseProps = {| +navigation: RootNavigationProp<'ColorPickerModal'>, +route: NavigationRoute<'ColorPickerModal'>, |}; type Props = {| ...BaseProps, // Redux state +colors: Colors, +styles: typeof unboundStyles, +windowWidth: number, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +changeThreadSettings: ( request: UpdateThreadRequest, ) => Promise, |}; class ColorPickerModal extends React.PureComponent { render() { const { color, threadInfo } = this.props.route.params; // Based on the assumption we are always in portrait, // and consequently width is the lowest dimensions const modalStyle = { height: this.props.windowWidth - 5 }; return ( ); } close = () => { this.props.navigation.goBackOnce(); }; onColorSelected = (color: string) => { const colorEditValue = color.substr(1); this.props.route.params.setColor(colorEditValue); this.close(); this.props.dispatchActionPromise( changeThreadSettingsActionTypes, this.editColor(colorEditValue), { customKeyName: `${changeThreadSettingsActionTypes.started}:color` }, ); }; async editColor(newColor: string) { const threadID = this.props.route.params.threadInfo.id; try { return await this.props.changeThreadSettings({ threadID, changes: { color: newColor }, }); } catch (e) { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: this.onErrorAcknowledged }], { cancelable: false }, ); throw e; } } onErrorAcknowledged = () => { const { threadInfo, setColor } = this.props.route.params; setColor(threadInfo.color); }; } const unboundStyles = { closeButton: { borderRadius: 3, height: 18, position: 'absolute', right: 5, top: 5, width: 18, }, closeButtonIcon: { color: 'modalBackgroundSecondaryLabel', left: 3, position: 'absolute', }, colorPicker: { bottom: 10, left: 10, position: 'absolute', right: 10, top: 10, }, colorPickerContainer: { backgroundColor: 'modalBackground', borderRadius: 5, flex: 0, marginHorizontal: 15, marginVertical: 20, }, }; export default React.memo(function ConnectedColorPickerModal( props: BaseProps, ) { const styles = useStyles(unboundStyles); const colors = useColors(); const windowWidth = useSelector((state) => state.dimensions.width); const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useServerCall(changeThreadSettings); return ( ); }); diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js index c16a335dc..e2eb5ee70 100644 --- a/native/chat/text-message.react.js +++ b/native/chat/text-message.react.js @@ -1,264 +1,264 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View } from 'react-native'; -import { useSelector } from 'react-redux'; import { messageKey } from 'lib/shared/message-utils'; import { relationshipBlockedInEitherDirection } from 'lib/shared/relationship-utils'; import { threadHasPermission } from 'lib/shared/thread-utils'; import type { LocalMessageInfo } from 'lib/types/message-types'; import type { TextMessageInfo } from 'lib/types/messages/text'; import { type ThreadInfo, threadPermissions } from 'lib/types/thread-types'; import type { UserInfo } from 'lib/types/user-types'; import { type KeyboardState, KeyboardContext, } from '../keyboard/keyboard-state'; import { MarkdownLinkContext } from '../markdown/markdown-link-context'; import { OverlayContext, type OverlayContextType, } from '../navigation/overlay-context'; import type { NavigationRoute } from '../navigation/route-names'; import { TextMessageTooltipModalRouteName } from '../navigation/route-names'; +import { useSelector } from '../redux/redux-utils'; import type { VerticalBounds } from '../types/layout-types'; import type { ChatNavigationProp } from './chat.react'; import { ComposedMessage, clusterEndHeight } from './composed-message.react'; import { failedSendHeight } from './failed-send.react'; import { inlineSidebarHeight, inlineSidebarMarginBottom, inlineSidebarMarginTop, } from './inline-sidebar.react'; import { InnerTextMessage } from './inner-text-message.react'; import { authorNameHeight } from './message-header.react'; import textMessageSendFailed from './text-message-send-failed'; import { textMessageTooltipHeight } from './text-message-tooltip-modal.react'; export type ChatTextMessageInfoItemWithHeight = {| +itemType: 'message', +messageShapeType: 'text', +messageInfo: TextMessageInfo, +localMessageInfo: ?LocalMessageInfo, +threadInfo: ThreadInfo, +startsConversation: boolean, +startsCluster: boolean, +endsCluster: boolean, +contentHeight: number, +threadCreatedFromMessage: ?ThreadInfo, |}; function textMessageItemHeight(item: ChatTextMessageInfoItemWithHeight) { 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) { height += inlineSidebarHeight + inlineSidebarMarginTop + inlineSidebarMarginBottom; } return height; } type BaseProps = {| ...React.ElementConfig, +item: ChatTextMessageInfoItemWithHeight, +navigation: ChatNavigationProp<'MessageList'>, +route: NavigationRoute<'MessageList'>, +focused: boolean, +toggleFocus: (messageKey: string) => void, +verticalBounds: ?VerticalBounds, |}; type Props = {| ...BaseProps, // Redux state +messageCreatorUserInfo: UserInfo, // withKeyboardState +keyboardState: ?KeyboardState, // withOverlayContext +overlayContext: ?OverlayContextType, // MarkdownLinkContext +linkPressActive: boolean, |}; class TextMessage extends React.PureComponent { message: ?React.ElementRef; render() { const { item, navigation, route, focused, toggleFocus, verticalBounds, keyboardState, overlayContext, linkPressActive, messageCreatorUserInfo, ...viewProps } = this.props; const canSwipe = threadHasPermission( item.threadInfo, threadPermissions.VOICED, ); return ( ); } messageRef = (message: ?React.ElementRef) => { this.message = message; }; visibleEntryIDs() { const result = ['copy']; const canReply = threadHasPermission( this.props.item.threadInfo, threadPermissions.VOICED, ); const canCreateSidebars = threadHasPermission( this.props.item.threadInfo, threadPermissions.CREATE_SIDEBARS, ); const creatorRelationship = this.props.messageCreatorUserInfo .relationshipStatus; const creatorRelationshipHasBlock = creatorRelationship && relationshipBlockedInEitherDirection(creatorRelationship); if (canReply) { result.push('reply'); } if (this.props.item.threadCreatedFromMessage) { result.push('open_sidebar'); } else if (canCreateSidebars && !creatorRelationshipHasBlock) { result.push('create_sidebar'); } return result; } onPress = () => { if (this.dismissKeyboardIfShowing()) { return; } const { message, props: { verticalBounds, linkPressActive }, } = this; if (!message || !verticalBounds || linkPressActive) { return; } const { focused, toggleFocus, item } = this.props; if (!focused) { toggleFocus(messageKey(item.messageInfo)); } const { overlayContext } = this.props; invariant(overlayContext, 'TextMessage should have OverlayContext'); overlayContext.setScrollBlockingModalStatus('open'); message.measure((x, y, width, height, pageX, pageY) => { const coordinates = { x: pageX, y: pageY, width, height }; const messageTop = pageY; const messageBottom = pageY + height; const boundsTop = verticalBounds.y; const boundsBottom = verticalBounds.y + verticalBounds.height; const belowMargin = 20; const belowSpace = textMessageTooltipHeight + belowMargin; const { isViewer } = item.messageInfo.creator; const aboveMargin = isViewer ? 30 : 50; const aboveSpace = textMessageTooltipHeight + aboveMargin; let location = 'below', margin = belowMargin; if ( messageBottom + belowSpace > boundsBottom && messageTop - aboveSpace > boundsTop ) { location = 'above'; margin = aboveMargin; } this.props.navigation.navigate({ name: TextMessageTooltipModalRouteName, params: { presentedFrom: this.props.route.key, initialCoordinates: coordinates, verticalBounds, visibleEntryIDs: this.visibleEntryIDs(), location, margin, item, }, }); }); }; dismissKeyboardIfShowing = () => { const { keyboardState } = this.props; return !!(keyboardState && keyboardState.dismissKeyboardIfShowing()); }; } const ConnectedTextMessage = React.memo( function ConnectedTextMessage(props: BaseProps) { const keyboardState = React.useContext(KeyboardContext); const overlayContext = React.useContext(OverlayContext); const [linkPressActive, setLinkPressActive] = React.useState(false); const markdownLinkContext = React.useMemo( () => ({ setLinkPressActive, }), [setLinkPressActive], ); const messageCreatorUserInfo = useSelector( (state) => state.userStore.userInfos[props.item.messageInfo.creator.id], ); return ( ); }, ); export { ConnectedTextMessage as TextMessage, textMessageItemHeight };