]> cat aescling's git repositories - mastodon.git/blob - app/javascript/mastodon/features/status/components/card.js
Revert Font Awesome 5 upgrade (#8810)
[mastodon.git] / app / javascript / mastodon / features / status / components / card.js
1 import React from 'react';
2 import PropTypes from 'prop-types';
3 import Immutable from 'immutable';
4 import ImmutablePropTypes from 'react-immutable-proptypes';
5 import punycode from 'punycode';
6 import classnames from 'classnames';
7
8 const IDNA_PREFIX = 'xn--';
9
10 const decodeIDNA = domain => {
11 return domain
12 .split('.')
13 .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
14 .join('.');
15 };
16
17 const getHostname = url => {
18 const parser = document.createElement('a');
19 parser.href = url;
20 return parser.hostname;
21 };
22
23 const trim = (text, len) => {
24 const cut = text.indexOf(' ', len);
25
26 if (cut === -1) {
27 return text;
28 }
29
30 return text.substring(0, cut) + (text.length > len ? '…' : '');
31 };
32
33 const domParser = new DOMParser();
34
35 const addAutoPlay = html => {
36 const document = domParser.parseFromString(html, 'text/html').documentElement;
37 const iframe = document.querySelector('iframe');
38
39 if (iframe) {
40 if (iframe.src.indexOf('?') !== -1) {
41 iframe.src += '&';
42 } else {
43 iframe.src += '?';
44 }
45
46 iframe.src += 'autoplay=1&auto_play=1';
47
48 // DOM parser creates html/body elements around original HTML fragment,
49 // so we need to get innerHTML out of the body and not the entire document
50 return document.querySelector('body').innerHTML;
51 }
52
53 return html;
54 };
55
56 export default class Card extends React.PureComponent {
57
58 static propTypes = {
59 card: ImmutablePropTypes.map,
60 maxDescription: PropTypes.number,
61 onOpenMedia: PropTypes.func.isRequired,
62 };
63
64 static defaultProps = {
65 maxDescription: 50,
66 };
67
68 state = {
69 width: 280,
70 embedded: false,
71 };
72
73 componentWillReceiveProps (nextProps) {
74 if (this.props.card !== nextProps.card) {
75 this.setState({ embedded: false });
76 }
77 }
78
79 handlePhotoClick = () => {
80 const { card, onOpenMedia } = this.props;
81
82 onOpenMedia(
83 Immutable.fromJS([
84 {
85 type: 'image',
86 url: card.get('embed_url'),
87 description: card.get('title'),
88 meta: {
89 original: {
90 width: card.get('width'),
91 height: card.get('height'),
92 },
93 },
94 },
95 ]),
96 0
97 );
98 };
99
100 handleEmbedClick = () => {
101 const { card } = this.props;
102
103 if (card.get('type') === 'photo') {
104 this.handlePhotoClick();
105 } else {
106 this.setState({ embedded: true });
107 }
108 }
109
110 setRef = c => {
111 if (c) {
112 this.setState({ width: c.offsetWidth });
113 }
114 }
115
116 renderVideo () {
117 const { card } = this.props;
118 const content = { __html: addAutoPlay(card.get('html')) };
119 const { width } = this.state;
120 const ratio = card.get('width') / card.get('height');
121 const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
122
123 return (
124 <div
125 ref={this.setRef}
126 className='status-card__image status-card-video'
127 dangerouslySetInnerHTML={content}
128 style={{ height }}
129 />
130 );
131 }
132
133 render () {
134 const { card, maxDescription } = this.props;
135 const { width, embedded } = this.state;
136
137 if (card === null) {
138 return null;
139 }
140
141 const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
142 const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width) || card.get('type') !== 'link';
143 const className = classnames('status-card', { horizontal });
144 const interactive = card.get('type') !== 'link';
145 const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
146 const ratio = card.get('width') / card.get('height');
147 const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
148
149 const description = (
150 <div className='status-card__content'>
151 {title}
152 {!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
153 <span className='status-card__host'>{provider}</span>
154 </div>
155 );
156
157 let embed = '';
158 let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />;
159
160 if (interactive) {
161 if (embedded) {
162 embed = this.renderVideo();
163 } else {
164 let iconVariant = 'play';
165
166 if (card.get('type') === 'photo') {
167 iconVariant = 'search-plus';
168 }
169
170 embed = (
171 <div className='status-card__image'>
172 {thumbnail}
173
174 <div className='status-card__actions'>
175 <div>
176 <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button>
177 <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>
178 </div>
179 </div>
180 </div>
181 );
182 }
183
184 return (
185 <div className={className} ref={this.setRef}>
186 {embed}
187 {description}
188 </div>
189 );
190 } else if (card.get('image')) {
191 embed = (
192 <div className='status-card__image'>
193 {thumbnail}
194 </div>
195 );
196 }
197
198 return (
199 <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
200 {embed}
201 {description}
202 </a>
203 );
204 }
205
206 }
This page took 0.26949 seconds and 4 git commands to generate.