import { AppBskyEmbedExternal, AppBskyEmbedImages, AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, AppBskyEmbedVideo, AppBskyFeedDefs, AppBskyFeedPost, AppBskyGraphDefs, AppBskyGraphStarterpack, AppBskyLabelerDefs, } from '@atproto/api' import {ComponentChildren, h} from 'preact' import {useMemo} from 'preact/hooks' import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg' import playIcon from '../../assets/play_filled_corner0_rounded.svg' import starterPackIcon from '../../assets/starterPack.svg' import {Globe} from '../icons/Globe' import {CONTENT_LABELS, labelsToInfo} from '../labels' import * as bsky from '../types/bsky' import {getRkey} from '../util/rkey' import {getVerificationState} from '../util/verification-state' import {Link} from './link' import {VerificationCheck} from './verification-check' export function Embed({ content, labels, hideRecord, }: { content: AppBskyFeedDefs.PostView['embed'] labels: AppBskyFeedDefs.PostView['labels'] hideRecord?: boolean }) { const labelInfo = useMemo(() => labelsToInfo(labels), [labels]) if (!content) return null try { // Case 1: Image if (AppBskyEmbedImages.isView(content)) { return } // Case 2: External link if (AppBskyEmbedExternal.isView(content)) { return } // Case 3: Record (quote or linked post) if (AppBskyEmbedRecord.isView(content)) { if (hideRecord) { return null } const record = content.record // Case 3.1: Post if (AppBskyEmbedRecord.isViewRecord(record)) { const pwiOptOut = !!record.author.labels?.find( label => label.val === '!no-unauthenticated', ) if (pwiOptOut) { return ( The author of the quoted post has requested their posts not be displayed on external sites. ) } let text if (AppBskyFeedPost.isRecord(record.value)) { text = record.value.text } const isAuthorLabeled = record.author.labels?.some(label => CONTENT_LABELS.includes(label.val), ) const verification = getVerificationState({profile: record.author}) return (

{record.author.displayName?.trim() || record.author.handle}

{verification.isVerified && ( )}

@{record.author.handle}

{text &&

{text}

} {record.embeds?.map(embed => ( ))} ) } // Case 3.2: List if (AppBskyGraphDefs.isListView(record)) { return ( ) } // Case 3.3: Feed if (AppBskyFeedDefs.isGeneratorView(record)) { return ( ) } // Case 3.4: Labeler if (AppBskyLabelerDefs.isLabelerView(record)) { // Embed type does not exist in the app, so show nothing return null } // Case 3.5: Starter pack if (AppBskyGraphDefs.isStarterPackViewBasic(record)) { return } // Case 3.6: Post not found if (AppBskyEmbedRecord.isViewNotFound(record)) { return Quoted post not found, it may have been deleted. } // Case 3.7: Post blocked if (AppBskyEmbedRecord.isViewBlocked(record)) { return The quoted post is blocked. } // Case 3.8: Detached quote post if (AppBskyEmbedRecord.isViewDetached(record)) { // Just don't show anything return null } // Unknown embed type return null } // Case 4: Video if (AppBskyEmbedVideo.isView(content)) { return } // Case 5: Record with media if ( AppBskyEmbedRecordWithMedia.isView(content) && AppBskyEmbedRecord.isViewRecord(content.record.record) ) { return (
) } // Unknown embed type return null } catch (err) { return ( {err instanceof Error ? err.message : 'An error occurred'} ) } } function Info({children}: {children: ComponentChildren}) { return (

{children}

) } function ImageEmbed({ content, labelInfo, }: { content: AppBskyEmbedImages.View labelInfo?: string }) { if (labelInfo) { return {labelInfo} } switch (content.images.length) { case 1: return ( {content.images[0].alt} ) case 2: return (
{content.images.map((image, i) => ( {image.alt} ))}
) case 3: return (
{content.images[0].alt}
{content.images.slice(1).map((image, i) => ( {image.alt} ))}
) case 4: return (
{content.images.map((image, i) => ( {image.alt} ))}
) default: return null } } function ExternalEmbed({ content, labelInfo, }: { content: AppBskyEmbedExternal.View labelInfo?: string }) { function toNiceDomain(url: string): string { try { const urlp = new URL(url) return urlp.host ? urlp.host : url } catch (e) { return url } } if (labelInfo) { return {labelInfo} } return ( {content.external.thumb && ( )}

{content.external.title}

{content.external.description}

{toNiceDomain(content.external.uri)}

) } function GenericWithImageEmbed({ title, subtitle, href, image, description, }: { title: string subtitle: string href: string image?: string description?: string }) { return (
{image ? ( {title} ) : (
)}

{title}

{subtitle}

{description && (

{description}

)} ) } function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) { let aspectRatio = 1 if (content.aspectRatio) { const {width, height} = content.aspectRatio aspectRatio = clamp(width / height, 1 / 1, 3 / 1) } const supportsHls = useMemo(() => { const video = document.createElement('video') return video.canPlayType('application/vnd.apple.mpegurl') !== '' }, []) if (supportsHls) { return (