diff --git a/ui/js/actions/rewards.js b/ui/js/actions/rewards.js index 95ae3b25b..227ab7a4a 100644 --- a/ui/js/actions/rewards.js +++ b/ui/js/actions/rewards.js @@ -2,7 +2,7 @@ import * as types from "constants/action_types"; import * as modals from "constants/modal_types"; import lbryio from "lbryio"; import rewards from "rewards"; -import { selectRewardsByType } from "selectors/rewards"; +import { selectUnclaimedRewardsByType } from "selectors/rewards"; export function doRewardList() { return function(dispatch, getState) { @@ -13,7 +13,7 @@ export function doRewardList() { }); lbryio - .call("reward", "list", {}) + .call("reward", "list", { multiple_rewards_per_type: true }) .then(userRewards => { dispatch({ type: types.FETCH_REWARDS_COMPLETED, @@ -31,17 +31,9 @@ export function doRewardList() { export function doClaimRewardType(rewardType) { return function(dispatch, getState) { - const rewardsByType = selectRewardsByType(getState()), + const rewardsByType = selectUnclaimedRewardsByType(getState()), reward = rewardsByType[rewardType]; - if (reward) { - dispatch(doClaimReward(reward)); - } - }; -} - -export function doClaimReward(reward, saveError = false) { - return function(dispatch, getState) { if (reward.transaction_id) { //already claimed, do nothing return; @@ -72,7 +64,7 @@ export function doClaimReward(reward, saveError = false) { type: types.CLAIM_REWARD_FAILURE, data: { reward, - error: saveError ? error : null, + error: error ? error : null, }, }); }; @@ -87,26 +79,18 @@ export function doClaimEligiblePurchaseRewards() { return; } - const rewardsByType = selectRewardsByType(getState()); + const rewardsByType = selectUnclaimedRewardsByType(getState()); - let types = {}; - - types[rewards.TYPE_FIRST_STREAM] = false; - types[rewards.TYPE_FEATURED_DOWNLOAD] = false; - types[rewards.TYPE_MANY_DOWNLOADS] = false; - Object.values(rewardsByType).forEach(reward => { - if (types[reward.reward_type] === false && reward.transaction_id) { - types[reward.reward_type] = true; - } - }); - - let unclaimedType = Object.keys(types).find(type => { - return types[type] === false && type !== rewards.TYPE_FEATURED_DOWNLOAD; //handled below - }); - if (unclaimedType) { - dispatch(doClaimRewardType(unclaimedType)); + if (rewardsByType[rewards.TYPE_FIRST_STREAM]) { + dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM)); + } else { + [ + rewards.TYPE_MANY_DOWNLOADS, + rewards.TYPE_FEATURED_DOWNLOAD, + ].forEach(type => { + dispatch(doClaimRewardType(type)); + }); } - dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD)); }; } diff --git a/ui/js/component/linkTransaction/index.js b/ui/js/component/linkTransaction/index.js new file mode 100644 index 000000000..601927420 --- /dev/null +++ b/ui/js/component/linkTransaction/index.js @@ -0,0 +1,5 @@ +import React from "react"; +import { connect } from "react-redux"; +import Link from "./view"; + +export default connect(null, null)(Link); diff --git a/ui/js/component/linkTransaction/view.jsx b/ui/js/component/linkTransaction/view.jsx new file mode 100644 index 000000000..e1fe32159 --- /dev/null +++ b/ui/js/component/linkTransaction/view.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import Link from "component/link"; + +const LinkTransaction = props => { + const { id } = props; + const linkProps = Object.assign({}, props); + + linkProps.href = "https://explorer.lbry.io/#!/transaction/" + id; + linkProps.label = id.substr(0, 7); + + return ; +}; + +export default LinkTransaction; diff --git a/ui/js/component/rewardLink/index.js b/ui/js/component/rewardLink/index.js index cf53b2367..3375fbfdf 100644 --- a/ui/js/component/rewardLink/index.js +++ b/ui/js/component/rewardLink/index.js @@ -6,7 +6,7 @@ import { makeSelectIsRewardClaimPending, } from "selectors/rewards"; import { doNavigate } from "actions/app"; -import { doClaimReward, doClaimRewardClearError } from "actions/rewards"; +import { doClaimRewardType, doClaimRewardClearError } from "actions/rewards"; import RewardLink from "./view"; const makeSelect = () => { @@ -24,7 +24,7 @@ const makeSelect = () => { }; const perform = dispatch => ({ - claimReward: reward => dispatch(doClaimReward(reward, true)), + claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)), clearError: reward => dispatch(doClaimRewardClearError(reward)), navigate: path => dispatch(doNavigate(path)), }); diff --git a/ui/js/component/rewardListClaimed/index.js b/ui/js/component/rewardListClaimed/index.js new file mode 100644 index 000000000..095121cad --- /dev/null +++ b/ui/js/component/rewardListClaimed/index.js @@ -0,0 +1,10 @@ +import React from "react"; +import { connect } from "react-redux"; +import { selectClaimedRewards } from "selectors/rewards"; +import RewardListClaimed from "./view"; + +const select = state => ({ + rewards: selectClaimedRewards(state), +}); + +export default connect(select, null)(RewardListClaimed); diff --git a/ui/js/component/rewardListClaimed/view.jsx b/ui/js/component/rewardListClaimed/view.jsx new file mode 100644 index 000000000..f568348cd --- /dev/null +++ b/ui/js/component/rewardListClaimed/view.jsx @@ -0,0 +1,44 @@ +import React from "react"; +import LinkTransaction from "component/linkTransaction"; + +const RewardListClaimed = props => { + const { rewards } = props; + + if (!rewards || !rewards.length) { + return null; + } + + return ( +
+

Claimed Rewards

+
+ + + + + + + + + + + {rewards.map(reward => { + return ( + + + + + + + ); + })} + +
{__("Title")}{__("Amount")}{__("Transaction")}{__("Date")}
{reward.reward_title}{reward.reward_amount} + {reward.created_at.replace("Z", " ").replace("T", " ")} +
+
+
+ ); +}; + +export default RewardListClaimed; diff --git a/ui/js/component/rewardTile/index.js b/ui/js/component/rewardTile/index.js new file mode 100644 index 000000000..2a1f0f485 --- /dev/null +++ b/ui/js/component/rewardTile/index.js @@ -0,0 +1,5 @@ +import React from "react"; +import { connect } from "react-redux"; +import RewardTile from "./view"; + +export default connect(null, null)(RewardTile); diff --git a/ui/js/component/rewardTile/view.jsx b/ui/js/component/rewardTile/view.jsx new file mode 100644 index 000000000..4545d0d46 --- /dev/null +++ b/ui/js/component/rewardTile/view.jsx @@ -0,0 +1,28 @@ +import React from "react"; +import { CreditAmount, Icon } from "component/common"; +import RewardLink from "component/rewardLink"; + +const RewardTile = props => { + const { reward } = props; + + const claimed = !!reward.transaction_id; + + return ( +
+
+
+ +

{reward.reward_title}

+
+
+ {claimed + ? {__("Reward claimed.")} + : } +
+
{reward.reward_description}
+
+
+ ); +}; + +export default RewardTile; diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx index 501b521a5..db114b861 100644 --- a/ui/js/component/router/view.jsx +++ b/ui/js/component/router/view.jsx @@ -13,6 +13,7 @@ import DeveloperPage from "page/developer.js"; import RewardsPage from "page/rewards"; import FileListDownloaded from "page/fileListDownloaded"; import FileListPublished from "page/fileListPublished"; +import TransactionHistoryPage from "page/transactionHistory"; import ChannelPage from "page/channel"; import SearchPage from "page/search"; import AuthPage from "page/auth"; @@ -36,6 +37,7 @@ const Router = props => { discover: , downloaded: , help: , + history: , invite: , publish: , published: , diff --git a/ui/js/component/transactionList/view.jsx b/ui/js/component/transactionList/view.jsx index db913eea6..cf63900dd 100644 --- a/ui/js/component/transactionList/view.jsx +++ b/ui/js/component/transactionList/view.jsx @@ -1,5 +1,6 @@ import React from "react"; -import { Address, BusyMessage, CreditAmount } from "component/common"; +import { BusyMessage } from "component/common"; +import LinkTransaction from "component/linkTransaction"; class TransactionList extends React.PureComponent { componentWillMount() { @@ -26,12 +27,7 @@ class TransactionList extends React.PureComponent { : {__("(Transaction pending)")}} - - {item.id.substr(0, 7)} - + ); diff --git a/ui/js/modal/modalCreditIntro/index.js b/ui/js/modal/modalCreditIntro/index.js index 0626eee8f..577e1803e 100644 --- a/ui/js/modal/modalCreditIntro/index.js +++ b/ui/js/modal/modalCreditIntro/index.js @@ -7,7 +7,7 @@ import { selectUserIsRewardApproved } from "selectors/user"; import { makeSelectHasClaimedReward, makeSelectRewardByType, - selectTotalRewardValue, + selectUnclaimedRewardValue, } from "selectors/rewards"; import * as settings from "constants/settings"; import ModalCreditIntro from "./view"; @@ -19,7 +19,7 @@ const select = (state, props) => { return { isRewardApproved: selectUserIsRewardApproved(state), reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), - totalRewardValue: selectTotalRewardValue(state), + totalRewardValue: selectUnclaimedRewardValue(state), }; }; diff --git a/ui/js/page/rewards/index.js b/ui/js/page/rewards/index.js index 8a7a29d98..db479ff15 100644 --- a/ui/js/page/rewards/index.js +++ b/ui/js/page/rewards/index.js @@ -1,23 +1,18 @@ import React from "react"; import { connect } from "react-redux"; import { - makeSelectRewardByType, selectFetchingRewards, - selectRewards, + selectUnclaimedRewards, } from "selectors/rewards"; import { selectUser } from "selectors/user"; import { doAuthNavigate, doNavigate } from "actions/app"; import { doRewardList } from "actions/rewards"; -import rewards from "rewards"; import RewardsPage from "./view"; const select = (state, props) => { - const selectReward = makeSelectRewardByType(); - return { fetching: selectFetchingRewards(state), - rewards: selectRewards(state), - newUserReward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), + rewards: selectUnclaimedRewards(state), user: selectUser(state), }; }; diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx index 1e7bba459..8276eb92c 100644 --- a/ui/js/page/rewards/view.jsx +++ b/ui/js/page/rewards/view.jsx @@ -1,31 +1,9 @@ import React from "react"; -import { BusyMessage, CreditAmount, Icon } from "component/common"; +import { BusyMessage } from "component/common"; +import RewardListClaimed from "component/rewardListClaimed"; +import RewardTile from "component/rewardTile"; import SubHeader from "component/subHeader"; import Link from "component/link"; -import RewardLink from "component/rewardLink"; - -const RewardTile = props => { - const { reward } = props; - - const claimed = !!reward.transaction_id; - - return ( -
-
-
- -

{reward.reward_title}

-
-
- {claimed - ? {__("Reward claimed.")} - : } -
-
{reward.reward_description}
-
-
- ); -}; class RewardsPage extends React.PureComponent { componentDidMount() { @@ -44,32 +22,8 @@ class RewardsPage extends React.PureComponent { } } - render() { - const { doAuth, fetching, navigate, rewards, user } = this.props; - - let content, cardHeader; - - if (fetching) { - content = ( -
- -
- ); - } else if (rewards.length > 0) { - content = ( -
- {rewards.map(reward => - - )} -
- ); - } else { - content = ( -
- {__("Failed to load rewards.")} -
- ); - } + renderPageHeader() { + const { doAuth, navigate, user } = this.props; if (user && !user.is_reward_approved) { if ( @@ -77,7 +31,7 @@ class RewardsPage extends React.PureComponent { !user.has_verified_email || !user.is_identity_verified ) { - cardHeader = ( + return (

@@ -90,7 +44,7 @@ class RewardsPage extends React.PureComponent {

); } else { - cardHeader = ( + return (

{__( @@ -122,25 +76,49 @@ class RewardsPage extends React.PureComponent {

); } - } else if (user === null) { - cardHeader = ( -
-
-

- {__( - "This application is unable to earn rewards due to an authentication failure." - )} -

-
+ } + } + + renderUnclaimedRewards() { + const { fetching, rewards, user } = this.props; + + if (fetching) { + return ( +
+
); - } + } else if (user === null) { + return ( +
+

+ {__( + "This application is unable to earn rewards due to an authentication failure." + )} +

+
+ ); + } else if (!rewards || rewards.length <= 0) { + return ( +
+ {__("Failed to load rewards.")} +
+ ); + } else { + return rewards.map(reward => + + ); + } + } + + render() { return (
- {cardHeader &&
{cardHeader}
} - {content} + {this.renderPageHeader()} + {this.renderUnclaimedRewards()} + {}
); } diff --git a/ui/js/page/transactionHistory/index.js b/ui/js/page/transactionHistory/index.js new file mode 100644 index 000000000..01fdf2a64 --- /dev/null +++ b/ui/js/page/transactionHistory/index.js @@ -0,0 +1,5 @@ +import React from "react"; +import { connect } from "react-redux"; +import WalletPage from "./view"; + +export default connect(null, null)(WalletPage); diff --git a/ui/js/page/transactionHistory/view.jsx b/ui/js/page/transactionHistory/view.jsx new file mode 100644 index 000000000..50efd014c --- /dev/null +++ b/ui/js/page/transactionHistory/view.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import SubHeader from "component/subHeader"; +import TransactionList from "component/transactionList"; + +const TransactionHistoryPage = props => { + return ( +
+ + +
+ ); +}; + +export default TransactionHistoryPage; diff --git a/ui/js/reducers/rewards.js b/ui/js/reducers/rewards.js index 0994d730b..42279d01d 100644 --- a/ui/js/reducers/rewards.js +++ b/ui/js/reducers/rewards.js @@ -3,7 +3,8 @@ import * as types from "constants/action_types"; const reducers = {}; const defaultState = { fetching: false, - rewardsByType: {}, + claimedRewardsById: {}, //id => reward + unclaimedRewardsByType: {}, claimPendingByType: {}, claimErrorsByType: {}, }; @@ -17,11 +18,19 @@ reducers[types.FETCH_REWARDS_STARTED] = function(state, action) { reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) { const { userRewards } = action.data; - const rewardsByType = {}; - userRewards.forEach(reward => (rewardsByType[reward.reward_type] = reward)); + let unclaimedRewards = {}, + claimedRewards = {}; + userRewards.forEach(reward => { + if (reward.transaction_id) { + claimedRewards[reward.id] = reward; + } else { + unclaimedRewards[reward.reward_type] = reward; + } + }); return Object.assign({}, state, { - rewardsByType: rewardsByType, + claimedRewardsById: claimedRewards, + unclaimedRewardsByType: unclaimedRewards, fetching: false, }); }; @@ -55,16 +64,22 @@ reducers[types.CLAIM_REWARD_STARTED] = function(state, action) { reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) { const { reward } = action.data; - const existingReward = state.rewardsByType[reward.reward_type]; + let unclaimedRewardsByType = Object.assign({}, state.unclaimedRewardsByType); + const existingReward = unclaimedRewardsByType[reward.reward_type]; + delete state.unclaimedRewardsByType[reward.reward_type]; + const newReward = Object.assign({}, reward, { reward_title: existingReward.reward_title, reward_description: existingReward.reward_description, }); - const rewardsByType = Object.assign({}, state.rewardsByType); - rewardsByType[reward.reward_type] = newReward; + let claimedRewardsById = Object.assign({}, state.claimedRewardsById); + claimedRewardsById[reward.id] = newReward; - const newState = Object.assign({}, state, { rewardsByType }); + const newState = Object.assign({}, state, { + unclaimedRewardsByType, + claimedRewardsById, + }); return setClaimRewardState(newState, newReward, false, ""); }; diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js index 959c80c99..82403a5c8 100644 --- a/ui/js/selectors/app.js +++ b/ui/js/selectors/app.js @@ -136,6 +136,7 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => { // This contains intentional fall throughs switch (page) { case "wallet": + case "history": case "send": case "receive": case "invite": @@ -143,6 +144,7 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => { case "backup": return { wallet: __("Overview"), + history: __("History"), send: __("Send"), receive: __("Receive"), invite: __("Invites"), diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index bddb7714e..88d4de730 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -3,13 +3,23 @@ import { selectUser } from "selectors/user"; const _selectState = state => state.rewards || {}; -export const selectRewardsByType = createSelector( +export const selectUnclaimedRewardsByType = createSelector( _selectState, - state => state.rewardsByType || {} + state => state.unclaimedRewardsByType ); -export const selectRewards = createSelector( - selectRewardsByType, +export const selectClaimedRewardsById = createSelector( + _selectState, + state => state.claimedRewardsById +); + +export const selectClaimedRewards = createSelector( + selectClaimedRewardsById, + byId => Object.values(byId) || [] +); + +export const selectUnclaimedRewards = createSelector( + selectUnclaimedRewardsByType, byType => Object.values(byType) || [] ); @@ -23,10 +33,12 @@ export const selectFetchingRewards = createSelector( state => !!state.fetching ); -export const selectTotalRewardValue = createSelector(selectRewards, rewards => - rewards.reduce((sum, reward) => { - return sum + reward.reward_amount; - }, 0) +export const selectUnclaimedRewardValue = createSelector( + selectUnclaimedRewards, + rewards => + rewards.reduce((sum, reward) => { + return sum + reward.reward_amount; + }, 0) ); export const selectHasClaimedReward = (state, props) => { @@ -65,7 +77,7 @@ export const makeSelectClaimRewardError = () => { }; const selectRewardByType = (state, props) => { - return selectRewardsByType(state)[props.reward_type]; + return selectUnclaimedRewardsByType(state)[props.reward_type]; }; export const makeSelectRewardByType = () => {