import {useCallback, useMemo} from 'react' import {View} from 'react-native' import {type $Typed} from '@atproto/lex' import {AtUri} from '@atproto/syntax' import {moderatePost} from '@bsky/sdk/moderation' import {RichText as RichTextAPI} from '@bsky/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {makeProfileLink} from '#/lib/routes/links' import {getChatInviteCodeFromUrl} from '#/lib/strings/url-helpers' import {useDirectFetchRecords} from '#/state/preferences/direct-fetch-records' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useDirectFetchEmbedRecord} from '#/state/queries/direct-fetch-record' import {unstableCacheProfileView} from '#/state/queries/profile' import {useSession} from '#/state/session' import {Link} from '#/view/com/util/Link' import {PostMeta} from '#/view/com/util/PostMeta' import {atoms as a, useTheme} from '#/alf' 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 * as ReportDialogMetadataContext from '#/components/moderation/ReportDialog/ReportDialogMetadataContext' import {StandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed' import {isStandardSiteEmbed} from '#/components/Post/Embed/StandardSiteEmbed/utils' import {ThemeEmbed} from '#/components/Post/Embed/ThemeEmbed' import {isThemeEmbed} from '#/components/Post/Embed/ThemeEmbed/utils' import {PostTags} from '#/components/PostTags' import {RichText} from '#/components/RichText' import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' import {SubtleHover} from '#/components/SubtleHover' import {IS_ANDROID, IS_WEB} from '#/env' import {app} from '#/lexicons' import * as bsky from '#/types/bsky' import { type Embed as TEmbed, type EmbedType, parseEmbed, } from '#/types/bsky/post' import {ChatInviteEmbed} from './ChatInviteEmbed' import {ExternalEmbed} from './ExternalEmbed' import {ModeratedFeedEmbed} from './FeedEmbed' import {ImageEmbed} from './ImageEmbed' import {ModeratedListEmbed} from './ListEmbed' import {PostPlaceholder as PostPlaceholderText} from './PostPlaceholder' import {type CommonProps, type EmbedProps, PostEmbedViewContext} from './types' import {VideoEmbed} from './VideoEmbed' export {PostEmbedViewContext} from './types' export function Embed(props: EmbedProps) { if (IS_WEB) { return (
) } return } function EmbedInner({embed: rawEmbed, ...rest}: EmbedProps) { const embed = parseEmbed(rawEmbed) switch (embed.type) { case 'images': case 'gallery': case 'link': case 'video': { return ( ) } case 'feed': case 'list': case 'starter_pack': case 'labeler': case 'post': case 'post_not_found': case 'post_blocked': case 'post_detached': { return ( ) } case 'post_with_media': { return ( ) } default: { return null } } } /** Keep each top-level attachment separate from media nested in quoted posts. */ function EmbedTarget({ kind, children, }: { kind: 'media' | 'embed' | 'quote' children: React.ReactNode }) { if (!IS_WEB) return children return (
{children}
) } function MediaEmbed({ embed, ...rest }: CommonProps & { embed: TEmbed }) { switch (embed.type) { case 'images': case 'gallery': { return ( ) } case 'link': { if (isThemeEmbed(embed.view.external)) { return ( ) } if (isStandardSiteEmbed(embed.view.external)) { return ( ) } const chatInviteCode = getChatInviteCodeFromUrl(embed.view.external.uri) if (chatInviteCode) { return ( ) } return ( ) } case 'video': { return ( ) } default: { return null } } } function RecordEmbed({ embed, ...rest }: CommonProps & { embed: TEmbed }) { const {_} = useLingui() const directFetchEnabled = useDirectFetchRecords() const shouldDirectFetch = (embed.type === 'post_blocked' || embed.type === 'post_detached' || embed.type === 'post_not_found') && directFetchEnabled const directRecord = useDirectFetchEmbedRecord({ uri: embed.type === 'post_blocked' || embed.type === 'post_detached' || embed.type === 'post_not_found' ? embed.view.uri : '', enabled: shouldDirectFetch, }) if ( (embed.type === 'post' || embed.type === 'post_blocked' || embed.type === 'post_detached' || embed.type === 'post_not_found') && embed.view.uri === rest.hideRecordEmbedUri ) { return null } switch (embed.type) { case 'feed': { return ( ) } case 'list': { return ( ) } case 'starter_pack': { return ( ) } case 'labeler': { // not implemented return null } case 'post': { if (rest.isWithinQuote && !rest.allowNestedQuotes) { return null } return ( ) } case 'post_not_found': { const directFetchResult = directRecord.data const record = directFetchResult?.record if (record !== undefined) { return ( ) } const directFetchPending = shouldDirectFetch && directRecord.isPending return ( {directFetchResult?.unavailableReason === 'account_suspended' ? ( Author account suspended ) : directFetchResult?.unavailableReason === 'repo_not_found' ? ( Author account unavailable ) : ( Deleted )} ) } case 'post_blocked': { const directFetchResult = directRecord.data const record = directFetchResult?.record if (record !== undefined) { return ( ) } const directFetchPending = shouldDirectFetch && directRecord.isPending return ( {directFetchResult?.unavailableReason === 'account_suspended' ? ( Author account suspended ) : directFetchResult?.unavailableReason === 'repo_not_found' ? ( Author account unavailable ) : shouldDirectFetch && !directFetchPending ? ( Post unavailable ) : ( Blocked )} ) } case 'post_detached': { const record = directRecord.data?.record if (record !== undefined) { return ( ) } return ( ) } default: { return null } } } export function DirectFetchEmbed({ embed, visibilityLabel, visibilityLabelOwner, ...rest }: Omit & { embed: EmbedType<'post'> viewContext?: PostEmbedViewContext visibilityLabel: string visibilityLabelOwner?: string }) { const {currentAccount} = useSession() const isViewerOwner = currentAccount?.did ? embed.view.uri.includes(currentAccount.did) : false return ( ) } export function PostDetachedEmbed({ embed, directFetchEnabled, }: { embed: EmbedType<'post_detached'> directFetchEnabled?: boolean }) { const {currentAccount} = useSession() const isViewerOwner = currentAccount?.did ? embed.view.uri.includes(currentAccount.did) : false return ( {isViewerOwner ? ( Removed by you ) : ( Removed by author )} ) } /* * Nests parent `Embed` component and therefore must live in this file to avoid * circular imports. */ export function QuoteEmbed({ embed, onOpen, style, linkDisabled, isWithinQuote: parentIsWithinQuote, allowNestedQuotes: parentAllowNestedQuotes, showPronouns, viewContext, }: Omit & { embed: EmbedType<'post'> viewContext?: PostEmbedViewContext visibilityLabel?: string linkDisabled?: boolean }) { const moderationOpts = useModerationOpts() const quote = useMemo<$Typed>( () => ({ ...embed.view, $type: 'app.bsky.feed.defs#postView', record: embed.view.value, embed: embed.view.embeds?.[0], }), [embed], ) const moderation = useMemo(() => { return moderationOpts ? moderatePost(quote, moderationOpts) : undefined }, [quote, moderationOpts]) const t = useTheme() const queryClient = useQueryClient() const itemUrip = new AtUri(quote.uri) const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) const itemTitle = `Post by ${quote.author.handle}` const isAndroidCarousel = IS_ANDROID && viewContext === PostEmbedViewContext.FeedCarousel const richText = useMemo(() => { if (!bsky.isType(app.bsky.feed.post, quote.record)) return undefined const {text, facets} = quote.record return text.trim() ? new RichTextAPI({text: text, facets: facets}) : undefined }, [quote.record]) const onBeforePress = useCallback(() => { unstableCacheProfileView(queryClient, quote.author) onOpen?.() }, [queryClient, quote.author, onOpen]) const { state: hover, onIn: onPointerEnter, onOut: onPointerLeave, } = useInteractionState() const { state: pressed, onIn: onPressIn, onOut: onPressOut, } = useInteractionState() const contents = ( {moderation ? ( ) : null} {richText ? ( ) : null} {quote.embed && ( )} ) return ( {({active}) => ( <> {!active && !linkDisabled && ( )} {linkDisabled ? ( {contents} ) : ( {contents} )} )} ) }