Improve byline linking and interaction states

This commit is contained in:
Eric Bailey
2026-05-22 11:59:12 -05:00
parent 4fc548eb17
commit 726c5b9a2d
3 changed files with 140 additions and 89 deletions
+10 -2
View File
@@ -326,6 +326,8 @@ export type InlineLinkProps = React.PropsWithChildren<
disableUnderline?: boolean disableUnderline?: boolean
title?: TextProps['title'] title?: TextProps['title']
overridePresentation?: boolean overridePresentation?: boolean
onMouseEnter?: () => void
onMouseLeave?: () => void
} }
> >
@@ -388,8 +390,14 @@ export function InlineLinkText({
role="link" role="link"
onPress={download ? undefined : onPress} onPress={download ? undefined : onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
onMouseEnter={onHoverIn} onMouseEnter={() => {
onMouseLeave={onHoverOut} rest.onMouseEnter?.()
onHoverIn()
}}
onMouseLeave={() => {
rest.onMouseLeave?.()
onHoverOut()
}}
accessibilityRole="link" accessibilityRole="link"
href={href} href={href}
{...web({ {...web({
@@ -13,9 +13,13 @@ import {Text} from '#/components/Typography'
export function PublicationMetaRow({ export function PublicationMetaRow({
view, view,
author, author,
onInteractWithin,
onInteractWithout,
}: { }: {
view: AppBskyEmbedExternal.ViewExternal view: AppBskyEmbedExternal.ViewExternal
author: {did: string | null | undefined} author: {did: string | null | undefined}
onInteractWithin: () => void
onInteractWithout: () => void
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
@@ -63,7 +67,13 @@ export function PublicationMetaRow({
<InlineLinkText <InlineLinkText
label={l`View @${handle}'s profile`} label={l`View @${handle}'s profile`}
to={makeProfileLink({did: author.did, handle})} to={makeProfileLink({did: author.did, handle})}
style={metaTextStyle}> style={metaTextStyle}
onPress={e => {
// this link is nested, yes it's not ideal
e.stopPropagation()
}}
onMouseEnter={onInteractWithin}
onMouseLeave={onInteractWithout}>
@{handle} @{handle}
</InlineLinkText> </InlineLinkText>
</Trans> </Trans>
@@ -18,6 +18,7 @@ import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, utils} from '#/alf' import {atoms as a, useBreakpoints, useTheme, utils} from '#/alf'
import {ButtonIcon, ButtonText} from '#/components/Button' import {ButtonIcon, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock' import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock'
import {Leaflet} from '#/components/icons/community/Leaflet' import {Leaflet} from '#/components/icons/community/Leaflet'
import {Offprint} from '#/components/icons/community/Offprint' import {Offprint} from '#/components/icons/community/Offprint'
@@ -309,8 +310,10 @@ export const StandardSiteEmbed = ({
</Link> </Link>
{view.source && ( {view.source && (
<View style={[a.px_md]}> <>
<Divider /> <View style={[a.px_md]}>
<Divider />
</View>
<PublicationFooter <PublicationFooter
view={view} view={view}
onPress={onPress} onPress={onPress}
@@ -318,7 +321,7 @@ export const StandardSiteEmbed = ({
themeColors={themeColors} themeColors={themeColors}
author={{did: maybeAuthorDid}} author={{did: maybeAuthorDid}}
/> />
</View> </>
)} )}
</View> </View>
) )
@@ -344,6 +347,11 @@ export function PublicationCard({
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const {gtPhone} = useBreakpoints() const {gtPhone} = useBreakpoints()
const {
state: interactedWithin,
onIn: onInteractWithin,
onOut: onInteractWithout,
} = useInteractionState()
if (!view.source) return null if (!view.source) return null
@@ -354,77 +362,85 @@ export function PublicationCard({
label={l`Subscribe`} label={l`Subscribe`}
onPress={onPress} onPress={onPress}
onLongPress={onLongPress}> onLongPress={onLongPress}>
{({hovered}) => ( {({hovered: maybeHovered}) => {
<View const hovered = maybeHovered && !interactedWithin
style={[ return (
a.flex_col,
a.rounded_md,
a.overflow_hidden,
a.w_full,
a.border,
a.p_md,
t.atoms.border_contrast_low,
style,
]}>
<View <View
style={[ style={[
a.flex_1, a.flex_col,
a.align_center, a.rounded_md,
a.justify_between, a.overflow_hidden,
a.gap_md, a.w_full,
gtPhone && [a.flex_row, a.gap_sm], a.border,
]} a.p_md,
testID="publication-embed-footer"> t.atoms.border_contrast_low,
style,
]}>
<View <View
style={[ style={[
a.w_full, a.flex_1,
a.flex_row,
a.align_center, a.align_center,
a.gap_sm, a.justify_between,
gtPhone && a.flex_1, a.gap_md,
]}> gtPhone && [a.flex_row, a.gap_sm],
<> ]}
<PublicationIcon testID="publication-embed-footer">
<View
style={[
a.w_full,
a.flex_row,
a.align_center,
a.gap_sm,
gtPhone && a.flex_1,
]}>
<>
<PublicationIcon
view={view}
size={40}
hovered={hovered}
themeColors={themeColors}
/>
<View style={[a.flex_1, a.gap_2xs]}>
<Text
numberOfLines={1}
style={[
a.text_md,
a.font_semi_bold,
t.atoms.text,
hovered && a.underline,
]}>
{view.source?.title}
</Text>
<PublicationMetaRow
view={view}
author={author}
onInteractWithin={onInteractWithin}
onInteractWithout={onInteractWithout}
/>
</View>
</>
</View>
{!hideSubscribe && (
<SubscribeButton
view={view} view={view}
size={40} style={[!gtPhone && [a.w_full, a.justify_center]]}
hovered={hovered} onPress={onPress}
themeColors={themeColors} onLongPress={onLongPress}
/> />
<View style={[a.flex_1, a.gap_2xs]}> )}
<Text
numberOfLines={1}
style={[
a.text_md,
a.font_semi_bold,
t.atoms.text,
hovered && a.underline,
]}>
{view.source?.title}
</Text>
<PublicationMetaRow view={view} author={author} />
</View>
</>
</View> </View>
{!hideSubscribe && ( {view.description && (
<SubscribeButton <View style={[a.pt_sm]}>
view={view} <Text style={[a.text_sm, a.leading_snug]} numberOfLines={3}>
style={[!gtPhone && [a.w_full, a.justify_center]]} {view.description}
onPress={onPress} </Text>
onLongPress={onLongPress} </View>
/>
)} )}
</View> </View>
)
{view.description && ( }}
<View style={[a.pt_sm]}>
<Text style={[a.text_sm, a.leading_snug]} numberOfLines={3}>
{view.description}
</Text>
</View>
)}
</View>
)}
</Link> </Link>
) )
} }
@@ -548,6 +564,17 @@ export function PublicationFooter({
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
const {gtPhone} = useBreakpoints() const {gtPhone} = useBreakpoints()
const {
state: maybeHovered,
onIn: onHoverIn,
onOut: onHoverOut,
} = useInteractionState()
const {
state: interactedWithin,
onIn: onInteractWithin,
onOut: onInteractWithout,
} = useInteractionState()
const hovered = maybeHovered && !interactedWithin
if (!view.source) return null if (!view.source) return null
@@ -557,11 +584,14 @@ export function PublicationFooter({
a.flex_1, a.flex_1,
a.align_center, a.align_center,
a.justify_between, a.justify_between,
a.py_md, a.p_md,
a.gap_md, a.gap_md,
gtPhone && [a.flex_row, a.gap_sm], gtPhone && [a.flex_row, a.gap_sm],
]} ]}
testID="publication-embed-footer"> testID="publication-embed-footer"
// @ts-ignore it's Fine™
onMouseEnter={onHoverIn}
onMouseLeave={onHoverOut}>
<Link <Link
shouldProxy shouldProxy
to={view.source.uri} to={view.source.uri}
@@ -575,29 +605,32 @@ export function PublicationFooter({
a.gap_sm, a.gap_sm,
gtPhone && a.flex_1, gtPhone && a.flex_1,
]}> ]}>
{({hovered}) => ( <>
<> <PublicationIcon
<PublicationIcon view={view}
size={32}
hovered={hovered}
themeColors={themeColors}
/>
<View style={[a.flex_1, a.gap_2xs]}>
<Text
numberOfLines={1}
style={[
a.text_sm,
a.font_medium,
t.atoms.text,
hovered && a.underline,
]}>
{view.source?.title}
</Text>
<PublicationMetaRow
view={view} view={view}
size={32} author={author}
hovered={hovered} onInteractWithin={onInteractWithin}
themeColors={themeColors} onInteractWithout={onInteractWithout}
/> />
<View style={[a.flex_1, a.gap_2xs]}> </View>
<Text </>
numberOfLines={1}
style={[
a.text_sm,
a.font_medium,
t.atoms.text,
hovered && a.underline,
]}>
{view.source?.title}
</Text>
<PublicationMetaRow view={view} author={author} />
</View>
</>
)}
</Link> </Link>
{!hideSubscribe && ( {!hideSubscribe && (