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';