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, type ModerationDecision,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Trans} from '@lingui/macro'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {MAX_POST_LINES} from '#/lib/constants' import {MAX_POST_LINES} from '#/lib/constants'
@@ -17,28 +15,25 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer'
import {usePalette} from '#/lib/hooks/usePalette' import {usePalette} from '#/lib/hooks/usePalette'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {colors, s} from '#/lib/styles' import {colors} from '#/lib/styles'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
type Shadow, type Shadow,
usePostShadow, usePostShadow,
} from '#/state/cache/post-shadow' } from '#/state/cache/post-shadow'
import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {precacheProfile} from '#/state/queries/profile' import {unstableCacheProfileView} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {Link} from '#/view/com/util/Link' import {Link} from '#/view/com/util/Link'
import {PostMeta} from '#/view/com/util/PostMeta' import {PostMeta} from '#/view/com/util/PostMeta'
import {Text} from '#/view/com/util/text/Text'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {UserInfoText} from '#/view/com/util/UserInfoText'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {ContentHider} from '#/components/moderation/ContentHider' import {ContentHider} from '#/components/moderation/ContentHider'
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe' import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
import {PostAlerts} from '#/components/moderation/PostAlerts' import {PostAlerts} from '#/components/moderation/PostAlerts'
import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' import {Embed, PostEmbedViewContext} from '#/components/Post/Embed'
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
import {PostControls} from '#/components/PostControls' import {PostControls} from '#/components/PostControls'
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {SubtleWebHover} from '#/components/SubtleWebHover' import {SubtleWebHover} from '#/components/SubtleWebHover'
import * as bsky from '#/types/bsky' import * as bsky from '#/types/bsky'
@@ -145,12 +140,9 @@ function PostInner({
}, [setLimitLines]) }, [setLimitLines])
const onBeforePress = useCallback(() => { const onBeforePress = useCallback(() => {
precacheProfile(queryClient, post.author) unstableCacheProfileView(queryClient, post.author)
}, [queryClient, post.author]) }, [queryClient, post.author])
const {currentAccount} = useSession()
const isMe = replyAuthorDid === currentAccount?.did
const [hover, setHover] = useState(false) const [hover, setHover] = useState(false)
return ( return (
<Link <Link
@@ -187,34 +179,7 @@ function PostInner({
postHref={itemHref} postHref={itemHref}
/> />
{replyAuthorDid !== '' && ( {replyAuthorDid !== '' && (
<View style={[s.flexRow, s.mb2, s.alignCenter]}> <PostRepliedTo parentAuthor={replyAuthorDid} />
<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>
)} )}
<LabelsOnMyPost post={post} /> <LabelsOnMyPost post={post} />
<ContentHider <ContentHider
+5 -79
View File
@@ -9,10 +9,6 @@ import {
type ModerationDecision, type ModerationDecision,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {
FontAwesomeIcon,
type FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
@@ -26,7 +22,6 @@ import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {countLines} from '#/lib/strings/helpers' import {countLines} from '#/lib/strings/helpers'
import {s} from '#/lib/styles'
import { import {
POST_TOMBSTONE, POST_TOMBSTONE,
type Shadow, type Shadow,
@@ -54,6 +49,7 @@ import {PostAlerts} from '#/components/moderation/PostAlerts'
import {type AppModerationCause} from '#/components/Pills' import {type AppModerationCause} from '#/components/Pills'
import {Embed} from '#/components/Post/Embed' import {Embed} from '#/components/Post/Embed'
import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {PostEmbedViewContext} from '#/components/Post/Embed/types'
import {PostRepliedTo} from '#/components/Post/PostRepliedTo'
import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton' import {ShowMoreTextButton} from '#/components/Post/ShowMoreTextButton'
import {PostControls} from '#/components/PostControls' import {PostControls} from '#/components/PostControls'
import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug' import {DiscoverDebug} from '#/components/PostControls/DiscoverDebug'
@@ -448,10 +444,10 @@ let FeedItemInner = ({
/> />
{showReplyTo && {showReplyTo &&
(parentAuthor || isParentBlocked || isParentNotFound) && ( (parentAuthor || isParentBlocked || isParentNotFound) && (
<ReplyToLabel <PostRepliedTo
blocked={isParentBlocked} parentAuthor={parentAuthor}
notFound={isParentNotFound} isParentBlocked={isParentBlocked}
profile={parentAuthor} isParentNotFound={isParentNotFound}
/> />
)} )}
<LabelsOnMyPost post={post} /> <LabelsOnMyPost post={post} />
@@ -576,80 +572,10 @@ let PostContent = ({
} }
PostContent = memo(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({ const styles = StyleSheet.create({
outer: { outer: {
paddingLeft: 10, paddingLeft: 10,
paddingRight: 15, paddingRight: 15,
// @ts-ignore web only -prf
cursor: 'pointer', cursor: 'pointer',
}, },
replyLine: { replyLine: {
+24 -34
View File
@@ -1,27 +1,25 @@
import {StyleProp, StyleSheet, TextStyle} from 'react-native' import {type StyleProp, type TextStyle} from 'react-native'
import {AppBskyActorGetProfile as GetProfile} from '@atproto/api' import {type AppBskyActorGetProfile} from '@atproto/api'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {TypographyVariant} from '#/lib/ThemeContext'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {useProfileQuery} from '#/state/queries/profile' 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 {LoadingPlaceholder} from './LoadingPlaceholder'
import {Text} from './text/Text'
export function UserInfoText({ export function UserInfoText({
type = 'md',
did, did,
attr, attr,
failed, failed,
prefix, prefix,
style, style,
}: { }: {
type?: TypographyVariant
did: string did: string
attr?: keyof GetProfile.OutputSchema attr?: keyof AppBskyActorGetProfile.OutputSchema
loading?: string loading?: string
failed?: string failed?: string
prefix?: string prefix?: string
@@ -35,45 +33,37 @@ export function UserInfoText({
staleTime: STALE.INFINITY, staleTime: STALE.INFINITY,
}) })
let inner
if (isError) { if (isError) {
inner = ( return (
<Text type={type} style={style} numberOfLines={1}> <Text style={style} numberOfLines={1}>
{failed} {failed}
</Text> </Text>
) )
} else if (profile) { } else if (profile) {
inner = ( const text = `${prefix || ''}${sanitizeDisplayName(
<TextLinkOnWebOnly
type={type}
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] typeof profile[attr] === 'string' && profile[attr]
? (profile[attr] as string) ? (profile[attr] as string)
: sanitizeHandle(profile.handle), : sanitizeHandle(profile.handle),
)}`} )}`
return (
<InlineLinkText
label={text}
style={style}
numberOfLines={1}
to={makeProfileLink(profile)}>
<Text emoji style={style}>
{text}
</Text> </Text>
} </InlineLinkText>
/>
) )
} else { }
inner = (
// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
return (
<LoadingPlaceholder <LoadingPlaceholder
width={80} width={80}
height={8} height={8}
style={styles.loadingPlaceholder} style={[a.relative, {top: 1, left: 2}]}
/> />
) )
}
return inner
} }
const styles = StyleSheet.create({
loadingPlaceholder: {position: 'relative', top: 1, left: 2},
})