about summary refs log tree commit diff
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-08-14 13:14:18 +0300
committerGitHub <noreply@github.com>2025-08-14 03:14:18 -0700
commit221623f55aa6c1bbe699c8d409832da110923c76 (patch)
tree7fca1fc109adf99ad3f92fb2e2b10d964c35a929
parentf4dca5d230fabf6f1f4f82617964f46e07b8a5be (diff)
downloadvoidsky-221623f55aa6c1bbe699c8d409832da110923c76.tar.zst
Improve "replied to a post" component (#8602)
* unify component

* change bottom padding from 2px to 4px
-rw-r--r--assets/icons/arrowCornerDownRight_stroke2_rounded_2_rounded.svg1
-rw-r--r--src/components/Post/PostRepliedTo.tsx63
-rw-r--r--src/components/icons/ArrowCornerDownRight.tsx7
-rw-r--r--src/view/com/post/Post.tsx45
-rw-r--r--src/view/com/posts/PostFeedItem.tsx84
-rw-r--r--src/view/com/util/UserInfoText.tsx68
6 files changed, 110 insertions, 158 deletions
diff --git a/assets/icons/arrowCornerDownRight_stroke2_rounded_2_rounded.svg b/assets/icons/arrowCornerDownRight_stroke2_rounded_2_rounded.svg
new file mode 100644
index 000000000..fa21c9824
--- /dev/null
+++ b/assets/icons/arrowCornerDownRight_stroke2_rounded_2_rounded.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" d="M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z"/></svg>
diff --git a/src/components/Post/PostRepliedTo.tsx b/src/components/Post/PostRepliedTo.tsx
new file mode 100644
index 000000000..3085826c2
--- /dev/null
+++ b/src/components/Post/PostRepliedTo.tsx
@@ -0,0 +1,63 @@
+import {View} from 'react-native'
+import {Trans} from '@lingui/macro'
+
+import {useSession} from '#/state/session'
+import {UserInfoText} from '#/view/com/util/UserInfoText'
+import {atoms as a, useTheme} from '#/alf'
+import {ArrowCornerDownRight_Stroke2_Corner2_Rounded as ArrowCornerDownRightIcon} from '#/components/icons/ArrowCornerDownRight'
+import {ProfileHoverCard} from '#/components/ProfileHoverCard'
+import {Text} from '#/components/Typography'
+import type * as bsky from '#/types/bsky'
+
+export function PostRepliedTo({
+  parentAuthor,
+  isParentBlocked,
+  isParentNotFound,
+}: {
+  parentAuthor: string | bsky.profile.AnyProfileView | undefined
+  isParentBlocked?: boolean
+  isParentNotFound?: boolean
+}) {
+  const t = useTheme()
+  const {currentAccount} = useSession()
+
+  const textStyle = [a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]
+
+  let label
+  if (isParentBlocked) {
+    label = <Trans context="description">Replied to a blocked post</Trans>
+  } else if (isParentNotFound) {
+    label = <Trans context="description">Replied to a post</Trans>
+  } else if (parentAuthor) {
+    const did =
+      typeof parentAuthor === 'string' ? parentAuthor : parentAuthor.did
+    const isMe = currentAccount?.did === did
+    if (isMe) {
+      label = <Trans context="description">Replied to you</Trans>
+    } else {
+      label = (
+        <Trans context="description">
+          Replied to{' '}
+          <ProfileHoverCard did={did}>
+            <UserInfoText did={did} attr="displayName" style={textStyle} />
+          </ProfileHoverCard>
+        </Trans>
+      )
+    }
+  }
+
+  if (!label) {
+    // Should not happen.
+    return null
+  }
+
+  return (
+    <View style={[a.flex_row, a.align_center, a.pb_xs, a.gap_xs]}>
+      <ArrowCornerDownRightIcon
+        size="xs"
+        style={[t.atoms.text_contrast_medium, {top: -1}]}
+      />
+      <Text style={textStyle}>{label}</Text>
+    </View>
+  )
+}
diff --git a/src/components/icons/ArrowCornerDownRight.tsx b/src/components/icons/ArrowCornerDownRight.tsx
new file mode 100644
index 000000000..86dde7015
--- /dev/null
+++ b/src/components/icons/ArrowCornerDownRight.tsx
@@ -0,0 +1,7 @@
+import {createSinglePathSVG} from './TEMPLATE'
+
+export const ArrowCornerDownRight_Stroke2_Corner2_Rounded = createSinglePathSVG(
+  {
+    path: 'M15.793 10.293a1 1 0 0 1 1.338-.068l.076.068 3.293 3.293a2 2 0 0 1 .138 2.677l-.138.151-3.293 3.293a1 1 0 1 1-1.414-1.414L18.086 16H8a5 5 0 0 1-5-5V5a1 1 0 0 1 2 0v6a3 3 0 0 0 3 3h10.086l-2.293-2.293-.068-.076a1 1 0 0 1 .068-1.338Z',
+  },
+)
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 6079f5c10..a8e32268e 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -8,8 +8,6 @@ import {
   type ModerationDecision,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {Trans} from '@lingui/macro'
 import {useQueryClient} from '@tanstack/react-query'
 
 import {MAX_POST_LINES} from '#/lib/constants'
@@ -17,28 +15,25 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
 import {usePalette} from '#/lib/hooks/usePalette'
 import {makeProfileLink} from '#/lib/routes/links'
 import {countLines} from '#/lib/strings/helpers'
-import {colors, s} from '#/lib/styles'
+import {colors} from '#/lib/styles'
 import {
   POST_TOMBSTONE,
   type Shadow,
   usePostShadow,
 } from '#/state/cache/post-shadow'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {precacheProfile} from '#/state/queries/profile'
-import {useSession} from '#/state/session'
+import {unstableCacheProfileView} from '#/state/queries/profile'
 import {Link} from '#/view/com/util/Link'
 import {PostMeta} from '#/view/com/util/PostMeta'
-import {Text} from '#/view/com/util/text/Text'
 import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
-import {UserInfoText} from '#/view/com/util/UserInfoText'
 import {atoms as a} from '#/alf'
 import {ContentHider} from '#/components/moderation/ContentHider'
 import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
 import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
+import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
 import {PostControls} from '#/components/PostControls'
-import {ProfileHoverCard} from '#/components/ProfileHoverCard'
 import {RichText} from '#/components/RichText'
 import {SubtleWebHover} from '#/components/SubtleWebHover'
 import * as bsky from '#/types/bsky'
@@ -145,12 +140,9 @@ function PostInner({
   }, [setLimitLines])
 
   const onBeforePress = useCallback(() => {
-    precacheProfile(queryClient, post.author)
+    unstableCacheProfileView(queryClient, post.author)
   }, [queryClient, post.author])
 
-  const {currentAccount} = useSession()
-  const isMe = replyAuthorDid === currentAccount?.did
-
   const [hover, setHover] = useState(false)
   return (
     <Link
@@ -187,34 +179,7 @@ function PostInner({
             postHref={itemHref}
           />
           {replyAuthorDid !== '' && (
-            <View style={[s.flexRow, s.mb2, s.alignCenter]}>
-              <FontAwesomeIcon
-                icon="reply"
-                size={9}
-                style={[pal.textLight, s.mr5]}
-              />
-              <Text
-                type="sm"
-                style={[pal.textLight, s.mr2]}
-                lineHeight={1.2}
-                numberOfLines={1}>
-                {isMe ? (
-                  <Trans context="description">Reply to you</Trans>
-                ) : (
-                  <Trans context="description">
-                    Reply to{' '}
-                    <ProfileHoverCard did={replyAuthorDid}>
-                      <UserInfoText
-                        type="sm"
-                        did={replyAuthorDid}
-                        attr="displayName"
-                        style={[pal.textLight]}
-                      />
-                    </ProfileHoverCard>
-                  </Trans>
-                )}
-              </Text>
-            </View>
+            <PostRepliedTo parentAuthor={replyAuthorDid} />
           )}
           <LabelsOnMyPost post={post} />
           <ContentHider
diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx
index 0593ba931..14bbc4746 100644
--- a/src/view/com/posts/PostFeedItem.tsx
+++ b/src/view/com/posts/PostFeedItem.tsx
@@ -9,10 +9,6 @@ import {
   type ModerationDecision,
   RichText as RichTextAPI,
 } from '@atproto/api'
-import {
-  FontAwesomeIcon,
-  type FontAwesomeIconStyle,
-} from '@fortawesome/react-native-fontawesome'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useQueryClient} from '@tanstack/react-query'
@@ -26,7 +22,6 @@ import {makeProfileLink} from '#/lib/routes/links'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {countLines} from '#/lib/strings/helpers'
-import {s} from '#/lib/styles'
 import {
   POST_TOMBSTONE,
   type Shadow,
@@ -54,6 +49,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
 import {type AppModerationCause} from '#/components/Pills'
 import {Embed} from '#/components/Post/Embed'
 import {PostEmbedViewContext} from '#/components/Post/Embed/types'
+import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
 import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
 import {PostControls} from '#/components/PostControls'
 import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
@@ -448,10 +444,10 @@ let FeedItemInner = ({
           />
           {showReplyTo &&
             (parentAuthor || isParentBlocked || isParentNotFound) && (
-              <ReplyToLabel
-                blocked={isParentBlocked}
-                notFound={isParentNotFound}
-                profile={parentAuthor}
+              <PostRepliedTo
+                parentAuthor={parentAuthor}
+                isParentBlocked={isParentBlocked}
+                isParentNotFound={isParentNotFound}
               />
             )}
           <LabelsOnMyPost post={post} />
@@ -576,80 +572,10 @@ let PostContent = ({
 }
 PostContent = memo(PostContent)
 
-function ReplyToLabel({
-  profile,
-  blocked,
-  notFound,
-}: {
-  profile: AppBskyActorDefs.ProfileViewBasic | undefined
-  blocked?: boolean
-  notFound?: boolean
-}) {
-  const pal = usePalette('default')
-  const {currentAccount} = useSession()
-
-  let label
-  if (blocked) {
-    label = <Trans context="description">Reply to a blocked post</Trans>
-  } else if (notFound) {
-    label = <Trans context="description">Reply to a post</Trans>
-  } else if (profile != null) {
-    const isMe = profile.did === currentAccount?.did
-    if (isMe) {
-      label = <Trans context="description">Reply to you</Trans>
-    } else {
-      label = (
-        <Trans context="description">
-          Reply to{' '}
-          <ProfileHoverCard did={profile.did}>
-            <TextLinkOnWebOnly
-              type="md"
-              style={pal.textLight}
-              lineHeight={1.2}
-              numberOfLines={1}
-              href={makeProfileLink(profile)}
-              text={
-                <Text emoji type="md" style={pal.textLight} lineHeight={1.2}>
-                  {profile.displayName
-                    ? sanitizeDisplayName(profile.displayName)
-                    : sanitizeHandle(profile.handle)}
-                </Text>
-              }
-            />
-          </ProfileHoverCard>
-        </Trans>
-      )
-    }
-  }
-
-  if (!label) {
-    // Should not happen.
-    return null
-  }
-
-  return (
-    <View style={[s.flexRow, s.mb2, s.alignCenter]}>
-      <FontAwesomeIcon
-        icon="reply"
-        size={9}
-        style={[{color: pal.colors.textLight} as FontAwesomeIconStyle, s.mr5]}
-      />
-      <Text
-        type="md"
-        style={[pal.textLight, s.mr2]}
-        lineHeight={1.2}
-        numberOfLines={1}>
-        {label}
-      </Text>
-    </View>
-  )
-}
-
 const styles = StyleSheet.create({
   outer: {
     paddingLeft: 10,
     paddingRight: 15,
-    // @ts-ignore web only -prf
     cursor: 'pointer',
   },
   replyLine: {
diff --git a/src/view/com/util/UserInfoText.tsx b/src/view/com/util/UserInfoText.tsx
index 64aa37ff2..028b85d38 100644
--- a/src/view/com/util/UserInfoText.tsx
+++ b/src/view/com/util/UserInfoText.tsx
@@ -1,27 +1,25 @@
-import {StyleProp, StyleSheet, TextStyle} from 'react-native'
-import {AppBskyActorGetProfile as GetProfile} from '@atproto/api'
+import {type StyleProp, type TextStyle} from 'react-native'
+import {type AppBskyActorGetProfile} from '@atproto/api'
 
 import {makeProfileLink} from '#/lib/routes/links'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
-import {TypographyVariant} from '#/lib/ThemeContext'
 import {STALE} from '#/state/queries'
 import {useProfileQuery} from '#/state/queries/profile'
-import {TextLinkOnWebOnly} from './Link'
+import {atoms as a} from '#/alf'
+import {InlineLinkText} from '#/components/Link'
+import {Text} from '#/components/Typography'
 import {LoadingPlaceholder} from './LoadingPlaceholder'
-import {Text} from './text/Text'
 
 export function UserInfoText({
-  type = 'md',
   did,
   attr,
   failed,
   prefix,
   style,
 }: {
-  type?: TypographyVariant
   did: string
-  attr?: keyof GetProfile.OutputSchema
+  attr?: keyof AppBskyActorGetProfile.OutputSchema
   loading?: string
   failed?: string
   prefix?: string
@@ -35,45 +33,37 @@ export function UserInfoText({
     staleTime: STALE.INFINITY,
   })
 
-  let inner
   if (isError) {
-    inner = (
-      <Text type={type} style={style} numberOfLines={1}>
+    return (
+      <Text style={style} numberOfLines={1}>
         {failed}
       </Text>
     )
   } else if (profile) {
-    inner = (
-      <TextLinkOnWebOnly
-        type={type}
+    const text = `${prefix || ''}${sanitizeDisplayName(
+      typeof profile[attr] === 'string' && profile[attr]
+        ? (profile[attr] as string)
+        : sanitizeHandle(profile.handle),
+    )}`
+    return (
+      <InlineLinkText
+        label={text}
         style={style}
-        lineHeight={1.2}
         numberOfLines={1}
-        href={makeProfileLink(profile)}
-        text={
-          <Text emoji type={type} style={style} lineHeight={1.2}>
-            {`${prefix || ''}${sanitizeDisplayName(
-              typeof profile[attr] === 'string' && profile[attr]
-                ? (profile[attr] as string)
-                : sanitizeHandle(profile.handle),
-            )}`}
-          </Text>
-        }
-      />
-    )
-  } else {
-    inner = (
-      <LoadingPlaceholder
-        width={80}
-        height={8}
-        style={styles.loadingPlaceholder}
-      />
+        to={makeProfileLink(profile)}>
+        <Text emoji style={style}>
+          {text}
+        </Text>
+      </InlineLinkText>
     )
   }
 
-  return inner
+  // eslint-disable-next-line bsky-internal/avoid-unwrapped-text
+  return (
+    <LoadingPlaceholder
+      width={80}
+      height={8}
+      style={[a.relative, {top: 1, left: 2}]}
+    />
+  )
 }
-
-const styles = StyleSheet.create({
-  loadingPlaceholder: {position: 'relative', top: 1, left: 2},
-})