Merge branch 'master' into fix-publish-my-claims-display

This commit is contained in:
Jeremy Kauffman 2017-06-09 10:49:12 -04:00 committed by GitHub
commit cfa3aa3bc4
54 changed files with 3191 additions and 2237 deletions

View file

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.12.0rc6
current_version = 0.12.0rc7
commit = True
tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)((?P<release>[a-z]+)(?P<candidate>\d+))?

View file

@ -9,6 +9,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased]
### Added
* 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
### Changed
@ -20,6 +21,7 @@ Web UI version numbers should always match the corresponding version of LBRY App
* Updated deprecated LBRY API call signatures
* App scrolls to the top of the page on navigation
* Download progress works properly for purchased but deleted files
* Publish channels for less than 1 LBC
### Deprecated
*

1290
app/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "LBRY",
"version": "0.12.0rc6",
"version": "0.12.0rc7",
"main": "main.js",
"description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.",
"author": {

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

958
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,10 @@ import {
selectCurrentParams,
} from "selectors/app";
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 path = require("path");
@ -216,8 +220,14 @@ export function doAlertError(errorList) {
}
export function doDaemonReady() {
return {
type: types.DAEMON_READY,
return function(dispatch, getState) {
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 lbryio from "lbryio";
import lbryuri from "lbryuri";
import rewards from "rewards";
import { selectBalance } from "selectors/wallet";
import {
selectFileInfoForUri,
@ -10,8 +9,8 @@ import {
} from "selectors/file_info";
import { selectResolvingUris } from "selectors/content";
import { selectCostInfoForUri } from "selectors/cost_info";
import { selectClaimsByUri } from "selectors/claims";
import { doOpenModal } from "actions/app";
import { doClaimEligiblePurchaseRewards } from "actions/rewards";
export function doResolveUri(uri) {
return function(dispatch, getState) {
@ -171,7 +170,7 @@ export function doDownloadFile(uri, streamInfo) {
})
.catch(() => {});
rewards.claimEligiblePurchaseRewards();
dispatch(doClaimEligiblePurchaseRewards());
};
}

View file

@ -2,8 +2,9 @@ import * as types from "constants/action_types";
import lbry from "lbry";
import lbryio from "lbryio";
import rewards from "rewards";
import { selectRewards, selectRewardsByType } from "selectors/rewards";
export function doFetchRewards() {
export function doRewardList() {
return function(dispatch, getState) {
const state = getState();
@ -11,25 +12,105 @@ export function doFetchRewards() {
type: types.FETCH_REWARDS_STARTED,
});
lbryio.call("reward", "list", {}).then(function(userRewards) {
dispatch({
type: types.FETCH_REWARDS_COMPLETED,
data: { userRewards },
lbryio
.call("reward", "list", {})
.then(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) {
try {
rewards.claimReward(rewards[rewardType]);
dispatch({
type: types.REWARD_CLAIMED,
data: {
reward: rewards[rewardType],
},
});
} catch (err) {}
const rewardsByType = selectRewardsByType(getState()),
reward = rewardsByType[rewardType];
if (reward) {
dispatch(doClaimReward(reward));
}
};
}
export function doClaimReward(reward, saveError = false) {
return function(dispatch, getState) {
if (reward.transaction_id) {
//already claimed, do nothing
return;
}
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 DownloadingModal from "component/downloadingModal";
import UpgradeModal from "component/upgradeModal";
import WelcomeModal from "component/welcomeModal";
import lbry from "lbry";
import { Line } from "rc-progress";
@ -34,6 +35,7 @@ class App extends React.PureComponent {
{modal == "upgrade" && <UpgradeModal />}
{modal == "downloading" && <DownloadingModal />}
{modal == "error" && <ErrorModal />}
{modal == "welcome" && <WelcomeModal />}
</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.state = {
isError: false,
errorMessage: null,
this.state = this.getStateFromProps(props);
}
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.helper;
delete fieldProps.errorMessage;
return (
<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 PublishPage from "page/publish";
import DiscoverPage from "page/discover";
import SplashScreen from "component/splash.js";
import DeveloperPage from "page/developer.js";
import RewardsPage from "page/rewards.js";
import RewardsPage from "page/rewards";
import FileListDownloaded from "page/fileListDownloaded";
import FileListPublished from "page/fileListPublished";
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
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 lbry from './lbry.js';
import { getSession, setSession, setLocal } from "./utils.js";
import lbry from "./lbry.js";
const querystring = require('querystring');
const querystring = require("querystring");
const lbryio = {
_accessToken: getSession('accessToken'),
_authenticationPromise: null,
_user: null,
enabled: true
_accessToken: getSession("accessToken"),
_authenticationPromise: null,
enabled: true,
};
const CONNECTION_STRING = process.env.LBRY_APP_API_URL
? process.env.LBRY_APP_API_URL.replace(/\/*$/, '/') // exactly one slash at the end
: 'https://api.lbry.io/';
? process.env.LBRY_APP_API_URL.replace(/\/*$/, "/") // exactly one slash at the end
: "https://api.lbry.io/";
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
lbryio._exchangePromise = null;
lbryio._exchangeLastFetched = null;
lbryio.getExchangeRates = function() {
if (
!lbryio._exchangeLastFetched ||
Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT
) {
lbryio._exchangePromise = new Promise((resolve, reject) => {
lbryio
.call('lbc', 'exchange_rate', {}, 'get', true)
.then(({ lbc_usd, lbc_btc, btc_usd }) => {
const rates = { lbc_usd, lbc_btc, btc_usd };
resolve(rates);
})
.catch(reject);
});
lbryio._exchangeLastFetched = Date.now();
}
return lbryio._exchangePromise;
if (
!lbryio._exchangeLastFetched ||
Date.now() - lbryio._exchangeLastFetched > EXCHANGE_RATE_TIMEOUT
) {
lbryio._exchangePromise = new Promise((resolve, reject) => {
lbryio
.call("lbc", "exchange_rate", {}, "get", true)
.then(({ lbc_usd, lbc_btc, btc_usd }) => {
const rates = { lbc_usd, lbc_btc, btc_usd };
resolve(rates);
})
.catch(reject);
});
lbryio._exchangeLastFetched = Date.now();
}
return lbryio._exchangePromise;
};
lbryio.call = function(
resource,
action,
params = {},
method = 'get',
evenIfDisabled = false
) {
// 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;
}
lbryio.call = function(resource, action, params = {}, method = "get") {
return new Promise((resolve, reject) => {
if (!lbryio.enabled && (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) {
reject(
new Error(__('Something went wrong making an internal API call.'))
);
});
xhr.addEventListener("error", function(event) {
reject(
new Error(__("Something went wrong making an internal API call."))
);
});
xhr.addEventListener('timeout', function() {
reject(new Error(__('XMLHttpRequest connection timed out')));
});
xhr.addEventListener("timeout", function() {
reject(new Error(__("XMLHttpRequest connection timed out")));
});
xhr.addEventListener('load', function() {
const response = JSON.parse(xhr.responseText);
xhr.addEventListener("load", function() {
const response = JSON.parse(xhr.responseText);
if (!response.success) {
if (reject) {
let error = new Error(response.error);
error.xhr = xhr;
reject(error);
} else {
document.dispatchEvent(
new CustomEvent('unhandledError', {
detail: {
connectionString: connectionString,
method: action,
params: params,
message: response.error.message,
...(response.error.data ? { data: response.error.data } : {})
}
})
);
}
} else {
resolve(response.data);
}
});
if (!response.success) {
if (reject) {
let error = new Error(response.error);
error.xhr = xhr;
reject(error);
} else {
document.dispatchEvent(
new CustomEvent("unhandledError", {
detail: {
connectionString: connectionString,
method: action,
params: params,
message: response.error.message,
...(response.error.data ? { data: response.error.data } : {}),
},
})
);
}
} else {
resolve(response.data);
}
});
// For social media auth:
//const accessToken = localStorage.getItem('accessToken');
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}};
// For social media auth:
//const accessToken = localStorage.getItem('accessToken');
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}};
// Temp app ID based auth:
const fullParams = { app_id: lbryio.getAccessToken(), ...params };
// Temp app ID based auth:
const fullParams = { app_id: lbryio.getAccessToken(), ...params };
if (method == 'get') {
xhr.open(
'get',
CONNECTION_STRING +
resource +
'/' +
action +
'?' +
querystring.stringify(fullParams),
true
);
xhr.send();
} else if (method == 'post') {
xhr.open('post', CONNECTION_STRING + resource + '/' + action, true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send(querystring.stringify(fullParams));
} else {
reject(new Error(__('Invalid method')));
}
});
if (method == "get") {
xhr.open(
"get",
CONNECTION_STRING +
resource +
"/" +
action +
"?" +
querystring.stringify(fullParams),
true
);
xhr.send();
} else if (method == "post") {
xhr.open("post", CONNECTION_STRING + resource + "/" + action, true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send(querystring.stringify(fullParams));
} else {
reject(new Error(__("Invalid method")));
}
});
};
lbryio.getAccessToken = () => {
const token = getSession('accessToken');
return token ? token.toString().trim() : token;
const token = getSession("accessToken");
return token ? token.toString().trim() : 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() {
if (!lbryio.enabled) {
return new Promise((resolve, reject) => {
resolve({
id: 1,
has_verified_email: true
});
});
}
if (lbryio._authenticationPromise === null) {
lbryio._authenticationPromise = new Promise((resolve, reject) => {
lbry
.status()
.then(response => {
let installation_id = response.installation_id;
if (!lbryio.enabled) {
return new Promise((resolve, reject) => {
resolve({
id: 1,
language: "en",
has_email: true,
has_verified_email: true,
is_reward_approved: false,
is_reward_eligible: false,
});
});
}
if (lbryio._authenticationPromise === null) {
lbryio._authenticationPromise = new Promise((resolve, reject) => {
lbry
.status()
.then(response => {
let installation_id = response.installation_id;
function setCurrentUser() {
lbryio
.call('user', 'me')
.then(data => {
lbryio.user = data;
resolve(data);
})
.catch(function(err) {
lbryio.setAccessToken(null);
if (!getSession('reloadedOnFailedAuth')) {
setSession('reloadedOnFailedAuth', true);
window.location.reload();
} else {
reject(err);
}
});
}
if (!lbryio.getAccessToken()) {
lbryio
.call(
'user',
'new',
{
language: 'en',
app_id: installation_id
},
'post'
)
.then(function(responseData) {
if (!responseData.id) {
reject(
new Error(__('Received invalid authentication response.'))
);
}
lbryio.setAccessToken(installation_id);
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;
if (!lbryio.getAccessToken()) {
lbryio
.call(
"user",
"new",
{
language: "en",
app_id: installation_id,
},
"post"
)
.then(function(responseData) {
if (!responseData.id) {
reject(
new Error("Received invalid authentication response.")
);
}
lbryio.setAccessToken(installation_id);
lbryio.setCurrentUser(resolve, reject);
})
.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);
lbryio.setCurrentUser(resolve, reject);
});
} else {
lbryio.setCurrentUser(resolve, reject);
}
})
.catch(reject);
});
}
return lbryio._authenticationPromise;
};
export default lbryio;

View file

@ -1,18 +1,13 @@
import React from "react";
import ReactDOM from "react-dom";
import whyDidYouUpdate from "why-did-you-update";
import lbry from "./lbry.js";
import lbryio from "./lbryio.js";
import lighthouse from "./lighthouse.js";
import App from "component/app/index.js";
import SnackBar from "component/snackBar";
import { Provider } from "react-redux";
import store from "store.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 { doFetchDaemonSettings } from "actions/settings";
import { doFileList } from "actions/file_info";
import { toQueryString } from "util/query_params";
const env = ENV;
@ -57,7 +52,10 @@ ipcRenderer.on("open-uri-requested", (event, uri) => {
document.addEventListener("click", event => {
var target = event.target;
while (target && target !== document) {
if (target.matches('a[href^="http"]')) {
if (
target.matches('a[href^="http"]') ||
target.matches('a[href^="mailto"]')
) {
event.preventDefault();
shell.openExternal(target.href);
return;
@ -68,31 +66,27 @@ document.addEventListener("click", event => {
const initialState = app.store.getState();
if (env === "development") {
/*
https://github.com/garbles/why-did-you-update
"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
*/
whyDidYouUpdate(React);
}
// 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
// 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
// */
// whyDidYouUpdate(React);
// }
var init = function() {
function onDaemonReady() {
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(doChangePath("/discover"));
app.store.dispatch(doFetchDaemonSettings());
app.store.dispatch(doFileList());
ReactDOM.render(
<Provider store={store}>
<div>{lbryio.enabled ? <AuthOverlay /> : ""}<App /><SnackBar /></div>
<div><AuthOverlay /><App /><SnackBar /></div>
</Provider>,
canvas
);

View file

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

View file

@ -16,24 +16,7 @@ class FileListPublished extends React.PureComponent {
}
componentDidUpdate() {
if (this.props.fileInfos.length > 0) this._requestPublishReward();
}
_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(() => {})
// }
// })
if (this.props.fileInfos.length > 0) this.props.claimFirstPublishReward();
}
render() {

View file

@ -1,8 +1,10 @@
import React from "react";
import { connect } from "react-redux";
import { doNavigate, doHistoryBack } from "actions/app";
import { doClaimRewardType } from "actions/rewards";
import { selectMyClaims } from "selectors/claims";
import { doFetchClaimListMine } from "actions/content";
import rewards from "rewards";
import PublishPage from "./view";
const select = state => ({
@ -13,6 +15,7 @@ const perform = dispatch => ({
back: () => dispatch(doHistoryBack()),
navigate: path => dispatch(doNavigate(path)),
fetchClaimListMine: () => dispatch(doFetchClaimListMine()),
claimFirstChannelReward: () => dispatch(doClaimRewardType(rewards.TYPE_FIRST_CHANNEL)),
});
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
// that channel at the same time (used immediately after creating a channel)
lbry.channel_list_mine().then(channels => {
rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {});
this.props.claimFirstChannelReward();
this.setState({
channels: channels,
...(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>
</section>
{/*}
<section className="card">
<div className="card__content">
<h3>{__("Language")}</h3>
</div>
<div className="card__content">
<div className="form-row">
<FormField type="radio"
name="language"
label={__("English")}
onChange={() => { this.onLanguageChange('en') }}
defaultChecked={this.state.language=='en'} />
<FormField
type="radio"
name="language"
label={__("English")}
onChange={() => {
this.onLanguageChange("en");
}}
defaultChecked={this.state.language == "en"}
/>
</div>
<div className="form-row">
<FormField type="radio"
name="language"
label="Serbian"
onChange={() => { this.onLanguageChange('rs') }}
defaultChecked={this.state.language=='rs'} />
<FormField
type="radio"
name="language"
label="Serbian"
onChange={() => {
this.onLanguageChange("rs");
}}
defaultChecked={this.state.language == "rs"}
/>
</div>
</div>
</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) {
const { message, linkText, linkTarget, isError } = action.data;
const snackBar = Object.assign({}, state.snackBar);

View file

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

View file

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

View file

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

View file

@ -1,3 +1,67 @@
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
);
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(
_selectState,
state => state.draftTransaction || {}

View file

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

334
ui/package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "lbry-web-ui",
"version": "0.12.0rc4",
"version": "0.12.0rc6",
"lockfileVersion": 1,
"dependencies": {
"abbrev": {
@ -89,6 +89,12 @@
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz",
"integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc="
},
"app-root-path": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.0.1.tgz",
"integrity": "sha1-zWLc+OT9WkF+/GZNLlsQZTxlG0Y=",
"dev": true
},
"aproba": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.2.tgz",
@ -223,14 +229,14 @@
"integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ="
},
"babel-core": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.24.1.tgz",
"integrity": "sha1-jEKFZNzh4fQfszfsNPTDsCK1rYM="
"version": "6.25.0",
"resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.25.0.tgz",
"integrity": "sha1-fdQrBGPHQunVKW3rPsZ6kyLa1yk="
},
"babel-generator": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.24.1.tgz",
"integrity": "sha1-5xX0hsWN7SVknYiJRNUqoHxdlJc="
"version": "6.25.0",
"resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.25.0.tgz",
"integrity": "sha1-M6GvcNXyiQrrRlpKd5PB32qeqfw="
},
"babel-helper-bindify-decorators": {
"version": "6.24.1",
@ -544,9 +550,9 @@
"dev": true
},
"babel-plugin-transform-react-display-name": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.23.0.tgz",
"integrity": "sha1-Q5iRDDWEQdxM7xh4cmTQQS7Tazc="
"version": "6.25.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz",
"integrity": "sha1-Z+K/Hx6ck6sI25Z5LgU5K/LMKNE="
},
"babel-plugin-transform-react-jsx": {
"version": "6.24.1",
@ -616,24 +622,24 @@
"integrity": "sha1-CpSJ8UTecO+zzkMArM2zKeL8VDs="
},
"babel-template": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.24.1.tgz",
"integrity": "sha1-BK5RTx+Ts6JTfyoPYKWkX7gwgzM="
"version": "6.25.0",
"resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.25.0.tgz",
"integrity": "sha1-ZlJBFmt8KqTGGdceGSlpVSsQwHE="
},
"babel-traverse": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.24.1.tgz",
"integrity": "sha1-qzZnP9NW+aCUhlnnszjV/q2zFpU="
"version": "6.25.0",
"resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.25.0.tgz",
"integrity": "sha1-IldJfi/NGbie3BPEyROB+VEklvE="
},
"babel-types": {
"version": "6.24.1",
"resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.24.1.tgz",
"integrity": "sha1-oTaHncFbNga9oNkMH8dDBML/CXU="
"version": "6.25.0",
"resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.25.0.tgz",
"integrity": "sha1-cK+ySNVmDl0Y+BHZHIMDtUE0oY4="
},
"babylon": {
"version": "6.17.2",
"resolved": "https://registry.npmjs.org/babylon/-/babylon-6.17.2.tgz",
"integrity": "sha1-IB0l71+JLEG65JSIsI2w3Udun1w="
"version": "6.17.3",
"resolved": "https://registry.npmjs.org/babylon/-/babylon-6.17.3.tgz",
"integrity": "sha512-mq0x3HCAGGmQyZXviOVe5TRsw37Ijy3D43jCqt/9WVf+onx2dUgW3PosnqCbScAFhRO9DGs8nxoMzU0iiosMqQ=="
},
"balanced-match": {
"version": "0.4.2",
@ -778,6 +784,12 @@
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
"integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg="
},
"ci-info": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.0.0.tgz",
"integrity": "sha1-3FKF8rTiUYIWg2gcOBwziPRuxTQ=",
"dev": true
},
"circular-json": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz",
@ -790,12 +802,24 @@
"integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
"dev": true
},
"cli-spinners": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-0.1.2.tgz",
"integrity": "sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw=",
"dev": true
},
"cli-table": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz",
"integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=",
"dev": true
},
"cli-truncate": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz",
"integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=",
"dev": true
},
"cli-usage": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/cli-usage/-/cli-usage-0.1.4.tgz",
@ -956,6 +980,20 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cosmiconfig": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-1.1.0.tgz",
"integrity": "sha1-DeoPmATv37kp+7GxiOJVU+oFPTc=",
"dev": true,
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
}
},
"create-react-class": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.5.3.tgz",
@ -1006,6 +1044,12 @@
}
}
},
"date-fns": {
"version": "1.28.5",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.28.5.tgz",
"integrity": "sha1-JXz8RdMi30XvVlhmWWfuhBzXP68=",
"dev": true
},
"date-now": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
@ -1102,6 +1146,12 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
"dev": true
},
"elegant-spinner": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz",
"integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=",
"dev": true
},
"element-class": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/element-class/-/element-class-0.2.2.tgz",
@ -1434,6 +1484,20 @@
"integrity": "sha1-Cs7ehJ7X3RzMMsgRuxG5RNTykjI=",
"dev": true
},
"execa": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/execa/-/execa-0.6.3.tgz",
"integrity": "sha1-V7aaWU8IF1nGnlNw8NF7nLEWWP4=",
"dev": true,
"dependencies": {
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"dev": true
}
}
},
"exenv": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.0.tgz",
@ -1550,6 +1614,12 @@
"integrity": "sha1-yN765XyKUqinhPnjHFfHQumToLk=",
"dev": true
},
"find-parent-dir": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz",
"integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=",
"dev": true
},
"find-up": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
@ -2232,6 +2302,12 @@
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
"integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4="
},
"get-stream": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
"integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=",
"dev": true
},
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
@ -2260,9 +2336,9 @@
"integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg="
},
"globals": {
"version": "9.17.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-9.17.0.tgz",
"integrity": "sha1-DAymltm5u2lNLlRwvTd3fKrVAoY="
"version": "9.18.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
"integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ=="
},
"globby": {
"version": "5.0.0",
@ -2422,6 +2498,20 @@
"integrity": "sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=",
"dev": true
},
"husky": {
"version": "0.13.4",
"resolved": "https://registry.npmjs.org/husky/-/husky-0.13.4.tgz",
"integrity": "sha1-SHhcUCjeNFKlHEjBLE+UshJKFAc=",
"dev": true,
"dependencies": {
"normalize-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-1.0.0.tgz",
"integrity": "sha1-MtDkcvkf80VwHBWoMRAY07CpA3k=",
"dev": true
}
}
},
"iconv-lite": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.17.tgz",
@ -2530,6 +2620,12 @@
"integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=",
"dev": true
},
"is-ci": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.0.10.tgz",
"integrity": "sha1-9zkzayYyNlBhqdSCcM1WrjNpMY4=",
"dev": true
},
"is-date-object": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
@ -2610,6 +2706,12 @@
"resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
"integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU="
},
"is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
"dev": true
},
"is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
@ -2674,12 +2776,6 @@
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"jodid25519": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz",
"integrity": "sha1-BtSRIlUJNBlHfUJWM2BuDpB4KWc=",
"optional": true
},
"js-base64": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.1.9.tgz",
@ -2795,6 +2891,44 @@
"integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
"dev": true
},
"lint-staged": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-3.6.0.tgz",
"integrity": "sha1-zajwvvFueSjMFLc1GGrhLNZiWZw=",
"dev": true
},
"listr": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/listr/-/listr-0.12.0.tgz",
"integrity": "sha1-a84sD1YD+klYDqF81qAMwOX6RRo=",
"dev": true
},
"listr-silent-renderer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz",
"integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=",
"dev": true
},
"listr-update-renderer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz",
"integrity": "sha1-yoDhd5tOcCZoB+ju0a1qvjmFUPk=",
"dev": true,
"dependencies": {
"indent-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.1.0.tgz",
"integrity": "sha1-CP9DNGAziDmbMp5rlTjcejz13n0=",
"dev": true
}
}
},
"listr-verbose-renderer": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.0.tgz",
"integrity": "sha1-RNwBuww0oDxXIVTU0Izemx3FYg8=",
"dev": true
},
"load-json-file": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
@ -2889,6 +3023,12 @@
"resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz",
"integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc="
},
"lodash.chunk": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.chunk/-/lodash.chunk-4.2.0.tgz",
"integrity": "sha1-ZuXOH3btJ7QwPYxlEujRIW6BBrw=",
"dev": true
},
"lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
@ -2918,6 +3058,18 @@
"integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=",
"dev": true
},
"log-symbols": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz",
"integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=",
"dev": true
},
"log-update": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz",
"integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=",
"dev": true
},
"longest": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
@ -2935,9 +3087,9 @@
"integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8="
},
"lru-cache": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz",
"integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4="
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.0.tgz",
"integrity": "sha512-aHGs865JXz6bkB4AHL+3AhyvTFKL3iZamKVWjIUKnXOXyasJvqPK8WAjOnAQKQZVpeXDVz19u1DD0r/12bWAdQ=="
},
"map-obj": {
"version": "1.0.1",
@ -3157,6 +3309,24 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk="
},
"npm-path": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.3.tgz",
"integrity": "sha1-Fc/04ciaONp39W9gVbJPl137K74=",
"dev": true
},
"npm-run-path": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
"dev": true
},
"npm-which": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/npm-which/-/npm-which-3.0.1.tgz",
"integrity": "sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=",
"dev": true
},
"npmlog": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz",
@ -3255,6 +3425,12 @@
"integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
"dev": true
},
"ora": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/ora/-/ora-0.2.3.tgz",
"integrity": "sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q=",
"dev": true
},
"original": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/original/-/original-1.0.0.tgz",
@ -3300,6 +3476,12 @@
"resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz",
"integrity": "sha1-0KM+7+YaIF+suQCS6CZZjVJFznY="
},
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"dev": true
},
"p-limit": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz",
@ -3312,6 +3494,12 @@
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"dev": true
},
"p-map": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-1.1.1.tgz",
"integrity": "sha1-BfXkrpegaDcbwqXMhr+9vBnErno=",
"dev": true
},
"pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
@ -3356,6 +3544,12 @@
"integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
"dev": true
},
"path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
"dev": true
},
"path-parse": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz",
@ -3428,6 +3622,12 @@
"resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
"integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks="
},
"prettier": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.4.4.tgz",
"integrity": "sha512-GuuPazIvjW1DG26yLQgO+nagmRF/h9M4RaCtZWqu/eFW7csdZkQEwPJUeXX10d+LzmCnR9DuIZndqIOn3p2YoA==",
"dev": true
},
"private": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/private/-/private-0.1.7.tgz",
@ -3542,9 +3742,9 @@
"integrity": "sha1-ugwoeG/VLtfk8hNf4CiNRirvk9o="
},
"react-modal": {
"version": "1.7.7",
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-1.7.7.tgz",
"integrity": "sha1-cCBfUcWHCMSHr/aBuj/teUbjkdk="
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-1.7.11.tgz",
"integrity": "sha512-mKKJlDp7mIWAuBpg0ZJLoGEzyVME63vs5jBy+D53R9V+hVJ775y8DXFYRGLQYSZW9IFE0qkmNSLm7HdgDGqzsg=="
},
"react-redux": {
"version": "5.0.5",
@ -3660,9 +3860,9 @@
}
},
"remove-trailing-separator": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.1.tgz",
"integrity": "sha1-YV67lq9VlVLUv0BXyENtSGq2PMQ="
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz",
"integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE="
},
"render-media": {
"version": "2.10.0",
@ -3694,6 +3894,12 @@
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-from-string": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.1.tgz",
"integrity": "sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg=",
"dev": true
},
"require-main-filename": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
@ -3763,6 +3969,12 @@
"integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=",
"dev": true
},
"rxjs": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.4.0.tgz",
"integrity": "sha1-p9sUqxV/nXqsalbmVeejhg05vyY=",
"dev": true
},
"safe-buffer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.0.1.tgz",
@ -3849,10 +4061,22 @@
"integrity": "sha1-F93t3F9yL7ZlAWWIlUYZd4ZzFbo=",
"dev": true
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
"dev": true
},
"shebang-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
"dev": true
},
"shelljs": {
"version": "0.7.7",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.7.tgz",
"integrity": "sha1-svXHfvlxSPS09uImguELuoZnz/E=",
"version": "0.7.8",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz",
"integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=",
"dev": true
},
"shellwords": {
@ -3960,9 +4184,9 @@
"dev": true
},
"sshpk": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.0.tgz",
"integrity": "sha1-/yo+T9BEl1Vf7Zezmg/YL6+zozw=",
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
"integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
"dependencies": {
"assert-plus": {
"version": "1.0.0",
@ -3971,6 +4195,12 @@
}
}
},
"staged-git-files": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/staged-git-files/-/staged-git-files-0.0.4.tgz",
"integrity": "sha1-15fhtVHKemOd7AI33G60u5vhfTU=",
"dev": true
},
"statuses": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
@ -3999,6 +4229,12 @@
"resolved": "https://registry.npmjs.org/stream-to-blob-url/-/stream-to-blob-url-2.1.0.tgz",
"integrity": "sha1-w0HRBQLsUSUGBzJyWOwvWGsH1iY="
},
"stream-to-observable": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/stream-to-observable/-/stream-to-observable-0.1.0.tgz",
"integrity": "sha1-Rb8dny19wJvtgfHDB8Qw5ouEz/4=",
"dev": true
},
"string_decoder": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.2.tgz",
@ -4030,6 +4266,12 @@
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
"integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4="
},
"strip-eof": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=",
"dev": true
},
"strip-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "lbry-web-ui",
"version": "0.12.0rc6",
"version": "0.12.0rc7",
"description": "LBRY UI",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",

View file

@ -165,3 +165,8 @@ p
section.section-spaced {
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 {
padding: 0 $padding-card-horizontal;
margin-top: $spacing-vertical;
margin-top: $spacing-vertical * 2/3;
}
.card__title-identity {
padding: 0 $padding-card-horizontal;

View file

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