Merge branch 'tear_down_this_wall'

* tear_down_this_wall:
  bump daemon version
  restore purecomponent, remove console.log
  changelog and bugfix
  reviewable
  mostly done?
  previously uncommitted copy change
  not enough progress
  uncommitted work from last night
  good chunk of progress towards auth and rewards refactor / degating
  Working on rewards refactor
  midway through auth rewrite
  slight progress
This commit is contained in:
Alex Grintsvayg 2017-06-09 10:18:25 -04:00
commit 902fb96878
48 changed files with 1641 additions and 1200 deletions

View file

@ -9,6 +9,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased] ## [Unreleased]
### Added ### Added
* More file types, like audio and documents, can be streamed and/or served from the app * More file types, like audio and documents, can be streamed and/or served from the app
* App is no longer gated. Reward authorization re-written. Added basic flows for new users.
* Videos now have a classy loading spinner * Videos now have a classy loading spinner
### Changed ### Changed
@ -20,6 +21,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
* Updated deprecated LBRY API call signatures * Updated deprecated LBRY API call signatures
* App scrolls to the top of the page on navigation * App scrolls to the top of the page on navigation
* Download progress works properly for purchased but deleted files * Download progress works properly for purchased but deleted files
* Publish channels for less than 1 LBC
### Deprecated ### Deprecated
* *

View file

@ -1 +1 @@
https://github.com/lbryio/lbry/releases/download/v0.11.0rc1/lbrynet-daemon-v0.11.0rc1-OSNAME.zip https://github.com/lbryio/lbry/releases/download/v0.11.0rc3/lbrynet-daemon-v0.11.0rc3-OSNAME.zip

View file

@ -10,6 +10,10 @@ import {
selectCurrentParams, selectCurrentParams,
} from "selectors/app"; } from "selectors/app";
import { doSearch } from "actions/search"; import { doSearch } from "actions/search";
import { doFetchDaemonSettings } from "actions/settings";
import { doAuthenticate } from "actions/user";
import { doRewardList } from "actions/rewards";
import { doFileList } from "actions/file_info";
const { remote, ipcRenderer, shell } = require("electron"); const { remote, ipcRenderer, shell } = require("electron");
const path = require("path"); const path = require("path");
@ -216,8 +220,14 @@ export function doAlertError(errorList) {
} }
export function doDaemonReady() { export function doDaemonReady() {
return { return function(dispatch, getState) {
type: types.DAEMON_READY, dispatch(doAuthenticate());
dispatch({
type: types.DAEMON_READY,
});
dispatch(doChangePath("/discover"));
dispatch(doFetchDaemonSettings());
dispatch(doFileList());
}; };
} }

View file

@ -2,7 +2,6 @@ import * as types from "constants/action_types";
import lbry from "lbry"; import lbry from "lbry";
import lbryio from "lbryio"; import lbryio from "lbryio";
import lbryuri from "lbryuri"; import lbryuri from "lbryuri";
import rewards from "rewards";
import { selectBalance } from "selectors/wallet"; import { selectBalance } from "selectors/wallet";
import { import {
selectFileInfoForUri, selectFileInfoForUri,
@ -10,8 +9,8 @@ import {
} from "selectors/file_info"; } from "selectors/file_info";
import { selectResolvingUris } from "selectors/content"; import { selectResolvingUris } from "selectors/content";
import { selectCostInfoForUri } from "selectors/cost_info"; import { selectCostInfoForUri } from "selectors/cost_info";
import { selectClaimsByUri } from "selectors/claims";
import { doOpenModal } from "actions/app"; import { doOpenModal } from "actions/app";
import { doClaimEligiblePurchaseRewards } from "actions/rewards";
export function doResolveUri(uri) { export function doResolveUri(uri) {
return function(dispatch, getState) { return function(dispatch, getState) {
@ -171,7 +170,7 @@ export function doDownloadFile(uri, streamInfo) {
}) })
.catch(() => {}); .catch(() => {});
rewards.claimEligiblePurchaseRewards(); dispatch(doClaimEligiblePurchaseRewards());
}; };
} }

View file

@ -2,8 +2,9 @@ import * as types from "constants/action_types";
import lbry from "lbry"; import lbry from "lbry";
import lbryio from "lbryio"; import lbryio from "lbryio";
import rewards from "rewards"; import rewards from "rewards";
import { selectRewards, selectRewardsByType } from "selectors/rewards";
export function doFetchRewards() { export function doRewardList() {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
@ -11,25 +12,105 @@ export function doFetchRewards() {
type: types.FETCH_REWARDS_STARTED, type: types.FETCH_REWARDS_STARTED,
}); });
lbryio.call("reward", "list", {}).then(function(userRewards) { lbryio
dispatch({ .call("reward", "list", {})
type: types.FETCH_REWARDS_COMPLETED, .then(userRewards => {
data: { userRewards }, dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards },
});
})
.catch(() => {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards: [] },
});
}); });
});
}; };
} }
export function doClaimReward(rewardType) { export function doClaimRewardType(rewardType) {
return function(dispatch, getState) { return function(dispatch, getState) {
try { const rewardsByType = selectRewardsByType(getState()),
rewards.claimReward(rewards[rewardType]); reward = rewardsByType[rewardType];
dispatch({
type: types.REWARD_CLAIMED, if (reward) {
data: { dispatch(doClaimReward(reward));
reward: rewards[rewardType], }
}, };
}); }
} catch (err) {}
export function doClaimReward(reward, saveError = false) {
return function(dispatch, getState) {
if (reward.transaction_id) {
//already claimed, do nothing
return;
}
dispatch({
type: types.CLAIM_REWARD_STARTED,
data: { reward },
});
const success = reward => {
dispatch({
type: types.CLAIM_REWARD_SUCCESS,
data: {
reward,
},
});
};
const failure = error => {
dispatch({
type: types.CLAIM_REWARD_FAILURE,
data: {
reward,
error: saveError ? error : null,
},
});
};
rewards.claimReward(reward.reward_type).then(success, failure);
};
}
export function doClaimEligiblePurchaseRewards() {
return function(dispatch, getState) {
if (!lbryio.enabled || !lbryio.getAccessToken()) {
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 (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) {
dispatch(doClaimRewardType(rewards.TYPE_FEATURED_DOWNLOAD));
}
};
}
export function doClaimRewardClearError(reward) {
return function(dispatch, getState) {
dispatch({
type: types.CLAIM_REWARD_CLEAR_ERROR,
data: { reward },
});
}; };
} }

132
ui/js/actions/user.js Normal file
View file

@ -0,0 +1,132 @@
import * as types from "constants/action_types";
import lbryio from "lbryio";
import { setLocal } from "utils";
import { doRewardList } from "actions/rewards";
import { selectEmailToVerify } from "selectors/user";
export function doAuthenticate() {
return function(dispatch, getState) {
dispatch({
type: types.AUTHENTICATION_STARTED,
});
lbryio
.authenticate()
.then(user => {
dispatch({
type: types.AUTHENTICATION_SUCCESS,
data: { user },
});
dispatch(doRewardList()); //FIXME - where should this happen?
})
.catch(error => {
dispatch({
type: types.AUTHENTICATION_FAILURE,
data: { error },
});
});
};
}
export function doUserFetch() {
return function(dispatch, getState) {
dispatch({
type: types.USER_FETCH_STARTED,
});
lbryio.setCurrentUser(
user => {
dispatch({
type: types.USER_FETCH_SUCCESS,
data: { user },
});
},
error => {
dispatch({
type: types.USER_FETCH_FAILURE,
data: { error },
});
}
);
};
}
export function doUserEmailNew(email) {
return function(dispatch, getState) {
dispatch({
type: types.USER_EMAIL_NEW_STARTED,
email: email,
});
lbryio.call("user_email", "new", { email }, "post").then(
() => {
dispatch({
type: types.USER_EMAIL_NEW_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
},
error => {
if (
error.xhr &&
(error.xhr.status == 409 ||
error.message == "This email is already in use")
) {
dispatch({
type: types.USER_EMAIL_NEW_EXISTS,
data: { email },
});
} else {
dispatch({
type: types.USER_EMAIL_NEW_FAILURE,
data: { error: error.message },
});
}
}
);
};
}
export function doUserEmailDecline() {
return function(dispatch, getState) {
setLocal("user_email_declined", true);
dispatch({
type: types.USER_EMAIL_DECLINE,
});
};
}
export function doUserEmailVerify(verificationToken) {
return function(dispatch, getState) {
const email = selectEmailToVerify(getState());
dispatch({
type: types.USER_EMAIL_VERIFY_STARTED,
code: verificationToken,
});
const failure = error => {
dispatch({
type: types.USER_EMAIL_VERIFY_FAILURE,
data: { error: error.message },
});
};
lbryio
.call(
"user_email",
"confirm",
{ verification_token: verificationToken, email: email },
"post"
)
.then(userEmail => {
if (userEmail.is_verified) {
dispatch({
type: types.USER_EMAIL_VERIFY_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
} else {
failure(new Error("Your email is still not verified.")); //shouldn't happen?
}
}, failure);
};
}

View file

@ -4,6 +4,7 @@ import Header from "component/header";
import ErrorModal from "component/errorModal"; import ErrorModal from "component/errorModal";
import DownloadingModal from "component/downloadingModal"; import DownloadingModal from "component/downloadingModal";
import UpgradeModal from "component/upgradeModal"; import UpgradeModal from "component/upgradeModal";
import WelcomeModal from "component/welcomeModal";
import lbry from "lbry"; import lbry from "lbry";
import { Line } from "rc-progress"; import { Line } from "rc-progress";
@ -34,6 +35,7 @@ class App extends React.PureComponent {
{modal == "upgrade" && <UpgradeModal />} {modal == "upgrade" && <UpgradeModal />}
{modal == "downloading" && <DownloadingModal />} {modal == "downloading" && <DownloadingModal />}
{modal == "error" && <ErrorModal />} {modal == "error" && <ErrorModal />}
{modal == "welcome" && <WelcomeModal />}
</div> </div>
); );
} }

View file

@ -1,515 +0,0 @@
import React from "react";
import lbry from "../lbry.js";
import lbryio from "../lbryio.js";
import Modal from "./modal.js";
import ModalPage from "./modal-page.js";
import Link from "component/link";
import { RewardLink } from "component/reward-link";
import { FormRow } from "../component/form.js";
import { CreditAmount, Address } from "../component/common.js";
import { getLocal, setLocal } from "../utils.js";
import rewards from "../rewards";
class SubmitEmailStage extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
rewardType: null,
email: "",
submitting: false,
};
}
handleEmailChanged(event) {
this.setState({
email: event.target.value,
});
}
onEmailSaved(email) {
this.props.setStage("confirm", { email: email });
}
handleSubmit(event) {
event.preventDefault();
this.setState({
submitting: true,
});
lbryio.call("user_email", "new", { email: this.state.email }, "post").then(
() => {
this.onEmailSaved(this.state.email);
},
error => {
if (
error.xhr &&
(error.xhr.status == 409 ||
error.message == __("This email is already in use"))
) {
this.onEmailSaved(this.state.email);
return;
} else if (this._emailRow) {
this._emailRow.showError(error.message);
}
this.setState({ submitting: false });
}
);
}
render() {
return (
<section>
<form
onSubmit={event => {
this.handleSubmit(event);
}}
>
<FormRow
ref={ref => {
this._emailRow = ref;
}}
type="text"
label={__("Email")}
placeholder="scrwvwls@lbry.io"
name="email"
value={this.state.email}
onChange={event => {
this.handleEmailChanged(event);
}}
/>
<div className="form-row-submit">
<Link
button="primary"
label={__("Next")}
disabled={this.state.submitting}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
</form>
</section>
);
}
}
class ConfirmEmailStage extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
rewardType: null,
code: "",
submitting: false,
errorMessage: null,
};
}
handleCodeChanged(event) {
this.setState({
code: event.target.value,
});
}
handleSubmit(event) {
event.preventDefault();
this.setState({
submitting: true,
});
const onSubmitError = error => {
if (this._codeRow) {
this._codeRow.showError(error.message);
}
this.setState({ submitting: false });
};
lbryio
.call(
"user_email",
"confirm",
{ verification_token: this.state.code, email: this.props.email },
"post"
)
.then(userEmail => {
if (userEmail.is_verified) {
this.props.setStage("welcome");
} else {
onSubmitError(new Error(__("Your email is still not verified."))); //shouldn't happen?
}
}, onSubmitError);
}
render() {
return (
<section>
<form
onSubmit={event => {
this.handleSubmit(event);
}}
>
<FormRow
label={__("Verification Code")}
ref={ref => {
this._codeRow = ref;
}}
type="text"
name="code"
placeholder="a94bXXXXXXXXXXXXXX"
value={this.state.code}
onChange={event => {
this.handleCodeChanged(event);
}}
helper={__(
"A verification code is required to access this version."
)}
/>
<div className="form-row-submit form-row-submit--with-footer">
<Link
button="primary"
label={__("Verify")}
disabled={this.state.submitting}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
<div className="form-field__helper">
{__("No code?")}
{" "}
<Link
onClick={() => {
this.props.setStage("nocode");
}}
label={__("Click here")}
/>.
</div>
</form>
</section>
);
}
}
class WelcomeStage extends React.PureComponent {
static propTypes = {
endAuth: React.PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
hasReward: false,
rewardAmount: null,
};
}
onRewardClaim(reward) {
this.setState({
hasReward: true,
rewardAmount: reward.amount,
});
}
render() {
return !this.state.hasReward
? <Modal
type="custom"
isOpen={true}
contentLabel={__("Welcome to LBRY")}
{...this.props}
>
<section>
<h3 className="modal__header">{__("Welcome to LBRY.")}</h3>
<p>
{__(
"Using LBRY is like dating a centaur. Totally normal up top, and way different underneath."
)}
</p>
<p>{__("Up top, LBRY is similar to popular media sites.")}</p>
<p>
{__(
"Below, LBRY is controlled by users -- you -- via blockchain and decentralization."
)}
</p>
<p>
{__(
"Thank you for making content freedom possible! Here's a nickel, kid."
)}
</p>
<div style={{ textAlign: "center", marginBottom: "12px" }}>
<RewardLink
type="new_user"
button="primary"
onRewardClaim={event => {
this.onRewardClaim(event);
}}
onRewardFailure={() => this.props.setStage(null)}
onConfirmed={() => {
this.props.setStage(null);
}}
/>
</div>
</section>
</Modal>
: <Modal
type="alert"
overlayClassName="modal-overlay modal-overlay--clear"
isOpen={true}
contentLabel={__("Welcome to LBRY")}
{...this.props}
onConfirmed={() => {
this.props.setStage(null);
}}
>
<section>
<h3 className="modal__header">{__("About Your Reward")}</h3>
<p>
{__("You earned a reward of ")}
{" "}
<CreditAmount amount={this.state.rewardAmount} label={false} />
{" "}{__('LBRY credits, or "LBC".')}
</p>
<p>
{__(
"This reward will show in your Wallet momentarily, probably while you are reading this message."
)}
</p>
<p>
{__(
"LBC is used to compensate creators, to publish, and to have say in how the network works."
)}
</p>
<p>
{__(
"No need to understand it all just yet! Try watching or downloading something next."
)}
</p>
<p>
{__(
"Finally, know that LBRY is an early beta and that it earns the name."
)}
</p>
</section>
</Modal>;
}
}
const ErrorStage = props => {
return (
<section>
<p>{__("An error was encountered that we cannot continue from.")}</p>
<p>{__("At least we're earning the name beta.")}</p>
{props.errorText ? <p>{__("Message:")} {props.errorText}</p> : ""}
<Link
button="alt"
label={__("Try Reload")}
onClick={() => {
window.location.reload();
}}
/>
</section>
);
};
const PendingStage = props => {
return (
<section>
<p>
{__("Preparing for first access")} <span className="busy-indicator" />
</p>
</section>
);
};
class CodeRequiredStage extends React.PureComponent {
constructor(props) {
super(props);
this._balanceSubscribeId = null;
this.state = {
balance: 0,
address: getLocal("wallet_address"),
};
}
componentWillMount() {
this._balanceSubscribeId = lbry.balanceSubscribe(balance => {
this.setState({
balance: balance,
});
});
if (!this.state.address) {
lbry.wallet_unused_address().then(address => {
setLocal("wallet_address", address);
this.setState({ address: address });
});
}
}
componentWillUnmount() {
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId);
}
}
render() {
const disabled = this.state.balance < 1;
return (
<div>
<section className="section-spaced">
<p>
{__(
"Access to LBRY is restricted as we build and scale the network."
)}
</p>
<p>{__("There are two ways in:")}</p>
<h3>{__("Own LBRY Credits")}</h3>
<p>{__("If you own at least 1 LBC, you can get in right now.")}</p>
<p style={{ textAlign: "center" }}>
<Link
onClick={() => {
setLocal("auth_bypassed", true);
this.props.setStage(null);
}}
disabled={disabled}
label={__("Let Me In")}
button={disabled ? "alt" : "primary"}
/>
</p>
<p>
{__("Your balance is ")}<CreditAmount
amount={this.state.balance}
/>. {__("To increase your balance, send credits to this address:")}
</p>
<p>
<Address
address={
this.state.address
? this.state.address
: __("Generating Address...")
}
/>
</p>
<p>{__("If you don't understand how to send credits, then...")}</p>
</section>
<section>
<h3>{__("Wait For A Code")}</h3>
<p>
{__(
"If you provide your email, you'll automatically receive a notification when the system is open."
)}
</p>
<p>
<Link
onClick={() => {
this.props.setStage("email");
}}
label={__("Return")}
/>
</p>
</section>
</div>
);
}
}
export class AuthOverlay extends React.PureComponent {
constructor(props) {
super(props);
this._stages = {
pending: PendingStage,
error: ErrorStage,
nocode: CodeRequiredStage,
email: SubmitEmailStage,
confirm: ConfirmEmailStage,
welcome: WelcomeStage,
};
this.state = {
stage: "pending",
stageProps: {},
};
}
setStage(stage, stageProps = {}) {
this.setState({
stage: stage,
stageProps: stageProps,
});
}
componentWillMount() {
lbryio
.authenticate()
.then(user => {
if (!user.has_verified_email) {
if (getLocal("auth_bypassed")) {
this.setStage(null);
} else {
this.setStage("email", {});
}
} else {
lbryio.call("reward", "list", {}).then(userRewards => {
userRewards.filter(function(reward) {
return (
reward.reward_type == rewards.TYPE_NEW_USER &&
reward.transaction_id
);
}).length
? this.setStage(null)
: this.setStage("welcome");
});
}
})
.catch(err => {
this.setStage("error", { errorText: err.message });
document.dispatchEvent(
new CustomEvent("unhandledError", {
detail: {
message: err.message,
data: err.stack,
},
})
);
});
}
render() {
if (!this.state.stage) {
return null;
}
const StageContent = this._stages[this.state.stage];
if (!StageContent) {
return (
<span className="empty">{__("Unknown authentication step.")}</span>
);
}
return this.state.stage != "welcome"
? <ModalPage
className="modal-page--full"
isOpen={true}
contentLabel={__("Authentication")}
>
<h1>{__("LBRY Early Access")}</h1>
<StageContent
{...this.state.stageProps}
setStage={(stage, stageProps) => {
this.setStage(stage, stageProps);
}}
/>
</ModalPage>
: <StageContent
setStage={(stage, stageProps) => {
this.setStage(stage, stageProps);
}}
{...this.state.stageProps}
/>;
}
}

View file

@ -0,0 +1,16 @@
import React from "react";
import { connect } from "react-redux";
import {
selectAuthenticationIsPending,
selectEmailToVerify,
selectUserIsVerificationCandidate,
} from "selectors/user";
import Auth from "./view";
const select = state => ({
isPending: selectAuthenticationIsPending(state),
email: selectEmailToVerify(state),
isVerificationCandidate: selectUserIsVerificationCandidate(state),
});
export default connect(select, null)(Auth);

View file

@ -0,0 +1,22 @@
import React from "react";
import { BusyMessage } from "component/common";
import UserEmailNew from "component/userEmailNew";
import UserEmailVerify from "component/userEmailVerify";
export class Auth extends React.PureComponent {
render() {
const { isPending, email, isVerificationCandidate } = this.props;
if (isPending) {
return <BusyMessage message={__("Authenticating")} />;
} else if (!email) {
return <UserEmailNew />;
} else if (isVerificationCandidate) {
return <UserEmailVerify />;
} else {
return <span className="empty">{__("No further steps.")}</span>;
}
}
}
export default Auth;

View file

@ -0,0 +1,24 @@
import React from "react";
import * as modal from "constants/modal_types";
import { connect } from "react-redux";
import { doUserEmailDecline } from "actions/user";
import { doOpenModal } from "actions/app";
import {
selectAuthenticationIsPending,
selectUserHasEmail,
selectUserIsAuthRequested,
} from "selectors/user";
import AuthOverlay from "./view";
const select = state => ({
hasEmail: selectUserHasEmail(state),
isPending: selectAuthenticationIsPending(state),
isShowing: selectUserIsAuthRequested(state),
});
const perform = dispatch => ({
userEmailDecline: () => dispatch(doUserEmailDecline()),
openWelcomeModal: () => dispatch(doOpenModal(modal.WELCOME)),
});
export default connect(select, perform)(AuthOverlay);

View file

@ -0,0 +1,83 @@
import React from "react";
import lbryio from "lbryio.js";
import ModalPage from "component/modal-page.js";
import Auth from "component/auth";
import Link from "component/link";
export class AuthOverlay extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
showNoEmailConfirm: false,
};
}
componentWillReceiveProps(nextProps) {
if (this.props.isShowing && !this.props.isPending && !nextProps.isShowing) {
setTimeout(() => this.props.openWelcomeModal(), 1);
}
}
onEmailSkipClick() {
this.setState({ showNoEmailConfirm: true });
}
onEmailSkipConfirm() {
this.props.userEmailDecline();
}
render() {
if (!lbryio.enabled) {
return null;
}
const { isPending, isShowing, hasEmail } = this.props;
if (isShowing) {
return (
<ModalPage
className="modal-page--full"
isOpen={true}
contentLabel="Authentication"
>
<h1>LBRY Early Access</h1>
<Auth />
{isPending
? ""
: <div className="form-row-submit">
{!hasEmail && this.state.showNoEmailConfirm
? <div className="help form-input-width">
<p>
{__(
"If you continue without an email, you will be ineligible to earn free LBC rewards, as well as unable to receive security related communications."
)}
</p>
<Link
onClick={() => {
this.onEmailSkipConfirm();
}}
label={__("Continue without email")}
/>
</div>
: <Link
className={"button-text-help"}
onClick={() => {
hasEmail
? this.onEmailSkipConfirm()
: this.onEmailSkipClick();
}}
label={
hasEmail ? __("Skip for now") : __("Do I have to?")
}
/>}
</div>}
</ModalPage>
);
}
return null;
}
}
export default AuthOverlay;

View file

@ -178,9 +178,19 @@ export class FormRow extends React.PureComponent {
this._fieldRequiredText = __("This field is required"); this._fieldRequiredText = __("This field is required");
this.state = { this.state = this.getStateFromProps(props);
isError: false, }
errorMessage: null,
componentWillReceiveProps(nextProps) {
this.setState(this.getStateFromProps(nextProps));
}
getStateFromProps(props) {
return {
isError: !!props.errorMessage,
errorMessage: typeof props.errorMessage === "string"
? props.errorMessage
: "",
}; };
} }
@ -225,6 +235,7 @@ export class FormRow extends React.PureComponent {
delete fieldProps.label; delete fieldProps.label;
} }
delete fieldProps.helper; delete fieldProps.helper;
delete fieldProps.errorMessage;
return ( return (
<div className="form-row"> <div className="form-row">

View file

@ -1,109 +0,0 @@
import React from "react";
import lbry from "lbry";
import { Icon } from "component/common";
import Modal from "component/modal";
import rewards from "rewards";
import Link from "component/link";
export class RewardLink extends React.PureComponent {
static propTypes = {
type: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool,
onRewardClaim: React.PropTypes.func,
onRewardFailure: React.PropTypes.func,
};
constructor(props) {
super(props);
this.state = {
claimable: true,
pending: false,
errorMessage: null,
};
}
refreshClaimable() {
switch (this.props.type) {
case "new_user":
this.setState({ claimable: true });
return;
case "first_publish":
lbry.claim_list_mine().then(list => {
this.setState({
claimable: list.length > 0,
});
});
return;
}
}
componentWillMount() {
this.refreshClaimable();
}
claimReward() {
this.setState({
pending: true,
});
rewards
.claimReward(this.props.type)
.then(reward => {
this.setState({
pending: false,
errorMessage: null,
});
if (this.props.onRewardClaim) {
this.props.onRewardClaim(reward);
}
})
.catch(error => {
this.setState({
errorMessage: error.message,
pending: false,
});
});
}
clearError() {
if (this.props.onRewardFailure) {
this.props.onRewardFailure();
}
this.setState({
errorMessage: null,
});
}
render() {
return (
<div className="reward-link">
{this.props.claimed
? <span><Icon icon="icon-check" /> {__("Reward claimed.")}</span>
: <Link
button={this.props.button ? this.props.button : "alt"}
disabled={this.state.pending || !this.state.claimable}
label={
this.state.pending ? __("Claiming...") : __("Claim Reward")
}
onClick={() => {
this.claimReward();
}}
/>}
{this.state.errorMessage
? <Modal
isOpen={true}
contentLabel={__("Reward Claim Error")}
className="error-modal"
onConfirmed={() => {
this.clearError();
}}
>
{this.state.errorMessage}
</Modal>
: ""}
</div>
);
}
}

View file

@ -0,0 +1,35 @@
import React from "react";
import { connect } from "react-redux";
import {
makeSelectHasClaimedReward,
makeSelectClaimRewardError,
makeSelectRewardByType,
makeSelectIsRewardClaimPending,
} from "selectors/rewards";
import { doNavigate } from "actions/app";
import { doClaimReward, doClaimRewardClearError } from "actions/rewards";
import RewardLink from "./view";
const makeSelect = () => {
const selectHasClaimedReward = makeSelectHasClaimedReward();
const selectIsPending = makeSelectIsRewardClaimPending();
const selectReward = makeSelectRewardByType();
const selectError = makeSelectClaimRewardError();
const select = (state, props) => ({
isClaimed: selectHasClaimedReward(state, props),
errorMessage: selectError(state, props),
isPending: selectIsPending(state, props),
reward: selectReward(state, props),
});
return select;
};
const perform = dispatch => ({
claimReward: reward => dispatch(doClaimReward(reward, true)),
clearError: reward => dispatch(doClaimRewardClearError(reward)),
navigate: path => dispatch(doNavigate(path)),
});
export default connect(makeSelect, perform)(RewardLink);

View file

@ -0,0 +1,44 @@
import React from "react";
import { Icon } from "component/common";
import Modal from "component/modal";
import Link from "component/link";
const RewardLink = props => {
const {
reward,
button,
claimReward,
clearError,
errorMessage,
isClaimed,
isPending,
} = props;
return (
<div className="reward-link">
{isClaimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <Link
button={button ? button : "alt"}
disabled={isPending}
label={isPending ? "Claiming..." : "Claim Reward"}
onClick={() => {
claimReward(reward);
}}
/>}
{errorMessage
? <Modal
isOpen={true}
contentLabel="Reward Claim Error"
className="error-modal"
onConfirmed={() => {
clearError(reward);
}}
>
{errorMessage}
</Modal>
: ""}
</div>
);
};
export default RewardLink;

View file

@ -7,9 +7,8 @@ import WalletPage from "page/wallet";
import ShowPage from "page/showPage"; import ShowPage from "page/showPage";
import PublishPage from "page/publish"; import PublishPage from "page/publish";
import DiscoverPage from "page/discover"; import DiscoverPage from "page/discover";
import SplashScreen from "component/splash.js";
import DeveloperPage from "page/developer.js"; import DeveloperPage from "page/developer.js";
import RewardsPage from "page/rewards.js"; import RewardsPage from "page/rewards";
import FileListDownloaded from "page/fileListDownloaded"; import FileListDownloaded from "page/fileListDownloaded";
import FileListPublished from "page/fileListPublished"; import FileListPublished from "page/fileListPublished";
import ChannelPage from "page/channel"; import ChannelPage from "page/channel";

View file

@ -0,0 +1,23 @@
import React from 'react'
import {
connect
} from 'react-redux'
import {
doUserEmailNew
} from 'actions/user'
import {
selectEmailNewIsPending,
selectEmailNewErrorMessage,
} from 'selectors/user'
import UserEmailNew from './view'
const select = (state) => ({
isPending: selectEmailNewIsPending(state),
errorMessage: selectEmailNewErrorMessage(state),
})
const perform = (dispatch) => ({
addUserEmail: (email) => dispatch(doUserEmailNew(email))
})
export default connect(select, perform)(UserEmailNew)

View file

@ -0,0 +1,61 @@
import React from "react";
import Link from "component/link";
import { FormRow } from "component/form.js";
class UserEmailNew extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
email: "",
};
}
handleEmailChanged(event) {
this.setState({
email: event.target.value,
});
}
handleSubmit(event) {
event.preventDefault();
this.props.addUserEmail(this.state.email);
}
render() {
const { errorMessage, isPending } = this.props;
return (
<form
className="form-input-width"
onSubmit={event => {
this.handleSubmit(event);
}}
>
<FormRow
type="text"
label="Email"
placeholder="scrwvwls@lbry.io"
name="email"
value={this.state.email}
errorMessage={errorMessage}
onChange={event => {
this.handleEmailChanged(event);
}}
/>
<div className="form-row-submit">
<Link
button="primary"
label="Next"
disabled={isPending}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
</form>
);
}
}
export default UserEmailNew;

View file

@ -0,0 +1,21 @@
import React from "react";
import { connect } from "react-redux";
import { doUserEmailVerify } from "actions/user";
import {
selectEmailVerifyIsPending,
selectEmailToVerify,
selectEmailVerifyErrorMessage,
} from "selectors/user";
import UserEmailVerify from "./view";
const select = state => ({
isPending: selectEmailVerifyIsPending(state),
email: selectEmailToVerify(state),
errorMessage: selectEmailVerifyErrorMessage(state),
});
const perform = dispatch => ({
verifyUserEmail: code => dispatch(doUserEmailVerify(code)),
});
export default connect(select, perform)(UserEmailVerify);

View file

@ -0,0 +1,68 @@
import React from "react";
import Link from "component/link";
import { FormRow } from "component/form.js";
class UserEmailVerify extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
code: "",
};
}
handleCodeChanged(event) {
this.setState({
code: event.target.value,
});
}
handleSubmit(event) {
event.preventDefault();
this.props.verifyUserEmail(this.state.code);
}
render() {
const { errorMessage, isPending } = this.props;
return (
<form
className="form-input-width"
onSubmit={event => {
this.handleSubmit(event);
}}
>
<FormRow
type="text"
label="Verification Code"
placeholder="a94bXXXXXXXXXXXXXX"
name="code"
value={this.state.code}
onChange={event => {
this.handleCodeChanged(event);
}}
errorMessage={errorMessage}
/>
{/* render help separately so it always shows */}
<div className="form-field__helper">
<p>
Email <Link href="mailto:help@lbry.io" label="help@lbry.io" /> if
you did not receive or are having trouble with your code.
</p>
</div>
<div className="form-row-submit form-row-submit--with-footer">
<Link
button="primary"
label="Verify"
disabled={this.state.submitting}
onClick={event => {
this.handleSubmit(event);
}}
/>
</div>
</form>
);
}
}
export default UserEmailVerify;

View file

@ -0,0 +1,28 @@
import React from "react";
import rewards from "rewards";
import { connect } from "react-redux";
import { doCloseModal } from "actions/app";
import { selectUserIsRewardApproved } from "selectors/user";
import {
makeSelectHasClaimedReward,
makeSelectClaimRewardError,
makeSelectRewardByType,
} from "selectors/rewards";
import WelcomeModal from "./view";
const select = (state, props) => {
const selectHasClaimed = makeSelectHasClaimedReward(),
selectReward = makeSelectRewardByType();
return {
hasClaimed: selectHasClaimed(state, { reward_type: rewards.TYPE_NEW_USER }),
isRewardApproved: selectUserIsRewardApproved(state),
reward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
};
};
const perform = dispatch => ({
closeModal: () => dispatch(doCloseModal()),
});
export default connect(select, perform)(WelcomeModal);

View file

@ -0,0 +1,75 @@
import React from "react";
import { Modal } from "component/modal";
import { CreditAmount } from "component/common";
import Link from "component/link";
import RewardLink from "component/rewardLink";
class WelcomeModal extends React.PureComponent {
render() {
const { closeModal, hasClaimed, isRewardApproved, reward } = this.props;
return !hasClaimed
? <Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY">
<section>
<h3 className="modal__header">Welcome to LBRY.</h3>
<p>
Using LBRY is like dating a centaur. Totally normal up top, and
{" "}<em>way different</em> underneath.
</p>
<p>Up top, LBRY is similar to popular media sites.</p>
<p>
Below, LBRY is controlled by users -- you -- via blockchain and
decentralization.
</p>
<p>
Thank you for making content freedom possible!
{" "}{isRewardApproved ? __("Here's a nickel, kid.") : ""}
</p>
<div className="text-center">
{isRewardApproved
? <RewardLink reward_type="new_user" button="primary" />
: <Link
button="primary"
onClick={closeModal}
label="Continue"
/>}
</div>
</section>
</Modal>
: <Modal
type="alert"
overlayClassName="modal-overlay modal-overlay--clear"
isOpen={true}
contentLabel="Welcome to LBRY"
onConfirmed={closeModal}
>
<section>
<h3 className="modal__header">About Your Reward</h3>
<p>
You earned a reward of
{" "}<CreditAmount amount={reward.reward_amount} label={false} />
{" "}LBRY
credits, or <em>LBC</em>.
</p>
<p>
This reward will show in your Wallet momentarily, probably while
you are reading this message.
</p>
<p>
LBC is used to compensate creators, to publish, and to have say in
how the network works.
</p>
<p>
No need to understand it all just yet! Try watching or downloading
something next.
</p>
<p>
Finally, know that LBRY is an early beta and that it earns the
name.
</p>
</section>
</Modal>;
}
}
export default WelcomeModal;

View file

@ -69,3 +69,27 @@ export const SEARCH_CANCELLED = "SEARCH_CANCELLED";
// Settings // Settings
export const DAEMON_SETTINGS_RECEIVED = "DAEMON_SETTINGS_RECEIVED"; export const DAEMON_SETTINGS_RECEIVED = "DAEMON_SETTINGS_RECEIVED";
// User
export const AUTHENTICATION_STARTED = "AUTHENTICATION_STARTED";
export const AUTHENTICATION_SUCCESS = "AUTHENTICATION_SUCCESS";
export const AUTHENTICATION_FAILURE = "AUTHENTICATION_FAILURE";
export const USER_EMAIL_DECLINE = "USER_EMAIL_DECLINE";
export const USER_EMAIL_NEW_STARTED = "USER_EMAIL_NEW_STARTED";
export const USER_EMAIL_NEW_SUCCESS = "USER_EMAIL_NEW_SUCCESS";
export const USER_EMAIL_NEW_EXISTS = "USER_EMAIL_NEW_EXISTS";
export const USER_EMAIL_NEW_FAILURE = "USER_EMAIL_NEW_FAILURE";
export const USER_EMAIL_VERIFY_STARTED = "USER_EMAIL_VERIFY_STARTED";
export const USER_EMAIL_VERIFY_SUCCESS = "USER_EMAIL_VERIFY_SUCCESS";
export const USER_EMAIL_VERIFY_FAILURE = "USER_EMAIL_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";
// Rewards
export const FETCH_REWARDS_STARTED = "FETCH_REWARDS_STARTED";
export const FETCH_REWARDS_COMPLETED = "FETCH_REWARDS_COMPLETED";
export const CLAIM_REWARD_STARTED = "CLAIM_REWARD_STARTED";
export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS";
export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE";
export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR";

View file

@ -0,0 +1 @@
export const WELCOME = "welcome";

View file

@ -1,206 +1,192 @@
import { getSession, setSession } from './utils.js'; import { getSession, setSession, setLocal } from "./utils.js";
import lbry from './lbry.js'; import lbry from "./lbry.js";
const querystring = require('querystring'); const querystring = require("querystring");
const lbryio = { const lbryio = {
_accessToken: getSession('accessToken'), _accessToken: getSession("accessToken"),
_authenticationPromise: null, _authenticationPromise: null,
_user: null, enabled: true,
enabled: true
}; };
const CONNECTION_STRING = process.env.LBRY_APP_API_URL const CONNECTION_STRING = process.env.LBRY_APP_API_URL
? process.env.LBRY_APP_API_URL.replace(/\/*$/, '/') // exactly one slash at the end ? process.env.LBRY_APP_API_URL.replace(/\/*$/, "/") // exactly one slash at the end
: 'https://api.lbry.io/'; : "https://api.lbry.io/";
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
lbryio._exchangePromise = null; lbryio._exchangePromise = null;
lbryio._exchangeLastFetched = null; lbryio._exchangeLastFetched = null;
lbryio.getExchangeRates = function() { lbryio.getExchangeRates = function() {
if ( if (
!lbryio._exchangeLastFetched || !lbryio._exchangeLastFetched ||
Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT
) { ) {
lbryio._exchangePromise = new Promise((resolve, reject) => { lbryio._exchangePromise = new Promise((resolve, reject) => {
lbryio lbryio
.call('lbc', 'exchange_rate', {}, 'get', true) .call("lbc", "exchange_rate", {}, "get", true)
.then(({ lbc_usd, lbc_btc, btc_usd }) => { .then(({ lbc_usd, lbc_btc, btc_usd }) => {
const rates = { lbc_usd, lbc_btc, btc_usd }; const rates = { lbc_usd, lbc_btc, btc_usd };
resolve(rates); resolve(rates);
}) })
.catch(reject); .catch(reject);
}); });
lbryio._exchangeLastFetched = Date.now(); lbryio._exchangeLastFetched = Date.now();
} }
return lbryio._exchangePromise; return lbryio._exchangePromise;
}; };
lbryio.call = function( lbryio.call = function(resource, action, params = {}, method = "get") {
resource, return new Promise((resolve, reject) => {
action, if (!lbryio.enabled && (resource != "discover" || action != "list")) {
params = {}, console.log(__("Internal API disabled"));
method = 'get', reject(new Error(__("LBRY internal API is disabled")));
evenIfDisabled = false return;
) { }
// evenIfDisabled is just for development, when we may have some calls working and some not
return new Promise((resolve, reject) => {
if (
!lbryio.enabled &&
!evenIfDisabled &&
(resource != 'discover' || action != 'list')
) {
console.log(__('Internal API disabled'));
reject(new Error(__('LBRY internal API is disabled')));
return;
}
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.addEventListener('error', function(event) { xhr.addEventListener("error", function(event) {
reject( reject(
new Error(__('Something went wrong making an internal API call.')) new Error(__("Something went wrong making an internal API call."))
); );
}); });
xhr.addEventListener('timeout', function() { xhr.addEventListener("timeout", function() {
reject(new Error(__('XMLHttpRequest connection timed out'))); reject(new Error(__("XMLHttpRequest connection timed out")));
}); });
xhr.addEventListener('load', function() { xhr.addEventListener("load", function() {
const response = JSON.parse(xhr.responseText); const response = JSON.parse(xhr.responseText);
if (!response.success) { if (!response.success) {
if (reject) { if (reject) {
let error = new Error(response.error); let error = new Error(response.error);
error.xhr = xhr; error.xhr = xhr;
reject(error); reject(error);
} else { } else {
document.dispatchEvent( document.dispatchEvent(
new CustomEvent('unhandledError', { new CustomEvent("unhandledError", {
detail: { detail: {
connectionString: connectionString, connectionString: connectionString,
method: action, method: action,
params: params, params: params,
message: response.error.message, message: response.error.message,
...(response.error.data ? { data: response.error.data } : {}) ...(response.error.data ? { data: response.error.data } : {}),
} },
}) })
); );
} }
} else { } else {
resolve(response.data); resolve(response.data);
} }
}); });
// For social media auth: // For social media auth:
//const accessToken = localStorage.getItem('accessToken'); //const accessToken = localStorage.getItem('accessToken');
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}}; //const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}};
// Temp app ID based auth: // Temp app ID based auth:
const fullParams = { app_id: lbryio.getAccessToken(), ...params }; const fullParams = { app_id: lbryio.getAccessToken(), ...params };
if (method == 'get') { if (method == "get") {
xhr.open( xhr.open(
'get', "get",
CONNECTION_STRING + CONNECTION_STRING +
resource + resource +
'/' + "/" +
action + action +
'?' + "?" +
querystring.stringify(fullParams), querystring.stringify(fullParams),
true true
); );
xhr.send(); xhr.send();
} else if (method == 'post') { } else if (method == "post") {
xhr.open('post', CONNECTION_STRING + resource + '/' + action, true); xhr.open("post", CONNECTION_STRING + resource + "/" + action, true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send(querystring.stringify(fullParams)); xhr.send(querystring.stringify(fullParams));
} else { } else {
reject(new Error(__('Invalid method'))); reject(new Error(__("Invalid method")));
} }
}); });
}; };
lbryio.getAccessToken = () => { lbryio.getAccessToken = () => {
const token = getSession('accessToken'); const token = getSession("accessToken");
return token ? token.toString().trim() : token; return token ? token.toString().trim() : token;
}; };
lbryio.setAccessToken = token => { lbryio.setAccessToken = token => {
setSession('accessToken', token ? token.toString().trim() : token); setSession("accessToken", token ? token.toString().trim() : token);
};
lbryio.setCurrentUser = (resolve, reject) => {
lbryio
.call("user", "me")
.then(data => {
resolve(data);
})
.catch(function(err) {
lbryio.setAccessToken(null);
reject(err);
});
}; };
lbryio.authenticate = function() { lbryio.authenticate = function() {
if (!lbryio.enabled) { if (!lbryio.enabled) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
resolve({ resolve({
id: 1, id: 1,
has_verified_email: true language: "en",
}); has_email: true,
}); has_verified_email: true,
} is_reward_approved: false,
if (lbryio._authenticationPromise === null) { is_reward_eligible: false,
lbryio._authenticationPromise = new Promise((resolve, reject) => { });
lbry });
.status() }
.then(response => { if (lbryio._authenticationPromise === null) {
let installation_id = response.installation_id; lbryio._authenticationPromise = new Promise((resolve, reject) => {
lbry
.status()
.then(response => {
let installation_id = response.installation_id;
function setCurrentUser() { if (!lbryio.getAccessToken()) {
lbryio lbryio
.call('user', 'me') .call(
.then(data => { "user",
lbryio.user = data; "new",
resolve(data); {
}) language: "en",
.catch(function(err) { app_id: installation_id,
lbryio.setAccessToken(null); },
if (!getSession('reloadedOnFailedAuth')) { "post"
setSession('reloadedOnFailedAuth', true); )
window.location.reload(); .then(function(responseData) {
} else { if (!responseData.id) {
reject(err); reject(
} new Error("Received invalid authentication response.")
}); );
} }
lbryio.setAccessToken(installation_id);
if (!lbryio.getAccessToken()) { lbryio.setCurrentUser(resolve, reject);
lbryio })
.call( .catch(function(error) {
'user', /*
'new', until we have better error code format, assume all errors are duplicate application id
{ if we're wrong, this will be caught by later attempts to make a valid call
language: 'en', */
app_id: installation_id lbryio.setAccessToken(installation_id);
}, lbryio.setCurrentUser(resolve, reject);
'post' });
) } else {
.then(function(responseData) { lbryio.setCurrentUser(resolve, reject);
if (!responseData.id) { }
reject( })
new Error(__('Received invalid authentication response.')) .catch(reject);
); });
} }
lbryio.setAccessToken(installation_id); return lbryio._authenticationPromise;
setCurrentUser();
})
.catch(function(error) {
/*
until we have better error code format, assume all errors are duplicate application id
if we're wrong, this will be caught by later attempts to make a valid call
*/
lbryio.setAccessToken(installation_id);
setCurrentUser();
});
} else {
setCurrentUser();
}
})
.catch(reject);
});
}
return lbryio._authenticationPromise;
}; };
export default lbryio; export default lbryio;

View file

@ -1,18 +1,13 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import whyDidYouUpdate from "why-did-you-update";
import lbry from "./lbry.js"; import lbry from "./lbry.js";
import lbryio from "./lbryio.js";
import lighthouse from "./lighthouse.js";
import App from "component/app/index.js"; import App from "component/app/index.js";
import SnackBar from "component/snackBar"; import SnackBar from "component/snackBar";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import store from "store.js"; import store from "store.js";
import SplashScreen from "component/splash.js"; import SplashScreen from "component/splash.js";
import { AuthOverlay } from "component/auth.js"; import AuthOverlay from "component/authOverlay";
import { doChangePath, doNavigate, doDaemonReady } from "actions/app"; import { doChangePath, doNavigate, doDaemonReady } from "actions/app";
import { doFetchDaemonSettings } from "actions/settings";
import { doFileList } from "actions/file_info";
import { toQueryString } from "util/query_params"; import { toQueryString } from "util/query_params";
const env = ENV; const env = ENV;
@ -57,7 +52,10 @@ ipcRenderer.on("open-uri-requested", (event, uri) => {
document.addEventListener("click", event => { document.addEventListener("click", event => {
var target = event.target; var target = event.target;
while (target && target !== document) { while (target && target !== document) {
if (target.matches('a[href^="http"]')) { if (
target.matches('a[href^="http"]') ||
target.matches('a[href^="mailto"]')
) {
event.preventDefault(); event.preventDefault();
shell.openExternal(target.href); shell.openExternal(target.href);
return; return;
@ -68,31 +66,27 @@ document.addEventListener("click", event => {
const initialState = app.store.getState(); const initialState = app.store.getState();
if (env === "development") { // import whyDidYouUpdate from "why-did-you-update";
/* // if (env === "development") {
https://github.com/garbles/why-did-you-update // /*
"A function that monkey patches React and notifies you in the console when // https://github.com/garbles/why-did-you-update
potentially unnecessary re-renders occur." // "A function that monkey patches React and notifies you in the console when
// potentially unnecessary re-renders occur."
Just checks if props change between updates. Can be fixed by manually //
adding a check in shouldComponentUpdate or using React.PureComponent // Just checks if props change between updates. Can be fixed by manually
*/ // adding a check in shouldComponentUpdate or using React.PureComponent
whyDidYouUpdate(React); // */
} // whyDidYouUpdate(React);
// }
var init = function() { var init = function() {
function onDaemonReady() { function onDaemonReady() {
window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again
const actions = [];
app.store.dispatch(doDaemonReady()); app.store.dispatch(doDaemonReady());
app.store.dispatch(doChangePath("/discover"));
app.store.dispatch(doFetchDaemonSettings());
app.store.dispatch(doFileList());
ReactDOM.render( ReactDOM.render(
<Provider store={store}> <Provider store={store}>
<div>{lbryio.enabled ? <AuthOverlay /> : ""}<App /><SnackBar /></div> <div><AuthOverlay /><App /><SnackBar /></div>
</Provider>, </Provider>,
canvas canvas
); );

View file

@ -1,10 +1,12 @@
import React from "react"; import React from "react";
import rewards from "rewards";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doFetchFileInfosAndPublishedClaims } from "actions/file_info"; import { doFetchFileInfosAndPublishedClaims } from "actions/file_info";
import { import {
selectFileInfosPublished, selectFileInfosPublished,
selectFileListDownloadedOrPublishedIsPending, selectFileListDownloadedOrPublishedIsPending,
} from "selectors/file_info"; } from "selectors/file_info";
import { doClaimRewardType } from "actions/rewards";
import { doNavigate } from "actions/app"; import { doNavigate } from "actions/app";
import FileListPublished from "./view"; import FileListPublished from "./view";
@ -16,6 +18,8 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
navigate: path => dispatch(doNavigate(path)), navigate: path => dispatch(doNavigate(path)),
fetchFileListPublished: () => dispatch(doFetchFileInfosAndPublishedClaims()), fetchFileListPublished: () => dispatch(doFetchFileInfosAndPublishedClaims()),
claimFirstPublishReward: () =>
dispatch(doClaimRewardType(rewards.TYPE_FIRST_PUBLISH)),
}); });
export default connect(select, perform)(FileListPublished); export default connect(select, perform)(FileListPublished);

View file

@ -16,24 +16,7 @@ class FileListPublished extends React.PureComponent {
} }
componentDidUpdate() { componentDidUpdate() {
if (this.props.fileInfos.length > 0) this._requestPublishReward(); if (this.props.fileInfos.length > 0) this.props.claimFirstPublishReward();
}
_requestPublishReward() {
// TODO this is throwing an error now
// Error: LBRY internal API is disabled
//
// lbryio.call('reward', 'list', {}).then(function(userRewards) {
// //already rewarded
// if (userRewards.filter(function (reward) {
// return reward.reward_type == rewards.TYPE_FIRST_PUBLISH && reward.transaction_id
// }).length) {
// return
// }
// else {
// rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
// }
// })
} }
render() { render() {

View file

@ -1,7 +1,9 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doNavigate, doHistoryBack } from "actions/app"; import { doNavigate, doHistoryBack } from "actions/app";
import { doClaimRewardType } from "actions/rewards";
import { selectMyClaims } from "selectors/claims"; import { selectMyClaims } from "selectors/claims";
import rewards from "rewards";
import PublishPage from "./view"; import PublishPage from "./view";
const select = state => ({ const select = state => ({
@ -11,6 +13,8 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
back: () => dispatch(doHistoryBack()), back: () => dispatch(doHistoryBack()),
navigate: path => dispatch(doNavigate(path)), navigate: path => dispatch(doNavigate(path)),
claimFirstChannelReward: () =>
dispatch(doClaimRewardType(rewards.TYPE_FIRST_CHANNEL)),
}); });
export default connect(select, perform)(PublishPage); export default connect(select, perform)(PublishPage);

View file

@ -44,7 +44,7 @@ class PublishPage extends React.PureComponent {
// Calls API to update displayed list of channels. If a channel name is provided, will select // Calls API to update displayed list of channels. If a channel name is provided, will select
// that channel at the same time (used immediately after creating a channel) // that channel at the same time (used immediately after creating a channel)
lbry.channel_list_mine().then(channels => { lbry.channel_list_mine().then(channels => {
rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {}); this.props.claimFirstChannelReward();
this.setState({ this.setState({
channels: channels, channels: channels,
...(channel ? { channel } : {}), ...(channel ? { channel } : {}),

View file

@ -1,100 +0,0 @@
import React from "react";
import lbryio from "lbryio";
import { CreditAmount, Icon } from "component/common.js";
import SubHeader from "component/subHeader";
import { RewardLink } from "component/reward-link";
export class RewardTile extends React.PureComponent {
static propTypes = {
type: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool.isRequired,
value: React.PropTypes.number.isRequired,
onRewardClaim: React.PropTypes.func,
};
render() {
return (
<section className="card">
<div className="card__inner">
<div className="card__title-primary">
<CreditAmount amount={this.props.value} />
<h3>{this.props.title}</h3>
</div>
<div className="card__actions">
{this.props.claimed
? <span><Icon icon="icon-check" /> {__("Reward claimed.")}</span>
: <RewardLink {...this.props} />}
</div>
<div className="card__content">{this.props.description}</div>
</div>
</section>
);
}
}
export class RewardsPage extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
userRewards: null,
failed: null,
};
}
componentWillMount() {
this.loadRewards();
}
loadRewards() {
lbryio.call("reward", "list", {}).then(
userRewards => {
this.setState({
userRewards: userRewards,
});
},
() => {
this.setState({ failed: true });
}
);
}
render() {
return (
<main className="main--single-column">
<SubHeader />
<div>
{!this.state.userRewards
? this.state.failed
? <div className="empty">{__("Failed to load rewards.")}</div>
: ""
: this.state.userRewards.map(
({
reward_type,
reward_title,
reward_description,
transaction_id,
reward_amount,
}) => {
return (
<RewardTile
key={reward_type}
onRewardClaim={this.loadRewards}
type={reward_type}
title={__(reward_title)}
description={__(reward_description)}
claimed={!!transaction_id}
value={reward_amount}
/>
);
}
)}
</div>
</main>
);
}
}
export default RewardsPage;

View file

@ -0,0 +1,20 @@
import React from "react";
import { connect } from "react-redux";
import { doNavigate } from "actions/app";
import { selectFetchingRewards, selectRewards } from "selectors/rewards";
import {
selectUserIsRewardEligible,
selectUserHasEmail,
selectUserIsVerificationCandidate,
} from "selectors/user";
import RewardsPage from "./view";
const select = state => ({
fetching: selectFetchingRewards(state),
rewards: selectRewards(state),
hasEmail: selectUserHasEmail(state),
isEligible: selectUserIsRewardEligible(state),
isVerificationCandidate: selectUserIsVerificationCandidate(state),
});
export default connect(select, null)(RewardsPage);

View file

@ -0,0 +1,92 @@
import React from "react";
import lbryio from "lbryio";
import { BusyMessage, CreditAmount, Icon } from "component/common";
import SubHeader from "component/subHeader";
import Auth from "component/auth";
import Link from "component/link";
import RewardLink from "component/rewardLink";
const RewardTile = props => {
const { reward } = props;
const claimed = !!reward.transaction_id;
return (
<section className="card">
<div className="card__inner">
<div className="card__title-primary">
<CreditAmount amount={reward.reward_amount} />
<h3>{reward.reward_title}</h3>
</div>
<div className="card__actions">
{claimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <RewardLink reward_type={reward.reward_type} />}
</div>
<div className="card__content">{reward.reward_description}</div>
</div>
</section>
);
};
const RewardsPage = props => {
const {
fetching,
isEligible,
isVerificationCandidate,
hasEmail,
rewards,
} = props;
let content,
isCard = false;
if (!hasEmail || isVerificationCandidate) {
content = (
<div>
<p>
{__(
"Additional information is required to be eligible for the rewards program."
)}
</p>
<Auth />
</div>
);
isCard = true;
} else if (!isEligible) {
isCard = true;
content = (
<div className="empty">
<p>{__("You are not eligible to claim rewards.")}</p>
<p>
To become eligible, email
{" "}<Link href="mailto:help@lbry.io" label="help@lbry.io" /> with a
link to a public social media profile.
</p>
</div>
);
} else if (fetching) {
content = <BusyMessage message="Fetching rewards" />;
} else if (rewards.length > 0) {
content = rewards.map(reward =>
<RewardTile key={reward.reward_type} reward={reward} />
);
} else {
content = <div className="empty">{__("Failed to load rewards.")}</div>;
}
return (
<main className="main--single-column">
<SubHeader />
{isCard
? <section className="card">
<div className="card__content">
{content}
</div>
</section>
: content}
</main>
);
};
export default RewardsPage;

View file

@ -229,25 +229,32 @@ class SettingsPage extends React.PureComponent {
</div> </div>
</section> </section>
{/*}
<section className="card"> <section className="card">
<div className="card__content"> <div className="card__content">
<h3>{__("Language")}</h3> <h3>{__("Language")}</h3>
</div> </div>
<div className="card__content"> <div className="card__content">
<div className="form-row"> <div className="form-row">
<FormField type="radio" <FormField
name="language" type="radio"
label={__("English")} name="language"
onChange={() => { this.onLanguageChange('en') }} label={__("English")}
defaultChecked={this.state.language=='en'} /> onChange={() => {
this.onLanguageChange("en");
}}
defaultChecked={this.state.language == "en"}
/>
</div> </div>
<div className="form-row"> <div className="form-row">
<FormField type="radio" <FormField
name="language" type="radio"
label="Serbian" name="language"
onChange={() => { this.onLanguageChange('rs') }} label="Serbian"
defaultChecked={this.state.language=='rs'} /> onChange={() => {
this.onLanguageChange("rs");
}}
defaultChecked={this.state.language == "rs"}
/>
</div> </div>
</div> </div>
</section>*/} </section>*/}

View file

@ -81,12 +81,6 @@ reducers[types.UPGRADE_DOWNLOAD_PROGRESSED] = function(state, action) {
}); });
}; };
reducers[types.DAEMON_READY] = function(state, action) {
return Object.assign({}, state, {
daemonReady: true,
});
};
reducers[types.SHOW_SNACKBAR] = function(state, action) { reducers[types.SHOW_SNACKBAR] = function(state, action) {
const { message, linkText, linkTarget, isError } = action.data; const { message, linkText, linkTarget, isError } = action.data;
const snackBar = Object.assign({}, state.snackBar); const snackBar = Object.assign({}, state.snackBar);

View file

@ -1,7 +1,90 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
const reducers = {}; const reducers = {};
const defaultState = {}; const defaultState = {
fetching: false,
rewardsByType: {},
claimPendingByType: {},
claimErrorsByType: {},
};
reducers[types.FETCH_REWARDS_STARTED] = function(state, action) {
return Object.assign({}, state, {
fetching: true,
});
};
reducers[types.FETCH_REWARDS_COMPLETED] = function(state, action) {
const { userRewards } = action.data;
const rewardsByType = {};
userRewards.forEach(reward => (rewardsByType[reward.reward_type] = reward));
return Object.assign({}, state, {
rewardsByType: rewardsByType,
fetching: false,
});
};
function setClaimRewardState(state, reward, isClaiming, errorMessage = "") {
const newClaimPendingByType = Object.assign({}, state.claimPendingByType);
const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType);
if (isClaiming) {
newClaimPendingByType[reward.reward_type] = isClaiming;
} else {
delete newClaimPendingByType[reward.reward_type];
}
if (errorMessage) {
newClaimErrorsByType[reward.reward_type] = errorMessage;
} else {
delete newClaimErrorsByType[reward.reward_type];
}
return Object.assign({}, state, {
claimPendingByType: newClaimPendingByType,
claimErrorsByType: newClaimErrorsByType,
});
}
reducers[types.CLAIM_REWARD_STARTED] = function(state, action) {
const { reward } = action.data;
return setClaimRewardState(state, reward, true, "");
};
reducers[types.CLAIM_REWARD_SUCCESS] = function(state, action) {
const { reward } = action.data;
const existingReward = state.rewardsByType[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;
const newState = Object.assign({}, state, { rewardsByType });
return setClaimRewardState(newState, newReward, false, "");
};
reducers[types.CLAIM_REWARD_FAILURE] = function(state, action) {
const { reward, error } = action.data;
return setClaimRewardState(state, reward, false, error ? error.message : "");
};
reducers[types.CLAIM_REWARD_CLEAR_ERROR] = function(state, action) {
const { reward } = action.data;
return setClaimRewardState(
state,
reward,
state.claimPendingByType[reward.reward_type],
""
);
};
export default function reducer(state = defaultState, action) { export default function reducer(state = defaultState, action) {
const handler = reducers[action.type]; const handler = reducers[action.type];

127
ui/js/reducers/user.js Normal file
View file

@ -0,0 +1,127 @@
import * as types from "constants/action_types";
import { getLocal } from "utils";
const reducers = {};
const defaultState = {
authenticationIsPending: false,
userIsPending: false,
emailNewIsPending: false,
emailNewErrorMessage: "",
emailNewDeclined: getLocal("user_email_declined", false),
emailToVerify: "",
user: undefined,
};
reducers[types.AUTHENTICATION_STARTED] = function(state, action) {
return Object.assign({}, state, {
authenticationIsPending: true,
userIsPending: true,
user: defaultState.user,
});
};
reducers[types.AUTHENTICATION_SUCCESS] = function(state, action) {
return Object.assign({}, state, {
authenticationIsPending: false,
userIsPending: false,
user: action.data.user,
});
};
reducers[types.AUTHENTICATION_FAILURE] = function(state, action) {
return Object.assign({}, state, {
authenticationIsPending: false,
userIsPending: false,
user: null,
});
};
reducers[types.USER_FETCH_STARTED] = function(state, action) {
return Object.assign({}, state, {
userIsPending: true,
user: defaultState.user,
});
};
reducers[types.USER_FETCH_SUCCESS] = function(state, action) {
return Object.assign({}, state, {
userIsPending: false,
user: action.data.user,
});
};
reducers[types.USER_FETCH_FAILURE] = function(state, action) {
return Object.assign({}, state, {
userIsPending: true,
user: null,
});
};
reducers[types.USER_EMAIL_DECLINE] = function(state, action) {
return Object.assign({}, state, {
emailNewDeclined: true,
});
};
reducers[types.USER_EMAIL_NEW_STARTED] = function(state, action) {
return Object.assign({}, state, {
emailNewIsPending: true,
emailNewErrorMessage: "",
});
};
reducers[types.USER_EMAIL_NEW_SUCCESS] = function(state, action) {
let user = Object.assign({}, state.user);
user.has_email = true;
return Object.assign({}, state, {
emailToVerify: action.data.email,
emailNewIsPending: false,
user: user,
});
};
reducers[types.USER_EMAIL_NEW_EXISTS] = function(state, action) {
let user = Object.assign({}, state.user);
return Object.assign({}, state, {
emailToVerify: action.data.email,
emailNewIsPending: false,
});
};
reducers[types.USER_EMAIL_NEW_FAILURE] = function(state, action) {
return Object.assign({}, state, {
emailNewIsPending: false,
emailNewErrorMessage: action.data.error,
});
};
reducers[types.USER_EMAIL_VERIFY_STARTED] = function(state, action) {
return Object.assign({}, state, {
emailVerifyIsPending: true,
emailVerifyErrorMessage: "",
});
};
reducers[types.USER_EMAIL_VERIFY_SUCCESS] = function(state, action) {
let user = Object.assign({}, state.user);
user.has_email = true;
return Object.assign({}, state, {
emailToVerify: "",
emailVerifyIsPending: false,
user: user,
});
};
reducers[types.USER_EMAIL_VERIFY_FAILURE] = function(state, action) {
return Object.assign({}, state, {
emailVerifyIsPending: false,
emailVerifyErrorMessage: action.data.error,
});
};
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -1,222 +1,191 @@
const hashes = require('jshashes'); const hashes = require("jshashes");
import lbry from 'lbry'; import lbry from "lbry";
import lbryio from 'lbryio'; import lbryio from "lbryio";
import { doShowSnackBar } from 'actions/app'; import { doShowSnackBar } from "actions/app";
function rewardMessage(type, amount) { function rewardMessage(type, amount) {
return { return {
new_developer: __( new_developer: __(
'You earned %s for registering as a new developer.', "You earned %s for registering as a new developer.",
amount amount
), ),
new_user: __('You earned %s LBC new user reward.', amount), new_user: __("You earned %s LBC new user reward.", amount),
confirm_email: __( confirm_email: __(
'You earned %s LBC for verifying your email address.', "You earned %s LBC for verifying your email address.",
amount amount
), ),
new_channel: __( new_channel: __(
'You earned %s LBC for creating a publisher identity.', "You earned %s LBC for creating a publisher identity.",
amount amount
), ),
first_stream: __( first_stream: __(
'You earned %s LBC for streaming your first video.', "You earned %s LBC for streaming your first video.",
amount amount
), ),
many_downloads: __( many_downloads: __(
'You earned %s LBC for downloading some of the things.', "You earned %s LBC for downloading some of the things.",
amount amount
), ),
first_publish: __( first_publish: __(
'You earned %s LBC for making your first publication.', "You earned %s LBC for making your first publication.",
amount amount
) ),
}[type]; }[type];
} }
function toHex(s) { function toHex(s) {
let h = ''; let h = "";
for (var i = 0; i < s.length; i++) { for (var i = 0; i < s.length; i++) {
let c = s.charCodeAt(i).toString(16); let c = s.charCodeAt(i).toString(16);
if (c.length < 2) { if (c.length < 2) {
c = '0'.concat(c); c = "0".concat(c);
} }
h += c; h += c;
} }
return h; return h;
} }
function fromHex(h) { function fromHex(h) {
let s = ''; let s = "";
for (let i = 0; i < h.length; i += 2) { for (let i = 0; i < h.length; i += 2) {
s += String.fromCharCode(parseInt(h.substr(i, 2), 16)); s += String.fromCharCode(parseInt(h.substr(i, 2), 16));
} }
return s; return s;
} }
function reverseString(s) { function reverseString(s) {
let o = ''; let o = "";
for (let i = s.length - 1; i >= 0; i--) { for (let i = s.length - 1; i >= 0; i--) {
o += s[i]; o += s[i];
} }
return o; return o;
} }
function pack(num) { function pack(num) {
return ( return (
'' + "" +
String.fromCharCode(num & 0xff) + String.fromCharCode(num & 0xff) +
String.fromCharCode((num >> 8) & 0xff) + String.fromCharCode((num >> 8) & 0xff) +
String.fromCharCode((num >> 16) & 0xff) + String.fromCharCode((num >> 16) & 0xff) +
String.fromCharCode((num >> 24) & 0xff) String.fromCharCode((num >> 24) & 0xff)
); );
} }
// Returns true if claim is an initial claim, false if it's an update to an existing claim // Returns true if claim is an initial claim, false if it's an update to an existing claim
function isInitialClaim(claim) { function isInitialClaim(claim) {
const reversed = reverseString(fromHex(claim.txid)); const reversed = reverseString(fromHex(claim.txid));
const concat = reversed.concat(pack(claim.nout)); const concat = reversed.concat(pack(claim.nout));
const sha256 = new hashes.SHA256({ utf8: false }).raw(concat); const sha256 = new hashes.SHA256({ utf8: false }).raw(concat);
const ripemd160 = new hashes.RMD160({ utf8: false }).raw(sha256); const ripemd160 = new hashes.RMD160({ utf8: false }).raw(sha256);
const hash = toHex(reverseString(ripemd160)); const hash = toHex(reverseString(ripemd160));
return hash == claim.claim_id; return hash == claim.claim_id;
} }
const rewards = {}; const rewards = {};
(rewards.TYPE_NEW_DEVELOPER = 'new_developer'), (rewards.TYPE_NEW_USER = (rewards.TYPE_NEW_DEVELOPER = "new_developer"), (rewards.TYPE_NEW_USER =
'new_user'), (rewards.TYPE_CONFIRM_EMAIL = "new_user"), (rewards.TYPE_CONFIRM_EMAIL =
'confirm_email'), (rewards.TYPE_FIRST_CHANNEL = "confirm_email"), (rewards.TYPE_FIRST_CHANNEL =
'new_channel'), (rewards.TYPE_FIRST_STREAM = "new_channel"), (rewards.TYPE_FIRST_STREAM =
'first_stream'), (rewards.TYPE_MANY_DOWNLOADS = "first_stream"), (rewards.TYPE_MANY_DOWNLOADS =
'many_downloads'), (rewards.TYPE_FIRST_PUBLISH = 'first_publish'); "many_downloads"), (rewards.TYPE_FIRST_PUBLISH = "first_publish");
rewards.TYPE_FEATURED_DOWNLOAD = 'featured_download'; rewards.TYPE_FEATURED_DOWNLOAD = "featured_download";
rewards.claimReward = function(type) { rewards.claimReward = function(type) {
function requestReward(resolve, reject, params) { function requestReward(resolve, reject, params) {
if (!lbryio.enabled) { if (!lbryio.enabled || !lbryio.getAccessToken()) {
reject(new Error(__('Rewards are not enabled.'))); reject(new Error(__("Rewards are not enabled.")));
return; return;
} }
lbryio.call('reward', 'new', params, 'post').then(({ reward_amount }) => { lbryio.call("reward", "new", params, "post").then(reward => {
const message = rewardMessage(type, reward_amount), const message = rewardMessage(type, reward.reward_amount);
result = {
type: type,
amount: reward_amount,
message: message
};
// Display global notice // Display global notice
const action = doShowSnackBar({ const action = doShowSnackBar({
message, message,
linkText: __('Show All'), linkText: __("Show All"),
linkTarget: '/rewards', linkTarget: "/rewards",
isError: false isError: false,
}); });
window.app.store.dispatch(action); window.app.store.dispatch(action);
// Add more events here to display other places // Add more events here to display other places
resolve(result); resolve(reward);
}, reject); }, reject);
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lbry.wallet_unused_address().then(address => { lbry.wallet_unused_address().then(address => {
const params = { const params = {
reward_type: type, reward_type: type,
wallet_address: address wallet_address: address,
}; };
switch (type) { switch (type) {
case rewards.TYPE_FIRST_CHANNEL: case rewards.TYPE_FIRST_CHANNEL:
lbry lbry
.claim_list_mine() .claim_list_mine()
.then(function(claims) { .then(function(claims) {
let claim = claims.find(function(claim) { let claim = claims.reverse().find(function(claim) {
return ( return (
claim.name.length && claim.name.length &&
claim.name[0] == '@' && claim.name[0] == "@" &&
claim.txid.length && claim.txid.length &&
isInitialClaim(claim) isInitialClaim(claim)
); );
}); });
if (claim) { if (claim) {
params.transaction_id = claim.txid; params.transaction_id = claim.txid;
requestReward(resolve, reject, params); requestReward(resolve, reject, params);
} else { } else {
reject( reject(
new Error(__('Please create a channel identity first.')) new Error(__("Please create a channel identity first."))
); );
} }
}) })
.catch(reject); .catch(reject);
break; break;
case rewards.TYPE_FIRST_PUBLISH: case rewards.TYPE_FIRST_PUBLISH:
lbry lbry
.claim_list_mine() .claim_list_mine()
.then(claims => { .then(claims => {
let claim = claims.find(function(claim) { let claim = claims.reverse().find(function(claim) {
return ( return (
claim.name.length && claim.name.length &&
claim.name[0] != '@' && claim.name[0] != "@" &&
claim.txid.length && claim.txid.length &&
isInitialClaim(claim) isInitialClaim(claim)
); );
}); });
if (claim) { if (claim) {
params.transaction_id = claim.txid; params.transaction_id = claim.txid;
requestReward(resolve, reject, params); requestReward(resolve, reject, params);
} else { } else {
reject( reject(
claims.length claims.length
? new Error( ? new Error(
__( __(
'Please publish something and wait for confirmation by the network to claim this reward.' "Please publish something and wait for confirmation by the network to claim this reward."
) )
) )
: new Error( : new Error(
__('Please publish something to claim this reward.') __("Please publish something to claim this reward.")
) )
); );
} }
}) })
.catch(reject); .catch(reject);
break; break;
case rewards.TYPE_FIRST_STREAM: case rewards.TYPE_FIRST_STREAM:
case rewards.TYPE_NEW_USER: case rewards.TYPE_NEW_USER:
default: default:
requestReward(resolve, reject, params); requestReward(resolve, reject, params);
} }
}); });
}); });
};
rewards.claimEligiblePurchaseRewards = function() {
let types = {};
types[rewards.TYPE_FIRST_STREAM] = false;
types[rewards.TYPE_FEATURED_DOWNLOAD] = false;
types[rewards.TYPE_MANY_DOWNLOADS] = false;
lbryio.call('reward', 'list', {}).then(
userRewards => {
userRewards.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) {
rewards.claimReward(unclaimedType);
}
if (types[rewards.TYPE_FEATURED_DOWNLOAD] === false) {
rewards.claimReward(rewards.TYPE_FEATURED_DOWNLOAD);
}
},
() => {}
);
}; };
export default rewards; export default rewards;

View file

@ -1,5 +1,4 @@
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { selectDaemonReady, selectCurrentPage } from "selectors/app";
const _selectState = state => state.availability; const _selectState = state => state.availability;

View file

@ -1,5 +1,4 @@
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { selectDaemonReady, selectCurrentPage } from "selectors/app";
export const _selectState = state => state.content || {}; export const _selectState = state => state.content || {};

View file

@ -1,3 +1,67 @@
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { selectUser } from "selectors/user";
export const _selectState = state => state.rewards || {}; const _selectState = state => state.rewards || {};
export const selectRewardsByType = createSelector(
_selectState,
state => state.rewardsByType || {}
);
export const selectRewards = createSelector(
selectRewardsByType,
byType => Object.values(byType) || []
);
export const selectIsRewardEligible = createSelector(
selectUser,
user => user.can_claim_rewards
);
export const selectFetchingRewards = createSelector(
_selectState,
state => !!state.fetching
);
export const selectHasClaimedReward = (state, props) => {
const reward = selectRewardsByType(state)[props.reward_type];
return reward && reward.transaction_id !== "";
};
export const makeSelectHasClaimedReward = () => {
return createSelector(selectHasClaimedReward, claimed => claimed);
};
export const selectClaimsPendingByType = createSelector(
_selectState,
state => state.claimPendingByType
);
const selectIsClaimRewardPending = (state, props) => {
return selectClaimsPendingByType(state, props)[props.reward_type];
};
export const makeSelectIsRewardClaimPending = () => {
return createSelector(selectIsClaimRewardPending, isClaiming => isClaiming);
};
export const selectClaimErrorsByType = createSelector(
_selectState,
state => state.claimErrorsByType
);
const selectClaimRewardError = (state, props) => {
return selectClaimErrorsByType(state, props)[props.reward_type];
};
export const makeSelectClaimRewardError = () => {
return createSelector(selectClaimRewardError, errorMessage => errorMessage);
};
const selectRewardByType = (state, props) => {
return selectRewardsByType(state)[props.reward_type];
};
export const makeSelectRewardByType = () => {
return createSelector(selectRewardByType, reward => reward);
};

82
ui/js/selectors/user.js Normal file
View file

@ -0,0 +1,82 @@
import { createSelector } from "reselect";
export const _selectState = state => state.user || {};
export const selectAuthenticationIsPending = createSelector(
_selectState,
state => state.authenticationIsPending
);
export const selectUserIsPending = createSelector(
_selectState,
state => state.userIsPending
);
export const selectUser = createSelector(
_selectState,
state => state.user || {}
);
export const selectEmailToVerify = createSelector(
_selectState,
state => state.emailToVerify
);
export const selectUserHasEmail = createSelector(
selectUser,
selectEmailToVerify,
(user, email) => (user && user.has_email) || email
);
export const selectUserIsRewardEligible = createSelector(
selectUser,
user => user && user.is_reward_eligible
);
export const selectUserIsRewardApproved = createSelector(
selectUser,
user => user && user.is_reward_approved
);
export const selectEmailNewIsPending = createSelector(
_selectState,
state => state.emailNewIsPending
);
export const selectEmailNewErrorMessage = createSelector(
_selectState,
state => state.emailNewErrorMessage
);
export const selectEmailNewDeclined = createSelector(
_selectState,
state => state.emailNewDeclined
);
export const selectEmailVerifyIsPending = createSelector(
_selectState,
state => state.emailVerifyIsPending
);
export const selectEmailVerifyErrorMessage = createSelector(
_selectState,
state => state.emailVerifyErrorMessage
);
export const selectUserIsVerificationCandidate = createSelector(
selectUserIsRewardEligible,
selectUserIsRewardApproved,
selectEmailToVerify,
selectUser,
(isEligible, isApproved, emailToVerify, user) =>
(isEligible && !isApproved) || (emailToVerify && user && !user.has_email)
);
export const selectUserIsAuthRequested = createSelector(
selectEmailNewDeclined,
selectAuthenticationIsPending,
selectUserIsVerificationCandidate,
selectUserHasEmail,
(isEmailDeclined, isPending, isVerificationCandidate, hasEmail) =>
!isEmailDeclined && (isPending || !hasEmail || isVerificationCandidate)
);

View file

@ -50,20 +50,6 @@ export const selectGettingNewAddress = createSelector(
state => state.gettingNewAddress state => state.gettingNewAddress
); );
export const shouldCheckAddressIsMine = createSelector(
_selectState,
selectCurrentPage,
selectReceiveAddress,
selectDaemonReady,
(state, page, address, daemonReady) => {
if (!daemonReady) return false;
if (address === undefined) return false;
if (state.addressOwnershipChecked) return false;
return true;
}
);
export const selectDraftTransaction = createSelector( export const selectDraftTransaction = createSelector(
_selectState, _selectState,
state => state.draftTransaction || {} state => state.draftTransaction || {}

View file

@ -13,6 +13,7 @@ import rewardsReducer from 'reducers/rewards';
import searchReducer from 'reducers/search'; import searchReducer from 'reducers/search';
import settingsReducer from 'reducers/settings'; import settingsReducer from 'reducers/settings';
import walletReducer from 'reducers/wallet'; import walletReducer from 'reducers/wallet';
import userReducer from 'reducers/user';
function isFunction(object) { function isFunction(object) {
return typeof object === 'function'; return typeof object === 'function';
@ -47,16 +48,17 @@ function enableBatching(reducer) {
} }
const reducers = redux.combineReducers({ const reducers = redux.combineReducers({
app: appReducer, app: appReducer,
availability: availabilityReducer, availability: availabilityReducer,
claims: claimsReducer, claims: claimsReducer,
fileInfo: fileInfoReducer, fileInfo: fileInfoReducer,
content: contentReducer, content: contentReducer,
costInfo: costInfoReducer, costInfo: costInfoReducer,
rewards: rewardsReducer, rewards: rewardsReducer,
search: searchReducer, search: searchReducer,
settings: settingsReducer, settings: settingsReducer,
wallet: walletReducer wallet: walletReducer,
user: userReducer,
}); });
const bulkThunk = createBulkThunkMiddleware(); const bulkThunk = createBulkThunkMiddleware();

View file

@ -165,3 +165,8 @@ p
section.section-spaced { section.section-spaced {
margin-bottom: $spacing-vertical; margin-bottom: $spacing-vertical;
} }
.text-center
{
text-align: center;
}

View file

@ -25,7 +25,7 @@ $padding-card-horizontal: $spacing-vertical * 2/3;
} }
.card__title-primary { .card__title-primary {
padding: 0 $padding-card-horizontal; padding: 0 $padding-card-horizontal;
margin-top: $spacing-vertical; margin-top: $spacing-vertical * 2/3;
} }
.card__title-identity { .card__title-identity {
padding: 0 $padding-card-horizontal; padding: 0 $padding-card-horizontal;

View file

@ -3,6 +3,10 @@
$width-input-border: 2px; $width-input-border: 2px;
$width-input-text: 330px; $width-input-text: 330px;
.form-input-width {
width: $width-input-text
}
.form-row-submit .form-row-submit
{ {
margin-top: $spacing-vertical; margin-top: $spacing-vertical;