Standard Site cards tweaks (#10614)
This commit is contained in:
@@ -1,30 +1,33 @@
|
||||
import {Fragment, type ReactNode} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {type AppBskyEmbedExternal, AtUri} from '@atproto/api'
|
||||
import {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 type * as ssTypes from '#/components/Post/Embed/StandardSiteEmbed/types'
|
||||
import {
|
||||
isStandardSiteDocumentUri,
|
||||
isStandardSitePublicationUri,
|
||||
} from '#/components/Post/Embed/StandardSiteEmbed/utils'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
|
||||
export function StandardSiteMetaRow({
|
||||
type = 'document',
|
||||
preview,
|
||||
view,
|
||||
}: {
|
||||
type?: 'document' | 'publication'
|
||||
view: AppBskyEmbedExternal.ViewExternal
|
||||
}) {
|
||||
}: ssTypes.CommonProps &
|
||||
ssTypes.PreviewProps & {
|
||||
type?: 'document' | 'publication'
|
||||
}) {
|
||||
const ax = useAnalytics()
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const highlightedPublisher = !!matchStandardSitePublisher(view)
|
||||
@@ -43,7 +46,7 @@ export function StandardSiteMetaRow({
|
||||
: undefined
|
||||
const articleDomain = toNiceDomain(view.uri)
|
||||
const articlePublisher = matchStandardSitePublisherByUri(view.uri)
|
||||
const DomainIcon = articlePublisher?.Icon ?? StandardSite
|
||||
const DomainIcon = articlePublisher?.Icon
|
||||
const metaTextStyle = [
|
||||
a.text_xs,
|
||||
a.leading_snug,
|
||||
@@ -57,7 +60,9 @@ export function StandardSiteMetaRow({
|
||||
key: 'domain',
|
||||
node: (
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<DomainIcon size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
{DomainIcon && (
|
||||
<DomainIcon size="sm" fill={t.atoms.text_contrast_medium.color} />
|
||||
)}
|
||||
<Text numberOfLines={1} style={metaTextStyle}>
|
||||
{articleDomain}
|
||||
</Text>
|
||||
@@ -76,7 +81,15 @@ export function StandardSiteMetaRow({
|
||||
<InlineLinkText
|
||||
label={l`View @${authorProfile.handle}'s profile`}
|
||||
to={makeProfileLink(authorProfile)}
|
||||
style={[metaTextStyle, a.pointer_events_auto]}>
|
||||
style={[
|
||||
metaTextStyle,
|
||||
preview ? a.pointer_events_none : a.pointer_events_auto,
|
||||
]}
|
||||
onPress={() => {
|
||||
ax.metric('embed:standardSite:authorHandle:press', {
|
||||
handle: authorProfile.handle,
|
||||
})
|
||||
}}>
|
||||
@{authorProfile.handle}
|
||||
</InlineLinkText>
|
||||
</Trans>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {Image} from 'expo-image'
|
||||
import {type AppBskyEmbedExternal, AtUri} from '@atproto/api'
|
||||
import {AtUri} from '@atproto/api'
|
||||
import {plural} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react/macro'
|
||||
|
||||
import {useHaptics} from '#/lib/haptics'
|
||||
import {useCallOnce} from '#/lib/once'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {niceDate} from '#/lib/strings/time'
|
||||
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||
@@ -13,37 +14,34 @@ import {atoms as a, useBreakpoints, useTheme, utils, web} from '#/alf'
|
||||
import {ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Divider} from '#/components/Divider'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {ArrowTopRight_Stroke2_Corner0_Rounded as ArrowTopRightIcon} from '#/components/icons/Arrow'
|
||||
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 type * as ssTypes from '#/components/Post/Embed/StandardSiteEmbed/types'
|
||||
import {isStandardSitePublicationEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useAnalytics} from '#/analytics'
|
||||
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 = ({
|
||||
preview,
|
||||
view,
|
||||
onOpen,
|
||||
onEmbedInteractionCallback,
|
||||
style,
|
||||
hideSubscribe,
|
||||
}: {
|
||||
view: AppBskyEmbedExternal.ViewExternal
|
||||
onOpen?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
hideSubscribe?: boolean
|
||||
}) => {
|
||||
}: ssTypes.CommonProps &
|
||||
ssTypes.PreviewProps &
|
||||
ssTypes.PublicApiProps & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) => {
|
||||
const ax = useAnalytics()
|
||||
const {t: l, i18n} = useLingui()
|
||||
const t = useTheme()
|
||||
const playHaptic = useHaptics()
|
||||
@@ -54,7 +52,7 @@ export const StandardSiteEmbed = ({
|
||||
new AtUri(ref.uri).collection.startsWith('site.standard.'),
|
||||
)
|
||||
const isStandardPublication = isStandardSitePublicationEmbed(view)
|
||||
let themeColors: ThemeColors = {
|
||||
let themeColors: ssTypes.ThemeColors = {
|
||||
custom: false,
|
||||
accent: t.atoms.text.color,
|
||||
accentForeground: t.atoms.text_inverted.color,
|
||||
@@ -77,28 +75,51 @@ export const StandardSiteEmbed = ({
|
||||
onIn: onInteract,
|
||||
onOut: onInteractOut,
|
||||
} = useInteractionState()
|
||||
|
||||
const onPress = () => {
|
||||
playHaptic('Light')
|
||||
onOpen?.()
|
||||
onEmbedInteractionCallback?.()
|
||||
ax.metric('embed:standardSite:article:press', {url: view.uri})
|
||||
}
|
||||
|
||||
const onLongPress = () => {
|
||||
if (view.uri && IS_NATIVE) {
|
||||
playHaptic('Heavy')
|
||||
shareUrl(view.uri)
|
||||
ax.metric('embed:standardSite:article:longPress', {url: view.uri})
|
||||
}
|
||||
}
|
||||
const onPressPublication = () => {
|
||||
playHaptic('Light')
|
||||
onEmbedInteractionCallback?.()
|
||||
ax.metric('embed:standardSite:publication:press', {
|
||||
url: view.source?.uri || '',
|
||||
})
|
||||
}
|
||||
const onLongPressPublication = () => {
|
||||
if (view.source?.uri && IS_NATIVE) {
|
||||
playHaptic('Heavy')
|
||||
shareUrl(view.source.uri)
|
||||
ax.metric('embed:standardSite:publication:longPress', {
|
||||
url: view.source.uri,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useCallOnce(() => {
|
||||
if (!preview) {
|
||||
ax.metric('embed:standardSite:view', {url: view.uri})
|
||||
}
|
||||
})()
|
||||
|
||||
if (isStandardPublication) {
|
||||
return (
|
||||
<PublicationCard
|
||||
hideSubscribe={hideSubscribe}
|
||||
preview={preview}
|
||||
view={view}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
onPress={onPressPublication}
|
||||
onLongPress={onLongPressPublication}
|
||||
style={style}
|
||||
themeColors={themeColors}
|
||||
onEmbedInteractionCallback={onEmbedInteractionCallback}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -111,7 +132,9 @@ export const StandardSiteEmbed = ({
|
||||
a.overflow_hidden,
|
||||
a.w_full,
|
||||
a.border,
|
||||
t.atoms.bg,
|
||||
interacted ? t.atoms.border_contrast_high : t.atoms.border_contrast_low,
|
||||
preview && a.pointer_events_none,
|
||||
style,
|
||||
]}>
|
||||
<Link
|
||||
@@ -130,7 +153,13 @@ export const StandardSiteEmbed = ({
|
||||
<></>
|
||||
</Link>
|
||||
|
||||
<View style={[a.w_full, a.z_10, a.pointer_events_none]}>
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.z_10,
|
||||
a.pointer_events_none,
|
||||
interacted && [t.atoms.bg_contrast_25],
|
||||
]}>
|
||||
{imageUri ? (
|
||||
<Image
|
||||
style={[a.aspect_card]}
|
||||
@@ -214,45 +243,51 @@ export const StandardSiteEmbed = ({
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[a.z_20]}>
|
||||
<View style={[a.px_md]}>
|
||||
<Divider />
|
||||
</View>
|
||||
{view.source ? (
|
||||
<PublicationFooter
|
||||
view={view}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
themeColors={themeColors}
|
||||
hideSubscribe={hideSubscribe}
|
||||
/>
|
||||
) : (
|
||||
<View style={[a.px_md, a.py_sm, a.pointer_events_none]}>
|
||||
<StandardSiteMetaRow view={view} />
|
||||
{!view.source && (
|
||||
<View style={[a.px_md]}>
|
||||
<Divider />
|
||||
<View style={[a.py_sm]}>
|
||||
<StandardSiteMetaRow preview={preview} view={view} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{view.source && (
|
||||
<View style={[a.z_20]}>
|
||||
<Divider />
|
||||
<PublicationFooter
|
||||
preview={preview}
|
||||
view={view}
|
||||
onPress={onPressPublication}
|
||||
onLongPress={onLongPressPublication}
|
||||
themeColors={themeColors}
|
||||
interactedOuter={interacted}
|
||||
onEmbedInteractionCallback={onEmbedInteractionCallback}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function PublicationCard({
|
||||
preview,
|
||||
view,
|
||||
hideSubscribe,
|
||||
onPress,
|
||||
onLongPress,
|
||||
themeColors,
|
||||
style,
|
||||
}: {
|
||||
view: AppBskyEmbedExternal.ViewExternal
|
||||
hideSubscribe?: boolean
|
||||
onPress?: () => void
|
||||
onLongPress?: () => void
|
||||
themeColors: ThemeColors
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
onEmbedInteractionCallback,
|
||||
}: ssTypes.CommonProps &
|
||||
ssTypes.PreviewProps &
|
||||
ssTypes.PublicApiProps & {
|
||||
onPress?: () => void
|
||||
onLongPress?: () => void
|
||||
themeColors: ssTypes.ThemeColors
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
@@ -272,7 +307,9 @@ export function PublicationCard({
|
||||
a.w_full,
|
||||
a.border,
|
||||
a.p_md,
|
||||
interacted ? t.atoms.border_contrast_high : t.atoms.border_contrast_low,
|
||||
interacted
|
||||
? [t.atoms.bg_contrast_25, t.atoms.border_contrast_high]
|
||||
: [t.atoms.bg, t.atoms.border_contrast_low],
|
||||
style,
|
||||
]}>
|
||||
<Link
|
||||
@@ -322,16 +359,20 @@ export function PublicationCard({
|
||||
style={[a.text_md, a.font_semi_bold, t.atoms.text]}>
|
||||
{view.source?.title}
|
||||
</Text>
|
||||
<StandardSiteMetaRow type="publication" view={view} />
|
||||
<StandardSiteMetaRow
|
||||
preview={preview}
|
||||
type="publication"
|
||||
view={view}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!hideSubscribe && gtPhone && (
|
||||
{gtPhone && (
|
||||
<SubscribeButton
|
||||
preview={preview}
|
||||
view={view}
|
||||
style={[!gtPhone && [a.w_full, a.justify_center]]}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
onEmbedInteractionCallback={onEmbedInteractionCallback}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
@@ -345,13 +386,13 @@ export function PublicationCard({
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!hideSubscribe && !gtPhone && (
|
||||
{!gtPhone && (
|
||||
<View style={[view.description && a.pt_sm]}>
|
||||
<SubscribeButton
|
||||
preview={preview}
|
||||
view={view}
|
||||
style={[!gtPhone && [a.w_full, a.justify_center]]}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
onEmbedInteractionCallback={onEmbedInteractionCallback}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
@@ -361,17 +402,19 @@ export function PublicationCard({
|
||||
}
|
||||
|
||||
export function SubscribeButton({
|
||||
preview,
|
||||
view,
|
||||
onPress,
|
||||
onLongPress,
|
||||
style,
|
||||
}: {
|
||||
view: AppBskyEmbedExternal.ViewExternal
|
||||
onPress?: () => void
|
||||
onLongPress?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
onEmbedInteractionCallback,
|
||||
}: ssTypes.CommonProps &
|
||||
ssTypes.PreviewProps &
|
||||
ssTypes.PublicApiProps & {
|
||||
style?: StyleProp<ViewStyle>
|
||||
}) {
|
||||
const ax = useAnalytics()
|
||||
const {t: l} = useLingui()
|
||||
const playHaptic = useHaptics()
|
||||
|
||||
const highlightedPublisher = matchStandardSitePublisher(view)
|
||||
const cta = highlightedPublisher
|
||||
? l`Subscribe on ${highlightedPublisher.name}`
|
||||
@@ -388,6 +431,36 @@ export function SubscribeButton({
|
||||
? l`View ${publicationTitle}`
|
||||
: l`View publication`
|
||||
|
||||
const onPress = () => {
|
||||
playHaptic('Light')
|
||||
onEmbedInteractionCallback?.()
|
||||
if (highlightedPublisher) {
|
||||
ax.metric('embed:standardSite:subscribe:press', {
|
||||
url: view.source?.uri || '',
|
||||
})
|
||||
} else {
|
||||
ax.metric('embed:standardSite:publicationCta:press', {
|
||||
url: view.source?.uri || '',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const onLongPress = () => {
|
||||
if (view.source?.uri && IS_NATIVE) {
|
||||
playHaptic('Heavy')
|
||||
shareUrl(view.source.uri)
|
||||
if (highlightedPublisher) {
|
||||
ax.metric('embed:standardSite:subscribe:longPress', {
|
||||
url: view.source?.uri || '',
|
||||
})
|
||||
} else {
|
||||
ax.metric('embed:standardSite:publicationCta:longPress', {
|
||||
url: view.source?.uri || '',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StandardSiteThemeProvider view={view}>
|
||||
<Link
|
||||
@@ -396,19 +469,25 @@ export function SubscribeButton({
|
||||
label={label}
|
||||
size="small"
|
||||
color="secondary_inverted"
|
||||
style={[style, a.gap_sm, a.pointer_events_auto]}
|
||||
style={[
|
||||
style,
|
||||
a.gap_sm,
|
||||
preview ? a.pointer_events_none : a.pointer_events_auto,
|
||||
]}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}>
|
||||
{highlightedPublisher ? (
|
||||
<>
|
||||
<View style={[a.flex_row, a.align_center, {gap: 7}]}>
|
||||
<ButtonIcon icon={highlightedPublisher.Icon} size="lg" />
|
||||
{/*<ButtonText>|</ButtonText>*/}
|
||||
<ButtonIcon icon={highlightedPublisher.Icon} size="md" />
|
||||
</View>
|
||||
<ButtonText>{cta}</ButtonText>
|
||||
</>
|
||||
) : (
|
||||
<ButtonText>{cta}</ButtonText>
|
||||
<>
|
||||
<ButtonText>{cta}</ButtonText>
|
||||
<ButtonIcon icon={ArrowTopRightIcon} />
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</StandardSiteThemeProvider>
|
||||
@@ -419,11 +498,10 @@ function PublicationIcon({
|
||||
view,
|
||||
size,
|
||||
themeColors,
|
||||
}: {
|
||||
view: AppBskyEmbedExternal.ViewExternal
|
||||
}: ssTypes.CommonProps & {
|
||||
size: number
|
||||
interacted?: boolean
|
||||
themeColors: ThemeColors
|
||||
themeColors: ssTypes.ThemeColors
|
||||
}) {
|
||||
if (!view.source) return null
|
||||
return view.source?.icon ? (
|
||||
@@ -460,18 +538,21 @@ function PublicationIcon({
|
||||
}
|
||||
|
||||
export function PublicationFooter({
|
||||
preview,
|
||||
view,
|
||||
hideSubscribe,
|
||||
themeColors,
|
||||
onPress,
|
||||
onLongPress,
|
||||
themeColors,
|
||||
}: {
|
||||
view: AppBskyEmbedExternal.ViewExternal
|
||||
hideSubscribe?: boolean
|
||||
themeColors: ThemeColors
|
||||
onPress?: () => void
|
||||
onLongPress?: () => void
|
||||
}) {
|
||||
interactedOuter,
|
||||
onEmbedInteractionCallback,
|
||||
}: ssTypes.CommonProps &
|
||||
ssTypes.PreviewProps &
|
||||
ssTypes.PublicApiProps & {
|
||||
themeColors: ssTypes.ThemeColors
|
||||
onPress?: () => void
|
||||
onLongPress?: () => void
|
||||
interactedOuter?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {t: l} = useLingui()
|
||||
const {gtPhone} = useBreakpoints()
|
||||
@@ -492,8 +573,9 @@ export function PublicationFooter({
|
||||
a.p_md,
|
||||
a.gap_md,
|
||||
gtPhone && [a.flex_row, a.gap_sm],
|
||||
]}
|
||||
testID="publication-embed-footer">
|
||||
interactedOuter && t.atoms.bg_contrast_25,
|
||||
preview && a.pointer_events_none,
|
||||
]}>
|
||||
<Link
|
||||
shouldProxy
|
||||
to={view.source.uri}
|
||||
@@ -538,18 +620,20 @@ export function PublicationFooter({
|
||||
]}>
|
||||
{view.source?.title}
|
||||
</Text>
|
||||
<StandardSiteMetaRow type="publication" view={view} />
|
||||
<StandardSiteMetaRow
|
||||
preview={preview}
|
||||
type="publication"
|
||||
view={view}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{!hideSubscribe && (
|
||||
<SubscribeButton
|
||||
view={view}
|
||||
style={[a.z_10, !gtPhone && [a.w_full, a.justify_center]]}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
/>
|
||||
)}
|
||||
<SubscribeButton
|
||||
preview={preview}
|
||||
view={view}
|
||||
style={[a.z_10, !gtPhone && [a.w_full, a.justify_center]]}
|
||||
onEmbedInteractionCallback={onEmbedInteractionCallback}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import {type AppBskyEmbedExternal} from '@atproto/api'
|
||||
|
||||
export type CommonProps = {
|
||||
view: AppBskyEmbedExternal.ViewExternal
|
||||
}
|
||||
|
||||
export type PreviewProps = {
|
||||
/**
|
||||
* Indicates the card is showing the composer. Interactive elements should be
|
||||
* disabled when this value is true.
|
||||
*/
|
||||
preview?: boolean
|
||||
}
|
||||
|
||||
export type PublicApiProps = {
|
||||
/**
|
||||
* Passed down from feed contexts etc, used to fire off interaction tracking
|
||||
*/
|
||||
onEmbedInteractionCallback?: () => void
|
||||
}
|
||||
|
||||
export type ThemeColors = {
|
||||
custom: boolean
|
||||
accent: string
|
||||
accentForeground: string
|
||||
}
|
||||
@@ -104,6 +104,7 @@ function MediaEmbed({
|
||||
activeStyle={[a.mt_sm]}>
|
||||
<StandardSiteEmbed
|
||||
view={embed.view.external}
|
||||
onEmbedInteractionCallback={rest.onOpen}
|
||||
style={[a.mt_sm, rest.style]}
|
||||
/>
|
||||
</ContentHider>
|
||||
|
||||
Reference in New Issue
Block a user