tooot/src/components/Parse/HTML.tsx

301 lines
8.5 KiB
TypeScript
Raw Normal View History

2021-01-24 02:25:43 +01:00
import analytics from '@components/analytics'
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'
2021-01-01 16:48:16 +01:00
import { StyleConstants } from '@utils/styles/constants'
2021-01-12 00:12:44 +01:00
import layoutAnimation from '@utils/styles/layoutAnimation'
2021-01-01 16:48:16 +01:00
import { useTheme } from '@utils/styles/ThemeManager'
2020-12-26 12:59:16 +01:00
import { LinearGradient } from 'expo-linear-gradient'
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'
2021-01-27 00:42:56 +01:00
import { Platform, Pressable, Text, View } from 'react-native'
2020-12-01 00:44:28 +01:00
import HTMLView from 'react-native-htmlview'
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,
2020-11-23 00:07:32 +01:00
theme,
2020-10-31 21:04:46 +01:00
node,
index,
2020-12-02 00:16:27 +01:00
size,
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
2020-11-23 00:07:32 +01:00
theme: any
node: any
2020-10-31 21:04:46 +01:00
index: number
2021-02-01 02:16:53 +01:00
size: 'S' | 'M' | 'L'
navigation: StackNavigationProp<Nav.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')) {
2021-01-10 02:12:14 +01:00
const tag = href.split(new RegExp(/\/tag\/(.*)|\/tags\/(.*)/))
const differentTag = routeParams?.hashtag
? routeParams.hashtag !== tag[1] && routeParams.hashtag !== tag[2]
: true
2021-01-07 19:13:09 +01:00
return (
2021-01-14 00:43:35 +01:00
<Text
2021-01-07 19:13:09 +01:00
key={index}
2021-01-14 00:43:35 +01:00
style={{
color: theme.blue,
...StyleConstants.FontStyle[size]
}}
2021-01-07 19:13:09 +01:00
onPress={() => {
2021-01-24 02:25:43 +01:00
analytics('status_hashtag_press')
2021-01-07 19:13:09 +01:00
!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}
</Text>
2021-01-07 19:13:09 +01:00
)
} else if (classes.includes('mention') && mentions) {
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 (
2021-01-14 00:43:35 +01:00
<Text
2021-01-07 19:13:09 +01:00
key={index}
2021-01-14 00:43:35 +01:00
style={{
color: accountIndex !== -1 ? theme.blue : undefined,
...StyleConstants.FontStyle[size]
}}
2021-01-07 19:13:09 +01:00
onPress={() => {
2021-01-24 02:25:43 +01:00
analytics('status_mention_press')
2021-01-07 19:13:09 +01:00
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}
</Text>
2021-01-07 19:13:09 +01:00
)
}
} 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
2020-10-28 00:02:37 +01:00
return (
2021-01-14 00:43:35 +01:00
<Text
2020-10-28 00:02:37 +01:00
key={index}
2021-01-14 00:43:35 +01:00
style={{
color: theme.blue,
2021-01-23 02:41:50 +01:00
...StyleConstants.FontStyle[size],
alignItems: 'center'
2021-01-14 00:43:35 +01:00
}}
2021-01-24 02:25:43 +01:00
onPress={async () => {
analytics('status_link_press')
2021-01-07 19:13:09 +01:00
!disableDetails && !shouldBeTag
? await openLink(href)
2021-01-30 01:29:15 +01:00
: navigation.push('Tab-Shared-Hashtag', {
2021-01-07 19:13:09 +01:00
hashtag: content.substring(1)
})
2021-01-24 02:25:43 +01:00
}}
2020-10-28 00:02:37 +01:00
>
2021-01-23 02:41:50 +01:00
{content || (showFullLink ? href : domain[1])}
2021-01-14 00:43:35 +01:00
{!shouldBeTag ? (
<Icon
color={theme.blue}
name='ExternalLink'
size={StyleConstants.Font.Size[size]}
2021-01-23 02:41:50 +01:00
style={{
transform: [{ translateY: size === 'L' ? -3 : -1 }]
}}
2021-01-14 00:43:35 +01:00
/>
) : null}
</Text>
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'
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-01-04 18:29:02 +01:00
disableDetails?: boolean
2020-10-31 21:04:46 +01:00
}
2021-01-01 16:48:16 +01:00
const ParseHTML: React.FC<Props> = ({
2020-10-28 00:02:37 +01:00
content,
2020-12-28 00:59:57 +01:00
size = 'M',
2020-10-28 00:02:37 +01:00
emojis,
mentions,
2020-12-28 17:30:20 +01:00
tags,
2020-10-29 14:52:28 +01:00
showFullLink = false,
2020-12-26 12:59:16 +01:00
numberOfLines = 10,
2021-01-22 01:34:20 +01:00
expandHint,
2021-01-04 18:29:02 +01:00
disableDetails = false
2020-10-31 21:04:46 +01:00
}) => {
2021-01-24 02:25:43 +01:00
const navigation = useNavigation<
2021-02-01 02:16:53 +01:00
StackNavigationProp<Nav.TabLocalStackParamList>
2021-01-24 02:25:43 +01:00
>()
2021-01-10 02:12:14 +01:00
const route = useRoute()
2020-11-23 00:07:32 +01:00
const { theme } = useTheme()
2021-01-22 01:34:20 +01:00
const { t, i18n } = useTranslation('componentParse')
if (!expandHint) {
expandHint = t('HTML.defaultHint')
}
2020-10-28 00:02:37 +01:00
2020-11-28 17:07:30 +01:00
const renderNodeCallback = useCallback(
(node, index) =>
2020-12-02 00:16:27 +01:00
renderNode({
2021-01-10 02:12:14 +01:00
routeParams: route.params,
2020-12-02 00:16:27 +01:00
theme,
node,
index,
size,
navigation,
mentions,
2020-12-28 17:30:20 +01:00
tags,
2021-01-04 18:29:02 +01:00
showFullLink,
disableDetails
2020-12-02 00:16:27 +01:00
}),
2020-11-28 17:07:30 +01:00
[]
)
const textComponent = useCallback(({ children }) => {
2020-12-26 12:59:16 +01:00
if (children) {
2021-01-04 14:55:34 +01:00
return (
<ParseEmojis
content={children.toString()}
emojis={emojis}
size={size}
/>
)
2020-12-26 12:59:16 +01:00
} else {
return null
}
}, [])
2020-12-26 23:05:17 +01:00
const rootComponent = useCallback(
({ children }) => {
2021-01-19 01:13:45 +01:00
const { t } = useTranslation('componentParse')
2020-12-26 23:05:17 +01:00
const lineHeight = StyleConstants.Font.LineHeight[size]
2020-12-26 12:59:16 +01:00
2021-01-04 14:55:34 +01:00
const [expandAllow, setExpandAllow] = useState(false)
const [expanded, setExpanded] = useState(false)
2020-12-26 12:59:16 +01:00
2021-01-12 00:12:44 +01:00
const onTextLayout = useCallback(({ nativeEvent }) => {
2021-01-27 00:42:56 +01:00
switch (Platform.OS) {
case 'ios':
if (nativeEvent.lines.length === numberOfLines + 1) {
setExpandAllow(true)
}
break
case 'android':
if (nativeEvent.lines.length > numberOfLines + 1) {
setExpandAllow(true)
}
break
}
2021-01-12 00:12:44 +01:00
}, [])
2021-01-01 16:48:16 +01:00
2020-12-26 23:05:17 +01:00
return (
2021-01-13 01:03:46 +01:00
<View style={{ overflow: 'hidden' }}>
2021-01-12 00:12:44 +01:00
<Text
children={children}
onTextLayout={onTextLayout}
numberOfLines={expanded ? 999 : numberOfLines + 1}
style={{
...StyleConstants.FontStyle[size],
color: theme.primary
}}
/>
2021-01-04 14:55:34 +01:00
{expandAllow ? (
2020-12-26 23:05:17 +01:00
<Pressable
2021-01-12 00:12:44 +01:00
onPress={() => {
2021-01-24 02:25:43 +01:00
analytics('status_readmore', { allow: expandAllow, expanded })
2021-01-12 00:12:44 +01:00
layoutAnimation()
setExpanded(!expanded)
}}
2021-01-13 01:03:46 +01:00
style={{
marginTop: expanded
? 0
: -lineHeight * (numberOfLines === 0 ? 1 : 2)
}}
2020-12-02 00:16:27 +01:00
>
2020-12-26 23:05:17 +01:00
<LinearGradient
colors={[
theme.backgroundGradientStart,
theme.backgroundGradientEnd
]}
2021-01-13 01:03:46 +01:00
locations={[
0,
lineHeight / (StyleConstants.Font.Size[size] * 5)
]}
2020-12-02 00:16:27 +01:00
style={{
2020-12-26 23:05:17 +01:00
paddingTop: StyleConstants.Font.Size.S * 2,
paddingBottom: StyleConstants.Font.Size.S
2020-12-02 00:16:27 +01:00
}}
>
2020-12-26 23:05:17 +01:00
<Text
style={{
textAlign: 'center',
...StyleConstants.FontStyle.S,
2020-12-26 23:05:17 +01:00
color: theme.primary
}}
>
2021-01-19 01:13:45 +01:00
{expanded
? t('HTML.expanded.true', { hint: expandHint })
: t('HTML.expanded.false', { hint: expandHint })}
2020-12-26 23:05:17 +01:00
</Text>
</LinearGradient>
</Pressable>
2021-01-01 16:48:16 +01:00
) : null}
2020-12-26 23:05:17 +01:00
</View>
)
},
2021-01-22 01:34:20 +01:00
[theme, i18n.language]
2020-12-26 23:05:17 +01:00
)
2020-11-28 17:07:30 +01:00
2020-10-28 00:02:37 +01:00
return (
<HTMLView
value={content}
2020-11-28 17:07:30 +01:00
TextComponent={textComponent}
RootComponent={rootComponent}
renderNode={renderNodeCallback}
2020-10-28 00:02:37 +01:00
/>
)
}
2021-01-12 00:12:44 +01:00
// export default ParseHTML
export default React.memo(ParseHTML, () => true)