import {useCallback, useMemo} from 'react' import {View} from 'react-native' import { type $Typed, type AppBskyFeedDefs, AppBskyFeedPost, AtUri, moderatePost, RichText as RichTextAPI, } from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useQueryClient} from '@tanstack/react-query' import {makeProfileLink} from '#/lib/routes/links' 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 {ContentHider} from '#/components/moderation/ContentHider' import {PostAlerts} from '#/components/moderation/PostAlerts' import {RichText} from '#/components/RichText' import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' import {SubtleHover} from '#/components/SubtleHover' import * as bsky from '#/types/bsky' import { type Embed as TEmbed, type EmbedType, parseEmbed, } from '#/types/bsky/post' 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, QuoteEmbedViewContext, } from './types' import {VideoEmbed} from './VideoEmbed' export {PostEmbedViewContext, QuoteEmbedViewContext} from './types' export function Embed({embed: rawEmbed, ...rest}: EmbedProps) { const embed = parseEmbed(rawEmbed) switch (embed.type) { case 'images': 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 } } } function MediaEmbed({ embed, ...rest }: CommonProps & { embed: TEmbed }) { switch (embed.type) { case 'images': { return ( ) } case 'link': { 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') && directFetchEnabled const directRecord = useDirectFetchEmbedRecord({ uri: embed.type === 'post_blocked' || embed.type === 'post_detached' ? embed.view.uri : '', enabled: shouldDirectFetch, }) 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': { return ( Deleted ) } case 'post_blocked': { const record = directRecord.data if (record !== undefined) { return ( ) } return ( Blocked ) } case 'post_detached': { const record = directRecord.data 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, isWithinQuote: parentIsWithinQuote, allowNestedQuotes: parentAllowNestedQuotes, }: Omit & { embed: EmbedType<'post'> viewContext?: QuoteEmbedViewContext visibilityLabel?: string }) { 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 richText = useMemo(() => { if ( !bsky.dangerousIsType( quote.record, AppBskyFeedPost.isRecord, ) ) 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() return ( {({active}) => ( <> {!active && ( )} {moderation ? ( ) : null} {richText ? ( ) : null} {quote.embed && ( )} )} ) }