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 {Leaflet} from '#/components/icons/community/Leaflet' import {Offprint} from '#/components/icons/community/Offprint' import {Pckt} from '#/components/icons/community/Pckt' import {StandardSite} from '#/components/icons/community/StandardSite' import {Earth_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' import {Link} from '#/components/Link' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {PublicationMetaRow} from '#/components/Post/Embed/StandardSiteEmbed/PublicationMetaRow' 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 } export function useStandardSitePublisherConfig( view: AppBskyEmbedExternal.ViewExternal, ) { try { const u = new URL(view.source?.uri || '') if (u.host.endsWith('leaflet.pub')) { return { name: 'Leaflet', Icon: Leaflet, } } else if (u.host.endsWith('pckt.blog')) { return { name: 'pckt', Icon: Pckt, } } else if (u.host.endsWith('offprint.app')) { return { name: 'Offprint', Icon: Offprint, } } return null } catch (e) { return null } } export const StandardSiteEmbed = ({ view, onOpen, style, hideSubscribe, }: { view: AppBskyEmbedExternal.ViewExternal onOpen?: () => void style?: StyleProp 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 publicationUri = view.associatedRefs?.find( ref => new AtUri(ref.uri).collection === 'site.standard.publication', )?.uri const maybeAuthorDid = publicationUri ? new AtUri(publicationUri)?.did : null const onPress = () => { playHaptic('Light') onOpen?.() } const onLongPress = () => { if (view.uri && IS_NATIVE) { playHaptic('Heavy') shareUrl(view.uri) } } if (isStandardPublication) { return ( ) } return ( {({hovered}) => ( {imageUri ? ( ) : undefined} {view.title} {view.description ? ( {view.description} ) : undefined} {isStandard && (view.createdAt || view.readingTime) && ( {view.createdAt && ( {niceDate(i18n, view.createdAt, 'medium', 'none')} )} {view.readingTime && ( {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.`, })} )} )} {!view.source && ( {toNiceDomain(view.uri)} )} )} {view.source && ( <> )} ) } export function PublicationCard({ view, hideSubscribe, onPress, onLongPress, themeColors, style, author, }: { view: AppBskyEmbedExternal.ViewExternal hideSubscribe?: boolean onPress?: () => void onLongPress?: () => void themeColors: ThemeColors style?: StyleProp author: {did: string | null | undefined} }) { const t = useTheme() const {t: l} = useLingui() const {gtPhone} = useBreakpoints() const { state: interactedWithin, onIn: onInteractWithin, onOut: onInteractWithout, } = useInteractionState() if (!view.source) return null return ( {({hovered: maybeHovered}) => { const hovered = maybeHovered && !interactedWithin return ( <> {view.source?.title} {!hideSubscribe && ( )} {view.description && ( {view.description} )} ) }} ) } export function SubscribeButton({ view, onPress, onLongPress, style, }: { view: AppBskyEmbedExternal.ViewExternal onPress?: () => void onLongPress?: () => void style?: StyleProp }) { const {t: l} = useLingui() const highlightedPublisher = useStandardSitePublisherConfig(view) const cta = highlightedPublisher ? l`Subscribe on ${highlightedPublisher.name}` : l`View publication` if (!view.source) return null return ( {highlightedPublisher ? ( <> {/*|*/} {cta} ) : ( {cta} )} ) } function PublicationIcon({ view, size, hovered, themeColors, }: { view: AppBskyEmbedExternal.ViewExternal size: number hovered?: boolean themeColors: ThemeColors }) { const opacity = hovered ? 0.6 : 0.2 return view.source?.icon ? ( ) : ( ) } export function PublicationFooter({ view, hideSubscribe, onPress, onLongPress, themeColors, author, }: { view: AppBskyEmbedExternal.ViewExternal hideSubscribe?: boolean themeColors: ThemeColors onPress?: () => void onLongPress?: () => void author: {did: string | null | undefined} }) { 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.source?.title} {!hideSubscribe && ( )} ) }