mirror of
https://github.com/mastodon/mastodon.git
synced 2025-11-27 10:00:50 +00:00
Refactor Card component to TypeScript (#36982)
This commit is contained in:
parent
1757a0f0f3
commit
f87f30c1ac
|
|
@ -538,9 +538,8 @@ class Status extends ImmutablePureComponent {
|
||||||
} else if (status.get('card') && !status.get('quote')) {
|
} else if (status.get('card') && !status.get('quote')) {
|
||||||
media = (
|
media = (
|
||||||
<Card
|
<Card
|
||||||
onOpenMedia={this.handleOpenMedia}
|
key={`${status.get('id')}-${status.get('edited_at')}`}
|
||||||
card={status.get('card')}
|
card={status.get('card')}
|
||||||
compact
|
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
import punycode from 'punycode';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
|
|
||||||
import { is } from 'immutable';
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
|
|
||||||
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
|
|
||||||
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
|
|
||||||
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
|
|
||||||
import { Blurhash } from 'mastodon/components/blurhash';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import { MoreFromAuthor } from 'mastodon/components/more_from_author';
|
|
||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
|
||||||
import { useBlurhash } from 'mastodon/initial_state';
|
|
||||||
|
|
||||||
const IDNA_PREFIX = 'xn--';
|
|
||||||
|
|
||||||
const decodeIDNA = domain => {
|
|
||||||
return domain
|
|
||||||
.split('.')
|
|
||||||
.map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
|
|
||||||
.join('.');
|
|
||||||
};
|
|
||||||
|
|
||||||
const getHostname = url => {
|
|
||||||
const parser = document.createElement('a');
|
|
||||||
parser.href = url;
|
|
||||||
return parser.hostname;
|
|
||||||
};
|
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
|
||||||
|
|
||||||
const handleIframeUrl = (html, url, providerName) => {
|
|
||||||
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
|
||||||
const iframe = document.querySelector('iframe');
|
|
||||||
const startTime = new URL(url).searchParams.get('t')
|
|
||||||
|
|
||||||
if (iframe) {
|
|
||||||
const iframeUrl = new URL(iframe.src)
|
|
||||||
|
|
||||||
iframeUrl.searchParams.set('autoplay', 1)
|
|
||||||
iframeUrl.searchParams.set('auto_play', 1)
|
|
||||||
|
|
||||||
if (startTime && providerName === "YouTube") iframeUrl.searchParams.set('start', startTime)
|
|
||||||
|
|
||||||
iframe.src = iframeUrl.href
|
|
||||||
|
|
||||||
// DOM parser creates html/body elements around original HTML fragment,
|
|
||||||
// so we need to get innerHTML out of the body and not the entire document
|
|
||||||
return document.querySelector('body').innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Card extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
card: ImmutablePropTypes.map,
|
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
|
||||||
sensitive: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
previewLoaded: false,
|
|
||||||
embedded: false,
|
|
||||||
revealed: !this.props.sensitive,
|
|
||||||
};
|
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
|
||||||
if (!is(this.props.card, nextProps.card)) {
|
|
||||||
this.setState({ embedded: false, previewLoaded: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.sensitive !== nextProps.sensitive) {
|
|
||||||
this.setState({ revealed: !nextProps.sensitive });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
window.removeEventListener('resize', this.handleResize);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleEmbedClick = () => {
|
|
||||||
this.setState({ embedded: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleExternalLinkClick = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.node = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleImageLoad = () => {
|
|
||||||
this.setState({ previewLoaded: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleReveal = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.setState({ revealed: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
renderVideo () {
|
|
||||||
const { card } = this.props;
|
|
||||||
const content = { __html: handleIframeUrl(card.get('html'), card.get('url'), card.get('provider_name')) };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={this.setRef}
|
|
||||||
className='status-card__image status-card-video'
|
|
||||||
dangerouslySetInnerHTML={content}
|
|
||||||
style={{ aspectRatio: '16 / 9' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { card } = this.props;
|
|
||||||
const { embedded, revealed } = this.state;
|
|
||||||
|
|
||||||
if (card === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
|
|
||||||
const interactive = card.get('type') === 'video';
|
|
||||||
const language = card.get('language') || '';
|
|
||||||
const largeImage = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive;
|
|
||||||
const showAuthor = !!card.getIn(['authors', 0, 'accountId']);
|
|
||||||
|
|
||||||
const description = (
|
|
||||||
<div className='status-card__content' dir='auto'>
|
|
||||||
<span className='status-card__host'>
|
|
||||||
<span lang={language}>{provider}</span>
|
|
||||||
{card.get('published_at') && <> · <RelativeTimestamp timestamp={card.get('published_at')} /></>}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<strong className='status-card__title' title={card.get('title')} lang={language}>{card.get('title')}</strong>
|
|
||||||
|
|
||||||
{!showAuthor && (card.get('author_name').length > 0 ? <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span> : <span className='status-card__description' lang={language}>{card.get('description')}</span>)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const thumbnailStyle = {
|
|
||||||
visibility: revealed ? null : 'hidden',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (largeImage && card.get('type') === 'video') {
|
|
||||||
thumbnailStyle.aspectRatio = `16 / 9`;
|
|
||||||
} else if (largeImage) {
|
|
||||||
thumbnailStyle.aspectRatio = '1.91 / 1';
|
|
||||||
} else {
|
|
||||||
thumbnailStyle.aspectRatio = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
let embed;
|
|
||||||
|
|
||||||
let canvas = (
|
|
||||||
<Blurhash
|
|
||||||
className={classNames('status-card__image-preview', {
|
|
||||||
'status-card__image-preview--hidden': revealed && this.state.previewLoaded,
|
|
||||||
})}
|
|
||||||
hash={card.get('blurhash')}
|
|
||||||
dummy={!useBlurhash}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const thumbnailDescription = card.get('image_description');
|
|
||||||
const thumbnail = <img src={card.get('image')} alt={thumbnailDescription} title={thumbnailDescription} lang={language} style={thumbnailStyle} onLoad={this.handleImageLoad} className='status-card__image-image' />;
|
|
||||||
|
|
||||||
let spoilerButton = (
|
|
||||||
<button type='button' onClick={this.handleReveal} className='spoiler-button__overlay'>
|
|
||||||
<span className='spoiler-button__overlay__label'>
|
|
||||||
<FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />
|
|
||||||
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
spoilerButton = (
|
|
||||||
<div className={classNames('spoiler-button', { 'spoiler-button--minified': revealed })}>
|
|
||||||
{spoilerButton}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (interactive) {
|
|
||||||
if (embedded) {
|
|
||||||
embed = this.renderVideo();
|
|
||||||
} else {
|
|
||||||
embed = (
|
|
||||||
<div className='status-card__image'>
|
|
||||||
{canvas}
|
|
||||||
{thumbnail}
|
|
||||||
|
|
||||||
{revealed ? (
|
|
||||||
<div className='status-card__actions' onClick={this.handleEmbedClick} role='none'>
|
|
||||||
<div>
|
|
||||||
<button type='button' onClick={this.handleEmbedClick}><Icon id='play' icon={PlayArrowIcon} /></button>
|
|
||||||
<a href={card.get('url')} onClick={this.handleExternalLinkClick} target='_blank' rel='noopener'><Icon id='external-link' icon={OpenInNewIcon} /></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : spoilerButton}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('status-card', { expanded: largeImage })} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
|
|
||||||
{embed}
|
|
||||||
<a href={card.get('url')} target='_blank' rel='noopener'>{description}</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (card.get('image')) {
|
|
||||||
embed = (
|
|
||||||
<div className='status-card__image'>
|
|
||||||
{canvas}
|
|
||||||
{thumbnail}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
embed = (
|
|
||||||
<div className='status-card__image'>
|
|
||||||
<Icon id='file-text' icon={DescriptionIcon} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<a href={card.get('url')} className={classNames('status-card', { expanded: largeImage, bottomless: showAuthor })} target='_blank' rel='noopener' ref={this.setRef}>
|
|
||||||
{embed}
|
|
||||||
{description}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{showAuthor && <MoreFromAuthor accountId={card.getIn(['authors', 0, 'accountId'])} />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
316
app/javascript/mastodon/features/status/components/card.tsx
Normal file
316
app/javascript/mastodon/features/status/components/card.tsx
Normal file
|
|
@ -0,0 +1,316 @@
|
||||||
|
import punycode from 'node:punycode';
|
||||||
|
|
||||||
|
import { useCallback, useId, useState } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import DescriptionIcon from '@/material-icons/400-24px/description-fill.svg?react';
|
||||||
|
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
|
||||||
|
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react';
|
||||||
|
import { Blurhash } from 'mastodon/components/blurhash';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { MoreFromAuthor } from 'mastodon/components/more_from_author';
|
||||||
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
|
import { useBlurhash } from 'mastodon/initial_state';
|
||||||
|
import type { Card as CardType } from 'mastodon/models/status';
|
||||||
|
|
||||||
|
const IDNA_PREFIX = 'xn--';
|
||||||
|
|
||||||
|
const decodeIDNA = (domain: string) => {
|
||||||
|
return domain
|
||||||
|
.split('.')
|
||||||
|
.map((part) =>
|
||||||
|
part.startsWith(IDNA_PREFIX)
|
||||||
|
? punycode.decode(part.slice(IDNA_PREFIX.length))
|
||||||
|
: part,
|
||||||
|
)
|
||||||
|
.join('.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHostname = (url: string) => {
|
||||||
|
const parser = document.createElement('a');
|
||||||
|
parser.href = url;
|
||||||
|
return parser.hostname;
|
||||||
|
};
|
||||||
|
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
|
const handleIframeUrl = (html: string, url: string, providerName: string) => {
|
||||||
|
const document = domParser.parseFromString(html, 'text/html').documentElement;
|
||||||
|
const iframe = document.querySelector('iframe');
|
||||||
|
const startTime = new URL(url).searchParams.get('t');
|
||||||
|
|
||||||
|
if (iframe) {
|
||||||
|
const iframeUrl = new URL(iframe.src);
|
||||||
|
|
||||||
|
iframeUrl.searchParams.set('autoplay', '1');
|
||||||
|
iframeUrl.searchParams.set('auto_play', '1');
|
||||||
|
|
||||||
|
if (startTime && providerName === 'YouTube')
|
||||||
|
iframeUrl.searchParams.set('start', startTime);
|
||||||
|
|
||||||
|
iframe.src = iframeUrl.href;
|
||||||
|
|
||||||
|
// DOM parser creates html/body elements around original HTML fragment,
|
||||||
|
// so we need to get innerHTML out of the body and not the entire document
|
||||||
|
return document.querySelector('body')?.innerHTML ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
card: CardType | null;
|
||||||
|
sensitive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardVideo: React.FC<Pick<CardProps, 'card'>> = ({ card }) => (
|
||||||
|
<div
|
||||||
|
className='status-card__image status-card-video'
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: card
|
||||||
|
? handleIframeUrl(
|
||||||
|
card.get('html'),
|
||||||
|
card.get('url'),
|
||||||
|
card.get('provider_name'),
|
||||||
|
)
|
||||||
|
: '',
|
||||||
|
}}
|
||||||
|
style={{ aspectRatio: '16 / 9' }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Card: React.FC<CardProps> = ({ card, sensitive }) => {
|
||||||
|
const [previewLoaded, setPreviewLoaded] = useState(false);
|
||||||
|
const [embedded, setEmbedded] = useState(false);
|
||||||
|
const [revealed, setRevealed] = useState(!sensitive);
|
||||||
|
|
||||||
|
const handleEmbedClick = useCallback(() => {
|
||||||
|
setEmbedded(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleExternalLinkClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleImageLoad = useCallback(() => {
|
||||||
|
setPreviewLoaded(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleReveal = useCallback((e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setRevealed(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const spoilerButtonId = useId();
|
||||||
|
|
||||||
|
if (card === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider =
|
||||||
|
card.get('provider_name').length === 0
|
||||||
|
? decodeIDNA(getHostname(card.get('url')))
|
||||||
|
: card.get('provider_name');
|
||||||
|
const interactive = card.get('type') === 'video';
|
||||||
|
const language = card.get('language') || '';
|
||||||
|
const largeImage =
|
||||||
|
(card.get('image').length > 0 && card.get('width') > card.get('height')) ||
|
||||||
|
interactive;
|
||||||
|
const showAuthor = !!card.getIn(['authors', 0, 'accountId']);
|
||||||
|
|
||||||
|
const description = (
|
||||||
|
<div className='status-card__content' dir='auto'>
|
||||||
|
<span className='status-card__host'>
|
||||||
|
<span lang={language}>{provider}</span>
|
||||||
|
{card.get('published_at') && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
· <RelativeTimestamp timestamp={card.get('published_at')} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<strong
|
||||||
|
className='status-card__title'
|
||||||
|
title={card.get('title')}
|
||||||
|
lang={language}
|
||||||
|
>
|
||||||
|
{card.get('title')}
|
||||||
|
</strong>
|
||||||
|
|
||||||
|
{!showAuthor &&
|
||||||
|
(card.get('author_name').length > 0 ? (
|
||||||
|
<span className='status-card__author'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='link_preview.author'
|
||||||
|
defaultMessage='By {name}'
|
||||||
|
values={{ name: <strong>{card.get('author_name')}</strong> }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className='status-card__description' lang={language}>
|
||||||
|
{card.get('description')}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const thumbnailStyle: React.CSSProperties = {
|
||||||
|
visibility: revealed ? undefined : 'hidden',
|
||||||
|
aspectRatio: '1',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (largeImage && card.get('type') === 'video') {
|
||||||
|
thumbnailStyle.aspectRatio = `16 / 9`;
|
||||||
|
} else if (largeImage) {
|
||||||
|
thumbnailStyle.aspectRatio = '1.91 / 1';
|
||||||
|
}
|
||||||
|
|
||||||
|
let embed;
|
||||||
|
|
||||||
|
const canvas = (
|
||||||
|
<Blurhash
|
||||||
|
className={classNames('status-card__image-preview', {
|
||||||
|
'status-card__image-preview--hidden': revealed && previewLoaded,
|
||||||
|
})}
|
||||||
|
hash={card.get('blurhash')}
|
||||||
|
dummy={!useBlurhash}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const thumbnailDescription = card.get('image_description');
|
||||||
|
const thumbnail = (
|
||||||
|
<img
|
||||||
|
src={card.get('image')}
|
||||||
|
alt={thumbnailDescription}
|
||||||
|
title={thumbnailDescription}
|
||||||
|
lang={language}
|
||||||
|
style={thumbnailStyle}
|
||||||
|
onLoad={handleImageLoad}
|
||||||
|
className='status-card__image-image'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const spoilerButton = (
|
||||||
|
<div
|
||||||
|
className={classNames('spoiler-button', {
|
||||||
|
'spoiler-button--minified': revealed,
|
||||||
|
})}
|
||||||
|
id={spoilerButtonId}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={handleReveal}
|
||||||
|
className='spoiler-button__overlay'
|
||||||
|
>
|
||||||
|
<span className='spoiler-button__overlay__label'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.sensitive_warning'
|
||||||
|
defaultMessage='Sensitive content'
|
||||||
|
/>
|
||||||
|
<span className='spoiler-button__overlay__action'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.media.show'
|
||||||
|
defaultMessage='Click to show'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (interactive) {
|
||||||
|
if (embedded) {
|
||||||
|
embed = <CardVideo card={card} />;
|
||||||
|
} else {
|
||||||
|
embed = (
|
||||||
|
<div className='status-card__image'>
|
||||||
|
{canvas}
|
||||||
|
{thumbnail}
|
||||||
|
|
||||||
|
{revealed ? (
|
||||||
|
<div
|
||||||
|
className='status-card__actions'
|
||||||
|
onClick={handleEmbedClick}
|
||||||
|
role='none'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<button type='button' onClick={handleEmbedClick}>
|
||||||
|
<Icon id='play' icon={PlayArrowIcon} />
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href={card.get('url')}
|
||||||
|
onClick={handleExternalLinkClick}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener'
|
||||||
|
>
|
||||||
|
<Icon id='external-link' icon={OpenInNewIcon} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
spoilerButton
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('status-card', { expanded: largeImage })}>
|
||||||
|
{embed}
|
||||||
|
<a
|
||||||
|
href={card.get('url')}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener'
|
||||||
|
onClick={revealed ? undefined : handleReveal}
|
||||||
|
aria-describedby={revealed ? undefined : spoilerButtonId}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (card.get('image')) {
|
||||||
|
embed = (
|
||||||
|
<div className='status-card__image'>
|
||||||
|
{canvas}
|
||||||
|
{thumbnail}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
embed = (
|
||||||
|
<div className='status-card__image'>
|
||||||
|
<Icon id='file-text' icon={DescriptionIcon} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
href={card.get('url')}
|
||||||
|
className={classNames('status-card', {
|
||||||
|
expanded: largeImage,
|
||||||
|
bottomless: showAuthor,
|
||||||
|
})}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener'
|
||||||
|
>
|
||||||
|
{embed}
|
||||||
|
{description}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{showAuthor && (
|
||||||
|
<MoreFromAuthor
|
||||||
|
accountId={card.getIn(['authors', 0, 'accountId']) as string}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default Card;
|
||||||
|
|
@ -262,8 +262,8 @@ export const DetailedStatus: React.FC<{
|
||||||
} else if (status.get('card') && !status.get('quote')) {
|
} else if (status.get('card') && !status.get('quote')) {
|
||||||
media = (
|
media = (
|
||||||
<Card
|
<Card
|
||||||
|
key={`${status.get('id')}-${status.get('edited_at')}`}
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
onOpenMedia={onOpenMedia}
|
|
||||||
card={status.get('card')}
|
card={status.get('card')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user