import classnames from 'clsx'; import { List as ImmutableList } from 'immutable'; import React, { useState, useEffect } from 'react'; import Blurhash from 'soapbox/components/blurhash'; import Icon from 'soapbox/components/icon'; import { HStack, Stack, Text } from 'soapbox/components/ui'; import { normalizeAttachment } from 'soapbox/normalizers'; import { addAutoPlay } from 'soapbox/utils/media'; import type { Card as CardEntity, Attachment } from 'soapbox/types/entities'; const trim = (text: string, len: number): string => { const cut = text.indexOf(' ', len); if (cut === -1) { return text; } return text.substring(0, cut) + (text.length > len ? '…' : ''); }; interface ICard { card: CardEntity, maxTitle?: number, maxDescription?: number, onOpenMedia: (attachments: ImmutableList, index: number) => void, compact?: boolean, defaultWidth?: number, cacheWidth?: (width: number) => void, horizontal?: boolean, } const Card: React.FC = ({ card, defaultWidth = 467, maxTitle = 120, maxDescription = 200, compact = false, cacheWidth, onOpenMedia, horizontal, }): JSX.Element => { const [width, setWidth] = useState(defaultWidth); const [embedded, setEmbedded] = useState(false); useEffect(() => { setEmbedded(false); }, [card.url]); const trimmedTitle = trim(card.title, maxTitle); const trimmedDescription = trim(card.description, maxDescription); const handlePhotoClick = () => { const attachment = normalizeAttachment({ type: 'image', url: card.embed_url, description: trimmedTitle, meta: { original: { width: card.width, height: card.height, }, }, }); onOpenMedia(ImmutableList([attachment]), 0); }; const handleEmbedClick: React.MouseEventHandler = (e) => { e.stopPropagation(); if (card.type === 'photo') { handlePhotoClick(); } else { setEmbedded(true); } }; const setRef: React.RefCallback = c => { if (c) { if (cacheWidth) { cacheWidth(c.offsetWidth); } setWidth(c.offsetWidth); } }; const renderVideo = () => { const content = { __html: addAutoPlay(card.html) }; const ratio = getRatio(card); const height = width / ratio; return (
); }; const getRatio = (card: CardEntity): number => { const ratio = (card.width / card.height) || 16 / 9; // Constrain to a sane limit // https://en.wikipedia.org/wiki/Aspect_ratio_(image) return Math.min(Math.max(9 / 16, ratio), 4); }; const interactive = card.type !== 'link'; horizontal = typeof horizontal === 'boolean' ? horizontal : interactive || embedded; const className = classnames('status-card', { horizontal, compact, interactive }, `status-card--${card.type}`); const ratio = getRatio(card); const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); const title = interactive ? ( e.stopPropagation()} href={card.url} title={trimmedTitle} rel='noopener' target='_blank' > {trimmedTitle} ) : ( {trimmedTitle} ); const description = ( {trimmedTitle && ( {title} )} {trimmedDescription && ( {trimmedDescription} )} {card.provider_name} ); let embed: React.ReactNode = null; const canvas = ( ); const thumbnail = (
); if (interactive) { if (embedded) { embed = renderVideo(); } else { let iconVariant = require('@tabler/icons/player-play.svg'); if (card.type === 'photo') { iconVariant = require('@tabler/icons/zoom-in.svg'); } embed = ( ); } return (
{embed} {description}
); } else if (card.image) { embed = (
{canvas} {thumbnail}
); } return ( e.stopPropagation()} > {embed} {description} ); }; export default Card;