Fix nested links and hover states
This commit is contained in:
+58
-20
@@ -1,5 +1,10 @@
|
|||||||
import {useCallback, useMemo} from 'react'
|
import {useCallback, useMemo} from 'react'
|
||||||
import {type GestureResponderEvent, Linking} from 'react-native'
|
import {
|
||||||
|
type GestureResponderEvent,
|
||||||
|
Linking,
|
||||||
|
type NativeSyntheticEvent,
|
||||||
|
type TargetedEvent,
|
||||||
|
} from 'react-native'
|
||||||
import {sanitizeUrl} from '@braintree/sanitize-url'
|
import {sanitizeUrl} from '@braintree/sanitize-url'
|
||||||
import {
|
import {
|
||||||
type LinkProps as RNLinkProps,
|
type LinkProps as RNLinkProps,
|
||||||
@@ -78,6 +83,15 @@ type BaseLinkProps = {
|
|||||||
* Whether the link should be opened through the redirect proxy.
|
* Whether the link should be opened through the redirect proxy.
|
||||||
*/
|
*/
|
||||||
shouldProxy?: boolean
|
shouldProxy?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web only
|
||||||
|
*/
|
||||||
|
onMouseEnter?: () => void
|
||||||
|
/**
|
||||||
|
* Web only
|
||||||
|
*/
|
||||||
|
onMouseLeave?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useLink({
|
export function useLink({
|
||||||
@@ -322,12 +336,10 @@ export type InlineLinkProps = React.PropsWithChildren<
|
|||||||
BaseLinkProps &
|
BaseLinkProps &
|
||||||
TextStyleProp &
|
TextStyleProp &
|
||||||
Pick<TextProps, 'selectable' | 'numberOfLines' | 'emoji'> &
|
Pick<TextProps, 'selectable' | 'numberOfLines' | 'emoji'> &
|
||||||
Pick<ButtonProps, 'label' | 'accessibilityHint'> & {
|
Pick<ButtonProps, 'label' | 'accessibilityHint' | 'onFocus' | 'onBlur'> & {
|
||||||
disableUnderline?: boolean
|
disableUnderline?: boolean
|
||||||
title?: TextProps['title']
|
title?: TextProps['title']
|
||||||
overridePresentation?: boolean
|
overridePresentation?: boolean
|
||||||
onMouseEnter?: () => void
|
|
||||||
onMouseLeave?: () => void
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
@@ -362,9 +374,9 @@ export function InlineLinkText({
|
|||||||
shouldProxy: shouldProxy,
|
shouldProxy: shouldProxy,
|
||||||
})
|
})
|
||||||
const {
|
const {
|
||||||
state: hovered,
|
state: interacted,
|
||||||
onIn: onHoverIn,
|
onIn: onInteract,
|
||||||
onOut: onHoverOut,
|
onOut: onInteractOut,
|
||||||
} = useInteractionState()
|
} = useInteractionState()
|
||||||
const flattenedStyle = flatten(style) || {}
|
const flattenedStyle = flatten(style) || {}
|
||||||
|
|
||||||
@@ -376,7 +388,7 @@ export function InlineLinkText({
|
|||||||
{...rest}
|
{...rest}
|
||||||
style={[
|
style={[
|
||||||
{color: t.palette.primary_500},
|
{color: t.palette.primary_500},
|
||||||
hovered &&
|
interacted &&
|
||||||
!disableUnderline && {
|
!disableUnderline && {
|
||||||
...web({
|
...web({
|
||||||
outline: 0,
|
outline: 0,
|
||||||
@@ -390,13 +402,23 @@ export function InlineLinkText({
|
|||||||
role="link"
|
role="link"
|
||||||
onPress={download ? undefined : onPress}
|
onPress={download ? undefined : onPress}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
onMouseEnter={() => {
|
{...web({
|
||||||
rest.onMouseEnter?.()
|
onMouseEnter: () => {
|
||||||
onHoverIn()
|
rest.onMouseEnter?.()
|
||||||
|
onInteract()
|
||||||
|
},
|
||||||
|
onMouseLeave: () => {
|
||||||
|
rest.onMouseLeave?.()
|
||||||
|
onInteractOut()
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
onFocus={(e: NativeSyntheticEvent<TargetedEvent>) => {
|
||||||
|
rest.onFocus?.(e)
|
||||||
|
onInteract()
|
||||||
}}
|
}}
|
||||||
onMouseLeave={() => {
|
onBlur={(e: NativeSyntheticEvent<TargetedEvent>) => {
|
||||||
rest.onMouseLeave?.()
|
rest.onBlur?.(e)
|
||||||
onHoverOut()
|
onInteractOut()
|
||||||
}}
|
}}
|
||||||
accessibilityRole="link"
|
accessibilityRole="link"
|
||||||
href={href}
|
href={href}
|
||||||
@@ -444,9 +466,9 @@ export function SimpleInlineLinkText({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {
|
const {
|
||||||
state: hovered,
|
state: interacted,
|
||||||
onIn: onHoverIn,
|
onIn: onInteract,
|
||||||
onOut: onHoverOut,
|
onOut: onInteractOut,
|
||||||
} = useInteractionState()
|
} = useInteractionState()
|
||||||
const flattenedStyle = flatten(style) || {}
|
const flattenedStyle = flatten(style) || {}
|
||||||
const isExternal = isExternalUrl(to)
|
const isExternal = isExternalUrl(to)
|
||||||
@@ -470,7 +492,7 @@ export function SimpleInlineLinkText({
|
|||||||
{...rest}
|
{...rest}
|
||||||
style={[
|
style={[
|
||||||
{color: t.palette.primary_500},
|
{color: t.palette.primary_500},
|
||||||
hovered &&
|
interacted &&
|
||||||
!disableUnderline && {
|
!disableUnderline && {
|
||||||
...web({
|
...web({
|
||||||
outline: 0,
|
outline: 0,
|
||||||
@@ -483,8 +505,24 @@ export function SimpleInlineLinkText({
|
|||||||
]}
|
]}
|
||||||
role="link"
|
role="link"
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onMouseEnter={onHoverIn}
|
{...web({
|
||||||
onMouseLeave={onHoverOut}
|
onMouseEnter: () => {
|
||||||
|
rest.onMouseEnter?.()
|
||||||
|
onInteract()
|
||||||
|
},
|
||||||
|
onMouseLeave: () => {
|
||||||
|
rest.onMouseLeave?.()
|
||||||
|
onInteractOut()
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
onFocus={(e: NativeSyntheticEvent<TargetedEvent>) => {
|
||||||
|
rest.onFocus?.(e)
|
||||||
|
onInteract()
|
||||||
|
}}
|
||||||
|
onBlur={(e: NativeSyntheticEvent<TargetedEvent>) => {
|
||||||
|
rest.onBlur?.(e)
|
||||||
|
onInteractOut()
|
||||||
|
}}
|
||||||
accessibilityRole="link"
|
accessibilityRole="link"
|
||||||
href={href}
|
href={href}
|
||||||
{...web({
|
{...web({
|
||||||
|
|||||||
@@ -21,13 +21,9 @@ import {Text} from '#/components/Typography'
|
|||||||
export function StandardSiteMetaRow({
|
export function StandardSiteMetaRow({
|
||||||
type = 'document',
|
type = 'document',
|
||||||
view,
|
view,
|
||||||
onInteractWithin,
|
|
||||||
onInteractWithout,
|
|
||||||
}: {
|
}: {
|
||||||
type?: 'document' | 'publication'
|
type?: 'document' | 'publication'
|
||||||
view: AppBskyEmbedExternal.ViewExternal
|
view: AppBskyEmbedExternal.ViewExternal
|
||||||
onInteractWithin: () => void
|
|
||||||
onInteractWithout: () => void
|
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
@@ -80,13 +76,7 @@ export function StandardSiteMetaRow({
|
|||||||
<InlineLinkText
|
<InlineLinkText
|
||||||
label={l`View @${authorProfile.handle}'s profile`}
|
label={l`View @${authorProfile.handle}'s profile`}
|
||||||
to={makeProfileLink(authorProfile)}
|
to={makeProfileLink(authorProfile)}
|
||||||
style={metaTextStyle}
|
style={[metaTextStyle, a.pointer_events_auto]}>
|
||||||
onPress={e => {
|
|
||||||
// this link is nested, yes it's not ideal
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
onMouseEnter={onInteractWithin}
|
|
||||||
onMouseLeave={onInteractWithout}>
|
|
||||||
@{authorProfile.handle}
|
@{authorProfile.handle}
|
||||||
</InlineLinkText>
|
</InlineLinkText>
|
||||||
</Trans>
|
</Trans>
|
||||||
@@ -98,7 +88,7 @@ export function StandardSiteMetaRow({
|
|||||||
if (items.length === 0) return null
|
if (items.length === 0) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
|
<View style={[a.flex_row, a.align_center, a.gap_xs, a.z_10]}>
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<Fragment key={item.key}>
|
<Fragment key={item.key}>
|
||||||
{i > 0 && <Text style={metaTextStyle}>•</Text>}
|
{i > 0 && <Text style={metaTextStyle}>•</Text>}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {shareUrl} from '#/lib/sharing'
|
|||||||
import {niceDate} from '#/lib/strings/time'
|
import {niceDate} from '#/lib/strings/time'
|
||||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {atoms as a, useBreakpoints, useTheme, utils} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, utils, web} 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 {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
@@ -73,9 +73,9 @@ export const StandardSiteEmbed = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
state: interactedWithin,
|
state: interacted,
|
||||||
onIn: onInteractWithin,
|
onIn: onInteract,
|
||||||
onOut: onInteractWithout,
|
onOut: onInteractOut,
|
||||||
} = useInteractionState()
|
} = useInteractionState()
|
||||||
|
|
||||||
const onPress = () => {
|
const onPress = () => {
|
||||||
@@ -111,7 +111,7 @@ export const StandardSiteEmbed = ({
|
|||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.border,
|
a.border,
|
||||||
t.atoms.border_contrast_low,
|
interacted ? t.atoms.border_contrast_high : t.atoms.border_contrast_low,
|
||||||
style,
|
style,
|
||||||
]}>
|
]}>
|
||||||
<Link
|
<Link
|
||||||
@@ -119,124 +119,108 @@ export const StandardSiteEmbed = ({
|
|||||||
to={view.uri}
|
to={view.uri}
|
||||||
label={view.title || l`Open link to ${niceUrl}`}
|
label={view.title || l`Open link to ${niceUrl}`}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={onLongPress}>
|
onLongPress={onLongPress}
|
||||||
{({hovered: maybeHovered}) => {
|
style={[a.absolute, a.inset_0, a.z_10]}
|
||||||
const hovered = maybeHovered && !interactedWithin
|
{...web({
|
||||||
return (
|
onMouseEnter: onInteract,
|
||||||
<View style={[a.w_full]}>
|
onMouseLeave: onInteractOut,
|
||||||
{imageUri ? (
|
})}
|
||||||
<Image
|
onFocus={onInteract}
|
||||||
style={[a.aspect_card]}
|
onBlur={onInteractOut}>
|
||||||
source={{uri: imageUri}}
|
<></>
|
||||||
accessibilityIgnoresInvertColors
|
</Link>
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
|
<View style={[a.w_full, a.z_10, a.pointer_events_none]}>
|
||||||
|
{imageUri ? (
|
||||||
|
<Image
|
||||||
|
style={[a.aspect_card]}
|
||||||
|
source={{uri: imageUri}}
|
||||||
|
accessibilityIgnoresInvertColors
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.pt_sm,
|
||||||
|
hasMedia && a.border_t,
|
||||||
|
interacted
|
||||||
|
? t.atoms.border_contrast_high
|
||||||
|
: t.atoms.border_contrast_low,
|
||||||
|
{gap: 3},
|
||||||
|
isStandard && a.pt_md,
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.pb_xs,
|
||||||
|
a.px_md,
|
||||||
|
{gap: 3},
|
||||||
|
isStandard && [{gap: 5}, a.pb_sm],
|
||||||
|
]}>
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
numberOfLines={3}
|
||||||
|
style={[
|
||||||
|
a.text_md,
|
||||||
|
a.font_semi_bold,
|
||||||
|
a.leading_snug,
|
||||||
|
isStandard && [a.text_lg, a.font_bold],
|
||||||
|
]}>
|
||||||
|
{view.title}
|
||||||
|
</Text>
|
||||||
|
{view.description ? (
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
numberOfLines={view.thumb ? 2 : 4}
|
||||||
|
style={[a.text_sm, a.leading_snug]}>
|
||||||
|
{view.description}
|
||||||
|
</Text>
|
||||||
|
) : undefined}
|
||||||
|
|
||||||
|
{isStandard && (view.createdAt || view.readingTime) && (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[a.flex_row, a.align_center, a.gap_md, {paddingTop: 2}]}>
|
||||||
a.flex_1,
|
{view.createdAt && (
|
||||||
a.pt_sm,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
hasMedia && a.border_t,
|
|
||||||
{gap: 3},
|
|
||||||
isStandard && a.pt_md,
|
|
||||||
]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.pb_xs,
|
|
||||||
a.px_md,
|
|
||||||
{gap: 3},
|
|
||||||
isStandard && [{gap: 5}, a.pb_sm],
|
|
||||||
]}>
|
|
||||||
<Text
|
<Text
|
||||||
emoji
|
|
||||||
numberOfLines={3}
|
|
||||||
style={[
|
style={[
|
||||||
a.text_md,
|
a.text_xs,
|
||||||
a.font_semi_bold,
|
|
||||||
a.leading_snug,
|
a.leading_snug,
|
||||||
isStandard && [
|
t.atoms.text_contrast_medium,
|
||||||
a.text_lg,
|
|
||||||
a.font_bold,
|
|
||||||
hovered && a.underline,
|
|
||||||
],
|
|
||||||
]}>
|
]}>
|
||||||
{view.title}
|
{niceDate(i18n, view.createdAt, 'long', 'none')}
|
||||||
</Text>
|
</Text>
|
||||||
{view.description ? (
|
)}
|
||||||
|
{view.readingTime && (
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
||||||
|
<Clock size="xs" style={t.atoms.text_contrast_medium} />
|
||||||
<Text
|
<Text
|
||||||
emoji
|
|
||||||
numberOfLines={view.thumb ? 2 : 4}
|
|
||||||
style={[a.text_sm, a.leading_snug]}>
|
|
||||||
{view.description}
|
|
||||||
</Text>
|
|
||||||
) : undefined}
|
|
||||||
|
|
||||||
{isStandard && (view.createdAt || view.readingTime) && (
|
|
||||||
<View
|
|
||||||
style={[
|
style={[
|
||||||
a.flex_row,
|
a.text_xs,
|
||||||
a.align_center,
|
a.leading_snug,
|
||||||
a.gap_md,
|
t.atoms.text_contrast_medium,
|
||||||
{paddingTop: 2},
|
|
||||||
]}>
|
]}>
|
||||||
{view.createdAt && (
|
{l({
|
||||||
<Text
|
message: plural(view.readingTime, {
|
||||||
style={[
|
one: '#m',
|
||||||
a.text_xs,
|
other: '#m',
|
||||||
a.leading_snug,
|
}),
|
||||||
t.atoms.text_contrast_medium,
|
comment: `How long it takes to read an article, in minutes. Displayed in a short form, e.g. "5m" for 5 minutes.`,
|
||||||
]}>
|
})}
|
||||||
{niceDate(i18n, view.createdAt, 'long', 'none')}
|
</Text>
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{view.readingTime && (
|
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
|
|
||||||
<Clock size="xs" style={t.atoms.text_contrast_high} />
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_xs,
|
|
||||||
a.leading_snug,
|
|
||||||
t.atoms.text_contrast_medium,
|
|
||||||
]}>
|
|
||||||
{l({
|
|
||||||
message: plural(view.readingTime, {
|
|
||||||
one: '#m',
|
|
||||||
other: '#m',
|
|
||||||
}),
|
|
||||||
comment: `How long it takes to read an article, in minutes. Displayed in a short form, e.g. "5m" for 5 minutes.`,
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{!view.source && (
|
|
||||||
<View style={[a.px_md]}>
|
|
||||||
<Divider />
|
|
||||||
<View style={[a.py_sm]}>
|
|
||||||
<StandardSiteMetaRow
|
|
||||||
view={view}
|
|
||||||
onInteractWithin={onInteractWithin}
|
|
||||||
onInteractWithout={onInteractWithout}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
)}
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{view.source && (
|
|
||||||
<>
|
|
||||||
<View style={[a.px_md]}>
|
|
||||||
<Divider />
|
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.z_20]}>
|
||||||
|
<View style={[a.px_md]}>
|
||||||
|
<Divider />
|
||||||
|
</View>
|
||||||
|
{view.source ? (
|
||||||
<PublicationFooter
|
<PublicationFooter
|
||||||
view={view}
|
view={view}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
@@ -244,8 +228,12 @@ export const StandardSiteEmbed = ({
|
|||||||
themeColors={themeColors}
|
themeColors={themeColors}
|
||||||
hideSubscribe={hideSubscribe}
|
hideSubscribe={hideSubscribe}
|
||||||
/>
|
/>
|
||||||
</>
|
) : (
|
||||||
)}
|
<View style={[a.px_md, a.py_sm, a.pointer_events_none]}>
|
||||||
|
<StandardSiteMetaRow view={view} />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -269,109 +257,109 @@ export function PublicationCard({
|
|||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const {gtPhone} = useBreakpoints()
|
const {gtPhone} = useBreakpoints()
|
||||||
const {
|
const {
|
||||||
state: interactedWithin,
|
state: interacted,
|
||||||
onIn: onInteractWithin,
|
onIn: onInteract,
|
||||||
onOut: onInteractWithout,
|
onOut: onInteractOut,
|
||||||
} = useInteractionState()
|
} = useInteractionState()
|
||||||
|
|
||||||
if (!view.source) return null
|
if (!view.source) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<View
|
||||||
shouldProxy
|
style={[
|
||||||
to={view.source.uri}
|
a.rounded_lg,
|
||||||
label={l`View publication`}
|
a.overflow_hidden,
|
||||||
onPress={onPress}
|
a.w_full,
|
||||||
onLongPress={onLongPress}>
|
a.border,
|
||||||
{({hovered: maybeHovered}) => {
|
a.p_md,
|
||||||
const hovered = maybeHovered && !interactedWithin
|
interacted ? t.atoms.border_contrast_high : t.atoms.border_contrast_low,
|
||||||
return (
|
style,
|
||||||
<View
|
]}>
|
||||||
style={[
|
<Link
|
||||||
a.flex_col,
|
shouldProxy
|
||||||
a.rounded_lg,
|
to={view.source.uri}
|
||||||
a.overflow_hidden,
|
label={l`View publication`}
|
||||||
a.w_full,
|
onPress={onPress}
|
||||||
a.border,
|
onLongPress={onLongPress}
|
||||||
a.p_md,
|
{...web({
|
||||||
t.atoms.border_contrast_low,
|
onMouseEnter: onInteract,
|
||||||
style,
|
onMouseLeave: onInteractOut,
|
||||||
]}>
|
})}
|
||||||
<View
|
onFocus={onInteract}
|
||||||
|
onBlur={onInteractOut}
|
||||||
|
style={[a.absolute, a.inset_0]}>
|
||||||
|
<></>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_between,
|
||||||
|
a.gap_md,
|
||||||
|
a.pointer_events_none,
|
||||||
|
gtPhone && [a.flex_row, a.gap_sm],
|
||||||
|
]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
a.flex_row,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
gtPhone && a.flex_1,
|
||||||
|
]}>
|
||||||
|
<PublicationIcon
|
||||||
|
view={view}
|
||||||
|
size={40}
|
||||||
|
interacted={interacted}
|
||||||
|
themeColors={themeColors}
|
||||||
|
/>
|
||||||
|
<View style={[a.flex_1, a.gap_2xs]}>
|
||||||
|
<Text
|
||||||
|
numberOfLines={1}
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
a.text_md,
|
||||||
a.align_center,
|
a.font_semi_bold,
|
||||||
a.justify_between,
|
t.atoms.text,
|
||||||
a.gap_md,
|
interacted && a.underline,
|
||||||
gtPhone && [a.flex_row, a.gap_sm],
|
]}>
|
||||||
]}
|
{view.source?.title}
|
||||||
testID="publication-embed-footer">
|
</Text>
|
||||||
<View
|
<StandardSiteMetaRow type="publication" view={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>
|
|
||||||
<StandardSiteMetaRow
|
|
||||||
type="publication"
|
|
||||||
view={view}
|
|
||||||
onInteractWithin={onInteractWithin}
|
|
||||||
onInteractWithout={onInteractWithout}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{!hideSubscribe && gtPhone && (
|
|
||||||
<SubscribeButton
|
|
||||||
view={view}
|
|
||||||
style={[!gtPhone && [a.w_full, a.justify_center]]}
|
|
||||||
onPress={onPress}
|
|
||||||
onLongPress={onLongPress}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{view.description && (
|
|
||||||
<View style={[a.pt_sm]}>
|
|
||||||
<Text style={[a.text_sm, a.leading_snug]} numberOfLines={3}>
|
|
||||||
{view.description}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!hideSubscribe && !gtPhone && (
|
|
||||||
<View style={[view.description && a.pt_sm]}>
|
|
||||||
<SubscribeButton
|
|
||||||
view={view}
|
|
||||||
style={[!gtPhone && [a.w_full, a.justify_center]]}
|
|
||||||
onPress={onPress}
|
|
||||||
onLongPress={onLongPress}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
</View>
|
||||||
}}
|
|
||||||
</Link>
|
{!hideSubscribe && gtPhone && (
|
||||||
|
<SubscribeButton
|
||||||
|
view={view}
|
||||||
|
style={[!gtPhone && [a.w_full, a.justify_center]]}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.pointer_events_none]}>
|
||||||
|
{view.description && (
|
||||||
|
<View style={[a.pt_sm]}>
|
||||||
|
<Text style={[a.text_sm, a.leading_snug]} numberOfLines={3}>
|
||||||
|
{view.description}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!hideSubscribe && !gtPhone && (
|
||||||
|
<View style={[view.description && a.pt_sm]}>
|
||||||
|
<SubscribeButton
|
||||||
|
view={view}
|
||||||
|
style={[!gtPhone && [a.w_full, a.justify_center]]}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +390,7 @@ export function SubscribeButton({
|
|||||||
label={cta}
|
label={cta}
|
||||||
size="small"
|
size="small"
|
||||||
color="secondary_inverted"
|
color="secondary_inverted"
|
||||||
style={[style, a.gap_sm]}
|
style={[style, a.gap_sm, a.pointer_events_auto]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={onLongPress}>
|
onLongPress={onLongPress}>
|
||||||
{highlightedPublisher ? (
|
{highlightedPublisher ? (
|
||||||
@@ -424,15 +412,13 @@ export function SubscribeButton({
|
|||||||
function PublicationIcon({
|
function PublicationIcon({
|
||||||
view,
|
view,
|
||||||
size,
|
size,
|
||||||
hovered,
|
|
||||||
themeColors,
|
themeColors,
|
||||||
}: {
|
}: {
|
||||||
view: AppBskyEmbedExternal.ViewExternal
|
view: AppBskyEmbedExternal.ViewExternal
|
||||||
size: number
|
size: number
|
||||||
hovered?: boolean
|
interacted?: boolean
|
||||||
themeColors: ThemeColors
|
themeColors: ThemeColors
|
||||||
}) {
|
}) {
|
||||||
const opacity = hovered ? 0.6 : 0.2
|
|
||||||
if (!view.source) return null
|
if (!view.source) return null
|
||||||
return view.source?.icon ? (
|
return view.source?.icon ? (
|
||||||
<View>
|
<View>
|
||||||
@@ -443,15 +429,7 @@ function PublicationIcon({
|
|||||||
avatar={view.source.icon}
|
avatar={view.source.icon}
|
||||||
extraAviStyle={PUBLICATION_AVATAR_STYLE}
|
extraAviStyle={PUBLICATION_AVATAR_STYLE}
|
||||||
/>
|
/>
|
||||||
<MediaInsetBorder
|
<MediaInsetBorder opaque style={[a.rounded_sm]} />
|
||||||
style={[
|
|
||||||
a.rounded_sm,
|
|
||||||
{
|
|
||||||
borderColor: themeColors.accentForeground,
|
|
||||||
opacity,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View
|
<View
|
||||||
@@ -470,15 +448,7 @@ function PublicationIcon({
|
|||||||
style={[a.text_xl, a.font_bold, {color: themeColors.accentForeground}]}>
|
style={[a.text_xl, a.font_bold, {color: themeColors.accentForeground}]}>
|
||||||
{[...view.source.title][0] ?? ''}
|
{[...view.source.title][0] ?? ''}
|
||||||
</Text>
|
</Text>
|
||||||
<MediaInsetBorder
|
<MediaInsetBorder opaque style={[a.rounded_sm]} />
|
||||||
style={[
|
|
||||||
a.rounded_sm,
|
|
||||||
{
|
|
||||||
borderColor: themeColors.accentForeground,
|
|
||||||
opacity,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -500,16 +470,10 @@ export function PublicationFooter({
|
|||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
const {gtPhone} = useBreakpoints()
|
const {gtPhone} = useBreakpoints()
|
||||||
const {
|
const {
|
||||||
state: maybeHovered,
|
state: interacted,
|
||||||
onIn: onHoverIn,
|
onIn: onInteract,
|
||||||
onOut: onHoverOut,
|
onOut: onInteractOut,
|
||||||
} = useInteractionState()
|
} = useInteractionState()
|
||||||
const {
|
|
||||||
state: interactedWithin,
|
|
||||||
onIn: onInteractWithin,
|
|
||||||
onOut: onInteractWithout,
|
|
||||||
} = useInteractionState()
|
|
||||||
const hovered = maybeHovered && !interactedWithin
|
|
||||||
|
|
||||||
if (!view.source) return null
|
if (!view.source) return null
|
||||||
|
|
||||||
@@ -523,27 +487,36 @@ export function PublicationFooter({
|
|||||||
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}
|
||||||
label={l`View publication`}
|
label={l`View publication`}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
|
style={[a.absolute, a.inset_0, web({outline: 0})]}
|
||||||
|
{...web({
|
||||||
|
onMouseEnter: onInteract,
|
||||||
|
onMouseLeave: onInteractOut,
|
||||||
|
})}
|
||||||
|
onFocus={onInteract}
|
||||||
|
onBlur={onInteractOut}>
|
||||||
|
<></>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.gap_sm,
|
a.gap_sm,
|
||||||
gtPhone && a.flex_1,
|
gtPhone && a.flex_1,
|
||||||
|
a.pointer_events_none,
|
||||||
]}>
|
]}>
|
||||||
<PublicationIcon
|
<PublicationIcon
|
||||||
view={view}
|
view={view}
|
||||||
size={32}
|
size={32}
|
||||||
hovered={hovered}
|
interacted={interacted}
|
||||||
themeColors={themeColors}
|
themeColors={themeColors}
|
||||||
/>
|
/>
|
||||||
<View style={[a.flex_1, a.gap_2xs]}>
|
<View style={[a.flex_1, a.gap_2xs]}>
|
||||||
@@ -553,23 +526,18 @@ export function PublicationFooter({
|
|||||||
a.text_sm,
|
a.text_sm,
|
||||||
a.font_medium,
|
a.font_medium,
|
||||||
t.atoms.text,
|
t.atoms.text,
|
||||||
hovered && a.underline,
|
interacted && a.underline,
|
||||||
]}>
|
]}>
|
||||||
{view.source?.title}
|
{view.source?.title}
|
||||||
</Text>
|
</Text>
|
||||||
<StandardSiteMetaRow
|
<StandardSiteMetaRow type="publication" view={view} />
|
||||||
type="publication"
|
|
||||||
view={view}
|
|
||||||
onInteractWithin={onInteractWithin}
|
|
||||||
onInteractWithout={onInteractWithout}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</Link>
|
</View>
|
||||||
|
|
||||||
{!hideSubscribe && (
|
{!hideSubscribe && (
|
||||||
<SubscribeButton
|
<SubscribeButton
|
||||||
view={view}
|
view={view}
|
||||||
style={[!gtPhone && [a.w_full, a.justify_center]]}
|
style={[a.z_10, !gtPhone && [a.w_full, a.justify_center]]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onLongPress={onLongPress}
|
onLongPress={onLongPress}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user