import {app} from '@bsky.app/sdk/lexicons' import {ComponentChildren, h} from 'preact' import {useMemo} from 'preact/hooks' import {Link} from '#/components/link' import {VerificationCheck} from '#/components/verification-check' import {Globe} from '#/icons/Globe' import {CONTENT_LABELS, labelsToInfo} from '#/labels' import {getRkey} from '#/util/rkey' import {getVerificationState} from '#/util/verification-state' import infoIcon from '../../assets/arrowBottom_stroke2_corner0_rounded.svg' import playIcon from '../../assets/play_filled_corner0_rounded.svg' import starterPackIcon from '../../assets/starterPack.svg' export function Embed({ content, labels, hideRecord, }: { content: app.bsky.feed.defs.PostView['embed'] labels: app.bsky.feed.defs.PostView['labels'] hideRecord?: boolean }) { const labelInfo = useMemo(() => labelsToInfo(labels), [labels]) if (!content) return null try { // Case 1: Image if (app.bsky.embed.images.view.isTypeOf(content)) { return } // Case 1b: Gallery (Photos v2) if (app.bsky.embed.gallery.view.isTypeOf(content)) { return } // Case 2: External link if (app.bsky.embed.external.view.isTypeOf(content)) { return } // Case 3: Record (quote or linked post) if (app.bsky.embed.record.view.isTypeOf(content)) { if (hideRecord) { return null } const record = content.record // Case 3.1: Post if (app.bsky.embed.record.viewRecord.isTypeOf(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 const validation = app.bsky.feed.post.$safeValidate(record.value) if (validation.success) { text = validation.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 (app.bsky.graph.defs.listView.isTypeOf(record)) { if (labelInfo) { return {labelInfo} } return ( ) } // Case 3.3: Feed if (app.bsky.feed.defs.generatorView.isTypeOf(record)) { if (labelInfo) { return {labelInfo} } return ( ) } // Case 3.4: Labeler if (app.bsky.labeler.defs.labelerView.isTypeOf(record)) { // Embed type does not exist in the app, so show nothing return null } // Case 3.5: Starter pack if (app.bsky.graph.defs.starterPackViewBasic.isTypeOf(record)) { return } // Case 3.6: Post not found if (app.bsky.embed.record.viewNotFound.isTypeOf(record)) { return Quoted post not found, it may have been deleted. } // Case 3.7: Post blocked if (app.bsky.embed.record.viewBlocked.isTypeOf(record)) { return The quoted post is blocked. } // Case 3.8: Detached quote post if (app.bsky.embed.record.viewDetached.isTypeOf(record)) { // Just don't show anything return null } // Unknown embed type return null } // Case 4: Video if (app.bsky.embed.video.view.isTypeOf(content)) { return } // Case 5: Record with media if ( app.bsky.embed.recordWithMedia.view.isTypeOf(content) && app.bsky.embed.record.viewRecord.isTypeOf(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}

) } type GridImage = {thumb: string; alt: string} function ImageEmbed({ content, labelInfo, }: { content: app.bsky.embed.images.View labelInfo?: string }) { if (labelInfo) { return {labelInfo} } return ( ({thumb: i.thumb, alt: i.alt}))} /> ) } function GalleryEmbed({ content, labelInfo, }: { content: app.bsky.embed.gallery.View labelInfo?: string }) { if (labelInfo) { return {labelInfo} } const images = content.items .filter(i => app.bsky.embed.gallery.viewImage.isTypeOf(i)) .map(i => ({thumb: i.thumbnail, alt: i.alt})) return } function ImageGrid({images}: {images: GridImage[]}) { switch (images.length) { case 0: return null case 1: return ( {images[0].alt} ) case 2: return (
{images.map((image, i) => ( {image.alt} ))}
) case 3: return (
{images[0].alt}
{images.slice(1).map((image, i) => ( {image.alt} ))}
) default: { const remaining = images.length - 4 return (
{images.slice(0, 4).map((image, i) => { const isOverflowCell = i === 3 && remaining > 0 return (
{image.alt} {isOverflowCell && (
+{remaining}
)}
) })}
) } } } function ExternalEmbed({ content, labelInfo, }: { content: app.bsky.embed.external.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: app.bsky.embed.video.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 (