diff --git a/app/components/markdown/at_mention/at_mention.tsx b/app/components/markdown/at_mention/at_mention.tsx
index 0f55943e4..b80d5ad36 100644
--- a/app/components/markdown/at_mention/at_mention.tsx
+++ b/app/components/markdown/at_mention/at_mention.tsx
@@ -89,7 +89,7 @@ const AtMention = ({
return user.mentionKeys;
}, [currentUserId, mentionKeys, user]);
- const goToUserProfile = useCallback(() => {
+ const goToUserProfile = () => {
const screen = 'UserProfile';
const title = intl.formatMessage({id: 'mobile.routes.user_profile', defaultMessage: 'Profile'});
const passProps = {
@@ -109,7 +109,7 @@ const AtMention = ({
};
showModal(screen, title, passProps, options);
- }, [user]);
+ };
const handleLongPress = useCallback(() => {
if (managedConfig?.copyAndPasteProtection !== 'true') {
@@ -230,4 +230,4 @@ const AtMention = ({
);
};
-export default React.memo(AtMention);
+export default AtMention;
diff --git a/app/components/markdown/channel_mention/channel_mention.tsx b/app/components/markdown/channel_mention/channel_mention.tsx
index df4b03332..b9fc20e4f 100644
--- a/app/components/markdown/channel_mention/channel_mention.tsx
+++ b/app/components/markdown/channel_mention/channel_mention.tsx
@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React, {useCallback} from 'react';
+import React from 'react';
import {useIntl} from 'react-intl';
import {StyleProp, Text, TextStyle} from 'react-native';
@@ -65,7 +65,7 @@ const ChannelMention = ({
const serverUrl = useServerUrl();
const channel = getChannelFromChannelName(channelName, channels, channelMentions, team.name);
- const handlePress = useCallback(preventDoubleTap(async () => {
+ const handlePress = preventDoubleTap(async () => {
let c = channel;
if (!c?.id && c?.display_name) {
@@ -90,7 +90,7 @@ const ChannelMention = ({
await dismissAllModals();
await popToRoot();
}
- }), [channel?.display_name, channel?.id]);
+ });
if (!channel) {
return {`~${channelName}`};
diff --git a/app/components/markdown/index.ts b/app/components/markdown/index.ts
new file mode 100644
index 000000000..5bad76742
--- /dev/null
+++ b/app/components/markdown/index.ts
@@ -0,0 +1,27 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
+import withObservables from '@nozbe/with-observables';
+import React from 'react';
+import {of as of$} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
+
+import {observeConfig} from '@queries/servers/system';
+
+import Markdown from './markdown';
+
+import type {WithDatabaseArgs} from '@typings/database/database';
+
+const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
+ const config = observeConfig(database);
+ const enableLatex = config.pipe(switchMap((c) => of$(c?.EnableLatex === 'true')));
+ const enableInlineLatex = config.pipe(switchMap((c) => of$(c?.EnableInlineLatex === 'true')));
+
+ return {
+ enableLatex,
+ enableInlineLatex,
+ };
+});
+
+export default React.memo(withDatabase(enhanced(Markdown)));
diff --git a/app/components/markdown/index.tsx b/app/components/markdown/index.tsx
deleted file mode 100644
index 0f7534124..000000000
--- a/app/components/markdown/index.tsx
+++ /dev/null
@@ -1,510 +0,0 @@
-// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
-// See LICENSE.txt for license information.
-
-import {Parser, Node} from 'commonmark';
-import Renderer from 'commonmark-react-renderer';
-import React, {PureComponent, ReactElement} from 'react';
-import {GestureResponderEvent, Platform, StyleProp, Text, TextStyle, View} from 'react-native';
-
-import Emoji from '@components/emoji';
-import FormattedText from '@components/formatted_text';
-import Hashtag from '@components/markdown/hashtag';
-import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
-import {getScheme} from '@utils/url';
-
-import AtMention from './at_mention';
-import ChannelMention, {ChannelMentions} from './channel_mention';
-import MarkdownBlockQuote from './markdown_block_quote';
-import MarkdownCodeBlock from './markdown_code_block';
-import MarkdownImage from './markdown_image';
-import MarkdownLink from './markdown_link';
-import MarkdownList from './markdown_list';
-import MarkdownListItem from './markdown_list_item';
-import MarkdownTable from './markdown_table';
-import MarkdownTableCell, {MarkdownTableCellProps} from './markdown_table_cell';
-import MarkdownTableImage from './markdown_table_image';
-import MarkdownTableRow, {MarkdownTableRowProps} from './markdown_table_row';
-import {addListItemIndices, combineTextNodes, highlightMentions, pullOutImages} from './transform';
-
-import type {
- MarkdownAtMentionRenderer, MarkdownBaseRenderer, MarkdownBlockStyles, MarkdownChannelMentionRenderer,
- MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownTextStyles, UserMentionKey,
-} from '@typings/global/markdown';
-
-type MarkdownProps = {
- autolinkedUrlSchemes?: string[];
- baseTextStyle: StyleProp;
- blockStyles: MarkdownBlockStyles;
- channelMentions?: ChannelMentions;
- disableAtMentions?: boolean;
- disableAtChannelMentionHighlight?: boolean;
- disableChannelLink?: boolean;
- disableGallery?: boolean;
- disableHashtags?: boolean;
- imagesMetadata?: Record;
- isEdited?: boolean;
- isReplyPost?: boolean;
- isSearchResult?: boolean;
- layoutWidth?: number;
- location?: string;
- mentionKeys?: UserMentionKey[];
- minimumHashtagLength?: number;
- onPostPress?: (event: GestureResponderEvent) => void;
- postId?: string;
- textStyles: MarkdownTextStyles;
- theme: Theme;
- value: string | number;
-}
-
-const getStyleSheet = makeStyleSheetFromTheme((theme) => {
- // Android has trouble giving text transparency depending on how it's nested,
- // so we calculate the resulting colour manually
- const editedOpacity = Platform.select({
- ios: 0.3,
- android: 1.0,
- });
- const editedColor = Platform.select({
- ios: theme.centerChannelColor,
- android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3),
- });
-
- return {
- block: {
- alignItems: 'flex-start',
- flexDirection: 'row',
- flexWrap: 'wrap',
- },
- editedIndicatorText: {
- color: editedColor,
- opacity: editedOpacity,
- },
- atMentionOpacity: {
- opacity: 1,
- },
- };
-});
-
-class Markdown extends PureComponent {
- static defaultProps = {
- textStyles: {},
- blockStyles: {},
- disableHashtags: false,
- disableAtMentions: false,
- disableAtChannelMentionHighlight: false,
- disableChannelLink: false,
- disableGallery: false,
- layoutWidth: undefined,
- value: '',
- minimumHashtagLength: 3,
- };
-
- private parser: Parser;
- private renderer: Renderer.Renderer;
-
- constructor(props: MarkdownProps) {
- super(props);
-
- this.parser = this.createParser();
- this.renderer = this.createRenderer();
- }
-
- createParser = () => {
- return new Parser({
- urlFilter: this.urlFilter,
- minimumHashtagLength: this.props.minimumHashtagLength,
- });
- };
-
- urlFilter = (url: string) => {
- const scheme = getScheme(url);
- return !scheme || this.props.autolinkedUrlSchemes?.indexOf(scheme) !== -1;
- };
-
- createRenderer = () => {
- const renderers: any = {
- text: this.renderText,
-
- emph: Renderer.forwardChildren,
- strong: Renderer.forwardChildren,
- del: Renderer.forwardChildren,
- code: this.renderCodeSpan,
- link: this.renderLink,
- image: this.renderImage,
- atMention: this.renderAtMention,
- channelLink: this.renderChannelLink,
- emoji: this.renderEmoji,
- hashtag: this.renderHashtag,
- latexinline: this.renderParagraph,
-
- paragraph: this.renderParagraph,
- heading: this.renderHeading,
- codeBlock: this.renderCodeBlock,
- blockQuote: this.renderBlockQuote,
-
- list: this.renderList,
- item: this.renderListItem,
-
- hardBreak: this.renderHardBreak,
- thematicBreak: this.renderThematicBreak,
- softBreak: this.renderSoftBreak,
-
- htmlBlock: this.renderHtml,
- htmlInline: this.renderHtml,
-
- table: this.renderTable,
- table_row: this.renderTableRow,
- table_cell: this.renderTableCell,
-
- mention_highlight: Renderer.forwardChildren,
-
- editedIndicator: this.renderEditedIndicator,
- };
-
- return new Renderer({
- renderers,
- renderParagraphsInLists: true,
- getExtraPropsForNode: this.getExtraPropsForNode,
- });
- };
-
- getExtraPropsForNode = (node: any) => {
- const extraProps: Record = {
- continue: node.continue,
- index: node.index,
- };
-
- if (node.type === 'image') {
- extraProps.reactChildren = node.react.children;
- extraProps.linkDestination = node.linkDestination;
- extraProps.size = node.size;
- }
-
- return extraProps;
- };
-
- computeTextStyle = (baseStyle: StyleProp, context: any) => {
- const {textStyles} = this.props;
- type TextType = keyof typeof textStyles;
- const contextStyles: TextStyle[] = context.map((type: any) => textStyles[type as TextType]).filter((f: any) => f !== undefined);
- return contextStyles.length ? concatStyles(baseStyle, contextStyles) : baseStyle;
- };
-
- renderText = ({context, literal}: MarkdownBaseRenderer) => {
- if (context.indexOf('image') !== -1) {
- // If this text is displayed, it will be styled by the image component
- return (
-
- {literal}
-
- );
- }
-
- // Construct the text style based off of the parents of this node since RN's inheritance is limited
- const style = this.computeTextStyle(this.props.baseTextStyle, context);
-
- return (
-
- {literal}
-
- );
- };
-
- renderCodeSpan = ({context, literal}: MarkdownBaseRenderer) => {
- const {baseTextStyle, textStyles: {code}} = this.props;
- return {literal};
- };
-
- renderImage = ({linkDestination, context, src, size}: MarkdownImageRenderer) => {
- if (!this.props.imagesMetadata) {
- return null;
- }
-
- if (context.indexOf('table') !== -1) {
- // We have enough problems rendering images as is, so just render a link inside of a table
- return (
-
- );
- }
-
- return (
-
- );
- };
-
- renderAtMention = ({context, mentionName}: MarkdownAtMentionRenderer) => {
- if (this.props.disableAtMentions) {
- return this.renderText({context, literal: `@${mentionName}`});
- }
-
- const style = getStyleSheet(this.props.theme);
-
- return (
-
- );
- };
-
- renderChannelLink = ({context, channelName}: MarkdownChannelMentionRenderer) => {
- if (this.props.disableChannelLink) {
- return this.renderText({context, literal: `~${channelName}`});
- }
-
- return (
-
- );
- };
-
- renderEmoji = ({context, emojiName, literal}: MarkdownEmojiRenderer) => {
- return (
-
- );
- };
-
- renderHashtag = ({context, hashtag}: {context: string[]; hashtag: string}) => {
- if (this.props.disableHashtags) {
- return this.renderText({context, literal: `#${hashtag}`});
- }
-
- return (
-
- );
- };
-
- renderParagraph = ({children, first}: {children: ReactElement[]; first: boolean}) => {
- if (!children || children.length === 0) {
- return null;
- }
-
- const style = getStyleSheet(this.props.theme);
- const blockStyle = [style.block];
- if (!first) {
- blockStyle.push(this.props.blockStyles.adjacentParagraph);
- }
-
- return (
-
-
- {children}
-
-
- );
- };
-
- renderHeading = ({children, level}: {children: ReactElement; level: string}) => {
- const {textStyles} = this.props;
- const containerStyle = [
- getStyleSheet(this.props.theme).block,
- textStyles[`heading${level}`],
- ];
- const textStyle = textStyles[`heading${level}Text`];
- return (
-
-
- {children}
-
-
- );
- };
-
- renderCodeBlock = (props: any) => {
- // These sometimes include a trailing newline
- const content = props.literal.replace(/\n$/, '');
-
- return (
-
- );
- };
-
- renderBlockQuote = ({children, ...otherProps}: any) => {
- return (
-
- {children}
-
- );
- };
-
- renderList = ({children, start, tight, type}: any) => {
- return (
-
- {children}
-
- );
- };
-
- renderListItem = ({children, context, ...otherProps}: any) => {
- const level = context.filter((type: string) => type === 'list').length;
-
- return (
-
- {children}
-
- );
- };
-
- renderHardBreak = () => {
- return {'\n'};
- };
-
- renderThematicBreak = () => {
- return (
-
- );
- };
-
- renderSoftBreak = () => {
- return {'\n'};
- };
-
- renderHtml = (props: any) => {
- let rendered = this.renderText(props);
-
- if (props.isBlock) {
- const style = getStyleSheet(this.props.theme);
-
- rendered = (
-
- {rendered}
-
- );
- }
-
- return rendered;
- };
-
- renderTable = ({children, numColumns}: {children: ReactElement; numColumns: number}) => {
- return (
-
- {children}
-
- );
- };
-
- renderTableRow = (args: MarkdownTableRowProps) => {
- return ;
- };
-
- renderTableCell = (args: MarkdownTableCellProps) => {
- return ;
- };
-
- renderLink = ({children, href}: {children: ReactElement; href: string}) => {
- return (
-
- {children}
-
- );
- };
-
- renderEditedIndicator = ({context}: {context: string[]}) => {
- let spacer = '';
- if (context[0] === 'paragraph') {
- spacer = ' ';
- }
-
- const style = getStyleSheet(this.props.theme);
- const styles = [
- this.props.baseTextStyle,
- style.editedIndicatorText,
- ];
-
- return (
-
- {spacer}
-
-
- );
- };
-
- render() {
- let ast = this.parser.parse(this.props.value.toString());
-
- ast = combineTextNodes(ast);
- ast = addListItemIndices(ast);
- ast = pullOutImages(ast);
- if (this.props.mentionKeys) {
- ast = highlightMentions(ast, this.props.mentionKeys);
- }
-
- if (this.props.isEdited) {
- const editIndicatorNode = new Node('edited_indicator');
- if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) {
- ast.appendChild(editIndicatorNode);
- } else {
- const node = new Node('paragraph');
- node.appendChild(editIndicatorNode);
-
- ast.appendChild(node);
- }
- }
-
- return this.renderer.render(ast);
- }
-}
-
-export default Markdown;
diff --git a/app/components/markdown/markdown.tsx b/app/components/markdown/markdown.tsx
new file mode 100644
index 000000000..ee99fcdb4
--- /dev/null
+++ b/app/components/markdown/markdown.tsx
@@ -0,0 +1,503 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {Parser, Node} from 'commonmark';
+import Renderer from 'commonmark-react-renderer';
+import React, {ReactElement, useRef} from 'react';
+import {Dimensions, GestureResponderEvent, Platform, StyleProp, Text, TextStyle, View} from 'react-native';
+
+import Emoji from '@components/emoji';
+import FormattedText from '@components/formatted_text';
+import Hashtag from '@components/markdown/hashtag';
+import {blendColors, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
+import {getScheme} from '@utils/url';
+
+import AtMention from './at_mention';
+import ChannelMention, {ChannelMentions} from './channel_mention';
+import MarkdownBlockQuote from './markdown_block_quote';
+import MarkdownCodeBlock from './markdown_code_block';
+import MarkdownImage from './markdown_image';
+import MarkdownLatexCodeBlock from './markdown_latex_block';
+import MarkdownLatexInline from './markdown_latex_inline';
+import MarkdownLink from './markdown_link';
+import MarkdownList from './markdown_list';
+import MarkdownListItem from './markdown_list_item';
+import MarkdownTable from './markdown_table';
+import MarkdownTableCell, {MarkdownTableCellProps} from './markdown_table_cell';
+import MarkdownTableImage from './markdown_table_image';
+import MarkdownTableRow, {MarkdownTableRowProps} from './markdown_table_row';
+import {addListItemIndices, combineTextNodes, highlightMentions, pullOutImages} from './transform';
+
+import type {
+ MarkdownAtMentionRenderer, MarkdownBaseRenderer, MarkdownBlockStyles, MarkdownChannelMentionRenderer,
+ MarkdownEmojiRenderer, MarkdownImageRenderer, MarkdownLatexRenderer, MarkdownTextStyles, UserMentionKey,
+} from '@typings/global/markdown';
+
+type MarkdownProps = {
+ autolinkedUrlSchemes?: string[];
+ baseTextStyle: StyleProp;
+ blockStyles?: MarkdownBlockStyles;
+ channelMentions?: ChannelMentions;
+ disableAtChannelMentionHighlight?: boolean;
+ disableAtMentions?: boolean;
+ disableChannelLink?: boolean;
+ disableGallery?: boolean;
+ disableHashtags?: boolean;
+ enableLatex: boolean;
+ enableInlineLatex: boolean;
+ imagesMetadata?: Record;
+ isEdited?: boolean;
+ isReplyPost?: boolean;
+ isSearchResult?: boolean;
+ layoutWidth?: number;
+ location?: string;
+ mentionKeys?: UserMentionKey[];
+ minimumHashtagLength?: number;
+ onPostPress?: (event: GestureResponderEvent) => void;
+ postId?: string;
+ textStyles?: MarkdownTextStyles;
+ theme: Theme;
+ value?: string | number;
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme) => {
+ // Android has trouble giving text transparency depending on how it's nested,
+ // so we calculate the resulting colour manually
+ const editedOpacity = Platform.select({
+ ios: 0.3,
+ android: 1.0,
+ });
+ const editedColor = Platform.select({
+ ios: theme.centerChannelColor,
+ android: blendColors(theme.centerChannelBg, theme.centerChannelColor, 0.3),
+ });
+
+ return {
+ block: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ },
+ editedIndicatorText: {
+ color: editedColor,
+ opacity: editedOpacity,
+ },
+ atMentionOpacity: {
+ opacity: 1,
+ },
+ };
+});
+
+const getExtraPropsForNode = (node: any) => {
+ const extraProps: Record = {
+ continue: node.continue,
+ index: node.index,
+ };
+
+ if (node.type === 'image') {
+ extraProps.reactChildren = node.react.children;
+ extraProps.linkDestination = node.linkDestination;
+ extraProps.size = node.size;
+ }
+
+ return extraProps;
+};
+
+const computeTextStyle = (textStyles: MarkdownTextStyles, baseStyle: StyleProp, context: any) => {
+ type TextType = keyof typeof textStyles;
+ const contextStyles: TextStyle[] = context.map((type: any) => textStyles[type as TextType]).filter((f: any) => f !== undefined);
+ return contextStyles.length ? concatStyles(baseStyle, contextStyles) : baseStyle;
+};
+
+const Markdown = ({
+ autolinkedUrlSchemes, baseTextStyle, blockStyles, channelMentions,
+ disableAtChannelMentionHighlight = false, disableAtMentions = false, disableChannelLink = false,
+ disableGallery = false, disableHashtags = false, enableInlineLatex, enableLatex,
+ imagesMetadata, isEdited, isReplyPost, isSearchResult, layoutWidth,
+ location, mentionKeys, minimumHashtagLength = 3, onPostPress, postId,
+ textStyles = {}, theme, value = '',
+}: MarkdownProps) => {
+ const style = getStyleSheet(theme);
+
+ const urlFilter = (url: string) => {
+ const scheme = getScheme(url);
+ return !scheme || autolinkedUrlSchemes?.indexOf(scheme) !== -1;
+ };
+
+ const renderAtMention = ({context, mentionName}: MarkdownAtMentionRenderer) => {
+ if (disableAtMentions) {
+ return renderText({context, literal: `@${mentionName}`});
+ }
+
+ return (
+
+ );
+ };
+
+ const renderBlockQuote = ({children, ...otherProps}: any) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const renderBreak = () => {
+ return {'\n'};
+ };
+
+ const renderChannelLink = ({context, channelName}: MarkdownChannelMentionRenderer) => {
+ if (disableChannelLink) {
+ return renderText({context, literal: `~${channelName}`});
+ }
+
+ return (
+
+ );
+ };
+
+ const renderCodeBlock = (props: any) => {
+ // These sometimes include a trailing newline
+ const content = props.literal.replace(/\n$/, '');
+
+ if (enableLatex && props.language === 'latex') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ };
+
+ const renderCodeSpan = ({context, literal}: MarkdownBaseRenderer) => {
+ const {code} = textStyles;
+ return {literal};
+ };
+
+ const renderEditedIndicator = ({context}: {context: string[]}) => {
+ let spacer = '';
+ const styles = [baseTextStyle, style.editedIndicatorText];
+
+ if (context[0] === 'paragraph') {
+ spacer = ' ';
+ }
+
+ return (
+
+ {spacer}
+
+
+ );
+ };
+
+ const renderEmoji = ({context, emojiName, literal}: MarkdownEmojiRenderer) => {
+ return (
+
+ );
+ };
+
+ const renderHashtag = ({context, hashtag}: {context: string[]; hashtag: string}) => {
+ if (disableHashtags) {
+ return renderText({context, literal: `#${hashtag}`});
+ }
+
+ return (
+
+ );
+ };
+
+ const renderHeading = ({children, level}: {children: ReactElement; level: string}) => {
+ const containerStyle = [
+ style.block,
+ textStyles[`heading${level}`],
+ ];
+ const textStyle = textStyles[`heading${level}Text`];
+ return (
+
+
+ {children}
+
+
+ );
+ };
+
+ const renderHtml = (props: any) => {
+ let rendered = renderText(props);
+
+ if (props.isBlock) {
+ rendered = (
+
+ {rendered}
+
+ );
+ }
+
+ return rendered;
+ };
+
+ const renderImage = ({linkDestination, context, src, size}: MarkdownImageRenderer) => {
+ if (!imagesMetadata) {
+ return null;
+ }
+
+ if (context.indexOf('table') !== -1) {
+ // We have enough problems rendering images as is, so just render a link inside of a table
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ };
+
+ const renderLatexInline = ({context, latexCode}: MarkdownLatexRenderer) => {
+ if (!enableInlineLatex) {
+ return renderText({context, literal: `$${latexCode}$`});
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ const renderLink = ({children, href}: {children: ReactElement; href: string}) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const renderList = ({children, start, tight, type}: any) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const renderListItem = ({children, context, ...otherProps}: any) => {
+ const level = context.filter((type: string) => type === 'list').length;
+
+ return (
+
+ {children}
+
+ );
+ };
+
+ const renderParagraph = ({children, first}: {children: ReactElement[]; first: boolean}) => {
+ if (!children || children.length === 0) {
+ return null;
+ }
+
+ const blockStyle = [style.block];
+ if (!first) {
+ blockStyle.push(blockStyles?.adjacentParagraph);
+ }
+
+ return (
+
+
+ {children}
+
+
+ );
+ };
+
+ const renderTable = ({children, numColumns}: {children: ReactElement; numColumns: number}) => {
+ return (
+
+ {children}
+
+ );
+ };
+
+ const renderTableCell = (args: MarkdownTableCellProps) => {
+ return ;
+ };
+
+ const renderTableRow = (args: MarkdownTableRowProps) => {
+ return ;
+ };
+
+ const renderText = ({context, literal}: MarkdownBaseRenderer) => {
+ if (context.indexOf('image') !== -1) {
+ // If this text is displayed, it will be styled by the image component
+ return (
+
+ {literal}
+
+ );
+ }
+
+ // Construct the text style based off of the parents of this node since RN's inheritance is limited
+ const styles = computeTextStyle(textStyles, baseTextStyle, context);
+
+ return (
+
+ {literal}
+
+ );
+ };
+
+ const renderThematicBreak = () => {
+ return (
+
+ );
+ };
+
+ const createRenderer = () => {
+ const renderers: any = {
+ text: renderText,
+
+ emph: Renderer.forwardChildren,
+ strong: Renderer.forwardChildren,
+ del: Renderer.forwardChildren,
+ code: renderCodeSpan,
+ link: renderLink,
+ image: renderImage,
+ atMention: renderAtMention,
+ channelLink: renderChannelLink,
+ emoji: renderEmoji,
+ hashtag: renderHashtag,
+ latexinline: renderLatexInline,
+
+ paragraph: renderParagraph,
+ heading: renderHeading,
+ codeBlock: renderCodeBlock,
+ blockQuote: renderBlockQuote,
+
+ list: renderList,
+ item: renderListItem,
+
+ hardBreak: renderBreak,
+ thematicBreak: renderThematicBreak,
+ softBreak: renderBreak,
+
+ htmlBlock: renderHtml,
+ htmlInline: renderHtml,
+
+ table: renderTable,
+ table_row: renderTableRow,
+ table_cell: renderTableCell,
+
+ mention_highlight: Renderer.forwardChildren,
+
+ editedIndicator: renderEditedIndicator,
+ };
+
+ return new Renderer({
+ renderers,
+ renderParagraphsInLists: true,
+ getExtraPropsForNode,
+ });
+ };
+
+ const parser = useRef(new Parser({urlFilter, minimumHashtagLength})).current;
+ const renderer = useRef(createRenderer()).current;
+ let ast = parser.parse(value.toString());
+
+ ast = combineTextNodes(ast);
+ ast = addListItemIndices(ast);
+ ast = pullOutImages(ast);
+ if (mentionKeys) {
+ ast = highlightMentions(ast, mentionKeys);
+ }
+
+ if (isEdited) {
+ const editIndicatorNode = new Node('edited_indicator');
+ if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) {
+ ast.appendChild(editIndicatorNode);
+ } else {
+ const node = new Node('paragraph');
+ node.appendChild(editIndicatorNode);
+
+ ast.appendChild(node);
+ }
+ }
+
+ return renderer.render(ast) as JSX.Element;
+};
+
+export default Markdown;
diff --git a/app/components/markdown/markdown_code_block/index.tsx b/app/components/markdown/markdown_code_block/index.tsx
index 1321e49ec..5226ad610 100644
--- a/app/components/markdown/markdown_code_block/index.tsx
+++ b/app/components/markdown/markdown_code_block/index.tsx
@@ -71,7 +71,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc
const insets = useSafeAreaInsets();
const style = getStyleSheet(theme);
- const handlePress = preventDoubleTap(() => {
+ const handlePress = useCallback(preventDoubleTap(() => {
const screen = Screens.CODE;
const passProps = {
code: content,
@@ -102,7 +102,7 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc
requestAnimationFrame(() => {
goToScreen(screen, title, passProps);
});
- });
+ }), [content, intl.locale, language]);
const handleLongPress = useCallback(() => {
if (managedConfig?.copyAndPasteProtection !== 'true') {
diff --git a/app/components/markdown/markdown_image/index.tsx b/app/components/markdown/markdown_image/index.tsx
index b5e9d3f12..58db85a5d 100644
--- a/app/components/markdown/markdown_image/index.tsx
+++ b/app/components/markdown/markdown_image/index.tsx
@@ -81,13 +81,7 @@ const MarkdownImage = ({
const originalSize = getMarkdownImageSize(isReplyPost, isTablet, sourceSize, metadata, layoutWidth);
const serverUrl = useServerUrl();
const galleryIdentifier = `${postId}-${genericFileId}-${location}`;
- const uri = useMemo(() => {
- if (source.startsWith('/')) {
- return serverUrl + source;
- }
-
- return source;
- }, [source, serverUrl]);
+ const uri = source.startsWith('/') ? serverUrl + source : source;
const fileInfo = useMemo(() => {
const link = decodeURIComponent(uri);
@@ -119,7 +113,7 @@ const MarkdownImage = ({
type: 'image',
};
openGalleryAtIndex(galleryIdentifier, 0, [item]);
- }, []);
+ }, [fileInfo]);
const {ref, onGestureEvent, styles} = useGalleryItem(
galleryIdentifier,
@@ -188,7 +182,7 @@ const MarkdownImage = ({
theme,
});
}
- }, [managedConfig, intl, insets, theme]);
+ }, [managedConfig, intl.locale, insets.bottom, theme]);
const handleOnError = useCallback(() => {
setFailed(true);
diff --git a/app/components/markdown/markdown_latex_block/index.tsx b/app/components/markdown/markdown_latex_block/index.tsx
new file mode 100644
index 000000000..55420036c
--- /dev/null
+++ b/app/components/markdown/markdown_latex_block/index.tsx
@@ -0,0 +1,222 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {useManagedConfig} from '@mattermost/react-native-emm';
+import Clipboard from '@react-native-community/clipboard';
+import React, {useCallback, useMemo} from 'react';
+import {useIntl} from 'react-intl';
+import {Keyboard, View, Text, StyleSheet, Platform} from 'react-native';
+import MathView from 'react-native-math-view';
+import {useSafeAreaInsets} from 'react-native-safe-area-context';
+
+import FormattedText from '@components/formatted_text';
+import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
+import TouchableWithFeedback from '@components/touchable_with_feedback';
+import {Screens} from '@constants';
+import {bottomSheet, dismissBottomSheet, goToScreen} from '@screens/navigation';
+import {bottomSheetSnapPoint} from '@utils/helpers';
+import {getHighlightLanguageName} from '@utils/markdown';
+import {splitLatexCodeInLines} from '@utils/markdown/latex';
+import {preventDoubleTap} from '@utils/tap';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+import {typography} from '@utils/typography';
+
+const MAX_LINES = 2;
+
+type Props = {
+ content: string;
+ theme: Theme;
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
+ const codeVerticalPadding = Platform.select({
+ ios: 4,
+ android: 0,
+ });
+
+ return {
+ bottomSheet: {
+ flex: 1,
+ },
+ container: {
+ borderColor: changeOpacity(theme.centerChannelColor, 0.15),
+ borderRadius: 3,
+ borderWidth: StyleSheet.hairlineWidth,
+ flexDirection: 'row',
+ flex: 1,
+ },
+ rightColumn: {
+ flexDirection: 'column',
+ flex: 1,
+ paddingLeft: 6,
+ paddingVertical: 4,
+ },
+ code: {
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ marginLeft: 5,
+ paddingVertical: codeVerticalPadding,
+ },
+ plusMoreLinesText: {
+ color: changeOpacity(theme.centerChannelColor, 0.4),
+ ...typography('Body', 50),
+ marginTop: 2,
+ },
+ language: {
+ alignItems: 'center',
+ backgroundColor: theme.sidebarHeaderBg,
+ justifyContent: 'center',
+ opacity: 0.8,
+ padding: 6,
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ },
+ languageText: {
+ color: theme.sidebarHeaderTextColor,
+ ...typography('Body', 75),
+ },
+ errorText: {
+ ...typography('Body', 100),
+ marginHorizontal: 5,
+ color: theme.errorTextColor,
+ },
+ };
+});
+
+const LatexCodeBlock = ({content, theme}: Props) => {
+ const intl = useIntl();
+ const insets = useSafeAreaInsets();
+ const managedConfig = useManagedConfig();
+ const styles = getStyleSheet(theme);
+ const languageDisplayName = getHighlightLanguageName('latex');
+
+ const split = useMemo(() => {
+ const lines = splitLatexCodeInLines(content);
+ const numberOfLines = lines.length;
+
+ if (numberOfLines > MAX_LINES) {
+ return {
+ content: lines.slice(0, MAX_LINES),
+ numberOfLines,
+ };
+ }
+
+ return {
+ lines,
+ numberOfLines,
+ };
+ }, [content]);
+
+ const handlePress = useCallback(preventDoubleTap(() => {
+ const screen = Screens.LATEX;
+ const passProps = {
+ content,
+ };
+ const title = intl.formatMessage({
+ id: 'mobile.routes.code',
+ defaultMessage: '{language} Code',
+ }, {
+ language: languageDisplayName,
+ });
+
+ Keyboard.dismiss();
+ requestAnimationFrame(() => {
+ goToScreen(screen, title, passProps);
+ });
+ }), [content, languageDisplayName, intl.locale]);
+
+ const handleLongPress = useCallback(() => {
+ if (managedConfig?.copyAndPasteProtection !== 'true') {
+ const renderContent = () => {
+ return (
+
+ {
+ dismissBottomSheet();
+ Clipboard.setString(content);
+ }}
+ testID='at_mention.bottom_sheet.copy_code'
+ text={intl.formatMessage({id: 'mobile.markdown.code.copy_code', defaultMessage: 'Copy Code'})}
+ />
+
+
+ );
+ };
+
+ bottomSheet({
+ closeButtonId: 'close-code-block',
+ renderContent,
+ snapPoints: [bottomSheetSnapPoint(2, ITEM_HEIGHT, insets.bottom), 10],
+ title: intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}),
+ theme,
+ });
+ }
+ }, [managedConfig?.copyAndPasteProtection, intl, insets, theme]);
+
+ const onRenderErrorMessage = useCallback(({error}: {error: Error}) => {
+ return {'Render error: ' + error.message};
+ }, []);
+
+ let plusMoreLines = null;
+ if (split.numberOfLines > MAX_LINES) {
+ plusMoreLines = (
+
+ );
+ }
+
+ /**
+ * Note on the error behavior of math view:
+ * - onError returns an Error object
+ * - renderError returns an options object with an error attribute that contains the real Error.
+ */
+ return (
+
+
+
+ {split.lines?.map((latexCode) => (
+
+
+
+ ))}
+ {plusMoreLines}
+
+
+
+ {languageDisplayName}
+
+
+
+
+ );
+};
+
+export default LatexCodeBlock;
diff --git a/app/components/markdown/markdown_latex_inline/index.tsx b/app/components/markdown/markdown_latex_inline/index.tsx
new file mode 100644
index 000000000..b7f532901
--- /dev/null
+++ b/app/components/markdown/markdown_latex_inline/index.tsx
@@ -0,0 +1,59 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {Platform, Text, View} from 'react-native';
+import MathView from 'react-native-math-view';
+
+import {makeStyleSheetFromTheme} from '@utils/theme';
+
+type Props = {
+ content: string;
+ maxMathWidth: number | string;
+ theme: Theme;
+}
+
+type MathViewErrorProps = {
+ error: Error;
+}
+
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
+ return {
+ mathStyle: {
+ marginBottom: Platform.select({default: -10, ios: 2.5}),
+ },
+ viewStyle: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ },
+ errorText: {
+ flexDirection: 'row',
+ color: theme.errorTextColor,
+ flexWrap: 'wrap',
+ },
+ };
+});
+
+const LatexInline = ({content, maxMathWidth, theme}: Props) => {
+ const style = getStyleSheet(theme);
+
+ const onRenderErrorMessage = (errorMsg: MathViewErrorProps) => {
+ return {'Latex render error: ' + errorMsg.error.message};
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default LatexInline;
diff --git a/app/components/markdown/markdown_link/markdown_link.tsx b/app/components/markdown/markdown_link/markdown_link.tsx
index fcec54183..774436819 100644
--- a/app/components/markdown/markdown_link/markdown_link.tsx
+++ b/app/components/markdown/markdown_link/markdown_link.tsx
@@ -36,6 +36,19 @@ const style = StyleSheet.create({
},
});
+const parseLinkLiteral = (literal: string) => {
+ let nextLiteral = literal;
+
+ const WWW_REGEX = /\b^(?:www.)/i;
+ if (nextLiteral.match(WWW_REGEX)) {
+ nextLiteral = literal.replace(WWW_REGEX, 'www.');
+ }
+
+ const parsed = urlParse(nextLiteral, {});
+
+ return parsed.href;
+};
+
const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteURL}: MarkdownLinkProps) => {
const intl = useIntl();
const insets = useSafeAreaInsets();
@@ -45,7 +58,7 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
const {formatMessage} = intl;
- const handlePress = preventDoubleTap(async () => {
+ const handlePress = useCallback(preventDoubleTap(async () => {
const url = normalizeProtocol(href);
if (!url) {
@@ -80,22 +93,9 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
tryOpenURL(url, onError);
}
- });
+ }), [href, intl.locale, serverUrl, siteURL]);
- const parseLinkLiteral = (literal: string) => {
- let nextLiteral = literal;
-
- const WWW_REGEX = /\b^(?:www.)/i;
- if (nextLiteral.match(WWW_REGEX)) {
- nextLiteral = literal.replace(WWW_REGEX, 'www.');
- }
-
- const parsed = urlParse(nextLiteral, {});
-
- return parsed.href;
- };
-
- const parseChildren = () => {
+ const parseChildren = useCallback(() => {
return Children.map(children, (child: ReactElement) => {
if (!child.props.literal || typeof child.props.literal !== 'string' || (child.props.context && child.props.context.length && !child.props.context.includes('link'))) {
return child;
@@ -115,7 +115,7 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
...otherChildProps,
};
});
- };
+ }, [children]);
const handleLongPress = useCallback(() => {
if (managedConfig?.copyAndPasteProtection !== 'true') {
diff --git a/app/components/markdown/markdown_table_image/index.tsx b/app/components/markdown/markdown_table_image/index.tsx
index 505d82eee..db7edfb1c 100644
--- a/app/components/markdown/markdown_table_image/index.tsx
+++ b/app/components/markdown/markdown_table_image/index.tsx
@@ -86,7 +86,7 @@ const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL,
type: 'image',
};
openGalleryAtIndex(galleryIdentifier, 0, [item]);
- }, []);
+ }, [metadata, source, serverURL, currentServerUrl, postId]);
const {ref, onGestureEvent, styles} = useGalleryItem(
galleryIdentifier,
diff --git a/app/constants/screens.ts b/app/constants/screens.ts
index 60511ed12..66916aa74 100644
--- a/app/constants/screens.ts
+++ b/app/constants/screens.ts
@@ -27,6 +27,7 @@ export const HOME = 'Home';
export const INTEGRATION_SELECTOR = 'IntegrationSelector';
export const INTERACTIVE_DIALOG = 'InteractiveDialog';
export const IN_APP_NOTIFICATION = 'InAppNotification';
+export const LATEX = 'Latex';
export const LOGIN = 'Login';
export const MENTIONS = 'Mentions';
export const MFA = 'MFA';
@@ -71,6 +72,7 @@ export default {
INTEGRATION_SELECTOR,
INTERACTIVE_DIALOG,
IN_APP_NOTIFICATION,
+ LATEX,
LOGIN,
MENTIONS,
MFA,
diff --git a/app/screens/index.tsx b/app/screens/index.tsx
index 6f67957ce..d73626697 100644
--- a/app/screens/index.tsx
+++ b/app/screens/index.tsx
@@ -132,6 +132,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
);
return;
}
+ case Screens.LATEX:
+ screen = withServerDatabase(require('@screens/latex').default);
+ break;
case Screens.LOGIN:
screen = withIntl(require('@screens/login').default);
break;
diff --git a/app/screens/latex/index.tsx b/app/screens/latex/index.tsx
new file mode 100644
index 000000000..80da7b039
--- /dev/null
+++ b/app/screens/latex/index.tsx
@@ -0,0 +1,99 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import React from 'react';
+import {Platform, ScrollView, Text, View} from 'react-native';
+import MathView from 'react-native-math-view';
+import {SafeAreaView, Edge} from 'react-native-safe-area-context';
+
+import {useTheme} from '@context/theme';
+import {splitLatexCodeInLines} from '@utils/markdown/latex';
+import {makeStyleSheetFromTheme} from '@utils/theme';
+import {typography} from '@utils/typography';
+
+type Props = {
+ content: string;
+}
+
+const edges: Edge[] = ['left', 'right'];
+
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
+ const codeVerticalPadding = Platform.select({
+ ios: 4,
+ android: 0,
+ });
+
+ return {
+ scrollContainer: {
+ flex: 1,
+ },
+ container: {
+ minHeight: '100%',
+ },
+ scrollCode: {
+ minHeight: '100%',
+ flexDirection: 'column',
+ paddingLeft: 10,
+ paddingVertical: 10,
+ },
+ code: {
+ flexDirection: 'row',
+ justifyContent: 'flex-start',
+ marginHorizontal: 5,
+ paddingVertical: codeVerticalPadding,
+ },
+ errorText: {
+ ...typography('Body', 100),
+ marginHorizontal: 5,
+ color: theme.errorTextColor,
+ },
+ };
+});
+
+const Latex = ({content}: Props) => {
+ const theme = useTheme();
+ const style = getStyleSheet(theme);
+ const lines = splitLatexCodeInLines(content);
+
+ const onErrorMessage = (errorMsg: Error) => {
+ return {'Error: ' + errorMsg.message};
+ };
+
+ const onRenderErrorMessage = ({error}: {error: Error}) => {
+ return {'Render error: ' + error.message};
+ };
+
+ return (
+
+
+
+ {lines.map((latexCode) => (
+
+
+
+ ))}
+
+
+
+ );
+};
+
+export default Latex;
diff --git a/app/utils/markdown/latex.test.ts b/app/utils/markdown/latex.test.ts
new file mode 100644
index 000000000..10413cd66
--- /dev/null
+++ b/app/utils/markdown/latex.test.ts
@@ -0,0 +1,66 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+/* eslint-disable no-useless-escape */
+
+import {splitLatexCodeInLines} from './latex';
+
+describe('LatexUtilTest', () => {
+ test('Simple lines test', () => {
+ const content = '\\frac{1}{2} = 0.5 \\\\ \\pi == 3';
+
+ const result = splitLatexCodeInLines(content);
+
+ expect(result.length).toEqual(2);
+ expect(result[0]).toEqual('\\frac{1}{2} = 0.5');
+ expect(result[1]).toEqual('\\pi == 3');
+ });
+
+ test('Multi line with cases test', () => {
+ const content = `b_n=\\frac{1}{\\pi}\\int\\limits_{-\\pi}^{\\pi}f(x)\\sin nx\\,\\mathrm{d}x=\\frac{1}{\\pi}\\int\\limits_{-\\pi}^{\\pi}x^2\\sin nx\\,\\mathrm{d}x\\\\
+X(m, n) = \\left.
+\\begin{cases}
+x(n), & \\text{for } 0 \\leq n \\leq 1 \\\\
+x(n - 1), & \\text{for } 0 \\leq n \\leq 1 \\\\
+x(n - 1), & \\text{for } 0 \\leq n \\leq 1
+\\end{cases} \\right\\} = xy\\\\
+\\lim_{a\\to \\infty} \\tfrac{1}{a}\\\\
+\\lim_{a \\underset{>}{\\to} 0} \\frac{1}{a}\\\\
+x = a_0 + \\frac{1}{a_1 + \\frac{1}{a_2 + \\frac{1}{a_3 + a_4}}}`;
+
+ const result = splitLatexCodeInLines(content);
+
+ expect(result.length).toEqual(5);
+ expect(result[0]).toEqual('b_n=\\frac{1}{\\pi}\\int\\limits_{-\\pi}^{\\pi}f(x)\\sin nx\\,\\mathrm{d}x=\\frac{1}{\\pi}\\int\\limits_{-\\pi}^{\\pi}x^2\\sin nx\\,\\mathrm{d}x');
+ expect(result[1]).toEqual(`X(m, n) = \\left.
+\\begin{cases}
+x(n), & \\text{for } 0 \\leq n \\leq 1 \\\\
+x(n - 1), & \\text{for } 0 \\leq n \\leq 1 \\\\
+x(n - 1), & \\text{for } 0 \\leq n \\leq 1
+\\end{cases} \\right\\} = xy`);
+ expect(result[2]).toEqual('\\lim_{a\\to \\infty} \\tfrac{1}{a}');
+ expect(result[3]).toEqual('\\lim_{a \\underset{>}{\\to} 0} \\frac{1}{a}');
+ expect(result[4]).toEqual('x = a_0 + \\frac{1}{a_1 + \\frac{1}{a_2 + \\frac{1}{a_3 + a_4}}}');
+ });
+
+ test('Escaped bracket test', () => {
+ const content = 'test = \\frac{1\\{}{2} = \\alpha \\\\ line = 2';
+
+ const result = splitLatexCodeInLines(content);
+
+ expect(result.length).toEqual(2);
+ expect(result[0]).toEqual('test = \\frac{1\\{}{2} = \\alpha');
+ expect(result[1]).toEqual('line = 2');
+ });
+
+ test('Escaped begin and end statement', () => {
+ const content = 'test = \\\\begin \\\\ line = 2';
+
+ const result = splitLatexCodeInLines(content);
+
+ expect(result.length).toEqual(3);
+ expect(result[0]).toEqual('test =');
+ expect(result[1]).toEqual('begin');
+ expect(result[2]).toEqual('line = 2');
+ });
+});
diff --git a/app/utils/markdown/latex.ts b/app/utils/markdown/latex.ts
new file mode 100644
index 000000000..4a939d8fe
--- /dev/null
+++ b/app/utils/markdown/latex.ts
@@ -0,0 +1,97 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+/**
+ * Splits up latex code in an array consisting of each line of latex code.
+ * A latex linebreak is denoted by `\\`.
+ * A line is not broken in 2 cases:
+ * - The linebreak is in between brackets.
+ * - The linebreak occurs inbetween a `\begin` and `\end` statement.
+ */
+
+export function splitLatexCodeInLines(content: string): string[] {
+ let outLines = content.split('\\\\');
+
+ let i = 0;
+ while (i < outLines.length) {
+ if (testLatexLineBreak(outLines[i])) { //Line has no linebreak in between brackets
+ i += 1;
+ } else if (i < outLines.length - 2) {
+ outLines = outLines.slice(0, i).concat([outLines[i] + '\\\\' + outLines[i + 1]], outLines.slice(i + 2));
+ } else if (i === outLines.length - 2) {
+ outLines = outLines.slice(0, i).concat([outLines[i] + '\\\\' + outLines[i + 1]]);
+ } else {
+ break;
+ }
+ }
+
+ return outLines.map((line) => line.trim());
+}
+
+function testLatexLineBreak(latexCode: string): boolean {
+ /**
+ * These checks are special because they need to check if the braces and statements are not escaped
+ */
+
+ //Check for cases
+ let beginCases = 0;
+ let endCases = 0;
+
+ let i = 0;
+ while (i < latexCode.length) {
+ const firstBegin = latexCode.indexOf('\\begin{', i);
+ const firstEnd = latexCode.indexOf('\\end{', i);
+
+ if (firstBegin === -1 && firstEnd === -1) {
+ break;
+ }
+
+ if (firstBegin !== -1 && (firstBegin < firstEnd || firstEnd === -1)) {
+ if (latexCode[firstBegin - 1] !== '\\') {
+ beginCases += 1;
+ }
+ i = firstBegin + '\\begin{'.length;
+ } else {
+ if (latexCode[firstEnd - 1] !== '\\') {
+ endCases += 1;
+ }
+ i = firstEnd + '\\end{'.length;
+ }
+ }
+
+ if (beginCases !== endCases) {
+ return false;
+ }
+
+ //Check for braces
+ let curlyOpenCases = 0;
+ let curlyCloseCases = 0;
+
+ i = 0;
+ while (i < latexCode.length) {
+ const firstBegin = latexCode.indexOf('{', i);
+ const firstEnd = latexCode.indexOf('}', i);
+
+ if (firstBegin === -1 && firstEnd === -1) {
+ break;
+ }
+
+ if (firstBegin !== -1 && (firstBegin < firstEnd || firstEnd === -1)) {
+ if (latexCode[firstBegin - 1] !== '\\') {
+ curlyOpenCases += 1;
+ }
+ i = firstBegin + 1;
+ } else {
+ if (latexCode[firstEnd - 1] !== '\\') {
+ curlyCloseCases += 1;
+ }
+ i = firstEnd + 1;
+ }
+ }
+
+ if (curlyOpenCases !== curlyCloseCases) {
+ return false;
+ }
+
+ return true;
+}
diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj
index 274da6bef..9c603f7d8 100644
--- a/ios/Mattermost.xcodeproj/project.pbxproj
+++ b/ios/Mattermost.xcodeproj/project.pbxproj
@@ -747,6 +747,7 @@
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle",
"${PODS_ROOT}/YoutubePlayer-in-WKWebView/WKYTPlayerView/WKYTPlayerView.bundle",
+ "${PODS_CONFIGURATION_BUILD_DIR}/iosMath/mathFonts.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
@@ -768,6 +769,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/WKYTPlayerView.bundle",
+ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/mathFonts.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index e96c861f4..8c0cc0987 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -78,6 +78,7 @@ PODS:
- glog (0.3.5)
- hermes-engine (0.11.0)
- HMSegmentedControl (1.5.6)
+ - iosMath (0.9.4)
- jail-monkey (2.6.0):
- React-Core
- libevent (2.1.12)
@@ -469,6 +470,8 @@ PODS:
- React-Core
- RNLocalize (2.2.1):
- React-Core
+ - RNMathView (1.0.0):
+ - iosMath
- RNPermissions (3.3.1):
- React-Core
- RNReactNativeHapticFeedback (1.13.1):
@@ -632,6 +635,7 @@ DEPENDENCIES:
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNLocalize (from `../node_modules/react-native-localize`)
+ - RNMathView (from `../node_modules/react-native-math-view/ios`)
- RNPermissions (from `../node_modules/react-native-permissions`)
- RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`)
- RNReanimated (from `../node_modules/react-native-reanimated`)
@@ -664,6 +668,7 @@ SPEC REPOS:
- fmt
- hermes-engine
- HMSegmentedControl
+ - iosMath
- libevent
- libwebp
- OpenSSL-Universal
@@ -811,6 +816,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-keychain"
RNLocalize:
:path: "../node_modules/react-native-localize"
+ RNMathView:
+ :path: "../node_modules/react-native-math-view/ios"
RNPermissions:
:path: "../node_modules/react-native-permissions"
RNReactNativeHapticFeedback:
@@ -865,6 +872,7 @@ SPEC CHECKSUMS:
glog: 476ee3e89abb49e07f822b48323c51c57124b572
hermes-engine: 84e3af1ea01dd7351ac5d8689cbbea1f9903ffc3
HMSegmentedControl: 34c1f54d822d8308e7b24f5d901ec674dfa31352
+ iosMath: f7a6cbadf9d836d2149c2a84c435b1effc244cba
jail-monkey: 07b83767601a373db876e939b8dbf3f5eb15f073
libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
@@ -928,6 +936,7 @@ SPEC CHECKSUMS:
RNGestureHandler: 6e757e487a4834e7280e98e9bac66d2d9c575e9c
RNKeychain: 4f63aada75ebafd26f4bc2c670199461eab85d94
RNLocalize: cbcb55d0e19c78086ea4eea20e03fe8000bbbced
+ RNMathView: 4c8a3c081fa671ab3136c51fa0bdca7ffb708bd5
RNPermissions: 34d678157c800b25b22a488e4d8babb57456e796
RNReactNativeHapticFeedback: 4085973f5a38b40d3c6793a3ee5724773eae045e
RNReanimated: 89a32ebf01d2dac2eb35b3b1628952f626db93d7
diff --git a/jest.config.js b/jest.config.js
index a081ff4b8..6e83d3adf 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -20,6 +20,6 @@ module.exports = {
'/dist/assets/images/video_player/$1@2x.png',
},
transformIgnorePatterns: [
- 'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|@react-native-community/cameraroll|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*)',
+ 'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|@react-native-community/cameraroll|react-clone-referenced-element|@react-native-community|react-navigation|@react-navigation/.*|validator|react-syntax-highlighter/.*|hast-util-from-selector|hastscript|property-information|hast-util-parse-selector|space-separated-tokens|comma-separated-tokens|zwitch)',
],
};
diff --git a/package-lock.json b/package-lock.json
index e22672f88..a64b2e1b0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -71,6 +71,7 @@
"react-native-keychain": "8.0.0",
"react-native-linear-gradient": "2.5.6",
"react-native-localize": "2.2.1",
+ "react-native-math-view": "3.9.5",
"react-native-navigation": "7.26.0",
"react-native-neomorph-shadows": "1.1.2",
"react-native-notifications": "4.2.4",
@@ -8896,6 +8897,11 @@
"url": "https://github.com/sponsors/fb55"
}
},
+ "node_modules/css-selector-parser": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz",
+ "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g=="
+ },
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
@@ -11988,6 +11994,76 @@
"minimalistic-assert": "^1.0.1"
}
},
+ "node_modules/hast-util-from-selector": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-from-selector/-/hast-util-from-selector-2.0.0.tgz",
+ "integrity": "sha512-ynzm+z7xEecWF8DvnJ5onpGIsfmXphKRsZUnWCfinKwP+fL1/2mYW1nWOVife61syQpF74j4h/57BT6e5niDwA==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "css-selector-parser": "^1.0.0",
+ "hastscript": "^7.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-from-selector/node_modules/comma-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/hast-util-from-selector/node_modules/hast-util-parse-selector": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz",
+ "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==",
+ "dependencies": {
+ "@types/hast": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-from-selector/node_modules/hastscript": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.0.2.tgz",
+ "integrity": "sha512-uA8ooUY4ipaBvKcMuPehTAB/YfFLSSzCwFSwT6ltJbocFUKH/GDHLN+tflq7lSRf9H86uOuxOFkh1KgIy3Gg2g==",
+ "dependencies": {
+ "@types/hast": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^3.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
+ "node_modules/hast-util-from-selector/node_modules/property-information": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
+ "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/hast-util-from-selector/node_modules/space-separated-tokens": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz",
+ "integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
"node_modules/hast-util-parse-selector": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
@@ -15806,6 +15882,17 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
},
+ "node_modules/mathjax-full": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.0.tgz",
+ "integrity": "sha512-D2EBNvUG+mJyhn+M1C858k0f2Fc4KxXvbEX2WCMXroV10212JwfYqaBJ336ECBSz5X9L5LRoamxb7AJtg3KaJA==",
+ "dependencies": {
+ "esm": "^3.2.25",
+ "mhchemparser": "^4.1.0",
+ "mj-context-menu": "^0.6.1",
+ "speech-rule-engine": "^3.3.3"
+ }
+ },
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -16752,6 +16839,11 @@
"node": ">=6"
}
},
+ "node_modules/mhchemparser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.1.1.tgz",
+ "integrity": "sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA=="
+ },
"node_modules/micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@@ -16886,6 +16978,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/mj-context-menu": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz",
+ "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA=="
+ },
"node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@@ -19244,6 +19341,21 @@
}
}
},
+ "node_modules/react-native-math-view": {
+ "version": "3.9.5",
+ "resolved": "https://registry.npmjs.org/react-native-math-view/-/react-native-math-view-3.9.5.tgz",
+ "integrity": "sha512-UJxrisNafszfqIW+utoSDylb72SkZ92cKz1IfE5Dm0s+uIaHxOxepF2DdRbktAV8c0FEFllnXfErcGdh8sfIBw==",
+ "dependencies": {
+ "hast-util-from-selector": "^2.0.0",
+ "lodash": "^4.17.21",
+ "mathjax-full": "^3.1.4",
+ "transformation-matrix": "^2.8.0"
+ },
+ "peerDependencies": {
+ "react-native": "*",
+ "react-native-svg": "*"
+ }
+ },
"node_modules/react-native-navigation": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-7.26.0.tgz",
@@ -20832,6 +20944,27 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/speech-rule-engine": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.3.3.tgz",
+ "integrity": "sha512-0exWw+0XauLjat+f/aFeo5T8SiDsO1JtwpY3qgJE4cWt+yL/Stl0WP4VNDWdh7lzGkubUD9lWP4J1ASnORXfyQ==",
+ "dependencies": {
+ "commander": ">=7.0.0",
+ "wicked-good-xpath": "^1.3.0",
+ "xmldom-sre": "^0.1.31"
+ },
+ "bin": {
+ "sre": "bin/sre"
+ }
+ },
+ "node_modules/speech-rule-engine/node_modules/commander": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz",
+ "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w==",
+ "engines": {
+ "node": "^12.20.0 || >=14"
+ }
+ },
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
@@ -21712,6 +21845,14 @@
"node": ">=8"
}
},
+ "node_modules/transformation-matrix": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.11.1.tgz",
+ "integrity": "sha512-srlkTrmetYwTBJ1RHdukkwJ8S8D+2JjgSb1DbvmTwj+DsIpCpRYHbWgOXe/Ql2rX37WqlKLIgidpYlHPGsrwgA==",
+ "funding": {
+ "url": "https://www.paypal.me/chrvadala/25"
+ }
+ },
"node_modules/traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
@@ -23147,6 +23288,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/wicked-good-xpath": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz",
+ "integrity": "sha1-gbDpXoZQ5JyUsiKY//hoa1VTz2w="
+ },
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
@@ -23357,6 +23503,14 @@
"node": ">=10.0.0"
}
},
+ "node_modules/xmldom-sre": {
+ "version": "0.1.31",
+ "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz",
+ "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw==",
+ "engines": {
+ "node": ">=0.1"
+ }
+ },
"node_modules/xregexp": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-5.1.0.tgz",
@@ -23504,6 +23658,15 @@
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+ },
+ "node_modules/zwitch": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
+ "integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
}
},
"dependencies": {
@@ -30100,6 +30263,11 @@
"nth-check": "^2.0.1"
}
},
+ "css-selector-parser": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-1.4.1.tgz",
+ "integrity": "sha512-HYPSb7y/Z7BNDCOrakL4raGO2zltZkbeXyAd6Tg9obzix6QhzxCotdBl6VT0Dv4vZfJGVz3WL/xaEI9Ly3ul0g=="
+ },
"css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
@@ -32482,6 +32650,54 @@
"minimalistic-assert": "^1.0.1"
}
},
+ "hast-util-from-selector": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hast-util-from-selector/-/hast-util-from-selector-2.0.0.tgz",
+ "integrity": "sha512-ynzm+z7xEecWF8DvnJ5onpGIsfmXphKRsZUnWCfinKwP+fL1/2mYW1nWOVife61syQpF74j4h/57BT6e5niDwA==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "css-selector-parser": "^1.0.0",
+ "hastscript": "^7.0.0",
+ "zwitch": "^2.0.0"
+ },
+ "dependencies": {
+ "comma-separated-tokens": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz",
+ "integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg=="
+ },
+ "hast-util-parse-selector": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.0.tgz",
+ "integrity": "sha512-AyjlI2pTAZEOeu7GeBPZhROx0RHBnydkQIXlhnFzDi0qfXTmGUWoCYZtomHbrdrheV4VFUlPcfJ6LMF5T6sQzg==",
+ "requires": {
+ "@types/hast": "^2.0.0"
+ }
+ },
+ "hastscript": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.0.2.tgz",
+ "integrity": "sha512-uA8ooUY4ipaBvKcMuPehTAB/YfFLSSzCwFSwT6ltJbocFUKH/GDHLN+tflq7lSRf9H86uOuxOFkh1KgIy3Gg2g==",
+ "requires": {
+ "@types/hast": "^2.0.0",
+ "comma-separated-tokens": "^2.0.0",
+ "hast-util-parse-selector": "^3.0.0",
+ "property-information": "^6.0.0",
+ "space-separated-tokens": "^2.0.0"
+ }
+ },
+ "property-information": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
+ "integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w=="
+ },
+ "space-separated-tokens": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz",
+ "integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw=="
+ }
+ }
+ },
"hast-util-parse-selector": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
@@ -35353,6 +35569,17 @@
}
}
},
+ "mathjax-full": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.0.tgz",
+ "integrity": "sha512-D2EBNvUG+mJyhn+M1C858k0f2Fc4KxXvbEX2WCMXroV10212JwfYqaBJ336ECBSz5X9L5LRoamxb7AJtg3KaJA==",
+ "requires": {
+ "esm": "^3.2.25",
+ "mhchemparser": "^4.1.0",
+ "mj-context-menu": "^0.6.1",
+ "speech-rule-engine": "^3.3.3"
+ }
+ },
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -36165,6 +36392,11 @@
"nullthrows": "^1.1.1"
}
},
+ "mhchemparser": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.1.1.tgz",
+ "integrity": "sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA=="
+ },
"micromatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@@ -36271,6 +36503,11 @@
"is-extendable": "^1.0.1"
}
},
+ "mj-context-menu": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz",
+ "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA=="
+ },
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@@ -38195,6 +38432,17 @@
"integrity": "sha512-BuPaQWvxLZG1NrCDGqgAnecDrNQu3LED9/Pyl4H2LwTMHcEngXpE5PfVntW2GiLumdr6nUOkWmMnh8PynZqrsw==",
"requires": {}
},
+ "react-native-math-view": {
+ "version": "3.9.5",
+ "resolved": "https://registry.npmjs.org/react-native-math-view/-/react-native-math-view-3.9.5.tgz",
+ "integrity": "sha512-UJxrisNafszfqIW+utoSDylb72SkZ92cKz1IfE5Dm0s+uIaHxOxepF2DdRbktAV8c0FEFllnXfErcGdh8sfIBw==",
+ "requires": {
+ "hast-util-from-selector": "^2.0.0",
+ "lodash": "^4.17.21",
+ "mathjax-full": "^3.1.4",
+ "transformation-matrix": "^2.8.0"
+ }
+ },
"react-native-navigation": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-7.26.0.tgz",
@@ -39371,6 +39619,23 @@
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
"integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="
},
+ "speech-rule-engine": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.3.3.tgz",
+ "integrity": "sha512-0exWw+0XauLjat+f/aFeo5T8SiDsO1JtwpY3qgJE4cWt+yL/Stl0WP4VNDWdh7lzGkubUD9lWP4J1ASnORXfyQ==",
+ "requires": {
+ "commander": ">=7.0.0",
+ "wicked-good-xpath": "^1.3.0",
+ "xmldom-sre": "^0.1.31"
+ },
+ "dependencies": {
+ "commander": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz",
+ "integrity": "sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w=="
+ }
+ }
+ },
"split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
@@ -40071,6 +40336,11 @@
"punycode": "^2.1.1"
}
},
+ "transformation-matrix": {
+ "version": "2.11.1",
+ "resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.11.1.tgz",
+ "integrity": "sha512-srlkTrmetYwTBJ1RHdukkwJ8S8D+2JjgSb1DbvmTwj+DsIpCpRYHbWgOXe/Ql2rX37WqlKLIgidpYlHPGsrwgA=="
+ },
"traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
@@ -41222,6 +41492,11 @@
"is-typed-array": "^1.1.7"
}
},
+ "wicked-good-xpath": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz",
+ "integrity": "sha1-gbDpXoZQ5JyUsiKY//hoa1VTz2w="
+ },
"wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
@@ -41386,6 +41661,11 @@
"resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz",
"integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg=="
},
+ "xmldom-sre": {
+ "version": "0.1.31",
+ "resolved": "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz",
+ "integrity": "sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw=="
+ },
"xregexp": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/xregexp/-/xregexp-5.1.0.tgz",
@@ -41498,6 +41778,11 @@
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.14.2.tgz",
"integrity": "sha512-iF+wrtzz7fQfkmn60PG6XFxaWBhYYKzp2i+nv24WbLUWb2JjymdkHlzBwP0erpc78WotwP5g9AAu7Sk8GWVVNw=="
+ },
+ "zwitch": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.2.tgz",
+ "integrity": "sha512-JZxotl7SxAJH0j7dN4pxsTV6ZLXoLdGME+PsjkL/DaBrVryK9kTGq06GfKrwcSOqypP+fdXGoCHE36b99fWVoA=="
}
}
}
diff --git a/package.json b/package.json
index 4a9515c74..dfeb1f027 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
"react-native-keychain": "8.0.0",
"react-native-linear-gradient": "2.5.6",
"react-native-localize": "2.2.1",
+ "react-native-math-view": "3.9.5",
"react-native-navigation": "7.26.0",
"react-native-neomorph-shadows": "1.1.2",
"react-native-notifications": "4.2.4",
diff --git a/types/api/config.d.ts b/types/api/config.d.ts
index 73f8bb96c..3639862dc 100644
--- a/types/api/config.d.ts
+++ b/types/api/config.d.ts
@@ -63,6 +63,7 @@ interface ClientConfig {
EnableGuestAccounts: string;
EnableIncomingWebhooks: string;
EnableLatex: string;
+ EnableInlineLatex: string;
EnableLdap: string;
EnableLinkPreviews: string;
EnableMarketplace: string;
diff --git a/types/global/markdown.ts b/types/global/markdown.ts
index f3c814623..f76b89d9f 100644
--- a/types/global/markdown.ts
+++ b/types/global/markdown.ts
@@ -46,3 +46,7 @@ export type MarkdownImageRenderer = {
height?: number;
};
}
+
+export type MarkdownLatexRenderer = MarkdownBaseRenderer & {
+ latexCode: string;
+}
diff --git a/types/modules/react-native-math-view.d.ts b/types/modules/react-native-math-view.d.ts
new file mode 100644
index 000000000..2aa15b59c
--- /dev/null
+++ b/types/modules/react-native-math-view.d.ts
@@ -0,0 +1,4 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+declare module 'react-native-math-view';