import Icon from '@components/Icon' import openLink from '@components/openLink' import ParseEmojis from '@components/Parse/Emojis' import CustomText from '@components/Text' import { useNavigation, useRoute } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import { TabLocalStackParamList } from '@utils/navigation/navigators' import { getSettingsFontsize } from '@utils/slices/settingsSlice' import { StyleConstants } from '@utils/styles/constants' import layoutAnimation from '@utils/styles/layoutAnimation' import { adaptiveScale } from '@utils/styles/scaling' import { useTheme } from '@utils/styles/ThemeManager' import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { Platform, Pressable, View } from 'react-native' import HTMLView from 'react-native-htmlview' import { useSelector } from 'react-redux' // Prevent going to the same hashtag multiple times const renderNode = ({ routeParams, colors, node, index, adaptedFontsize, adaptedLineheight, navigation, mentions, tags, showFullLink, disableDetails }: { routeParams?: any colors: any node: any index: number adaptedFontsize: number adaptedLineheight: number navigation: StackNavigationProp mentions?: Mastodon.Mention[] tags?: Mastodon.Tag[] showFullLink: boolean disableDetails: boolean }) => { switch (node.name) { case 'a': const classes = node.attribs.class const href = node.attribs.href if (classes) { if (classes.includes('hashtag')) { const tag = href?.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/)) const differentTag = routeParams?.hashtag ? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2] : true return ( { !disableDetails && differentTag && navigation.push('Tab-Shared-Hashtag', { hashtag: tag[1] || tag[2] }) }} > {node.children[0].data} {node.children[1]?.children[0].data} ) } else if (classes.includes('mention') && mentions) { const accountIndex = mentions.findIndex(mention => mention.url === href) const differentAccount = routeParams?.account ? routeParams.account.id !== mentions[accountIndex]?.id : true return ( { accountIndex !== -1 && !disableDetails && differentAccount && navigation.push('Tab-Shared-Account', { account: mentions[accountIndex] }) }} > {node.children[0].data} {node.children[1]?.children[0].data} ) } } else { const domain = href?.split(new RegExp(/:\/\/(.[^\/]+)/)) // Need example here const content = node.children && node.children[0] && node.children[0].data const shouldBeTag = tags && tags.filter(tag => `#${tag.name}` === content).length > 0 return ( { if (!disableDetails) { if (shouldBeTag) { navigation.push('Tab-Shared-Hashtag', { hashtag: content.substring(1) }) } else { await openLink(href, navigation) } } }} > {(content && content !== href && content) || (showFullLink ? href : domain[1])} {!shouldBeTag ? ( ) : null} ) } break case 'p': if (!node.children.length) { return // bug when the tag is empty } break } } export interface Props { content: string size?: 'S' | 'M' | 'L' adaptiveSize?: boolean emojis?: Mastodon.Emoji[] mentions?: Mastodon.Mention[] tags?: Mastodon.Tag[] showFullLink?: boolean numberOfLines?: number expandHint?: string highlighted?: boolean disableDetails?: boolean selectable?: boolean setSpoilerExpanded?: React.Dispatch> } const ParseHTML = React.memo( ({ content, size = 'M', adaptiveSize = false, emojis, mentions, tags, showFullLink = false, numberOfLines = 10, expandHint, highlighted = false, disableDetails = false, selectable = false, setSpoilerExpanded }: Props) => { const adaptiveFontsize = useSelector(getSettingsFontsize) const adaptedFontsize = adaptiveScale( StyleConstants.Font.Size[size], adaptiveSize ? adaptiveFontsize : 0 ) const adaptedLineheight = adaptiveScale( StyleConstants.Font.LineHeight[size], adaptiveSize ? adaptiveFontsize : 0 ) const navigation = useNavigation>() const route = useRoute() const { colors, theme } = useTheme() const { t, i18n } = useTranslation('componentParse') if (!expandHint) { expandHint = t('HTML.defaultHint') } if (disableDetails) { numberOfLines = 4 } const renderNodeCallback = useCallback( (node: any, index: any) => renderNode({ routeParams: route.params, colors, node, index, adaptedFontsize, adaptedLineheight, navigation, mentions, tags, showFullLink, disableDetails }), [] ) const textComponent = useCallback(({ children }: any) => { if (children) { return ( ) } else { return null } }, []) const rootComponent = useCallback( ({ children }: any) => { const { t } = useTranslation('componentParse') const [totalLines, setTotalLines] = useState() const [expanded, setExpanded] = useState(highlighted) return ( {(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? ( { layoutAnimation() setExpanded(!expanded) if (setSpoilerExpanded) { setSpoilerExpanded(!expanded) } }} style={{ flexDirection: 'row', justifyContent: 'center', alignItems: 'center', minHeight: 44, backgroundColor: colors.backgroundDefault }} > 1 && typeof totalLines === 'number' ? t('HTML.moreLines', { count: totalLines - numberOfLines }) : '' })} /> ) : null} { if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) { setTotalLines(nativeEvent.lines.length) } }} style={{ height: numberOfLines === 1 && !expanded ? 0 : undefined }} numberOfLines={ typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined } selectable={selectable} /> ) }, [theme, i18n.language] ) return ( ) }, (prev, next) => prev.content === next.content ) export default ParseHTML