9aa360d328
* Do not render links if uri is invalid * Update comment
177 lines
4.7 KiB
TypeScript
177 lines
4.7 KiB
TypeScript
import React from 'react'
|
|
import {type StyleProp, type TextStyle} from 'react-native'
|
|
import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api'
|
|
|
|
import {toShortUrl} from '#/lib/strings/url-helpers'
|
|
import {atoms as a, flatten, type TextStyleProp} from '#/alf'
|
|
import {isOnlyEmoji} from '#/alf/typography'
|
|
import {InlineLinkText, type LinkProps} from '#/components/Link'
|
|
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
|
import {RichTextTag} from '#/components/RichTextTag'
|
|
import {Text, type TextProps} from '#/components/Typography'
|
|
|
|
const WORD_WRAP = {wordWrap: 1}
|
|
// lifted from facet detection in `RichText` impl, _without_ `gm` flags
|
|
const URL_REGEX =
|
|
/(^|\s|\()((https?:\/\/[\S]+)|((?<domain>[a-z][a-z0-9]*(\.[a-z0-9]+)+)[\S]*))/i
|
|
|
|
export type RichTextProps = TextStyleProp &
|
|
Pick<TextProps, 'selectable' | 'onLayout' | 'onTextLayout'> & {
|
|
value: RichTextAPI | string
|
|
testID?: string
|
|
numberOfLines?: number
|
|
disableLinks?: boolean
|
|
enableTags?: boolean
|
|
authorHandle?: string
|
|
onLinkPress?: LinkProps['onPress']
|
|
interactiveStyle?: StyleProp<TextStyle>
|
|
emojiMultiplier?: number
|
|
shouldProxyLinks?: boolean
|
|
}
|
|
|
|
export function RichText({
|
|
testID,
|
|
value,
|
|
style,
|
|
numberOfLines,
|
|
disableLinks,
|
|
selectable,
|
|
enableTags = false,
|
|
authorHandle,
|
|
onLinkPress,
|
|
interactiveStyle,
|
|
emojiMultiplier = 1.85,
|
|
onLayout,
|
|
onTextLayout,
|
|
shouldProxyLinks,
|
|
}: RichTextProps) {
|
|
const richText = React.useMemo(
|
|
() =>
|
|
value instanceof RichTextAPI ? value : new RichTextAPI({text: value}),
|
|
[value],
|
|
)
|
|
|
|
const plainStyles = [a.leading_snug, style]
|
|
const interactiveStyles = [plainStyles, interactiveStyle]
|
|
|
|
const {text, facets} = richText
|
|
|
|
if (!facets?.length) {
|
|
if (isOnlyEmoji(text)) {
|
|
const flattenedStyle = flatten(style) ?? {}
|
|
const fontSize =
|
|
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
|
|
return (
|
|
<Text
|
|
emoji
|
|
selectable={selectable}
|
|
testID={testID}
|
|
style={[plainStyles, {fontSize}]}
|
|
onLayout={onLayout}
|
|
onTextLayout={onTextLayout}
|
|
// @ts-ignore web only -prf
|
|
dataSet={WORD_WRAP}>
|
|
{text}
|
|
</Text>
|
|
)
|
|
}
|
|
return (
|
|
<Text
|
|
emoji
|
|
selectable={selectable}
|
|
testID={testID}
|
|
style={plainStyles}
|
|
numberOfLines={numberOfLines}
|
|
onLayout={onLayout}
|
|
onTextLayout={onTextLayout}
|
|
// @ts-ignore web only -prf
|
|
dataSet={WORD_WRAP}>
|
|
{text}
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
const els = []
|
|
let key = 0
|
|
// N.B. must access segments via `richText.segments`, not via destructuring
|
|
for (const segment of richText.segments()) {
|
|
const link = segment.link
|
|
const mention = segment.mention
|
|
const tag = segment.tag
|
|
if (
|
|
mention &&
|
|
AppBskyRichtextFacet.validateMention(mention).success &&
|
|
!disableLinks
|
|
) {
|
|
els.push(
|
|
<ProfileHoverCard key={key} did={mention.did}>
|
|
<InlineLinkText
|
|
selectable={selectable}
|
|
to={`/profile/${mention.did}`}
|
|
style={interactiveStyles}
|
|
// @ts-ignore TODO
|
|
dataSet={WORD_WRAP}
|
|
shouldProxy={shouldProxyLinks}
|
|
onPress={onLinkPress}>
|
|
{segment.text}
|
|
</InlineLinkText>
|
|
</ProfileHoverCard>,
|
|
)
|
|
} else if (link && AppBskyRichtextFacet.validateLink(link).success) {
|
|
const isValidLink = URL_REGEX.test(link.uri)
|
|
if (!isValidLink || disableLinks) {
|
|
els.push(toShortUrl(segment.text))
|
|
} else {
|
|
els.push(
|
|
<InlineLinkText
|
|
selectable={selectable}
|
|
key={key}
|
|
to={link.uri}
|
|
style={interactiveStyles}
|
|
// @ts-ignore TODO
|
|
dataSet={WORD_WRAP}
|
|
shareOnLongPress
|
|
shouldProxy={shouldProxyLinks}
|
|
onPress={onLinkPress}
|
|
emoji>
|
|
{toShortUrl(segment.text)}
|
|
</InlineLinkText>,
|
|
)
|
|
}
|
|
} else if (
|
|
!disableLinks &&
|
|
enableTags &&
|
|
tag &&
|
|
AppBskyRichtextFacet.validateTag(tag).success
|
|
) {
|
|
els.push(
|
|
<RichTextTag
|
|
key={key}
|
|
display={segment.text}
|
|
tag={tag.tag}
|
|
textStyle={interactiveStyles}
|
|
authorHandle={authorHandle}
|
|
/>,
|
|
)
|
|
} else {
|
|
els.push(segment.text)
|
|
}
|
|
key++
|
|
}
|
|
|
|
return (
|
|
<Text
|
|
emoji
|
|
selectable={selectable}
|
|
testID={testID}
|
|
style={plainStyles}
|
|
numberOfLines={numberOfLines}
|
|
onLayout={onLayout}
|
|
onTextLayout={onTextLayout}
|
|
// @ts-ignore web only -prf
|
|
dataSet={WORD_WRAP}>
|
|
{els}
|
|
</Text>
|
|
)
|
|
}
|