initial onboarding commit

This commit is contained in:
Sean Yesmunt 2019-09-26 12:07:11 -04:00
parent 5c06fa2dd8
commit 3bee71f514
94 changed files with 1873 additions and 1627 deletions

View file

@ -29,6 +29,7 @@
"jsx-quotes": ["error", "prefer-double"], "jsx-quotes": ["error", "prefer-double"],
"new-cap": 0, "new-cap": 0,
"no-console": 1, "no-console": 1,
"no-control-regex": 0,
"no-multi-spaces": 0, "no-multi-spaces": 0,
"no-redeclare": 0, "no-redeclare": 0,
"no-return-await": 0, "no-return-await": 0,
@ -38,11 +39,14 @@
"react/jsx-indent": 0, "react/jsx-indent": 0,
"react-hooks/exhaustive-deps": "warn", "react-hooks/exhaustive-deps": "warn",
"react-hooks/rules-of-hooks": "error", "react-hooks/rules-of-hooks": "error",
"space-before-function-paren": ["error", { "space-before-function-paren": [
"anonymous": "never", "error",
"named": "never", {
"asyncArrow": "always" "anonymous": "never",
}], "named": "never",
"asyncArrow": "always"
}
],
"standard/object-curly-even-spacing": 0, "standard/object-curly-even-spacing": 0,
"standard/no-callback-literal": 0, "standard/no-callback-literal": 0,
"react/display-name": 0, "react/display-name": 0,

View file

@ -5,6 +5,7 @@
[libs] [libs]
./flow-typed ./flow-typed
node_modules/lbry-redux/flow-typed/ node_modules/lbry-redux/flow-typed/
node_modules/lbryinc/flow-typed/
[lints] [lints]

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
FROM node:10
EXPOSE 1337
RUN yarn -v && npm -v
RUN apt-get update -y && apt-get upgrade -y && apt-get install -y libsecret-1-0 libsecret-1-dev
WORKDIR /app
ENV PATH="/app/node_modules/.bin:${PATH}"
COPY ./ ./
RUN rm -rf node_modules && APP_ENV=web yarn && SDK_API_URL='https://api.lbry.tv/api/proxy' NODE_ENV=production yarn compile:web --display errors-only
CMD node ./dist/web/server.js

View file

@ -1,60 +0,0 @@
// flow-typed signature: dbdb6148e2902ceaf3e437a7fe96ffa1
// flow-typed version: <<STUB>>/react-pose_v^4.0.5/flow_v0.94.0
/**
* This is an autogenerated libdef stub for:
*
* 'react-pose'
*
* Fill this stub out by replacing all the `any` types.
*
* Once filled out, we encourage you to share your work with the
* community by sending a pull request to:
* https://github.com/flowtype/flow-typed
*/
declare module 'react-pose' {
declare module.exports: any;
}
/**
* We include stubs for each file inside this npm package in case you need to
* require those files directly. Feel free to delete any files that aren't
* needed.
*/
declare module 'react-pose/dist/react-pose.dev' {
declare module.exports: any;
}
declare module 'react-pose/dist/react-pose.es' {
declare module.exports: any;
}
declare module 'react-pose/dist/react-pose' {
declare module.exports: any;
}
declare module 'react-pose/lib/index' {
declare module.exports: any;
}
declare module 'react-pose/rollup.config' {
declare module.exports: any;
}
// Filename aliases
declare module 'react-pose/dist/react-pose.dev.js' {
declare module.exports: $Exports<'react-pose/dist/react-pose.dev'>;
}
declare module 'react-pose/dist/react-pose.es.js' {
declare module.exports: $Exports<'react-pose/dist/react-pose.es'>;
}
declare module 'react-pose/dist/react-pose.js' {
declare module.exports: $Exports<'react-pose/dist/react-pose'>;
}
declare module 'react-pose/lib/index.js' {
declare module.exports: $Exports<'react-pose/lib/index'>;
}
declare module 'react-pose/rollup.config.js' {
declare module.exports: $Exports<'react-pose/rollup.config'>;
}

24
flow-typed/user.js vendored
View file

@ -1,24 +0,0 @@
// @flow
// Move this to lbryinc
declare type User = {
created_at: string,
family_name: ?string,
given_name: ?string,
groups: Array<string>,
has_verified_email: boolean,
id: number,
invite_reward_claimed: boolean,
invited_at: ?number,
invited_by_id: number,
invites_remaining: number,
is_email_enabled: boolean,
is_identity_verified: boolean,
is_reward_approved: boolean,
language: string,
manual_approval_user_id: ?number,
primary_email: string,
reward_status_change_trigger: string,
updated_at: string,
youtube_channels: ?Array<string>,
};

View file

@ -128,8 +128,8 @@
"husky": "^0.14.3", "husky": "^0.14.3",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#64383d57873ce59dea9df7216ee6cf52c4e95dc6", "lbry-redux": "lbryio/lbry-redux#42bf926138872d14523be7191694309be4f37605",
"lbryinc": "lbryio/lbryinc#d250096a6fc5df16be4f82812ecce28d6e558b6e", "lbryinc": "lbryio/lbryinc#67bb3e215be3f13605c5e3f9f2b0e2fb880724cf",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",
"lodash-es": "^4.17.14", "lodash-es": "^4.17.14",
@ -157,7 +157,6 @@
"react-hot-loader": "^4.11.1", "react-hot-loader": "^4.11.1",
"react-modal": "^3.1.7", "react-modal": "^3.1.7",
"react-paginate": "^5.2.1", "react-paginate": "^5.2.1",
"react-pose": "^4.0.5",
"react-redux": "^6.0.1", "react-redux": "^6.0.1",
"react-router": "^5.0.0", "react-router": "^5.0.0",
"react-router-dom": "^5.0.0", "react-router-dom": "^5.0.0",

View file

@ -17,7 +17,7 @@ export default appState => {
}); });
const windowConfiguration = { const windowConfiguration = {
backgroundColor: '#270f34', // Located in src/scss/init/_vars.scss `--color-background` backgroundColor: '#270f34', // Located in src/scss/init/_vars.scss `--color-background--splash`
minWidth: 950, minWidth: 950,
minHeight: 600, minHeight: 600,
autoHideMenuBar: true, autoHideMenuBar: true,

View file

@ -31,8 +31,7 @@ let showingAutoUpdateCloseAlert = false;
// object is garbage collected. // object is garbage collected.
let rendererWindow; let rendererWindow;
// eslint-disable-next-line no-unused-vars let tray; // eslint-disable-line
let tray;
let daemon; let daemon;
const appState = {}; const appState = {};
@ -47,7 +46,6 @@ if (isDev) {
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true; process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true;
} }
// eslint-disable-next-line space-before-function-paren
const startDaemon = async () => { const startDaemon = async () => {
let isDaemonRunning = false; let isDaemonRunning = false;
@ -114,7 +112,6 @@ if (!gotSingleInstanceLock) {
} }
}); });
// eslint-disable-next-line space-before-function-paren
app.on('ready', async () => { app.on('ready', async () => {
await startDaemon(); await startDaemon();
startSandbox(); startSandbox();
@ -317,6 +314,12 @@ ipcMain.on('set-auth-token', (event, token) => {
keytar.setPassword('LBRY', 'auth_token', token ? token.toString().trim() : null); keytar.setPassword('LBRY', 'auth_token', token ? token.toString().trim() : null);
}); });
ipcMain.on('delete-auth-token', (event, password) => {
keytar.deletePassword('LBRY', 'auth_token', password).then(res => {
event.sender.send('delete-auth-token-response', res);
});
});
ipcMain.on('get-password', event => { ipcMain.on('get-password', event => {
keytar.getPassword('LBRY', 'wallet_password').then(password => { keytar.getPassword('LBRY', 'wallet_password').then(password => {
event.sender.send('get-password-response', password ? password.toString() : null); event.sender.send('get-password-response', password ? password.toString() : null);

View file

@ -1,8 +1,5 @@
const { parseURI } = require('lbry-redux'); const { parseURI } = require('lbry-redux');
// const { generateStreamUrl } = require('../../src/ui/util/lbrytv'); // const { generateStreamUrl } = require('../../src/ui/util/lbrytv');
function generateStreamUrl(claimName, claimId) {
return `https://api.lbry.tv/content/claims/${claimName}/${claimId}/stream`;
}
const { WEB_SERVER_PORT } = require('../../config'); const { WEB_SERVER_PORT } = require('../../config');
const { readFileSync } = require('fs'); const { readFileSync } = require('fs');
const express = require('express'); const express = require('express');
@ -72,7 +69,7 @@ app.get('*', async (req, res) => {
let html = readFileSync(path.join(__dirname, '/index.html'), 'utf8'); let html = readFileSync(path.join(__dirname, '/index.html'), 'utf8');
const urlPath = req.path.substr(1); // trim leading slash const urlPath = req.path.substr(1); // trim leading slash
if (urlPath.match(/^([^@/:]+)\/([^:/]+)$/)) { if (!urlPath.startsWith('$/') && urlPath.match(/^([^@/:]+)\/([^:/]+)$/)) {
return res.redirect(301, req.url.replace(/([^/:]+)\/([^:/]+)/, '$1:$2')); // test against urlPath, but use req.url to retain parameters return res.redirect(301, req.url.replace(/([^/:]+)\/([^:/]+)/, '$1:$2')); // test against urlPath, but use req.url to retain parameters
} }

View file

@ -1,11 +1,11 @@
import * as SETTINGS from 'constants/settings';
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doFetchTransactions } from 'lbry-redux';
import { selectUser, doRewardList, doFetchRewardedContent, doFetchAccessToken, selectAccessToken } from 'lbryinc'; import { selectUser, doRewardList, doFetchRewardedContent, doFetchAccessToken, selectAccessToken } from 'lbryinc';
import { doFetchTransactions, doFetchChannelListMine } from 'lbry-redux';
import { makeSelectClientSetting, selectThemePath } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectThemePath } from 'redux/selectors/settings';
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app'; import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app';
import { doDownloadUpgradeRequested } from 'redux/actions/app'; import { doDownloadUpgradeRequested, doSignIn } from 'redux/actions/app';
import * as SETTINGS from 'constants/settings';
import App from './view'; import App from './view';
const select = state => ({ const select = state => ({
@ -23,6 +23,8 @@ const perform = dispatch => ({
fetchTransactions: () => dispatch(doFetchTransactions()), fetchTransactions: () => dispatch(doFetchTransactions()),
fetchAccessToken: () => dispatch(doFetchAccessToken()), fetchAccessToken: () => dispatch(doFetchAccessToken()),
requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()), requestDownloadUpgrade: () => dispatch(doDownloadUpgradeRequested()),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
onSignedIn: () => dispatch(doSignIn()),
}); });
export default hot( export default hot(

View file

@ -2,7 +2,7 @@
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import analytics from 'analytics'; import analytics from 'analytics';
import { Lbry, buildURI, parseURI } from 'lbry-redux'; import { buildURI, parseURI } from 'lbry-redux';
import Router from 'component/router/index'; import Router from 'component/router/index';
import ModalRouter from 'modal/modalRouter'; import ModalRouter from 'modal/modalRouter';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
@ -12,6 +12,7 @@ import Yrbl from 'component/yrbl';
import FileViewer from 'component/fileViewer'; import FileViewer from 'component/fileViewer';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import usePrevious from 'util/use-previous'; import usePrevious from 'util/use-previous';
import Button from 'component/button';
export const MAIN_WRAPPER_CLASS = 'main-wrapper'; export const MAIN_WRAPPER_CLASS = 'main-wrapper';
@ -20,7 +21,6 @@ type Props = {
pageTitle: ?string, pageTitle: ?string,
language: string, language: string,
theme: string, theme: string,
accessToken: ?string,
user: ?{ id: string, has_verified_email: boolean, is_reward_approved: boolean }, user: ?{ id: string, has_verified_email: boolean, is_reward_approved: boolean },
location: { pathname: string, hash: string }, location: { pathname: string, hash: string },
fetchRewards: () => void, fetchRewards: () => void,
@ -30,6 +30,8 @@ type Props = {
autoUpdateDownloaded: boolean, autoUpdateDownloaded: boolean,
isUpgradeAvailable: boolean, isUpgradeAvailable: boolean,
requestDownloadUpgrade: () => void, requestDownloadUpgrade: () => void,
fetchChannelListMine: () => void,
onSignedIn: () => void,
}; };
function App(props: Props) { function App(props: Props) {
@ -40,10 +42,11 @@ function App(props: Props) {
fetchTransactions, fetchTransactions,
user, user,
fetchAccessToken, fetchAccessToken,
accessToken,
requestDownloadUpgrade, requestDownloadUpgrade,
autoUpdateDownloaded, autoUpdateDownloaded,
isUpgradeAvailable, isUpgradeAvailable,
fetchChannelListMine,
onSignedIn,
} = props; } = props;
const appRef = useRef(); const appRef = useRef();
const isEnhancedLayout = useKonamiListener(); const isEnhancedLayout = useKonamiListener();
@ -70,8 +73,9 @@ function App(props: Props) {
// @if TARGET='app' // @if TARGET='app'
fetchRewards(); fetchRewards();
fetchTransactions(); fetchTransactions();
fetchChannelListMine(); // This needs to be done for web too...
// @endif // @endif
}, [fetchRewards, fetchRewardedContent, fetchTransactions, fetchAccessToken]); }, [fetchRewards, fetchRewardedContent, fetchTransactions, fetchAccessToken, fetchChannelListMine]);
useEffect(() => { useEffect(() => {
// $FlowFixMe // $FlowFixMe
@ -87,24 +91,27 @@ function App(props: Props) {
useEffect(() => { useEffect(() => {
// Check that previousHasVerifiedEmail was not undefined instead of just not truthy // Check that previousHasVerifiedEmail was not undefined instead of just not truthy
// This ensures we don't fire the emailVerified event on the initial user fetch // This ensures we don't fire the emailVerified event on the initial user fetch
if (previousHasVerifiedEmail !== undefined && hasVerifiedEmail) { if (previousHasVerifiedEmail === false && hasVerifiedEmail) {
analytics.emailVerifiedEvent(); analytics.emailVerifiedEvent();
} }
}, [previousHasVerifiedEmail, hasVerifiedEmail]); }, [previousHasVerifiedEmail, hasVerifiedEmail, onSignedIn]);
useEffect(() => { useEffect(() => {
if (previousRewardApproved !== undefined && isRewardApproved) { if (previousRewardApproved === false && isRewardApproved) {
analytics.rewardEligibleEvent(); analytics.rewardEligibleEvent();
} }
}, [previousRewardApproved, isRewardApproved]); }, [previousRewardApproved, isRewardApproved]);
// @if TARGET='web' // Keep this at the end to ensure initial setup effects are run first
useEffect(() => { useEffect(() => {
if (hasVerifiedEmail && accessToken) { if (!previousHasVerifiedEmail && hasVerifiedEmail) {
Lbry.setApiHeader('X-Lbry-Auth-Token', accessToken); onSignedIn();
} }
}, [hasVerifiedEmail, accessToken]); }, [previousHasVerifiedEmail, hasVerifiedEmail, onSignedIn]);
// @endif
if (!user) {
return null;
}
return ( return (
<div className={MAIN_WRAPPER_CLASS} ref={appRef} onContextMenu={e => openContextMenu(e)}> <div className={MAIN_WRAPPER_CLASS} ref={appRef} onContextMenu={e => openContextMenu(e)}>

View file

@ -1,4 +1,5 @@
// @flow /* eslint-disable no-undef */
/* eslint-disable react/prop-types */
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
@ -6,35 +7,33 @@ let scriptLoading = false;
let scriptLoaded = false; let scriptLoaded = false;
let scriptDidError = false; let scriptDidError = false;
type Props = { // Flow does not like the way this stripe plugin works
disabled: boolean, // Disabled because it was a huge pain
label: ?string, // type Props = {
email: string, // disabled: boolean,
// label: ?string,
// email: string,
// ===================================================== // // =====================================================
// Required by stripe // // Required by stripe
// see Stripe docs for more info: // // see Stripe docs for more info:
// https://stripe.com/docs/checkout#integration-custom // // https://stripe.com/docs/checkout#integration-custom
// ===================================================== // // =====================================================
// Your publishable key (test or live). // // Your publishable key (test or live).
// can't use "key" as a prop in react, so have to change the keyname // // can't use "key" as a prop in react, so have to change the keyname
stripeKey: string, // stripeKey: string,
// The callback to invoke when the Checkout process is complete. // // The callback to invoke when the Checkout process is complete.
// function(token) // // function(token)
// token is the token object created. // // token is the token object created.
// token.id can be used to create a charge or customer. // // token.id can be used to create a charge or customer.
// token.email contains the email address entered by the user. // // token.email contains the email address entered by the user.
token: string, // token: string,
}; // };
type State = { class CardVerify extends React.Component {
open: boolean, constructor(props) {
};
class CardVerify extends React.Component<Props, State> {
constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
open: false, open: false,
@ -87,6 +86,7 @@ class CardVerify extends React.Component<Props, State> {
this.loadPromise.promise.then(this.onScriptLoaded).catch(this.onScriptError); this.loadPromise.promise.then(this.onScriptLoaded).catch(this.onScriptError);
// $FlowFixMe
document.body.appendChild(script); document.body.appendChild(script);
} }
@ -161,7 +161,7 @@ class CardVerify extends React.Component<Props, State> {
render() { render() {
return ( return (
<Button <Button
button="inverse" button="primary"
label={this.props.label} label={this.props.label}
disabled={this.props.disabled || this.state.open || this.hasPendingClick} disabled={this.props.disabled || this.state.open || this.hasPendingClick}
onClick={this.onClick.bind(this)} onClick={this.onClick.bind(this)}
@ -171,3 +171,5 @@ class CardVerify extends React.Component<Props, State> {
} }
export default CardVerify; export default CardVerify;
/* eslint-enable no-undef */
/* eslint-enable react/prop-types */

View file

@ -15,7 +15,6 @@ type Props = {
uris: Array<string>, uris: Array<string>,
header: Node | boolean, header: Node | boolean,
headerAltControls: Node, headerAltControls: Node,
injectedItem?: Node,
loading: boolean, loading: boolean,
type: string, type: string,
empty?: string, empty?: string,
@ -33,7 +32,6 @@ export default function ClaimList(props: Props) {
const { const {
uris, uris,
headerAltControls, headerAltControls,
injectedItem,
loading, loading,
persistedStorageKey, persistedStorageKey,
empty, empty,
@ -57,7 +55,7 @@ export default function ClaimList(props: Props) {
useEffect(() => { useEffect(() => {
setScrollBottomCbMap({}); setScrollBottomCbMap({});
}, [id]); }, [id, setScrollBottomCbMap]);
useEffect(() => { useEffect(() => {
function handleScroll(e) { function handleScroll(e) {
@ -112,8 +110,6 @@ export default function ClaimList(props: Props) {
</div> </div>
)} )}
{injectedItem && <div>{injectedItem}</div>}
{urisLength > 0 && ( {urisLength > 0 && (
<ul className="ul--no-style"> <ul className="ul--no-style">
{sortedUris.map((uri, index) => ( {sortedUris.map((uri, index) => (
@ -121,6 +117,7 @@ export default function ClaimList(props: Props) {
))} ))}
</ul> </ul>
)} )}
{urisLength === 0 && !loading && <p className="main--empty empty">{empty || __('No results')}</p>} {urisLength === 0 && !loading && <p className="main--empty empty">{empty || __('No results')}</p>}
</section> </section>
); );

View file

@ -32,13 +32,13 @@ type Props = {
uris: Array<string>, uris: Array<string>,
subscribedChannels: Array<Subscription>, subscribedChannels: Array<Subscription>,
doClaimSearch: ({}) => void, doClaimSearch: ({}) => void,
injectedItem: any,
tags: Array<string>, tags: Array<string>,
loading: boolean, loading: boolean,
personalView: boolean, personalView: boolean,
doToggleTagFollow: string => void, doToggleTagFollow: string => void,
meta?: Node, meta?: Node,
showNsfw: boolean, showNsfw: boolean,
hideCustomization: boolean,
history: { action: string, push: string => void, replace: string => void }, history: { action: string, push: string => void, replace: string => void },
location: { search: string, pathname: string }, location: { search: string, pathname: string },
claimSearchByQuery: { claimSearchByQuery: {
@ -54,21 +54,21 @@ function ClaimListDiscover(props: Props) {
tags, tags,
loading, loading,
personalView, personalView,
injectedItem,
meta, meta,
subscribedChannels, subscribedChannels,
showNsfw, showNsfw,
history, history,
location, location,
hiddenUris, hiddenUris,
hideCustomization,
} = props; } = props;
const didNavigateForward = history.action === 'PUSH'; const didNavigateForward = history.action === 'PUSH';
const [page, setPage] = useState(1);
const { search } = location; const { search } = location;
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const personalSort = urlParams.get('sort') || SEARCH_SORT_YOU; const personalSort = urlParams.get('sort') || (hideCustomization ? SEARCH_SORT_ALL : SEARCH_SORT_YOU);
const typeSort = urlParams.get('type') || TYPE_TRENDING; const typeSort = urlParams.get('type') || TYPE_TRENDING;
const timeSort = urlParams.get('time') || TIME_WEEK; const timeSort = urlParams.get('time') || TIME_WEEK;
const [page, setPage] = useState(1);
const tagsInUrl = urlParams.get('t') || ''; const tagsInUrl = urlParams.get('t') || '';
const options: { const options: {
page_size: number, page_size: number,
@ -86,7 +86,7 @@ function ClaimListDiscover(props: Props) {
// no_totals makes it so the sdk doesn't have to calculate total number pages for pagination // no_totals makes it so the sdk doesn't have to calculate total number pages for pagination
// it's faster, but we will need to remove it if we start using total_pages // it's faster, but we will need to remove it if we start using total_pages
no_totals: true, no_totals: true,
any_tags: (personalView && personalSort === SEARCH_SORT_YOU) || !personalView ? tags : [], any_tags: (personalView && !hideCustomization && personalSort === SEARCH_SORT_YOU) || !personalView ? tags : [],
channel_ids: personalSort === SEARCH_SORT_CHANNELS ? subscribedChannels.map(sub => sub.uri.split('#')[1]) : [], channel_ids: personalSort === SEARCH_SORT_CHANNELS ? subscribedChannels.map(sub => sub.uri.split('#')[1]) : [],
not_channel_ids: hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [], not_channel_ids: hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
not_tags: !showNsfw ? MATURE_TAGS : [], not_tags: !showNsfw ? MATURE_TAGS : [],
@ -191,29 +191,33 @@ function ClaimListDiscover(props: Props) {
</option> </option>
))} ))}
</FormField> </FormField>
<span>{__('For')}</span> {!hideCustomization && (
{!personalView && tags && tags.length ? ( <Fragment>
tags.map(tag => <Tag key={tag} name={tag} disabled />) <span>{__('For')}</span>
) : ( {!personalView && tags && tags.length ? (
<FormField tags.map(tag => <Tag key={tag} name={tag} disabled />)
type="select" ) : (
name="trending_overview" <FormField
className="claim-list__dropdown" type="select"
value={personalSort} name="trending_overview"
onChange={e => { className="claim-list__dropdown"
handlePersonalSort(e.target.value); value={personalSort}
}} onChange={e => {
> handlePersonalSort(e.target.value);
{SEARCH_FILTER_TYPES.map(type => ( }}
<option key={type} value={type}> >
{type === SEARCH_SORT_ALL {SEARCH_FILTER_TYPES.map(type => (
? __('Everyone') <option key={type} value={type}>
: type === SEARCH_SORT_YOU {type === SEARCH_SORT_ALL
? __('Tags You Follow') ? __('Everyone')
: __('Channels You Follow')} : type === SEARCH_SORT_YOU
</option> ? __('Tags You Follow')
))} : __('Channels You Follow')}
</FormField> </option>
))}
</FormField>
)}
</Fragment>
)} )}
{typeSort === 'top' && ( {typeSort === 'top' && (
<FormField <FormField
@ -242,7 +246,6 @@ function ClaimListDiscover(props: Props) {
id={claimSearchCacheQuery} id={claimSearchCacheQuery}
loading={loading} loading={loading}
uris={uris} uris={uris}
injectedItem={personalSort === SEARCH_SORT_YOU && injectedItem}
header={header} header={header}
headerAltControls={meta} headerAltControls={meta}
onScrollBottom={handleScrollBottom} onScrollBottom={handleScrollBottom}

View file

@ -0,0 +1,35 @@
// @flow
import type { Node } from 'react';
import React from 'react';
import classnames from 'classnames';
import Icon from 'component/common/icon';
type Props = {
title: string | Node,
subtitle?: string | Node,
body?: string | Node,
actions?: string | Node,
icon?: string,
};
export default function Card(props: Props) {
const { title, subtitle, body, actions, icon } = props;
return (
<section className={classnames('card')}>
<div className="card__header">
<div className="section__flex">
{icon && <Icon sectionIcon icon={icon} />}
<div>
<h2 className="section__title">{title}</h2>
<p className="section__subtitle">{subtitle}</p>
</div>
</div>
</div>
{body && <div className={classnames('card__body', { 'card__body--with-icon': icon })}>{body}</div>}
{actions && (
<div className={classnames('card__main-actions', { 'card__main-actions--with-icon': icon })}>{actions}</div>
)}
</section>
);
}

View file

@ -162,11 +162,7 @@ export class FormField extends React.PureComponent<Props> {
<React.Fragment> <React.Fragment>
<fieldset-section> <fieldset-section>
<label htmlFor={name}>{errorMessage ? <span className="error-text">{errorMessage}</span> : label}</label> <label htmlFor={name}>{errorMessage ? <span className="error-text">{errorMessage}</span> : label}</label>
{prefix && ( {prefix && <label htmlFor={name}>{prefix}</label>}
<label className="form-field--inline-prefix" htmlFor={name}>
{prefix}
</label>
)}
{inner} {inner}
</fieldset-section> </fieldset-section>
</React.Fragment> </React.Fragment>

View file

@ -11,6 +11,7 @@ export class Form extends React.PureComponent<Props> {
const { children, onSubmit, ...otherProps } = this.props; const { children, onSubmit, ...otherProps } = this.props;
return ( return (
<form <form
noValidate
className="form" className="form"
onSubmit={event => { onSubmit={event => {
event.preventDefault(); event.preventDefault();

View file

@ -302,4 +302,9 @@ export const icons = {
<line x1="21" y1="12" x2="9" y2="12" /> <line x1="21" y1="12" x2="9" y2="12" />
</g> </g>
), ),
[ICONS.PHONE]: buildIcon(
<g>
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
</g>
),
}; };

View file

@ -17,6 +17,7 @@ type Props = {
iconColor?: string, iconColor?: string,
size?: number, size?: number,
className?: string, className?: string,
sectionIcon?: boolean,
}; };
class IconComponent extends React.PureComponent<Props> { class IconComponent extends React.PureComponent<Props> {
@ -49,11 +50,10 @@ class IconComponent extends React.PureComponent<Props> {
}; };
render() { render() {
const { icon, tooltip, iconColor, size, className } = this.props; const { icon, tooltip, iconColor, size, className, sectionIcon = false } = this.props;
const Icon = icons[this.props.icon]; const Icon = icons[this.props.icon];
if (!Icon) { if (!Icon) {
console.error('no icon found for ', icon);
return null; return null;
} }
@ -69,7 +69,11 @@ class IconComponent extends React.PureComponent<Props> {
tooltipText = this.getTooltip(icon); tooltipText = this.getTooltip(icon);
} }
const inner = <Icon size={iconSize} className={classnames(`icon icon--${icon}`, className)} color={color} />; const component = (
<Icon size={sectionIcon ? 20 : iconSize} className={classnames(`icon icon--${icon}`, className)} color={color} />
);
const inner = sectionIcon ? <span className="icon__wrapper">{component}</span> : component;
return tooltipText ? <Tooltip label={tooltipText}>{inner}</Tooltip> : inner; return tooltipText ? <Tooltip label={tooltipText}>{inner}</Tooltip> : inner;
} }

View file

@ -1,9 +1,10 @@
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectBalance, formatCredits } from 'lbry-redux'; import { selectBalance, formatCredits } from 'lbry-redux';
import { selectUserEmail } from 'lbryinc'; import { selectUserVerifiedEmail } from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSignOut } from 'redux/actions/app';
import Header from './view'; import Header from './view';
const select = state => ({ const select = state => ({
@ -13,11 +14,12 @@ const select = state => ({
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state), currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
automaticDarkModeEnabled: makeSelectClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED)(state), automaticDarkModeEnabled: makeSelectClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED)(state),
hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state), hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state),
email: selectUserEmail(state), email: selectUserVerifiedEmail(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
signOut: () => dispatch(doSignOut()),
}); });
export default connect( export default connect(

View file

@ -12,22 +12,6 @@ import Icon from 'component/common/icon';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button'; import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import Tooltip from 'component/common/tooltip'; import Tooltip from 'component/common/tooltip';
// Move this into jessops password util
import cookie from 'cookie';
// @if TARGET='app'
import keytar from 'keytar';
// @endif;
function deleteAuthToken() {
// @if TARGET='app'
keytar.deletePassword('LBRY', 'auth_token').catch(console.error); //eslint-disable-line
// @endif;
// @if TARGET='web'
document.cookie = cookie.serialize('auth_token', '', {
expires: new Date(),
});
// @endif
}
type Props = { type Props = {
autoUpdateDownloaded: boolean, autoUpdateDownloaded: boolean,
balance: string, balance: string,
@ -41,6 +25,7 @@ type Props = {
hideBalance: boolean, hideBalance: boolean,
email: ?string, email: ?string,
minimal: boolean, minimal: boolean,
signOut: () => void,
}; };
const Header = (props: Props) => { const Header = (props: Props) => {
@ -53,11 +38,9 @@ const Header = (props: Props) => {
hideBalance, hideBalance,
email, email,
minimal, minimal,
signOut,
} = props; } = props;
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable); const authenticated = Boolean(email);
const isAuthenticated = Boolean(email);
// Auth is optional in the desktop app
const showFullHeader = IS_WEB ? isAuthenticated : true;
function handleThemeToggle() { function handleThemeToggle() {
if (automaticDarkModeEnabled) { if (automaticDarkModeEnabled) {
@ -71,42 +54,14 @@ const Header = (props: Props) => {
} }
} }
function signOut() { function getWalletTitle() {
// Replace this with actual clearUser function return hideBalance ? (
window.store.dispatch({ type: 'USER_FETCH_FAILURE' }); __('Wallet')
deleteAuthToken(); ) : (
location.reload(); <React.Fragment>
} {roundedBalance} <LbcSymbol />
</React.Fragment>
const accountMenuButtons = [ );
{
path: `/$/account`,
icon: ICONS.OVERVIEW,
label: __('Overview'),
},
{
path: `/$/rewards`,
icon: ICONS.FEATURED,
label: __('Rewards'),
},
{
path: `/$/wallet`,
icon: ICONS.WALLET,
label: __('Wallet'),
},
{
path: `/$/publish`,
icon: ICONS.PUBLISH,
label: __('Publish'),
},
];
if (!isAuthenticated) {
accountMenuButtons.push({
onClick: signOut,
icon: ICONS.PUBLISH,
label: __('Publish'),
});
} }
return ( return (
@ -146,19 +101,17 @@ const Header = (props: Props) => {
</div> </div>
{!minimal ? ( {!minimal ? (
<div className="header__menu"> <div className={classnames('header__menu', { 'header__menu--small': IS_WEB && !authenticated })}>
{showFullHeader ? ( {!IS_WEB || authenticated ? (
<Fragment> <Fragment>
<Menu> <Menu>
<MenuButton className="header__navigation-item menu__title"> <MenuButton className="header__navigation-item menu__title">{getWalletTitle()}</MenuButton>
{roundedBalance} <LbcSymbol />
</MenuButton>
<MenuList className="menu__list--header"> <MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/wallet`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.WALLET}`)}>
<Icon aria-hidden icon={ICONS.WALLET} /> <Icon aria-hidden icon={ICONS.WALLET} />
{__('Wallet')} {__('Wallet')}
</MenuItem> </MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/rewards`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.FEATURED} /> <Icon aria-hidden icon={ICONS.FEATURED} />
{__('Rewards')} {__('Rewards')}
</MenuItem> </MenuItem>
@ -169,19 +122,16 @@ const Header = (props: Props) => {
<Icon size={18} icon={ICONS.ACCOUNT} /> <Icon size={18} icon={ICONS.ACCOUNT} />
</MenuButton> </MenuButton>
<MenuList className="menu__list--header"> <MenuList className="menu__list--header">
<MenuItem <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.ACCOUNT}`)}>
className="menu__link"
onSelect={() => history.push(isAuthenticated ? `/$/account` : `/$/auth/signup`)}
>
<Icon aria-hidden icon={ICONS.OVERVIEW} /> <Icon aria-hidden icon={ICONS.OVERVIEW} />
{__('Overview')} {__('Overview')}
</MenuItem> </MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/publish`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISH}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} /> <Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Publish')} {__('Publish')}
</MenuItem> </MenuItem>
{isAuthenticated ? ( {authenticated ? (
<MenuItem className="menu__link" onSelect={signOut}> <MenuItem className="menu__link" onSelect={signOut}>
<Icon aria-hidden icon={ICONS.SIGN_OUT} /> <Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')} {__('Sign Out')}
@ -197,11 +147,11 @@ const Header = (props: Props) => {
<Icon size={18} icon={ICONS.SETTINGS} /> <Icon size={18} icon={ICONS.SETTINGS} />
</MenuButton> </MenuButton>
<MenuList className="menu__list--header"> <MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/settings`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.SETTINGS}`)}>
<Icon aria-hidden tootlip icon={ICONS.SETTINGS} /> <Icon aria-hidden tootlip icon={ICONS.SETTINGS} />
{__('Settings')} {__('Settings')}
</MenuItem> </MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/help`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.HELP}`)}>
<Icon aria-hidden icon={ICONS.HELP} /> <Icon aria-hidden icon={ICONS.HELP} />
{__('Help')} {__('Help')}
</MenuItem> </MenuItem>
@ -213,10 +163,7 @@ const Header = (props: Props) => {
</Menu> </Menu>
</Fragment> </Fragment>
) : ( ) : (
<Fragment> <Button navigate={`/$/${PAGES.AUTH}`} button="primary" label={__('Sign In')} />
<span />
<Button navigate={`/$/${PAGES.AUTH}/signin`} button="primary" label={__('Sign In')} />
</Fragment>
)} )}
</div> </div>
) : ( ) : (

View file

@ -1,7 +1,6 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import RewardLink from 'component/rewardLink'; import RewardLink from 'component/rewardLink';
import Yrbl from 'component/yrbl';
import { rewards } from 'lbryinc'; import { rewards } from 'lbryinc';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
@ -20,22 +19,10 @@ class InviteList extends React.PureComponent<Props> {
render() { render() {
const { invitees, referralReward } = this.props; const { invitees, referralReward } = this.props;
if (!invitees) { if (!invitees || !invitees.length) {
return null; return null;
} }
if (!invitees.length) {
return (
<Yrbl
type="happy"
title={__('Power To The People')}
subtitle={__(
'LBRY is powered by the users. More users, more power… and with great power comes great responsibility.'
)}
/>
);
}
let rewardAmount = 0; let rewardAmount = 0;
let rewardHelp = __( let rewardHelp = __(
"Woah, you have a lot of friends! You've claimed the maximum amount of referral rewards. Check back soon to see if more are available!." "Woah, you have a lot of friends! You've claimed the maximum amount of referral rewards. Check back soon to see if more are available!."
@ -60,10 +47,10 @@ class InviteList extends React.PureComponent<Props> {
/> />
)} )}
</h2> </h2>
<p className="card__subtitle">{rewardHelp}</p> <p className="section__subtitle">{rewardHelp}</p>
</div> </div>
<table className="table table--invites"> <table className="table section">
<thead> <thead>
<tr> <tr>
<th>{__('Invitee Email')}</th> <th>{__('Invitee Email')}</th>

View file

@ -3,6 +3,7 @@ import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import CopyableText from 'component/copyableText'; import CopyableText from 'component/copyableText';
import Card from 'component/common/card';
type FormProps = { type FormProps = {
inviteNew: string => void, inviteNew: string => void,
@ -73,25 +74,28 @@ class InviteNew extends React.PureComponent<Props> {
const { errorMessage, inviteNew, isPending, rewardAmount, referralLink } = this.props; const { errorMessage, inviteNew, isPending, rewardAmount, referralLink } = this.props;
return ( return (
<section className="card card--section"> <Card
<h2 className="card__title">{__('Invite a Friend')}</h2> title={__('Invite a Friend')}
<p className="card__subtitle">{__('When your friends start using LBRY, the network gets stronger!')}</p> subtitle={__('When your friends start using LBRY, the network gets stronger!')}
body={
<React.Fragment>
<FormInviteNew
errorMessage={errorMessage}
inviteNew={inviteNew}
isPending={isPending}
rewardAmount={rewardAmount}
/>
<CopyableText label={__('Or share this link with your friends')} copyable={referralLink} />
<FormInviteNew <p className="help">
errorMessage={errorMessage} {__('Earn')} <Button button="link" navigate="/$/rewards" label={__('rewards')} />{' '}
inviteNew={inviteNew} {__('for inviting your friends.')} {__('Read our')}{' '}
isPending={isPending} <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/referrals" />{' '}
rewardAmount={rewardAmount} {__('to learn more about referrals')}.
/> </p>
<CopyableText label={__('Or share this link with your friends')} copyable={referralLink} /> </React.Fragment>
}
<p className="help"> />
{__('Earn')} <Button button="link" navigate="/$/rewards" label={__('rewards')} />{' '}
{__('for inviting your friends.')} {__('Read our')}{' '}
<Button button="link" label={__('FAQ')} href="https://lbry.com/faq/referrals" />{' '}
{__('to learn more about referrals')}.
</p>
</section>
); );
} }
} }

View file

@ -1,3 +1,9 @@
import { connect } from 'react-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
import Page from './view'; import Page from './view';
export default Page; const select = state => ({
authenticated: Boolean(selectUserVerifiedEmail(state)),
});
export default connect(select)(Page);

View file

@ -1,11 +1,9 @@
// @flow // @flow
import type { Node } from 'react'; import type { Node } from 'react';
import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import SideBar from 'component/sideBar'; import SideBar from 'component/sideBar';
import Header from 'component/header'; import Header from 'component/header';
import Button from 'component/button';
type Props = { type Props = {
children: Node | Array<Node>, children: Node | Array<Node>,
@ -13,35 +11,19 @@ type Props = {
autoUpdateDownloaded: boolean, autoUpdateDownloaded: boolean,
isUpgradeAvailable: boolean, isUpgradeAvailable: boolean,
fullscreen: boolean, fullscreen: boolean,
doDownloadUpgradeRequested: () => void, authenticated: boolean,
}; };
function Page(props: Props) { function Page(props: Props) {
const { const { children, className, fullscreen = false, authenticated } = props;
children, const obscureSideBar = IS_WEB ? !authenticated : false;
className,
fullscreen = false,
autoUpdateDownloaded,
isUpgradeAvailable,
doDownloadUpgradeRequested,
} = props;
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
return ( return (
<Fragment> <Fragment>
<Header minimal={fullscreen} /> <Header minimal={fullscreen} />
<div className={classnames('main-wrapper__inner')}> <div className={classnames('main-wrapper__inner')}>
{/* @if TARGET='app' */} <main className={classnames('main', className, { 'main--full-width': fullscreen })}>{children}</main>
{showUpgradeButton && ( {!fullscreen && <SideBar obscureSideBar={obscureSideBar} />}
<div className="main__status">
{__('Upgrade is ready')}
<Button button="alt" icon={ICONS.DOWNLOAD} label={__('Install now')} onClick={doDownloadUpgradeRequested} />
</div>
)}
{/* @endif */}
<main className={classnames('main', className)}>{children}</main>
{!fullscreen && <SideBar />}
</div> </div>
</Fragment> </Fragment>
); );

View file

@ -32,7 +32,7 @@ const RewardLink = (props: Props) => {
return !reward ? null : ( return !reward ? null : (
<Button <Button
button={button ? 'inverse' : 'link'} button={button ? 'primary' : 'link'}
disabled={isPending} disabled={isPending}
label={displayLabel} label={displayLabel}
onClick={() => { onClick={() => {

View file

@ -3,6 +3,7 @@ import * as React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import CreditAmount from 'component/common/credit-amount'; import CreditAmount from 'component/common/credit-amount';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import Card from 'component/common/card';
type Props = { type Props = {
unclaimedRewardAmount: number, unclaimedRewardAmount: number,
@ -14,33 +15,35 @@ class RewardSummary extends React.Component<Props> {
const { unclaimedRewardAmount, fetching } = this.props; const { unclaimedRewardAmount, fetching } = this.props;
const hasRewards = unclaimedRewardAmount > 0; const hasRewards = unclaimedRewardAmount > 0;
return ( return (
<section className="card card--section"> <Card
<h2 className="card__title">{__('Rewards')}</h2> title={__('Rewards')}
subtitle={
<p className="card__subtitle"> <React.Fragment>
{fetching && __('You have...')} {fetching && __('You have...')}
{!fetching && hasRewards ? ( {!fetching && hasRewards ? (
<I18nMessage <I18nMessage
tokens={{ tokens={{
credit_amount: <CreditAmount inheritStyle amount={unclaimedRewardAmount} precision={8} />, credit_amount: <CreditAmount inheritStyle amount={unclaimedRewardAmount} precision={8} />,
}} }}
> >
You have %credit_amount% in unclaimed rewards. You have %credit_amount% in unclaimed rewards.
</I18nMessage> </I18nMessage>
) : ( ) : (
__('You have no rewards available.') __('You have no rewards available.')
)} )}
</p> </React.Fragment>
}
<div className="card__actions"> actions={
<Button <div className="section__actions">
button="primary" <Button
navigate="/$/rewards" button="primary"
label={hasRewards ? __('Claim Rewards') : __('View Rewards')} navigate="/$/rewards"
/> label={hasRewards ? __('Claim Rewards') : __('View Rewards')}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/rewards" />. />
</div> <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/rewards" />.
</section> </div>
}
/>
); );
} }
} }

View file

@ -4,6 +4,7 @@ import React from 'react';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import RewardLink from 'component/rewardLink'; import RewardLink from 'component/rewardLink';
import Button from 'component/button'; import Button from 'component/button';
import Card from 'component/common/card';
import { rewards } from 'lbryinc'; import { rewards } from 'lbryinc';
type Props = { type Props = {
@ -25,27 +26,28 @@ const RewardTile = (props: Props) => {
const claimed = !!reward.transaction_id; const claimed = !!reward.transaction_id;
return ( return (
<section className="card card--section"> <Card
<h2 className="card__title">{reward.reward_title}</h2> title={reward.reward_title}
<p className="card__subtitle">{reward.reward_description}</p> subtitle={reward.reward_description}
actions={
<div className="card__actions"> <div className="card__actions">
{reward.reward_type === rewards.TYPE_GENERATED_CODE && ( {reward.reward_type === rewards.TYPE_GENERATED_CODE && (
<Button button="inverse" onClick={openRewardCodeModal} label={__('Enter Code')} /> <Button button="primary" onClick={openRewardCodeModal} label={__('Enter Code')} />
)} )}
{reward.reward_type === rewards.TYPE_REFERRAL && ( {reward.reward_type === rewards.TYPE_REFERRAL && (
<Button button="inverse" navigate="/$/invite" label={__('Go To Invites')} /> <Button button="primary" navigate="/$/invite" label={__('Go To Invites')} />
)} )}
{reward.reward_type !== rewards.TYPE_REFERRAL && {reward.reward_type !== rewards.TYPE_REFERRAL &&
(claimed ? ( (claimed ? (
<span> <span>
<Icon icon={ICONS.COMPLETED} /> {__('Reward claimed.')} <Icon icon={ICONS.COMPLETED} /> {__('Reward claimed.')}
</span> </span>
) : ( ) : (
<RewardLink button reward_type={reward.reward_type} /> <RewardLink button reward_type={reward.reward_type} />
))} ))}
</div> </div>
</section> }
/>
); );
}; };

View file

@ -1,9 +1,11 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
import { selectScrollStartingPosition } from 'redux/selectors/app'; import { selectScrollStartingPosition } from 'redux/selectors/app';
import Router from './view'; import Router from './view';
const select = state => ({ const select = state => ({
currentScroll: selectScrollStartingPosition(state), currentScroll: selectScrollStartingPosition(state),
isAuthenticated: selectUserVerifiedEmail(state),
}); });
export default connect(select)(Router); export default connect(select)(Router);

View file

@ -13,12 +13,10 @@ import RewardsPage from 'page/rewards';
import FileListDownloaded from 'page/fileListDownloaded'; import FileListDownloaded from 'page/fileListDownloaded';
import FileListPublished from 'page/fileListPublished'; import FileListPublished from 'page/fileListPublished';
import TransactionHistoryPage from 'page/transactionHistory'; import TransactionHistoryPage from 'page/transactionHistory';
import AuthPage from 'page/auth';
import InvitePage from 'page/invite'; import InvitePage from 'page/invite';
import SearchPage from 'page/search'; import SearchPage from 'page/search';
import LibraryPage from 'page/library'; import LibraryPage from 'page/library';
import WalletPage from 'page/wallet'; import WalletPage from 'page/wallet';
import NavigationHistory from 'page/navigationHistory';
import TagsPage from 'page/tags'; import TagsPage from 'page/tags';
import FollowingPage from 'page/following'; import FollowingPage from 'page/following';
import ListBlockedPage from 'page/listBlocked'; import ListBlockedPage from 'page/listBlocked';
@ -30,9 +28,32 @@ if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual'; history.scrollRestoration = 'manual';
} }
type PrivateRouteProps = {
component: any,
isAuthenticated: boolean,
location: { pathname: string },
};
function PrivateRoute(props: PrivateRouteProps) {
const { component: Component, isAuthenticated, ...rest } = props;
return (
<Route
{...rest}
render={props =>
isAuthenticated || !IS_WEB ? (
<Component {...props} />
) : (
<Redirect to={`/$/${PAGES.AUTH}?redirect=${props.location.pathname}`} />
)
}
/>
);
}
type Props = { type Props = {
currentScroll: number, currentScroll: number,
location: { pathname: string, search: string }, location: { pathname: string, search: string },
isAuthenticated: boolean,
}; };
function AppRouter(props: Props) { function AppRouter(props: Props) {
@ -46,25 +67,25 @@ function AppRouter(props: Props) {
<Switch> <Switch>
<Route path="/" exact component={DiscoverPage} /> <Route path="/" exact component={DiscoverPage} />
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} /> <Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={AuthPage} /> <Route path={`/$/${PAGES.AUTH}`} exact component={SignInPage} />
<Route path={`/$/${PAGES.AUTH}/signin`} exact component={SignInPage} />
<Route path={`/$/${PAGES.INVITE}`} exact component={InvitePage} />
<Route path={`/$/${PAGES.DOWNLOADED}`} exact component={FileListDownloaded} />
<Route path={`/$/${PAGES.PUBLISHED}`} exact component={FileListPublished} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
<Route path={`/$/${PAGES.PUBLISH}`} exact component={PublishPage} />
<Route path={`/$/${PAGES.REPORT}`} exact component={ReportPage} />
<Route path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} />
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
<Route path={`/$/${PAGES.LIBRARY}`} exact component={LibraryPage} />
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
<Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} />
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} /> <Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.FOLLOWING}`} exact component={FollowingPage} /> <Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
<Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} /> <Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
<Route path={`/$/${PAGES.BLOCKED}`} exact component={ListBlockedPage} />
<PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} />
<PrivateRoute {...props} path={`/$/${PAGES.DOWNLOADED}`} component={FileListDownloaded} />
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISHED}`} component={FileListPublished} />
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISH}`} component={PublishPage} />
<PrivateRoute {...props} path={`/$/${PAGES.REPORT}`} component={ReportPage} />
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS}`} component={RewardsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS}`} component={SettingsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TRANSACTIONS}`} component={TransactionHistoryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.ACCOUNT}`} component={AccountPage} />
<PrivateRoute {...props} path={`/$/${PAGES.FOLLOWING}`} component={FollowingPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} component={WalletPage} />
<PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} />
{/* Below need to go at the end to make sure we don't match any of our pages first */} {/* Below need to go at the end to make sure we don't match any of our pages first */}
<Route path="/:claimName" exact component={ShowPage} /> <Route path="/:claimName" exact component={ShowPage} />
<Route path="/:claimName/:streamName" exact component={ShowPage} /> <Route path="/:claimName/:streamName" exact component={ShowPage} />

View file

@ -3,14 +3,8 @@ import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import { SEARCH_OPTIONS } from 'lbry-redux'; import { SEARCH_OPTIONS } from 'lbry-redux';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import posed from 'react-pose';
import Button from 'component/button'; import Button from 'component/button';
const ExpandableOptions = posed.div({
hide: { height: 0, opacity: 0 },
show: { height: 380, opacity: 1 },
});
type Props = { type Props = {
setSearchOption: (string, boolean | string | number) => void, setSearchOption: (string, boolean | string | number) => void,
options: {}, options: {},
@ -30,93 +24,91 @@ const SearchOptions = (props: Props) => {
iconRight={expanded ? ICONS.UP : ICONS.DOWN} iconRight={expanded ? ICONS.UP : ICONS.DOWN}
onClick={toggleSearchExpanded} onClick={toggleSearchExpanded}
/> />
<ExpandableOptions pose={expanded ? 'show' : 'hide'}> {expanded && (
{expanded && ( <Form className="search__options">
<Form className="search__options"> <fieldset>
<fieldset> <legend className="search__legend--1">{__('Search For')}</legend>
<legend className="search__legend--1">{__('Search For')}</legend> {[
{[ {
{ option: SEARCH_OPTIONS.INCLUDE_FILES,
option: SEARCH_OPTIONS.INCLUDE_FILES, label: __('Files'),
label: __('Files'), },
}, {
{ option: SEARCH_OPTIONS.INCLUDE_CHANNELS,
option: SEARCH_OPTIONS.INCLUDE_CHANNELS, label: __('Channels'),
label: __('Channels'), },
}, {
{ option: SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS,
option: SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS, label: __('Everything'),
label: __('Everything'), },
}, ].map(({ option, label }) => (
].map(({ option, label }) => (
<FormField
key={option}
name={option}
type="radio"
blockWrap={false}
label={label}
checked={options[SEARCH_OPTIONS.CLAIM_TYPE] === option}
onChange={() => setSearchOption(SEARCH_OPTIONS.CLAIM_TYPE, option)}
/>
))}
</fieldset>
<fieldset>
<legend className="search__legend--2">{__('File Types')}</legend>
{[
{
option: SEARCH_OPTIONS.MEDIA_VIDEO,
label: __('Videos'),
},
{
option: SEARCH_OPTIONS.MEDIA_AUDIO,
label: __('Audio'),
},
{
option: SEARCH_OPTIONS.MEDIA_IMAGE,
label: __('Images'),
},
{
option: SEARCH_OPTIONS.MEDIA_TEXT,
label: __('Text'),
},
{
option: SEARCH_OPTIONS.MEDIA_APPLICATION,
label: __('Other Files'),
},
].map(({ option, label }) => (
<FormField
key={option}
name={option}
type="checkbox"
blockWrap={false}
disabled={options[SEARCH_OPTIONS.CLAIM_TYPE] === SEARCH_OPTIONS.INCLUDE_CHANNELS}
label={label}
checked={options[option]}
onChange={() => setSearchOption(option, !options[option])}
/>
))}
</fieldset>
<fieldset>
<legend className="search__legend--3">{__('Other Options')}</legend>
<FormField <FormField
type="select" key={option}
name="result-count" name={option}
value={resultCount} type="radio"
onChange={e => setSearchOption(SEARCH_OPTIONS.RESULT_COUNT, e.target.value)}
blockWrap={false} blockWrap={false}
label={__('Returned Results')} label={label}
> checked={options[SEARCH_OPTIONS.CLAIM_TYPE] === option}
<option value={10}>10</option> onChange={() => setSearchOption(SEARCH_OPTIONS.CLAIM_TYPE, option)}
<option value={30}>30</option> />
<option value={50}>50</option> ))}
<option value={100}>100</option> </fieldset>
</FormField>
</fieldset> <fieldset>
</Form> <legend className="search__legend--2">{__('File Types')}</legend>
)} {[
</ExpandableOptions> {
option: SEARCH_OPTIONS.MEDIA_VIDEO,
label: __('Videos'),
},
{
option: SEARCH_OPTIONS.MEDIA_AUDIO,
label: __('Audio'),
},
{
option: SEARCH_OPTIONS.MEDIA_IMAGE,
label: __('Images'),
},
{
option: SEARCH_OPTIONS.MEDIA_TEXT,
label: __('Text'),
},
{
option: SEARCH_OPTIONS.MEDIA_APPLICATION,
label: __('Other Files'),
},
].map(({ option, label }) => (
<FormField
key={option}
name={option}
type="checkbox"
blockWrap={false}
disabled={options[SEARCH_OPTIONS.CLAIM_TYPE] === SEARCH_OPTIONS.INCLUDE_CHANNELS}
label={label}
checked={options[option]}
onChange={() => setSearchOption(option, !options[option])}
/>
))}
</fieldset>
<fieldset>
<legend className="search__legend--3">{__('Other Options')}</legend>
<FormField
type="select"
name="result-count"
value={resultCount}
onChange={e => setSearchOption(SEARCH_OPTIONS.RESULT_COUNT, e.target.value)}
blockWrap={false}
label={__('Returned Results')}
>
<option value={10}>10</option>
<option value={30}>30</option>
<option value={50}>50</option>
<option value={100}>100</option>
</FormField>
</fieldset>
</Form>
)}
</div> </div>
); );
}; };

View file

@ -8,7 +8,7 @@ import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
type Props = { type Props = {
channel: string, // currently selected channel channel: string, // currently selected channel
channels: Array<{ name: string }>, channels: ?Array<{ name: string }>,
balance: number, balance: number,
onChannelChange: string => void, onChannelChange: string => void,
createChannel: (string, number) => Promise<any>, createChannel: (string, number) => Promise<any>,

View file

@ -1,21 +1,21 @@
// @flow // @flow
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import Tag from 'component/tag'; import Tag from 'component/tag';
import StickyBox from 'react-sticky-box/dist/esnext'; import StickyBox from 'react-sticky-box/dist/esnext';
import 'css-doodle';
type Props = { type Props = {
subscriptions: Array<Subscription>, subscriptions: Array<Subscription>,
followedTags: Array<Tag>, followedTags: Array<Tag>,
email: ?string, email: ?string,
obscureSideBar: boolean,
}; };
function SideBar(props: Props) { function SideBar(props: Props) {
const { subscriptions, followedTags, email } = props; const { subscriptions, followedTags, obscureSideBar } = props;
const showSideBar = IS_WEB ? Boolean(email) : true;
function buildLink(path, label, icon, guide) { function buildLink(path, label, icon, guide) {
return { return {
navigate: path ? `$/${path}` : '/', navigate: path ? `$/${path}` : '/',
@ -25,68 +25,67 @@ function SideBar(props: Props) {
}; };
} }
return ( return obscureSideBar ? (
<StickyBox offsetTop={100} offsetBottom={20}> <StickyBox offsetTop={100} offsetBottom={20}>
{showSideBar ? ( <div className="card navigation--placeholder">
<nav className="navigation"> <div className="wrap">
<ul className="navigation-links"> <h2>LBRY</h2>
{[
{
...buildLink(null, __('Home'), ICONS.HOME),
},
// @if TARGET='app'
{
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
},
// @endif
{
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISH),
},
].map(linkProps => (
<li key={linkProps.label}>
<Button {...linkProps} className="navigation-link" activeClass="navigation-link--active" />
</li>
))}
</ul>
{email ? ( <p>{__('The best decentralized content platform on the web.')}</p>
<Fragment> <div className="card__actions">{/* <Button button="primary" label={__('Do Something')} /> */}</div>
</div>
</div>
</StickyBox>
) : (
<StickyBox offsetTop={100} offsetBottom={20}>
<nav className="navigation">
<ul className="navigation-links">
{[
{
...buildLink(null, __('Home'), ICONS.HOME),
},
// @if TARGET='app'
{
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
},
// @endif
{
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISH),
},
].map(linkProps => (
<li key={linkProps.label}>
<Button {...linkProps} className="navigation-link" activeClass="navigation-link--active" />
</li>
))}
</ul>
<Button
navigate={`/$/${PAGES.FOLLOWING}`}
label={__('Customize')}
icon={ICONS.EDIT}
className="navigation-link"
activeClass="navigation-link--active"
/>
<ul className="navigation-links tags--vertical">
{followedTags.map(({ name }, key) => (
<li className="navigation-link__wrapper" key={name}>
<Tag navigate={`/$/tags?t${name}`} name={name} />
</li>
))}
</ul>
<ul className="navigation-links--small">
{subscriptions.map(({ uri, channelName }, index) => (
<li key={uri} className="navigation-link__wrapper">
<Button <Button
navigate={`/$/${PAGES.FOLLOWING}`} navigate={uri}
label={__('Customize')} label={channelName}
icon={ICONS.EDIT}
className="navigation-link" className="navigation-link"
activeClass="navigation-link--active" activeClass="navigation-link--active"
/> />
<ul className="navigation-links tags--vertical"> </li>
{followedTags.map(({ name }, key) => ( ))}
<li className="navigation-link__wrapper" key={name}> </ul>
<Tag navigate={`/$/tags?t${name}`} name={name} /> </nav>
</li>
))}
</ul>
<ul className="navigation-links--small">
{subscriptions.map(({ uri, channelName }, index) => (
<li key={uri} className="navigation-link__wrapper">
<Button
navigate={uri}
label={channelName}
className="navigation-link"
activeClass="navigation-link--active"
/>
</li>
))}
</ul>
</Fragment>
) : (
<div className="navigation--placeholder" style={{ height: '20vh', marginTop: '1rem', padding: '1rem' }}>
Something about logging up to customize here
</div>
)}
</nav>
) : (
<div className="navigation--placeholder" />
)}
</StickyBox> </StickyBox>
); );
} }

View file

@ -1,26 +1,30 @@
// @flow // @flow
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useTransition, animated } from 'react-spring';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import Tag from 'component/tag'; import Tag from 'component/tag';
const unfollowedTagsAnimation = {
from: { opacity: 0 },
enter: { opacity: 1, maxWidth: 200 },
leave: { opacity: 0, maxWidth: 0 },
};
type Props = { type Props = {
tagsPasssedIn: Array<Tag>,
unfollowedTags: Array<Tag>, unfollowedTags: Array<Tag>,
followedTags: Array<Tag>, followedTags: Array<Tag>,
doToggleTagFollow: string => void, doToggleTagFollow: string => void,
doAddTag: string => void, doAddTag: string => void,
onSelect?: Tag => void, onSelect?: Tag => void,
suggestMature?: boolean, suggestMature?: boolean,
onRemove: Tag => void,
}; };
export default function TagSelect(props: Props) { export default function TagSelect(props: Props) {
const { unfollowedTags = [], followedTags = [], doToggleTagFollow, doAddTag, onSelect, suggestMature } = props; const {
tagsPasssedIn,
unfollowedTags = [],
followedTags = [],
doToggleTagFollow,
doAddTag,
onSelect,
onRemove,
suggestMature,
} = props;
const [newTag, setNewTag] = useState(''); const [newTag, setNewTag] = useState('');
let tags = unfollowedTags.slice(); let tags = unfollowedTags.slice();
@ -39,8 +43,6 @@ export default function TagSelect(props: Props) {
suggestedTags.push('mature'); suggestedTags.push('mature');
} }
const suggestedTransitions = useTransition(suggestedTags, tag => tag, unfollowedTagsAnimation);
function onChange(e) { function onChange(e) {
setNewTag(e.target.value); setNewTag(e.target.value);
} }
@ -76,24 +78,37 @@ export default function TagSelect(props: Props) {
} }
return ( return (
<div> <React.Fragment>
<Form onSubmit={handleSubmit}> <Form className="tags__input-wrapper" onSubmit={handleSubmit}>
<FormField <ul className="tags--remove">
label={__('Find New Tags')} {tagsPasssedIn.map(tag => (
onChange={onChange} <Tag
placeholder={__('Search for more tags')} key={tag.name}
type="text" name={tag.name}
value={newTag} type="remove"
/> onClick={() => {
onRemove(tag);
}}
/>
))}
<li>
<FormField
autoFocus
className="tag__input"
onChange={onChange}
placeholder={__('Follow more tags')}
type="text"
value={newTag}
/>
</li>
</ul>
</Form> </Form>
<ul className="tags"> <ul className="tags">
{suggestedTransitions.map(({ item, key, props }) => ( {suggestedTags.map(tag => (
<animated.li key={key} style={props}> <Tag key={tag} name={tag} type="add" onClick={() => handleTagClick(tag)} />
<Tag name={item} type="add" onClick={() => handleTagClick(item)} />
</animated.li>
))} ))}
{!suggestedTransitions.length && <p className="empty tags__empty-message">No suggested tags</p>} {!suggestedTags.length && <p className="empty tags__empty-message">No suggested tags</p>}
</ul> </ul>
</div> </React.Fragment>
); );
} }

View file

@ -1,12 +1,12 @@
// @flow // @flow
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React from 'react'; import * as React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import Tag from 'component/tag'; import Tag from 'component/tag';
import TagsSearch from 'component/tagsSearch'; import TagsSearch from 'component/tagsSearch';
import usePersistedState from 'util/use-persisted-state'; import usePersistedState from 'util/use-persisted-state';
import { useTransition, animated } from 'react-spring';
import analytics from 'analytics'; import analytics from 'analytics';
import Card from 'component/common/card';
type Props = { type Props = {
showClose?: boolean, showClose?: boolean,
@ -17,17 +17,9 @@ type Props = {
// The default component is for following tags // The default component is for following tags
title?: string | boolean, title?: string | boolean,
help?: string, help?: string,
empty?: string,
tagsChosen?: Array<Tag>, tagsChosen?: Array<Tag>,
onSelect?: Tag => void, onSelect?: Tag => void,
onRemove?: Tag => void, onRemove?: Tag => void,
className?: string,
};
const tagsAnimation = {
from: { opacity: 0 },
enter: { opacity: 1, maxWidth: 400 },
leave: { opacity: 0, maxWidth: 0 },
}; };
export default function TagSelect(props: Props) { export default function TagSelect(props: Props) {
@ -37,16 +29,14 @@ export default function TagSelect(props: Props) {
doToggleTagFollow = null, doToggleTagFollow = null,
title, title,
help, help,
empty,
tagsChosen, tagsChosen,
onSelect, onSelect,
onRemove, onRemove,
suggestMature, suggestMature,
className,
} = props; } = props;
const [hasClosed, setHasClosed] = usePersistedState('tag-select:has-closed', false); const [hasClosed, setHasClosed] = usePersistedState('tag-select:has-closed', false);
const tagsToDisplay = tagsChosen || followedTags; const tagsToDisplay = tagsChosen || followedTags;
const transitions = useTransition(tagsToDisplay, tag => tag.name, tagsAnimation); const tagCount = tagsToDisplay.length;
const hasMatureTag = tagsToDisplay.map(tag => tag.name).includes('mature'); const hasMatureTag = tagsToDisplay.map(tag => tag.name).includes('mature');
function handleClose() { function handleClose() {
@ -65,40 +55,43 @@ export default function TagSelect(props: Props) {
} }
} }
React.useEffect(() => {
if (tagCount === 0) {
setHasClosed(false);
}
}, [tagCount, setHasClosed]);
return ( return (
((showClose && !hasClosed) || !showClose) && ( ((showClose && !hasClosed) || !showClose) && (
<div className={className}> <Card
{title !== false && ( icon={ICONS.TAG}
<h2 className="card__title"> title={
<React.Fragment>
{title} {title}
{showClose && !hasClosed && <Button button="close" icon={ICONS.REMOVE} onClick={handleClose} />} {showClose && tagsToDisplay.length > 0 && !hasClosed && (
</h2> <Button button="close" icon={ICONS.REMOVE} onClick={handleClose} />
)} )}
</React.Fragment>
<ul className="tags--remove"> }
{transitions.map(({ item, props, key }) => ( body={
<animated.li key={key} style={props}> <React.Fragment>
<Tag <section className="section">
name={item.name} <TagsSearch
type="remove" onRemove={handleTagClick}
onClick={() => { onSelect={onSelect}
handleTagClick(item); suggestMature={suggestMature && !hasMatureTag}
}} tagsPasssedIn={tagsToDisplay}
/> />
</animated.li> {help !== false && (
))} <p className="help">
{!transitions.length && ( {help || __("The tags you follow will change what's trending for you.")}{' '}
<div className="empty">{empty || __("You aren't following any tags, try searching for one.")}</div> <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/trending" />.
)} </p>
</ul> )}
<TagsSearch onSelect={onSelect} suggestMature={suggestMature && !hasMatureTag} /> </section>
{help !== false && ( </React.Fragment>
<p className="help"> }
{help || __("The tags you follow will change what's trending for you.")}{' '} />
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/trending" />.
</p>
)}
</div>
) )
); );
} }

View file

@ -14,10 +14,8 @@ class TransactionListRecent extends React.PureComponent<Props> {
componentDidMount() { componentDidMount() {
const { fetchMyClaims, fetchTransactions } = this.props; const { fetchMyClaims, fetchTransactions } = this.props;
// @if TARGET='app'
fetchMyClaims(); fetchMyClaims();
fetchTransactions(); fetchTransactions();
// @endif
} }
render() { render() {

View file

@ -1,11 +1,11 @@
// @flow // @flow
import * as PAGES from 'constants/pages';
import type { Node } from 'react'; import type { Node } from 'react';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import UserEmailNew from 'component/userEmailNew'; import UserSignOutButton from 'component/userSignOutButton';
import UserEmailVerify from 'component/userEmailVerify'; import Card from 'component/common/card';
import UserEmailResetButton from 'component/userEmailResetButton';
type Props = { type Props = {
cancelButton: Node, cancelButton: Node,
@ -34,44 +34,34 @@ function UserEmail(props: Props) {
}, [accessToken, fetchAccessToken]); }, [accessToken, fetchAccessToken]);
return ( return (
<section className="card card--section"> <Card
{!email && <UserEmailNew />} title={__('Email')}
{user && email && !isVerified && <UserEmailVerify />} subtitle={__(
{email && isVerified && ( 'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to save account information and earn rewards.'
<React.Fragment>
<h2 className="card__title">{__('Email')}</h2>
<p className="card__subtitle">
{email && isVerified && __('Your email has been successfully verified')}
{!email && __('')}.
</p>
{isVerified && (
<FormField
type="text"
className="form-field--copyable"
readOnly
label={
<React.Fragment>
{__('Your Email')}{' '}
<Button
button="link"
label={__('Update mailing preferences')}
href={`http://lbry.io/list/edit/${accessToken}`}
/>
</React.Fragment>
}
value={email}
inputButton={<UserEmailResetButton button="inverse" />}
/>
)}
<p className="help">
{`${__(
'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to save account information and earn rewards.'
)} `}
</p>
</React.Fragment>
)} )}
</section> body={
isVerified ? (
<FormField
type="text"
className="form-field--copyable"
readOnly
label={
<React.Fragment>
{__('Your Email')}{' '}
<Button
button="link"
label={__('Update mailing preferences')}
href={`http://lbry.io/list/edit/${accessToken}`}
/>
</React.Fragment>
}
inputButton={<UserSignOutButton button="inverse" />}
value={email || ''}
/>
) : null
}
actions={!isVerified ? <Button button="primary" label={__('Add Email')} navigate={`/$/${PAGES.AUTH}`} /> : null}
/>
); );
} }

View file

@ -11,10 +11,14 @@ type Props = {
addUserEmail: string => void, addUserEmail: string => void,
}; };
// "Email regex that 99.99% works"
// https://emailregex.com/
const EMAIL_REGEX = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;
function UserEmailNew(props: Props) { function UserEmailNew(props: Props) {
const { errorMessage, isPending, addUserEmail } = props; const { errorMessage, isPending, addUserEmail } = props;
const [newEmail, setEmail] = useState(''); const [newEmail, setEmail] = useState('');
const [sync, setSync] = useState(false); const valid = newEmail.match(EMAIL_REGEX);
function handleSubmit() { function handleSubmit() {
addUserEmail(newEmail); addUserEmail(newEmail);
@ -26,26 +30,26 @@ function UserEmailNew(props: Props) {
} }
return ( return (
<Form onSubmit={handleSubmit}> <div>
<FormField <h1 className="section__title--large">{__('Welcome To LBRY')}</h1>
type="email" <p className="section__subtitle">{__('Create a new account or sign in.')}</p>
id="sign_up_email" <Form onSubmit={handleSubmit} className="section__body">
label={__('Email')} <FormField
value={newEmail} autoFocus
error={errorMessage} className="form-field--short"
onChange={e => setEmail(e.target.value)} placeholder={__('hotstuff_96@hotmail.com')}
/> type="email"
<FormField id="sign_up_email"
type="checkbox" label={__('Email')}
id="sign_up_sync" value={newEmail}
label={__('Sync my bidnezz on this device')} error={errorMessage}
helper={__('Maybe some additional text with this field')} onChange={e => setEmail(e.target.value)}
checked={sync} />
onChange={() => setSync(!sync)} <div className="card__actions">
/> <Button button="primary" type="submit" label={__('Continue')} disabled={!newEmail || !valid || isPending} />
</div>
<Button button="primary" type="submit" label={__('Continue')} disabled={isPending} /> </Form>
</Form> </div>
); );
} }

View file

@ -1,11 +0,0 @@
import { connect } from 'react-redux';
import UserEmailResetButton from './view';
const select = state => ({});
const perform = dispatch => ({});
export default connect(
select,
perform
)(UserEmailResetButton);

View file

@ -1,24 +0,0 @@
// @flow
import React from 'react';
import Button from 'component/button';
import cookie from 'cookie';
type Props = {
button: string,
};
function UserEmailResetButton(props: Props) {
const { button = 'link' } = props;
const buttonsProps = IS_WEB
? {
onClick: () => {
document.cookie = cookie.serialize('auth_token', '');
window.location.reload();
},
}
: { href: 'https://lbry.com/faq/how-to-change-email' };
return <Button button={button} label={__('Change... fix me sean')} {...buttonsProps} />;
}
export default UserEmailResetButton;

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import UserEmailResetButton from 'component/userEmailResetButton'; import UserSignOutButton from 'component/userSignOutButton';
type Props = { type Props = {
email: string, email: string,
@ -52,18 +52,19 @@ class UserEmailVerify extends React.PureComponent<Props> {
return ( return (
<React.Fragment> <React.Fragment>
<p className="card__subtitle"> <h1 className="section__title--large">{__('Confirm Your Email')}</h1>
{__('An email was sent to')} {email}.{' '}
{__('Follow the link and you will be good to go. This will update automatically.')} <p className="section__subtitle">
{__('An email was sent to')} {email}. {__('Follow the link to sign in. This will update automatically.')}
</p> </p>
<div className="card__actions"> <div className="section__body section__actions">
<Button <Button
button="primary" button="primary"
label={__('Resend Verification Email')} label={__('Resend Verification Email')}
onClick={this.handleResendVerificationEmail} onClick={this.handleResendVerificationEmail}
/> />
<UserEmailResetButton /> <UserSignOutButton label={__('Start Over')} />
</div> </div>
<p className="help"> <p className="help">

View file

@ -1,28 +1,21 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectUser, selectEmailToVerify, rewards as REWARD_TYPES, doClaimRewardType } from 'lbryinc'; import { selectUser, selectEmailToVerify } from 'lbryinc';
import { doCreateChannel, selectCreatingChannel, selectMyChannelClaims } from 'lbry-redux'; import { doCreateChannel, selectCreatingChannel, selectMyChannelClaims, selectCreateChannelError } from 'lbry-redux';
import UserSignUp from './view'; import UserFirstChannel from './view';
const select = state => ({ const select = state => ({
email: selectEmailToVerify(state), email: selectEmailToVerify(state),
user: selectUser(state), user: selectUser(state),
creatingChannel: selectCreatingChannel(state),
channels: selectMyChannelClaims(state), channels: selectMyChannelClaims(state),
creatingChannel: selectCreatingChannel(state),
createChannelError: selectCreateChannelError(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)), createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
claimReward: cb =>
dispatch(
doClaimRewardType(REWARD_TYPES.TYPE_REWARD_CODE, {
notifyError: true,
successCallback: cb,
params: { code: 'sean-test' },
})
),
}); });
export default connect( export default connect(
select, select,
perform perform
)(UserSignUp); )(UserFirstChannel);

View file

@ -4,23 +4,25 @@ import { isNameValid } from 'lbry-redux';
import Button from 'component/button'; import Button from 'component/button';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
const DEFAULT_BID_FOR_FIRST_CHANNEL = 0.1; export const DEFAULT_BID_FOR_FIRST_CHANNEL = 0.9;
type Props = { type Props = {
createChannel: (string, number) => void, createChannel: (string, number) => void,
claimReward: (() => void) => void,
creatingChannel: boolean, creatingChannel: boolean,
createChannelError: string,
claimingReward: boolean,
user: User,
}; };
function UserFirstChannel(props: Props) { function UserFirstChannel(props: Props) {
const { createChannel, creatingChannel, claimReward } = props; const { createChannel, creatingChannel, claimingReward, user, createChannelError } = props;
const [channel, setChannel] = useState(''); const { primary_email: primaryEmail } = user;
const [nameError, setNameError] = useState(); const initialChannel = primaryEmail.split('@')[0];
const [channel, setChannel] = useState(initialChannel);
const [nameError, setNameError] = useState(undefined);
function handleCreateChannel() { function handleCreateChannel() {
claimReward(() => { createChannel(`@${channel}`, DEFAULT_BID_FOR_FIRST_CHANNEL);
createChannel(`@${channel}`, DEFAULT_BID_FOR_FIRST_CHANNEL);
});
} }
function handleChannelChange(e) { function handleChannelChange(e) {
@ -35,29 +37,43 @@ function UserFirstChannel(props: Props) {
return ( return (
<Form onSubmit={handleCreateChannel}> <Form onSubmit={handleCreateChannel}>
<h1 className="card__title--large">{__('Choose Your Channel Name')}</h1> <h1 className="section__title--large">{__('Create A Channel')}</h1>
<p className="card__subtitle"> <div className="section__subtitle">
{__("Normally this would cost LBRY credits, but we're hooking you up for your first one.")} <p>{__('A channel is your identity on the LBRY network.')}</p>
</p> <p>{__('You can have more than one or change this later.')}</p>
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section>
<label htmlFor="auth_first_channel">
{nameError ? <span className="error-text">{nameError}</span> : __('Your Channel')}
</label>
<div className="form-field__prefix">@</div>
</fieldset-section>
<FormField type="text" name="auth_first_channel" value={channel} onChange={handleChannelChange} />
</fieldset-group>
<div className="card__actions">
<Button
button="primary"
type="submit"
disabled={nameError || !channel || creatingChannel}
label={__('Create')}
onClick={handleCreateChannel}
/>
</div> </div>
<section className="section__body">
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section>
<label htmlFor="auth_first_channel">
{createChannelError || nameError ? (
<span className="error-text">{createChannelError || nameError}</span>
) : (
__('Your Channel')
)}
</label>
<div className="form-field__prefix">@</div>
</fieldset-section>
<FormField
autoFocus
placeholder={__('channel')}
type="text"
name="auth_first_channel"
className="form-field--short"
value={channel}
onChange={handleChannelChange}
/>
</fieldset-group>
<div className="section__actions">
<Button
button="primary"
type="submit"
disabled={nameError || !channel || creatingChannel || claimingReward}
label={creatingChannel || claimingReward ? __('Creating') : __('Create')}
/>
</div>
</section>
</Form> </Form>
); );
} }

View file

@ -1,27 +1,39 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
selectEmailToVerify, selectEmailToVerify,
doUserResendVerificationEmail,
doUserCheckEmailVerified,
selectUser, selectUser,
doFetchAccessToken,
selectAccessToken, selectAccessToken,
makeSelectIsRewardClaimPending,
selectClaimedRewards,
rewards as REWARD_TYPES,
doClaimRewardType,
} from 'lbryinc'; } from 'lbryinc';
import UserSignUp from './view'; import { selectMyChannelClaims, selectBalance, selectFetchingMyChannels } from 'lbry-redux';
import UserSignIn from './view';
const select = state => ({ const select = state => ({
email: selectEmailToVerify(state), email: selectEmailToVerify(state),
user: selectUser(state), user: selectUser(state),
accessToken: selectAccessToken(state), accessToken: selectAccessToken(state),
channels: selectMyChannelClaims(state),
claimedRewards: selectClaimedRewards(state),
claimingReward: makeSelectIsRewardClaimPending()(state, {
reward_type: REWARD_TYPES.TYPE_CONFIRM_EMAIL,
}),
balance: selectBalance(state),
fetchingChannels: selectFetchingMyChannels(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email)), claimReward: () =>
checkEmailVerified: () => dispatch(doUserCheckEmailVerified()), dispatch(
fetchAccessToken: () => dispatch(doFetchAccessToken()), doClaimRewardType(REWARD_TYPES.TYPE_CONFIRM_EMAIL, {
notifyError: false,
})
),
}); });
export default connect( export default connect(
select, select,
perform perform
)(UserSignUp); )(UserSignIn);

View file

@ -1,32 +1,101 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { withRouter } from 'react-router';
import UserEmailNew from 'component/userEmailNew'; import UserEmailNew from 'component/userEmailNew';
import UserEmailVerify from 'component/userEmailVerify'; import UserEmailVerify from 'component/userEmailVerify';
import UserFirstChannel from 'component/userFirstChannel';
import UserVerify from 'component/userVerify';
import Spinner from 'component/spinner';
import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view';
import { rewards as REWARDS } from 'lbryinc';
import usePrevious from 'util/use-previous';
type Props = { type Props = {
user: ?User, user: ?User,
email: ?string, email: ?string,
fetchingChannels: boolean,
channels: ?Array<string>,
balance: ?number,
fetchingChannels: boolean,
claimingReward: boolean,
claimReward: () => void,
claimedRewards: Array<Reward>,
history: { replace: string => void },
location: { search: string },
}; };
function UserSignUp(props: Props) { function useFetched(fetching) {
const { email, user } = props; const wasFetching = usePrevious(fetching);
const verifiedEmail = user && email && user.has_verified_email; const [fetched, setFetched] = React.useState(false);
function getTitle() { React.useEffect(() => {
if (!email) { if (wasFetching && !fetching) {
return __('Get Rockin'); setFetched(true);
} else if (email && !verifiedEmail) {
return __('We Sent You An Email');
} }
}, [wasFetching, fetching, setFetched]);
return fetched;
}
function UserSignIn(props: Props) {
const { email, user, channels, claimingReward, claimReward, claimedRewards, balance, history, location } = props;
const { search } = location;
const urlParams = new URLSearchParams(search);
const redirect = urlParams.get('redirect');
const hasFetchedReward = useFetched(claimingReward);
const hasVerifiedEmail = user && user.has_verified_email;
const rewardsApproved = user && user.is_reward_approved;
const channelCount = channels ? channels.length : 0;
const hasFetchedChannels = channels !== undefined;
const hasClaimedEmailAward = claimedRewards.some(reward => reward.reward_type === REWARDS.TYPE_CONFIRM_EMAIL);
const memoizedClaimReward = React.useCallback(() => {
claimReward();
}, [claimReward]);
React.useEffect(() => {
if (hasVerifiedEmail && balance !== undefined && !hasClaimedEmailAward && !hasFetchedReward) {
memoizedClaimReward();
}
}, [hasVerifiedEmail, memoizedClaimReward, balance, hasClaimedEmailAward, hasFetchedReward]);
if (
!user ||
(balance === 0 && !hasFetchedReward) ||
(hasVerifiedEmail && balance === undefined) ||
(hasVerifiedEmail && !hasFetchedChannels)
) {
return (
<div className="main--empty">
<Spinner delayed />
</div>
);
}
if (balance === 0 && hasClaimedEmailAward) {
history.replace(redirect || '/');
}
if (rewardsApproved && channelCount > 0) {
history.replace(redirect || '/');
}
if (rewardsApproved && hasFetchedReward && balance && (balance === 0 || balance < DEFAULT_BID_FOR_FIRST_CHANNEL)) {
history.replace(redirect || '/');
} }
return ( return (
<section> <section>
<h1 className="card__title--large">{getTitle()}</h1> {hasVerifiedEmail && !rewardsApproved ? (
{!email && <UserEmailNew />} <UserVerify />
{email && !verifiedEmail && <UserEmailVerify />} ) : (
<div className="main--contained">
{!email && !hasVerifiedEmail && <UserEmailNew />}
{email && !hasVerifiedEmail && <UserEmailVerify />}
{hasVerifiedEmail && balance && balance > 0 && channelCount === 0 && <UserFirstChannel />}
</div>
)}
</section> </section>
); );
} }
export default UserSignUp; export default withRouter(UserSignIn);

View file

@ -0,0 +1,14 @@
import { connect } from 'react-redux';
import { doSignOut } from 'redux/actions/app';
import UserSignOutButton from './view';
const select = state => ({});
const perform = dispatch => ({
signOut: () => dispatch(doSignOut()),
});
export default connect(
select,
perform
)(UserSignOutButton);

View file

@ -0,0 +1,17 @@
// @flow
import React from 'react';
import Button from 'component/button';
type Props = {
button: string,
label?: string,
signOut: () => void,
};
function UserSignOutButton(props: Props) {
const { button = 'link', signOut, label } = props;
return <Button button={button} label={label || __('Sign Out')} onClick={signOut} />;
}
export default UserSignOutButton;

View file

@ -1,8 +1,10 @@
// @flow // @flow
import * as React from 'react'; import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import CardVerify from 'component/cardVerify'; import CardVerify from 'component/cardVerify';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import Card from 'component/common/card';
type Props = { type Props = {
errorMessage: ?string, errorMessage: ?string,
@ -26,91 +28,87 @@ class UserVerify extends React.PureComponent<Props> {
const { errorMessage, isPending, verifyPhone } = this.props; const { errorMessage, isPending, verifyPhone } = this.props;
return ( return (
<React.Fragment> <React.Fragment>
<section className="card card--section"> <section className="section--large">
<h1 className="card__title">{__('Final Human Proof')}</h1> <h1 className="section__title--large">{__('Extra Verification Needed')}</h1>
<p className="card__subtitle"> <p>
To start the rewards approval process, please complete <strong>one and only one</strong> of the options
below. This is optional, and can be skipped at the bottom of the page.
</p>
</section>
<section className="card card--section">
<h2 className="card__title">{__('1) Proof via Phone')}</h2>
<p className="card__subtitle">
{`${__(
'You will receive an SMS text message confirming that your phone number is correct. Does not work for Canada and possibly other regions'
)}`}
</p>
<div className="card__actions">
<Button
onClick={() => {
verifyPhone();
}}
button="inverse"
label={__('Submit Phone Number')}
/>
</div>
<div className="help">
{__('Standard messaging rates apply. LBRY will not text or call you otherwise. Having trouble?')}{' '}
<Button button="link" href="https://lbry.com/faq/phone" label={__('Read more.')} />
</div>
</section>
<section className="card card--section">
<h2 className="card__title">{__('2) Proof via Credit')}</h2>
<p className="card__subtitle">
{`${__('If you have a valid credit or debit card, you can use it to instantly prove your humanity.')} ${__(
'LBRY does not store your credit card information. There is no charge at all for this, now or in the future.'
)} `}
</p>
<div className="card__actions">
{errorMessage && <p className="error-text">{errorMessage}</p>}
<CardVerify
label={__('Perform Card Verification')}
disabled={isPending}
token={this.onToken}
stripeKey={Lbryio.getStripeToken()}
/>
</div>
<div className="help">
{__('A $1 authorization may temporarily appear with your provider.')}{' '}
<Button
button="link"
href="https://lbry.com/faq/identity-requirements"
label={__('Read more about why we do this.')}
/>
</div>
</section>
<section className="card card--section">
<h2 className="card__title">{__('3) Proof via Chat')}</h2>
<p className="card__subtitle">
{__( {__(
'A moderator capable of approving you is typically available in the discord server. Check out the #rewards-approval channel for more information.' "We weren't able to auto-approve you for rewards. Please complete one of the steps below to unlock them."
)}{' '} )}{' '}
{__( <Button navigate="/" button="link" label={__('Skip')} />.
'This process will likely involve providing proof of a stable and established online or real-life identity.' </p>
</section>
<div className="section">
<Card
icon={ICONS.PHONE}
title={__('Proof via Text')}
subtitle={__(
'You will receive an SMS text message confirming that your phone number is correct. Does not work for Canada and possibly other regions'
)} )}
</p> actions={
<Fragment>
<Button
onClick={() => {
verifyPhone();
}}
button="primary"
label={__('Verify Phone Number')}
/>
<p className="help">
{__('Standard messaging rates apply. LBRY will not text or call you otherwise. Having trouble?')}{' '}
<Button button="link" href="https://lbry.com/faq/phone" label={__('Read more.')} />
</p>
</Fragment>
}
/>
<div className="card__actions"> <div className="section__divider">
<Button href="https://chat.lbry.com" button="inverse" label={__('Join LBRY Chat')} /> <hr />
<p>{__('OR')}</p>
</div> </div>
</section>
<section className="card card--section"> <Card
<h2 className="card__title">{__('Or, Skip It Entirely')}</h2> icon={ICONS.WALLET}
<p className="card__subtitle"> title={__('Proof via Credit')}
{__('You can continue without this step, but you will not be eligible to earn rewards.')} subtitle={__(
</p> 'If you have a valid credit or debit card, you can use it to instantly prove your humanity. LBRY does not store your credit card information. There is no charge at all for this, now or in the future.'
)}
actions={
<Fragment>
{errorMessage && <p className="error-text">{errorMessage}</p>}
<CardVerify
label={__('Verify Card')}
disabled={isPending}
token={this.onToken}
stripeKey={Lbryio.getStripeToken()}
/>
<p className="help">
{__('A $1 authorization may temporarily appear with your provider.')}{' '}
<Button button="link" href="https://lbry.com/faq/identity-requirements" label={__('Read more')} />.
</p>
</Fragment>
}
/>
<div className="card__actions"> <div className="section__divider">
<Button navigate="/" button="primary" label={__('Skip Rewards')} /> <hr />
<p>{__('OR')}</p>
</div> </div>
</section>
<Card
icon={ICONS.CHAT}
title={__('Proof via Chat')}
subtitle={__(
'A moderator capable of approving you is typically available in the discord server. Check out the #rewards-approval channel for more information. This process will likely involve providing proof of a stable and established online or real-life identity.'
)}
actions={
<Fragment>
<Button href="https://chat.lbry.com" button="primary" label={__('Join LBRY Chat')} />
<p className="help">{__("We're friendly. We promise.")}</p>
</Fragment>
}
/>
</div>
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -3,6 +3,7 @@ import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import CopyableText from 'component/copyableText'; import CopyableText from 'component/copyableText';
import QRCode from 'component/common/qr-code'; import QRCode from 'component/common/qr-code';
import Card from 'component/common/card';
type Props = { type Props = {
checkAddressIsMine: string => void, checkAddressIsMine: string => void,
@ -46,29 +47,31 @@ class WalletAddress extends React.PureComponent<Props, State> {
const { showQR } = this.state; const { showQR } = this.state;
return ( return (
<section className="card card--section"> <Card
<h2 className="card__title">{__('Receive Credits')}</h2> title={__('Receive Credits')}
<p className="card__subtitle"> subtitle={__(
{__( 'Use this address to receive LBC. You can generate a new address at any time, and any previous addresses will continue to work.'
'Use this address to receive LBC. You can generate a new address at any time, and any previous addresses will continue to work.' )}
)} actions={
</p> <React.Fragment>
<CopyableText label={__('Your Address')} copyable={receiveAddress} snackMessage={__('Address copied.')} /> <CopyableText label={__('Your Address')} copyable={receiveAddress} snackMessage={__('Address copied.')} />
<div className="card__actions"> <div className="card__actions">
{!IS_WEB && ( {!IS_WEB && (
<Button <Button
button="inverse" button="inverse"
label={__('Get New Address')} label={__('Get New Address')}
onClick={getNewAddress} onClick={getNewAddress}
disabled={gettingNewAddress} disabled={gettingNewAddress}
/> />
)} )}
<Button button="link" label={showQR ? __('Hide QR code') : __('Show QR code')} onClick={this.toggleQR} /> <Button button="link" label={showQR ? __('Hide QR code') : __('Show QR code')} onClick={this.toggleQR} />
</div> </div>
{showQR && <QRCode value={receiveAddress} paddingTop />} {showQR && <QRCode value={receiveAddress} paddingTop />}
</section> </React.Fragment>
}
/>
); );
} }
} }

View file

@ -1,4 +1,3 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
doWalletStatus, doWalletStatus,
@ -27,7 +26,6 @@ import {
selectUser, selectUser,
} from 'lbryinc'; } from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
const select = state => ({ const select = state => ({
walletEncryptSucceeded: selectWalletEncryptSucceeded(state), walletEncryptSucceeded: selectWalletEncryptSucceeded(state),
@ -36,7 +34,6 @@ const select = state => ({
walletEncrypted: selectWalletIsEncrypted(state), walletEncrypted: selectWalletIsEncrypted(state),
walletHasTransactions: selectHasTransactions(state), walletHasTransactions: selectHasTransactions(state),
user: selectUser(state), user: selectUser(state),
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
hasSyncedWallet: selectHasSyncedWallet(state), hasSyncedWallet: selectHasSyncedWallet(state),
getSyncIsPending: selectGetSyncIsPending(state), getSyncIsPending: selectGetSyncIsPending(state),
setSyncIsPending: selectSetSyncIsPending(state), setSyncIsPending: selectSetSyncIsPending(state),

View file

@ -1,404 +1,405 @@
// @flow export default () => null;
import React, { useState, useEffect } from 'react'; // // @flow
import { Form, FormField, Submit } from 'component/common/form'; // import React, { useState } from 'react';
import Button from 'component/button'; // import { Form, FormField, Submit } from 'component/common/form';
import UserEmail from 'component/userEmail'; // import Button from 'component/button';
import * as ICONS from 'constants/icons'; // import UserEmail from 'component/userEmail';
// import * as ICONS from 'constants/icons';
import { getSavedPassword, setSavedPassword, deleteSavedPassword } from 'util/saved-passwords'; // import { getSavedPassword, setSavedPassword, deleteAuthToken } from 'util/saved-passwords';
type Props = { // type Props = {
// wallet statuses // // wallet statuses
walletEncryptSucceeded: boolean, // walletEncryptSucceeded: boolean,
walletEncryptPending: boolean, // walletEncryptPending: boolean,
walletDecryptSucceeded: boolean, // walletDecryptSucceeded: boolean,
walletDecryptPending: boolean, // walletDecryptPending: boolean,
updateWalletStatus: boolean, // updateWalletStatus: boolean,
walletEncrypted: boolean, // walletEncrypted: boolean,
// wallet methods // // wallet methods
encryptWallet: (?string) => void, // encryptWallet: (?string) => void,
decryptWallet: (?string) => void, // decryptWallet: (?string) => void,
updateWalletStatus: () => void, // updateWalletStatus: () => void,
// housekeeping // // housekeeping
setPasswordSaved: () => void, // setPasswordSaved: () => void,
syncEnabled: boolean, // syncEnabled: boolean,
setClientSetting: (string, boolean | string) => void, // setClientSetting: (string, boolean | string) => void,
isPasswordSaved: boolean, // isPasswordSaved: boolean,
// data // // data
user: any, // user: any,
// sync statuses // // sync statuses
hasSyncedWallet: boolean, // hasSyncedWallet: boolean,
getSyncIsPending?: boolean, // getSyncIsPending?: boolean,
syncApplyErrorMessage?: string, // syncApplyErrorMessage?: string,
hashChanged: boolean, // hashChanged: boolean,
// sync data // // sync data
syncData: string | null, // syncData: string | null,
syncHash: string | null, // syncHash: string | null,
// sync methods // // sync methods
syncApply: (string | null, string | null, string) => void, // syncApply: (string | null, string | null, string) => void,
checkSync: () => void, // checkSync: () => void,
setDefaultAccount: () => void, // setDefaultAccount: () => void,
hasTransactions: boolean, // hasTransactions: boolean,
}; // };
type State = { // type State = {
newPassword: string, // newPassword: string,
newPasswordConfirm: string, // newPasswordConfirm: string,
passwordMatch: boolean, // passwordMatch: boolean,
understandConfirmed: boolean, // understandConfirmed: boolean,
understandError: boolean, // understandError: boolean,
submitted: boolean, // submitted: boolean,
failMessage: ?string, // failMessage: ?string,
rememberPassword: boolean, // rememberPassword: boolean,
showEmailReg: boolean, // showEmailReg: boolean,
failed: boolean, // failed: boolean,
enableSync: boolean, // enableSync: boolean,
encryptWallet: boolean, // encryptWallet: boolean,
obscurePassword: boolean, // obscurePassword: boolean,
advancedMode: boolean, // advancedMode: boolean,
showPasswordFields: boolean, // showPasswordFields: boolean,
}; // };
function WalletSecurityAndSync(props: Props) { // function WalletSecurityAndSync(props: Props) {
const { // const {
// walletEncryptSucceeded, // // walletEncryptSucceeded,
// walletEncryptPending, // // walletEncryptPending,
// walletDecryptSucceeded, // // walletDecryptSucceeded,
// walletDecryptPending, // // walletDecryptPending,
// updateWalletStatus, // // updateWalletStatus,
walletEncrypted, // walletEncrypted,
encryptWallet, // encryptWallet,
decryptWallet, // decryptWallet,
syncEnabled, // syncEnabled,
user, // user,
hasSyncedWallet, // hasSyncedWallet,
getSyncIsPending, // getSyncIsPending,
syncApplyErrorMessage, // syncApplyErrorMessage,
hashChanged, // hashChanged,
syncData, // syncData,
syncHash, // syncHash,
syncApply, // syncApply,
checkSync, // checkSync,
hasTransactions, // hasTransactions,
setDefaultAccount, // // setDefaultAccount,
} = props; // } = props;
const defaultComponentState: State = { // const defaultComponentState: State = {
newPassword: '', // newPassword: '',
newPasswordConfirm: '', // newPasswordConfirm: '',
passwordMatch: false, // passwordMatch: false,
understandConfirmed: false, // understandConfirmed: false,
understandError: false, // understandError: false,
submitted: false, // Prior actions could be marked complete // submitted: false, // Prior actions could be marked complete
failMessage: undefined, // failMessage: undefined,
rememberPassword: false, // rememberPassword: false,
showEmailReg: false, // showEmailReg: false,
failed: false, // failed: false,
enableSync: syncEnabled, // enableSync: syncEnabled,
encryptWallet: walletEncrypted, // encryptWallet: walletEncrypted,
obscurePassword: true, // obscurePassword: true,
advancedMode: false, // advancedMode: false,
showPasswordFields: false, // showPasswordFields: false,
}; // };
const [componentState, setComponentState] = useState<State>(defaultComponentState); // const [componentState, setComponentState] = useState<State>(defaultComponentState);
const safeToSync = !hasTransactions || !hashChanged; // const safeToSync = !hasTransactions || !hashChanged;
// on mount // // on mount
useEffect(() => { // // useEffect(() => {
checkSync(); // // checkSync();
getSavedPassword().then(p => { // // getSavedPassword().then(p => {
if (p) { // // if (p) {
setComponentState({ // // setComponentState({
...componentState, // // ...componentState,
newPassword: p, // // newPassword: p,
newPasswordConfirm: p, // // newPasswordConfirm: p,
showPasswordFields: true, // // showPasswordFields: true,
rememberPassword: true, // // rememberPassword: true,
}); // // });
} // // }
}); // // });
}, []); // // }, []);
useEffect(() => { // // useEffect(() => {
setComponentState({ // // setComponentState({
...componentState, // // ...componentState,
passwordMatch: componentState.newPassword === componentState.newPasswordConfirm, // // passwordMatch: componentState.newPassword === componentState.newPasswordConfirm,
}); // // });
}, [componentState.newPassword, componentState.newPasswordConfirm]); // // }, [componentState.newPassword, componentState.newPasswordConfirm]);
const isEmailVerified = user && user.primary_email && user.has_verified_email; // const isEmailVerified = user && user.primary_email && user.has_verified_email;
// const syncDisabledMessage = 'You cannot sync without an email'; // // const syncDisabledMessage = 'You cannot sync without an email';
function onChangeNewPassword(event: SyntheticInputEvent<>) { // function onChangeNewPassword(event: SyntheticInputEvent<>) {
setComponentState({ ...componentState, newPassword: event.target.value || '' }); // setComponentState({ ...componentState, newPassword: event.target.value || '' });
} // }
function onChangeRememberPassword(event: SyntheticInputEvent<>) { // function onChangeRememberPassword(event: SyntheticInputEvent<>) {
if (componentState.rememberPassword) { // if (componentState.rememberPassword) {
deleteSavedPassword(); // deleteAuthToken();
} // }
setComponentState({ ...componentState, rememberPassword: event.target.checked }); // setComponentState({ ...componentState, rememberPassword: event.target.checked });
} // }
function onChangeNewPasswordConfirm(event: SyntheticInputEvent<>) { // function onChangeNewPasswordConfirm(event: SyntheticInputEvent<>) {
setComponentState({ ...componentState, newPasswordConfirm: event.target.value || '' }); // setComponentState({ ...componentState, newPasswordConfirm: event.target.value || '' });
} // }
function onChangeUnderstandConfirm(event: SyntheticInputEvent<>) { // function onChangeUnderstandConfirm(event: SyntheticInputEvent<>) {
setComponentState({ ...componentState, understandConfirmed: /^.?i understand.?$/i.test(event.target.value) }); // setComponentState({ ...componentState, understandConfirmed: /^.?i understand.?$/i.test(event.target.value) });
} // }
function onChangeSync(event: SyntheticInputEvent<>) { // function onChangeSync(event: SyntheticInputEvent<>) {
if (componentState.enableSync) { // if (componentState.enableSync) {
setComponentState({ ...componentState, enableSync: false, newPassword: '', newPasswordConfirm: '' }); // setComponentState({ ...componentState, enableSync: false, newPassword: '', newPasswordConfirm: '' });
setComponentState({ ...componentState, enableSync: false, newPassword: '', newPasswordConfirm: '' }); // setComponentState({ ...componentState, enableSync: false, newPassword: '', newPasswordConfirm: '' });
} // }
if (!(walletEncrypted || syncApplyErrorMessage || componentState.advancedMode)) { // if (!(walletEncrypted || syncApplyErrorMessage || componentState.advancedMode)) {
easyApply(); // easyApply();
} else { // } else {
setComponentState({ ...componentState, enableSync: true }); // setComponentState({ ...componentState, enableSync: true });
} // }
} // }
function onChangeEncrypt(event: SyntheticInputEvent<>) { // function onChangeEncrypt(event: SyntheticInputEvent<>) {
setComponentState({ ...componentState, encryptWallet: event.target.checked }); // setComponentState({ ...componentState, encryptWallet: event.target.checked });
} // }
async function easyApply() { // async function easyApply() {
return new Promise((resolve, reject) => { // return new Promise((resolve, reject) => {
return syncApply(syncHash, syncData, componentState.newPassword); // return syncApply(syncHash, syncData, componentState.newPassword);
}) // })
.then(() => { // .then(() => {
setComponentState({ ...componentState, enableSync: event.target.checked }); // setComponentState({ ...componentState, enableSync: event.target.checked });
}) // })
.catch(); // .catch();
} // }
async function apply() { // async function apply() {
setComponentState({ ...componentState, failed: false }); // setComponentState({ ...componentState, failed: false });
await checkSync(); // await checkSync();
if (componentState.enableSync) { // if (componentState.enableSync) {
await syncApply(syncHash, syncData, componentState.newPassword); // await syncApply(syncHash, syncData, componentState.newPassword);
if (syncApplyErrorMessage) { // if (syncApplyErrorMessage) {
setComponentState({ ...componentState, failed: true }); // setComponentState({ ...componentState, failed: true });
} // }
} // }
if (walletEncrypted) { // if (walletEncrypted) {
await decryptWallet(); // await decryptWallet();
} // }
if (componentState.encryptWallet && !componentState.failed) { // if (componentState.encryptWallet && !componentState.failed) {
await encryptWallet(componentState.newPassword) // await encryptWallet(componentState.newPassword)
.then(() => {}) // .then(() => {})
.catch(() => { // .catch(() => {
setComponentState({ ...componentState, failed: false }); // setComponentState({ ...componentState, failed: false });
}); // });
} // }
if (componentState.rememberPassword && !componentState.failed) { // if (componentState.rememberPassword && !componentState.failed) {
setSavedPassword(componentState.newPassword); // setSavedPassword(componentState.newPassword);
} // }
} // }
return ( // return (
<React.Fragment> // <React.Fragment>
<section className="card card--section"> // <section className="card card--section">
<h2 className="card__title">{__('Wallet Sync and Security')}</h2> // <h2 className="card__title">{__('Wallet Sync and Security')}</h2>
{!isEmailVerified && ( // {!isEmailVerified && (
<React.Fragment> // <React.Fragment>
<p className="card__subtitle"> // <p className="card__subtitle">
{__(`It looks like we don't have your email.`)}{' '} // {__(`It looks like we don't have your email.`)}{' '}
<Button // <Button
button="link" // button="link"
label={__('Verify your email')} // label={__('Verify your email')}
onClick={() => setComponentState({ ...componentState, showEmailReg: !componentState.showEmailReg })} // onClick={() => setComponentState({ ...componentState, showEmailReg: !componentState.showEmailReg })}
/>{' '} // />{' '}
{__(`and then come back here.`)} // {__(`and then come back here.`)}
</p> // </p>
{componentState.showEmailReg && <UserEmail />} // {componentState.showEmailReg && <UserEmail />}
</React.Fragment> // </React.Fragment>
)} // )}
<p> // <p>
{__( // {__(
'Your LBRY password can help you encrypt your wallet or sync it to another device. You must use the same LBRY password on every device if you wish to sync.' // 'Your LBRY password can help you encrypt your wallet or sync it to another device. You must use the same LBRY password on every device if you wish to sync.'
)}{' '} // )}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />. // <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />.
</p> // </p>
{/* Errors and status */} // {/* Errors and status */}
{!componentState.advancedMode && ( // {!componentState.advancedMode && (
<React.Fragment> // <React.Fragment>
<p className="card__subtitle"> // <p className="card__subtitle">
{__(`Easy Mode: Sync and go with default security! Don't trust your roommate?`)}{' '} // {__(`Easy Mode: Sync and go with default security! Don't trust your roommate?`)}{' '}
<Button // <Button
button="link" // button="link"
label={__('Advanced Mode')} // label={__('Advanced Mode')}
onClick={() => setComponentState({ ...componentState, advancedMode: !componentState.advancedMode })} // onClick={() => setComponentState({ ...componentState, advancedMode: !componentState.advancedMode })}
/> // />
. // .
</p> // </p>
</React.Fragment> // </React.Fragment>
)} // )}
{componentState.advancedMode && ( // {componentState.advancedMode && (
<React.Fragment> // <React.Fragment>
<p className="card__subtitle"> // <p className="card__subtitle">
{__('Advanced Mode: Enter a password that matches your other devices LBRY password.')}{' '} // {__('Advanced Mode: Enter a password that matches your other devices LBRY password.')}{' '}
<Button // <Button
button="link" // button="link"
label={__('Easy Mode')} // label={__('Easy Mode')}
onClick={() => setComponentState({ ...componentState, advancedMode: !componentState.advancedMode })} // onClick={() => setComponentState({ ...componentState, advancedMode: !componentState.advancedMode })}
/> // />
. // .
</p> // </p>
</React.Fragment> // </React.Fragment>
)} // )}
{syncApplyErrorMessage && <div className="card__subtitle--status">{__(syncApplyErrorMessage)}</div>} // {syncApplyErrorMessage && <div className="card__subtitle--status">{__(syncApplyErrorMessage)}</div>}
<Form onSubmit={() => apply()}> // <Form onSubmit={() => apply()}>
{componentState.advancedMode && ( // {componentState.advancedMode && (
<FormField // <FormField
type="checkbox" // type="checkbox"
name="sync_enabled" // name="sync_enabled"
checked={componentState.enableSync} // checked={componentState.enableSync}
disabled={!isEmailVerified || !safeToSync} // disabled={!isEmailVerified || !safeToSync}
prefix={<span className="badge badge--alert">ALPHA</span>} // prefix={<span className="badge badge--alert">ALPHA</span>}
onChange={event => onChangeSync(event)} // onChange={event => onChangeSync(event)}
label={ // label={
<React.Fragment> // <React.Fragment>
{__('Enable Sync')} <Button button="link" label={__('(?)')} href="https://lbry.com/privacypolicy" />{' '} // {__('Enable Sync')} <Button button="link" label={__('(?)')} href="https://lbry.com/privacypolicy" />{' '}
<span className="badge badge--alert">ALPHA</span> // <span className="badge badge--alert">ALPHA</span>
</React.Fragment> // </React.Fragment>
} // }
/> // />
)} // )}
{!componentState.advancedMode && ( // {!componentState.advancedMode && (
<Button // <Button
button="primary" // button="primary"
label={__('Sync my wallet')} // label={__('Sync my wallet')}
onClick={() => syncApply(syncHash, syncData, componentState.newPassword)} // onClick={() => syncApply(syncHash, syncData, componentState.newPassword)}
/> // />
)} // )}
{(walletEncrypted || // {(walletEncrypted ||
syncApplyErrorMessage || // syncApplyErrorMessage ||
componentState.advancedMode || // componentState.advancedMode ||
componentState.showPasswordFields) && ( // componentState.showPasswordFields) && (
<React.Fragment> // <React.Fragment>
<FormField // <FormField
autoFocus // autoFocus
inputButton={ // inputButton={
<Button // <Button
icon={componentState.obscurePassword ? ICONS.EYE : ICONS.EYE_OFF} // icon={componentState.obscurePassword ? ICONS.EYE : ICONS.EYE_OFF}
button={'primary'} // button={'primary'}
onClick={() => // onClick={() =>
setComponentState({ ...componentState, obscurePassword: !componentState.obscurePassword }) // setComponentState({ ...componentState, obscurePassword: !componentState.obscurePassword })
} // }
/> // />
} // }
label={__('Password')} // label={__('Password')}
placeholder={__('Shh...')} // placeholder={__('Shh...')}
type={componentState.obscurePassword ? 'password' : 'text'} // type={componentState.obscurePassword ? 'password' : 'text'}
name="wallet-new-password" // name="wallet-new-password"
onChange={event => onChangeNewPassword(event)} // onChange={event => onChangeNewPassword(event)}
value={componentState.newPassword} // value={componentState.newPassword}
/> // />
<FormField // <FormField
error={componentState.passwordMatch === false ? 'No match' : false} // error={componentState.passwordMatch === false ? 'No match' : false}
label={__('Same Password')} // label={__('Same Password')}
placeholder={__('Your eyes only')} // placeholder={__('Your eyes only')}
type="password" // type="password"
name="wallet-new-password-confirm" // name="wallet-new-password-confirm"
onChange={event => onChangeNewPasswordConfirm(event)} // onChange={event => onChangeNewPasswordConfirm(event)}
value={componentState.newPasswordConfirm} // value={componentState.newPasswordConfirm}
/> // />
<FormField // <FormField
label={__('Remember Password')} // label={__('Remember Password')}
type="checkbox" // type="checkbox"
name="wallet-remember-password" // name="wallet-remember-password"
onChange={event => onChangeRememberPassword(event)} // onChange={event => onChangeRememberPassword(event)}
checked={componentState.rememberPassword} // checked={componentState.rememberPassword}
/> // />
</React.Fragment> // </React.Fragment>
)} // )}
{/* Confirmation */} // {/* Confirmation */}
{(walletEncrypted || componentState.advancedMode) && ( // {(walletEncrypted || componentState.advancedMode) && (
<React.Fragment> // <React.Fragment>
<FormField // <FormField
type="checkbox" // type="checkbox"
name="encrypt_enabled" // name="encrypt_enabled"
checked={componentState.encryptWallet} // checked={componentState.encryptWallet}
disabled={false} // disabled={false}
onChange={event => onChangeEncrypt(event)} // onChange={event => onChangeEncrypt(event)}
label={__('Encrypt Wallet')} // label={__('Encrypt Wallet')}
/> // />
<div className="card__subtitle--status"> // <div className="card__subtitle--status">
{__( // {__(
'If your password is lost, it cannot be recovered. You will not be able to access your wallet without a password.' // 'If your password is lost, it cannot be recovered. You will not be able to access your wallet without a password.'
)} // )}
</div> // </div>
<FormField // <FormField
error={componentState.understandError === true ? 'You must enter "I understand"' : false} // error={componentState.understandError === true ? 'You must enter "I understand"' : false}
label={__('Enter "I understand"')} // label={__('Enter "I understand"')}
placeholder={__('I understand')} // placeholder={__('I understand')}
type="text" // type="text"
name="wallet-understand" // name="wallet-understand"
onChange={event => onChangeUnderstandConfirm(event)} // onChange={event => onChangeUnderstandConfirm(event)}
/> // />
</React.Fragment> // </React.Fragment>
)} // )}
{componentState.failMessage && <div className="error-text">{__(componentState.failMessage)}</div>} // {componentState.failMessage && <div className="error-text">{__(componentState.failMessage)}</div>}
{(walletEncrypted || componentState.advancedMode || syncApplyErrorMessage) && ( // {(walletEncrypted || componentState.advancedMode || syncApplyErrorMessage) && (
<Submit // <Submit
disabled={!componentState.passwordMatch || (!componentState.enableSync && !componentState.encryptWallet)} // disabled={!componentState.passwordMatch || (!componentState.enableSync && !componentState.encryptWallet)}
label={componentState.failMessage ? __('Encrypting Wallet') : __('Apply')} // label={componentState.failMessage ? __('Encrypting Wallet') : __('Apply')}
/> // />
)} // )}
</Form> // </Form>
</section> // </section>
{/* Testing stuff and Diagnostics */} // {/* Testing stuff and Diagnostics */}
<section className="card card--section"> // <section className="card card--section">
<Button // <Button
button="primary" // button="primary"
label={__('Sync Apply')} // label={__('Sync Apply')}
onClick={() => syncApply(syncHash, syncData, componentState.newPassword)} // onClick={() => syncApply(syncHash, syncData, componentState.newPassword)}
/>{' '} // />{' '}
<Button button="primary" label={__('Check Sync')} onClick={() => checkSync()} />{' '} // <Button button="primary" label={__('Check Sync')} onClick={() => checkSync()} />{' '}
<Button button="primary" label={__('Setpass test')} onClick={() => setSavedPassword('testpass')} />{' '} // <Button button="primary" label={__('Setpass test')} onClick={() => setSavedPassword('testpass')} />{' '}
<Button // <Button
button="primary" // button="primary"
label={__('Getpass test')} // label={__('Getpass test')}
onClick={() => getSavedPassword().then(p => setComponentState({ ...componentState, newPassword: p }))} // onClick={() => getSavedPassword().then(p => setComponentState({ ...componentState, newPassword: p }))}
/>{' '} // />{' '}
<Button button="primary" label={__('Deletepass test')} onClick={() => deleteSavedPassword()} />{' '} // <Button button="primary" label={__('Deletepass test')} onClick={() => deleteAuthToken()} />{' '}
<p> // <p>
password:{' '} // password:{' '}
{componentState.newPassword // {componentState.newPassword
? componentState.newPassword // ? componentState.newPassword
: componentState.newPassword === '' // : componentState.newPassword === ''
? 'blankString' // ? 'blankString'
: 'null'} // : 'null'}
</p> // </p>
<p>encryptWallet: {String(componentState.encryptWallet)}</p> // <p>encryptWallet: {String(componentState.encryptWallet)}</p>
<p>enableSync: {String(componentState.enableSync)}</p> // <p>enableSync: {String(componentState.enableSync)}</p>
<p>syncApplyError: {String(syncApplyErrorMessage)}</p> // <p>syncApplyError: {String(syncApplyErrorMessage)}</p>
<p>Has Synced: {String(hasSyncedWallet)}</p> // <p>Has Synced: {String(hasSyncedWallet)}</p>
<p>getSyncPending: {String(getSyncIsPending)}</p> // <p>getSyncPending: {String(getSyncIsPending)}</p>
<p>syncEnabled: {String(syncEnabled)}</p> // <p>syncEnabled: {String(syncEnabled)}</p>
<p>syncHash: {syncHash ? syncHash.slice(0, 10) : 'null'}</p> // <p>syncHash: {syncHash ? syncHash.slice(0, 10) : 'null'}</p>
<p>syncData: {syncData ? syncData.slice(0, 10) : 'null'}</p> // <p>syncData: {syncData ? syncData.slice(0, 10) : 'null'}</p>
<p>walletEncrypted: {String(walletEncrypted)}</p> // <p>walletEncrypted: {String(walletEncrypted)}</p>
<p>emailRegistered: {String(isEmailVerified)}</p> // <p>emailRegistered: {String(isEmailVerified)}</p>
<p>hashChanged: {String(hashChanged)}</p> // <p>hashChanged: {String(hashChanged)}</p>
</section> // </section>
</React.Fragment> // </React.Fragment>
); // );
} // }
export default WalletSecurityAndSync; // export default WalletSecurityAndSync;

View file

@ -5,6 +5,7 @@ import Button from 'component/button';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import { Formik } from 'formik'; import { Formik } from 'formik';
import { validateSendTx } from 'util/form-validation'; import { validateSendTx } from 'util/form-validation';
import Card from 'component/common/card';
type DraftTransaction = { type DraftTransaction = {
address: string, address: string,
@ -36,71 +37,73 @@ class WalletSend extends React.PureComponent<Props> {
const { balance } = this.props; const { balance } = this.props;
return ( return (
<section className="card card--section"> <Card
<h2 className="card__title">{__('Send Credits')}</h2> title={__('Send Credits')}
<p className="card__subtitle">{__('Send LBC to your friends or favorite creators.')}</p> subtitle={__('Send LBC to your friends or favorite creators.')}
actions={
<Formik
initialValues={{
address: '',
amount: '',
}}
onSubmit={this.handleSubmit}
validate={validateSendTx}
render={({ values, errors, touched, handleChange, handleBlur, handleSubmit }) => (
<Form onSubmit={handleSubmit}>
<fieldset-group class="fieldset-group--smushed">
<FormField
type="number"
name="amount"
label={__('Amount')}
postfix={__('LBC')}
className="form-field--price-amount"
affixClass="form-field--fix-no-height"
min="0"
step="any"
placeholder="12.34"
onChange={handleChange}
onBlur={handleBlur}
value={values.amount}
/>
<Formik <FormField
initialValues={{ type="text"
address: '', name="address"
amount: '', placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs"
}} className="form-field--address"
onSubmit={this.handleSubmit} label={__('Recipient address')}
validate={validateSendTx} onChange={handleChange}
render={({ values, errors, touched, handleChange, handleBlur, handleSubmit }) => ( onBlur={handleBlur}
<Form onSubmit={handleSubmit}> value={values.address}
<fieldset-group class="fieldset-group--smushed"> />
<FormField </fieldset-group>
type="number" <div className="card__actions">
name="amount" <Button
label={__('Amount')} button="primary"
postfix={__('LBC')} type="submit"
className="form-field--price-amount" label={__('Send')}
affixClass="form-field--fix-no-height" disabled={
min="0" !values.address ||
step="any" !!Object.keys(errors).length ||
placeholder="12.34" !(parseFloat(values.amount) > 0.0) ||
onChange={handleChange} parseFloat(values.amount) === balance
onBlur={handleBlur} }
value={values.amount} />
/> {!!Object.keys(errors).length || (
<span className="error-text">
<FormField {(!!values.address && touched.address && errors.address) ||
type="text" (!!values.amount && touched.amount && errors.amount) ||
name="address" (parseFloat(values.amount) === balance &&
placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" __('Decrease amount to account for transaction fee')) ||
className="form-field--address" (parseFloat(values.amount) > balance && __('Not enough credits'))}
label={__('Recipient address')} </span>
onChange={handleChange} )}
onBlur={handleBlur} </div>
value={values.address} </Form>
/> )}
</fieldset-group> />
<div className="card__actions"> }
<Button />
button="primary"
type="submit"
label={__('Send')}
disabled={
!values.address ||
!!Object.keys(errors).length ||
!(parseFloat(values.amount) > 0.0) ||
parseFloat(values.amount) === balance
}
/>
{!!Object.keys(errors).length || (
<span className="error-text">
{(!!values.address && touched.address && errors.address) ||
(!!values.amount && touched.amount && errors.amount) ||
(parseFloat(values.amount) === balance && __('Decrease amount to account for transaction fee')) ||
(parseFloat(values.amount) > balance && __('Not enough credits'))}
</span>
)}
</div>
</Form>
)}
/>
</section>
); );
} }
} }

View file

@ -158,20 +158,9 @@ export const CLAIM_REWARD_FAILURE = 'CLAIM_REWARD_FAILURE';
export const CLAIM_REWARD_CLEAR_ERROR = 'CLAIM_REWARD_CLEAR_ERROR'; export const CLAIM_REWARD_CLEAR_ERROR = 'CLAIM_REWARD_CLEAR_ERROR';
export const FETCH_REWARD_CONTENT_COMPLETED = 'FETCH_REWARD_CONTENT_COMPLETED'; export const FETCH_REWARD_CONTENT_COMPLETED = 'FETCH_REWARD_CONTENT_COMPLETED';
// ShapeShift // Language
export const GET_SUPPORTED_COINS_START = 'GET_SUPPORTED_COINS_START'; export const DOWNLOAD_LANGUAGE_SUCCEEDED = 'DOWNLOAD_LANGUAGE_SUCCEEDED';
export const GET_SUPPORTED_COINS_SUCCESS = 'GET_SUPPORTED_COINS_SUCCESS'; export const DOWNLOAD_LANGUAGE_FAILED = 'DOWNLOAD_LANGUAGE_FAILED';
export const GET_SUPPORTED_COINS_FAIL = 'GET_SUPPORTED_COINS_FAIL';
export const GET_COIN_STATS_START = 'GET_COIN_STATS_START';
export const GET_COIN_STATS_SUCCESS = 'GET_COIN_STATS_SUCCESS';
export const GET_COIN_STATS_FAIL = 'GET_COIN_STATS_FAIL';
export const PREPARE_SHAPE_SHIFT_START = 'PREPARE_SHAPE_SHIFT_START';
export const PREPARE_SHAPE_SHIFT_SUCCESS = 'PREPARE_SHAPE_SHIFT_SUCCESS';
export const PREPARE_SHAPE_SHIFT_FAIL = 'PREPARE_SHAPE_SHIFT_FAIL';
export const GET_ACTIVE_SHIFT_START = 'GET_ACTIVE_SHIFT_START';
export const GET_ACTIVE_SHIFT_SUCCESS = 'GET_ACTIVE_SHIFT_SUCCESS';
export const GET_ACTIVE_SHIFT_FAIL = 'GET_ACTIVE_SHIFT_FAIL';
export const CLEAR_SHAPE_SHIFT = 'CLEAR_SHAPE_SHIFT';
// Subscriptions // Subscriptions
export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE'; export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';

View file

@ -1,4 +1,4 @@
export const AUTH = 'auth'; export const AUTH = 'signin';
export const BACKUP = 'backup'; export const BACKUP = 'backup';
export const CHANNEL = 'channel'; export const CHANNEL = 'channel';
export const DISCOVER = 'discover'; export const DISCOVER = 'discover';

View file

@ -36,7 +36,6 @@ import { PersistGate } from 'redux-persist/integration/react';
import 'scss/all.scss'; import 'scss/all.scss';
const APPPAGEURL = 'lbry://?'; const APPPAGEURL = 'lbry://?';
const COOKIE_EXPIRE_TIME = 60 * 60 * 24 * 365; // 1 year
// @if TARGET='app' // @if TARGET='app'
const { autoUpdater } = remote.require('electron-updater'); const { autoUpdater } = remote.require('electron-updater');
autoUpdater.logger = remote.require('electron-log'); autoUpdater.logger = remote.require('electron-log');
@ -78,18 +77,19 @@ Lbryio.setOverride(
throw new Error(__('auth_token is missing from response')); throw new Error(__('auth_token is missing from response'));
} }
const newAuthToken = response.auth_token; authToken = response.auth_token;
authToken = newAuthToken;
// @if TARGET='web' let date = new Date();
cookie.serialize('auth_token', authToken, { date.setFullYear(date.getFullYear() + 1);
maxAge: COOKIE_EXPIRE_TIME, document.cookie = cookie.serialize('auth_token', authToken, {
expires: date,
}); });
// @endif
// @if TARGET='app' // @if TARGET='app'
ipcRenderer.send('set-auth-token', authToken); ipcRenderer.send('set-auth-token', authToken);
// @endif // @endif
resolve();
resolve(authToken);
}); });
}) })
); );

View file

@ -12,19 +12,24 @@ const ModalFirstSubscription = (props: Props) => {
return ( return (
<Modal type="custom" isOpen contentLabel="Subscriptions 101" title={__('Subscriptions 101')}> <Modal type="custom" isOpen contentLabel="Subscriptions 101" title={__('Subscriptions 101')}>
<p>{__('You just subscribed to your first channel. Awesome!')}</p> <div className="section__subtitle">
<p>{__('A few quick things to know:')}</p> <p>{__('You just subscribed to your first channel. Awesome!')}</p>
<p> <p>{__('A few quick things to know:')}</p>
{__( </div>
'1) This app will automatically download new free content from channels you are subscribed to. You may configure this in Settings or on the Subscriptions page.' <ol className="section">
)} <li>
</p> {__(
<p> 'This app will automatically download new free content from channels you are subscribed to. You may configure this in Settings or on the Subscriptions page.'
{__( )}
'2) If we have your email address, we will send you notifications related to new content. You may configure these emails from the Help page.' {__('(Only available on the desktop app.)')}
)} </li>
</p> <li>
<div className="modal__buttons"> {__(
'If we have your email address, we will send you notifications related to new content. You may configure these emails from the Help page.'
)}
</li>
</ol>
<div className="section__actions">
<Button button="primary" onClick={closeModal} label={__('Got it')} /> <Button button="primary" onClick={closeModal} label={__('Got it')} />
</div> </div>
</Modal> </Modal>

View file

@ -1,7 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { Modal } from 'modal/modal'; import { Modal } from 'modal/modal';
import { deleteSavedPassword } from 'util/saved-passwords'; import { deleteAuthToken } from 'util/saved-passwords';
type Props = { type Props = {
closeModal: () => void, closeModal: () => void,
@ -18,7 +18,7 @@ class ModalPasswordUnsave extends React.PureComponent<Props> {
confirmButtonLabel={__('Forget')} confirmButtonLabel={__('Forget')}
abortButtonLabel={__('Nevermind')} abortButtonLabel={__('Nevermind')}
onConfirmed={() => onConfirmed={() =>
deleteSavedPassword().then(() => { deleteAuthToken().then(() => {
this.props.closeModal(); this.props.closeModal();
}) })
} }

View file

@ -6,8 +6,10 @@ import UserPhoneVerify from 'component/userPhoneVerify';
import { Redirect } from 'react-router'; import { Redirect } from 'react-router';
const LazyUserPhoneNew = React.lazy(() => const LazyUserPhoneNew = React.lazy(() =>
import(/* webpackChunkName: "userPhoneNew" */ import(
'component/userPhoneNew') /* webpackChunkName: "userPhoneNew" */
'component/userPhoneNew'
)
); );
type Props = { type Props = {

View file

@ -1,3 +1,4 @@
import * as PAGES from 'constants/pages';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
@ -11,7 +12,7 @@ const perform = (dispatch, ownProps) => ({
} = ownProps; } = ownProps;
const currentPath = pathname.split('/$/')[1]; const currentPath = pathname.split('/$/')[1];
dispatch(doHideModal()); dispatch(doHideModal());
history.push(`/$/auth?redirect=${currentPath}`); history.push(`/$/${PAGES.AUTH}?redirect=${currentPath}`);
}, },
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
}); });

View file

@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import { Modal } from 'modal/modal'; import { Modal } from 'modal/modal';
import Button from 'component/button'; import Button from 'component/button';
import { deleteSavedPassword } from 'util/saved-passwords'; import { deleteAuthToken } from 'util/saved-passwords';
type Props = { type Props = {
closeModal: () => void, closeModal: () => void,
@ -24,7 +24,7 @@ class ModalWalletDecrypt extends React.PureComponent<Props, State> {
const { props, state } = this; const { props, state } = this;
if (state.submitted && props.walletDecryptSucceded === true) { if (state.submitted && props.walletDecryptSucceded === true) {
deleteSavedPassword(); deleteAuthToken();
props.closeModal(); props.closeModal();
props.updateWalletStatus(); props.updateWalletStatus();
} }

View file

@ -1,40 +1,27 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import classnames from 'classnames';
import RewardSummary from 'component/rewardSummary'; import RewardSummary from 'component/rewardSummary';
import RewardTotal from 'component/rewardTotal'; import RewardTotal from 'component/rewardTotal';
import Page from 'component/page'; import Page from 'component/page';
import UnsupportedOnWeb from 'component/common/unsupported-on-web';
import UserEmail from 'component/userEmail'; import UserEmail from 'component/userEmail';
import InvitePage from 'page/invite'; import InviteNew from 'component/inviteNew';
// import YoutubeChannelList from 'component/youtubeChannelList'; import InviteList from 'component/inviteList';
type Props = { const AccountPage = () => {
// ytChannels: Array<any>,
};
const AccountPage = (props: Props) => {
// const { ytChannels } = props;
// const hasYoutubeChannels = Boolean(ytChannels.length);
return ( return (
<Page> <Page>
{/* @if TARGET='web' */} <div className="columns">
<UserEmail /> <div>
{/* @endif */} <RewardSummary />
<UnsupportedOnWeb /> <RewardTotal />
<div className={classnames({ 'card--disabled': IS_WEB })}> </div>
<div className="columns"> <div>
<UserEmail /> <UserEmail />
<div> <InviteNew />
<RewardSummary />
<RewardTotal />
</div>
</div> </div>
{/* {hasYoutubeChannels && <YoutubeChannelList />} */}
<InvitePage />
</div> </div>
<InviteList />
</Page> </Page>
); );
}; };
export default AccountPage; export default AccountPage;

View file

@ -1,11 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectFollowedTags } from 'lbry-redux'; import { selectFollowedTags } from 'lbry-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import DiscoverPage from './view'; import DiscoverPage from './view';
const select = state => ({ const select = state => ({
followedTags: selectFollowedTags(state), followedTags: selectFollowedTags(state),
subscribedChannels: selectSubscriptions(state), subscribedChannels: selectSubscriptions(state),
email: selectUserVerifiedEmail(state),
}); });
const perform = {}; const perform = {};

View file

@ -8,20 +8,20 @@ import Button from 'component/button';
type Props = { type Props = {
followedTags: Array<Tag>, followedTags: Array<Tag>,
email: string,
}; };
function DiscoverPage(props: Props) { function DiscoverPage(props: Props) {
const { followedTags } = props; const { followedTags, email } = props;
return ( return (
<Page> <Page>
{email && <TagsSelect showClose title={__('Customize Your Homepage')} />}
<ClaimListDiscover <ClaimListDiscover
hideCustomization={IS_WEB && !email}
personalView personalView
tags={followedTags.map(tag => tag.name)} tags={followedTags.map(tag => tag.name)}
meta={<Button button="link" label={__('Customize')} navigate={`/$/${PAGES.FOLLOWING}`} />} meta={<Button button="link" label={__('Customize')} navigate={`/$/${PAGES.FOLLOWING}`} />}
injectedItem={
<TagsSelect showClose title={__('Customize Your Homepage')} className="claim-preview--injected" />
}
/> />
</Page> </Page>
); );

View file

@ -124,7 +124,6 @@ class FilePage extends React.Component<Props> {
nsfw, nsfw,
supportOption, supportOption,
} = this.props; } = this.props;
// File info // File info
const { signing_channel: signingChannel } = claim; const { signing_channel: signingChannel } = claim;
const channelName = signingChannel && signingChannel.name; const channelName = signingChannel && signingChannel.name;

View file

@ -35,14 +35,15 @@ function FileListPublished(props: Props) {
<Paginate totalPages={Math.ceil(Number(urlTotal) / Number(PAGE_SIZE))} loading={fetching} /> <Paginate totalPages={Math.ceil(Number(urlTotal) / Number(PAGE_SIZE))} loading={fetching} />
</div> </div>
) : ( ) : (
<div className="main--empty"> <section className="main--empty">
<section className="card card--section"> <div className=" section--small">
<h2 className="card__title">{__("It looks like you haven't published anything to LBRY yet.")}</h2> <h2 className="section__title--large">{__('Nothing published to LBRY yet.')}</h2>
<div className="card__actions card__actions--center">
<div className="section__actions">
<Button button="primary" navigate="/$/publish" label={__('Publish something new')} /> <Button button="primary" navigate="/$/publish" label={__('Publish something new')} />
</div> </div>
</section> </div>
</div> </section>
)} )}
</Page> </Page>
); );

View file

@ -1,3 +1,4 @@
import * as PAGES from 'constants/pages';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doFetchAccessToken, selectAccessToken, selectUser } from 'lbryinc'; import { doFetchAccessToken, selectAccessToken, selectUser } from 'lbryinc';
import { selectDaemonSettings } from 'redux/selectors/settings'; import { selectDaemonSettings } from 'redux/selectors/settings';
@ -10,7 +11,7 @@ const select = state => ({
}); });
const perform = (dispatch, ownProps) => ({ const perform = (dispatch, ownProps) => ({
doAuth: () => ownProps.history.push('/$/auth?redirect=help'), doAuth: () => ownProps.history.push(`/$/${PAGES.AUTH}?redirect=help`),
fetchAccessToken: () => dispatch(doFetchAccessToken()), fetchAccessToken: () => dispatch(doFetchAccessToken()),
}); });

View file

@ -3,6 +3,7 @@ import React from 'react';
import BusyIndicator from 'component/common/busy-indicator'; import BusyIndicator from 'component/common/busy-indicator';
import InviteNew from 'component/inviteNew'; import InviteNew from 'component/inviteNew';
import InviteList from 'component/inviteList'; import InviteList from 'component/inviteList';
import Page from 'component/page';
type Props = { type Props = {
isPending: boolean, isPending: boolean,
@ -26,16 +27,17 @@ class InvitePage extends React.PureComponent<Props> {
const { isPending, isFailed } = this.props; const { isPending, isFailed } = this.props;
return ( return (
<div> <Page>
{isPending && <BusyIndicator message={__('Checking your invite status')} />} {isPending && <BusyIndicator message={__('Checking your invite status')} />}
{!isPending && isFailed && <span className="empty">{__('Failed to retrieve invite status.')}</span>} {!isPending && isFailed && <span className="empty">{__('Failed to retrieve invite status.')}</span>}
{!isPending && !isFailed && ( {!isPending && !isFailed && (
<React.Fragment> <React.Fragment>
{' '}
<InviteNew /> <InviteNew />
<InviteList /> <InviteList />
</React.Fragment> </React.Fragment>
)} )}
</div> </Page>
); );
} }
} }

View file

@ -8,7 +8,6 @@ import Button from 'component/button';
import Page from 'component/page'; import Page from 'component/page';
import classnames from 'classnames'; import classnames from 'classnames';
import { rewards as REWARD_TYPES } from 'lbryinc'; import { rewards as REWARD_TYPES } from 'lbryinc';
import UnsupportedOnWeb from 'component/common/unsupported-on-web';
type Props = { type Props = {
doAuth: () => void, doAuth: () => void,
@ -31,7 +30,6 @@ class RewardsPage extends PureComponent<Props> {
componentDidMount() { componentDidMount() {
this.props.fetchRewards(); this.props.fetchRewards();
} }
renderPageHeader() { renderPageHeader() {
const { user, daemonSettings } = this.props; const { user, daemonSettings } = this.props;
@ -48,7 +46,7 @@ class RewardsPage extends PureComponent<Props> {
</p> </p>
<Button <Button
navigate={`/$/${PAGES.AUTH}/signin?redirect=rewards`} navigate={`/$/${PAGES.AUTH}?redirect=/$/${PAGES.REWARDS}`}
button="primary" button="primary"
label={__('Unlock Rewards')} label={__('Unlock Rewards')}
/> />
@ -95,7 +93,7 @@ class RewardsPage extends PureComponent<Props> {
renderUnclaimedRewards() { renderUnclaimedRewards() {
const { fetching, rewards, user, daemonSettings, claimed } = this.props; const { fetching, rewards, user, daemonSettings, claimed } = this.props;
if (daemonSettings && !daemonSettings.share_usage_data) { if (!IS_WEB && daemonSettings && !daemonSettings.share_usage_data) {
return ( return (
<section className="card card--section"> <section className="card card--section">
<h2 className="card__title">{__('Disabled')}</h2> <h2 className="card__title">{__('Disabled')}</h2>
@ -150,7 +148,6 @@ class RewardsPage extends PureComponent<Props> {
render() { render() {
return ( return (
<Page> <Page>
{IS_WEB && <UnsupportedOnWeb />}
{this.renderPageHeader()} {this.renderPageHeader()}
{this.renderUnclaimedRewards()} {this.renderUnclaimedRewards()}
{<RewardListClaimed />} {<RewardListClaimed />}

View file

@ -20,10 +20,11 @@ const select = (state, props) => {
try { try {
uri = normalizeURI(path); uri = normalizeURI(path);
} catch (e) { } catch (e) {
// Probably an old channel url, redirect to the vanity channel
// @routinghax
const match = path.match(/[#/:]/); const match = path.match(/[#/:]/);
if (match && match.index) {
if (path === '$/') {
props.history.replace(`/`);
} else if (!path.startsWith('$/') && match && match.index) {
uri = `lbry://${path.slice(0, match.index)}`; uri = `lbry://${path.slice(0, match.index)}`;
props.history.replace(`/${path.slice(0, match.index)}`); props.history.replace(`/${path.slice(0, match.index)}`);
} }

View file

@ -1,15 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectEmailToVerify, selectUser } from 'lbryinc';
import { selectMyChannelClaims } from 'lbry-redux';
import SignUpPage from './view'; import SignUpPage from './view';
const select = state => ({ const select = () => ({});
email: selectEmailToVerify(state), const perform = () => ({});
user: selectUser(state),
channels: selectMyChannelClaims(state),
});
export default connect( export default connect(
select, select,
null perform
)(SignUpPage); )(SignUpPage);

View file

@ -1,42 +1,12 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import UserSignIn from 'component/userSignIn'; import UserSignIn from 'component/userSignIn';
import UserFirstChannel from 'component/userFirstChannel';
import UserVerify from 'component/userVerify';
import Page from 'component/page'; import Page from 'component/page';
type Props = { export default function SignInPage() {
user: ?User,
channels: ?Array<ChannelClaim>,
location: { search: string },
history: { replace: string => void },
};
export default function SignInPage(props: Props) {
const { user, channels, location, history } = props;
const { search } = location;
const urlParams = new URLSearchParams(search);
const redirect = urlParams.get('redirect');
const hasVerifiedEmail = user && user.has_verified_email;
const rewardsApproved = user && user.is_reward_approved;
const channelCount = channels ? channels.length : 0;
const showWelcome = !hasVerifiedEmail || channelCount === 0;
if (rewardsApproved && channelCount > 0) {
history.replace(redirect ? `/$/${redirect}` : '/');
}
return ( return (
<Page fullscreen className="main--auth-page"> <Page fullscreen className="main--auth-page">
{showWelcome && ( <UserSignIn />
<div className="columns">
{!hasVerifiedEmail && <UserSignIn />}
{hasVerifiedEmail && channelCount === 0 && <UserFirstChannel />}
<div style={{ width: '100%', height: '20rem', borderRadius: 20, backgroundColor: '#ffc7e6' }} />
</div>
)}
{hasVerifiedEmail && channelCount > 0 && <UserVerify />}
</Page> </Page>
); );
} }

View file

@ -1,9 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import classnames from 'classnames';
import TransactionList from 'component/transactionList'; import TransactionList from 'component/transactionList';
import Page from 'component/page'; import Page from 'component/page';
import UnsupportedOnWeb from 'component/common/unsupported-on-web';
type Props = { type Props = {
fetchMyClaims: () => void, fetchMyClaims: () => void,
@ -26,12 +24,7 @@ class TransactionHistoryPage extends React.PureComponent<Props> {
return ( return (
<Page> <Page>
{IS_WEB && <UnsupportedOnWeb />} <section className="card">
<section
className={classnames('card', {
'card--disabled': IS_WEB,
})}
>
<TransactionList <TransactionList
transactions={filteredTransactionPage} transactions={filteredTransactionPage}
transactionCount={filteredTransactionsCount} transactionCount={filteredTransactionsCount}

View file

@ -6,6 +6,7 @@ import { ipcRenderer, remote } from 'electron';
import path from 'path'; import path from 'path';
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import * as PAGES from 'constants/pages';
import { import {
Lbry, Lbry,
doBalanceSubscribe, doBalanceSubscribe,
@ -13,6 +14,8 @@ import {
doError, doError,
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectClaimIsMine, makeSelectClaimIsMine,
doPopulateUserSettings,
doFetchChannelListMine,
} from 'lbry-redux'; } from 'lbry-redux';
import Native from 'native'; import Native from 'native';
import { doFetchDaemonSettings } from 'redux/actions/settings'; import { doFetchDaemonSettings } from 'redux/actions/settings';
@ -28,10 +31,12 @@ import {
selectUpgradeTimer, selectUpgradeTimer,
selectModal, selectModal,
} from 'redux/selectors/app'; } from 'redux/selectors/app';
import { doAuthenticate } from 'lbryinc'; import { Lbryio, doAuthenticate } from 'lbryinc';
import { lbrySettings as config, version as appVersion } from 'package.json'; import { lbrySettings as config, version as appVersion } from 'package.json';
import { push } from 'connected-react-router'; import { push } from 'connected-react-router';
import analytics from 'analytics'; import analytics from 'analytics';
import { deleteAuthToken } from 'util/saved-passwords';
import cookie from 'cookie';
// @if TARGET='app' // @if TARGET='app'
const { autoUpdater } = remote.require('electron-updater'); const { autoUpdater } = remote.require('electron-updater');
@ -322,13 +327,12 @@ export function doAlertError(errorList) {
export function doDaemonReady() { export function doDaemonReady() {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
dispatch(doAuthenticate(appVersion)); dispatch(doAuthenticate(appVersion));
dispatch({ type: ACTIONS.DAEMON_READY }); dispatch({ type: ACTIONS.DAEMON_READY });
// @if TARGET='app' // @if TARGET='app'
dispatch(doFetchDaemonSettings());
dispatch(doBalanceSubscribe()); dispatch(doBalanceSubscribe());
dispatch(doFetchDaemonSettings());
dispatch(doFetchFileInfosAndPublishedClaims()); dispatch(doFetchFileInfosAndPublishedClaims());
if (!selectIsUpgradeSkipped(state)) { if (!selectIsUpgradeSkipped(state)) {
dispatch(doCheckUpgradeAvailable()); dispatch(doCheckUpgradeAvailable());
@ -414,7 +418,7 @@ export function doConditionalAuthNavigate(newSession) {
const modal = selectModal(state); const modal = selectModal(state);
if (newSession || (modal && modal.id !== MODALS.EMAIL_COLLECTION)) { if (newSession || (modal && modal.id !== MODALS.EMAIL_COLLECTION)) {
dispatch(push('/$/auth')); dispatch(push(`/$/${PAGES.AUTH}`));
} }
}; };
} }
@ -439,3 +443,32 @@ export function doAnalyticsView(uri, timeToStart) {
return analytics.apiLogView(uri, outpoint, claimId, timeToStart); return analytics.apiLogView(uri, outpoint, claimId, timeToStart);
}; };
} }
export function doSignIn() {
return (dispatch, getState) => {
// The balance is subscribed to on launch for desktop
// @if TARGET='web'
const { auth_token: authToken } = cookie.parse(document.cookie);
Lbry.setApiHeader('X-Lbry-Auth-Token', authToken);
dispatch(doBalanceSubscribe());
dispatch(doCheckSubscriptionsInit());
dispatch(doFetchChannelListMine());
// @endif
Lbryio.call('user_settings', 'get').then(settings => {
dispatch(doPopulateUserSettings(settings));
});
};
}
export function doSignOut() {
return dispatch => {
deleteAuthToken()
.then(window.persistor.purge)
.then(() => {
location.reload();
})
.catch(() => location.reload());
};
}

View file

@ -1,5 +1,6 @@
// @flow // @flow
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import { parseURI, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
import { VIEW_ALL } from 'constants/subscriptions'; import { VIEW_ALL } from 'constants/subscriptions';
import { handleActions } from 'util/redux-utils'; import { handleActions } from 'util/redux-utils';
@ -12,6 +13,7 @@ const defaultState: SubscriptionState = {
loadingSuggested: false, loadingSuggested: false,
firstRunCompleted: false, firstRunCompleted: false,
showSuggestedSubs: false, showSuggestedSubs: false,
enabledChannelNotifications: [],
}; };
export default handleActions( export default handleActions(
@ -134,6 +136,39 @@ export default handleActions(
...state, ...state,
loadingSuggested: false, loadingSuggested: false,
}), }),
[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: (
state: SubscriptionState,
action: { data: { subscriptions: ?Array<string> } }
) => {
const { subscriptions } = action.data;
let newSubscriptions;
if (!subscriptions) {
newSubscriptions = state.subscriptions;
} else {
const parsedSubscriptions = subscriptions.map(uri => {
const { channelName } = parseURI(uri);
return {
uri,
channelName: `@${channelName}`,
};
});
if (!state.subscriptions || !state.subscriptions.length) {
newSubscriptions = parsedSubscriptions;
} else {
const map = {};
newSubscriptions = parsedSubscriptions.concat(state.subscriptions).filter(sub => {
return map[sub.uri] ? false : (map[sub.uri] = true);
}, {});
}
}
return {
...state,
subscriptions: newSubscriptions,
};
},
}, },
defaultState defaultState
); );

View file

@ -22,6 +22,7 @@
@import 'component/file-render'; @import 'component/file-render';
@import 'component/form-field'; @import 'component/form-field';
@import 'component/header'; @import 'component/header';
@import 'component/icon';
@import 'component/item-list'; @import 'component/item-list';
@import 'component/main'; @import 'component/main';
@import 'component/markdown-editor'; @import 'component/markdown-editor';
@ -34,6 +35,7 @@
@import 'component/pagination'; @import 'component/pagination';
@import 'component/placeholder'; @import 'component/placeholder';
@import 'component/search'; @import 'component/search';
@import 'component/section';
@import 'component/snack-bar'; @import 'component/snack-bar';
@import 'component/spinner'; @import 'component/spinner';
@import 'component/splash'; @import 'component/splash';

View file

@ -15,9 +15,16 @@
} }
.button--primary { .button--primary {
background-color: $lbry-teal-5;
&:hover { &:hover {
background-color: $lbry-teal-4; background-color: $lbry-teal-4;
} }
&:disabled {
opacity: 0.5;
color: white !important;
}
} }
// Play/View button that is overlayed ontop of the video player // Play/View button that is overlayed ontop of the video player

View file

@ -55,6 +55,12 @@
margin-right: auto; margin-right: auto;
} }
.card--inline {
box-shadow: none;
border-radius: none;
margin-bottom: 0;
}
// C A R D // C A R D
// A C T I O N S // A C T I O N S
@ -195,12 +201,6 @@
} }
} }
.card__title--large {
@extend .card__title;
font-weight: 700;
font-size: var(--font-heading);
}
.card__title--between { .card__title--between {
@extend .card__title; @extend .card__title;
justify-content: space-between; justify-content: space-between;
@ -214,3 +214,29 @@
opacity: 0.5; opacity: 0.5;
pointer-events: none; pointer-events: none;
} }
.card__header {
padding: var(--spacing-large);
}
.card__body {
padding: var(--spacing-large);
padding-top: 0;
}
.card__main-actions {
padding: var(--spacing-large);
background-color: rgba($lbry-blue-1, 0.1);
color: darken($lbry-gray-5, 15%);
font-size: var(--font-body);
[data-mode='dark'] & {
background-color: var(--dm-color-04);
color: inherit;
}
}
.card__body--with-icon,
.card__main-actions--with-icon {
padding-left: 7.5rem;
}

View file

@ -58,6 +58,7 @@ select,
textarea { textarea {
border-color: lighten($lbry-black, 20%); border-color: lighten($lbry-black, 20%);
border-radius: var(--input-border-radius); border-radius: var(--input-border-radius);
background-color: $lbry-white;
border-width: 1px; border-width: 1px;
} }
@ -67,6 +68,7 @@ fieldset-section {
label { label {
width: auto; width: auto;
text-transform: none; text-transform: none;
color: lighten($lbry-black, 20%);
} }
} }
@ -90,7 +92,6 @@ radio-element {
} }
label { label {
color: lighten($lbry-black, 20%);
margin-bottom: 0; margin-bottom: 0;
margin-left: var(--spacing-miniscule); margin-left: var(--spacing-miniscule);
font-size: var(--font-body); font-size: var(--font-body);
@ -184,9 +185,12 @@ fieldset-group {
height: var(--input-height); height: var(--input-height);
padding-right: 0; padding-right: 0;
border: 1px solid; border: 1px solid;
border-top-left-radius: var(--input-border-radius);
border-bottom-left-radius: var(--input-border-radius);
border-right: 0; border-right: 0;
border-color: $lbry-black; border-color: $lbry-black;
color: $lbry-gray-4; color: $lbry-gray-4;
background-color: $lbry-white;
[data-mode='dark'] & { [data-mode='dark'] & {
border-color: $lbry-gray-4; border-color: $lbry-gray-4;
@ -275,7 +279,6 @@ fieldset-section {
max-width: 12em; max-width: 12em;
background-position: 95% center; background-position: 95% center;
background-size: 1.2rem; background-size: 1.2rem;
background-color: $lbry-white;
[data-mode='dark'] & { [data-mode='dark'] & {
background-color: transparent; background-color: transparent;
@ -305,7 +308,6 @@ fieldset-section {
background-color: rgba($lbry-gray-1, 0.5); background-color: rgba($lbry-gray-1, 0.5);
border: 1px solid $lbry-gray-1; border: 1px solid $lbry-gray-1;
color: $lbry-gray-5; color: $lbry-gray-5;
flex: 1;
padding: 0.2rem 0.75rem; padding: 0.2rem 0.75rem;
text-overflow: ellipsis; text-overflow: ellipsis;
user-select: text; user-select: text;
@ -324,6 +326,10 @@ fieldset-section {
margin-bottom: var(--spacing-large); margin-bottom: var(--spacing-large);
} }
.form-field--short {
width: 25em;
}
.form-field--price-amount { .form-field--price-amount {
width: 7em; width: 7em;
} }

View file

@ -54,11 +54,15 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
.button { > .button:only-child {
margin-left: auto; margin-left: auto;
} }
} }
.header__menu--small {
width: auto;
}
.header__navigation-arrows { .header__navigation-arrows {
display: flex; display: flex;
margin-right: var(--spacing-small); margin-right: var(--spacing-small);

View file

@ -0,0 +1,18 @@
.icon__wrapper {
@extend .card__subtitle;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
margin-top: 0;
margin-bottom: 0;
height: 3.5rem;
width: 3.5rem;
border-radius: calc(3.5rem / 2);
position: relative;
.icon {
position: absolute;
stroke: $lbry-gray-5;
}
}

View file

@ -19,7 +19,8 @@
} }
.main { .main {
width: calc(100% - var(--side-nav-width) - var(--spacing-main-padding)); position: relative;
width: calc(100% - var(--side-nav-width) - var(--spacing-large));
@media (max-width: 600px) { @media (max-width: 600px) {
width: 100%; width: 100%;
@ -43,7 +44,7 @@
.main--auth-page { .main--auth-page {
max-width: 60rem; max-width: 60rem;
margin-top: calc(var(--spacing-main-padding) * 2); margin-top: var(--spacing-main-padding);
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
@ -63,22 +64,15 @@
background-color: var(--color-background); background-color: var(--color-background);
} }
.main--fullscreen { .main--contained {
width: 100vw; max-width: 35rem;
height: 100vh; min-width: 25rem;
z-index: 9999; margin: auto;
background-color: $lbry-white; margin-top: 5rem;
position: absolute; }
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: 0;
margin-top: 0;
* { .main--full-width {
z-index: 10000; width: 100%;
}
} }
.main__status { .main__status {

View file

@ -9,9 +9,22 @@
.navigation--placeholder { .navigation--placeholder {
@extend .navigation; @extend .navigation;
height: 80vh; padding: 2rem 1.5rem;
background-color: $lbry-blue-1;
border-radius: var(--card-radius); border-radius: var(--card-radius);
font-size: var(--font-title);
font-weight: 600;
color: $lbry-white;
position: relative;
background-color: $lbry-black;
h2 {
font-size: 2rem;
font-weight: 600;
}
p {
font-size: 1.5rem;
}
} }
.navigation-links { .navigation-links {

View file

@ -2,11 +2,11 @@
table, table,
.table { .table {
background-color: transparent;
margin: var(--spacing-small) 0; margin: var(--spacing-small) 0;
padding-top: var(--spacing-small);
[data-mode='dark'] & { [data-mode='dark'] & {
background-color: transparent;
th { th {
border-bottom: 2px solid $lbry-white; border-bottom: 2px solid $lbry-white;
} }

View file

@ -15,8 +15,11 @@ $main: $lbry-teal-5;
.tags--remove { .tags--remove {
@extend .tags; @extend .tags;
@extend .ul--no-style;
margin-bottom: var(--spacing-large); .tag {
margin-top: 0;
margin-bottom: var(--spacing-small);
}
} }
.tags--vertical { .tags--vertical {
@ -31,8 +34,24 @@ $main: $lbry-teal-5;
margin: var(--spacing-large) 0; margin: var(--spacing-large) 0;
} }
.tags__empty-message { .tags__input-wrapper {
margin-top: var(--spacing-medium); display: flex;
// Nested style needed for more specificity
.tag__input {
@extend .tag--remove;
border: 1px dashed;
border-color: $lbry-teal-5;
background-color: mix($lbry-teal-1, $lbry-white, 10%);
height: auto;
padding: calc(var(--spacing-miniscule) - 1px) var(--spacing-small);
margin-top: -2px;
border: 1px dashed lighten($lbry-teal-5, 10%);
::placeholder {
color: black;
}
}
} }
.tag { .tag {
@ -68,7 +87,11 @@ $main: $lbry-teal-5;
} }
.tag--add { .tag--add {
background-color: lighten($lbry-teal-5, 60%); background-color: $lbry-teal-5;
color: $lbry-white;
.icon {
stroke: $lbry-white;
}
&.tag--mature { &.tag--mature {
@extend .badge--mature; @extend .badge--mature;

View file

@ -6,7 +6,7 @@
align-items: center; align-items: center;
position: relative; position: relative;
z-index: 1; z-index: 1;
margin-right: var(--spacing-main-padding); margin-right: var(--spacing-large);
font-size: var(--font-label); font-size: var(--font-label);
@media (max-width: 600px) { @media (max-width: 600px) {

View file

@ -87,6 +87,7 @@
.menu__title, .menu__title,
.menu__link { .menu__link {
color: lighten($lbry-black, 20%); color: lighten($lbry-black, 20%);
color: $lbry-gray-5;
.icon { .icon {
stroke: $lbry-gray-5; stroke: $lbry-gray-5;
@ -104,7 +105,7 @@
.menu__link { .menu__link {
.icon { .icon {
margin-right: var(--spacing-small); margin-right: var(--spacing-small);
stroke: $lbry-gray-5; stroke: $lbry-gray-4;
} }
} }

View file

@ -0,0 +1,77 @@
.section {
margin-top: var(--spacing-medium);
&:first-of-type {
margin-top: 0;
}
}
.section--large {
margin-bottom: var(--spacing-main-padding);
}
.section--small {
max-width: 30rem;
}
.section__flex {
display: flex;
align-items: flex-start;
& > :first-child {
margin-right: var(--spacing-large);
}
}
.section__title {
text-align: left;
font-size: var(--font-section-title);
font-weight: 300;
}
.section__title--large {
@extend .section__title;
font-weight: 700;
font-size: var(--font-heading);
}
.section__subtitle {
color: $lbry-gray-5;
}
.section__divider {
padding: var(--spacing-large) 0;
display: flex;
flex-direction: column;
p {
color: $lbry-gray-4;
text-align: center;
font-size: var(--font-title);
font-weight: 500;
background-color: var(--color-background);
transform: translateY(-50%);
padding: 0 var(--spacing-large);
display: inline-block;
margin: auto;
}
}
.section__actions {
display: flex;
align-items: center;
margin-top: var(--spacing-medium);
font-size: var(--font-body);
&:only-child {
margin-top: 0;
}
> *:not(:last-child) {
margin-right: var(--spacing-medium);
}
}
.section__body {
margin-top: var(--spacing-large);
}

View file

@ -25,7 +25,7 @@ body {
font-size: 1rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
background-color: mix($lbry-white, $lbry-gray-1, 70%); background-color: var(--color-background);
[data-mode='dark'] & { [data-mode='dark'] & {
background-color: var(--dm-color-08); background-color: var(--dm-color-08);
@ -48,13 +48,15 @@ p {
ul, ul,
ol { ol {
margin-bottom: var(--spacing-large);
} }
ul { ul,
ol {
list-style: initial; list-style: initial;
margin-bottom: var(--spacing-large);
li { li {
list-style-position: outside;
margin: var(--spacing-medium) 0; margin: var(--spacing-medium) 0;
} }
} }

View file

@ -34,10 +34,12 @@ $large-breakpoint: 1921px;
--font-label: 0.9em; --font-label: 0.9em;
--font-subtext: 1em; --font-subtext: 1em;
--font-title: 1.6em; --font-title: 1.6em;
--font-section-title: 2rem;
--font-heading: 3rem; --font-heading: 3rem;
// Color // Color
--color-background: #270f34; --color-background: #f7f7f7;
--color-background--splash: #270f34;
// Dark Mode // Dark Mode
--dm-color-01: #ddd; --dm-color-01: #ddd;

View file

@ -8,6 +8,8 @@ import thunk from 'redux-thunk';
import { createHashHistory, createBrowserHistory } from 'history'; import { createHashHistory, createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router'; import { routerMiddleware } from 'connected-react-router';
import createRootReducer from './reducers'; import createRootReducer from './reducers';
import { Lbryio } from 'lbryinc';
import isEqual from 'util/deep-equal';
function isFunction(object) { function isFunction(object) {
return typeof object === 'function'; return typeof object === 'function';
@ -53,13 +55,12 @@ const whiteListedReducers = [
// @if TARGET='app' // @if TARGET='app'
'publish', 'publish',
'wallet', 'wallet',
'tags',
// 'fileInfo', // 'fileInfo',
// @endif // @endif
'content', 'content',
'subscriptions',
'app', 'app',
'search', 'search',
'tags',
'blocked', 'blocked',
'settings', 'settings',
]; ];
@ -69,6 +70,7 @@ const transforms = [
walletFilter, walletFilter,
fileInfoFilter, fileInfoFilter,
blockedFilter, blockedFilter,
tagsFilter,
// @endif // @endif
appFilter, appFilter,
searchFilter, searchFilter,
@ -106,6 +108,29 @@ const store = createStore(
composeEnhancers(applyMiddleware(...middleware)) composeEnhancers(applyMiddleware(...middleware))
); );
let currentPayload;
store.subscribe(() => {
const state = store.getState();
const subscriptions = state.subscriptions.subscriptions.map(({ uri }) => uri);
const tags = state.tags.followedTags;
const authToken = state.user.accessToken;
const newPayload = {
version: '0.1',
shared: {
subscriptions,
tags,
},
};
if (!isEqual(newPayload, currentPayload)) {
currentPayload = newPayload;
if (authToken) {
Lbryio.call('user_settings', 'set', { settings: newPayload });
}
}
});
const persistor = persistStore(store); const persistor = persistStore(store);
window.persistor = persistor; window.persistor = persistor;

117
src/ui/util/deep-equal.js Normal file
View file

@ -0,0 +1,117 @@
/* eslint-disable */
// underscore's deep equal function
// https://github.com/jashkenas/underscore/blob/master/underscore.js#L1189
export default function isEqual(a, b, aStack, bStack) {
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
if (a === b) return a !== 0 || 1 / a === 1 / b;
// `null` or `undefined` only equal to itself (strict comparison).
if (a == null || b == null) return false;
// `NaN`s are equivalent, but non-reflexive.
if (a !== a) return b !== b;
// Exhaust primitive checks
var type = typeof a;
if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
return deepEq(a, b, aStack, bStack);
}
function deepEq(a, b, aStack, bStack) {
// Compare `[[Class]]` names.
var className = toString.call(a);
if (className !== toString.call(b)) return false;
switch (className) {
// Strings, numbers, regular expressions, dates, and booleans are compared by value.
case '[object RegExp]':
// RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i')
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
return '' + a === '' + b;
case '[object Number]':
// `NaN`s are equivalent, but non-reflexive.
// Object(NaN) is equivalent to NaN.
if (+a !== +a) return +b !== +b;
// An `egal` comparison is performed for other numeric values.
return +a === 0 ? 1 / +a === 1 / b : +a === +b;
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
// millisecond representations. Note that invalid dates with millisecond representations
// of `NaN` are not equivalent.
return +a === +b;
case '[object Symbol]':
return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
}
var areArrays = className === '[object Array]';
if (!areArrays) {
if (typeof a != 'object' || typeof b != 'object') return false;
// Objects with different constructors are not equivalent, but `Object`s or `Array`s
// from different frames are.
var aCtor = a.constructor,
bCtor = b.constructor;
if (
aCtor !== bCtor &&
!(
typeof aCtor === 'function' &&
aCtor instanceof aCtor &&
typeof bCtor === 'function' &&
bCtor instanceof bCtor
) &&
('constructor' in a && 'constructor' in b)
) {
return false;
}
}
// Assume equality for cyclic structures. The algorithm for detecting cyclic
// structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
// Initializing stack of traversed objects.
// It's done here since we only need them for objects and arrays comparison.
aStack = aStack || [];
bStack = bStack || [];
var length = aStack.length;
while (length--) {
// Linear search. Performance is inversely proportional to the number of
// unique nested structures.
if (aStack[length] === a) return bStack[length] === b;
}
// Add the first object to the stack of traversed objects.
aStack.push(a);
bStack.push(b);
// Recursively compare objects and arrays.
if (areArrays) {
// Compare array lengths to determine if a deep comparison is necessary.
length = a.length;
if (length !== b.length) return false;
// Deep compare the contents, ignoring non-numeric properties.
while (length--) {
if (!isEqual(a[length], b[length], aStack, bStack)) return false;
}
} else {
// Deep compare objects.
var keys = Object.keys(a),
key;
length = keys.length;
// Ensure that both objects contain the same number of properties before comparing deep equality.
if (Object.keys(b).length !== length) return false;
while (length--) {
// Deep compare each member
key = keys[length];
if (!(has(b, key) && isEqual(a[key], b[key], aStack, bStack))) return false;
}
}
// Remove the first object from the stack of traversed objects.
aStack.pop();
bStack.pop();
return true;
}
function has(obj, path) {
return obj != null && hasOwnProperty.call(obj, path);
}
/* eslint-enable */

View file

@ -26,13 +26,19 @@ export const getSavedPassword = () => {
); );
}; };
export const deleteSavedPassword = () => { export const deleteAuthToken = () => {
return new Promise( return new Promise(
resolve => { resolve => {
ipcRenderer.once('delete-password-response', (event, success) => { // @if TARGET='app'
resolve(success); ipcRenderer.once('delete-auth-token-response', (event, success) => {
resolve();
}); });
ipcRenderer.send('delete-password'); ipcRenderer.send('delete-auth-token');
// @endif;
// @if TARGET='web'
document.cookie = 'auth_token= ; expires = Thu, 01 Jan 1970 00:00:00 GMT';
resolve();
// @endif
}, },
reject => { reject => {
reject(false); reject(false);

View file

@ -1,4 +1,5 @@
{ {
"404": "404",
"Thumbnail Image": "Thumbnail Image", "Thumbnail Image": "Thumbnail Image",
"OK": "OK", "OK": "OK",
"Cancel": "Cancel", "Cancel": "Cancel",
@ -708,6 +709,6 @@
"Tip %amount% LBC": "Tip %amount% LBC", "Tip %amount% LBC": "Tip %amount% LBC",
"Not enough credits": "Not enough credits", "Not enough credits": "Not enough credits",
"You have %credit_amount% in unclaimed rewards.": "You have %credit_amount% in unclaimed rewards.", "You have %credit_amount% in unclaimed rewards.": "You have %credit_amount% in unclaimed rewards.",
"You haven't downloaded anything from LBRY yet.": "You haven't downloaded anything from LBRY yet.", "URI does not include name.": "URI does not include name.",
"Explore new content": "Explore new content" "to fix it. If that doesn't work, press CMD/CTRL-R to reset to the homepage.": "to fix it. If that doesn't work, press CMD/CTRL-R to reset to the homepage."
} }

124
yarn.lock
View file

@ -842,18 +842,6 @@
ajv "^6.1.0" ajv "^6.1.0"
ajv-keywords "^3.1.0" ajv-keywords "^3.1.0"
"@emotion/is-prop-valid@^0.7.3":
version "0.7.3"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz#a6bf4fa5387cbba59d44e698a4680f481a8da6cc"
integrity sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==
dependencies:
"@emotion/memoize" "0.7.1"
"@emotion/memoize@0.7.1":
version "0.7.1"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.1.tgz#e93c13942592cf5ef01aa8297444dc192beee52f"
integrity sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==
"@exponent/electron-cookies@^2.0.0": "@exponent/electron-cookies@^2.0.0":
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/@exponent/electron-cookies/-/electron-cookies-2.0.0.tgz#4cf8dcf851454036cc524c40e9e482fc4e23f2d9" resolved "https://registry.yarnpkg.com/@exponent/electron-cookies/-/electron-cookies-2.0.0.tgz#4cf8dcf851454036cc524c40e9e482fc4e23f2d9"
@ -904,21 +892,6 @@
universal-user-agent "^2.0.0" universal-user-agent "^2.0.0"
url-template "^2.0.8" url-template "^2.0.8"
"@popmotion/easing@^1.0.1":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@popmotion/easing/-/easing-1.0.2.tgz#17d925c45b4bf44189e5a38038d149df42d8c0b4"
integrity sha512-IkdW0TNmRnWTeWI7aGQIVDbKXPWHVEYdGgd5ZR4SH/Ty/61p63jCjrPxX1XrR7IGkl08bjhJROStD7j+RKgoIw==
"@popmotion/popcorn@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@popmotion/popcorn/-/popcorn-0.4.0.tgz#089ba62a7a8801ba18876d42149d4289992b926c"
integrity sha512-vrCzLNT/ZscfrviWirZwRvpD9hSzCTNRxgUwHb1xvDNKJaXSNKTAeS/aiIdV3A/o/09Gu9CyMI0BpVGhK78wnw==
dependencies:
"@popmotion/easing" "^1.0.1"
framesync "^4.0.1"
hey-listen "^1.0.8"
style-value-types "^3.1.4"
"@posthtml/esm@^1.0.0": "@posthtml/esm@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@posthtml/esm/-/esm-1.0.0.tgz#09bcb28a02438dcee22ad1970ca1d85a000ae0cf" resolved "https://registry.yarnpkg.com/@posthtml/esm/-/esm-1.0.0.tgz#09bcb28a02438dcee22ad1970ca1d85a000ae0cf"
@ -1057,11 +1030,6 @@
"@types/minimatch" "*" "@types/minimatch" "*"
"@types/node" "*" "@types/node" "*"
"@types/invariant@^2.2.29":
version "2.2.29"
resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.29.tgz#aa845204cd0a289f65d47e0de63a6a815e30cc66"
integrity sha512-lRVw09gOvgviOfeUrKc/pmTiRZ7g7oDOU6OAutyuSHpm1/o2RaBQvRhgK8QEdu+FFuw/wnWb29A/iuxv9i8OpQ==
"@types/minimatch@*": "@types/minimatch@*":
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@ -1072,7 +1040,7 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.2.tgz#3452a24edf9fea138b48fad4a0a028a683da1e40" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.2.tgz#3452a24edf9fea138b48fad4a0a028a683da1e40"
integrity sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA== integrity sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==
"@types/node@^10.0.5", "@types/node@^10.12.18": "@types/node@^10.12.18":
version "10.14.7" version "10.14.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.7.tgz#1854f0a9aa8d2cd6818d607b3d091346c6730362" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.7.tgz#1854f0a9aa8d2cd6818d607b3d091346c6730362"
integrity sha512-on4MmIDgHXiuJDELPk1NFaKVUxxCFr37tm8E9yN6rAiF5Pzp/9bBfBHkoexqRiY+hk/Z04EJU9kKEb59YqJ82A== integrity sha512-on4MmIDgHXiuJDELPk1NFaKVUxxCFr37tm8E9yN6rAiF5Pzp/9bBfBHkoexqRiY+hk/Z04EJU9kKEb59YqJ82A==
@ -5087,13 +5055,6 @@ fragment-cache@^0.2.1:
dependencies: dependencies:
map-cache "^0.2.2" map-cache "^0.2.2"
framesync@^4.0.0, framesync@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/framesync/-/framesync-4.0.2.tgz#b03b62852f12b0d80086b60834b089718f03cda5"
integrity sha512-hQLD5NURHmzB4Symo6JJ5HDw2TWwhr6T3gw9aChNMsZvkxcD8U8Gcz/hllAOOMGE+HO3ScpRPahpXDQRgF19JQ==
dependencies:
hey-listen "^1.0.5"
fresh@0.5.2: fresh@0.5.2:
version "0.5.2" version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@ -5637,11 +5598,6 @@ hex-color-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
hey-listen@^1.0.5, hey-listen@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
highlight.js@^9.3.0: highlight.js@^9.3.0:
version "9.15.6" version "9.15.6"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.6.tgz#72d4d8d779ec066af9a17cb14360c3def0aa57c4" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.6.tgz#72d4d8d779ec066af9a17cb14360c3def0aa57c4"
@ -6894,17 +6850,17 @@ lazy-val@^1.0.3, lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#64383d57873ce59dea9df7216ee6cf52c4e95dc6: lbry-redux@lbryio/lbry-redux#42bf926138872d14523be7191694309be4f37605:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/64383d57873ce59dea9df7216ee6cf52c4e95dc6" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/42bf926138872d14523be7191694309be4f37605"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"
uuid "^3.3.2" uuid "^3.3.2"
lbryinc@lbryio/lbryinc#d250096a6fc5df16be4f82812ecce28d6e558b6e: lbryinc@lbryio/lbryinc#67bb3e215be3f13605c5e3f9f2b0e2fb880724cf:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/d250096a6fc5df16be4f82812ecce28d6e558b6e" resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/67bb3e215be3f13605c5e3f9f2b0e2fb880724cf"
dependencies: dependencies:
reselect "^3.0.0" reselect "^3.0.0"
@ -8715,32 +8671,6 @@ please-upgrade-node@^3.0.2:
dependencies: dependencies:
semver-compare "^1.0.0" semver-compare "^1.0.0"
popmotion-pose@^3.4.0:
version "3.4.8"
resolved "https://registry.yarnpkg.com/popmotion-pose/-/popmotion-pose-3.4.8.tgz#a50d7d2e91014405402f23400b08994fd148b5ce"
integrity sha512-/dkEhDiTYkbLb15dkrU3Okh58KU5I8z3f18V7kciN/cJmSc8ZD8tWgOc8U9yJf3lUHnf/va5PMCX4/4RnVeUiQ==
dependencies:
"@popmotion/easing" "^1.0.1"
hey-listen "^1.0.5"
popmotion "^8.6.2"
pose-core "^2.1.0"
style-value-types "^3.0.6"
ts-essentials "^1.0.3"
tslib "^1.9.1"
popmotion@^8.6.2:
version "8.6.10"
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-8.6.10.tgz#1133105f234fb617285bd254e473276153279219"
integrity sha512-pXGkj1Iy61sgwJvdKabHVP+5IeyCN2DdjdpAAVGhRhT31VYoLlKVOI6+SI747CecuyBI6zxSdZDNrrjdrldKuQ==
dependencies:
"@popmotion/easing" "^1.0.1"
"@popmotion/popcorn" "^0.4.0"
framesync "^4.0.0"
hey-listen "^1.0.5"
style-value-types "^3.1.4"
stylefire "^4.1.3"
tslib "^1.9.1"
portfinder@^1.0.20: portfinder@^1.0.20:
version "1.0.20" version "1.0.20"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.20.tgz#bea68632e54b2e13ab7b0c4775e9b41bf270e44a" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.20.tgz#bea68632e54b2e13ab7b0c4775e9b41bf270e44a"
@ -8750,16 +8680,6 @@ portfinder@^1.0.20:
debug "^2.2.0" debug "^2.2.0"
mkdirp "0.5.x" mkdirp "0.5.x"
pose-core@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/pose-core/-/pose-core-2.1.0.tgz#653c85a9a06f924611b909b4a2180ce102bbb258"
integrity sha512-36mVAnIgbM6jfyRug8EqqFbazHUAk9dxwVRpX61FlVw3amI/j7AFegtVU56N0Dht2aYDJIhgYPUYraT1CzjHDw==
dependencies:
"@types/invariant" "^2.2.29"
"@types/node" "^10.0.5"
hey-listen "^1.0.5"
tslib "^1.9.1"
posix-character-classes@^0.1.0: posix-character-classes@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -9805,16 +9725,6 @@ react-paginate@^5.2.1:
dependencies: dependencies:
prop-types "^15.6.1" prop-types "^15.6.1"
react-pose@^4.0.5:
version "4.0.8"
resolved "https://registry.yarnpkg.com/react-pose/-/react-pose-4.0.8.tgz#91bdfafde60e4096e7878a35dcc77715bed68f24"
integrity sha512-WN/583nKJZkKmKg5ha+eErOGWF9GV6A5EngC7WHQX5b910X9rTlOlxzdKlUy/dDcsTRMZEtHV0Sy2gLPYsVQCQ==
dependencies:
"@emotion/is-prop-valid" "^0.7.3"
hey-listen "^1.0.5"
popmotion-pose "^3.4.0"
tslib "^1.9.1"
react-redux@^6.0.1: react-redux@^6.0.1:
version "6.0.1" version "6.0.1"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-6.0.1.tgz#0d423e2c1cb10ada87293d47e7de7c329623ba4d"
@ -11330,23 +11240,6 @@ style-loader@^0.23.1:
loader-utils "^1.1.0" loader-utils "^1.1.0"
schema-utils "^1.0.0" schema-utils "^1.0.0"
style-value-types@^3.0.6, style-value-types@^3.1.4:
version "3.1.4"
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-3.1.4.tgz#8c0959d26405eb0cbad40097b496220d41c22169"
integrity sha512-jHxRZWQpx6imY+QIveHTZwGOJWJqX3Cmt6Yk1zCGeQjk4noEsX+lfvFJUmRPpZL3VTrfGrHtjVWTcvcHx/OFhQ==
dependencies:
hey-listen "^1.0.8"
stylefire@^4.1.3:
version "4.1.4"
resolved "https://registry.yarnpkg.com/stylefire/-/stylefire-4.1.4.tgz#42de066da75762bf086de33856a45ac95f7d66ce"
integrity sha512-bp9nNTTFHdIQp/4szBuF2z85rMAq5oySeAHdpNgPTcVlXDrwsi1FjjOLug/4+yx1p8eMFFGrkAex7b5/M95ivg==
dependencies:
"@popmotion/popcorn" "^0.4.0"
framesync "^4.0.0"
hey-listen "^1.0.8"
style-value-types "^3.1.4"
stylehacks@^4.0.0: stylehacks@^4.0.0:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
@ -11781,12 +11674,7 @@ tryer@^1.0.0:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
ts-essentials@^1.0.3: tslib@^1.9.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-1.0.4.tgz#ce3b5dade5f5d97cf69889c11bf7d2da8555b15a"
integrity sha512-q3N1xS4vZpRouhYHDPwO0bDW3EZ6SK9CrrDHxi/D6BPReSjpVgWIOpLS2o0gSBZm+7q/wyKp6RVM1AeeW7uyfQ==
tslib@^1.9.0, tslib@^1.9.1:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==