You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

214 lines
5.8 KiB

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