import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';
import Image from './common/Image';
import url from 'url';
import MarkupUtils from '../utils/MarkupUtils';
import moment from 'moment';
import classnames from 'classnames';
import {int2hex} from '../../discord_common/js/utils/ColorUtils';
import MaskedLink from '../components/common/MaskedLink';
import SnowflakeUtils from '../utils/SnowflakeUtils';
import './Embed.styl';
const VIDEO_PROVIDERS = /youtube|steam|imgur|vimeo|sketchfab|vine|soundcloud|streamable|twitch|vid\.me|twitter/i;
const VIDEO_PROVIDER_CHECK_UNIX_TIMESTAMP = 1492472454139;
const EmbedGIFV = React.createClass({
mixins: [PureRenderMixin],
propTypes: {
url: React.PropTypes.string.isRequired,
thumbnail: React.PropTypes.shape({
url: React.PropTypes.string,
// eslint-disable-next-line camelcase
proxy_url: React.PropTypes.string,
width: React.PropTypes.width,
height: React.PropTypes.height,
}),
video: React.PropTypes.shape({
url: React.PropTypes.string,
width: React.PropTypes.width,
height: React.PropTypes.height,
}),
scale: React.PropTypes.bool,
},
render() {
const {url, scale, thumbnail, video, trusted} = this.props;
let {width, height} = thumbnail;
const maxWidth = scale ? 512 : width;
const maxHeight = scale ? 384 : height;
let widthRatio = 1;
if (width > maxWidth) {
widthRatio = maxWidth / width;
}
width = Math.round(width * widthRatio);
height = Math.round(height * widthRatio);
let heightRatio = 1;
if (height > maxHeight) {
heightRatio = maxHeight / height;
}
width = Math.round(width * heightRatio);
height = Math.round(height * heightRatio);
return (
);
},
});
const EmbedVideo = React.createClass({
mixins: [PureRenderMixin],
propTypes: {
url: React.PropTypes.string.isRequired,
playable: React.PropTypes.bool,
thumbnail: React.PropTypes.shape({
url: React.PropTypes.string,
// eslint-disable-next-line camelcase
proxy_url: React.PropTypes.string,
width: React.PropTypes.width,
height: React.PropTypes.height,
}),
video: React.PropTypes.shape({
url: React.PropTypes.string,
width: React.PropTypes.width,
height: React.PropTypes.height,
}),
scale: React.PropTypes.bool,
useVideoSizeWhenPlaying: React.PropTypes.bool,
},
getInitialState() {
return {
play: false,
};
},
getDefaultProps() {
return {
scale: true,
useVideoSizeWhenPlaying: false,
playable: false,
};
},
handlePlay() {
this.setState({play: true});
},
componentWillMount() {
this.messageListener = null;
},
componentWillUnmount() {
this.removeMessageListener();
},
removeMessageListener() {
if (this.messageListener != null) {
window.removeEventListener('message', this.messageListener);
this.messageListener = null;
}
},
installMessageListener(listener) {
this.removeMessageListener();
const messageListener = (this.messageListener = e => {
if (listener(e) === false && messageListener === this.messageListener) {
this.removeMessageListener();
}
});
window.addEventListener('message', messageListener);
},
handleRef(src, el) {
if (this.props.provider === 'Vine') {
// This hack tricks vine into thinking that the video is in view and ready to be played, by emulating
// functionality in their embed code that does scroll watching. In our case, we can just treat loading
// the iframe as being in view.
const expectedMessage = `ping::${src}`;
this.installMessageListener(e => {
if (el && el.contentWindow === e.source && e.origin === 'https://vine.co' && e.data === expectedMessage) {
e.source.postMessage('pong', '*');
e.source.postMessage('fullyInView', '*');
e.source.postMessage('play', '*');
return false;
}
});
}
},
render() {
let {width, height} = this.props.thumbnail;
if (this.state.play && this.props.useVideoSizeWhenPlaying) {
if (this.props.video.width && this.props.video.height) {
width = this.props.video.width;
height = this.props.video.height;
}
}
const maxWidth = this.props.scale ? 400 : width;
const maxHeight = this.props.scale ? 300 : height;
let widthRatio = 1;
if (width > maxWidth) {
widthRatio = maxWidth / width;
}
width = Math.round(width * widthRatio);
height = Math.round(height * widthRatio);
let heightRatio = 1;
if (height > maxHeight) {
heightRatio = maxHeight / height;
}
width = Math.round(width * heightRatio);
height = Math.round(height * heightRatio);
if (this.state.play) {
const urlObj = url.parse(this.props.video.url, true);
urlObj.query.autoplay = 1;
urlObj.query['auto_play'] = 1;
delete urlObj.search;
const src = url.format(urlObj);
return (
);
} else {
let playButton;
if (this.props.playable) {
playButton = ;
}
return (
);
}
},
});
const EmbedWrapper = React.createClass({
mixins: [PureRenderMixin],
propTypes: {
className: React.PropTypes.string,
children: React.PropTypes.node.isRequired,
color: React.PropTypes.number,
maxWidth: React.PropTypes.number,
},
render() {
const {maxWidth} = this.props;
const style = this.props.color ? {backgroundColor: int2hex(this.props.color)} : null;
return (
);
},
});
const Embed = React.createClass({
mixins: [PureRenderMixin],
propTypes: {
/* eslint-disable camelcase */
message: React.PropTypes.object,
channelId: React.PropTypes.string,
inlineMedia: React.PropTypes.bool,
type: React.PropTypes.string.isRequired,
url: React.PropTypes.string,
title: React.PropTypes.string,
description: React.PropTypes.string,
color: React.PropTypes.number,
timestamp: React.PropTypes.string,
provider: React.PropTypes.shape({
name: React.PropTypes.string,
url: React.PropTypes.string,
}),
author: React.PropTypes.shape({
name: React.PropTypes.string,
url: React.PropTypes.string,
icon_url: React.PropTypes.string,
proxy_icon_url: React.PropTypes.string,
}),
thumbnail: React.PropTypes.shape({
url: React.PropTypes.string,
proxy_url: React.PropTypes.string,
width: React.PropTypes.width,
height: React.PropTypes.height,
}),
image: React.PropTypes.shape({
url: React.PropTypes.string,
proxy_url: React.PropTypes.string,
width: React.PropTypes.width,
height: React.PropTypes.height,
}),
footer: React.PropTypes.shape({
text: React.PropTypes.string,
icon_url: React.PropTypes.string,
proxy_icon_url: React.PropTypes.string,
}),
fields: React.PropTypes.arrayOf(
React.PropTypes.shape({
name: React.PropTypes.string,
value: React.PropTypes.string,
inline: React.PropTypes.bool,
})
),
scale: React.PropTypes.bool,
/* eslint-enable camelcase */
},
getDefaultProps() {
return {
inlineMedia: true,
scale: true,
};
},
isMaskedLinkTrusted() {
return this.props.type !== 'rich';
},
renderProvider() {
let {provider, url} = this.props;
if (provider) {
url = provider.url || url;
provider = url
?
{provider.name}
:
{provider.name}
;
return (
{provider}
);
}
},
renderAuthor() {
let {author, url} = this.props;
if (author) {
let icon = null;
if (author.proxy_icon_url) {
icon = ;
}
url = author.url || url;
author = url
?
{author.name}
:
{author.name}
;
return (
{icon}
{author}
);
}
},
renderFooter() {
let timestamp = null;
if (this.props.timestamp) {
timestamp = moment(this.props.timestamp).format('ddd MMM Do, YYYY [at] h:mm A');
}
if (this.props.footer) {
let icon = null;
if (this.props.footer.proxy_icon_url) {
icon = ;
}
return (
{icon}
{this.props.footer.text}
{timestamp ? ' | ' : null}
{timestamp}
);
} else if (timestamp) {
return {timestamp};
}
},
renderTitle(markdown = false) {
let {title, url, channelId} = this.props;
if (title == null) {
return;
}
title = markdown ? MarkupUtils.parseEmbedTitle(title, true, {channelId}) : title;
if (url) {
return (
{title}
);
}
return (
{title}
);
},
renderDescription(markdown = false) {
const {description, channelId} = this.props;
if (description) {
return (
{markdown ? MarkupUtils.parseAllowLinks(description, true, {channelId}) : description}
);
}
},
renderFields() {
const {fields, channelId} = this.props;
if (fields) {
return (
{fields.map(({name, value, inline}, i) =>
{MarkupUtils.parseEmbedTitle(name, true, {channelId})}
{MarkupUtils.parseAllowLinks(value, true, {channelId})}
)}
);
}
},
renderImage(lightbox = false, maxWidth = 400, maxHeight = 250, thumbnail = this.props.thumbnail) {
if (thumbnail && this.props.inlineMedia) {
const {width, height} = thumbnail;
if (width > 0 && height > 0) {
const image = (
);
return lightbox
?
{image}
:
{image}
;
}
}
},
renderVideo() {
const {provider, message, thumbnail, video, inlineMedia, scale, url} = this.props;
if (thumbnail && inlineMedia) {
const {width, height} = thumbnail;
if (width > 0 && height > 0) {
let playable = video && /^https:/i.test(video.url);
// Messages sent after this point contain server-side validated video urls
if (message != null && SnowflakeUtils.extractTimestamp(message.id) < VIDEO_PROVIDER_CHECK_UNIX_TIMESTAMP) {
playable = playable && provider && VIDEO_PROVIDERS.test(provider.name);
}
const useVideoSizeWhenPlaying = provider && /soundcloud|twitch/i.test(provider.name);
return (
);
}
}
},
renderGIFV() {
const {thumbnail, video, inlineMedia, scale, url} = this.props;
if (thumbnail && inlineMedia && video) {
const {width, height} = thumbnail;
if (width > 0 && height > 0) {
return (
);
}
}
},
hasProvider() {
return this.provider && this.provider.name;
},
hasAuthor() {
return this.author && this.author.name;
},
isInline() {
const {provider, title} = this.props;
return (provider != null && /giphy|tenor|gfycat/i.test(provider.name)) || (!this.hasAuthor() && !title);
},
/**
* Determine a maxWidth based on whether the embed has a fairly large image.
* This way the image is framed by the borders to have equal padding and results a nicer looking UI.
*/
getMaxWidth(): ?number {
const {image, thumbnail, video, type} = this.props;
let target = thumbnail;
const maxWidth = video != null ? 512 : 400;
switch (type) {
case 'changelog':
return;
case 'rich':
if (thumbnail != null) {
return;
}
target = image;
break;
}
return target && target.width >= maxWidth ? maxWidth + 20 /* PADDING */ : undefined;
},
render() {
const {thumbnail, type, color, image, video, description} = this.props;
switch (type) {
case 'rich':
return (
{this.renderProvider()}
{this.renderAuthor()}
{this.renderTitle(true)}
{this.renderDescription(true)}
{this.renderFields()}
{!video && thumbnail && thumbnail.width
?
: null}
{video ? this.renderVideo() : null}
{image ? this.renderImage(true, undefined, undefined, image) : null}
{this.renderFooter()}
);
case 'article':
return (
{this.renderProvider()}
{this.renderAuthor()}
{this.renderTitle()}
{this.renderDescription()}
{this.renderImage(true)}
);
case 'image':
if (this.isInline()) {
return (
{this.renderImage(true)}
);
} else {
return (
{this.renderProvider()}
{this.renderAuthor()}
{this.renderTitle()}
{this.renderImage(true)}
);
}
case 'gifv':
if (this.isInline()) {
return (
{this.renderGIFV()}
);
} else {
return (
{this.renderProvider()}
{this.renderAuthor()}
{this.renderTitle()}
{this.renderGIFV()}
);
}
case 'changelog':
case 'video':
return (
{this.renderProvider()}
{this.renderAuthor()}
{this.renderTitle()}
{this.renderVideo()}
);
case 'tweet':
return (
{this.renderTitle()}
{this.renderAuthor()}
{this.renderDescription()}
{video != null ? this.renderVideo() : this.renderImage(true)}
);
case 'link':
default:
if (!description && !thumbnail) {
return null;
} else {
return (
{this.renderProvider()}
{this.renderAuthor()}
{this.renderTitle()}
{this.renderDescription()}
{this.renderImage(false, 75, 75)}
);
}
}
},
});
export default Embed;
// WEBPACK FOOTER //
// ./discord_app/components/Embed.js