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"