diff --git a/package.json b/package.json index f34b0e865..261c816c2 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "formik": "^0.10.4", "hast-util-sanitize": "^1.1.2", "keytar": "^4.2.1", - "lbry-redux": "lbryio/lbry-redux#421321a78397251589e5a890f4caa95e79975e2b", + "lbry-redux": "lbryio/lbry-redux#d1cee82af119c0c5f98ec27f94b2e7f61e34b54c", "localforage": "^1.7.1", "mammoth": "^1.4.6", "mime": "^2.3.1", diff --git a/src/renderer/component/common/form-components/form-field.jsx b/src/renderer/component/common/form-components/form-field.jsx index a38722d94..e2e1960d0 100644 --- a/src/renderer/component/common/form-components/form-field.jsx +++ b/src/renderer/component/common/form-components/form-field.jsx @@ -24,6 +24,9 @@ type Props = { stretch?: boolean, affixClass?: string, // class applied to prefix/postfix label firstInList?: boolean, // at the top of a list, no padding top + inputProps: { + disabled?: boolean, + }, }; export class FormField extends React.PureComponent { diff --git a/src/renderer/component/common/form-components/form-row.jsx b/src/renderer/component/common/form-components/form-row.jsx index 79f1b5cd0..e0d0959e1 100644 --- a/src/renderer/component/common/form-components/form-row.jsx +++ b/src/renderer/component/common/form-components/form-row.jsx @@ -9,6 +9,7 @@ type Props = { verticallyCentered?: boolean, stretch?: boolean, alignRight?: boolean, + centered?: boolean, }; export class FormRow extends React.PureComponent { @@ -17,7 +18,7 @@ export class FormRow extends React.PureComponent { }; render() { - const { children, padded, verticallyCentered, stretch, alignRight } = this.props; + const { children, padded, verticallyCentered, stretch, alignRight, centered } = this.props; return (
{ 'form-row--vertically-centered': verticallyCentered, 'form-row--stretch': stretch, 'form-row--right': alignRight, + 'form-row--centered': centered, })} > {children} diff --git a/src/renderer/component/fileCard/index.js b/src/renderer/component/fileCard/index.js index 3754e3ffa..95a7f9a67 100644 --- a/src/renderer/component/fileCard/index.js +++ b/src/renderer/component/fileCard/index.js @@ -8,7 +8,10 @@ import { makeSelectClaimIsMine, } from 'lbry-redux'; import { doNavigate } from 'redux/actions/navigation'; -import { selectRewardContentClaimIds } from 'redux/selectors/content'; +import { + selectRewardContentClaimIds, + makeSelectContentPositionForUri, +} from 'redux/selectors/content'; import { selectShowNsfw } from 'redux/selectors/settings'; import { selectPendingPublish } from 'redux/selectors/publish'; import FileCard from './view'; @@ -32,12 +35,14 @@ const select = (state, props) => { rewardedContentClaimIds: selectRewardContentClaimIds(state, props), ...fileCardInfo, pending: !!pendingPublish, + position: makeSelectContentPositionForUri(props.uri)(state), }; }; const perform = dispatch => ({ navigate: (path, params) => dispatch(doNavigate(path, params)), resolveUri: uri => dispatch(doResolveUri(uri)), + clearHistoryUri: uri => dispatch(doClearContentHistoryUri(uri)), }); export default connect( diff --git a/src/renderer/component/fileCard/view.jsx b/src/renderer/component/fileCard/view.jsx index f8dd8582c..0a9effbac 100644 --- a/src/renderer/component/fileCard/view.jsx +++ b/src/renderer/component/fileCard/view.jsx @@ -1,15 +1,16 @@ // @flow import * as React from 'react'; +import moment from 'moment'; import { normalizeURI, convertToShareLink } from 'lbry-redux'; import type { Claim, Metadata } from 'types/claim'; import CardMedia from 'component/cardMedia'; import TruncatedText from 'component/common/truncated-text'; import Icon from 'component/common/icon'; -import FilePrice from 'component/filePrice'; import UriIndicator from 'component/uriIndicator'; import * as icons from 'constants/icons'; import classnames from 'classnames'; -import { openCopyLinkMenu } from '../../util/contextMenu'; +import FilePrice from 'component/filePrice'; +import { openCopyLinkMenu } from 'util/contextMenu'; // TODO: iron these out type Props = { @@ -21,8 +22,10 @@ type Props = { rewardedContentClaimIds: Array, obscureNsfw: boolean, claimIsMine: boolean, - showPrice: boolean, pending?: boolean, + position: ?number, + lastViewed: ?number, + clearHistoryUri: string => void, /* eslint-disable react/no-unused-prop-types */ resolveUri: string => void, isResolvingUri: boolean, @@ -59,8 +62,10 @@ class FileCard extends React.PureComponent { rewardedContentClaimIds, obscureNsfw, claimIsMine, - showPrice, pending, + position, + clearHistoryUri, + showPrice, } = this.props; const shouldHide = !claimIsMine && !pending && obscureNsfw && metadata && metadata.nsfw; @@ -103,6 +108,7 @@ class FileCard extends React.PureComponent { {showPrice && } {isRewardContent && } {fileInfo && } + {position && }
diff --git a/src/renderer/component/fileDownloadLink/index.js b/src/renderer/component/fileDownloadLink/index.js index 23ed6b3f7..6d4334e01 100644 --- a/src/renderer/component/fileDownloadLink/index.js +++ b/src/renderer/component/fileDownloadLink/index.js @@ -7,8 +7,7 @@ import { makeSelectClaimForUri, } from 'lbry-redux'; import { doOpenFileInShell } from 'redux/actions/file'; -import { doPurchaseUri, doStartDownload } from 'redux/actions/content'; -import { doPause } from 'redux/actions/media'; +import { doPurchaseUri, doStartDownload, doSetPlayingUri } from 'redux/actions/content'; import FileDownloadLink from './view'; const select = (state, props) => ({ @@ -24,7 +23,7 @@ const perform = dispatch => ({ openInShell: path => dispatch(doOpenFileInShell(path)), purchaseUri: uri => dispatch(doPurchaseUri(uri)), restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)), - doPause: () => dispatch(doPause()), + pause: () => dispatch(doSetPlayingUri(null)), }); export default connect( diff --git a/src/renderer/component/fileDownloadLink/view.jsx b/src/renderer/component/fileDownloadLink/view.jsx index bcb352eb2..1ffe15690 100644 --- a/src/renderer/component/fileDownloadLink/view.jsx +++ b/src/renderer/component/fileDownloadLink/view.jsx @@ -22,7 +22,7 @@ type Props = { restartDownload: (string, number) => void, openInShell: string => void, purchaseUri: string => void, - doPause: () => void, + pause: () => void, }; class FileDownloadLink extends React.PureComponent { @@ -50,14 +50,13 @@ class FileDownloadLink extends React.PureComponent { purchaseUri, costInfo, loading, - doPause, - claim, + pause, } = this.props; const openFile = () => { if (fileInfo) { openInShell(fileInfo.download_path); - doPause(); + pause(); } }; diff --git a/src/renderer/component/fileViewer/index.js b/src/renderer/component/fileViewer/index.js index c21e5f5c1..3dca7eb02 100644 --- a/src/renderer/component/fileViewer/index.js +++ b/src/renderer/component/fileViewer/index.js @@ -2,9 +2,7 @@ import { connect } from 'react-redux'; import * as settings from 'constants/settings'; import { doChangeVolume } from 'redux/actions/app'; import { selectVolume } from 'redux/selectors/app'; -import { doPlayUri, doSetPlayingUri } from 'redux/actions/content'; -import { doPlay, doPause, savePosition } from 'redux/actions/media'; -import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; +import { doPlayUri, doSetPlayingUri, savePosition } from 'redux/actions/content'; import { makeSelectMetadataForUri, makeSelectContentTypeForUri, @@ -15,9 +13,9 @@ import { makeSelectDownloadingForUri, selectSearchBarFocused, } from 'lbry-redux'; +import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards'; import { makeSelectClientSetting, selectShowNsfw } from 'redux/selectors/settings'; -import { selectMediaPaused, makeSelectMediaPositionForUri } from 'redux/selectors/media'; -import { selectPlayingUri } from 'redux/selectors/content'; +import { selectPlayingUri, makeSelectContentPositionForUri } from 'redux/selectors/content'; import { selectFileInfoErrors } from 'redux/selectors/file_info'; import FileViewer from './view'; @@ -32,8 +30,7 @@ const select = (state, props) => ({ playingUri: selectPlayingUri(state), contentType: makeSelectContentTypeForUri(props.uri)(state), volume: selectVolume(state), - mediaPaused: selectMediaPaused(state), - mediaPosition: makeSelectMediaPositionForUri(props.uri)(state), + position: makeSelectContentPositionForUri(props.uri)(state), autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state), searchBarFocused: selectSearchBarFocused(state), fileInfoErrors: selectFileInfoErrors(state), @@ -43,10 +40,9 @@ const perform = dispatch => ({ play: uri => dispatch(doPlayUri(uri)), cancelPlay: () => dispatch(doSetPlayingUri(null)), changeVolume: volume => dispatch(doChangeVolume(volume)), - doPlay: () => dispatch(doPlay()), - doPause: () => dispatch(doPause()), - savePosition: (claimId, position) => dispatch(savePosition(claimId, position)), claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()), + savePosition: (claimId, outpoint, position) => + dispatch(savePosition(claimId, outpoint, position)), }); export default connect( diff --git a/src/renderer/component/fileViewer/internal/player.jsx b/src/renderer/component/fileViewer/internal/player.jsx index d76d0bcaf..10b47fd0e 100644 --- a/src/renderer/component/fileViewer/internal/player.jsx +++ b/src/renderer/component/fileViewer/internal/player.jsx @@ -27,9 +27,11 @@ class MediaPlayer extends React.PureComponent { this.toggleFullScreenVideo = this.toggleFullScreen.bind(this); } - componentWillReceiveProps(nextProps) { + componentDidUpdate(nextProps) { const el = this.refs.media.children[0]; - if (!this.props.paused && nextProps.paused && !el.paused) el.pause(); + if (this.props.playingUri && !nextProps.playingUri && !el.paused) { + el.pause(); + } } componentDidMount() { @@ -86,11 +88,15 @@ class MediaPlayer extends React.PureComponent { document.addEventListener('keydown', this.togglePlayListener); const mediaElement = this.media.children[0]; if (mediaElement) { - mediaElement.currentTime = position || 0; - mediaElement.addEventListener('play', () => this.props.doPlay()); - mediaElement.addEventListener('pause', () => this.props.doPause()); + if (position) { + mediaElement.currentTime = position; + } mediaElement.addEventListener('timeupdate', () => - this.props.savePosition(claim.claim_id, mediaElement.currentTime) + this.props.savePosition( + claim.claim_id, + `${claim.txid}:${claim.nout}`, + mediaElement.currentTime + ) ); mediaElement.addEventListener('click', this.togglePlayListener); mediaElement.addEventListener('loadedmetadata', loadedMetadata.bind(this), { @@ -136,7 +142,6 @@ class MediaPlayer extends React.PureComponent { if (mediaElement) { mediaElement.removeEventListener('click', this.togglePlayListener); } - this.props.doPause(); } toggleFullScreen(event) { diff --git a/src/renderer/component/fileViewer/view.jsx b/src/renderer/component/fileViewer/view.jsx index 3e51bab10..21451e123 100644 --- a/src/renderer/component/fileViewer/view.jsx +++ b/src/renderer/component/fileViewer/view.jsx @@ -34,11 +34,8 @@ type Props = { volume: number, claim: Claim, uri: string, - doPlay: () => void, - doPause: () => void, - savePosition: (string, number) => void, - mediaPaused: boolean, - mediaPosition: ?number, + savePosition: (string, string, number) => void, + position: ?number, className: ?string, obscureNsfw: boolean, play: string => void, @@ -202,11 +199,8 @@ class FileViewer extends React.PureComponent { volume, claim, uri, - doPlay, - doPause, savePosition, - mediaPaused, - mediaPosition, + position, className, obscureNsfw, mediaType, @@ -251,14 +245,12 @@ class FileViewer extends React.PureComponent { downloadCompleted={fileInfo.completed} changeVolume={changeVolume} volume={volume} - doPlay={doPlay} - doPause={doPause} savePosition={savePosition} claim={claim} uri={uri} - paused={mediaPaused} - position={mediaPosition} + position={position} startedPlayingCb={this.startedPlayingCb} + playingUri={playingUri} /> )} diff --git a/src/renderer/component/page/index.js b/src/renderer/component/page/index.js index f9111f85e..1cbffa8ce 100644 --- a/src/renderer/component/page/index.js +++ b/src/renderer/component/page/index.js @@ -4,11 +4,10 @@ import { selectPageTitle, selectIsBackDisabled, selectIsForwardDisabled, - selectNavLinks, } from 'lbry-redux'; import { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation'; import { doDownloadUpgrade } from 'redux/actions/app'; -import { selectIsUpgradeAvailable } from 'redux/selectors/app'; +import { selectIsUpgradeAvailable, selectNavLinks } from 'redux/selectors/app'; import { formatCredits } from 'util/formatCredits'; import Page from './view'; @@ -28,4 +27,7 @@ const perform = dispatch => ({ downloadUpgrade: () => dispatch(doDownloadUpgrade()), }); -export default connect(select, perform)(Page); +export default connect( + select, + perform +)(Page); diff --git a/src/renderer/component/router/view.jsx b/src/renderer/component/router/view.jsx index fa1a0fea3..c6cb09026 100644 --- a/src/renderer/component/router/view.jsx +++ b/src/renderer/component/router/view.jsx @@ -18,6 +18,7 @@ import InvitePage from 'page/invite'; import BackupPage from 'page/backup'; import SubscriptionsPage from 'page/subscriptions'; import SearchPage from 'page/search'; +import UserHistoryPage from 'page/userHistory'; const route = (props, page, routesMap) => { const component = routesMap[page]; @@ -53,6 +54,7 @@ const Router = props => { wallet: , subscriptions: , search: , + user_history: , }); }; diff --git a/src/renderer/component/sideBar/index.js b/src/renderer/component/sideBar/index.js index 6e3d03f2e..36fc0d811 100644 --- a/src/renderer/component/sideBar/index.js +++ b/src/renderer/component/sideBar/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { selectNavLinks } from 'lbry-redux'; +import { selectNavLinks } from 'redux/selectors/app'; import { selectNotifications } from 'redux/selectors/subscriptions'; import SideBar from './view'; diff --git a/src/renderer/component/userHistory/index.js b/src/renderer/component/userHistory/index.js new file mode 100644 index 000000000..1b77955b1 --- /dev/null +++ b/src/renderer/component/userHistory/index.js @@ -0,0 +1,27 @@ +import { connect } from 'react-redux'; +import { selectHistoryPageCount, makeSelectHistoryForPage } from 'redux/selectors/content'; +import { doNavigate } from 'redux/actions/navigation'; +import { selectCurrentParams, makeSelectCurrentParam } from 'lbry-redux'; +import { doClearContentHistoryUri } from 'redux/actions/content'; +import UserHistory from './view'; + +const select = state => { + const paramPage = Number(makeSelectCurrentParam('page')(state) || 0); + return { + pageCount: selectHistoryPageCount(state), + page: paramPage, + params: selectCurrentParams(state), + history: makeSelectHistoryForPage(paramPage)(state), + }; +}; + +const perform = dispatch => ({ + navigate: (path, params) => dispatch(doNavigate(path, params)), + clearHistoryUri: uri => dispatch(doClearContentHistoryUri(uri)), + +}); + +export default connect( + select, + perform +)(UserHistory); diff --git a/src/renderer/component/userHistory/view.jsx b/src/renderer/component/userHistory/view.jsx new file mode 100644 index 000000000..ae8f4b532 --- /dev/null +++ b/src/renderer/component/userHistory/view.jsx @@ -0,0 +1,167 @@ +// @flow +import * as React from 'react'; +import Button from 'component/button'; +import { FormField, FormRow } from 'component/common/form'; +import ReactPaginate from 'react-paginate'; +import UserHistoryItem from 'component/userHistoryItem'; + +type HistoryItem = { + uri: string, + lastViewed: number, +}; + +type Props = { + history: Array, + page: number, + pageCount: number, + navigate: (string, {}) => void, + clearHistoryUri: string => void, + params: { page: number }, +}; + +type State = { + itemsSelected: {}, +}; + +class UserHistoryPage extends React.PureComponent { + constructor() { + super(); + + this.state = { + itemsSelected: {}, + }; + + (this: any).selectAll = this.selectAll.bind(this); + (this: any).unselectAll = this.unselectAll.bind(this); + (this: any).removeSelected = this.removeSelected.bind(this); + } + + onSelect(uri: string) { + const { itemsSelected } = this.state; + + const newItemsSelected = { ...itemsSelected }; + if (itemsSelected[uri]) { + delete newItemsSelected[uri]; + } else { + newItemsSelected[uri] = true; + } + + this.setState({ + itemsSelected: { ...newItemsSelected }, + }); + } + + changePage(pageNumber: number) { + const { params } = this.props; + const newParams = { ...params, page: pageNumber }; + this.props.navigate('/user_history', newParams); + } + + paginate(e: SyntheticKeyboardEvent<*>) { + const pageFromInput = Number(e.currentTarget.value); + if ( + pageFromInput && + e.keyCode === 13 && + !Number.isNaN(pageFromInput) && + pageFromInput > 0 && + pageFromInput <= this.props.pageCount + ) { + this.changePage(pageFromInput); + } + } + + selectAll() { + const { history } = this.props; + const newSelectedState = {}; + history.forEach(({ uri }) => (newSelectedState[uri] = true)); + this.setState({ itemsSelected: newSelectedState }); + } + + unselectAll() { + this.setState({ + itemsSelected: {}, + }); + } + + removeSelected() { + const { clearHistoryUri } = this.props; + const { itemsSelected } = this.state; + + Object.keys(itemsSelected).forEach(uri => clearHistoryUri(uri)); + this.setState({ + itemsSelected: {}, + }); + } + + render() { + const { history, page, pageCount } = this.props; + const { itemsSelected } = this.state; + + const allSelected = Object.keys(itemsSelected).length === history.length; + const selectHandler = allSelected ? this.unselectAll : this.selectAll; + return ( + +
+ {Object.keys(itemsSelected).length ? ( +
+ {!!history.length && ( + + + {history.map(item => ( + { + this.onSelect(item.uri); + }} + /> + ))} + +
+ )} + {pageCount > 1 && ( + + this.changePage(e.selected)} + forcePage={page} + initialPage={page} + containerClassName="pagination" + /> + + this.paginate(e)} + prefix={__('Go to page:')} + type="text" + /> + + )} +
+ ); + } +} +export default UserHistoryPage; diff --git a/src/renderer/component/userHistoryItem/index.js b/src/renderer/component/userHistoryItem/index.js new file mode 100644 index 000000000..a2a00ac58 --- /dev/null +++ b/src/renderer/component/userHistoryItem/index.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { doResolveUri, makeSelectClaimForUri } from 'lbry-redux'; +import UserHistoryItem from './view'; + +const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), +}); + +const perform = dispatch => ({ + resolveUri: uri => dispatch(doResolveUri(uri)), +}); + +export default connect( + select, + perform +)(UserHistoryItem); diff --git a/src/renderer/component/userHistoryItem/view.jsx b/src/renderer/component/userHistoryItem/view.jsx new file mode 100644 index 000000000..de35a919b --- /dev/null +++ b/src/renderer/component/userHistoryItem/view.jsx @@ -0,0 +1,64 @@ +// @flow +import React from 'react'; +import type { Claim } from 'types/claim'; +import moment from 'moment'; +import classnames from 'classnames'; +import Button from 'component/button'; + +type Props = { + lastViewed: number, + uri: string, + claim: ?Claim, + selected: boolean, + onSelect: () => void, + resolveUri: string => void, +}; + +class UserHistoryItem extends React.PureComponent { + componentDidMount() { + const { claim, uri, resolveUri } = this.props; + + if (!claim) { + resolveUri(uri); + } + } + + render() { + const { lastViewed, selected, onSelect, claim } = this.props; + + let name; + let title; + let uri; + if (claim && claim.value && claim.value.stream) { + ({ name } = claim); + ({ title } = claim.value.stream.metadata); + uri = claim.permanent_url; + } + + return ( + + + + + {moment(lastViewed).from(moment())} + {title} + +