2022-06-27 21:27:22 +00:00
import React , { useCallback , useEffect , useReducer , useRef } from 'react' ;
2022-02-17 15:27:01 +00:00
import { Image , StyleProp , Text , TextStyle } from 'react-native' ;
2022-05-10 17:40:08 +00:00
import { Parser } from 'commonmark' ;
2019-08-27 12:25:38 +00:00
import Renderer from 'commonmark-react-renderer' ;
2021-10-20 16:32:58 +00:00
import { MarkdownAST } from '@rocket.chat/message-parser' ;
2019-08-27 12:25:38 +00:00
import MarkdownLink from './Link' ;
import MarkdownList from './List' ;
import MarkdownListItem from './ListItem' ;
import MarkdownAtMention from './AtMention' ;
import MarkdownHashtag from './Hashtag' ;
import MarkdownBlockQuote from './BlockQuote' ;
import MarkdownEmoji from './Emoji' ;
import MarkdownTable from './Table' ;
2022-06-27 21:27:22 +00:00
import MarkdownTableRow , { ITableRow } from './TableRow' ;
import MarkdownTableCell , { ITableCell } from './TableCell' ;
2020-02-28 16:18:03 +00:00
import mergeTextNodes from './mergeTextNodes' ;
2019-08-27 12:25:38 +00:00
import styles from './styles' ;
2022-06-27 21:27:22 +00:00
import { isValidURL } from '../../lib/methods/helpers' ;
2021-10-20 16:32:58 +00:00
import NewMarkdown from './new' ;
2022-02-17 15:27:01 +00:00
import { formatText } from './formatText' ;
import { IUserMention , IUserChannel , TOnLinkPress } from './interfaces' ;
2022-06-27 21:27:22 +00:00
import { TGetCustomEmoji } from '../../definitions' ;
2022-02-17 15:27:01 +00:00
import { formatHyperlink } from './formatHyperlink' ;
2022-06-27 21:27:22 +00:00
import { useTheme } from '../../theme' ;
import { IRoomInfoParam } from '../../views/SearchMessagesView' ;
2022-02-17 15:27:01 +00:00
export { default as MarkdownPreview } from './Preview' ;
2021-10-20 16:32:58 +00:00
2022-06-27 21:27:22 +00:00
export interface IMarkdownProps {
2022-04-14 20:30:41 +00:00
msg? : string | null ;
2022-02-17 15:27:01 +00:00
md? : MarkdownAST ;
mentions? : IUserMention [ ] ;
getCustomEmoji? : TGetCustomEmoji ;
baseUrl? : string ;
username? : string ;
tmid? : string ;
numberOfLines? : number ;
customEmojis? : boolean ;
useRealName? : boolean ;
channels? : IUserChannel [ ] ;
enableMessageParser? : boolean ;
2022-06-27 21:27:22 +00:00
navToRoomInfo ? : ( params : IRoomInfoParam ) = > void ;
2022-02-17 15:27:01 +00:00
testID? : string ;
style? : StyleProp < TextStyle > [ ] ;
onLinkPress? : TOnLinkPress ;
2021-09-13 20:41:05 +00:00
}
type TLiteral = {
literal : string ;
} ;
2019-08-27 12:25:38 +00:00
const emojiRanges = [
'\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]' , // unicode emoji from https://www.regextester.com/106421
':.{1,40}:' , // custom emoji
' |\n' // allow spaces and line breaks
] . join ( '|' ) ;
2021-09-13 20:41:05 +00:00
const removeSpaces = ( str : string ) = > str && str . replace ( /\s/g , '' ) ;
2019-11-27 20:53:14 +00:00
2021-09-13 20:41:05 +00:00
const removeAllEmoji = ( str : string ) = > str . replace ( new RegExp ( emojiRanges , 'g' ) , '' ) ;
2019-08-27 12:25:38 +00:00
2021-09-13 20:41:05 +00:00
const isOnlyEmoji = ( str : string ) = > {
2019-11-27 20:53:14 +00:00
str = removeSpaces ( str ) ;
return ! removeAllEmoji ( str ) . length ;
} ;
2019-08-27 12:25:38 +00:00
2021-09-13 20:41:05 +00:00
const removeOneEmoji = ( str : string ) = > str . replace ( new RegExp ( emojiRanges ) , '' ) ;
2019-08-27 12:25:38 +00:00
2021-09-13 20:41:05 +00:00
const emojiCount = ( str : string ) = > {
2019-11-27 20:53:14 +00:00
str = removeSpaces ( str ) ;
2019-08-27 12:25:38 +00:00
let oldLength = 0 ;
let counter = 0 ;
while ( oldLength !== str . length ) {
oldLength = str . length ;
str = removeOneEmoji ( str ) ;
if ( oldLength !== str . length ) {
counter += 1 ;
}
}
return counter ;
} ;
2020-02-17 19:06:18 +00:00
const parser = new Parser ( ) ;
2022-06-27 21:27:22 +00:00
export const markdownTestID = 'markdown' ;
const Markdown = ( {
msg ,
md ,
mentions ,
getCustomEmoji ,
baseUrl ,
username ,
tmid ,
numberOfLines ,
customEmojis ,
useRealName ,
channels ,
enableMessageParser ,
navToRoomInfo ,
style ,
onLinkPress
} : IMarkdownProps ) = > {
const { colors } = useTheme ( ) ;
const renderer = useRef < any > ( ) ;
const isMessageContainsOnlyEmoji = useRef ( false ) ;
const [ , forceUpdate ] = useReducer ( x = > x + 1 , 0 ) ;
const isNewMarkdown = useCallback ( ( ) = > ! ! enableMessageParser && ! ! md , [ enableMessageParser , md ] ) ;
const createRenderer = useCallback (
( ) = >
new Renderer ( {
renderers : {
text : renderText ,
emph : Renderer.forwardChildren ,
strong : Renderer.forwardChildren ,
del : Renderer.forwardChildren ,
code : renderCodeInline ,
link : renderLink ,
image : renderImage ,
atMention : renderAtMention ,
emoji : renderEmoji ,
hashtag : renderHashtag ,
paragraph : renderParagraph ,
heading : renderHeading ,
codeBlock : renderCodeBlock ,
blockQuote : renderBlockQuote ,
list : renderList ,
item : renderListItem ,
hardBreak : renderBreak ,
thematicBreak : renderBreak ,
softBreak : renderBreak ,
htmlBlock : renderText ,
htmlInline : renderText ,
table : renderTable ,
table_row : renderTableRow ,
table_cell : renderTableCell
} ,
renderParagraphsInLists : true
} ) ,
[ ]
) ;
useEffect ( ( ) = > {
if ( ! isNewMarkdown ( ) && msg ) {
renderer . current = createRenderer ( ) ;
forceUpdate ( ) ;
2021-10-20 16:32:58 +00:00
}
2022-06-27 21:27:22 +00:00
} , [ createRenderer , isNewMarkdown , msg ] ) ;
2019-08-27 12:25:38 +00:00
2022-06-27 21:27:22 +00:00
if ( ! msg ) {
return null ;
2021-10-20 16:32:58 +00:00
}
2022-06-27 21:27:22 +00:00
const renderText = ( { context , literal } : { context : [ ] ; literal : string } ) = > {
const defaultStyle = [ isMessageContainsOnlyEmoji . current ? styles . textBig : { } , . . . context . map ( type = > styles [ type ] ) ] ;
2019-12-04 16:39:53 +00:00
return (
2022-06-27 21:27:22 +00:00
< Text accessibilityLabel = { literal } style = { [ styles . text , defaultStyle , . . . ( style || [ ] ) ] } numberOfLines = { numberOfLines } >
2019-12-04 16:39:53 +00:00
{ literal }
< / Text >
) ;
2019-10-02 12:41:51 +00:00
} ;
2019-08-27 12:25:38 +00:00
2022-06-27 21:27:22 +00:00
const renderCodeInline = ( { literal } : TLiteral ) = > (
< Text
testID = { ` ${ markdownTestID } -code-in-line ` }
style = { [
{
. . . styles . codeInline ,
color : colors.bodyText ,
backgroundColor : colors.bannerBackground ,
borderColor : colors.bannerBackground
} ,
. . . ( style || [ ] )
] } >
{ literal }
< / Text >
) ;
const renderCodeBlock = ( { literal } : TLiteral ) = > (
< Text
testID = { ` ${ markdownTestID } -code-block ` }
style = { [
{
. . . styles . codeBlock ,
color : colors.bodyText ,
backgroundColor : colors.bannerBackground ,
borderColor : colors.bannerBackground
} ,
. . . ( style || [ ] )
] } >
{ literal }
< / Text >
) ;
const renderBreak = ( ) = > < Text > { tmid ? ' ' : '\n' } < / Text > ;
const renderParagraph = ( { children } : { children : React.ReactElement [ ] } ) = > {
2019-08-27 12:25:38 +00:00
if ( ! children || children . length === 0 ) {
return null ;
}
return (
2022-06-27 21:27:22 +00:00
< Text style = { [ styles . text , style , { color : colors.bodyText } ] } numberOfLines = { numberOfLines } >
2019-10-02 12:41:51 +00:00
{ children }
< / Text >
2019-08-27 12:25:38 +00:00
) ;
} ;
2022-06-27 21:27:22 +00:00
const renderLink = ( { children , href } : { children : React.ReactElement | null ; href : string } ) = > (
< MarkdownLink link = { href } onLinkPress = { onLinkPress } testID = { markdownTestID } >
{ children }
< / MarkdownLink >
) ;
const renderHashtag = ( { hashtag } : { hashtag : string } ) = > (
< MarkdownHashtag hashtag = { hashtag } channels = { channels } navToRoomInfo = { navToRoomInfo } style = { style } testID = { markdownTestID } / >
) ;
const renderAtMention = ( { mentionName } : { mentionName : string } ) = > (
< MarkdownAtMention
mentions = { mentions }
mention = { mentionName }
useRealName = { useRealName }
username = { username }
navToRoomInfo = { navToRoomInfo }
style = { style }
testID = { markdownTestID }
/ >
) ;
const renderEmoji = ( { literal } : TLiteral ) = > (
< MarkdownEmoji
literal = { literal }
isMessageContainsOnlyEmoji = { isMessageContainsOnlyEmoji . current }
getCustomEmoji = { getCustomEmoji }
baseUrl = { baseUrl || '' }
customEmojis = { customEmojis }
style = { style }
testID = { markdownTestID }
/ >
) ;
const renderImage = ( { src } : { src : string } ) = > {
2020-03-20 16:26:50 +00:00
if ( ! isValidURL ( src ) ) {
return null ;
}
2022-06-27 21:27:22 +00:00
return < Image style = { styles . inlineImage } source = { { uri : encodeURI ( src ) } } testID = { ` ${ markdownTestID } -image ` } / > ;
2021-09-13 20:41:05 +00:00
} ;
2019-08-27 12:25:38 +00:00
2022-06-27 21:27:22 +00:00
const renderHeading = ( { children , level } : { children : React.ReactElement ; level : string } ) = > {
2022-03-21 20:44:06 +00:00
// @ts-ignore
2021-09-13 20:41:05 +00:00
const textStyle = styles [ ` heading ${ level } Text ` ] ;
2019-08-27 12:25:38 +00:00
return (
2022-06-27 21:27:22 +00:00
< Text testID = { ` ${ markdownTestID } -header ` } numberOfLines = { numberOfLines } style = { [ textStyle , { color : colors.bodyText } ] } >
2019-08-27 12:25:38 +00:00
{ children }
< / Text >
) ;
} ;
2022-06-27 21:27:22 +00:00
const renderList = ( { children , start , tight , type } : any ) = > (
< MarkdownList ordered = { type !== 'bullet' } start = { start } tight = { tight } numberOfLines = { numberOfLines } >
{ children }
< / MarkdownList >
) ;
2019-08-27 12:25:38 +00:00
2022-06-27 21:27:22 +00:00
const renderListItem = ( { children , context , . . . otherProps } : any ) = > {
2021-09-13 20:41:05 +00:00
const level = context . filter ( ( type : string ) = > type === 'list' ) . length ;
2019-08-27 12:25:38 +00:00
return (
2022-06-27 21:27:22 +00:00
< MarkdownListItem level = { level } { ...otherProps } >
2019-08-27 12:25:38 +00:00
{ children }
< / MarkdownListItem >
) ;
} ;
2022-06-27 21:27:22 +00:00
const renderBlockQuote = ( { children } : { children : React.ReactElement } ) = > (
< MarkdownBlockQuote > { children } < / MarkdownBlockQuote >
) ;
2019-08-27 12:25:38 +00:00
2022-06-27 21:27:22 +00:00
const renderTable = ( { children , numColumns } : { children : React.ReactElement ; numColumns : number } ) = > (
< MarkdownTable numColumns = { numColumns } testID = { markdownTestID } >
{ children }
< / MarkdownTable >
) ;
2019-08-27 12:25:38 +00:00
2022-06-27 21:27:22 +00:00
const renderTableRow = ( { children , isLastRow } : ITableRow ) = > (
< MarkdownTableRow isLastRow = { isLastRow } > { children } < / MarkdownTableRow >
) ;
2019-08-27 12:25:38 +00:00
2022-06-27 21:27:22 +00:00
const renderTableCell = ( { align , children , isLastCell } : ITableCell ) = > (
< MarkdownTableCell align = { align } isLastCell = { isLastCell } >
{ children }
< / MarkdownTableCell >
) ;
2021-10-20 16:32:58 +00:00
2022-06-27 21:27:22 +00:00
if ( isNewMarkdown ( ) ) {
return (
< NewMarkdown
username = { username || '' }
baseUrl = { baseUrl || '' }
getCustomEmoji = { getCustomEmoji }
useRealName = { useRealName }
tokens = { md }
mentions = { mentions }
channels = { channels }
navToRoomInfo = { navToRoomInfo }
onLinkPress = { onLinkPress }
/ >
) ;
2019-08-27 12:25:38 +00:00
}
2019-12-04 16:39:53 +00:00
2022-06-27 21:27:22 +00:00
const formattedMessage = formatHyperlink ( formatText ( msg ) ) ;
const ast = mergeTextNodes ( parser . parse ( formattedMessage ) ) ;
isMessageContainsOnlyEmoji . current = isOnlyEmoji ( formattedMessage ) && emojiCount ( formattedMessage ) <= 3 ;
return renderer ? . current ? . render ( ast ) || null ;
} ;
2019-12-04 16:39:53 +00:00
export default Markdown ;