tooot/src/components/Parse/HTML.tsx

272 lines
9.7 KiB
TypeScript
Raw Normal View History

import Icon from '@components/Icon'
2021-01-01 16:48:16 +01:00
import openLink from '@components/openLink'
2021-01-01 17:52:14 +01:00
import ParseEmojis from '@components/Parse/Emojis'
2021-01-10 02:12:14 +01:00
import { useNavigation, useRoute } from '@react-navigation/native'
2021-01-24 02:25:43 +01:00
import { StackNavigationProp } from '@react-navigation/stack'
2022-04-29 23:57:18 +02:00
import { TabLocalStackParamList } from '@utils/navigation/navigators'
2022-12-26 01:06:33 +01:00
import { useFollowedTagsQuery } from '@utils/queryHooks/tags'
2021-03-10 10:22:53 +01:00
import { getSettingsFontsize } from '@utils/slices/settingsSlice'
2021-03-10 11:49:14 +01:00
import { StyleConstants } from '@utils/styles/constants'
2021-01-12 00:12:44 +01:00
import layoutAnimation from '@utils/styles/layoutAnimation'
2021-03-10 11:49:14 +01:00
import { adaptiveScale } from '@utils/styles/scaling'
2021-01-01 16:48:16 +01:00
import { useTheme } from '@utils/styles/ThemeManager'
2022-12-26 01:06:33 +01:00
import { ChildNode } from 'domhandler'
import { ElementType, parseDocument } from 'htmlparser2'
2022-12-11 14:08:27 +01:00
import { isEqual } from 'lodash'
2022-12-26 01:06:33 +01:00
import React, { useState } from 'react'
2021-01-19 01:13:45 +01:00
import { useTranslation } from 'react-i18next'
2022-12-26 01:06:33 +01:00
import { Pressable, Text, TextStyleIOS, View } from 'react-native'
2021-03-10 10:22:53 +01:00
import { useSelector } from 'react-redux'
2020-11-23 00:07:32 +01:00
2020-10-31 21:04:46 +01:00
export interface Props {
content: string
2021-02-01 02:16:53 +01:00
size?: 'S' | 'M' | 'L'
textStyles?: TextStyleIOS
2021-03-10 10:22:53 +01:00
adaptiveSize?: boolean
emojis?: Mastodon.Emoji[]
mentions?: Mastodon.Mention[]
2020-12-28 17:30:20 +01:00
tags?: Mastodon.Tag[]
2020-10-31 21:04:46 +01:00
showFullLink?: boolean
2020-12-01 00:44:28 +01:00
numberOfLines?: number
2020-12-26 12:59:16 +01:00
expandHint?: string
2021-05-19 23:28:01 +02:00
highlighted?: boolean
2021-01-04 18:29:02 +01:00
disableDetails?: boolean
2021-05-23 22:40:42 +02:00
selectable?: boolean
2022-12-03 20:47:11 +01:00
setSpoilerExpanded?: React.Dispatch<React.SetStateAction<boolean>>
2020-10-31 21:04:46 +01:00
}
2021-03-13 17:56:57 +01:00
const ParseHTML = React.memo(
({
content,
size = 'M',
textStyles,
2021-03-13 17:56:57 +01:00
adaptiveSize = false,
emojis,
mentions,
tags,
showFullLink = false,
numberOfLines = 10,
expandHint,
2021-05-19 23:28:01 +02:00
highlighted = false,
2021-05-23 22:40:42 +02:00
disableDetails = false,
2022-12-03 20:47:11 +01:00
selectable = false,
setSpoilerExpanded
2021-03-13 17:56:57 +01:00
}: 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
)
2021-03-10 10:22:53 +01:00
2022-10-10 23:28:40 +02:00
const navigation = useNavigation<StackNavigationProp<TabLocalStackParamList>>()
2022-12-26 01:06:33 +01:00
const { params } = useRoute()
const { colors } = useTheme()
2022-12-14 23:37:41 +01:00
const { t } = useTranslation('componentParse')
2021-03-13 17:56:57 +01:00
if (!expandHint) {
expandHint = t('HTML.defaultHint')
2020-12-26 12:59:16 +01:00
}
2022-11-12 17:52:50 +01:00
if (disableDetails) {
numberOfLines = 4
}
2022-12-26 01:06:33 +01:00
const followedTags = useFollowedTagsQuery()
2020-12-26 12:59:16 +01:00
2022-12-26 01:06:33 +01:00
const [totalLines, setTotalLines] = useState<number>()
const [expanded, setExpanded] = useState(highlighted)
2021-01-01 16:48:16 +01:00
2022-12-26 01:06:33 +01:00
const document = parseDocument(content)
const unwrapNode = (node: ChildNode): string => {
switch (node.type) {
case ElementType.Text:
return node.data
case ElementType.Tag:
if (node.name === 'span') {
if (node.attribs.class?.includes('invisible')) return ''
if (node.attribs.class?.includes('ellipsis'))
return node.children.map(child => unwrapNode(child)).join('') + '...'
}
return node.children.map(child => unwrapNode(child)).join('')
default:
return ''
}
}
const renderNode = (node: ChildNode, index: number) => {
switch (node.type) {
case ElementType.Text:
return (
<ParseEmojis
key={index}
content={node.data}
emojis={emojis}
size={size}
adaptiveSize={adaptiveSize}
/>
)
case ElementType.Tag:
switch (node.name) {
case 'a':
const classes = node.attribs.class
const href = node.attribs.href
if (classes) {
if (classes.includes('hashtag')) {
const tag = href.match(new RegExp(/\/tags?\/(.*)/, 'i'))?.[1]
const paramsHashtag = (params as { hashtag: Mastodon.Tag['name'] } | undefined)
?.hashtag
const sameHashtag = paramsHashtag === tag
const isFollowing = followedTags.data?.pages[0]?.body.find(t => t.name === tag)
return (
<Text
key={index}
style={[
{ color: tag?.length ? colors.blue : colors.red },
isFollowing
? {
textDecorationColor: tag?.length ? colors.blue : colors.red,
textDecorationLine: 'underline',
textDecorationStyle: 'dotted'
}
: null
]}
onPress={() =>
tag?.length &&
!disableDetails &&
!sameHashtag &&
navigation.push('Tab-Shared-Hashtag', { hashtag: tag })
}
children={node.children.map(unwrapNode).join('')}
/>
)
}
if (classes.includes('mention') && mentions?.length) {
const mentionIndex = mentions.findIndex(mention => mention.url === href)
const paramsAccount = (params as { account: Mastodon.Account } | undefined)
?.account
const sameAccount = paramsAccount?.id === mentions[mentionIndex]?.id
return (
<Text
key={index}
style={{ color: mentionIndex > -1 ? colors.blue : undefined }}
onPress={() =>
mentionIndex > -1 &&
!disableDetails &&
!sameAccount &&
navigation.push('Tab-Shared-Account', { account: mentions[mentionIndex] })
}
children={node.children.map(unwrapNode).join('')}
/>
)
}
}
const content = node.children.map(child => unwrapNode(child)).join('')
const shouldBeTag = tags && tags.find(tag => `#${tag.name}` === content)
return (
<Text
key={index}
style={{ color: colors.blue }}
onPress={async () => {
if (!disableDetails) {
if (shouldBeTag) {
navigation.push('Tab-Shared-Hashtag', {
hashtag: content.substring(1)
})
} else {
await openLink(href, navigation)
}
}
2020-12-26 23:05:17 +01:00
}}
2022-12-26 01:06:33 +01:00
children={content !== href ? content : showFullLink ? href : content}
2021-03-13 17:56:57 +01:00
/>
2022-12-26 01:06:33 +01:00
)
break
case 'p':
if (index < document.children.length - 1) {
return (
<Text key={index}>
{node.children.map((c, i) => renderNode(c, i))}
<Text style={{ lineHeight: adaptedLineheight / 2 }}>{'\n\n'}</Text>
</Text>
)
} else {
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
}
default:
return <Text key={index} children={node.children.map((c, i) => renderNode(c, i))} />
}
}
return null
}
return (
<View style={{ overflow: 'hidden' }}>
{(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? (
<Pressable
accessibilityLabel={t('HTML.accessibilityHint')}
onPress={() => {
layoutAnimation()
setExpanded(!expanded)
if (setSpoilerExpanded) {
setSpoilerExpanded(!expanded)
}
}}
style={{
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
minHeight: 44,
backgroundColor: colors.backgroundDefault
}}
>
<Text
2022-08-10 00:46:43 +02:00
style={{
2022-12-26 01:06:33 +01:00
textAlign: 'center',
...StyleConstants.FontStyle.S,
color: colors.primaryDefault,
marginRight: StyleConstants.Spacing.S
2022-08-10 00:46:43 +02:00
}}
2022-12-26 01:06:33 +01:00
children={t('HTML.expanded', {
hint: expandHint,
moreLines:
numberOfLines > 1 && typeof totalLines === 'number'
? t('HTML.moreLines', { count: totalLines - numberOfLines })
: ''
})}
2022-08-10 00:46:43 +02:00
/>
2022-12-26 01:06:33 +01:00
<Icon
name={expanded ? 'Minimize2' : 'Maximize2'}
color={colors.primaryDefault}
strokeWidth={2}
size={StyleConstants.Font.Size[size]}
/>
</Pressable>
) : null}
<Text
children={document.children.map(renderNode)}
onTextLayout={({ nativeEvent }) => {
if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) {
setTotalLines(nativeEvent.lines.length)
}
}}
style={{
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight,
...textStyles,
height: numberOfLines === 1 && !expanded ? 0 : undefined
}}
numberOfLines={
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined
}
selectable={selectable}
/>
</View>
2021-03-13 17:56:57 +01:00
)
},
2022-12-11 14:08:27 +01:00
(prev, next) => prev.content === next.content && isEqual(prev.emojis, next.emojis)
2021-03-13 17:56:57 +01:00
)
2020-10-28 00:02:37 +01:00
2021-03-13 17:56:57 +01:00
export default ParseHTML