2023-05-24 01:15:17 +10:00
import punycode from 'punycode' ;
2017-09-14 11:39:10 +10:00
import PropTypes from 'prop-types' ;
2023-05-24 01:15:17 +10:00
import { PureComponent } from 'react' ;
2020-06-07 01:41:56 +10:00
import { FormattedMessage } from 'react-intl' ;
2023-05-24 01:15:17 +10:00
2023-07-30 11:35:17 +10:00
import classNames from 'classnames' ;
2024-05-29 09:34:33 +10:00
import { Link } from 'react-router-dom' ;
2023-05-24 01:15:17 +10:00
import Immutable from 'immutable' ;
import ImmutablePropTypes from 'react-immutable-proptypes' ;
2024-01-16 21:27:26 +11:00
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' ;
2024-05-29 09:34:33 +10:00
import { Avatar } from 'mastodon/components/avatar' ;
2023-05-24 01:15:17 +10:00
import { Blurhash } from 'mastodon/components/blurhash' ;
2023-05-09 11:11:56 +10:00
import { Icon } from 'mastodon/components/icon' ;
2023-07-24 21:47:28 +10:00
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp' ;
2020-06-07 01:41:56 +10:00
import { useBlurhash } from 'mastodon/initial_state' ;
2019-08-06 20:09:28 +10:00
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 ( '.' ) ;
} ;
2017-01-20 11:00:14 +11:00
const getHostname = url => {
const parser = document . createElement ( 'a' ) ;
parser . href = url ;
return parser . hostname ;
} ;
2018-02-16 09:05:12 +11:00
const domParser = new DOMParser ( ) ;
const addAutoPlay = html => {
const document = domParser . parseFromString ( html , 'text/html' ) . documentElement ;
const iframe = document . querySelector ( 'iframe' ) ;
if ( iframe ) {
if ( iframe . src . indexOf ( '?' ) !== - 1 ) {
iframe . src += '&' ;
} else {
iframe . src += '?' ;
}
iframe . src += 'autoplay=1&auto_play=1' ;
// 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 ;
} ;
2024-05-29 09:34:33 +10:00
const MoreFromAuthor = ( { author } ) => (
< div className = 'more-from-author' >
< svg viewBox = '0 0 79 79' className = 'logo logo--icon' role = 'img' >
< use xlinkHref = '#logo-symbol-icon' / >
< / svg >
< FormattedMessage id = 'link_preview.more_from_author' defaultMessage = 'More from {name}' values = { { name : < Link to = { ` /@ ${ author . get ( 'acct' ) } ` } > < Avatar account = { author } size = { 16 } / > { author . get ( 'display_name' ) } < / Link > } } / >
< / div >
) ;
MoreFromAuthor . propTypes = {
author : ImmutablePropTypes . map ,
} ;
2023-05-23 18:52:27 +10:00
export default class Card extends PureComponent {
2017-01-20 11:00:14 +11:00
2017-05-12 22:44:10 +10:00
static propTypes = {
2017-05-21 01:31:47 +10:00
card : ImmutablePropTypes . map ,
2017-11-26 01:41:45 +11:00
onOpenMedia : PropTypes . func . isRequired ,
2020-06-07 01:41:56 +10:00
sensitive : PropTypes . bool ,
2017-09-14 11:39:10 +10:00
} ;
2017-10-08 11:34:49 +11:00
state = {
2020-06-07 01:41:56 +10:00
previewLoaded : false ,
2018-02-15 17:04:28 +11:00
embedded : false ,
2020-06-07 01:41:56 +10:00
revealed : ! this . props . sensitive ,
2017-10-08 11:34:49 +11:00
} ;
2023-05-10 17:05:32 +10:00
UNSAFE _componentWillReceiveProps ( nextProps ) {
2018-11-13 08:07:31 +11:00
if ( ! Immutable . is ( this . props . card , nextProps . card ) ) {
2020-06-07 01:41:56 +10:00
this . setState ( { embedded : false , previewLoaded : false } ) ;
}
2023-07-30 11:35:17 +10:00
2020-06-07 01:41:56 +10:00
if ( this . props . sensitive !== nextProps . sensitive ) {
this . setState ( { revealed : ! nextProps . sensitive } ) ;
}
}
componentDidMount ( ) {
2020-06-24 18:25:32 +10:00
window . addEventListener ( 'resize' , this . handleResize , { passive : true } ) ;
2020-06-07 01:41:56 +10:00
}
2020-06-24 18:25:32 +10:00
componentWillUnmount ( ) {
window . removeEventListener ( 'resize' , this . handleResize ) ;
}
2018-02-15 17:04:28 +11:00
handleEmbedClick = ( ) => {
2023-07-30 11:35:17 +10:00
this . setState ( { embedded : true } ) ;
2023-01-30 11:45:35 +11:00
} ;
2017-04-27 22:42:22 +10:00
2024-03-01 00:54:02 +11:00
handleExternalLinkClick = ( e ) => {
e . stopPropagation ( ) ;
} ;
2017-10-08 11:34:49 +11:00
setRef = c => {
2020-06-24 18:25:32 +10:00
this . node = c ;
2023-01-30 11:45:35 +11:00
} ;
2017-10-08 11:34:49 +11:00
2020-06-07 01:41:56 +10:00
handleImageLoad = ( ) => {
this . setState ( { previewLoaded : true } ) ;
2023-01-30 11:45:35 +11:00
} ;
2020-06-07 01:41:56 +10:00
2020-06-26 06:42:01 +10:00
handleReveal = e => {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
2020-06-07 01:41:56 +10:00
this . setState ( { revealed : true } ) ;
2023-01-30 11:45:35 +11:00
} ;
2020-06-07 01:41:56 +10:00
2017-04-27 22:42:22 +10:00
renderVideo ( ) {
2023-07-30 11:35:17 +10:00
const { card } = this . props ;
const content = { _ _html : addAutoPlay ( card . get ( 'html' ) ) } ;
2017-04-27 22:42:22 +10:00
return (
< div
2017-10-08 11:34:49 +11:00
ref = { this . setRef }
2018-02-15 17:04:28 +11:00
className = 'status-card__image status-card-video'
2017-04-27 22:42:22 +10:00
dangerouslySetInnerHTML = { content }
2023-07-30 11:35:17 +10:00
style = { { aspectRatio : '16 / 9' } }
2017-04-27 22:42:22 +10:00
/ >
) ;
}
render ( ) {
2023-07-24 21:47:28 +10:00
const { card } = this . props ;
2023-05-02 21:58:48 +10:00
const { embedded , revealed } = this . state ;
2017-04-27 22:42:22 +10:00
if ( card === null ) {
return null ;
}
2018-02-15 17:04:28 +11:00
const provider = card . get ( 'provider_name' ) . length === 0 ? decodeIDNA ( getHostname ( card . get ( 'url' ) ) ) : card . get ( 'provider_name' ) ;
2023-07-30 11:35:17 +10:00
const interactive = card . get ( 'type' ) === 'video' ;
2023-02-25 06:04:38 +11:00
const language = card . get ( 'language' ) || '' ;
2023-07-30 11:35:17 +10:00
const largeImage = ( card . get ( 'image' ) ? . length > 0 && card . get ( 'width' ) > card . get ( 'height' ) ) || interactive ;
2024-05-29 09:34:33 +10:00
const showAuthor = ! ! card . get ( 'author_account' ) ;
2018-02-15 17:04:28 +11:00
const description = (
2023-07-25 07:01:31 +10:00
< div className = 'status-card__content' >
< span className = 'status-card__host' >
< span lang = { language } > { provider } < / span >
{ card . get ( 'published_at' ) && < > · < RelativeTimestamp timestamp = { card . get ( 'published_at' ) } / > < / > }
2023-07-30 11:35:17 +10:00
< / span >
2023-07-25 07:01:31 +10:00
< strong className = 'status-card__title' title = { card . get ( 'title' ) } lang = { language } > { card . get ( 'title' ) } < / strong >
2023-07-30 11:35:17 +10:00
2024-05-29 09:34:33 +10:00
{ ! 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 > ) }
2018-02-15 17:04:28 +11:00
< / div >
) ;
2023-05-02 21:58:48 +10:00
const thumbnailStyle = {
2023-07-24 21:47:28 +10:00
visibility : revealed ? null : 'hidden' ,
2023-05-02 21:58:48 +10:00
} ;
2023-07-30 11:35:17 +10:00
if ( largeImage && card . get ( 'type' ) === 'video' ) {
thumbnailStyle . aspectRatio = ` 16 / 9 ` ;
} else if ( largeImage ) {
thumbnailStyle . aspectRatio = '1.91 / 1' ;
} else {
thumbnailStyle . aspectRatio = 1 ;
}
2023-07-24 21:47:28 +10:00
let embed ;
2023-05-02 21:58:48 +10:00
2020-07-09 21:01:30 +10:00
let canvas = (
< Blurhash
2023-07-30 11:35:17 +10:00
className = { classNames ( 'status-card__image-preview' , {
2020-07-09 21:01:30 +10:00
'status-card__image-preview--hidden' : revealed && this . state . previewLoaded ,
} ) }
hash = { card . get ( 'blurhash' ) }
dummy = { ! useBlurhash }
/ >
) ;
2023-07-24 21:47:28 +10:00
2023-08-03 23:41:51 +10:00
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' / > ;
2023-07-24 21:47:28 +10:00
2020-06-07 01:41:56 +10:00
let spoilerButton = (
< button type = 'button' onClick = { this . handleReveal } className = 'spoiler-button__overlay' >
2023-07-25 06:04:38 +10:00
< 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 >
2020-06-07 01:41:56 +10:00
< / button >
) ;
2023-07-24 21:47:28 +10:00
2020-06-07 01:41:56 +10:00
spoilerButton = (
2023-07-30 11:35:17 +10:00
< div className = { classNames ( 'spoiler-button' , { 'spoiler-button--minified' : revealed } ) } >
2020-06-07 01:41:56 +10:00
{ spoilerButton }
< / div >
) ;
2018-02-15 17:04:28 +11:00
if ( interactive ) {
if ( embedded ) {
embed = this . renderVideo ( ) ;
} else {
embed = (
< div className = 'status-card__image' >
2020-06-07 01:41:56 +10:00
{ canvas }
2018-02-15 17:04:28 +11:00
{ thumbnail }
2023-07-30 11:35:17 +10:00
{ revealed ? (
< div className = 'status-card__actions' onClick = { this . handleEmbedClick } role = 'none' >
2020-06-07 01:41:56 +10:00
< div >
2023-10-25 04:45:08 +11:00
< button type = 'button' onClick = { this . handleEmbedClick } > < Icon id = 'play' icon = { PlayArrowIcon } / > < / button >
2024-03-01 00:54:02 +11:00
< a href = { card . get ( 'url' ) } onClick = { this . handleExternalLinkClick } target = '_blank' rel = 'noopener noreferrer' > < Icon id = 'external-link' icon = { OpenInNewIcon } / > < / a >
2020-06-07 01:41:56 +10:00
< / div >
2018-02-15 17:04:28 +11:00
< / div >
2023-07-30 11:35:17 +10:00
) : spoilerButton }
2018-02-15 17:04:28 +11:00
< / div >
) ;
}
return (
2023-07-30 11:35:17 +10:00
< div className = { classNames ( 'status-card' , { expanded : largeImage } ) } ref = { this . setRef } onClick = { revealed ? null : this . handleReveal } role = { revealed ? 'button' : null } >
2018-02-15 17:04:28 +11:00
{ embed }
2023-07-24 21:47:28 +10:00
< a href = { card . get ( 'url' ) } target = '_blank' rel = 'noopener noreferrer' > { description } < / a >
2018-02-15 17:04:28 +11:00
< / div >
) ;
} else if ( card . get ( 'image' ) ) {
embed = (
< div className = 'status-card__image' >
2020-06-07 01:41:56 +10:00
{ canvas }
2018-02-15 17:04:28 +11:00
{ thumbnail }
< / div >
) ;
2019-01-04 22:44:46 +11:00
} else {
embed = (
2023-07-30 11:35:17 +10:00
< div className = 'status-card__image' >
2023-10-25 04:45:08 +11:00
< Icon id = 'file-text' icon = { DescriptionIcon } / >
2019-01-04 22:44:46 +11:00
< / div >
) ;
2017-04-27 22:42:22 +10:00
}
2018-02-15 17:04:28 +11:00
return (
2024-05-29 09:34:33 +10:00
< >
< a href = { card . get ( 'url' ) } className = { classNames ( 'status-card' , { expanded : largeImage , bottomless : showAuthor } ) } target = '_blank' rel = 'noopener noreferrer' ref = { this . setRef } >
{ embed }
{ description }
< / a >
{ showAuthor && < MoreFromAuthor author = { card . get ( 'author_account' ) } / > }
< / >
2018-02-15 17:04:28 +11:00
) ;
2017-04-27 22:42:22 +10:00
}
2017-05-21 01:31:47 +10:00
2017-04-22 04:05:35 +10:00
}