User history #1846

Merged
daovist merged 17 commits from user-history into master 2018-09-07 07:21:38 +02:00
36 changed files with 716 additions and 161 deletions

View file

@ -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",

View file

@ -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<Props> {

View file

@ -9,6 +9,7 @@ type Props = {
verticallyCentered?: boolean,
stretch?: boolean,
alignRight?: boolean,
centered?: boolean,
};
export class FormRow extends React.PureComponent<Props> {
@ -17,7 +18,7 @@ export class FormRow extends React.PureComponent<Props> {
};
render() {
const { children, padded, verticallyCentered, stretch, alignRight } = this.props;
const { children, padded, verticallyCentered, stretch, alignRight, centered } = this.props;
return (
<div
className={classnames('form-row', {
@ -25,6 +26,7 @@ export class FormRow extends React.PureComponent<Props> {
'form-row--vertically-centered': verticallyCentered,
'form-row--stretch': stretch,
'form-row--right': alignRight,
'form-row--centered': centered,
})}
>
{children}

View file

@ -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(

View file

@ -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<string>,
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<Props> {
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<Props> {
{showPrice && <FilePrice hideFree uri={uri} />}
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{fileInfo && <Icon icon={icons.LOCAL} />}
{position && <Icon icon={icons.REFRESH} />}
</div>
</div>
</section>

View file

@ -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(

View file

@ -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<Props> {
@ -50,14 +50,13 @@ class FileDownloadLink extends React.PureComponent<Props> {
purchaseUri,
costInfo,
loading,
doPause,
claim,
pause,
} = this.props;
const openFile = () => {
if (fileInfo) {
openInShell(fileInfo.download_path);
doPause();
pause();
}
};

View file

@ -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(

View file

@ -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) {

View file

@ -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<Props> {
volume,
claim,
uri,
doPlay,
doPause,
savePosition,
mediaPaused,
mediaPosition,
position,
className,
obscureNsfw,
mediaType,
@ -251,14 +245,12 @@ class FileViewer extends React.PureComponent<Props> {
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}
/>
)}
</div>

View file

@ -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);

View file

@ -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: <WalletPage params={params} />,
subscriptions: <SubscriptionsPage params={params} />,
search: <SearchPage {...params} />,
user_history: <UserHistoryPage {...params} />,
});
};

View file

@ -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';

View file

@ -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);

View file

@ -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<HistoryItem>,
page: number,
pageCount: number,
navigate: (string, {}) => void,
clearHistoryUri: string => void,
params: { page: number },
};
type State = {
itemsSelected: {},
};
class UserHistoryPage extends React.PureComponent<Props, State> {
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 (
<React.Fragment>
<div className="card__actions card__actions--between">
{Object.keys(itemsSelected).length ? (
<Button button="link" label={__('Delete')} onClick={this.removeSelected} />
) : (
<span>
{/* Using an empty span so spacing stays the same if the button isn't rendered */}
</span>
)}
<Button
button="link"
label={allSelected ? __('Cancel') : __('Select All')}
onClick={selectHandler}
/>
</div>
{!!history.length && (
<table className="card--section table table--stretch table--history">
<tbody>
{history.map(item => (
<UserHistoryItem
key={item.uri}
uri={item.uri}
lastViewed={item.lastViewed}
selected={!!itemsSelected[item.uri]}
onSelect={() => {
this.onSelect(item.uri);
}}
/>
))}
</tbody>
</table>
)}
{pageCount > 1 && (
<FormRow padded verticallyCentered centered>
<ReactPaginate
pageCount={pageCount}
pageRangeDisplayed={2}
previousLabel=""
nextLabel=""
activeClassName="pagination__item--selected"
pageClassName="pagination__item"
previousClassName="pagination__item pagination__item--previous"
nextClassName="pagination__item pagination__item--next"
breakClassName="pagination__item pagination__item--break"
marginPagesDisplayed={2}
onPageChange={e => this.changePage(e.selected)}
forcePage={page}
initialPage={page}
containerClassName="pagination"
/>
<FormField
className="paginate-channel"
onKeyUp={e => this.paginate(e)}
prefix={__('Go to page:')}
type="text"
/>
</FormRow>
)}
</React.Fragment>
);
}
}
export default UserHistoryPage;

View file

@ -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);

View file

@ -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<Props> {
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 (
<tr
onClick={onSelect}
className={classnames({
history__selected: selected,
})}
>
<td>
<input checked={selected} type="checkbox" onClick={onSelect} />
</td>
<td>{moment(lastViewed).from(moment())}</td>
<td>{title}</td>
<td>
<Button
tourniquet
button="link"
label={name ? `lbry://${name}` : `lbry://...`}
navigate="/show"
navigateParams={{ uri }}
/>
</td>
</tr>
);
}
}
export default UserHistoryItem;

View file

@ -73,7 +73,11 @@ export const CREATE_CHANNEL_COMPLETED = 'CREATE_CHANNEL_COMPLETED';
export const PUBLISH_STARTED = 'PUBLISH_STARTED';
export const PUBLISH_COMPLETED = 'PUBLISH_COMPLETED';
export const PUBLISH_FAILED = 'PUBLISH_FAILED';
export const SET_PLAYING_URI = 'PLAY_URI';
export const SET_PLAYING_URI = 'SET_PLAYING_URI';
export const SET_CONTENT_POSITION = 'SET_CONTENT_POSITION';
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
// Files
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED';
@ -181,14 +185,6 @@ export const FETCH_SUBSCRIPTIONS_START = 'FETCH_SUBSCRIPTIONS_START';
export const FETCH_SUBSCRIPTIONS_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL';
export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS';
// Video controls
export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE';
// Media controls
export const MEDIA_PLAY = 'MEDIA_PLAY';
export const MEDIA_PAUSE = 'MEDIA_PAUSE';
export const MEDIA_POSITION = 'MEDIA_POSITION';
// Publishing
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';
export const UPDATE_PUBLISH_FORM = 'UPDATE_PUBLISH_FORM';

View file

@ -0,0 +1 @@
export const HISTORY_ITEMS_PER_PAGE = 50;

View file

@ -4,6 +4,7 @@ import { doNavigate } from 'redux/actions/navigation';
import { selectRewardContentClaimIds, selectPlayingUri } from 'redux/selectors/content';
import { doCheckSubscription } from 'redux/actions/subscriptions';
import { doSetClientSetting } from 'redux/actions/settings';
import { doSetContentHistoryItem } from 'redux/actions/content';
import {
doFetchFileInfo,
doFetchCostInfoForUri,
@ -17,7 +18,6 @@ import {
} from 'lbry-redux';
import { selectShowNsfw, makeSelectClientSetting } from 'redux/selectors/settings';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectMediaPaused } from 'redux/selectors/media';
import { doPrepareEdit } from 'redux/actions/publish';
import FilePage from './view';
@ -31,7 +31,6 @@ const select = (state, props) => ({
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
subscriptions: selectSubscriptions(state),
playingUri: selectPlayingUri(state),
isPaused: selectMediaPaused(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state),
});
@ -44,6 +43,10 @@ const perform = dispatch => ({
openModal: (modal, props) => dispatch(doNotify(modal, props)),
prepareEdit: (publishData, uri) => dispatch(doPrepareEdit(publishData, uri)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
});
export default connect(select, perform)(FilePage);
export default connect(
select,
perform
)(FilePage);

View file

@ -46,8 +46,7 @@ type Props = {
prepareEdit: ({}, string) => void,
checkSubscription: (uri: string) => void,
subscriptions: Array<Subscription>,
setClientSetting: (string, boolean | string) => void,
autoplay: boolean,
setViewed: string => void,
};
class FilePage extends React.Component<Props> {
@ -71,7 +70,7 @@ class FilePage extends React.Component<Props> {
}
componentDidMount() {
const { uri, fileInfo, fetchFileInfo, fetchCostInfo } = this.props;
const { uri, fileInfo, fetchFileInfo, fetchCostInfo, setViewed } = this.props;
if (fileInfo === undefined) {
fetchFileInfo(uri);
@ -81,13 +80,19 @@ class FilePage extends React.Component<Props> {
fetchCostInfo(uri);
this.checkSubscription(this.props);
setViewed(uri);
}
componentWillReceiveProps(nextProps: Props) {
const { fetchFileInfo, uri } = this.props;
const { fetchFileInfo, uri, setViewed } = this.props;
if (nextProps.fileInfo === undefined) {
fetchFileInfo(uri);
}
if (uri !== nextProps.uri) {
setViewed(nextProps.uri);
}
}
onAutoplayChange(event: SyntheticInputEvent<*>) {
@ -129,7 +134,7 @@ class FilePage extends React.Component<Props> {
const { title, thumbnail } = metadata;
const { height, channel_name: channelName, value } = claim;
const { PLAYABLE_MEDIA_TYPES, PREVIEW_MEDIA_TYPES } = FilePage;
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const isRewardContent = (rewardedContentClaimIds || []).includes(claim.claim_id);
const shouldObscureThumbnail = obscureNsfw && metadata.nsfw;
const fileName = fileInfo ? fileInfo.file_name : null;
const mediaType = getMediaType(contentType, fileName);

View file

@ -0,0 +1,4 @@
import { connect } from 'react-redux';
import UserHistoryPage from './view';
export default connect(null, null)(UserHistoryPage);

View file

@ -0,0 +1,15 @@
// @flow
import React from 'react';
import Page from 'component/page';
import UserHistory from 'component/userHistory';
class UserHistoryPage extends React.PureComponent {
render() {
return (
<Page>
<UserHistory />
</Page>
);
}
}
export default UserHistoryPage;

View file

@ -16,7 +16,7 @@ import Native from 'native';
import { doFetchRewardedContent } from 'redux/actions/content';
import { doFetchDaemonSettings } from 'redux/actions/settings';
import { doAuthNavigate } from 'redux/actions/navigation';
import { doPause } from 'redux/actions/media';
import { doAuthenticate } from 'redux/actions/user';
import { doCheckSubscriptionsInit } from 'redux/actions/subscriptions';
import {
selectIsUpgradeSkipped,
@ -28,7 +28,6 @@ import {
selectRemoteVersion,
selectUpgradeTimer,
} from 'redux/selectors/app';
import { doAuthenticate } from 'redux/actions/user';
import { lbrySettings as config } from 'package.json';
const { autoUpdater } = remote.require('electron-updater');
@ -109,9 +108,6 @@ export function doDownloadUpgradeRequested() {
return (dispatch, getState) => {
const state = getState();
// Pause video if needed
dispatch(doPause());
const autoUpdateDeclined = selectAutoUpdateDeclined(state);
if (['win32', 'darwin'].includes(process.platform)) {

View file

@ -1,3 +1,4 @@
// @flow
import * as NOTIFICATION_TYPES from 'constants/notification_types';
import { ipcRenderer } from 'electron';
import Lbryio from 'lbryio';
@ -494,3 +495,45 @@ export function doPublish(params) {
Lbry.publishDeprecated(params, null, success, failure);
});
}
export function savePosition(claimId: string, outpoint: string, position: number) {
return dispatch => {
dispatch({
type: ACTIONS.SET_CONTENT_POSITION,
data: { claimId, outpoint, position },
});
};
}
export function doSetContentHistoryItem(uri: string) {
return dispatch => {
dispatch({
type: ACTIONS.SET_CONTENT_LAST_VIEWED,
data: { uri, lastViewed: Date.now() },
});
};
}
export function doClearContentHistoryUri(uri: string) {
return dispatch => {
dispatch({
type: ACTIONS.CLEAR_CONTENT_HISTORY_URI,
data: { uri },
});
};
}
export function doClearContentHistoryAll() {
return dispatch => {
dispatch({ type: ACTIONS.CLEAR_CONTENT_HISTORY_ALL });
};
}
export function doSetHistoryPage(page) {
return dispatch => {
dispatch({
type: ACTIONS.SET_CONTENT_HISTORY_PAGE,
data: { page },
});
};
}

View file

@ -1,28 +0,0 @@
// @flow
import * as actions from 'constants/action_types';
import type { Dispatch } from 'redux/reducers/media';
export const doPlay = () => (dispatch: Dispatch) =>
dispatch({
type: actions.MEDIA_PLAY,
});
export const doPause = () => (dispatch: Dispatch) =>
dispatch({
type: actions.MEDIA_PAUSE,
});
export function savePosition(claimId: String, position: Number) {
return function(dispatch: Dispatch, getState: Function) {
const state = getState();
const claim = state.claims.byId[claimId];
const outpoint = `${claim.txid}:${claim.nout}`;
dispatch({
type: actions.MEDIA_POSITION,
data: {
outpoint,
position,
},
});
};
}

View file

@ -3,9 +3,10 @@ import * as ACTIONS from 'constants/action_types';
const reducers = {};
const defaultState = {
playingUri: null,
currentlyIsPlaying: false,
rewardedContentClaimIds: [],
channelClaimCounts: {},
positions: {},
history: [],
};
reducers[ACTIONS.FETCH_FEATURED_CONTENT_STARTED] = state =>
@ -80,6 +81,46 @@ reducers[ACTIONS.FETCH_CHANNEL_CLAIM_COUNT_COMPLETED] = (state, action) => {
});
};
reducers[ACTIONS.SET_CONTENT_POSITION] = (state, action) => {
const { claimId, outpoint, position } = action.data;
return {
...state,
positions: {
...state.positions,
[claimId]: {
...state.positions[claimId],
[outpoint]: position,
},
},
};
};
reducers[ACTIONS.SET_CONTENT_LAST_VIEWED] = (state, action) => {
const { uri, lastViewed } = action.data;
const { history } = state;
const historyObj = { uri, lastViewed };
const index = history.findIndex(i => i.uri === uri);
const newHistory =
index === -1
? [historyObj].concat(history)
: [historyObj].concat(history.slice(0, index), history.slice(index + 1));
return { ...state, history: [...newHistory] };
};
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => {
const { uri } = action.data;
const { history } = state;
const index = history.findIndex(i => i.uri === uri);
return index === -1
? state
: {
...state,
history: history.slice(0, index).concat(history.slice(index + 1)),
};
};
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = state => ({ ...state, history: [] });
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

View file

@ -1,41 +0,0 @@
// @flow
import * as actions from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
export type MediaState = {
paused: Boolean,
positions: {
[string]: number,
},
};
export type Action = any;
export type Dispatch = (action: Action) => any;
const defaultState = { paused: true, positions: {} };
export default handleActions(
{
[actions.MEDIA_PLAY]: (state: MediaState, action: Action) => ({
...state,
paused: false,
}),
[actions.MEDIA_PAUSE]: (state: MediaState, action: Action) => ({
...state,
paused: true,
}),
[actions.MEDIA_POSITION]: (state: MediaState, action: Action) => {
const { outpoint, position } = action.data;
return {
...state,
positions: {
...state.positions,
[outpoint]: position,
},
};
},
},
defaultState
);

View file

@ -3,7 +3,7 @@ import * as ACTIONS from 'constants/action_types';
const getCurrentPath = () => {
const { hash } = document.location;
if (hash !== '') return hash.replace(/^#/, '');
return '/discover';
return '/user_history';
};
const reducers = {};

View file

@ -1,4 +1,5 @@
import { createSelector } from 'reselect';
import { selectCurrentPage, selectHistoryStack } from 'lbry-redux';
export const selectState = state => state.app || {};
@ -86,3 +87,162 @@ export const selectCurrentLanguage = createSelector(
export const selectVolume = createSelector(selectState, state => state.volume);
export const selectUpgradeTimer = createSelector(selectState, state => state.checkUpgradeTimer);
export const selectNavLinks = createSelector(
selectCurrentPage,
selectHistoryStack,
(currentPage, historyStack) => {
const isWalletPage = page =>
page === 'wallet' ||
page === 'send' ||
page === 'getcredits' ||
page === 'rewards' ||
page === 'history' ||
page === 'invite' ||
page === 'backup';
const isMyLbryPage = page =>
page === 'downloaded' || page === 'published' || page === 'user_history';
const previousStack = historyStack.slice().reverse();
const getPreviousSubLinkPath = checkIfValidPage => {
for (let i = 0; i < previousStack.length; i += 1) {
const currentStackItem = previousStack[i];
// Trim off the "/" from the path
const pageInStack = currentStackItem.path.slice(1);
if (checkIfValidPage(pageInStack)) {
return currentStackItem.path;
}
}
return undefined;
};
// Gets the last active sublink in a section
const getActiveSublink = category => {
if (category === 'wallet') {
const previousPath = getPreviousSubLinkPath(isWalletPage);
return previousPath || '/wallet';
} else if (category === 'myLbry') {
const previousPath = getPreviousSubLinkPath(isMyLbryPage);
return previousPath || '/downloaded';
}
return undefined;
};
const isCurrentlyWalletPage = isWalletPage(currentPage);
const isCurrentlyMyLbryPage = isMyLbryPage(currentPage);
const walletSubLinks = [
{
label: 'Overview',
path: '/wallet',
active: currentPage === 'wallet',
},
{
label: 'Send & Receive',
path: '/send',
active: currentPage === 'send',
},
{
label: 'Transactions',
path: '/history',
active: currentPage === 'history',
},
{
label: 'Get Credits',
path: '/getcredits',
active: currentPage === 'getcredits',
},
{
label: 'Rewards',
path: '/rewards',
active: currentPage === 'rewards',
},
{
label: 'Invites',
path: '/invite',
active: currentPage === 'invite',
},
{
label: 'Backup',
path: '/backup',
active: currentPage === 'backup',
},
];
const myLbrySubLinks = [
{
label: 'Downloads',
path: '/downloaded',
active: currentPage === 'downloaded',
},
{
label: 'Publishes',
path: '/published',
active: currentPage === 'published',
},
{
label: 'History',
path: '/user_history',
active: currentPage === 'user_history',
},
];
const navLinks = {
primary: [
{
label: 'Explore',
path: '/discover',
active: currentPage === 'discover',
icon: 'Compass',
},
{
label: 'Subscriptions',
path: '/subscriptions',
active: currentPage === 'subscriptions',
icon: 'AtSign',
},
],
secondary: [
{
label: 'Wallet',
icon: 'CreditCard',
subLinks: walletSubLinks,
path: isCurrentlyWalletPage ? '/wallet' : getActiveSublink('wallet'),
active: isWalletPage(currentPage),
},
{
label: 'My LBRY',
icon: 'Folder',
subLinks: myLbrySubLinks,
path: isCurrentlyMyLbryPage ? '/downloaded' : getActiveSublink('myLbry'),
active: isMyLbryPage(currentPage),
},
{
label: 'Publish',
icon: 'UploadCloud',
path: '/publish',
active: currentPage === 'publish',
},
{
label: 'Settings',
icon: 'Settings',
path: '/settings',
active: currentPage === 'settings',
},
{
label: 'Help',
path: '/help',
icon: 'HelpCircle',
active: currentPage === 'help',
},
],
};
return navLinks;
}
);

View file

@ -1,4 +1,6 @@
import { createSelector } from 'reselect';
import { makeSelectClaimForUri, selectClaimsByUri } from 'lbry-redux';
import { HISTORY_ITEMS_PER_PAGE } from 'constants/content';
export const selectState = state => state.content || {};
@ -29,3 +31,40 @@ export const selectRewardContentClaimIds = createSelector(
selectState,
state => state.rewardedContentClaimIds
);
export const makeSelectContentPositionForUri = uri =>
createSelector(selectState, makeSelectClaimForUri(uri), (state, claim) => {
if (!claim) {
return null;
}
const outpoint = `${claim.txid}:${claim.nout}`;
const id = claim.claim_id;
return state.positions[id] ? state.positions[id][outpoint] : null;
});
export const selectHistoryPageCount = createSelector(selectState, state =>
Math.ceil(state.history.length / HISTORY_ITEMS_PER_PAGE)
);
export const makeSelectHistoryForPage = page =>
createSelector(selectState, selectClaimsByUri, (state, claimsByUri) => {
const left = page * HISTORY_ITEMS_PER_PAGE;
const historyItems = state.history.slice(left, left + HISTORY_ITEMS_PER_PAGE);
// See if we have the claim info for the uris in your history
// If not, it will need to be fetched in the component
return historyItems.map((historyItem) => {
const { uri, lastViewed } = historyItem;
const claimAtUri = claimsByUri[uri];
if (claimAtUri) {
return { lastViewed, uri, ...claimAtUri }
} else {
console.log("jsut returning item")
return historyItem;
}
})
});
export const makeSelectHistoryForUri = uri =>
createSelector(selectState, state => state.history.find(i => i.uri === uri));

View file

@ -1,12 +0,0 @@
import { createSelector } from 'reselect';
import { makeSelectClaimForUri } from 'lbry-redux';
const selectState = state => state.media || {};
export const selectMediaPaused = createSelector(selectState, state => state.paused);
export const makeSelectMediaPositionForUri = uri =>
createSelector(selectState, makeSelectClaimForUri(uri), (state, claim) => {
const outpoint = `${claim.txid}:${claim.nout}`;
return state.positions[outpoint] || null;
});

View file

@ -19,6 +19,10 @@
align-items: center;
}
&.form-row--centered {
justify-content: center;
}
&.form-row--stretch {
flex: 1;
}

View file

@ -54,10 +54,10 @@ table.table,
tr {
border-bottom: var(--table-item-border);
padding: 8px 0;
&:nth-child(even):not(.odd) {
&:nth-child(even) {
background-color: var(--table-item-odd);
}
&:nth-child(odd):not(.even) {
&:nth-child(odd) {
background-color: var(--table-item-even);
}
&.thead {
@ -107,3 +107,48 @@ table.table--transactions {
width: 15%;
}
}
table.table--history {
margin-top: $spacing-vertical * 1/3;
tbody {
tr {
&:nth-child(even),
&:nth-child(odd) {
background-color: var(--table-item-even);
&.history__selected {
color: red;
background-color: var(--table-item-odd);
}
}
}
td {
cursor: default;
padding: $spacing-vertical * 1/3 0;
}
td:nth-of-type(1) {
width: 7.5%;
}
td:nth-of-type(2) {
width: 17.5%;
}
td:nth-of-type(3) {
width: 40%;
max-width: 30vw;
padding-right: $spacing-vertical * 2/3;
}
td:nth-of-type(4) {
width: 30%;
}
td:nth-of-type(3),
td:nth-of-type(4) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}

View file

@ -17,7 +17,6 @@ import settingsReducer from 'redux/reducers/settings';
import userReducer from 'redux/reducers/user';
import shapeShiftReducer from 'redux/reducers/shape_shift';
import subscriptionsReducer from 'redux/reducers/subscriptions';
import mediaReducer from 'redux/reducers/media';
import publishReducer from 'redux/reducers/publish';
import { persistStore, autoRehydrate } from 'redux-persist';
import createCompressor from 'redux-persist-transform-compress';
@ -69,7 +68,6 @@ const reducers = combineReducers({
user: userReducer,
shapeShift: shapeShiftReducer,
subscriptions: subscriptionsReducer,
media: mediaReducer,
publish: publishReducer,
notifications: notificationsReducer,
blacklist: blacklistReducer,
@ -102,15 +100,16 @@ const store = createStore(
const compressor = createCompressor();
const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
const contentFilter = createFilter('content', ['positions', 'history']);
// We only need to persist the receiveAddress for the wallet
const walletFilter = createFilter('wallet', ['receiveAddress']);
const persistOptions = {
whitelist: ['claims', 'subscriptions', 'publish', 'wallet'],
whitelist: ['claims', 'subscriptions', 'publish', 'wallet', 'content'],
// Order is important. Needs to be compressed last or other transforms can't
// read the data
transforms: [saveClaimsFilter, subscriptionsFilter, walletFilter, compressor],
transforms: [saveClaimsFilter, subscriptionsFilter, walletFilter, contentFilter, compressor],
debounce: 10000,
storage: localForage,
};

View file

@ -5655,9 +5655,9 @@ lazy-val@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc"
lbry-redux@lbryio/lbry-redux#421321a78397251589e5a890f4caa95e79975e2b:
lbry-redux@lbryio/lbry-redux#d1cee82af119c0c5f98ec27f94b2e7f61e34b54c:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/421321a78397251589e5a890f4caa95e79975e2b"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/d1cee82af119c0c5f98ec27f94b2e7f61e34b54c"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"