tooot/src/components/Parse/HTML.tsx

327 lines
9.9 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'
import CustomText from '@components/Text'
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'
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'
2021-01-07 19:13:09 +01:00
import React, { useCallback, useState } from 'react'
2021-01-19 01:13:45 +01:00
import { useTranslation } from 'react-i18next'
2022-06-02 00:28:42 +02:00
import { Platform, Pressable, View } from 'react-native'
2020-12-01 00:44:28 +01:00
import HTMLView from 'react-native-htmlview'
2021-03-10 10:22:53 +01:00
import { useSelector } from 'react-redux'
2020-11-23 00:07:32 +01:00
2020-11-05 21:47:50 +01:00
// Prevent going to the same hashtag multiple times
2020-10-31 21:04:46 +01:00
const renderNode = ({
2021-01-10 02:12:14 +01:00
routeParams,
2022-02-12 14:51:01 +01:00
colors,
2020-10-31 21:04:46 +01:00
node,
index,
2021-03-10 10:22:53 +01:00
adaptedFontsize,
adaptedLineheight,
2020-10-31 21:04:46 +01:00
navigation,
mentions,
2020-12-28 17:30:20 +01:00
tags,
2021-01-04 18:29:02 +01:00
showFullLink,
disableDetails
2020-10-31 21:04:46 +01:00
}: {
2021-01-10 02:12:14 +01:00
routeParams?: any
2022-02-12 14:51:01 +01:00
colors: any
2020-11-23 00:07:32 +01:00
node: any
2020-10-31 21:04:46 +01:00
index: number
2021-03-10 10:22:53 +01:00
adaptedFontsize: number
adaptedLineheight: number
2022-04-29 23:57:18 +02:00
navigation: StackNavigationProp<TabLocalStackParamList>
mentions?: Mastodon.Mention[]
2020-12-28 17:30:20 +01:00
tags?: Mastodon.Tag[]
2020-10-31 21:04:46 +01:00
showFullLink: boolean
2021-01-04 18:29:02 +01:00
disableDetails: boolean
2020-10-31 21:04:46 +01:00
}) => {
2021-01-07 19:13:09 +01:00
switch (node.name) {
case 'a':
const classes = node.attribs.class
const href = node.attribs.href
if (classes) {
if (classes.includes('hashtag')) {
2022-06-09 23:25:01 +02:00
const tag = href?.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
2021-01-10 02:12:14 +01:00
const differentTag = routeParams?.hashtag
? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2]
: true
2021-01-07 19:13:09 +01:00
return (
<CustomText
2021-04-09 21:43:12 +02:00
accessible
2021-01-07 19:13:09 +01:00
key={index}
2021-01-14 00:43:35 +01:00
style={{
2022-02-12 14:51:01 +01:00
color: colors.blue,
2021-03-10 10:22:53 +01:00
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight
2021-01-14 00:43:35 +01:00
}}
2021-01-07 19:13:09 +01:00
onPress={() => {
!disableDetails &&
2021-01-10 02:12:14 +01:00
differentTag &&
2021-01-30 01:29:15 +01:00
navigation.push('Tab-Shared-Hashtag', {
2021-01-07 19:13:09 +01:00
hashtag: tag[1] || tag[2]
})
}}
>
2021-01-14 00:43:35 +01:00
{node.children[0].data}
{node.children[1]?.children[0].data}
</CustomText>
2021-01-07 19:13:09 +01:00
)
} else if (classes.includes('mention') && mentions) {
2022-10-10 23:28:40 +02:00
const accountIndex = mentions.findIndex(mention => mention.url === href)
2021-01-10 02:12:14 +01:00
const differentAccount = routeParams?.account
2021-01-17 22:37:05 +01:00
? routeParams.account.id !== mentions[accountIndex]?.id
2021-01-10 02:12:14 +01:00
: true
2021-01-07 19:13:09 +01:00
return (
<CustomText
2021-01-07 19:13:09 +01:00
key={index}
2021-01-14 00:43:35 +01:00
style={{
2022-10-10 23:28:40 +02:00
color: accountIndex !== -1 ? colors.blue : colors.primaryDefault,
2021-03-10 10:22:53 +01:00
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight
2021-01-14 00:43:35 +01:00
}}
2021-01-07 19:13:09 +01:00
onPress={() => {
accountIndex !== -1 &&
!disableDetails &&
2021-01-10 02:12:14 +01:00
differentAccount &&
2021-01-30 01:29:15 +01:00
navigation.push('Tab-Shared-Account', {
2021-01-07 19:13:09 +01:00
account: mentions[accountIndex]
})
}}
>
2021-01-14 00:43:35 +01:00
{node.children[0].data}
{node.children[1]?.children[0].data}
</CustomText>
2021-01-07 19:13:09 +01:00
)
}
} else {
2022-06-09 23:25:01 +02:00
const domain = href?.split(new RegExp(/:\/\/(.[^\/]+)/))
2021-01-07 19:13:09 +01:00
// Need example here
2022-10-10 23:28:40 +02:00
const content = node.children && node.children[0] && node.children[0].data
const shouldBeTag = tags && tags.filter(tag => `#${tag.name}` === content).length > 0
2020-10-28 00:02:37 +01:00
return (
<CustomText
2020-10-28 00:02:37 +01:00
key={index}
2021-01-14 00:43:35 +01:00
style={{
2022-02-12 14:51:01 +01:00
color: colors.blue,
2021-03-10 10:22:53 +01:00
alignItems: 'center',
fontSize: adaptedFontsize,
lineHeight: adaptedLineheight
2021-01-14 00:43:35 +01:00
}}
2021-01-24 02:25:43 +01:00
onPress={async () => {
2022-06-09 23:25:01 +02:00
if (!disableDetails) {
if (shouldBeTag) {
navigation.push('Tab-Shared-Hashtag', {
2021-01-07 19:13:09 +01:00
hashtag: content.substring(1)
})
2022-06-09 23:25:01 +02:00
} else {
await openLink(href, navigation)
}
}
2021-01-24 02:25:43 +01:00
}}
2020-10-28 00:02:37 +01:00
>
2022-10-10 23:28:40 +02:00
{(content && content !== href && content) || (showFullLink ? href : domain[1])}
2021-01-14 00:43:35 +01:00
{!shouldBeTag ? (
<Icon
2022-02-12 14:51:01 +01:00
color={colors.blue}
2021-01-14 00:43:35 +01:00
name='ExternalLink'
2021-03-10 10:22:53 +01:00
size={adaptedFontsize}
2021-01-23 02:41:50 +01:00
style={{
2022-06-02 00:28:42 +02:00
...(Platform.OS === 'ios'
? {
transform: [{ translateY: -2 }]
}
: {
transform: [{ translateY: 1 }]
})
2021-01-23 02:41:50 +01:00
}}
2021-01-14 00:43:35 +01:00
/>
) : null}
</CustomText>
2020-10-28 00:02:37 +01:00
)
}
2021-01-07 19:13:09 +01:00
break
case 'p':
if (!node.children.length) {
2020-12-26 12:59:16 +01:00
return <View key={index} /> // bug when the tag is empty
}
2021-01-07 19:13:09 +01:00
break
2020-10-28 00:02:37 +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'
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',
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>>()
2021-03-13 17:56:57 +01:00
const route = useRoute()
2022-02-12 14:51:01 +01:00
const { colors, theme } = useTheme()
2021-03-13 17:56:57 +01:00
const { t, i18n } = useTranslation('componentParse')
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
}
2021-03-13 17:56:57 +01:00
const renderNodeCallback = useCallback(
2022-08-07 01:18:10 +02:00
(node: any, index: any) =>
2021-03-13 17:56:57 +01:00
renderNode({
routeParams: route.params,
2022-02-12 14:51:01 +01:00
colors,
2021-03-13 17:56:57 +01:00
node,
index,
adaptedFontsize,
adaptedLineheight,
navigation,
mentions,
tags,
showFullLink,
disableDetails
}),
[]
)
2022-08-07 01:18:10 +02:00
const textComponent = useCallback(({ children }: any) => {
2021-03-13 17:56:57 +01:00
if (children) {
return (
<ParseEmojis
content={children.toString()}
emojis={emojis}
size={size}
adaptiveSize={adaptiveSize}
/>
)
} else {
return null
}
}, [])
const rootComponent = useCallback(
2022-08-07 01:18:10 +02:00
({ children }: any) => {
2021-03-13 17:56:57 +01:00
const { t } = useTranslation('componentParse')
2020-12-26 12:59:16 +01:00
2022-08-10 00:46:43 +02:00
const [totalLines, setTotalLines] = useState<number>()
2021-05-19 23:28:01 +02:00
const [expanded, setExpanded] = useState(highlighted)
2021-01-01 16:48:16 +01:00
2021-03-13 17:56:57 +01:00
return (
<View style={{ overflow: 'hidden' }}>
2022-11-12 17:52:50 +01:00
{(!disableDetails && typeof totalLines === 'number') || numberOfLines === 1 ? (
2021-03-13 17:56:57 +01:00
<Pressable
2022-08-10 00:46:43 +02:00
accessibilityLabel={t('HTML.accessibilityHint')}
2021-03-13 17:56:57 +01:00
onPress={() => {
layoutAnimation()
setExpanded(!expanded)
2022-12-03 20:47:11 +01:00
if (setSpoilerExpanded) {
setSpoilerExpanded(!expanded)
}
2021-03-13 17:56:57 +01:00
}}
2020-12-02 00:16:27 +01:00
style={{
2022-08-10 00:46:43 +02:00
flexDirection: 'row',
2021-03-13 17:56:57 +01:00
justifyContent: 'center',
2022-08-10 00:46:43 +02:00
alignItems: 'center',
2021-03-13 17:56:57 +01:00
minHeight: 44,
2022-02-12 14:51:01 +01:00
backgroundColor: colors.backgroundDefault
2020-12-02 00:16:27 +01:00
}}
>
<CustomText
2020-12-26 23:05:17 +01:00
style={{
textAlign: 'center',
...StyleConstants.FontStyle.S,
2022-08-10 00:46:43 +02:00
color: colors.primaryDefault,
marginRight: StyleConstants.Spacing.S
2020-12-26 23:05:17 +01:00
}}
2022-08-14 22:54:54 +02:00
children={t('HTML.expanded', {
2022-08-10 00:46:43 +02:00
hint: expandHint,
2022-10-30 14:49:37 +01:00
moreLines:
2022-08-10 00:46:43 +02:00
numberOfLines > 1 && typeof totalLines === 'number'
2022-10-30 14:49:37 +01:00
? t('HTML.moreLines', { count: totalLines - numberOfLines })
2022-08-10 00:46:43 +02:00
: ''
2021-03-13 17:56:57 +01:00
})}
/>
2022-08-10 00:46:43 +02:00
<Icon
name={expanded ? 'Minimize2' : 'Maximize2'}
color={colors.primaryDefault}
strokeWidth={2}
size={StyleConstants.Font.Size[size]}
/>
2021-03-13 17:56:57 +01:00
</Pressable>
) : null}
2022-08-10 00:46:43 +02:00
<CustomText
children={children}
onTextLayout={({ nativeEvent }) => {
2022-10-10 23:28:40 +02:00
if (numberOfLines === 1 || nativeEvent.lines.length >= numberOfLines + 5) {
2022-08-10 00:46:43 +02:00
setTotalLines(nativeEvent.lines.length)
}
}}
style={{
height: numberOfLines === 1 && !expanded ? 0 : undefined
}}
numberOfLines={
2022-10-10 23:28:40 +02:00
typeof totalLines === 'number' ? (expanded ? 999 : numberOfLines) : undefined
2022-08-10 00:46:43 +02:00
}
selectable={selectable}
/>
2021-03-13 17:56:57 +01:00
</View>
)
},
2022-02-12 14:51:01 +01:00
[theme, i18n.language]
2021-03-13 17:56:57 +01:00
)
2020-11-28 17:07:30 +01:00
2021-03-13 17:56:57 +01:00
return (
<HTMLView
value={content}
TextComponent={textComponent}
RootComponent={rootComponent}
renderNode={renderNodeCallback}
/>
)
},
2022-04-30 17:44:39 +02:00
(prev, next) => prev.content === next.content
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