diff --git a/CHANGELOG.md b/CHANGELOG.md index f77c8679d..578fcb338 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,19 +8,24 @@ Web UI version numbers should always match the corresponding version of LBRY App ## [Unreleased] ### Added + * Added an Invites area inside of the Wallet. This allows users to invite others and shows the status of all past invites (including all invite data from the past year). * Added a forward button and improved history behavior. Back/forward disable when unusable. - * Added a new component, `FormFieldPrice` which is now used in Publish and Settings. + * Added new summary components for rewards and invites to the Wallet landing page. + * Added past history of rewards to the rewards page. * Added wallet backup guide reference. + * Added a new widget for setting prices (`FormFieldPrice`), used in Publish and Settings. ### Changed * Updated to daemon [0.15](https://github.com/lbryio/lbry/releases). Most relevant changes for app are improved announcing of content and a fix for the daemon getting stuck running. * Continued to refine first-run process, process for new users, and introducing people to LBRY and LBRY credits. - * Changed the default price settings. + * Changed Wallet landing page to summarize status of other areas. Refactored wallet and transaction logic. + * Added icons to missing page, improved icon and title logic. + * Changed the default price settings for priced publishes. * When an "Open" button is clicked on a show page, if the file fails to open, the app will try to open the file's folder. + * Updated several packages and fixed warnings in build process (all but the [fsevents warning](https://github.com/yarnpkg/yarn/issues/3738), which is a rather dramatic debate) * Some form field refactoring as we take baby steps towards form sanity. * Replaced confusing placeholder text from email input. * Refactored modal and settings logic. - * Updated several packages and fixed warnings in build process (all but the [fsevents warning](https://github.com/yarnpkg/yarn/issues/3738), which is a rather dramatic debate) ### Fixed * Tiles will no longer be blurry on hover (Windows only bug) diff --git a/app/main.js b/app/main.js index 27c8f72c8..b572eba82 100644 --- a/app/main.js +++ b/app/main.js @@ -51,7 +51,7 @@ let daemonStopRequested = false; let readyToQuit = false; // If we receive a URI to open from an external app but there's no window to -// send it to, it's cached in this variable. +// sendCredits it to, it's cached in this variable. let openUri = null; function processRequestedUri(uri) { diff --git a/ui/dist/font/FontAwesome.otf b/ui/dist/font/FontAwesome.otf index f7936cc1e..401ec0f36 100644 Binary files a/ui/dist/font/FontAwesome.otf and b/ui/dist/font/FontAwesome.otf differ diff --git a/ui/dist/font/fontawesome-webfont.eot b/ui/dist/font/fontawesome-webfont.eot index 33b2bb800..e9f60ca95 100644 Binary files a/ui/dist/font/fontawesome-webfont.eot and b/ui/dist/font/fontawesome-webfont.eot differ diff --git a/ui/dist/font/fontawesome-webfont.svg b/ui/dist/font/fontawesome-webfont.svg index 1ee89d436..855c845e5 100644 --- a/ui/dist/font/fontawesome-webfont.svg +++ b/ui/dist/font/fontawesome-webfont.svg @@ -1,565 +1,2671 @@ - - + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ui/dist/font/fontawesome-webfont.ttf b/ui/dist/font/fontawesome-webfont.ttf index ed9372f8e..35acda2fa 100644 Binary files a/ui/dist/font/fontawesome-webfont.ttf and b/ui/dist/font/fontawesome-webfont.ttf differ diff --git a/ui/dist/font/fontawesome-webfont.woff b/ui/dist/font/fontawesome-webfont.woff index 8b280b98f..400014a4b 100644 Binary files a/ui/dist/font/fontawesome-webfont.woff and b/ui/dist/font/fontawesome-webfont.woff differ diff --git a/ui/dist/font/fontawesome-webfont.woff2 b/ui/dist/font/fontawesome-webfont.woff2 index 3311d5851..4d13fc604 100644 Binary files a/ui/dist/font/fontawesome-webfont.woff2 and b/ui/dist/font/fontawesome-webfont.woff2 differ diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js index 34cceed37..ec2af78f0 100644 --- a/ui/js/actions/app.js +++ b/ui/js/actions/app.js @@ -26,6 +26,10 @@ const { lbrySettings: config } = require("../../../app/package.json"); export function doNavigate(path, params = {}, options = {}) { return function(dispatch, getState) { + if (!path) { + return; + } + let url = path; if (params) url = `${url}?${toQueryString(params)}`; diff --git a/ui/js/actions/rewards.js b/ui/js/actions/rewards.js index 95ae3b25b..bc7e13e4a 100644 --- a/ui/js/actions/rewards.js +++ b/ui/js/actions/rewards.js @@ -2,7 +2,8 @@ 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"; +import { selectUserIsRewardApproved } from "selectors/user"; export function doRewardList() { return function(dispatch, getState) { @@ -13,7 +14,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,22 +32,23 @@ export function doRewardList() { export function doClaimRewardType(rewardType) { return function(dispatch, getState) { - const rewardsByType = selectRewardsByType(getState()), - reward = rewardsByType[rewardType]; + const state = getState(), + rewardsByType = selectUnclaimedRewardsByType(state), + reward = rewardsByType[rewardType], + userIsRewardApproved = selectUserIsRewardApproved(state); - if (reward) { - dispatch(doClaimReward(reward)); - } - }; -} - -export function doClaimReward(reward, saveError = false) { - return function(dispatch, getState) { if (reward.transaction_id) { //already claimed, do nothing return; } + if (!userIsRewardApproved) { + return dispatch({ + type: types.OPEN_MODAL, + data: { modal: modals.REWARD_APPROVAL_REQUIRED }, + }); + } + dispatch({ type: types.CLAIM_REWARD_STARTED, data: { reward }, @@ -70,10 +72,7 @@ export function doClaimReward(reward, saveError = false) { const failure = error => { dispatch({ type: types.CLAIM_REWARD_FAILURE, - data: { - reward, - error: saveError ? error : null, - }, + data: { reward, error }, }); }; @@ -83,30 +82,24 @@ export function doClaimReward(reward, saveError = false) { export function doClaimEligiblePurchaseRewards() { return function(dispatch, getState) { - if (!lbryio.enabled) { + const state = getState(), + rewardsByType = selectUnclaimedRewardsByType(state), + userIsRewardApproved = selectUserIsRewardApproved(state); + + if (!userIsRewardApproved || !lbryio.enabled) { return; } - const rewardsByType = selectRewardsByType(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/actions/user.js b/ui/js/actions/user.js index 8a0e59ef1..360707a43 100644 --- a/ui/js/actions/user.js +++ b/ui/js/actions/user.js @@ -1,7 +1,7 @@ import * as types from "constants/action_types"; import * as modals from "constants/modal_types"; import lbryio from "lbryio"; -import { doOpenModal } from "actions/app"; +import { doOpenModal, doShowSnackBar } from "actions/app"; import { doRewardList, doClaimRewardType } from "actions/rewards"; import { selectEmailToVerify, selectUser } from "selectors/user"; import rewards from "rewards"; @@ -19,6 +19,7 @@ export function doAuthenticate() { data: { user }, }); dispatch(doRewardList()); + dispatch(doFetchInviteStatus()); }) .catch(error => { dispatch(doOpenModal(modals.AUTHENTICATION_FAILURE)); @@ -172,3 +173,62 @@ export function doFetchAccessToken() { lbryio.getAuthToken().then(success); }; } + +export function doFetchInviteStatus() { + return function(dispatch, getState) { + dispatch({ + type: types.USER_INVITE_STATUS_FETCH_STARTED, + }); + + lbryio + .call("user", "invite_status") + .then(status => { + dispatch({ + type: types.USER_INVITE_STATUS_FETCH_SUCCESS, + data: { + invitesRemaining: status.invites_remaining + ? status.invites_remaining + : 0, + invitees: status.invitees, + }, + }); + }) + .catch(error => { + dispatch({ + type: types.USER_INVITE_STATUS_FETCH_FAILURE, + data: { error }, + }); + }); + }; +} + +export function doUserInviteNew(email) { + return function(dispatch, getState) { + dispatch({ + type: types.USER_INVITE_NEW_STARTED, + }); + + lbryio + .call("user", "invite", { email: email }, "post") + .then(invite => { + dispatch({ + type: types.USER_INVITE_NEW_SUCCESS, + data: { email }, + }); + + dispatch( + doShowSnackBar({ + message: __("Invite sent to %s", email), + }) + ); + + dispatch(doFetchInviteStatus()); + }) + .catch(error => { + dispatch({ + type: types.USER_INVITE_NEW_FAILURE, + data: { error }, + }); + }); + }; +} diff --git a/ui/js/app.js b/ui/js/app.js index 2504b72e8..ecbb5f83c 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -22,7 +22,6 @@ const app = { i18n: i18n, logs: logs, log: function(message) { - console.log(message); logs.push(message); }, }; diff --git a/ui/js/component/app/index.js b/ui/js/component/app/index.js index 252fdf783..fdacf035d 100644 --- a/ui/js/component/app/index.js +++ b/ui/js/component/app/index.js @@ -1,30 +1,22 @@ import React from "react"; import { connect } from "react-redux"; -import { selectCurrentModal } from "selectors/app"; import { doCheckUpgradeAvailable, - doOpenModal, doAlertError, doRecordScroll, } from "actions/app"; import { doFetchRewardedContent } from "actions/content"; - import { doUpdateBalance } from "actions/wallet"; -import { selectWelcomeModalAcknowledged } from "selectors/app"; import { selectUser } from "selectors/user"; import App from "./view"; -import * as modals from "constants/modal_types"; const select = (state, props) => ({ - modal: selectCurrentModal(state), - isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state), user: selectUser(state), }); const perform = dispatch => ({ alertError: errorList => dispatch(doAlertError(errorList)), checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()), - openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)), updateBalance: balance => dispatch(doUpdateBalance(balance)), fetchRewardedContent: () => dispatch(doFetchRewardedContent()), recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)), diff --git a/ui/js/component/cardVerify/view.jsx b/ui/js/component/cardVerify/view.jsx index 0432a69e1..4494cbf6c 100644 --- a/ui/js/component/cardVerify/view.jsx +++ b/ui/js/component/cardVerify/view.jsx @@ -165,7 +165,7 @@ class CardVerify extends React.Component { render() { return ( 0) { + amountText = "+" + amountText; + } } return ( diff --git a/ui/js/component/fileActions/view.jsx b/ui/js/component/fileActions/view.jsx index 1a2ed7931..73b48ba3b 100644 --- a/ui/js/component/fileActions/view.jsx +++ b/ui/js/component/fileActions/view.jsx @@ -167,18 +167,20 @@ class FileActions extends React.PureComponent {
{content} {showMenu - ? - openInFolder(fileInfo)} - label={openInFolderMessage} - /> - openModal(modals.CONFIRM_FILE_REMOVE)} - label={__("Remove...")} - /> - + ?
+ + openInFolder(fileInfo)} + label={openInFolderMessage} + /> + openModal(modals.CONFIRM_FILE_REMOVE)} + label={__("Remove...")} + /> + +
: ""} ({ + invitees: selectUserInvitees(state), + isPending: selectUserInviteStatusIsPending(state), +}); + +const perform = dispatch => ({}); + +export default connect(select, perform)(InviteList); diff --git a/ui/js/component/inviteList/view.jsx b/ui/js/component/inviteList/view.jsx new file mode 100644 index 000000000..a233f045c --- /dev/null +++ b/ui/js/component/inviteList/view.jsx @@ -0,0 +1,70 @@ +import React from "react"; +import { Icon } from "component/common"; +import RewardLink from "component/rewardLink"; +import rewards from "rewards.js"; + +class InviteList extends React.PureComponent { + render() { + const { invitees } = this.props; + + if (!invitees) { + return null; + } + + return ( +
+
+

{__("Invite History")}

+
+
+ {invitees.length === 0 && + {__("You haven't invited anyone.")} } + {invitees.length > 0 && + + + + + + + + + + {invitees.map((invitee, index) => { + return ( + + + + + + ); + })} + +
+ {__("Invitee Email")} + + {__("Invite Status")} + + {__("Reward")} +
{invitee.email} + {invitee.invite_accepted + ? + : {__("unused")}} + + {invitee.invite_reward_claimed + ? + : invitee.invite_accepted + ? + : + {__("unclaimable")} + } +
} +
+
+ ); + } +} + +export default InviteList; diff --git a/ui/js/component/inviteNew/index.js b/ui/js/component/inviteNew/index.js new file mode 100644 index 000000000..b31213b3f --- /dev/null +++ b/ui/js/component/inviteNew/index.js @@ -0,0 +1,29 @@ +import React from "react"; +import { connect } from "react-redux"; +import InviteNew from "./view"; +import { + selectUserInvitesRemaining, + selectUserInviteNewIsPending, + selectUserInviteNewErrorMessage, +} from "selectors/user"; +import rewards from "rewards"; +import { makeSelectRewardAmountByType } from "selectors/rewards"; + +import { doUserInviteNew } from "actions/user"; + +const select = state => { + const selectReward = makeSelectRewardAmountByType(); + + return { + errorMessage: selectUserInviteNewErrorMessage(state), + invitesRemaining: selectUserInvitesRemaining(state), + isPending: selectUserInviteNewIsPending(state), + rewardAmount: selectReward(state, { reward_type: rewards.TYPE_REFERRAL }), + }; +}; + +const perform = dispatch => ({ + inviteNew: email => dispatch(doUserInviteNew(email)), +}); + +export default connect(select, perform)(InviteNew); diff --git a/ui/js/component/inviteNew/view.jsx b/ui/js/component/inviteNew/view.jsx new file mode 100644 index 000000000..3f37e9c1b --- /dev/null +++ b/ui/js/component/inviteNew/view.jsx @@ -0,0 +1,100 @@ +import React from "react"; +import { BusyMessage, CreditAmount } from "component/common"; +import Link from "component/link"; +import { FormRow } from "component/form.js"; + +class FormInviteNew extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + email: "", + }; + } + + handleEmailChanged(event) { + this.setState({ + email: event.target.value, + }); + } + + handleSubmit(event) { + event.preventDefault(); + this.props.inviteNew(this.state.email); + } + + render() { + const { errorMessage, isPending } = this.props; + + return ( +
+ { + this.handleEmailChanged(event); + }} + /> +
+ { + this.handleSubmit(event); + }} + /> +
+ + ); + } +} + +class InviteNew extends React.PureComponent { + render() { + const { + errorMessage, + invitesRemaining, + inviteNew, + inviteStatusIsPending, + isPending, + rewardAmount, + } = this.props; + + return ( +
+
+ +

+ {__("Invite a Friend")} +

+
+ {/* +
+ {invitesRemaining > 0 && +

{__("You have %s invites remaining.", invitesRemaining)}

} + {invitesRemaining <= 0 && +

{__("You have no invites.")}

} +
*/} +
+

+ {__( + "Or an enemy. Or your cousin Jerry, who you're kind of unsure about." + )} +

+ +
+
+ ); + } +} + +export default InviteNew; diff --git a/ui/js/component/link/index.js b/ui/js/component/link/index.js index 601927420..f8134f7ff 100644 --- a/ui/js/component/link/index.js +++ b/ui/js/component/link/index.js @@ -1,5 +1,10 @@ import React from "react"; import { connect } from "react-redux"; +import { doNavigate } from "actions/app"; import Link from "./view"; -export default connect(null, null)(Link); +const perform = dispatch => ({ + doNavigate: path => dispatch(doNavigate(path)), +}); + +export default connect(null, perform)(Link); diff --git a/ui/js/component/link/view.jsx b/ui/js/component/link/view.jsx index f385225f3..6760c6cd3 100644 --- a/ui/js/component/link/view.jsx +++ b/ui/js/component/link/view.jsx @@ -5,7 +5,6 @@ const Link = props => { const { href, title, - onClick, style, label, icon, @@ -13,6 +12,8 @@ const Link = props => { button, disabled, children, + navigate, + doNavigate, } = props; const className = @@ -21,6 +22,12 @@ const Link = props => { (button ? " button-block button-" + button + " button-set-item" : "") + (disabled ? " disabled" : ""); + const onClick = !props.onClick && navigate + ? () => { + doNavigate(navigate); + } + : props.onClick; + let content; if (children) { content = children; 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/publishForm/view.jsx b/ui/js/component/publishForm/view.jsx index 39bce0d4d..4ce8039f8 100644 --- a/ui/js/component/publishForm/view.jsx +++ b/ui/js/component/publishForm/view.jsx @@ -16,6 +16,7 @@ class PublishForm extends React.PureComponent { this._requiredFields = ["name", "bid", "meta_title", "tosAgree"]; this._defaultCopyrightNotice = "All rights reserved."; + this._defaultPaidPrice = 0.01; this.state = { rawName: "", @@ -318,7 +319,9 @@ class PublishForm extends React.PureComponent { handleFeePrefChange(feeEnabled) { this.setState({ isFee: feeEnabled, - feeAmount: this.state.feeAmount == "" ? "5.00" : this.state.feeAmount, + feeAmount: this.state.feeAmount == "" + ? this._defaultPaidPrice + : this.state.feeAmount, }); } @@ -786,7 +789,6 @@ class PublishForm extends React.PureComponent { ref="bid" type="number" step="0.01" - min="0" label={__("Deposit")} postfix="LBC" onChange={event => { 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/rewardLink/view.jsx b/ui/js/component/rewardLink/view.jsx index 1f36dd86e..9eeee17e2 100644 --- a/ui/js/component/rewardLink/view.jsx +++ b/ui/js/component/rewardLink/view.jsx @@ -9,15 +9,18 @@ const RewardLink = props => { claimReward, clearError, errorMessage, + label, isPending, } = props; return (
{ claimReward(reward); }} 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/rewardSummary/index.js b/ui/js/component/rewardSummary/index.js new file mode 100644 index 000000000..390395472 --- /dev/null +++ b/ui/js/component/rewardSummary/index.js @@ -0,0 +1,11 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doNavigate } from "actions/app"; +import { selectUnclaimedRewardValue } from "selectors/rewards"; +import RewardSummary from "./view"; + +const select = state => ({ + unclaimedRewardAmount: selectUnclaimedRewardValue(state), +}); + +export default connect(select, null)(RewardSummary); diff --git a/ui/js/component/rewardSummary/view.jsx b/ui/js/component/rewardSummary/view.jsx new file mode 100644 index 000000000..d96fe79aa --- /dev/null +++ b/ui/js/component/rewardSummary/view.jsx @@ -0,0 +1,29 @@ +import React from "react"; +import Link from "component/link"; +import { CreditAmount } from "component/common"; + +const RewardSummary = props => { + const { balance, unclaimedRewardAmount } = props; + + return ( +
+
+

{__("Rewards")}

+
+
+ {unclaimedRewardAmount > 0 && +

+ You have{" "} + in + unclaimed rewards. +

} +
+
+ + +
+
+ ); +}; + +export default RewardSummary; 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..027fc484f --- /dev/null +++ b/ui/js/component/rewardTile/view.jsx @@ -0,0 +1,37 @@ +import React from "react"; +import { CreditAmount, Icon } from "component/common"; +import RewardLink from "component/rewardLink"; +import Link from "component/link"; +import rewards from "rewards"; + +const RewardTile = props => { + const { reward } = props; + + const claimed = !!reward.transaction_id; + + return ( +
+
+
+ +

{reward.reward_title}

+
+
{reward.reward_description}
+
+ {reward.reward_type == rewards.TYPE_REFERRAL && + } + {reward.reward_type !== rewards.TYPE_REFERRAL && + (claimed + ? {__("Reward claimed.")} + : )} +
+
+
+ ); +}; + +export default RewardTile; diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx index fa26f61eb..db114b861 100644 --- a/ui/js/component/router/view.jsx +++ b/ui/js/component/router/view.jsx @@ -4,16 +4,20 @@ import HelpPage from "page/help"; import ReportPage from "page/report.js"; import StartPage from "page/start.js"; import WalletPage from "page/wallet"; -import ShowPage from "page/showPage"; +import ReceiveCreditsPage from "page/receiveCredits"; +import SendCreditsPage from "page/sendCredits"; +import ShowPage from "page/show"; import PublishPage from "page/publish"; import DiscoverPage from "page/discover"; 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"; +import InvitePage from "page/invite"; import BackupPage from "page/backup"; const route = (page, routesMap) => { @@ -33,13 +37,15 @@ const Router = props => { discover: , downloaded: , help: , + history: , + invite: , publish: , published: , - receive: , + receive: , report: , rewards: , search: , - send: , + send: , settings: , show: , start: , diff --git a/ui/js/component/transactionList/index.js b/ui/js/component/transactionList/index.js index 7e15a67b4..9e2d9ec6b 100644 --- a/ui/js/component/transactionList/index.js +++ b/ui/js/component/transactionList/index.js @@ -1,21 +1,5 @@ import React from "react"; import { connect } from "react-redux"; -import { doFetchTransactions } from "actions/wallet"; -import { - selectBalance, - selectTransactionItems, - selectIsFetchingTransactions, -} from "selectors/wallet"; - import TransactionList from "./view"; -const select = state => ({ - fetchingTransactions: selectIsFetchingTransactions(state), - transactionItems: selectTransactionItems(state), -}); - -const perform = dispatch => ({ - fetchTransactions: () => dispatch(doFetchTransactions()), -}); - -export default connect(select, perform)(TransactionList); +export default connect(null, null)(TransactionList); diff --git a/ui/js/component/transactionList/view.jsx b/ui/js/component/transactionList/view.jsx index db913eea6..0ad7f4eeb 100644 --- a/ui/js/component/transactionList/view.jsx +++ b/ui/js/component/transactionList/view.jsx @@ -1,73 +1,57 @@ import React from "react"; -import { Address, BusyMessage, CreditAmount } from "component/common"; +import LinkTransaction from "component/linkTransaction"; +import { CreditAmount } from "component/common"; -class TransactionList extends React.PureComponent { - componentWillMount() { - this.props.fetchTransactions(); - } - - render() { - const { fetchingTransactions, transactionItems } = this.props; - - const rows = []; - if (transactionItems.length > 0) { - transactionItems.forEach(function(item) { - rows.push( - - {(item.amount > 0 ? "+" : "") + item.amount} - - {item.date - ? item.date.toLocaleDateString() - : {__("(Transaction pending)")}} - - - {item.date - ? item.date.toLocaleTimeString() - : {__("(Transaction pending)")}} - - - - {item.id.substr(0, 7)} - - - - ); - }); - } +const TransactionList = props => { + const { emptyMessage, transactions } = props; + if (!transactions || !transactions.length) { return ( -
-
-

{__("Transaction History")}

-
-
- {fetchingTransactions && - } - {!fetchingTransactions && rows.length === 0 - ?
{__("You have no transactions.")}
- : ""} - {rows.length > 0 - ? - - - - - - - - - - {rows} - -
{__("Amount")}{__("Date")}{__("Time")}{__("Transaction")}
- : ""} -
-
+
+ {emptyMessage || __("No transactions to list.")} +
); } -} + + return ( + + + + + + + + + + {transactions.map(item => { + return ( + + + + + + ); + })} + +
{__("Date")}{__("Amount")}{__("Transaction")}
+ {item.date + ? item.date.toLocaleDateString() + + " " + + item.date.toLocaleTimeString() + : + {__("(Transaction pending)")} + } + + {" "} + + +
+ ); +}; export default TransactionList; diff --git a/ui/js/component/transactionListRecent/index.js b/ui/js/component/transactionListRecent/index.js new file mode 100644 index 000000000..bea2fdac1 --- /dev/null +++ b/ui/js/component/transactionListRecent/index.js @@ -0,0 +1,23 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doFetchTransactions } from "actions/wallet"; +import { + selectBalance, + selectRecentTransactions, + selectHasTransactions, + selectIsFetchingTransactions, +} from "selectors/wallet"; + +import TransactionListRecent from "./view"; + +const select = state => ({ + fetchingTransactions: selectIsFetchingTransactions(state), + transactions: selectRecentTransactions(state), + hasTransactions: selectHasTransactions(state), +}); + +const perform = dispatch => ({ + fetchTransactions: () => dispatch(doFetchTransactions()), +}); + +export default connect(select, perform)(TransactionListRecent); diff --git a/ui/js/component/transactionListRecent/view.jsx b/ui/js/component/transactionListRecent/view.jsx new file mode 100644 index 000000000..82c09995d --- /dev/null +++ b/ui/js/component/transactionListRecent/view.jsx @@ -0,0 +1,41 @@ +import React from "react"; +import { BusyMessage } from "component/common"; +import Link from "component/link"; +import TransactionList from "component/transactionList"; + +class TransactionListRecent extends React.PureComponent { + componentWillMount() { + this.props.fetchTransactions(); + } + + render() { + const { fetchingTransactions, hasTransactions, transactions } = this.props; + + return ( +
+
+

{__("Recent Transactions")}

+
+
+ {fetchingTransactions && + } + {!fetchingTransactions && + } +
+ {hasTransactions && +
+ +
} +
+ ); + } +} + +export default TransactionListRecent; diff --git a/ui/js/component/userEmailNew/index.js b/ui/js/component/userEmailNew/index.js index 4040606eb..699f794ec 100644 --- a/ui/js/component/userEmailNew/index.js +++ b/ui/js/component/userEmailNew/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doUserEmailNew } from "actions/user"; +import { doUserEmailNew, doUserInviteNew } from "actions/user"; import { selectEmailNewIsPending, selectEmailNewErrorMessage, diff --git a/ui/js/component/userEmailNew/view.jsx b/ui/js/component/userEmailNew/view.jsx index 7f20ecd7c..70ed0f45e 100644 --- a/ui/js/component/userEmailNew/view.jsx +++ b/ui/js/component/userEmailNew/view.jsx @@ -31,6 +31,16 @@ class UserEmailNew extends React.PureComponent { this.handleSubmit(event); }} > +

+ {__( + "This process is required to prevent abuse of the rewards program." + )} +

+

+ {__( + "We will also contact you about updates and new content, but you can unsubscribe at any time." + )} +

-

- {__( - "To ensure you are a real person, we require a valid credit or debit card." - ) + - " " + - __("There is no charge at all, now or in the future.") + - " "} - -

- {errorMessage &&

{errorMessage}

} -

- -

-

- {__( - "You can continue without this step, but you will not be eligible to earn rewards." - )} -

- navigate("/discover")} - button="alt" - label={__("Skip Rewards")} - /> +
+
+

{__("Final Human Proof")}

+
+
+

+ Finally, please complete one and only one of the + options below. +

+
+
+
+
+

{__("1) Proof via Credit")}

+
+
+ {__( + "If you have a valid credit or debit card, you can use it to instantly prove your humanity." + ) + + " " + + __("There is no charge at all for this, now or in the future.") + + " "} +
+
+ {errorMessage && +

{errorMessage}

} + +
+
+
+ {__( + "A $1 authorization may temporarily appear with your provider." + )}{" "} + {" "} + +
+
+
+
+
+

{__("2) Proof via YouTube")}

+
+
+

+ {__( + "If you have a YouTube account with published videos, you can sync your account to be granted instant verification." + )} +

+
+
+ +
+
+
+ This will not automatically refresh after approval. Once you have + synced your account, just navigate away or click + {" "} . +
+
+
+
+
+

{__("3) Proof via Chat")}

+
+
+

+ {__( + "A moderator capable of approving you is typically available in the #verification channel of our chat room." + )} +

+

+ {__( + "This process will likely involve providing proof of a stable and established online or real-life identity." + )} +

+
+
+ +
+
+
+
+
{__("Or, Skip It Entirely")}
+
+
+ +

+ {__( + "You can continue without this step, but you will not be eligible to earn rewards." + )} +

+ +
+
+ navigate("/discover")} + button="alt" + label={__("Skip Rewards")} + /> +
+
); } diff --git a/ui/js/component/walletAddress/index.js b/ui/js/component/walletAddress/index.js index ad0021f41..d5a8d7696 100644 --- a/ui/js/component/walletAddress/index.js +++ b/ui/js/component/walletAddress/index.js @@ -5,7 +5,7 @@ import { selectReceiveAddress, selectGettingNewAddress, } from "selectors/wallet"; -import WalletPage from "./view"; +import WalletAddress from "./view"; const select = state => ({ receiveAddress: selectReceiveAddress(state), @@ -17,4 +17,4 @@ const perform = dispatch => ({ getNewAddress: () => dispatch(doGetNewAddress()), }); -export default connect(select, perform)(WalletPage); +export default connect(select, perform)(WalletAddress); diff --git a/ui/js/component/walletAddress/view.jsx b/ui/js/component/walletAddress/view.jsx index 2e1121cc6..bdf6f81fe 100644 --- a/ui/js/component/walletAddress/view.jsx +++ b/ui/js/component/walletAddress/view.jsx @@ -16,6 +16,11 @@ class WalletAddress extends React.PureComponent {

{__("Wallet Address")}

+

+ {__( + "Use this address to receive credits sent by another user (or yourself)." + )} +

@@ -29,11 +34,6 @@ class WalletAddress extends React.PureComponent {
-

- {__( - 'Other LBRY users may send credits to you by entering this address on the "Send" page.' - )} -

{__( "You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources." diff --git a/ui/js/component/walletBalance/index.js b/ui/js/component/walletBalance/index.js new file mode 100644 index 000000000..1121cc463 --- /dev/null +++ b/ui/js/component/walletBalance/index.js @@ -0,0 +1,10 @@ +import React from "react"; +import { connect } from "react-redux"; +import { selectBalance } from "selectors/wallet"; +import WalletBalance from "./view"; + +const select = state => ({ + balance: selectBalance(state), +}); + +export default connect(select, null)(WalletBalance); diff --git a/ui/js/component/walletBalance/view.jsx b/ui/js/component/walletBalance/view.jsx new file mode 100644 index 000000000..5e0eca86f --- /dev/null +++ b/ui/js/component/walletBalance/view.jsx @@ -0,0 +1,43 @@ +import React from "react"; +import Link from "component/link"; +import { CreditAmount } from "component/common"; + +const WalletBalance = props => { + const { balance, navigate } = props; + /* +

+ navigate("/backup")} + label={__("Backup Your Wallet")} + /> +
+ */ + return ( +
+
+

{__("Balance")}

+
+
+ {(balance || balance === 0) && + } +
+
+ + + +
+
+ ); +}; + +export default WalletBalance; diff --git a/ui/js/component/walletSend/view.jsx b/ui/js/component/walletSend/view.jsx index 444acca4a..680106584 100644 --- a/ui/js/component/walletSend/view.jsx +++ b/ui/js/component/walletSend/view.jsx @@ -42,15 +42,15 @@ const WalletSend = props => { onChange={setAddress} value={address} /> -
-
- 0.0) || !address} - /> - +
+ 0.0) || !address} + /> + +
{modal == "insufficientBalance" && diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index e44102c54..edb60621f 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -108,6 +108,15 @@ export const USER_IDENTITY_VERIFY_FAILURE = "USER_IDENTITY_VERIFY_FAILURE"; export const USER_FETCH_STARTED = "USER_FETCH_STARTED"; export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS"; export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE"; +export const USER_INVITE_STATUS_FETCH_STARTED = + "USER_INVITE_STATUS_FETCH_STARTED"; +export const USER_INVITE_STATUS_FETCH_SUCCESS = + "USER_INVITE_STATUS_FETCH_SUCCESS"; +export const USER_INVITE_STATUS_FETCH_FAILURE = + "USER_INVITE_STATUS_FETCH_FAILURE"; +export const USER_INVITE_NEW_STARTED = "USER_INVITE_NEW_STARTED"; +export const USER_INVITE_NEW_SUCCESS = "USER_INVITE_NEW_SUCCESS"; +export const USER_INVITE_NEW_FAILURE = "USER_INVITE_NEW_FAILURE"; export const FETCH_ACCESS_TOKEN_SUCCESS = "FETCH_ACCESS_TOKEN_SUCCESS"; // Rewards diff --git a/ui/js/constants/modal_types.js b/ui/js/constants/modal_types.js index d6686f15f..7dd057046 100644 --- a/ui/js/constants/modal_types.js +++ b/ui/js/constants/modal_types.js @@ -7,4 +7,5 @@ export const UPGRADE = "upgrade"; export const WELCOME = "welcome"; export const FIRST_REWARD = "first_reward"; export const AUTHENTICATION_FAILURE = "auth_failure"; +export const REWARD_APPROVAL_REQUIRED = "reward_approval_required"; export const CREDIT_INTRO = "credit_intro"; diff --git a/ui/js/constants/settings.js b/ui/js/constants/settings.js index 5187baea1..146c882cc 100644 --- a/ui/js/constants/settings.js +++ b/ui/js/constants/settings.js @@ -1,5 +1,8 @@ +/*hardcoded names still exist for these in reducers/settings.js - only discovered when debugging*/ +/*Many settings are stored in the localStorage by their name - + be careful about changing the value of a settings constant, as doing so can invalidate existing settings*/ export const CREDIT_INTRO_ACKNOWLEDGED = "credit_intro_acknowledged"; -export const FIRST_RUN_ACKNOWLEDGED = "welcome_acknowledged"; +export const NEW_USER_ACKNOWLEDGED = "welcome_acknowledged"; export const LANGUAGE = "language"; export const SHOW_NSFW = "showNsfw"; export const SHOW_UNAVAILABLE = "showUnavailable"; diff --git a/ui/js/modal/modalCreditIntro/index.js b/ui/js/modal/modalCreditIntro/index.js index 0626eee8f..37e72dc9a 100644 --- a/ui/js/modal/modalCreditIntro/index.js +++ b/ui/js/modal/modalCreditIntro/index.js @@ -1,13 +1,13 @@ import React from "react"; -import rewards from "rewards"; import { connect } from "react-redux"; import { doCloseModal, doAuthNavigate } from "actions/app"; import { doSetClientSetting } from "actions/settings"; import { selectUserIsRewardApproved } from "selectors/user"; +import { selectBalance } from "selectors/wallet"; import { makeSelectHasClaimedReward, makeSelectRewardByType, - selectTotalRewardValue, + selectUnclaimedRewardValue, } from "selectors/rewards"; import * as settings from "constants/settings"; import ModalCreditIntro from "./view"; @@ -17,9 +17,9 @@ const select = (state, props) => { selectReward = makeSelectRewardByType(); return { + currentBalance: selectBalance(state), isRewardApproved: selectUserIsRewardApproved(state), - reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), - totalRewardValue: selectTotalRewardValue(state), + totalRewardValue: selectUnclaimedRewardValue(state), }; }; diff --git a/ui/js/modal/modalCreditIntro/view.jsx b/ui/js/modal/modalCreditIntro/view.jsx index 8a2b7b500..5f56c11b4 100644 --- a/ui/js/modal/modalCreditIntro/view.jsx +++ b/ui/js/modal/modalCreditIntro/view.jsx @@ -1,40 +1,64 @@ import React from "react"; import { Modal } from "modal/modal"; -import { CreditAmount } from "component/common"; +import { CreditAmount, CurrencySymbol } from "component/common"; import Link from "component/link/index"; +import { formatCredits } from "util/formatCredits"; const ModalCreditIntro = props => { - const { closeModal, totalRewardValue, verifyAccount } = props; + const { closeModal, currentBalance, totalRewardValue, verifyAccount } = props; const totalRewardRounded = Math.round(totalRewardValue / 10) * 10; return (
-

{__("Claim Your Credits")}

-

- The LBRY network is controlled and powered by credits called{" "} - LBC, a blockchain asset. -

-

- {__("New patrons receive ")} {" "} - {totalRewardValue - ? - : {__("credits")}} - {" "} {__("in rewards for usage and influence of the network.")} -

+

{__("Blockchain 101")}

+ LBRY is controlled and powered by a blockchain asset called {" "} + .{" "} + {" "} {__( - "You'll also earn weekly bonuses for checking out the greatest new stuff." + "is used to publish content, to have a say in the network rules, and to access paid content." )}

+ {currentBalance <= 0 + ?
+

+ You currently have , so + the actions you can take are limited. +

+

+ However, there are a variety of ways to get credits, including + more than {" "} + {totalRewardValue + ? + : {__("?? credits")}} + {" "}{" "} + {__( + " in rewards available for being a proven human during the LBRY beta." + )} +

+
+ :
+

+ But you probably knew this, since you've already got{" "} + . +

+
} +
+ -
diff --git a/ui/js/modal/modalInsufficientCredits/index.js b/ui/js/modal/modalInsufficientCredits/index.js index 0ece4f1fe..eefaa7c68 100644 --- a/ui/js/modal/modalInsufficientCredits/index.js +++ b/ui/js/modal/modalInsufficientCredits/index.js @@ -7,7 +7,7 @@ const select = state => ({}); const perform = dispatch => ({ addFunds: () => { - dispatch(doNavigate("/rewards")); + dispatch(doNavigate("/wallet")); dispatch(doCloseModal()); }, closeModal: () => dispatch(doCloseModal()), diff --git a/ui/js/modal/modalInsufficientCredits/view.jsx b/ui/js/modal/modalInsufficientCredits/view.jsx index 681fc4c9f..397fc0301 100644 --- a/ui/js/modal/modalInsufficientCredits/view.jsx +++ b/ui/js/modal/modalInsufficientCredits/view.jsx @@ -1,5 +1,6 @@ import React from "react"; import { Modal } from "modal/modal"; +import { CurrencySymbol } from "component/common"; class ModalInsufficientCredits extends React.PureComponent { render() { @@ -11,11 +12,12 @@ class ModalInsufficientCredits extends React.PureComponent { type="confirm" contentLabel={__("Not enough credits")} confirmButtonLabel={__("Get Credits")} - abortButtonLabel={__("Cancel")} + abortButtonLabel={__("Not Now")} onAborted={closeModal} onConfirmed={addFunds} > - {__("More LBRY credits are required to purchase this.")} +

{__("More Credits Required")}

+

You'll need more to do this.

); } diff --git a/ui/js/modal/modalRewardApprovalRequired/index.js b/ui/js/modal/modalRewardApprovalRequired/index.js new file mode 100644 index 000000000..c42c54a83 --- /dev/null +++ b/ui/js/modal/modalRewardApprovalRequired/index.js @@ -0,0 +1,14 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doCloseModal, doAuthNavigate } from "actions/app"; +import ModalRewardApprovalRequired from "./view"; + +const perform = dispatch => ({ + doAuth: () => { + dispatch(doCloseModal()); + dispatch(doAuthNavigate()); + }, + closeModal: () => dispatch(doCloseModal()), +}); + +export default connect(null, perform)(ModalRewardApprovalRequired); diff --git a/ui/js/modal/modalRewardApprovalRequired/view.jsx b/ui/js/modal/modalRewardApprovalRequired/view.jsx new file mode 100644 index 000000000..fcdab9f98 --- /dev/null +++ b/ui/js/modal/modalRewardApprovalRequired/view.jsx @@ -0,0 +1,33 @@ +import React from "react"; +import { Modal } from "modal/modal"; + +class ModalRewardApprovalRequired extends React.PureComponent { + render() { + const { closeModal, doAuth } = this.props; + + return ( + +
+

+ {__("This is awkward. Are you real?")} +

+

+ {__( + "Before we can give you any credits, we need to perform a brief check to make sure you're a new and unique person." + )} +

+
+
+ ); + } +} + +export default ModalRewardApprovalRequired; diff --git a/ui/js/modal/modalRouter/index.js b/ui/js/modal/modalRouter/index.js index 971f83139..9bf9601c5 100644 --- a/ui/js/modal/modalRouter/index.js +++ b/ui/js/modal/modalRouter/index.js @@ -1,20 +1,30 @@ import React from "react"; import { connect } from "react-redux"; -import { selectCurrentModal } from "selectors/app"; +import { selectCurrentModal, selectCurrentPage } from "selectors/app"; import { doOpenModal } from "actions/app"; -import { selectWelcomeModalAcknowledged } from "selectors/app"; +import { makeSelectClientSetting } from "selectors/settings"; import { selectUser } from "selectors/user"; +import { selectCostForCurrentPageUri } from "selectors/cost_info"; +import * as settings from "constants/settings"; +import { selectBalance } from "selectors/wallet"; import ModalRouter from "./view"; -import * as modals from "constants/modal_types"; const select = (state, props) => ({ + balance: selectBalance(state), + showPageCost: selectCostForCurrentPageUri(state), modal: selectCurrentModal(state), - isWelcomeAcknowledged: selectWelcomeModalAcknowledged(state), + page: selectCurrentPage(state), + isWelcomeAcknowledged: makeSelectClientSetting( + settings.NEW_USER_ACKNOWLEDGED + )(state), + isCreditIntroAcknowledged: makeSelectClientSetting( + settings.CREDIT_INTRO_ACKNOWLEDGED + )(state), user: selectUser(state), }); const perform = dispatch => ({ - openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)), + openModal: modal => dispatch(doOpenModal(modal)), }); export default connect(select, perform)(ModalRouter); diff --git a/ui/js/modal/modalRouter/view.jsx b/ui/js/modal/modalRouter/view.jsx index d2280821e..a21851c3b 100644 --- a/ui/js/modal/modalRouter/view.jsx +++ b/ui/js/modal/modalRouter/view.jsx @@ -6,31 +6,95 @@ import ModalInsufficientCredits from "modal/modalInsufficientCredits"; import ModalUpgrade from "modal/modalUpgrade"; import ModalWelcome from "modal/modalWelcome"; import ModalFirstReward from "modal/modalFirstReward"; +import ModalRewardApprovalRequired from "modal/modalRewardApprovalRequired"; import * as modals from "constants/modal_types"; import ModalCreditIntro from "modal/modalCreditIntro"; class ModalRouter extends React.PureComponent { + constructor(props) { + super(props); + + this.state = { + lastTransitionModal: null, + lastTransitionPage: null, + }; + } + componentWillMount() { - this.showWelcome(this.props); + this.showTransitionModals(this.props); } componentWillReceiveProps(nextProps) { - this.showWelcome(nextProps); + this.showTransitionModals(nextProps); } - showWelcome(props) { - const { isWelcomeAcknowledged, openWelcomeModal, user } = props; + showTransitionModals(props) { + const { modal, openModal, page } = props; + if (modal) { + return; + } + + const transitionModal = [ + this.checkShowWelcome, + this.checkShowCreditIntro, + this.checkShowInsufficientCredits, + ].reduce((acc, func) => { + return !acc ? func.bind(this)(props) : acc; + }, false); + + if ( + transitionModal && + (transitionModal != this.state.lastTransitionModal || + page != this.state.lastTransitionPage) + ) { + openModal(transitionModal); + this.setState({ + lastTransitionModal: transitionModal, + lastTransitionPage: page, + }); + } + } + + checkShowWelcome(props) { + const { isWelcomeAcknowledged, user } = props; if ( !isWelcomeAcknowledged && user && !user.is_reward_approved && !user.is_identity_verified ) { - openWelcomeModal(); + return modals.WELCOME; } } + checkShowCreditIntro(props) { + const { page, isCreditIntroAcknowledged, user } = props; + + if ( + !isCreditIntroAcknowledged && + user && + !user.is_reward_approved && + (["rewards", "send", "receive", "publish", "wallet"].includes(page) || + this.isPaidShowPage(props)) + ) { + return modals.CREDIT_INTRO; + } + } + + checkShowInsufficientCredits(props) { + const { balance, page } = props; + + if (balance <= 0 && ["send", "publish"].includes(page)) { + return modals.INSUFFICIENT_CREDITS; + } + } + + isPaidShowPage(props) { + const { page, showPageCost } = props; + return page === "show" && showPageCost > 0; + } + render() { const { modal } = this.props; @@ -51,6 +115,8 @@ class ModalRouter extends React.PureComponent { return ; case modals.CREDIT_INTRO: return ; + case modals.REWARD_APPROVAL_REQUIRED: + return ; default: return null; } diff --git a/ui/js/modal/modalWelcome/index.js b/ui/js/modal/modalWelcome/index.js index 1b62eba42..dd2cc0f29 100644 --- a/ui/js/modal/modalWelcome/index.js +++ b/ui/js/modal/modalWelcome/index.js @@ -8,9 +8,8 @@ import ModalWelcome from "./view"; const perform = dispatch => () => ({ closeModal: () => { - dispatch(doSetClientSetting(settings.FIRST_RUN_ACKNOWLEDGED, true)); + dispatch(doSetClientSetting(settings.NEW_USER_ACKNOWLEDGED, true)); dispatch(doCloseModal()); - dispatch(doOpenModal(modals.CREDIT_INTRO)); }, }); diff --git a/ui/js/modal/modalWelcome/view.jsx b/ui/js/modal/modalWelcome/view.jsx index 85b33a224..adcf76ab6 100644 --- a/ui/js/modal/modalWelcome/view.jsx +++ b/ui/js/modal/modalWelcome/view.jsx @@ -22,7 +22,11 @@ const ModalWelcome = props => { )}

- +
diff --git a/ui/js/page/auth/view.jsx b/ui/js/page/auth/view.jsx index 55bef96a8..b9ac51bbd 100644 --- a/ui/js/page/auth/view.jsx +++ b/ui/js/page/auth/view.jsx @@ -30,11 +30,11 @@ export class AuthPage extends React.PureComponent { const { email, isPending, isVerificationCandidate, user } = this.props; if (isPending || (user && !user.has_verified_email && !email)) { - return __("Welcome to LBRY"); + return __("Human Proofing"); } else if (user && !user.has_verified_email) { return __("Confirm Email"); } else if (user && !user.is_identity_verified && !user.is_reward_approved) { - return __("Confirm Identity"); + return __("Final Verification"); } else { return __("Welcome to LBRY"); } @@ -44,51 +44,45 @@ export class AuthPage extends React.PureComponent { const { email, isPending, isVerificationCandidate, user } = this.props; if (isPending) { - return ; + return [, true]; } else if (user && !user.has_verified_email && !email) { - return ; + return [, true]; } else if (user && !user.has_verified_email) { - return ; + return [, true]; } else if (user && !user.is_identity_verified) { - return ; + return [, false]; } else { - return {__("No further steps.")}; + return [{__("No further steps.")}, true]; } } render() { const { email, user, isPending, navigate } = this.props; + const [innerContent, useTemplate] = this.renderMain(); - return ( -
-
-
-

{this.getTitle()}

-
-
- {!isPending && - !email && - user && - !user.has_verified_email && -

- {__("Create a verified identity and receive LBC rewards.")} -

} - {this.renderMain()} -
-
-
- {__( - "This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards." - ) + " "} - navigate("/discover")} - label={__("Return home")} - />. + return useTemplate + ?
+
+
+

{this.getTitle()}

-
-
-
- ); +
+ {innerContent} +
+
+
+ {__( + "This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards." + ) + " "} + navigate("/discover")} + label={__("Return home")} + />. +
+
+ + + : innerContent; } } diff --git a/ui/js/page/filePage/index.js b/ui/js/page/file/index.js similarity index 100% rename from ui/js/page/filePage/index.js rename to ui/js/page/file/index.js diff --git a/ui/js/page/filePage/view.jsx b/ui/js/page/file/view.jsx similarity index 100% rename from ui/js/page/filePage/view.jsx rename to ui/js/page/file/view.jsx diff --git a/ui/js/page/help/index.js b/ui/js/page/help/index.js index c4ea548b2..93492d915 100644 --- a/ui/js/page/help/index.js +++ b/ui/js/page/help/index.js @@ -1,5 +1,5 @@ import React from "react"; -import { doNavigate } from "actions/app"; +import { doAuthNavigate } from "actions/app"; import { connect } from "react-redux"; import { doFetchAccessToken } from "actions/user"; import { selectAccessToken, selectUser } from "selectors/user"; @@ -11,7 +11,7 @@ const select = state => ({ }); const perform = dispatch => ({ - navigate: (path, params) => dispatch(doNavigate(path, params)), + doAuth: () => dispatch(doAuthNavigate("/help")), fetchAccessToken: () => dispatch(doFetchAccessToken()), }); diff --git a/ui/js/page/help/view.jsx b/ui/js/page/help/view.jsx index f56d058e8..cb1627635 100644 --- a/ui/js/page/help/view.jsx +++ b/ui/js/page/help/view.jsx @@ -3,7 +3,7 @@ import React from "react"; import lbry from "lbry.js"; import Link from "component/link"; import SubHeader from "component/subHeader"; -import { BusyMessage } from "component/common"; +import { BusyMessage, Icon } from "component/common"; class HelpPage extends React.PureComponent { constructor(props) { @@ -50,7 +50,7 @@ class HelpPage extends React.PureComponent { render() { let ver, osName, platform, newVerLink; - const { navigate, user } = this.props; + const { doAuth, user } = this.props; if (this.state.versionInfo) { ver = this.state.versionInfo; @@ -119,7 +119,7 @@ class HelpPage extends React.PureComponent {

{__("Did you find something wrong?")}

navigate("report")} + navigate="/report" label={__("Submit a Bug Report")} icon="icon-bug" button="alt" @@ -143,7 +143,7 @@ class HelpPage extends React.PureComponent {

:

{__("Your copy of LBRY is up to date.")}

} {this.state.uiVersion && ver - ? + ?
@@ -162,7 +162,21 @@ class HelpPage extends React.PureComponent { + + + + diff --git a/ui/js/page/invite/index.js b/ui/js/page/invite/index.js new file mode 100644 index 000000000..c97cd1b29 --- /dev/null +++ b/ui/js/page/invite/index.js @@ -0,0 +1,19 @@ +import React from "react"; +import { connect } from "react-redux"; +import InvitePage from "./view"; +import { doFetchInviteStatus } from "actions/user"; +import { + selectUserInviteStatusFailed, + selectUserInviteStatusIsPending, +} from "selectors/user"; + +const select = state => ({ + isFailed: selectUserInviteStatusFailed(state), + isPending: selectUserInviteStatusIsPending(state), +}); + +const perform = dispatch => ({ + fetchInviteStatus: () => dispatch(doFetchInviteStatus()), +}); + +export default connect(select, perform)(InvitePage); diff --git a/ui/js/page/invite/view.jsx b/ui/js/page/invite/view.jsx new file mode 100644 index 000000000..0488ca1f3 --- /dev/null +++ b/ui/js/page/invite/view.jsx @@ -0,0 +1,32 @@ +import React from "react"; +import { BusyMessage } from "component/common"; +import SubHeader from "component/subHeader"; +import InviteNew from "component/inviteNew"; +import InviteList from "component/inviteList"; + +class InvitePage extends React.PureComponent { + componentWillMount() { + this.props.fetchInviteStatus(); + } + + render() { + const { isPending, isFailed } = this.props; + + return ( +
+ + {isPending && + } + {!isPending && + isFailed && + + {__("Failed to retrieve invite status.")} + } + {!isPending && !isFailed && } + {!isPending && !isFailed && } +
+ ); + } +} + +export default InvitePage; diff --git a/ui/js/page/publish/index.js b/ui/js/page/publish/index.js index f296f1687..1816bf2d5 100644 --- a/ui/js/page/publish/index.js +++ b/ui/js/page/publish/index.js @@ -16,10 +16,12 @@ import { doCreateChannel, doPublish, } from "actions/content"; +import { selectBalance } from "selectors/wallet"; import rewards from "rewards"; import PublishPage from "./view"; const select = state => ({ + balance: selectBalance(state), myClaims: selectMyClaims(state), fetchingChannels: selectFetchingMyChannels(state), channels: selectMyChannelClaims(state), diff --git a/ui/js/page/receiveCredits/index.js b/ui/js/page/receiveCredits/index.js new file mode 100644 index 000000000..475548da7 --- /dev/null +++ b/ui/js/page/receiveCredits/index.js @@ -0,0 +1,5 @@ +import React from "react"; +import { connect } from "react-redux"; +import ReceiveCreditsPage from "./view"; + +export default connect(null, null)(ReceiveCreditsPage); diff --git a/ui/js/page/receiveCredits/view.jsx b/ui/js/page/receiveCredits/view.jsx new file mode 100644 index 000000000..120071748 --- /dev/null +++ b/ui/js/page/receiveCredits/view.jsx @@ -0,0 +1,34 @@ +import React from "react"; +import SubHeader from "component/subHeader"; +import Link from "component/link"; +import WalletAddress from "component/walletAddress"; + +const ReceiveCreditsPage = props => { + return ( +
+ + +
+
+

{__("Where To Find Credits")}

+
+
+

+ { + "LBRY credits can be purchased on exchanges, earned for contributions, for mining, and more." + } +

+
+
+ +
+
+
+ ); +}; + +export default ReceiveCreditsPage; 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..51d75e406 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,20 +31,27 @@ class RewardsPage extends React.PureComponent { !user.has_verified_email || !user.is_identity_verified ) { - cardHeader = ( -
+ return ( +
+
+

{__("Humans Only")}

+

- {__("Only verified accounts are eligible to earn rewards.")} + {__("Rewards are for human beings only.")} + {" "} + {__( + "You'll have to prove you're one of us before you can claim any rewards." + )}

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

{__( @@ -122,25 +83,52 @@ class RewardsPage extends React.PureComponent {

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

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

-
+ 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/sendCredits/index.js b/ui/js/page/sendCredits/index.js new file mode 100644 index 000000000..2a36f2719 --- /dev/null +++ b/ui/js/page/sendCredits/index.js @@ -0,0 +1,5 @@ +import React from "react"; +import { connect } from "react-redux"; +import SendCreditsPage from "./view"; + +export default connect(null, null)(SendCreditsPage); diff --git a/ui/js/page/sendCredits/view.jsx b/ui/js/page/sendCredits/view.jsx new file mode 100644 index 000000000..96937bcc6 --- /dev/null +++ b/ui/js/page/sendCredits/view.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import SubHeader from "component/subHeader"; +import WalletSend from "component/walletSend"; + +const SendCreditsPage = props => { + return ( +
+ + +
+ ); +}; + +export default SendCreditsPage; diff --git a/ui/js/page/showPage/index.js b/ui/js/page/show/index.js similarity index 100% rename from ui/js/page/showPage/index.js rename to ui/js/page/show/index.js diff --git a/ui/js/page/showPage/view.jsx b/ui/js/page/show/view.jsx similarity index 97% rename from ui/js/page/showPage/view.jsx rename to ui/js/page/show/view.jsx index 67bbf6178..aae0aabfe 100644 --- a/ui/js/page/showPage/view.jsx +++ b/ui/js/page/show/view.jsx @@ -2,7 +2,7 @@ import React from "react"; import lbryuri from "lbryuri"; import { BusyMessage } from "component/common"; import ChannelPage from "page/channel"; -import FilePage from "page/filePage"; +import FilePage from "page/file"; class ShowPage extends React.PureComponent { componentWillMount() { diff --git a/ui/js/page/transactionHistory/index.js b/ui/js/page/transactionHistory/index.js new file mode 100644 index 000000000..12acf47ad --- /dev/null +++ b/ui/js/page/transactionHistory/index.js @@ -0,0 +1,19 @@ +import React from "react"; +import { connect } from "react-redux"; +import { doFetchTransactions } from "actions/wallet"; +import { + selectTransactionItems, + selectIsFetchingTransactions, +} from "selectors/wallet"; +import TransactionHistoryPage from "./view"; + +const select = state => ({ + fetchingTransactions: selectIsFetchingTransactions(state), + transactions: selectTransactionItems(state), +}); + +const perform = dispatch => ({ + fetchTransactions: () => dispatch(doFetchTransactions()), +}); + +export default connect(select, perform)(TransactionHistoryPage); diff --git a/ui/js/page/transactionHistory/view.jsx b/ui/js/page/transactionHistory/view.jsx new file mode 100644 index 000000000..8b9f1a90b --- /dev/null +++ b/ui/js/page/transactionHistory/view.jsx @@ -0,0 +1,32 @@ +import React from "react"; +import { BusyMessage } from "component/common"; +import SubHeader from "component/subHeader"; +import TransactionList from "component/transactionList"; + +class TransactionHistoryPage extends React.PureComponent { + componentWillMount() { + this.props.fetchTransactions(); + } + + render() { + const { fetchingTransactions, transactions } = this.props; + return ( +
+ +
+
+

{__("Transaction History")}

+
+
+ {fetchingTransactions && + } + {!fetchingTransactions && + } +
+
+
+ ); + } +} + +export default TransactionHistoryPage; diff --git a/ui/js/page/wallet/index.js b/ui/js/page/wallet/index.js index 810899d54..01fdf2a64 100644 --- a/ui/js/page/wallet/index.js +++ b/ui/js/page/wallet/index.js @@ -1,17 +1,5 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; -import { selectCurrentPage } from "selectors/app"; -import { selectBalance } from "selectors/wallet"; import WalletPage from "./view"; -const select = state => ({ - currentPage: selectCurrentPage(state), - balance: selectBalance(state), -}); - -const perform = dispatch => ({ - navigate: path => dispatch(doNavigate(path)), -}); - -export default connect(select, perform)(WalletPage); +export default connect(null, null)(WalletPage); diff --git a/ui/js/page/wallet/view.jsx b/ui/js/page/wallet/view.jsx index 423c15473..b8bee1c94 100644 --- a/ui/js/page/wallet/view.jsx +++ b/ui/js/page/wallet/view.jsx @@ -1,36 +1,18 @@ import React from "react"; import SubHeader from "component/subHeader"; -import TransactionList from "component/transactionList"; -import WalletAddress from "component/walletAddress"; -import WalletSend from "component/walletSend"; -import Link from "component/link"; -import { CreditAmount } from "component/common"; +import WalletBalance from "component/walletBalance"; +import RewardSummary from "component/rewardSummary"; +import TransactionListRecent from "component/transactionListRecent"; const WalletPage = props => { - const { balance, currentPage, navigate } = props; - return ( -
+
-
-
-

{__("Balance")}

-
-
- -
-
-
- navigate("/backup")} - label={__("Backup Your Wallet")} - /> -
-
-
- {currentPage === "wallet" ? : ""} - {currentPage === "send" ? : ""} - {currentPage === "receive" ? : ""} +
+ + +
+
); }; 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/reducers/settings.js b/ui/js/reducers/settings.js index e6d32f0f0..16e53e2da 100644 --- a/ui/js/reducers/settings.js +++ b/ui/js/reducers/settings.js @@ -1,4 +1,5 @@ import * as types from "constants/action_types"; +import * as settings from "constants/settings"; import LANGUAGES from "constants/languages"; import lbry from "lbry"; @@ -6,7 +7,11 @@ const reducers = {}; const defaultState = { clientSettings: { showNsfw: lbry.getClientSetting("showNsfw"), - language: lbry.getClientSetting("language"), + welcome_acknowledged: lbry.getClientSetting(settings.NEW_USER_ACKNOWLEDGED), + credit_intro_acknowledged: lbry.getClientSetting( + settings.CREDIT_INTRO_ACKNOWLEDGED + ), + language: lbry.getClientSetting(settings.LANGUAGE), }, languages: {}, }; diff --git a/ui/js/reducers/user.js b/ui/js/reducers/user.js index 02203cdfe..bc19f3464 100644 --- a/ui/js/reducers/user.js +++ b/ui/js/reducers/user.js @@ -8,6 +8,11 @@ const defaultState = { emailNewIsPending: false, emailNewErrorMessage: "", emailToVerify: "", + inviteNewErrorMessage: "", + inviteNewIsPending: false, + inviteStatusIsPending: false, + invitesRemaining: undefined, + invitees: undefined, user: undefined, }; @@ -142,6 +147,49 @@ reducers[types.FETCH_ACCESS_TOKEN_SUCCESS] = function(state, action) { }); }; +reducers[types.USER_INVITE_STATUS_FETCH_STARTED] = function(state, action) { + return Object.assign({}, state, { + inviteStatusIsPending: true, + }); +}; + +reducers[types.USER_INVITE_STATUS_FETCH_SUCCESS] = function(state, action) { + return Object.assign({}, state, { + inviteStatusIsPending: false, + invitesRemaining: action.data.invitesRemaining, + invitees: action.data.invitees, + }); +}; + +reducers[types.USER_INVITE_NEW_STARTED] = function(state, action) { + return Object.assign({}, state, { + inviteNewIsPending: true, + inviteNewErrorMessage: "", + }); +}; + +reducers[types.USER_INVITE_NEW_SUCCESS] = function(state, action) { + return Object.assign({}, state, { + inviteNewIsPending: false, + inviteNewErrorMessage: "", + }); +}; + +reducers[types.USER_INVITE_NEW_FAILURE] = function(state, action) { + return Object.assign({}, state, { + inviteNewIsPending: false, + inviteNewErrorMessage: action.data.error.message, + }); +}; + +reducers[types.USER_INVITE_STATUS_FETCH_FAILURE] = function(state, action) { + return Object.assign({}, state, { + inviteStatusIsPending: false, + invitesRemaining: null, + invitees: null, + }); +}; + export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/ui/js/rewards.js b/ui/js/rewards.js index fd952032c..0a3475246 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -95,6 +95,18 @@ rewards.TYPE_FIRST_STREAM = "first_stream"; rewards.TYPE_MANY_DOWNLOADS = "many_downloads"; rewards.TYPE_FIRST_PUBLISH = "first_publish"; rewards.TYPE_FEATURED_DOWNLOAD = "featured_download"; +rewards.TYPE_REFERRAL = "referral"; +rewards.SORT_ORDER = [ + rewards.TYPE_NEW_USER, + rewards.TYPE_CONFIRM_EMAIL, + rewards.TYPE_FIRST_STREAM, + rewards.TYPE_FIRST_CHANNEL, + rewards.TYPE_FIRST_PUBLISH, + rewards.TYPE_FEATURED_DOWNLOAD, + rewards.TYPE_MANY_DOWNLOADS, + rewards.TYPE_REFERRAL, + rewards.TYPE_NEW_DEVELOPER, +]; rewards.claimReward = function(type) { function requestReward(resolve, reject, params) { diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js index 1ad6a81b9..eb950f3b1 100644 --- a/ui/js/selectors/app.js +++ b/ui/js/selectors/app.js @@ -1,7 +1,6 @@ import { createSelector } from "reselect"; import { parseQueryParams, toQueryString } from "util/query_params"; import * as settings from "constants/settings.js"; -import lbry from "lbry"; import lbryuri from "lbryuri"; export const _selectState = state => state.app || {}; @@ -39,13 +38,15 @@ export const selectPageTitle = createSelector( case "wallet": return __("Wallet"); case "send": - return __("Send"); + return __("Send Credits"); case "receive": - return __("Receive"); + return __("Wallet Address"); case "backup": - return __("Backup"); + return __("Backup Your Wallet"); case "rewards": return __("Rewards"); + case "invite": + return __("Invites"); case "start": return __("Start"); case "publish": @@ -72,8 +73,12 @@ export const selectPageTitle = createSelector( return __("Publishes"); case "discover": return __("Home"); - default: + case false: + case null: + case "": return ""; + default: + return page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : ""); } } ); @@ -136,15 +141,19 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => { // This contains intentional fall throughs switch (page) { case "wallet": + case "history": case "send": case "receive": + case "invite": case "rewards": case "backup": return { wallet: __("Overview"), + history: __("History"), send: __("Send"), receive: __("Receive"), rewards: __("Rewards"), + invite: __("Invites"), }; case "downloaded": case "published": @@ -203,16 +212,6 @@ export const selectSnackBarSnacks = createSelector( snackBar => snackBar.snacks || [] ); -export const selectCreditsIntroAcknowledged = createSelector( - _selectState, - state => lbry.getClientSetting(settings.CREDIT_INTRO_ACKNOWLEDGED) -); - -export const selectWelcomeModalAcknowledged = createSelector( - _selectState, - state => lbry.getClientSetting(settings.FIRST_RUN_ACKNOWLEDGED) -); - export const selectBadgeNumber = createSelector( _selectState, state => state.badgeNumber diff --git a/ui/js/selectors/cost_info.js b/ui/js/selectors/cost_info.js index ec5280485..034c4572f 100644 --- a/ui/js/selectors/cost_info.js +++ b/ui/js/selectors/cost_info.js @@ -1,4 +1,5 @@ import { createSelector } from "reselect"; +import { selectCurrentParams } from "./app"; export const _selectState = state => state.costInfo || {}; @@ -15,6 +16,13 @@ export const makeSelectCostInfoForUri = () => { return createSelector(selectCostInfoForUri, costInfo => costInfo); }; +export const selectCostForCurrentPageUri = createSelector( + selectAllCostInfoByUri, + selectCurrentParams, + (costInfo, params) => + params.uri && costInfo[params.uri] ? costInfo[params.uri].cost : undefined +); + export const selectFetchingCostInfo = createSelector( _selectState, state => state.fetching || {} diff --git a/ui/js/selectors/rewards.js b/ui/js/selectors/rewards.js index bddb7714e..c2a646563 100644 --- a/ui/js/selectors/rewards.js +++ b/ui/js/selectors/rewards.js @@ -1,16 +1,33 @@ import { createSelector } from "reselect"; import { selectUser } from "selectors/user"; +import rewards from "rewards"; 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, - byType => Object.values(byType) || [] +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).sort(function(a, b) { + return rewards.SORT_ORDER.indexOf(a.reward_type) < + rewards.SORT_ORDER.indexOf(b.reward_type) + ? -1 + : 1; + }) || [] ); export const selectIsRewardEligible = createSelector( @@ -23,10 +40,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,9 +84,16 @@ export const makeSelectClaimRewardError = () => { }; const selectRewardByType = (state, props) => { - return selectRewardsByType(state)[props.reward_type]; + return selectUnclaimedRewardsByType(state)[props.reward_type]; }; export const makeSelectRewardByType = () => { return createSelector(selectRewardByType, reward => reward); }; + +export const makeSelectRewardAmountByType = () => { + return createSelector( + selectRewardByType, + reward => (reward ? reward.reward_amount : 0) + ); +}; diff --git a/ui/js/selectors/search.js b/ui/js/selectors/search.js index 1daa1ef79..2ef8a1410 100644 --- a/ui/js/selectors/search.js +++ b/ui/js/selectors/search.js @@ -38,6 +38,8 @@ export const selectWunderBarAddress = createSelector( export const selectWunderBarIcon = createSelector(selectCurrentPage, page => { switch (page) { + case "auth": + return "icon-user"; case "search": return "icon-search"; case "settings": @@ -52,22 +54,29 @@ export const selectWunderBarIcon = createSelector(selectCurrentPage, page => { return "icon-folder"; case "start": return "icon-file"; - case "rewards": - return "icon-bank"; - case "wallet": + case "history": + return "icon-history"; case "send": + return "icon-send"; + case "rewards": + return "icon-rocket"; + case "invite": + return "icon-envelope-open"; + case "address": case "receive": + return "icon-address-book"; + case "wallet": case "backup": return "icon-bank"; case "show": return "icon-file"; case "publish": return "icon-upload"; - case "developer": - return "icon-file"; case "developer": return "icon-code"; case "discover": return "icon-home"; + default: + return "icon-file"; } }); diff --git a/ui/js/selectors/settings.js b/ui/js/selectors/settings.js index 61c7dc4f0..10b9191df 100644 --- a/ui/js/selectors/settings.js +++ b/ui/js/selectors/settings.js @@ -12,6 +12,13 @@ export const selectClientSettings = createSelector( state => state.clientSettings || {} ); +export const makeSelectClientSetting = setting => { + return createSelector( + selectClientSettings, + settings => (settings ? settings[setting] : undefined) + ); +}; + export const selectSettingsIsGenerous = createSelector( selectDaemonSettings, settings => settings && settings.is_generous_host diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js index a499e5666..43ac07413 100644 --- a/ui/js/selectors/user.js +++ b/ui/js/selectors/user.js @@ -68,3 +68,33 @@ export const selectAccessToken = createSelector( _selectState, state => state.accessToken ); + +export const selectUserInviteStatusIsPending = createSelector( + _selectState, + state => state.inviteStatusIsPending +); + +export const selectUserInvitesRemaining = createSelector( + _selectState, + state => state.invitesRemaining +); + +export const selectUserInvitees = createSelector( + _selectState, + state => state.invitees +); + +export const selectUserInviteStatusFailed = createSelector( + selectUserInvitesRemaining, + inviteStatus => selectUserInvitesRemaining === null +); + +export const selectUserInviteNewIsPending = createSelector( + _selectState, + state => state.inviteNewIsPending +); + +export const selectUserInviteNewErrorMessage = createSelector( + _selectState, + state => state.inviteNewErrorMessage +); diff --git a/ui/js/selectors/wallet.js b/ui/js/selectors/wallet.js index 1027bb6e7..1334a17db 100644 --- a/ui/js/selectors/wallet.js +++ b/ui/js/selectors/wallet.js @@ -35,6 +35,24 @@ export const selectTransactionItems = createSelector( } ); +export const selectRecentTransactions = createSelector( + selectTransactionItems, + transactions => { + let threshold = new Date(); + threshold.setDate(threshold.getDate() - 7); + return transactions.filter(transaction => { + return transaction.date > threshold; + }); + } +); + +export const selectHasTransactions = createSelector( + selectTransactionItems, + transactions => { + return transactions && transactions.length > 0; + } +); + export const selectIsFetchingTransactions = createSelector( _selectState, state => state.fetchingTransactions diff --git a/ui/scss/_global.scss b/ui/scss/_global.scss index b554b77a1..9a838b2d7 100644 --- a/ui/scss/_global.scss +++ b/ui/scss/_global.scss @@ -2,8 +2,8 @@ $spacing-vertical: 24px; -$padding-button: 12px; -$padding-text-link: 4px; +$padding-button: $spacing-vertical * 2/3; +$padding-text-link: 0px; $color-primary: #155B4A; $color-primary-light: saturate(lighten($color-primary, 50%), 20%); diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index b8d9ec040..338ccac3c 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -134,15 +134,6 @@ p } } -/*should this be here or work this way? had to hack additional rule below*/ -.icon:only-child { - position: relative; - top: 0.16em; -} -.icon-featured > .icon { - top: 0; -} - .help { font-size: .85em; color: $color-help; diff --git a/ui/scss/_icons.scss b/ui/scss/_icons.scss index 441113e39..cb487975b 100644 --- a/ui/scss/_icons.scss +++ b/ui/scss/_icons.scss @@ -2,8 +2,8 @@ @font-face { font-family: 'FontAwesome'; - src: url('../font/fontawesome-webfont.eot?v=4.3.0'); - src: url('../font/fontawesome-webfont.eot?#iefix&v=4.3.0') format('embedded-opentype'), url('../font/fontawesome-webfont.woff2?v=4.3.0') format('woff2'), url('../font/fontawesome-webfont.woff?v=4.3.0') format('woff'), url('../font/fontawesome-webfont.ttf?v=4.3.0') format('truetype'), url('../font/fontawesome-webfont.svg?v=4.3.0#fontawesomeregular') format('svg'); + src: url('../font/fontawesome-webfont.eot?v=4.7.0'); + src: url('../font/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../font/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../font/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../font/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../font/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); font-weight: normal; font-style: normal; } diff --git a/ui/scss/component/_button.scss b/ui/scss/component/_button.scss index 5c6fed22f..cd00b8fe4 100644 --- a/ui/scss/component/_button.scss +++ b/ui/scss/component/_button.scss @@ -8,7 +8,7 @@ $button-focus-shift: 12%; + .button-set-item { - margin-left: $padding-button; + margin-left: $spacing-vertical; } } diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index 712eb9ea7..c5e17b66b 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -23,10 +23,11 @@ $width-card-small: $spacing-vertical * 10; } .card__title-primary, .card__title-identity, -.card__actions, .card__content, -.card__subtext { - padding: 0 $padding-card-horizontal; +.card__subtext, +.card__actions { + padding-left: $padding-card-horizontal; + padding-right: $padding-card-horizontal; } .card--small { .card__title-primary, @@ -39,6 +40,7 @@ $width-card-small: $spacing-vertical * 10; } .card__title-primary { margin-top: $spacing-vertical * 2/3; + margin-bottom: $spacing-vertical * 2/3; } .card__title-identity { margin-top: $spacing-vertical * 1/3; @@ -46,13 +48,6 @@ $width-card-small: $spacing-vertical * 10; } .card__actions { margin-top: $spacing-vertical * 2/3; -} -.card__actions--bottom { - margin-top: $spacing-vertical * 1/3; - margin-bottom: $spacing-vertical * 1/3; -} -.card__actions--form-submit { - margin-top: $spacing-vertical; margin-bottom: $spacing-vertical * 2/3; } .card__content { @@ -259,4 +254,21 @@ $padding-right-card-hover-hack: 30px; .card__icon-featured-content { color: orangered; +} + + +/* +if we keep doing things like this, we should add a real grid system, but I'm going to be a selective dick about it - Jeremy + */ +.card-grid { + $margin-card-grid: $spacing-vertical * 2/3; + display:flex; + flex-wrap: wrap; + > .card { + width: $width-page-constrained / 2 - $margin-card-grid / 2; + flex-grow:1; + } + > .card:nth-of-type(2n - 1) { + margin-right: $margin-card-grid; + } } \ No newline at end of file diff --git a/ui/scss/component/_table.scss b/ui/scss/component/_table.scss index 9d60cf6e8..38893eae5 100644 --- a/ui/scss/component/_table.scss +++ b/ui/scss/component/_table.scss @@ -24,6 +24,9 @@ table.table-standard { img { vertical-align: text-bottom; } + &.text-center { + text-align: center; + } } tr.thead:not(:first-child) th { border-top: 1px solid #e2e2e2; @@ -49,6 +52,11 @@ table.table-standard { } } } +.table-standard--definition-list { + th { + text-align: right; + } +} table.table-stretch { width: 100%;
{__("App")} {user && user.primary_email ? user.primary_email - : {__("none")}} + : + {__("none")} + ( doAuth()} + label={__("set email")} + />) + } +
{__("Reward Eligible")} + {user && user.is_reward_approved + ? + : }