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 <mozzius@protonmail.com>
This commit is contained in:
Eric Bailey
2025-06-13 12:05:41 -05:00
committed by GitHub
parent ba0f5a9bde
commit 45f0f7eefe
75 changed files with 812 additions and 799 deletions
+10 -5
View File
@@ -72,7 +72,7 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {mimeToExt} from '#/lib/media/video/util'
import {logEvent} from '#/lib/statsig/statsig'
import {cleanError} from '#/lib/strings/errors'
import {colors, s} from '#/lib/styles'
import {colors} from '#/lib/styles'
import {logger} from '#/logger'
import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection'
import {useDialogStateControlContext} from '#/state/dialogs'
@@ -97,6 +97,7 @@ import {
ExternalEmbedGif,
ExternalEmbedLink,
} from '#/view/com/composer/ExternalEmbed'
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
import {GifAltTextDialog} from '#/view/com/composer/GifAltText'
import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
import {Gallery} from '#/view/com/composer/photos/Gallery'
@@ -116,7 +117,6 @@ import {SelectVideoBtn} from '#/view/com/composer/videos/SelectVideoBtn'
import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog'
import {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress'
import {LazyQuoteEmbed, QuoteX} from '#/view/com/util/post-embeds/QuoteEmbed'
import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
@@ -125,6 +125,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmile} from '#/components/icons/Emoji'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed'
import * as Prompt from '#/components/Prompt'
import {Text as NewText} from '#/components/Typography'
import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet'
@@ -1149,13 +1150,17 @@ function ComposerEmbeds({
)}
</LayoutAnimationConfig>
{embed.quote?.uri ? (
<View style={!video ? [a.mt_md] : []}>
<View style={[s.mt5, s.mb2, isWeb && s.mb10]}>
<View
style={[a.pb_sm, video ? [a.pt_md] : [a.pt_xl], isWeb && [a.pb_md]]}>
<View style={[a.relative]}>
<View style={{pointerEvents: 'none'}}>
<LazyQuoteEmbed uri={embed.quote.uri} />
</View>
{canRemoveQuote && (
<QuoteX onRemove={() => dispatch({type: 'embed_remove_quote'})} />
<ExternalEmbedRemoveBtn
onRemove={() => dispatch({type: 'embed_remove_quote'})}
style={{top: 16}}
/>
)}
</View>
</View>
+11 -2
View File
@@ -13,12 +13,13 @@ import {useLingui} from '@lingui/react'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {type ComposerOptsPostRef} from '#/state/shell/composer'
import {MaybeQuoteEmbed} from '#/view/com/util/post-embeds/QuoteEmbed'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme, web} from '#/alf'
import {QuoteEmbed} from '#/components/Post/Embed'
import {Text} from '#/components/Typography'
import {useSimpleVerificationState} from '#/components/verification'
import {VerificationCheck} from '#/components/verification/VerificationCheck'
import {parseEmbed} from '#/types/bsky/post'
export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
const t = useTheme()
@@ -51,6 +52,12 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
}
return null
}, [embed])
const parsedQuoteEmbed = quoteEmbed
? parseEmbed({
$type: 'app.bsky.embed.record#view',
...quoteEmbed,
})
: null
const images = useMemo(() => {
if (AppBskyEmbedImages.isView(embed)) {
@@ -124,7 +131,9 @@ export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) {
<ComposerReplyToImages images={images} showFull={showFull} />
)}
</View>
{showFull && quoteEmbed && <MaybeQuoteEmbed embed={quoteEmbed} />}
{showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && (
<QuoteEmbed embed={parsedQuoteEmbed} />
)}
</View>
</Pressable>
)
+29 -8
View File
@@ -1,19 +1,20 @@
import React from 'react'
import {StyleProp, View, ViewStyle} from 'react-native'
import {type StyleProp, View, type ViewStyle} from 'react-native'
import {cleanError} from '#/lib/strings/errors'
import {
useResolveGifQuery,
useResolveLinkQuery,
} from '#/state/queries/resolve-link'
import {Gif} from '#/state/queries/tenor'
import {type Gif} from '#/state/queries/tenor'
import {ExternalEmbedRemoveBtn} from '#/view/com/composer/ExternalEmbedRemoveBtn'
import {ExternalLinkEmbed} from '#/view/com/util/post-embeds/ExternalLinkEmbed'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {ExternalEmbed} from '#/components/Post/Embed/ExternalEmbed'
import {ModeratedFeedEmbed} from '#/components/Post/Embed/FeedEmbed'
import {ModeratedListEmbed} from '#/components/Post/Embed/ListEmbed'
import {Embed as StarterPackEmbed} from '#/components/StarterPack/StarterPackCard'
import {Text} from '#/components/Typography'
import {MaybeFeedCard, MaybeListCard} from '../util/post-embeds'
export const ExternalEmbedGif = ({
onRemove,
@@ -44,7 +45,7 @@ export const ExternalEmbedGif = ({
<View style={[a.overflow_hidden, t.atoms.border_contrast_medium]}>
{linkInfo ? (
<View style={{pointerEvents: 'auto'}}>
<ExternalLinkEmbed link={linkInfo} hideAlt />
<ExternalEmbed link={linkInfo} hideAlt />
</View>
) : error ? (
<Container style={[a.align_start, a.p_md, a.gap_xs]}>
@@ -80,7 +81,7 @@ export const ExternalEmbedLink = ({
if (data) {
if (data.type === 'external') {
return (
<ExternalLinkEmbed
<ExternalEmbed
link={{
title: data.title || uri,
uri,
@@ -91,9 +92,29 @@ export const ExternalEmbedLink = ({
/>
)
} else if (data.kind === 'feed') {
return <MaybeFeedCard view={data.view} />
return (
<ModeratedFeedEmbed
embed={{
type: 'feed',
view: {
$type: 'app.bsky.feed.defs#generatorView',
...data.view,
},
}}
/>
)
} else if (data.kind === 'list') {
return <MaybeListCard view={data.view} />
return (
<ModeratedListEmbed
embed={{
type: 'list',
view: {
$type: 'app.bsky.graph.defs#listView',
...data.view,
},
}}
/>
)
} else if (data.kind === 'starter-pack') {
return <StarterPackEmbed starterPack={data.view} />
}
@@ -2,22 +2,27 @@ import {View} from 'react-native'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a} from '#/alf'
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
export function ExternalEmbedRemoveBtn({onRemove}: {onRemove: () => void}) {
export function ExternalEmbedRemoveBtn({
onRemove,
style,
}: {onRemove: () => void} & ViewStyleProp) {
const t = useTheme()
const {_} = useLingui()
return (
<View style={[a.absolute, {top: 8, right: 8}, a.z_50]}>
<View style={[a.absolute, {top: 8, right: 8}, a.z_50, style]}>
<Button
label={_(msg`Remove attachment`)}
onPress={onRemove}
size="small"
variant="solid"
color="secondary"
shape="round">
shape="round"
style={[t.atoms.shadow_sm]}>
<ButtonIcon icon={X} size="sm" />
</Button>
</View>
+4 -4
View File
@@ -6,23 +6,23 @@ import {useLingui} from '@lingui/react'
import {HITSLOP_10, MAX_ALT_TEXT} from '#/lib/constants'
import {parseAltFromGIFDescription} from '#/lib/gif-alt-text'
import {
EmbedPlayerParams,
type EmbedPlayerParams,
parseEmbedPlayerFromUrl,
} from '#/lib/strings/embed-player'
import {isAndroid} from '#/platform/detection'
import {useResolveGifQuery} from '#/state/queries/resolve-link'
import {Gif} from '#/state/queries/tenor'
import {type Gif} from '#/state/queries/tenor'
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {DialogControlProps} from '#/components/Dialog'
import {type DialogControlProps} from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif'
import {Text} from '#/components/Typography'
import {GifEmbed} from '../util/post-embeds/GifEmbed'
import {AltTextReminder} from './photos/Gallery'
export function GifAltTextDialog({
+3 -3
View File
@@ -4,10 +4,10 @@ import {useLingui} from '@lingui/react'
import {
ADULT_CONTENT_LABELS,
AdultSelfLabel,
type AdultSelfLabel,
OTHER_SELF_LABELS,
OtherSelfLabel,
SelfLabel,
type OtherSelfLabel,
type SelfLabel,
} from '#/lib/moderation'
import {isWeb} from '#/platform/detection'
import {atoms as a, native, useTheme, web} from '#/alf'
@@ -1,5 +1,5 @@
import React from 'react'
import {ImageStyle, useWindowDimensions, View} from 'react-native'
import {type ImageStyle, useWindowDimensions, View} from 'react-native'
import {Image} from 'expo-image'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -7,12 +7,12 @@ import {useLingui} from '@lingui/react'
import {MAX_ALT_TEXT} from '#/lib/constants'
import {enforceLen} from '#/lib/strings/helpers'
import {isAndroid, isWeb} from '#/platform/detection'
import {ComposerImage} from '#/state/gallery'
import {type ComposerImage} from '#/state/gallery'
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {DialogControlProps} from '#/components/Dialog'
import {type DialogControlProps} from '#/components/Dialog'
import * as TextField from '#/components/forms/TextField'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {Text} from '#/components/Typography'
@@ -8,7 +8,7 @@ import {useCameraPermission} from '#/lib/hooks/usePermissions'
import {openCamera} from '#/lib/media/picker'
import {logger} from '#/logger'
import {isMobileWeb, isNative} from '#/platform/detection'
import {ComposerImage, createComposerImage} from '#/state/gallery'
import {type ComposerImage, createComposerImage} from '#/state/gallery'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Camera_Stroke2_Corner0_Rounded as Camera} from '#/components/icons/Camera'