Skip to content
This repository has been archived by the owner on Feb 29, 2020. It is now read-only.

Commit

Permalink
feat(content): show inline video players for youtube and vimeo
Browse files Browse the repository at this point in the history
  • Loading branch information
rittme committed Jun 2, 2016
1 parent 9297b76 commit d541b39
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 4 deletions.
2 changes: 1 addition & 1 deletion bin/generate-html.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const defaults = {
function template(rawOptions) {
const options = Object.assign({}, defaults, rawOptions || {});
const csp = options.csp === "on" ?
"<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'self'; img-src http: https: data:; style-src 'self' 'unsafe-inline'\">" :
"<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'self'; img-src http: https: data:; style-src 'self' 'unsafe-inline'; child-src 'self' https://*.youtube.com https://*.vimeo.com; frame-src 'self' https://*.youtube.com https://*.vimeo.com\">" :
"";
return `<!doctype html>
<html lang="en-us">
Expand Down
15 changes: 14 additions & 1 deletion content-src/components/ActivityFeed/ActivityFeed.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const React = require("react");
const {connect} = require("react-redux");
const {justDispatch} = require("selectors/selectors");
const {justDispatch, selectSitePreview} = require("selectors/selectors");
const {actions} = require("common/action-manager");
const SiteIcon = require("components/SiteIcon/SiteIcon");
const MediaPreview = require("components/MediaPreview/MediaPreview");
const DeleteMenu = require("components/DeleteMenu/DeleteMenu");
const {prettyUrl, getRandomFromTimestamp} = require("lib/utils");
const moment = require("moment");
Expand Down Expand Up @@ -63,6 +64,17 @@ const ActivityFeedItem = React.createClass({
dateLabel = moment(date).format("h:mm A");
}

let preview;
if (site.media && site.media.type === "video") {
const previewInfo = selectSitePreview(site);
if (previewInfo.previewURL) {
const previewProps = {
previewInfo,
};
preview = (<MediaPreview {...previewProps} />);
}
}

return (<li className={classNames("feed-item", {bookmark: site.bookmarkGuid, fixed: this.state.showContextMenu})}>
<a onClick={this.props.onClick} href={site.url} ref="link">
<span className="star" hidden={!site.bookmarkGuid} />
Expand All @@ -71,6 +83,7 @@ const ActivityFeedItem = React.createClass({
<div className="feed-description">
<h4 className="feed-title" ref="title">{title}</h4>
<span className="feed-url" ref="url">{prettyUrl(site.url)}</span>
{preview}
</div>
<div className="feed-stats">
<div ref="lastVisit">{dateLabel}</div>
Expand Down
5 changes: 3 additions & 2 deletions content-src/components/ActivityFeed/ActivityFeed.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
&:first-child {
&::before {
margin-top: -($feed-circle-size / 2);
top: 50%;
top: 32px;
}
}

Expand Down Expand Up @@ -72,7 +72,7 @@
height: $feed-circle-size;
left: $feed-gutter-h + 3;
position: absolute;
top: 50%;
top: 28px;
width: $feed-circle-size;
margin-top: -($feed-circle-size / 2);
}
Expand Down Expand Up @@ -172,6 +172,7 @@
align-items: baseline;
display: flex;
flex-grow: 1;
flex-wrap: wrap;
overflow: hidden;

.feed-description {
Expand Down
53 changes: 53 additions & 0 deletions content-src/components/MediaPreview/MediaPreview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const React = require("react");
const classNames = require("classnames");

const MediaPreview = React.createClass({
getInitialState() {
return {
showPlayer: false
};
},

getDefaultProps() {
return {
previewInfo: {},
};
},

onPreviewClick(evt) {
evt.preventDefault();
this.setState({showPlayer: true});
},

render() {
const previewInfo = this.props.previewInfo;

let player;
if (this.state.showPlayer && previewInfo.previewURL) {
player = (<iframe className="video-preview-player"
src={previewInfo.previewURL}
width="367"
height="206"
type="text/html"
ref="previewPlayer"
frameBorder="0"
allowFullScreen></iframe>);
}

const style = previewInfo.thumbnail ? {backgroundImage: `url(${previewInfo.thumbnail.url})`} : null;
return (<div className={classNames("video-preview", {isPlaying: this.state.showPlayer})}
style={style} onClick={this.onPreviewClick}>
{player}
</div>);
}
});

MediaPreview.propTypes = {
previewInfo: React.PropTypes.shape({
previewURL: React.PropTypes.string,
thumbnail: React.PropTypes.object,
type: React.PropTypes.string,
})
};

module.exports = MediaPreview;
36 changes: 36 additions & 0 deletions content-src/components/MediaPreview/MediaPreview.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.video-preview {
display: flex;
align-items: center;
justify-content: center;
background: $black no-repeat center center;
width: 367px;
height: 206px;
border-radius: 6px;
transition: 0.2s opacity;
overflow: hidden;
margin-top: 20px;

&::after {
content: '';
width: 65px;
height: 40px;
background: url('img/playButton@2x.png') no-repeat center center;
background-size: 65px 40px;
display: block;
}

&:hover::after {
background-image: url('img/playButton-hover@2x.png');
}

&.isPlaying::after {
display: none;
}
}

.video-preview-player {
width: 100%;
height: 100%;
display: block;
border: 0;
}
32 changes: 32 additions & 0 deletions content-src/lib/getVideoPreview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const getYouTubeID = require("get-youtube-id");

module.exports = function getVideoPreview(url) {
if (!url) {
return null;
}
return getVideoURL.youtube(url) ||
getVideoURL.vimeo(url) ||
null;
};

const getVideoURL = {
youtube: function(url) {
let videoId = getYouTubeID(url, {fuzzy: false});
if (!videoId) {
return null;
}

return `https://www.youtube.com/embed/${videoId}?autoplay=1`;
},

vimeo: function(url) {
const vimeoRegex = /(http|https)?:\/\/(www\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|)(\d+)(?:|\/\?)/;
let idMatches = url.match(vimeoRegex);
if (!idMatches) {
return null;
}
let videoId = idMatches[idMatches.length - 1];

return `https://player.vimeo.com/video/${videoId}?autoplay=1`;
}
};
1 change: 1 addition & 0 deletions content-src/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,4 @@ a {
@import './components/Loader/Loader';
@import './components/LoadMore/LoadMore';
@import './components/ContextMenu/ContextMenu';
@import './components/MediaPreview/MediaPreview';
21 changes: 21 additions & 0 deletions content-src/selectors/selectors.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const {createSelector} = require("reselect");
const dedupe = require("lib/dedupe");
const getBestImage = require("lib/getBestImage");
const getVideoPreview = require("lib/getVideoPreview");
const firstRunData = require("lib/first-run-data");
const {prettyUrl, getBlackOrWhite, toRGBString, getRandomColor} = require("lib/utils");
const urlParse = require("url-parse");
Expand Down Expand Up @@ -132,6 +133,26 @@ const selectSiteIcon = createSelector(
}
);

module.exports.selectSitePreview = createSelector(
site => site,
site => {
const type = site.media ? site.media.type : null;
let thumbnail;
let previewURL;
if (type) {
thumbnail = getBestImage(site.images);
if (type === "video") {
previewURL = getVideoPreview(site.url);
}
}
return {
type,
thumbnail,
previewURL
};
}
);

// Timeline History view
module.exports.selectHistory = createSelector(
[
Expand Down
Binary file added content-src/static/img/playButton-hover@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added content-src/static/img/playButton@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 57 additions & 0 deletions content-test/components/MediaPreview.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const {assert} = require("chai");
const TestUtils = require("react-addons-test-utils");
const React = require("react");
const ReactDOM = require("react-dom");
const MediaPreview = require("components/MediaPreview/MediaPreview");

const fakeProps = {
previewInfo: {
previewURL: "https://www.youtube.com/embed/lDv68xYHFXM",
thumbnail: {
"url": "https://i.ytimg.com/vi/lDv68xYHFXM/hqdefault.jpg",
"height": 360,
"width": 480,
},
type: "video",
}
};

describe("MediaPreview", () => {
let instance;
let el;

function setup(customPreviewProps = {}) {
const previewInfo = Object.assign({}, fakeProps.previewInfo, customPreviewProps);
instance = TestUtils.renderIntoDocument(<MediaPreview {...{previewInfo}} />);
el = ReactDOM.findDOMNode(instance);
}

beforeEach(() => setup());

it("should not throw if missing props", () => {
assert.doesNotThrow(() => {
TestUtils.renderIntoDocument(<MediaPreview />);
});
});

it("should create a MediaPreview instance", () => {
assert.instanceOf(instance, MediaPreview);
});

it("should have a background thumbnail", () => {
assert.equal(el.style.backgroundImage, `url("${fakeProps.previewInfo.thumbnail.url}")`);
});

it("should show the video iframe when the preview is clicked", () => {
TestUtils.Simulate.click(el);
const previewPlayer = instance.refs.previewPlayer;
assert.equal(previewPlayer.src, fakeProps.previewInfo.previewURL);
});

it("should not show the iframe when clicked and previewURL is falsey", () => {
setup({previewURL: null});
TestUtils.Simulate.click(el);
const previewPlayer = instance.refs.previewPlayer;
assert.equal(typeof previewPlayer, "undefined");
});
});
27 changes: 27 additions & 0 deletions content-test/lib/getVideoPreview.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const {assert} = require("chai");

const getVideoPreview = require("lib/getVideoPreview");

describe("getVideoPreview", () => {
it("should return null if url is falsey", () => {
assert.equal(getVideoPreview(), null);
assert.equal(getVideoPreview(null), null);
});
it("should return null if url is not an accepted video service url", () => {
assert.equal(getVideoPreview("http://foo.com"), null);
});
it("should return null for a youtube URL without a valid video id", () => {
assert.equal(getVideoPreview("https://www.youtube.com/feed/trending"), null);
});
it("should return null for a vimeo URL without a valid video id", () => {
assert.equal(getVideoPreview("https://vimeo.com/channels/staffpicks"), null);
});
it("should return an embed url for a valid youtube url", () => {
const videoId = "lDv68xYHFXM";
assert.equal(getVideoPreview(`https://www.youtube.com/watch?v=${videoId}`), `https://www.youtube.com/embed/${videoId}?autoplay=1`);
});
it("should return an embed url for a valid vimeo url", () => {
const videoId = "1202674";
assert.equal(getVideoPreview(`https://vimeo.com/${videoId}`), `https://player.vimeo.com/video/${videoId}?autoplay=1`);
});
});
53 changes: 53 additions & 0 deletions content-test/selectors/selectors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ const {
selectTopSites,
selectNewTabSites,
selectSiteIcon,
selectSitePreview,
getBackgroundRGB,
SPOTLIGHT_LENGTH,
DEFAULT_FAVICON_BG_COLOR
} = require("selectors/selectors");
const {rawMockData, createMockProvider} = require("test/test-utils");
const {IMG_WIDTH, IMG_HEIGHT} = require("lib/getBestImage");

const validSpotlightSite = {
"title": "man throws alligator in wendys wptv dnt cnn",
Expand Down Expand Up @@ -252,6 +254,57 @@ describe("selectors", () => {
assert.equal(state.label, "foo.com");
});
});

describe("selectSitePreview", () => {
const siteWithMedia = {
url: "https://www.youtube.com/watch?v=lDv68xYHFXM",
images: [{url: "foo.jpg", height: IMG_HEIGHT, width: IMG_WIDTH}],
media: {
type: "video"
},
};
const embedPreviewURL = "https://www.youtube.com/embed/lDv68xYHFXM?autoplay=1";
let state;
beforeEach(() => {
state = selectSitePreview(siteWithMedia);
});

it("should not throw", () => {
assert.doesNotThrow(() => {
selectSitePreview({});
});
});

it("should have a preview url", () => {
assert.equal(state.previewURL, embedPreviewURL);
});
it("should have a thumbnail", () => {
assert.property(state, "thumbnail");
assert.isObject(state.thumbnail);
assert.property(state.thumbnail, "url");
assert.equal(state.thumbnail.url, siteWithMedia.images[0].url);
});
it("should have a type", () => {
assert.equal(state.type, siteWithMedia.media.type);
});
it("should return null thumbnail if no images exists", () => {
state = selectSitePreview(Object.assign({}, siteWithMedia, {images: []}));
assert.equal(state.thumbnail, null);
});
it("should return null preview url if no valid embed was found", () => {
state = selectSitePreview(Object.assign({}, siteWithMedia, {url: "http://foo.com"}));
assert.equal(state.previewURL, null);
});
it("no video preview if type is not video", () => {
state = selectSitePreview(Object.assign({}, siteWithMedia, {media: {type: "image"}}));
assert.equal(state.previewURL, null);
});
it("no video preview or thumbnail if no media type is set", () => {
state = selectSitePreview(Object.assign({}, siteWithMedia, {media: null}));
assert.equal(state.thumbnail, null);
assert.equal(state.previewURL, null);
});
});
});

describe("getBackgroundRGB", () => {
Expand Down
Loading

0 comments on commit d541b39

Please sign in to comment.