diff --git a/package.json b/package.json
index 05b5f086db..f4c63f9590 100644
--- a/package.json
+++ b/package.json
@@ -116,6 +116,7 @@
"deprecated-react-native-prop-types": "^5.0.0",
"email-validator": "^2.0.4",
"emoji-mart": "^5.5.2",
+ "emoji-regex": "^10.4.0",
"eventemitter3": "^5.0.1",
"expo": "^51.0.8",
"expo-application": "^5.9.1",
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index 7511775978..1c65a87acc 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -66,6 +66,7 @@ export function RichText({
(flattenedStyle.fontSize ?? a.text_sm.fontSize) * emojiMultiplier
return (
,
)
} else {
- els.push(segment.text)
+ els.push(
+
+ {segment.text}
+ ,
+ )
}
key++
}
@@ -213,6 +219,7 @@ function RichTextTag({
& {
/**
* 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
+ /**
+ * 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 (
+
+ {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) => (
+
+ {stringPart}
+ {emojis[index] ? (
+
+ {emojis[index]}
+
+ ) : null}
+
+ ))
+ })}
+
+ )
}
/**
@@ -64,7 +134,15 @@ export function normalizeTextStyles(
/**
* 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 t = useTheme()
const s = normalizeTextStyles([atoms.text_sm, t.atoms.text, flatten(style)], {
@@ -73,7 +151,31 @@ export function Text({style, selectable, ...rest}: TextProps) {
flags,
})
- return
+ if (IS_DEV) {
+ if (!emoji && childHasEmoji(children)) {
+ logger.warn(
+ `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add '`,
+ )
+ }
+
+ if (emoji && !childIsString(children)) {
+ throw new Error(
+ 'Text: when , children can only be strings.',
+ )
+ }
+ }
+
+ return (
+
+ {isIOS && emoji ? renderChildrenWithEmoji(children) : children}
+
+ )
}
export function createHeadingElement({level}: {level: number}) {
diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx
index 52a45b0e2e..9e4f7e5f5d 100644
--- a/src/view/com/util/text/Text.tsx
+++ b/src/view/com/util/text/Text.tsx
@@ -2,27 +2,40 @@ import React from 'react'
import {StyleSheet, Text as RNText, TextProps} from 'react-native'
import {UITextView} from 'react-native-uitextview'
-import {lh, s} from 'lib/styles'
-import {TypographyVariant, useTheme} from 'lib/ThemeContext'
-import {isIOS, isWeb} from 'platform/detection'
+import {lh, s} from '#/lib/styles'
+import {TypographyVariant, useTheme} from '#/lib/ThemeContext'
+import {logger} from '#/logger'
+import {isIOS} from '#/platform/detection'
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 & {
type?: TypographyVariant
lineHeight?: number
title?: string
dataSet?: Record
selectable?: boolean
-}
-
-const fontFamilyStyle = {
- fontFamily:
- '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif',
-}
+} & (
+ | {
+ emoji: true
+ children: StringChild
+ }
+ | {
+ emoji?: false
+ children: TextProps['children']
+ }
+ )
export function Text({
type = 'md',
children,
+ emoji,
lineHeight,
style,
title,
@@ -35,6 +48,20 @@ export function Text({
const lineHeightStyle = lineHeight ? lh(theme, type, lineHeight) : undefined
const {fonts} = useAlf()
+ if (IS_DEV) {
+ if (!emoji && childHasEmoji(children)) {
+ logger.warn(
+ `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add '`,
+ )
+ }
+
+ if (emoji && !childIsString(children)) {
+ throw new Error(
+ 'Text: when , children can only be strings.',
+ )
+ }
+ }
+
if (selectable && isIOS) {
const flattened = StyleSheet.flatten([
s.black,
@@ -58,7 +85,7 @@ export function Text({
selectable={selectable}
uiTextView
{...props}>
- {children}
+ {isIOS && emoji ? renderChildrenWithEmoji(children) : children}
)
}
@@ -66,7 +93,6 @@ export function Text({
const flattened = StyleSheet.flatten([
s.black,
typography,
- isWeb && fontFamilyStyle,
lineHeightStyle,
style,
])
@@ -87,7 +113,7 @@ export function Text({
dataSet={Object.assign({tooltip: title}, dataSet || {})}
selectable={selectable}
{...props}>
- {children}
+ {isIOS && emoji ? renderChildrenWithEmoji(children) : children}
)
}
diff --git a/yarn.lock b/yarn.lock
index d199d5869f..7077aeda3b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11360,6 +11360,11 @@ emoji-mart@^5.5.2:
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af"
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:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"