117 lines
2.8 KiB
TypeScript
117 lines
2.8 KiB
TypeScript
import {Children} from 'react'
|
|
import {
|
|
type StyleProp,
|
|
type TextProps as RNTextProps,
|
|
type TextStyle,
|
|
} from 'react-native'
|
|
import {UITextView} from 'react-native-uitextview'
|
|
import createEmojiRegex from 'emoji-regex'
|
|
|
|
import {type Alf, applyFonts, atoms, flatten} from '#/alf'
|
|
import {IS_IOS, IS_NATIVE} from '#/env'
|
|
|
|
/**
|
|
* Ensures that `lineHeight` defaults to a relative value of `1`, or applies
|
|
* other relative leading atoms.
|
|
*
|
|
* If the `lineHeight` value is > 2, we assume it's an absolute value and
|
|
* returns it as-is.
|
|
*/
|
|
export function normalizeTextStyles(
|
|
styles: StyleProp<TextStyle>,
|
|
{
|
|
fontScale,
|
|
fontFamily,
|
|
}: {
|
|
fontScale: number
|
|
fontFamily: Alf['fonts']['family']
|
|
} & Pick<Alf, 'flags'>,
|
|
) {
|
|
const s = flatten(styles) ?? {}
|
|
|
|
// should always be defined on these components
|
|
s.fontSize = (s.fontSize || atoms.text_md.fontSize) * fontScale
|
|
|
|
if (s?.lineHeight) {
|
|
if (s.lineHeight !== 0 && s.lineHeight <= 2) {
|
|
s.lineHeight = Math.round(s.fontSize * s.lineHeight)
|
|
}
|
|
} else if (!IS_NATIVE) {
|
|
s.lineHeight = s.fontSize
|
|
}
|
|
|
|
applyFonts(s, fontFamily)
|
|
|
|
return s
|
|
}
|
|
|
|
export type StringChild = string | (string | null)[]
|
|
export type TextProps = RNTextProps & {
|
|
/**
|
|
* Lets the user select text, to use the native copy and paste functionality.
|
|
*/
|
|
selectable?: boolean
|
|
/**
|
|
* Provides `data-*` attributes to the underlying `UITextView` component on
|
|
* web only.
|
|
*/
|
|
dataSet?: Record<string, string | number | undefined>
|
|
/**
|
|
* Appears as a small tooltip on web hover.
|
|
*/
|
|
title?: string
|
|
/**
|
|
* Whether the children could possibly contain emoji.
|
|
*/
|
|
emoji?: boolean
|
|
}
|
|
|
|
const EMOJI = createEmojiRegex()
|
|
|
|
export function childHasEmoji(children: React.ReactNode) {
|
|
let hasEmoji = false
|
|
Children.forEach(children, child => {
|
|
if (typeof child === 'string' && createEmojiRegex().test(child)) {
|
|
hasEmoji = true
|
|
}
|
|
})
|
|
return hasEmoji
|
|
}
|
|
|
|
export function renderChildrenWithEmoji(
|
|
children: React.ReactNode,
|
|
props: Omit<TextProps, 'children'> = {},
|
|
emoji: boolean,
|
|
) {
|
|
if (!IS_IOS || !emoji) {
|
|
return children
|
|
}
|
|
return Children.map(children, child => {
|
|
if (typeof child !== 'string') return child
|
|
|
|
const emojis = child.match(EMOJI)
|
|
|
|
if (emojis === null) {
|
|
return child
|
|
}
|
|
|
|
return child.split(EMOJI).map((stringPart, index) => [
|
|
stringPart,
|
|
emojis[index] ? (
|
|
<UITextView
|
|
{...props}
|
|
style={[props?.style, {fontFamily: 'System'}]}
|
|
key={index}>
|
|
{emojis[index]}
|
|
</UITextView>
|
|
) : null,
|
|
])
|
|
})
|
|
}
|
|
|
|
const SINGLE_EMOJI_RE =
|
|
/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\uFE0F\u200D]+$/u
|
|
export function isOnlyEmoji(text: string) {
|
|
return text.length <= 15 && SINGLE_EMOJI_RE.test(text)
|
|
}
|