'use no memo'; // generated cache cost outweighs reuse in these shallow renderers
import type { ReactNode } from 'react';
import { unwrapEmbed, type AppBskyFeedDefs } from '@atcute/bluesky';
import type { DisplayRestrictions } from '@atcute/bluesky-moderation';
import { clsx } from 'clsx';
import { videoThumbnailUrl } from '#/lib/bsky-cdn';
import { isGifEmbed } from '#/lib/media/gif-embed';
import * as styles from '#/components/MediaPreview.css';
import { PlayButtonIcon } from '#/components/PlayButtonIcon';
import { Text } from '#/components/Text';
import { m } from '#/paraglide/messages';
export function Embed({
className,
embed,
moderation,
}: {
className?: string;
embed: AppBskyFeedDefs.PostView['embed'];
moderation?: DisplayRestrictions;
}) {
const { media } = unwrapEmbed(embed);
if (!media) {
return null;
}
const blurred = !!moderation?.blurs.length;
switch (media.$type) {
case 'app.bsky.embed.images#view': {
return (
<Outer className={className}>
{media.images.map((image) => (
<ImageItem key={image.thumb} thumbnail={image.thumb} alt={image.alt} blurred={blurred} />
))}
</Outer>
);
}
case 'app.bsky.embed.gallery#view': {
// cap at 4 tiles so a large gallery doesn't blow out this narrow inline strip
return (
<Outer className={className}>
{media.items.slice(0, 4).map((image) => (
<ImageItem key={image.thumbnail} thumbnail={image.thumbnail} alt={image.alt} blurred={blurred} />
))}
</Outer>
);
}
case 'app.bsky.embed.external#view': {
if (!media.external.thumb || !isGifEmbed(media.external.uri)) {
return null;
}
return (
<Outer className={className}>
<GifItem thumbnail={media.external.thumb} alt={media.external.title} blurred={blurred} />
</Outer>
);
}
case 'app.bsky.embed.video#view': {
return (
<Outer className={className}>
{media.presentation === 'gif' ? (
<GifItem thumbnail={videoThumbnailUrl(media)} alt={media.alt} blurred={blurred} />
) : (
<VideoItem thumbnail={videoThumbnailUrl(media)} alt={media.alt} blurred={blurred} />
)}
</Outer>
);
}
default: {
return null;
}
}
}
function Outer({ children, className }: { children?: ReactNode; className?: string }) {
return <div className={clsx(styles.outer, className)}>{children}</div>;
}
function ImageItem({
thumbnail,
alt,
blurred,
children,
}: {
thumbnail?: string;
alt?: string;
blurred?: boolean;
children?: ReactNode;
}) {
return (
<div
className={clsx(styles.tile, !thumbnail && styles.tileEmpty)}
aria-label={thumbnail ? undefined : alt}
>
{thumbnail && (
<img className={clsx(styles.image, blurred && styles.blurred)} src={thumbnail} alt={alt} />
)}
{children}
</div>
);
}
function GifItem({ thumbnail, alt, blurred }: { thumbnail?: string; alt?: string; blurred?: boolean }) {
return (
<ImageItem thumbnail={thumbnail} alt={alt} blurred={blurred}>
<div className={styles.overlay} aria-hidden>
<PlayButtonIcon size={24} />
</div>
<div className={styles.gifBadge} aria-hidden>
<Text className={styles.gifBadgeText}>{m['common.gif.label']()}</Text>
</div>
</ImageItem>
);
}
function VideoItem({ thumbnail, alt, blurred }: { thumbnail?: string; alt?: string; blurred?: boolean }) {
return (
<ImageItem thumbnail={thumbnail} alt={alt} blurred={blurred}>
<div className={styles.overlay} aria-hidden>
<PlayButtonIcon size={24} />
</div>
</ImageItem>
);
}