Improve "replied to a post" component (#8602)
* unify component * change bottom padding from 2px to 4px
This commit is contained in:
@@ -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 |
@@ -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',
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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},
|
|
||||||
})
|
|
||||||
|
|||||||
Reference in New Issue
Block a user