Support emoji in text with custom font
This commit is contained in:
@@ -116,6 +116,7 @@
|
|||||||
"deprecated-react-native-prop-types": "^5.0.0",
|
"deprecated-react-native-prop-types": "^5.0.0",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
|
"emoji-regex": "^10.4.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"expo": "^51.0.8",
|
"expo": "^51.0.8",
|
||||||
"expo-application": "^5.9.1",
|
"expo-application": "^5.9.1",
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export function RichText({
|
|||||||
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
|
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={[plainStyles, {fontSize}]}
|
style={[plainStyles, {fontSize}]}
|
||||||
@@ -77,6 +78,7 @@ export function RichText({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
style={plainStyles}
|
style={plainStyles}
|
||||||
@@ -148,7 +150,11 @@ export function RichText({
|
|||||||
/>,
|
/>,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
els.push(segment.text)
|
els.push(
|
||||||
|
<Text key={key} emoji style={plainStyles}>
|
||||||
|
{segment.text}
|
||||||
|
</Text>,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
key++
|
key++
|
||||||
}
|
}
|
||||||
@@ -213,6 +219,7 @@ function RichTextTag({
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<TagMenu control={control} tag={tag} authorHandle={authorHandle}>
|
<TagMenu control={control} tag={tag} authorHandle={authorHandle}>
|
||||||
<Text
|
<Text
|
||||||
|
emoji
|
||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
{...native({
|
{...native({
|
||||||
accessibilityLabel: _(msg`Hashtag: #${tag}`),
|
accessibilityLabel: _(msg`Hashtag: #${tag}`),
|
||||||
|
|||||||
@@ -1,15 +1,85 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
|
import {StyleProp, TextProps as RNTextProps, TextStyle} from 'react-native'
|
||||||
import {UITextView} from 'react-native-uitextview'
|
import {UITextView} from 'react-native-uitextview'
|
||||||
|
import createEmojiRegex from 'emoji-regex'
|
||||||
|
|
||||||
import {isNative} from '#/platform/detection'
|
import {logger} from '#/logger'
|
||||||
|
import {isIOS, isNative} from '#/platform/detection'
|
||||||
import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
|
import {Alf, applyFonts, atoms, flatten, useAlf, useTheme, web} from '#/alf'
|
||||||
|
import {IS_DEV} from '#/env'
|
||||||
|
|
||||||
export type TextProps = RNTextProps & {
|
export type StringChild = string | (string | null)[]
|
||||||
|
|
||||||
|
export type TextProps = Omit<RNTextProps, 'children'> & {
|
||||||
/**
|
/**
|
||||||
* Lets the user select text, to use the native copy and paste functionality.
|
* Lets the user select text, to use the native copy and paste functionality.
|
||||||
*/
|
*/
|
||||||
selectable?: boolean
|
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
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
emoji: true
|
||||||
|
children: StringChild
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
emoji?: false
|
||||||
|
children: RNTextProps['children']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const EMOJI = createEmojiRegex()
|
||||||
|
|
||||||
|
export function childHasEmoji(children: React.ReactNode) {
|
||||||
|
return (Array.isArray(children) ? children : [children]).some(
|
||||||
|
child => typeof child === 'string' && createEmojiRegex().test(child),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function childIsString(
|
||||||
|
children: React.ReactNode,
|
||||||
|
): children is StringChild {
|
||||||
|
return (
|
||||||
|
typeof children === 'string' ||
|
||||||
|
(Array.isArray(children) &&
|
||||||
|
children.every(child => typeof child === 'string' || child === null))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderChildrenWithEmoji(children: StringChild) {
|
||||||
|
const normalized = Array.isArray(children) ? children : [children]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UITextView>
|
||||||
|
{normalized.map(child => {
|
||||||
|
if (typeof child !== 'string') return child
|
||||||
|
|
||||||
|
const emojis = child.match(EMOJI)
|
||||||
|
|
||||||
|
if (emojis === null) {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
return child.split(EMOJI).map((stringPart, index) => (
|
||||||
|
<UITextView key={index}>
|
||||||
|
{stringPart}
|
||||||
|
{emojis[index] ? (
|
||||||
|
<UITextView style={{color: 'black', fontFamily: 'System'}}>
|
||||||
|
{emojis[index]}
|
||||||
|
</UITextView>
|
||||||
|
) : null}
|
||||||
|
</UITextView>
|
||||||
|
))
|
||||||
|
})}
|
||||||
|
</UITextView>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +134,15 @@ export function normalizeTextStyles(
|
|||||||
/**
|
/**
|
||||||
* Our main text component. Use this most of the time.
|
* Our main text component. Use this most of the time.
|
||||||
*/
|
*/
|
||||||
export function Text({style, selectable, ...rest}: TextProps) {
|
export function Text({
|
||||||
|
children,
|
||||||
|
emoji,
|
||||||
|
style,
|
||||||
|
selectable,
|
||||||
|
title,
|
||||||
|
dataSet,
|
||||||
|
...rest
|
||||||
|
}: TextProps) {
|
||||||
const {fonts, flags} = useAlf()
|
const {fonts, flags} = useAlf()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
|
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
|
||||||
@@ -73,7 +151,31 @@ export function Text({style, selectable, ...rest}: TextProps) {
|
|||||||
flags,
|
flags,
|
||||||
})
|
})
|
||||||
|
|
||||||
return <UITextView selectable={selectable} uiTextView style={s} {...rest} />
|
if (IS_DEV) {
|
||||||
|
if (!emoji && childHasEmoji(children)) {
|
||||||
|
logger.warn(
|
||||||
|
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji && !childIsString(children)) {
|
||||||
|
throw new Error(
|
||||||
|
'Text: when <Text emoji />, children can only be strings.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UITextView
|
||||||
|
selectable={selectable}
|
||||||
|
uiTextView
|
||||||
|
style={s}
|
||||||
|
{...rest}
|
||||||
|
// @ts-ignore
|
||||||
|
dataSet={Object.assign({tooltip: title}, dataSet || {})}>
|
||||||
|
{isIOS && emoji ? renderChildrenWithEmoji(children) : children}
|
||||||
|
</UITextView>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHeadingElement({level}: {level: number}) {
|
export function createHeadingElement({level}: {level: number}) {
|
||||||
|
|||||||
@@ -2,27 +2,40 @@ import React from 'react'
|
|||||||
import {StyleSheet, Text as RNText, TextProps} from 'react-native'
|
import {StyleSheet, Text as RNText, TextProps} from 'react-native'
|
||||||
import {UITextView} from 'react-native-uitextview'
|
import {UITextView} from 'react-native-uitextview'
|
||||||
|
|
||||||
import {lh, s} from 'lib/styles'
|
import {lh, s} from '#/lib/styles'
|
||||||
import {TypographyVariant, useTheme} from 'lib/ThemeContext'
|
import {TypographyVariant, useTheme} from '#/lib/ThemeContext'
|
||||||
import {isIOS, isWeb} from 'platform/detection'
|
import {logger} from '#/logger'
|
||||||
|
import {isIOS} from '#/platform/detection'
|
||||||
import {applyFonts, useAlf} from '#/alf'
|
import {applyFonts, useAlf} from '#/alf'
|
||||||
|
import {
|
||||||
|
childHasEmoji,
|
||||||
|
childIsString,
|
||||||
|
renderChildrenWithEmoji,
|
||||||
|
StringChild,
|
||||||
|
} from '#/components/Typography'
|
||||||
|
import {IS_DEV} from '#/env'
|
||||||
|
|
||||||
export type CustomTextProps = TextProps & {
|
export type CustomTextProps = Omit<TextProps, 'children'> & {
|
||||||
type?: TypographyVariant
|
type?: TypographyVariant
|
||||||
lineHeight?: number
|
lineHeight?: number
|
||||||
title?: string
|
title?: string
|
||||||
dataSet?: Record<string, string | number>
|
dataSet?: Record<string, string | number>
|
||||||
selectable?: boolean
|
selectable?: boolean
|
||||||
}
|
} & (
|
||||||
|
| {
|
||||||
const fontFamilyStyle = {
|
emoji: true
|
||||||
fontFamily:
|
children: StringChild
|
||||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif',
|
}
|
||||||
}
|
| {
|
||||||
|
emoji?: false
|
||||||
|
children: TextProps['children']
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export function Text({
|
export function Text({
|
||||||
type = 'md',
|
type = 'md',
|
||||||
children,
|
children,
|
||||||
|
emoji,
|
||||||
lineHeight,
|
lineHeight,
|
||||||
style,
|
style,
|
||||||
title,
|
title,
|
||||||
@@ -35,6 +48,20 @@ export function Text({
|
|||||||
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
|
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
|
||||||
const {fonts} = useAlf()
|
const {fonts} = useAlf()
|
||||||
|
|
||||||
|
if (IS_DEV) {
|
||||||
|
if (!emoji && childHasEmoji(children)) {
|
||||||
|
logger.warn(
|
||||||
|
`Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emoji && !childIsString(children)) {
|
||||||
|
throw new Error(
|
||||||
|
'Text: when <Text emoji />, children can only be strings.',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (selectable && isIOS) {
|
if (selectable && isIOS) {
|
||||||
const flattened = StyleSheet.flatten([
|
const flattened = StyleSheet.flatten([
|
||||||
s.black,
|
s.black,
|
||||||
@@ -58,7 +85,7 @@ export function Text({
|
|||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
uiTextView
|
uiTextView
|
||||||
{...props}>
|
{...props}>
|
||||||
{children}
|
{isIOS && emoji ? renderChildrenWithEmoji(children) : children}
|
||||||
</UITextView>
|
</UITextView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -66,7 +93,6 @@ export function Text({
|
|||||||
const flattened = StyleSheet.flatten([
|
const flattened = StyleSheet.flatten([
|
||||||
s.black,
|
s.black,
|
||||||
typography,
|
typography,
|
||||||
isWeb && fontFamilyStyle,
|
|
||||||
lineHeightStyle,
|
lineHeightStyle,
|
||||||
style,
|
style,
|
||||||
])
|
])
|
||||||
@@ -87,7 +113,7 @@ export function Text({
|
|||||||
dataSet={Object.assign({tooltip: title}, dataSet || {})}
|
dataSet={Object.assign({tooltip: title}, dataSet || {})}
|
||||||
selectable={selectable}
|
selectable={selectable}
|
||||||
{...props}>
|
{...props}>
|
||||||
{children}
|
{isIOS && emoji ? renderChildrenWithEmoji(children) : children}
|
||||||
</RNText>
|
</RNText>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11360,6 +11360,11 @@ emoji-mart@^5.5.2:
|
|||||||
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af"
|
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af"
|
||||||
integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==
|
integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==
|
||||||
|
|
||||||
|
emoji-regex@^10.4.0:
|
||||||
|
version "10.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4"
|
||||||
|
integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||||
|
|||||||
Reference in New Issue
Block a user