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

View file

@ -5,6 +5,7 @@
[libs]
./flow-typed
node_modules/lbry-redux/flow-typed/
node_modules/lbryinc/flow-typed/
[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",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#64383d57873ce59dea9df7216ee6cf52c4e95dc6",
"lbryinc": "lbryio/lbryinc#d250096a6fc5df16be4f82812ecce28d6e558b6e",
"lbry-redux": "lbryio/lbry-redux#42bf926138872d14523be7191694309be4f37605",
"lbryinc": "lbryio/lbryinc#67bb3e215be3f13605c5e3f9f2b0e2fb880724cf",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",
"lodash-es": "^4.17.14",
@ -157,7 +157,6 @@
"react-hot-loader": "^4.11.1",
"react-modal": "^3.1.7",
"react-paginate": "^5.2.1",
"react-pose": "^4.0.5",
"react-redux": "^6.0.1",
"react-router": "^5.0.0",
"react-router-dom": "^5.0.0",

View file

@ -17,7 +17,7 @@ export default appState => {
});
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,
minHeight: 600,
autoHideMenuBar: true,

View file

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

View file

@ -1,8 +1,5 @@
const { parseURI } = require('lbry-redux');
// 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 { readFileSync } = require('fs');
const express = require('express');
@ -72,7 +69,7 @@ app.get('*', async (req, res) => {
let html = readFileSync(path.join(__dirname, '/index.html'), 'utf8');
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
}

View file

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

View file

@ -2,7 +2,7 @@
import * as ICONS from 'constants/icons';
import React, { useEffect, useRef } from 'react';
import analytics from 'analytics';
import { Lbry, buildURI, parseURI } from 'lbry-redux';
import { buildURI, parseURI } from 'lbry-redux';
import Router from 'component/router/index';
import ModalRouter from 'modal/modalRouter';
import ReactModal from 'react-modal';
@ -12,6 +12,7 @@ import Yrbl from 'component/yrbl';
import FileViewer from 'component/fileViewer';
import { withRouter } from 'react-router';
import usePrevious from 'util/use-previous';
import Button from 'component/button';
export const MAIN_WRAPPER_CLASS = 'main-wrapper';
@ -20,7 +21,6 @@ type Props = {
pageTitle: ?string,
language: string,
theme: string,
accessToken: ?string,
user: ?{ id: string, has_verified_email: boolean, is_reward_approved: boolean },
location: { pathname: string, hash: string },
fetchRewards: () => void,
@ -30,6 +30,8 @@ type Props = {
autoUpdateDownloaded: boolean,
isUpgradeAvailable: boolean,
requestDownloadUpgrade: () => void,
fetchChannelListMine: () => void,
onSignedIn: () => void,
};
function App(props: Props) {
@ -40,10 +42,11 @@ function App(props: Props) {
fetchTransactions,
user,
fetchAccessToken,
accessToken,
requestDownloadUpgrade,
autoUpdateDownloaded,
isUpgradeAvailable,
fetchChannelListMine,
onSignedIn,
} = props;
const appRef = useRef();
const isEnhancedLayout = useKonamiListener();
@ -70,8 +73,9 @@ function App(props: Props) {
// @if TARGET='app'
fetchRewards();
fetchTransactions();
fetchChannelListMine(); // This needs to be done for web too...
// @endif
}, [fetchRewards, fetchRewardedContent, fetchTransactions, fetchAccessToken]);
}, [fetchRewards, fetchRewardedContent, fetchTransactions, fetchAccessToken, fetchChannelListMine]);
useEffect(() => {
// $FlowFixMe
@ -87,24 +91,27 @@ function App(props: Props) {
useEffect(() => {
// 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
if (previousHasVerifiedEmail !== undefined && hasVerifiedEmail) {
if (previousHasVerifiedEmail === false && hasVerifiedEmail) {
analytics.emailVerifiedEvent();
}
}, [previousHasVerifiedEmail, hasVerifiedEmail]);
}, [previousHasVerifiedEmail, hasVerifiedEmail, onSignedIn]);
useEffect(() => {
if (previousRewardApproved !== undefined && isRewardApproved) {
if (previousRewardApproved === false && isRewardApproved) {
analytics.rewardEligibleEvent();
}
}, [previousRewardApproved, isRewardApproved]);
// @if TARGET='web'
// Keep this at the end to ensure initial setup effects are run first
useEffect(() => {
if (hasVerifiedEmail && accessToken) {
Lbry.setApiHeader('X-Lbry-Auth-Token', accessToken);
if (!previousHasVerifiedEmail && hasVerifiedEmail) {
onSignedIn();
}
}, [previousHasVerifiedEmail, hasVerifiedEmail, onSignedIn]);
if (!user) {
return null;
}
}, [hasVerifiedEmail, accessToken]);
// @endif
return (
<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 Button from 'component/button';
@ -6,35 +7,33 @@ let scriptLoading = false;
let scriptLoaded = false;
let scriptDidError = false;
type Props = {
disabled: boolean,
label: ?string,
email: string,
// Flow does not like the way this stripe plugin works
// Disabled because it was a huge pain
// type Props = {
// disabled: boolean,
// label: ?string,
// email: string,
// =====================================================
// Required by stripe
// see Stripe docs for more info:
// https://stripe.com/docs/checkout#integration-custom
// =====================================================
// // =====================================================
// // Required by stripe
// // see Stripe docs for more info:
// // https://stripe.com/docs/checkout#integration-custom
// // =====================================================
// Your publishable key (test or live).
// can't use "key" as a prop in react, so have to change the keyname
stripeKey: string,
// // Your publishable key (test or live).
// // can't use "key" as a prop in react, so have to change the keyname
// stripeKey: string,
// The callback to invoke when the Checkout process is complete.
// function(token)
// token is the token object created.
// token.id can be used to create a charge or customer.
// token.email contains the email address entered by the user.
token: string,
};
// // The callback to invoke when the Checkout process is complete.
// // function(token)
// // token is the token object created.
// // token.id can be used to create a charge or customer.
// // token.email contains the email address entered by the user.
// token: string,
// };
type State = {
open: boolean,
};
class CardVerify extends React.Component<Props, State> {
constructor(props: Props) {
class CardVerify extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false,
@ -87,6 +86,7 @@ class CardVerify extends React.Component<Props, State> {
this.loadPromise.promise.then(this.onScriptLoaded).catch(this.onScriptError);
// $FlowFixMe
document.body.appendChild(script);
}
@ -161,7 +161,7 @@ class CardVerify extends React.Component<Props, State> {
render() {
return (
<Button
button="inverse"
button="primary"
label={this.props.label}
disabled={this.props.disabled || this.state.open || this.hasPendingClick}
onClick={this.onClick.bind(this)}
@ -171,3 +171,5 @@ class CardVerify extends React.Component<Props, State> {
}
export default CardVerify;
/* eslint-enable no-undef */
/* eslint-enable react/prop-types */

View file

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

View file

@ -32,13 +32,13 @@ type Props = {
uris: Array<string>,
subscribedChannels: Array<Subscription>,
doClaimSearch: ({}) => void,
injectedItem: any,
tags: Array<string>,
loading: boolean,
personalView: boolean,
doToggleTagFollow: string => void,
meta?: Node,
showNsfw: boolean,
hideCustomization: boolean,
history: { action: string, push: string => void, replace: string => void },
location: { search: string, pathname: string },
claimSearchByQuery: {
@ -54,21 +54,21 @@ function ClaimListDiscover(props: Props) {
tags,
loading,
personalView,
injectedItem,
meta,
subscribedChannels,
showNsfw,
history,
location,
hiddenUris,
hideCustomization,
} = props;
const didNavigateForward = history.action === 'PUSH';
const [page, setPage] = useState(1);
const { search } = location;
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 timeSort = urlParams.get('time') || TIME_WEEK;
const [page, setPage] = useState(1);
const tagsInUrl = urlParams.get('t') || '';
const options: {
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
// it's faster, but we will need to remove it if we start using total_pages
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]) : [],
not_channel_ids: hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
not_tags: !showNsfw ? MATURE_TAGS : [],
@ -191,6 +191,8 @@ function ClaimListDiscover(props: Props) {
</option>
))}
</FormField>
{!hideCustomization && (
<Fragment>
<span>{__('For')}</span>
{!personalView && tags && tags.length ? (
tags.map(tag => <Tag key={tag} name={tag} disabled />)
@ -215,6 +217,8 @@ function ClaimListDiscover(props: Props) {
))}
</FormField>
)}
</Fragment>
)}
{typeSort === 'top' && (
<FormField
className="claim-list__dropdown"
@ -242,7 +246,6 @@ function ClaimListDiscover(props: Props) {
id={claimSearchCacheQuery}
loading={loading}
uris={uris}
injectedItem={personalSort === SEARCH_SORT_YOU && injectedItem}
header={header}
headerAltControls={meta}
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>
<fieldset-section>
<label htmlFor={name}>{errorMessage ? <span className="error-text">{errorMessage}</span> : label}</label>
{prefix && (
<label className="form-field--inline-prefix" htmlFor={name}>
{prefix}
</label>
)}
{prefix && <label htmlFor={name}>{prefix}</label>}
{inner}
</fieldset-section>
</React.Fragment>

View file

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

View file

@ -302,4 +302,9 @@ export const icons = {
<line x1="21" y1="12" x2="9" y2="12" />
</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,
size?: number,
className?: string,
sectionIcon?: boolean,
};
class IconComponent extends React.PureComponent<Props> {
@ -49,11 +50,10 @@ class IconComponent extends React.PureComponent<Props> {
};
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];
if (!Icon) {
console.error('no icon found for ', icon);
return null;
}
@ -69,7 +69,11 @@ class IconComponent extends React.PureComponent<Props> {
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;
}

View file

@ -1,9 +1,10 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { selectBalance, formatCredits } from 'lbry-redux';
import { selectUserEmail } from 'lbryinc';
import { selectUserVerifiedEmail } from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSignOut } from 'redux/actions/app';
import Header from './view';
const select = state => ({
@ -13,11 +14,12 @@ const select = state => ({
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
automaticDarkModeEnabled: makeSelectClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED)(state),
hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state),
email: selectUserEmail(state),
email: selectUserVerifiedEmail(state),
});
const perform = dispatch => ({
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
signOut: () => dispatch(doSignOut()),
});
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 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 = {
autoUpdateDownloaded: boolean,
balance: string,
@ -41,6 +25,7 @@ type Props = {
hideBalance: boolean,
email: ?string,
minimal: boolean,
signOut: () => void,
};
const Header = (props: Props) => {
@ -53,11 +38,9 @@ const Header = (props: Props) => {
hideBalance,
email,
minimal,
signOut,
} = props;
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
const isAuthenticated = Boolean(email);
// Auth is optional in the desktop app
const showFullHeader = IS_WEB ? isAuthenticated : true;
const authenticated = Boolean(email);
function handleThemeToggle() {
if (automaticDarkModeEnabled) {
@ -71,42 +54,14 @@ const Header = (props: Props) => {
}
}
function signOut() {
// Replace this with actual clearUser function
window.store.dispatch({ type: 'USER_FETCH_FAILURE' });
deleteAuthToken();
location.reload();
}
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'),
});
function getWalletTitle() {
return hideBalance ? (
__('Wallet')
) : (
<React.Fragment>
{roundedBalance} <LbcSymbol />
</React.Fragment>
);
}
return (
@ -146,19 +101,17 @@ const Header = (props: Props) => {
</div>
{!minimal ? (
<div className="header__menu">
{showFullHeader ? (
<div className={classnames('header__menu', { 'header__menu--small': IS_WEB && !authenticated })}>
{!IS_WEB || authenticated ? (
<Fragment>
<Menu>
<MenuButton className="header__navigation-item menu__title">
{roundedBalance} <LbcSymbol />
</MenuButton>
<MenuButton className="header__navigation-item menu__title">{getWalletTitle()}</MenuButton>
<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} />
{__('Wallet')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/rewards`)}>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.FEATURED} />
{__('Rewards')}
</MenuItem>
@ -169,19 +122,16 @@ const Header = (props: Props) => {
<Icon size={18} icon={ICONS.ACCOUNT} />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem
className="menu__link"
onSelect={() => history.push(isAuthenticated ? `/$/account` : `/$/auth/signup`)}
>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.ACCOUNT}`)}>
<Icon aria-hidden icon={ICONS.OVERVIEW} />
{__('Overview')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/publish`)}>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISH}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Publish')}
</MenuItem>
{isAuthenticated ? (
{authenticated ? (
<MenuItem className="menu__link" onSelect={signOut}>
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')}
@ -197,11 +147,11 @@ const Header = (props: Props) => {
<Icon size={18} icon={ICONS.SETTINGS} />
</MenuButton>
<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} />
{__('Settings')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/help`)}>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.HELP}`)}>
<Icon aria-hidden icon={ICONS.HELP} />
{__('Help')}
</MenuItem>
@ -213,10 +163,7 @@ const Header = (props: Props) => {
</Menu>
</Fragment>
) : (
<Fragment>
<span />
<Button navigate={`/$/${PAGES.AUTH}/signin`} button="primary" label={__('Sign In')} />
</Fragment>
<Button navigate={`/$/${PAGES.AUTH}`} button="primary" label={__('Sign In')} />
)}
</div>
) : (

View file

@ -1,7 +1,6 @@
// @flow
import React from 'react';
import RewardLink from 'component/rewardLink';
import Yrbl from 'component/yrbl';
import { rewards } from 'lbryinc';
import Icon from 'component/common/icon';
import * as ICONS from 'constants/icons';
@ -20,22 +19,10 @@ class InviteList extends React.PureComponent<Props> {
render() {
const { invitees, referralReward } = this.props;
if (!invitees) {
if (!invitees || !invitees.length) {
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 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!."
@ -60,10 +47,10 @@ class InviteList extends React.PureComponent<Props> {
/>
)}
</h2>
<p className="card__subtitle">{rewardHelp}</p>
<p className="section__subtitle">{rewardHelp}</p>
</div>
<table className="table table--invites">
<table className="table section">
<thead>
<tr>
<th>{__('Invitee Email')}</th>

View file

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

View file

@ -1,3 +1,9 @@
import { connect } from 'react-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
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
import type { Node } from 'react';
import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react';
import classnames from 'classnames';
import SideBar from 'component/sideBar';
import Header from 'component/header';
import Button from 'component/button';
type Props = {
children: Node | Array<Node>,
@ -13,35 +11,19 @@ type Props = {
autoUpdateDownloaded: boolean,
isUpgradeAvailable: boolean,
fullscreen: boolean,
doDownloadUpgradeRequested: () => void,
authenticated: boolean,
};
function Page(props: Props) {
const {
children,
className,
fullscreen = false,
autoUpdateDownloaded,
isUpgradeAvailable,
doDownloadUpgradeRequested,
} = props;
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
const { children, className, fullscreen = false, authenticated } = props;
const obscureSideBar = IS_WEB ? !authenticated : false;
return (
<Fragment>
<Header minimal={fullscreen} />
<div className={classnames('main-wrapper__inner')}>
{/* @if TARGET='app' */}
{showUpgradeButton && (
<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 />}
<main className={classnames('main', className, { 'main--full-width': fullscreen })}>{children}</main>
{!fullscreen && <SideBar obscureSideBar={obscureSideBar} />}
</div>
</Fragment>
);

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import React from 'react';
import Icon from 'component/common/icon';
import RewardLink from 'component/rewardLink';
import Button from 'component/button';
import Card from 'component/common/card';
import { rewards } from 'lbryinc';
type Props = {
@ -25,16 +26,16 @@ const RewardTile = (props: Props) => {
const claimed = !!reward.transaction_id;
return (
<section className="card card--section">
<h2 className="card__title">{reward.reward_title}</h2>
<p className="card__subtitle">{reward.reward_description}</p>
<Card
title={reward.reward_title}
subtitle={reward.reward_description}
actions={
<div className="card__actions">
{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 && (
<Button button="inverse" navigate="/$/invite" label={__('Go To Invites')} />
<Button button="primary" navigate="/$/invite" label={__('Go To Invites')} />
)}
{reward.reward_type !== rewards.TYPE_REFERRAL &&
(claimed ? (
@ -45,7 +46,8 @@ const RewardTile = (props: Props) => {
<RewardLink button reward_type={reward.reward_type} />
))}
</div>
</section>
}
/>
);
};

View file

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

View file

@ -13,12 +13,10 @@ import RewardsPage from 'page/rewards';
import FileListDownloaded from 'page/fileListDownloaded';
import FileListPublished from 'page/fileListPublished';
import TransactionHistoryPage from 'page/transactionHistory';
import AuthPage from 'page/auth';
import InvitePage from 'page/invite';
import SearchPage from 'page/search';
import LibraryPage from 'page/library';
import WalletPage from 'page/wallet';
import NavigationHistory from 'page/navigationHistory';
import TagsPage from 'page/tags';
import FollowingPage from 'page/following';
import ListBlockedPage from 'page/listBlocked';
@ -30,9 +28,32 @@ if ('scrollRestoration' in history) {
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 = {
currentScroll: number,
location: { pathname: string, search: string },
isAuthenticated: boolean,
};
function AppRouter(props: Props) {
@ -46,25 +67,25 @@ function AppRouter(props: Props) {
<Switch>
<Route path="/" exact component={DiscoverPage} />
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={AuthPage} />
<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.AUTH}`} exact component={SignInPage} />
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.FOLLOWING}`} exact component={FollowingPage} />
<Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<Route path={`/$/${PAGES.BLOCKED}`} exact component={ListBlockedPage} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
<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 */}
<Route path="/:claimName" 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 { SEARCH_OPTIONS } from 'lbry-redux';
import { Form, FormField } from 'component/common/form';
import posed from 'react-pose';
import Button from 'component/button';
const ExpandableOptions = posed.div({
hide: { height: 0, opacity: 0 },
show: { height: 380, opacity: 1 },
});
type Props = {
setSearchOption: (string, boolean | string | number) => void,
options: {},
@ -30,7 +24,6 @@ const SearchOptions = (props: Props) => {
iconRight={expanded ? ICONS.UP : ICONS.DOWN}
onClick={toggleSearchExpanded}
/>
<ExpandableOptions pose={expanded ? 'show' : 'hide'}>
{expanded && (
<Form className="search__options">
<fieldset>
@ -116,7 +109,6 @@ const SearchOptions = (props: Props) => {
</fieldset>
</Form>
)}
</ExpandableOptions>
</div>
);
};

View file

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

View file

@ -1,21 +1,21 @@
// @flow
import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react';
import React from 'react';
import Button from 'component/button';
import Tag from 'component/tag';
import StickyBox from 'react-sticky-box/dist/esnext';
import 'css-doodle';
type Props = {
subscriptions: Array<Subscription>,
followedTags: Array<Tag>,
email: ?string,
obscureSideBar: boolean,
};
function SideBar(props: Props) {
const { subscriptions, followedTags, email } = props;
const showSideBar = IS_WEB ? Boolean(email) : true;
const { subscriptions, followedTags, obscureSideBar } = props;
function buildLink(path, label, icon, guide) {
return {
navigate: path ? `$/${path}` : '/',
@ -25,9 +25,19 @@ function SideBar(props: Props) {
};
}
return (
return obscureSideBar ? (
<StickyBox offsetTop={100} offsetBottom={20}>
<div className="card navigation--placeholder">
<div className="wrap">
<h2>LBRY</h2>
<p>{__('The best decentralized content platform on the web.')}</p>
<div className="card__actions">{/* <Button button="primary" label={__('Do Something')} /> */}</div>
</div>
</div>
</StickyBox>
) : (
<StickyBox offsetTop={100} offsetBottom={20}>
{showSideBar ? (
<nav className="navigation">
<ul className="navigation-links">
{[
@ -49,8 +59,6 @@ function SideBar(props: Props) {
))}
</ul>
{email ? (
<Fragment>
<Button
navigate={`/$/${PAGES.FOLLOWING}`}
label={__('Customize')}
@ -77,16 +85,7 @@ function SideBar(props: Props) {
</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>
);
}

View file

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

View file

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

View file

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

View file

@ -1,11 +1,11 @@
// @flow
import * as PAGES from 'constants/pages';
import type { Node } from 'react';
import React, { useEffect } from 'react';
import Button from 'component/button';
import { FormField } from 'component/common/form';
import UserEmailNew from 'component/userEmailNew';
import UserEmailVerify from 'component/userEmailVerify';
import UserEmailResetButton from 'component/userEmailResetButton';
import UserSignOutButton from 'component/userSignOutButton';
import Card from 'component/common/card';
type Props = {
cancelButton: Node,
@ -34,18 +34,13 @@ function UserEmail(props: Props) {
}, [accessToken, fetchAccessToken]);
return (
<section className="card card--section">
{!email && <UserEmailNew />}
{user && email && !isVerified && <UserEmailVerify />}
{email && isVerified && (
<React.Fragment>
<h2 className="card__title">{__('Email')}</h2>
<p className="card__subtitle">
{email && isVerified && __('Your email has been successfully verified')}
{!email && __('')}.
</p>
{isVerified && (
<Card
title={__('Email')}
subtitle={__(
'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.'
)}
body={
isVerified ? (
<FormField
type="text"
className="form-field--copyable"
@ -60,18 +55,13 @@ function UserEmail(props: Props) {
/>
</React.Fragment>
}
value={email}
inputButton={<UserEmailResetButton button="inverse" />}
inputButton={<UserSignOutButton button="inverse" />}
value={email || ''}
/>
) : null
}
actions={!isVerified ? <Button button="primary" label={__('Add Email')} navigate={`/$/${PAGES.AUTH}`} /> : null}
/>
)}
<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>
);
}

View file

@ -11,10 +11,14 @@ type Props = {
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) {
const { errorMessage, isPending, addUserEmail } = props;
const [newEmail, setEmail] = useState('');
const [sync, setSync] = useState(false);
const valid = newEmail.match(EMAIL_REGEX);
function handleSubmit() {
addUserEmail(newEmail);
@ -26,8 +30,14 @@ function UserEmailNew(props: Props) {
}
return (
<Form onSubmit={handleSubmit}>
<div>
<h1 className="section__title--large">{__('Welcome To LBRY')}</h1>
<p className="section__subtitle">{__('Create a new account or sign in.')}</p>
<Form onSubmit={handleSubmit} className="section__body">
<FormField
autoFocus
className="form-field--short"
placeholder={__('hotstuff_96@hotmail.com')}
type="email"
id="sign_up_email"
label={__('Email')}
@ -35,17 +45,11 @@ function UserEmailNew(props: Props) {
error={errorMessage}
onChange={e => setEmail(e.target.value)}
/>
<FormField
type="checkbox"
id="sign_up_sync"
label={__('Sync my bidnezz on this device')}
helper={__('Maybe some additional text with this field')}
checked={sync}
onChange={() => setSync(!sync)}
/>
<Button button="primary" type="submit" label={__('Continue')} disabled={isPending} />
<div className="card__actions">
<Button button="primary" type="submit" label={__('Continue')} disabled={!newEmail || !valid || isPending} />
</div>
</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
import * as React from 'react';
import Button from 'component/button';
import UserEmailResetButton from 'component/userEmailResetButton';
import UserSignOutButton from 'component/userSignOutButton';
type Props = {
email: string,
@ -52,18 +52,19 @@ class UserEmailVerify extends React.PureComponent<Props> {
return (
<React.Fragment>
<p className="card__subtitle">
{__('An email was sent to')} {email}.{' '}
{__('Follow the link and you will be good to go. This will update automatically.')}
<h1 className="section__title--large">{__('Confirm Your Email')}</h1>
<p className="section__subtitle">
{__('An email was sent to')} {email}. {__('Follow the link to sign in. This will update automatically.')}
</p>
<div className="card__actions">
<div className="section__body section__actions">
<Button
button="primary"
label={__('Resend Verification Email')}
onClick={this.handleResendVerificationEmail}
/>
<UserEmailResetButton />
<UserSignOutButton label={__('Start Over')} />
</div>
<p className="help">

View file

@ -1,28 +1,21 @@
import { connect } from 'react-redux';
import { selectUser, selectEmailToVerify, rewards as REWARD_TYPES, doClaimRewardType } from 'lbryinc';
import { doCreateChannel, selectCreatingChannel, selectMyChannelClaims } from 'lbry-redux';
import UserSignUp from './view';
import { selectUser, selectEmailToVerify } from 'lbryinc';
import { doCreateChannel, selectCreatingChannel, selectMyChannelClaims, selectCreateChannelError } from 'lbry-redux';
import UserFirstChannel from './view';
const select = state => ({
email: selectEmailToVerify(state),
user: selectUser(state),
creatingChannel: selectCreatingChannel(state),
channels: selectMyChannelClaims(state),
creatingChannel: selectCreatingChannel(state),
createChannelError: selectCreateChannelError(state),
});
const perform = dispatch => ({
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(
select,
perform
)(UserSignUp);
)(UserFirstChannel);

View file

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

View file

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

View file

@ -1,32 +1,101 @@
// @flow
import React from 'react';
import { withRouter } from 'react-router';
import UserEmailNew from 'component/userEmailNew';
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 = {
user: ?User,
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) {
const { email, user } = props;
const verifiedEmail = user && email && user.has_verified_email;
function useFetched(fetching) {
const wasFetching = usePrevious(fetching);
const [fetched, setFetched] = React.useState(false);
function getTitle() {
if (!email) {
return __('Get Rockin');
} else if (email && !verifiedEmail) {
return __('We Sent You An Email');
React.useEffect(() => {
if (wasFetching && !fetching) {
setFetched(true);
}
}, [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 (
<section>
<h1 className="card__title--large">{getTitle()}</h1>
{!email && <UserEmailNew />}
{email && !verifiedEmail && <UserEmailVerify />}
{hasVerifiedEmail && !rewardsApproved ? (
<UserVerify />
) : (
<div className="main--contained">
{!email && !hasVerifiedEmail && <UserEmailNew />}
{email && !hasVerifiedEmail && <UserEmailVerify />}
{hasVerifiedEmail && balance && balance > 0 && channelCount === 0 && <UserFirstChannel />}
</div>
)}
</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
import * as React from 'react';
import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react';
import Button from 'component/button';
import CardVerify from 'component/cardVerify';
import { Lbryio } from 'lbryinc';
import Card from 'component/common/card';
type Props = {
errorMessage: ?string,
@ -26,91 +28,87 @@ class UserVerify extends React.PureComponent<Props> {
const { errorMessage, isPending, verifyPhone } = this.props;
return (
<React.Fragment>
<section className="card card--section">
<h1 className="card__title">{__('Final Human Proof')}</h1>
<p className="card__subtitle">
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.
<section className="section--large">
<h1 className="section__title--large">{__('Extra Verification Needed')}</h1>
<p>
{__(
"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')} />.
</p>
</section>
<section className="card card--section">
<h2 className="card__title">{__('1) Proof via Phone')}</h2>
<p className="card__subtitle">
{`${__(
<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>
<div className="card__actions">
)}
actions={
<Fragment>
<Button
onClick={() => {
verifyPhone();
}}
button="inverse"
label={__('Submit Phone Number')}
button="primary"
label={__('Verify Phone Number')}
/>
</div>
<div className="help">
<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.')} />
</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>
</Fragment>
}
/>
<div className="card__actions">
<div className="section__divider">
<hr />
<p>{__('OR')}</p>
</div>
<Card
icon={ICONS.WALLET}
title={__('Proof via Credit')}
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.'
)}
actions={
<Fragment>
{errorMessage && <p className="error-text">{errorMessage}</p>}
<CardVerify
label={__('Perform Card Verification')}
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="section__divider">
<hr />
<p>{__('OR')}</p>
</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.')}
<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>
</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.'
)}{' '}
{__(
'This process will likely involve providing proof of a stable and established online or real-life identity.'
)}
</p>
<div className="card__actions">
<Button href="https://chat.lbry.com" button="inverse" label={__('Join LBRY Chat')} />
</div>
</section>
<section className="card card--section">
<h2 className="card__title">{__('Or, Skip It Entirely')}</h2>
<p className="card__subtitle">
{__('You can continue without this step, but you will not be eligible to earn rewards.')}
</p>
<div className="card__actions">
<Button navigate="/" button="primary" label={__('Skip Rewards')} />
</div>
</section>
</React.Fragment>
);
}

View file

@ -3,6 +3,7 @@ import React from 'react';
import Button from 'component/button';
import CopyableText from 'component/copyableText';
import QRCode from 'component/common/qr-code';
import Card from 'component/common/card';
type Props = {
checkAddressIsMine: string => void,
@ -46,13 +47,13 @@ class WalletAddress extends React.PureComponent<Props, State> {
const { showQR } = this.state;
return (
<section className="card card--section">
<h2 className="card__title">{__('Receive Credits')}</h2>
<p className="card__subtitle">
{__(
<Card
title={__('Receive Credits')}
subtitle={__(
'Use this address to receive LBC. You can generate a new address at any time, and any previous addresses will continue to work.'
)}
</p>
actions={
<React.Fragment>
<CopyableText label={__('Your Address')} copyable={receiveAddress} snackMessage={__('Address copied.')} />
<div className="card__actions">
@ -68,7 +69,9 @@ class WalletAddress extends React.PureComponent<Props, State> {
</div>
{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 {
doWalletStatus,
@ -27,7 +26,6 @@ import {
selectUser,
} from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
const select = state => ({
walletEncryptSucceeded: selectWalletEncryptSucceeded(state),
@ -36,7 +34,6 @@ const select = state => ({
walletEncrypted: selectWalletIsEncrypted(state),
walletHasTransactions: selectHasTransactions(state),
user: selectUser(state),
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
hasSyncedWallet: selectHasSyncedWallet(state),
getSyncIsPending: selectGetSyncIsPending(state),
setSyncIsPending: selectSetSyncIsPending(state),

View file

@ -1,404 +1,405 @@
// @flow
import React, { useState, useEffect } from 'react';
import { Form, FormField, Submit } from 'component/common/form';
import Button from 'component/button';
import UserEmail from 'component/userEmail';
import * as ICONS from 'constants/icons';
export default () => null;
// // @flow
// import React, { useState } from 'react';
// import { Form, FormField, Submit } from 'component/common/form';
// import Button from 'component/button';
// 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 = {
// wallet statuses
walletEncryptSucceeded: boolean,
walletEncryptPending: boolean,
walletDecryptSucceeded: boolean,
walletDecryptPending: boolean,
updateWalletStatus: boolean,
walletEncrypted: boolean,
// wallet methods
encryptWallet: (?string) => void,
decryptWallet: (?string) => void,
updateWalletStatus: () => void,
// housekeeping
setPasswordSaved: () => void,
syncEnabled: boolean,
setClientSetting: (string, boolean | string) => void,
isPasswordSaved: boolean,
// data
user: any,
// sync statuses
hasSyncedWallet: boolean,
getSyncIsPending?: boolean,
syncApplyErrorMessage?: string,
hashChanged: boolean,
// sync data
syncData: string | null,
syncHash: string | null,
// sync methods
syncApply: (string | null, string | null, string) => void,
checkSync: () => void,
setDefaultAccount: () => void,
hasTransactions: boolean,
};
// type Props = {
// // wallet statuses
// walletEncryptSucceeded: boolean,
// walletEncryptPending: boolean,
// walletDecryptSucceeded: boolean,
// walletDecryptPending: boolean,
// updateWalletStatus: boolean,
// walletEncrypted: boolean,
// // wallet methods
// encryptWallet: (?string) => void,
// decryptWallet: (?string) => void,
// updateWalletStatus: () => void,
// // housekeeping
// setPasswordSaved: () => void,
// syncEnabled: boolean,
// setClientSetting: (string, boolean | string) => void,
// isPasswordSaved: boolean,
// // data
// user: any,
// // sync statuses
// hasSyncedWallet: boolean,
// getSyncIsPending?: boolean,
// syncApplyErrorMessage?: string,
// hashChanged: boolean,
// // sync data
// syncData: string | null,
// syncHash: string | null,
// // sync methods
// syncApply: (string | null, string | null, string) => void,
// checkSync: () => void,
// setDefaultAccount: () => void,
// hasTransactions: boolean,
// };
type State = {
newPassword: string,
newPasswordConfirm: string,
passwordMatch: boolean,
understandConfirmed: boolean,
understandError: boolean,
submitted: boolean,
failMessage: ?string,
rememberPassword: boolean,
showEmailReg: boolean,
failed: boolean,
enableSync: boolean,
encryptWallet: boolean,
obscurePassword: boolean,
advancedMode: boolean,
showPasswordFields: boolean,
};
// type State = {
// newPassword: string,
// newPasswordConfirm: string,
// passwordMatch: boolean,
// understandConfirmed: boolean,
// understandError: boolean,
// submitted: boolean,
// failMessage: ?string,
// rememberPassword: boolean,
// showEmailReg: boolean,
// failed: boolean,
// enableSync: boolean,
// encryptWallet: boolean,
// obscurePassword: boolean,
// advancedMode: boolean,
// showPasswordFields: boolean,
// };
function WalletSecurityAndSync(props: Props) {
const {
// walletEncryptSucceeded,
// walletEncryptPending,
// walletDecryptSucceeded,
// walletDecryptPending,
// updateWalletStatus,
walletEncrypted,
encryptWallet,
decryptWallet,
syncEnabled,
user,
hasSyncedWallet,
getSyncIsPending,
syncApplyErrorMessage,
hashChanged,
syncData,
syncHash,
syncApply,
checkSync,
hasTransactions,
setDefaultAccount,
} = props;
// function WalletSecurityAndSync(props: Props) {
// const {
// // walletEncryptSucceeded,
// // walletEncryptPending,
// // walletDecryptSucceeded,
// // walletDecryptPending,
// // updateWalletStatus,
// walletEncrypted,
// encryptWallet,
// decryptWallet,
// syncEnabled,
// user,
// hasSyncedWallet,
// getSyncIsPending,
// syncApplyErrorMessage,
// hashChanged,
// syncData,
// syncHash,
// syncApply,
// checkSync,
// hasTransactions,
// // setDefaultAccount,
// } = props;
const defaultComponentState: State = {
newPassword: '',
newPasswordConfirm: '',
passwordMatch: false,
understandConfirmed: false,
understandError: false,
submitted: false, // Prior actions could be marked complete
failMessage: undefined,
rememberPassword: false,
showEmailReg: false,
failed: false,
enableSync: syncEnabled,
encryptWallet: walletEncrypted,
obscurePassword: true,
advancedMode: false,
showPasswordFields: false,
};
const [componentState, setComponentState] = useState<State>(defaultComponentState);
// const defaultComponentState: State = {
// newPassword: '',
// newPasswordConfirm: '',
// passwordMatch: false,
// understandConfirmed: false,
// understandError: false,
// submitted: false, // Prior actions could be marked complete
// failMessage: undefined,
// rememberPassword: false,
// showEmailReg: false,
// failed: false,
// enableSync: syncEnabled,
// encryptWallet: walletEncrypted,
// obscurePassword: true,
// advancedMode: false,
// showPasswordFields: false,
// };
// const [componentState, setComponentState] = useState<State>(defaultComponentState);
const safeToSync = !hasTransactions || !hashChanged;
// const safeToSync = !hasTransactions || !hashChanged;
// on mount
useEffect(() => {
checkSync();
getSavedPassword().then(p => {
if (p) {
setComponentState({
...componentState,
newPassword: p,
newPasswordConfirm: p,
showPasswordFields: true,
rememberPassword: true,
});
}
});
}, []);
// // on mount
// // useEffect(() => {
// // checkSync();
// // getSavedPassword().then(p => {
// // if (p) {
// // setComponentState({
// // ...componentState,
// // newPassword: p,
// // newPasswordConfirm: p,
// // showPasswordFields: true,
// // rememberPassword: true,
// // });
// // }
// // });
// // }, []);
useEffect(() => {
setComponentState({
...componentState,
passwordMatch: componentState.newPassword === componentState.newPasswordConfirm,
});
}, [componentState.newPassword, componentState.newPasswordConfirm]);
// // useEffect(() => {
// // setComponentState({
// // ...componentState,
// // passwordMatch: componentState.newPassword === componentState.newPasswordConfirm,
// // });
// // }, [componentState.newPassword, componentState.newPasswordConfirm]);
const isEmailVerified = user && user.primary_email && user.has_verified_email;
// const syncDisabledMessage = 'You cannot sync without an email';
// const isEmailVerified = user && user.primary_email && user.has_verified_email;
// // const syncDisabledMessage = 'You cannot sync without an email';
function onChangeNewPassword(event: SyntheticInputEvent<>) {
setComponentState({ ...componentState, newPassword: event.target.value || '' });
}
// function onChangeNewPassword(event: SyntheticInputEvent<>) {
// setComponentState({ ...componentState, newPassword: event.target.value || '' });
// }
function onChangeRememberPassword(event: SyntheticInputEvent<>) {
if (componentState.rememberPassword) {
deleteSavedPassword();
}
setComponentState({ ...componentState, rememberPassword: event.target.checked });
}
// function onChangeRememberPassword(event: SyntheticInputEvent<>) {
// if (componentState.rememberPassword) {
// deleteAuthToken();
// }
// setComponentState({ ...componentState, rememberPassword: event.target.checked });
// }
function onChangeNewPasswordConfirm(event: SyntheticInputEvent<>) {
setComponentState({ ...componentState, newPasswordConfirm: event.target.value || '' });
}
// function onChangeNewPasswordConfirm(event: SyntheticInputEvent<>) {
// setComponentState({ ...componentState, newPasswordConfirm: event.target.value || '' });
// }
function onChangeUnderstandConfirm(event: SyntheticInputEvent<>) {
setComponentState({ ...componentState, understandConfirmed: /^.?i understand.?$/i.test(event.target.value) });
}
// function onChangeUnderstandConfirm(event: SyntheticInputEvent<>) {
// setComponentState({ ...componentState, understandConfirmed: /^.?i understand.?$/i.test(event.target.value) });
// }
function onChangeSync(event: SyntheticInputEvent<>) {
if (componentState.enableSync) {
setComponentState({ ...componentState, enableSync: false, newPassword: '', newPasswordConfirm: '' });
setComponentState({ ...componentState, enableSync: false, newPassword: '', newPasswordConfirm: '' });
}
if (!(walletEncrypted || syncApplyErrorMessage || componentState.advancedMode)) {
easyApply();
} else {
setComponentState({ ...componentState, enableSync: true });
}
}
// function onChangeSync(event: SyntheticInputEvent<>) {
// if (componentState.enableSync) {
// setComponentState({ ...componentState, enableSync: false, newPassword: '', newPasswordConfirm: '' });
// setComponentState({ ...componentState, enableSync: false, newPassword: '', newPasswordConfirm: '' });
// }
// if (!(walletEncrypted || syncApplyErrorMessage || componentState.advancedMode)) {
// easyApply();
// } else {
// setComponentState({ ...componentState, enableSync: true });
// }
// }
function onChangeEncrypt(event: SyntheticInputEvent<>) {
setComponentState({ ...componentState, encryptWallet: event.target.checked });
}
// function onChangeEncrypt(event: SyntheticInputEvent<>) {
// setComponentState({ ...componentState, encryptWallet: event.target.checked });
// }
async function easyApply() {
return new Promise((resolve, reject) => {
return syncApply(syncHash, syncData, componentState.newPassword);
})
.then(() => {
setComponentState({ ...componentState, enableSync: event.target.checked });
})
.catch();
}
// async function easyApply() {
// return new Promise((resolve, reject) => {
// return syncApply(syncHash, syncData, componentState.newPassword);
// })
// .then(() => {
// setComponentState({ ...componentState, enableSync: event.target.checked });
// })
// .catch();
// }
async function apply() {
setComponentState({ ...componentState, failed: false });
// async function apply() {
// setComponentState({ ...componentState, failed: false });
await checkSync();
// await checkSync();
if (componentState.enableSync) {
await syncApply(syncHash, syncData, componentState.newPassword);
if (syncApplyErrorMessage) {
setComponentState({ ...componentState, failed: true });
}
}
// if (componentState.enableSync) {
// await syncApply(syncHash, syncData, componentState.newPassword);
// if (syncApplyErrorMessage) {
// setComponentState({ ...componentState, failed: true });
// }
// }
if (walletEncrypted) {
await decryptWallet();
}
// if (walletEncrypted) {
// await decryptWallet();
// }
if (componentState.encryptWallet && !componentState.failed) {
await encryptWallet(componentState.newPassword)
.then(() => {})
.catch(() => {
setComponentState({ ...componentState, failed: false });
});
}
// if (componentState.encryptWallet && !componentState.failed) {
// await encryptWallet(componentState.newPassword)
// .then(() => {})
// .catch(() => {
// setComponentState({ ...componentState, failed: false });
// });
// }
if (componentState.rememberPassword && !componentState.failed) {
setSavedPassword(componentState.newPassword);
}
}
// if (componentState.rememberPassword && !componentState.failed) {
// setSavedPassword(componentState.newPassword);
// }
// }
return (
<React.Fragment>
<section className="card card--section">
<h2 className="card__title">{__('Wallet Sync and Security')}</h2>
{!isEmailVerified && (
<React.Fragment>
<p className="card__subtitle">
{__(`It looks like we don't have your email.`)}{' '}
<Button
button="link"
label={__('Verify your email')}
onClick={() => setComponentState({ ...componentState, showEmailReg: !componentState.showEmailReg })}
/>{' '}
{__(`and then come back here.`)}
</p>
{componentState.showEmailReg && <UserEmail />}
</React.Fragment>
)}
<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.'
)}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />.
</p>
{/* Errors and status */}
{!componentState.advancedMode && (
<React.Fragment>
<p className="card__subtitle">
{__(`Easy Mode: Sync and go with default security! Don't trust your roommate?`)}{' '}
<Button
button="link"
label={__('Advanced Mode')}
onClick={() => setComponentState({ ...componentState, advancedMode: !componentState.advancedMode })}
/>
.
</p>
</React.Fragment>
)}
{componentState.advancedMode && (
<React.Fragment>
<p className="card__subtitle">
{__('Advanced Mode: Enter a password that matches your other devices LBRY password.')}{' '}
<Button
button="link"
label={__('Easy Mode')}
onClick={() => setComponentState({ ...componentState, advancedMode: !componentState.advancedMode })}
/>
.
</p>
</React.Fragment>
)}
{syncApplyErrorMessage && <div className="card__subtitle--status">{__(syncApplyErrorMessage)}</div>}
// return (
// <React.Fragment>
// <section className="card card--section">
// <h2 className="card__title">{__('Wallet Sync and Security')}</h2>
// {!isEmailVerified && (
// <React.Fragment>
// <p className="card__subtitle">
// {__(`It looks like we don't have your email.`)}{' '}
// <Button
// button="link"
// label={__('Verify your email')}
// onClick={() => setComponentState({ ...componentState, showEmailReg: !componentState.showEmailReg })}
// />{' '}
// {__(`and then come back here.`)}
// </p>
// {componentState.showEmailReg && <UserEmail />}
// </React.Fragment>
// )}
// <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.'
// )}{' '}
// <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/wallet-encryption" />.
// </p>
// {/* Errors and status */}
// {!componentState.advancedMode && (
// <React.Fragment>
// <p className="card__subtitle">
// {__(`Easy Mode: Sync and go with default security! Don't trust your roommate?`)}{' '}
// <Button
// button="link"
// label={__('Advanced Mode')}
// onClick={() => setComponentState({ ...componentState, advancedMode: !componentState.advancedMode })}
// />
// .
// </p>
// </React.Fragment>
// )}
// {componentState.advancedMode && (
// <React.Fragment>
// <p className="card__subtitle">
// {__('Advanced Mode: Enter a password that matches your other devices LBRY password.')}{' '}
// <Button
// button="link"
// label={__('Easy Mode')}
// onClick={() => setComponentState({ ...componentState, advancedMode: !componentState.advancedMode })}
// />
// .
// </p>
// </React.Fragment>
// )}
// {syncApplyErrorMessage && <div className="card__subtitle--status">{__(syncApplyErrorMessage)}</div>}
<Form onSubmit={() => apply()}>
{componentState.advancedMode && (
<FormField
type="checkbox"
name="sync_enabled"
checked={componentState.enableSync}
disabled={!isEmailVerified || !safeToSync}
prefix={<span className="badge badge--alert">ALPHA</span>}
onChange={event => onChangeSync(event)}
label={
<React.Fragment>
{__('Enable Sync')} <Button button="link" label={__('(?)')} href="https://lbry.com/privacypolicy" />{' '}
<span className="badge badge--alert">ALPHA</span>
</React.Fragment>
}
/>
)}
{!componentState.advancedMode && (
<Button
button="primary"
label={__('Sync my wallet')}
onClick={() => syncApply(syncHash, syncData, componentState.newPassword)}
/>
)}
{(walletEncrypted ||
syncApplyErrorMessage ||
componentState.advancedMode ||
componentState.showPasswordFields) && (
<React.Fragment>
<FormField
autoFocus
inputButton={
<Button
icon={componentState.obscurePassword ? ICONS.EYE : ICONS.EYE_OFF}
button={'primary'}
onClick={() =>
setComponentState({ ...componentState, obscurePassword: !componentState.obscurePassword })
}
/>
}
label={__('Password')}
placeholder={__('Shh...')}
type={componentState.obscurePassword ? 'password' : 'text'}
name="wallet-new-password"
onChange={event => onChangeNewPassword(event)}
value={componentState.newPassword}
/>
<FormField
error={componentState.passwordMatch === false ? 'No match' : false}
label={__('Same Password')}
placeholder={__('Your eyes only')}
type="password"
name="wallet-new-password-confirm"
onChange={event => onChangeNewPasswordConfirm(event)}
value={componentState.newPasswordConfirm}
/>
<FormField
label={__('Remember Password')}
type="checkbox"
name="wallet-remember-password"
onChange={event => onChangeRememberPassword(event)}
checked={componentState.rememberPassword}
/>
</React.Fragment>
)}
// <Form onSubmit={() => apply()}>
// {componentState.advancedMode && (
// <FormField
// type="checkbox"
// name="sync_enabled"
// checked={componentState.enableSync}
// disabled={!isEmailVerified || !safeToSync}
// prefix={<span className="badge badge--alert">ALPHA</span>}
// onChange={event => onChangeSync(event)}
// label={
// <React.Fragment>
// {__('Enable Sync')} <Button button="link" label={__('(?)')} href="https://lbry.com/privacypolicy" />{' '}
// <span className="badge badge--alert">ALPHA</span>
// </React.Fragment>
// }
// />
// )}
// {!componentState.advancedMode && (
// <Button
// button="primary"
// label={__('Sync my wallet')}
// onClick={() => syncApply(syncHash, syncData, componentState.newPassword)}
// />
// )}
// {(walletEncrypted ||
// syncApplyErrorMessage ||
// componentState.advancedMode ||
// componentState.showPasswordFields) && (
// <React.Fragment>
// <FormField
// autoFocus
// inputButton={
// <Button
// icon={componentState.obscurePassword ? ICONS.EYE : ICONS.EYE_OFF}
// button={'primary'}
// onClick={() =>
// setComponentState({ ...componentState, obscurePassword: !componentState.obscurePassword })
// }
// />
// }
// label={__('Password')}
// placeholder={__('Shh...')}
// type={componentState.obscurePassword ? 'password' : 'text'}
// name="wallet-new-password"
// onChange={event => onChangeNewPassword(event)}
// value={componentState.newPassword}
// />
// <FormField
// error={componentState.passwordMatch === false ? 'No match' : false}
// label={__('Same Password')}
// placeholder={__('Your eyes only')}
// type="password"
// name="wallet-new-password-confirm"
// onChange={event => onChangeNewPasswordConfirm(event)}
// value={componentState.newPasswordConfirm}
// />
// <FormField
// label={__('Remember Password')}
// type="checkbox"
// name="wallet-remember-password"
// onChange={event => onChangeRememberPassword(event)}
// checked={componentState.rememberPassword}
// />
// </React.Fragment>
// )}
{/* Confirmation */}
// {/* Confirmation */}
{(walletEncrypted || componentState.advancedMode) && (
<React.Fragment>
<FormField
type="checkbox"
name="encrypt_enabled"
checked={componentState.encryptWallet}
disabled={false}
onChange={event => onChangeEncrypt(event)}
label={__('Encrypt Wallet')}
/>
<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.'
)}
</div>
<FormField
error={componentState.understandError === true ? 'You must enter "I understand"' : false}
label={__('Enter "I understand"')}
placeholder={__('I understand')}
type="text"
name="wallet-understand"
onChange={event => onChangeUnderstandConfirm(event)}
/>
</React.Fragment>
)}
// {(walletEncrypted || componentState.advancedMode) && (
// <React.Fragment>
// <FormField
// type="checkbox"
// name="encrypt_enabled"
// checked={componentState.encryptWallet}
// disabled={false}
// onChange={event => onChangeEncrypt(event)}
// label={__('Encrypt Wallet')}
// />
// <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.'
// )}
// </div>
// <FormField
// error={componentState.understandError === true ? 'You must enter "I understand"' : false}
// label={__('Enter "I understand"')}
// placeholder={__('I understand')}
// type="text"
// name="wallet-understand"
// onChange={event => onChangeUnderstandConfirm(event)}
// />
// </React.Fragment>
// )}
{componentState.failMessage && <div className="error-text">{__(componentState.failMessage)}</div>}
{(walletEncrypted || componentState.advancedMode || syncApplyErrorMessage) && (
<Submit
disabled={!componentState.passwordMatch || (!componentState.enableSync && !componentState.encryptWallet)}
label={componentState.failMessage ? __('Encrypting Wallet') : __('Apply')}
/>
)}
</Form>
</section>
// {componentState.failMessage && <div className="error-text">{__(componentState.failMessage)}</div>}
// {(walletEncrypted || componentState.advancedMode || syncApplyErrorMessage) && (
// <Submit
// disabled={!componentState.passwordMatch || (!componentState.enableSync && !componentState.encryptWallet)}
// label={componentState.failMessage ? __('Encrypting Wallet') : __('Apply')}
// />
// )}
// </Form>
// </section>
{/* Testing stuff and Diagnostics */}
// {/* Testing stuff and Diagnostics */}
<section className="card card--section">
<Button
button="primary"
label={__('Sync Apply')}
onClick={() => syncApply(syncHash, syncData, componentState.newPassword)}
/>{' '}
<Button button="primary" label={__('Check Sync')} onClick={() => checkSync()} />{' '}
<Button button="primary" label={__('Setpass test')} onClick={() => setSavedPassword('testpass')} />{' '}
<Button
button="primary"
label={__('Getpass test')}
onClick={() => getSavedPassword().then(p => setComponentState({ ...componentState, newPassword: p }))}
/>{' '}
<Button button="primary" label={__('Deletepass test')} onClick={() => deleteSavedPassword()} />{' '}
<p>
password:{' '}
{componentState.newPassword
? componentState.newPassword
: componentState.newPassword === ''
? 'blankString'
: 'null'}
</p>
<p>encryptWallet: {String(componentState.encryptWallet)}</p>
<p>enableSync: {String(componentState.enableSync)}</p>
<p>syncApplyError: {String(syncApplyErrorMessage)}</p>
<p>Has Synced: {String(hasSyncedWallet)}</p>
<p>getSyncPending: {String(getSyncIsPending)}</p>
<p>syncEnabled: {String(syncEnabled)}</p>
<p>syncHash: {syncHash ? syncHash.slice(0, 10) : 'null'}</p>
<p>syncData: {syncData ? syncData.slice(0, 10) : 'null'}</p>
<p>walletEncrypted: {String(walletEncrypted)}</p>
<p>emailRegistered: {String(isEmailVerified)}</p>
<p>hashChanged: {String(hashChanged)}</p>
</section>
</React.Fragment>
);
}
// <section className="card card--section">
// <Button
// button="primary"
// label={__('Sync Apply')}
// onClick={() => syncApply(syncHash, syncData, componentState.newPassword)}
// />{' '}
// <Button button="primary" label={__('Check Sync')} onClick={() => checkSync()} />{' '}
// <Button button="primary" label={__('Setpass test')} onClick={() => setSavedPassword('testpass')} />{' '}
// <Button
// button="primary"
// label={__('Getpass test')}
// onClick={() => getSavedPassword().then(p => setComponentState({ ...componentState, newPassword: p }))}
// />{' '}
// <Button button="primary" label={__('Deletepass test')} onClick={() => deleteAuthToken()} />{' '}
// <p>
// password:{' '}
// {componentState.newPassword
// ? componentState.newPassword
// : componentState.newPassword === ''
// ? 'blankString'
// : 'null'}
// </p>
// <p>encryptWallet: {String(componentState.encryptWallet)}</p>
// <p>enableSync: {String(componentState.enableSync)}</p>
// <p>syncApplyError: {String(syncApplyErrorMessage)}</p>
// <p>Has Synced: {String(hasSyncedWallet)}</p>
// <p>getSyncPending: {String(getSyncIsPending)}</p>
// <p>syncEnabled: {String(syncEnabled)}</p>
// <p>syncHash: {syncHash ? syncHash.slice(0, 10) : 'null'}</p>
// <p>syncData: {syncData ? syncData.slice(0, 10) : 'null'}</p>
// <p>walletEncrypted: {String(walletEncrypted)}</p>
// <p>emailRegistered: {String(isEmailVerified)}</p>
// <p>hashChanged: {String(hashChanged)}</p>
// </section>
// </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 { Formik } from 'formik';
import { validateSendTx } from 'util/form-validation';
import Card from 'component/common/card';
type DraftTransaction = {
address: string,
@ -36,10 +37,10 @@ class WalletSend extends React.PureComponent<Props> {
const { balance } = this.props;
return (
<section className="card card--section">
<h2 className="card__title">{__('Send Credits')}</h2>
<p className="card__subtitle">{__('Send LBC to your friends or favorite creators.')}</p>
<Card
title={__('Send Credits')}
subtitle={__('Send LBC to your friends or favorite creators.')}
actions={
<Formik
initialValues={{
address: '',
@ -92,7 +93,8 @@ class WalletSend extends React.PureComponent<Props> {
<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 &&
__('Decrease amount to account for transaction fee')) ||
(parseFloat(values.amount) > balance && __('Not enough credits'))}
</span>
)}
@ -100,7 +102,8 @@ class WalletSend extends React.PureComponent<Props> {
</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 FETCH_REWARD_CONTENT_COMPLETED = 'FETCH_REWARD_CONTENT_COMPLETED';
// ShapeShift
export const GET_SUPPORTED_COINS_START = 'GET_SUPPORTED_COINS_START';
export const GET_SUPPORTED_COINS_SUCCESS = 'GET_SUPPORTED_COINS_SUCCESS';
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';
// Language
export const DOWNLOAD_LANGUAGE_SUCCEEDED = 'DOWNLOAD_LANGUAGE_SUCCEEDED';
export const DOWNLOAD_LANGUAGE_FAILED = 'DOWNLOAD_LANGUAGE_FAILED';
// Subscriptions
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 CHANNEL = 'channel';
export const DISCOVER = 'discover';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -124,7 +124,6 @@ class FilePage extends React.Component<Props> {
nsfw,
supportOption,
} = this.props;
// File info
const { signing_channel: signingChannel } = claim;
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} />
</div>
) : (
<div className="main--empty">
<section className="card card--section">
<h2 className="card__title">{__("It looks like you haven't published anything to LBRY yet.")}</h2>
<div className="card__actions card__actions--center">
<section className="main--empty">
<div className=" section--small">
<h2 className="section__title--large">{__('Nothing published to LBRY yet.')}</h2>
<div className="section__actions">
<Button button="primary" navigate="/$/publish" label={__('Publish something new')} />
</div>
</section>
</div>
</section>
)}
</Page>
);

View file

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

View file

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

View file

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

View file

@ -20,10 +20,11 @@ const select = (state, props) => {
try {
uri = normalizeURI(path);
} catch (e) {
// Probably an old channel url, redirect to the vanity channel
// @routinghax
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)}`;
props.history.replace(`/${path.slice(0, match.index)}`);
}

View file

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

View file

@ -1,42 +1,12 @@
// @flow
import React from 'react';
import UserSignIn from 'component/userSignIn';
import UserFirstChannel from 'component/userFirstChannel';
import UserVerify from 'component/userVerify';
import Page from 'component/page';
type Props = {
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}` : '/');
}
export default function SignInPage() {
return (
<Page fullscreen className="main--auth-page">
{showWelcome && (
<div className="columns">
{!hasVerifiedEmail && <UserSignIn />}
{hasVerifiedEmail && channelCount === 0 && <UserFirstChannel />}
<div style={{ width: '100%', height: '20rem', borderRadius: 20, backgroundColor: '#ffc7e6' }} />
</div>
)}
{hasVerifiedEmail && channelCount > 0 && <UserVerify />}
<UserSignIn />
</Page>
);
}

View file

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

View file

@ -6,6 +6,7 @@ import { ipcRenderer, remote } from 'electron';
import path from 'path';
import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types';
import * as PAGES from 'constants/pages';
import {
Lbry,
doBalanceSubscribe,
@ -13,6 +14,8 @@ import {
doError,
makeSelectClaimForUri,
makeSelectClaimIsMine,
doPopulateUserSettings,
doFetchChannelListMine,
} from 'lbry-redux';
import Native from 'native';
import { doFetchDaemonSettings } from 'redux/actions/settings';
@ -28,10 +31,12 @@ import {
selectUpgradeTimer,
selectModal,
} from 'redux/selectors/app';
import { doAuthenticate } from 'lbryinc';
import { Lbryio, doAuthenticate } from 'lbryinc';
import { lbrySettings as config, version as appVersion } from 'package.json';
import { push } from 'connected-react-router';
import analytics from 'analytics';
import { deleteAuthToken } from 'util/saved-passwords';
import cookie from 'cookie';
// @if TARGET='app'
const { autoUpdater } = remote.require('electron-updater');
@ -322,13 +327,12 @@ export function doAlertError(errorList) {
export function doDaemonReady() {
return (dispatch, getState) => {
const state = getState();
dispatch(doAuthenticate(appVersion));
dispatch({ type: ACTIONS.DAEMON_READY });
// @if TARGET='app'
dispatch(doFetchDaemonSettings());
dispatch(doBalanceSubscribe());
dispatch(doFetchDaemonSettings());
dispatch(doFetchFileInfosAndPublishedClaims());
if (!selectIsUpgradeSkipped(state)) {
dispatch(doCheckUpgradeAvailable());
@ -414,7 +418,7 @@ export function doConditionalAuthNavigate(newSession) {
const modal = selectModal(state);
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);
};
}
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
import * as ACTIONS from 'constants/action_types';
import { parseURI, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
import { VIEW_ALL } from 'constants/subscriptions';
import { handleActions } from 'util/redux-utils';
@ -12,6 +13,7 @@ const defaultState: SubscriptionState = {
loadingSuggested: false,
firstRunCompleted: false,
showSuggestedSubs: false,
enabledChannelNotifications: [],
};
export default handleActions(
@ -134,6 +136,39 @@ export default handleActions(
...state,
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
);

View file

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

View file

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

View file

@ -55,6 +55,12 @@
margin-right: auto;
}
.card--inline {
box-shadow: none;
border-radius: none;
margin-bottom: 0;
}
// C A R D
// 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 {
@extend .card__title;
justify-content: space-between;
@ -214,3 +214,29 @@
opacity: 0.5;
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 {
border-color: lighten($lbry-black, 20%);
border-radius: var(--input-border-radius);
background-color: $lbry-white;
border-width: 1px;
}
@ -67,6 +68,7 @@ fieldset-section {
label {
width: auto;
text-transform: none;
color: lighten($lbry-black, 20%);
}
}
@ -90,7 +92,6 @@ radio-element {
}
label {
color: lighten($lbry-black, 20%);
margin-bottom: 0;
margin-left: var(--spacing-miniscule);
font-size: var(--font-body);
@ -184,9 +185,12 @@ fieldset-group {
height: var(--input-height);
padding-right: 0;
border: 1px solid;
border-top-left-radius: var(--input-border-radius);
border-bottom-left-radius: var(--input-border-radius);
border-right: 0;
border-color: $lbry-black;
color: $lbry-gray-4;
background-color: $lbry-white;
[data-mode='dark'] & {
border-color: $lbry-gray-4;
@ -275,7 +279,6 @@ fieldset-section {
max-width: 12em;
background-position: 95% center;
background-size: 1.2rem;
background-color: $lbry-white;
[data-mode='dark'] & {
background-color: transparent;
@ -305,7 +308,6 @@ fieldset-section {
background-color: rgba($lbry-gray-1, 0.5);
border: 1px solid $lbry-gray-1;
color: $lbry-gray-5;
flex: 1;
padding: 0.2rem 0.75rem;
text-overflow: ellipsis;
user-select: text;
@ -324,6 +326,10 @@ fieldset-section {
margin-bottom: var(--spacing-large);
}
.form-field--short {
width: 25em;
}
.form-field--price-amount {
width: 7em;
}

View file

@ -54,11 +54,15 @@
justify-content: space-between;
align-items: center;
.button {
> .button:only-child {
margin-left: auto;
}
}
.header__menu--small {
width: auto;
}
.header__navigation-arrows {
display: flex;
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 {
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) {
width: 100%;
@ -43,7 +44,7 @@
.main--auth-page {
max-width: 60rem;
margin-top: calc(var(--spacing-main-padding) * 2);
margin-top: var(--spacing-main-padding);
margin-left: auto;
margin-right: auto;
}
@ -63,22 +64,15 @@
background-color: var(--color-background);
}
.main--fullscreen {
width: 100vw;
height: 100vh;
z-index: 9999;
background-color: $lbry-white;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: 0;
margin-top: 0;
.main--contained {
max-width: 35rem;
min-width: 25rem;
margin: auto;
margin-top: 5rem;
}
* {
z-index: 10000;
}
.main--full-width {
width: 100%;
}
.main__status {

View file

@ -9,9 +9,22 @@
.navigation--placeholder {
@extend .navigation;
height: 80vh;
background-color: $lbry-blue-1;
padding: 2rem 1.5rem;
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 {

View file

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

View file

@ -15,8 +15,11 @@ $main: $lbry-teal-5;
.tags--remove {
@extend .tags;
@extend .ul--no-style;
margin-bottom: var(--spacing-large);
.tag {
margin-top: 0;
margin-bottom: var(--spacing-small);
}
}
.tags--vertical {
@ -31,8 +34,24 @@ $main: $lbry-teal-5;
margin: var(--spacing-large) 0;
}
.tags__empty-message {
margin-top: var(--spacing-medium);
.tags__input-wrapper {
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 {
@ -68,7 +87,11 @@ $main: $lbry-teal-5;
}
.tag--add {
background-color: lighten($lbry-teal-5, 60%);
background-color: $lbry-teal-5;
color: $lbry-white;
.icon {
stroke: $lbry-white;
}
&.tag--mature {
@extend .badge--mature;

View file

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

View file

@ -87,6 +87,7 @@
.menu__title,
.menu__link {
color: lighten($lbry-black, 20%);
color: $lbry-gray-5;
.icon {
stroke: $lbry-gray-5;
@ -104,7 +105,7 @@
.menu__link {
.icon {
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-weight: 400;
line-height: 1.5;
background-color: mix($lbry-white, $lbry-gray-1, 70%);
background-color: var(--color-background);
[data-mode='dark'] & {
background-color: var(--dm-color-08);
@ -48,13 +48,15 @@ p {
ul,
ol {
margin-bottom: var(--spacing-large);
}
ul {
ul,
ol {
list-style: initial;
margin-bottom: var(--spacing-large);
li {
list-style-position: outside;
margin: var(--spacing-medium) 0;
}
}

View file

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

View file

@ -8,6 +8,8 @@ import thunk from 'redux-thunk';
import { createHashHistory, createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router';
import createRootReducer from './reducers';
import { Lbryio } from 'lbryinc';
import isEqual from 'util/deep-equal';
function isFunction(object) {
return typeof object === 'function';
@ -53,13 +55,12 @@ const whiteListedReducers = [
// @if TARGET='app'
'publish',
'wallet',
'tags',
// 'fileInfo',
// @endif
'content',
'subscriptions',
'app',
'search',
'tags',
'blocked',
'settings',
];
@ -69,6 +70,7 @@ const transforms = [
walletFilter,
fileInfoFilter,
blockedFilter,
tagsFilter,
// @endif
appFilter,
searchFilter,
@ -106,6 +108,29 @@ const store = createStore(
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);
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(
resolve => {
ipcRenderer.once('delete-password-response', (event, success) => {
resolve(success);
// @if TARGET='app'
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(false);

View file

@ -1,4 +1,5 @@
{
"404": "404",
"Thumbnail Image": "Thumbnail Image",
"OK": "OK",
"Cancel": "Cancel",
@ -708,6 +709,6 @@
"Tip %amount% LBC": "Tip %amount% LBC",
"Not enough credits": "Not enough credits",
"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.",
"Explore new content": "Explore new content"
"URI does not include name.": "URI does not include name.",
"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-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":
version "2.0.0"
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"
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":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@posthtml/esm/-/esm-1.0.0.tgz#09bcb28a02438dcee22ad1970ca1d85a000ae0cf"
@ -1057,11 +1030,6 @@
"@types/minimatch" "*"
"@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@*":
version "3.0.3"
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"
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"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.7.tgz#1854f0a9aa8d2cd6818d607b3d091346c6730362"
integrity sha512-on4MmIDgHXiuJDELPk1NFaKVUxxCFr37tm8E9yN6rAiF5Pzp/9bBfBHkoexqRiY+hk/Z04EJU9kKEb59YqJ82A==
@ -5087,13 +5055,6 @@ fragment-cache@^0.2.1:
dependencies:
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:
version "0.5.2"
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"
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:
version "9.15.6"
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"
zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#64383d57873ce59dea9df7216ee6cf52c4e95dc6:
lbry-redux@lbryio/lbry-redux#42bf926138872d14523be7191694309be4f37605:
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:
proxy-polyfill "0.1.6"
reselect "^3.0.0"
uuid "^3.3.2"
lbryinc@lbryio/lbryinc#d250096a6fc5df16be4f82812ecce28d6e558b6e:
lbryinc@lbryio/lbryinc#67bb3e215be3f13605c5e3f9f2b0e2fb880724cf:
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:
reselect "^3.0.0"
@ -8715,32 +8671,6 @@ please-upgrade-node@^3.0.2:
dependencies:
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:
version "1.0.20"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.20.tgz#bea68632e54b2e13ab7b0c4775e9b41bf270e44a"
@ -8750,16 +8680,6 @@ portfinder@^1.0.20:
debug "^2.2.0"
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:
version "0.1.1"
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:
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:
version "6.0.1"
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"
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:
version "4.0.3"
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"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
ts-essentials@^1.0.3:
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:
tslib@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==