Improve "replied to a post" component (#8602)

* unify component

* change bottom padding from 2px to 4px
This commit is contained in:
Samuel Newman
2025-08-14 13:14:18 +03:00
committed by GitHub
parent f4dca5d230
commit 221623f55a
6 changed files with 110 additions and 158 deletions
@@ -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>

After

Width:  |  Height:  |  Size: 334 B

+63
View File
@@ -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>
)
}
@@ -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',
},
)
+5 -40
View File
@@ -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
+5 -79
View File
@@ -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: {
+29 -39
View File
@@ -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},
})