Progress on featured content

This commit is contained in:
6ea86b96 2017-04-23 16:56:50 +07:00 committed by Jeremy Kauffman
parent 4fe3fe5dcb
commit ecd2063fed
24 changed files with 500 additions and 46 deletions

29
ui/js/actions/content.js Normal file
View file

@ -0,0 +1,29 @@
import * as types from 'constants/action_types'
import lbry from 'lbry'
import lbryio from 'lbryio';
export function doFetchFeaturedContent() {
return function(dispatch, getState) {
const state = getState()
dispatch({
type: types.FETCH_FEATURED_CONTENT_STARTED,
})
const success = ({ Categories, Uris }) => {
dispatch({
type: types.FETCH_FEATURED_CONTENT_COMPLETED,
data: {
categories: Categories,
uris: Uris,
}
})
}
const failure = () => {
}
lbryio.call('discover', 'list', { version: "early-access" } )
.then(success, failure)
}
}

View file

@ -38,7 +38,7 @@ module.exports = app;
// import {FileListDownloaded, FileListPublished} from './page/file-list.js'; // import {FileListDownloaded, FileListPublished} from './page/file-list.js';
// import Header from './component/header.js'; // import Header from './component/header.js';
// import {Modal, ExpandableModal} from './component/modal.js'; // import {Modal, ExpandableModal} from './component/modal.js';
// import {Link} from './component/link.js'; // import {Link} from './component/link';
// //
// //
// const {remote, ipcRenderer, shell} = require('electron'); // const {remote, ipcRenderer, shell} = require('electron');

View file

@ -2,7 +2,7 @@ import React from "react";
import lbryio from "../lbryio.js"; import lbryio from "../lbryio.js";
import Modal from "./modal.js"; import Modal from "./modal.js";
import ModalPage from "./modal-page.js"; import ModalPage from "./modal-page.js";
import {Link, RewardLink} from "../component/link.js"; import {Link, RewardLink} from "../component/link";
import {FormRow} from "../component/form.js"; import {FormRow} from "../component/form.js";
import {CreditAmount, Address} from "../component/common.js"; import {CreditAmount, Address} from "../component/common.js";
import {getLocal, getSession, setSession, setLocal} from '../utils.js'; import {getLocal, getSession, setSession, setLocal} from '../utils.js';

View file

@ -0,0 +1,13 @@
import React from 'react'
import {
connect
} from 'react-redux'
import FileTile from './view'
const select = (state) => ({
})
const perform = (dispatch) => ({
})
export default connect(select, perform)(FileTile)

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from 'lbry.js';
import lbryuri from '../lbryuri.js'; import lbryuri from 'lbryuri.js';
import Link from 'component/link'; import Link from 'component/link';
import {FileActions} from '../component/file-actions.js'; import {FileActions} from 'component/file-actions.js';
import {BusyMessage, TruncatedText, FilePrice} from '../component/common.js'; import {Thumbnail, TruncatedText, FilePrice} from 'component/common.js';
import UriIndicator from '../component/channel-indicator.js'; import UriIndicator from 'component/channel-indicator.js';
/*should be merged into FileTile once FileTile is refactored to take a single id*/ /*should be merged into FileTile once FileTile is refactored to take a single id*/
export let FileTileStream = React.createClass({ export let FileTileStream = React.createClass({
@ -217,7 +217,7 @@ export let FileCardStream = React.createClass({
} }
}); });
export let FileTile = React.createClass({ let FileTile = React.createClass({
_isMounted: false, _isMounted: false,
_isResolvePending: false, _isResolvePending: false,

View file

@ -0,0 +1,13 @@
import React from 'react'
import {
connect
} from 'react-redux'
import FileTileStream from './view'
const select = (state) => ({
})
const perform = (dispatch) => ({
})
export default connect(select, perform)(FileTileStream)

View file

@ -0,0 +1,129 @@
import React from 'react';
import lbry from 'lbry.js';
import lbryuri from 'lbryuri.js';
import Link from 'component/link';
import {
FileActions
} from 'component/file-actions.js';
import {Thumbnail, TruncatedText, FilePrice} from 'component/common.js';
import UriIndicator from 'component/channel-indicator.js';
/*should be merged into FileTile once FileTile is refactored to take a single id*/
const FileTileStream = React.createClass({
_fileInfoSubscribeId: null,
_isMounted: null,
propTypes: {
uri: React.PropTypes.string,
metadata: React.PropTypes.object,
contentType: React.PropTypes.string.isRequired,
outpoint: React.PropTypes.string,
hasSignature: React.PropTypes.bool,
signatureIsValid: React.PropTypes.bool,
hideOnRemove: React.PropTypes.bool,
hidePrice: React.PropTypes.bool,
obscureNsfw: React.PropTypes.bool
},
getInitialState: function() {
return {
showNsfwHelp: false,
isHidden: false,
}
},
getDefaultProps: function() {
return {
obscureNsfw: !lbry.getClientSetting('showNsfw'),
hidePrice: false,
hasSignature: false,
}
},
componentDidMount: function() {
this._isMounted = true;
if (this.props.hideOnRemove) {
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
}
},
componentWillUnmount: function() {
if (this._fileInfoSubscribeId) {
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
}
},
onFileInfoUpdate: function(fileInfo) {
if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
this.setState({
isHidden: true
});
}
},
handleMouseOver: function() {
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
this.setState({
showNsfwHelp: true,
});
}
},
handleMouseOut: function() {
if (this.state.showNsfwHelp) {
this.setState({
showNsfwHelp: false,
});
}
},
render: function() {
if (this.state.isHidden) {
return null;
}
const uri = lbryuri.normalize(this.props.uri);
const metadata = this.props.metadata;
const isConfirmed = !!metadata;
const title = isConfirmed ? metadata.title : uri;
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
return (
<section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className={"row-fluid card__inner file-tile__row"}>
<div className="span3 file-tile__thumbnail-container">
<a href={'?show=' + uri}><Thumbnail className="file-tile__thumbnail" {... metadata && metadata.thumbnail ? {src: metadata.thumbnail} : {}} alt={'Photo for ' + this.props.uri} /></a>
</div>
<div className="span9">
<div className="card__title-primary">
{ !this.props.hidePrice
? <FilePrice uri={this.props.uri} />
: null}
<div className="meta"><a href={'?show=' + this.props.uri}>{uri}</a></div>
<h3>
<a href={'?show=' + uri} title={title}>
<TruncatedText lines={1}>
{title}
</TruncatedText>
</a>
</h3>
</div>
<div className="card__actions">
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
</div>
<div className="card__content">
<p className="file-tile__description">
<TruncatedText lines={2}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
</TruncatedText>
</p>
</div>
</div>
</div>
{this.state.showNsfwHelp
? <div className='card-overlay'>
<p>
This content is Not Safe For Work.
To view adult content, please change your <Link className="button-text" href="?settings" label="Settings" />.
</p>
</div>
: null}
</section>
);
}
});
export default FileTileStream

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import lbryuri from '../lbryuri.js'; import lbryuri from 'lbryuri.js';
import {Icon, CreditAmount} from './common.js'; import {Icon, CreditAmount} from 'component/common.js';
import Link from 'component/link'; import Link from 'component/link';
let Header = React.createClass({ let Header = React.createClass({

View file

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import SettingsPage from 'page/settings.js'; import SettingsPage from 'page/settings.js';
import HelpPage from 'page/help'; import HelpPage from 'page/help';
import WatchPage from 'page/watch.js';
import ReportPage from 'page/report.js'; import ReportPage from 'page/report.js';
import StartPage from 'page/start.js'; import StartPage from 'page/start.js';
import WalletPage from 'page/wallet'; import WalletPage from 'page/wallet';
import DetailPage from 'page/show.js'; import DetailPage from 'page/show.js';
import PublishPage from 'page/publish.js'; import PublishPage from 'page/publish.js';
import DiscoverPage from 'page/discover.js'; import DiscoverPage from 'page/discover';
import SplashScreen from 'component/splash.js'; import SplashScreen from 'component/splash.js';
import DeveloperPage from 'page/developer.js'; import DeveloperPage from 'page/developer.js';
import { import {
@ -30,7 +29,6 @@ const Router = (props) => {
return route(currentPage, { return route(currentPage, {
'settings': <SettingsPage {...props} />, 'settings': <SettingsPage {...props} />,
'help': <HelpPage {...props} />, 'help': <HelpPage {...props} />,
'watch': <WatchPage {...props} />,
'report': <ReportPage {...props} />, 'report': <ReportPage {...props} />,
'downloaded': <FileListDownloaded {...props} />, 'downloaded': <FileListDownloaded {...props} />,
'published': <FileListPublished {...props} />, 'published': <FileListPublished {...props} />,

View file

@ -0,0 +1,42 @@
import React from 'react'
import {
connect,
} from 'react-redux'
import {
doCloseModal,
} from 'actions/app'
import {
selectCurrentModal,
} from 'selectors/app'
import {
doWatchVideo,
doLoadVideo,
} from 'actions/content'
import {
selectLoadingCurrentUri,
selectCurrentUriFileReadyToPlay,
selectCurrentUriIsPlaying,
selectCurrentUriFileInfo,
selectDownloadingCurrentUri,
} from 'selectors/file_info'
import {
selectCurrentUriCostInfo,
} from 'selectors/cost_info'
import Video from './view'
const select = (state) => ({
costInfo: selectCurrentUriCostInfo(state),
fileInfo: selectCurrentUriFileInfo(state),
modal: selectCurrentModal(state),
isLoading: selectLoadingCurrentUri(state),
readyToPlay: selectCurrentUriFileReadyToPlay(state),
isDownloading: selectDownloadingCurrentUri(state),
})
const perform = (dispatch) => ({
loadVideo: () => dispatch(doLoadVideo()),
watchVideo: (elem) => dispatch(doWatchVideo()),
closeModal: () => dispatch(doCloseModal()),
})
export default connect(select, perform)(Video)

View file

@ -0,0 +1,130 @@
import React from 'react';
import {
Icon,
Thumbnail,
} from 'component/common';
import FilePrice from 'component/filePrice'
import Link from 'component/link';
import Modal from 'component/modal';
const plyr = require('plyr')
class Video extends React.Component {
constructor(props) {
super(props)
// TODO none of this mouse handling stuff seems to actually do anything?
this._controlsHideDelay = 3000 // Note: this needs to be shorter than the built-in delay in Electron, or Electron will hide the controls before us
this._controlsHideTimeout = null
this.state = {}
}
handleMouseMove() {
if (this._controlsTimeout) {
clearTimeout(this._controlsTimeout);
}
if (!this.state.controlsShown) {
this.setState({
controlsShown: true,
});
}
this._controlsTimeout = setTimeout(() => {
if (!this.isMounted) {
return;
}
this.setState({
controlsShown: false,
});
}, this._controlsHideDelay);
}
handleMouseLeave() {
if (this._controlsTimeout) {
clearTimeout(this._controlsTimeout);
}
if (this.state.controlsShown) {
this.setState({
controlsShown: false,
});
}
}
onWatchClick() {
this.props.watchVideo().then(() => {
if (!this.props.modal) {
this.setState({
isPlaying: true
})
}
})
}
startPlaying() {
this.setState({
isPlaying: true
})
}
render() {
const {
readyToPlay = false,
thumbnail,
metadata,
isLoading,
isDownloading,
fileInfo,
} = this.props
const {
isPlaying = false,
} = this.state
let loadStatusMessage = ''
if (isLoading) {
loadStatusMessage = "Requesting stream... it may sit here for like 15-20 seconds in a really awkward way... we're working on it"
} else if (isDownloading) {
loadStatusMessage = "Downloading stream... not long left now!"
}
return (
<div onMouseMove={this.handleMouseMove.bind(this)} onMouseLeave={this.handleMouseLeave.bind(this)} className={"video " + this.props.className + (isPlaying && readyToPlay ? " video--active" : " video--hidden")}>{
isPlaying ?
!readyToPlay ?
<span>this is the world's worst loading screen and we shipped our software with it anyway... <br /><br />{loadStatusMessage}</span> :
<VideoPlayer downloadPath={fileInfo.download_path} /> :
<div className="video__cover" style={{backgroundImage: 'url("' + metadata.thumbnail + '")'}}>
<WatchLink icon="icon-play" onWatchClick={this.onWatchClick.bind(this)} startPlaying={this.startPlaying.bind(this)} {...this.props}></WatchLink>
</div>
}</div>
);
}
}
class VideoPlayer extends React.PureComponent {
componentDidMount() {
const elem = this.refs.video
const {
downloadPath,
contentType,
} = this.props
const players = plyr.setup(elem)
players[0].play()
}
render() {
const {
downloadPath,
contentType,
} = this.props
return (
<video controls id="video" ref="video">
<source src={downloadPath} type={contentType} />
</video>
)
}
}
export default Video

View file

@ -33,3 +33,7 @@ export const SET_DRAFT_TRANSACTION_ADDRESS = 'SET_DRAFT_TRANSACTION_ADDRESS'
export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED' export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED'
export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED' export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED'
export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED' export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED'
// Content
export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED'
export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED'

View file

@ -0,0 +1,17 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
selectFeaturedContentByCategory
} from 'selectors/content'
import DiscoverPage from './view'
const select = (state) => ({
featuredContentByCategory: selectFeaturedContentByCategory(state),
})
const perform = (dispatch) => ({
})
export default connect(select, perform)(DiscoverPage)

View file

@ -1,27 +1,26 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbryio from 'lbryio.js';
import lighthouse from '../lighthouse.js'; import FileTile from 'component/fileTile';
import {FileTile} from '../component/file-tile.js'; import { FileTileStream } from 'component/fileTileStream'
import Link from 'component/link'; import {ToolTip} from 'component/tooltip.js';
import {ToolTip} from '../component/tooltip.js';
const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' + const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' +
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' + 'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
'"five" to put your content here!'); '"five" to put your content here!');
let FeaturedCategory = React.createClass({ const FeaturedCategory = (props) => {
render: function() { const {
return (<div className="card-row card-row--small"> category,
{ this.props.category ? names,
<h3 className="card-row__header">{this.props.category} } = props
{ this.props.category.match(/^community/i) ?
<ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header"/> return <div className="card-row card-row--small">
: '' }</h3> <h3 className="card-row__header">{category}
: '' } {category && category.match(/^community/i) && <ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header" />}
{ this.props.names.map((name) => { return <FileTile key={name} displayStyle="card" uri={name} /> }) } </h3>
</div>) {names.map(name => <FileTile key={name} displayStyle="card" uri={name} />)}
} </div>
}) }
let DiscoverPage = React.createClass({ let DiscoverPage = React.createClass({
getInitialState: function() { getInitialState: function() {

View file

@ -1,14 +1,13 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from 'lbry.js';
import lbryuri from '../lbryuri.js'; import lbryuri from 'lbryuri.js';
import Link from 'component/link'; import Link from 'component/link';
import FormField from '../component/form.js'; import {FormField} from 'component/form.js';
import {SubHeader} from '../component/header.js'; import SubHeader from '../component/sub-header';
import {FileTileStream} from '../component/file-tile.js'; import {FileTileStream} from 'component/fileTile';
import rewards from '../rewards.js'; import rewards from 'rewards.js';
import lbryio from '../lbryio.js'; import lbryio from 'lbryio.js';
import {BusyMessage, Thumbnail} from '../component/common.js'; import {BusyMessage, Thumbnail} from 'component/common.js';
export let FileListNav = React.createClass({ export let FileListNav = React.createClass({
render: function() { render: function() {

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import lbryio from '../lbryio.js'; import lbryio from '../lbryio.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link';
import Notice from '../component/notice.js'; import Notice from '../component/notice.js';
import {CreditAmount} from '../component/common.js'; import {CreditAmount} from '../component/common.js';
// //

View file

@ -5,7 +5,7 @@ import {CreditAmount, Icon} from '../component/common.js';
import rewards from '../rewards.js'; import rewards from '../rewards.js';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
import {WalletNav} from './wallet.js'; import {WalletNav} from './wallet.js';
import {RewardLink} from '../component/link.js'; import {RewardLink} from '../component/link';
const RewardTile = React.createClass({ const RewardTile = React.createClass({
propTypes: { propTypes: {

View file

@ -4,7 +4,7 @@ import lbryio from '../lbryio.js';
import lbryuri from '../lbryuri.js'; import lbryuri from '../lbryuri.js';
import lighthouse from '../lighthouse.js'; import lighthouse from '../lighthouse.js';
import {FileTile, FileTileStream} from '../component/file-tile.js'; import {FileTile, FileTileStream} from '../component/file-tile.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link';
import {ToolTip} from '../component/tooltip.js'; import {ToolTip} from '../component/tooltip.js';
import {BusyMessage} from '../component/common.js'; import {BusyMessage} from '../component/common.js';

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import {FormField, FormRow} from '../component/form.js'; import {FormField, FormRow} from '../component/form.js';
import {SubHeader} from '../component/header.js'; import {SubHeader} from '../component/sub-header.js';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
export let SettingsNav = React.createClass({ export let SettingsNav = React.createClass({

View file

@ -2,7 +2,7 @@ import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lighthouse from '../lighthouse.js'; import lighthouse from '../lighthouse.js';
import lbryuri from '../lbryuri.js'; import lbryuri from '../lbryuri.js';
import {Video} from '../page/watch.js' import Video from 'component/video'
import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js'; import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js';
import {FileActions} from '../component/file-actions.js'; import {FileActions} from '../component/file-actions.js';
import UriIndicator from '../component/channel-indicator.js'; import UriIndicator from '../component/channel-indicator.js';

31
ui/js/reducers/content.js Normal file
View file

@ -0,0 +1,31 @@
import * as types from 'constants/action_types'
const reducers = {}
const defaultState = {
}
reducers[types.FETCH_FEATURED_CONTENT_STARTED] = function(state, action) {
return Object.assign({}, state, {
fetchingFeaturedContent: true
})
}
reducers[types.FETCH_FEATURED_CONTENT_COMPLETED] = function(state, action) {
const {
uris
} = action.data
const newFeaturedContent = Object.assign({}, state.featuredContent, {
byCategory: uris,
})
return Object.assign({}, state, {
fetchingFeaturedContent: false,
featuredContent: newFeaturedContent
})
}
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -0,0 +1,37 @@
import { createSelector } from 'reselect'
import {
selectDaemonReady,
selectCurrentPage,
} from 'selectors/app'
export const _selectState = state => state.content || {}
export const selectFeaturedContent = createSelector(
_selectState,
(state) => state.featuredContent || {}
)
export const selectFeaturedContentByCategory = createSelector(
selectFeaturedContent,
(featuredContent) => featuredContent.byCategory || {}
)
export const selectFetchingFeaturedContent = createSelector(
_selectState,
(state) => !!state.fetchingFeaturedContent
)
export const shouldFetchFeaturedContent = createSelector(
selectDaemonReady,
selectCurrentPage,
selectFetchingFeaturedContent,
selectFeaturedContentByCategory,
(daemonReady, page, fetching, byCategory) => {
if (!daemonReady) return false
if (page != 'discover') return false
if (fetching) return false
if (Object.keys(byCategory).length != 0) return false
return true
}
)

View file

@ -6,6 +6,7 @@ import {
createLogger createLogger
} from 'redux-logger' } from 'redux-logger'
import appReducer from 'reducers/app'; import appReducer from 'reducers/app';
import contentReducer from 'reducers/content';
import walletReducer from 'reducers/wallet' import walletReducer from 'reducers/wallet'
function isFunction(object) { function isFunction(object) {
@ -18,6 +19,7 @@ function isNotFunction(object) {
const reducers = redux.combineReducers({ const reducers = redux.combineReducers({
app: appReducer, app: appReducer,
content: contentReducer,
wallet: walletReducer, wallet: walletReducer,
}); });

View file

@ -2,10 +2,16 @@ import {
shouldFetchTransactions, shouldFetchTransactions,
shouldGetReceiveAddress, shouldGetReceiveAddress,
} from 'selectors/wallet' } from 'selectors/wallet'
import {
shouldFetchFeaturedContent,
} from 'selectors/content'
import { import {
doFetchTransactions, doFetchTransactions,
doGetNewAddress, doGetNewAddress,
} from 'actions/wallet' } from 'actions/wallet'
import {
doFetchFeaturedContent,
} from 'actions/content'
const triggers = [] const triggers = []
@ -19,6 +25,11 @@ triggers.push({
action: doGetNewAddress action: doGetNewAddress
}) })
triggers.push({
selector: shouldFetchFeaturedContent,
action: doFetchFeaturedContent,
})
const runTriggers = function() { const runTriggers = function() {
triggers.forEach(function(trigger) { triggers.forEach(function(trigger) {
const state = app.store.getState(); const state = app.store.getState();