From ff23ddb556be4b2a9c4029dce6f857df34fc0b6b Mon Sep 17 00:00:00 2001
From: dan <dan.abramov@gmail.com>
Date: Thu, 21 Nov 2024 19:20:46 +0000
Subject: Don't underline links on native hover (#6588)

---
 src/components/RichText.tsx | 18 ++++++------------
 1 file changed, 6 insertions(+), 12 deletions(-)

(limited to 'src/components/RichText.tsx')

diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index 8f6358dd5..e2d05ac6c 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -53,7 +53,6 @@ export function RichText({
   const plainStyles = [a.leading_snug, flattenedStyle]
   const interactiveStyles = [
     a.leading_snug,
-    a.pointer_events_auto,
     flatten(interactiveStyle),
     flattenedStyle,
   ]
@@ -194,11 +193,6 @@ function RichTextTag({
     onOut: onHoverOut,
   } = useInteractionState()
   const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
-  const {
-    state: pressed,
-    onIn: onPressIn,
-    onOut: onPressOut,
-  } = useInteractionState()
   const navigation = useNavigation<NavigationProp>()
 
   const navigateToPage = React.useCallback(() => {
@@ -228,8 +222,6 @@ function RichTextTag({
             accessibilityRole: isNative ? 'button' : undefined,
             onPress: navigateToPage,
             onLongPress: openDialog,
-            onPressIn: onPressIn,
-            onPressOut: onPressOut,
           })}
           {...web({
             onMouseEnter: onHoverIn,
@@ -243,10 +235,12 @@ function RichTextTag({
               cursor: 'pointer',
             }),
             {color: t.palette.primary_500},
-            (hovered || focused || pressed) && {
-              ...web({outline: 0}),
-              textDecorationLine: 'underline',
-              textDecorationColor: t.palette.primary_500,
+            (hovered || focused) && {
+              ...web({
+                outline: 0,
+                textDecorationLine: 'underline',
+                textDecorationColor: t.palette.primary_500,
+              }),
             },
             style,
           ]}>
-- 
cgit 1.4.1


From dc3a42edb1bcdb8c7f07111e936dddcfac1c1bb1 Mon Sep 17 00:00:00 2001
From: dan <dan.abramov@gmail.com>
Date: Thu, 21 Nov 2024 22:52:16 +0000
Subject: Reduce <Text> nesting (#6615)

* Move isOnlyEmoji out of RichText

To fix Fast Refresh.

* Make renderChildrenWithEmoji work with any children

* Always go through UITextView for consistency

It already contains the `selectable` and `iOS` checks inside.

* Move `emoji` check into `renderChildrenWithEmoji`

* Remove unnecessary intermediate UITextView nodes

* Make childHasEmoji check recursive

It didn't handle nested arrays etc correctly before.

* Remove the "children must be string" limitation

Should not be necessary now that we correctly handle nested arrays etc.

* Fix unnecessary regex reallocation

This doesn't have a global flag so it's okay to reuse.

* Remove unnecessary <Text> wrapper in RichText
---
 src/alf/typography.tsx             | 91 +++++++++++++++++---------------------
 src/components/RichText.tsx        | 15 ++-----
 src/components/Typography.tsx      |  8 +---
 src/components/dms/MessageItem.tsx |  3 +-
 src/view/com/util/text/Text.tsx    | 23 ++--------
 5 files changed, 51 insertions(+), 89 deletions(-)

(limited to 'src/components/RichText.tsx')

diff --git a/src/alf/typography.tsx b/src/alf/typography.tsx
index d00910bf8..1b1e81bd4 100644
--- a/src/alf/typography.tsx
+++ b/src/alf/typography.tsx
@@ -1,10 +1,11 @@
-import React from 'react'
+import React, {Children} from 'react'
 import {TextProps as RNTextProps} from 'react-native'
 import {StyleProp, TextStyle} from 'react-native'
 import {UITextView} from 'react-native-uitextview'
 import createEmojiRegex from 'emoji-regex'
 
 import {isNative} from '#/platform/detection'
+import {isIOS} from '#/platform/detection'
 import {Alf, applyFonts, atoms, flatten} from '#/alf'
 
 /**
@@ -57,7 +58,7 @@ export function normalizeTextStyles(
 }
 
 export type StringChild = string | (string | null)[]
-export type TextProps = Omit<RNTextProps, 'children'> & {
+export type TextProps = RNTextProps & {
   /**
    * Lets the user select text, to use the native copy and paste functionality.
    */
@@ -71,65 +72,55 @@ export type TextProps = Omit<RNTextProps, 'children'> & {
    * Appears as a small tooltip on web hover.
    */
   title?: string
-} & (
-    | {
-        emoji?: true
-        children: StringChild
-      }
-    | {
-        emoji?: false
-        children: RNTextProps['children']
-      }
-  )
+  /**
+   * Whether the children could possibly contain emoji.
+   */
+  emoji?: boolean
+}
 
 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))
-  )
+  let hasEmoji = false
+  Children.forEach(children, child => {
+    if (typeof child === 'string' && createEmojiRegex().test(child)) {
+      hasEmoji = true
+    }
+  })
+  return hasEmoji
 }
 
 export function renderChildrenWithEmoji(
-  children: StringChild,
+  children: React.ReactNode,
   props: Omit<TextProps, 'children'> = {},
+  emoji: boolean,
 ) {
-  const normalized = Array.isArray(children) ? children : [children]
+  if (!isIOS || !emoji) {
+    return children
+  }
+  return Children.map(children, child => {
+    if (typeof child !== 'string') return child
 
-  return (
-    <UITextView {...props}>
-      {normalized.map(child => {
-        if (typeof child !== 'string') return child
+    const emojis = child.match(EMOJI)
 
-        const emojis = child.match(EMOJI)
+    if (emojis === null) {
+      return child
+    }
 
-        if (emojis === null) {
-          return child
-        }
+    return child.split(EMOJI).map((stringPart, index) => [
+      stringPart,
+      emojis[index] ? (
+        <UITextView
+          {...props}
+          style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
+          {emojis[index]}
+        </UITextView>
+      ) : null,
+    ])
+  })
+}
 
-        return child.split(EMOJI).map((stringPart, index) => (
-          <UITextView key={index} {...props}>
-            {stringPart}
-            {emojis[index] ? (
-              <UITextView
-                {...props}
-                style={[props?.style, {color: 'black', fontFamily: 'System'}]}>
-                {emojis[index]}
-              </UITextView>
-            ) : null}
-          </UITextView>
-        ))
-      })}
-    </UITextView>
-  )
+const SINGLE_EMOJI_RE = /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u
+export function isOnlyEmoji(text: string) {
+  return text.length <= 15 && SINGLE_EMOJI_RE.test(text)
 }
diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx
index e2d05ac6c..6d7e50e48 100644
--- a/src/components/RichText.tsx
+++ b/src/components/RichText.tsx
@@ -9,6 +9,7 @@ import {NavigationProp} from '#/lib/routes/types'
 import {toShortUrl} from '#/lib/strings/url-helpers'
 import {isNative} from '#/platform/detection'
 import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf'
+import {isOnlyEmoji} from '#/alf/typography'
 import {useInteractionState} from '#/components/hooks/useInteractionState'
 import {InlineLinkText, LinkProps} from '#/components/Link'
 import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -150,17 +151,14 @@ export function RichText({
         />,
       )
     } else {
-      els.push(
-        <Text key={key} emoji style={plainStyles}>
-          {segment.text}
-        </Text>,
-      )
+      els.push(segment.text)
     }
     key++
   }
 
   return (
     <Text
+      emoji
       selectable={selectable}
       testID={testID}
       style={plainStyles}
@@ -250,10 +248,3 @@ function RichTextTag({
     </React.Fragment>
   )
 }
-
-export function isOnlyEmoji(text: string) {
-  return (
-    text.length <= 15 &&
-    /^[\p{Emoji_Presentation}\p{Extended_Pictographic}]+$/u.test(text)
-  )
-}
diff --git a/src/components/Typography.tsx b/src/components/Typography.tsx
index 4bcbf9f09..3e202cb8f 100644
--- a/src/components/Typography.tsx
+++ b/src/components/Typography.tsx
@@ -1,11 +1,9 @@
 import {UITextView} from 'react-native-uitextview'
 
 import {logger} from '#/logger'
-import {isIOS} from '#/platform/detection'
 import {atoms, flatten, useAlf, useTheme, web} from '#/alf'
 import {
   childHasEmoji,
-  childIsString,
   normalizeTextStyles,
   renderChildrenWithEmoji,
   TextProps,
@@ -39,10 +37,6 @@ export function Text({
         `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
       )
     }
-
-    if (emoji && !childIsString(children)) {
-      logger.error('Text: when <Text emoji />, children can only be strings.')
-    }
   }
 
   const shared = {
@@ -55,7 +49,7 @@ export function Text({
 
   return (
     <UITextView {...shared}>
-      {isIOS && emoji ? renderChildrenWithEmoji(children, shared) : children}
+      {renderChildrenWithEmoji(children, shared, emoji ?? false)}
     </UITextView>
   )
 }
diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx
index 52220e2ca..79f0997fd 100644
--- a/src/components/dms/MessageItem.tsx
+++ b/src/components/dms/MessageItem.tsx
@@ -19,10 +19,11 @@ import {ConvoItem} from '#/state/messages/convo/types'
 import {useSession} from '#/state/session'
 import {TimeElapsed} from '#/view/com/util/TimeElapsed'
 import {atoms as a, useTheme} from '#/alf'
+import {isOnlyEmoji} from '#/alf/typography'
 import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
 import {InlineLinkText} from '#/components/Link'
 import {Text} from '#/components/Typography'
-import {isOnlyEmoji, RichText} from '../RichText'
+import {RichText} from '../RichText'
 import {DateDivider} from './DateDivider'
 import {MessageItemEmbed} from './MessageItemEmbed'
 import {localDateString} from './util'
diff --git a/src/view/com/util/text/Text.tsx b/src/view/com/util/text/Text.tsx
index a5278e0a0..f05274f44 100644
--- a/src/view/com/util/text/Text.tsx
+++ b/src/view/com/util/text/Text.tsx
@@ -1,5 +1,5 @@
 import React from 'react'
-import {StyleSheet, Text as RNText, TextProps} from 'react-native'
+import {StyleSheet, TextProps} from 'react-native'
 import {UITextView} from 'react-native-uitextview'
 
 import {lh, s} from '#/lib/styles'
@@ -9,7 +9,6 @@ import {isIOS, isWeb} from '#/platform/detection'
 import {applyFonts, useAlf} from '#/alf'
 import {
   childHasEmoji,
-  childIsString,
   renderChildrenWithEmoji,
   StringChild,
 } from '#/alf/typography'
@@ -56,10 +55,6 @@ function Text_DEPRECATED({
         `Text: emoji detected but emoji not enabled: "${children}"\n\nPlease add <Text emoji />'`,
       )
     }
-
-    if (emoji && !childIsString(children)) {
-      logger.error('Text: when <Text emoji />, children can only be strings.')
-    }
   }
 
   const textProps = React.useMemo(() => {
@@ -107,19 +102,9 @@ function Text_DEPRECATED({
     type,
   ])
 
-  if (selectable && isIOS) {
-    return (
-      <UITextView {...textProps}>
-        {isIOS && emoji
-          ? renderChildrenWithEmoji(children, textProps)
-          : children}
-      </UITextView>
-    )
-  }
-
   return (
-    <RNText {...textProps}>
-      {isIOS && emoji ? renderChildrenWithEmoji(children, textProps) : children}
-    </RNText>
+    <UITextView {...textProps}>
+      {renderChildrenWithEmoji(children, textProps, emoji ?? false)}
+    </UITextView>
   )
 }
-- 
cgit 1.4.1

