From 45f0f7eefecae1922c2f30d4e7760d2b93b1ae56 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 13 Jun 2025 12:05:41 -0500 Subject: Port post embeds to new arch (#7408) * Direct port of embeds to new arch (cherry picked from commit cc3fa1f6cea396dd9222486c633a508bfee1ecd6) * Re-org * Split out ListEmbed and FeedEmbed * Split out ImageEmbed * DRY up a bit * Port over ExternalLinkEmbed * Port over Player and Gif embeds * Migrate ComposerReplyTo * Replace other usages of old post-embeds * Migrate view contexts * Copy pasta VideoEmbed * Copy pasta GifEmbed * Swap in new file location * Clean up * Fix up native * Add back in correct moderation on List and Feed embeds * Format * Prettier * delete old video utils * move bandwidth-estimate.ts * Remove log * Add LazyQuoteEmbed for composer use * Clean up unused things * Remove remaining items * Prettier * Fix imports * Handle nested quotes same as prod * Add back silenced error handling * Fix lint --------- Co-authored-by: Samuel Newman --- src/components/Post/Embed/index.tsx | 332 ++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 src/components/Post/Embed/index.tsx (limited to 'src/components/Post/Embed/index.tsx') diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx new file mode 100644 index 000000000..ace85dc98 --- /dev/null +++ b/src/components/Post/Embed/index.tsx @@ -0,0 +1,332 @@ +import React from 'react' +import {View} from 'react-native' +import { + type $Typed, + type AppBskyFeedDefs, + AppBskyFeedPost, + AtUri, + moderatePost, + RichText as RichTextAPI, +} from '@atproto/api' +import {Trans} from '@lingui/macro' +import {useQueryClient} from '@tanstack/react-query' + +import {usePalette} from '#/lib/hooks/usePalette' +import {makeProfileLink} from '#/lib/routes/links' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +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 {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 {SubtleWebHover} from '#/components/SubtleWebHover' +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 +}) { + 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': { + return ( + + Blocked + + ) + } + case 'post_detached': { + return + } + default: { + return null + } + } +} + +export function PostDetachedEmbed({ + embed, +}: { + embed: EmbedType<'post_detached'> +}) { + 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 +}) { + const moderationOpts = useModerationOpts() + const quote = React.useMemo<$Typed>( + () => ({ + ...embed.view, + $type: 'app.bsky.feed.defs#postView', + record: embed.view.value, + embed: embed.view.embeds?.[0], + }), + [embed], + ) + const moderation = React.useMemo(() => { + return moderationOpts ? moderatePost(quote, moderationOpts) : undefined + }, [quote, moderationOpts]) + + const t = useTheme() + const queryClient = useQueryClient() + const pal = usePalette('default') + const itemUrip = new AtUri(quote.uri) + const itemHref = makeProfileLink(quote.author, 'post', itemUrip.rkey) + const itemTitle = `Post by ${quote.author.handle}` + + const richText = React.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 = React.useCallback(() => { + unstableCacheProfileView(queryClient, quote.author) + onOpen?.() + }, [queryClient, quote.author, onOpen]) + + const [hover, setHover] = React.useState(false) + return ( + { + setHover(true) + }} + onPointerLeave={() => { + setHover(false) + }}> + + + + + + + {moderation ? ( + + ) : null} + {richText ? ( + + ) : null} + {quote.embed && ( + + )} + + + + ) +} -- cgit 1.4.1