diff --git a/native/chat/inner-text-message.react.js b/native/chat/inner-text-message.react.js index 22e7bf4ab..e2855b570 100644 --- a/native/chat/inner-text-message.react.js +++ b/native/chat/inner-text-message.react.js @@ -1,146 +1,153 @@ // @flow import * as React from 'react'; import { View, StyleSheet, TouchableWithoutFeedback } from 'react-native'; import Animated from 'react-native-reanimated'; import { colorIsDark } from 'lib/shared/thread-utils'; import GestureTouchableOpacity from '../components/gesture-touchable-opacity.react'; import Markdown from '../markdown/markdown.react'; import { useSelector } from '../redux/redux-utils'; import { useColors, colors } from '../themes/colors'; import type { ChatTextMessageInfoItemWithHeight } from '../types/chat-types'; import { useComposedMessageMaxWidth } from './composed-message-width'; import { useTextMessageMarkdownRules } from './message-list-types'; import { allCorners, filterCorners, getRoundedContainerStyle, } from './rounded-corners'; +import { + TextMessageMarkdownContext, + useTextMessageMarkdown, +} from './text-message-markdown-context'; /* eslint-disable import/no-named-as-default-member */ const { Node } = Animated; /* eslint-enable import/no-named-as-default-member */ function dummyNodeForTextMessageHeightMeasurement( text: string, ): React.Element { return {text}; } type DummyTextNodeProps = { ...React.ElementConfig, +children: string, }; function DummyTextNode(props: DummyTextNodeProps): React.Node { const { children, style, ...rest } = props; const maxWidth = useComposedMessageMaxWidth(); const viewStyle = [props.style, styles.dummyMessage, { maxWidth }]; const rules = useTextMessageMarkdownRules(false); return ( {children} ); } type Props = { +item: ChatTextMessageInfoItemWithHeight, +onPress: () => void, +messageRef?: (message: ?React.ElementRef) => void, +threadColorOverride?: ?Node, +isThreadColorDarkOverride?: ?boolean, }; function InnerTextMessage(props: Props): React.Node { const { item } = props; const { text, creator } = item.messageInfo; const { isViewer } = creator; const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const boundColors = useColors(); const messageStyle = {}; let darkColor; if (isViewer) { const threadColor = item.threadInfo.color; messageStyle.backgroundColor = props.threadColorOverride ?? `#${threadColor}`; darkColor = props.isThreadColorDarkOverride ?? colorIsDark(threadColor); } else { messageStyle.backgroundColor = boundColors.listChatBubble; darkColor = activeTheme === 'dark'; } const cornerStyle = getRoundedContainerStyle(filterCorners(allCorners, item)); if (!__DEV__) { // We don't force view height in dev mode because we // want to measure it in Message to see if it's correct messageStyle.height = item.contentHeight; } const rules = useTextMessageMarkdownRules(darkColor); + const textMessageMarkdown = useTextMessageMarkdown(item.messageInfo); const markdownStyles = React.useMemo(() => { const textStyle = { color: darkColor ? colors.dark.listForegroundLabel : colors.light.listForegroundLabel, }; return [styles.text, textStyle]; }, [darkColor]); const message = ( - - - - - {text} - - - - + + + + + + {text} + + + + + ); // We need to set onLayout in order to allow .measure() to be on the ref const onLayout = React.useCallback(() => {}, []); const { messageRef } = props; if (!messageRef) { return message; } return ( {message} ); } const styles = StyleSheet.create({ dummyMessage: { paddingHorizontal: 12, paddingVertical: 6, }, message: { overflow: 'hidden', paddingHorizontal: 12, paddingVertical: 6, }, text: { fontFamily: 'Arial', fontSize: 18, }, }); export { InnerTextMessage, dummyNodeForTextMessageHeightMeasurement }; diff --git a/native/chat/text-message-markdown-context.js b/native/chat/text-message-markdown-context.js new file mode 100644 index 000000000..4c6d9acc7 --- /dev/null +++ b/native/chat/text-message-markdown-context.js @@ -0,0 +1,45 @@ +// @flow + +import * as React from 'react'; +import * as SimpleMarkdown from 'simple-markdown'; + +import type { SingleASTNode } from 'lib/shared/markdown'; +import { messageKey } from 'lib/shared/message-utils'; +import type { TextMessageInfo } from 'lib/types/messages/text'; + +import { useTextMessageMarkdownRules } from '../chat/message-list-types'; + +export type TextMessageMarkdownContextType = { + +messageKey: string, + +markdownAST: $ReadOnlyArray, +}; + +const TextMessageMarkdownContext: React.Context = React.createContext( + null, +); + +function useTextMessageMarkdown( + messageInfo: TextMessageInfo, +): TextMessageMarkdownContextType { + // useDarkStyle doesn't affect the AST (only the styles), + // so we can safely just set it to false here + const rules = useTextMessageMarkdownRules(false); + const { simpleMarkdownRules, container } = rules; + + const { text } = messageInfo; + const ast = React.useMemo(() => { + const parser = SimpleMarkdown.parserFor(simpleMarkdownRules); + return parser(text, { disableAutoBlockNewlines: true, container }); + }, [simpleMarkdownRules, text, container]); + + const key = messageKey(messageInfo); + return React.useMemo( + () => ({ + messageKey: key, + markdownAST: ast, + }), + [key, ast], + ); +} + +export { TextMessageMarkdownContext, useTextMessageMarkdown }; diff --git a/native/markdown/markdown.react.js b/native/markdown/markdown.react.js index 672d2cf6e..0046700c5 100644 --- a/native/markdown/markdown.react.js +++ b/native/markdown/markdown.react.js @@ -1,76 +1,81 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import type { TextStyle as FlattenedTextStyle } from 'react-native/Libraries/StyleSheet/StyleSheet'; import * as SimpleMarkdown from 'simple-markdown'; import { onlyEmojiRegex } from 'lib/shared/emojis'; +import { TextMessageMarkdownContext } from '../chat/text-message-markdown-context'; import type { TextStyle } from '../types/styles'; import type { MarkdownRules } from './rules.react'; type Props = { +style: TextStyle, +children: string, +rules: MarkdownRules, }; function Markdown(props: Props): React.Node { const { style, children, rules } = props; const { simpleMarkdownRules, emojiOnlyFactor, container } = rules; - const parser = React.useMemo( - () => SimpleMarkdown.parserFor(simpleMarkdownRules), - [simpleMarkdownRules], - ); - const ast = React.useMemo( - () => parser(children, { disableAutoBlockNewlines: true, container }), - [parser, children, container], + const textMessageMarkdownContext = React.useContext( + TextMessageMarkdownContext, ); + const textMessageMarkdownAST = textMessageMarkdownContext?.markdownAST; + + const ast = React.useMemo(() => { + if (textMessageMarkdownAST) { + return textMessageMarkdownAST; + } + const parser = SimpleMarkdown.parserFor(simpleMarkdownRules); + return parser(children, { disableAutoBlockNewlines: true, container }); + }, [textMessageMarkdownAST, simpleMarkdownRules, children, container]); const output = React.useMemo( () => SimpleMarkdown.outputFor(simpleMarkdownRules, 'react'), [simpleMarkdownRules], ); const emojiOnly = React.useMemo(() => { if (emojiOnlyFactor === null || emojiOnlyFactor === undefined) { return false; } return onlyEmojiRegex.test(children); }, [emojiOnlyFactor, children]); const textStyle = React.useMemo(() => { if ( !emojiOnly || emojiOnlyFactor === null || emojiOnlyFactor === undefined ) { return style; } const flattened: FlattenedTextStyle = (StyleSheet.flatten(style): any); invariant( flattened && typeof flattened === 'object', `Markdown component should have style`, ); const { fontSize } = flattened; invariant( fontSize, `style prop should have fontSize if using emojiOnlyFactor`, ); return { ...flattened, fontSize: fontSize * emojiOnlyFactor }; }, [emojiOnly, style, emojiOnlyFactor]); const renderedOutput = React.useMemo( () => output(ast, { textStyle, container }), [ast, output, textStyle, container], ); if (container === 'Text') { return {renderedOutput}; } else { return {renderedOutput}; } } export default Markdown;