45f0f7eefe
* 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>
333 lines
8.3 KiB
TypeScript
333 lines
8.3 KiB
TypeScript
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 <MediaEmbed embed={embed} {...rest} />
|
|
}
|
|
case 'feed':
|
|
case 'list':
|
|
case 'starter_pack':
|
|
case 'labeler':
|
|
case 'post':
|
|
case 'post_not_found':
|
|
case 'post_blocked':
|
|
case 'post_detached': {
|
|
return <RecordEmbed embed={embed} {...rest} />
|
|
}
|
|
case 'post_with_media': {
|
|
return (
|
|
<View style={rest.style}>
|
|
<MediaEmbed embed={embed.media} {...rest} />
|
|
<RecordEmbed embed={embed.view} {...rest} />
|
|
</View>
|
|
)
|
|
}
|
|
default: {
|
|
return null
|
|
}
|
|
}
|
|
}
|
|
|
|
function MediaEmbed({
|
|
embed,
|
|
...rest
|
|
}: CommonProps & {
|
|
embed: TEmbed
|
|
}) {
|
|
switch (embed.type) {
|
|
case 'images': {
|
|
return (
|
|
<ContentHider modui={rest.moderation?.ui('contentMedia')}>
|
|
<ImageEmbed embed={embed} {...rest} />
|
|
</ContentHider>
|
|
)
|
|
}
|
|
case 'link': {
|
|
return (
|
|
<ContentHider modui={rest.moderation?.ui('contentMedia')}>
|
|
<ExternalEmbed
|
|
link={embed.view.external}
|
|
onOpen={rest.onOpen}
|
|
style={[a.mt_sm, rest.style]}
|
|
/>
|
|
</ContentHider>
|
|
)
|
|
}
|
|
case 'video': {
|
|
return (
|
|
<ContentHider modui={rest.moderation?.ui('contentMedia')}>
|
|
<VideoEmbed embed={embed.view} />
|
|
</ContentHider>
|
|
)
|
|
}
|
|
default: {
|
|
return null
|
|
}
|
|
}
|
|
}
|
|
|
|
function RecordEmbed({
|
|
embed,
|
|
...rest
|
|
}: CommonProps & {
|
|
embed: TEmbed
|
|
}) {
|
|
switch (embed.type) {
|
|
case 'feed': {
|
|
return (
|
|
<View style={a.mt_sm}>
|
|
<ModeratedFeedEmbed embed={embed} {...rest} />
|
|
</View>
|
|
)
|
|
}
|
|
case 'list': {
|
|
return (
|
|
<View style={a.mt_sm}>
|
|
<ModeratedListEmbed embed={embed} />
|
|
</View>
|
|
)
|
|
}
|
|
case 'starter_pack': {
|
|
return (
|
|
<View style={a.mt_sm}>
|
|
<StarterPackCard starterPack={embed.view} />
|
|
</View>
|
|
)
|
|
}
|
|
case 'labeler': {
|
|
// not implemented
|
|
return null
|
|
}
|
|
case 'post': {
|
|
if (rest.isWithinQuote && !rest.allowNestedQuotes) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<QuoteEmbed
|
|
{...rest}
|
|
embed={embed}
|
|
viewContext={
|
|
rest.viewContext === PostEmbedViewContext.Feed
|
|
? QuoteEmbedViewContext.FeedEmbedRecordWithMedia
|
|
: undefined
|
|
}
|
|
isWithinQuote={rest.isWithinQuote}
|
|
allowNestedQuotes={rest.allowNestedQuotes}
|
|
/>
|
|
)
|
|
}
|
|
case 'post_not_found': {
|
|
return (
|
|
<PostPlaceholderText>
|
|
<Trans>Deleted</Trans>
|
|
</PostPlaceholderText>
|
|
)
|
|
}
|
|
case 'post_blocked': {
|
|
return (
|
|
<PostPlaceholderText>
|
|
<Trans>Blocked</Trans>
|
|
</PostPlaceholderText>
|
|
)
|
|
}
|
|
case 'post_detached': {
|
|
return <PostDetachedEmbed embed={embed} />
|
|
}
|
|
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 (
|
|
<PostPlaceholderText>
|
|
{isViewerOwner ? (
|
|
<Trans>Removed by you</Trans>
|
|
) : (
|
|
<Trans>Removed by author</Trans>
|
|
)}
|
|
</PostPlaceholderText>
|
|
)
|
|
}
|
|
|
|
/*
|
|
* 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<CommonProps, 'viewContext'> & {
|
|
embed: EmbedType<'post'>
|
|
viewContext?: QuoteEmbedViewContext
|
|
}) {
|
|
const moderationOpts = useModerationOpts()
|
|
const quote = React.useMemo<$Typed<AppBskyFeedDefs.PostView>>(
|
|
() => ({
|
|
...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<AppBskyFeedPost.Record>(
|
|
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 (
|
|
<View
|
|
onPointerEnter={() => {
|
|
setHover(true)
|
|
}}
|
|
onPointerLeave={() => {
|
|
setHover(false)
|
|
}}>
|
|
<ContentHider
|
|
modui={moderation?.ui('contentList')}
|
|
style={[
|
|
a.rounded_md,
|
|
a.p_md,
|
|
a.mt_sm,
|
|
a.border,
|
|
t.atoms.border_contrast_low,
|
|
style,
|
|
]}
|
|
childContainerStyle={[a.pt_sm]}>
|
|
<SubtleWebHover hover={hover} />
|
|
<Link
|
|
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
|
href={itemHref}
|
|
title={itemTitle}
|
|
onBeforePress={onBeforePress}>
|
|
<View pointerEvents="none">
|
|
<PostMeta
|
|
author={quote.author}
|
|
moderation={moderation}
|
|
showAvatar
|
|
postHref={itemHref}
|
|
timestamp={quote.indexedAt}
|
|
/>
|
|
</View>
|
|
{moderation ? (
|
|
<PostAlerts
|
|
modui={moderation.ui('contentView')}
|
|
style={[a.py_xs]}
|
|
/>
|
|
) : null}
|
|
{richText ? (
|
|
<RichText
|
|
value={richText}
|
|
style={a.text_md}
|
|
numberOfLines={20}
|
|
disableLinks
|
|
/>
|
|
) : null}
|
|
{quote.embed && (
|
|
<Embed
|
|
embed={quote.embed}
|
|
moderation={moderation}
|
|
isWithinQuote={parentIsWithinQuote ?? true}
|
|
// already within quote? override nested
|
|
allowNestedQuotes={
|
|
parentIsWithinQuote ? false : parentAllowNestedQuotes
|
|
}
|
|
/>
|
|
)}
|
|
</Link>
|
|
</ContentHider>
|
|
</View>
|
|
)
|
|
}
|