Make StandardSiteEmbed peekable (#10866)

This commit is contained in:
Samuel Newman
2026-06-11 23:49:09 +03:00
committed by GitHub
parent b281e23f11
commit f0e20d4c56
4 changed files with 129 additions and 120 deletions
+1 -22
View File
@@ -617,11 +617,6 @@
"count": 3
}
},
"src/components/dialogs/InAppBrowserConsent.tsx": {
"@typescript-eslint/no-floating-promises": {
"count": 2
}
},
"src/components/dialogs/LanguageSelectDialog.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 1
@@ -629,7 +624,7 @@
},
"src/components/dialogs/LinkWarning.tsx": {
"@typescript-eslint/no-floating-promises": {
"count": 2
"count": 1
}
},
"src/components/dialogs/MutedWords.tsx": {
@@ -848,11 +843,6 @@
"count": 1
}
},
"src/features/liveNow/components/LiveStatusDialog.tsx": {
"@typescript-eslint/no-floating-promises": {
"count": 1
}
},
"src/features/liveNow/index.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 3
@@ -975,14 +965,6 @@
"count": 4
}
},
"src/lib/hooks/useOpenLink.ts": {
"@typescript-eslint/no-floating-promises": {
"count": 2
},
"@typescript-eslint/require-await": {
"count": 1
}
},
"src/lib/hooks/usePermissions.ts": {
"@typescript-eslint/no-misused-promises": {
"count": 1
@@ -2394,9 +2376,6 @@
"src/view/com/util/Link.tsx": {
"@typescript-eslint/no-explicit-any": {
"count": 2
},
"@typescript-eslint/no-misused-promises": {
"count": 2
}
},
"src/view/com/util/LoadingPlaceholder.tsx": {
@@ -127,7 +127,6 @@ export const StandardSiteEmbed = ({
style={[
a.flex_col,
a.rounded_lg,
a.overflow_hidden,
a.w_full,
a.border,
t.atoms.bg,
@@ -135,126 +134,149 @@ export const StandardSiteEmbed = ({
preview && a.pointer_events_none,
style,
]}>
{/*
* The article is an in-flow `Link` wrapping the content so the iOS peek
* menu has something to lift. The publication footer below lives outside
* it - it has its own link (to the publication) and Subscribe button, so
* it can't be nested inside the article's peek.
*/}
<Link
peek
shouldProxy
to={view.uri}
label={view.title || l`Open link to ${niceUrl}`}
onPress={onPress}
onLongPress={onLongPress}
style={[a.absolute, a.inset_0, a.z_10]}
style={[a.rounded_lg]}
{...web({
onMouseEnter: onInteract,
onMouseLeave: onInteractOut,
})}
onFocus={onInteract}
onBlur={onInteractOut}>
<></>
</Link>
<View
style={[
a.w_full,
a.z_10,
a.pointer_events_none,
interacted && [t.atoms.bg_contrast_25],
]}>
{imageUri ? (
<Image
style={[a.aspect_card]}
source={{uri: imageUri}}
accessibilityIgnoresInvertColors
loading="lazy"
useAppleWebpCodec
/>
) : 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],
a.w_full,
a.overflow_hidden,
// The container can't clip (overflow_hidden there breaks the peek
// lift animation), so each half rounds its own outer corners to
// match the container border. The article rounds the top, plus the
// bottom when there's no footer beneath it.
{
borderTopLeftRadius: a.rounded_lg.borderRadius,
borderTopRightRadius: a.rounded_lg.borderRadius,
},
!view.source && {
borderBottomLeftRadius: a.rounded_lg.borderRadius,
borderBottomRightRadius: a.rounded_lg.borderRadius,
},
interacted ? t.atoms.bg_contrast_25 : t.atoms.bg,
]}>
<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>
{imageUri ? (
<Image
style={[a.aspect_card]}
source={{uri: imageUri}}
accessibilityIgnoresInvertColors
loading="lazy"
useAppleWebpCodec
/>
) : undefined}
{isStandard && (view.createdAt || view.readingTime) && (
<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.flex_row, a.align_center, a.gap_md, {paddingTop: 2}]}>
{view.createdAt && (
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
style={[
a.text_xs,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
{niceDate(i18n, view.createdAt, 'long', 'none')}
emoji
numberOfLines={view.thumb ? 2 : 4}
style={[a.text_sm, a.leading_snug]}>
{view.description}
</Text>
)}
{view.readingTime && (
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
<Clock size="xs" style={t.atoms.text_contrast_medium} />
<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>
) : undefined}
{isStandard && (view.createdAt || view.readingTime) && (
<View
style={[
a.flex_row,
a.align_center,
a.gap_md,
{paddingTop: 2},
]}>
{view.createdAt && (
<Text
style={[
a.text_xs,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
{niceDate(i18n, view.createdAt, 'long', 'none')}
</Text>
)}
{view.readingTime && (
<View style={[a.flex_row, a.align_center, a.gap_2xs]}>
<Clock size="xs" style={t.atoms.text_contrast_medium} />
<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>
{!view.source && (
<View style={[a.px_md]}>
<Divider />
<View style={[a.py_sm]}>
<StandardSiteMetaRow preview={preview} view={view} />
</View>
</View>
)}
</View>
</View>
{!view.source && (
<View style={[a.px_md]}>
<Divider />
<View style={[a.py_sm]}>
<StandardSiteMetaRow preview={preview} view={view} />
</View>
</View>
)}
</View>
</Link>
{view.source && (
<View style={[a.z_20]}>
<View>
<Divider />
<PublicationFooter
preview={preview}
@@ -629,6 +651,14 @@ export function PublicationFooter({
a.justify_between,
a.p_md,
a.gap_md,
a.overflow_hidden,
// Rounds the bottom corners to match the container border, since the
// container itself can't clip (overflow_hidden there breaks the peek
// lift animation on the article above).
{
borderBottomLeftRadius: a.rounded_lg.borderRadius,
borderBottomRightRadius: a.rounded_lg.borderRadius,
},
gtPhone && [a.flex_row, a.gap_sm],
interactedOuter && t.atoms.bg_contrast_25,
preview && a.pointer_events_none,
+2 -2
View File
@@ -50,10 +50,10 @@ export function useGoogleTranslate() {
} catch (err) {
if (__DEV__) console.error(err)
// most likely means they don't have the translate app
await openLink(translateUrl)
openLink(translateUrl)
}
} else {
await openLink(translateUrl)
openLink(translateUrl)
}
},
[openLink],
+3 -3
View File
@@ -26,7 +26,7 @@ export function useOpenLink() {
const {inAppBrowserConsentControl} = useGlobalDialogsControlContext()
const openLink = useCallback(
async (url: string, override?: boolean, shouldProxy?: boolean) => {
(url: string, override?: boolean, shouldProxy?: boolean) => {
if (isBskyRSSUrl(url) && isRelativeUrl(url)) {
url = createBskyAppAbsoluteUrl(url)
}
@@ -66,12 +66,12 @@ export function useOpenLink() {
}).catch(err => {
if (__DEV__)
logger.error('Could not open web browser', {message: err})
Linking.openURL(url)
void Linking.openURL(url)
})
return
}
}
Linking.openURL(url)
void Linking.openURL(url)
},
[ax, enabled, inAppBrowserConsentControl, t, dialogContext],
)