Enable profile hover card on quoted post authors (#9896)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-02-18 14:35:38 +00:00
committed by GitHub
parent c188fd4f04
commit b12d703353
3 changed files with 28 additions and 25 deletions
+8 -9
View File
@@ -285,15 +285,14 @@ export function QuoteEmbed({
const contents = ( const contents = (
<> <>
<View pointerEvents="none"> <PostMeta
<PostMeta author={quote.author}
author={quote.author} moderation={moderation}
moderation={moderation} showAvatar
showAvatar postHref={itemHref}
postHref={itemHref} timestamp={quote.indexedAt}
timestamp={quote.indexedAt} linkDisabled
/> />
</View>
{moderation ? ( {moderation ? (
<PostAlerts modui={moderation.ui('contentView')} style={[a.py_xs]} /> <PostAlerts modui={moderation.ui('contentView')} style={[a.py_xs]} />
) : null} ) : null}
+16 -12
View File
@@ -12,7 +12,7 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {niceDate} from '#/lib/strings/time' import {niceDate} from '#/lib/strings/time'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {precacheProfile} from '#/state/queries/profile' import {unstableCacheProfileView} from '#/state/queries/profile'
import {atoms as a, platform, useTheme, web} from '#/alf' import {atoms as a, platform, useTheme, web} from '#/alf'
import {WebOnlyInlineLinkText} from '#/components/Link' import {WebOnlyInlineLinkText} from '#/components/Link'
import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {ProfileHoverCard} from '#/components/ProfileHoverCard'
@@ -29,6 +29,7 @@ interface PostMetaOpts {
moderation: ModerationDecision | undefined moderation: ModerationDecision | undefined
postHref: string postHref: string
timestamp: string timestamp: string
linkDisabled?: boolean
showAvatar?: boolean showAvatar?: boolean
avatarSize?: number avatarSize?: number
onOpenAuthor?: () => void onOpenAuthor?: () => void
@@ -46,17 +47,19 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const onOpenAuthor = opts.onOpenAuthor const onOpenAuthor = opts.onOpenAuthor
const onBeforePressAuthor = useCallback(() => { const onBeforePressAuthor = useCallback(() => {
precacheProfile(queryClient, author) unstableCacheProfileView(queryClient, author)
onOpenAuthor?.() onOpenAuthor?.()
}, [queryClient, author, onOpenAuthor]) }, [queryClient, author, onOpenAuthor])
const onBeforePressPost = useCallback(() => { const onBeforePressPost = useCallback(() => {
precacheProfile(queryClient, author) unstableCacheProfileView(queryClient, author)
}, [queryClient, author]) }, [queryClient, author])
const timestampLabel = niceDate(i18n, opts.timestamp) const timestampLabel = niceDate(i18n, opts.timestamp)
const verification = useSimpleVerificationState({profile: author}) const verification = useSimpleVerificationState({profile: author})
const {isActive: live} = useActorStatus(author) const {isActive: live} = useActorStatus(author)
const MaybeLinkText = opts.linkDisabled ? Text : WebOnlyInlineLinkText
return ( return (
<View <View
style={[ style={[
@@ -77,19 +80,20 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
type={author.associated?.labeler ? 'labeler' : 'user'} type={author.associated?.labeler ? 'labeler' : 'user'}
live={live} live={live}
hideLiveBadge hideLiveBadge
disableNavigation={opts.linkDisabled}
/> />
</View> </View>
)} )}
<View style={[a.flex_row, a.align_end, a.flex_shrink]}> <View style={[a.flex_row, a.align_end, a.flex_shrink]}>
<ProfileHoverCard did={author.did}> <ProfileHoverCard did={author.did}>
<View style={[a.flex_row, a.align_end, a.flex_shrink]}> <View style={[a.flex_row, a.align_end, a.flex_shrink]}>
<WebOnlyInlineLinkText <MaybeLinkText
emoji emoji
numberOfLines={1} numberOfLines={1}
to={profileLink} to={profileLink}
label={_(msg`View profile`)} label={_(msg`View profile`)}
disableMismatchWarning disableMismatchWarning
onPress={onBeforePressAuthor} onPress={opts.linkDisabled ? undefined : onBeforePressAuthor}
style={[ style={[
a.text_md, a.text_md,
a.font_semi_bold, a.font_semi_bold,
@@ -104,7 +108,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
opts.moderation?.ui('displayName'), opts.moderation?.ui('displayName'),
), ),
)} )}
</WebOnlyInlineLinkText> </MaybeLinkText>
{verification.showBadge && ( {verification.showBadge && (
<View <View
style={[ style={[
@@ -120,14 +124,14 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
/> />
</View> </View>
)} )}
<WebOnlyInlineLinkText <MaybeLinkText
emoji emoji
numberOfLines={1} numberOfLines={1}
to={profileLink} to={profileLink}
label={_(msg`View profile`)} label={_(msg`View profile`)}
disableMismatchWarning disableMismatchWarning
disableUnderline disableUnderline
onPress={onBeforePressAuthor} onPress={opts.linkDisabled ? undefined : onBeforePressAuthor}
style={[ style={[
a.text_md, a.text_md,
t.atoms.text_contrast_medium, t.atoms.text_contrast_medium,
@@ -135,19 +139,19 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
{flexShrink: 10}, {flexShrink: 10},
]}> ]}>
{NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
</WebOnlyInlineLinkText> </MaybeLinkText>
</View> </View>
</ProfileHoverCard> </ProfileHoverCard>
<TimeElapsed timestamp={opts.timestamp}> <TimeElapsed timestamp={opts.timestamp}>
{({timeElapsed}) => ( {({timeElapsed}) => (
<WebOnlyInlineLinkText <MaybeLinkText
to={opts.postHref} to={opts.postHref}
label={timestampLabel} label={timestampLabel}
title={timestampLabel} title={timestampLabel}
disableMismatchWarning disableMismatchWarning
disableUnderline disableUnderline
onPress={onBeforePressPost} onPress={opts.linkDisabled ? undefined : onBeforePressPost}
style={[ style={[
a.pl_xs, a.pl_xs,
a.text_md, a.text_md,
@@ -171,7 +175,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
</Text> </Text>
)} )}
{timeElapsed} {timeElapsed}
</WebOnlyInlineLinkText> </MaybeLinkText>
)} )}
</TimeElapsed> </TimeElapsed>
</View> </View>
+4 -4
View File
@@ -1,4 +1,4 @@
import React, {type JSX} from 'react' import {useState} from 'react'
import {type I18n} from '@lingui/core' import {type I18n} from '@lingui/core'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -11,17 +11,17 @@ export function TimeElapsed({
timeToString, timeToString,
}: { }: {
timestamp: string timestamp: string
children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element children: ({timeElapsed}: {timeElapsed: string}) => React.ReactElement
timeToString?: (i18n: I18n, timeElapsed: string) => string timeToString?: (i18n: I18n, timeElapsed: string) => string
}) { }) {
const {i18n} = useLingui() const {i18n} = useLingui()
const ago = useGetTimeAgo() const ago = useGetTimeAgo()
const tick = useTickEveryMinute() const tick = useTickEveryMinute()
const [timeElapsed, setTimeAgo] = React.useState(() => const [timeElapsed, setTimeAgo] = useState(() =>
timeToString ? timeToString(i18n, timestamp) : ago(timestamp, tick), timeToString ? timeToString(i18n, timestamp) : ago(timestamp, tick),
) )
const [prevTick, setPrevTick] = React.useState(tick) const [prevTick, setPrevTick] = useState(tick)
if (prevTick !== tick) { if (prevTick !== tick) {
setPrevTick(tick) setPrevTick(tick)
setTimeAgo( setTimeAgo(