about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/features/status/components/card.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/javascript/flavours/glitch/features/status/components/card.js')
-rw-r--r--app/javascript/flavours/glitch/features/status/components/card.js209
1 files changed, 209 insertions, 0 deletions
diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js
new file mode 100644
index 000000000..108b6e3b2
--- /dev/null
+++ b/app/javascript/flavours/glitch/features/status/components/card.js
@@ -0,0 +1,209 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Immutable from 'immutable';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import punycode from 'punycode';
+import classnames from 'classnames';
+import { decode as decodeIDNA } from 'flavours/glitch/util/idna';
+
+const getHostname = url => {
+  const parser = document.createElement('a');
+  parser.href = url;
+  return parser.hostname;
+};
+
+const trim = (text, len) => {
+  const cut = text.indexOf(' ', len);
+
+  if (cut === -1) {
+    return text;
+  }
+
+  return text.substring(0, cut) + (text.length > len ? '…' : '');
+};
+
+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;
+};
+
+export default class Card extends React.PureComponent {
+
+  static propTypes = {
+    card: ImmutablePropTypes.map,
+    maxDescription: PropTypes.number,
+    onOpenMedia: PropTypes.func.isRequired,
+    compact: PropTypes.bool,
+    defaultWidth: PropTypes.number,
+    cacheWidth: PropTypes.func,
+  };
+
+  static defaultProps = {
+    maxDescription: 50,
+    compact: false,
+  };
+
+  state = {
+    width: this.props.defaultWidth || 280,
+    embedded: false,
+  };
+
+  componentWillReceiveProps (nextProps) {
+    if (!Immutable.is(this.props.card, nextProps.card)) {
+      this.setState({ embedded: false });
+    }
+  }
+
+  handlePhotoClick = () => {
+    const { card, onOpenMedia } = this.props;
+
+    onOpenMedia(
+      Immutable.fromJS([
+        {
+          type: 'image',
+          url: card.get('embed_url'),
+          description: card.get('title'),
+          meta: {
+            original: {
+              width: card.get('width'),
+              height: card.get('height'),
+            },
+          },
+        },
+      ]),
+      0
+    );
+  };
+
+  handleEmbedClick = () => {
+    const { card } = this.props;
+
+    if (card.get('type') === 'photo') {
+      this.handlePhotoClick();
+    } else {
+      this.setState({ embedded: true });
+    }
+  }
+
+  setRef = c => {
+    if (c) {
+      if (this.props.cacheWidth) this.props.cacheWidth(c.offsetWidth);
+      this.setState({ width: c.offsetWidth });
+    }
+  }
+
+  renderVideo () {
+    const { card }  = this.props;
+    const content   = { __html: addAutoPlay(card.get('html')) };
+    const { width } = this.state;
+    const ratio     = card.get('width') / card.get('height');
+    const height    = width / ratio;
+
+    return (
+      <div
+        ref={this.setRef}
+        className='status-card__image status-card-video'
+        dangerouslySetInnerHTML={content}
+        style={{ height }}
+      />
+    );
+  }
+
+  render () {
+    const { card, maxDescription, compact, defaultWidth } = this.props;
+    const { width, embedded } = this.state;
+
+    if (card === null) {
+      return null;
+    }
+
+    const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
+    const horizontal  = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
+    const interactive = card.get('type') !== 'link';
+    const className   = classnames('status-card', { horizontal, compact, interactive });
+    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>;
+    const ratio       = card.get('width') / card.get('height');
+    const height      = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio);
+
+    const description = (
+      <div className='status-card__content'>
+        {title}
+        {!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
+        <span className='status-card__host'>{provider}</span>
+      </div>
+    );
+
+    let embed     = '';
+    let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />;
+
+    if (interactive) {
+      if (embedded) {
+        embed = this.renderVideo();
+      } else {
+        let iconVariant = 'play';
+
+        if (card.get('type') === 'photo') {
+          iconVariant = 'search-plus';
+        }
+
+        embed = (
+          <div className='status-card__image'>
+            {thumbnail}
+
+            <div className='status-card__actions'>
+              <div>
+                <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button>
+                {horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>}
+              </div>
+            </div>
+          </div>
+        );
+      }
+
+      return (
+        <div className={className} ref={this.setRef}>
+          {embed}
+          {!compact && description}
+        </div>
+      );
+    } else if (card.get('image')) {
+      embed = (
+        <div className='status-card__image'>
+          {thumbnail}
+        </div>
+      );
+    } else {
+      embed = (
+        <div className='status-card__image'>
+          <i className='fa fa-file-text' />
+        </div>
+      );
+    }
+
+    return (
+      <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}>
+        {embed}
+        {description}
+      </a>
+    );
+  }
+
+}