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.

183 lines
5.0 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. export default class Card extends React.PureComponent {
  27. static propTypes = {
  28. card: ImmutablePropTypes.map,
  29. maxDescription: PropTypes.number,
  30. onOpenMedia: PropTypes.func.isRequired,
  31. };
  32. static defaultProps = {
  33. maxDescription: 50,
  34. };
  35. state = {
  36. width: 280,
  37. embedded: false,
  38. };
  39. componentWillReceiveProps (nextProps) {
  40. if (this.props.card !== nextProps.card) {
  41. this.setState({ embedded: false });
  42. }
  43. }
  44. handlePhotoClick = () => {
  45. const { card, onOpenMedia } = this.props;
  46. onOpenMedia(
  47. Immutable.fromJS([
  48. {
  49. type: 'image',
  50. url: card.get('embed_url'),
  51. description: card.get('title'),
  52. meta: {
  53. original: {
  54. width: card.get('width'),
  55. height: card.get('height'),
  56. },
  57. },
  58. },
  59. ]),
  60. 0
  61. );
  62. };
  63. handleEmbedClick = () => {
  64. const { card } = this.props;
  65. if (card.get('type') === 'photo') {
  66. this.handlePhotoClick();
  67. } else {
  68. this.setState({ embedded: true });
  69. }
  70. }
  71. setRef = c => {
  72. if (c) {
  73. this.setState({ width: c.offsetWidth });
  74. }
  75. }
  76. renderVideo () {
  77. const { card } = this.props;
  78. const content = { __html: card.get('html') };
  79. const { width } = this.state;
  80. const ratio = card.get('width') / card.get('height');
  81. const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
  82. return (
  83. <div
  84. ref={this.setRef}
  85. className='status-card__image status-card-video'
  86. dangerouslySetInnerHTML={content}
  87. style={{ height }}
  88. />
  89. );
  90. }
  91. render () {
  92. const { card, maxDescription } = this.props;
  93. const { width, embedded } = this.state;
  94. if (card === null) {
  95. return null;
  96. }
  97. const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
  98. const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width) || card.get('type') !== 'link';
  99. const className = classnames('status-card', { horizontal });
  100. const interactive = card.get('type') !== 'link';
  101. 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>;
  102. const ratio = card.get('width') / card.get('height');
  103. const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
  104. const description = (
  105. <div className='status-card__content'>
  106. {title}
  107. {!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
  108. <span className='status-card__host'>{provider}</span>
  109. </div>
  110. );
  111. let embed = '';
  112. let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />;
  113. if (interactive) {
  114. if (embedded) {
  115. embed = this.renderVideo();
  116. } else {
  117. let iconVariant = 'play';
  118. if (card.get('type') === 'photo') {
  119. iconVariant = 'search-plus';
  120. }
  121. embed = (
  122. <div className='status-card__image'>
  123. {thumbnail}
  124. <div className='status-card__actions'>
  125. <div>
  126. <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button>
  127. <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>
  128. </div>
  129. </div>
  130. </div>
  131. );
  132. }
  133. return (
  134. <div className={className} ref={this.setRef}>
  135. {embed}
  136. {description}
  137. </div>
  138. );
  139. } else if (card.get('image')) {
  140. embed = (
  141. <div className='status-card__image'>
  142. {thumbnail}
  143. </div>
  144. );
  145. }
  146. return (
  147. <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
  148. {embed}
  149. {description}
  150. </a>
  151. );
  152. }
  153. }