diff --git a/app/components/all.scss b/app/components/all.scss index 261f720c6b22233100ee20d1e27dff5fce2d0dc3..a6135e7bad22bedcd3671127cea7fa806e91eae9 100644 --- a/app/components/all.scss +++ b/app/components/all.scss @@ -22,6 +22,7 @@ @import "./elements/TagList"; @import "./elements/ChangePassword"; @import "./elements/Reputation"; +@import "./elements/YoutubePreview"; // modules @import "./modules/Header"; diff --git a/app/components/cards/MarkdownViewer.jsx b/app/components/cards/MarkdownViewer.jsx index db1e25afac1060268c7107ab3705cc537b6c4d91..ff1880fb975aee1baef8b337381897981cc69fe2 100644 --- a/app/components/cards/MarkdownViewer.jsx +++ b/app/components/cards/MarkdownViewer.jsx @@ -3,9 +3,11 @@ import {connect} from 'react-redux' import {Component} from 'react' import Remarkable from 'remarkable' // import CardView from 'app/components/cards/CardView' +import YoutubePreview from 'app/components/elements/YoutubePreview' import sanitizeConfig, {noImageText} from 'app/utils/SanitizeConfig' +import {renderToString} from 'react-dom/server'; import sanitize from 'sanitize-html' -import HtmlReady, {sectionHtml} from 'shared/HtmlReady' +import HtmlReady from 'shared/HtmlReady' const remarkable = new Remarkable({ html: true, // remarkable renders first then sanitize runs... @@ -75,7 +77,7 @@ class MarkdownViewer extends Component { let renderedText = html ? text : remarkable.render(text) // Embed videos, link mentions and hashtags, etc... - if(renderedText) renderedText = HtmlReady(renderedText, {large}).html + if(renderedText) renderedText = HtmlReady(renderedText).html // Complete removal of javascript and other dangerous tags.. // The must remain as close as possible to dangerouslySetInnerHTML @@ -89,8 +91,24 @@ class MarkdownViewer extends Component { const noImageActive = cleanText.indexOf(noImageText) !== -1 - // Split and key HTML doc by its root children. This allows react to compare separately preventing excessive re-rendering. - const sections = sectionHtml(cleanText).map( (s, idx) => <div key={idx++} dangerouslySetInnerHTML={{__html: s}} />); + // In addition to inserting the youtube compoennt, this allows react to compare separately preventing excessive re-rendering. + let idx = 0 + const sections = [] + // HtmlReady inserts ~~~ youtube:${id} ~~~ + for(let section of cleanText.split('~~~ youtube:')) { + if(/^[A-Za-z0-9\_\-]+ ~~~/.test(section)) { + const youTubeId = section.split(' ')[0] + section = section.substring(youTubeId.length + ' ~~~'.length) + const w = large ? 640 : 320, + h = large ? 480 : 180 + sections.push( + <YoutubePreview key={idx++} width={w} height={h} youTubeId={youTubeId} + frameBorder="0" allowFullScreen="true" /> + ) + } + if(section === '') continue + sections.push(<div key={idx++} dangerouslySetInnerHTML={{__html: section}} />) + } const cn = 'Markdown' + (this.props.className ? ` ${this.props.className}` : '') + (html ? ' html' : '') return (<div className={"MarkdownViewer " + cn}> diff --git a/app/components/elements/Template.jsx b/app/components/elements/Template.jsx index 1f827c641e8644c48223fdd62437bb004b3fc7b4..da4b80ddb1695356aac1ecad13bd60c6931ab146 100644 --- a/app/components/elements/Template.jsx +++ b/app/components/elements/Template.jsx @@ -5,30 +5,40 @@ import transaction from 'app/redux/Transaction' import shouldComponentUpdate from 'app/utils/shouldComponentUpdate' import {Map} from 'immutable' -const {func, string, object} = React.PropTypes +const {string, object} = React.PropTypes class Template extends React.Component { static propTypes = { } + static defaultProps = { } + constructor() { super() this.state = {} } + componentWillMount() { } + componentDidMount() { } + componentWillReceiveProps(nextProps) { } - shouldComponentUpdate = shouldComponentUpdate(this, 'Template') // This is based on react PureRenderMixin, it makes the component very efficient by not re-rendering unless something in the props or state changed.. PureRenderMixin comes highly recommended. shouldComponentUpdate adds a debug boolean to show you why your component rendered (what changed, in the browser console type: steemDebug_shouldComponentUpdate=true). + + // This is based on react PureRenderMixin, it makes the component very efficient by not re-rendering unless something in the props or state changed.. PureRenderMixin comes highly recommended. shouldComponentUpdate adds a debug boolean to show you why your component rendered (what changed, in the browser console type: steemDebug_shouldComponentUpdate=true). + shouldComponentUpdate = shouldComponentUpdate(this, 'Template') componentWillUpdate(nextProps, nextState) { } + componentDidUpdate(prevProps, prevState) { } + componentWillUnmount() { } + render() { const {} = this.props return ( @@ -37,7 +47,9 @@ class Template extends React.Component { ) } } + import {connect} from 'react-redux' + export default connect( (state, ownProps) => { return { diff --git a/app/components/elements/YoutubePreview.jsx b/app/components/elements/YoutubePreview.jsx new file mode 100644 index 0000000000000000000000000000000000000000..55b3d842605718f727262304f44e421192010acd --- /dev/null +++ b/app/components/elements/YoutubePreview.jsx @@ -0,0 +1,52 @@ +/* eslint react/prop-types: 0 */ +import React from 'react' +import shouldComponentUpdate from 'app/utils/shouldComponentUpdate' + +const {string, number} = React.PropTypes + +/** Lots of iframes in a post can be very slow. This component only inserts the iframe when it is actually needed. */ +export default class YoutubePreview extends React.Component { + static propTypes = { + youTubeId: string.isRequired, + width: number, + height: number, + dataParams: string, + } + + static defaultProps = { + width: 640, + height: 480, + dataParams: 'enablejsapi=0&rel=0&origin=https://steemit.com' + } + + constructor() { + super() + this.state = {} + } + + shouldComponentUpdate = shouldComponentUpdate(this, 'YoutubePreview') + + onPlay = () => { + this.setState({play: true}) + } + + render() { + const {youTubeId, width, height, dataParams} = this.props + const {play} = this.state + if(!play) { + // mqdefault.jpg (medium quality version, 320px × 180px) + // hqdefault.jpg (high quality version, 480px × 360px + // sddefault.jpg (standard definition version, 640px × 480px) + const thumbnail = width <= 320 ? 'mqdefault.jpg' : width <= 480 ? 'hqdefault.jpg' : 'sddefault.jpg' + const previewLink = `http://img.youtube.com/vi/${youTubeId}/${thumbnail}` + return ( + <div className="youtube" onClick={this.onPlay}> + <div className="play"></div> + <img src={previewLink} style={{width, maxWidth: width, height, maxHeight: height}} /> + </div> + ) + } + const autoPlaySrc = `//www.youtube.com/embed/${youTubeId}?autoplay=1&autohide=1&${dataParams}` + return <iframe width={width} height={height} src={autoPlaySrc} frameBorder="0" allowFullScreen="true"></iframe> + } +} diff --git a/app/components/elements/YoutubePreview.scss b/app/components/elements/YoutubePreview.scss new file mode 100644 index 0000000000000000000000000000000000000000..c9df0f272821f019f19f36667a838db69a57a209 --- /dev/null +++ b/app/components/elements/YoutubePreview.scss @@ -0,0 +1,25 @@ +.youtube { + background-position: center; + background-repeat: no-repeat; + position: relative; + display: inline-block; + overflow: hidden; + transition: all 200ms ease-out; + cursor: pointer; +} + +.youtube .play { + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAERklEQVR4nOWbTWhcVRTHb1IJVoxGtNCNdal2JYJReC6GWuO83PM/59yUS3FRFARdFlwYP1CfiojQWt36sRCUurRIdVFXIn41lAoVdRGrG1M01YpKrWjiYmaSl8ybZJL3cd+YA//NLObd3++eO8x79z5jSq5Gw+8kov0AP8vMR5l1BtBZQM4B8ks75wCdZdYZZj5qLZ4hov2Nht9Z9vhKKSIaB/gI4M4w62KeAO6Mte4lYOq20FxrlqqOibhHmeWbvNC9ZfDX1mLae391aN6limO/gwgvAPJbWeAZuSDingdwXTBw7/0IsyaA/Fkh+KqOkD+YNfHej1QKD+y7iVlOhgLvFqFfNJvNGyuBJ+KDAF8MDd0tgS8y64OlgSdJMsysL4cG7SOHkyQZLhTee7+d2R2rAVy/S+Jd7/32ouBHAP4gNNRGQyTHc/84NhqNywZp5rvjjnnvt21aABFeCQ+RLwAf2hQ8s7sv9OCLk6AHNgQvIrvbfzKCD76g/O6cu7lf/iER/aQGgy448pExZmhdegAPhR9sObFWH1gT3lp7DaA/5bkIgJhZPgsNmz02novj+KqeApj1ubwXWe4kdyeznAgNvTpE/HQmvKqOMeuFogTUVQSRno+iaLRLAJF7uIgL9O4ubgL8aWgB7S44mNX+35YpICUiAvS9sBLkq1WzT+NFffl6AuoiApi6NT37h6sWkBIRZGkQ8YtLgyji6e1mBYTqCEBPG2Naz+0BWQgtoGoRgCzEsd9hAN1X5BfnFZASUfrSAFQNsyZ1FJASUVpHiLinDJG8U2cBZYogkrcNs5waBAGdstbeU9zdqpw0gPwwSAI6VUxHyFlDpOcHUUBBIuYNs14aZAE5RVwyzPr3/0EAEY0TyfGNjBWQvwZ +CTSbehfAH29mrID8bET0+0EUkAd8WYDOmqJ3ecsG30yr9wqRfm6Y+a1BEFDEjHfHvWmY9ck6CygHvBVr8Xhtb4ZE5HZA3y8DvBNA1TjnrmXWf+sioMwZX5V/VHXMGGMMoKdDCxCRvRWBdzKzdHEO+EisilbPyopHYqp6S9UCAsz4iojI7hUDAtyXVQgIDd6KnOoaWNkbI6FaPSuZGyMArsi7MZoloB4zviI/Nhr3X95jltwTRQmoIfgisy5ai+me67OI7fE4nrqjrqfK1t0eby0FPRB6oGVlchL3rgnfrq19RKbVBdhV9IOSwJmfmJi4vi/4ThERitwyCxVAFqydshuCX5awhQ9KtmuIWd8IDZED/nXT77rvVVv6sHRKwjYi91poqP7Dr+Y6JJ1VSZIMA3wkPNy6bX+o8Bcm0sXMdwM8Fxo0A3xORPaWBp6uPXsmbxCRD0NDL0dOANhVCXy6iAjMcjbcrMt3RITKwdMVRdFo+y5yvkL4eWZ+zHt/ZVD4dEVRNGotpst+dZZZH8k86lqn2pIvT/eqrNfn2xuyqYPZ8mv7s8pfn/8Pybm4TIjanscAAAAASUVORK5CYII=") no-repeat center center; + background-size: 64px 64px; + position: absolute; + height: 100%; + width: 100%; + opacity: .8; + filter: alpha(opacity=80); + transition: all 0.2s ease-out; +} + +.youtube .play:hover { + opacity: 1; + filter: alpha(opacity=100); +} diff --git a/shared/HtmlReady.js b/shared/HtmlReady.js index 6889be81c4e0e2d0b943213ffc6179fa7d53a981..f82a22bb5d1088609257e1ee02fa63b90ede53ba 100644 --- a/shared/HtmlReady.js +++ b/shared/HtmlReady.js @@ -11,16 +11,16 @@ const XMLSerializer = new xmldom.XMLSerializer() /** Split the HTML on top-level elements. This allows react to compare separately, preventing excessive re-rendering. * Used in MarkdownViewer.jsx */ -export function sectionHtml (html) { - const doc = DOMParser.parseFromString(html, 'text/html') - const sections = Array(...doc.childNodes).map(child => XMLSerializer.serializeToString(child)) - return sections -} +// export function sectionHtml (html) { +// const doc = DOMParser.parseFromString(html, 'text/html') +// const sections = Array(...doc.childNodes).map(child => XMLSerializer.serializeToString(child)) +// return sections +// } /** Embed videos, link mentions and hashtags, etc... */ -export default function (html, {large = false, mutate = true}) { - const state = {large, mutate} +export default function (html, {mutate = true} = {}) { + const state = {mutate} state.hashtags = new Set() state.usertags = new Set() state.htmltags = new Set() @@ -50,7 +50,7 @@ function traverse(node, state, depth = 0) { img(state, child) else if(/a/i.test(child.tagName)) link(state, child) - else if(!embedYouTubeNode(child, state.large, state.links)) + else if(!embedYouTubeNode(child, state.links)) linkifyNode(child, state) traverse(child, state, ++depth) }) @@ -143,7 +143,7 @@ function linkify(content, mutate, hashtags, usertags, images, links) { return content } -function embedYouTubeNode(child, large, links) {try{ +function embedYouTubeNode(child, links) {try{ if(!child.data) return false const data = child.data if(/code/i.test(child.parentNode.tagName)) return false @@ -152,10 +152,7 @@ function embedYouTubeNode(child, large, links) {try{ const match = url.match(linksRe.youTubeId) if(match && match.length >= 2) { const id = match[1] - const src = `//www.youtube.com/embed/${id}?enablejsapi=0&rel=0&origin=https://steemit.com` - const w = large ? 640 : 384, - h = large ? 360 : 240 - const v = DOMParser.parseFromString(`<iframe width="${w}" height="${h}" src="${src}" frameBorder="0" allowFullScreen="true"></iframe>`) + const v = DOMParser.parseFromString(`~~~ youtube:${id} ~~~`) child.parentNode.replaceChild(v, child) replaced = true if(links) links.add(url)