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.

125 lines
3.0 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import ImmutablePropTypes from 'react-immutable-proptypes';
  4. import punycode from 'punycode';
  5. import classnames from 'classnames';
  6. const IDNA_PREFIX = 'xn--';
  7. const decodeIDNA = domain => {
  8. return domain
  9. .split('.')
  10. .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
  11. .join('.');
  12. };
  13. const getHostname = url => {
  14. const parser = document.createElement('a');
  15. parser.href = url;
  16. return parser.hostname;
  17. };
  18. export default class Card extends React.PureComponent {
  19. static propTypes = {
  20. card: ImmutablePropTypes.map,
  21. maxDescription: PropTypes.number,
  22. };
  23. static defaultProps = {
  24. maxDescription: 50,
  25. };
  26. state = {
  27. width: 0,
  28. };
  29. renderLink () {
  30. const { card, maxDescription } = this.props;
  31. let image = '';
  32. let provider = card.get('provider_name');
  33. if (card.get('image')) {
  34. image = (
  35. <div className='status-card__image'>
  36. <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} />
  37. </div>
  38. );
  39. }
  40. if (provider.length < 1) {
  41. provider = decodeIDNA(getHostname(card.get('url')));
  42. }
  43. const className = classnames('status-card', {
  44. 'horizontal': card.get('width') > card.get('height'),
  45. });
  46. return (
  47. <a href={card.get('url')} className={className} target='_blank' rel='noopener'>
  48. {image}
  49. <div className='status-card__content'>
  50. <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>
  51. <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p>
  52. <span className='status-card__host'>{provider}</span>
  53. </div>
  54. </a>
  55. );
  56. }
  57. renderPhoto () {
  58. const { card } = this.props;
  59. return (
  60. <a href={card.get('url')} className='status-card-photo' target='_blank' rel='noopener'>
  61. <img src={card.get('url')} alt={card.get('title')} width={card.get('width')} height={card.get('height')} />
  62. </a>
  63. );
  64. }
  65. setRef = c => {
  66. if (c) {
  67. this.setState({ width: c.offsetWidth });
  68. }
  69. }
  70. renderVideo () {
  71. const { card } = this.props;
  72. const content = { __html: card.get('html') };
  73. const { width } = this.state;
  74. const ratio = card.get('width') / card.get('height');
  75. const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
  76. return (
  77. <div
  78. ref={this.setRef}
  79. className='status-card-video'
  80. dangerouslySetInnerHTML={content}
  81. style={{ height }}
  82. />
  83. );
  84. }
  85. render () {
  86. const { card } = this.props;
  87. if (card === null) {
  88. return null;
  89. }
  90. switch(card.get('type')) {
  91. case 'link':
  92. return this.renderLink();
  93. case 'photo':
  94. return this.renderPhoto();
  95. case 'video':
  96. return this.renderVideo();
  97. case 'rich':
  98. default:
  99. return null;
  100. }
  101. }
  102. }