[APP-2160] Build new standard.site link card UI (#10468)

Co-authored-by: vineyardbovines <spencer@thepope.dev>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Spence Pope
2026-05-22 18:36:06 -04:00
committed by GitHub
parent 6159cd7777
commit 5db37729bb
28 changed files with 1265 additions and 38 deletions
@@ -0,0 +1,110 @@
import {Fragment, type ReactNode} from 'react'
import {View} from 'react-native'
import {type AppBskyEmbedExternal, AtUri} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro'
import {makeProfileLink} from '#/lib/routes/links'
import {toNiceDomain} from '#/lib/strings/url-helpers'
import {atoms as a, useTheme} from '#/alf'
import {StandardSite} from '#/components/icons/community/StandardSite'
import {InlineLinkText} from '#/components/Link'
import {
matchStandardSitePublisher,
matchStandardSitePublisherByUri,
} from '#/components/Post/Embed/StandardSiteEmbed/publishers'
import {
isStandardSiteDocumentUri,
isStandardSitePublicationUri,
} from '#/components/Post/Embed/StandardSiteEmbed/utils'
import {Text} from '#/components/Typography'
export function StandardSiteMetaRow({
type = 'document',
view,
onInteractWithin,
onInteractWithout,
}: {
type?: 'document' | 'publication'
view: AppBskyEmbedExternal.ViewExternal
onInteractWithin: () => void
onInteractWithout: () => void
}) {
const t = useTheme()
const {t: l} = useLingui()
const highlightedPublisher = !!matchStandardSitePublisher(view)
const didsFromRecords =
view.associatedRefs
?.filter(
type === 'document'
? isStandardSiteDocumentUri
: isStandardSitePublicationUri,
)
.map(ref => new AtUri(ref.uri).host) || []
// atm should only be one docment
const authorDid = didsFromRecords.at(0)
const authorProfile = authorDid
? view.associatedProfiles?.find(p => p.did === authorDid)
: undefined
const articleDomain = toNiceDomain(view.uri)
const articlePublisher = matchStandardSitePublisherByUri(view.uri)
const DomainIcon = articlePublisher?.Icon ?? StandardSite
const metaTextStyle = [
a.text_xs,
a.leading_snug,
t.atoms.text_contrast_medium,
]
const items: {key: string; node: ReactNode}[] = []
if (!highlightedPublisher) {
items.push({
key: 'domain',
node: (
<View style={[a.flex_row, a.align_center]}>
<DomainIcon size="sm" fill={t.atoms.text_contrast_medium.color} />
<Text numberOfLines={1} style={metaTextStyle}>
{articleDomain}
</Text>
</View>
),
})
}
if (authorProfile) {
items.push({
key: 'author',
node: (
<Text numberOfLines={1} style={metaTextStyle}>
<Trans>
by{' '}
<InlineLinkText
label={l`View @${authorProfile.handle}'s profile`}
to={makeProfileLink(authorProfile)}
style={metaTextStyle}
onPress={e => {
// this link is nested, yes it's not ideal
e.stopPropagation()
}}
onMouseEnter={onInteractWithin}
onMouseLeave={onInteractWithout}>
@{authorProfile.handle}
</InlineLinkText>
</Trans>
</Text>
),
})
}
if (items.length === 0) return null
return (
<View style={[a.flex_row, a.align_center, a.gap_xs]}>
{items.map((item, i) => (
<Fragment key={item.key}>
{i > 0 && <Text style={metaTextStyle}></Text>}
{item.node}
</Fragment>
))}
</View>
)
}
@@ -0,0 +1,67 @@
import {type AppBskyEmbedExternal} from '@atproto/api'
import {Context, useAlf, utils} from '#/alf'
/**
* Overrides only the values needed for `secondary_inverted` buttons atm.
*
* Renders the alf Context directly (rather than nesting a ThemeProvider) so
* that font-scale and other parent state stay live in the subtree.
*/
export function StandardSiteThemeProvider({
view,
children,
}: {
view: AppBskyEmbedExternal.ViewExternal
children: React.ReactNode
}) {
const alf = useAlf()
const {accentRGB, accentForegroundRGB} = view.source?.theme || {}
if (!accentRGB || !accentForegroundRGB) return children
const accent = utils.rgbToHex(accentRGB.r, accentRGB.g, accentRGB.b)
const accentForeground = utils.rgbToHex(
accentForegroundRGB.r,
accentForegroundRGB.g,
accentForegroundRGB.b,
)
const atomsOverride = {
text_inverted: {color: accentForeground},
}
const paletteOverride = {
contrast_975: utils.darken(accent, 5), // hover
contrast_900: accent, // bg
contrast_600: utils.lighten(accent, 5), // disabled bg
contrast_300: accentForeground, // disabled text
}
const themes = {
...alf.themes,
lightPalette: {...alf.themes.lightPalette, ...paletteOverride},
darkPalette: {...alf.themes.darkPalette, ...paletteOverride},
dimPalette: {...alf.themes.dimPalette, ...paletteOverride},
light: {
...alf.themes.light,
atoms: {...alf.themes.light.atoms, ...atomsOverride},
palette: {...alf.themes.light.palette, ...paletteOverride},
},
dark: {
...alf.themes.dark,
atoms: {...alf.themes.dark.atoms, ...atomsOverride},
palette: {...alf.themes.dark.palette, ...paletteOverride},
},
dim: {
...alf.themes.dim,
atoms: {...alf.themes.dim.atoms, ...atomsOverride},
palette: {...alf.themes.dim.palette, ...paletteOverride},
},
}
const value = {
...alf,
themes,
theme: themes[alf.themeName],
}
return <Context.Provider value={value}>{children}</Context.Provider>
}
@@ -0,0 +1,579 @@
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {Image} from 'expo-image'
import {type AppBskyEmbedExternal, AtUri} from '@atproto/api'
import {plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react/macro'
import {useHaptics} from '#/lib/haptics'
import {shareUrl} from '#/lib/sharing'
import {niceDate} from '#/lib/strings/time'
import {toNiceDomain} from '#/lib/strings/url-helpers'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useBreakpoints, useTheme, utils} from '#/alf'
import {ButtonIcon, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {useInteractionState} from '#/components/hooks/useInteractionState'
import {Clock_Stroke2_Corner0_Rounded as Clock} from '#/components/icons/Clock'
import {Link} from '#/components/Link'
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
import {matchStandardSitePublisher} from '#/components/Post/Embed/StandardSiteEmbed/publishers'
import {StandardSiteMetaRow} from '#/components/Post/Embed/StandardSiteEmbed/StandardSiteMetaRow'
import {StandardSiteThemeProvider} from '#/components/Post/Embed/StandardSiteEmbed/StandardSiteThemeProvider'
import {isStandardSitePublicationEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
import {Text} from '#/components/Typography'
import {IS_NATIVE} from '#/env'
export type ThemeColors = {
custom: boolean
accent: string
accentForeground: string
}
const PUBLICATION_AVATAR_STYLE = {
borderRadius: a.rounded_sm.borderRadius,
}
export const StandardSiteEmbed = ({
view,
onOpen,
style,
hideSubscribe,
}: {
view: AppBskyEmbedExternal.ViewExternal
onOpen?: () => void
style?: StyleProp<ViewStyle>
hideSubscribe?: boolean
}) => {
const {t: l, i18n} = useLingui()
const t = useTheme()
const playHaptic = useHaptics()
const niceUrl = toNiceDomain(view.uri)
const imageUri = view.thumb
const hasMedia = Boolean(imageUri)
const isStandard = view.associatedRefs?.some(ref =>
new AtUri(ref.uri).collection.startsWith('site.standard.'),
)
const isStandardPublication = isStandardSitePublicationEmbed(view)
let themeColors: ThemeColors = {
custom: false,
accent: t.atoms.text.color,
accentForeground: t.atoms.text_inverted.color,
}
const {accentRGB, accentForegroundRGB} = view.source?.theme || {}
if (accentRGB && accentForegroundRGB) {
themeColors = {
custom: true,
accent: utils.rgbToHex(accentRGB.r, accentRGB.g, accentRGB.b),
accentForeground: utils.rgbToHex(
accentForegroundRGB.r,
accentForegroundRGB.g,
accentForegroundRGB.b,
),
}
}
const {
state: interactedWithin,
onIn: onInteractWithin,
onOut: onInteractWithout,
} = useInteractionState()
const onPress = () => {
playHaptic('Light')
onOpen?.()
}
const onLongPress = () => {
if (view.uri && IS_NATIVE) {
playHaptic('Heavy')
shareUrl(view.uri)
}
}
if (isStandardPublication) {
return (
<PublicationCard
hideSubscribe={hideSubscribe}
view={view}
onPress={onPress}
onLongPress={onLongPress}
style={style}
themeColors={themeColors}
/>
)
}
return (
<View
style={[
a.flex_col,
a.rounded_lg,
a.overflow_hidden,
a.w_full,
a.border,
t.atoms.border_contrast_low,
style,
]}>
<Link
shouldProxy
to={view.uri}
label={view.title || l`Open link to ${niceUrl}`}
onPress={onPress}
onLongPress={onLongPress}>
{({hovered: maybeHovered}) => {
const hovered = maybeHovered && !interactedWithin
return (
<View style={[a.w_full]}>
{imageUri ? (
<Image
style={[a.aspect_card]}
source={{uri: imageUri}}
accessibilityIgnoresInvertColors
loading="lazy"
/>
) : undefined}
<View
style={[
a.flex_1,
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
emoji
numberOfLines={3}
style={[
a.text_md,
a.font_semi_bold,
a.leading_snug,
isStandard && [
a.text_lg,
a.font_bold,
hovered && a.underline,
],
]}>
{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
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_high,
]}>
{niceDate(i18n, view.createdAt, 'medium', 'none')}
</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_high,
]}>
{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>
)
}}
</Link>
{view.source && (
<>
<View style={[a.px_md]}>
<Divider />
</View>
<PublicationFooter
view={view}
onPress={onPress}
onLongPress={onLongPress}
themeColors={themeColors}
hideSubscribe={hideSubscribe}
/>
</>
)}
</View>
)
}
export function PublicationCard({
view,
hideSubscribe,
onPress,
onLongPress,
themeColors,
style,
}: {
view: AppBskyEmbedExternal.ViewExternal
hideSubscribe?: boolean
onPress?: () => void
onLongPress?: () => void
themeColors: ThemeColors
style?: StyleProp<ViewStyle>
}) {
const t = useTheme()
const {t: l} = useLingui()
const {gtPhone} = useBreakpoints()
const {
state: interactedWithin,
onIn: onInteractWithin,
onOut: onInteractWithout,
} = useInteractionState()
if (!view.source) return null
return (
<Link
shouldProxy
to={view.source.uri}
label={l`View publication`}
onPress={onPress}
onLongPress={onLongPress}>
{({hovered: maybeHovered}) => {
const hovered = maybeHovered && !interactedWithin
return (
<View
style={[
a.flex_col,
a.rounded_lg,
a.overflow_hidden,
a.w_full,
a.border,
a.p_md,
t.atoms.border_contrast_low,
style,
]}>
<View
style={[
a.flex_1,
a.align_center,
a.justify_between,
a.gap_md,
gtPhone && [a.flex_row, a.gap_sm],
]}
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>
<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>
)
}}
</Link>
)
}
export function SubscribeButton({
view,
onPress,
onLongPress,
style,
}: {
view: AppBskyEmbedExternal.ViewExternal
onPress?: () => void
onLongPress?: () => void
style?: StyleProp<ViewStyle>
}) {
const {t: l} = useLingui()
const highlightedPublisher = matchStandardSitePublisher(view)
const cta = highlightedPublisher
? l`Subscribe on ${highlightedPublisher.name}`
: l`View publication`
if (!view.source) return null
return (
<StandardSiteThemeProvider view={view}>
<Link
shouldProxy
to={view.source.uri}
label={cta}
size="small"
color="secondary_inverted"
style={[style, a.gap_sm]}
onPress={onPress}
onLongPress={onLongPress}>
{highlightedPublisher ? (
<>
<View style={[a.flex_row, a.align_center, {gap: 7}]}>
<ButtonIcon icon={highlightedPublisher.Icon} size="lg" />
{/*<ButtonText>|</ButtonText>*/}
</View>
<ButtonText>{cta}</ButtonText>
</>
) : (
<ButtonText>{cta}</ButtonText>
)}
</Link>
</StandardSiteThemeProvider>
)
}
function PublicationIcon({
view,
size,
hovered,
themeColors,
}: {
view: AppBskyEmbedExternal.ViewExternal
size: number
hovered?: boolean
themeColors: ThemeColors
}) {
const opacity = hovered ? 0.6 : 0.2
if (!view.source) return null
return view.source?.icon ? (
<View>
<UserAvatar
noBorder
type="labeler"
size={size}
avatar={view.source.icon}
extraAviStyle={PUBLICATION_AVATAR_STYLE}
/>
<MediaInsetBorder
style={[
a.rounded_sm,
{
borderColor: themeColors.accentForeground,
opacity,
},
]}
/>
</View>
) : (
<View
style={[
a.align_center,
a.justify_center,
a.rounded_sm,
{
width: size,
height: size,
backgroundColor: themeColors.accent,
},
]}>
<Text
emoji
style={[a.text_xl, a.font_bold, {color: themeColors.accentForeground}]}>
{[...view.source.title][0] ?? ''}
</Text>
<MediaInsetBorder
style={[
a.rounded_sm,
{
borderColor: themeColors.accentForeground,
opacity,
},
]}
/>
</View>
)
}
export function PublicationFooter({
view,
hideSubscribe,
onPress,
onLongPress,
themeColors,
}: {
view: AppBskyEmbedExternal.ViewExternal
hideSubscribe?: boolean
themeColors: ThemeColors
onPress?: () => void
onLongPress?: () => void
}) {
const t = useTheme()
const {t: l} = useLingui()
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
return (
<View
style={[
a.flex_1,
a.align_center,
a.justify_between,
a.p_md,
a.gap_md,
gtPhone && [a.flex_row, a.gap_sm],
]}
testID="publication-embed-footer"
// @ts-ignore it's Fine™
onMouseEnter={onHoverIn}
onMouseLeave={onHoverOut}>
<Link
shouldProxy
to={view.source.uri}
label={l`View publication`}
onPress={onPress}
onLongPress={onLongPress}
style={[
a.w_full,
a.flex_row,
a.align_center,
a.gap_sm,
gtPhone && a.flex_1,
]}>
<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>
<StandardSiteMetaRow
type="publication"
view={view}
onInteractWithin={onInteractWithin}
onInteractWithout={onInteractWithout}
/>
</View>
</Link>
{!hideSubscribe && (
<SubscribeButton
view={view}
style={[!gtPhone && [a.w_full, a.justify_center]]}
onPress={onPress}
onLongPress={onLongPress}
/>
)}
</View>
)
}
@@ -0,0 +1,52 @@
import {type AppBskyEmbedExternal} from '@atproto/api'
import {Leaflet} from '#/components/icons/community/Leaflet'
import {Offprint} from '#/components/icons/community/Offprint'
import {Pckt} from '#/components/icons/community/Pckt'
export type StandardSitePublisher = {
host: string
name: string
Icon: typeof Leaflet
}
const STANDARD_SITE_PUBLISHERS: StandardSitePublisher[] = [
{host: 'leaflet.pub', name: 'Leaflet', Icon: Leaflet},
{host: 'pckt.blog', name: 'pckt', Icon: Pckt},
{host: 'offprint.app', name: 'Offprint', Icon: Offprint},
]
function hostFromUri(uri: string | undefined): string | null {
try {
return new URL(uri || '').host
} catch {
return null
}
}
export function getStandardSitePublisherHost(
view: AppBskyEmbedExternal.ViewExternal,
): string | null {
return hostFromUri(view.source?.uri)
}
export function hostMatches(host: string, target: string): boolean {
return host === target || host.endsWith('.' + target)
}
function matchByHost(host: string | null): StandardSitePublisher | null {
if (!host) return null
return STANDARD_SITE_PUBLISHERS.find(p => hostMatches(host, p.host)) ?? null
}
export function matchStandardSitePublisher(
view: AppBskyEmbedExternal.ViewExternal,
): StandardSitePublisher | null {
return matchByHost(getStandardSitePublisherHost(view))
}
export function matchStandardSitePublisherByUri(
uri: string | undefined,
): StandardSitePublisher | null {
return matchByHost(hostFromUri(uri))
}
@@ -0,0 +1,70 @@
import {type AppBskyEmbedExternal} from '@atproto/api'
import {isStandardSiteEmbed, isStandardSitePublicationEmbed} from './utils'
function makeView(
partial: Record<string, unknown>,
): AppBskyEmbedExternal.ViewExternal {
return {
uri: 'https://example.com/post',
title: 'title',
description: 'description',
...partial,
}
}
describe('isStandardSiteEmbed', () => {
it('returns true when any associated ref is in the site.standard.* namespace', () => {
const view = makeView({
associatedRefs: [{uri: 'at://did:plc:abc/site.standard.publication/foo'}],
})
expect(isStandardSiteEmbed(view)).toBe(true)
})
it('returns false when no associated refs are in the site.standard.* namespace', () => {
const view = makeView({
associatedRefs: [{uri: 'at://did:plc:abc/app.bsky.feed.post/foo'}],
})
expect(isStandardSiteEmbed(view)).toBe(false)
})
it('returns falsy when associatedRefs is missing', () => {
expect(isStandardSiteEmbed(makeView({}))).toBeFalsy()
})
})
describe('isStandardSitePublicationEmbed', () => {
it('returns true with at least one publication ref and no document refs', () => {
const view = makeView({
associatedRefs: [{uri: 'at://did:plc:abc/site.standard.publication/foo'}],
})
expect(isStandardSitePublicationEmbed(view)).toBe(true)
})
it('returns false when any ref is a document', () => {
const view = makeView({
associatedRefs: [
{uri: 'at://did:plc:abc/site.standard.publication/foo'},
{uri: 'at://did:plc:abc/site.standard.document/bar'},
],
})
expect(isStandardSitePublicationEmbed(view)).toBe(false)
})
it('returns false when there are no publication refs', () => {
const view = makeView({
associatedRefs: [{uri: 'at://did:plc:abc/site.standard.other/foo'}],
})
expect(isStandardSitePublicationEmbed(view)).toBe(false)
})
it('returns falsy for an empty associatedRefs array', () => {
expect(isStandardSitePublicationEmbed(makeView({associatedRefs: []}))).toBe(
false,
)
})
it('returns falsy when associatedRefs is missing', () => {
expect(isStandardSitePublicationEmbed(makeView({}))).toBeFalsy()
})
})
@@ -0,0 +1,36 @@
import {
type AppBskyEmbedExternal,
AtUri,
type ComAtprotoRepoStrongRef,
} from '@atproto/api'
export function isStandardSiteDocumentUri(ref: ComAtprotoRepoStrongRef.Main) {
return new AtUri(ref.uri).collection.startsWith('site.standard.document')
}
export function isStandardSitePublicationUri(
ref: ComAtprotoRepoStrongRef.Main,
) {
return new AtUri(ref.uri).collection.startsWith('site.standard.publication')
}
export function isStandardSiteUri(ref: ComAtprotoRepoStrongRef.Main) {
return new AtUri(ref.uri).collection.startsWith('site.standard.')
}
export function isStandardSiteEmbed(view: AppBskyEmbedExternal.ViewExternal) {
return view.associatedRefs?.some(ref => isStandardSiteUri(ref))
}
export function isStandardSitePublicationEmbed(
view: AppBskyEmbedExternal.ViewExternal,
) {
return (
view.associatedRefs?.some(
ref => new AtUri(ref.uri).collection === 'site.standard.publication',
) &&
view.associatedRefs.every(
ref => new AtUri(ref.uri).collection !== 'site.standard.document',
)
)
}
+14
View File
@@ -22,6 +22,8 @@ import {useInteractionState} from '#/components/hooks/useInteractionState'
import {GalleryBleed} from '#/components/images/Gallery'
import {ContentHider} from '#/components/moderation/ContentHider'
import {PostAlerts} from '#/components/moderation/PostAlerts'
import {StandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed'
import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
import {RichText} from '#/components/RichText'
import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {SubtleHover} from '#/components/SubtleHover'
@@ -95,6 +97,18 @@ function MediaEmbed({
)
}
case 'link': {
if (isStandardSiteEmbed(embed.view.external)) {
return (
<ContentHider
modui={rest.moderation?.ui('contentMedia')}
activeStyle={[a.mt_sm]}>
<StandardSiteEmbed
view={embed.view.external}
style={[a.mt_sm, rest.style]}
/>
</ContentHider>
)
}
return (
<ContentHider
modui={rest.moderation?.ui('contentMedia')}