more user flow improvements, possibly ready

This commit is contained in:
Jeremy Kauffman 2017-07-19 21:07:17 -04:00
commit c2a611f11c
35 changed files with 317 additions and 213 deletions

View file

@ -25,6 +25,8 @@ Web UI version numbers should always match the corresponding version of LBRY App
* Fixed inappropriate text showing on searches * Fixed inappropriate text showing on searches
* Stop discover page from pushing jumping vertically while loading * Stop discover page from pushing jumping vertically while loading
* Restored feedback on claim amounts * Restored feedback on claim amounts
* Fixed hiding price input when Free is checked on publish form
* Fixed hiding new identity fields on publish form
### Deprecated ### Deprecated
* *

View file

@ -27,7 +27,7 @@ const {version: localVersion} = require(app.getAppPath() + '/package.json');
const VERSION_CHECK_INTERVAL = 30 * 60 * 1000; const VERSION_CHECK_INTERVAL = 30 * 60 * 1000;
const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest'; const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
const DAEMON_PATH = process.env.LBRY_DAEMON || path.join(__dirname, 'dist', 'lbrynet-daemon');
let client = jayson.client.http({ let client = jayson.client.http({
host: 'localhost', host: 'localhost',
@ -207,13 +207,8 @@ function handleDaemonSubprocessExited() {
function launchDaemon() { function launchDaemon() {
assert(!daemonSubprocess, 'Tried to launch daemon twice'); assert(!daemonSubprocess, 'Tried to launch daemon twice');
if (process.env.LBRY_DAEMON) { console.log('Launching daemon:', DAEMON_PATH)
executable = process.env.LBRY_DAEMON; daemonSubprocess = child_process.spawn(DAEMON_PATH)
} else {
executable = path.join(__dirname, 'dist', 'lbrynet-daemon');
}
console.log('Launching daemon:', executable)
daemonSubprocess = child_process.spawn(executable)
// Need to handle the data event instead of attaching to // Need to handle the data event instead of attaching to
// process.stdout because the latter doesn't work. I believe on // process.stdout because the latter doesn't work. I believe on
// windows it buffers stdout and we don't get any meaningful output // windows it buffers stdout and we don't get any meaningful output

View file

@ -18,5 +18,8 @@
}, },
"devDependencies": { "devDependencies": {
"electron-rebuild": "^1.5.11" "electron-rebuild": "^1.5.11"
},
"lbrySettings": {
"lbrynetDaemonVersion": "0.14.1"
} }
} }

View file

@ -1 +0,0 @@
https://github.com/lbryio/lbry/releases/download/v0.14.1/lbrynet-daemon-v0.14.1-OSNAME.zip

View file

@ -79,11 +79,14 @@ if $OSX; then
else else
OSNAME="linux" OSNAME="linux"
fi fi
DAEMON_URL="$(cat "$BUILD_DIR/DAEMON_URL" | sed "s/OSNAME/${OSNAME}/")" DAEMON_VER=$(node -e "console.log(require(\"$ROOT/app/package.json\").lbrySettings.lbrynetDaemonVersion)")
DAEMON_URL="https://github.com/lbryio/lbry/releases/download/v${DAEMON_VER}/lbrynet-daemon-v${DAEMON_VER}-${OSNAME}.zip"
wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip" wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip"
unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/" unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/"
rm "$BUILD_DIR/daemon.zip" rm "$BUILD_DIR/daemon.zip"
################### ###################
# Build the app # # Build the app #
################### ###################

View file

@ -16,9 +16,9 @@ import { doFileList } from "actions/file_info";
const { remote, ipcRenderer, shell } = require("electron"); const { remote, ipcRenderer, shell } = require("electron");
const path = require("path"); const path = require("path");
const app = require("electron").remote.app;
const { download } = remote.require("electron-dl"); const { download } = remote.require("electron-dl");
const fs = remote.require("fs"); const fs = remote.require("fs");
const { lbrySettings: config } = require("../../../app/package.json");
const queryStringFromParams = params => { const queryStringFromParams = params => {
return Object.keys(params).map(key => `${key}=${params[key]}`).join("&"); return Object.keys(params).map(key => `${key}=${params[key]}`).join("&");
@ -33,7 +33,7 @@ export function doNavigate(path, params = {}) {
const state = getState(); const state = getState();
const pageTitle = selectPageTitle(state); const pageTitle = selectPageTitle(state);
dispatch(doHistoryPush(params, pageTitle, url)); dispatch(doHistoryPush({ params }, pageTitle, url));
}; };
} }
@ -51,7 +51,7 @@ export function doAuthNavigate(pathAfterAuth = null, params = {}) {
}; };
} }
export function doChangePath(path) { export function doChangePath(path, options = {}) {
return function(dispatch, getState) { return function(dispatch, getState) {
dispatch({ dispatch({
type: types.CHANGE_PATH, type: types.CHANGE_PATH,
@ -62,8 +62,12 @@ export function doChangePath(path) {
const state = getState(); const state = getState();
const pageTitle = selectPageTitle(state); const pageTitle = selectPageTitle(state);
const scrollY = options.scrollY;
window.document.title = pageTitle; window.document.title = pageTitle;
window.scrollTo(0, 0);
if (scrollY) window.scrollTo(0, scrollY);
else window.scrollTo(0, 0);
const currentPage = selectCurrentPage(state); const currentPage = selectCurrentPage(state);
if (currentPage === "search") { if (currentPage === "search") {
@ -81,10 +85,26 @@ export function doHistoryBack() {
}; };
} }
export function doHistoryPush(params, title, relativeUrl) { export function doHistoryPush(currentState, title, relativeUrl) {
return function(dispatch, getState) { return function(dispatch, getState) {
title += " - LBRY"; title += " - LBRY";
history.pushState(params, title, `#${relativeUrl}`); history.pushState(currentState, title, `#${relativeUrl}`);
};
}
export function doRecordScroll(scroll) {
return function(dispatch, getState) {
const state = getState();
const historyState = history.state;
if (!historyState) return;
historyState.scrollY = scroll;
history.replaceState(
historyState,
document.title,
`#${state.app.currentPath}`
);
}; };
} }
@ -131,8 +151,9 @@ export function doDownloadUpgrade() {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
// Make a new directory within temp directory so the filename is guaranteed to be available // Make a new directory within temp directory so the filename is guaranteed to be available
const dir = fs.mkdtempSync(app.getPath("temp") + require("path").sep); const dir = fs.mkdtempSync(
const upgradeFilename = selectUpgradeFilename(state); remote.app.getPath("temp") + require("path").sep
);
let options = { let options = {
onProgress: p => dispatch(doUpdateDownloadProgress(Math.round(p * 100))), onProgress: p => dispatch(doUpdateDownloadProgress(Math.round(p * 100))),
@ -216,6 +237,18 @@ export function doCheckUpgradeAvailable() {
}; };
} }
export function doCheckDaemonVersion() {
return function(dispatch, getState) {
lbry.version().then(({ lbrynet_version }) => {
dispatch({
type: config.lbrynetDaemonVersion == lbrynet_version
? types.DAEMON_VERSION_MATCH
: types.DAEMON_VERSION_MISMATCH,
});
});
};
}
export function doAlertError(errorList) { export function doAlertError(errorList) {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
@ -232,7 +265,8 @@ export function doAlertError(errorList) {
} }
export function doDaemonReady() { export function doDaemonReady() {
return function(dispatch) { return function(dispatch, getState) {
history.replaceState({}, document.title, `#/discover`);
dispatch(doAuthenticate()); dispatch(doAuthenticate());
dispatch({ dispatch({
type: types.DAEMON_READY, type: types.DAEMON_READY,
@ -262,3 +296,10 @@ export function doClearCache() {
return Promise.resolve(); return Promise.resolve();
}; };
} }
export function doQuitAndLaunchDaemonHelp() {
return function(dispatch, getState) {
shell.openExternal("https://lbry.io/faq/incompatible-protocol-version");
remote.app.quit();
};
}

View file

@ -159,22 +159,22 @@ export function doUserIdentityVerify(stripeToken) {
} }
}) })
.catch(error => { .catch(error => {
let user = selectUser(getState()); // let user = selectUser(getState());
user.is_identity_verified = true; // user.is_identity_verified = true;
if (user.is_identity_verified) { // if (user.is_identity_verified) {
dispatch({
type: types.USER_IDENTITY_VERIFY_SUCCESS,
data: { user },
});
} else {
throw new Error(
"Your identity is still not verified. This should not happen."
); //shouldn't happen
}
// dispatch({ // dispatch({
// type: types.USER_IDENTITY_VERIFY_FAILURE, // type: types.USER_IDENTITY_VERIFY_SUCCESS,
// data: { error: error.toString() }, // data: { user },
// }); // });
// } else {
// throw new Error(
// "Your identity is still not verified. This should not happen."
// ); //shouldn't happen
// }
dispatch({
type: types.USER_IDENTITY_VERIFY_FAILURE,
data: { error: error.toString() },
});
}); });
}; };
} }

View file

@ -2,7 +2,9 @@ import store from "store.js";
import lbry from "./lbry.js"; import lbry from "./lbry.js";
const env = ENV; const env = ENV;
const config = require(`./config/${env}`); const config = {
...require(`./config/${env}`),
};
const language = lbry.getClientSetting("language") const language = lbry.getClientSetting("language")
? lbry.getClientSetting("language") ? lbry.getClientSetting("language")
: "en"; : "en";

View file

@ -5,6 +5,7 @@ import {
doCheckUpgradeAvailable, doCheckUpgradeAvailable,
doOpenModal, doOpenModal,
doAlertError, doAlertError,
doRecordScroll,
} from "actions/app"; } from "actions/app";
import { doUpdateBalance } from "actions/wallet"; import { doUpdateBalance } from "actions/wallet";
import { selectWelcomeModalAcknowledged } from "selectors/app"; import { selectWelcomeModalAcknowledged } from "selectors/app";
@ -36,6 +37,7 @@ const perform = dispatch => ({
checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()), checkUpgradeAvailable: () => dispatch(doCheckUpgradeAvailable()),
openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)), openWelcomeModal: () => dispatch(doOpenModal(modals.WELCOME)),
updateBalance: balance => dispatch(doUpdateBalance(balance)), updateBalance: balance => dispatch(doUpdateBalance(balance)),
recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)),
}); });
export default connect(select, perform)(App); export default connect(select, perform)(App);

View file

@ -26,6 +26,10 @@ class App extends React.PureComponent {
}); });
this.showWelcome(this.props); this.showWelcome(this.props);
this.scrollListener = () => this.props.recordScroll(window.scrollY);
window.addEventListener("scroll", this.scrollListener);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
@ -50,6 +54,10 @@ class App extends React.PureComponent {
} }
} }
componentWillUnmount() {
window.removeEventListener("scroll", this.scrollListener);
}
render() { render() {
const { modal } = this.props; const { modal } = this.props;

View file

@ -78,10 +78,11 @@ class FileCard extends React.PureComponent {
onClick={() => navigate("/show", { uri })} onClick={() => navigate("/show", { uri })}
className="card__link" className="card__link"
> >
<CardMedia title={title} thumbnail={thumbnail} />
<div className="card__title-identity"> <div className="card__title-identity">
<h5 title={title}> <div className="card__title" title={title}>
<TruncatedText lines={1}>{title}</TruncatedText> <TruncatedText lines={1}>{title}</TruncatedText>
</h5> </div>
<div className="card__subtitle"> <div className="card__subtitle">
<span style={{ float: "right" }}> <span style={{ float: "right" }}>
<FilePrice uri={uri} /> <FilePrice uri={uri} />
@ -92,7 +93,6 @@ class FileCard extends React.PureComponent {
<UriIndicator uri={uri} /> <UriIndicator uri={uri} />
</div> </div>
</div> </div>
<CardMedia title={title} thumbnail={thumbnail} />
<div className="card__content card__subtext card__subtext--two-lines"> <div className="card__content card__subtext card__subtext--two-lines">
<TruncatedMarkdown lines={2}>{description}</TruncatedMarkdown> <TruncatedMarkdown lines={2}>{description}</TruncatedMarkdown>
</div> </div>

View file

@ -11,7 +11,6 @@ const Link = props => {
icon, icon,
badge, badge,
button, button,
hidden,
disabled, disabled,
children, children,
} = props; } = props;

View file

@ -1,21 +0,0 @@
import React from "react";
import ReactModal from "react-modal";
export class ModalPage extends React.PureComponent {
render() {
return (
<ReactModal
onCloseRequested={this.props.onAborted || this.props.onConfirmed}
{...this.props}
className={(this.props.className || "") + " modal-page"}
overlayClassName="modal-overlay"
>
<div className="modal-page__content">
{this.props.children}
</div>
</ReactModal>
);
}
}
export default ModalPage;

View file

@ -0,0 +1,13 @@
import React from "react";
import { connect } from "react-redux";
import { doQuit, doSkipWrongDaemonNotice } from "actions/app";
import { doQuitAndLaunchDaemonHelp } from "actions/app";
import ModalIncompatibleDaemon from "./view";
const select = state => ({});
const perform = dispatch => ({
quitAndLaunchDaemonHelp: () => dispatch(doQuitAndLaunchDaemonHelp()),
});
export default connect(select, perform)(ModalIncompatibleDaemon);

View file

@ -0,0 +1,24 @@
import React from "react";
import { Modal } from "component/modal";
class ModalIncompatibleDaemon extends React.PureComponent {
render() {
const { quitAndLaunchDaemonHelp } = this.props;
return (
<Modal
isOpen={true}
contentLabel={__("Incompatible daemon running")}
type="alert"
confirmButtonLabel={__("Quit and Learn More")}
onConfirmed={quitAndLaunchDaemonHelp}
>
{__(
"This browser is running with an incompatible version of the LBRY protocol and your install must be repaired."
)}
</Modal>
);
}
}
export default ModalIncompatibleDaemon;

View file

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import rewards from "rewards"; import rewards from "rewards";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doCloseModal, doNavigate } from "actions/app"; import { doCloseModal, doAuthNavigate } from "actions/app";
import { doSetClientSetting } from "actions/settings"; import { doSetClientSetting } from "actions/settings";
import { selectUserIsRewardApproved } from "selectors/user"; import { selectUserIsRewardApproved } from "selectors/user";
import { import {
@ -30,7 +30,7 @@ const perform = dispatch => () => {
return { return {
verifyAccount: () => { verifyAccount: () => {
closeModal(); closeModal();
dispatch(doNavigate("/auth")); dispatch(doAuthNavigate("/rewards"));
}, },
closeModal: closeModal, closeModal: closeModal,
}; };

View file

@ -93,6 +93,7 @@ class ChannelSection extends React.PureComponent {
"This LBC remains yours and the deposit can be undone at any time." "This LBC remains yours and the deposit can be undone at any time."
); );
const channel = this.state.addingChannel ? "new" : this.props.channel;
const { fetchingChannels, channels = [] } = this.props; const { fetchingChannels, channels = [] } = this.props;
let channelContent = []; let channelContent = [];
@ -102,7 +103,7 @@ class ChannelSection extends React.PureComponent {
type="select" type="select"
tabIndex="1" tabIndex="1"
onChange={this.handleChannelChange.bind(this)} onChange={this.handleChannelChange.bind(this)}
value={this.props.channel} value={channel}
> >
<option key="anonymous" value="anonymous"> <option key="anonymous" value="anonymous">
{__("Anonymous")} {__("Anonymous")}
@ -111,7 +112,7 @@ class ChannelSection extends React.PureComponent {
<option key={name} value={name}>{name}</option> <option key={name} value={name}>{name}</option>
)} )}
<option key="new" value="new"> <option key="new" value="new">
{__("New identity...")} {__("New channel...")}
</option> </option>
</FormRow> </FormRow>
); );
@ -124,9 +125,10 @@ class ChannelSection extends React.PureComponent {
return ( return (
<section className="card"> <section className="card">
<div className="card__title-primary"> <div className="card__title-primary">
<h4>{__("Identity")}</h4> <h4>{__("Channel Name")}</h4>
<div className="card__subtitle"> <div className="card__subtitle">
{__("Who created this content?")} {__("This is the channel that broadcasts your content.")}
{__("Ex. @Marvel, @TheBeatles, @BooksByJoe")}
</div> </div>
</div> </div>
<div className="card__content"> <div className="card__content">
@ -137,9 +139,7 @@ class ChannelSection extends React.PureComponent {
<FormRow <FormRow
label={__("Name")} label={__("Name")}
type="text" type="text"
onChange={event => { onChange={this.handleNewChannelNameChange.bind(this)}
this.handleNewChannelNameChange(event);
}}
value={this.state.newChannelName} value={this.state.newChannelName}
/> />
<FormRow <FormRow
@ -158,8 +158,8 @@ class ChannelSection extends React.PureComponent {
button="primary" button="primary"
label={ label={
!this.state.creatingChannel !this.state.creatingChannel
? __("Create identity") ? __("Create channel")
: __("Creating identity...") : __("Creating channel...")
} }
onClick={this.handleCreateChannelClick.bind(this)} onClick={this.handleCreateChannelClick.bind(this)}
disabled={this.state.creatingChannel} disabled={this.state.creatingChannel}

View file

@ -42,6 +42,7 @@ class PublishForm extends React.PureComponent {
submitting: false, submitting: false,
creatingChannel: false, creatingChannel: false,
modal: null, modal: null,
isFee: false,
}; };
} }
@ -635,11 +636,8 @@ class PublishForm extends React.PureComponent {
label={__("Free")} label={__("Free")}
type="radio" type="radio"
name="isFree" name="isFree"
value="1" onChange={() => this.handleFeePrefChange(false)}
onChange={() => { checked={!this.state.isFee}
this.handleFeePrefChange(false);
}}
defaultChecked={!this.state.isFee}
/> />
<FormField <FormField
type="radio" type="radio"
@ -648,7 +646,7 @@ class PublishForm extends React.PureComponent {
onChange={() => { onChange={() => {
this.handleFeePrefChange(true); this.handleFeePrefChange(true);
}} }}
defaultChecked={this.state.isFee} checked={this.state.isFee}
/> />
<span className={!this.state.isFee ? "hidden" : ""}> <span className={!this.state.isFee ? "hidden" : ""}>
<FormField <FormField
@ -789,12 +787,14 @@ class PublishForm extends React.PureComponent {
<section className="card"> <section className="card">
<div className="card__title-primary"> <div className="card__title-primary">
<h4>{__("Address")}</h4> <h4>{__("Content URL")}</h4>
<div className="card__subtitle"> <div className="card__subtitle">
{__("Where should this content permanently reside?")} {__(
"This is the exact address where people find your content (ex. lbry://myvideo)."
)}
{" "} {" "}
<Link <Link
label={__("Read more")} label={__("Learn more")}
href="https://lbry.io/faq/naming" href="https://lbry.io/faq/naming"
/>. />.
</div> </div>

View file

@ -0,0 +1,19 @@
import React from "react";
import { connect } from "react-redux";
import { selectCurrentModal, selectDaemonVersionMatched } from "selectors/app";
import { doCheckDaemonVersion } from "actions/app";
import SplashScreen from "./view";
const select = state => {
return {
modal: selectCurrentModal(state),
daemonVersionMatched: selectDaemonVersionMatched(state),
};
};
const perform = dispatch => ({
checkDaemonVersion: () => dispatch(doCheckDaemonVersion()),
});
export default connect(select, perform)(SplashScreen);

View file

@ -1,6 +1,9 @@
import React from "react"; import React from "react";
import lbry from "../lbry.js"; import lbry from "../../lbry.js";
import LoadScreen from "./load_screen.js"; import LoadScreen from "../load_screen.js";
import ModalIncompatibleDaemon from "../modalIncompatibleDaemon";
import ModalUpgrade from "component/modalUpgrade";
import ModalDownloading from "component/modalDownloading";
export class SplashScreen extends React.PureComponent { export class SplashScreen extends React.PureComponent {
static propTypes = { static propTypes = {
@ -14,6 +17,7 @@ export class SplashScreen extends React.PureComponent {
this.state = { this.state = {
details: __("Starting daemon"), details: __("Starting daemon"),
message: __("Connecting"), message: __("Connecting"),
isRunning: false,
isLagging: false, isLagging: false,
}; };
} }
@ -35,10 +39,16 @@ export class SplashScreen extends React.PureComponent {
message: __("Testing Network"), message: __("Testing Network"),
details: __("Waiting for name resolution"), details: __("Waiting for name resolution"),
isLagging: false, isLagging: false,
isRunning: true,
}); });
lbry.resolve({ uri: "lbry://one" }).then(() => { lbry.resolve({ uri: "lbry://one" }).then(() => {
this.props.onLoadDone(); // Only leave the load screen if the daemon version matched;
// otherwise we'll notify the user at the end of the load screen.
if (this.props.daemonVersionMatched) {
this.props.onReadyToLaunch();
}
}); });
return; return;
} }
@ -54,6 +64,7 @@ export class SplashScreen extends React.PureComponent {
componentDidMount() { componentDidMount() {
lbry lbry
.connect() .connect()
.then(this.props.checkDaemonVersion)
.then(() => { .then(() => {
this.updateStatus(); this.updateStatus();
}) })
@ -69,12 +80,24 @@ export class SplashScreen extends React.PureComponent {
} }
render() { render() {
const { modal } = this.props;
return ( return (
<div>
<LoadScreen <LoadScreen
message={this.state.message} message={this.state.message}
details={this.state.details} details={this.state.details}
isWarning={this.state.isLagging} isWarning={this.state.isLagging}
/> />
{/* Temp hack: don't show any modals on splash screen daemon is running;
daemon doesn't let you quit during startup, so the "Quit" buttons
in the modals won't work. */}
{modal == "incompatibleDaemon" &&
this.state.isRunning &&
<ModalIncompatibleDaemon />}
{modal == "upgrade" && this.state.isRunning && <ModalUpgrade />}
{modal == "downloading" && this.state.isRunning && <ModalDownloading />}
</div>
); );
} }
} }

View file

@ -25,15 +25,13 @@ class UserVerify extends React.PureComponent {
const { errorMessage, isPending, reward } = this.props; const { errorMessage, isPending, reward } = this.props;
return ( return (
<div> <div>
{(!reward || !reward.transaction_id) &&
<p> <p>
<span>
Please link a credit card to confirm your identity and receive{" "} Please link a credit card to confirm your identity and receive{" "}
</span>
{reward {reward
? <CreditAmount amount={parseFloat(reward.reward_amount)} /> ? <CreditAmount amount={parseFloat(reward.reward_amount)} />
: <span>your reward</span>} : <span>your reward</span>}
{"."} </p>}
</p>
<p>{__("This is to prevent abuse. You will not be charged.")}</p> <p>{__("This is to prevent abuse. You will not be charged.")}</p>
{errorMessage && <p className="form-field__error">{errorMessage}</p>} {errorMessage && <p className="form-field__error">{errorMessage}</p>}
<CardVerify <CardVerify

View file

@ -7,6 +7,8 @@ export const REMOVE_SNACKBAR_SNACK = "REMOVE_SNACKBAR_SNACK";
export const WINDOW_FOCUSED = "WINDOW_FOCUSED"; export const WINDOW_FOCUSED = "WINDOW_FOCUSED";
export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH"; export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH";
export const DAEMON_READY = "DAEMON_READY"; export const DAEMON_READY = "DAEMON_READY";
export const DAEMON_VERSION_MATCH = "DAEMON_VERSION_MATCH";
export const DAEMON_VERSION_MISMATCH = "DAEMON_VERSION_MISMATCH";
// Upgrades // Upgrades
export const UPGRADE_CANCELLED = "UPGRADE_CANCELLED"; export const UPGRADE_CANCELLED = "UPGRADE_CANCELLED";

View file

@ -1,4 +1,5 @@
export const CONFIRM_FILE_REMOVE = "confirmFileRemove"; export const CONFIRM_FILE_REMOVE = "confirmFileRemove";
export const INCOMPATIBLE_DAEMON = "incompatibleDaemon";
export const DOWNLOADING = "downloading"; export const DOWNLOADING = "downloading";
export const ERROR = "error"; export const ERROR = "error";
export const INSUFFICIENT_CREDITS = "insufficient_credits"; export const INSUFFICIENT_CREDITS = "insufficient_credits";

View file

@ -138,6 +138,7 @@ lbryio.authenticate = function() {
language: "en", language: "en",
has_email: true, has_email: true,
has_verified_email: true, has_verified_email: true,
is_identity_verified: true,
is_reward_approved: false, is_reward_approved: false,
is_reward_eligible: false, is_reward_eligible: false,
}); });

View file

@ -5,7 +5,7 @@ import App from "component/app/index.js";
import SnackBar from "component/snackBar"; import SnackBar from "component/snackBar";
import { Provider } from "react-redux"; import { Provider } from "react-redux";
import store from "store.js"; import store from "store.js";
import SplashScreen from "component/splash.js"; import SplashScreen from "component/splash";
import { doChangePath, doNavigate, doDaemonReady } from "actions/app"; import { doChangePath, doNavigate, doDaemonReady } from "actions/app";
import { toQueryString } from "util/query_params"; import { toQueryString } from "util/query_params";
import * as types from "constants/action_types"; import * as types from "constants/action_types";
@ -35,10 +35,10 @@ window.addEventListener("popstate", (event, param) => {
if (hash !== "") { if (hash !== "") {
const url = hash.split("#")[1]; const url = hash.split("#")[1];
const params = event.state; const { params, scrollY } = event.state || {};
const queryString = toQueryString(params); const queryString = toQueryString(params);
app.store.dispatch(doChangePath(`${url}?${queryString}`)); app.store.dispatch(doChangePath(`${url}?${queryString}`, { scrollY }));
} else { } else {
app.store.dispatch(doChangePath("/discover")); app.store.dispatch(doChangePath("/discover"));
} }
@ -111,7 +111,12 @@ var init = function() {
if (window.sessionStorage.getItem("loaded") == "y") { if (window.sessionStorage.getItem("loaded") == "y") {
onDaemonReady(); onDaemonReady();
} else { } else {
ReactDOM.render(<SplashScreen onLoadDone={onDaemonReady} />, canvas); ReactDOM.render(
<Provider store={store}>
<SplashScreen onReadyToLaunch={onDaemonReady} />
</Provider>,
canvas
);
} }
}; };

View file

@ -25,7 +25,7 @@ const makeSelect = () => {
contentType: selectContentType(state, props), contentType: selectContentType(state, props),
costInfo: selectCostInfo(state, props), costInfo: selectCostInfo(state, props),
metadata: selectMetadata(state, props), metadata: selectMetadata(state, props),
showNsfw: !selectShowNsfw(state), obscureNsfw: !selectShowNsfw(state),
fileInfo: selectFileInfo(state, props), fileInfo: selectFileInfo(state, props),
}); });

View file

@ -6,11 +6,11 @@ import {
selectRewards, selectRewards,
} from "selectors/rewards"; } from "selectors/rewards";
import { import {
selectUserIsRewardEligible, selectUser,
selectUserHasEmail, selectUserHasEmail,
selectUserIsVerificationCandidate, selectUserIsVerificationCandidate,
} from "selectors/user"; } from "selectors/user";
import { doAuthNavigate } from "actions/app"; import { doAuthNavigate, doNavigate } from "actions/app";
import { doRewardList } from "actions/rewards"; import { doRewardList } from "actions/rewards";
import rewards from "rewards"; import rewards from "rewards";
import RewardsPage from "./view"; import RewardsPage from "./view";
@ -21,15 +21,14 @@ const select = (state, props) => {
return { return {
fetching: selectFetchingRewards(state), fetching: selectFetchingRewards(state),
rewards: selectRewards(state), rewards: selectRewards(state),
hasEmail: selectUserHasEmail(state),
isEligible: selectUserIsRewardEligible(state),
isVerificationCandidate: selectUserIsVerificationCandidate(state),
newUserReward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }), newUserReward: selectReward(state, { reward_type: rewards.TYPE_NEW_USER }),
user: selectUser(state),
}; };
}; };
const perform = dispatch => ({ const perform = dispatch => ({
fetchRewards: () => dispatch(doRewardList()), fetchRewards: () => dispatch(doRewardList()),
navigate: path => dispatch(doNavigate(path)),
doAuth: () => { doAuth: () => {
dispatch(doAuthNavigate("/rewards")); dispatch(doAuthNavigate("/rewards"));
}, },

View file

@ -45,39 +45,11 @@ class RewardsPage extends React.PureComponent {
} }
render() { render() {
const { const { doAuth, fetching, navigate, rewards, user } = this.props;
doAuth,
fetching,
isEligible,
isVerificationCandidate,
hasEmail,
rewards,
newUserReward,
} = this.props;
let content, let content, cardHeader;
isCard = false;
if (!hasEmail || isVerificationCandidate) { if (fetching) {
content = (
<div>
<div className="card__content empty">
<p>{__("Only verified accounts are eligible to earn rewards.")}</p>
</div>
<div className="card__content">
<Link onClick={doAuth} button="primary" label="Become Verified" />
</div>
</div>
);
isCard = true;
} else if (!isEligible) {
isCard = true;
content = (
<div className="card__content empty">
<p>{__("You are not eligible to claim rewards.")}</p>
</div>
);
} else if (fetching) {
content = ( content = (
<div className="card__content"> <div className="card__content">
<BusyMessage message={__("Fetching rewards")} /> <BusyMessage message={__("Fetching rewards")} />
@ -99,10 +71,54 @@ class RewardsPage extends React.PureComponent {
); );
} }
if (
user &&
(!user.has_email ||
!user.has_verified_email ||
!user.is_identity_verified)
) {
cardHeader = (
<div>
<div className="card__content empty">
<p>{__("Only verified accounts are eligible to earn rewards.")}</p>
</div>
<div className="card__content">
<Link onClick={doAuth} button="primary" label="Become Verified" />
</div>
</div>
);
} else if (user && !user.is_reward_approved) {
cardHeader = (
<div className="card__content">
<p>
{__(
"This account must undergo review before you can participate in the rewards program."
)}
{" "}
{__("This can take anywhere from several minutes to several days.")}
</p>
<p>
{__("You will receive an email when this process is complete.")}
</p>
<p>
{__("Please enjoy free content in the meantime!")}
</p>
<p>
<Link
onClick={() => navigate("/discover")}
button="primary"
label="Return Home"
/>
</p>
</div>
);
}
return ( return (
<main className="main--single-column"> <main className="main--single-column">
<SubHeader /> <SubHeader />
{isCard ? <section className="card">{content}</section> : content} {cardHeader && <section className="card">{cardHeader}</section>}
{content}
</main> </main>
); );
} }

View file

@ -1,4 +1,5 @@
import * as types from "constants/action_types"; import * as types from "constants/action_types";
import * as modalTypes from "constants/modal_types";
import lbry from "lbry"; import lbry from "lbry";
const currentPath = () => { const currentPath = () => {
@ -18,6 +19,7 @@ const defaultState = {
pathAfterAuth: "/discover", pathAfterAuth: "/discover",
platform: process.platform, platform: process.platform,
upgradeSkipped: sessionStorage.getItem("upgradeSkipped"), upgradeSkipped: sessionStorage.getItem("upgradeSkipped"),
daemonVersionMatched: null,
daemonReady: false, daemonReady: false,
hasSignature: false, hasSignature: false,
badgeNumber: 0, badgeNumber: 0,
@ -29,6 +31,19 @@ reducers[types.DAEMON_READY] = function(state, action) {
}); });
}; };
reducers[types.DAEMON_VERSION_MATCH] = function(state, action) {
return Object.assign({}, state, {
daemonVersionMatched: true,
});
};
reducers[types.DAEMON_VERSION_MISMATCH] = function(state, action) {
return Object.assign({}, state, {
daemonVersionMatched: false,
modal: modalTypes.INCOMPATIBLE_DAEMON,
});
};
reducers[types.CHANGE_PATH] = function(state, action) { reducers[types.CHANGE_PATH] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
currentPath: action.data.path, currentPath: action.data.path,

View file

@ -177,6 +177,11 @@ export const selectDaemonReady = createSelector(
state => state.daemonReady state => state.daemonReady
); );
export const selectDaemonVersionMatched = createSelector(
_selectState,
state => state.daemonVersionMatched
);
export const selectSnackBar = createSelector( export const selectSnackBar = createSelector(
_selectState, _selectState,
state => state.snackBar || {} state => state.snackBar || {}

View file

@ -30,11 +30,6 @@ export const selectUserHasEmail = createSelector(
(user, email) => (user && user.has_email) || !!email (user, email) => (user && user.has_email) || !!email
); );
export const selectUserIsRewardEligible = createSelector(
selectUser,
user => user && user.is_reward_eligible
);
export const selectUserIsRewardApproved = createSelector( export const selectUserIsRewardApproved = createSelector(
selectUser, selectUser,
user => user && user.is_reward_approved user => user && user.is_reward_approved

View file

@ -1,3 +1,5 @@
const { remote } = require("electron");
/** /**
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value * Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
* is not set yet. * is not set yet.

View file

@ -15,7 +15,6 @@
@import "component/_channel-indicator.scss"; @import "component/_channel-indicator.scss";
@import "component/_notice.scss"; @import "component/_notice.scss";
@import "component/_modal.scss"; @import "component/_modal.scss";
@import "component/_modal-page.scss";
@import "component/_snack-bar.scss"; @import "component/_snack-bar.scss";
@import "component/_video.scss"; @import "component/_video.scss";
@import "page/_developer.scss"; @import "page/_developer.scss";

View file

@ -19,18 +19,29 @@ $padding-card-horizontal: $spacing-vertical * 2/3;
.card--obscured .card__inner { .card--obscured .card__inner {
filter: blur($blur-intensity-nsfw); filter: blur($blur-intensity-nsfw);
} }
.card__title-primary { .card__title-primary,
.card__title-identity,
.card__actions,
.card__content,
.card__subtext {
padding: 0 $padding-card-horizontal; padding: 0 $padding-card-horizontal;
}
.card--small {
.card__title-primary,
.card__title-identity,
.card__actions,
.card__content,
.card__subtext {
padding: 0 $padding-card-horizontal / 2;
}
}
.card__title-primary {
margin-top: $spacing-vertical * 2/3; margin-top: $spacing-vertical * 2/3;
} }
.card__title-identity { .card__title-identity {
padding: 0 $padding-card-horizontal;
margin-top: $spacing-vertical * 1/3; margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3; margin-bottom: $spacing-vertical * 1/3;
} }
.card__actions {
padding: 0 $padding-card-horizontal;
}
.card__actions { .card__actions {
margin-top: $spacing-vertical * 2/3; margin-top: $spacing-vertical * 2/3;
} }
@ -45,21 +56,18 @@ $padding-card-horizontal: $spacing-vertical * 2/3;
.card__content { .card__content {
margin-top: $spacing-vertical * 2/3; margin-top: $spacing-vertical * 2/3;
margin-bottom: $spacing-vertical * 2/3; margin-bottom: $spacing-vertical * 2/3;
padding: 0 $padding-card-horizontal;
} }
.card__subtext { .card__subtext {
color: #444; color: $color-meta-light;
margin-top: 12px; font-size: 0.82em;
font-size: 0.9em; margin-top: $spacing-vertical * 1/3;
margin-top: $spacing-vertical * 2/3; margin-bottom: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 2/3;
padding: 0 $padding-card-horizontal;
} }
.card__subtext--allow-newlines { .card__subtext--allow-newlines {
white-space: pre-wrap; white-space: pre-wrap;
} }
.card__subtext--two-lines { .card__subtext--two-lines {
height: $font-size * 0.9 * $font-line-height * 2; height: $font-size * 0.82 * $font-line-height * 2; /*this is so one line text still has the proper height*/
} }
.card-overlay { .card-overlay {
position: absolute; position: absolute;
@ -144,7 +152,7 @@ $card-link-scaling: 1.1;
top: 36% top: 36%
} }
$width-card-small: $spacing-vertical * 12; $width-card-small: $spacing-vertical * 10;
$height-card-small: $spacing-vertical * 15; $height-card-small: $spacing-vertical * 15;
.card--small { .card--small {

View file

@ -1,54 +0,0 @@
@import "../global";
.modal-page {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
}
.modal-page--full {
left: 0;
right: 0;
top: 0;
bottom: 0;
.modal-page__content {
max-width: 500px;
}
}
/*
.modal-page {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
outline: none;
padding: 36px;
top: 25px;
left: 25px;
right: 25px;
bottom: 25px;
}
*/
.modal-page__content {
h1, h2 {
margin-bottom: $spacing-vertical / 2;
}
h3, h4 {
margin-bottom: $spacing-vertical / 4;
}
}